1use 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#[derive(Debug, Clone)]
38pub enum MatchResult {
39 Fill(Fill),
41 SelfTrade { maker_order_id: u64 },
43 NoMatch,
45}
46
47#[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#[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#[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#[derive(Debug, Clone)]
128pub enum OrderBookEvent {
129 OrderFilled(Fill),
131 Trade(TradeMessage),
133 L2Update(L2Message),
135 OrderbookUpdated(OrderbookUpdate),
137}
138
139#[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>, asks: BTreeMap<Decimal, PriceLevel>,
151 orders: HashMap<u64, OrderMeta>,
152 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 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 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 pub fn order_count(&self) -> usize {
316 self.orders.len()
317 }
318
319 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 pub fn has_order(&self, order_id: u64) -> bool {
333 self.orders.contains_key(&order_id)
334 }
335
336 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 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 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 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 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 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 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 match self.try_match_one_maker(
572 order_id, price, quantity, &side, &wallet, timestamp, trade_id,
573 ) {
574 MatchResult::Fill(fill) => {
575 self.emit_events(std::slice::from_ref(&fill), timestamp);
577
578 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 (MatchResult::SelfTrade { maker_order_id }, false)
588 }
589 MatchResult::NoMatch => {
590 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 (MatchResult::NoMatch, false)
604 }
605 }
606 }
607
608 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 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 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 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 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 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 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 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 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 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 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 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 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 self.last_bid_snapshot = bids.clone();
884 self.last_ask_snapshot = asks.clone();
885
886 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 for fill in fills {
900 self.pending_events
901 .push(OrderBookEvent::OrderFilled(fill.clone()));
902
903 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 let (bids, asks) = self.get_orderbook_snapshot();
919
920 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 self.last_bid_snapshot = bids.clone();
936 self.last_ask_snapshot = asks.clone();
937
938 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 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 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 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 self.last_bid_snapshot = bids.clone();
986 self.last_ask_snapshot = asks.clone();
987
988 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 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 for (price_str, ¤t_size) in ¤t_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 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 for (price_str, ¤t_size) in ¤t_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 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 pub fn get_all_orders(&self) -> Vec<OrderRecord> {
1098 let mut all_orders = Vec::new();
1099
1100 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 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 pub fn restore_from_orders(&mut self, mut orders: Vec<OrderRecord>) {
1151 self.bids.clear();
1153 self.asks.clear();
1154 self.orders.clear();
1155
1156 orders.sort_by_key(|r| r.timestamp);
1162
1163 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 let (bids, asks) = self.get_orderbook_snapshot();
1205 self.last_bid_snapshot = bids;
1206 self.last_ask_snapshot = asks;
1207 }
1208}
1209
1210pub 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 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 #[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}