FPGA order book
2025-12-15 | By Mustahsin Zarif
License: Attribution FPGA
If you have read my previous post on the low latency market data feed handler, you know that FPGAs are increasingly being used in trading firms to make trades as fast as possible. Picking up from that blog, the next step is using parsed ITCH to maintain a live order book. In this blog, we will simulate an order book that maintains an ask array and a bid array. Depending on the type of message received, the order book automatically makes updates and sorts the bid and ask arrays to reflect the best bid/ask price quantity.
From the block diagram, we can infer the following:
Asynchronous reset to clear the order book records
Synchronous clock to handle read operations
MSG_OUT from the previous stage (parsed message) is fed into the order book
No message can be read if the previous stage is empty
Handle the MSG_OUT when the read signal is asserted
Traders want to sell a stock at the highest price they can and buy a stock they want at the lowest price possible to maximize profit. Hence, our order book should reflect:
the highest bid price
and the lowest ask price
As always, I declare my module with a capacity of holding 32 orders (16 bids and 16 asks):
module order_book #( parameter MAX_ORDERS = 16 ) ( input logic clk, input logic reset, input logic read_en, // signal to read a message from FIFO input parsed_msg_t parsed_message, // message read from FIFO input logic empty, output logic [31:0] best_bid_price, output logic [31:0] best_ask_price, output logic [31:0] best_bid_quantity, output logic [31:0] best_ask_quantity ); bid_order_t bid_orders [0:MAX_ORDERS-1]; ask_order_t ask_orders [0:MAX_ORDERS-1]; // 16 + 16 = 32
Depending on the type of message received, I have to either add an order, delete an order, or find an order and update its information, so I reuse my finite state machines (FSMs) from my low-latency market data feed handles:
typedef enum logic[7:0] {
MSG_ADD = 8'h41, // 'A'
MSG_DELETE = 8'h44, // 'D'
MSG_UPDATE = 8'h55, // 'U'
MSG_NULL = 8'hFF // for unrecognized order types
} msg_type_t;
// Order sides
typedef enum logic[7:0] {
ORDER_SIDE_BID = 8'h42,
ORDER_SIDE_ASK = 8'h41,
ORDER_SIDE_UNKNOWN = 8'hFF // for unrecognized order sides
} order_side_t;Here comes the tricky part: we cannot use linked lists in SystemVerilog designs if we want to synthesize the design, because the synthesis tool needs to know how much memory to allocate beforehand; it cannot use dynamic memory. What this means is that we have to shift every order in a fixed array when a new one is added/updated/deleted with a price in the middle. This is scary, but bear with me as I show you the ropes to handle bid orders.
First, we check if the message type is ADD. If it is, then we check whether the message is telling us to bid or ask. We will only look at the bids, since the ask side has similar logic. To begin with, we use a valid index to flag which array indices have data. If the price of the bid is the lowest, then we keep iterating over the array until we reach the index behind the last valid position. Otherwise, we find where the order is supposed to fit, shift orders from that position onward down, and place the new data in the correct position.
case (parsed_message.msg_type) MSG_ADD: begin //add order to the respective array based on order_side if (parsed_message.order_side == ORDER_SIDE_BID) begin int insert_idx = MAX_ORDERS; for (int i = 0; i < MAX_ORDERS; i++) begin if (!bid_orders[i].valid || parsed_message.price > bid_orders[i].price) begin insert_idx = i; // find first invalid index break; end end if (insert_idx < MAX_ORDERS) begin // shift existing orders down to make space for new order for (int j = MAX_ORDERS-2; j >= insert_idx; j--) begin //-2 so dont overflow when copying to j+1 bid_orders[j+1] <= bid_orders[j]; end bid_orders[insert_idx].stock_id <= parsed_message.stock_id; bid_orders[insert_idx].order_id <= parsed_message.order_id; bid_orders[insert_idx].price <= parsed_message.price; bid_orders[insert_idx].quantity <= parsed_message.quantity; bid_orders[insert_idx].valid <= 1; end end
Fig: $500 can go to the last position, but $850 has to go in the 3rd position by shifting the other orders.
When it comes to MSG_UPDATE, we only need to update two elements of the order: the price and quantity. However, we still need to check if the order_id and stock_id match between the existing and new message for validity:
MSG_UPDATE: begin //update order in the respective array based on order_side if (parsed_message.order_side == ORDER_SIDE_BID) begin for (int i = 0; i < MAX_ORDERS; i++) begin if (bid_orders[i].valid && bid_orders[i].stock_id == parsed_message.stock_id && bid_orders[i].order_id == parsed_message.order_id) begin bid_orders[i].price <= parsed_message.price; bid_orders[i].quantity <= parsed_message.quantity; break; // exit loop after updating order end end
Delete is simple at first, since all we need to do is mark the valid bit invalid, then sort the array if needed, similar to the MSG_ADD stage.
MSG_DELETE: begin //delete order from the respective array based on order_side if (parsed_message.order_side == ORDER_SIDE_BID) begin for (int i = 0; i < MAX_ORDERS; i++) begin if (bid_orders[i].valid && bid_orders[i].order_id == parsed_message.order_id) begin bid_orders[i].valid <= 0; // mark as invalid for (int j = i; j < MAX_ORDERS-1; j++) begin if (bid_orders[j+1].valid) // only shift if next order is valid bid_orders[j] <= bid_orders[j+1]; // shift ip else begin bid_orders[j].valid <= 0; // mark as invalid if next order is not valid break; end end break; // exit loop after deleting order end end
That wasn’t too bad, right (although it was a pain to code and debug)? The full code can be found on my repo, including a custom testbench to see the orderbook in action. Here is a sample waveform and TCL output:
------ BID SIDE ------
BID[0] ID:11111111 P:1000 Q:10
------ ASK SIDE ------
Best Bid: $0 (0 shares)
Best Ask: $0 (0 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:10
------ ASK SIDE ------
Best Bid: $1000 (10 shares)
Best Ask: $0 (0 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:10
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
Best Bid: $1050 (5 shares)
Best Ask: $0 (0 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:10
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:aaaaaaaa P:1100 Q:15
Best Bid: $1050 (5 shares)
Best Ask: $0 (0 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:10
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:bbbbbbbb P:1080 Q:10
ASK[1] ID:aaaaaaaa P:1100 Q:15
Best Bid: $1050 (5 shares)
Best Ask: $1100 (15 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:10
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:bbbbbbbb P:1080 Q:10
ASK[1] ID:aaaaaaaa P:1100 Q:15
ASK[2] ID:cccccccc P:1150 Q:5
Best Bid: $1050 (5 shares)
Best Ask: $1080 (10 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:999
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:bbbbbbbb P:1080 Q:10
ASK[1] ID:aaaaaaaa P:1100 Q:15
ASK[2] ID:cccccccc P:1150 Q:5
Best Bid: $1050 (5 shares)
Best Ask: $1080 (10 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:11111111 P:1000 Q:999
BID[2] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:bbbbbbbb P:1075 Q:11
ASK[1] ID:aaaaaaaa P:1100 Q:15
ASK[2] ID:cccccccc P:1150 Q:5
Best Bid: $1050 (5 shares)
Best Ask: $1080 (10 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:bbbbbbbb P:1075 Q:11
ASK[1] ID:aaaaaaaa P:1100 Q:15
ASK[2] ID:cccccccc P:1150 Q:5
Best Bid: $1050 (5 shares)
Best Ask: $1075 (11 shares)
----------------------
------ BID SIDE ------
BID[0] ID:22222222 P:1050 Q:5
BID[1] ID:33333333 P:990 Q:20
------ ASK SIDE ------
ASK[0] ID:aaaaaaaa P:1100 Q:15
ASK[1] ID:cccccccc P:1150 Q:5
Best Bid: $1050 (5 shares)
Best Ask: $1075 (11 shares)
----------------------
Testbench complete.
Looks like a success! With that, we have an order book setup to impress those trading firms if your low-latency market data feed handler didn’t do the job. A good thing about this project was how reusable the design from the market handler was. We also saw how to implement arrays in hardware description language, and maybe even appreciate linked lists in C/C++ programming now.

