Reacting toBuilding a Financial Exchange from first principlesbyHarsh Iyer

Financially Illiterate Person Sees How a Stock Exchange is Built

Speed and simplicity rarely come together. In pursuit of one we more often than not have to sacrifice the other. Yet, all these stock exchanges somehow manage sub millisecond latency, for millions?!

Harsh Iyer, Building a Financial Exchange from first principles

I would have said speed and complexity rarely come together, and as an outsider to the financial world, I would guess that a stock exchange would be quite complex. But hey, I’m not the one who built an exchange.

This treasure hunt led me to make farzi.exchange.

farzi (फ़र्ज़ी) translates to “fake” in Hindi, which is precisely what this is. The goal was to build a fake stock exchange, ground up from first principles, while trying to retain a decent amount of performance without overengineering, or overspending for compute.

You know what the say, “If you can’t beat em, join em”, which in this case is if you can’t buy API rights to quote data, sell your own. Eagerly awaiting farzi.exchange/pricing.

The core purpose of a financial exchange is to match orders, primarily buy and sell orders, between buyers and sellers (who wouldve thought?!). Financial Exchanges work best when they are trade fungible instruments, i.e. one unit is economically indistinguishable from all other units.

Ah, so that’s what NFT folks are on about when talking about non-fungible instruments.

Taking inspiration from NSE, farzi.exchange is a price-time exchange, which adheres to the rule of Orders are matched based on price and time, with the best bid/ask that arrived the earliest, matched first. For tie breaks, exchanges usually adopt different parameters to tip one order over the another, usually these are:

  • Disclosed Quantity (the more you disclose, the higher you shall stand)
  • Order Size (can go either way, with exchanges preferring larger orders over smaller ones and vice versa)

I was curious if two orders with the same price arrive at the exact same time, well the NSE says that their matching engine is capable of keeping a deterministic nanosecond-timestamp sequence. So basically it’s just too fast for that. I hope Farzi Exchange is held to the same standards.

Limit Orders are one of the most common order types, which is basically a traders way of telling the exchange “I am willing to Buy/Sell XYZ, but only if its under/atleast this price.”

This gives a trader the illusion of being in control.

Well, an illusion of control is better than no control. Also worth to mention there is some expiry time for the orders as well cause you don’t want to be hit with a 5 year old order for 100 shares of RCOM on a Tuesday morning. メ“•﹏•`

If you’re wondering whether the exchange goes the extra mile to ensure you get the best price and not just any price under your limit? Yes, it does. That’s what the “price-time” part of the exchange is all about. So you can’t overpay for a share, even if you set a very high limit you will be getting the best market price available at the time of your order.

Market Orders

Another common order type present in almost all exchanges are Market Orders, which are used to execute a trade at the current best market price, no strings attached. Do note that between the time an order is placed on the client and it reaches the exchange, the price may have changed.

Another con of Market Orders is they fill against the Best Bid/Ask opposite to you, thus making you eat the spread.

This is what I use in my amateur trading on the actual NSE, mostly because I’m not bothered by technical analysis. This is also the place where HFTs and other algorithmic traders make their money by pocketing the “spread”.

An iceberg order is a trading technique that splits a large buy or sell order into smaller, sequential limit orders to conceal the true trade size.

Only a small visible portion (the “tip of the iceberg”) hits the order book, while the rest remains hidden. As each visible leg executes, the next automatically replenishes.

Pretty much not used by retail traders, Warren Buffet territory.

The Exchange Architecture

The Matching Engine

I decided to design the engine in a way that I was able to run parallel goroutines for each unique instrument my exchange served, which is a fair assumption to make in such a setup, although some exchanges don’t…

Ok, so we get to know that the exchange is built in Go, and that we are using multithreading. Would have been nice to know what actual exchanges use (NSE uses C/C++), why Harsh chose Go, and what the tradeoffs are.

Hence the Matching Engine initialization inside the Exchange looked something like this:

func NewExchange() (*Exchange, error) {
 
	instruments, err := repo.ListInstruments(context.Background())
	
	// Make engines for all the instruments
	e := &Exchange{
		engines:        make(map[string]*Engine),
		Events:         make(chan Event, eventsBuffer),
		wg:             sync.WaitGroup{},
		...
	}
  
	// Finally make goroutines for each engine
	for _, engine := range e.engines {
		e.wg.Add(1)
		
		go func(engine *Engine) {
			engine.Run(		wg:             sync.WaitGroup{},)
		}(engine)
	}
 
	return e, nil
}

… each command the exchange recieves for a symbol, we just use a simple Routing Map to route it to its specific engine!

That looks really clean, so I think I did get my answer to why Go was chosen. Now, we have one exchange instance which has multiple engines, one for each instrument, and each engine has its own orderbook.

The Order Book

This can be as simple as a 2D array of prices and orders, to something complex. But with simplicity you sometimes lose speed.

In the end, I decided to go forward with using a Binary Tree for representing the Bid and Ask Order Books. This was partly because the tick indexing decisions weren’t final which would be the base for tick indexed arrays, and could change depending on how the engine is used.

I was confused what tick indexing was, because exchanges have a “Tick Index” as well which is supposed to predict market sentiment. In this case though, after consulting an LLM, I think it is referring to creating a hashmap-like structure for the orderbook, where each price level or “tick” is a key in the map, and the value is a list of orders at that price level.

Essentially, we have a binary tree made up of nodes of PriceLevel, where each PriceLevel has the Volume at that price and a pointer to a list of Orders that are at the node’s price. Most of everything else is made to aid frequently performed actions on the tree, by making maps for O(1) indexed lookups.

We got real-life use of DSA here before GTA VI.

The Emitter

A lot of things need to reflect a trade’s success. The instrument price must go up if the price the trade was executed at is higher than the current price, the clients who placed the trade need to know about it, all people watching the stock need to know!

So the trade gets matched and succeeds, and I assume the orderbook gets updated i.e. the order is deleted in O(1) time as well since it seems to a binary tree of linked lists. Which is a quite underrated pro of this architecture. The emitter is a pretty simple, at its core a loop that notifies all the subscribers.

The Aggregator

The primary job of the aggregator is to eat up events emitted by the engines, and make candles out of it.

That looked a bit like:


type Aggregator struct {
	aggregatorTrades  <-chan *types.Trade  // the input channel it gets events from the engine
	aggregatorCandles chan<- *types.Candle // the output channel it pushes made candles to
 
	aggregatorDone chan struct{} // the done channel is closed when the aggregator is done
 
	// the live candle state in memory, per symbol. A 2D map of [interval][symbol] which has
	// the candle at that position in the matrix.
	live map[types.Interval]map[string]*types.Candle
 
	...
}

That is the first time I’m seeing candle building logic up close, and it goes to show how complex seemingly simple things are.

Just goes to show how effective building a system is to learn about it.

Wait, we have a database?!

I have heard this statement more than once when discovering abandoned “simple” microservices.

The Persistence Layer

The persistence repository is exposed as part of the Exchange itself and any component can call it and it’s helpers to help store data into the postgres database via the persistence layer.

…we make use of our good ol goroutine buffered channel approach…

Wow it took me 3 reads to realise “and any component can call it and it’s helpers to help” was not me having a stroke, a comma would have helped.

A goroutine buffered channel means a channel with kind of a queue built in, so that the producer can keep producing until the buffer is full. In this case, the producer is the data coming in from all components, and the consumer is the flushing to postgres part, so we keep I/O responsive.

Pretty neat.

Here’s a snippet of what I think it looks like in Go:

func NewPersistenceLayer(bufferSize, batchSize int) *PersistenceLayer {
	return &PersistenceLayer{
		// A buffered channel allows the exchange to drop data and immediately move on,
		// as long as the channel isn't completely full.
		dataChan:  make(chan TradeData, bufferSize),
		batchSize: batchSize,
	}
}

and then potentially using a ticker to flush the data in batches, could be a separate goroutine.

The MarketData Layer

The market data layer is responsible for serving market data to clients via websockets.

…This CQRS seperation allows us to serve reads scaled independently without blocking the main exchange loop.

This reminds me of the data-flow architecture of React. Adding the CQRS layer is like enforcing rules on data flow, and what seems like adding restrictions actually makes the system more robust.

Again, really neat thinking and very helpful diagram which I will not be reproducing here, go read Harsh’s blog!

Who’s gonna fill my order?

Since farzi.exchange doesn’t have a lot of active traders (who wouldve guessed?!), I decided to make my own persona based Market Makers.

This is really creative way to make the exchange feel alive, because without these bots, there would no one to buy your Vi shares that come with a SEBI warning. Speaking of SEBI, do we get SEBF? Hope it has an API to bribe directly.

The Farzi 10 Index

Essentially, if the value of an underlying instrument increases, so does the index’s value.

We have the Index Calculator that listens for events from the Emitter out of the Matching engine and computes the index value accordingly on each trade, sampled at 100ms.

So the index calculator gets first-class treatment, not having to go through the MarketData layer and consuming directly from the Emitter. Great for low latency.

I will say the index graph looks quite organic, meaning the bots are working.

Where is this running?

…the exchange is hosted on an Ubuntu m7i-flex.large box on AWS…

The Big Leagues are calling

On 15th July 2026, farzi.exchange was able to process a total of 2,81,35,609 trades and a total of 12,21,15,996 orders!

most amount of trades recorded by NSE in a single day was 24,25,13,525, on Jun 19, 2024. That’s only about 8.6x our daily volume

This is very impressive given that NSE has got basically infinite money to throw at their infrastructure compared to Farzi, and yet Farzi is able to achieve this on a single box.

Review

This was a really fun read, and went surprisingly deep.

As a retail trader who mostly loses money on real exchanges, it’s nice to know why I’m losing it so fast.

Go read: Building a Financial Exchange from first principles