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