1use std::sync::atomic::{AtomicBool, Ordering};
24
25use ahash::AHashMap;
26use dashmap::DashMap;
27use nautilus_core::{UUID4, UnixNanos};
28use nautilus_live::ExecutionEventEmitter;
29use nautilus_model::{
30 enums::{OrderSide, OrderType},
31 events::{OrderAccepted, OrderEventAny, OrderFilled, OrderUpdated},
32 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
33 instruments::{Instrument, InstrumentAny},
34 reports::FillReport,
35 types::Currency,
36};
37use parking_lot::Mutex;
38use ustr::Ustr;
39
40use crate::{
41 common::enums::{BitmexExecType, BitmexOrderType, BitmexPegPriceType},
42 http::parse::{InstrumentParseResult, parse_instrument_any},
43 websocket::{
44 enums::BitmexAction,
45 messages::{
46 BitmexExecutionMsg, BitmexTableMessage, BitmexWsMessage, OrderData, OrderRowCache,
47 ResolvedOrderData,
48 },
49 parse::{
50 ParsedOrderEvent, parse_execution_msg, parse_margin_account_state, parse_order_event,
51 parse_order_msg, parse_order_update_msg, parse_position_msg, parse_wallet_msg,
52 },
53 },
54};
55
56const DEDUP_GENERATION_CAPACITY: usize = 10_000;
58
59#[derive(Debug, Clone)]
66pub struct OrderIdentity {
67 pub instrument_id: InstrumentId,
68 pub strategy_id: StrategyId,
69 pub order_side: OrderSide,
70 pub order_type: OrderType,
71}
72
73#[derive(Debug)]
82struct GenerationalDedupSet {
83 inner: Mutex<DedupInner>,
84}
85
86#[derive(Debug)]
87struct DedupInner {
88 current: ahash::AHashSet<ClientOrderId>,
89 previous: ahash::AHashSet<ClientOrderId>,
90}
91
92impl Default for GenerationalDedupSet {
93 fn default() -> Self {
94 Self {
95 inner: Mutex::new(DedupInner {
96 current: ahash::AHashSet::new(),
97 previous: ahash::AHashSet::new(),
98 }),
99 }
100 }
101}
102
103impl GenerationalDedupSet {
104 fn contains(&self, key: &ClientOrderId) -> bool {
105 let guard = self.inner.lock();
106 guard.current.contains(key) || guard.previous.contains(key)
107 }
108
109 fn insert(&self, key: ClientOrderId) {
110 let mut guard = self.inner.lock();
111 let inner = &mut *guard;
112 inner.current.insert(key);
113 if inner.current.len() >= DEDUP_GENERATION_CAPACITY {
114 inner.previous.clear();
115 std::mem::swap(&mut inner.current, &mut inner.previous);
116 }
117 }
118
119 fn remove(&self, key: &ClientOrderId) {
120 let mut guard = self.inner.lock();
121 guard.current.remove(key);
122 guard.previous.remove(key);
123 }
124}
125
126#[derive(Debug)]
131pub struct WsDispatchState {
132 pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
133 order_rows: Mutex<OrderRowCache>,
134 emitted_accepted: GenerationalDedupSet,
135 triggered_orders: GenerationalDedupSet,
136 filled_orders: GenerationalDedupSet,
137 tombstoned: GenerationalDedupSet,
138 pub margin_subscribed: AtomicBool,
139}
140
141impl Default for WsDispatchState {
142 fn default() -> Self {
143 Self {
144 order_identities: DashMap::new(),
145 order_rows: Mutex::new(OrderRowCache::default()),
146 emitted_accepted: GenerationalDedupSet::default(),
147 triggered_orders: GenerationalDedupSet::default(),
148 filled_orders: GenerationalDedupSet::default(),
149 tombstoned: GenerationalDedupSet::default(),
150 margin_subscribed: AtomicBool::new(false),
151 }
152 }
153}
154
155impl WsDispatchState {
156 pub(crate) fn accepted_contains(&self, cid: &ClientOrderId) -> bool {
157 self.emitted_accepted.contains(cid)
158 }
159
160 pub(crate) fn filled_contains(&self, cid: &ClientOrderId) -> bool {
161 self.filled_orders.contains(cid)
162 }
163
164 pub(crate) fn triggered_contains(&self, cid: &ClientOrderId) -> bool {
165 self.triggered_orders.contains(cid)
166 }
167
168 pub(crate) fn insert_accepted(&self, cid: ClientOrderId) {
169 self.emitted_accepted.insert(cid);
170 }
171
172 pub(crate) fn insert_filled(&self, cid: ClientOrderId) {
173 self.filled_orders.insert(cid);
174 }
175
176 pub(crate) fn insert_triggered(&self, cid: ClientOrderId) {
177 self.triggered_orders.insert(cid);
178 }
179
180 pub(crate) fn remove_triggered(&self, cid: &ClientOrderId) {
181 self.triggered_orders.remove(cid);
182 }
183
184 pub(crate) fn remove_filled(&self, cid: &ClientOrderId) {
185 self.filled_orders.remove(cid);
186 }
187
188 pub(crate) fn remove_accepted(&self, cid: &ClientOrderId) {
189 self.emitted_accepted.remove(cid);
190 }
191
192 pub(crate) fn is_tombstoned(&self, cid: &ClientOrderId) -> bool {
194 self.tombstoned.contains(cid)
195 }
196
197 pub(crate) fn tombstone_order(&self, cid: &ClientOrderId) {
203 self.tombstoned.insert(*cid);
204 self.order_identities.remove(cid);
205 self.remove_accepted(cid);
206 self.remove_triggered(cid);
207 self.remove_filled(cid);
208 }
209}
210
211#[expect(clippy::too_many_arguments)]
213pub fn dispatch_ws_message(
214 ts_init: UnixNanos,
215 message: BitmexWsMessage,
216 emitter: &ExecutionEventEmitter,
217 state: &WsDispatchState,
218 instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
219 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
220 order_symbol_cache: &mut AHashMap<ClientOrderId, Ustr>,
221 account_id: AccountId,
222) {
223 match message {
224 BitmexWsMessage::Table(table_msg) => match table_msg {
225 BitmexTableMessage::Order { action, data } => {
226 dispatch_order_messages(
227 state.order_rows_apply(action, data),
228 emitter,
229 state,
230 instruments_by_symbol,
231 order_type_cache,
232 order_symbol_cache,
233 account_id,
234 ts_init,
235 );
236 }
237 BitmexTableMessage::Execution { data, .. } => {
238 dispatch_execution_messages(
239 data,
240 emitter,
241 state,
242 instruments_by_symbol,
243 order_symbol_cache,
244 account_id,
245 ts_init,
246 );
247 }
248 BitmexTableMessage::Position { data, .. } => {
249 for pos_msg in data {
250 let Some(instrument) = instruments_by_symbol.get(&pos_msg.symbol) else {
251 log::error!(
252 "Instrument cache miss: position dropped for symbol={}, account={}",
253 pos_msg.symbol,
254 pos_msg.account,
255 );
256 continue;
257 };
258 let mut report = parse_position_msg(&pos_msg, instrument, ts_init);
259 report.account_id = account_id;
260 emitter.send_position_report(report);
261 }
262 }
263 BitmexTableMessage::Wallet { data, .. } => {
264 if !state.margin_subscribed.load(Ordering::Relaxed) {
265 for wallet_msg in data {
266 let mut acct_state = parse_wallet_msg(&wallet_msg, ts_init);
267 acct_state.account_id = account_id;
268 emitter.send_account_state(acct_state);
269 }
270 }
271 }
272 BitmexTableMessage::Margin { data, .. } => {
273 state.margin_subscribed.store(true, Ordering::Relaxed);
274
275 for margin_msg in data {
276 let mut acct_state = parse_margin_account_state(&margin_msg, ts_init);
277 acct_state.account_id = account_id;
278 emitter.send_account_state(acct_state);
279 }
280 }
281 BitmexTableMessage::Instrument { action, data } => {
282 if matches!(action, BitmexAction::Partial | BitmexAction::Insert) {
283 for msg in data {
284 match msg.try_into() {
285 Ok(http_inst) => match parse_instrument_any(&http_inst, ts_init) {
286 InstrumentParseResult::Ok(boxed) => {
287 let inst = *boxed;
288 let symbol = inst.symbol().inner();
289 instruments_by_symbol.insert(symbol, inst);
290 }
291 InstrumentParseResult::Unsupported { .. }
292 | InstrumentParseResult::Inactive { .. } => {}
293 InstrumentParseResult::Failed { symbol, error, .. } => {
294 log::warn!("Failed to parse instrument {symbol}: {error}");
295 }
296 },
297 Err(e) => {
298 log::debug!("Skipping instrument (missing required fields): {e}");
299 }
300 }
301 }
302 }
303 }
304 BitmexTableMessage::OrderBookL2 { .. }
305 | BitmexTableMessage::OrderBookL2_25 { .. }
306 | BitmexTableMessage::OrderBook10 { .. }
307 | BitmexTableMessage::Quote { .. }
308 | BitmexTableMessage::Trade { .. }
309 | BitmexTableMessage::TradeBin1m { .. }
310 | BitmexTableMessage::TradeBin5m { .. }
311 | BitmexTableMessage::TradeBin1h { .. }
312 | BitmexTableMessage::TradeBin1d { .. }
313 | BitmexTableMessage::Funding { .. } => {
314 log::debug!("Ignoring BitMEX data message on execution stream");
315 }
316 _ => {
317 log::warn!("Unhandled table message type on execution stream");
318 }
319 },
320 BitmexWsMessage::Reconnected => {
321 state.order_rows_clear();
322 order_type_cache.clear();
323 order_symbol_cache.clear();
324 log::info!("BitMEX execution websocket reconnected");
325 }
326 BitmexWsMessage::Authenticated => {
327 log::debug!("BitMEX execution websocket authenticated");
328 }
329 }
330}
331
332impl WsDispatchState {
333 pub(crate) fn order_rows_apply(
334 &self,
335 action: BitmexAction,
336 data: Vec<OrderData>,
337 ) -> Vec<ResolvedOrderData> {
338 self.order_rows.lock().apply(action, data)
339 }
340
341 pub(crate) fn order_rows_clear(&self) {
342 self.order_rows.lock().clear();
343 }
344}
345
346#[expect(clippy::too_many_arguments)]
349fn dispatch_order_messages(
350 data: Vec<ResolvedOrderData>,
351 emitter: &ExecutionEventEmitter,
352 state: &WsDispatchState,
353 instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
354 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
355 order_symbol_cache: &mut AHashMap<ClientOrderId, Ustr>,
356 account_id: AccountId,
357 ts_init: UnixNanos,
358) {
359 for order_data in data {
360 let order_data = match order_data {
361 ResolvedOrderData::Terminal(order_msg) => {
362 let tracked = order_msg.cl_ord_id.as_ref().is_some_and(|cl_ord_id| {
363 state
364 .order_identities
365 .contains_key(&ClientOrderId::new(cl_ord_id))
366 });
367
368 if !tracked {
369 log::debug!(
370 "Skipping terminal update for untracked order: order_id={}",
371 order_msg.order_id,
372 );
373 continue;
374 }
375 ResolvedOrderData::Full(order_msg)
376 }
377 order_data => order_data,
378 };
379
380 match order_data {
381 ResolvedOrderData::Full(order_msg) => {
382 let Some(instrument) = instruments_by_symbol.get(&order_msg.symbol) else {
383 log::error!(
384 "Instrument cache miss: order dropped for symbol={}, order_id={}",
385 order_msg.symbol,
386 order_msg.order_id,
387 );
388 continue;
389 };
390
391 let client_order_id = order_msg.cl_ord_id.map(ClientOrderId::new);
392
393 if let Some(ref cid) = client_order_id {
396 if let Some(ord_type) = &order_msg.ord_type {
397 let order_type: OrderType = if *ord_type == BitmexOrderType::Pegged
398 && order_msg.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
399 {
400 if order_msg.price.is_some() {
401 OrderType::TrailingStopLimit
402 } else {
403 OrderType::TrailingStopMarket
404 }
405 } else {
406 (*ord_type).into()
407 };
408 order_type_cache.insert(*cid, order_type);
409 }
410 order_symbol_cache.insert(*cid, order_msg.symbol);
411 }
412
413 if let Some(ref cid) = client_order_id
415 && state.is_tombstoned(cid)
416 {
417 log::debug!("Skipping tombstoned order {cid}");
418 continue;
419 }
420
421 let identity = client_order_id
422 .and_then(|cid| state.order_identities.get(&cid).map(|r| (cid, r.clone())));
423
424 if let Some((cid, ident)) = identity {
425 if let Some(event) = parse_order_event(
427 &order_msg,
428 cid,
429 account_id,
430 emitter.trader_id(),
431 ident.strategy_id,
432 ts_init,
433 ) {
434 let venue_order_id = VenueOrderId::new(order_msg.order_id.to_string());
435 dispatch_parsed_order_event(
436 event,
437 cid,
438 account_id,
439 venue_order_id,
440 &ident,
441 emitter,
442 state,
443 ts_init,
444 );
445 }
446
447 if order_msg.ord_status.is_terminal() {
449 order_type_cache.remove(&cid);
450 order_symbol_cache.remove(&cid);
451 }
452 } else {
453 match parse_order_msg(&order_msg, instrument, order_type_cache, ts_init) {
455 Ok(mut report) => {
456 if report.order_status.is_closed()
457 && let Some(cid) = report.client_order_id
458 {
459 order_type_cache.remove(&cid);
460 order_symbol_cache.remove(&cid);
461 }
462 report.account_id = account_id;
463 emitter.send_order_status_report(report);
464 }
465 Err(e) => {
466 log::error!(
467 "Failed to parse order report: error={e}, symbol={}, order_id={}",
468 order_msg.symbol,
469 order_msg.order_id,
470 );
471 }
472 }
473 }
474 }
475 ResolvedOrderData::Update(msg) => {
476 let Some(symbol) = msg.symbol else {
477 log::warn!(
478 "Order update missing cached symbol: order_id={}",
479 msg.order_id,
480 );
481 continue;
482 };
483 let Some(instrument) = instruments_by_symbol.get(&symbol) else {
484 log::error!(
485 "Instrument cache miss: order update dropped for symbol={}, order_id={}",
486 symbol,
487 msg.order_id,
488 );
489 continue;
490 };
491
492 if let Some(cl_ord_id) = &msg.cl_ord_id {
494 let client_order_id = ClientOrderId::new(cl_ord_id);
495 order_symbol_cache.insert(client_order_id, symbol);
496 }
497
498 let identity = msg.cl_ord_id.as_ref().and_then(|cl| {
499 let cid = ClientOrderId::new(cl);
500 state.order_identities.get(&cid).map(|r| (cid, r.clone()))
501 });
502
503 if let Some((cid, ident)) = identity {
504 if let Some(event) =
506 parse_order_update_msg(&msg, instrument, account_id, ts_init)
507 {
508 let enriched = OrderUpdated::new(
509 emitter.trader_id(),
510 ident.strategy_id,
511 event.instrument_id,
512 cid,
513 event.quantity,
514 event.event_id,
515 event.ts_event,
516 event.ts_init,
517 false,
518 event.venue_order_id,
519 Some(account_id),
520 event.price,
521 event.trigger_price,
522 event.protection_price,
523 false, );
525 ensure_accepted_emitted(
526 cid,
527 account_id,
528 enriched
529 .venue_order_id
530 .unwrap_or_else(|| VenueOrderId::new(msg.order_id.to_string())),
531 &ident,
532 emitter,
533 state,
534 ts_init,
535 );
536 emitter.send_order_event(OrderEventAny::Updated(enriched));
537 } else {
538 log::warn!(
539 "Skipped order update (insufficient data): order_id={}, price={:?}",
540 msg.order_id,
541 msg.price,
542 );
543 }
544 } else {
545 log::debug!(
546 "Skipping order update for untracked order: order_id={}",
547 msg.order_id,
548 );
549 }
550 }
551 ResolvedOrderData::Terminal(_) => unreachable!("terminal order update was normalized"),
552 }
553 }
554}
555
556fn dispatch_execution_messages(
559 data: Vec<BitmexExecutionMsg>,
560 emitter: &ExecutionEventEmitter,
561 state: &WsDispatchState,
562 instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
563 order_symbol_cache: &AHashMap<ClientOrderId, Ustr>,
564 account_id: AccountId,
565 ts_init: UnixNanos,
566) {
567 for exec_msg in data {
568 let symbol_opt = if let Some(sym) = &exec_msg.symbol {
569 Some(*sym)
570 } else if let Some(cl_ord_id) = &exec_msg.cl_ord_id {
571 let client_order_id = ClientOrderId::new(cl_ord_id);
572 order_symbol_cache.get(&client_order_id).copied()
573 } else {
574 None
575 };
576
577 let Some(symbol) = symbol_opt else {
578 if let Some(cl_ord_id) = &exec_msg.cl_ord_id {
579 if exec_msg.exec_type == Some(BitmexExecType::Trade) {
580 log::warn!(
581 "Execution missing symbol and not in cache: \
582 cl_ord_id={cl_ord_id}, exec_id={:?}",
583 exec_msg.exec_id,
584 );
585 } else {
586 log::debug!(
587 "Execution missing symbol and not in cache: \
588 cl_ord_id={cl_ord_id}, exec_type={:?}",
589 exec_msg.exec_type,
590 );
591 }
592 } else if exec_msg.exec_type == Some(BitmexExecType::CancelReject) {
593 log::debug!(
594 "CancelReject missing symbol/clOrdID (expected with redundant cancels): \
595 exec_id={:?}, order_id={:?}",
596 exec_msg.exec_id,
597 exec_msg.order_id,
598 );
599 } else {
600 log::warn!(
601 "Execution missing both symbol and clOrdID: \
602 exec_id={:?}, order_id={:?}, exec_type={:?}",
603 exec_msg.exec_id,
604 exec_msg.order_id,
605 exec_msg.exec_type,
606 );
607 }
608 continue;
609 };
610
611 let Some(instrument) = instruments_by_symbol.get(&symbol) else {
612 log::error!(
613 "Instrument cache miss: execution dropped for symbol={}, exec_id={:?}, exec_type={:?}",
614 symbol,
615 exec_msg.exec_id,
616 exec_msg.exec_type,
617 );
618 continue;
619 };
620
621 let Some(mut fill) = parse_execution_msg(exec_msg, instrument, ts_init) else {
622 continue;
623 };
624 fill.account_id = account_id;
625
626 let identity = fill
627 .client_order_id
628 .and_then(|cid| state.order_identities.get(&cid).map(|r| (cid, r.clone())));
629
630 if let Some((cid, ident)) = identity {
631 let venue_order_id = fill.venue_order_id;
633 ensure_accepted_emitted(
634 cid,
635 fill.account_id,
636 venue_order_id,
637 &ident,
638 emitter,
639 state,
640 ts_init,
641 );
642 state.insert_filled(cid);
643 state.remove_triggered(&cid);
644 let filled = fill_report_to_order_filled(
645 &fill,
646 emitter.trader_id(),
647 &ident,
648 instrument.quote_currency(),
649 );
650 emitter.send_order_event(OrderEventAny::Filled(filled));
651 } else {
652 emitter.send_fill_report(fill);
654 }
655 }
656}
657
658#[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
663fn dispatch_parsed_order_event(
664 event: ParsedOrderEvent,
665 client_order_id: ClientOrderId,
666 account_id: AccountId,
667 venue_order_id: VenueOrderId,
668 identity: &OrderIdentity,
669 emitter: &ExecutionEventEmitter,
670 state: &WsDispatchState,
671 ts_init: UnixNanos,
672) {
673 let is_terminal;
674
675 match event {
676 ParsedOrderEvent::Accepted(e) => {
677 if state.accepted_contains(&client_order_id)
678 || state.filled_contains(&client_order_id)
679 || state.triggered_contains(&client_order_id)
680 {
681 log::debug!("Skipping duplicate Accepted for {client_order_id}");
682 return;
683 }
684 state.insert_accepted(client_order_id);
685 is_terminal = false;
686 emitter.send_order_event(OrderEventAny::Accepted(e));
687 }
688 ParsedOrderEvent::Triggered(e) => {
689 if state.filled_contains(&client_order_id) {
690 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
691 return;
692 }
693 ensure_accepted_emitted(
694 client_order_id,
695 account_id,
696 venue_order_id,
697 identity,
698 emitter,
699 state,
700 ts_init,
701 );
702 state.insert_triggered(client_order_id);
703 is_terminal = false;
704 emitter.send_order_event(OrderEventAny::Triggered(e));
705 }
706 ParsedOrderEvent::Canceled(e) => {
707 ensure_accepted_emitted(
708 client_order_id,
709 account_id,
710 venue_order_id,
711 identity,
712 emitter,
713 state,
714 ts_init,
715 );
716 state.remove_triggered(&client_order_id);
717 state.remove_filled(&client_order_id);
718 is_terminal = true;
719 emitter.send_order_event(OrderEventAny::Canceled(e));
720 }
721 ParsedOrderEvent::Expired(e) => {
722 ensure_accepted_emitted(
723 client_order_id,
724 account_id,
725 venue_order_id,
726 identity,
727 emitter,
728 state,
729 ts_init,
730 );
731 state.remove_triggered(&client_order_id);
732 state.remove_filled(&client_order_id);
733 is_terminal = true;
734 emitter.send_order_event(OrderEventAny::Expired(e));
735 }
736 ParsedOrderEvent::Rejected(e) => {
737 state.remove_triggered(&client_order_id);
738 state.remove_filled(&client_order_id);
739 is_terminal = true;
740 emitter.send_order_event(OrderEventAny::Rejected(e));
741 }
742 }
743
744 if is_terminal {
745 state.order_identities.remove(&client_order_id);
746 state.remove_accepted(&client_order_id);
747 }
748}
749
750fn ensure_accepted_emitted(
753 client_order_id: ClientOrderId,
754 account_id: AccountId,
755 venue_order_id: VenueOrderId,
756 identity: &OrderIdentity,
757 emitter: &ExecutionEventEmitter,
758 state: &WsDispatchState,
759 ts_init: UnixNanos,
760) {
761 if state.accepted_contains(&client_order_id) {
762 return;
763 }
764 state.insert_accepted(client_order_id);
765 let accepted = OrderAccepted::new(
766 emitter.trader_id(),
767 identity.strategy_id,
768 identity.instrument_id,
769 client_order_id,
770 venue_order_id,
771 account_id,
772 UUID4::new(),
773 ts_init,
774 ts_init,
775 false,
776 );
777 emitter.send_order_event(OrderEventAny::Accepted(accepted));
778}
779
780pub(crate) fn fill_report_to_order_filled(
782 report: &FillReport,
783 trader_id: TraderId,
784 identity: &OrderIdentity,
785 quote_currency: Currency,
786) -> OrderFilled {
787 OrderFilled::new(
788 trader_id,
789 identity.strategy_id,
790 report.instrument_id,
791 report
792 .client_order_id
793 .expect("tracked order has client_order_id"),
794 report.venue_order_id,
795 report.account_id,
796 report.trade_id,
797 identity.order_side,
798 identity.order_type,
799 report.last_qty,
800 report.last_px,
801 quote_currency,
802 report.liquidity_side,
803 UUID4::new(),
804 report.ts_event,
805 report.ts_init,
806 false,
807 report.venue_position_id,
808 Some(report.commission),
809 None,
810 )
811}