1use std::{collections::VecDeque, fmt::Debug, hash::Hash, sync::Arc};
24
25use ahash::AHashMap;
26use dashmap::DashMap;
27use nautilus_common::cache::fifo::{FifoCache, FifoCacheMap};
28use nautilus_core::{AtomicMap, UUID4, UnixNanos, time::AtomicTime};
29use nautilus_live::{
30 ExecutionEventEmitter,
31 execution::{
32 context::{OrderContext, OrderIdentity},
33 failure::CommandFailure,
34 },
35};
36use nautilus_model::{
37 enums::{AccountType, OrderStatus},
38 events::{
39 OrderAccepted, OrderCanceled, OrderEventAny, OrderFilled, OrderRejected, OrderTriggered,
40 OrderUpdated,
41 },
42 identifiers::{
43 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
44 },
45 instruments::{Instrument, InstrumentAny},
46 orders::TRIGGERABLE_ORDER_TYPES,
47 reports::FillReport,
48 types::Currency,
49};
50use parking_lot::Mutex;
51use ustr::Ustr;
52
53use crate::{
54 common::{
55 consts::{
56 OKX_FIELD_CLORDID, OKX_FIELD_SCODE, OKX_FIELD_SMSG, OKX_FIELD_SUBCODE,
57 OKX_POST_ONLY_CANCEL_REASON, OKX_POST_ONLY_CANCEL_SOURCE, OKX_SUCCESS_CODE,
58 },
59 enums::{OKXAlgoOrderStatus, OKXAlgoOrderType, OKXOrderStatus, OKXOrderType},
60 failure::{classify_okx_venue_code, classify_okx_ws_failure},
61 parse::{
62 is_market_price, parse_client_order_id, parse_millisecond_timestamp, parse_price,
63 parse_quantity,
64 },
65 },
66 http::models::{OKXAccount, OKXCancelAlgoOrderResponse, OKXPosition, OKXSpreadOrder},
67 websocket::{
68 client::PendingOrderInfo,
69 enums::OKXWsOperation,
70 handler::{is_post_only_auto_cancel, is_unfilled_rpi_cancel},
71 messages::{ExecutionReport, OKXAlgoOrderMsg, OKXOrderMsg, OKXWsMessage},
72 parse::{
73 FeeCache, FilledQtyCache, OrderStateSnapshot, ParsedOrderEvent,
74 is_terminal_order_state, parse_algo_order_msg, parse_algo_order_status_report,
75 parse_order_event, parse_order_msg, parse_order_status_report,
76 parse_spread_order_event, parse_spread_order_msg, update_fee_fill_caches,
77 },
78 },
79};
80
81const DEDUP_CAPACITY: usize = 10_000;
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85struct OrderVenueBinding {
86 parent: VenueOrderId,
87 child: Option<VenueOrderId>,
88}
89
90#[derive(Debug)]
91struct OrderLifecycleBindings {
92 client_by_parent: AHashMap<VenueOrderId, ClientOrderId>,
93 venue_by_client: AHashMap<ClientOrderId, OrderVenueBinding>,
94 terminal_client_by_parent: FifoCacheMap<VenueOrderId, ClientOrderId, DEDUP_CAPACITY>,
95}
96
97impl Default for OrderLifecycleBindings {
98 fn default() -> Self {
99 Self {
100 client_by_parent: AHashMap::new(),
101 venue_by_client: AHashMap::new(),
102 terminal_client_by_parent: FifoCacheMap::new(),
103 }
104 }
105}
106
107impl OrderLifecycleBindings {
108 fn client_order_id(&self, parent: &VenueOrderId) -> Option<ClientOrderId> {
109 self.client_by_parent
110 .get(parent)
111 .or_else(|| self.terminal_client_by_parent.get(parent))
112 .copied()
113 }
114
115 fn finish(&mut self, client_order_id: ClientOrderId, binding: OrderVenueBinding) {
116 self.client_by_parent.remove(&binding.parent);
117 self.venue_by_client.remove(&client_order_id);
118 self.terminal_client_by_parent
119 .insert(binding.parent, client_order_id);
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124enum ExecutionUpdateRoute {
125 Tracked(ClientOrderId, OrderContext),
126 External,
127 Suppressed,
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131enum LinkedChildResolution {
132 Bound(ClientOrderId),
133 Held,
134 External,
135}
136
137#[derive(Debug)]
138struct PendingLinkedChild {
139 parent_venue_order_id: VenueOrderId,
140 candidate_client_order_ids: Vec<ClientOrderId>,
141 message: OKXOrderMsg,
142}
143
144#[derive(Debug)]
145struct DedupCache<K>
146where
147 K: Clone + Debug + Eq + Hash,
148{
149 inner: Mutex<FifoCache<K, DEDUP_CAPACITY>>,
150}
151
152impl<K> DedupCache<K>
153where
154 K: Clone + Debug + Eq + Hash,
155{
156 fn new() -> Self {
157 Self {
158 inner: Mutex::new(FifoCache::new()),
159 }
160 }
161
162 fn contains(&self, key: &K) -> bool {
163 self.inner.lock().contains(key)
164 }
165
166 fn insert(&self, key: K) -> bool {
167 self.inner.lock().insert(key)
168 }
169
170 fn remove(&self, key: &K) {
171 self.inner.lock().remove(key);
172 }
173}
174
175#[derive(Debug)]
178pub struct WsDispatchState {
179 pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
180 order_contexts: DashMap<ClientOrderId, OrderContext>,
181 pub(crate) algo_order_states: DashMap<ClientOrderId, AlgoOrderState>,
182 pub(crate) pending_orders: Arc<DashMap<String, PendingOrderInfo>>,
183 pub(crate) pending_cancels: Arc<DashMap<String, PendingOrderInfo>>,
184 pub(crate) pending_amends: Arc<DashMap<String, PendingOrderInfo>>,
185 accepted_venue_order_ids: Mutex<FifoCacheMap<ClientOrderId, VenueOrderId, DEDUP_CAPACITY>>,
186 triggered_orders: DedupCache<ClientOrderId>,
187 filled_orders: DedupCache<ClientOrderId>,
188 terminal_orders: DedupCache<ClientOrderId>,
189 emitted_trades: DedupCache<TradeId>,
190 post_only_rejections: DedupCache<Ustr>,
191 lifecycle_bindings: Mutex<OrderLifecycleBindings>,
192 pending_linked_children: Mutex<VecDeque<PendingLinkedChild>>,
193 linked_child_notify: tokio::sync::Notify,
194}
195
196#[derive(Clone, Copy, Debug)]
197pub(crate) struct AlgoOrderState {
198 pub(crate) order_type: OKXAlgoOrderType,
199 pub(crate) ts_last: UnixNanos,
200}
201
202impl Default for WsDispatchState {
203 fn default() -> Self {
204 Self {
205 order_identities: DashMap::new(),
206 order_contexts: DashMap::new(),
207 algo_order_states: DashMap::new(),
208 pending_orders: Arc::new(DashMap::new()),
209 pending_cancels: Arc::new(DashMap::new()),
210 pending_amends: Arc::new(DashMap::new()),
211 accepted_venue_order_ids: Mutex::new(FifoCacheMap::new()),
212 triggered_orders: DedupCache::new(),
213 filled_orders: DedupCache::new(),
214 terminal_orders: DedupCache::new(),
215 emitted_trades: DedupCache::new(),
216 post_only_rejections: DedupCache::new(),
217 lifecycle_bindings: Mutex::new(OrderLifecycleBindings::default()),
218 pending_linked_children: Mutex::new(VecDeque::new()),
219 linked_child_notify: tokio::sync::Notify::new(),
220 }
221 }
222}
223
224impl WsDispatchState {
225 pub(crate) fn with_pending_maps(
228 pending_orders: Arc<DashMap<String, PendingOrderInfo>>,
229 pending_cancels: Arc<DashMap<String, PendingOrderInfo>>,
230 pending_amends: Arc<DashMap<String, PendingOrderInfo>>,
231 ) -> Self {
232 Self {
233 pending_orders,
234 pending_cancels,
235 pending_amends,
236 ..Default::default()
237 }
238 }
239
240 pub(crate) fn track_order_context(&self, context: OrderContext) {
241 self.order_contexts
242 .insert(context.identity.client_order_id, context);
243 }
244
245 pub(crate) fn bind_algo_parent(
246 &self,
247 client_order_id: ClientOrderId,
248 venue_order_id: VenueOrderId,
249 ) {
250 let mut bindings = self.lifecycle_bindings.lock();
251 let binding = bindings.venue_by_client.get(&client_order_id).copied();
252 if binding.is_some_and(|binding| binding.parent != venue_order_id) {
253 log::error!(
254 "Ignoring conflicting algo parent binding for {client_order_id}: expected={:?} received={venue_order_id}",
255 binding.map(|binding| binding.parent),
256 );
257 return;
258 }
259
260 bindings
261 .client_by_parent
262 .insert(venue_order_id, client_order_id);
263 bindings.venue_by_client.insert(
264 client_order_id,
265 binding.unwrap_or(OrderVenueBinding {
266 parent: venue_order_id,
267 child: None,
268 }),
269 );
270 self.linked_child_notify.notify_one();
271 }
272
273 pub(crate) fn order_venue_binding(
274 &self,
275 client_order_id: ClientOrderId,
276 ) -> Option<(VenueOrderId, bool)> {
277 let bindings = self.lifecycle_bindings.lock();
278 bindings
279 .venue_by_client
280 .get(&client_order_id)
281 .map(|binding| {
282 (
283 binding.child.unwrap_or(binding.parent),
284 binding.child.is_some(),
285 )
286 })
287 }
288
289 pub(crate) fn order_identity(&self, client_order_id: ClientOrderId) -> Option<OrderIdentity> {
290 self.order_context(client_order_id)
291 .map(|context| context.identity)
292 .or_else(|| {
293 self.order_identities
294 .get(&client_order_id)
295 .map(|entry| *entry)
296 })
297 }
298
299 pub(crate) fn order_context(&self, client_order_id: ClientOrderId) -> Option<OrderContext> {
300 self.order_contexts
301 .get(&client_order_id)
302 .map(|entry| *entry)
303 }
304
305 pub(crate) fn remove_order_tracking(&self, client_order_id: ClientOrderId) {
306 let pending = self.pending_linked_children.lock();
307 let removed_context = self.order_contexts.remove(&client_order_id).is_some();
308 self.order_identities.remove(&client_order_id);
309 self.algo_order_states.remove(&client_order_id);
310 drop(pending);
311
312 if removed_context {
313 self.linked_child_notify.notify_one();
314 }
315 }
316
317 pub(crate) fn resolve_algo_submit_failure(
318 &self,
319 client_order_id: ClientOrderId,
320 failure: &CommandFailure,
321 ) {
322 let bindings = self.lifecycle_bindings.lock();
323 if matches!(failure, CommandFailure::Ambiguous(_))
324 && bindings.venue_by_client.contains_key(&client_order_id)
325 {
326 return;
327 }
328
329 self.remove_order_tracking(client_order_id);
330 }
331
332 pub(crate) async fn wait_for_linked_child_route(&self) {
333 self.linked_child_notify.notified().await;
334 }
335
336 fn resolve_or_hold_linked_child(
337 &self,
338 parent_venue_order_id: VenueOrderId,
339 instrument_id: InstrumentId,
340 message: &OKXOrderMsg,
341 ) -> LinkedChildResolution {
342 let candidate_client_order_ids = self
343 .order_contexts
344 .iter()
345 .filter_map(|entry| {
346 (entry.identity.instrument_id == instrument_id)
347 .then_some(entry.identity.client_order_id)
348 })
349 .collect::<Vec<_>>();
350 let bindings = self.lifecycle_bindings.lock();
351 if let Some(client_order_id) = bindings.client_order_id(&parent_venue_order_id) {
352 return LinkedChildResolution::Bound(client_order_id);
353 }
354
355 let mut pending = self.pending_linked_children.lock();
356 let candidate_client_order_ids = candidate_client_order_ids
357 .iter()
358 .filter(|client_order_id| {
359 self.order_contexts.contains_key(client_order_id)
360 && !bindings.venue_by_client.contains_key(client_order_id)
361 })
362 .copied()
363 .collect::<Vec<_>>();
364
365 if candidate_client_order_ids.is_empty() {
366 return LinkedChildResolution::External;
367 }
368
369 pending.push_back(PendingLinkedChild {
370 parent_venue_order_id,
371 candidate_client_order_ids,
372 message: message.clone(),
373 });
374 LinkedChildResolution::Held
375 }
376
377 fn take_routable_linked_children(&self) -> Vec<OKXOrderMsg> {
378 if self.pending_linked_children.lock().is_empty() {
379 return Vec::new();
380 }
381
382 let bindings = self.lifecycle_bindings.lock();
383 let mut pending = self.pending_linked_children.lock();
384 let mut held = VecDeque::with_capacity(pending.len());
385 let mut routable = Vec::new();
386
387 while let Some(child) = pending.pop_front() {
388 let is_bound = bindings
389 .client_order_id(&child.parent_venue_order_id)
390 .is_some();
391 let is_pending = child
392 .candidate_client_order_ids
393 .iter()
394 .any(|client_order_id| {
395 self.order_contexts.contains_key(client_order_id)
396 && !bindings.venue_by_client.contains_key(client_order_id)
397 });
398
399 if is_bound || !is_pending {
400 routable.push(child.message);
401 } else {
402 held.push_back(child);
403 }
404 }
405
406 *pending = held;
407 routable
408 }
409}
410
411impl WsDispatchState {
412 #[must_use]
414 pub fn contains_accepted(&self, cid: &ClientOrderId) -> bool {
415 self.accepted_venue_order_ids.lock().contains_key(cid)
416 }
417
418 pub fn insert_accepted(&self, cid: ClientOrderId, venue_order_id: VenueOrderId) {
420 self.accepted_venue_order_ids
421 .lock()
422 .insert(cid, venue_order_id);
423 }
424
425 fn accepted_venue_order_id(&self, cid: &ClientOrderId) -> Option<VenueOrderId> {
426 self.accepted_venue_order_ids.lock().get(cid).copied()
427 }
428
429 #[must_use]
431 pub fn contains_triggered(&self, cid: &ClientOrderId) -> bool {
432 self.triggered_orders.contains(cid)
433 }
434
435 pub fn insert_triggered(&self, cid: ClientOrderId) {
437 let _ = self.triggered_orders.insert(cid);
438 }
439
440 #[must_use]
442 pub fn contains_filled(&self, cid: &ClientOrderId) -> bool {
443 self.filled_orders.contains(cid)
444 }
445
446 pub fn insert_filled(&self, cid: ClientOrderId) {
448 let _ = self.filled_orders.insert(cid);
449 }
450
451 #[must_use]
453 pub fn contains_terminal(&self, cid: &ClientOrderId) -> bool {
454 self.terminal_orders.contains(cid)
455 }
456
457 pub fn insert_terminal(&self, cid: ClientOrderId) {
459 let _ = self.terminal_orders.insert(cid);
460 }
461
462 pub fn check_and_insert_trade(&self, trade_id: TradeId) -> bool {
465 !self.emitted_trades.insert(trade_id)
466 }
467
468 #[must_use]
469 pub fn contains_trade(&self, trade_id: &TradeId) -> bool {
470 self.emitted_trades.contains(trade_id)
471 }
472
473 fn remove_accepted(&self, cid: &ClientOrderId) {
474 self.accepted_venue_order_ids.lock().remove(cid);
475 }
476
477 fn remove_triggered(&self, cid: &ClientOrderId) {
478 self.triggered_orders.remove(cid);
479 }
480
481 fn remove_filled(&self, cid: &ClientOrderId) {
482 self.filled_orders.remove(cid);
483 }
484
485 fn insert_post_only_rejection(&self, order_id: Ustr) {
486 let _ = self.post_only_rejections.insert(order_id);
487 }
488
489 fn contains_post_only_rejection(&self, order_id: &Ustr) -> bool {
490 self.post_only_rejections.contains(order_id)
491 }
492}
493
494#[expect(clippy::too_many_arguments)]
501pub fn dispatch_ws_message(
502 message: OKXWsMessage,
503 emitter: &ExecutionEventEmitter,
504 state: &WsDispatchState,
505 account_id: AccountId,
506 account_type: AccountType,
507 instruments: &AtomicMap<Ustr, InstrumentAny>,
508 fee_cache: &mut FeeCache,
509 filled_qty_cache: &mut FilledQtyCache,
510 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
511 clock: &AtomicTime,
512) {
513 let guard = instruments.load();
514 let instruments: &AHashMap<Ustr, InstrumentAny> = &guard;
515
516 match message {
517 OKXWsMessage::Orders(order_msgs) => {
518 let ts_init = clock.get_time_ns();
519 let pending_order_msgs = state.take_routable_linked_children();
520 if !pending_order_msgs.is_empty() {
521 dispatch_order_messages(
522 &pending_order_msgs,
523 emitter,
524 state,
525 account_id,
526 instruments,
527 fee_cache,
528 filled_qty_cache,
529 order_state_cache,
530 ts_init,
531 );
532 }
533 dispatch_order_messages(
534 &order_msgs,
535 emitter,
536 state,
537 account_id,
538 instruments,
539 fee_cache,
540 filled_qty_cache,
541 order_state_cache,
542 ts_init,
543 );
544 }
545 OKXWsMessage::SpreadOrders(order_msgs) => {
546 let ts_init = clock.get_time_ns();
547 dispatch_spread_order_messages(
548 &order_msgs,
549 emitter,
550 state,
551 account_id,
552 instruments,
553 filled_qty_cache,
554 order_state_cache,
555 ts_init,
556 );
557 }
558 OKXWsMessage::AlgoOrders(algo_msgs) => {
559 let ts_init = clock.get_time_ns();
560 for msg in &algo_msgs {
561 dispatch_algo_order_message(msg, emitter, state, account_id, instruments, ts_init);
562 }
563 }
564 OKXWsMessage::Account(data) => {
565 let ts_init = clock.get_time_ns();
566
567 match serde_json::from_value::<Vec<OKXAccount>>(data) {
568 Ok(accounts) => {
569 for account in &accounts {
570 match crate::common::parse::parse_account_state(
571 account,
572 account_id,
573 account_type,
574 ts_init,
575 ) {
576 Ok(account_state) => emitter.send_account_state(account_state),
577 Err(e) => log::error!("Failed to parse account state: {e}"),
578 }
579 }
580 }
581 Err(e) => log::error!("Failed to deserialize account data: {e}"),
582 }
583 }
584 OKXWsMessage::Positions(data) => {
585 let ts_init = clock.get_time_ns();
586
587 match serde_json::from_value::<Vec<OKXPosition>>(data) {
588 Ok(positions) => {
589 for position in positions {
590 let Some(instrument) = instruments.get(&position.inst_id) else {
591 log::warn!("No cached instrument for position: {}", position.inst_id);
592 continue;
593 };
594 let instrument_id = instrument.id();
595 let size_precision = instrument.size_precision();
596
597 match crate::common::parse::parse_position_status_report(
598 &position,
599 account_id,
600 instrument_id,
601 size_precision,
602 ts_init,
603 ) {
604 Ok(report) => emitter.send_position_report(report),
605 Err(e) => log::error!("Failed to parse position report: {e}"),
606 }
607 }
608 }
609 Err(e) => log::error!("Failed to deserialize positions data: {e}"),
610 }
611 }
612 OKXWsMessage::OrderResponse {
613 id,
614 op,
615 code,
616 msg,
617 data,
618 } => {
619 let ts_init = clock.get_time_ns();
620
621 for item in &data {
622 let s_code = item
623 .get(OKX_FIELD_SCODE)
624 .and_then(|v| v.as_str())
625 .unwrap_or("");
626 let s_msg = item
627 .get(OKX_FIELD_SMSG)
628 .and_then(|v| v.as_str())
629 .unwrap_or("");
630 let sub_code = item
631 .get(OKX_FIELD_SUBCODE)
632 .and_then(|v| v.as_str())
633 .unwrap_or("");
634 let reason = format_order_response_reason(s_code, s_msg, sub_code);
635 let cl_ord_id = item
636 .get(OKX_FIELD_CLORDID)
637 .and_then(|v| v.as_str())
638 .unwrap_or("");
639
640 if s_code == OKX_SUCCESS_CODE {
641 log::debug!("Order response ok: op={op:?} cl_ord_id={cl_ord_id}");
642 match op {
643 OKXWsOperation::Order
644 | OKXWsOperation::BatchOrders
645 | OKXWsOperation::OrderAlgo => {
646 state.pending_orders.remove(cl_ord_id);
647 }
648 OKXWsOperation::CancelOrder
649 | OKXWsOperation::BatchCancelOrders
650 | OKXWsOperation::MassCancel
651 | OKXWsOperation::CancelAlgos => {
652 state.pending_cancels.remove(cl_ord_id);
653 }
654 OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
655 state.pending_amends.remove(cl_ord_id);
656 }
657 _ => {}
658 }
659 continue;
660 }
661
662 let Some(client_order_id) = parse_client_order_id(cl_ord_id) else {
663 log::warn!(
664 "Order response error without client_order_id: \
665 op={op:?} s_code={s_code} s_msg={s_msg}"
666 );
667 continue;
668 };
669
670 let Some(ident) = state.order_identity(client_order_id) else {
671 log::warn!(
672 "Order response error for untracked order: \
673 op={op:?} cl_ord_id={cl_ord_id} s_code={s_code} s_msg={s_msg}"
674 );
675 continue;
676 };
677
678 let venue_order_id = item
679 .get("ordId")
680 .and_then(|v| v.as_str())
681 .filter(|s| !s.is_empty())
682 .map(VenueOrderId::new);
683
684 match classify_okx_venue_code(s_code, reason.clone()) {
685 CommandFailure::Ambiguous(reason) => {
686 log::warn!(
687 "Ambiguous order response for {client_order_id}, awaiting reconciliation: \
688 op={op:?} s_code={s_code} {reason}"
689 );
690 continue;
691 }
692 CommandFailure::NotSent(_) => {
693 log::warn!(
694 "Unexpected NotSent classification for venue order response: \
695 op={op:?} cl_ord_id={cl_ord_id} s_code={s_code}"
696 );
697 continue;
698 }
699 CommandFailure::VenueRejected(_) => {}
700 }
701
702 match op {
703 OKXWsOperation::Order | OKXWsOperation::BatchOrders => {
704 state.remove_order_tracking(client_order_id);
705 state.pending_orders.remove(cl_ord_id);
706 emitter.emit_order_rejected_event(
707 ident.strategy_id,
708 ident.instrument_id,
709 client_order_id,
710 &reason,
711 ts_init,
712 false,
713 );
714 }
715 OKXWsOperation::CancelOrder
716 | OKXWsOperation::BatchCancelOrders
717 | OKXWsOperation::MassCancel => {
718 state.pending_cancels.remove(cl_ord_id);
719 emitter.emit_order_cancel_rejected_event(
720 ident.strategy_id,
721 ident.instrument_id,
722 client_order_id,
723 venue_order_id,
724 &reason,
725 ts_init,
726 );
727 }
728 OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
729 state.pending_amends.remove(cl_ord_id);
730 emitter.emit_order_modify_rejected_event(
731 ident.strategy_id,
732 ident.instrument_id,
733 client_order_id,
734 venue_order_id,
735 &reason,
736 ts_init,
737 );
738 }
739 _ => {
740 log::warn!(
741 "Order response error for unhandled op: \
742 op={op:?} cl_ord_id={cl_ord_id} s_code={s_code} s_msg={s_msg}"
743 );
744 }
745 }
746 }
747
748 if code != "0" && data.is_empty() {
749 log::warn!(
750 "Order response error (no data): id={id:?} op={op:?} code={code} msg={msg}"
751 );
752 }
753 }
754 OKXWsMessage::SendFailed {
755 request_id,
756 client_order_ids,
757 op,
758 error,
759 } => {
760 let failure = classify_okx_ws_failure(&error);
761 let is_ambiguous = matches!(failure, CommandFailure::Ambiguous(_));
762 log::warn!(
763 "WebSocket send failed without structured venue response: \
764 request_id={request_id}, client_order_ids={client_order_ids:?}, \
765 op={op:?}, {failure:?}"
766 );
767
768 for client_order_id in client_order_ids {
769 let key = client_order_id.as_str();
770
771 match op {
772 Some(
773 OKXWsOperation::Order
774 | OKXWsOperation::BatchOrders
775 | OKXWsOperation::OrderAlgo,
776 ) => {
777 if !is_ambiguous {
778 state.pending_orders.remove(key);
779 }
780 emit_send_failed_submit(&failure, state, emitter, clock, client_order_id);
781 }
782 Some(
783 OKXWsOperation::CancelOrder
784 | OKXWsOperation::BatchCancelOrders
785 | OKXWsOperation::MassCancel
786 | OKXWsOperation::CancelAlgos,
787 ) => {
788 if !is_ambiguous {
789 state.pending_cancels.remove(key);
790 }
791 }
792 Some(OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders) => {
793 if !is_ambiguous {
794 state.pending_amends.remove(key);
795 }
796 emit_send_failed_modify(&failure, state, emitter, clock, client_order_id);
797 }
798 _ => {}
799 }
800 }
801 }
802 OKXWsMessage::ChannelData { channel, .. } => {
803 log::debug!("Ignoring data channel message on execution client: {channel:?}");
804 }
805 OKXWsMessage::SubscriptionFailed {
806 channel,
807 inst_id,
808 code,
809 msg,
810 } => {
811 log::error!(
812 "OKX rejected {channel:?} subscription for {inst_id:?} \
813 (code={code}, msg={msg}); execution updates for it will not flow"
814 );
815 }
816 OKXWsMessage::LiquidationWarnings(warnings) => {
817 for warning in warnings {
818 log::warn!(
819 "Liquidation warning: inst_id={}, pos_side={:?}, pos={}, mgn_ratio={}, mark_px={}, mgn_mode={:?}",
820 warning.inst_id,
821 warning.pos_side,
822 warning.pos,
823 warning.mgn_ratio,
824 warning.mark_px,
825 warning.mgn_mode,
826 );
827 }
828 }
829 OKXWsMessage::BookData { .. }
830 | OKXWsMessage::RpiBookData { .. }
831 | OKXWsMessage::Instruments(_) => {
832 log::debug!("Ignoring data message on execution client");
833 }
834 OKXWsMessage::Error(_) => {}
835 OKXWsMessage::Reconnected => {
836 log::info!("Websocket reconnected");
837 }
838 OKXWsMessage::Authenticated => {
839 log::debug!("Websocket authenticated");
840 }
841 }
842}
843
844fn route_algo_order_message(
845 msg: &OKXAlgoOrderMsg,
846 state: &WsDispatchState,
847) -> ExecutionUpdateRoute {
848 if matches!(
849 msg.ord_type,
850 OKXAlgoOrderType::Iceberg
851 | OKXAlgoOrderType::SmartIceberg
852 | OKXAlgoOrderType::Twap
853 | OKXAlgoOrderType::Chase
854 | OKXAlgoOrderType::Other
855 ) || msg.state == OKXAlgoOrderStatus::Unknown
856 {
857 return ExecutionUpdateRoute::Suppressed;
858 }
859
860 let direct_client_order_id = parse_client_order_id(&msg.algo_cl_ord_id)
861 .or_else(|| parse_client_order_id(&msg.cl_ord_id));
862
863 if let Some(client_order_id) = direct_client_order_id {
864 if state.contains_terminal(&client_order_id) {
865 return ExecutionUpdateRoute::Suppressed;
866 }
867
868 if let Some(context) = state
869 .order_contexts
870 .get(&client_order_id)
871 .map(|entry| *entry)
872 {
873 return ExecutionUpdateRoute::Tracked(client_order_id, context);
874 }
875
876 if state.order_identities.contains_key(&client_order_id) {
877 return ExecutionUpdateRoute::Suppressed;
878 }
879 }
880
881 let parent_venue_order_id = VenueOrderId::new(msg.algo_id.as_str());
882 let client_order_id = {
883 let bindings = state.lifecycle_bindings.lock();
884 bindings.client_order_id(&parent_venue_order_id)
885 };
886
887 let Some(client_order_id) = client_order_id else {
888 return ExecutionUpdateRoute::External;
889 };
890
891 if state.contains_terminal(&client_order_id) {
892 return ExecutionUpdateRoute::Suppressed;
893 }
894
895 state
896 .order_contexts
897 .get(&client_order_id)
898 .map_or(ExecutionUpdateRoute::Suppressed, |entry| {
899 ExecutionUpdateRoute::Tracked(client_order_id, *entry)
900 })
901}
902
903fn dispatch_algo_order_message(
904 msg: &OKXAlgoOrderMsg,
905 emitter: &ExecutionEventEmitter,
906 state: &WsDispatchState,
907 account_id: AccountId,
908 instruments: &AHashMap<Ustr, InstrumentAny>,
909 ts_init: UnixNanos,
910) {
911 let route = route_algo_order_message(msg, state);
912
913 match route {
914 ExecutionUpdateRoute::External => {
915 match parse_algo_order_msg(msg, account_id, instruments, ts_init) {
916 Ok(Some(report)) => dispatch_execution_reports(vec![report], emitter, state),
917 Ok(None) => {}
918 Err(e) => log::error!("Failed to parse external algo order message: {e}"),
919 }
920 }
921 ExecutionUpdateRoute::Suppressed => {
922 log::debug!(
923 "Suppressing algo order update: algo_id={} state={:?}",
924 msg.algo_id,
925 msg.state,
926 );
927 }
928 ExecutionUpdateRoute::Tracked(client_order_id, context) => {
929 let Some(instrument) = instruments.get(&msg.inst_id) else {
930 log::warn!(
931 "No instrument for {}, skipping algo order message",
932 msg.inst_id
933 );
934 return;
935 };
936 dispatch_tracked_algo_order_message(
937 msg,
938 client_order_id,
939 context,
940 instrument,
941 emitter,
942 state,
943 account_id,
944 ts_init,
945 );
946 }
947 }
948}
949
950#[expect(
951 clippy::too_many_arguments,
952 reason = "tracked routing requires the resolved context, venue state, and event timestamps"
953)]
954fn dispatch_tracked_algo_order_message(
955 msg: &OKXAlgoOrderMsg,
956 client_order_id: ClientOrderId,
957 mut context: OrderContext,
958 instrument: &InstrumentAny,
959 emitter: &ExecutionEventEmitter,
960 state: &WsDispatchState,
961 account_id: AccountId,
962 ts_init: UnixNanos,
963) {
964 let parent_venue_order_id = VenueOrderId::new(msg.algo_id.as_str());
965 let ts_event = parse_millisecond_timestamp(msg.u_time);
966 let mut bindings = state.lifecycle_bindings.lock();
967 let mut is_terminal = false;
968 let mut binding = bindings
969 .venue_by_client
970 .get(&client_order_id)
971 .copied()
972 .unwrap_or(OrderVenueBinding {
973 parent: parent_venue_order_id,
974 child: None,
975 });
976
977 if binding.parent != parent_venue_order_id {
978 log::error!(
979 "Suppressing conflicting algo parent binding for {client_order_id}: expected={} received={parent_venue_order_id}",
980 binding.parent,
981 );
982 return;
983 }
984
985 if binding.child.is_none() {
986 let Some(refreshed) = dispatch_pending_algo_order(
987 msg, context, instrument, account_id, ts_event, ts_init, emitter, state,
988 ) else {
989 return;
990 };
991
992 context = refreshed;
993 }
994
995 bindings
996 .client_by_parent
997 .insert(parent_venue_order_id, client_order_id);
998
999 match msg.state {
1000 OKXAlgoOrderStatus::Live | OKXAlgoOrderStatus::Pause => {}
1001 OKXAlgoOrderStatus::Effective
1002 | OKXAlgoOrderStatus::OrderPlaced
1003 | OKXAlgoOrderStatus::PartiallyEffective
1004 | OKXAlgoOrderStatus::Filled
1005 | OKXAlgoOrderStatus::PartiallyFailed => {
1006 let child_venue_order_id = algo_child_venue_order_id(msg);
1007 if let Some(child_venue_order_id) = child_venue_order_id {
1008 bind_algo_child_and_emit_transition(
1009 client_order_id,
1010 parent_venue_order_id,
1011 child_venue_order_id,
1012 &mut binding,
1013 context,
1014 account_id,
1015 ts_event,
1016 ts_init,
1017 emitter,
1018 state,
1019 );
1020 } else {
1021 ensure_accepted_emitted(
1022 client_order_id,
1023 account_id,
1024 parent_venue_order_id,
1025 &context.identity,
1026 emitter,
1027 state,
1028 ts_event,
1029 ts_init,
1030 );
1031 }
1032
1033 if matches!(
1034 msg.state,
1035 OKXAlgoOrderStatus::Filled | OKXAlgoOrderStatus::PartiallyFailed
1036 ) {
1037 log::debug!(
1038 "Deferring tracked algo {:?} update for {client_order_id} to regular child execution updates",
1039 msg.state,
1040 );
1041 }
1042 }
1043 OKXAlgoOrderStatus::Canceled => {
1044 if binding.child.is_some() {
1045 log::debug!(
1046 "Suppressing stale canceled algo parent for triggered order {client_order_id}"
1047 );
1048 } else {
1049 ensure_accepted_emitted(
1050 client_order_id,
1051 account_id,
1052 parent_venue_order_id,
1053 &context.identity,
1054 emitter,
1055 state,
1056 ts_event,
1057 ts_init,
1058 );
1059 let canceled = OrderCanceled::new(
1060 emitter.trader_id(),
1061 context.identity.strategy_id,
1062 context.identity.instrument_id,
1063 client_order_id,
1064 UUID4::new(),
1065 ts_event,
1066 ts_init,
1067 false,
1068 Some(parent_venue_order_id),
1069 Some(account_id),
1070 None,
1071 );
1072 state.insert_terminal(client_order_id);
1073 state.remove_accepted(&client_order_id);
1074 state.remove_order_tracking(client_order_id);
1075 is_terminal = true;
1076 emitter.send_order_event(OrderEventAny::Canceled(canceled));
1077 }
1078 }
1079 OKXAlgoOrderStatus::OrderFailed => {
1080 if binding.child.is_some() {
1081 log::debug!(
1082 "Suppressing stale failed algo parent for triggered order {client_order_id}"
1083 );
1084 } else {
1085 let reason = if msg.fail_code.is_empty() {
1086 "OKX algo order failed".to_string()
1087 } else {
1088 format_order_response_reason(&msg.fail_code, "", "")
1089 };
1090 let rejected = OrderRejected::new(
1091 emitter.trader_id(),
1092 context.identity.strategy_id,
1093 context.identity.instrument_id,
1094 client_order_id,
1095 account_id,
1096 Ustr::from(reason.as_str()),
1097 UUID4::new(),
1098 ts_event,
1099 ts_init,
1100 false,
1101 false,
1102 );
1103 state.insert_terminal(client_order_id);
1104 state.remove_accepted(&client_order_id);
1105 state.remove_order_tracking(client_order_id);
1106 is_terminal = true;
1107 emitter.send_order_event(OrderEventAny::Rejected(rejected));
1108 }
1109 }
1110 OKXAlgoOrderStatus::Unknown => {}
1111 }
1112
1113 if is_terminal {
1114 bindings.finish(client_order_id, binding);
1115 } else {
1116 bindings.venue_by_client.insert(client_order_id, binding);
1117 }
1118
1119 drop(bindings);
1120 state.linked_child_notify.notify_one();
1121}
1122
1123fn algo_child_venue_order_id(msg: &OKXAlgoOrderMsg) -> Option<VenueOrderId> {
1124 if !msg.ord_id.is_empty() {
1125 return Some(VenueOrderId::new(msg.ord_id.as_str()));
1126 }
1127
1128 match msg.ord_id_list.as_slice() {
1129 [order_id] if !order_id.is_empty() => Some(VenueOrderId::new(order_id.as_str())),
1130 [] => None,
1131 order_ids => {
1132 log::warn!(
1133 "Cannot bind algo order {} to {} triggered child IDs",
1134 msg.algo_id,
1135 order_ids.len(),
1136 );
1137 None
1138 }
1139 }
1140}
1141
1142#[expect(clippy::too_many_arguments)]
1143fn dispatch_pending_algo_order(
1144 msg: &OKXAlgoOrderMsg,
1145 mut context: OrderContext,
1146 instrument: &InstrumentAny,
1147 account_id: AccountId,
1148 ts_event: UnixNanos,
1149 ts_init: UnixNanos,
1150 emitter: &ExecutionEventEmitter,
1151 state: &WsDispatchState,
1152) -> Option<OrderContext> {
1153 if state
1154 .algo_order_states
1155 .get(&context.identity.client_order_id)
1156 .is_some_and(|previous| ts_event < previous.ts_last)
1157 {
1158 return None;
1159 }
1160
1161 let client_order_id = context.identity.client_order_id;
1162 let pending = matches!(
1163 msg.state,
1164 OKXAlgoOrderStatus::Live | OKXAlgoOrderStatus::Pause
1165 );
1166 let parent_venue_order_id = VenueOrderId::new(msg.algo_id.as_str());
1167
1168 if pending {
1169 ensure_accepted_emitted(
1170 client_order_id,
1171 account_id,
1172 parent_venue_order_id,
1173 &context.identity,
1174 emitter,
1175 state,
1176 ts_event,
1177 ts_init,
1178 );
1179 }
1180
1181 let previous = context;
1182
1183 match parse_algo_order_status_report(msg, instrument, account_id, ts_init) {
1184 Ok(report) => {
1185 if !msg.sz.is_empty() {
1186 context.quantity = report.quantity;
1187 }
1188
1189 context.price = report.price;
1190 context.trigger_price = report.trigger_price;
1191 context.trigger_type = report.trigger_type;
1192 }
1193 Err(e) => {
1194 log::error!(
1195 "Failed to refresh tracked algo order context for {}: {e}",
1196 context.identity.client_order_id,
1197 );
1198 return Some(context);
1199 }
1200 }
1201
1202 if !msg.actual_sz.is_empty() && msg.actual_sz != "0" {
1203 match parse_quantity(msg.actual_sz.as_str(), instrument.size_precision()) {
1204 Ok(quantity) => context.quantity = quantity,
1205 Err(e) => log::error!(
1206 "Failed to refresh tracked algo actual quantity for {}: {e}",
1207 context.identity.client_order_id,
1208 ),
1209 }
1210 }
1211
1212 state.track_order_context(context);
1213 state.algo_order_states.insert(
1214 client_order_id,
1215 AlgoOrderState {
1216 order_type: msg.ord_type,
1217 ts_last: ts_event,
1218 },
1219 );
1220
1221 if pending
1222 && msg.ord_type != OKXAlgoOrderType::MoveOrderStop
1223 && (previous.quantity != context.quantity
1224 || previous.price != context.price
1225 || previous.trigger_price != context.trigger_price)
1226 {
1227 emit_order_update(
1228 client_order_id,
1229 parent_venue_order_id,
1230 context,
1231 account_id,
1232 ts_event,
1233 ts_init,
1234 emitter,
1235 );
1236 }
1237
1238 Some(context)
1239}
1240
1241fn refresh_regular_child_context(
1242 msg: &OKXOrderMsg,
1243 mut context: OrderContext,
1244 instrument: &InstrumentAny,
1245) -> OrderContext {
1246 match parse_quantity(&msg.sz, instrument.size_precision()) {
1247 Ok(quantity) => context.quantity = quantity,
1248 Err(e) => log::error!(
1249 "Failed to refresh tracked child quantity for {}: {e}",
1250 context.identity.client_order_id,
1251 ),
1252 }
1253
1254 context.price = if is_market_price(&msg.px) {
1255 None
1256 } else {
1257 match parse_price(&msg.px, instrument.price_precision()) {
1258 Ok(price) => Some(price),
1259 Err(e) => {
1260 log::error!(
1261 "Failed to refresh tracked child price for {}: {e}",
1262 context.identity.client_order_id,
1263 );
1264 context.price
1265 }
1266 }
1267 };
1268 context
1269}
1270
1271#[expect(clippy::too_many_arguments)]
1272fn bind_algo_child_and_emit_transition(
1273 client_order_id: ClientOrderId,
1274 parent_venue_order_id: VenueOrderId,
1275 child_venue_order_id: VenueOrderId,
1276 binding: &mut OrderVenueBinding,
1277 context: OrderContext,
1278 account_id: AccountId,
1279 ts_event: UnixNanos,
1280 ts_init: UnixNanos,
1281 emitter: &ExecutionEventEmitter,
1282 state: &WsDispatchState,
1283) {
1284 if let Some(bound_child) = binding.child {
1285 if bound_child != child_venue_order_id {
1286 log::error!(
1287 "Suppressing conflicting algo child binding for {client_order_id}: expected={bound_child} received={child_venue_order_id}"
1288 );
1289 }
1290 return;
1291 }
1292
1293 binding.child = Some(child_venue_order_id);
1294 state.track_order_context(context);
1295 ensure_accepted_emitted(
1296 client_order_id,
1297 account_id,
1298 parent_venue_order_id,
1299 &context.identity,
1300 emitter,
1301 state,
1302 ts_event,
1303 ts_init,
1304 );
1305
1306 if state.accepted_venue_order_id(&client_order_id) != Some(child_venue_order_id) {
1307 state.insert_accepted(client_order_id, child_venue_order_id);
1308 emit_order_update(
1309 client_order_id,
1310 child_venue_order_id,
1311 context,
1312 account_id,
1313 ts_event,
1314 ts_init,
1315 emitter,
1316 );
1317 }
1318
1319 if !state.contains_triggered(&client_order_id) {
1320 state.insert_triggered(client_order_id);
1321
1322 if TRIGGERABLE_ORDER_TYPES.contains(&context.identity.order_type) {
1323 let triggered = OrderTriggered::new(
1324 emitter.trader_id(),
1325 context.identity.strategy_id,
1326 context.identity.instrument_id,
1327 client_order_id,
1328 UUID4::new(),
1329 ts_event,
1330 ts_init,
1331 false,
1332 Some(child_venue_order_id),
1333 Some(account_id),
1334 );
1335 emitter.send_order_event(OrderEventAny::Triggered(triggered));
1336 }
1337 }
1338}
1339
1340#[expect(clippy::too_many_arguments)]
1341fn refresh_bound_child_and_emit_update(
1342 client_order_id: ClientOrderId,
1343 child_venue_order_id: VenueOrderId,
1344 context: OrderContext,
1345 account_id: AccountId,
1346 ts_event: UnixNanos,
1347 ts_init: UnixNanos,
1348 emitter: &ExecutionEventEmitter,
1349 state: &WsDispatchState,
1350 emit_update: bool,
1351) {
1352 let terms_changed = state
1353 .order_contexts
1354 .get(&client_order_id)
1355 .is_some_and(|previous| {
1356 previous.quantity != context.quantity
1357 || previous.price != context.price
1358 || previous.trigger_price != context.trigger_price
1359 });
1360 state.track_order_context(context);
1361
1362 if !terms_changed || !emit_update {
1363 return;
1364 }
1365
1366 state.insert_accepted(client_order_id, child_venue_order_id);
1367 emit_order_update(
1368 client_order_id,
1369 child_venue_order_id,
1370 context,
1371 account_id,
1372 ts_event,
1373 ts_init,
1374 emitter,
1375 );
1376}
1377
1378fn emit_order_update(
1379 client_order_id: ClientOrderId,
1380 child_venue_order_id: VenueOrderId,
1381 context: OrderContext,
1382 account_id: AccountId,
1383 ts_event: UnixNanos,
1384 ts_init: UnixNanos,
1385 emitter: &ExecutionEventEmitter,
1386) {
1387 let updated = OrderUpdated::new(
1388 emitter.trader_id(),
1389 context.identity.strategy_id,
1390 context.identity.instrument_id,
1391 client_order_id,
1392 context.quantity,
1393 UUID4::new(),
1394 ts_event,
1395 ts_init,
1396 false,
1397 Some(child_venue_order_id),
1398 Some(account_id),
1399 context.price,
1400 context.trigger_price,
1401 None,
1402 false,
1403 );
1404 emitter.send_order_event(OrderEventAny::Updated(updated));
1405}
1406
1407#[expect(clippy::too_many_arguments)]
1410fn dispatch_order_messages(
1411 order_msgs: &[OKXOrderMsg],
1412 emitter: &ExecutionEventEmitter,
1413 state: &WsDispatchState,
1414 account_id: AccountId,
1415 instruments: &AHashMap<Ustr, InstrumentAny>,
1416 fee_cache: &mut FeeCache,
1417 filled_qty_cache: &mut FilledQtyCache,
1418 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
1419 ts_init: UnixNanos,
1420) {
1421 for msg in order_msgs {
1422 let Some(instrument) = instruments.get(&msg.inst_id) else {
1423 log::warn!("No instrument for {}, skipping order message", msg.inst_id);
1424 continue;
1425 };
1426
1427 let direct_client_order_id = parse_client_order_id(&msg.cl_ord_id);
1428 let parent_client_order_id = msg
1429 .algo_cl_ord_id
1430 .as_deref()
1431 .and_then(parse_client_order_id);
1432 let linked_parent_venue_order_id = msg
1433 .algo_id
1434 .as_deref()
1435 .filter(|value| !value.is_empty())
1436 .or_else(|| {
1437 msg.linked_algo_ord
1438 .as_ref()
1439 .map(|linked| linked.algo_id.as_str())
1440 .filter(|value| !value.is_empty())
1441 })
1442 .map(VenueOrderId::new);
1443
1444 let direct_resolution = [direct_client_order_id, parent_client_order_id]
1447 .into_iter()
1448 .flatten()
1449 .find_map(|client_order_id| {
1450 state
1451 .order_identity(client_order_id)
1452 .map(|identity| (client_order_id, Some(identity)))
1453 });
1454
1455 let bound_client_order_id = if direct_resolution.is_none()
1456 && let Some(parent_venue_order_id) = linked_parent_venue_order_id
1457 {
1458 match state.resolve_or_hold_linked_child(parent_venue_order_id, instrument.id(), msg) {
1459 LinkedChildResolution::Bound(client_order_id) => Some(client_order_id),
1460 LinkedChildResolution::Held => {
1461 log::debug!(
1462 "Holding linked child update during algo parent binding: ord_id={} parent_id={parent_venue_order_id}",
1463 msg.ord_id,
1464 );
1465 continue;
1466 }
1467 LinkedChildResolution::External => None,
1468 }
1469 } else {
1470 None
1471 };
1472
1473 let resolved = direct_resolution
1474 .or_else(|| {
1475 bound_client_order_id.and_then(|client_order_id| {
1476 state
1477 .order_identity(client_order_id)
1478 .map(|identity| (client_order_id, Some(identity)))
1479 })
1480 })
1481 .or_else(|| {
1482 bound_client_order_id
1483 .or(parent_client_order_id)
1484 .or(direct_client_order_id)
1485 .map(|client_order_id| (client_order_id, None))
1486 });
1487
1488 let Some((client_order_id, identity)) = resolved else {
1489 log::debug!(
1490 "Order without client or algo client order ID (ord_id={}), sending as report",
1491 msg.ord_id
1492 );
1493 dispatch_order_msg_as_report(
1494 msg,
1495 account_id,
1496 instruments,
1497 fee_cache,
1498 filled_qty_cache,
1499 emitter,
1500 state,
1501 ts_init,
1502 );
1503 continue;
1504 };
1505
1506 if let Some(ident) = identity {
1507 let context = state
1508 .order_contexts
1509 .get(&client_order_id)
1510 .map(|entry| refresh_regular_child_context(msg, *entry, instrument));
1511 let mut lifecycle_bindings = context.map(|_| state.lifecycle_bindings.lock());
1512
1513 if let (Some(context), Some(bindings)) = (context, lifecycle_bindings.as_mut()) {
1514 let parent_venue_order_id = linked_parent_venue_order_id.or_else(|| {
1515 bindings
1516 .venue_by_client
1517 .get(&client_order_id)
1518 .map(|binding| binding.parent)
1519 });
1520
1521 let child_venue_order_id = VenueOrderId::new(msg.ord_id);
1522 let parent_venue_order_id = parent_venue_order_id.unwrap_or(child_venue_order_id);
1523 let mut binding = bindings
1524 .venue_by_client
1525 .get(&client_order_id)
1526 .copied()
1527 .unwrap_or(OrderVenueBinding {
1528 parent: parent_venue_order_id,
1529 child: None,
1530 });
1531
1532 if binding.parent != parent_venue_order_id {
1533 log::error!(
1534 "Suppressing conflicting child parent binding for {client_order_id}: expected={} received={parent_venue_order_id}",
1535 binding.parent,
1536 );
1537 continue;
1538 }
1539
1540 bindings
1541 .client_by_parent
1542 .insert(parent_venue_order_id, client_order_id);
1543 let ts_event = parse_millisecond_timestamp(msg.u_time);
1544
1545 if binding.child == Some(child_venue_order_id) {
1546 refresh_bound_child_and_emit_update(
1547 client_order_id,
1548 child_venue_order_id,
1549 context,
1550 account_id,
1551 ts_event,
1552 ts_init,
1553 emitter,
1554 state,
1555 !order_state_cache.contains_key(&client_order_id),
1556 );
1557 } else {
1558 bind_algo_child_and_emit_transition(
1559 client_order_id,
1560 parent_venue_order_id,
1561 child_venue_order_id,
1562 &mut binding,
1563 context,
1564 account_id,
1565 ts_event,
1566 ts_init,
1567 emitter,
1568 state,
1569 );
1570 }
1571
1572 bindings.venue_by_client.insert(client_order_id, binding);
1573 }
1574
1575 let is_post_only_cancel = is_post_only_auto_cancel(msg);
1576
1577 if is_post_only_cancel
1578 || (!state.contains_accepted(&client_order_id) && is_unfilled_rpi_cancel(msg))
1579 {
1580 if is_post_only_cancel {
1581 state.insert_post_only_rejection(msg.ord_id);
1582 }
1583
1584 let ts_event = parse_millisecond_timestamp(msg.u_time);
1585 let reason = if msg.ord_type == OKXOrderType::Rpi {
1586 msg.cancel_source_reason
1587 .as_deref()
1588 .filter(|reason| !reason.is_empty())
1589 .unwrap_or("RPI order canceled before acceptance")
1590 } else {
1591 "Post-only order would have taken liquidity"
1592 };
1593 let rejected = OrderRejected::new(
1594 emitter.trader_id(),
1595 ident.strategy_id,
1596 instrument.id(),
1597 client_order_id,
1598 account_id,
1599 Ustr::from(reason),
1600 UUID4::new(),
1601 ts_event,
1602 ts_init,
1603 false,
1604 true, );
1606 state.remove_order_tracking(client_order_id);
1607 if let Some(bindings) = lifecycle_bindings.as_mut() {
1608 state.insert_terminal(client_order_id);
1609 if let Some(binding) = bindings.venue_by_client.get(&client_order_id).copied() {
1610 bindings.finish(client_order_id, binding);
1611 }
1612 }
1613
1614 order_state_cache.remove(&client_order_id);
1615 fee_cache.remove(&msg.ord_id);
1616 filled_qty_cache.remove(&msg.ord_id);
1617 emitter.send_order_event(OrderEventAny::Rejected(rejected));
1618 continue;
1619 }
1620
1621 let previous_fee = fee_cache.get(&msg.ord_id).copied();
1622 let previous_filled_qty = filled_qty_cache.get(&msg.ord_id).copied();
1623 let previous_state = order_state_cache.get(&client_order_id);
1624
1625 match parse_order_event(
1626 msg,
1627 client_order_id,
1628 account_id,
1629 emitter.trader_id(),
1630 ident.strategy_id,
1631 instrument,
1632 previous_fee,
1633 previous_filled_qty,
1634 previous_state,
1635 ts_init,
1636 ) {
1637 Ok(event) => {
1638 let needs_status =
1639 matches!(event, ParsedOrderEvent::Fill(_) | ParsedOrderEvent::Skipped);
1640 update_order_state_cache(msg, instrument, client_order_id, order_state_cache);
1641 dispatch_parsed_order_event(
1642 event,
1643 client_order_id,
1644 account_id,
1645 VenueOrderId::new(msg.ord_id),
1646 &ident,
1647 instrument,
1648 msg.state,
1649 emitter,
1650 state,
1651 order_state_cache,
1652 ts_init,
1653 );
1654
1655 dispatch_filled_order_status(
1656 msg,
1657 Some(client_order_id),
1658 instrument,
1659 emitter,
1660 state,
1661 ts_init,
1662 needs_status,
1663 );
1664
1665 if state.contains_terminal(&client_order_id)
1666 && let Some(bindings) = lifecycle_bindings.as_mut()
1667 && let Some(binding) =
1668 bindings.venue_by_client.get(&client_order_id).copied()
1669 {
1670 bindings.finish(client_order_id, binding);
1671 }
1672
1673 update_fee_fill_caches(msg, instrument, fee_cache, filled_qty_cache);
1674 }
1675 Err(e) => log::error!("Failed to parse order event for {client_order_id}: {e}"),
1676 }
1677 } else if state.contains_terminal(&client_order_id) {
1678 dispatch_terminal_order_fill_as_report(
1679 msg,
1680 client_order_id,
1681 account_id,
1682 instruments,
1683 fee_cache,
1684 filled_qty_cache,
1685 emitter,
1686 state,
1687 ts_init,
1688 );
1689 } else if is_post_only_auto_cancel(msg) && state.contains_post_only_rejection(&msg.ord_id) {
1690 log::debug!(
1691 "Skipping replayed post-only rejection for {client_order_id}: ord_id={}",
1692 msg.ord_id
1693 );
1694 } else {
1695 log::debug!(
1696 "Untracked order {client_order_id} (ord_id={}), sending as report for reconciliation",
1697 msg.ord_id
1698 );
1699 dispatch_order_msg_as_report(
1700 msg,
1701 account_id,
1702 instruments,
1703 fee_cache,
1704 filled_qty_cache,
1705 emitter,
1706 state,
1707 ts_init,
1708 );
1709 }
1710 }
1711}
1712
1713#[expect(clippy::too_many_arguments)]
1714fn dispatch_spread_order_messages(
1715 order_msgs: &[OKXSpreadOrder],
1716 emitter: &ExecutionEventEmitter,
1717 state: &WsDispatchState,
1718 account_id: AccountId,
1719 instruments: &AHashMap<Ustr, InstrumentAny>,
1720 filled_qty_cache: &mut FilledQtyCache,
1721 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
1722 ts_init: UnixNanos,
1723) {
1724 for msg in order_msgs {
1725 let Some(instrument) = instruments.get(&msg.sprd_id) else {
1726 log::warn!(
1727 "No instrument for {}, skipping spread order message",
1728 msg.sprd_id
1729 );
1730 continue;
1731 };
1732
1733 let Some(client_order_id) = parse_client_order_id(msg.cl_ord_id.as_str()) else {
1734 log::debug!(
1735 "Spread order without client_order_id (ord_id={}), sending as report",
1736 msg.ord_id
1737 );
1738 dispatch_spread_order_msg_as_report(
1739 msg,
1740 account_id,
1741 instruments,
1742 filled_qty_cache,
1743 emitter,
1744 state,
1745 ts_init,
1746 );
1747 continue;
1748 };
1749
1750 let identity = state.order_identity(client_order_id);
1751
1752 if let Some(ident) = identity {
1753 if is_spread_post_only_auto_cancel(msg) {
1754 let ts_event = msg
1755 .u_time
1756 .or(msg.c_time)
1757 .map_or(ts_init, parse_millisecond_timestamp);
1758 let rejected = OrderRejected::new(
1759 emitter.trader_id(),
1760 ident.strategy_id,
1761 instrument.id(),
1762 client_order_id,
1763 account_id,
1764 Ustr::from(OKX_POST_ONLY_CANCEL_REASON),
1765 UUID4::new(),
1766 ts_event,
1767 ts_init,
1768 false,
1769 true,
1770 );
1771 state.remove_order_tracking(client_order_id);
1772 order_state_cache.remove(&client_order_id);
1773 filled_qty_cache.remove(&msg.ord_id);
1774 emitter.send_order_event(OrderEventAny::Rejected(rejected));
1775 continue;
1776 }
1777
1778 let previous_filled_qty = filled_qty_cache.get(&msg.ord_id).copied();
1779 let previous_state = order_state_cache.get(&client_order_id);
1780
1781 match parse_spread_order_event(
1782 msg,
1783 client_order_id,
1784 account_id,
1785 emitter.trader_id(),
1786 ident.strategy_id,
1787 instrument,
1788 previous_filled_qty,
1789 previous_state,
1790 ts_init,
1791 ) {
1792 Ok(event) => {
1793 update_spread_order_state_cache(
1794 msg,
1795 instrument,
1796 client_order_id,
1797 order_state_cache,
1798 );
1799 dispatch_parsed_order_event(
1800 event,
1801 client_order_id,
1802 account_id,
1803 VenueOrderId::new(msg.ord_id.as_str()),
1804 &ident,
1805 instrument,
1806 msg.state,
1807 emitter,
1808 state,
1809 order_state_cache,
1810 ts_init,
1811 );
1812 update_spread_fill_cache(msg, instrument, filled_qty_cache);
1813 }
1814 Err(e) => {
1815 log::error!("Failed to parse spread order event for {client_order_id}: {e}");
1816 }
1817 }
1818 } else {
1819 log::debug!(
1820 "Untracked spread order {client_order_id} (ord_id={}), sending as report for reconciliation",
1821 msg.ord_id
1822 );
1823 dispatch_spread_order_msg_as_report(
1824 msg,
1825 account_id,
1826 instruments,
1827 filled_qty_cache,
1828 emitter,
1829 state,
1830 ts_init,
1831 );
1832 }
1833 }
1834}
1835
1836#[expect(clippy::too_many_arguments)]
1842fn dispatch_parsed_order_event(
1843 event: ParsedOrderEvent,
1844 client_order_id: ClientOrderId,
1845 account_id: AccountId,
1846 venue_order_id: VenueOrderId,
1847 identity: &OrderIdentity,
1848 instrument: &InstrumentAny,
1849 venue_status: OKXOrderStatus,
1850 emitter: &ExecutionEventEmitter,
1851 state: &WsDispatchState,
1852 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
1853 ts_init: UnixNanos,
1854) {
1855 let is_terminal;
1856
1857 match event {
1858 ParsedOrderEvent::Accepted(e) => {
1859 if state.contains_filled(&client_order_id) || state.contains_terminal(&client_order_id)
1860 {
1861 log::debug!("Skipping duplicate Accepted for {client_order_id}");
1862 return;
1863 }
1864
1865 if state.contains_accepted(&client_order_id) {
1866 emit_venue_order_id_update_if_changed(
1867 client_order_id,
1868 account_id,
1869 venue_order_id,
1870 identity,
1871 e.ts_event,
1872 emitter,
1873 state,
1874 order_state_cache,
1875 ts_init,
1876 );
1877 return;
1878 }
1879
1880 if state.contains_triggered(&client_order_id) {
1881 log::debug!("Skipping duplicate Accepted for {client_order_id}");
1882 return;
1883 }
1884
1885 state.insert_accepted(client_order_id, venue_order_id);
1886 is_terminal = false;
1887 emitter.send_order_event(OrderEventAny::Accepted(e));
1888 }
1889 ParsedOrderEvent::Triggered(e) => {
1890 if state.contains_filled(&client_order_id) {
1891 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
1892 return;
1893 }
1894
1895 if !TRIGGERABLE_ORDER_TYPES.contains(&identity.order_type) {
1896 log::debug!(
1897 "Skipping OrderTriggered for {} order {client_order_id}: market-style stops have no TRIGGERED state",
1898 identity.order_type,
1899 );
1900 state.insert_triggered(client_order_id);
1901 return;
1902 }
1903
1904 ensure_accepted_emitted(
1905 client_order_id,
1906 account_id,
1907 venue_order_id,
1908 identity,
1909 emitter,
1910 state,
1911 ts_init,
1912 ts_init,
1913 );
1914 state.insert_triggered(client_order_id);
1915 is_terminal = false;
1916 emitter.send_order_event(OrderEventAny::Triggered(e));
1917 }
1918 ParsedOrderEvent::Canceled(e) => {
1919 ensure_accepted_emitted(
1920 client_order_id,
1921 account_id,
1922 venue_order_id,
1923 identity,
1924 emitter,
1925 state,
1926 ts_init,
1927 ts_init,
1928 );
1929 state.remove_triggered(&client_order_id);
1930 state.remove_filled(&client_order_id);
1931 is_terminal = true;
1932 emitter.send_order_event(OrderEventAny::Canceled(e));
1933 }
1934 ParsedOrderEvent::Expired(e) => {
1935 ensure_accepted_emitted(
1936 client_order_id,
1937 account_id,
1938 venue_order_id,
1939 identity,
1940 emitter,
1941 state,
1942 ts_init,
1943 ts_init,
1944 );
1945 state.remove_triggered(&client_order_id);
1946 state.remove_filled(&client_order_id);
1947 is_terminal = true;
1948 emitter.send_order_event(OrderEventAny::Expired(e));
1949 }
1950 ParsedOrderEvent::Updated(e) => {
1951 ensure_accepted_emitted(
1952 client_order_id,
1953 account_id,
1954 venue_order_id,
1955 identity,
1956 emitter,
1957 state,
1958 ts_init,
1959 ts_init,
1960 );
1961 is_terminal = false;
1962 emitter.send_order_event(OrderEventAny::Updated(e));
1963 }
1964 ParsedOrderEvent::Fill(fill_report) => {
1965 is_terminal = venue_status == OKXOrderStatus::Filled;
1966
1967 if state.check_and_insert_trade(fill_report.trade_id) {
1968 log::debug!(
1969 "Skipping duplicate fill for {client_order_id}: trade_id={}",
1970 fill_report.trade_id
1971 );
1972 } else {
1973 emit_venue_order_id_update_if_changed(
1974 client_order_id,
1975 account_id,
1976 venue_order_id,
1977 identity,
1978 fill_report.ts_event,
1979 emitter,
1980 state,
1981 order_state_cache,
1982 ts_init,
1983 );
1984 ensure_accepted_emitted(
1985 client_order_id,
1986 account_id,
1987 venue_order_id,
1988 identity,
1989 emitter,
1990 state,
1991 ts_init,
1992 ts_init,
1993 );
1994 state.insert_filled(client_order_id);
1995 state.remove_triggered(&client_order_id);
1996 let filled = fill_report_to_order_filled(
1997 &fill_report,
1998 emitter.trader_id(),
1999 identity,
2000 instrument.quote_currency(),
2001 );
2002 emitter.send_order_event(OrderEventAny::Filled(filled));
2003 }
2004 }
2005 ParsedOrderEvent::StatusOnly(report) => {
2006 is_terminal = matches!(
2007 report.order_status,
2008 OrderStatus::Filled | OrderStatus::Canceled | OrderStatus::Expired
2009 );
2010 emitter.send_order_status_report(*report);
2011 }
2012 ParsedOrderEvent::Skipped => {
2013 is_terminal = venue_status == OKXOrderStatus::Filled;
2014 }
2015 }
2016
2017 if is_terminal {
2018 state.insert_terminal(client_order_id);
2019 state.remove_order_tracking(client_order_id);
2020 state.remove_accepted(&client_order_id);
2021 order_state_cache.remove(&client_order_id);
2022 }
2026}
2027
2028#[expect(
2031 clippy::too_many_arguments,
2032 reason = "acceptance reconstruction requires identity, venue state, and event timestamps"
2033)]
2034fn ensure_accepted_emitted(
2035 client_order_id: ClientOrderId,
2036 account_id: AccountId,
2037 venue_order_id: VenueOrderId,
2038 identity: &OrderIdentity,
2039 emitter: &ExecutionEventEmitter,
2040 state: &WsDispatchState,
2041 ts_event: UnixNanos,
2042 ts_init: UnixNanos,
2043) {
2044 if state.contains_accepted(&client_order_id)
2045 || state.contains_terminal(&client_order_id)
2046 || state.contains_filled(&client_order_id)
2047 {
2048 return;
2049 }
2050
2051 state.insert_accepted(client_order_id, venue_order_id);
2052 let accepted = OrderAccepted::new(
2053 emitter.trader_id(),
2054 identity.strategy_id,
2055 identity.instrument_id,
2056 client_order_id,
2057 venue_order_id,
2058 account_id,
2059 UUID4::new(),
2060 ts_event,
2061 ts_init,
2062 false,
2063 );
2064 emitter.send_order_event(OrderEventAny::Accepted(accepted));
2065}
2066
2067#[expect(clippy::too_many_arguments)]
2068fn emit_venue_order_id_update_if_changed(
2069 client_order_id: ClientOrderId,
2070 account_id: AccountId,
2071 venue_order_id: VenueOrderId,
2072 identity: &OrderIdentity,
2073 ts_event: UnixNanos,
2074 emitter: &ExecutionEventEmitter,
2075 state: &WsDispatchState,
2076 order_state_cache: &AHashMap<ClientOrderId, OrderStateSnapshot>,
2077 ts_init: UnixNanos,
2078) {
2079 let Some(accepted_venue_order_id) = state.accepted_venue_order_id(&client_order_id) else {
2080 return;
2081 };
2082
2083 if accepted_venue_order_id == venue_order_id {
2084 return;
2085 }
2086
2087 let Some(snapshot) = order_state_cache.get(&client_order_id) else {
2088 return;
2089 };
2090
2091 state.insert_accepted(client_order_id, venue_order_id);
2092 let updated = OrderUpdated::new(
2093 emitter.trader_id(),
2094 identity.strategy_id,
2095 identity.instrument_id,
2096 client_order_id,
2097 snapshot.quantity,
2098 UUID4::new(),
2099 ts_event,
2100 ts_init,
2101 false,
2102 Some(venue_order_id),
2103 Some(account_id),
2104 snapshot.price,
2105 None,
2106 None,
2107 false,
2108 );
2109 emitter.send_order_event(OrderEventAny::Updated(updated));
2110}
2111
2112fn fill_report_to_order_filled(
2114 report: &FillReport,
2115 trader_id: TraderId,
2116 identity: &OrderIdentity,
2117 quote_currency: Currency,
2118) -> OrderFilled {
2119 OrderFilled::new(
2120 trader_id,
2121 identity.strategy_id,
2122 report.instrument_id,
2123 report
2124 .client_order_id
2125 .expect("tracked order has client_order_id"),
2126 report.venue_order_id,
2127 report.account_id,
2128 report.trade_id,
2129 identity.order_side,
2130 identity.order_type,
2131 report.last_qty,
2132 report.last_px,
2133 quote_currency,
2134 report.liquidity_side,
2135 UUID4::new(),
2136 report.ts_event,
2137 report.ts_init,
2138 false,
2139 report.venue_position_id,
2140 Some(report.commission),
2141 None,
2142 )
2143}
2144
2145#[expect(clippy::too_many_arguments)]
2147fn dispatch_order_msg_as_report(
2148 msg: &OKXOrderMsg,
2149 account_id: AccountId,
2150 instruments: &AHashMap<Ustr, InstrumentAny>,
2151 fee_cache: &mut FeeCache,
2152 filled_qty_cache: &mut FilledQtyCache,
2153 emitter: &ExecutionEventEmitter,
2154 state: &WsDispatchState,
2155 ts_init: UnixNanos,
2156) {
2157 match parse_order_msg(
2158 msg,
2159 account_id,
2160 instruments,
2161 fee_cache,
2162 filled_qty_cache,
2163 ts_init,
2164 ) {
2165 Ok(report) => {
2166 let needs_status = matches!(report, ExecutionReport::Fill(_));
2167 dispatch_execution_reports(vec![report], emitter, state);
2168
2169 if let Some(instrument) = instruments.get(&msg.inst_id) {
2170 dispatch_filled_order_status(
2171 msg,
2172 None,
2173 instrument,
2174 emitter,
2175 state,
2176 ts_init,
2177 needs_status,
2178 );
2179 update_fee_fill_caches(msg, instrument, fee_cache, filled_qty_cache);
2180 }
2181 }
2182 Err(e) => log::error!("Failed to parse order message as report: {e}"),
2183 }
2184}
2185
2186fn dispatch_filled_order_status(
2187 msg: &OKXOrderMsg,
2188 client_order_id: Option<ClientOrderId>,
2189 instrument: &InstrumentAny,
2190 emitter: &ExecutionEventEmitter,
2191 state: &WsDispatchState,
2192 ts_init: UnixNanos,
2193 needs_status: bool,
2194) {
2195 if !needs_status || msg.state != OKXOrderStatus::Filled {
2196 return;
2197 }
2198
2199 let report = parse_order_status_report(msg, instrument, emitter.account_id(), ts_init)
2200 .and_then(|report| {
2201 let requested = parse_quantity(&msg.sz, instrument.size_precision())?;
2202 Ok((report, requested))
2203 });
2204
2205 match report {
2206 Ok((mut report, requested)) => {
2207 if report.filled_qty.is_positive() && report.quantity != requested {
2208 report.client_order_id = client_order_id.or(report.client_order_id);
2209
2210 dispatch_execution_reports(vec![ExecutionReport::Order(report)], emitter, state);
2212 }
2213 }
2214 Err(e) => log::error!(
2215 "Failed to parse filled order status for {}: {e}",
2216 msg.ord_id
2217 ),
2218 }
2219}
2220
2221#[expect(clippy::too_many_arguments)]
2222fn dispatch_terminal_order_fill_as_report(
2223 msg: &OKXOrderMsg,
2224 client_order_id: ClientOrderId,
2225 account_id: AccountId,
2226 instruments: &AHashMap<Ustr, InstrumentAny>,
2227 fee_cache: &mut FeeCache,
2228 filled_qty_cache: &mut FilledQtyCache,
2229 emitter: &ExecutionEventEmitter,
2230 state: &WsDispatchState,
2231 ts_init: UnixNanos,
2232) {
2233 match parse_order_msg(
2234 msg,
2235 account_id,
2236 instruments,
2237 fee_cache,
2238 filled_qty_cache,
2239 ts_init,
2240 ) {
2241 Ok(ExecutionReport::Fill(mut report)) => {
2242 report.client_order_id = Some(client_order_id);
2243 dispatch_execution_reports(vec![ExecutionReport::Fill(report)], emitter, state);
2244
2245 if let Some(instrument) = instruments.get(&msg.inst_id) {
2246 update_fee_fill_caches(msg, instrument, fee_cache, filled_qty_cache);
2247 }
2248 }
2249 Ok(ExecutionReport::Order(_)) => {
2250 log::debug!(
2251 "Suppressing stale regular order status for terminal tracked order {client_order_id}: ord_id={}",
2252 msg.ord_id,
2253 );
2254 }
2255 Err(e) => log::error!("Failed to parse terminal order update: {e}"),
2256 }
2257}
2258
2259fn dispatch_spread_order_msg_as_report(
2260 msg: &OKXSpreadOrder,
2261 account_id: AccountId,
2262 instruments: &AHashMap<Ustr, InstrumentAny>,
2263 filled_qty_cache: &mut FilledQtyCache,
2264 emitter: &ExecutionEventEmitter,
2265 state: &WsDispatchState,
2266 ts_init: UnixNanos,
2267) {
2268 match parse_spread_order_msg(msg, account_id, instruments, filled_qty_cache, ts_init) {
2269 Ok(report) => {
2270 dispatch_execution_reports(vec![report], emitter, state);
2271
2272 if let Some(instrument) = instruments.get(&msg.sprd_id) {
2273 update_spread_fill_cache(msg, instrument, filled_qty_cache);
2274 }
2275 }
2276 Err(e) => log::error!("Failed to parse spread order message as report: {e}"),
2277 }
2278}
2279
2280fn update_order_state_cache(
2282 msg: &OKXOrderMsg,
2283 instrument: &InstrumentAny,
2284 client_order_id: ClientOrderId,
2285 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
2286) {
2287 let venue_order_id = VenueOrderId::new(msg.ord_id);
2288 let quantity = parse_quantity(&msg.sz, instrument.size_precision()).unwrap_or_default();
2289 let price = if is_market_price(&msg.px) {
2290 None
2291 } else {
2292 parse_price(&msg.px, instrument.price_precision()).ok()
2293 };
2294
2295 order_state_cache.insert(
2296 client_order_id,
2297 OrderStateSnapshot {
2298 venue_order_id,
2299 quantity,
2300 price,
2301 },
2302 );
2303}
2304
2305fn update_spread_order_state_cache(
2306 msg: &OKXSpreadOrder,
2307 instrument: &InstrumentAny,
2308 client_order_id: ClientOrderId,
2309 order_state_cache: &mut AHashMap<ClientOrderId, OrderStateSnapshot>,
2310) {
2311 let venue_order_id = VenueOrderId::new(msg.ord_id.as_str());
2312 let quantity = parse_quantity(&msg.sz, instrument.size_precision()).unwrap_or_default();
2313 let price = if is_market_price(&msg.px) {
2314 None
2315 } else {
2316 parse_price(&msg.px, instrument.price_precision()).ok()
2317 };
2318
2319 order_state_cache.insert(
2320 client_order_id,
2321 OrderStateSnapshot {
2322 venue_order_id,
2323 quantity,
2324 price,
2325 },
2326 );
2327}
2328
2329fn update_spread_fill_cache(
2330 msg: &OKXSpreadOrder,
2331 instrument: &InstrumentAny,
2332 filled_qty_cache: &mut FilledQtyCache,
2333) {
2334 if !msg.acc_fill_sz.is_empty()
2335 && msg.acc_fill_sz != "0"
2336 && let Ok(qty) = parse_quantity(&msg.acc_fill_sz, instrument.size_precision())
2337 {
2338 filled_qty_cache.record(msg.ord_id, qty, is_terminal_order_state(msg.state));
2339 }
2340}
2341
2342fn is_spread_post_only_auto_cancel(msg: &OKXSpreadOrder) -> bool {
2343 msg.state == OKXOrderStatus::Canceled && msg.cancel_source == OKX_POST_ONLY_CANCEL_SOURCE
2344}
2345
2346pub fn dispatch_execution_reports(
2348 reports: Vec<ExecutionReport>,
2349 emitter: &ExecutionEventEmitter,
2350 state: &WsDispatchState,
2351) {
2352 log::debug!("Processing {} execution report(s)", reports.len());
2353
2354 for report in reports {
2355 match report {
2356 ExecutionReport::Order(order_report) => {
2357 if let Some(cid) = order_report.client_order_id {
2358 match order_report.order_status {
2359 #[allow(clippy::collapsible_match)]
2361 OrderStatus::Accepted => {
2362 if state.contains_terminal(&cid)
2363 || state.contains_filled(&cid)
2364 || state.contains_triggered(&cid)
2365 {
2366 log::debug!(
2367 "Skipping stale OrderStatusReport(Accepted) \
2368 for {cid} (order already terminal)"
2369 );
2370 continue;
2371 }
2372
2373 if !state.contains_accepted(&cid) {
2374 state.insert_accepted(cid, order_report.venue_order_id);
2375 }
2376 }
2377 OrderStatus::Triggered => {
2378 if state.contains_filled(&cid) {
2379 log::debug!(
2380 "Skipping stale OrderStatusReport(Triggered) \
2381 for {cid} (already filled)"
2382 );
2383 continue;
2384 }
2385 state.insert_triggered(cid);
2386 }
2387 OrderStatus::Filled => {
2388 state.insert_filled(cid);
2389 state.insert_terminal(cid);
2390 state.remove_triggered(&cid);
2391 }
2392 OrderStatus::Canceled | OrderStatus::Expired | OrderStatus::Rejected => {
2393 state.insert_terminal(cid);
2394 state.remove_triggered(&cid);
2395 state.remove_filled(&cid);
2396 }
2397 _ => {}
2398 }
2399 }
2400 emitter.send_order_status_report(order_report);
2401 }
2402 ExecutionReport::Fill(fill_report) => {
2403 if state.check_and_insert_trade(fill_report.trade_id) {
2404 log::debug!(
2405 "Skipping duplicate fill report: trade_id={}",
2406 fill_report.trade_id
2407 );
2408 continue;
2409 }
2410
2411 if let Some(cid) = fill_report.client_order_id {
2412 state.insert_filled(cid);
2413 state.remove_triggered(&cid);
2414 }
2415 emitter.send_fill_report(fill_report);
2416 }
2417 }
2418 }
2419}
2420
2421fn emit_send_failed_submit(
2422 failure: &CommandFailure,
2423 state: &WsDispatchState,
2424 emitter: &ExecutionEventEmitter,
2425 clock: &AtomicTime,
2426 client_order_id: ClientOrderId,
2427) {
2428 let (CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason)) = failure else {
2429 return;
2430 };
2431 let Some(ident) = state.order_identity(client_order_id) else {
2432 return;
2433 };
2434
2435 state.remove_order_tracking(client_order_id);
2436 emitter.emit_order_rejected_event(
2437 ident.strategy_id,
2438 ident.instrument_id,
2439 client_order_id,
2440 reason,
2441 clock.get_time_ns(),
2442 false,
2443 );
2444}
2445
2446fn emit_send_failed_modify(
2447 failure: &CommandFailure,
2448 state: &WsDispatchState,
2449 emitter: &ExecutionEventEmitter,
2450 clock: &AtomicTime,
2451 client_order_id: ClientOrderId,
2452) {
2453 let (CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason)) = failure else {
2454 return;
2455 };
2456 let Some(ident) = state.order_identity(client_order_id) else {
2457 return;
2458 };
2459
2460 emitter.emit_order_modify_rejected_event(
2461 ident.strategy_id,
2462 ident.instrument_id,
2463 client_order_id,
2464 None,
2465 reason,
2466 clock.get_time_ns(),
2467 );
2468}
2469
2470#[derive(Debug, Clone)]
2471pub struct AlgoCancelContext {
2472 pub client_order_id: ClientOrderId,
2473 pub instrument_id: InstrumentId,
2474 pub strategy_id: StrategyId,
2475 pub venue_order_id: Option<VenueOrderId>,
2476}
2477
2478pub fn emit_algo_cancel_rejections(
2481 responses: &[OKXCancelAlgoOrderResponse],
2482 contexts: &[AlgoCancelContext],
2483 emitter: &ExecutionEventEmitter,
2484 clock: &'static AtomicTime,
2485) {
2486 for (i, item) in responses.iter().enumerate() {
2487 let code = item.s_code.as_deref().unwrap_or(OKX_SUCCESS_CODE);
2488 if code == OKX_SUCCESS_CODE {
2489 continue;
2490 }
2491
2492 let msg = item.s_msg.as_deref().unwrap_or("");
2493
2494 if matches!(
2495 classify_okx_venue_code(code, msg),
2496 CommandFailure::Ambiguous(_) | CommandFailure::NotSent(_)
2497 ) {
2498 if let Some(ctx) = contexts.get(i) {
2499 log::warn!(
2500 "Ambiguous algo cancel response for {}, awaiting reconciliation: \
2501 algo_id={} sCode={code} sMsg={msg}",
2502 ctx.client_order_id,
2503 item.algo_id
2504 );
2505 } else {
2506 log::warn!(
2507 "Ambiguous algo cancel response without context at index {i}: \
2508 algo_id={} sCode={code} sMsg={msg}",
2509 item.algo_id
2510 );
2511 }
2512 continue;
2513 }
2514
2515 if let Some(ctx) = contexts.get(i) {
2516 let reason = format_order_response_reason(code, msg, "");
2517 let ts = clock.get_time_ns();
2518 emitter.emit_order_cancel_rejected_event(
2519 ctx.strategy_id,
2520 ctx.instrument_id,
2521 ctx.client_order_id,
2522 ctx.venue_order_id,
2523 &reason,
2524 ts,
2525 );
2526 } else {
2527 log::warn!(
2528 "Algo cancel rejected but no context at index {i}: \
2529 algo_id={} sCode={code} sMsg={msg}",
2530 item.algo_id
2531 );
2532 }
2533 }
2534}
2535
2536pub fn emit_batch_cancel_failure(
2537 contexts: &[AlgoCancelContext],
2538 error: &str,
2539 _emitter: &ExecutionEventEmitter,
2540 _clock: &'static AtomicTime,
2541) {
2542 for ctx in contexts {
2543 log::warn!(
2544 "Ambiguous algo batch cancel failure for {}, awaiting reconciliation: {error}",
2545 ctx.client_order_id
2546 );
2547 }
2548}
2549
2550pub(crate) fn format_order_response_reason(s_code: &str, s_msg: &str, sub_code: &str) -> String {
2551 match (s_code.is_empty(), s_msg.is_empty(), sub_code.is_empty()) {
2552 (false, false, true) => format!("OKX error {s_code}: {s_msg}"),
2553 (false, false, false) => format!("OKX error {s_code}: {s_msg} (subCode={sub_code})"),
2554 (false, true, true) => format!("OKX error {s_code}"),
2555 (false, true, false) => format!("OKX error {s_code} (subCode={sub_code})"),
2556 (true, false, true) => s_msg.to_string(),
2557 (true, false, false) => format!("{s_msg} (subCode={sub_code})"),
2558 (true, true, false) => format!("subCode={sub_code}"),
2559 (true, true, true) => String::new(),
2560 }
2561}
2562
2563#[cfg(test)]
2564mod tests {
2565 use std::time::Duration;
2566
2567 use nautilus_common::messages::{ExecutionEvent, ExecutionReport as CommonExecutionReport};
2568 use nautilus_core::time::get_atomic_clock_realtime;
2569 use nautilus_model::{
2570 enums::{AccountType, OrderSide, OrderType, TimeInForce, TriggerType},
2571 identifiers::Symbol,
2572 instruments::{CryptoPerpetual, CurrencyPair},
2573 types::{Price, Quantity},
2574 };
2575 use rstest::rstest;
2576
2577 use super::*;
2578 use crate::{
2579 common::enums::{OKXInstrumentType, OKXSide, OKXTargetCurrency},
2580 websocket::{error::OKXWsError, messages::OKXWsFrame},
2581 };
2582
2583 fn load_algo_order_messages(fixture: &str) -> Vec<OKXAlgoOrderMsg> {
2584 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2585 .join("test_data")
2586 .join(fixture);
2587 let content = std::fs::read_to_string(path).unwrap();
2588 let frame: OKXWsFrame = serde_json::from_str(&content).unwrap();
2589 let OKXWsFrame::Data { data, .. } = frame else {
2590 panic!("Expected algo order data frame");
2591 };
2592 serde_json::from_value(data).unwrap()
2593 }
2594
2595 fn load_regular_order_messages(fixture: &str) -> Vec<OKXOrderMsg> {
2596 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2597 .join("test_data")
2598 .join(fixture);
2599 let content = std::fs::read_to_string(path).unwrap();
2600 let frame: OKXWsFrame = serde_json::from_str(&content).unwrap();
2601 let OKXWsFrame::Data { data, .. } = frame else {
2602 panic!("Expected regular order data frame");
2603 };
2604 serde_json::from_value(data).unwrap()
2605 }
2606
2607 fn test_algo_context(client_order_id: ClientOrderId) -> OrderContext {
2608 OrderContext {
2609 identity: OrderIdentity {
2610 client_order_id,
2611 strategy_id: StrategyId::from("STRATEGY-001"),
2612 instrument_id: InstrumentId::from("BTC-USDT-SWAP.OKX"),
2613 order_side: OrderSide::Sell,
2614 order_type: OrderType::StopLimit,
2615 },
2616 quantity: Quantity::from("0.01"),
2617 price: Some(Price::from("102900")),
2618 trigger_price: Some(Price::from("95000")),
2619 trigger_type: Some(TriggerType::LastPrice),
2620 time_in_force: TimeInForce::Gtc,
2621 is_post_only: false,
2622 is_reduce_only: true,
2623 is_quote_quantity: false,
2624 }
2625 }
2626
2627 fn test_algo_instruments() -> AtomicMap<Ustr, InstrumentAny> {
2628 let instrument = CryptoPerpetual::builder()
2629 .instrument_id(InstrumentId::from("BTC-USDT-SWAP.OKX"))
2630 .raw_symbol(Symbol::from("BTC-USDT-SWAP"))
2631 .base_currency(Currency::BTC())
2632 .quote_currency(Currency::USDT())
2633 .settlement_currency(Currency::USDT())
2634 .is_inverse(false)
2635 .price_precision(2)
2636 .size_precision(8)
2637 .price_increment(Price::from("0.01"))
2638 .size_increment(Quantity::from("0.00000001"))
2639 .ts_event(UnixNanos::default())
2640 .ts_init(UnixNanos::default())
2641 .build()
2642 .unwrap();
2643 let instruments = AtomicMap::new();
2644 instruments.insert(
2645 Ustr::from("BTC-USDT-SWAP"),
2646 InstrumentAny::CryptoPerpetual(instrument),
2647 );
2648 instruments
2649 }
2650
2651 fn test_execution_emitter() -> (
2652 ExecutionEventEmitter,
2653 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2654 ) {
2655 let clock = get_atomic_clock_realtime();
2656 let mut emitter = ExecutionEventEmitter::new(
2657 clock,
2658 TraderId::from("TRADER-001"),
2659 AccountId::from("OKX-001"),
2660 AccountType::Margin,
2661 None,
2662 );
2663 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
2664 emitter.set_sender(sender);
2665 (emitter, receiver)
2666 }
2667
2668 fn dispatch_test_message(
2669 message: OKXWsMessage,
2670 emitter: &ExecutionEventEmitter,
2671 state: &WsDispatchState,
2672 instruments: &AtomicMap<Ustr, InstrumentAny>,
2673 ) {
2674 dispatch_ws_message(
2675 message,
2676 emitter,
2677 state,
2678 AccountId::from("OKX-001"),
2679 AccountType::Margin,
2680 instruments,
2681 &mut FeeCache::new(),
2682 &mut FilledQtyCache::new(),
2683 &mut AHashMap::new(),
2684 get_atomic_clock_realtime(),
2685 );
2686 }
2687
2688 fn drain_execution_events(
2689 receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2690 ) -> Vec<ExecutionEvent> {
2691 let mut events = Vec::new();
2692 while let Ok(event) = receiver.try_recv() {
2693 events.push(event);
2694 }
2695 events
2696 }
2697
2698 #[rstest]
2699 #[case::single_fill(false, false)]
2700 #[case::partial_fill(true, false)]
2701 #[case::status_only(true, true)]
2702 fn short_final_fill_dispatches_terminal_status(
2703 #[values(false, true)] tracked: bool,
2704 #[values(OKXTargetCurrency::BaseCcy, OKXTargetCurrency::QuoteCcy)]
2705 target: OKXTargetCurrency,
2706 #[case] partial_first: bool,
2707 #[case] status_only: bool,
2708 ) {
2709 let mut msg = load_regular_order_messages("ws_orders.json").remove(0);
2710 msg.inst_id = Ustr::from("BTC-USDC");
2711 msg.inst_type = OKXInstrumentType::Spot;
2712 msg.ord_type = OKXOrderType::Market;
2713 msg.side = OKXSide::Buy;
2714 msg.tgt_ccy = Some(target);
2715 msg.sz = match target {
2716 OKXTargetCurrency::BaseCcy => "0.00296487",
2717 OKXTargetCurrency::QuoteCcy => "296.487",
2718 }
2719 .to_string();
2720
2721 msg.acc_fill_sz = Some("0.00296475".to_string());
2722
2723 let last_qty = if partial_first {
2724 "0.00196475"
2725 } else {
2726 "0.00296475"
2727 };
2728
2729 msg.fill_sz = last_qty.to_string();
2730 msg.fill_px = "100000".to_string();
2731 msg.avg_px = "100000".to_string();
2732 msg.px.clear();
2733 msg.state = OKXOrderStatus::Filled;
2734 let client_order_id = ClientOrderId::new(msg.cl_ord_id.as_str());
2735 let instrument_id = InstrumentId::from("BTC-USDC.OKX");
2736 let instrument = CurrencyPair::builder()
2737 .instrument_id(instrument_id)
2738 .raw_symbol(Symbol::from("BTC-USDC"))
2739 .base_currency(Currency::BTC())
2740 .quote_currency(Currency::USDC())
2741 .price_precision(2)
2742 .size_precision(8)
2743 .price_increment(Price::from("0.01"))
2744 .size_increment(Quantity::from("0.00000001"))
2745 .ts_event(UnixNanos::default())
2746 .ts_init(UnixNanos::default())
2747 .build()
2748 .unwrap();
2749 let instruments = AtomicMap::new();
2750 instruments.insert(msg.inst_id, InstrumentAny::CurrencyPair(instrument));
2751 let state = WsDispatchState::default();
2752
2753 if tracked {
2754 state.order_identities.insert(
2755 client_order_id,
2756 OrderIdentity {
2757 client_order_id,
2758 strategy_id: StrategyId::from("STRATEGY-001"),
2759 instrument_id,
2760 order_side: OrderSide::Buy,
2761 order_type: OrderType::Market,
2762 },
2763 );
2764 }
2765
2766 let (emitter, mut receiver) = test_execution_emitter();
2767 let mut fee_cache = FeeCache::new();
2768 let mut filled_qty_cache = FilledQtyCache::new();
2769 let mut order_state_cache = AHashMap::new();
2770 let clock = get_atomic_clock_realtime();
2771
2772 if partial_first {
2773 let mut partial = msg.clone();
2774 partial.state = OKXOrderStatus::PartiallyFilled;
2775
2776 if status_only {
2777 partial.fill_sz = "0.00296475".to_string();
2778 msg.fill_sz = "0".to_string();
2779 } else {
2780 partial.acc_fill_sz = Some("0.001".to_string());
2781 partial.fill_sz = "0.001".to_string();
2782 partial.trade_id = "prior-trade".to_string();
2783 }
2784
2785 dispatch_ws_message(
2786 OKXWsMessage::Orders(vec![partial]),
2787 &emitter,
2788 &state,
2789 emitter.account_id(),
2790 AccountType::Cash,
2791 &instruments,
2792 &mut fee_cache,
2793 &mut filled_qty_cache,
2794 &mut order_state_cache,
2795 clock,
2796 );
2797 let events = drain_execution_events(&mut receiver);
2798 assert_eq!(events.len(), if tracked { 2 } else { 1 });
2799 assert!(!state.contains_terminal(&client_order_id));
2800 }
2801
2802 dispatch_ws_message(
2803 OKXWsMessage::Orders(vec![msg.clone()]),
2804 &emitter,
2805 &state,
2806 emitter.account_id(),
2807 AccountType::Cash,
2808 &instruments,
2809 &mut fee_cache,
2810 &mut filled_qty_cache,
2811 &mut order_state_cache,
2812 clock,
2813 );
2814
2815 let events = drain_execution_events(&mut receiver);
2816 let fill_index = usize::from(tracked && !partial_first);
2817 assert_eq!(events.len(), if status_only { 1 } else { fill_index + 2 });
2818
2819 if !status_only {
2820 match &events[fill_index] {
2821 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
2822 assert!(tracked);
2823 assert_eq!(fill.last_qty, Quantity::from(last_qty));
2824 }
2825 ExecutionEvent::Report(CommonExecutionReport::Fill(fill)) => {
2826 assert!(!tracked);
2827 assert_eq!(fill.last_qty, Quantity::from(last_qty));
2828 }
2829 other => panic!("Expected fill before terminal status, was {other:?}"),
2830 }
2831 }
2832
2833 let report_index = if status_only { 0 } else { fill_index + 1 };
2834
2835 let ExecutionEvent::Report(CommonExecutionReport::Order(report)) = &events[report_index]
2836 else {
2837 panic!("Expected terminal order status report");
2838 };
2839
2840 assert_eq!(report.client_order_id, Some(client_order_id));
2841 assert_eq!(report.order_status, OrderStatus::Filled);
2842 assert_eq!(report.quantity, Quantity::from("0.00296475"));
2843 assert_eq!(report.filled_qty, Quantity::from("0.00296475"));
2844 assert!(state.contains_terminal(&client_order_id));
2845 assert!(!state.order_identities.contains_key(&client_order_id));
2846 assert!(!state.contains_accepted(&client_order_id));
2847 assert!(!order_state_cache.contains_key(&client_order_id));
2848
2849 dispatch_ws_message(
2850 OKXWsMessage::Orders(vec![msg]),
2851 &emitter,
2852 &state,
2853 emitter.account_id(),
2854 AccountType::Cash,
2855 &instruments,
2856 &mut fee_cache,
2857 &mut filled_qty_cache,
2858 &mut order_state_cache,
2859 clock,
2860 );
2861 assert!(drain_execution_events(&mut receiver).is_empty());
2862 }
2863
2864 #[rstest]
2865 #[case::margin(AccountType::Margin)]
2866 #[case::cash(AccountType::Cash)]
2867 fn account_updates_report_the_configured_account_type(#[case] account_type: AccountType) {
2868 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2869 .join("test_data")
2870 .join("http_get_account_balance.json");
2871 let content = std::fs::read_to_string(path).unwrap();
2872 let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
2873 let instruments = AtomicMap::new();
2874 let (emitter, mut receiver) = test_execution_emitter();
2875
2876 dispatch_ws_message(
2877 OKXWsMessage::Account(payload["data"].clone()),
2878 &emitter,
2879 &WsDispatchState::default(),
2880 AccountId::from("OKX-001"),
2881 account_type,
2882 &instruments,
2883 &mut FeeCache::new(),
2884 &mut FilledQtyCache::new(),
2885 &mut AHashMap::new(),
2886 get_atomic_clock_realtime(),
2887 );
2888
2889 let events = drain_execution_events(&mut receiver);
2890 assert_eq!(events.len(), 1);
2891
2892 let ExecutionEvent::Account(state) = &events[0] else {
2893 panic!("expected an account state event");
2894 };
2895
2896 assert_eq!(state.account_type, account_type);
2897 assert_eq!(state.account_id, AccountId::from("OKX-001"));
2898 }
2899
2900 #[rstest]
2901 fn tracked_algo_live_and_pause_emit_one_parent_acceptance() {
2902 let mut messages = load_algo_order_messages("ws_orders_algo.json");
2903 let live = messages.remove(0);
2904 let mut pause = live.clone();
2905 pause.state = OKXAlgoOrderStatus::Pause;
2906 pause.u_time += 1;
2907 let client_order_id = ClientOrderId::new(live.algo_cl_ord_id.as_str());
2908 let state = WsDispatchState::default();
2909 let mut context = test_algo_context(client_order_id);
2910 context.identity.order_type = OrderType::StopMarket;
2911 context.price = None;
2912 state.track_order_context(context);
2913 let instruments = test_algo_instruments();
2914 let (emitter, mut receiver) = test_execution_emitter();
2915
2916 dispatch_test_message(
2917 OKXWsMessage::AlgoOrders(vec![live, pause]),
2918 &emitter,
2919 &state,
2920 &instruments,
2921 );
2922
2923 let events = drain_execution_events(&mut receiver);
2924 assert_eq!(events.len(), 1);
2925 match &events[0] {
2926 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => {
2927 assert_eq!(accepted.client_order_id, client_order_id);
2928 assert_eq!(
2929 accepted.venue_order_id,
2930 VenueOrderId::new("706620792746729472")
2931 );
2932 }
2933 other => panic!("Expected tracked algo acceptance, was {other:?}"),
2934 }
2935 }
2936
2937 #[rstest]
2938 #[case::after_acceptance(false)]
2939 #[case::first_acceptance(true)]
2940 fn tracked_algo_amend_emits_updated_terms_once(#[case] first_acceptance: bool) {
2941 let live = load_algo_order_messages("ws_orders_algo.json").remove(1);
2942 let client_order_id = ClientOrderId::new(live.algo_cl_ord_id.as_str());
2943 let state = WsDispatchState::default();
2944 let mut context = test_algo_context(client_order_id);
2945 context.identity.order_side = OrderSide::Buy;
2946 context.quantity = Quantity::from("0.02");
2947 context.price = Some(Price::from("106000.00"));
2948 context.trigger_price = Some(Price::from("105000.00"));
2949 context.trigger_type = Some(TriggerType::MarkPrice);
2950 state.track_order_context(context);
2951 let instruments = test_algo_instruments();
2952 let (emitter, mut receiver) = test_execution_emitter();
2953
2954 if !first_acceptance {
2955 dispatch_test_message(
2956 OKXWsMessage::AlgoOrders(vec![live.clone()]),
2957 &emitter,
2958 &state,
2959 &instruments,
2960 );
2961 let events = drain_execution_events(&mut receiver);
2962 assert_eq!(events.len(), 1);
2963 assert!(matches!(
2964 &events[0],
2965 ExecutionEvent::Order(OrderEventAny::Accepted(_))
2966 ));
2967 }
2968
2969 let mut amended = live.clone();
2970 amended.sz = "0.03".into();
2971 amended.trigger_px = "104000".into();
2972 amended.ord_px = "103000".into();
2973 amended.u_time += 2;
2974 dispatch_test_message(
2975 OKXWsMessage::AlgoOrders(vec![amended.clone(), amended, live]),
2976 &emitter,
2977 &state,
2978 &instruments,
2979 );
2980 let events = drain_execution_events(&mut receiver);
2981 assert_eq!(events.len(), if first_acceptance { 2 } else { 1 });
2982
2983 if first_acceptance {
2984 assert!(matches!(
2985 &events[0],
2986 ExecutionEvent::Order(OrderEventAny::Accepted(_))
2987 ));
2988 }
2989
2990 let ExecutionEvent::Order(OrderEventAny::Updated(updated)) = events.last().unwrap() else {
2991 panic!("expected OrderUpdated");
2992 };
2993
2994 assert_eq!(updated.client_order_id, client_order_id);
2995 assert_eq!(
2996 updated.venue_order_id,
2997 Some(VenueOrderId::from("706620792746729473"))
2998 );
2999 assert_eq!(updated.instrument_id, context.identity.instrument_id);
3000 assert_eq!(updated.strategy_id, context.identity.strategy_id);
3001 assert_eq!(updated.account_id, Some(AccountId::from("OKX-001")));
3002 assert_eq!(updated.quantity, Quantity::from("0.03"));
3003 assert_eq!(updated.price, Some(Price::from("103000.00")));
3004 assert_eq!(updated.trigger_price, Some(Price::from("104000.00")));
3005 let final_context = state.order_context(client_order_id).unwrap();
3006 assert_eq!(final_context.quantity, updated.quantity);
3007 assert_eq!(final_context.price, updated.price);
3008 assert_eq!(final_context.trigger_price, updated.trigger_price);
3009 assert_eq!(final_context.trigger_type, Some(TriggerType::MarkPrice));
3010 }
3011
3012 #[rstest]
3013 fn tracked_algo_trailing_acceptance_does_not_emit_update() {
3014 let message = load_algo_order_messages("ws_orders_algo_states.json").remove(2);
3015 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3016 let state = WsDispatchState::default();
3017 let mut context = test_algo_context(client_order_id);
3018 context.identity.order_type = OrderType::TrailingStopMarket;
3019 context.price = None;
3020 state.track_order_context(context);
3021 let instruments = test_algo_instruments();
3022 let (emitter, mut receiver) = test_execution_emitter();
3023
3024 dispatch_test_message(
3025 OKXWsMessage::AlgoOrders(vec![message]),
3026 &emitter,
3027 &state,
3028 &instruments,
3029 );
3030
3031 let events = drain_execution_events(&mut receiver);
3032 assert_eq!(events.len(), 1);
3033
3034 let ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) = &events[0] else {
3035 panic!("expected OrderAccepted");
3036 };
3037
3038 assert_eq!(accepted.client_order_id, client_order_id);
3039 assert_eq!(
3040 accepted.venue_order_id,
3041 VenueOrderId::from("706620792746729482")
3042 );
3043 assert_eq!(
3044 state.order_context(client_order_id).unwrap().trigger_price,
3045 None
3046 );
3047 }
3048
3049 #[rstest]
3050 #[case::live(OKXAlgoOrderStatus::Live)]
3051 #[case::canceled(OKXAlgoOrderStatus::Canceled)]
3052 #[case::failed(OKXAlgoOrderStatus::OrderFailed)]
3053 fn tracked_algo_malformed_terms_preserve_lifecycle(#[case] status: OKXAlgoOrderStatus) {
3054 let mut message = load_algo_order_messages("ws_orders_algo.json").remove(1);
3055 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3056 let state = WsDispatchState::default();
3057 let context = test_algo_context(client_order_id);
3058 state.track_order_context(context);
3059 let instruments = test_algo_instruments();
3060 let (emitter, mut receiver) = test_execution_emitter();
3061 message.state = status;
3062 message.trigger_px = "invalid".into();
3063 message.fail_code = "51008".into();
3064
3065 dispatch_test_message(
3066 OKXWsMessage::AlgoOrders(vec![message]),
3067 &emitter,
3068 &state,
3069 &instruments,
3070 );
3071
3072 let events = drain_execution_events(&mut receiver);
3073 assert_eq!(
3074 events.len(),
3075 if status == OKXAlgoOrderStatus::Canceled {
3076 2
3077 } else {
3078 1
3079 }
3080 );
3081
3082 match status {
3083 OKXAlgoOrderStatus::Live => {
3084 assert!(
3085 matches!(&events[0], ExecutionEvent::Order(OrderEventAny::Accepted(event))
3086 if event.client_order_id == client_order_id
3087 && event.venue_order_id == VenueOrderId::from("706620792746729473"))
3088 );
3089 let retained = state.order_context(client_order_id).unwrap();
3090 assert_eq!(retained.quantity, context.quantity);
3091 assert_eq!(retained.price, context.price);
3092 assert_eq!(retained.trigger_price, context.trigger_price);
3093 assert_eq!(retained.trigger_type, context.trigger_type);
3094 assert!(!state.contains_terminal(&client_order_id));
3095 }
3096 OKXAlgoOrderStatus::Canceled => {
3097 assert!(matches!(
3098 &events[0],
3099 ExecutionEvent::Order(OrderEventAny::Accepted(_))
3100 ));
3101 assert!(
3102 matches!(&events[1], ExecutionEvent::Order(OrderEventAny::Canceled(event))
3103 if event.client_order_id == client_order_id
3104 && event.venue_order_id == Some(VenueOrderId::from("706620792746729473")))
3105 );
3106 assert!(state.contains_terminal(&client_order_id));
3107 assert!(state.order_context(client_order_id).is_none());
3108 }
3109 OKXAlgoOrderStatus::OrderFailed => {
3110 assert!(
3111 matches!(&events[0], ExecutionEvent::Order(OrderEventAny::Rejected(event))
3112 if event.client_order_id == client_order_id
3113 && event.reason == Ustr::from("OKX error 51008"))
3114 );
3115 assert!(state.contains_terminal(&client_order_id));
3116 assert!(state.order_context(client_order_id).is_none());
3117 }
3118 _ => unreachable!(),
3119 }
3120 }
3121
3122 #[rstest]
3123 fn algo_update_routes_are_explicit_for_external_tracked_and_suppressed() {
3124 let mut message = load_algo_order_messages("ws_orders_algo.json").remove(0);
3125 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3126 let state = WsDispatchState::default();
3127
3128 assert_eq!(
3129 route_algo_order_message(&message, &state),
3130 ExecutionUpdateRoute::External
3131 );
3132
3133 let context = test_algo_context(client_order_id);
3134 state.track_order_context(context);
3135 assert_eq!(
3136 route_algo_order_message(&message, &state),
3137 ExecutionUpdateRoute::Tracked(client_order_id, context)
3138 );
3139
3140 message.state = OKXAlgoOrderStatus::Unknown;
3141 assert_eq!(
3142 route_algo_order_message(&message, &state),
3143 ExecutionUpdateRoute::Suppressed
3144 );
3145 }
3146
3147 #[rstest]
3148 fn rest_parent_binding_routes_linked_child_before_parent_stream_update() {
3149 let mut child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3150 let client_order_id = ClientOrderId::new("STOP003BTCUSDT20250120");
3151 let parent_venue_order_id = VenueOrderId::new("706620792746729474");
3152 let child_venue_order_id = VenueOrderId::new(child.ord_id);
3153 child.algo_cl_ord_id = None;
3154 let state = WsDispatchState::default();
3155 state.track_order_context(test_algo_context(client_order_id));
3156 state.bind_algo_parent(client_order_id, parent_venue_order_id);
3157 let instruments = test_algo_instruments();
3158 let (emitter, mut receiver) = test_execution_emitter();
3159
3160 dispatch_test_message(
3161 OKXWsMessage::Orders(vec![child.clone()]),
3162 &emitter,
3163 &state,
3164 &instruments,
3165 );
3166
3167 let events = drain_execution_events(&mut receiver);
3168 assert_eq!(events.len(), 4);
3169 assert!(matches!(
3170 &events[0],
3171 ExecutionEvent::Order(OrderEventAny::Accepted(accepted))
3172 if accepted.client_order_id == client_order_id
3173 && accepted.venue_order_id == parent_venue_order_id
3174 ));
3175 assert!(matches!(
3176 &events[1],
3177 ExecutionEvent::Order(OrderEventAny::Updated(updated))
3178 if updated.client_order_id == client_order_id
3179 && updated.venue_order_id == Some(child_venue_order_id)
3180 ));
3181 assert!(matches!(
3182 &events[2],
3183 ExecutionEvent::Order(OrderEventAny::Triggered(triggered))
3184 if triggered.client_order_id == client_order_id
3185 && triggered.venue_order_id == Some(child_venue_order_id)
3186 ));
3187 assert!(matches!(
3188 &events[3],
3189 ExecutionEvent::Order(OrderEventAny::Filled(filled))
3190 if filled.client_order_id == client_order_id
3191 && filled.venue_order_id == child_venue_order_id
3192 ));
3193
3194 dispatch_test_message(
3195 OKXWsMessage::Orders(vec![child]),
3196 &emitter,
3197 &state,
3198 &instruments,
3199 );
3200 assert!(drain_execution_events(&mut receiver).is_empty());
3201 }
3202
3203 #[rstest]
3204 #[tokio::test]
3205 async fn pre_binding_child_is_held_until_parent_binding() {
3206 let child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3207 let parent_venue_order_id = VenueOrderId::new(
3208 child
3209 .linked_algo_ord
3210 .as_ref()
3211 .expect("Expected linked algo order")
3212 .algo_id
3213 .as_str(),
3214 );
3215 let client_order_id = ClientOrderId::new("STOP003BTCUSDT20250120");
3216 let child_venue_order_id = VenueOrderId::new(child.ord_id);
3217 let state = WsDispatchState::default();
3218 state.track_order_context(test_algo_context(client_order_id));
3219 let instruments = test_algo_instruments();
3220 let (emitter, mut receiver) = test_execution_emitter();
3221
3222 dispatch_test_message(
3223 OKXWsMessage::Orders(vec![child.clone()]),
3224 &emitter,
3225 &state,
3226 &instruments,
3227 );
3228 assert!(drain_execution_events(&mut receiver).is_empty());
3229
3230 state.bind_algo_parent(client_order_id, parent_venue_order_id);
3231 tokio::time::timeout(
3232 Duration::from_millis(100),
3233 state.wait_for_linked_child_route(),
3234 )
3235 .await
3236 .expect("Expected parent binding to wake the private dispatch loop");
3237 dispatch_test_message(
3238 OKXWsMessage::Orders(Vec::new()),
3239 &emitter,
3240 &state,
3241 &instruments,
3242 );
3243
3244 let events = drain_execution_events(&mut receiver);
3245 assert_eq!(events.len(), 4);
3246 assert!(matches!(
3247 &events[0],
3248 ExecutionEvent::Order(OrderEventAny::Accepted(accepted))
3249 if accepted.client_order_id == client_order_id
3250 && accepted.venue_order_id == parent_venue_order_id
3251 ));
3252 assert!(matches!(
3253 &events[1],
3254 ExecutionEvent::Order(OrderEventAny::Updated(updated))
3255 if updated.client_order_id == client_order_id
3256 && updated.venue_order_id == Some(child_venue_order_id)
3257 ));
3258 assert!(matches!(
3259 &events[2],
3260 ExecutionEvent::Order(OrderEventAny::Triggered(triggered))
3261 if triggered.client_order_id == client_order_id
3262 && triggered.venue_order_id == Some(child_venue_order_id)
3263 ));
3264 assert!(matches!(
3265 &events[3],
3266 ExecutionEvent::Order(OrderEventAny::Filled(filled))
3267 if filled.client_order_id == client_order_id
3268 && filled.venue_order_id == child_venue_order_id
3269 ));
3270
3271 dispatch_test_message(OKXWsMessage::Reconnected, &emitter, &state, &instruments);
3272 dispatch_test_message(
3273 OKXWsMessage::Orders(vec![child]),
3274 &emitter,
3275 &state,
3276 &instruments,
3277 );
3278 assert!(drain_execution_events(&mut receiver).is_empty());
3279 }
3280
3281 #[rstest]
3282 fn held_child_becomes_external_when_candidate_binds_another_parent() {
3283 let child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3284 let client_order_id = ClientOrderId::new("STOP003BTCUSDT20250120");
3285 let child_venue_order_id = VenueOrderId::new(child.ord_id);
3286 let state = WsDispatchState::default();
3287 state.track_order_context(test_algo_context(client_order_id));
3288 let instruments = test_algo_instruments();
3289 let (emitter, mut receiver) = test_execution_emitter();
3290
3291 dispatch_test_message(
3292 OKXWsMessage::Orders(vec![child]),
3293 &emitter,
3294 &state,
3295 &instruments,
3296 );
3297 assert!(drain_execution_events(&mut receiver).is_empty());
3298
3299 let authoritative_parent = VenueOrderId::new("706620792746729475");
3300 state.bind_algo_parent(client_order_id, authoritative_parent);
3301 dispatch_test_message(
3302 OKXWsMessage::Orders(Vec::new()),
3303 &emitter,
3304 &state,
3305 &instruments,
3306 );
3307
3308 let events = drain_execution_events(&mut receiver);
3309 assert_eq!(events.len(), 1);
3310 assert!(matches!(
3311 &events[0],
3312 ExecutionEvent::Report(CommonExecutionReport::Fill(report))
3313 if report.client_order_id
3314 == Some(ClientOrderId::new("706620792746729474_0"))
3315 && report.venue_order_id == child_venue_order_id
3316 ));
3317 assert_eq!(
3318 state.order_venue_binding(client_order_id),
3319 Some((authoritative_parent, false))
3320 );
3321 }
3322
3323 #[rstest]
3324 fn active_algo_bindings_are_not_evicted_with_replay_state() {
3325 let state = WsDispatchState::default();
3326 let first_client_order_id = ClientOrderId::new("STOP-ACTIVE-00000");
3327 let first_venue_order_id = VenueOrderId::new("700000000000000000");
3328
3329 for index in 0..=DEDUP_CAPACITY {
3330 let client_order_id = ClientOrderId::new(format!("STOP-ACTIVE-{index:05}").as_str());
3331 let venue_order_id = VenueOrderId::new(
3332 format!("{}", 700_000_000_000_000_000_u64 + index as u64).as_str(),
3333 );
3334 state.bind_algo_parent(client_order_id, venue_order_id);
3335 }
3336
3337 assert_eq!(
3338 state.order_venue_binding(first_client_order_id),
3339 Some((first_venue_order_id, false))
3340 );
3341 }
3342
3343 #[rstest]
3344 fn ambiguous_submit_failure_preserves_only_bound_context() {
3345 let client_order_id = ClientOrderId::new("STOP-AMBIGUOUS-001");
3346 let failure = CommandFailure::Ambiguous("request timed out".to_string());
3347 let unbound_state = WsDispatchState::default();
3348 unbound_state.track_order_context(test_algo_context(client_order_id));
3349
3350 unbound_state.resolve_algo_submit_failure(client_order_id, &failure);
3351 assert_eq!(unbound_state.order_identity(client_order_id), None);
3352
3353 let bound_state = WsDispatchState::default();
3354 let context = test_algo_context(client_order_id);
3355 let parent_venue_order_id = VenueOrderId::new("706620792746729476");
3356 bound_state.track_order_context(context);
3357 bound_state.bind_algo_parent(client_order_id, parent_venue_order_id);
3358
3359 bound_state.resolve_algo_submit_failure(client_order_id, &failure);
3360 assert_eq!(
3361 bound_state.order_identity(client_order_id),
3362 Some(context.identity)
3363 );
3364 assert_eq!(
3365 bound_state.order_venue_binding(client_order_id),
3366 Some((parent_venue_order_id, false))
3367 );
3368 }
3369
3370 #[rstest]
3371 #[case::effective(0, false)]
3372 #[case::effective_order_id_list(0, true)]
3373 #[case::partially_effective(1, false)]
3374 #[case::order_placed(2, false)]
3375 fn tracked_algo_trigger_states_bind_child_before_trigger(
3376 #[case] index: usize,
3377 #[case] use_order_id_list: bool,
3378 ) {
3379 let mut messages = if index == 2 {
3380 load_algo_order_messages("ws_orders_algo.json")
3381 } else {
3382 load_algo_order_messages("ws_orders_algo_states.json")
3383 };
3384 let mut message = if index == 2 {
3385 messages.remove(2)
3386 } else {
3387 messages.remove(index)
3388 };
3389 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3390 let child_venue_order_id = VenueOrderId::new(message.ord_id.as_str());
3391 if use_order_id_list {
3392 message.ord_id_list = vec![message.ord_id.clone()];
3393 message.ord_id.clear();
3394 }
3395
3396 let state = WsDispatchState::default();
3397 state.track_order_context(test_algo_context(client_order_id));
3398 let instruments = test_algo_instruments();
3399 let (emitter, mut receiver) = test_execution_emitter();
3400
3401 dispatch_test_message(
3402 OKXWsMessage::AlgoOrders(vec![message]),
3403 &emitter,
3404 &state,
3405 &instruments,
3406 );
3407
3408 let events = drain_execution_events(&mut receiver);
3409 assert_eq!(events.len(), 3);
3410 assert!(matches!(
3411 &events[0],
3412 ExecutionEvent::Order(OrderEventAny::Accepted(_))
3413 ));
3414 assert!(matches!(
3415 &events[1],
3416 ExecutionEvent::Order(OrderEventAny::Updated(updated))
3417 if updated.venue_order_id == Some(child_venue_order_id)
3418 ));
3419 assert!(matches!(
3420 &events[2],
3421 ExecutionEvent::Order(OrderEventAny::Triggered(triggered))
3422 if triggered.venue_order_id == Some(child_venue_order_id)
3423 ));
3424 assert_eq!(
3425 state.order_venue_binding(client_order_id),
3426 Some((child_venue_order_id, true))
3427 );
3428 }
3429
3430 #[rstest]
3431 fn tracked_algo_transition_uses_venue_actual_quantity() {
3432 let mut message = load_algo_order_messages("ws_orders_algo_states.json").remove(0);
3433 message.sz.clear();
3434 message.close_fraction = "1".to_string();
3435 message.actual_sz = "0.025".to_string();
3436 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3437 let state = WsDispatchState::default();
3438 state.track_order_context(test_algo_context(client_order_id));
3439 let instruments = test_algo_instruments();
3440 let (emitter, mut receiver) = test_execution_emitter();
3441
3442 dispatch_test_message(
3443 OKXWsMessage::AlgoOrders(vec![message]),
3444 &emitter,
3445 &state,
3446 &instruments,
3447 );
3448
3449 let events = drain_execution_events(&mut receiver);
3450 assert_eq!(events.len(), 3);
3451 assert!(matches!(
3452 &events[1],
3453 ExecutionEvent::Order(OrderEventAny::Updated(updated))
3454 if updated.quantity == Quantity::from("0.025")
3455 && updated.price.is_none()
3456 ));
3457 assert_eq!(
3458 state
3459 .order_contexts
3460 .get(&client_order_id)
3461 .map(|context| context.quantity),
3462 Some(Quantity::from("0.025"))
3463 );
3464 }
3465
3466 #[rstest]
3467 fn regular_child_refreshes_parent_transition_terms() {
3468 let mut parent = load_algo_order_messages("ws_orders_algo_states.json").remove(0);
3469 parent.ord_px = "94950".to_string();
3470 let parent_replay = parent.clone();
3471 let client_order_id = ClientOrderId::new(parent.algo_cl_ord_id.as_str());
3472 let parent_venue_order_id = parent.algo_id.clone();
3473 let child_venue_order_id = parent.ord_id.clone();
3474 let state = WsDispatchState::default();
3475 state.track_order_context(test_algo_context(client_order_id));
3476 let instruments = test_algo_instruments();
3477 let (emitter, mut receiver) = test_execution_emitter();
3478
3479 dispatch_test_message(
3480 OKXWsMessage::AlgoOrders(vec![parent]),
3481 &emitter,
3482 &state,
3483 &instruments,
3484 );
3485 assert_eq!(drain_execution_events(&mut receiver).len(), 3);
3486
3487 let mut child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3488 child.algo_id = Some(parent_venue_order_id.clone());
3489 child.algo_cl_ord_id = None;
3490 child.linked_algo_ord = Some(crate::websocket::messages::OKXLinkedAlgoOrd {
3491 algo_id: parent_venue_order_id,
3492 });
3493 child.ord_id = Ustr::from(child_venue_order_id.as_str());
3494 child.ord_type = OKXOrderType::Limit;
3495 child.state = OKXOrderStatus::Live;
3496 child.sz = "0.025".to_string();
3497 child.px = "94950".to_string();
3498 child.acc_fill_sz = Some("0".to_string());
3499 child.fill_sz.clear();
3500 child.fill_px.clear();
3501 child.trade_id.clear();
3502
3503 dispatch_test_message(
3504 OKXWsMessage::Orders(vec![child]),
3505 &emitter,
3506 &state,
3507 &instruments,
3508 );
3509
3510 let events = drain_execution_events(&mut receiver);
3511 assert_eq!(events.len(), 1);
3512 assert!(matches!(
3513 &events[0],
3514 ExecutionEvent::Order(OrderEventAny::Updated(updated))
3515 if updated.quantity == Quantity::from("0.025")
3516 && updated.price == Some(Price::from("94950"))
3517 ));
3518 assert_eq!(
3519 state
3520 .order_contexts
3521 .get(&client_order_id)
3522 .map(|context| (context.quantity, context.price)),
3523 Some((Quantity::from("0.025"), Some(Price::from("94950"))))
3524 );
3525
3526 dispatch_test_message(
3527 OKXWsMessage::AlgoOrders(vec![parent_replay]),
3528 &emitter,
3529 &state,
3530 &instruments,
3531 );
3532 assert!(drain_execution_events(&mut receiver).is_empty());
3533 assert_eq!(
3534 state
3535 .order_contexts
3536 .get(&client_order_id)
3537 .map(|context| context.quantity),
3538 Some(Quantity::from("0.025"))
3539 );
3540 }
3541
3542 #[rstest]
3543 fn tracked_algo_holds_trigger_until_linked_child_arrives() {
3544 let mut parent = load_algo_order_messages("ws_orders_algo_states.json").remove(0);
3545 parent.algo_id = "706620792746729474".to_string();
3546 parent.algo_cl_ord_id = "STOP003BTCUSDT20250120".to_string();
3547 parent.ord_id.clear();
3548 parent.ord_id_list.clear();
3549 let client_order_id = ClientOrderId::new(parent.algo_cl_ord_id.as_str());
3550 let state = WsDispatchState::default();
3551 state.track_order_context(test_algo_context(client_order_id));
3552 let instruments = test_algo_instruments();
3553 let (emitter, mut receiver) = test_execution_emitter();
3554
3555 dispatch_test_message(
3556 OKXWsMessage::AlgoOrders(vec![parent]),
3557 &emitter,
3558 &state,
3559 &instruments,
3560 );
3561
3562 let parent_events = drain_execution_events(&mut receiver);
3563 assert_eq!(parent_events.len(), 1);
3564 assert!(matches!(
3565 &parent_events[0],
3566 ExecutionEvent::Order(OrderEventAny::Accepted(_))
3567 ));
3568 assert!(!state.contains_triggered(&client_order_id));
3569
3570 let child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3571 assert!(child.algo_cl_ord_id.is_none());
3572 assert_eq!(child.cl_ord_id, "706620792746729474_0");
3573 assert_eq!(
3574 child
3575 .linked_algo_ord
3576 .as_ref()
3577 .map(|linked| linked.algo_id.as_str()),
3578 Some("706620792746729474")
3579 );
3580 dispatch_test_message(
3581 OKXWsMessage::Orders(vec![child]),
3582 &emitter,
3583 &state,
3584 &instruments,
3585 );
3586
3587 let child_events = drain_execution_events(&mut receiver);
3588 assert_eq!(child_events.len(), 3);
3589 assert!(matches!(
3590 &child_events[0],
3591 ExecutionEvent::Order(OrderEventAny::Updated(_))
3592 ));
3593 assert!(matches!(
3594 &child_events[1],
3595 ExecutionEvent::Order(OrderEventAny::Triggered(_))
3596 ));
3597
3598 match &child_events[2] {
3599 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
3600 assert_eq!(filled.client_order_id, client_order_id);
3601 assert_eq!(
3602 filled.venue_order_id,
3603 VenueOrderId::new("706620792746729999")
3604 );
3605 assert_eq!(filled.trade_id, TradeId::new("1518905530"));
3606 }
3607 other => panic!("Expected tracked child fill, was {other:?}"),
3608 }
3609
3610 assert!(state.contains_terminal(&client_order_id));
3611 }
3612
3613 #[rstest]
3614 fn tracked_algo_child_cancellation_routes_late_fill_report() {
3615 let parent = load_algo_order_messages("ws_orders_algo_states.json").remove(0);
3616 let client_order_id = ClientOrderId::new(parent.algo_cl_ord_id.as_str());
3617 let parent_venue_order_id = parent.algo_id.clone();
3618 let child_venue_order_id = parent.ord_id.clone();
3619 let state = WsDispatchState::default();
3620 state.track_order_context(test_algo_context(client_order_id));
3621 let instruments = test_algo_instruments();
3622 let (emitter, mut receiver) = test_execution_emitter();
3623
3624 dispatch_test_message(
3625 OKXWsMessage::AlgoOrders(vec![parent]),
3626 &emitter,
3627 &state,
3628 &instruments,
3629 );
3630 assert_eq!(drain_execution_events(&mut receiver).len(), 3);
3631
3632 let mut child = load_regular_order_messages("ws_orders_trigger.json").remove(0);
3633 child.algo_id = Some(parent_venue_order_id.clone());
3634 child.linked_algo_ord = Some(crate::websocket::messages::OKXLinkedAlgoOrd {
3635 algo_id: parent_venue_order_id,
3636 });
3637 child.ord_id = Ustr::from(child_venue_order_id.as_str());
3638 let mut canceled = child.clone();
3639 canceled.state = OKXOrderStatus::Canceled;
3640 canceled.acc_fill_sz = Some("0".to_string());
3641 canceled.fill_sz.clear();
3642 canceled.fill_px.clear();
3643 canceled.trade_id.clear();
3644 dispatch_test_message(
3645 OKXWsMessage::Orders(vec![canceled]),
3646 &emitter,
3647 &state,
3648 &instruments,
3649 );
3650
3651 let events = drain_execution_events(&mut receiver);
3652 assert_eq!(events.len(), 1);
3653 assert!(matches!(
3654 &events[0],
3655 ExecutionEvent::Order(OrderEventAny::Canceled(canceled))
3656 if canceled.client_order_id == client_order_id
3657 && canceled.venue_order_id
3658 == Some(VenueOrderId::new(child_venue_order_id.as_str()))
3659 ));
3660 assert!(state.contains_terminal(&client_order_id));
3661
3662 dispatch_test_message(
3663 OKXWsMessage::Orders(vec![child.clone()]),
3664 &emitter,
3665 &state,
3666 &instruments,
3667 );
3668 let events = drain_execution_events(&mut receiver);
3669 assert_eq!(events.len(), 1);
3670 assert!(matches!(
3671 &events[0],
3672 ExecutionEvent::Report(CommonExecutionReport::Fill(report))
3673 if report.client_order_id == Some(client_order_id)
3674 && report.venue_order_id
3675 == VenueOrderId::new(child_venue_order_id.as_str())
3676 && report.trade_id == TradeId::new("1518905530")
3677 ));
3678
3679 dispatch_test_message(
3680 OKXWsMessage::Orders(vec![child]),
3681 &emitter,
3682 &state,
3683 &instruments,
3684 );
3685 assert!(drain_execution_events(&mut receiver).is_empty());
3686 }
3687
3688 #[rstest]
3689 fn reconnect_replay_and_stale_parent_acceptance_are_suppressed() {
3690 let message = load_algo_order_messages("ws_orders_algo_states.json").remove(0);
3691 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3692 let state = WsDispatchState::default();
3693 state.track_order_context(test_algo_context(client_order_id));
3694 let instruments = test_algo_instruments();
3695 let (emitter, mut receiver) = test_execution_emitter();
3696
3697 dispatch_test_message(
3698 OKXWsMessage::AlgoOrders(vec![message.clone()]),
3699 &emitter,
3700 &state,
3701 &instruments,
3702 );
3703 assert_eq!(drain_execution_events(&mut receiver).len(), 3);
3704
3705 let mut stale_live = message.clone();
3706 stale_live.state = OKXAlgoOrderStatus::Live;
3707 stale_live.ord_id.clear();
3708 dispatch_test_message(OKXWsMessage::Reconnected, &emitter, &state, &instruments);
3709 dispatch_test_message(
3710 OKXWsMessage::AlgoOrders(vec![message, stale_live]),
3711 &emitter,
3712 &state,
3713 &instruments,
3714 );
3715
3716 assert!(drain_execution_events(&mut receiver).is_empty());
3717 assert_eq!(
3718 state
3719 .order_venue_binding(client_order_id)
3720 .map(|(venue_order_id, _)| venue_order_id),
3721 Some(VenueOrderId::new("706620792746730010"))
3722 );
3723 }
3724
3725 #[rstest]
3726 #[case::filled("ws_orders_algo.json", 4, 3)]
3727 #[case::partially_failed("ws_orders_algo_states.json", 4, 1)]
3728 fn tracked_algo_aggregate_state_never_enters_reconciliation(
3729 #[case] fixture: &str,
3730 #[case] index: usize,
3731 #[case] expected_order_events: usize,
3732 ) {
3733 let message = load_algo_order_messages(fixture).remove(index);
3734 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3735 let state = WsDispatchState::default();
3736 state.track_order_context(test_algo_context(client_order_id));
3737 let instruments = test_algo_instruments();
3738 let (emitter, mut receiver) = test_execution_emitter();
3739
3740 dispatch_test_message(
3741 OKXWsMessage::AlgoOrders(vec![message]),
3742 &emitter,
3743 &state,
3744 &instruments,
3745 );
3746
3747 let events = drain_execution_events(&mut receiver);
3748 assert_eq!(events.len(), expected_order_events);
3749 assert!(
3750 events
3751 .iter()
3752 .all(|event| matches!(event, ExecutionEvent::Order(_)))
3753 );
3754 assert!(state.order_contexts.contains_key(&client_order_id));
3755 assert!(!state.contains_terminal(&client_order_id));
3756 }
3757
3758 #[rstest]
3759 fn tracked_algo_cancel_is_terminal_and_stale_live_is_suppressed() {
3760 let mut message = load_algo_order_messages("ws_orders_algo.json").remove(3);
3761 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3762 let state = WsDispatchState::default();
3763 state.track_order_context(test_algo_context(client_order_id));
3764 let instruments = test_algo_instruments();
3765 let (emitter, mut receiver) = test_execution_emitter();
3766
3767 dispatch_test_message(
3768 OKXWsMessage::AlgoOrders(vec![message.clone()]),
3769 &emitter,
3770 &state,
3771 &instruments,
3772 );
3773 message.state = OKXAlgoOrderStatus::Live;
3774 message.algo_cl_ord_id.clear();
3775 message.cl_ord_id.clear();
3776 dispatch_test_message(
3777 OKXWsMessage::AlgoOrders(vec![message]),
3778 &emitter,
3779 &state,
3780 &instruments,
3781 );
3782
3783 let events = drain_execution_events(&mut receiver);
3784 assert_eq!(events.len(), 2);
3785 assert!(matches!(
3786 &events[0],
3787 ExecutionEvent::Order(OrderEventAny::Accepted(_))
3788 ));
3789 assert!(matches!(
3790 &events[1],
3791 ExecutionEvent::Order(OrderEventAny::Canceled(canceled))
3792 if canceled.client_order_id == client_order_id
3793 ));
3794 assert!(state.contains_terminal(&client_order_id));
3795 assert!(!state.order_contexts.contains_key(&client_order_id));
3796 assert_eq!(state.order_venue_binding(client_order_id), None);
3797 }
3798
3799 #[rstest]
3800 fn tracked_algo_failure_uses_typed_rejection() {
3801 let mut message = load_algo_order_messages("ws_orders_algo_states.json").remove(3);
3802 message.fail_code = "51008".to_string();
3803 let client_order_id = ClientOrderId::new(message.algo_cl_ord_id.as_str());
3804 let state = WsDispatchState::default();
3805 state.track_order_context(test_algo_context(client_order_id));
3806 let instruments = test_algo_instruments();
3807 let (emitter, mut receiver) = test_execution_emitter();
3808
3809 dispatch_test_message(
3810 OKXWsMessage::AlgoOrders(vec![message]),
3811 &emitter,
3812 &state,
3813 &instruments,
3814 );
3815
3816 let events = drain_execution_events(&mut receiver);
3817 assert_eq!(events.len(), 1);
3818 assert!(matches!(
3819 &events[0],
3820 ExecutionEvent::Order(OrderEventAny::Rejected(rejected))
3821 if rejected.client_order_id == client_order_id
3822 && rejected.reason == Ustr::from("OKX error 51008")
3823 ));
3824 assert!(state.contains_terminal(&client_order_id));
3825 }
3826
3827 #[rstest]
3828 #[case("51000", "Rejected", "", "OKX error 51000: Rejected")]
3829 #[case(
3830 "51000",
3831 "Rejected",
3832 "51004",
3833 "OKX error 51000: Rejected (subCode=51004)"
3834 )]
3835 #[case("51000", "", "", "OKX error 51000")]
3836 #[case("51000", "", "51004", "OKX error 51000 (subCode=51004)")]
3837 #[case("", "Rejected", "", "Rejected")]
3838 #[case("", "Rejected", "51004", "Rejected (subCode=51004)")]
3839 #[case("", "", "51004", "subCode=51004")]
3840 #[case("", "", "", "")]
3841 fn test_format_order_response_reason(
3842 #[case] s_code: &str,
3843 #[case] s_msg: &str,
3844 #[case] sub_code: &str,
3845 #[case] expected: &str,
3846 ) {
3847 assert_eq!(
3848 format_order_response_reason(s_code, s_msg, sub_code),
3849 expected
3850 );
3851 }
3852
3853 #[rstest]
3854 #[case("order", false, false)]
3855 #[case("batch-orders", false, false)]
3856 #[case("batch-orders", false, true)]
3857 #[case("amend-order", true, false)]
3858 #[case("batch-amend-orders", true, false)]
3859 #[case("batch-amend-orders", true, true)]
3860 fn rpi_minimum_notional_rejection_preserves_order_lifecycle(
3861 #[case] case: &str,
3862 #[case] amend: bool,
3863 #[case] reverse: bool,
3864 ) {
3865 let fixtures: serde_json::Value =
3866 serde_json::from_str(include_str!("../../test_data/rpi_minimum_notional.json"))
3867 .unwrap();
3868 let mut response = fixtures[case]["response"].clone();
3869 if reverse {
3870 response["data"].as_array_mut().unwrap().reverse();
3871 }
3872 let frame: OKXWsFrame = serde_json::from_value(response.clone()).unwrap();
3873 let OKXWsFrame::OrderResponse {
3874 id,
3875 op,
3876 code,
3877 msg,
3878 data,
3879 } = frame
3880 else {
3881 panic!("Expected order response");
3882 };
3883 let state = WsDispatchState::default();
3884 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
3885 let strategy_id = StrategyId::from("RPI-003");
3886 let pending = if amend {
3887 &state.pending_amends
3888 } else {
3889 &state.pending_orders
3890 };
3891 let unrelated = "ORPI003";
3892 pending.insert(
3893 unrelated.to_string(),
3894 PendingOrderInfo {
3895 trader_id: TraderId::from("TRADER-001"),
3896 strategy_id,
3897 instrument_id,
3898 },
3899 );
3900 let mut originals = Vec::new();
3901 for item in &data {
3902 let client_order_id = ClientOrderId::new(item["clOrdId"].as_str().unwrap());
3903 let context = OrderContext {
3904 identity: OrderIdentity {
3905 client_order_id,
3906 strategy_id,
3907 instrument_id,
3908 order_side: OrderSide::Sell,
3909 order_type: OrderType::Limit,
3910 },
3911 quantity: Quantity::from("0.2"),
3912 price: Some(Price::from("65123")),
3913 trigger_price: None,
3914 trigger_type: None,
3915 time_in_force: TimeInForce::Gtc,
3916 is_post_only: true,
3917 is_reduce_only: false,
3918 is_quote_quantity: false,
3919 };
3920 state.track_order_context(context);
3921 state
3922 .order_identities
3923 .insert(client_order_id, context.identity);
3924 pending.insert(
3925 client_order_id.to_string(),
3926 PendingOrderInfo {
3927 trader_id: TraderId::from("TRADER-001"),
3928 strategy_id,
3929 instrument_id,
3930 },
3931 );
3932 originals.push(context);
3933 }
3934 let (emitter, mut receiver) = test_execution_emitter();
3935
3936 dispatch_test_message(
3937 OKXWsMessage::OrderResponse {
3938 id,
3939 op,
3940 code,
3941 msg,
3942 data,
3943 },
3944 &emitter,
3945 &state,
3946 &AtomicMap::new(),
3947 );
3948
3949 let events = drain_execution_events(&mut receiver);
3950 assert_eq!(events.len(), 1);
3951 let rejected_id = ClientOrderId::from("ORPI002");
3952 let s_msg = response["data"]
3953 .as_array()
3954 .unwrap()
3955 .iter()
3956 .find(|item| item["sCode"] == "54051")
3957 .unwrap()["sMsg"]
3958 .as_str()
3959 .unwrap();
3960 let reason = format!("OKX error 54051: {s_msg}");
3961
3962 match &events[0] {
3963 ExecutionEvent::Order(OrderEventAny::Rejected(event)) if !amend => {
3964 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3965 assert_eq!(event.strategy_id, strategy_id);
3966 assert_eq!(event.instrument_id, instrument_id);
3967 assert_eq!(event.client_order_id, rejected_id);
3968 assert_eq!(event.account_id, AccountId::from("OKX-001"));
3969 assert_eq!(event.reason.as_str(), reason);
3970 assert!(!event.reconciliation);
3971 assert!(!event.due_post_only);
3972 }
3973 ExecutionEvent::Order(OrderEventAny::ModifyRejected(event)) if amend => {
3974 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3975 assert_eq!(event.strategy_id, strategy_id);
3976 assert_eq!(event.instrument_id, instrument_id);
3977 assert_eq!(event.client_order_id, rejected_id);
3978 assert_eq!(event.account_id, Some(AccountId::from("OKX-001")));
3979 assert_eq!(
3980 event.venue_order_id,
3981 Some(VenueOrderId::from("2500000000000000002"))
3982 );
3983 assert_eq!(event.reason.as_str(), reason);
3984 assert!(!event.reconciliation);
3985 }
3986 event => panic!("Unexpected event: {event:?}"),
3987 }
3988 assert_eq!(pending.len(), 1);
3989 assert!(pending.contains_key(unrelated));
3990
3991 for original in originals {
3992 let client_order_id = original.identity.client_order_id;
3993 if amend || client_order_id != rejected_id {
3994 assert_eq!(
3995 *state.order_identities.get(&client_order_id).unwrap(),
3996 original.identity
3997 );
3998 assert_eq!(
3999 *state.order_contexts.get(&client_order_id).unwrap(),
4000 original
4001 );
4002 assert!(!state.terminal_orders.contains(&client_order_id));
4003 } else {
4004 assert!(!state.order_identities.contains_key(&client_order_id));
4005 assert!(!state.order_contexts.contains_key(&client_order_id));
4006 }
4007 }
4008 }
4009
4010 #[rstest]
4011 #[case::ambiguous(OKXWsError::SendFailed("connection reset".to_string()), true)]
4012 #[case::not_sent(OKXWsError::NoActiveClient, false)]
4013 fn send_failure_preserves_only_ambiguous_pending_orders(
4014 #[case] error: OKXWsError,
4015 #[case] expected_pending: bool,
4016 ) {
4017 let client_order_ids = [
4018 ClientOrderId::from("O-batch-pending-1"),
4019 ClientOrderId::from("O-batch-pending-2"),
4020 ];
4021 let state = WsDispatchState::default();
4022 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
4023 let strategy_id = StrategyId::from("STRATEGY-001");
4024
4025 for client_order_id in client_order_ids {
4026 state.pending_orders.insert(
4027 client_order_id.to_string(),
4028 PendingOrderInfo {
4029 trader_id: TraderId::from("TRADER-001"),
4030 strategy_id,
4031 instrument_id,
4032 },
4033 );
4034 state.order_identities.insert(
4035 client_order_id,
4036 OrderIdentity {
4037 client_order_id,
4038 instrument_id,
4039 strategy_id,
4040 order_side: OrderSide::Buy,
4041 order_type: OrderType::Limit,
4042 },
4043 );
4044 }
4045
4046 let clock = get_atomic_clock_realtime();
4047 let mut emitter = ExecutionEventEmitter::new(
4048 clock,
4049 TraderId::from("TRADER-001"),
4050 AccountId::from("OKX-001"),
4051 AccountType::Margin,
4052 None,
4053 );
4054 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
4055 emitter.set_sender(sender);
4056 let instruments = AtomicMap::new();
4057 let mut fee_cache = FeeCache::new();
4058 let mut filled_qty_cache = FilledQtyCache::new();
4059 let mut order_state_cache = AHashMap::new();
4060
4061 dispatch_ws_message(
4062 OKXWsMessage::SendFailed {
4063 request_id: "req-batch-send-failure".to_string(),
4064 client_order_ids: client_order_ids.to_vec(),
4065 op: Some(OKXWsOperation::BatchOrders),
4066 error,
4067 },
4068 &emitter,
4069 &state,
4070 AccountId::from("OKX-001"),
4071 AccountType::Margin,
4072 &instruments,
4073 &mut fee_cache,
4074 &mut filled_qty_cache,
4075 &mut order_state_cache,
4076 clock,
4077 );
4078
4079 for client_order_id in client_order_ids {
4080 assert_eq!(
4081 state.pending_orders.contains_key(client_order_id.as_str()),
4082 expected_pending,
4083 "pending state mismatch for {client_order_id}"
4084 );
4085 }
4086 }
4087}