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 = 32Depending 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
endFig: $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
endDelete 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
endThat 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.

