1use std::sync::atomic::{AtomicBool, Ordering};
24
25use ahash::AHashMap;
26use anyhow::Context;
27use dashmap::{DashMap, DashSet};
28use nautilus_core::{UUID4, UnixNanos, time::AtomicTime};
29use nautilus_live::ExecutionEventEmitter;
30use nautilus_model::{
31 enums::{LiquiditySide, OrderSide, OrderType},
32 events::{
33 OrderAccepted, OrderCanceled, OrderEventAny, OrderFilled, OrderTriggered, OrderUpdated,
34 },
35 identifiers::{
36 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, VenueOrderId,
37 },
38 instruments::{Instrument, InstrumentAny},
39 orders::TRIGGERABLE_ORDER_TYPES,
40 types::{Money, Price, Quantity},
41};
42use rust_decimal::Decimal;
43use ustr::Ustr;
44
45use super::{
46 messages::{
47 BybitWsAccountExecution, BybitWsAccountExecutionFast, BybitWsAccountOrder, BybitWsMessage,
48 },
49 parse::{
50 parse_millis_i64, parse_ws_account_state, parse_ws_fill_report_fast,
51 parse_ws_position_status_report,
52 },
53};
54use crate::{
55 common::{
56 enums::BybitOrderStatus,
57 parse::{
58 make_bybit_symbol, parse_millis_timestamp, parse_price_with_precision,
59 parse_quantity_with_precision,
60 },
61 },
62 http::error::is_bybit_ambiguous_order_error_code,
63};
64
65const DEDUP_CAPACITY: usize = 10_000;
66
67const BYBIT_OP_ORDER_CREATE: &str = "order.create";
68const BYBIT_OP_ORDER_AMEND: &str = "order.amend";
69const BYBIT_OP_ORDER_CANCEL: &str = "order.cancel";
70const BYBIT_OP_ORDER_CREATE_BATCH: &str = "order.create-batch";
71const BYBIT_OP_ORDER_AMEND_BATCH: &str = "order.amend-batch";
72const BYBIT_OP_ORDER_CANCEL_BATCH: &str = "order.cancel-batch";
73
74#[derive(Debug, Clone)]
77pub struct OrderIdentity {
78 pub instrument_id: InstrumentId,
79 pub strategy_id: StrategyId,
80 pub order_side: OrderSide,
81 pub order_type: OrderType,
82 pub venue_position_id: Option<PositionId>,
83}
84
85#[derive(Debug, Clone, Copy)]
87pub enum PendingOperation {
88 Place,
89 Cancel,
90 Amend,
91}
92
93pub type PendingRequestData = (
96 Vec<ClientOrderId>,
97 Vec<Option<VenueOrderId>>,
98 PendingOperation,
99);
100
101#[derive(Debug, Clone)]
105pub struct OrderStateSnapshot {
106 pub quantity: Quantity,
107 pub price: Option<Price>,
108 pub trigger_price: Option<Price>,
109}
110
111#[derive(Debug)]
112pub struct WsDispatchState {
113 pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
114 pub pending_requests: DashMap<String, PendingRequestData>,
115 pub order_snapshots: DashMap<ClientOrderId, OrderStateSnapshot>,
116 pub emitted_accepted: DashSet<ClientOrderId>,
117 pub triggered_orders: DashSet<ClientOrderId>,
118 pub filled_orders: DashSet<ClientOrderId>,
119 clearing: AtomicBool,
120}
121
122impl Default for WsDispatchState {
123 fn default() -> Self {
124 Self {
125 order_identities: DashMap::new(),
126 pending_requests: DashMap::new(),
127 order_snapshots: DashMap::new(),
128 emitted_accepted: DashSet::default(),
129 triggered_orders: DashSet::default(),
130 filled_orders: DashSet::default(),
131 clearing: AtomicBool::new(false),
132 }
133 }
134}
135
136impl WsDispatchState {
137 fn evict_if_full(&self, set: &DashSet<ClientOrderId>) {
138 if set.len() >= DEDUP_CAPACITY
139 && self
140 .clearing
141 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
142 .is_ok()
143 {
144 set.clear();
145 self.clearing.store(false, Ordering::Release);
146 }
147 }
148
149 fn insert_accepted(&self, cid: ClientOrderId) {
150 self.evict_if_full(&self.emitted_accepted);
151 self.emitted_accepted.insert(cid);
152 }
153
154 fn insert_filled(&self, cid: ClientOrderId) {
155 self.evict_if_full(&self.filled_orders);
156 self.filled_orders.insert(cid);
157 }
158
159 fn insert_triggered(&self, cid: ClientOrderId) {
160 self.evict_if_full(&self.triggered_orders);
161 self.triggered_orders.insert(cid);
162 }
163}
164
165pub fn dispatch_ws_message(
172 message: &BybitWsMessage,
173 emitter: &ExecutionEventEmitter,
174 state: &WsDispatchState,
175 account_id: AccountId,
176 instruments: &AHashMap<Ustr, InstrumentAny>,
177 clock: &AtomicTime,
178) {
179 match message {
180 BybitWsMessage::AccountOrder(msg) => {
181 let ts_init = clock.get_time_ns();
182
183 for order in &msg.data {
184 let symbol = make_bybit_symbol(order.symbol, order.category);
185 let Some(instrument) = instruments.get(&symbol) else {
186 log::warn!("No instrument for order update: {symbol}");
187 continue;
188 };
189 dispatch_order_update(order, instrument, emitter, state, account_id, ts_init);
190 }
191 }
192 BybitWsMessage::AccountExecution(msg) => {
193 let ts_init = clock.get_time_ns();
194
195 for exec in &msg.data {
196 let symbol = make_bybit_symbol(exec.symbol, exec.category);
197 let Some(instrument) = instruments.get(&symbol) else {
198 log::warn!("No instrument for execution update: {symbol}");
199 continue;
200 };
201 dispatch_execution_fill(exec, instrument, emitter, state, account_id, ts_init);
202 }
203 }
204 BybitWsMessage::AccountExecutionFast(msg) => {
205 let ts_init = clock.get_time_ns();
206
207 for exec in &msg.data {
208 let symbol = make_bybit_symbol(exec.symbol, exec.category);
209 let Some(instrument) = instruments.get(&symbol) else {
210 log::warn!("No instrument for fast-execution update: {symbol}");
211 continue;
212 };
213 dispatch_execution_fill_fast(exec, instrument, emitter, state, account_id, ts_init);
214 }
215 }
216 BybitWsMessage::AccountWallet(msg) => {
217 let ts_init = clock.get_time_ns();
218 let ts_event = parse_millis_i64(msg.creation_time, "wallet.creation_time")
219 .unwrap_or_else(|e| {
220 log::warn!("Failed to parse wallet creation_time, using ts_init: {e}");
221 ts_init
222 });
223
224 for wallet in &msg.data {
225 match parse_ws_account_state(wallet, account_id, ts_event, ts_init) {
226 Ok(state) => emitter.send_account_state(state),
227 Err(e) => log::error!("Failed to parse account state: {e}"),
228 }
229 }
230 }
231 BybitWsMessage::AccountPosition(msg) => {
232 let ts_init = clock.get_time_ns();
233
234 for position in &msg.data {
235 let symbol = make_bybit_symbol(position.symbol, position.category);
236 let Some(instrument) = instruments.get(&symbol) else {
237 log::warn!("No instrument for position update: {symbol}");
238 continue;
239 };
240
241 match parse_ws_position_status_report(position, account_id, instrument, ts_init) {
242 Ok(report) => emitter.send_position_report(report),
243 Err(e) => log::error!("Failed to parse position status report: {e}"),
244 }
245 }
246 }
247 BybitWsMessage::OrderResponse(resp) => {
248 let ts_init = clock.get_time_ns();
249 dispatch_order_response(resp, emitter, state, ts_init);
250 }
251 BybitWsMessage::Error(e) => {
252 log::warn!("WebSocket error: code={} message={}", e.code, e.message);
253 }
254 BybitWsMessage::Reconnected => {
255 log::info!("WebSocket reconnected");
256 }
257 BybitWsMessage::Auth(_)
258 | BybitWsMessage::Orderbook(_)
259 | BybitWsMessage::Trade(_)
260 | BybitWsMessage::Kline(_)
261 | BybitWsMessage::TickerLinear(_)
262 | BybitWsMessage::TickerOption(_) => {}
263 }
264}
265
266fn dispatch_order_update(
272 order: &BybitWsAccountOrder,
273 instrument: &InstrumentAny,
274 emitter: &ExecutionEventEmitter,
275 state: &WsDispatchState,
276 account_id: AccountId,
277 ts_init: UnixNanos,
278) {
279 let client_order_id = if order.order_link_id.is_empty() {
280 None
281 } else {
282 Some(ClientOrderId::new(order.order_link_id.as_str()))
283 };
284
285 let identity = client_order_id
286 .as_ref()
287 .and_then(|cid| state.order_identities.get(cid).map(|r| r.clone()));
288
289 if let (Some(client_order_id), Some(identity)) = (client_order_id, identity) {
290 let venue_order_id = VenueOrderId::new(order.order_id.as_str());
291
292 match order.order_status {
293 BybitOrderStatus::Created | BybitOrderStatus::New | BybitOrderStatus::Untriggered => {
294 let snapshot = parse_order_snapshot(order, instrument);
295
296 if state.emitted_accepted.contains(&client_order_id)
297 || state.filled_orders.contains(&client_order_id)
298 || state.triggered_orders.contains(&client_order_id)
299 {
300 if let Some(snapshot) = snapshot
301 && is_snapshot_updated(&snapshot, &client_order_id, state)
302 {
303 let updated = OrderUpdated::new(
304 emitter.trader_id(),
305 identity.strategy_id,
306 identity.instrument_id,
307 client_order_id,
308 snapshot.quantity,
309 UUID4::new(),
310 ts_init,
311 ts_init,
312 false,
313 Some(venue_order_id),
314 Some(account_id),
315 snapshot.price,
316 snapshot.trigger_price,
317 None,
318 false,
319 );
320 state.order_snapshots.insert(client_order_id, snapshot);
321 emitter.send_order_event(OrderEventAny::Updated(updated));
322 return;
323 }
324 log::debug!("Skipping duplicate Accepted for {client_order_id}");
325 return;
326 }
327
328 state.insert_accepted(client_order_id);
329
330 let venue_differs_from_submitted = snapshot
333 .as_ref()
334 .is_some_and(|s| is_snapshot_updated(s, &client_order_id, state));
335
336 if let Some(snapshot) = snapshot.as_ref() {
337 state
338 .order_snapshots
339 .insert(client_order_id, snapshot.clone());
340 }
341
342 let accepted = OrderAccepted::new(
343 emitter.trader_id(),
344 identity.strategy_id,
345 identity.instrument_id,
346 client_order_id,
347 venue_order_id,
348 account_id,
349 UUID4::new(),
350 ts_init,
351 ts_init,
352 false,
353 );
354 emitter.send_order_event(OrderEventAny::Accepted(accepted));
355
356 if venue_differs_from_submitted && let Some(snapshot) = snapshot {
357 let updated = OrderUpdated::new(
358 emitter.trader_id(),
359 identity.strategy_id,
360 identity.instrument_id,
361 client_order_id,
362 snapshot.quantity,
363 UUID4::new(),
364 ts_init,
365 ts_init,
366 false,
367 Some(venue_order_id),
368 Some(account_id),
369 snapshot.price,
370 snapshot.trigger_price,
371 None,
372 false,
373 );
374 emitter.send_order_event(OrderEventAny::Updated(updated));
375 }
376 }
377 BybitOrderStatus::Triggered => {
378 if state.filled_orders.contains(&client_order_id) {
379 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
380 return;
381 }
382
383 if !TRIGGERABLE_ORDER_TYPES.contains(&identity.order_type) {
384 log::debug!(
385 "Skipping OrderTriggered for {} order {client_order_id}: market-style stops have no TRIGGERED state",
386 identity.order_type,
387 );
388 return;
389 }
390
391 ensure_accepted_emitted(
392 client_order_id,
393 account_id,
394 venue_order_id,
395 &identity,
396 emitter,
397 state,
398 ts_init,
399 );
400 state.insert_triggered(client_order_id);
401 let triggered = OrderTriggered::new(
402 emitter.trader_id(),
403 identity.strategy_id,
404 identity.instrument_id,
405 client_order_id,
406 UUID4::new(),
407 ts_init,
408 ts_init,
409 false,
410 Some(venue_order_id),
411 Some(account_id),
412 );
413 emitter.send_order_event(OrderEventAny::Triggered(triggered));
414 }
415 BybitOrderStatus::Rejected => {
416 let filled_qty = parse_quantity_with_precision(
417 &order.cum_exec_qty,
418 instrument.size_precision(),
419 "order.cumExecQty",
420 )
421 .unwrap_or_default();
422
423 if filled_qty.is_positive() {
424 ensure_accepted_emitted(
426 client_order_id,
427 account_id,
428 venue_order_id,
429 &identity,
430 emitter,
431 state,
432 ts_init,
433 );
434 let canceled = OrderCanceled::new(
435 emitter.trader_id(),
436 identity.strategy_id,
437 identity.instrument_id,
438 client_order_id,
439 UUID4::new(),
440 ts_init,
441 ts_init,
442 false,
443 Some(venue_order_id),
444 Some(account_id),
445 );
446 cleanup_terminal(client_order_id, state);
447 emitter.send_order_event(OrderEventAny::Canceled(canceled));
448 } else {
449 let reason = if order.reject_reason.is_empty() {
450 Ustr::from("Order rejected by venue")
451 } else {
452 order.reject_reason
453 };
454 state.order_identities.remove(&client_order_id);
455 state.order_snapshots.remove(&client_order_id);
456 emitter.emit_order_rejected_event(
457 identity.strategy_id,
458 identity.instrument_id,
459 client_order_id,
460 reason.as_str(),
461 ts_init,
462 false,
463 );
464 }
465 }
466 BybitOrderStatus::PartiallyFilled => {
467 ensure_accepted_emitted(
470 client_order_id,
471 account_id,
472 venue_order_id,
473 &identity,
474 emitter,
475 state,
476 ts_init,
477 );
478
479 if let Some(snapshot) = parse_order_snapshot(order, instrument)
483 && is_snapshot_updated(&snapshot, &client_order_id, state)
484 {
485 let updated = OrderUpdated::new(
486 emitter.trader_id(),
487 identity.strategy_id,
488 identity.instrument_id,
489 client_order_id,
490 snapshot.quantity,
491 UUID4::new(),
492 ts_init,
493 ts_init,
494 false,
495 Some(venue_order_id),
496 Some(account_id),
497 snapshot.price,
498 snapshot.trigger_price,
499 None,
500 false,
501 );
502 state.order_snapshots.insert(client_order_id, snapshot);
503 emitter.send_order_event(OrderEventAny::Updated(updated));
504 }
505 }
506 BybitOrderStatus::Filled => {
507 ensure_accepted_emitted(
510 client_order_id,
511 account_id,
512 venue_order_id,
513 &identity,
514 emitter,
515 state,
516 ts_init,
517 );
518
519 if let Some(snapshot) = parse_order_snapshot(order, instrument)
522 && is_snapshot_updated(&snapshot, &client_order_id, state)
523 {
524 let updated = OrderUpdated::new(
525 emitter.trader_id(),
526 identity.strategy_id,
527 identity.instrument_id,
528 client_order_id,
529 snapshot.quantity,
530 UUID4::new(),
531 ts_init,
532 ts_init,
533 false,
534 Some(venue_order_id),
535 Some(account_id),
536 snapshot.price,
537 snapshot.trigger_price,
538 None,
539 false,
540 );
541 state.order_snapshots.insert(client_order_id, snapshot);
542 emitter.send_order_event(OrderEventAny::Updated(updated));
543 }
544 }
548 BybitOrderStatus::Canceled
549 | BybitOrderStatus::PartiallyFilledCanceled
550 | BybitOrderStatus::Deactivated => {
551 ensure_accepted_emitted(
552 client_order_id,
553 account_id,
554 venue_order_id,
555 &identity,
556 emitter,
557 state,
558 ts_init,
559 );
560 let canceled = OrderCanceled::new(
561 emitter.trader_id(),
562 identity.strategy_id,
563 identity.instrument_id,
564 client_order_id,
565 UUID4::new(),
566 ts_init,
567 ts_init,
568 false,
569 Some(venue_order_id),
570 Some(account_id),
571 );
572 cleanup_terminal(client_order_id, state);
573 emitter.send_order_event(OrderEventAny::Canceled(canceled));
574 }
575 }
576 } else {
577 match super::parse::parse_ws_order_status_report(order, instrument, account_id, ts_init) {
579 Ok(report) => emitter.send_order_status_report(report),
580 Err(e) => log::error!("Failed to parse order status report: {e}"),
581 }
582 }
583}
584
585fn dispatch_execution_fill(
590 exec: &BybitWsAccountExecution,
591 instrument: &InstrumentAny,
592 emitter: &ExecutionEventEmitter,
593 state: &WsDispatchState,
594 account_id: AccountId,
595 ts_init: UnixNanos,
596) {
597 if exec.exec_type.is_exchange_generated() {
598 log::warn!(
599 "Exchange-generated execution: exec_type={:?}, symbol={}, order_id={}, order_link_id={}, side={:?}, qty={}, price={}",
600 exec.exec_type,
601 exec.symbol,
602 exec.order_id,
603 exec.order_link_id,
604 exec.side,
605 exec.exec_qty,
606 exec.exec_price,
607 );
608 }
609
610 let client_order_id = if exec.order_link_id.is_empty() {
611 None
612 } else {
613 Some(ClientOrderId::new(exec.order_link_id.as_str()))
614 };
615
616 let identity = client_order_id
617 .as_ref()
618 .and_then(|cid| state.order_identities.get(cid).map(|r| r.clone()));
619
620 if let (Some(client_order_id), Some(identity)) = (client_order_id, identity) {
621 let venue_order_id = VenueOrderId::new(exec.order_id.as_str());
622
623 ensure_accepted_emitted(
624 client_order_id,
625 account_id,
626 venue_order_id,
627 &identity,
628 emitter,
629 state,
630 ts_init,
631 );
632
633 match parse_order_filled(exec, instrument, &identity, emitter, account_id, ts_init) {
634 Ok(filled) => {
635 state.insert_filled(client_order_id);
636 state.triggered_orders.remove(&client_order_id);
637 emitter.send_order_event(OrderEventAny::Filled(filled));
638
639 if exec.leaves_qty == "0" {
640 cleanup_terminal(client_order_id, state);
641 }
642 }
643 Err(e) => log::error!("Failed to parse OrderFilled for {client_order_id}: {e}"),
644 }
645 } else {
646 match super::parse::parse_ws_fill_report(exec, account_id, instrument, ts_init) {
648 Ok(report) => emitter.send_fill_report(report),
649 Err(e) => log::error!("Failed to parse fill report: {e}"),
650 }
651 }
652}
653
654fn dispatch_execution_fill_fast(
661 exec: &BybitWsAccountExecutionFast,
662 instrument: &InstrumentAny,
663 emitter: &ExecutionEventEmitter,
664 state: &WsDispatchState,
665 account_id: AccountId,
666 ts_init: UnixNanos,
667) {
668 let client_order_id = if exec.order_link_id.is_empty() {
669 None
670 } else {
671 Some(ClientOrderId::new(exec.order_link_id.as_str()))
672 };
673
674 let mut venue_position_id = None;
675
676 if let Some(cid) = client_order_id.as_ref()
677 && let Some(identity) = state.order_identities.get(cid).map(|r| r.clone())
678 {
679 venue_position_id = identity.venue_position_id;
680 let venue_order_id = VenueOrderId::new(exec.order_id.as_str());
681 ensure_accepted_emitted(
682 *cid,
683 account_id,
684 venue_order_id,
685 &identity,
686 emitter,
687 state,
688 ts_init,
689 );
690 }
691
692 match parse_ws_fill_report_fast(exec, account_id, instrument, venue_position_id, ts_init) {
693 Ok(report) => emitter.send_fill_report(report),
694 Err(e) => log::error!("Failed to parse fast fill report: {e}"),
695 }
696}
697
698fn parse_order_filled(
700 exec: &BybitWsAccountExecution,
701 instrument: &InstrumentAny,
702 identity: &OrderIdentity,
703 emitter: &ExecutionEventEmitter,
704 account_id: AccountId,
705 ts_init: UnixNanos,
706) -> anyhow::Result<OrderFilled> {
707 let client_order_id = ClientOrderId::new(exec.order_link_id.as_str());
708 let venue_order_id = VenueOrderId::new(exec.order_id.as_str());
709 let trade_id =
710 TradeId::new_checked(exec.exec_id.as_str()).context("invalid execId in Bybit execution")?;
711
712 let last_qty = parse_quantity_with_precision(
713 &exec.exec_qty,
714 instrument.size_precision(),
715 "execution.execQty",
716 )?;
717 let last_px = parse_price_with_precision(
718 &exec.exec_price,
719 instrument.price_precision(),
720 "execution.execPrice",
721 )?;
722
723 let liquidity_side = if exec.is_maker {
724 LiquiditySide::Maker
725 } else {
726 LiquiditySide::Taker
727 };
728
729 let fee_decimal: Decimal = exec
730 .exec_fee
731 .parse()
732 .with_context(|| format!("failed to parse execFee='{}'", exec.exec_fee))?;
733 let commission_currency = instrument.quote_currency();
734 let commission = Money::from_decimal(fee_decimal, commission_currency).with_context(|| {
735 format!(
736 "failed to create commission from execFee='{}'",
737 exec.exec_fee
738 )
739 })?;
740
741 let ts_event = parse_millis_timestamp(&exec.exec_time, "execution.execTime")?;
742
743 Ok(OrderFilled::new(
744 emitter.trader_id(),
745 identity.strategy_id,
746 identity.instrument_id,
747 client_order_id,
748 venue_order_id,
749 account_id,
750 trade_id,
751 identity.order_side,
752 identity.order_type,
753 last_qty,
754 last_px,
755 commission_currency,
756 liquidity_side,
757 UUID4::new(),
758 ts_event,
759 ts_init,
760 false,
761 identity.venue_position_id,
762 Some(commission),
763 ))
764}
765
766fn dispatch_order_response(
768 resp: &super::messages::BybitWsOrderResponse,
769 emitter: &ExecutionEventEmitter,
770 state: &WsDispatchState,
771 ts_init: UnixNanos,
772) {
773 if resp.ret_code == 0 {
774 let pending = resp
776 .req_id
777 .as_ref()
778 .and_then(|rid| state.pending_requests.remove(rid))
779 .map(|(_, v)| v);
780
781 if let Some((cids, voids, pending_op)) = pending {
782 let batch_errors = resp.extract_batch_errors();
783 let data_array = resp.data.as_array();
784
785 for (idx, error) in batch_errors.iter().enumerate() {
786 if error.code == 0 {
787 continue;
788 }
789
790 if is_bybit_ambiguous_order_error_code(error.code) {
791 log::warn!(
792 "Ambiguous batch order item failure at index {idx}: code={}, msg={}; awaiting reconciliation",
793 error.code,
794 error.msg,
795 );
796 continue;
797 }
798
799 let cid = data_array
801 .and_then(|arr| arr.get(idx))
802 .and_then(extract_order_link_id_from_data)
803 .or_else(|| cids.get(idx).copied());
804
805 let Some(cid) = cid else {
806 log::warn!(
807 "Batch error at index {idx} without correlation: code={}, msg={}",
808 error.code,
809 error.msg,
810 );
811 continue;
812 };
813
814 let Some(identity) = state.order_identities.get(&cid).map(|r| r.clone()) else {
815 log::warn!(
816 "Batch error for untracked order: client_order_id={cid}, msg={}",
817 error.msg,
818 );
819 continue;
820 };
821
822 let stored_void = voids.get(idx).and_then(|v| *v);
823
824 emit_rejection_for_op(
825 &pending_op,
826 cid,
827 &identity,
828 stored_void,
829 &error.msg,
830 emitter,
831 state,
832 ts_init,
833 );
834 }
835 }
836 return;
837 }
838
839 let pending = resp
841 .req_id
842 .as_ref()
843 .and_then(|rid| state.pending_requests.remove(rid))
844 .map(|(_, v)| v);
845
846 let is_batch_response = is_batch_order_op(resp.op.as_str())
847 || pending.as_ref().is_some_and(|(cids, _, _)| cids.len() > 1);
848
849 if is_batch_response {
850 let order_count = pending.as_ref().map_or(0, |(cids, _, _)| cids.len());
851 log::warn!(
852 "Ambiguous batch order response failure for {order_count} orders: op={}, ret_code={}, ret_msg={}; awaiting reconciliation",
853 resp.op,
854 resp.ret_code,
855 resp.ret_msg,
856 );
857 return;
858 }
859
860 if is_bybit_ambiguous_order_error_code(resp.ret_code) {
861 log::warn!(
862 "Ambiguous order response failure: op={}, ret_code={}, ret_msg={}; awaiting reconciliation",
863 resp.op,
864 resp.ret_code,
865 resp.ret_msg,
866 );
867 return;
868 }
869
870 let effective_op = pending
871 .as_ref()
872 .map(|(_, _, op)| *op)
873 .or_else(|| pending_op_from_str(resp.op.as_str()))
874 .unwrap_or_else(|| {
875 log::warn!("Unknown order operation '{}', defaulting to Place", resp.op);
876 PendingOperation::Place
877 });
878
879 let client_order_id = extract_order_link_id_from_data(&resp.data).or_else(|| {
881 pending
882 .as_ref()
883 .and_then(|(cids, _, _)| cids.first().copied())
884 });
885
886 let stored_venue_order_id = pending
887 .as_ref()
888 .and_then(|(_, voids, _)| voids.first().and_then(|v| *v));
889
890 let Some(client_order_id) = client_order_id else {
891 log::warn!(
892 "Order response error without correlation: op={}, ret_code={}, ret_msg={}, req_id={:?}",
893 resp.op,
894 resp.ret_code,
895 resp.ret_msg,
896 resp.req_id,
897 );
898 return;
899 };
900
901 let Some(identity) = state
902 .order_identities
903 .get(&client_order_id)
904 .map(|r| r.clone())
905 else {
906 log::warn!(
907 "Order response error for untracked order: op={}, client_order_id={client_order_id}, ret_msg={}",
908 resp.op,
909 resp.ret_msg,
910 );
911 return;
912 };
913
914 let venue_order_id = extract_venue_order_id_from_data(&resp.data).or(stored_venue_order_id);
915
916 emit_rejection_for_op(
917 &effective_op,
918 client_order_id,
919 &identity,
920 venue_order_id,
921 &resp.ret_msg,
922 emitter,
923 state,
924 ts_init,
925 );
926}
927
928#[expect(clippy::too_many_arguments)]
930fn emit_rejection_for_op(
931 pending_op: &PendingOperation,
932 client_order_id: ClientOrderId,
933 identity: &OrderIdentity,
934 venue_order_id: Option<VenueOrderId>,
935 reason: &str,
936 emitter: &ExecutionEventEmitter,
937 state: &WsDispatchState,
938 ts_init: UnixNanos,
939) {
940 match pending_op {
941 PendingOperation::Place => {
942 state.order_identities.remove(&client_order_id);
943 state.order_snapshots.remove(&client_order_id);
944 emitter.emit_order_rejected_event(
945 identity.strategy_id,
946 identity.instrument_id,
947 client_order_id,
948 reason,
949 ts_init,
950 false,
951 );
952 }
953 PendingOperation::Cancel => {
954 emitter.emit_order_cancel_rejected_event(
955 identity.strategy_id,
956 identity.instrument_id,
957 client_order_id,
958 venue_order_id,
959 reason,
960 ts_init,
961 );
962 }
963 PendingOperation::Amend => {
964 emitter.emit_order_modify_rejected_event(
965 identity.strategy_id,
966 identity.instrument_id,
967 client_order_id,
968 venue_order_id,
969 reason,
970 ts_init,
971 );
972 }
973 }
974}
975
976fn pending_op_from_str(op: &str) -> Option<PendingOperation> {
978 match op {
979 BYBIT_OP_ORDER_CREATE => Some(PendingOperation::Place),
980 BYBIT_OP_ORDER_CANCEL => Some(PendingOperation::Cancel),
981 BYBIT_OP_ORDER_AMEND => Some(PendingOperation::Amend),
982 _ => None,
983 }
984}
985
986fn is_batch_order_op(op: &str) -> bool {
987 matches!(
988 op,
989 BYBIT_OP_ORDER_CREATE_BATCH | BYBIT_OP_ORDER_AMEND_BATCH | BYBIT_OP_ORDER_CANCEL_BATCH
990 )
991}
992
993fn parse_order_snapshot(
995 order: &BybitWsAccountOrder,
996 instrument: &InstrumentAny,
997) -> Option<OrderStateSnapshot> {
998 let quantity =
999 parse_quantity_with_precision(&order.qty, instrument.size_precision(), "order.qty").ok()?;
1000
1001 let price = if !order.price.is_empty() && order.price != "0" {
1002 parse_price_with_precision(&order.price, instrument.price_precision(), "order.price").ok()
1003 } else {
1004 None
1005 };
1006
1007 let trigger_price = if !order.trigger_price.is_empty() && order.trigger_price != "0" {
1008 parse_price_with_precision(
1009 &order.trigger_price,
1010 instrument.price_precision(),
1011 "order.triggerPrice",
1012 )
1013 .ok()
1014 } else {
1015 None
1016 };
1017
1018 Some(OrderStateSnapshot {
1019 quantity,
1020 price,
1021 trigger_price,
1022 })
1023}
1024
1025fn is_snapshot_updated(
1027 snapshot: &OrderStateSnapshot,
1028 client_order_id: &ClientOrderId,
1029 state: &WsDispatchState,
1030) -> bool {
1031 let Some(previous) = state.order_snapshots.get(client_order_id) else {
1032 return false;
1033 };
1034
1035 if let (Some(prev_price), Some(new_price)) = (previous.price, snapshot.price)
1036 && prev_price != new_price
1037 {
1038 return true;
1039 }
1040
1041 if let (Some(prev_trigger), Some(new_trigger)) =
1042 (previous.trigger_price, snapshot.trigger_price)
1043 && prev_trigger != new_trigger
1044 {
1045 return true;
1046 }
1047
1048 previous.quantity != snapshot.quantity
1049}
1050
1051fn ensure_accepted_emitted(
1054 client_order_id: ClientOrderId,
1055 account_id: AccountId,
1056 venue_order_id: VenueOrderId,
1057 identity: &OrderIdentity,
1058 emitter: &ExecutionEventEmitter,
1059 state: &WsDispatchState,
1060 ts_init: UnixNanos,
1061) {
1062 if state.emitted_accepted.contains(&client_order_id) {
1063 return;
1064 }
1065 state.insert_accepted(client_order_id);
1066 let accepted = OrderAccepted::new(
1067 emitter.trader_id(),
1068 identity.strategy_id,
1069 identity.instrument_id,
1070 client_order_id,
1071 venue_order_id,
1072 account_id,
1073 UUID4::new(),
1074 ts_init,
1075 ts_init,
1076 false,
1077 );
1078 emitter.send_order_event(OrderEventAny::Accepted(accepted));
1079}
1080
1081fn cleanup_terminal(client_order_id: ClientOrderId, state: &WsDispatchState) {
1083 state.order_identities.remove(&client_order_id);
1084 state.order_snapshots.remove(&client_order_id);
1085 state.emitted_accepted.remove(&client_order_id);
1086 state.triggered_orders.remove(&client_order_id);
1087 state.filled_orders.remove(&client_order_id);
1088}
1089
1090fn extract_order_link_id_from_data(data: &serde_json::Value) -> Option<ClientOrderId> {
1092 data.get("orderLinkId")
1093 .and_then(|v| v.as_str())
1094 .filter(|s| !s.is_empty())
1095 .map(ClientOrderId::new)
1096}
1097
1098fn extract_venue_order_id_from_data(data: &serde_json::Value) -> Option<VenueOrderId> {
1100 data.get("orderId")
1101 .and_then(|v| v.as_str())
1102 .filter(|s| !s.is_empty())
1103 .map(VenueOrderId::new)
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use ahash::AHashMap;
1109 use nautilus_common::messages::{ExecutionEvent, execution::ExecutionReport};
1110 use nautilus_core::{
1111 UnixNanos,
1112 time::{AtomicTime, get_atomic_clock_realtime},
1113 };
1114 use nautilus_live::emitter::ExecutionEventEmitter;
1115 use nautilus_model::{
1116 enums::{AccountType, OrderSide, OrderType},
1117 events::OrderEventAny,
1118 identifiers::{
1119 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId, VenueOrderId,
1120 },
1121 instruments::{Instrument, InstrumentAny},
1122 };
1123 use rstest::rstest;
1124 use ustr::Ustr;
1125
1126 use super::*;
1127 use crate::{
1128 common::{
1129 enums::{BybitOrderSide, BybitProductType},
1130 parse::parse_linear_instrument,
1131 testing::load_test_json,
1132 },
1133 http::models::{BybitFeeRate, BybitInstrumentLinearResponse},
1134 websocket::messages::{
1135 BybitWsAccountExecutionFastMsg, BybitWsMessage, BybitWsOrderResponse,
1136 },
1137 };
1138
1139 fn sample_fee_rate(
1140 symbol: &str,
1141 taker: &str,
1142 maker: &str,
1143 base_coin: Option<&str>,
1144 ) -> BybitFeeRate {
1145 BybitFeeRate {
1146 symbol: Ustr::from(symbol),
1147 taker_fee_rate: taker.to_string(),
1148 maker_fee_rate: maker.to_string(),
1149 base_coin: base_coin.map(Ustr::from),
1150 }
1151 }
1152
1153 fn linear_instrument() -> InstrumentAny {
1154 let json = load_test_json("http_get_instruments_linear.json");
1155 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1156 let instrument = &response.result.list[0];
1157 let fee_rate = sample_fee_rate("BTCUSDT", "0.00055", "0.0001", Some("BTC"));
1158 let ts = UnixNanos::new(1_700_000_000_000_000_000);
1159 parse_linear_instrument(instrument, &fee_rate, ts, ts).unwrap()
1160 }
1161
1162 fn build_instruments(instruments: &[InstrumentAny]) -> AHashMap<Ustr, InstrumentAny> {
1163 let mut map = AHashMap::new();
1164 for inst in instruments {
1165 map.insert(inst.id().symbol.inner(), inst.clone());
1166 }
1167 map
1168 }
1169
1170 fn test_account_id() -> AccountId {
1171 AccountId::from("BYBIT-001")
1172 }
1173
1174 fn create_emitter() -> (
1175 ExecutionEventEmitter,
1176 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1177 ) {
1178 let clock = get_atomic_clock_realtime();
1179 let trader_id = TraderId::from("TESTER-001");
1180 let account_id = test_account_id();
1181 let mut emitter =
1182 ExecutionEventEmitter::new(clock, trader_id, account_id, AccountType::Margin, None);
1183 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1184 emitter.set_sender(tx);
1185 (emitter, rx)
1186 }
1187
1188 fn default_identity() -> OrderIdentity {
1189 OrderIdentity {
1190 instrument_id: InstrumentId::from("BTCUSDT-LINEAR.BYBIT"),
1191 strategy_id: StrategyId::from("S-001"),
1192 order_side: OrderSide::Buy,
1193 order_type: OrderType::Limit,
1194 venue_position_id: None,
1195 }
1196 }
1197
1198 #[rstest]
1199 fn test_dispatch_tracked_canceled_order_emits_accepted_then_canceled() {
1200 let instrument = linear_instrument();
1201 let instruments = build_instruments(std::slice::from_ref(&instrument));
1202 let (emitter, mut rx) = create_emitter();
1203 let clock = get_atomic_clock_realtime();
1204 let state = WsDispatchState::default();
1205
1206 let json = load_test_json("ws_account_order.json");
1208 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1209 serde_json::from_str(&json).unwrap();
1210
1211 if let Some(order) = msg.data.first()
1212 && !order.order_link_id.is_empty()
1213 {
1214 let cid = ClientOrderId::new(order.order_link_id.as_str());
1215 state.order_identities.insert(cid, default_identity());
1216 }
1217
1218 let ws_msg = BybitWsMessage::AccountOrder(msg);
1219 dispatch_ws_message(
1220 &ws_msg,
1221 &emitter,
1222 &state,
1223 test_account_id(),
1224 &instruments,
1225 clock,
1226 );
1227
1228 let event1 = rx.try_recv().unwrap();
1230 assert!(
1231 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(ref a)) if a.strategy_id == StrategyId::from("S-001")),
1232 "Expected Accepted, found {event1:?}"
1233 );
1234
1235 let event2 = rx.try_recv().unwrap();
1237 assert!(
1238 matches!(event2, ExecutionEvent::Order(OrderEventAny::Canceled(_))),
1239 "Expected Canceled, found {event2:?}"
1240 );
1241 }
1242
1243 #[rstest]
1244 fn test_dispatch_untracked_order_emits_report() {
1245 let instrument = linear_instrument();
1246 let instruments = build_instruments(std::slice::from_ref(&instrument));
1247 let (emitter, mut rx) = create_emitter();
1248 let clock = get_atomic_clock_realtime();
1249 let state = WsDispatchState::default();
1250
1251 let json = load_test_json("ws_account_order.json");
1252 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1253 serde_json::from_str(&json).unwrap();
1254
1255 let ws_msg = BybitWsMessage::AccountOrder(msg);
1257 dispatch_ws_message(
1258 &ws_msg,
1259 &emitter,
1260 &state,
1261 test_account_id(),
1262 &instruments,
1263 clock,
1264 );
1265
1266 let event = rx.try_recv().unwrap();
1267 assert!(matches!(
1268 event,
1269 ExecutionEvent::Report(ExecutionReport::Order(_))
1270 ));
1271 }
1272
1273 #[rstest]
1274 fn test_dispatch_tracked_execution_emits_order_filled() {
1275 let instrument = linear_instrument();
1276 let instruments = build_instruments(std::slice::from_ref(&instrument));
1277 let (emitter, mut rx) = create_emitter();
1278 let clock = get_atomic_clock_realtime();
1279 let state = WsDispatchState::default();
1280
1281 let json = load_test_json("ws_account_execution.json");
1282 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1283 serde_json::from_str(&json).unwrap();
1284
1285 if let Some(exec) = msg.data.first()
1287 && !exec.order_link_id.is_empty()
1288 {
1289 let cid = ClientOrderId::new(exec.order_link_id.as_str());
1290 state.order_identities.insert(cid, default_identity());
1291 }
1292
1293 let ws_msg = BybitWsMessage::AccountExecution(msg);
1294 dispatch_ws_message(
1295 &ws_msg,
1296 &emitter,
1297 &state,
1298 test_account_id(),
1299 &instruments,
1300 clock,
1301 );
1302
1303 let event1 = rx.try_recv().unwrap();
1305 assert!(
1306 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
1307 "Expected Accepted, found {event1:?}"
1308 );
1309
1310 let event2 = rx.try_recv().unwrap();
1312 match event2 {
1313 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1314 assert_eq!(filled.strategy_id, StrategyId::from("S-001"));
1315 assert_eq!(filled.order_side, OrderSide::Buy);
1316 assert_eq!(filled.order_type, OrderType::Limit);
1317 }
1318 other => panic!("Expected Filled event, found {other:?}"),
1319 }
1320 }
1321
1322 #[rstest]
1323 fn test_dispatch_tracked_execution_preserves_venue_position_id() {
1324 let instrument = linear_instrument();
1325 let instruments = build_instruments(std::slice::from_ref(&instrument));
1326 let (emitter, mut rx) = create_emitter();
1327 let clock = get_atomic_clock_realtime();
1328 let state = WsDispatchState::default();
1329
1330 let json = load_test_json("ws_account_execution.json");
1331 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1332 serde_json::from_str(&json).unwrap();
1333 let venue_position_id = PositionId::from("BTCUSDT-LINEAR.BYBIT-LONG");
1334
1335 if let Some(exec) = msg.data.first()
1336 && !exec.order_link_id.is_empty()
1337 {
1338 let cid = ClientOrderId::new(exec.order_link_id.as_str());
1339 state.order_identities.insert(
1340 cid,
1341 OrderIdentity {
1342 venue_position_id: Some(venue_position_id),
1343 ..default_identity()
1344 },
1345 );
1346 }
1347
1348 let ws_msg = BybitWsMessage::AccountExecution(msg);
1349 dispatch_ws_message(
1350 &ws_msg,
1351 &emitter,
1352 &state,
1353 test_account_id(),
1354 &instruments,
1355 clock,
1356 );
1357
1358 let _accepted = rx.try_recv().unwrap();
1359 let event = rx.try_recv().unwrap();
1360 match event {
1361 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1362 assert_eq!(filled.position_id, Some(venue_position_id));
1363 }
1364 other => panic!("Expected Filled event, found {other:?}"),
1365 }
1366 }
1367
1368 fn fast_execution_msg(is_maker: bool, order_link_id: &str) -> BybitWsAccountExecutionFastMsg {
1369 BybitWsAccountExecutionFastMsg {
1370 topic: Ustr::from("execution.fast"),
1371 id: String::new(),
1372 creation_time: 1_716_800_399_338,
1373 data: vec![BybitWsAccountExecutionFast {
1374 category: BybitProductType::Linear,
1375 symbol: Ustr::from("BTCUSDT"),
1376 exec_id: "fast-1".to_string(),
1377 exec_price: "50000.0".to_string(),
1378 exec_qty: "0.5".to_string(),
1379 order_id: Ustr::from("ord-1"),
1380 order_link_id: Ustr::from(order_link_id),
1381 side: BybitOrderSide::Buy,
1382 exec_time: "1716800399334".to_string(),
1383 is_maker,
1384 seq: 42,
1385 }],
1386 }
1387 }
1388
1389 #[rstest]
1390 fn test_dispatch_tracked_fast_execution_preserves_venue_position_id() {
1391 let instrument = linear_instrument();
1392 let instruments = build_instruments(std::slice::from_ref(&instrument));
1393 let (emitter, mut rx) = create_emitter();
1394 let clock = get_atomic_clock_realtime();
1395 let state = WsDispatchState::default();
1396 let venue_position_id = PositionId::from("BTCUSDT-LINEAR.BYBIT-LONG");
1397
1398 let msg = fast_execution_msg(false, "link-1");
1400 let cid = ClientOrderId::new(msg.data[0].order_link_id.as_str());
1401 state.order_identities.insert(
1402 cid,
1403 OrderIdentity {
1404 venue_position_id: Some(venue_position_id),
1405 ..default_identity()
1406 },
1407 );
1408
1409 let ws_msg = BybitWsMessage::AccountExecutionFast(msg);
1410 dispatch_ws_message(
1411 &ws_msg,
1412 &emitter,
1413 &state,
1414 test_account_id(),
1415 &instruments,
1416 clock,
1417 );
1418
1419 let event1 = rx.try_recv().unwrap();
1421 assert!(
1422 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
1423 "Expected Accepted, found {event1:?}",
1424 );
1425
1426 let event2 = rx.try_recv().unwrap();
1428 match event2 {
1429 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
1430 assert_eq!(report.venue_position_id, Some(venue_position_id));
1431 assert_eq!(report.client_order_id, Some(cid));
1432 }
1433 other => panic!("Expected FillReport, found {other:?}"),
1434 }
1435 }
1436
1437 #[rstest]
1438 fn test_dispatch_untracked_fast_execution_emits_fill_report_without_position() {
1439 let instrument = linear_instrument();
1440 let instruments = build_instruments(std::slice::from_ref(&instrument));
1441 let (emitter, mut rx) = create_emitter();
1442 let clock = get_atomic_clock_realtime();
1443 let state = WsDispatchState::default();
1444
1445 let msg = fast_execution_msg(true, "");
1447 let ws_msg = BybitWsMessage::AccountExecutionFast(msg);
1448 dispatch_ws_message(
1449 &ws_msg,
1450 &emitter,
1451 &state,
1452 test_account_id(),
1453 &instruments,
1454 clock,
1455 );
1456
1457 let event = rx.try_recv().unwrap();
1459 match event {
1460 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
1461 assert_eq!(report.client_order_id, None);
1462 assert_eq!(report.venue_position_id, None);
1463 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
1464 assert_eq!(report.commission.as_f64(), 0.0);
1465 }
1466 other => panic!("Expected FillReport, found {other:?}"),
1467 }
1468 }
1469
1470 #[rstest]
1471 fn test_dispatch_untracked_execution_emits_fill_report() {
1472 let instrument = linear_instrument();
1473 let instruments = build_instruments(std::slice::from_ref(&instrument));
1474 let (emitter, mut rx) = create_emitter();
1475 let clock = get_atomic_clock_realtime();
1476 let state = WsDispatchState::default();
1477
1478 let json = load_test_json("ws_account_execution.json");
1479 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1480 serde_json::from_str(&json).unwrap();
1481
1482 let ws_msg = BybitWsMessage::AccountExecution(msg);
1484 dispatch_ws_message(
1485 &ws_msg,
1486 &emitter,
1487 &state,
1488 test_account_id(),
1489 &instruments,
1490 clock,
1491 );
1492
1493 let event = rx.try_recv().unwrap();
1494 assert!(matches!(
1495 event,
1496 ExecutionEvent::Report(ExecutionReport::Fill(_))
1497 ));
1498 }
1499
1500 #[rstest]
1501 fn test_dispatch_wallet_emits_account_state() {
1502 let instruments = AHashMap::new();
1503 let (emitter, mut rx) = create_emitter();
1504 let clock = get_atomic_clock_realtime();
1505 let state = WsDispatchState::default();
1506
1507 let json = load_test_json("ws_account_wallet.json");
1508 let msg: crate::websocket::messages::BybitWsAccountWalletMsg =
1509 serde_json::from_str(&json).unwrap();
1510 let ws_msg = BybitWsMessage::AccountWallet(msg);
1511
1512 dispatch_ws_message(
1513 &ws_msg,
1514 &emitter,
1515 &state,
1516 test_account_id(),
1517 &instruments,
1518 clock,
1519 );
1520
1521 let event = rx.try_recv().unwrap();
1522 assert!(matches!(event, ExecutionEvent::Account(_)));
1523 }
1524
1525 #[rstest]
1526 fn test_dispatch_data_message_ignored() {
1527 let instruments = AHashMap::new();
1528 let (emitter, mut rx) = create_emitter();
1529 let clock = get_atomic_clock_realtime();
1530 let state = WsDispatchState::default();
1531
1532 let json = load_test_json("ws_public_trade.json");
1533 let msg: crate::websocket::messages::BybitWsTradeMsg = serde_json::from_str(&json).unwrap();
1534 let ws_msg = BybitWsMessage::Trade(msg);
1535
1536 dispatch_ws_message(
1537 &ws_msg,
1538 &emitter,
1539 &state,
1540 test_account_id(),
1541 &instruments,
1542 clock,
1543 );
1544
1545 rx.try_recv().unwrap_err();
1546 }
1547
1548 #[rstest]
1549 fn test_accepted_dedup_prevents_duplicate() {
1550 let instrument = linear_instrument();
1551 let instruments = build_instruments(std::slice::from_ref(&instrument));
1552 let (emitter, mut rx) = create_emitter();
1553 let clock = get_atomic_clock_realtime();
1554 let state = WsDispatchState::default();
1555
1556 let json = load_test_json("ws_account_order.json");
1558 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
1559 value["data"][0]["orderStatus"] = serde_json::Value::String("New".to_string());
1560 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1561 serde_json::from_value(value).unwrap();
1562
1563 if let Some(order) = msg.data.first()
1564 && !order.order_link_id.is_empty()
1565 {
1566 let cid = ClientOrderId::new(order.order_link_id.as_str());
1567 state.order_identities.insert(cid, default_identity());
1568 }
1569
1570 let ws_msg = BybitWsMessage::AccountOrder(msg.clone());
1571 dispatch_ws_message(
1572 &ws_msg,
1573 &emitter,
1574 &state,
1575 test_account_id(),
1576 &instruments,
1577 clock,
1578 );
1579
1580 let event = rx.try_recv().unwrap();
1581 assert!(matches!(
1582 event,
1583 ExecutionEvent::Order(OrderEventAny::Accepted(_))
1584 ));
1585
1586 let ws_msg2 = BybitWsMessage::AccountOrder(msg);
1588 dispatch_ws_message(
1589 &ws_msg2,
1590 &emitter,
1591 &state,
1592 test_account_id(),
1593 &instruments,
1594 clock,
1595 );
1596
1597 rx.try_recv().unwrap_err();
1598 }
1599
1600 fn new_order_value() -> serde_json::Value {
1601 let json = load_test_json("ws_account_order.json");
1602 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
1603 value["data"][0]["orderStatus"] = serde_json::Value::String("New".to_string());
1604 value
1605 }
1606
1607 struct DispatchTestContext {
1608 instruments: AHashMap<Ustr, InstrumentAny>,
1609 emitter: ExecutionEventEmitter,
1610 rx: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1611 clock: &'static AtomicTime,
1612 state: WsDispatchState,
1613 }
1614
1615 impl DispatchTestContext {
1616 fn new() -> Self {
1617 let instrument = linear_instrument();
1618 let instruments = build_instruments(std::slice::from_ref(&instrument));
1619 let (emitter, rx) = create_emitter();
1620 let clock = get_atomic_clock_realtime();
1621 let state = WsDispatchState::default();
1622 Self {
1623 instruments,
1624 emitter,
1625 rx,
1626 clock,
1627 state,
1628 }
1629 }
1630
1631 fn accept_order(&mut self, value: &serde_json::Value) {
1632 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1633 serde_json::from_value(value.clone()).unwrap();
1634
1635 if let Some(order) = msg.data.first()
1636 && !order.order_link_id.is_empty()
1637 && !self
1638 .state
1639 .order_identities
1640 .contains_key(&ClientOrderId::new(order.order_link_id.as_str()))
1641 {
1642 let cid = ClientOrderId::new(order.order_link_id.as_str());
1643 self.state.order_identities.insert(cid, default_identity());
1644 }
1645
1646 self.dispatch_value(value);
1647
1648 let event = self.rx.try_recv().unwrap();
1649 assert!(
1650 matches!(event, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
1651 "Expected Accepted, found {event:?}"
1652 );
1653 }
1654
1655 fn dispatch_value(&self, value: &serde_json::Value) {
1656 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1657 serde_json::from_value(value.clone()).unwrap();
1658 let ws_msg = BybitWsMessage::AccountOrder(msg);
1659 dispatch_ws_message(
1660 &ws_msg,
1661 &self.emitter,
1662 &self.state,
1663 test_account_id(),
1664 &self.instruments,
1665 self.clock,
1666 );
1667 }
1668
1669 fn recv_updated(&mut self) -> OrderUpdated {
1670 let event = self.rx.try_recv().unwrap();
1671 match event {
1672 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => updated,
1673 other => panic!("Expected Updated event, found {other:?}"),
1674 }
1675 }
1676 }
1677
1678 #[rstest]
1679 fn test_dispatch_order_updated_on_price_change() {
1680 let mut ctx = DispatchTestContext::new();
1681 let value = new_order_value();
1682 ctx.accept_order(&value);
1683
1684 let mut amended = value;
1685 amended["data"][0]["price"] = serde_json::Value::String("31000".to_string());
1686 ctx.dispatch_value(&amended);
1687
1688 let updated = ctx.recv_updated();
1689 assert_eq!(updated.client_order_id, ClientOrderId::from("client-1"));
1690 assert_eq!(updated.price, Some(Price::from("31000.00")));
1691 assert_eq!(updated.quantity, Quantity::from("0.010"));
1692 assert_eq!(updated.trigger_price, None);
1693 assert!(updated.venue_order_id.is_some());
1694 }
1695
1696 #[rstest]
1697 fn test_dispatch_order_updated_on_quantity_change() {
1698 let mut ctx = DispatchTestContext::new();
1699 let value = new_order_value();
1700 ctx.accept_order(&value);
1701
1702 let mut amended = value;
1703 amended["data"][0]["qty"] = serde_json::Value::String("0.020".to_string());
1704 ctx.dispatch_value(&amended);
1705
1706 let updated = ctx.recv_updated();
1707 assert_eq!(updated.quantity, Quantity::from("0.020"));
1708 assert_eq!(updated.price, Some(Price::from("30000.00")));
1709 }
1710
1711 #[rstest]
1712 fn test_dispatch_order_updated_on_trigger_price_change() {
1713 let mut ctx = DispatchTestContext::new();
1714 let mut value = new_order_value();
1715 value["data"][0]["triggerPrice"] = serde_json::Value::String("29000".to_string());
1716 ctx.accept_order(&value);
1717
1718 let mut amended = value;
1719 amended["data"][0]["triggerPrice"] = serde_json::Value::String("28000".to_string());
1720 ctx.dispatch_value(&amended);
1721
1722 let updated = ctx.recv_updated();
1723 assert_eq!(updated.trigger_price, Some(Price::from("28000.00")));
1724 assert_eq!(updated.price, Some(Price::from("30000.00")));
1725 }
1726
1727 #[rstest]
1728 fn test_dispatch_dedup_suppresses_identical_after_snapshot() {
1729 let mut ctx = DispatchTestContext::new();
1730 let value = new_order_value();
1731 ctx.accept_order(&value);
1732
1733 ctx.dispatch_value(&value);
1734
1735 assert!(
1736 ctx.rx.try_recv().is_err(),
1737 "Expected no event for identical redelivery"
1738 );
1739 }
1740
1741 #[rstest]
1742 fn test_dispatch_order_updated_stores_snapshot_for_subsequent_change() {
1743 let mut ctx = DispatchTestContext::new();
1744 let value = new_order_value();
1745 ctx.accept_order(&value);
1746
1747 let mut amended1 = value.clone();
1748 amended1["data"][0]["price"] = serde_json::Value::String("31000".to_string());
1749 ctx.dispatch_value(&amended1);
1750 let _result = ctx.recv_updated();
1751
1752 let mut amended2 = value;
1753 amended2["data"][0]["price"] = serde_json::Value::String("32000".to_string());
1754 ctx.dispatch_value(&amended2);
1755
1756 let updated = ctx.recv_updated();
1757 assert_eq!(updated.price, Some(Price::from("32000.00")));
1758 }
1759
1760 #[rstest]
1761 fn test_dispatch_accepted_with_seeded_snapshot_emits_updated_for_bbo() {
1762 let mut ctx = DispatchTestContext::new();
1763 let value = new_order_value();
1764 let cid = ClientOrderId::from("client-1");
1765 ctx.state.order_identities.insert(cid, default_identity());
1766 ctx.state.order_snapshots.insert(
1767 cid,
1768 OrderStateSnapshot {
1769 quantity: Quantity::from("0.010"),
1770 price: Some(Price::from("29000.00")),
1771 trigger_price: None,
1772 },
1773 );
1774
1775 ctx.dispatch_value(&value);
1776
1777 let accepted = ctx.rx.try_recv().unwrap();
1778 assert!(
1779 matches!(accepted, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
1780 "Expected Accepted first, found {accepted:?}"
1781 );
1782
1783 let updated = ctx.recv_updated();
1784 assert_eq!(updated.client_order_id, cid);
1785 assert_eq!(updated.price, Some(Price::from("30000.00")));
1786 assert_eq!(updated.quantity, Quantity::from("0.010"));
1787 assert!(updated.venue_order_id.is_some());
1788 }
1789
1790 #[rstest]
1791 fn test_emit_rejection_for_place_clears_snapshot() {
1792 let ctx = DispatchTestContext::new();
1793 let cid = ClientOrderId::from("client-1");
1794 let identity = default_identity();
1795 ctx.state.order_identities.insert(cid, identity.clone());
1796 ctx.state.order_snapshots.insert(
1797 cid,
1798 OrderStateSnapshot {
1799 quantity: Quantity::from("0.010"),
1800 price: Some(Price::from("30000.00")),
1801 trigger_price: None,
1802 },
1803 );
1804
1805 emit_rejection_for_op(
1806 &PendingOperation::Place,
1807 cid,
1808 &identity,
1809 None,
1810 "rejected",
1811 &ctx.emitter,
1812 &ctx.state,
1813 UnixNanos::from(1u64),
1814 );
1815
1816 assert!(!ctx.state.order_identities.contains_key(&cid));
1817 assert!(!ctx.state.order_snapshots.contains_key(&cid));
1818 }
1819
1820 fn order_response(
1821 op: &str,
1822 ret_code: i64,
1823 ret_msg: &str,
1824 req_id: &str,
1825 data: serde_json::Value,
1826 ret_ext_info: Option<serde_json::Value>,
1827 ) -> BybitWsOrderResponse {
1828 BybitWsOrderResponse {
1829 op: Ustr::from(op),
1830 conn_id: Some("test-conn-id".to_string()),
1831 ret_code,
1832 ret_msg: ret_msg.to_string(),
1833 data,
1834 req_id: Some(req_id.to_string()),
1835 header: None,
1836 ret_ext_info,
1837 }
1838 }
1839
1840 #[rstest]
1841 fn test_dispatch_single_cancel_rejection_emits_cancel_rejected() {
1842 let mut ctx = DispatchTestContext::new();
1843 let cid = ClientOrderId::from("cancel-reject-1");
1844 let venue_order_id = VenueOrderId::from("venue-cancel-1");
1845 ctx.state.order_identities.insert(cid, default_identity());
1846 ctx.state.pending_requests.insert(
1847 "req-cancel".to_string(),
1848 (
1849 vec![cid],
1850 vec![Some(venue_order_id)],
1851 PendingOperation::Cancel,
1852 ),
1853 );
1854
1855 let response = order_response(
1856 BYBIT_OP_ORDER_CANCEL,
1857 110001,
1858 "Order does not exist.",
1859 "req-cancel",
1860 serde_json::json!({
1861 "orderId": venue_order_id.to_string(),
1862 "orderLinkId": cid.to_string(),
1863 }),
1864 None,
1865 );
1866
1867 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
1868
1869 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
1870 assert!(
1871 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected)) if rejected.client_order_id == cid),
1872 "Expected CancelRejected for {cid}, found {event:?}"
1873 );
1874 assert!(
1875 ctx.rx.try_recv().is_err(),
1876 "Expected no extra events for single cancel rejection"
1877 );
1878 }
1879
1880 #[rstest]
1881 fn test_dispatch_single_modify_rejection_emits_modify_rejected() {
1882 let mut ctx = DispatchTestContext::new();
1883 let cid = ClientOrderId::from("modify-reject-1");
1884 let venue_order_id = VenueOrderId::from("venue-modify-1");
1885 ctx.state.order_identities.insert(cid, default_identity());
1886 ctx.state.pending_requests.insert(
1887 "req-modify".to_string(),
1888 (
1889 vec![cid],
1890 vec![Some(venue_order_id)],
1891 PendingOperation::Amend,
1892 ),
1893 );
1894
1895 let response = order_response(
1896 BYBIT_OP_ORDER_AMEND,
1897 110003,
1898 "Order price exceeds allowable range.",
1899 "req-modify",
1900 serde_json::json!({
1901 "orderId": venue_order_id.to_string(),
1902 "orderLinkId": cid.to_string(),
1903 }),
1904 None,
1905 );
1906
1907 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
1908
1909 let event = ctx.rx.try_recv().expect("expected ModifyRejected event");
1910 assert!(
1911 matches!(event, ExecutionEvent::Order(OrderEventAny::ModifyRejected(ref rejected)) if rejected.client_order_id == cid),
1912 "Expected ModifyRejected for {cid}, found {event:?}"
1913 );
1914 assert!(
1915 ctx.rx.try_recv().is_err(),
1916 "Expected no extra events for single modify rejection"
1917 );
1918 }
1919
1920 #[rstest]
1921 fn test_dispatch_single_cancel_rate_limit_keeps_outcome_unresolved() {
1922 let mut ctx = DispatchTestContext::new();
1923 let cid = ClientOrderId::from("cancel-rate-limit-1");
1924 let venue_order_id = VenueOrderId::from("venue-cancel-rate-limit-1");
1925 ctx.state.order_identities.insert(cid, default_identity());
1926 ctx.state.pending_requests.insert(
1927 "req-cancel-rate-limit".to_string(),
1928 (
1929 vec![cid],
1930 vec![Some(venue_order_id)],
1931 PendingOperation::Cancel,
1932 ),
1933 );
1934
1935 let response = order_response(
1936 BYBIT_OP_ORDER_CANCEL,
1937 10429,
1938 "System level frequency protection.",
1939 "req-cancel-rate-limit",
1940 serde_json::json!({
1941 "orderId": venue_order_id.to_string(),
1942 "orderLinkId": cid.to_string(),
1943 }),
1944 None,
1945 );
1946
1947 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
1948
1949 assert!(
1950 ctx.rx.try_recv().is_err(),
1951 "Expected no CancelRejected event for rate-limit response"
1952 );
1953 assert!(ctx.state.order_identities.contains_key(&cid));
1954 }
1955
1956 #[rstest]
1957 fn test_dispatch_single_modify_server_error_keeps_outcome_unresolved() {
1958 let mut ctx = DispatchTestContext::new();
1959 let cid = ClientOrderId::from("modify-server-error-1");
1960 let venue_order_id = VenueOrderId::from("venue-modify-server-error-1");
1961 ctx.state.order_identities.insert(cid, default_identity());
1962 ctx.state.pending_requests.insert(
1963 "req-modify-server-error".to_string(),
1964 (
1965 vec![cid],
1966 vec![Some(venue_order_id)],
1967 PendingOperation::Amend,
1968 ),
1969 );
1970
1971 let response = order_response(
1972 BYBIT_OP_ORDER_AMEND,
1973 10016,
1974 "Internal server error.",
1975 "req-modify-server-error",
1976 serde_json::json!({
1977 "orderId": venue_order_id.to_string(),
1978 "orderLinkId": cid.to_string(),
1979 }),
1980 None,
1981 );
1982
1983 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
1984
1985 assert!(
1986 ctx.rx.try_recv().is_err(),
1987 "Expected no ModifyRejected event for server error response"
1988 );
1989 assert!(ctx.state.order_identities.contains_key(&cid));
1990 }
1991
1992 #[rstest]
1993 fn test_dispatch_batch_cancel_top_level_failure_does_not_emit_cancel_rejected() {
1994 let mut ctx = DispatchTestContext::new();
1995 let cid_1 = ClientOrderId::from("batch-cancel-1");
1996 let cid_2 = ClientOrderId::from("batch-cancel-2");
1997 ctx.state.order_identities.insert(cid_1, default_identity());
1998 ctx.state.order_identities.insert(cid_2, default_identity());
1999 ctx.state.pending_requests.insert(
2000 "req-batch-cancel".to_string(),
2001 (
2002 vec![cid_1, cid_2],
2003 vec![
2004 Some(VenueOrderId::from("venue-batch-1")),
2005 Some(VenueOrderId::from("venue-batch-2")),
2006 ],
2007 PendingOperation::Cancel,
2008 ),
2009 );
2010
2011 let response = order_response(
2012 BYBIT_OP_ORDER_CANCEL_BATCH,
2013 10016,
2014 "Internal server error.",
2015 "req-batch-cancel",
2016 serde_json::json!({}),
2017 None,
2018 );
2019
2020 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2021
2022 assert!(
2023 ctx.rx.try_recv().is_err(),
2024 "Expected no CancelRejected events for ambiguous batch failure"
2025 );
2026 assert!(ctx.state.order_identities.contains_key(&cid_1));
2027 assert!(ctx.state.order_identities.contains_key(&cid_2));
2028 }
2029
2030 #[rstest]
2031 fn test_dispatch_single_item_batch_cancel_top_level_failure_does_not_emit_cancel_rejected() {
2032 let mut ctx = DispatchTestContext::new();
2033 let cid = ClientOrderId::from("batch-cancel-single");
2034 ctx.state.order_identities.insert(cid, default_identity());
2035 ctx.state.pending_requests.insert(
2036 "req-batch-cancel-single".to_string(),
2037 (
2038 vec![cid],
2039 vec![Some(VenueOrderId::from("venue-batch-single"))],
2040 PendingOperation::Cancel,
2041 ),
2042 );
2043
2044 let response = order_response(
2045 BYBIT_OP_ORDER_CANCEL_BATCH,
2046 10016,
2047 "Internal server error.",
2048 "req-batch-cancel-single",
2049 serde_json::json!({}),
2050 None,
2051 );
2052
2053 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2054
2055 assert!(
2056 ctx.rx.try_recv().is_err(),
2057 "Expected no CancelRejected event for ambiguous one-item batch failure"
2058 );
2059 assert!(ctx.state.order_identities.contains_key(&cid));
2060 }
2061
2062 #[rstest]
2063 fn test_dispatch_batch_cancel_per_item_error_emits_only_failed_item() {
2064 let mut ctx = DispatchTestContext::new();
2065 let cid_1 = ClientOrderId::from("batch-cancel-ok");
2066 let cid_2 = ClientOrderId::from("batch-cancel-reject");
2067 ctx.state.order_identities.insert(cid_1, default_identity());
2068 ctx.state.order_identities.insert(cid_2, default_identity());
2069 ctx.state.pending_requests.insert(
2070 "req-batch-mixed".to_string(),
2071 (
2072 vec![cid_1, cid_2],
2073 vec![
2074 Some(VenueOrderId::from("venue-batch-ok")),
2075 Some(VenueOrderId::from("venue-batch-reject")),
2076 ],
2077 PendingOperation::Cancel,
2078 ),
2079 );
2080
2081 let response = order_response(
2082 BYBIT_OP_ORDER_CANCEL_BATCH,
2083 0,
2084 "OK",
2085 "req-batch-mixed",
2086 serde_json::json!({
2087 "list": [
2088 {"orderId": "venue-batch-ok", "orderLinkId": cid_1.to_string()},
2089 {"orderId": "venue-batch-reject", "orderLinkId": cid_2.to_string()}
2090 ]
2091 }),
2092 Some(serde_json::json!({
2093 "list": [
2094 {"code": 0, "msg": "OK"},
2095 {"code": 170213, "msg": "Order does not exist."}
2096 ]
2097 })),
2098 );
2099
2100 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2101
2102 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
2103 assert!(
2104 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected)) if rejected.client_order_id == cid_2),
2105 "Expected CancelRejected for failed batch item {cid_2}, found {event:?}"
2106 );
2107 assert!(
2108 ctx.rx.try_recv().is_err(),
2109 "Expected no CancelRejected event for successful batch item"
2110 );
2111 }
2112
2113 #[rstest]
2114 fn test_dispatch_batch_cancel_per_item_rate_limit_keeps_outcome_unresolved() {
2115 let mut ctx = DispatchTestContext::new();
2116 let cid = ClientOrderId::from("batch-cancel-rate-limit");
2117 let venue_order_id = VenueOrderId::from("venue-batch-rate-limit");
2118 ctx.state.order_identities.insert(cid, default_identity());
2119 ctx.state.pending_requests.insert(
2120 "req-batch-rate-limit".to_string(),
2121 (
2122 vec![cid],
2123 vec![Some(venue_order_id)],
2124 PendingOperation::Cancel,
2125 ),
2126 );
2127
2128 let response = order_response(
2129 BYBIT_OP_ORDER_CANCEL_BATCH,
2130 0,
2131 "OK",
2132 "req-batch-rate-limit",
2133 serde_json::json!({
2134 "list": [
2135 {"orderId": venue_order_id.to_string(), "orderLinkId": cid.to_string()}
2136 ]
2137 }),
2138 Some(serde_json::json!({
2139 "list": [
2140 {"code": 10006, "msg": "Too many visits."}
2141 ]
2142 })),
2143 );
2144
2145 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2146
2147 assert!(
2148 ctx.rx.try_recv().is_err(),
2149 "Expected no CancelRejected event for per-item rate-limit response"
2150 );
2151 assert!(ctx.state.order_identities.contains_key(&cid));
2152 }
2153
2154 #[rstest]
2155 fn test_dispatch_filled_with_seeded_snapshot_emits_updated_for_bbo() {
2156 let mut ctx = DispatchTestContext::new();
2157 let mut value = new_order_value();
2158 value["data"][0]["orderStatus"] = serde_json::Value::String("Filled".to_string());
2159 value["data"][0]["cumExecQty"] = serde_json::Value::String("0.010".to_string());
2160 let cid = ClientOrderId::from("client-1");
2161 ctx.state.order_identities.insert(cid, default_identity());
2162 ctx.state.order_snapshots.insert(
2163 cid,
2164 OrderStateSnapshot {
2165 quantity: Quantity::from("0.010"),
2166 price: Some(Price::from("29000.00")),
2167 trigger_price: None,
2168 },
2169 );
2170
2171 ctx.dispatch_value(&value);
2172
2173 let accepted_event = ctx.rx.try_recv().unwrap();
2174 let accepted = match accepted_event {
2175 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => accepted,
2176 other => panic!("Expected Accepted first, found {other:?}"),
2177 };
2178 assert_eq!(accepted.client_order_id, cid);
2179 assert!(!accepted.venue_order_id.to_string().is_empty());
2180
2181 let updated = ctx.recv_updated();
2182 assert_eq!(updated.client_order_id, cid);
2183 assert_eq!(updated.price, Some(Price::from("30000.00")));
2184 assert_eq!(updated.quantity, Quantity::from("0.010"));
2185 assert_eq!(updated.trigger_price, None);
2186 assert_eq!(updated.venue_order_id, Some(accepted.venue_order_id));
2187
2188 let stored = ctx.state.order_snapshots.get(&cid).unwrap();
2189 assert_eq!(stored.price, Some(Price::from("30000.00")));
2190 }
2191
2192 #[rstest]
2193 fn test_dispatch_accepted_with_matching_seeded_snapshot_no_updated() {
2194 let mut ctx = DispatchTestContext::new();
2195 let value = new_order_value();
2196 let cid = ClientOrderId::from("client-1");
2197 ctx.state.order_identities.insert(cid, default_identity());
2198 ctx.state.order_snapshots.insert(
2199 cid,
2200 OrderStateSnapshot {
2201 quantity: Quantity::from("0.010"),
2202 price: Some(Price::from("30000.00")),
2203 trigger_price: None,
2204 },
2205 );
2206
2207 ctx.dispatch_value(&value);
2208
2209 let accepted = ctx.rx.try_recv().unwrap();
2210 assert!(matches!(
2211 accepted,
2212 ExecutionEvent::Order(OrderEventAny::Accepted(_))
2213 ));
2214
2215 assert!(
2216 ctx.rx.try_recv().is_err(),
2217 "Expected no OrderUpdated when seed matches venue snapshot"
2218 );
2219 }
2220
2221 #[rstest]
2222 #[case::price_changed(
2223 Some(Price::from("100.00")),
2224 None,
2225 Quantity::from("1.000"),
2226 Some(Price::from("200.00")),
2227 None,
2228 Quantity::from("1.000"),
2229 true
2230 )]
2231 #[case::trigger_changed(
2232 None,
2233 Some(Price::from("100.00")),
2234 Quantity::from("1.000"),
2235 None,
2236 Some(Price::from("90.00")),
2237 Quantity::from("1.000"),
2238 true
2239 )]
2240 #[case::qty_changed(
2241 Some(Price::from("100.00")),
2242 None,
2243 Quantity::from("1.000"),
2244 Some(Price::from("100.00")),
2245 None,
2246 Quantity::from("2.000"),
2247 true
2248 )]
2249 #[case::no_change(
2250 Some(Price::from("100.00")),
2251 None,
2252 Quantity::from("1.000"),
2253 Some(Price::from("100.00")),
2254 None,
2255 Quantity::from("1.000"),
2256 false
2257 )]
2258 fn test_is_snapshot_updated(
2259 #[case] prev_price: Option<Price>,
2260 #[case] prev_trigger: Option<Price>,
2261 #[case] prev_qty: Quantity,
2262 #[case] new_price: Option<Price>,
2263 #[case] new_trigger: Option<Price>,
2264 #[case] new_qty: Quantity,
2265 #[case] expected: bool,
2266 ) {
2267 let state = WsDispatchState::default();
2268 let cid = ClientOrderId::from("test-1");
2269 state.order_snapshots.insert(
2270 cid,
2271 OrderStateSnapshot {
2272 quantity: prev_qty,
2273 price: prev_price,
2274 trigger_price: prev_trigger,
2275 },
2276 );
2277
2278 let new_snapshot = OrderStateSnapshot {
2279 quantity: new_qty,
2280 price: new_price,
2281 trigger_price: new_trigger,
2282 };
2283 assert_eq!(is_snapshot_updated(&new_snapshot, &cid, &state), expected);
2284 }
2285
2286 #[rstest]
2287 fn test_is_snapshot_updated_no_previous() {
2288 let state = WsDispatchState::default();
2289 let cid = ClientOrderId::from("test-1");
2290
2291 let new_snapshot = OrderStateSnapshot {
2292 quantity: Quantity::from("1.000"),
2293 price: Some(Price::from("100.00")),
2294 trigger_price: None,
2295 };
2296 assert!(!is_snapshot_updated(&new_snapshot, &cid, &state));
2297 }
2298
2299 #[rstest]
2300 #[case::limit_order("30000", "0", Some(Price::from("30000.00")), None)]
2301 #[case::conditional("0", "29000", None, Some(Price::from("29000.00")))]
2302 #[case::both(
2303 "30000",
2304 "29000",
2305 Some(Price::from("30000.00")),
2306 Some(Price::from("29000.00"))
2307 )]
2308 fn test_parse_order_snapshot(
2309 #[case] price: &str,
2310 #[case] trigger: &str,
2311 #[case] expected_price: Option<Price>,
2312 #[case] expected_trigger: Option<Price>,
2313 ) {
2314 let instrument = linear_instrument();
2315 let json = load_test_json("ws_account_order.json");
2316 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2317 value["data"][0]["price"] = serde_json::Value::String(price.to_string());
2318 value["data"][0]["triggerPrice"] = serde_json::Value::String(trigger.to_string());
2319 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
2320 serde_json::from_value(value).unwrap();
2321
2322 let snapshot = parse_order_snapshot(&msg.data[0], &instrument).unwrap();
2323 assert_eq!(snapshot.price, expected_price);
2324 assert_eq!(snapshot.trigger_price, expected_trigger);
2325 assert_eq!(snapshot.quantity, Quantity::from("0.010"));
2326 }
2327
2328 #[rstest]
2329 fn test_parse_order_snapshot_invalid_qty_returns_none() {
2330 let instrument = linear_instrument();
2331 let json = load_test_json("ws_account_order.json");
2332 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2333 value["data"][0]["qty"] = serde_json::Value::String(String::new());
2334 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
2335 serde_json::from_value(value).unwrap();
2336
2337 assert!(parse_order_snapshot(&msg.data[0], &instrument).is_none());
2338 }
2339
2340 #[rstest]
2341 fn test_dispatch_order_updated_on_partially_filled_price_change() {
2342 let mut ctx = DispatchTestContext::new();
2343 let value = new_order_value();
2344 ctx.accept_order(&value);
2345
2346 let mut amended = value;
2347 amended["data"][0]["orderStatus"] =
2348 serde_json::Value::String("PartiallyFilled".to_string());
2349 amended["data"][0]["cumExecQty"] = serde_json::Value::String("0.005".to_string());
2350 amended["data"][0]["price"] = serde_json::Value::String("31000".to_string());
2351 ctx.dispatch_value(&amended);
2352
2353 let updated = ctx.recv_updated();
2354 assert_eq!(updated.client_order_id, ClientOrderId::from("client-1"));
2355 assert_eq!(updated.price, Some(Price::from("31000.00")));
2356 }
2357}