diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 975267e4..c465f4ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,27 @@ on: branches: [ main ] jobs: + pipebomb: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: deps + run: | + sudo apt-get update + sudo apt-get install -y verilator make g++ + + - name: lint + run: | + cd pipebomb + make lint + + - name: test + run: | + cd pipebomb + make test + itch_streamgen: runs-on: ubuntu-24.04 steps: diff --git a/README.md b/README.md index 42f1859f..58b70ac7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # punt-engine -is an open-source FPGA-accelerated high-frequency trading engine. See https://punt-engine.com for full documentation. +is an collection of open software and gateware modules for high-frequency trading. -> [!IMPORTANT] -> development paused, resuming dec 2025 - -## Components +## Modules ### Pipebomb **P**ipebomb **I**s a **P**ipelined and **E**ventually **B**alanced **O**rder-**M**anaging **B**ook. @@ -21,4 +18,7 @@ Learn more by reading the source or [the docs](https://punt-engine.com/notes/the Most contributors are welcome. Begin by reading through our [docs](https://punt-engine.com) and looking through [open issues tagged "help wanted"](https://github.com/raquentin/punt-engine/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). +## Resources +1. [Exploring the Potential of Reconfigurable Platforms for Order Book Update](https://www.doc.ic.ac.uk/~wl/papers/17/fpl17ch.pdf) +2. [Xilinx DMA IP Reference drivers](https://github.com/Xilinx/dma_ip_drivers/tree/master) diff --git a/flake.lock b/flake.lock index 7eb3ae72..bc1633ff 100644 --- a/flake.lock +++ b/flake.lock @@ -1,25 +1,59 @@ { "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1741048562, - "narHash": "sha256-W4YZ3fvWZiFYYyd900kh8P8wU6DHSiwaH0j4+fai1Sk=", + "lastModified": 1759439645, + "narHash": "sha256-oiAyQaRilPk525Z5aTtTNWNzSrcdJ7IXM0/PL3CGlbI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6af28b834daca767a7ef99f8a7defa957d0ade6f", + "rev": "879bd460b3d3e8571354ce172128fbcbac1ed633", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.11", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { + "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 8b137891..076893cf 100644 --- a/flake.nix +++ b/flake.nix @@ -1 +1,34 @@ +{ + description = "punt-engine dev env"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in { + devShells.default = pkgs.mkShell { + name = "punt-engine"; + buildInputs = [ + pkgs.verilator + pkgs.yosys + pkgs.verible + pkgs.svls + pkgs.gtkwave + pkgs.gcc + pkgs.cmake + pkgs.pkg-config + ]; + shellHook = '' + echo "shell ready, $(verilator --version | head -1)" + alias svfmt='verible-verilog-format -inplace' + alias svlint='verible-verilog-lint --ruleset=all' + ''; + }; + formatter = pkgs.nixpkgs-fmt; + }); +} diff --git a/madlib/examples/gitkeep.cpp b/madlib/examples/gitkeep.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/pipebomb/.gitignore b/pipebomb/.gitignore new file mode 100644 index 00000000..40c6dd25 --- /dev/null +++ b/pipebomb/.gitignore @@ -0,0 +1 @@ +wave.vcd diff --git a/pipebomb/Makefile b/pipebomb/Makefile index e69de29b..1185d788 100644 --- a/pipebomb/Makefile +++ b/pipebomb/Makefile @@ -0,0 +1,52 @@ +TOP ?= pipebomb_top +RTL_DIR ?= ./rtl +TB ?= ./tb/tb_pipebomb_cross.cpp # override with: make sim TB=./tb/other.cpp + + + + +TRACE_FMT ?= vcd +ifeq ($(TRACE_FMT),fst) + TRACE_FLAG = --trace-fst + WAVE_FILE = wave.fst +else + TRACE_FLAG = --trace + WAVE_FILE = wave.vcd +endif + + + + +VERILATOR_FLAGS = -sv --language 1800-2017 $(TRACE_FLAG) --top-module $(TOP) -I$(RTL_DIR) +CXXFLAGS ?= -std=c++17 -O2 + +PKG = $(RTL_DIR)/pipebomb_pkg.sv +RTL_ALL = $(filter-out $(PKG),$(wildcard $(RTL_DIR)/*.sv)) +SRC_SV = $(PKG) $(RTL_ALL) + + + + +.PHONY: all sim test binary lint clean waves help + +all: test + +obj_dir/V$(TOP): $(SRC_SV) $(TB) + verilator $(VERILATOR_FLAGS) --cc --exe $(SRC_SV) $(TB) -CFLAGS "$(CXXFLAGS)" + $(MAKE) -C obj_dir -f V$(TOP).mk V$(TOP) + +sim: obj_dir/V$(TOP) + ./obj_dir/V$(TOP) + +test: sim + @test -s $(WAVE_FILE) || (echo "Wavefile '$(WAVE_FILE)' missing or empty"; exit 1) + @echo "OK: simulation completed and $(WAVE_FILE) generated." + +binary: + verilator $(VERILATOR_FLAGS) --cc --exe --build $(SRC_SV) $(TB) -CFLAGS "$(CXXFLAGS)" + +lint: + verilator $(VERILATOR_FLAGS) --lint-only $(SRC_SV) + +clean: + rm -rf obj_dir *.vcd *.fst diff --git a/pipebomb/rtl/.gitkeep b/pipebomb/rtl/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/pipebomb/rtl/accumulators.sv b/pipebomb/rtl/accumulators.sv new file mode 100644 index 00000000..cbcea7ef --- /dev/null +++ b/pipebomb/rtl/accumulators.sv @@ -0,0 +1,39 @@ +module accumulators #( + parameter int PRICE_W = 48 +) ( + input logic clk, + rstn, + input logic [PRICE_W-1:0] best_bid_price, + input logic [PRICE_W-1:0] best_ask_price, + input logic best_valid, + + output logic [pipebomb_pkg::PRICE_Q_W-1:0] mid_q32_16, + output logic [pipebomb_pkg::PRICE_Q_W-1:0] ema_q32_16, + output logic ema_valid +); + import pipebomb_pkg::*; + + // convert integer prices -> Q32.16 + logic [PRICE_Q_W-1:0] bid_fx, ask_fx, mid_next; + always_comb begin + bid_fx = {best_bid_price[PRICE_W-1:0], {PRICE_FRAC_W{1'b0}}}; + ask_fx = {best_ask_price[PRICE_W-1:0], {PRICE_FRAC_W{1'b0}}}; + mid_next = (bid_fx + ask_fx) >> 1; + end + + // ema = ema + α*(mid - ema) + // α≈0.04 in Q1.15 (0.04*32768 ≈ 1311) + localparam logic [15:0] ALPHA_Q1_15 = 16'd1311; + + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + mid_q32_16 <= '0; + ema_q32_16 <= '0; + ema_valid <= 1'b0; + end else if (best_valid) begin + mid_q32_16 <= mid_next; + ema_q32_16 <= ema_q32_16 + fxmul_q32_16_q1_15(mid_next - ema_q32_16, ALPHA_Q1_15); + ema_valid <= 1'b1; + end + end +endmodule diff --git a/pipebomb/rtl/match_stub.sv b/pipebomb/rtl/match_stub.sv new file mode 100644 index 00000000..585f76e8 --- /dev/null +++ b/pipebomb/rtl/match_stub.sv @@ -0,0 +1,268 @@ +module match_stub #( + parameter PRICE_W = 48, + parameter QTY_W = 32, + parameter MIRROR_ENTRIES = 64 +) ( + input logic clk, + input logic rstn, + + // incoming micro-op (resolved) + input logic in_v, + output logic in_r, + input logic [ 2:0] in_opcode, + input logic in_side, // 0=bid (taker), 1=ask (taker) + input logic [PRICE_W-1:0] in_price, // taker limit + input logic [ QTY_W-1:0] in_qty, // taker qty + + // current bests + input logic [PRICE_W-1:0] best_bid_price, + input logic [PRICE_W-1:0] best_ask_price, + input logic best_bid_v, + input logic best_ask_v, + + // to side_processor (passthrough or synthetic EXEC/ADD) + output logic sp_v, + input logic sp_r, + output logic sp_valid, + output logic [ 2:0] sp_opcode, + output logic sp_side, + output logic [PRICE_W-1:0] sp_price, + output logic [ QTY_W-1:0] sp_qty, + + // fills out + output logic tfill_v, + input logic tfill_r, + output logic tfill_side, // maker (resting) side + output logic [PRICE_W-1:0] tfill_price, + output logic [ QTY_W-1:0] tfill_qty, + + // tap from sp outputs to maintain mirror + input logic sp_tap_v, + input logic sp_tap_side, + input logic [PRICE_W-1:0] sp_tap_price, + input logic [ QTY_W-1:0] sp_tap_newqty +); + + // mirror (side,price) -> qty + typedef struct packed { + logic side; + logic [PRICE_W-1:0] price; + logic [QTY_W-1:0] qty; + logic val; + } mentry_t; + + mentry_t mirror[MIRROR_ENTRIES]; + + function automatic int m_find(input logic side, input logic [PRICE_W-1:0] p); + m_find = -1; + for (int i = 0; i < MIRROR_ENTRIES; i++) begin + if (mirror[i].val && mirror[i].side == side && mirror[i].price == p) begin + m_find = i; + break; + end + end + endfunction + + function automatic int m_free(); + m_free = -1; + for (int i = 0; i < MIRROR_ENTRIES; i++) begin + if (!mirror[i].val) begin + m_free = i; + break; + end + end + endfunction + + // keep mirror synced with sp aggregate outputs + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + for (int i = 0; i < MIRROR_ENTRIES; i++) mirror[i].val <= 1'b0; + end else if (sp_tap_v) begin + int idx = m_find(sp_tap_side, sp_tap_price); + if (sp_tap_newqty == '0) begin + if (idx >= 0) mirror[idx].val <= 1'b0; // delete level + end else begin + if (idx < 0) idx = m_free(); + if (idx < 0) idx = 0; // naive overwrite under pressure + mirror[idx].side <= sp_tap_side; + mirror[idx].price <= sp_tap_price; + mirror[idx].qty <= sp_tap_newqty; + mirror[idx].val <= 1'b1; + end + end + end + + // read mirror qty + function automatic logic [QTY_W-1:0] m_get_qty(input logic side, input logic [PRICE_W-1:0] p); + int idx; + m_get_qty = '0; + idx = m_find(side, p); + if (idx >= 0 && mirror[idx].val) m_get_qty = mirror[idx].qty; + endfunction + + // matcher fsm + typedef enum logic [1:0] { + IDLE, + EMIT_EXEC, + EMIT_RESID + } st_t; + st_t st; + + // latched taker state + logic t_side; + logic [PRICE_W-1:0] t_px_limit; + logic [ QTY_W-1:0] t_rem; + + // derived comb signals + logic taker_is_bid; + logic [PRICE_W-1:0] maker_px; + logic maker_side; + logic marketable_in; // for new input + logic best_valid_now; // for current maker view + logic limit_ok_now; // taker limit check vs maker_px + + logic [ QTY_W-1:0] resting_q_comb; + logic [ QTY_W-1:0] fill_q_comb; + + // maker view for current state + assign taker_is_bid = (in_side == 1'b0); + assign maker_side = taker_is_bid ? 1'b1 : 1'b0; + assign maker_px = taker_is_bid ? best_ask_price : best_bid_price; + assign marketable_in = (in_opcode == 3'd1) && // ITCH_ADD + (taker_is_bid ? (best_ask_v && (in_price >= best_ask_price)) + : (best_bid_v && (in_price <= best_bid_price))); + + + integer idx_exec; // used in always_ff for mirror update + + wire em_taker_is_bid = (t_side == 1'b0); + wire [PRICE_W-1:0] em_maker_px = em_taker_is_bid ? best_ask_price : best_bid_price; + wire em_best_valid = em_taker_is_bid ? best_ask_v : best_bid_v; + wire em_limit_ok = em_taker_is_bid ? (em_maker_px <= t_px_limit) : (em_maker_px >= t_px_limit); + wire em_maker_side = em_taker_is_bid ? 1'b1 : 1'b0; + + // compute resting & fill quantities in a comb block + always_comb begin + resting_q_comb = '0; + fill_q_comb = '0; + if (st == EMIT_EXEC) begin + resting_q_comb = m_get_qty(em_taker_is_bid ? 1'b1 : 1'b0, em_maker_px); + // cap by resting if known; sp will still clamp if mirror missed + if (resting_q_comb != '0) fill_q_comb = (t_rem <= resting_q_comb) ? t_rem : resting_q_comb; + else fill_q_comb = '0; // no resting at best + end + end + + // input ready policy + assign in_r = (st == IDLE) ? (marketable_in ? (sp_r && tfill_r) : sp_r) : 1'b0; + + // defaults + always_comb begin + sp_v = 1'b0; + sp_valid = 1'b1; + sp_opcode = 3'd0; // NOP + sp_side = 1'b0; + sp_price = '0; + sp_qty = '0; + tfill_v = 1'b0; + tfill_side = 1'b0; + tfill_price = '0; + tfill_qty = '0; + + unique case (st) + IDLE: begin + if (in_v && in_r && !marketable_in) begin + // passthrough original op + sp_v = 1'b1; + sp_opcode = in_opcode; + sp_side = in_side; + sp_price = in_price; + sp_qty = in_qty; + end + end + + EMIT_EXEC: begin + if (em_best_valid && em_limit_ok && (resting_q_comb != '0) && (fill_q_comb != '0)) begin + sp_v = 1'b1; + sp_opcode = 3'd4; // ITCH_EXECUTE + sp_side = em_maker_side; + sp_price = em_maker_px; + sp_qty = fill_q_comb; + + tfill_v = 1'b1; + tfill_side = em_maker_side; + tfill_price = em_maker_px; + tfill_qty = fill_q_comb; + end + end + + EMIT_RESID: begin + // book leftover at taker limit price + sp_v = 1'b1; + sp_opcode = 3'd1; // ITCH_ADD + sp_side = t_side; + sp_price = t_px_limit; + sp_qty = t_rem; + end + endcase + end + + // FSM + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + st <= IDLE; + t_side <= '0; + t_px_limit <= '0; + t_rem <= '0; + end else begin + unique case (st) + IDLE: begin + if (in_v && in_r) begin + if (marketable_in) begin + // latch taker + t_side <= in_side; + t_px_limit <= in_price; + t_rem <= in_qty; + st <= EMIT_EXEC; + end + end + end + + EMIT_EXEC: begin + if (!(em_best_valid && em_limit_ok)) begin + // book is empty or price would violate limit → book leftover + if (t_rem != '0) st <= EMIT_RESID; + else st <= IDLE; + + end else if (resting_q_comb == '0) begin + // best price exists but no qty at that price (mirror says 0) → book leftover + if (t_rem != '0) st <= EMIT_RESID; + else st <= IDLE; + + end else if (sp_r && tfill_r) begin + // consumed a real EXEC this cycle + t_rem <= t_rem - fill_q_comb; + + idx_exec = m_find(em_maker_side, em_maker_px); + if (idx_exec >= 0 && mirror[idx_exec].val) begin + if (mirror[idx_exec].qty <= fill_q_comb) mirror[idx_exec].val <= 1'b0; + else mirror[idx_exec].qty <= mirror[idx_exec].qty - fill_q_comb; + end + + if ((t_rem - fill_q_comb) != '0) st <= EMIT_EXEC; + else st <= IDLE; + end + end + + EMIT_RESID: begin + if (sp_r) begin + // booked leftover + st <= IDLE; + end + end + + default: st <= IDLE; + endcase + end + end +endmodule diff --git a/pipebomb/rtl/msg_decode.sv b/pipebomb/rtl/msg_decode.sv deleted file mode 100644 index 4e8fad6a..00000000 --- a/pipebomb/rtl/msg_decode.sv +++ /dev/null @@ -1,37 +0,0 @@ -import orderbook_pkg::*; - -module msg_decode ( - input logic clk, - input logic rst_n, - - input [296:0] parser_data, - input logic parser_valid, - - output inst_t inst -); - - logic [7:0] msg_type; - logic [63:0] order_id; - logic [63:0] old_order_id; // replace only - logic [15:0] locate; - logic buy_side; - logic [31:0] price; - logic [31:0] num_shares; - logic [31:0] seqnum32; - logic [47:0] timestamp; - - always_comb begin - { - msg_type, - order_id, - old_order_id, - locate, - buy_size, - price, - num_shares, - seqnum32, - timestamp - } = parser_data; - end - -endmodule diff --git a/pipebomb/rtl/msg_fifo.sv b/pipebomb/rtl/msg_fifo.sv new file mode 100644 index 00000000..9badea99 --- /dev/null +++ b/pipebomb/rtl/msg_fifo.sv @@ -0,0 +1,43 @@ +module msg_fifo #( + parameter type T = logic [127:0], + parameter int DEPTH = 16 +) ( + input logic clk, + input logic rstn, + + input logic in_v, + output logic in_r, + input T in_d, + + output logic out_v, + input logic out_r, + output T out_d, + + output logic overflow +); + T mem[DEPTH]; + int wptr, rptr, count; + + assign in_r = (count < DEPTH); + assign out_v = (count > 0); + assign out_d = mem[rptr]; + assign overflow = in_v && !in_r; + + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + wptr <= 0; + rptr <= 0; + count <= 0; + end else begin + if (in_v && in_r) begin + mem[wptr] <= in_d; + wptr <= (wptr + 1) % DEPTH; + count <= count + 1; + end + if (out_v && out_r) begin + rptr <= (rptr + 1) % DEPTH; + count <= count - 1; + end + end + end +endmodule diff --git a/pipebomb/rtl/orderbook_pkg.sv b/pipebomb/rtl/orderbook_pkg.sv deleted file mode 100644 index d7973410..00000000 --- a/pipebomb/rtl/orderbook_pkg.sv +++ /dev/null @@ -1,229 +0,0 @@ -package orderbook_pkg; - - // First, these defs are for external ITCH messages. I'm not sure we'll use - // these. See https://github.com/mbattyani/sub-25-ns-nasdaq-itch-fpga-parser/blob/main/nasdaq-itch-parser.lisp. - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [7:0] event_code; - } raw_sysevent_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] symbol; - logic [7:0] trading_state; - } raw_sta_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] symbol; - logic [7:0] regsho_action; - } raw_regsho_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [7:0] buy_sell; - logic [31:0] num_shares; - logic [63:0] symbol; - logic [31:0] price; - } raw_add_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [7:0] buy_sell; - logic [31:0] num_shares; - logic [63:0] symbol; - logic [31:0] price; - logic [31:0] attribution; - } raw_addwmpid_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] old_order_id; - logic [63:0] new_order_id; - logic [31:0] num_shares; - logic [31:0] price; - } raw_replace_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [31:0] num_shares; - logic [63:0] match_number; - } raw_exec_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [31:0] num_shares; - logic [63:0] match_number; - logic [7:0] printable; - logic [31:0] price; - } raw_execwp_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [31:0] num_shares; - } raw_cancel_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - } raw_delete_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [63:0] order_id; - logic [7:0] buy_sell; - logic [31:0] num_shares; - logic [63:0] symbol; - logic [31:0] price; - logic [63:0] match_number; - } raw_trade_t; - - typedef struct packed { - logic [15:0] locate; - logic [15:0] tracking; - logic [47:0] timestamp; - logic [31:0] num_shares_msb; - logic [31:0] num_shares; - logic [31:0] price; - logic [63:0] match_number; - logic [7:0] cross_type; - } raw_crosstrade_t; - - // Params used for internal bit depths thoughout the core. - // Ensure these match the values in other parts of the repo. - parameter int PRICE_BITS = 32; - parameter int QUANTITY_BITS = 32; - parameter int ORDER_ID_BITS = 32; - parameter int RING_SIZE = 1024; // Size of side processor buffers - parameter int CACHE_SIZE = 5; // Size of k-best cache - parameter int MAX_ORDERS = 1024; // Order map size - - // The ITCH instructions that have an effect on the book. These types are - // straight from the parser. - typedef enum logic [3:0] { - OP_SYSEVENT = 4'h0, - OP_STA = 4'h1, - OP_REGSHO = 4'h2, - OP_ADD = 4'h3, - OP_ADDWMPID = 4'h4, - OP_REPLACE = 4'h5, - OP_EXEC = 4'h6, - OP_EXECWP = 4'h7, - OP_CANCEL = 4'h8, - OP_DELETE = 4'h9, - OP_TRADE = 4'ha, - OP_CROSSTRADE = 4'hb - } opcode_t; - - // These structs are now internal. - // For instance, price bucketing can allow us to use less area and have - // better cache usage. This bucketing isn't a concern of the parser, it's - // a concern of the decoder. - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] order_id; - order_side_t side; - logic [PRICE_BITS-1:0] price; - logic [QUANTITY_BITS-1:0] quantity; - } int_add_t; - - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] order_id; - logic [QUANTITY_BITS-1:0] quantity; - } int_exec_t; - - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] order_id; - logic [QUANTITY_BITS-1:0] quantity; - logic [PRICE_BITS-1:0] price; - } int_execwp_t; - - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] order_id; - logic [QUANTITY_BITS-1:0] quantity; - } int_cancel_t; - - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] order_id; - } int_delete_t; - - typedef struct packed { - itch_msg_type_t msg_type; - logic [ORDER_ID_BITS-1:0] old_order_id; - logic [ORDER_ID_BITS-1:0] new_order_id; - logic [PRICE_BITS-1:0] price; - logic [QUANTITY_BITS-1:0] quantity; - } int_replace_t; - - // Might need more of these for trade, crosstrade, etc. Idk much about - // those rn. - typedef union packed { - int_add_t add; - int_exec_t exec; - int_execwp_t execwp; - int_cancel_t cancel; - int_delete_t delete; - int_replace_t replace; - } inst_t; - - // The effect of an order on the book. - typedef struct packed { - logic valid; - order_side_t side; - logic [PRICE_BITS-1:0] price; - logic [QUANTITY_BITS-1:0] quantity; - } book_effect_t; - - // An entry in the k-best. - typedef struct packed { - logic [PRICE_BITS-1:0] price; - logic [QUANTITY_BITS-1:0] total; - } price_level_t; - - // The output of the order book that drives strategy. - typedef struct packed { - logic [PRICE_BITS-1:0] best_bid_price; - logic [QUANTITY_BITS-1:0] best_bid_quantity; - logic [PRICE_BITS-1:0] best_ask_price; - logic [QUANTITY_BITS-1:0] best_ask_quantity; - price_level_t bid_levels[CACHE_SIZE-1]; - price_level_t ask_levels[CACHE_SIZE-1]; - logic [PRICE_BITS-1:0] vwap; - logic [PRICE_BITS-1:0] moving_avg_short; - logic [PRICE_BITS-1:0] moving_avg_long; - logic [PRICE_BITS-1:0] volatility; - logic [QUANTITY_BITS-1:0] order_imbalance; - } accumulators_t; - -endpackage diff --git a/pipebomb/rtl/pipebomb_pkg.sv b/pipebomb/rtl/pipebomb_pkg.sv new file mode 100644 index 00000000..07254471 --- /dev/null +++ b/pipebomb/rtl/pipebomb_pkg.sv @@ -0,0 +1,53 @@ +package pipebomb_pkg; + parameter int ORDER_ID_BITS = 48; + parameter int PRICE_BITS = 48; // in ticks or cents + parameter int QTY_BITS = 32; + + // Q-format for prices, ema + parameter int PRICE_INT_W = 32; + parameter int PRICE_FRAC_W = 16; + parameter int PRICE_Q_W = PRICE_BITS + PRICE_FRAC_W; // 48 + 16 = 64 + + typedef enum logic [2:0] { + ITCH_NOP = 3'd0, + ITCH_ADD = 3'd1, + ITCH_CANCEL = 3'd2, + ITCH_DELETE = 3'd3, + ITCH_EXECUTE = 3'd4, + ITCH_REPLACE = 3'd5 + } opcode_t; + + typedef enum logic { + SIDE_BID = 1'b0, + SIDE_ASK = 1'b1 + } side_t; + + typedef struct packed { + opcode_t opcode; + logic valid; + logic [63:0] timestamp; + side_t side; + logic [ORDER_ID_BITS-1:0] order_id; + logic [PRICE_BITS-1:0] price; + logic [QTY_BITS-1:0] quantity; // for ADD/CANCEL/EXECUTE + logic [ORDER_ID_BITS-1:0] new_order_id; // REPLACE only + logic last_in_bundle; // for atomic REPLACE micro-seq bundling + } inst_t; + + typedef struct packed { + side_t side; + logic [PRICE_BITS-1:0] price; + logic [QTY_BITS-1:0] qty; + logic valid; + } order_info_t; + + function automatic logic [PRICE_Q_W-1:0] fxmul_q32_16_q1_15(input logic [PRICE_Q_W-1:0] a_q32_16, + input logic [15:0] b_q1_15); + logic [PRICE_Q_W+15:0] prod; + begin + prod = a_q32_16 * b_q1_15; + fxmul_q32_16_q1_15 = prod[PRICE_Q_W+15-:PRICE_Q_W]; + end + endfunction + +endpackage diff --git a/pipebomb/rtl/pipebomb_top.sv b/pipebomb/rtl/pipebomb_top.sv new file mode 100644 index 00000000..0ecacd2e --- /dev/null +++ b/pipebomb/rtl/pipebomb_top.sv @@ -0,0 +1,241 @@ +module pipebomb_top #( + parameter int K_LEVELS = 8 +) ( + input logic clk, + input logic rstn, + + input logic s_v, + output logic s_r, + input logic [ 2:0] s_opcode, + input logic s_valid, + input logic [63:0] s_timestamp, + input logic s_side, + input logic [47:0] s_order_id, + input logic [47:0] s_price, + input logic [31:0] s_quantity, + input logic [47:0] s_new_order_id, + input logic s_last_in_bundle, + + output logic tfill_tvalid, + input logic tfill_tready, + output logic tfill_side, + output logic [47:0] tfill_price, + output logic [31:0] tfill_qty, + output logic [47:0] best_bid_price, + output logic [47:0] best_ask_price, + output logic best_valid +); + import pipebomb_pkg::*; + + // repack as inst_t before fifo + inst_t s_inst; + always_comb begin + s_inst = '0; + + s_inst.opcode = opcode_t'(s_opcode); + s_inst.valid = s_valid; + s_inst.timestamp = s_timestamp; + s_inst.side = side_t'(s_side); + s_inst.order_id = s_order_id; + s_inst.price = s_price; + s_inst.quantity = s_quantity; + s_inst.new_order_id = s_new_order_id; + s_inst.last_in_bundle = s_last_in_bundle; + end + + inst_t f_d; + logic f_v, f_r; + logic overflow; + msg_fifo #( + .T(inst_t), + .DEPTH(32) + ) u_fifo ( + .clk(clk), + .rstn(rstn), + .in_v(s_v), + .in_r(s_r), + .in_d(s_inst), + .out_v(f_v), + .out_r(f_r), + .out_d(f_d), + .overflow(overflow) + ); + + always_ff @(posedge clk) if (rstn) assert (!overflow); + + // router/ordermap + inst_t r_d; + logic r_v, r_r; + + logic [ 2:0] r_d_opcode; + logic r_d_valid_bit; + logic r_d_side_bit; + logic [47:0] r_d_price_w; + logic [31:0] r_d_qty_w; + + assign r_d_opcode = r_d.opcode; + assign r_d_valid_bit= r_d.valid; + assign r_d_side_bit = r_d.side; + assign r_d_price_w = r_d.price; + assign r_d_qty_w = r_d.quantity; + + logic ask_side_sel, ask_opcode_ok, ask_valid_ok, ask_gate, bid_gate; + assign ask_side_sel = (r_d_side_bit == pipebomb_pkg::SIDE_ASK); + assign ask_opcode_ok = (r_d_opcode != pipebomb_pkg::ITCH_NOP); + assign ask_valid_ok = r_d_valid_bit; + assign ask_gate = ask_side_sel & ask_opcode_ok & ask_valid_ok; + + assign bid_gate = (r_d_side_bit == pipebomb_pkg::SIDE_BID) + & (r_d_opcode != pipebomb_pkg::ITCH_NOP) + & r_d_valid_bit; + + order_info_t prev_info; + router_ordermap #( + .DEPTH_LOG2 (10), + .CAM_ENTRIES(8) + ) u_map ( + .clk(clk), + .rstn(rstn), + .in_v(f_v), + .in_r(f_r), + .in_d(f_d), + .out_v(r_v), + .out_r(r_r), + .out_d(r_d), + .resolved_prev(prev_info) + ); + + logic sp_v; + logic sp_side; + logic [47:0] sp_price; + logic [31:0] sp_new_qty; + + logic [47:0] bid_best, ask_best; + logic bid_v, ask_v; + + logic ms_sp_v, ms_sp_r; + logic ms_sp_valid; + logic [ 2:0] ms_sp_opcode; + logic ms_sp_side; + logic [47:0] ms_sp_price; + logic [31:0] ms_sp_qty; + + match_stub u_match ( + .clk (clk), + .rstn(rstn), + + .in_v (r_v), + .in_r (r_r), // closes the loop to router + .in_opcode(r_d_opcode), + .in_side (r_d_side_bit), + .in_price (r_d_price_w), + .in_qty (r_d_qty_w), + + .best_bid_price(bid_best), + .best_ask_price(ask_best), + .best_bid_v (bid_v), + .best_ask_v (ask_v), + + .sp_v (ms_sp_v), + .sp_r (ms_sp_r), + .sp_valid (ms_sp_valid), + .sp_opcode (ms_sp_opcode), + .sp_side (ms_sp_side), + .sp_price (ms_sp_price), + .sp_qty (ms_sp_qty), + .sp_tap_v (sp_v), + .sp_tap_side (sp_side), + .sp_tap_price (sp_price), + .sp_tap_newqty(sp_new_qty), + + .tfill_v (tfill_tvalid), + .tfill_r (tfill_tready), + .tfill_side (tfill_side), + .tfill_price(tfill_price), + .tfill_qty (tfill_qty) + ); + + side_processor #( + .N_ENTRIES(64) + ) u_sp ( + .clk (clk), + .rstn (rstn), + .in_v (ms_sp_v), + .in_r (ms_sp_r), + .in_valid (ms_sp_valid), + .in_opcode (ms_sp_opcode), + .in_side (ms_sp_side), + .in_price (ms_sp_price), + .in_qty (ms_sp_qty), + .out_v (sp_v), + .out_r (1'b1), + .out_side (sp_side), + .out_price (sp_price), + .out_new_qty(sp_new_qty) + ); + + price_cache_sorted #( + .K_LEVELS(K_LEVELS) + ) u_bid ( + .clk (clk), + .rstn (rstn), + .update_valid(sp_v && (sp_side == SIDE_BID)), + .is_ask (1'b0), + .price (sp_price), + .new_qty (sp_new_qty), + .best_price (bid_best), + .best_valid (bid_v) + ); + price_cache_sorted #( + .K_LEVELS(K_LEVELS) + ) u_ask ( + .clk (clk), + .rstn (rstn), + .update_valid(sp_v && (sp_side == SIDE_ASK)), + .is_ask (1'b1), + .price (sp_price), + .new_qty (sp_new_qty), + .best_price (ask_best), + .best_valid (ask_v) + ); + + // if both sides valid, best bid < best ask + property p_spread_ok; + @(posedge clk) disable iff (!rstn) (bid_v && ask_v) |-> (bid_best < ask_best); + endproperty + assert property (p_spread_ok); + + logic [pipebomb_pkg::PRICE_Q_W-1:0] mid_q32_16, ema_q32_16; + logic ema_valid; + accumulators u_acc ( + .clk(clk), + .rstn(rstn), + .best_bid_price(best_bid_price), + .best_ask_price(best_ask_price), + .best_valid(best_valid), + .mid_q32_16(mid_q32_16), + .ema_q32_16(ema_q32_16), + .ema_valid(ema_valid) + ); + + // exposed combined + logic [47:0] best_bid_q, best_ask_q; + logic bid_v_q, ask_v_q; + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + best_bid_q <= '0; + best_ask_q <= '0; + bid_v_q <= 1'b0; + ask_v_q <= 1'b0; + end else begin + best_bid_q <= bid_best; + best_ask_q <= ask_best; + bid_v_q <= bid_v; + ask_v_q <= ask_v; + end + end + assign best_bid_price = best_bid_q; + assign best_ask_price = best_ask_q; + assign best_valid = bid_v_q && ask_v_q; + +endmodule diff --git a/pipebomb/rtl/price_cache_sorted.sv b/pipebomb/rtl/price_cache_sorted.sv new file mode 100644 index 00000000..7c401b7f --- /dev/null +++ b/pipebomb/rtl/price_cache_sorted.sv @@ -0,0 +1,101 @@ +module price_cache_sorted #( + parameter int K_LEVELS = 8 +) ( + input logic clk, + input logic rstn, + input logic update_valid, + input logic is_ask, + + input logic [47:0] price, + input logic [31:0] new_qty, + + output logic [47:0] best_price, + output logic best_valid +); + typedef struct packed { + logic [47:0] price; + logic [31:0] qty; + logic valid; + } level_t; + level_t levels[K_LEVELS]; + + function automatic int find_index(input logic [47:0] p); + find_index = -1; + for (int i = 0; i < K_LEVELS; i++) if (levels[i].valid && levels[i].price == p) find_index = i; + endfunction + + function automatic int worse_idx(); + for (int i = 0; i < K_LEVELS; i++) if (!levels[i].valid) return i; + return K_LEVELS - 1; + endfunction + + function automatic logic better(input level_t a, input level_t b, input logic is_ask_q); + if (!b.valid) return 1'b1; + if (!is_ask_q) begin + // for bid, higher price wins; tie -> larger qty + if (a.price > b.price) return 1'b1; + if (a.price < b.price) return 1'b0; + return (a.qty > b.qty); + end else begin + // for ask, lower price wins; tie -> larger qty + if (a.price < b.price) return 1'b1; + if (a.price > b.price) return 1'b0; + return (a.qty > b.qty); + end + endfunction + + task automatic bubble_up(input int idx, input logic is_ask_q); + level_t t; + for (int i = idx; i > 0; i--) begin + if (better(levels[i], levels[i-1], is_ask_q)) begin + t = levels[i-1]; + levels[i-1] = levels[i]; + levels[i] = t; + end + end + endtask + + task automatic bubble_down(input int idx, input logic is_ask_q); + level_t t; + for (int i = idx; i < K_LEVELS - 1; i++) begin + if (!better(levels[i], levels[i+1], is_ask_q)) begin + t = levels[i+1]; + levels[i+1] = levels[i]; + levels[i] = t; + end + end + endtask + + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + for (int i = 0; i < K_LEVELS; i++) levels[i].valid <= 1'b0; + end else if (update_valid) begin + int idx = find_index(price); + if (new_qty == 0) begin + if (idx >= 0) begin + for (int j = idx; j < K_LEVELS - 1; j++) levels[j] = levels[j+1]; + levels[K_LEVELS-1].valid <= 1'b0; + end + end else begin + if (idx >= 0) begin + levels[idx].qty <= new_qty; + bubble_up(idx, is_ask); + bubble_down(idx, is_ask); + end else begin + int wi = worse_idx(); + level_t cand; + cand.price = price; + cand.qty = new_qty; + cand.valid = 1'b1; + if (!levels[wi].valid || better(cand, levels[K_LEVELS-1], is_ask)) begin + levels[wi] = cand; + bubble_up(wi, is_ask); + end + end + end + end + end + + assign best_valid = levels[0].valid; + assign best_price = levels[0].price; +endmodule diff --git a/pipebomb/rtl/router_ordermap.sv b/pipebomb/rtl/router_ordermap.sv new file mode 100644 index 00000000..a4cc2624 --- /dev/null +++ b/pipebomb/rtl/router_ordermap.sv @@ -0,0 +1,207 @@ +module router_ordermap #( + parameter int DEPTH_LOG2 = 12, + parameter int CAM_ENTRIES = 8 +) ( + input logic clk, + input logic rstn, + + input logic in_v, + output logic in_r, + input pipebomb_pkg::inst_t in_d, + + output logic out_v, + input logic out_r, + output pipebomb_pkg::inst_t out_d, + + output pipebomb_pkg::order_info_t resolved_prev +); + import pipebomb_pkg::*; + + function automatic logic [DEPTH_LOG2-1:0] h0(input logic [ORDER_ID_BITS-1:0] k); + h0 = (k[DEPTH_LOG2-1:0] ^ (k[23+:DEPTH_LOG2])); + endfunction + + function automatic logic [DEPTH_LOG2-1:0] h1(input logic [ORDER_ID_BITS-1:0] k); + h1 = (k[DEPTH_LOG2+1-:DEPTH_LOG2] ^ (k[7+:DEPTH_LOG2])) + 13; + endfunction + + localparam int DEPTH = 1 << DEPTH_LOG2; + + typedef struct packed { + logic [ORDER_ID_BITS-1:0] key; + order_info_t info; + } entry_t; + + entry_t bank0 [DEPTH]; + entry_t bank1 [DEPTH]; + entry_t cam [CAM_ENTRIES]; + + // pipeline regs + inst_t s0_inst; + logic s0_v, s0_r; + logic [DEPTH_LOG2-1:0] s0_i0, s0_i1; + inst_t s1_inst; + logic s1_v, s1_r; + entry_t s1_b0_q, s1_b1_q; + + // ready chain + assign s1_r = (!s1_v) || out_r; + assign s0_r = s1_r; + assign in_r = (!s0_v) || s1_r; + + // S0: accept & index + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + s0_v <= 1'b0; + s0_inst <= '0; + s0_i0 <= '0; + s0_i1 <= '0; + end else if (in_r) begin + s0_v <= in_v; + s0_inst <= in_d; + s0_i0 <= h0(in_d.order_id); + s0_i1 <= h1(in_d.order_id); + end + end + + // S1: capture reads & instruction + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + s1_v <= 1'b0; + s1_inst <= '0; + s1_b0_q <= '0; + s1_b1_q <= '0; + end else if (s1_r) begin + s1_v <= s0_v; + s1_inst <= s0_inst; + // read banks for the instruction that just left S0 + s1_b0_q <= bank0[s0_i0]; + s1_b1_q <= bank1[s0_i1]; + end + end + + // hit detection & selection + logic s1_b0_hit, s1_b1_hit, s1_cam_hit; + entry_t s1_cam_ent, s1_sel_ent; + + always_comb begin + // bank hits against the registered S1 instruction + s1_b0_hit = s1_b0_q.info.valid && (s1_b0_q.key == s1_inst.order_id); + s1_b1_hit = s1_b1_q.info.valid && (s1_b1_q.key == s1_inst.order_id); + + // CAM search against S1 instruction + s1_cam_hit = 1'b0; + s1_cam_ent = '0; + for (int i = 0; i < CAM_ENTRIES; i++) begin + if (cam[i].info.valid && cam[i].key == s1_inst.order_id) begin + s1_cam_hit = 1'b1; + s1_cam_ent = cam[i]; + end + end + + // select hit entry + if (s1_b0_hit) s1_sel_ent = s1_b0_q; + else if (s1_b1_hit) s1_sel_ent = s1_b1_q; + else if (s1_cam_hit) s1_sel_ent = s1_cam_ent; + else s1_sel_ent = '0; + end + + // semantics + inst_t out_d_next; + order_info_t prev_info; + logic [QTY_BITS-1:0] dec; + + always_comb begin + out_d_next = s1_inst; + prev_info = s1_sel_ent.info; + dec = '0; + + unique case (s1_inst.opcode) + ITCH_ADD: begin + out_d_next.side = s1_inst.side; + out_d_next.price = s1_inst.price; + out_d_next.quantity = s1_inst.quantity; + out_d_next.valid = 1'b1; + end + ITCH_CANCEL, ITCH_EXECUTE: begin + out_d_next.side = prev_info.side; + out_d_next.price = prev_info.price; + dec = (s1_inst.quantity > prev_info.qty) ? prev_info.qty : s1_inst.quantity; + out_d_next.quantity = dec; + out_d_next.valid = prev_info.valid; + end + ITCH_DELETE: begin + out_d_next.side = prev_info.side; + out_d_next.price = prev_info.price; + out_d_next.quantity = prev_info.qty; + out_d_next.valid = prev_info.valid; + end + default: begin + out_d_next.opcode = ITCH_NOP; + out_d_next.valid = 1'b0; + end + endcase + end + + // out + assign out_v = s1_v && out_d_next.valid; + assign out_d = out_d_next; + assign resolved_prev = prev_info; + + // commit + task automatic write_map(input logic [ORDER_ID_BITS-1:0] key, input order_info_t info); + logic [DEPTH_LOG2-1:0] i0 = h0(key); + logic [DEPTH_LOG2-1:0] i1 = h1(key); + if (!bank0[i0].info.valid || bank0[i0].key == key) begin + bank0[i0].key = key; + bank0[i0].info = info; + end else if (!bank1[i1].info.valid || bank1[i1].key == key) begin + bank1[i1].key = key; + bank1[i1].info = info; + end else begin + for (int i = CAM_ENTRIES - 1; i > 0; i--) cam[i] = cam[i-1]; + cam[0] = '{key: key, info: info}; + end + endtask + + task automatic delete_map(input logic [ORDER_ID_BITS-1:0] key); + logic [DEPTH_LOG2-1:0] i0 = h0(key); + logic [DEPTH_LOG2-1:0] i1 = h1(key); + if (bank0[i0].info.valid && bank0[i0].key == key) bank0[i0].info.valid = 1'b0; + else if (bank1[i1].info.valid && bank1[i1].key == key) bank1[i1].info.valid = 1'b0; + else + for (int i = 0; i < CAM_ENTRIES; i++) + if (cam[i].info.valid && cam[i].key == key) cam[i].info.valid = 1'b0; + endtask + + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + for (int i = 0; i < DEPTH; i++) begin + bank0[i].info.valid = 1'b0; + bank1[i].info.valid = 1'b0; + end + for (int i = 0; i < CAM_ENTRIES; i++) cam[i].info.valid <= 1'b0; + end else if (s1_v && out_r) begin + case (s1_inst.opcode) + ITCH_ADD: begin + order_info_t ni; + ni.side = s1_inst.side; + ni.price = s1_inst.price; + ni.qty = s1_inst.quantity; + ni.valid = 1'b1; + write_map(s1_inst.order_id, ni); + end + ITCH_CANCEL, ITCH_EXECUTE: begin + if (prev_info.valid) begin + order_info_t ni = prev_info; + ni.qty = (s1_inst.quantity >= prev_info.qty) ? '0 : (prev_info.qty - s1_inst.quantity); + if (ni.qty == '0) ni.valid = 1'b0; + write_map(s1_inst.order_id, ni); + end + end + ITCH_DELETE: delete_map(s1_inst.order_id); + default: ; + endcase + end + end +endmodule diff --git a/pipebomb/rtl/side_processor.sv b/pipebomb/rtl/side_processor.sv new file mode 100644 index 00000000..826f6861 --- /dev/null +++ b/pipebomb/rtl/side_processor.sv @@ -0,0 +1,156 @@ +module side_processor #( + parameter int N_ENTRIES = 64, + parameter int PRICE_W = 48, + parameter int QTY_W = 32 +) ( + input logic clk, + input logic rstn, + + // resolved micro-op + input logic in_v, + output logic in_r, + input logic in_valid, + input pipebomb_pkg::opcode_t in_opcode, + input logic in_side, + input logic [PRICE_W-1:0] in_price, + input logic [ QTY_W-1:0] in_qty, + + // absolute totals + output logic out_v, + input logic out_r, + output logic out_side, + output logic [PRICE_W-1:0] out_price, + output logic [ QTY_W-1:0] out_new_qty +); + import pipebomb_pkg::*; + + typedef struct packed { + logic [PRICE_W-1:0] price; + logic [QTY_W-1:0] qty; + logic valid; + } entry_t; + + entry_t bid_tab[N_ENTRIES]; + entry_t ask_tab[N_ENTRIES]; + + assign in_r = out_r; + + // linear search; N small + function automatic int find_idx(input logic side, input logic [PRICE_W-1:0] p); + find_idx = -1; + if (!side) begin + for (int i = 0; i < N_ENTRIES; i++) + if (bid_tab[i].valid && bid_tab[i].price == p) begin + find_idx = i; + break; + end + end else begin + for (int i = 0; i < N_ENTRIES; i++) + if (ask_tab[i].valid && ask_tab[i].price == p) begin + find_idx = i; + break; + end + end + endfunction + + function automatic int first_free(input logic side); + first_free = -1; + if (!side) begin + for (int i = 0; i < N_ENTRIES; i++) + if (!bid_tab[i].valid) begin + first_free = i; + break; + end + end else begin + for (int i = 0; i < N_ENTRIES; i++) + if (!ask_tab[i].valid) begin + first_free = i; + break; + end + end + endfunction + + // single sequential block: compute nxt, update table, and register outputs + always_ff @(posedge clk or negedge rstn) begin + if (!rstn) begin + for (int i = 0; i < N_ENTRIES; i++) begin + bid_tab[i].valid <= 1'b0; + ask_tab[i].valid <= 1'b0; + end + out_v <= 1'b0; + out_side <= 1'b0; + out_price <= '0; + out_new_qty <= '0; + end else begin + // default: no event this cycle + out_v <= 1'b0; + + if (in_v && in_valid && out_r) begin + int idx; + logic [QTY_W-1:0] cur; + logic [QTY_W-1:0] nxt; + + // read current total + idx = find_idx(in_side, in_price); + cur = '0; + if (!in_side) begin + if (idx >= 0) cur = bid_tab[idx].qty; + end else begin + if (idx >= 0) cur = ask_tab[idx].qty; + end + + // compute new per opcode + nxt = cur; + unique case (in_opcode) + ITCH_ADD: nxt = cur + in_qty; + ITCH_CANCEL, ITCH_EXECUTE: nxt = (in_qty >= cur) ? '0 : (cur - in_qty); + ITCH_DELETE: nxt = '0; + default: nxt = cur; + endcase + + // one-cycle latency on this + out_v <= 1'b1; + out_side <= in_side; + out_price <= in_price; + out_new_qty <= nxt; + + // wb + if (!in_side) begin + if (nxt == '0) begin + if (idx >= 0) bid_tab[idx].valid <= 1'b0; + end else begin + if (idx < 0) begin + int freei; + freei = first_free(1'b0); + if (freei < 0) freei = 0; // naive overwrite if full + bid_tab[freei].price <= in_price; + bid_tab[freei].qty <= nxt; + bid_tab[freei].valid <= 1'b1; + end else begin + bid_tab[idx].qty <= nxt; + bid_tab[idx].valid <= 1'b1; + end + end + end else begin + if (nxt == '0) begin + if (idx >= 0) ask_tab[idx].valid <= 1'b0; + end else begin + if (idx < 0) begin + int freei; + freei = first_free(1'b1); + if (freei < 0) freei = 0; + ask_tab[freei].price <= in_price; + ask_tab[freei].qty <= nxt; + ask_tab[freei].valid <= 1'b1; + end else begin + ask_tab[idx].qty <= nxt; + ask_tab[idx].valid <= 1'b1; + end + end + end + end + end + end + +endmodule + diff --git a/pipebomb/tb/.gitkeep b/pipebomb/tb/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/pipebomb/tb/tb_pipebomb_cross.cpp b/pipebomb/tb/tb_pipebomb_cross.cpp new file mode 100644 index 00000000..91baeda3 --- /dev/null +++ b/pipebomb/tb/tb_pipebomb_cross.cpp @@ -0,0 +1,201 @@ +#include "Vpipebomb_top.h" +#include "verilated.h" +#include "verilated_vcd_c.h" +#include +#include +#include +#include + +static vluint64_t sim_time = 0; + +static VerilatedVcdC* g_tfp = nullptr; // global so we can flush on failure + +[[noreturn]] static void die_with_trace(const char* msg) { + fprintf(stderr, "FATAL @%llu: %s\n", + (unsigned long long)sim_time, msg); + if (g_tfp) { g_tfp->flush(); g_tfp->close(); } + Verilated::flushCall(); + exit(1); +} + +struct Sim { + Vpipebomb_top *dut; + VerilatedVcdC *tfp; + bool tracing; + + Sim(bool trace = true) : tracing(trace) { + Verilated::traceEverOn(tracing); + dut = new Vpipebomb_top; + if (tracing) { + tfp = new VerilatedVcdC; + g_tfp = tfp; + dut->trace(tfp, 99); + tfp->open("wave.vcd"); + } else { + tfp = nullptr; + } + } + ~Sim() { + if (tfp) { tfp->flush(); tfp->close(); delete tfp; } + delete dut; + } + + void tick() { + dut->tfill_tready = 1; // keep fills path open every cycle + + // 10ns clock, posedge sim_time%10==5 + dut->clk = 0; + dut->eval(); + if (tfp) { tfp->dump(sim_time); tfp->flush(); } + sim_time += 5; + dut->clk = 1; + dut->eval(); + if (tfp) { tfp->dump(sim_time); tfp->flush(); } + + sim_time += 5; + } + + void idle(int cycles = 1) { + for (int i = 0; i < cycles; i++) + tick(); + } +}; + +static void drive_defaults(Vpipebomb_top *d) { + d->s_v = 0; + d->s_valid = 0; + d->s_opcode = 0; + d->s_timestamp = 0; + d->s_side = 0; + d->s_order_id = 0; + d->s_price = 0; + d->s_quantity = 0; + d->s_new_order_id = 0; + d->s_last_in_bundle = 1; +} + +static void reset_seq(Sim &S, int low_cycles = 4, int high_wait = 4) { + auto *d = S.dut; + drive_defaults(d); + d->rstn = 0; + S.idle(low_cycles); + d->rstn = 1; + S.idle(high_wait); +} + +static void send_add(Sim &S, bool side_is_ask, uint64_t px, uint32_t qty) { + auto *d = S.dut; + // prepare payload + d->s_opcode = 1; // ITCH_ADD + d->s_valid = 1; + d->s_side = side_is_ask ? 1 : 0; + d->s_price = px; + d->s_quantity = qty; + d->s_v = 1; + + // hand over when DUT is ready + int spins = 0; + while (!d->s_r) { + S.tick(); + if (++spins > 1000) { + die_with_trace("ERROR: s_r never went high"); + } + } + + // one cycle where s_v && s_r are both 1 + S.tick(); + + // deassert + d->s_v = 0; + d->s_valid = 0; + S.idle(1); +} + +#define CHECK(cond, msg) do { \ + if (!(cond)) die_with_trace(msg); \ +} while(0) + +static void wait_cycles(Sim &S, int n) { + for (int i = 0; i < n; ++i) + S.idle(1); +} + +template +static int wait_until(Sim &S, Pred p, int max_cycles, const char *what) { + for (int i = 0; i < max_cycles; ++i) { + S.idle(1); + if (p()) + return i + 1; // cycles waited + } + char buf[256]; + snprintf(buf, sizeof(buf), "timeout waiting for: %s (>%d cycles)", what, max_cycles); + die_with_trace(buf); +} + +static int expect_fill(Sim &S, uint32_t exp_side, uint64_t exp_px, + uint32_t exp_qty, int max_cycles) { + auto *d = S.dut; + int waited = wait_until( + S, [&] { return d->tfill_tvalid; }, max_cycles, "tfill_tvalid"); + printf("[FILL @%llu] maker_side=%u price=%llu qty=%u (waited %d cycles)\n", + (unsigned long long)sim_time, (unsigned)d->tfill_side, + (unsigned long long)d->tfill_price, (unsigned)d->tfill_qty, waited); + CHECK(d->tfill_side == exp_side, "fill maker side mismatch"); + CHECK(d->tfill_price == exp_px, "fill price mismatch"); + CHECK(d->tfill_qty == exp_qty, "fill qty mismatch"); + return waited; +} + +static void sigint_handler(int){ die_with_trace("SIGINT"); } +int main(int argc, char **argv) { + Verilated::commandArgs(argc, argv); + Sim S(true); + auto *d = S.dut; + + reset_seq(S); + + // ask 101 x 5 (becomes best_ask) + send_add(S, /*ask*/ true, 101, 5); + int lat1 = wait_until( + S, [&] { return d->best_ask_price == 101; }, 100, + "best_ask == 101 after first insert"); + printf("best_ask=101 visible after %d cycles\n", lat1); + CHECK(d->best_ask_price == 101, "best_ask should be 101 after first insert"); + + + // 2) Add BID 103 x 7 -> marketable; expect a fill at 101 *for 5* (capped) + send_add(S, /*ask*/ false, 103, 7); + + // fill should be qty 5 now (min(resting=5, taker=7)) + int lat_fill = expect_fill(S, /*maker side=*/1, /*px=*/101, /*qty=*/5, /*max*/100); + + // After the capped fill, the residual (2) should be booked as BID @ 103 + int lat_bid = wait_until(S, [&]{ return d->best_bid_price == 103; }, 100, + "best_bid == 103 after booking residual"); + printf("residual booked: best_bid=103 visible after %d cycles\n", lat_bid); + + + // wait a couple cycles for book aggregate decrement to ripple + // TODO: optimize this + wait_cycles(S, 2); + printf("post-cross: best_bid=%llu best_ask=%llu best_valid=%u\n", + (unsigned long long)d->best_bid_price, + (unsigned long long)d->best_ask_price, (unsigned)d->best_valid); + + // ask 120 x 4 (so new best_ask=120) + send_add(S, /*ask*/ true, 120, 4); + int lat2 = wait_until( + S, [&] { return d->best_ask_price == 120; }, 100, + "best_ask == 120 after non-marketable add"); + printf("best_ask=120 visible after %d cycles\n", lat2); + CHECK(d->best_ask_price == 120, + "best_ask should reflect new non-marketable add"); + + printf("PASS: tb_pipebomb_cross (lat1=%d, lat_fill=%d, lat2=%d)\n", lat1, + lat_fill, lat2); + + + wait_cycles(S, 29); + + return 0; +}