1use std::{
24 collections::VecDeque,
25 hash::Hash,
26 sync::{Arc, Mutex},
27};
28
29use ahash::AHashMap;
30use dashmap::DashMap;
31use nautilus_core::{AtomicMap, MUTEX_POISONED, UUID4, UnixNanos, time::AtomicTime};
32use nautilus_live::ExecutionEventEmitter;
33use nautilus_model::{
34 enums::{OrderSide, OrderStatus, OrderType},
35 events::{OrderAccepted, OrderEventAny, OrderFilled, OrderRejected},
36 identifiers::{
37 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
38 },
39 instruments::{Instrument, InstrumentAny},
40 orders::TRIGGERABLE_ORDER_TYPES,
41 reports::FillReport,
42 types::{Currency, Money, Quantity},
43};
44use ustr::Ustr;
45
46use crate::{
47 common::{
48 consts::{
49 OKX_FIELD_CLORDID, OKX_FIELD_SCODE, OKX_FIELD_SMSG, OKX_FIELD_SUBCODE,
50 OKX_POST_ONLY_CANCEL_REASON, OKX_POST_ONLY_CANCEL_SOURCE, OKX_SUCCESS_CODE,
51 },
52 enums::OKXOrderStatus,
53 parse::{
54 is_market_price, parse_client_order_id, parse_millisecond_timestamp, parse_price,
55 parse_quantity,
56 },
57 },
58 http::models::{OKXAccount, OKXCancelAlgoOrderResponse, OKXPosition, OKXSpreadOrder},
59 websocket::{
60 client::PendingOrderInfo,
61 enums::OKXWsOperation,
62 handler::is_post_only_auto_cancel,
63 messages::{ExecutionReport, OKXOrderMsg, OKXWsMessage},
64 parse::{
65 OrderStateSnapshot, ParsedOrderEvent, parse_algo_order_msg, parse_order_event,
66 parse_order_msg, parse_spread_order_event, parse_spread_order_msg,
67 update_fee_fill_caches,
68 },
69 },
70};
71
72const DEDUP_CAPACITY: usize = 10_000;
74
75#[derive(Debug)]
81pub struct BoundedDedup<K> {
82 inner: Mutex<BoundedDedupInner<K>>,
83 capacity: usize,
84}
85
86#[derive(Debug)]
87struct BoundedDedupInner<K> {
88 set: AHashMap<K, u64>,
89 queue: VecDeque<(K, u64)>,
90 next_seq: u64,
91}
92
93impl<K> BoundedDedup<K>
94where
95 K: Eq + Hash + Clone,
96{
97 pub fn new(capacity: usize) -> Self {
99 Self {
100 inner: Mutex::new(BoundedDedupInner {
101 set: AHashMap::with_capacity(capacity),
102 queue: VecDeque::with_capacity(capacity),
103 next_seq: 0,
104 }),
105 capacity,
106 }
107 }
108
109 #[allow(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
111 pub fn contains(&self, key: &K) -> bool {
112 self.inner
113 .lock()
114 .expect(MUTEX_POISONED)
115 .set
116 .contains_key(key)
117 }
118
119 #[allow(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
122 pub fn check_and_insert(&self, key: K) -> bool {
123 let mut inner = self.inner.lock().expect(MUTEX_POISONED);
124 if inner.set.contains_key(&key) {
125 return true;
126 }
127 let seq = inner.next_seq;
128 inner.next_seq = inner.next_seq.wrapping_add(1);
129 inner.set.insert(key.clone(), seq);
130 inner.queue.push_back((key, seq));
131 while inner.queue.len() > self.capacity
132 && let Some((old_key, old_seq)) = inner.queue.pop_front()
133 {
134 if inner.set.get(&old_key) == Some(&old_seq) {
136 inner.set.remove(&old_key);
137 }
138 }
139 false
140 }
141
142 pub fn insert(&self, key: K) {
144 let _ = self.check_and_insert(key);
145 }
146
147 #[allow(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
149 pub fn remove(&self, key: &K) -> bool {
150 self.inner
151 .lock()
152 .expect(MUTEX_POISONED)
153 .set
154 .remove(key)
155 .is_some()
156 }
157}
158
159#[derive(Debug, Clone)]
166pub struct OrderIdentity {
167 pub instrument_id: InstrumentId,
168 pub strategy_id: StrategyId,
169 pub order_side: OrderSide,
170 pub order_type: OrderType,
171}
172
173#[derive(Debug)]
176pub struct WsDispatchState {
177 pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
178 pub emitted_accepted: BoundedDedup<ClientOrderId>,
179 pub triggered_orders: BoundedDedup<ClientOrderId>,
180 pub filled_orders: BoundedDedup<ClientOrderId>,
181 pub terminal_orders: BoundedDedup<ClientOrderId>,
182 pub emitted_trades: BoundedDedup<TradeId>,
183 pub(crate) pending_orders: Arc<DashMap<String, PendingOrderInfo>>,
184 pub(crate) pending_cancels: Arc<DashMap<String, PendingOrderInfo>>,
185 pub(crate) pending_amends: Arc<DashMap<String, PendingOrderInfo>>,
186}
187
188impl Default for WsDispatchState {
189 fn default() -> Self {
190 Self {
191 order_identities: DashMap::new(),
192 emitted_accepted: BoundedDedup::new(DEDUP_CAPACITY),
193 triggered_orders: BoundedDedup::new(DEDUP_CAPACITY),
194 filled_orders: BoundedDedup::new(DEDUP_CAPACITY),
195 terminal_orders: BoundedDedup::new(DEDUP_CAPACITY),
196 emitted_trades: BoundedDedup::new(DEDUP_CAPACITY),
197 pending_orders: Arc::new(DashMap::new()),
198 pending_cancels: Arc::new(DashMap::new()),
199 pending_amends: Arc::new(DashMap::new()),
200 }
201 }
202}
203
204impl WsDispatchState {
205 pub(crate) fn with_pending_maps(
208 pending_orders: Arc<DashMap<String, PendingOrderInfo>>,
209 pending_cancels: Arc<DashMap<String, PendingOrderInfo>>,
210 pending_amends: Arc<DashMap<String, PendingOrderInfo>>,
211 ) -> Self {
212 Self {
213 pending_orders,
214 pending_cancels,
215 pending_amends,
216 ..Default::default()
217 }
218 }
219}
220
221impl WsDispatchState {
222 pub(crate) fn insert_accepted(&self, cid: ClientOrderId) {
223 self.emitted_accepted.insert(cid);
224 }
225
226 pub(crate) fn insert_triggered(&self, cid: ClientOrderId) {
227 self.triggered_orders.insert(cid);
228 }
229
230 pub(crate) fn insert_filled(&self, cid: ClientOrderId) {
231 self.filled_orders.insert(cid);
232 }
233
234 pub(crate) fn insert_terminal(&self, cid: ClientOrderId) {
235 self.terminal_orders.insert(cid);
236 }
237
238 pub fn check_and_insert_trade(&self, trade_id: TradeId) -> bool {
241 self.emitted_trades.check_and_insert(trade_id)
242 }
243}
244
245#[expect(clippy::too_many_arguments)]
252pub fn dispatch_ws_message(
253 message: OKXWsMessage,
254 emitter: &ExecutionEventEmitter,
255 state: &WsDispatchState,
256 account_id: AccountId,
257 instruments: &AtomicMap<Ustr, InstrumentAny>,
258 fee_cache: &mut AHashMap<Ustr, Money>,
259 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
260 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
261 clock: &AtomicTime,
262) {
263 let guard = instruments.load();
264 let instruments: &AHashMap<Ustr, InstrumentAny> = &guard;
265
266 match message {
267 OKXWsMessage::Orders(order_msgs) => {
268 let ts_init = clock.get_time_ns();
269 dispatch_order_messages(
270 &order_msgs,
271 emitter,
272 state,
273 account_id,
274 instruments,
275 fee_cache,
276 filled_qty_cache,
277 order_state_cache,
278 ts_init,
279 );
280 }
281 OKXWsMessage::SpreadOrders(order_msgs) => {
282 let ts_init = clock.get_time_ns();
283 dispatch_spread_order_messages(
284 &order_msgs,
285 emitter,
286 state,
287 account_id,
288 instruments,
289 filled_qty_cache,
290 order_state_cache,
291 ts_init,
292 );
293 }
294 OKXWsMessage::AlgoOrders(algo_msgs) => {
295 let ts_init = clock.get_time_ns();
296 let mut reports = Vec::new();
297
298 for msg in algo_msgs {
299 match parse_algo_order_msg(&msg, account_id, instruments, ts_init) {
300 Ok(Some(report)) => reports.push(report),
301 Ok(None) => {}
302 Err(e) => log::error!("Failed to parse algo order message: {e}"),
303 }
304 }
305 dispatch_execution_reports(reports, emitter, state);
306 }
307 OKXWsMessage::Account(data) => {
308 let ts_init = clock.get_time_ns();
309
310 match serde_json::from_value::<Vec<OKXAccount>>(data) {
311 Ok(accounts) => {
312 for account in &accounts {
313 match crate::common::parse::parse_account_state(
314 account, account_id, ts_init,
315 ) {
316 Ok(account_state) => emitter.send_account_state(account_state),
317 Err(e) => log::error!("Failed to parse account state: {e}"),
318 }
319 }
320 }
321 Err(e) => log::error!("Failed to deserialize account data: {e}"),
322 }
323 }
324 OKXWsMessage::Positions(data) => {
325 let ts_init = clock.get_time_ns();
326
327 match serde_json::from_value::<Vec<OKXPosition>>(data) {
328 Ok(positions) => {
329 for position in positions {
330 let Some(instrument) = instruments.get(&position.inst_id) else {
331 log::warn!("No cached instrument for position: {}", position.inst_id);
332 continue;
333 };
334 let instrument_id = instrument.id();
335 let size_precision = instrument.size_precision();
336
337 match crate::common::parse::parse_position_status_report(
338 &position,
339 account_id,
340 instrument_id,
341 size_precision,
342 ts_init,
343 ) {
344 Ok(report) => emitter.send_position_report(report),
345 Err(e) => log::error!("Failed to parse position report: {e}"),
346 }
347 }
348 }
349 Err(e) => log::error!("Failed to deserialize positions data: {e}"),
350 }
351 }
352 OKXWsMessage::OrderResponse {
353 id,
354 op,
355 code,
356 msg,
357 data,
358 } => {
359 let ts_init = clock.get_time_ns();
360
361 for item in &data {
362 let s_code = item
363 .get(OKX_FIELD_SCODE)
364 .and_then(|v| v.as_str())
365 .unwrap_or("");
366 let s_msg = item
367 .get(OKX_FIELD_SMSG)
368 .and_then(|v| v.as_str())
369 .unwrap_or("");
370 let sub_code = item
371 .get(OKX_FIELD_SUBCODE)
372 .and_then(|v| v.as_str())
373 .unwrap_or("");
374 let reason = format_order_response_reason(s_code, s_msg, sub_code);
375 let cl_ord_id = item
376 .get(OKX_FIELD_CLORDID)
377 .and_then(|v| v.as_str())
378 .unwrap_or("");
379
380 if s_code == OKX_SUCCESS_CODE {
381 log::debug!("Order response ok: op={op:?} cl_ord_id={cl_ord_id}");
382 match op {
383 OKXWsOperation::Order
384 | OKXWsOperation::BatchOrders
385 | OKXWsOperation::OrderAlgo => {
386 state.pending_orders.remove(cl_ord_id);
387 }
388 OKXWsOperation::CancelOrder
389 | OKXWsOperation::BatchCancelOrders
390 | OKXWsOperation::MassCancel
391 | OKXWsOperation::CancelAlgos => {
392 state.pending_cancels.remove(cl_ord_id);
393 }
394 OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
395 state.pending_amends.remove(cl_ord_id);
396 }
397 _ => {}
398 }
399 continue;
400 }
401
402 let Some(client_order_id) = parse_client_order_id(cl_ord_id) else {
403 log::warn!(
404 "Order response error without client_order_id: \
405 op={op:?} s_code={s_code} s_msg={s_msg}"
406 );
407 continue;
408 };
409
410 let Some(ident) = state
411 .order_identities
412 .get(&client_order_id)
413 .map(|entry| entry.clone())
414 else {
415 log::warn!(
416 "Order response error for untracked order: \
417 op={op:?} cl_ord_id={cl_ord_id} s_code={s_code} s_msg={s_msg}"
418 );
419 continue;
420 };
421
422 let venue_order_id = item
423 .get("ordId")
424 .and_then(|v| v.as_str())
425 .filter(|s| !s.is_empty())
426 .map(VenueOrderId::new);
427
428 match op {
429 OKXWsOperation::Order | OKXWsOperation::BatchOrders => {
430 state.order_identities.remove(&client_order_id);
431 state.pending_orders.remove(cl_ord_id);
432 emitter.emit_order_rejected_event(
433 ident.strategy_id,
434 ident.instrument_id,
435 client_order_id,
436 &reason,
437 ts_init,
438 false,
439 );
440 }
441 OKXWsOperation::CancelOrder
442 | OKXWsOperation::BatchCancelOrders
443 | OKXWsOperation::MassCancel => {
444 state.pending_cancels.remove(cl_ord_id);
445 emitter.emit_order_cancel_rejected_event(
446 ident.strategy_id,
447 ident.instrument_id,
448 client_order_id,
449 venue_order_id,
450 &reason,
451 ts_init,
452 );
453 }
454 OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
455 state.pending_amends.remove(cl_ord_id);
456 emitter.emit_order_modify_rejected_event(
457 ident.strategy_id,
458 ident.instrument_id,
459 client_order_id,
460 venue_order_id,
461 &reason,
462 ts_init,
463 );
464 }
465 _ => {
466 log::warn!(
467 "Order response error for unhandled op: \
468 op={op:?} cl_ord_id={cl_ord_id} s_code={s_code} s_msg={s_msg}"
469 );
470 }
471 }
472 }
473
474 if code != "0" && data.is_empty() {
475 log::warn!(
476 "Order response error (no data): id={id:?} op={op:?} code={code} msg={msg}"
477 );
478 }
479 }
480 OKXWsMessage::SendFailed {
481 request_id,
482 client_order_id,
483 op,
484 error,
485 } => {
486 log::warn!(
487 "WebSocket send failed without structured venue response: \
488 request_id={request_id}, client_order_id={client_order_id:?}, \
489 op={op:?}, awaiting reconciliation: {error}"
490 );
491
492 if let Some(client_order_id) = client_order_id {
493 let key = client_order_id.as_str();
494
495 match op {
496 Some(
497 OKXWsOperation::Order
498 | OKXWsOperation::BatchOrders
499 | OKXWsOperation::OrderAlgo,
500 ) => {
501 state.pending_orders.remove(key);
502 }
503 Some(
504 OKXWsOperation::CancelOrder
505 | OKXWsOperation::BatchCancelOrders
506 | OKXWsOperation::MassCancel
507 | OKXWsOperation::CancelAlgos,
508 ) => {
509 state.pending_cancels.remove(key);
510 }
511 Some(OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders) => {
512 state.pending_amends.remove(key);
513 }
514 _ => {}
515 }
516 }
517 }
518 OKXWsMessage::ChannelData { channel, .. } => {
519 log::debug!("Ignoring data channel message on execution client: {channel:?}");
520 }
521 OKXWsMessage::BookData { .. } | OKXWsMessage::Instruments(_) => {
522 log::debug!("Ignoring data message on execution client");
523 }
524 OKXWsMessage::Error(e) => {
525 log::warn!(
526 "Websocket error: code={} message={} conn_id={:?}",
527 e.code,
528 e.message,
529 e.conn_id
530 );
531 }
532 OKXWsMessage::Reconnected => {
533 log::info!("Websocket reconnected");
534 }
535 OKXWsMessage::Authenticated => {
536 log::debug!("Websocket authenticated");
537 }
538 }
539}
540
541#[expect(clippy::too_many_arguments)]
544fn dispatch_order_messages(
545 order_msgs: &[OKXOrderMsg],
546 emitter: &ExecutionEventEmitter,
547 state: &WsDispatchState,
548 account_id: AccountId,
549 instruments: &AHashMap<Ustr, InstrumentAny>,
550 fee_cache: &mut AHashMap<Ustr, Money>,
551 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
552 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
553 ts_init: UnixNanos,
554) {
555 for msg in order_msgs {
556 let Some(instrument) = instruments.get(&msg.inst_id) else {
557 log::warn!("No instrument for {}, skipping order message", msg.inst_id);
558 continue;
559 };
560
561 let Some(client_order_id) = parse_client_order_id(&msg.cl_ord_id) else {
562 log::debug!(
563 "Order without client_order_id (ord_id={}), sending as report",
564 msg.ord_id
565 );
566 dispatch_order_msg_as_report(
567 msg,
568 account_id,
569 instruments,
570 fee_cache,
571 filled_qty_cache,
572 emitter,
573 state,
574 ts_init,
575 );
576 continue;
577 };
578
579 let (client_order_id, identity) = match state
584 .order_identities
585 .get(&client_order_id)
586 .map(|r| r.clone())
587 {
588 Some(ident) => (client_order_id, Some(ident)),
589 None => {
590 if let Some(parent_id) = msg
591 .algo_cl_ord_id
592 .as_deref()
593 .and_then(parse_client_order_id)
594 {
595 let parent_ident = state.order_identities.get(&parent_id).map(|r| r.clone());
596
597 if parent_ident.is_some() {
598 (parent_id, parent_ident)
599 } else {
600 (client_order_id, None)
601 }
602 } else {
603 (client_order_id, None)
604 }
605 }
606 };
607
608 if let Some(ident) = identity {
609 if is_post_only_auto_cancel(msg) {
610 let ts_event = parse_millisecond_timestamp(msg.u_time);
611 let rejected = OrderRejected::new(
612 emitter.trader_id(),
613 ident.strategy_id,
614 instrument.id(),
615 client_order_id,
616 account_id,
617 Ustr::from("Post-only order would have taken liquidity"),
618 UUID4::new(),
619 ts_event,
620 ts_init,
621 false,
622 true, );
624 state.order_identities.remove(&client_order_id);
625 order_state_cache.remove(&client_order_id);
626 fee_cache.remove(&msg.ord_id);
627 filled_qty_cache.remove(&msg.ord_id);
628 emitter.send_order_event(OrderEventAny::Rejected(rejected));
629 continue;
630 }
631
632 let previous_fee = fee_cache.get(&msg.ord_id).copied();
633 let previous_filled_qty = filled_qty_cache.get(&msg.ord_id).copied();
634 let previous_state = order_state_cache.get(&client_order_id);
635
636 match parse_order_event(
637 msg,
638 client_order_id,
639 account_id,
640 emitter.trader_id(),
641 ident.strategy_id,
642 instrument,
643 previous_fee,
644 previous_filled_qty,
645 previous_state,
646 ts_init,
647 ) {
648 Ok(event) => {
649 update_order_caches(
650 msg,
651 instrument,
652 client_order_id,
653 fee_cache,
654 filled_qty_cache,
655 order_state_cache,
656 );
657 dispatch_parsed_order_event(
658 event,
659 client_order_id,
660 account_id,
661 VenueOrderId::new(msg.ord_id),
662 &ident,
663 instrument,
664 msg.state,
665 emitter,
666 state,
667 order_state_cache,
668 ts_init,
669 );
670 }
671 Err(e) => log::error!("Failed to parse order event for {client_order_id}: {e}"),
672 }
673 } else {
674 log::debug!(
675 "Untracked order {client_order_id} (ord_id={}), sending as report for reconciliation",
676 msg.ord_id
677 );
678 dispatch_order_msg_as_report(
679 msg,
680 account_id,
681 instruments,
682 fee_cache,
683 filled_qty_cache,
684 emitter,
685 state,
686 ts_init,
687 );
688 }
689 }
690}
691
692#[expect(clippy::too_many_arguments)]
693fn dispatch_spread_order_messages(
694 order_msgs: &[OKXSpreadOrder],
695 emitter: &ExecutionEventEmitter,
696 state: &WsDispatchState,
697 account_id: AccountId,
698 instruments: &AHashMap<Ustr, InstrumentAny>,
699 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
700 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
701 ts_init: UnixNanos,
702) {
703 for msg in order_msgs {
704 let Some(instrument) = instruments.get(&msg.sprd_id) else {
705 log::warn!(
706 "No instrument for {}, skipping spread order message",
707 msg.sprd_id
708 );
709 continue;
710 };
711
712 let Some(client_order_id) = parse_client_order_id(msg.cl_ord_id.as_str()) else {
713 log::debug!(
714 "Spread order without client_order_id (ord_id={}), sending as report",
715 msg.ord_id
716 );
717 dispatch_spread_order_msg_as_report(
718 msg,
719 account_id,
720 instruments,
721 filled_qty_cache,
722 emitter,
723 state,
724 ts_init,
725 );
726 continue;
727 };
728
729 let identity = state
730 .order_identities
731 .get(&client_order_id)
732 .map(|r| r.clone());
733
734 if let Some(ident) = identity {
735 if is_spread_post_only_auto_cancel(msg) {
736 let ts_event = msg
737 .u_time
738 .or(msg.c_time)
739 .map_or(ts_init, parse_millisecond_timestamp);
740 let rejected = OrderRejected::new(
741 emitter.trader_id(),
742 ident.strategy_id,
743 instrument.id(),
744 client_order_id,
745 account_id,
746 Ustr::from(OKX_POST_ONLY_CANCEL_REASON),
747 UUID4::new(),
748 ts_event,
749 ts_init,
750 false,
751 true,
752 );
753 state.order_identities.remove(&client_order_id);
754 order_state_cache.remove(&client_order_id);
755 filled_qty_cache.remove(&msg.ord_id);
756 emitter.send_order_event(OrderEventAny::Rejected(rejected));
757 continue;
758 }
759
760 let previous_filled_qty = filled_qty_cache.get(&msg.ord_id).copied();
761 let previous_state = order_state_cache.get(&client_order_id);
762
763 match parse_spread_order_event(
764 msg,
765 client_order_id,
766 account_id,
767 emitter.trader_id(),
768 ident.strategy_id,
769 instrument,
770 previous_filled_qty,
771 previous_state,
772 ts_init,
773 ) {
774 Ok(event) => {
775 update_spread_order_caches(
776 msg,
777 instrument,
778 client_order_id,
779 filled_qty_cache,
780 order_state_cache,
781 );
782 dispatch_parsed_order_event(
783 event,
784 client_order_id,
785 account_id,
786 VenueOrderId::new(msg.ord_id.as_str()),
787 &ident,
788 instrument,
789 msg.state,
790 emitter,
791 state,
792 order_state_cache,
793 ts_init,
794 );
795 }
796 Err(e) => {
797 log::error!("Failed to parse spread order event for {client_order_id}: {e}");
798 }
799 }
800 } else {
801 log::debug!(
802 "Untracked spread order {client_order_id} (ord_id={}), sending as report for reconciliation",
803 msg.ord_id
804 );
805 dispatch_spread_order_msg_as_report(
806 msg,
807 account_id,
808 instruments,
809 filled_qty_cache,
810 emitter,
811 state,
812 ts_init,
813 );
814 }
815 }
816}
817
818#[expect(clippy::too_many_arguments)]
824fn dispatch_parsed_order_event(
825 event: ParsedOrderEvent,
826 client_order_id: ClientOrderId,
827 account_id: AccountId,
828 venue_order_id: VenueOrderId,
829 identity: &OrderIdentity,
830 instrument: &InstrumentAny,
831 venue_status: OKXOrderStatus,
832 emitter: &ExecutionEventEmitter,
833 state: &WsDispatchState,
834 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
835 ts_init: UnixNanos,
836) {
837 let is_terminal;
838
839 match event {
840 ParsedOrderEvent::Accepted(e) => {
841 if state.emitted_accepted.contains(&client_order_id)
842 || state.filled_orders.contains(&client_order_id)
843 || state.triggered_orders.contains(&client_order_id)
844 || state.terminal_orders.contains(&client_order_id)
845 {
846 log::debug!("Skipping duplicate Accepted for {client_order_id}");
847 return;
848 }
849 state.insert_accepted(client_order_id);
850 is_terminal = false;
851 emitter.send_order_event(OrderEventAny::Accepted(e));
852 }
853 ParsedOrderEvent::Triggered(e) => {
854 if state.filled_orders.contains(&client_order_id) {
855 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
856 return;
857 }
858
859 if !TRIGGERABLE_ORDER_TYPES.contains(&identity.order_type) {
860 log::debug!(
861 "Skipping OrderTriggered for {} order {client_order_id}: market-style stops have no TRIGGERED state",
862 identity.order_type,
863 );
864 state.insert_triggered(client_order_id);
865 return;
866 }
867
868 ensure_accepted_emitted(
869 client_order_id,
870 account_id,
871 venue_order_id,
872 identity,
873 emitter,
874 state,
875 ts_init,
876 );
877 state.insert_triggered(client_order_id);
878 is_terminal = false;
879 emitter.send_order_event(OrderEventAny::Triggered(e));
880 }
881 ParsedOrderEvent::Canceled(e) => {
882 ensure_accepted_emitted(
883 client_order_id,
884 account_id,
885 venue_order_id,
886 identity,
887 emitter,
888 state,
889 ts_init,
890 );
891 state.triggered_orders.remove(&client_order_id);
892 state.filled_orders.remove(&client_order_id);
893 is_terminal = true;
894 emitter.send_order_event(OrderEventAny::Canceled(e));
895 }
896 ParsedOrderEvent::Expired(e) => {
897 ensure_accepted_emitted(
898 client_order_id,
899 account_id,
900 venue_order_id,
901 identity,
902 emitter,
903 state,
904 ts_init,
905 );
906 state.triggered_orders.remove(&client_order_id);
907 state.filled_orders.remove(&client_order_id);
908 is_terminal = true;
909 emitter.send_order_event(OrderEventAny::Expired(e));
910 }
911 ParsedOrderEvent::Updated(e) => {
912 ensure_accepted_emitted(
913 client_order_id,
914 account_id,
915 venue_order_id,
916 identity,
917 emitter,
918 state,
919 ts_init,
920 );
921 is_terminal = false;
922 emitter.send_order_event(OrderEventAny::Updated(e));
923 }
924 ParsedOrderEvent::Fill(fill_report) => {
925 let is_duplicate = state.check_and_insert_trade(fill_report.trade_id);
926 is_terminal = venue_status == OKXOrderStatus::Filled;
927
928 if is_duplicate {
929 log::debug!(
930 "Skipping duplicate fill for {client_order_id}: trade_id={}",
931 fill_report.trade_id
932 );
933 } else {
934 ensure_accepted_emitted(
935 client_order_id,
936 account_id,
937 venue_order_id,
938 identity,
939 emitter,
940 state,
941 ts_init,
942 );
943 state.insert_filled(client_order_id);
944 state.triggered_orders.remove(&client_order_id);
945 let filled = fill_report_to_order_filled(
946 &fill_report,
947 emitter.trader_id(),
948 identity,
949 instrument.quote_currency(),
950 );
951 emitter.send_order_event(OrderEventAny::Filled(filled));
952 }
953 }
954 ParsedOrderEvent::StatusOnly(report) => {
955 is_terminal = matches!(
956 report.order_status,
957 OrderStatus::Filled | OrderStatus::Canceled | OrderStatus::Expired
958 );
959 emitter.send_order_status_report(*report);
960 }
961 ParsedOrderEvent::Skipped => return,
962 }
963
964 if is_terminal {
965 state.insert_terminal(client_order_id);
966 state.order_identities.remove(&client_order_id);
967 state.emitted_accepted.remove(&client_order_id);
968 order_state_cache.remove(&client_order_id);
969 }
973}
974
975fn ensure_accepted_emitted(
978 client_order_id: ClientOrderId,
979 account_id: AccountId,
980 venue_order_id: VenueOrderId,
981 identity: &OrderIdentity,
982 emitter: &ExecutionEventEmitter,
983 state: &WsDispatchState,
984 ts_init: UnixNanos,
985) {
986 if state.emitted_accepted.contains(&client_order_id) {
987 return;
988 }
989 state.insert_accepted(client_order_id);
990 let accepted = OrderAccepted::new(
991 emitter.trader_id(),
992 identity.strategy_id,
993 identity.instrument_id,
994 client_order_id,
995 venue_order_id,
996 account_id,
997 UUID4::new(),
998 ts_init,
999 ts_init,
1000 false,
1001 );
1002 emitter.send_order_event(OrderEventAny::Accepted(accepted));
1003}
1004
1005fn fill_report_to_order_filled(
1007 report: &FillReport,
1008 trader_id: TraderId,
1009 identity: &OrderIdentity,
1010 quote_currency: Currency,
1011) -> OrderFilled {
1012 OrderFilled::new(
1013 trader_id,
1014 identity.strategy_id,
1015 report.instrument_id,
1016 report
1017 .client_order_id
1018 .expect("tracked order has client_order_id"),
1019 report.venue_order_id,
1020 report.account_id,
1021 report.trade_id,
1022 identity.order_side,
1023 identity.order_type,
1024 report.last_qty,
1025 report.last_px,
1026 quote_currency,
1027 report.liquidity_side,
1028 UUID4::new(),
1029 report.ts_event,
1030 report.ts_init,
1031 false,
1032 report.venue_position_id,
1033 Some(report.commission),
1034 )
1035}
1036
1037#[expect(clippy::too_many_arguments)]
1039fn dispatch_order_msg_as_report(
1040 msg: &OKXOrderMsg,
1041 account_id: AccountId,
1042 instruments: &AHashMap<Ustr, InstrumentAny>,
1043 fee_cache: &mut AHashMap<Ustr, Money>,
1044 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1045 emitter: &ExecutionEventEmitter,
1046 state: &WsDispatchState,
1047 ts_init: UnixNanos,
1048) {
1049 match parse_order_msg(
1050 msg,
1051 account_id,
1052 instruments,
1053 fee_cache,
1054 filled_qty_cache,
1055 ts_init,
1056 ) {
1057 Ok(report) => {
1058 if let Some(instrument) = instruments.get(&msg.inst_id) {
1059 update_fee_fill_caches(msg, instrument, fee_cache, filled_qty_cache);
1060 }
1061 dispatch_execution_reports(vec![report], emitter, state);
1062 }
1063 Err(e) => log::error!("Failed to parse order message as report: {e}"),
1064 }
1065}
1066
1067fn dispatch_spread_order_msg_as_report(
1068 msg: &OKXSpreadOrder,
1069 account_id: AccountId,
1070 instruments: &AHashMap<Ustr, InstrumentAny>,
1071 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1072 emitter: &ExecutionEventEmitter,
1073 state: &WsDispatchState,
1074 ts_init: UnixNanos,
1075) {
1076 match parse_spread_order_msg(msg, account_id, instruments, filled_qty_cache, ts_init) {
1077 Ok(report) => {
1078 if let Some(instrument) = instruments.get(&msg.sprd_id) {
1079 update_spread_fill_cache(msg, instrument, filled_qty_cache);
1080 }
1081 dispatch_execution_reports(vec![report], emitter, state);
1082 }
1083 Err(e) => log::error!("Failed to parse spread order message as report: {e}"),
1084 }
1085}
1086
1087fn update_order_caches(
1089 msg: &OKXOrderMsg,
1090 instrument: &InstrumentAny,
1091 client_order_id: ClientOrderId,
1092 fee_cache: &mut AHashMap<Ustr, Money>,
1093 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1094 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
1095) {
1096 update_fee_fill_caches(msg, instrument, fee_cache, filled_qty_cache);
1097
1098 let venue_order_id = VenueOrderId::new(msg.ord_id);
1099 let quantity = parse_quantity(&msg.sz, instrument.size_precision()).unwrap_or_default();
1100 let price = if is_market_price(&msg.px) {
1101 None
1102 } else {
1103 parse_price(&msg.px, instrument.price_precision()).ok()
1104 };
1105
1106 order_state_cache.insert(
1107 client_order_id,
1108 OrderStateSnapshot {
1109 venue_order_id,
1110 quantity,
1111 price,
1112 },
1113 );
1114}
1115
1116fn update_spread_order_caches(
1117 msg: &OKXSpreadOrder,
1118 instrument: &InstrumentAny,
1119 client_order_id: ClientOrderId,
1120 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1121 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
1122) {
1123 update_spread_fill_cache(msg, instrument, filled_qty_cache);
1124
1125 let venue_order_id = VenueOrderId::new(msg.ord_id.as_str());
1126 let quantity = parse_quantity(&msg.sz, instrument.size_precision()).unwrap_or_default();
1127 let price = if is_market_price(&msg.px) {
1128 None
1129 } else {
1130 parse_price(&msg.px, instrument.price_precision()).ok()
1131 };
1132
1133 order_state_cache.insert(
1134 client_order_id,
1135 OrderStateSnapshot {
1136 venue_order_id,
1137 quantity,
1138 price,
1139 },
1140 );
1141}
1142
1143fn update_spread_fill_cache(
1144 msg: &OKXSpreadOrder,
1145 instrument: &InstrumentAny,
1146 filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1147) {
1148 if !msg.acc_fill_sz.is_empty()
1149 && msg.acc_fill_sz != "0"
1150 && let Ok(qty) = parse_quantity(&msg.acc_fill_sz, instrument.size_precision())
1151 {
1152 filled_qty_cache.insert(msg.ord_id, qty);
1153 }
1154}
1155
1156fn is_spread_post_only_auto_cancel(msg: &OKXSpreadOrder) -> bool {
1157 msg.state == OKXOrderStatus::Canceled && msg.cancel_source == OKX_POST_ONLY_CANCEL_SOURCE
1158}
1159
1160pub fn dispatch_execution_reports(
1162 reports: Vec<ExecutionReport>,
1163 emitter: &ExecutionEventEmitter,
1164 state: &WsDispatchState,
1165) {
1166 log::debug!("Processing {} execution report(s)", reports.len());
1167
1168 for report in reports {
1169 match report {
1170 ExecutionReport::Order(order_report) => {
1171 if let Some(cid) = order_report.client_order_id {
1172 match order_report.order_status {
1173 #[allow(clippy::collapsible_match)]
1175 OrderStatus::Accepted => {
1176 if state.terminal_orders.contains(&cid)
1177 || state.filled_orders.contains(&cid)
1178 || state.triggered_orders.contains(&cid)
1179 {
1180 log::debug!(
1181 "Skipping stale OrderStatusReport(Accepted) \
1182 for {cid} (order already terminal)"
1183 );
1184 continue;
1185 }
1186 }
1187 OrderStatus::Triggered => {
1188 if state.filled_orders.contains(&cid) {
1189 log::debug!(
1190 "Skipping stale OrderStatusReport(Triggered) \
1191 for {cid} (already filled)"
1192 );
1193 continue;
1194 }
1195 state.insert_triggered(cid);
1196 }
1197 OrderStatus::Filled => {
1198 state.insert_filled(cid);
1199 state.insert_terminal(cid);
1200 state.triggered_orders.remove(&cid);
1201 }
1202 OrderStatus::Canceled | OrderStatus::Expired | OrderStatus::Rejected => {
1203 state.insert_terminal(cid);
1204 state.triggered_orders.remove(&cid);
1205 state.filled_orders.remove(&cid);
1206 }
1207 _ => {}
1208 }
1209 }
1210 emitter.send_order_status_report(order_report);
1211 }
1212 ExecutionReport::Fill(fill_report) => {
1213 if state.check_and_insert_trade(fill_report.trade_id) {
1214 log::debug!(
1215 "Skipping duplicate fill report: trade_id={}",
1216 fill_report.trade_id
1217 );
1218 continue;
1219 }
1220
1221 if let Some(cid) = fill_report.client_order_id {
1222 state.insert_filled(cid);
1223 state.triggered_orders.remove(&cid);
1224 }
1225 emitter.send_fill_report(fill_report);
1226 }
1227 }
1228 }
1229}
1230
1231fn format_order_response_reason(s_code: &str, s_msg: &str, sub_code: &str) -> String {
1232 match (s_msg.is_empty(), sub_code.is_empty(), s_code.is_empty()) {
1233 (false, true, _) => s_msg.to_string(),
1234 (false, false, _) => format!("{s_msg} (subCode={sub_code})"),
1235 (true, false, false) => format!("sCode={s_code} subCode={sub_code}"),
1236 (true, false, true) => format!("subCode={sub_code}"),
1237 (true, true, false) => format!("sCode={s_code}"),
1238 (true, true, true) => String::new(),
1239 }
1240}
1241
1242#[derive(Debug, Clone)]
1243pub struct AlgoCancelContext {
1244 pub client_order_id: ClientOrderId,
1245 pub instrument_id: InstrumentId,
1246 pub strategy_id: StrategyId,
1247 pub venue_order_id: Option<VenueOrderId>,
1248}
1249
1250pub fn emit_algo_cancel_rejections(
1253 responses: &[OKXCancelAlgoOrderResponse],
1254 contexts: &[AlgoCancelContext],
1255 emitter: &ExecutionEventEmitter,
1256 clock: &'static AtomicTime,
1257) {
1258 for (i, item) in responses.iter().enumerate() {
1259 let code = item.s_code.as_deref().unwrap_or(OKX_SUCCESS_CODE);
1260 if code == OKX_SUCCESS_CODE {
1261 continue;
1262 }
1263
1264 let msg = item.s_msg.as_deref().unwrap_or("");
1265
1266 if let Some(ctx) = contexts.get(i) {
1267 let ts = clock.get_time_ns();
1268 emitter.emit_order_cancel_rejected_event(
1269 ctx.strategy_id,
1270 ctx.instrument_id,
1271 ctx.client_order_id,
1272 ctx.venue_order_id,
1273 msg,
1274 ts,
1275 );
1276 } else {
1277 log::warn!(
1278 "Algo cancel rejected but no context at index {i}: \
1279 algo_id={} sCode={code} sMsg={msg}",
1280 item.algo_id
1281 );
1282 }
1283 }
1284}
1285
1286pub fn emit_batch_cancel_failure(
1287 contexts: &[AlgoCancelContext],
1288 error: &str,
1289 _emitter: &ExecutionEventEmitter,
1290 _clock: &'static AtomicTime,
1291) {
1292 for ctx in contexts {
1293 log::warn!(
1294 "Ambiguous algo batch cancel failure for {}, awaiting reconciliation: {error}",
1295 ctx.client_order_id
1296 );
1297 }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use rstest::rstest;
1303
1304 use super::{BoundedDedup, format_order_response_reason};
1305
1306 #[rstest]
1307 #[case("51000", "Rejected", "", "Rejected")]
1308 #[case("51000", "Rejected", "51004", "Rejected (subCode=51004)")]
1309 #[case("51000", "", "51004", "sCode=51000 subCode=51004")]
1310 #[case("51000", "", "", "sCode=51000")]
1311 #[case("", "", "51004", "subCode=51004")]
1312 #[case("", "", "", "")]
1313 fn test_format_order_response_reason(
1314 #[case] s_code: &str,
1315 #[case] s_msg: &str,
1316 #[case] sub_code: &str,
1317 #[case] expected: &str,
1318 ) {
1319 assert_eq!(
1320 format_order_response_reason(s_code, s_msg, sub_code),
1321 expected
1322 );
1323 }
1324
1325 #[rstest]
1326 fn test_bounded_dedup_check_and_insert_returns_false_on_first_insert() {
1327 let dedup = BoundedDedup::<u32>::new(4);
1328 assert!(!dedup.check_and_insert(1));
1329 assert!(dedup.contains(&1));
1330 }
1331
1332 #[rstest]
1333 fn test_bounded_dedup_check_and_insert_returns_true_on_duplicate() {
1334 let dedup = BoundedDedup::<u32>::new(4);
1335 dedup.insert(1);
1336 assert!(dedup.check_and_insert(1));
1337 }
1338
1339 #[rstest]
1340 fn test_bounded_dedup_evicts_oldest_on_overflow() {
1341 let dedup = BoundedDedup::<u32>::new(3);
1342 dedup.insert(1);
1343 dedup.insert(2);
1344 dedup.insert(3);
1345 dedup.insert(4);
1346
1347 assert!(!dedup.contains(&1));
1348 assert!(dedup.contains(&2));
1349 assert!(dedup.contains(&3));
1350 assert!(dedup.contains(&4));
1351 }
1352
1353 #[rstest]
1354 fn test_bounded_dedup_evicted_key_is_not_treated_as_duplicate() {
1355 let dedup = BoundedDedup::<u32>::new(2);
1356 dedup.insert(1);
1357 dedup.insert(2);
1358 dedup.insert(3);
1359
1360 assert!(!dedup.check_and_insert(1));
1361 assert!(dedup.contains(&1));
1362 }
1363
1364 #[rstest]
1365 fn test_bounded_dedup_remove_drops_entry() {
1366 let dedup = BoundedDedup::<u32>::new(4);
1367 dedup.insert(1);
1368 assert!(dedup.remove(&1));
1369 assert!(!dedup.contains(&1));
1370 assert!(!dedup.remove(&1));
1371 }
1372
1373 #[rstest]
1374 fn test_bounded_dedup_remove_then_reinsert_does_not_double_count() {
1375 let dedup = BoundedDedup::<u32>::new(2);
1376 for k in 0u32..1000 {
1377 dedup.insert(k);
1378 dedup.remove(&k);
1379 }
1380 assert!(!dedup.contains(&0));
1381 assert!(!dedup.contains(&500));
1382 }
1383
1384 #[rstest]
1385 fn test_bounded_dedup_reinsert_survives_stale_marker_eviction() {
1386 let dedup = BoundedDedup::<u32>::new(3);
1387 dedup.insert(1);
1388 dedup.remove(&1);
1389
1390 dedup.insert(2);
1391 dedup.insert(3);
1392 dedup.insert(1);
1393
1394 assert!(dedup.contains(&1));
1395 assert!(dedup.contains(&2));
1396 assert!(dedup.contains(&3));
1397 }
1398}