Skip to main content

hypercall_engine/
orderbook.rs

1//! Pure orderbook data structure and matching engine.
2//!
3//! This module provides [`OrderBook`], a price-time priority limit order book
4//! for a single instrument. It supports:
5//!
6//! - Adding and cancelling orders with full metadata
7//! - Incremental matching (one maker at a time) with self-trade prevention
8//! - L2 (price-level) snapshot and incremental diff computation
9//! - Snapshot/restore for WAL-based persistence
10//!
11//! # Design
12//!
13//! The orderbook is a **pure data structure** with no async runtime, no channels,
14//! and no side effects. All operations are synchronous and deterministic.
15//!
16//! Event emission is handled by the caller. The orderbook computes L2 diffs and
17//! returns fill results, but never sends events itself. The runtime layer in the
18//! `hypercall` crate is responsible for routing these to WebSocket clients,
19//! persistence, and downstream caches.
20//!
21//! # Price-level storage
22//!
23//! Bids are stored in a `BTreeMap` keyed by negated price for descending order.
24//! Asks are stored with natural (ascending) price keys. Within each price level,
25//! orders are stored in a `VecDeque` for FIFO (time priority) matching.
26
27use hypercall_types::{
28    Fill, L2Message, L2Update, OptionType, OrderbookUpdate, Side, TradeMessage, TradeSide,
29    WalletAddress,
30};
31use rust_decimal::Decimal;
32use rust_decimal_macros::dec;
33use std::collections::{BTreeMap, HashMap, VecDeque};
34use tracing::{debug, error, info, warn};
35
36/// Result of attempting to match a single maker order.
37#[derive(Debug, Clone)]
38pub enum MatchResult {
39    /// Successful fill.
40    Fill(Fill),
41    /// Self-trade detected -- contains maker order ID that should be cancelled.
42    SelfTrade { maker_order_id: u64 },
43    /// No match possible (price does not cross or no liquidity).
44    NoMatch,
45}
46
47/// An order resting on a price level.
48#[derive(Debug, Clone)]
49pub struct Order {
50    pub id: u64,
51    pub price: Decimal,
52    pub quantity: Decimal,
53    pub side: Side,
54    pub timestamp: u64,
55}
56
57/// Metadata stored alongside the price-level [`Order`].
58///
59/// Keeps the hot-path `Order` struct lean (used in matching) while storing
60/// extra metadata needed for snapshot persistence and order index rebuild.
61#[derive(Debug, Clone)]
62struct OrderMeta {
63    side: Side,
64    price: Decimal,
65    wallet: WalletAddress,
66    client_id: Option<String>,
67    mmp_enabled: bool,
68    original_size: Decimal,
69}
70
71/// Full order record returned by [`OrderBook::get_all_orders`] and accepted
72/// by [`OrderBook::restore_from_orders`].
73#[derive(Debug, Clone)]
74pub struct OrderRecord {
75    pub order_id: u64,
76    pub price: Decimal,
77    pub quantity: Decimal,
78    pub side: Side,
79    pub wallet: WalletAddress,
80    pub timestamp: u64,
81    pub client_id: Option<String>,
82    pub mmp_enabled: bool,
83    pub original_size: Decimal,
84}
85
86#[derive(Debug, Clone)]
87struct PriceLevel {
88    price: Decimal,
89    orders: VecDeque<Order>,
90    total_quantity: Decimal,
91}
92
93impl PriceLevel {
94    fn new(price: Decimal) -> Self {
95        Self {
96            price,
97            orders: VecDeque::new(),
98            total_quantity: dec!(0),
99        }
100    }
101
102    fn add_order(&mut self, order: Order) {
103        self.total_quantity += order.quantity;
104        self.orders.push_back(order);
105    }
106
107    fn remove_order(&mut self, order_id: u64) -> Option<Order> {
108        if let Some(pos) = self.orders.iter().position(|o| o.id == order_id) {
109            let order = self.orders.remove(pos)?;
110            self.total_quantity -= order.quantity;
111            Some(order)
112        } else {
113            None
114        }
115    }
116
117    fn is_empty(&self) -> bool {
118        self.orders.is_empty()
119    }
120}
121
122/// Events produced by orderbook operations.
123///
124/// The caller collects these from [`OrderBook::drain_events`] after each
125/// mutation and routes them to the appropriate downstream consumers
126/// (WebSocket clients, persistence, metrics).
127#[derive(Debug, Clone)]
128pub enum OrderBookEvent {
129    /// An order was filled (a trade occurred).
130    OrderFilled(Fill),
131    /// A trade message for the public trade feed.
132    Trade(TradeMessage),
133    /// Incremental L2 (price-level) update.
134    L2Update(L2Message),
135    /// Full orderbook snapshot (legacy, for backward compatibility).
136    OrderbookUpdated(OrderbookUpdate),
137}
138
139/// A pure, synchronous limit order book for a single instrument.
140///
141/// All state transitions are deterministic. Events are collected into an
142/// internal buffer and must be drained by the caller via [`drain_events`](Self::drain_events).
143#[derive(Debug, Clone)]
144pub struct OrderBook {
145    pub expiry: u64,
146    pub strike: Decimal,
147    pub option_type: OptionType,
148    pub symbol: String,
149    bids: BTreeMap<Decimal, PriceLevel>, // Keys are negated for descending order
150    asks: BTreeMap<Decimal, PriceLevel>,
151    orders: HashMap<u64, OrderMeta>,
152    /// Buffered events produced by the last operation.
153    pending_events: Vec<OrderBookEvent>,
154    pub last_bid_snapshot: Vec<(Decimal, Decimal)>,
155    pub last_ask_snapshot: Vec<(Decimal, Decimal)>,
156    pub pending_l2_sequence: Option<i64>,
157}
158
159impl OrderBook {
160    fn remove_order_from_side_at_price(
161        book: &mut BTreeMap<Decimal, PriceLevel>,
162        price_key: Decimal,
163        order_id: u64,
164    ) -> Option<Order> {
165        let (order, remove_level) = {
166            let level = book.get_mut(&price_key)?;
167            let order = level.remove_order(order_id);
168            let remove_level = level.is_empty();
169            (order, remove_level)
170        };
171
172        if remove_level {
173            book.remove(&price_key);
174        }
175
176        order
177    }
178
179    fn remove_order_from_side_anywhere(
180        book: &mut BTreeMap<Decimal, PriceLevel>,
181        order_id: u64,
182    ) -> Option<Order> {
183        let price_key = book.iter().find_map(|(price_key, level)| {
184            level
185                .orders
186                .iter()
187                .any(|order| order.id == order_id)
188                .then_some(*price_key)
189        })?;
190
191        Self::remove_order_from_side_at_price(book, price_key, order_id)
192    }
193
194    pub fn new(expiry: u64, strike: Decimal, option_type: OptionType) -> Self {
195        Self::with_symbol(expiry, strike, option_type, String::new())
196    }
197
198    pub fn with_symbol(
199        expiry: u64,
200        strike: Decimal,
201        option_type: OptionType,
202        symbol: String,
203    ) -> Self {
204        Self {
205            expiry,
206            strike,
207            option_type,
208            symbol,
209            bids: BTreeMap::new(),
210            asks: BTreeMap::new(),
211            orders: HashMap::new(),
212            pending_events: Vec::new(),
213            last_bid_snapshot: Vec::new(),
214            last_ask_snapshot: Vec::new(),
215            pending_l2_sequence: None,
216        }
217    }
218
219    /// Drain all pending events produced by recent operations.
220    ///
221    /// The caller must drain events after each mutation to forward them to
222    /// downstream consumers. Events are returned in the order they were produced.
223    pub fn drain_events(&mut self) -> Vec<OrderBookEvent> {
224        std::mem::take(&mut self.pending_events)
225    }
226
227    pub fn add_order(
228        &mut self,
229        order_id: u64,
230        price: Decimal,
231        quantity: Decimal,
232        side: Side,
233        timestamp: u64,
234    ) {
235        self.add_order_with_wallet(
236            order_id,
237            price,
238            quantity,
239            side,
240            WalletAddress::from(alloy::primitives::Address::ZERO),
241            timestamp,
242        )
243    }
244
245    pub fn set_pending_l2_sequence(&mut self, sequence: i64) {
246        self.pending_l2_sequence = Some(sequence);
247    }
248
249    pub fn add_order_with_wallet(
250        &mut self,
251        order_id: u64,
252        price: Decimal,
253        quantity: Decimal,
254        side: Side,
255        wallet: WalletAddress,
256        timestamp: u64,
257    ) {
258        self.add_order_with_metadata(
259            order_id, price, quantity, side, wallet, timestamp, None, false, quantity,
260        );
261    }
262
263    /// Add an order with full metadata (client_id, mmp_enabled, original_size).
264    ///
265    /// Production code paths should prefer this over `add_order_with_wallet()`
266    /// so that metadata survives WAL snapshot round-trips.
267    pub fn add_order_with_metadata(
268        &mut self,
269        order_id: u64,
270        price: Decimal,
271        quantity: Decimal,
272        side: Side,
273        wallet: WalletAddress,
274        timestamp: u64,
275        client_id: Option<String>,
276        mmp_enabled: bool,
277        original_size: Decimal,
278    ) {
279        let order = Order {
280            id: order_id,
281            price,
282            quantity,
283            side,
284            timestamp,
285        };
286
287        let price_key = match side {
288            Side::Buy => -price,
289            Side::Sell => price,
290        };
291
292        let book = match side {
293            Side::Buy => &mut self.bids,
294            Side::Sell => &mut self.asks,
295        };
296
297        self.orders.insert(
298            order_id,
299            OrderMeta {
300                side,
301                price,
302                wallet,
303                client_id,
304                mmp_enabled,
305                original_size,
306            },
307        );
308
309        book.entry(price_key)
310            .or_insert_with(|| PriceLevel::new(price))
311            .add_order(order);
312    }
313
314    /// O(1) count of live orders in this book.
315    pub fn order_count(&self) -> usize {
316        self.orders.len()
317    }
318
319    /// Append lightweight `(order_id, wallet)` pairs for live orders.
320    ///
321    /// This avoids constructing full [`OrderRecord`] values when callers only
322    /// need ownership metadata for snapshot fallback paths.
323    pub fn append_order_wallets(&self, out: &mut Vec<(u64, WalletAddress)>) {
324        out.extend(
325            self.orders
326                .iter()
327                .map(|(order_id, meta)| (*order_id, meta.wallet)),
328        );
329    }
330
331    /// Check if an order exists in the orderbook.
332    pub fn has_order(&self, order_id: u64) -> bool {
333        self.orders.contains_key(&order_id)
334    }
335
336    /// Get all order IDs in this orderbook.
337    pub fn get_all_order_ids(&self) -> Vec<u64> {
338        self.orders.keys().copied().collect()
339    }
340
341    pub fn cancel_order(&mut self, order_id: u64) -> Option<Order> {
342        let meta = self.orders.remove(&order_id)?;
343        let (side, price) = (meta.side, meta.price);
344
345        let price_key = match side {
346            Side::Buy => -price,
347            Side::Sell => price,
348        };
349
350        let book = match side {
351            Side::Buy => &mut self.bids,
352            Side::Sell => &mut self.asks,
353        };
354
355        if let Some(level) = book.get_mut(&price_key) {
356            let order = level.remove_order(order_id);
357            if level.is_empty() {
358                book.remove(&price_key);
359            }
360            order
361        } else {
362            None
363        }
364    }
365
366    /// Recovery-only cancel path.
367    ///
368    /// During replay, the cancel command is authoritative. If a stale snapshot
369    /// left an order at the wrong price level, wrong side, or with missing
370    /// metadata, scrub that `order_id` from the book instead of leaving a
371    /// ghost order that can cross a newer replayed order.
372    pub fn cancel_order_for_replay(&mut self, order_id: u64) -> Option<Order> {
373        let meta = self.orders.get(&order_id).cloned();
374
375        let removed = if let Some(meta) = meta.as_ref() {
376            let price_key = match meta.side {
377                Side::Buy => -meta.price,
378                Side::Sell => meta.price,
379            };
380
381            let primary = match meta.side {
382                Side::Buy => {
383                    Self::remove_order_from_side_at_price(&mut self.bids, price_key, order_id)
384                }
385                Side::Sell => {
386                    Self::remove_order_from_side_at_price(&mut self.asks, price_key, order_id)
387                }
388            };
389
390            if primary.is_some() {
391                primary
392            } else {
393                let fallback = Self::remove_order_from_side_anywhere(&mut self.bids, order_id)
394                    .or_else(|| Self::remove_order_from_side_anywhere(&mut self.asks, order_id));
395
396                if fallback.is_some() {
397                    error!(
398                        "RECOVERY_INVARIANT: order {} for {} was not present at replay cancel target {:?}@{}; removed via full-book scan",
399                        order_id, self.symbol, meta.side, meta.price
400                    );
401                }
402
403                fallback
404            }
405        } else {
406            let fallback = Self::remove_order_from_side_anywhere(&mut self.bids, order_id)
407                .or_else(|| Self::remove_order_from_side_anywhere(&mut self.asks, order_id));
408
409            if fallback.is_some() {
410                error!(
411                    "RECOVERY_INVARIANT: order {} for {} existed in price levels without metadata during replay cancel; removed ghost order",
412                    order_id, self.symbol
413                );
414            }
415
416            fallback
417        };
418
419        let removed_meta = self.orders.remove(&order_id);
420        if removed.is_none() && removed_meta.is_some() {
421            error!(
422                "RECOVERY_INVARIANT: replay cancel removed metadata-only ghost order {} from {}",
423                order_id, self.symbol
424            );
425        }
426
427        removed
428    }
429
430    /// Reduce an order's quantity by `qty`. If remaining quantity hits zero,
431    /// the order is removed from the book. Returns `true` if the order was
432    /// fully consumed (removed), `false` if partially reduced or not found.
433    ///
434    /// Used during journal replay to apply fills that occurred after the
435    /// maker's original command was replayed.
436    pub fn reduce_order_quantity(&mut self, order_id: u64, qty: Decimal) -> bool {
437        let (side, price) = match self.orders.get(&order_id) {
438            Some(meta) => (meta.side, meta.price),
439            None => return false,
440        };
441
442        let price_key = match side {
443            Side::Buy => -price,
444            Side::Sell => price,
445        };
446
447        let book = match side {
448            Side::Buy => &mut self.bids,
449            Side::Sell => &mut self.asks,
450        };
451
452        if let Some(level) = book.get_mut(&price_key) {
453            if let Some(order) = level.orders.iter_mut().find(|o| o.id == order_id) {
454                order.quantity -= qty;
455                level.total_quantity -= qty;
456
457                if order.quantity <= dec!(0) {
458                    // Fully consumed -- remove from the book
459                    level.remove_order(order_id);
460                    if level.is_empty() {
461                        book.remove(&price_key);
462                    }
463                    self.orders.remove(&order_id);
464                    return true;
465                }
466            }
467        }
468        false
469    }
470
471    pub fn get_best_bid(&self) -> Option<Decimal> {
472        self.bids.keys().next().map(|k| -(*k))
473    }
474
475    pub fn get_best_ask(&self) -> Option<Decimal> {
476        self.asks.keys().next().copied()
477    }
478
479    /// Returns true if the orderbook is in a crossed state (best bid >= best ask).
480    pub fn is_crossed(&self) -> bool {
481        if let (Some(best_bid), Some(best_ask)) = (self.get_best_bid(), self.get_best_ask()) {
482            best_bid >= best_ask
483        } else {
484            false
485        }
486    }
487
488    pub fn get_bid_depth(&self) -> Vec<(Decimal, Decimal)> {
489        self.bids
490            .values()
491            .map(|level| (level.price, level.total_quantity))
492            .collect()
493    }
494
495    pub fn get_ask_depth(&self) -> Vec<(Decimal, Decimal)> {
496        self.asks
497            .values()
498            .map(|level| (level.price, level.total_quantity))
499            .collect()
500    }
501
502    pub fn get_spread(&self) -> Option<Decimal> {
503        match (self.get_best_bid(), self.get_best_ask()) {
504            (Some(bid), Some(ask)) => Some(ask - bid),
505            _ => None,
506        }
507    }
508
509    pub fn total_bid_volume(&self) -> Decimal {
510        self.bids.values().map(|level| level.total_quantity).sum()
511    }
512
513    pub fn total_ask_volume(&self) -> Decimal {
514        self.asks.values().map(|level| level.total_quantity).sum()
515    }
516
517    /// Process an order incrementally -- matches one maker order at a time.
518    ///
519    /// Convenience wrapper that stores default metadata. Tests should use this;
520    /// production callers should prefer `process_order_with_metadata()`.
521    pub fn process_order(
522        &mut self,
523        order_id: u64,
524        price: Decimal,
525        quantity: Decimal,
526        side: Side,
527        wallet: WalletAddress,
528        timestamp: u64,
529        trade_id: u64,
530    ) -> (MatchResult, bool) {
531        self.process_order_with_metadata(
532            order_id, price, quantity, side, wallet, timestamp, trade_id, None, false, quantity,
533        )
534    }
535
536    /// Process an order incrementally with full metadata.
537    ///
538    /// Returns: `(MatchResult, bool)`
539    ///   - `MatchResult`: The result of matching (Fill, SelfTrade, or NoMatch)
540    ///   - `bool`: Whether more matching is possible (`true` = can call again,
541    ///     `false` = order complete/rejected/self-trade)
542    ///
543    /// Note: For SelfTrade result, the order is NOT added to the book.
544    /// The caller must handle cancellation.
545    ///
546    /// Events (fills, L2 updates, orderbook snapshots) are buffered internally.
547    /// Call [`drain_events`](Self::drain_events) to retrieve them.
548    pub fn process_order_with_metadata(
549        &mut self,
550        order_id: u64,
551        price: Decimal,
552        quantity: Decimal,
553        side: Side,
554        wallet: WalletAddress,
555        timestamp: u64,
556        trade_id: u64,
557        client_id: Option<String>,
558        mmp_enabled: bool,
559        original_size: Decimal,
560    ) -> (MatchResult, bool) {
561        debug!(
562            "Processing order incrementally: symbol={}, price={}, quantity={}, side={:?}, wallet={}",
563            self.symbol, price, quantity, side, wallet
564        );
565
566        if quantity == dec!(0) {
567            return (MatchResult::NoMatch, false);
568        }
569
570        // Try to match one maker order
571        match self.try_match_one_maker(
572            order_id, price, quantity, &side, &wallet, timestamp, trade_id,
573        ) {
574            MatchResult::Fill(fill) => {
575                // Emit events for the fill
576                self.emit_events(std::slice::from_ref(&fill), timestamp);
577
578                // Check if more matching is possible
579                let remaining = quantity - fill.size;
580                let has_more = remaining > dec!(0) && self.has_more_liquidity(&side, price);
581
582                (MatchResult::Fill(fill), has_more)
583            }
584            MatchResult::SelfTrade { maker_order_id } => {
585                // Self-trade detected - DO NOT add order to book
586                // Caller (unified_engine) will handle cancelling both orders
587                (MatchResult::SelfTrade { maker_order_id }, false)
588            }
589            MatchResult::NoMatch => {
590                // No match - add order to book and emit events
591                self.add_order_to_book_with_events_full(
592                    order_id,
593                    price,
594                    quantity,
595                    side,
596                    wallet,
597                    timestamp,
598                    client_id,
599                    mmp_enabled,
600                    original_size,
601                );
602                // Return false for should_continue since order is now in the book
603                (MatchResult::NoMatch, false)
604            }
605        }
606    }
607
608    /// Try to match with one maker order.
609    fn try_match_one_maker(
610        &mut self,
611        order_id: u64,
612        price: Decimal,
613        quantity: Decimal,
614        side: &Side,
615        wallet: &WalletAddress,
616        timestamp: u64,
617        trade_id: u64,
618    ) -> MatchResult {
619        match side {
620            Side::Buy => self.try_match_buy(order_id, price, quantity, wallet, timestamp, trade_id),
621            Side::Sell => {
622                self.try_match_sell(order_id, price, quantity, wallet, timestamp, trade_id)
623            }
624        }
625    }
626
627    /// Try to match a buy order with the best ask.
628    fn try_match_buy(
629        &mut self,
630        order_id: u64,
631        price: Decimal,
632        quantity: Decimal,
633        wallet: &WalletAddress,
634        timestamp: u64,
635        trade_id: u64,
636    ) -> MatchResult {
637        let best_ask_price = match self.get_best_ask() {
638            Some(p) => p,
639            None => return MatchResult::NoMatch,
640        };
641        if best_ask_price > price {
642            return MatchResult::NoMatch;
643        }
644
645        let price_key = best_ask_price;
646        let level = match self.asks.get_mut(&price_key) {
647            Some(l) => l,
648            None => return MatchResult::NoMatch,
649        };
650        let maker_order = match level.orders.front().cloned() {
651            Some(o) => o,
652            None => return MatchResult::NoMatch,
653        };
654
655        let maker_id = maker_order.id;
656        let maker_wallet = match self.orders.get(&maker_id) {
657            Some(meta) => meta.wallet,
658            None => {
659                // Data inconsistency: order in price level but not in orders map
660                warn!(
661                    "Data inconsistency: maker_order_id={} in ask level but not in orders map, symbol={}",
662                    maker_id, self.symbol
663                );
664                return MatchResult::NoMatch;
665            }
666        };
667
668        // SELF-TRADE CHECK: Compare taker wallet with maker wallet
669        if wallet == &maker_wallet {
670            warn!(
671                "STP_AUDIT: self-trade on BUY side: symbol={}, taker_id={}, taker_price={}, taker_qty={}, maker_id={}, maker_price={}, maker_qty={}, wallet={}",
672                self.symbol, order_id, price, quantity,
673                maker_id, best_ask_price, maker_order.quantity, wallet
674            );
675            return MatchResult::SelfTrade {
676                maker_order_id: maker_id,
677            };
678        }
679
680        let match_quantity = maker_order.quantity.min(quantity);
681
682        info!(
683            "MATCH FOUND: Buy order matched with ask - price={}, quantity={}, maker_id={}, trade_id={}",
684            best_ask_price, match_quantity, maker_id, trade_id
685        );
686
687        // Update or remove maker order
688        if match_quantity >= maker_order.quantity {
689            self.cancel_order(maker_id);
690        } else if let Some(level) = self.asks.get_mut(&price_key) {
691            if let Some(order) = level.orders.front_mut() {
692                order.quantity -= match_quantity;
693                level.total_quantity -= match_quantity;
694            }
695        }
696
697        MatchResult::Fill(Fill {
698            trade_id,
699            taker_order_id: order_id,
700            maker_order_id: maker_id,
701            symbol: self.symbol.clone(),
702            price: best_ask_price,
703            size: match_quantity,
704            taker_side: Side::Buy,
705            taker_wallet_address: *wallet,
706            maker_wallet_address: maker_wallet,
707            fee: dec!(0),
708            is_taker: true,
709            timestamp,
710            builder_code_address: None,
711            builder_code_fee: None,
712            source: Default::default(),
713            taker_realized_pnl: None,
714            maker_realized_pnl: None,
715            underlying_notional: None,
716        })
717    }
718
719    /// Try to match a sell order with the best bid.
720    fn try_match_sell(
721        &mut self,
722        order_id: u64,
723        price: Decimal,
724        quantity: Decimal,
725        wallet: &WalletAddress,
726        timestamp: u64,
727        trade_id: u64,
728    ) -> MatchResult {
729        let best_bid_price = match self.get_best_bid() {
730            Some(p) => p,
731            None => return MatchResult::NoMatch,
732        };
733        if best_bid_price < price {
734            return MatchResult::NoMatch;
735        }
736
737        let price_key = -best_bid_price;
738        let level = match self.bids.get_mut(&price_key) {
739            Some(l) => l,
740            None => return MatchResult::NoMatch,
741        };
742        let maker_order = match level.orders.front().cloned() {
743            Some(o) => o,
744            None => return MatchResult::NoMatch,
745        };
746
747        let maker_id = maker_order.id;
748        let maker_wallet = match self.orders.get(&maker_id) {
749            Some(meta) => meta.wallet,
750            None => {
751                // Data inconsistency: order in price level but not in orders map
752                warn!(
753                    "Data inconsistency: maker_order_id={} in bid level but not in orders map, symbol={}",
754                    maker_id, self.symbol
755                );
756                return MatchResult::NoMatch;
757            }
758        };
759
760        // SELF-TRADE CHECK: Compare taker wallet with maker wallet
761        if wallet == &maker_wallet {
762            warn!(
763                "STP_AUDIT: self-trade on SELL side: symbol={}, taker_id={}, taker_price={}, taker_qty={}, maker_id={}, maker_price={}, maker_qty={}, wallet={}",
764                self.symbol, order_id, price, quantity,
765                maker_id, best_bid_price, maker_order.quantity, wallet
766            );
767            return MatchResult::SelfTrade {
768                maker_order_id: maker_id,
769            };
770        }
771
772        let match_quantity = maker_order.quantity.min(quantity);
773
774        info!(
775            "MATCH FOUND: Sell order matched with bid - price={}, quantity={}, maker_id={}, trade_id={}",
776            best_bid_price, match_quantity, maker_id, trade_id
777        );
778
779        // Update or remove maker order
780        if match_quantity >= maker_order.quantity {
781            self.cancel_order(maker_id);
782        } else if let Some(level) = self.bids.get_mut(&price_key) {
783            if let Some(order) = level.orders.front_mut() {
784                order.quantity -= match_quantity;
785                level.total_quantity -= match_quantity;
786            }
787        }
788
789        MatchResult::Fill(Fill {
790            trade_id,
791            taker_order_id: order_id,
792            maker_order_id: maker_id,
793            symbol: self.symbol.clone(),
794            price: best_bid_price,
795            size: match_quantity,
796            taker_side: Side::Sell,
797            taker_wallet_address: *wallet,
798            maker_wallet_address: maker_wallet,
799            fee: dec!(0),
800            is_taker: true,
801            timestamp,
802            builder_code_address: None,
803            builder_code_fee: None,
804            source: Default::default(),
805            taker_realized_pnl: None,
806            maker_realized_pnl: None,
807            underlying_notional: None,
808        })
809    }
810
811    /// Check if there is more liquidity available at the given price.
812    fn has_more_liquidity(&self, side: &Side, price: Decimal) -> bool {
813        match side {
814            Side::Buy => self.get_best_ask().is_some_and(|ask| ask <= price),
815            Side::Sell => self.get_best_bid().is_some_and(|bid| bid >= price),
816        }
817    }
818
819    /// Add order to book and emit L2 and orderbook update events.
820    pub fn add_order_to_book_with_events(
821        &mut self,
822        order_id: u64,
823        price: Decimal,
824        quantity: Decimal,
825        side: Side,
826        wallet: WalletAddress,
827        timestamp: u64,
828    ) {
829        self.add_order_to_book_with_events_full(
830            order_id, price, quantity, side, wallet, timestamp, None, false, quantity,
831        );
832    }
833
834    /// Add order to book with full metadata and emit L2/orderbook update events.
835    ///
836    /// Production code paths should prefer this over `add_order_to_book_with_events()`
837    /// so that metadata survives WAL snapshot round-trips.
838    pub fn add_order_to_book_with_events_full(
839        &mut self,
840        order_id: u64,
841        price: Decimal,
842        quantity: Decimal,
843        side: Side,
844        wallet: WalletAddress,
845        timestamp: u64,
846        client_id: Option<String>,
847        mmp_enabled: bool,
848        original_size: Decimal,
849    ) {
850        info!(
851            "No match found - adding {} to orderbook at price {}",
852            quantity, price
853        );
854        self.add_order_with_metadata(
855            order_id,
856            price,
857            quantity,
858            side,
859            wallet,
860            timestamp,
861            client_id,
862            mmp_enabled,
863            original_size,
864        );
865
866        let (bids, asks) = self.get_orderbook_snapshot();
867        let l2_update = self.compute_l2_updates(&bids, &asks);
868        let sequence = self.pending_l2_sequence.take();
869
870        // Emit L2 update if there are changes
871        if !l2_update.bid_updates.is_empty() || !l2_update.ask_updates.is_empty() {
872            let l2_msg = L2Message {
873                symbol: self.symbol.clone(),
874                bid_updates: l2_update.bid_updates,
875                ask_updates: l2_update.ask_updates,
876                timestamp,
877                sequence,
878            };
879            self.pending_events.push(OrderBookEvent::L2Update(l2_msg));
880        }
881
882        // Store current snapshot
883        self.last_bid_snapshot = bids.clone();
884        self.last_ask_snapshot = asks.clone();
885
886        // Emit legacy orderbook update
887        let update = OrderbookUpdate {
888            symbol: self.symbol.clone(),
889            bids,
890            asks,
891            timestamp,
892        };
893        self.pending_events
894            .push(OrderBookEvent::OrderbookUpdated(update));
895    }
896
897    fn emit_events(&mut self, fills: &[Fill], timestamp: u64) {
898        // Emit fill events
899        for fill in fills {
900            self.pending_events
901                .push(OrderBookEvent::OrderFilled(fill.clone()));
902
903            // Also emit TradeMessage
904            let trade_msg = TradeMessage {
905                symbol: self.symbol.clone(),
906                price: fill.price,
907                size: fill.size,
908                side: match fill.taker_side {
909                    Side::Buy => TradeSide::Buy,
910                    Side::Sell => TradeSide::Sell,
911                },
912                timestamp: fill.timestamp,
913            };
914            self.pending_events.push(OrderBookEvent::Trade(trade_msg));
915        }
916
917        // Get current orderbook snapshot
918        let (bids, asks) = self.get_orderbook_snapshot();
919
920        // Emit L2 updates (incremental changes)
921        let l2_update = self.compute_l2_updates(&bids, &asks);
922        let sequence = self.pending_l2_sequence.take();
923        if !l2_update.bid_updates.is_empty() || !l2_update.ask_updates.is_empty() {
924            let l2_msg = L2Message {
925                symbol: self.symbol.clone(),
926                bid_updates: l2_update.bid_updates,
927                ask_updates: l2_update.ask_updates,
928                timestamp,
929                sequence,
930            };
931            self.pending_events.push(OrderBookEvent::L2Update(l2_msg));
932        }
933
934        // Store current snapshot for next update
935        self.last_bid_snapshot = bids.clone();
936        self.last_ask_snapshot = asks.clone();
937
938        // Also emit legacy orderbook update for backward compatibility
939        let update = OrderbookUpdate {
940            symbol: self.symbol.clone(),
941            bids,
942            asks,
943            timestamp,
944        };
945        self.pending_events
946            .push(OrderBookEvent::OrderbookUpdated(update));
947    }
948
949    pub fn get_orderbook_snapshot(&self) -> (Vec<(Decimal, Decimal)>, Vec<(Decimal, Decimal)>) {
950        let bids = self.get_bid_depth();
951        let asks = self.get_ask_depth();
952        (bids, asks)
953    }
954
955    /// Sync the L2 snapshot baseline to the current orderbook state.
956    ///
957    /// Must be called after journal replay so that subsequent L2 diffs
958    /// are computed against the replayed state, not against an empty book.
959    pub fn sync_l2_snapshot_baseline(&mut self) {
960        let (bids, asks) = self.get_orderbook_snapshot();
961        self.last_bid_snapshot = bids;
962        self.last_ask_snapshot = asks;
963    }
964
965    /// Emit L2 and orderbook update events after a modification (cancel, etc.)
966    /// Call this after modifying the orderbook when you need to notify subscribers.
967    pub fn emit_orderbook_events(&mut self, timestamp: u64) {
968        let (bids, asks) = self.get_orderbook_snapshot();
969        let l2_update = self.compute_l2_updates(&bids, &asks);
970        let sequence = self.pending_l2_sequence.take();
971
972        // Emit L2 update if there are changes
973        if !l2_update.bid_updates.is_empty() || !l2_update.ask_updates.is_empty() {
974            let l2_msg = L2Message {
975                symbol: self.symbol.clone(),
976                bid_updates: l2_update.bid_updates,
977                ask_updates: l2_update.ask_updates,
978                timestamp,
979                sequence,
980            };
981            self.pending_events.push(OrderBookEvent::L2Update(l2_msg));
982        }
983
984        // Store current snapshot
985        self.last_bid_snapshot = bids.clone();
986        self.last_ask_snapshot = asks.clone();
987
988        // Emit legacy orderbook update
989        let update = OrderbookUpdate {
990            symbol: self.symbol.clone(),
991            bids,
992            asks,
993            timestamp,
994        };
995        self.pending_events
996            .push(OrderBookEvent::OrderbookUpdated(update));
997    }
998
999    pub fn compute_l2_updates(
1000        &self,
1001        current_bids: &[(Decimal, Decimal)],
1002        current_asks: &[(Decimal, Decimal)],
1003    ) -> L2UpdateSet {
1004        let mut bid_updates = Vec::new();
1005        let mut ask_updates = Vec::new();
1006
1007        // Convert to hashmaps for easy lookup using string keys for precise comparison
1008        let last_bids: HashMap<String, Decimal> = self
1009            .last_bid_snapshot
1010            .iter()
1011            .map(|(p, s)| (p.to_string(), *s))
1012            .collect();
1013        let current_bids_map: HashMap<String, Decimal> = current_bids
1014            .iter()
1015            .map(|(p, s)| (p.to_string(), *s))
1016            .collect();
1017
1018        let last_asks: HashMap<String, Decimal> = self
1019            .last_ask_snapshot
1020            .iter()
1021            .map(|(p, s)| (p.to_string(), *s))
1022            .collect();
1023        let current_asks_map: HashMap<String, Decimal> = current_asks
1024            .iter()
1025            .map(|(p, s)| (p.to_string(), *s))
1026            .collect();
1027
1028        let threshold = dec!(0.0001);
1029
1030        // Check for bid changes
1031        for (price_str, &current_size) in &current_bids_map {
1032            if let Some(&last_size) = last_bids.get(price_str) {
1033                let diff = current_size - last_size;
1034                if diff.abs() > threshold {
1035                    bid_updates.push(L2Update {
1036                        price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1037                        size: current_size,
1038                    });
1039                }
1040            } else {
1041                bid_updates.push(L2Update {
1042                    price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1043                    size: current_size,
1044                });
1045            }
1046        }
1047
1048        // Check for removed bid levels
1049        for price_str in last_bids.keys() {
1050            if !current_bids_map.contains_key(price_str) {
1051                bid_updates.push(L2Update {
1052                    price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1053                    size: dec!(0),
1054                });
1055            }
1056        }
1057
1058        // Check for ask changes
1059        for (price_str, &current_size) in &current_asks_map {
1060            if let Some(&last_size) = last_asks.get(price_str) {
1061                let diff = current_size - last_size;
1062                if diff.abs() > threshold {
1063                    ask_updates.push(L2Update {
1064                        price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1065                        size: current_size,
1066                    });
1067                }
1068            } else {
1069                ask_updates.push(L2Update {
1070                    price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1071                    size: current_size,
1072                });
1073            }
1074        }
1075
1076        // Check for removed ask levels
1077        for price_str in last_asks.keys() {
1078            if !current_asks_map.contains_key(price_str) {
1079                ask_updates.push(L2Update {
1080                    price: Decimal::from_str_exact(price_str).unwrap_or_default(),
1081                    size: dec!(0),
1082                });
1083            }
1084        }
1085
1086        L2UpdateSet {
1087            bid_updates,
1088            ask_updates,
1089        }
1090    }
1091
1092    pub fn has_open_orders(&self) -> bool {
1093        !self.orders.is_empty()
1094    }
1095
1096    /// Get all orders as a snapshot for persistence.
1097    pub fn get_all_orders(&self) -> Vec<OrderRecord> {
1098        let mut all_orders = Vec::new();
1099
1100        // Collect all bid orders
1101        for level in self.bids.values() {
1102            for order in &level.orders {
1103                let meta = self.orders.get(&order.id).unwrap_or_else(|| {
1104                    panic!(
1105                        "CRITICAL_FAILURE: order {} exists in bid price levels but not in metadata map for {}",
1106                        order.id, self.symbol
1107                    )
1108                });
1109                all_orders.push(OrderRecord {
1110                    order_id: order.id,
1111                    price: order.price,
1112                    quantity: order.quantity,
1113                    side: order.side,
1114                    wallet: meta.wallet,
1115                    timestamp: order.timestamp,
1116                    client_id: meta.client_id.clone(),
1117                    mmp_enabled: meta.mmp_enabled,
1118                    original_size: meta.original_size,
1119                });
1120            }
1121        }
1122
1123        // Collect all ask orders
1124        for level in self.asks.values() {
1125            for order in &level.orders {
1126                let meta = self.orders.get(&order.id).unwrap_or_else(|| {
1127                    panic!(
1128                        "CRITICAL_FAILURE: order {} exists in ask price levels but not in metadata map for {}",
1129                        order.id, self.symbol
1130                    )
1131                });
1132                all_orders.push(OrderRecord {
1133                    order_id: order.id,
1134                    price: order.price,
1135                    quantity: order.quantity,
1136                    side: order.side,
1137                    wallet: meta.wallet,
1138                    timestamp: order.timestamp,
1139                    client_id: meta.client_id.clone(),
1140                    mmp_enabled: meta.mmp_enabled,
1141                    original_size: meta.original_size,
1142                });
1143            }
1144        }
1145
1146        all_orders
1147    }
1148
1149    /// Restore orderbook from a list of orders.
1150    pub fn restore_from_orders(&mut self, mut orders: Vec<OrderRecord>) {
1151        // Clear existing orders
1152        self.bids.clear();
1153        self.asks.clear();
1154        self.orders.clear();
1155
1156        // Sort by timestamp to preserve original time priority within each
1157        // price level. This is critical for journal replay fill inference:
1158        // when a taker's response says Filled, we walk the opposing book in
1159        // price-time priority to infer which makers were filled. Without
1160        // correct time ordering, the wrong makers get filled.
1161        orders.sort_by_key(|r| r.timestamp);
1162
1163        // Restore each order
1164        for r in orders {
1165            let order = Order {
1166                id: r.order_id,
1167                price: r.price,
1168                quantity: r.quantity,
1169                side: r.side,
1170                timestamp: r.timestamp,
1171            };
1172
1173            let price_key = match r.side {
1174                Side::Buy => -r.price,
1175                Side::Sell => r.price,
1176            };
1177
1178            let book = match r.side {
1179                Side::Buy => &mut self.bids,
1180                Side::Sell => &mut self.asks,
1181            };
1182
1183            self.orders.insert(
1184                r.order_id,
1185                OrderMeta {
1186                    side: r.side,
1187                    price: r.price,
1188                    wallet: r.wallet,
1189                    client_id: r.client_id,
1190                    mmp_enabled: r.mmp_enabled,
1191                    original_size: r.original_size,
1192                },
1193            );
1194
1195            book.entry(price_key)
1196                .or_insert_with(|| PriceLevel::new(r.price))
1197                .add_order(order);
1198        }
1199
1200        // CRITICAL: Initialize L2 snapshots so that compute_l2_updates() can
1201        // correctly detect removed levels after fills. Without this, the first
1202        // fill after restore will not emit delete updates for fully-filled levels,
1203        // causing L2-derived caches to retain stale price levels.
1204        let (bids, asks) = self.get_orderbook_snapshot();
1205        self.last_bid_snapshot = bids;
1206        self.last_ask_snapshot = asks;
1207    }
1208}
1209
1210/// Set of L2 incremental updates computed by [`OrderBook::compute_l2_updates`].
1211pub struct L2UpdateSet {
1212    pub bid_updates: Vec<L2Update>,
1213    pub ask_updates: Vec<L2Update>,
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218    use super::*;
1219
1220    // Test-only MatchingEngine for managing multiple orderbooks
1221    struct MatchingEngine {
1222        books: HashMap<u64, HashMap<Decimal, HashMap<OptionType, OrderBook>>>,
1223        next_order_id: u64,
1224    }
1225
1226    impl MatchingEngine {
1227        fn new() -> Self {
1228            Self {
1229                books: HashMap::new(),
1230                next_order_id: 1,
1231            }
1232        }
1233
1234        fn get_or_create_book(
1235            &mut self,
1236            expiry: u64,
1237            strike: Decimal,
1238            option_type: OptionType,
1239        ) -> &mut OrderBook {
1240            self.books
1241                .entry(expiry)
1242                .or_default()
1243                .entry(strike)
1244                .or_default()
1245                .entry(option_type)
1246                .or_insert_with(|| OrderBook::new(expiry, strike, option_type))
1247        }
1248
1249        fn get_book(
1250            &self,
1251            expiry: u64,
1252            strike: Decimal,
1253            option_type: &OptionType,
1254        ) -> Option<&OrderBook> {
1255            self.books.get(&expiry)?.get(&strike)?.get(option_type)
1256        }
1257
1258        fn get_book_mut(
1259            &mut self,
1260            expiry: u64,
1261            strike: Decimal,
1262            option_type: &OptionType,
1263        ) -> Option<&mut OrderBook> {
1264            self.books
1265                .get_mut(&expiry)?
1266                .get_mut(&strike)?
1267                .get_mut(option_type)
1268        }
1269
1270        fn add_order(
1271            &mut self,
1272            expiry: u64,
1273            strike: Decimal,
1274            option_type: OptionType,
1275            price: Decimal,
1276            quantity: Decimal,
1277            side: Side,
1278            timestamp: u64,
1279        ) -> u64 {
1280            let order_id = self.next_order_id;
1281            self.next_order_id += 1;
1282            let book = self.get_or_create_book(expiry, strike, option_type);
1283            book.add_order(order_id, price, quantity, side, timestamp);
1284            order_id
1285        }
1286
1287        fn cancel_order(
1288            &mut self,
1289            expiry: u64,
1290            strike: Decimal,
1291            option_type: &OptionType,
1292            order_id: u64,
1293        ) -> Option<Order> {
1294            self.get_book_mut(expiry, strike, option_type)?
1295                .cancel_order(order_id)
1296        }
1297
1298        #[allow(dead_code)]
1299        fn get_all_books(&self) -> Vec<&OrderBook> {
1300            self.books
1301                .values()
1302                .flat_map(|by_strike| by_strike.values())
1303                .flat_map(|by_type| by_type.values())
1304                .collect()
1305        }
1306
1307        fn count_books(&self) -> usize {
1308            self.books
1309                .values()
1310                .map(|by_strike| {
1311                    by_strike
1312                        .values()
1313                        .map(|by_type| by_type.len())
1314                        .sum::<usize>()
1315                })
1316                .sum()
1317        }
1318    }
1319
1320    #[test]
1321    fn test_orderbook_basic_operations() {
1322        let mut book = OrderBook::new(1735689600, dec!(100), OptionType::Call);
1323
1324        let order1 = 1u64;
1325        book.add_order(order1, dec!(99.5), dec!(100), Side::Buy, 1000);
1326        let order2 = 2u64;
1327        book.add_order(order2, dec!(100.5), dec!(150), Side::Sell, 1001);
1328        let order3 = 3u64;
1329        book.add_order(order3, dec!(99), dec!(50), Side::Buy, 1002);
1330
1331        assert_eq!(book.get_best_bid(), Some(dec!(99.5)));
1332        assert_eq!(book.get_best_ask(), Some(dec!(100.5)));
1333        assert_eq!(book.get_spread(), Some(dec!(1)));
1334
1335        book.cancel_order(order1);
1336        assert_eq!(book.get_best_bid(), Some(dec!(99)));
1337
1338        assert_eq!(book.total_bid_volume(), dec!(50));
1339        assert_eq!(book.total_ask_volume(), dec!(150));
1340    }
1341
1342    #[test]
1343    fn test_append_order_wallets_uses_live_order_metadata() {
1344        let mut book = OrderBook::new(1735689600, dec!(100), OptionType::Call);
1345        let wallet_a = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1346        let wallet_b = WalletAddress::from(alloy::primitives::Address::repeat_byte(2));
1347
1348        book.add_order_with_wallet(1, dec!(99), dec!(10), Side::Buy, wallet_a, 1000);
1349        book.add_order_with_wallet(2, dec!(101), dec!(20), Side::Sell, wallet_b, 1001);
1350
1351        let mut order_wallets = Vec::new();
1352        book.append_order_wallets(&mut order_wallets);
1353        order_wallets.sort_by_key(|(order_id, _)| *order_id);
1354
1355        assert_eq!(order_wallets, vec![(1, wallet_a), (2, wallet_b)]);
1356    }
1357
1358    #[test]
1359    fn test_orderbook_engine_multiple_books() {
1360        let mut engine = MatchingEngine::new();
1361
1362        let order1 = engine.add_order(
1363            1735689600,
1364            dec!(100),
1365            OptionType::Call,
1366            dec!(99.5),
1367            dec!(100),
1368            Side::Buy,
1369            1000,
1370        );
1371        let _order2 = engine.add_order(
1372            1735689600,
1373            dec!(100),
1374            OptionType::Put,
1375            dec!(5.5),
1376            dec!(200),
1377            Side::Sell,
1378            1001,
1379        );
1380        let _order3 = engine.add_order(
1381            1735689600,
1382            dec!(110),
1383            OptionType::Call,
1384            dec!(89.5),
1385            dec!(150),
1386            Side::Buy,
1387            1002,
1388        );
1389        let _order4 = engine.add_order(
1390            1738368000,
1391            dec!(100),
1392            OptionType::Call,
1393            dec!(105),
1394            dec!(75),
1395            Side::Sell,
1396            1003,
1397        );
1398
1399        assert_eq!(engine.count_books(), 4);
1400
1401        let call_100_book = engine
1402            .get_book(1735689600, dec!(100), &OptionType::Call)
1403            .unwrap();
1404        assert_eq!(call_100_book.get_best_bid(), Some(dec!(99.5)));
1405
1406        let put_100_book = engine
1407            .get_book(1735689600, dec!(100), &OptionType::Put)
1408            .unwrap();
1409        assert_eq!(put_100_book.get_best_ask(), Some(dec!(5.5)));
1410
1411        engine.cancel_order(1735689600, dec!(100), &OptionType::Call, order1);
1412        let call_100_book = engine
1413            .get_book(1735689600, dec!(100), &OptionType::Call)
1414            .unwrap();
1415        assert_eq!(call_100_book.get_best_bid(), None);
1416    }
1417
1418    #[test]
1419    fn test_multiple_orders_same_price() {
1420        let mut book = OrderBook::new(1735689600, dec!(100), OptionType::Call);
1421
1422        let order1 = 1u64;
1423        book.add_order(order1, dec!(100), dec!(100), Side::Buy, 1000);
1424        let order2 = 2u64;
1425        book.add_order(order2, dec!(100), dec!(200), Side::Buy, 1001);
1426        let order3 = 3u64;
1427        book.add_order(order3, dec!(100), dec!(150), Side::Buy, 1002);
1428
1429        assert_eq!(book.total_bid_volume(), dec!(450));
1430        assert_eq!(book.get_best_bid(), Some(dec!(100)));
1431
1432        book.cancel_order(order2);
1433        assert_eq!(book.total_bid_volume(), dec!(250));
1434        assert_eq!(book.get_best_bid(), Some(dec!(100)));
1435
1436        book.cancel_order(order1);
1437        book.cancel_order(order3);
1438        assert_eq!(book.total_bid_volume(), dec!(0));
1439        assert_eq!(book.get_best_bid(), None);
1440    }
1441
1442    #[test]
1443    fn test_order_depth() {
1444        let mut book = OrderBook::new(1735689600, dec!(100), OptionType::Put);
1445        let mut order_id_counter = 1u64;
1446
1447        book.add_order(order_id_counter, dec!(10), dec!(100), Side::Buy, 1000);
1448        order_id_counter += 1;
1449        book.add_order(order_id_counter, dec!(9.5), dec!(200), Side::Buy, 1001);
1450        order_id_counter += 1;
1451        book.add_order(order_id_counter, dec!(9), dec!(150), Side::Buy, 1002);
1452        order_id_counter += 1;
1453
1454        book.add_order(order_id_counter, dec!(11), dec!(120), Side::Sell, 1003);
1455        order_id_counter += 1;
1456        book.add_order(order_id_counter, dec!(11.5), dec!(180), Side::Sell, 1004);
1457        order_id_counter += 1;
1458        book.add_order(order_id_counter, dec!(12), dec!(90), Side::Sell, 1005);
1459
1460        let bid_depth = book.get_bid_depth();
1461        assert_eq!(bid_depth.len(), 3);
1462        assert_eq!(bid_depth[0], (dec!(10), dec!(100)));
1463        assert_eq!(bid_depth[1], (dec!(9.5), dec!(200)));
1464        assert_eq!(bid_depth[2], (dec!(9), dec!(150)));
1465
1466        let ask_depth = book.get_ask_depth();
1467        assert_eq!(ask_depth.len(), 3);
1468        assert_eq!(ask_depth[0], (dec!(11), dec!(120)));
1469        assert_eq!(ask_depth[1], (dec!(11.5), dec!(180)));
1470        assert_eq!(ask_depth[2], (dec!(12), dec!(90)));
1471
1472        assert_eq!(book.get_spread(), Some(dec!(1)));
1473    }
1474
1475    #[test]
1476    fn test_engine_hierarchical_structure() {
1477        let mut engine = MatchingEngine::new();
1478
1479        let expiries = vec![1735689600u64, 1738368000u64];
1480        let strikes = vec![dec!(90), dec!(100), dec!(110)];
1481        let types = vec![OptionType::Call, OptionType::Put];
1482
1483        for &expiry in &expiries {
1484            for strike in &strikes {
1485                for option_type in &types {
1486                    engine.add_order(
1487                        expiry,
1488                        *strike,
1489                        *option_type,
1490                        dec!(50),
1491                        dec!(100),
1492                        Side::Buy,
1493                        1000,
1494                    );
1495                }
1496            }
1497        }
1498
1499        assert_eq!(engine.count_books(), 12);
1500
1501        for &expiry in &expiries {
1502            for strike in &strikes {
1503                for option_type in &types {
1504                    let book = engine.get_book(expiry, *strike, option_type).unwrap();
1505                    assert_eq!(book.expiry, expiry);
1506                    assert_eq!(book.strike, *strike);
1507                    assert_eq!(book.option_type, option_type.clone());
1508                    assert_eq!(book.get_best_bid(), Some(dec!(50)));
1509                }
1510            }
1511        }
1512    }
1513
1514    // ============================================
1515    // Self-Trade Prevention Tests
1516    // ============================================
1517
1518    #[test]
1519    fn test_self_trade_prevention_same_wallet_buy() {
1520        let mut book = OrderBook::with_symbol(
1521            1735689600,
1522            dec!(100),
1523            OptionType::Call,
1524            "BTC-20260131-100000-C".to_string(),
1525        );
1526
1527        let wallet = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1528
1529        book.add_order_with_wallet(1, dec!(100), dec!(10), Side::Sell, wallet, 1000);
1530
1531        let (result, should_continue) =
1532            book.process_order(2, dec!(100), dec!(5), Side::Buy, wallet, 1001, 100);
1533
1534        assert!(
1535            matches!(result, MatchResult::SelfTrade { maker_order_id: 1 }),
1536            "Expected SelfTrade with maker_order_id=1, got {:?}",
1537            result
1538        );
1539        assert!(!should_continue);
1540
1541        assert_eq!(book.get_best_ask(), Some(dec!(100)));
1542        assert_eq!(book.total_ask_volume(), dec!(10));
1543    }
1544
1545    #[test]
1546    fn test_self_trade_prevention_same_wallet_sell() {
1547        let mut book = OrderBook::with_symbol(
1548            1735689600,
1549            dec!(100),
1550            OptionType::Call,
1551            "BTC-20260131-100000-C".to_string(),
1552        );
1553
1554        let wallet = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1555
1556        book.add_order_with_wallet(1, dec!(100), dec!(10), Side::Buy, wallet, 1000);
1557
1558        let (result, should_continue) =
1559            book.process_order(2, dec!(100), dec!(5), Side::Sell, wallet, 1001, 100);
1560
1561        assert!(
1562            matches!(result, MatchResult::SelfTrade { maker_order_id: 1 }),
1563            "Expected SelfTrade with maker_order_id=1, got {:?}",
1564            result
1565        );
1566        assert!(!should_continue);
1567
1568        assert_eq!(book.get_best_bid(), Some(dec!(100)));
1569        assert_eq!(book.total_bid_volume(), dec!(10));
1570    }
1571
1572    #[test]
1573    fn test_no_self_trade_different_wallets() {
1574        let mut book = OrderBook::with_symbol(
1575            1735689600,
1576            dec!(100),
1577            OptionType::Call,
1578            "BTC-20260131-100000-C".to_string(),
1579        );
1580
1581        let wallet_a = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1582        let wallet_b = WalletAddress::from(alloy::primitives::Address::repeat_byte(2));
1583
1584        book.add_order_with_wallet(1, dec!(100), dec!(10), Side::Sell, wallet_a, 1000);
1585
1586        let (result, _should_continue) =
1587            book.process_order(2, dec!(100), dec!(5), Side::Buy, wallet_b, 1001, 100);
1588
1589        assert!(
1590            matches!(result, MatchResult::Fill(_)),
1591            "Expected Fill, got {:?}",
1592            result
1593        );
1594
1595        if let MatchResult::Fill(fill) = result {
1596            assert_eq!(fill.size, dec!(5));
1597            assert_eq!(fill.price, dec!(100));
1598            assert_eq!(fill.taker_wallet_address, wallet_b);
1599            assert_eq!(fill.maker_wallet_address, wallet_a);
1600        }
1601
1602        assert_eq!(book.total_ask_volume(), dec!(5));
1603    }
1604
1605    #[test]
1606    fn test_partial_fill_then_self_trade() {
1607        let mut book = OrderBook::with_symbol(
1608            1735689600,
1609            dec!(100),
1610            OptionType::Call,
1611            "BTC-20260131-100000-C".to_string(),
1612        );
1613
1614        let wallet_a = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1615        let wallet_b = WalletAddress::from(alloy::primitives::Address::repeat_byte(2));
1616
1617        book.add_order_with_wallet(1, dec!(100), dec!(5), Side::Sell, wallet_b, 1000);
1618        book.add_order_with_wallet(2, dec!(100), dec!(5), Side::Sell, wallet_a, 1001);
1619
1620        let (result1, should_continue1) =
1621            book.process_order(3, dec!(100), dec!(10), Side::Buy, wallet_a, 1002, 100);
1622
1623        assert!(
1624            matches!(result1, MatchResult::Fill(_)),
1625            "First call should produce a Fill, got {:?}",
1626            result1
1627        );
1628        assert!(
1629            should_continue1,
1630            "Should indicate more matching is possible"
1631        );
1632
1633        let (result2, should_continue2) =
1634            book.process_order(3, dec!(100), dec!(5), Side::Buy, wallet_a, 1003, 101);
1635
1636        assert!(
1637            matches!(result2, MatchResult::SelfTrade { maker_order_id: 2 }),
1638            "Second call should detect SelfTrade with maker_order_id=2, got {:?}",
1639            result2
1640        );
1641        assert!(!should_continue2);
1642
1643        assert_eq!(book.get_best_ask(), Some(dec!(100)));
1644        assert_eq!(book.total_ask_volume(), dec!(5));
1645    }
1646
1647    #[test]
1648    fn test_self_trade_no_match_when_price_doesnt_cross() {
1649        let mut book = OrderBook::with_symbol(
1650            1735689600,
1651            dec!(100),
1652            OptionType::Call,
1653            "BTC-20260131-100000-C".to_string(),
1654        );
1655
1656        let wallet = WalletAddress::from(alloy::primitives::Address::repeat_byte(1));
1657
1658        book.add_order_with_wallet(1, dec!(100), dec!(10), Side::Sell, wallet, 1000);
1659
1660        let (result, should_continue) =
1661            book.process_order(2, dec!(99), dec!(5), Side::Buy, wallet, 1001, 100);
1662
1663        assert!(
1664            matches!(result, MatchResult::NoMatch),
1665            "Expected NoMatch when price does not cross, got {:?}",
1666            result
1667        );
1668        assert!(!should_continue);
1669
1670        assert_eq!(book.get_best_bid(), Some(dec!(99)));
1671        assert_eq!(book.get_best_ask(), Some(dec!(100)));
1672    }
1673}