Maker.io main logo

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.

queues

io

From the block diagram, we can infer the following:

  1. Asynchronous reset to clear the order book records

  2. Synchronous clock to handle read operations

  3. MSG_OUT from the previous stage (parsed message) is fed into the order book

  4. No message can be read if the previous stage is empty

  5. 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:

  1. the highest bid price 

  2. and the lowest ask price

As always, I declare my module with a capacity of holding 32 orders (16 bids and 16 asks):

Copy Code
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:

Copy Code
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.

Copy Code
 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


bid array

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:

Copy Code
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.

Copy Code
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:

order book waveform

------ 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.

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.