1use std::sync::{
24 Arc,
25 atomic::{AtomicBool, Ordering},
26};
27
28use ahash::AHashMap;
29use anyhow::Context;
30use arc_swap::ArcSwapOption;
31use dashmap::{DashMap, DashSet};
32use nautilus_core::{UUID4, UnixNanos, time::AtomicTime};
33use nautilus_live::ExecutionEventEmitter;
34use nautilus_model::{
35 enums::{LiquiditySide, OrderSide, OrderType},
36 events::{
37 OrderAccepted, OrderCanceled, OrderEventAny, OrderFilled, OrderTriggered, OrderUpdated,
38 },
39 identifiers::{
40 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, VenueOrderId,
41 },
42 instruments::{Instrument, InstrumentAny},
43 orders::TRIGGERABLE_ORDER_TYPES,
44 types::{Money, Price, Quantity},
45};
46use rust_decimal::Decimal;
47use ustr::Ustr;
48
49use super::{
50 messages::{
51 BybitWsAccountExecution, BybitWsAccountExecutionFast, BybitWsAccountOrder, BybitWsMessage,
52 },
53 parse::{
54 parse_millis_i64, parse_ws_account_state, parse_ws_fill_report_fast,
55 parse_ws_position_status_report,
56 },
57};
58use crate::{
59 common::{
60 enums::{BybitExecType, BybitOrderSide, BybitOrderStatus, BybitProductType},
61 parse::{
62 bybit_rejection_due_post_only, get_currency, make_bybit_symbol, parse_millis_timestamp,
63 parse_price_with_precision, parse_quantity_with_precision,
64 },
65 },
66 http::error::is_bybit_ambiguous_order_error_code,
67 repay::RepayRequest,
68};
69
70const DEDUP_CAPACITY: usize = 10_000;
71
72const BYBIT_OP_ORDER_CREATE: &str = "order.create";
73const BYBIT_OP_ORDER_AMEND: &str = "order.amend";
74const BYBIT_OP_ORDER_CANCEL: &str = "order.cancel";
75const BYBIT_OP_ORDER_CREATE_BATCH: &str = "order.create-batch";
76const BYBIT_OP_ORDER_AMEND_BATCH: &str = "order.amend-batch";
77const BYBIT_OP_ORDER_CANCEL_BATCH: &str = "order.cancel-batch";
78
79#[derive(Debug, Clone)]
82pub struct OrderIdentity {
83 pub instrument_id: InstrumentId,
84 pub strategy_id: StrategyId,
85 pub order_side: OrderSide,
86 pub order_type: OrderType,
87 pub venue_position_id: Option<PositionId>,
88}
89
90#[derive(Debug, Clone, Copy)]
92pub enum PendingOperation {
93 Place,
94 Cancel,
95 Amend,
96}
97
98pub type PendingRequestData = (
101 Vec<ClientOrderId>,
102 Vec<Option<VenueOrderId>>,
103 PendingOperation,
104);
105
106#[derive(Debug, Clone)]
110pub struct OrderStateSnapshot {
111 pub quantity: Quantity,
112 pub price: Option<Price>,
113 pub trigger_price: Option<Price>,
114}
115
116#[derive(Clone, Copy, Debug)]
117struct SpotRepayFill {
118 quantity: Quantity,
119 base_fee: Decimal,
120}
121
122#[derive(Debug)]
123pub struct WsDispatchState {
124 pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
125 pub pending_requests: DashMap<String, PendingRequestData>,
126 pub order_snapshots: DashMap<ClientOrderId, OrderStateSnapshot>,
127 pub emitted_accepted: DashSet<ClientOrderId>,
128 pub triggered_orders: DashSet<ClientOrderId>,
129 pub filled_orders: DashSet<ClientOrderId>,
130 spot_repay_fills: DashMap<ClientOrderId, SpotRepayFill>,
131 repay_tx: ArcSwapOption<tokio::sync::mpsc::UnboundedSender<RepayRequest>>,
132 clearing: AtomicBool,
133}
134
135impl Default for WsDispatchState {
136 fn default() -> Self {
137 Self {
138 order_identities: DashMap::new(),
139 pending_requests: DashMap::new(),
140 order_snapshots: DashMap::new(),
141 emitted_accepted: DashSet::default(),
142 triggered_orders: DashSet::default(),
143 filled_orders: DashSet::default(),
144 spot_repay_fills: DashMap::new(),
145 repay_tx: ArcSwapOption::empty(),
146 clearing: AtomicBool::new(false),
147 }
148 }
149}
150
151impl WsDispatchState {
152 fn evict_if_full(&self, set: &DashSet<ClientOrderId>) {
153 if set.len() >= DEDUP_CAPACITY
154 && self
155 .clearing
156 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
157 .is_ok()
158 {
159 set.clear();
160 self.clearing.store(false, Ordering::Release);
161 }
162 }
163
164 fn insert_accepted(&self, cid: ClientOrderId) {
165 self.evict_if_full(&self.emitted_accepted);
166 self.emitted_accepted.insert(cid);
167 }
168
169 fn insert_filled(&self, cid: ClientOrderId) {
170 self.evict_if_full(&self.filled_orders);
171 self.filled_orders.insert(cid);
172 }
173
174 fn insert_triggered(&self, cid: ClientOrderId) {
175 self.evict_if_full(&self.triggered_orders);
176 self.triggered_orders.insert(cid);
177 }
178
179 pub(crate) fn set_repay_sender(&self, tx: tokio::sync::mpsc::UnboundedSender<RepayRequest>) {
180 self.repay_tx.store(Some(Arc::new(tx)));
181 }
182
183 pub(crate) fn clear_repay_sender(&self) {
184 self.repay_tx.store(None);
185 }
186
187 fn enqueue_repay(&self, req: RepayRequest) {
188 if let Some(tx) = self.repay_tx.load_full()
189 && let Err(e) = tx.send(req)
190 {
191 log::warn!("Failed to enqueue spot borrow repayment: {e}");
192 }
193 }
194}
195
196pub fn dispatch_ws_message(
203 message: &BybitWsMessage,
204 emitter: &ExecutionEventEmitter,
205 state: &WsDispatchState,
206 account_id: AccountId,
207 instruments: &AHashMap<Ustr, InstrumentAny>,
208 clock: &AtomicTime,
209) {
210 match message {
211 BybitWsMessage::AccountOrder(msg) => {
212 let ts_init = clock.get_time_ns();
213
214 for order in &msg.data {
215 let symbol = make_bybit_symbol(order.symbol, order.category);
216 let Some(instrument) = instruments.get(&symbol) else {
217 log::warn!("No instrument for order update: {symbol}");
218 continue;
219 };
220 dispatch_order_update(order, instrument, emitter, state, account_id, ts_init);
221 }
222 }
223 BybitWsMessage::AccountExecution(msg) => {
224 let ts_init = clock.get_time_ns();
225
226 for exec in &msg.data {
227 let symbol = make_bybit_symbol(exec.symbol, exec.category);
228 let Some(instrument) = instruments.get(&symbol) else {
229 log::warn!("No instrument for execution update: {symbol}");
230 continue;
231 };
232 dispatch_execution_fill(exec, instrument, emitter, state, account_id, ts_init);
233 }
234 }
235 BybitWsMessage::AccountExecutionFast(msg) => {
236 let ts_init = clock.get_time_ns();
237
238 for exec in &msg.data {
239 let symbol = make_bybit_symbol(exec.symbol, exec.category);
240 let Some(instrument) = instruments.get(&symbol) else {
241 log::warn!("No instrument for fast-execution update: {symbol}");
242 continue;
243 };
244 dispatch_execution_fill_fast(exec, instrument, emitter, state, account_id, ts_init);
245 }
246 }
247 BybitWsMessage::AccountWallet(msg) => {
248 let ts_init = clock.get_time_ns();
249 let ts_event = parse_millis_i64(msg.creation_time, "wallet.creation_time")
250 .unwrap_or_else(|e| {
251 log::warn!("Failed to parse wallet creation_time, using ts_init: {e}");
252 ts_init
253 });
254
255 for wallet in &msg.data {
256 match parse_ws_account_state(wallet, account_id, ts_event, ts_init) {
257 Ok(state) => emitter.send_account_state(state),
258 Err(e) => log::error!("Failed to parse account state: {e}"),
259 }
260 }
261 }
262 BybitWsMessage::AccountPosition(msg) => {
263 let ts_init = clock.get_time_ns();
264
265 for position in &msg.data {
266 let symbol = make_bybit_symbol(position.symbol, position.category);
267 let Some(instrument) = instruments.get(&symbol) else {
268 log::warn!("No instrument for position update: {symbol}");
269 continue;
270 };
271
272 match parse_ws_position_status_report(position, account_id, instrument, ts_init) {
273 Ok(report) => emitter.send_position_report(report),
274 Err(e) => log::error!("Failed to parse position status report: {e}"),
275 }
276 }
277 }
278 BybitWsMessage::OrderResponse(resp) => {
279 let ts_init = clock.get_time_ns();
280 dispatch_order_response(resp, emitter, state, ts_init);
281 }
282 BybitWsMessage::Error(e) => {
283 log::warn!("WebSocket error: code={} message={}", e.code, e.message);
284 }
285 BybitWsMessage::Reconnected => {
286 log::info!("WebSocket reconnected");
287 }
288 BybitWsMessage::Auth(_)
289 | BybitWsMessage::Orderbook(_)
290 | BybitWsMessage::Trade(_)
291 | BybitWsMessage::Liquidation(_)
292 | BybitWsMessage::Kline(_)
293 | BybitWsMessage::TickerLinear(_)
294 | BybitWsMessage::TickerOption(_) => {}
295 }
296}
297
298fn dispatch_order_update(
304 order: &BybitWsAccountOrder,
305 instrument: &InstrumentAny,
306 emitter: &ExecutionEventEmitter,
307 state: &WsDispatchState,
308 account_id: AccountId,
309 ts_init: UnixNanos,
310) {
311 let client_order_id = if order.order_link_id.is_empty() {
312 None
313 } else {
314 Some(ClientOrderId::new(order.order_link_id))
315 };
316
317 let identity = client_order_id
318 .as_ref()
319 .and_then(|cid| state.order_identities.get(cid).map(|r| r.clone()));
320
321 if let (Some(client_order_id), Some(identity)) = (client_order_id, identity) {
322 let venue_order_id = VenueOrderId::new(order.order_id);
323
324 match order.order_status {
325 BybitOrderStatus::Created | BybitOrderStatus::New | BybitOrderStatus::Untriggered => {
326 let snapshot = parse_order_snapshot(order, instrument);
327
328 if state.emitted_accepted.contains(&client_order_id)
329 || state.filled_orders.contains(&client_order_id)
330 || state.triggered_orders.contains(&client_order_id)
331 {
332 if let Some(snapshot) = snapshot
333 && is_snapshot_updated(&snapshot, &client_order_id, state)
334 {
335 let updated = OrderUpdated::new(
336 emitter.trader_id(),
337 identity.strategy_id,
338 identity.instrument_id,
339 client_order_id,
340 snapshot.quantity,
341 UUID4::new(),
342 ts_init,
343 ts_init,
344 false,
345 Some(venue_order_id),
346 Some(account_id),
347 snapshot.price,
348 snapshot.trigger_price,
349 None,
350 false,
351 );
352 state.order_snapshots.insert(client_order_id, snapshot);
353 emitter.send_order_event(OrderEventAny::Updated(updated));
354 return;
355 }
356 log::debug!("Skipping duplicate Accepted for {client_order_id}");
357 return;
358 }
359
360 state.insert_accepted(client_order_id);
361
362 let venue_differs_from_submitted = snapshot
365 .as_ref()
366 .is_some_and(|s| is_snapshot_updated(s, &client_order_id, state));
367
368 if let Some(snapshot) = snapshot.as_ref() {
369 state
370 .order_snapshots
371 .insert(client_order_id, snapshot.clone());
372 }
373
374 let accepted = OrderAccepted::new(
375 emitter.trader_id(),
376 identity.strategy_id,
377 identity.instrument_id,
378 client_order_id,
379 venue_order_id,
380 account_id,
381 UUID4::new(),
382 ts_init,
383 ts_init,
384 false,
385 );
386 emitter.send_order_event(OrderEventAny::Accepted(accepted));
387
388 if venue_differs_from_submitted && let Some(snapshot) = snapshot {
389 let updated = OrderUpdated::new(
390 emitter.trader_id(),
391 identity.strategy_id,
392 identity.instrument_id,
393 client_order_id,
394 snapshot.quantity,
395 UUID4::new(),
396 ts_init,
397 ts_init,
398 false,
399 Some(venue_order_id),
400 Some(account_id),
401 snapshot.price,
402 snapshot.trigger_price,
403 None,
404 false,
405 );
406 emitter.send_order_event(OrderEventAny::Updated(updated));
407 }
408 }
409 BybitOrderStatus::Triggered => {
410 if state.filled_orders.contains(&client_order_id) {
411 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
412 return;
413 }
414
415 if !TRIGGERABLE_ORDER_TYPES.contains(&identity.order_type) {
416 log::debug!(
417 "Skipping OrderTriggered for {} order {client_order_id}: market-style stops have no TRIGGERED state",
418 identity.order_type,
419 );
420 return;
421 }
422
423 ensure_accepted_emitted(
424 client_order_id,
425 account_id,
426 venue_order_id,
427 &identity,
428 emitter,
429 state,
430 ts_init,
431 );
432 state.insert_triggered(client_order_id);
433 let triggered = OrderTriggered::new(
434 emitter.trader_id(),
435 identity.strategy_id,
436 identity.instrument_id,
437 client_order_id,
438 UUID4::new(),
439 ts_init,
440 ts_init,
441 false,
442 Some(venue_order_id),
443 Some(account_id),
444 );
445 emitter.send_order_event(OrderEventAny::Triggered(triggered));
446 }
447 BybitOrderStatus::Rejected => {
448 let filled_qty = parse_quantity_with_precision(
449 &order.cum_exec_qty,
450 instrument.size_precision(),
451 "order.cumExecQty",
452 )
453 .unwrap_or_default();
454
455 if filled_qty.is_positive() {
456 ensure_accepted_emitted(
458 client_order_id,
459 account_id,
460 venue_order_id,
461 &identity,
462 emitter,
463 state,
464 ts_init,
465 );
466 let canceled = OrderCanceled::new(
467 emitter.trader_id(),
468 identity.strategy_id,
469 identity.instrument_id,
470 client_order_id,
471 UUID4::new(),
472 ts_init,
473 ts_init,
474 false,
475 Some(venue_order_id),
476 Some(account_id),
477 None,
478 );
479 cleanup_terminal(client_order_id, state);
480 emitter.send_order_event(OrderEventAny::Canceled(canceled));
481 } else {
482 let reason = if order.reject_reason.is_empty() {
483 Ustr::from("Order rejected by venue")
484 } else {
485 order.reject_reason
486 };
487 state.order_identities.remove(&client_order_id);
488 state.order_snapshots.remove(&client_order_id);
489 emitter.emit_order_rejected_event(
490 identity.strategy_id,
491 identity.instrument_id,
492 client_order_id,
493 reason.as_str(),
494 ts_init,
495 bybit_rejection_due_post_only(reason.as_str()),
496 );
497 }
498 }
499 BybitOrderStatus::PartiallyFilled => {
500 ensure_accepted_emitted(
503 client_order_id,
504 account_id,
505 venue_order_id,
506 &identity,
507 emitter,
508 state,
509 ts_init,
510 );
511
512 if let Some(snapshot) = parse_order_snapshot(order, instrument)
516 && is_snapshot_updated(&snapshot, &client_order_id, state)
517 {
518 let updated = OrderUpdated::new(
519 emitter.trader_id(),
520 identity.strategy_id,
521 identity.instrument_id,
522 client_order_id,
523 snapshot.quantity,
524 UUID4::new(),
525 ts_init,
526 ts_init,
527 false,
528 Some(venue_order_id),
529 Some(account_id),
530 snapshot.price,
531 snapshot.trigger_price,
532 None,
533 false,
534 );
535 state.order_snapshots.insert(client_order_id, snapshot);
536 emitter.send_order_event(OrderEventAny::Updated(updated));
537 }
538 }
539 BybitOrderStatus::Filled => {
540 ensure_accepted_emitted(
543 client_order_id,
544 account_id,
545 venue_order_id,
546 &identity,
547 emitter,
548 state,
549 ts_init,
550 );
551
552 if let Some(snapshot) = parse_order_snapshot(order, instrument)
555 && is_snapshot_updated(&snapshot, &client_order_id, state)
556 {
557 let updated = OrderUpdated::new(
558 emitter.trader_id(),
559 identity.strategy_id,
560 identity.instrument_id,
561 client_order_id,
562 snapshot.quantity,
563 UUID4::new(),
564 ts_init,
565 ts_init,
566 false,
567 Some(venue_order_id),
568 Some(account_id),
569 snapshot.price,
570 snapshot.trigger_price,
571 None,
572 false,
573 );
574 state.order_snapshots.insert(client_order_id, snapshot);
575 emitter.send_order_event(OrderEventAny::Updated(updated));
576 }
577 }
581 BybitOrderStatus::Canceled
582 | BybitOrderStatus::PartiallyFilledCanceled
583 | BybitOrderStatus::Deactivated => {
584 let filled_qty = parse_quantity_with_precision(
585 &order.cum_exec_qty,
586 instrument.size_precision(),
587 "order.cumExecQty",
588 )
589 .unwrap_or_default();
590
591 if filled_qty.is_zero()
595 && bybit_rejection_due_post_only(order.reject_reason.as_str())
596 {
597 cleanup_terminal(client_order_id, state);
598 emitter.emit_order_rejected_event(
599 identity.strategy_id,
600 identity.instrument_id,
601 client_order_id,
602 order.reject_reason.as_str(),
603 ts_init,
604 true,
605 );
606 } else {
607 ensure_accepted_emitted(
608 client_order_id,
609 account_id,
610 venue_order_id,
611 &identity,
612 emitter,
613 state,
614 ts_init,
615 );
616 let canceled = OrderCanceled::new(
617 emitter.trader_id(),
618 identity.strategy_id,
619 identity.instrument_id,
620 client_order_id,
621 UUID4::new(),
622 ts_init,
623 ts_init,
624 false,
625 Some(venue_order_id),
626 Some(account_id),
627 None,
628 );
629 cleanup_terminal(client_order_id, state);
630 emitter.send_order_event(OrderEventAny::Canceled(canceled));
631 }
632 }
633 }
634 } else {
635 match super::parse::parse_ws_order_status_report(order, instrument, account_id, ts_init) {
637 Ok(report) => emitter.send_order_status_report(report),
638 Err(e) => log::error!("Failed to parse order status report: {e}"),
639 }
640 }
641}
642
643fn dispatch_execution_fill(
648 exec: &BybitWsAccountExecution,
649 instrument: &InstrumentAny,
650 emitter: &ExecutionEventEmitter,
651 state: &WsDispatchState,
652 account_id: AccountId,
653 ts_init: UnixNanos,
654) {
655 if exec.exec_type == BybitExecType::Funding {
656 log::debug!(
657 "Skipping funding execution: symbol={}, order_id={}, exec_id={}",
658 exec.symbol,
659 exec.order_id,
660 exec.exec_id,
661 );
662 return;
663 }
664
665 if exec.exec_type.is_exchange_generated() {
666 log::warn!(
667 "Exchange-generated execution: exec_type={:?}, symbol={}, order_id={}, order_link_id={}, side={:?}, qty={}, price={}",
668 exec.exec_type,
669 exec.symbol,
670 exec.order_id,
671 exec.order_link_id,
672 exec.side,
673 exec.exec_qty,
674 exec.exec_price,
675 );
676 }
677
678 let client_order_id = if exec.order_link_id.is_empty() {
679 None
680 } else {
681 Some(ClientOrderId::new(exec.order_link_id))
682 };
683
684 let identity = client_order_id
685 .as_ref()
686 .and_then(|cid| state.order_identities.get(cid).map(|r| r.clone()));
687
688 if let (Some(client_order_id), Some(identity)) = (client_order_id, identity) {
689 match parse_order_filled(exec, instrument, &identity, emitter, account_id, ts_init) {
690 Ok(filled) => {
691 let venue_order_id = filled.venue_order_id;
692 ensure_accepted_emitted(
693 client_order_id,
694 account_id,
695 venue_order_id,
696 &identity,
697 emitter,
698 state,
699 ts_init,
700 );
701
702 let is_spot_buy =
703 exec.category == BybitProductType::Spot && exec.side == BybitOrderSide::Buy;
704
705 if is_spot_buy {
706 record_spot_repay_fill(client_order_id, &filled, instrument, state);
707 }
708
709 state.insert_filled(client_order_id);
710 state.triggered_orders.remove(&client_order_id);
711 emitter.send_order_event(OrderEventAny::Filled(filled));
712
713 if exec.leaves_qty == "0" {
714 if is_spot_buy {
715 enqueue_spot_repay(client_order_id, instrument, state);
716 }
717 cleanup_terminal(client_order_id, state);
718 }
719 }
720 Err(e) => log::error!("Failed to parse OrderFilled for {client_order_id}: {e}"),
721 }
722 } else {
723 match super::parse::parse_ws_fill_report(exec, account_id, instrument, ts_init) {
725 Ok(report) => emitter.send_fill_report(report),
726 Err(e) => log::error!("Failed to parse fill report: {e}"),
727 }
728 }
729}
730
731fn dispatch_execution_fill_fast(
738 exec: &BybitWsAccountExecutionFast,
739 instrument: &InstrumentAny,
740 emitter: &ExecutionEventEmitter,
741 state: &WsDispatchState,
742 account_id: AccountId,
743 ts_init: UnixNanos,
744) {
745 let client_order_id = if exec.order_link_id.is_empty() {
746 None
747 } else {
748 Some(ClientOrderId::new(exec.order_link_id))
749 };
750
751 let mut venue_position_id = None;
752
753 if let Some(cid) = client_order_id.as_ref()
754 && let Some(identity) = state.order_identities.get(cid).map(|r| r.clone())
755 {
756 venue_position_id = identity.venue_position_id;
757 let venue_order_id = VenueOrderId::new(exec.order_id);
758 ensure_accepted_emitted(
759 *cid,
760 account_id,
761 venue_order_id,
762 &identity,
763 emitter,
764 state,
765 ts_init,
766 );
767 }
768
769 match parse_ws_fill_report_fast(exec, account_id, instrument, venue_position_id, ts_init) {
770 Ok(report) => emitter.send_fill_report(report),
771 Err(e) => log::error!("Failed to parse fast fill report: {e}"),
772 }
773}
774
775fn parse_order_filled(
777 exec: &BybitWsAccountExecution,
778 instrument: &InstrumentAny,
779 identity: &OrderIdentity,
780 emitter: &ExecutionEventEmitter,
781 account_id: AccountId,
782 ts_init: UnixNanos,
783) -> anyhow::Result<OrderFilled> {
784 let client_order_id = ClientOrderId::new(exec.order_link_id);
785 let venue_order_id = VenueOrderId::new(exec.order_id);
786 let trade_id =
787 TradeId::new_checked(exec.exec_id.as_str()).context("invalid execId in Bybit execution")?;
788
789 let last_qty = parse_quantity_with_precision(
790 &exec.exec_qty,
791 instrument.size_precision(),
792 "execution.execQty",
793 )?;
794 let last_px = parse_price_with_precision(
795 &exec.exec_price,
796 instrument.price_precision(),
797 "execution.execPrice",
798 )?;
799
800 let liquidity_side = if exec.is_maker {
801 LiquiditySide::Maker
802 } else {
803 LiquiditySide::Taker
804 };
805
806 let fee_decimal: Decimal = exec
807 .exec_fee
808 .parse()
809 .with_context(|| format!("failed to parse execFee='{}'", exec.exec_fee))?;
810 let price_currency = instrument.quote_currency();
811 let commission_currency = get_currency(&exec.fee_currency);
812 let commission = Money::from_decimal(fee_decimal, commission_currency).with_context(|| {
813 format!(
814 "failed to create commission from execFee='{}'",
815 exec.exec_fee
816 )
817 })?;
818
819 let ts_event = parse_millis_timestamp(&exec.exec_time, "execution.execTime")?;
820
821 Ok(OrderFilled::new(
822 emitter.trader_id(),
823 identity.strategy_id,
824 identity.instrument_id,
825 client_order_id,
826 venue_order_id,
827 account_id,
828 trade_id,
829 identity.order_side,
830 identity.order_type,
831 last_qty,
832 last_px,
833 price_currency,
834 liquidity_side,
835 UUID4::new(),
836 ts_event,
837 ts_init,
838 false,
839 identity.venue_position_id,
840 Some(commission),
841 None,
842 ))
843}
844
845fn dispatch_order_response(
847 resp: &super::messages::BybitWsOrderResponse,
848 emitter: &ExecutionEventEmitter,
849 state: &WsDispatchState,
850 ts_init: UnixNanos,
851) {
852 if resp.ret_code == 0 {
853 let pending = resp
855 .req_id
856 .as_ref()
857 .and_then(|rid| state.pending_requests.remove(rid))
858 .map(|(_, v)| v);
859
860 if let Some((cids, voids, pending_op)) = pending {
861 let batch_errors = resp.extract_batch_errors();
862 let data_array = resp.data.as_array();
863
864 for (idx, error) in batch_errors.iter().enumerate() {
865 if error.code == 0 {
866 continue;
867 }
868
869 if is_bybit_ambiguous_order_error_code(error.code) {
870 log::warn!(
871 "Ambiguous batch order item failure at index {idx}: code={}, msg={}; awaiting reconciliation",
872 error.code,
873 error.msg,
874 );
875 continue;
876 }
877
878 let cid = data_array
880 .and_then(|arr| arr.get(idx))
881 .and_then(extract_order_link_id_from_data)
882 .or_else(|| cids.get(idx).copied());
883
884 let Some(cid) = cid else {
885 log::warn!(
886 "Batch error at index {idx} without correlation: code={}, msg={}",
887 error.code,
888 error.msg,
889 );
890 continue;
891 };
892
893 let Some(identity) = state.order_identities.get(&cid).map(|r| r.clone()) else {
894 log::warn!(
895 "Batch error for untracked order: client_order_id={cid}, msg={}",
896 error.msg,
897 );
898 continue;
899 };
900
901 let stored_void = voids.get(idx).and_then(|v| *v);
902
903 emit_rejection_for_op(
904 &pending_op,
905 cid,
906 &identity,
907 stored_void,
908 &error.msg,
909 emitter,
910 state,
911 ts_init,
912 );
913 }
914 }
915 return;
916 }
917
918 let pending = resp
920 .req_id
921 .as_ref()
922 .and_then(|rid| state.pending_requests.remove(rid))
923 .map(|(_, v)| v);
924
925 let is_batch_response = is_batch_order_op(resp.op.as_str())
926 || pending.as_ref().is_some_and(|(cids, _, _)| cids.len() > 1);
927
928 if is_batch_response {
929 if is_bybit_ambiguous_order_error_code(resp.ret_code) {
930 let order_count = pending.as_ref().map_or(0, |(cids, _, _)| cids.len());
931 log::warn!(
932 "Ambiguous batch order response failure for {order_count} orders: op={}, ret_code={}, ret_msg={}; awaiting reconciliation",
933 resp.op,
934 resp.ret_code,
935 resp.ret_msg,
936 );
937 return;
938 }
939
940 let Some((client_order_ids, venue_order_ids, pending_op)) = pending else {
941 log::warn!(
942 "Batch order response error without correlation: op={}, ret_code={}, ret_msg={}, req_id={:?}",
943 resp.op,
944 resp.ret_code,
945 resp.ret_msg,
946 resp.req_id,
947 );
948 return;
949 };
950
951 for (index, client_order_id) in client_order_ids.into_iter().enumerate() {
952 let Some(identity) = state
953 .order_identities
954 .get(&client_order_id)
955 .map(|identity| identity.clone())
956 else {
957 log::warn!(
958 "Batch order response error for untracked order: op={}, client_order_id={client_order_id}, ret_msg={}",
959 resp.op,
960 resp.ret_msg,
961 );
962 continue;
963 };
964 emit_rejection_for_op(
965 &pending_op,
966 client_order_id,
967 &identity,
968 venue_order_ids.get(index).copied().flatten(),
969 &resp.ret_msg,
970 emitter,
971 state,
972 ts_init,
973 );
974 }
975 return;
976 }
977
978 if is_bybit_ambiguous_order_error_code(resp.ret_code) {
979 log::warn!(
980 "Ambiguous order response failure: op={}, ret_code={}, ret_msg={}; awaiting reconciliation",
981 resp.op,
982 resp.ret_code,
983 resp.ret_msg,
984 );
985 return;
986 }
987
988 let effective_op = pending
989 .as_ref()
990 .map(|(_, _, op)| *op)
991 .or_else(|| pending_op_from_str(resp.op.as_str()))
992 .unwrap_or_else(|| {
993 log::warn!("Unknown order operation '{}', defaulting to Place", resp.op);
994 PendingOperation::Place
995 });
996
997 let client_order_id = extract_order_link_id_from_data(&resp.data).or_else(|| {
999 pending
1000 .as_ref()
1001 .and_then(|(cids, _, _)| cids.first().copied())
1002 });
1003
1004 let stored_venue_order_id = pending
1005 .as_ref()
1006 .and_then(|(_, voids, _)| voids.first().and_then(|v| *v));
1007
1008 let Some(client_order_id) = client_order_id else {
1009 log::warn!(
1010 "Order response error without correlation: op={}, ret_code={}, ret_msg={}, req_id={:?}",
1011 resp.op,
1012 resp.ret_code,
1013 resp.ret_msg,
1014 resp.req_id,
1015 );
1016 return;
1017 };
1018 let Some(identity) = state
1019 .order_identities
1020 .get(&client_order_id)
1021 .map(|r| r.clone())
1022 else {
1023 log::warn!(
1024 "Order response error for untracked order: op={}, client_order_id={client_order_id}, ret_msg={}",
1025 resp.op,
1026 resp.ret_msg,
1027 );
1028 return;
1029 };
1030
1031 let venue_order_id = extract_venue_order_id_from_data(&resp.data).or(stored_venue_order_id);
1032
1033 emit_rejection_for_op(
1034 &effective_op,
1035 client_order_id,
1036 &identity,
1037 venue_order_id,
1038 &resp.ret_msg,
1039 emitter,
1040 state,
1041 ts_init,
1042 );
1043}
1044
1045#[expect(clippy::too_many_arguments)]
1047fn emit_rejection_for_op(
1048 pending_op: &PendingOperation,
1049 client_order_id: ClientOrderId,
1050 identity: &OrderIdentity,
1051 venue_order_id: Option<VenueOrderId>,
1052 reason: &str,
1053 emitter: &ExecutionEventEmitter,
1054 state: &WsDispatchState,
1055 ts_init: UnixNanos,
1056) {
1057 match pending_op {
1058 PendingOperation::Place => {
1059 state.order_identities.remove(&client_order_id);
1060 state.order_snapshots.remove(&client_order_id);
1061 emitter.emit_order_rejected_event(
1062 identity.strategy_id,
1063 identity.instrument_id,
1064 client_order_id,
1065 reason,
1066 ts_init,
1067 false,
1068 );
1069 }
1070 PendingOperation::Cancel => {
1071 emitter.emit_order_cancel_rejected_event(
1072 identity.strategy_id,
1073 identity.instrument_id,
1074 client_order_id,
1075 venue_order_id,
1076 reason,
1077 ts_init,
1078 );
1079 }
1080 PendingOperation::Amend => {
1081 emitter.emit_order_modify_rejected_event(
1082 identity.strategy_id,
1083 identity.instrument_id,
1084 client_order_id,
1085 venue_order_id,
1086 reason,
1087 ts_init,
1088 );
1089 }
1090 }
1091}
1092
1093fn pending_op_from_str(op: &str) -> Option<PendingOperation> {
1095 match op {
1096 BYBIT_OP_ORDER_CREATE => Some(PendingOperation::Place),
1097 BYBIT_OP_ORDER_CANCEL => Some(PendingOperation::Cancel),
1098 BYBIT_OP_ORDER_AMEND => Some(PendingOperation::Amend),
1099 _ => None,
1100 }
1101}
1102
1103fn is_batch_order_op(op: &str) -> bool {
1104 matches!(
1105 op,
1106 BYBIT_OP_ORDER_CREATE_BATCH | BYBIT_OP_ORDER_AMEND_BATCH | BYBIT_OP_ORDER_CANCEL_BATCH
1107 )
1108}
1109
1110fn parse_order_snapshot(
1112 order: &BybitWsAccountOrder,
1113 instrument: &InstrumentAny,
1114) -> Option<OrderStateSnapshot> {
1115 let quantity =
1116 parse_quantity_with_precision(&order.qty, instrument.size_precision(), "order.qty").ok()?;
1117
1118 let price = if !order.price.is_empty() && order.price != "0" {
1119 parse_price_with_precision(&order.price, instrument.price_precision(), "order.price").ok()
1120 } else {
1121 None
1122 };
1123
1124 let trigger_price = if !order.trigger_price.is_empty() && order.trigger_price != "0" {
1125 parse_price_with_precision(
1126 &order.trigger_price,
1127 instrument.price_precision(),
1128 "order.triggerPrice",
1129 )
1130 .ok()
1131 } else {
1132 None
1133 };
1134
1135 Some(OrderStateSnapshot {
1136 quantity,
1137 price,
1138 trigger_price,
1139 })
1140}
1141
1142fn is_snapshot_updated(
1144 snapshot: &OrderStateSnapshot,
1145 client_order_id: &ClientOrderId,
1146 state: &WsDispatchState,
1147) -> bool {
1148 let Some(previous) = state.order_snapshots.get(client_order_id) else {
1149 return false;
1150 };
1151
1152 if let (Some(prev_price), Some(new_price)) = (previous.price, snapshot.price)
1153 && prev_price != new_price
1154 {
1155 return true;
1156 }
1157
1158 if let (Some(prev_trigger), Some(new_trigger)) =
1159 (previous.trigger_price, snapshot.trigger_price)
1160 && prev_trigger != new_trigger
1161 {
1162 return true;
1163 }
1164
1165 previous.quantity != snapshot.quantity
1166}
1167
1168fn ensure_accepted_emitted(
1171 client_order_id: ClientOrderId,
1172 account_id: AccountId,
1173 venue_order_id: VenueOrderId,
1174 identity: &OrderIdentity,
1175 emitter: &ExecutionEventEmitter,
1176 state: &WsDispatchState,
1177 ts_init: UnixNanos,
1178) {
1179 if state.emitted_accepted.contains(&client_order_id) {
1180 return;
1181 }
1182 state.insert_accepted(client_order_id);
1183 let accepted = OrderAccepted::new(
1184 emitter.trader_id(),
1185 identity.strategy_id,
1186 identity.instrument_id,
1187 client_order_id,
1188 venue_order_id,
1189 account_id,
1190 UUID4::new(),
1191 ts_init,
1192 ts_init,
1193 false,
1194 );
1195 emitter.send_order_event(OrderEventAny::Accepted(accepted));
1196}
1197
1198fn cleanup_terminal(client_order_id: ClientOrderId, state: &WsDispatchState) {
1200 state.order_identities.remove(&client_order_id);
1201 state.order_snapshots.remove(&client_order_id);
1202 state.emitted_accepted.remove(&client_order_id);
1203 state.triggered_orders.remove(&client_order_id);
1204 state.filled_orders.remove(&client_order_id);
1205 state.spot_repay_fills.remove(&client_order_id);
1206}
1207
1208fn extract_order_link_id_from_data(data: &serde_json::Value) -> Option<ClientOrderId> {
1210 data.get("orderLinkId")
1211 .and_then(|v| v.as_str())
1212 .filter(|s| !s.is_empty())
1213 .map(ClientOrderId::new)
1214}
1215
1216fn extract_venue_order_id_from_data(data: &serde_json::Value) -> Option<VenueOrderId> {
1218 data.get("orderId")
1219 .and_then(|v| v.as_str())
1220 .filter(|s| !s.is_empty())
1221 .map(VenueOrderId::new)
1222}
1223
1224fn record_spot_repay_fill(
1225 client_order_id: ClientOrderId,
1226 filled: &OrderFilled,
1227 instrument: &InstrumentAny,
1228 state: &WsDispatchState,
1229) {
1230 let Some(base_currency) = instrument.base_currency() else {
1231 return;
1232 };
1233
1234 let base_fee = filled
1235 .commission
1236 .filter(|fee| fee.currency.code == base_currency.code)
1237 .map_or(Decimal::ZERO, |fee| fee.as_decimal().max(Decimal::ZERO));
1238 let fill = SpotRepayFill {
1239 quantity: filled.last_qty,
1240 base_fee,
1241 };
1242 state
1243 .spot_repay_fills
1244 .entry(client_order_id)
1245 .and_modify(|total| {
1246 total.quantity = total.quantity + fill.quantity;
1247 total.base_fee += fill.base_fee;
1248 })
1249 .or_insert(fill);
1250}
1251
1252fn enqueue_spot_repay(
1254 client_order_id: ClientOrderId,
1255 instrument: &InstrumentAny,
1256 state: &WsDispatchState,
1257) {
1258 let Some(base_currency) = instrument.base_currency() else {
1259 return;
1260 };
1261 let Some((_, fill)) = state.spot_repay_fills.remove(&client_order_id) else {
1262 return;
1263 };
1264
1265 state.enqueue_repay(RepayRequest {
1266 coin: base_currency.code,
1267 quantity: fill.quantity,
1268 base_fee: fill.base_fee,
1269 repayment_precision: fill.quantity.precision.max(base_currency.precision),
1270 });
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use ahash::AHashMap;
1276 use nautilus_common::messages::{ExecutionEvent, execution::ExecutionReport};
1277 use nautilus_core::{
1278 UnixNanos,
1279 time::{AtomicTime, get_atomic_clock_realtime},
1280 };
1281 use nautilus_live::{emitter::ExecutionEventEmitter, testing::ExecutionHarness};
1282 use nautilus_model::{
1283 accounts::MarginAccount,
1284 enums::{AccountType, OrderSide, OrderStatus, OrderType},
1285 events::OrderEventAny,
1286 identifiers::{
1287 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId, VenueOrderId,
1288 },
1289 instruments::{Instrument, InstrumentAny},
1290 orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
1291 reports::FillReport,
1292 types::Currency,
1293 };
1294 use rstest::rstest;
1295 use ustr::Ustr;
1296
1297 use super::*;
1298 use crate::{
1299 common::{
1300 enums::{BybitExecType, BybitOrderSide, BybitProductType},
1301 parse::{parse_inverse_instrument, parse_linear_instrument, parse_spot_instrument},
1302 testing::load_test_json,
1303 },
1304 http::models::{
1305 BybitInstrumentInverseResponse, BybitInstrumentLinearResponse,
1306 BybitInstrumentSpotResponse,
1307 },
1308 websocket::messages::{
1309 BybitWsAccountExecutionFastMsg, BybitWsMessage, BybitWsOrderResponse,
1310 },
1311 };
1312
1313 fn linear_instrument() -> InstrumentAny {
1314 let json = load_test_json("http_get_instruments_linear.json");
1315 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1316 let instrument = &response.result.list[0];
1317 let ts = UnixNanos::new(1_700_000_000_000_000_000);
1318 parse_linear_instrument(instrument, ts, ts).unwrap()
1319 }
1320
1321 fn spot_instrument() -> InstrumentAny {
1322 let json = load_test_json("http_get_instruments_spot.json");
1323 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1324 let instrument = &response.result.list[0];
1325 let ts = UnixNanos::new(1_700_000_000_000_000_000);
1326 parse_spot_instrument(instrument, ts, ts).unwrap()
1327 }
1328
1329 fn inverse_instrument() -> InstrumentAny {
1330 let json = load_test_json("http_get_instruments_inverse.json");
1331 let response: BybitInstrumentInverseResponse = serde_json::from_str(&json).unwrap();
1332 let instrument = &response.result.list[0];
1333 let ts = UnixNanos::new(1_700_000_000_000_000_000);
1334 parse_inverse_instrument(instrument, ts, ts).unwrap()
1335 }
1336
1337 fn build_instruments(instruments: &[InstrumentAny]) -> AHashMap<Ustr, InstrumentAny> {
1338 let mut map = AHashMap::new();
1339 for inst in instruments {
1340 map.insert(inst.id().symbol.inner(), inst.clone());
1341 }
1342 map
1343 }
1344
1345 fn test_account_id() -> AccountId {
1346 AccountId::from("BYBIT-001")
1347 }
1348
1349 fn create_emitter() -> (
1350 ExecutionEventEmitter,
1351 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1352 ) {
1353 let clock = get_atomic_clock_realtime();
1354 let trader_id = TraderId::from("TESTER-001");
1355 let account_id = test_account_id();
1356 let mut emitter =
1357 ExecutionEventEmitter::new(clock, trader_id, account_id, AccountType::Margin, None);
1358 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1359 emitter.set_sender(tx);
1360 (emitter, rx)
1361 }
1362
1363 fn default_identity() -> OrderIdentity {
1364 OrderIdentity {
1365 instrument_id: InstrumentId::from("BTCUSDT-LINEAR.BYBIT"),
1366 strategy_id: StrategyId::from("S-001"),
1367 order_side: OrderSide::Buy,
1368 order_type: OrderType::Limit,
1369 venue_position_id: None,
1370 }
1371 }
1372
1373 #[rstest]
1374 #[case::base_fee("BTC", "0.0000015", "0.0000025", "0.000004")]
1375 #[case::base_rebate("BTC", "-0.0000015", "-0.0000025", "0")]
1376 #[case::quote_fee("USDT", "0.075", "0.125", "0")]
1377 fn test_spot_repay_uses_accumulated_execution_quantity(
1378 #[case] fee_currency: &str,
1379 #[case] first_fee: &str,
1380 #[case] second_fee: &str,
1381 #[case] expected_base_fee: &str,
1382 ) {
1383 let instrument = spot_instrument();
1384 let instruments = build_instruments(std::slice::from_ref(&instrument));
1385 let (emitter, _rx) = create_emitter();
1386 let clock = get_atomic_clock_realtime();
1387 let state = WsDispatchState::default();
1388 let (repay_tx, mut repay_rx) = tokio::sync::mpsc::unbounded_channel();
1389 state.set_repay_sender(repay_tx);
1390
1391 let json = load_test_json("ws_account_execution.json");
1392 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
1393 value["data"][0]["category"] = serde_json::Value::String("spot".to_string());
1394 value["data"][0]["symbol"] = serde_json::Value::String("BTCUSDT".to_string());
1395 value["data"][0]["side"] = serde_json::Value::String("Buy".to_string());
1396 value["data"][0]["orderType"] = serde_json::Value::String("Market".to_string());
1397 value["data"][0]["orderQty"] = serde_json::Value::String("100".to_string());
1398 value["data"][0]["execQty"] = serde_json::Value::String("0.0015".to_string());
1399 value["data"][0]["leavesQty"] = serde_json::Value::String("0.0025".to_string());
1400 value["data"][0]["execFee"] = serde_json::Value::String(first_fee.to_string());
1401 value["data"][0]["feeCurrency"] = serde_json::Value::String(fee_currency.to_string());
1402
1403 let first: crate::websocket::messages::BybitWsAccountExecutionMsg =
1404 serde_json::from_value(value.clone()).unwrap();
1405 let client_order_id = ClientOrderId::new(first.data[0].order_link_id);
1406 state.order_identities.insert(
1407 client_order_id,
1408 OrderIdentity {
1409 instrument_id: InstrumentId::from("BTCUSDT-SPOT.BYBIT"),
1410 order_type: OrderType::Market,
1411 ..default_identity()
1412 },
1413 );
1414
1415 dispatch_ws_message(
1416 &BybitWsMessage::AccountExecution(first),
1417 &emitter,
1418 &state,
1419 test_account_id(),
1420 &instruments,
1421 clock,
1422 );
1423 assert!(repay_rx.try_recv().is_err());
1424
1425 value["data"][0]["execId"] = serde_json::Value::String("second-execution".to_string());
1426 value["data"][0]["execQty"] = serde_json::Value::String("0.0025".to_string());
1427 value["data"][0]["leavesQty"] = serde_json::Value::String("0".to_string());
1428 value["data"][0]["execFee"] = serde_json::Value::String(second_fee.to_string());
1429 let second: crate::websocket::messages::BybitWsAccountExecutionMsg =
1430 serde_json::from_value(value).unwrap();
1431
1432 dispatch_ws_message(
1433 &BybitWsMessage::AccountExecution(second),
1434 &emitter,
1435 &state,
1436 test_account_id(),
1437 &instruments,
1438 clock,
1439 );
1440
1441 let repay = repay_rx.try_recv().expect("expected a repay request");
1442 assert_eq!(repay.coin, "BTC");
1443 assert_eq!(repay.quantity, Quantity::from("0.0040"));
1444 assert_eq!(
1445 repay.base_fee,
1446 expected_base_fee.parse::<Decimal>().unwrap()
1447 );
1448 assert_eq!(repay.repayment_precision, 8);
1449 assert!(repay_rx.try_recv().is_err());
1450 }
1451
1452 #[rstest]
1453 fn test_dispatch_tracked_canceled_order_emits_accepted_then_canceled() {
1454 let instrument = linear_instrument();
1455 let instruments = build_instruments(std::slice::from_ref(&instrument));
1456 let (emitter, mut rx) = create_emitter();
1457 let clock = get_atomic_clock_realtime();
1458 let state = WsDispatchState::default();
1459
1460 let json = load_test_json("ws_account_order.json");
1462 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1463 serde_json::from_str(&json).unwrap();
1464
1465 if let Some(order) = msg.data.first()
1466 && !order.order_link_id.is_empty()
1467 {
1468 let cid = ClientOrderId::new(order.order_link_id);
1469 state.order_identities.insert(cid, default_identity());
1470 }
1471
1472 let ws_msg = BybitWsMessage::AccountOrder(msg);
1473 dispatch_ws_message(
1474 &ws_msg,
1475 &emitter,
1476 &state,
1477 test_account_id(),
1478 &instruments,
1479 clock,
1480 );
1481
1482 let event1 = rx.try_recv().unwrap();
1484 assert!(
1485 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(ref a)) if a.strategy_id == StrategyId::from("S-001")),
1486 "Expected Accepted, found {event1:?}"
1487 );
1488
1489 let event2 = rx.try_recv().unwrap();
1491 assert!(
1492 matches!(event2, ExecutionEvent::Order(OrderEventAny::Canceled(_))),
1493 "Expected Canceled, found {event2:?}"
1494 );
1495 }
1496
1497 #[rstest]
1498 fn test_dispatch_tracked_post_only_cancel_emits_rejected() {
1499 const BYBIT_POST_ONLY_REJECT_REASON: &str = "EC_PostOnlyWillTakeLiquidity";
1500
1501 let instrument = linear_instrument();
1502 let instruments = build_instruments(std::slice::from_ref(&instrument));
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_order.json");
1510 let mut msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1511 serde_json::from_str(&json).unwrap();
1512
1513 let order = msg.data.first_mut().expect("fixture has an order");
1514 order.reject_reason = Ustr::from(BYBIT_POST_ONLY_REJECT_REASON);
1515 order.cum_exec_qty = "0".to_string();
1516 let cid = ClientOrderId::new(order.order_link_id);
1517 state.order_identities.insert(cid, default_identity());
1518
1519 let ws_msg = BybitWsMessage::AccountOrder(msg);
1520 dispatch_ws_message(
1521 &ws_msg,
1522 &emitter,
1523 &state,
1524 test_account_id(),
1525 &instruments,
1526 clock,
1527 );
1528
1529 let event = rx.try_recv().unwrap();
1530 let ExecutionEvent::Order(OrderEventAny::Rejected(rejected)) = event else {
1531 panic!("Expected Rejected, found {event:?}");
1532 };
1533 assert!(rejected.due_post_only);
1534 assert_eq!(rejected.reason, BYBIT_POST_ONLY_REJECT_REASON);
1535 assert_eq!(rejected.client_order_id, cid);
1536 assert!(rx.try_recv().is_err(), "expected only a single event");
1537 }
1538
1539 #[rstest]
1540 fn test_dispatch_untracked_order_emits_report() {
1541 let instrument = linear_instrument();
1542 let instruments = build_instruments(std::slice::from_ref(&instrument));
1543 let (emitter, mut rx) = create_emitter();
1544 let clock = get_atomic_clock_realtime();
1545 let state = WsDispatchState::default();
1546
1547 let json = load_test_json("ws_account_order.json");
1548 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1549 serde_json::from_str(&json).unwrap();
1550
1551 let ws_msg = BybitWsMessage::AccountOrder(msg);
1553 dispatch_ws_message(
1554 &ws_msg,
1555 &emitter,
1556 &state,
1557 test_account_id(),
1558 &instruments,
1559 clock,
1560 );
1561
1562 let event = rx.try_recv().unwrap();
1563 assert!(matches!(
1564 event,
1565 ExecutionEvent::Report(ExecutionReport::Order(_))
1566 ));
1567 }
1568
1569 #[rstest]
1570 fn test_dispatch_tracked_execution_emits_order_filled() {
1571 let instrument = linear_instrument();
1572 let instruments = build_instruments(std::slice::from_ref(&instrument));
1573 let (emitter, mut rx) = create_emitter();
1574 let clock = get_atomic_clock_realtime();
1575 let state = WsDispatchState::default();
1576
1577 let json = load_test_json("ws_account_execution.json");
1578 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1579 serde_json::from_str(&json).unwrap();
1580
1581 if let Some(exec) = msg.data.first()
1583 && !exec.order_link_id.is_empty()
1584 {
1585 let cid = ClientOrderId::new(exec.order_link_id);
1586 state.order_identities.insert(cid, default_identity());
1587 }
1588
1589 let ws_msg = BybitWsMessage::AccountExecution(msg);
1590 dispatch_ws_message(
1591 &ws_msg,
1592 &emitter,
1593 &state,
1594 test_account_id(),
1595 &instruments,
1596 clock,
1597 );
1598
1599 let event1 = rx.try_recv().unwrap();
1601 assert!(
1602 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
1603 "Expected Accepted, found {event1:?}"
1604 );
1605
1606 let event2 = rx.try_recv().unwrap();
1608 match event2 {
1609 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1610 assert_eq!(filled.strategy_id, StrategyId::from("S-001"));
1611 assert_eq!(filled.order_side, OrderSide::Buy);
1612 assert_eq!(filled.order_type, OrderType::Limit);
1613 }
1614 other => panic!("Expected Filled event, found {other:?}"),
1615 }
1616 }
1617
1618 #[rstest]
1619 #[case::commission_overflow("execFee", "9999999999999999999999999999")]
1620 #[case::commission_parse("execFee", "invalid")]
1621 #[case::qty("execQty", "invalid")]
1622 #[case::price("execPrice", "invalid")]
1623 #[case::timestamp("execTime", "invalid")]
1624 #[case::trade_id("execId", "")]
1625 fn test_dispatch_invalid_execution_preserves_state(
1626 #[case] field: &str,
1627 #[case] value: &str,
1628 #[values(false, true)] spot: bool,
1629 ) {
1630 let instrument = if spot {
1631 spot_instrument()
1632 } else {
1633 linear_instrument()
1634 };
1635
1636 let instruments = build_instruments(std::slice::from_ref(&instrument));
1637 let (emitter, mut rx) = create_emitter();
1638 let state = WsDispatchState::default();
1639 let (repay_tx, mut repay_rx) = tokio::sync::mpsc::unbounded_channel();
1640 state.set_repay_sender(repay_tx);
1641 let mut payload: serde_json::Value =
1642 serde_json::from_str(&load_test_json("ws_account_execution.json")).unwrap();
1643 payload["data"][0][field] = serde_json::Value::String(value.to_string());
1644 payload["data"][0]["side"] = serde_json::Value::String("Buy".to_string());
1645 if spot {
1646 payload["data"][0]["category"] = serde_json::Value::String("spot".to_string());
1647 }
1648
1649 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1650 serde_json::from_value(payload).unwrap();
1651 let cid = ClientOrderId::new(msg.data[0].order_link_id);
1652
1653 let identity = OrderIdentity {
1654 instrument_id: instrument.id(),
1655 venue_position_id: Some(PositionId::from("P-001")),
1656 ..default_identity()
1657 };
1658
1659 state.order_identities.insert(cid, identity.clone());
1660 state.order_snapshots.insert(
1661 cid,
1662 OrderStateSnapshot {
1663 quantity: Quantity::from("0.500"),
1664 price: Some(Price::from("95900.1")),
1665 trigger_price: Some(Price::from("95800.2")),
1666 },
1667 );
1668
1669 state.insert_triggered(cid);
1670
1671 dispatch_ws_message(
1672 &BybitWsMessage::AccountExecution(msg),
1673 &emitter,
1674 &state,
1675 test_account_id(),
1676 &instruments,
1677 get_atomic_clock_realtime(),
1678 );
1679
1680 assert!(rx.try_recv().is_err());
1681 assert!(repay_rx.try_recv().is_err());
1682 assert!(state.emitted_accepted.is_empty());
1683 assert!(state.filled_orders.is_empty());
1684 assert!(state.spot_repay_fills.is_empty());
1685 assert!(state.pending_requests.is_empty());
1686 assert_eq!(state.triggered_orders.len(), 1);
1687 assert!(state.triggered_orders.contains(&cid));
1688 assert_eq!(state.order_identities.len(), 1);
1689 let retained = state.order_identities.get(&cid).unwrap();
1690 assert_eq!(retained.instrument_id, identity.instrument_id);
1691 assert_eq!(retained.strategy_id, identity.strategy_id);
1692 assert_eq!(retained.order_side, identity.order_side);
1693 assert_eq!(retained.order_type, identity.order_type);
1694 assert_eq!(retained.venue_position_id, identity.venue_position_id);
1695 assert_eq!(state.order_snapshots.len(), 1);
1696 let snapshot = state.order_snapshots.get(&cid).unwrap();
1697 assert_eq!(snapshot.quantity, Quantity::from("0.500"));
1698 assert_eq!(snapshot.price, Some(Price::from("95900.1")));
1699 assert_eq!(snapshot.trigger_price, Some(Price::from("95800.2")));
1700 }
1701
1702 #[rstest]
1703 #[case::partial("0.5", "1.000", OrderStatus::PartiallyFilled)]
1704 #[case::terminal("0", "0.500", OrderStatus::Filled)]
1705 fn test_dispatch_execution_commission_failure_recovery_and_replay(
1706 #[case] leaves_qty: &str,
1707 #[case] order_qty: &str,
1708 #[case] expected_status: OrderStatus,
1709 ) {
1710 let instrument = linear_instrument();
1711 let instruments = build_instruments(std::slice::from_ref(&instrument));
1712 let (emitter, mut rx) = create_emitter();
1713 let state = WsDispatchState::default();
1714 let clock = AtomicTime::new(false, UnixNanos::new(1_746_270_400_400_000_000));
1715 let mut msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1716 serde_json::from_str(&load_test_json("ws_account_execution.json")).unwrap();
1717 msg.data[0].leaves_qty = leaves_qty.to_string();
1718 msg.data[0].side = BybitOrderSide::Buy;
1719 msg.data[0].exec_fee = "9999999999999999999999999999".to_string();
1720 let cid = ClientOrderId::new(msg.data[0].order_link_id);
1721 let identity = default_identity();
1722 state.order_identities.insert(cid, identity.clone());
1723
1724 let harness = ExecutionHarness::new(
1725 emitter.trader_id(),
1726 *crate::common::consts::BYBIT_CLIENT_ID,
1727 test_account_id(),
1728 instrument.clone(),
1729 );
1730 let wallet: crate::websocket::messages::BybitWsAccountWalletMsg =
1731 serde_json::from_str(&load_test_json("ws_account_wallet.json")).unwrap();
1732 let account_state = parse_ws_account_state(
1733 &wallet.data[0],
1734 test_account_id(),
1735 UnixNanos::default(),
1736 UnixNanos::default(),
1737 )
1738 .unwrap();
1739 let account = MarginAccount::new(account_state, false);
1740 harness
1741 .cache()
1742 .borrow_mut()
1743 .add_account(account.into())
1744 .unwrap();
1745 let mut order = OrderTestBuilder::new(identity.order_type)
1746 .trader_id(emitter.trader_id())
1747 .strategy_id(identity.strategy_id)
1748 .instrument_id(instrument.id())
1749 .client_order_id(cid)
1750 .side(identity.order_side)
1751 .quantity(Quantity::from(order_qty))
1752 .price(Price::from("94942.5"))
1753 .build();
1754 let submitted = TestOrderEventStubs::submitted(&order, test_account_id());
1755 order.apply(submitted).unwrap();
1756 harness
1757 .cache()
1758 .borrow_mut()
1759 .add_order(order, None, None, false)
1760 .unwrap();
1761
1762 dispatch_ws_message(
1763 &BybitWsMessage::AccountExecution(msg.clone()),
1764 &emitter,
1765 &state,
1766 test_account_id(),
1767 &instruments,
1768 &clock,
1769 );
1770
1771 assert!(rx.try_recv().is_err());
1772 assert!(state.emitted_accepted.is_empty());
1773 assert!(state.filled_orders.is_empty());
1774 assert_eq!(state.order_identities.len(), 1);
1775
1776 msg.data[0].exec_fee = "0.01234567".to_string();
1777 let message = BybitWsMessage::AccountExecution(msg);
1778 dispatch_ws_message(
1779 &message,
1780 &emitter,
1781 &state,
1782 test_account_id(),
1783 &instruments,
1784 &clock,
1785 );
1786
1787 let ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) = rx.try_recv().unwrap()
1788 else {
1789 panic!("expected Accepted before Filled");
1790 };
1791
1792 let expected_accepted = OrderAccepted::new(
1793 emitter.trader_id(),
1794 identity.strategy_id,
1795 instrument.id(),
1796 cid,
1797 VenueOrderId::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
1798 test_account_id(),
1799 accepted.event_id,
1800 clock.get_time_ns(),
1801 clock.get_time_ns(),
1802 false,
1803 );
1804 assert_eq!(accepted, expected_accepted);
1805 harness
1806 .exec_engine()
1807 .borrow_mut()
1808 .process(&OrderEventAny::Accepted(accepted));
1809
1810 let ExecutionEvent::Order(OrderEventAny::Filled(filled)) = rx.try_recv().unwrap() else {
1811 panic!("expected Filled after Accepted");
1812 };
1813
1814 let commission = Money::from("0.01234567 USDT");
1815
1816 let expected_filled = OrderFilled::new(
1817 emitter.trader_id(),
1818 identity.strategy_id,
1819 instrument.id(),
1820 cid,
1821 VenueOrderId::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
1822 test_account_id(),
1823 TradeId::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
1824 OrderSide::Buy,
1825 OrderType::Limit,
1826 Quantity::from("0.500"),
1827 Price::from("95900.1"),
1828 Currency::USDT(),
1829 LiquiditySide::Taker,
1830 filled.event_id,
1831 UnixNanos::new(1_746_270_400_353_000_000),
1832 clock.get_time_ns(),
1833 false,
1834 None,
1835 Some(commission),
1836 None,
1837 );
1838 assert_eq!(filled, expected_filled);
1839 assert!(rx.try_recv().is_err());
1840 harness
1841 .exec_engine()
1842 .borrow_mut()
1843 .process(&OrderEventAny::Filled(filled.clone()));
1844 let applied = harness.cache().borrow().order(&cid).unwrap().clone();
1845 assert_eq!(applied.status(), expected_status);
1846 assert_eq!(applied.filled_qty(), Quantity::from("0.500"));
1847 assert_eq!(
1848 applied.commissions().get(&Currency::USDT()),
1849 Some(&commission)
1850 );
1851 assert_eq!(applied.trade_ids(), vec![&filled.trade_id]);
1852 let position_id = *harness.cache().borrow().position_id(&cid).unwrap();
1853 let position = harness
1854 .cache()
1855 .borrow()
1856 .position(&position_id)
1857 .unwrap()
1858 .clone();
1859 assert_eq!(position.quantity, Quantity::from("0.500"));
1860 assert_eq!(position.commissions(), vec![commission]);
1861 let mut expected_position_fill = filled.clone();
1862 expected_position_fill.position_id = Some(position_id);
1863 assert_eq!(position.events, vec![expected_position_fill]);
1864
1865 dispatch_ws_message(
1866 &message,
1867 &emitter,
1868 &state,
1869 test_account_id(),
1870 &instruments,
1871 &clock,
1872 );
1873 let replay = rx.try_recv().unwrap();
1874 match replay {
1875 ExecutionEvent::Order(OrderEventAny::Filled(replayed)) => {
1876 let expected_replay = OrderFilled {
1877 event_id: replayed.event_id,
1878 ..filled.clone()
1879 };
1880
1881 assert_eq!(replayed, expected_replay);
1882 assert_eq!(leaves_qty, "0.5");
1883 harness
1884 .exec_engine()
1885 .borrow_mut()
1886 .process(&OrderEventAny::Filled(replayed));
1887 }
1888 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
1889 let expected_report = FillReport::new(
1890 filled.account_id,
1891 filled.instrument_id,
1892 filled.venue_order_id,
1893 filled.trade_id,
1894 filled.order_side,
1895 filled.last_qty,
1896 filled.last_px,
1897 commission,
1898 filled.liquidity_side,
1899 Some(cid),
1900 None,
1901 filled.ts_event,
1902 filled.ts_init,
1903 Some(report.report_id),
1904 );
1905 assert_eq!(*report, expected_report);
1906 assert_eq!(leaves_qty, "0");
1907 harness
1908 .exec_engine()
1909 .borrow_mut()
1910 .reconcile_fill_report(&report);
1911 }
1912 other => panic!("unexpected replay: {other:?}"),
1913 }
1914
1915 harness
1916 .exec_engine()
1917 .borrow_mut()
1918 .process(&OrderEventAny::Filled(filled));
1919
1920 assert!(rx.try_recv().is_err());
1921 assert_eq!(
1922 serde_json::to_value(harness.cache().borrow().order(&cid).unwrap().clone()).unwrap(),
1923 serde_json::to_value(&applied).unwrap()
1924 );
1925 assert_eq!(
1926 serde_json::to_value(
1927 harness
1928 .cache()
1929 .borrow()
1930 .position(&position_id)
1931 .unwrap()
1932 .clone()
1933 )
1934 .unwrap(),
1935 serde_json::to_value(&position).unwrap()
1936 );
1937 }
1938
1939 #[rstest]
1940 fn parse_order_filled_uses_payload_fee_currency() {
1941 let instrument = linear_instrument();
1942 let (emitter, _rx) = create_emitter();
1943
1944 let json = load_test_json("ws_account_execution.json");
1945 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1946 serde_json::from_str(&json).unwrap();
1947
1948 let mut exec = msg.data[0].clone();
1949 exec.fee_currency = Ustr::from("BTC");
1950
1951 let filled = parse_order_filled(
1952 &exec,
1953 &instrument,
1954 &default_identity(),
1955 &emitter,
1956 test_account_id(),
1957 UnixNanos::default(),
1958 )
1959 .unwrap();
1960
1961 let commission = filled.commission.expect("commission present");
1962 assert_eq!(commission.currency.code, "BTC");
1963 }
1964
1965 #[rstest]
1966 #[case::spot_base_fee(BybitProductType::Spot, "BTC", "0.000001", "USDT")]
1967 #[case::spot_quote_fee(BybitProductType::Spot, "USDT", "0.1", "USDT")]
1968 #[case::linear_fee(BybitProductType::Linear, "USDT", "0.1", "USDT")]
1969 #[case::inverse_fee(BybitProductType::Inverse, "BTC", "0.000001", "USD")]
1970 #[case::spot_zero_fee(BybitProductType::Spot, "BTC", "0", "USDT")]
1971 #[case::inverse_rebate(BybitProductType::Inverse, "BTC", "-0.000001", "USD")]
1972 fn parse_order_filled_separates_price_and_commission_currencies(
1973 #[case] category: BybitProductType,
1974 #[case] fee_currency: &str,
1975 #[case] fee: &str,
1976 #[case] expected_price_currency: &str,
1977 ) {
1978 let instrument = match category {
1979 BybitProductType::Spot => spot_instrument(),
1980 BybitProductType::Linear => linear_instrument(),
1981 BybitProductType::Inverse => inverse_instrument(),
1982 _ => unreachable!("unsupported test product"),
1983 };
1984 let (emitter, _rx) = create_emitter();
1985 let json = load_test_json("ws_account_execution.json");
1986 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1987 serde_json::from_str(&json).unwrap();
1988 let mut exec = msg.data[0].clone();
1989 exec.category = category;
1990 exec.symbol = instrument.raw_symbol().inner();
1991 exec.exec_qty = "1".to_string();
1992 exec.exec_fee = fee.to_string();
1993 exec.fee_currency = Ustr::from(fee_currency);
1994 let identity = OrderIdentity {
1995 instrument_id: instrument.id(),
1996 order_side: OrderSide::Sell,
1997 order_type: OrderType::Market,
1998 ..default_identity()
1999 };
2000
2001 let filled = parse_order_filled(
2002 &exec,
2003 &instrument,
2004 &identity,
2005 &emitter,
2006 test_account_id(),
2007 UnixNanos::default(),
2008 )
2009 .unwrap();
2010
2011 let commission = filled.commission.expect("commission present");
2012 assert_eq!(commission.currency.code, fee_currency);
2013 assert_eq!(commission.as_decimal(), fee.parse::<Decimal>().unwrap());
2014 assert_eq!(filled.currency.code, expected_price_currency);
2015 }
2016
2017 #[rstest]
2018 fn test_dispatch_tracked_execution_preserves_venue_position_id() {
2019 let instrument = linear_instrument();
2020 let instruments = build_instruments(std::slice::from_ref(&instrument));
2021 let (emitter, mut rx) = create_emitter();
2022 let clock = get_atomic_clock_realtime();
2023 let state = WsDispatchState::default();
2024
2025 let json = load_test_json("ws_account_execution.json");
2026 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
2027 serde_json::from_str(&json).unwrap();
2028 let venue_position_id = PositionId::from("BTCUSDT-LINEAR.BYBIT-LONG");
2029
2030 if let Some(exec) = msg.data.first()
2031 && !exec.order_link_id.is_empty()
2032 {
2033 let cid = ClientOrderId::new(exec.order_link_id);
2034 state.order_identities.insert(
2035 cid,
2036 OrderIdentity {
2037 venue_position_id: Some(venue_position_id),
2038 ..default_identity()
2039 },
2040 );
2041 }
2042
2043 let ws_msg = BybitWsMessage::AccountExecution(msg);
2044 dispatch_ws_message(
2045 &ws_msg,
2046 &emitter,
2047 &state,
2048 test_account_id(),
2049 &instruments,
2050 clock,
2051 );
2052
2053 let _accepted = rx.try_recv().unwrap();
2054 let event = rx.try_recv().unwrap();
2055 match event {
2056 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
2057 assert_eq!(filled.position_id, Some(venue_position_id));
2058 }
2059 other => panic!("Expected Filled event, found {other:?}"),
2060 }
2061 }
2062
2063 fn fast_execution_msg(is_maker: bool, order_link_id: &str) -> BybitWsAccountExecutionFastMsg {
2064 BybitWsAccountExecutionFastMsg {
2065 topic: Ustr::from("execution.fast"),
2066 id: String::new(),
2067 creation_time: 1_716_800_399_338,
2068 data: vec![BybitWsAccountExecutionFast {
2069 category: BybitProductType::Linear,
2070 symbol: Ustr::from("BTCUSDT"),
2071 exec_id: "fast-1".to_string(),
2072 exec_price: "50000.0".to_string(),
2073 exec_qty: "0.5".to_string(),
2074 order_id: Ustr::from("ord-1"),
2075 order_link_id: Ustr::from(order_link_id),
2076 side: BybitOrderSide::Buy,
2077 exec_time: "1716800399334".to_string(),
2078 is_maker,
2079 seq: 42,
2080 }],
2081 }
2082 }
2083
2084 #[rstest]
2085 fn test_dispatch_tracked_fast_execution_preserves_venue_position_id() {
2086 let instrument = linear_instrument();
2087 let instruments = build_instruments(std::slice::from_ref(&instrument));
2088 let (emitter, mut rx) = create_emitter();
2089 let clock = get_atomic_clock_realtime();
2090 let state = WsDispatchState::default();
2091 let venue_position_id = PositionId::from("BTCUSDT-LINEAR.BYBIT-LONG");
2092
2093 let msg = fast_execution_msg(false, "link-1");
2095 let cid = ClientOrderId::new(msg.data[0].order_link_id);
2096 state.order_identities.insert(
2097 cid,
2098 OrderIdentity {
2099 venue_position_id: Some(venue_position_id),
2100 ..default_identity()
2101 },
2102 );
2103
2104 let ws_msg = BybitWsMessage::AccountExecutionFast(msg);
2105 dispatch_ws_message(
2106 &ws_msg,
2107 &emitter,
2108 &state,
2109 test_account_id(),
2110 &instruments,
2111 clock,
2112 );
2113
2114 let event1 = rx.try_recv().unwrap();
2116 assert!(
2117 matches!(event1, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
2118 "Expected Accepted, found {event1:?}",
2119 );
2120
2121 let event2 = rx.try_recv().unwrap();
2123 match event2 {
2124 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
2125 assert_eq!(report.venue_position_id, Some(venue_position_id));
2126 assert_eq!(report.client_order_id, Some(cid));
2127 }
2128 other => panic!("Expected FillReport, found {other:?}"),
2129 }
2130 }
2131
2132 #[rstest]
2133 fn test_dispatch_untracked_fast_execution_emits_fill_report_without_position() {
2134 let instrument = linear_instrument();
2135 let instruments = build_instruments(std::slice::from_ref(&instrument));
2136 let (emitter, mut rx) = create_emitter();
2137 let clock = get_atomic_clock_realtime();
2138 let state = WsDispatchState::default();
2139
2140 let msg = fast_execution_msg(true, "");
2142 let ws_msg = BybitWsMessage::AccountExecutionFast(msg);
2143 dispatch_ws_message(
2144 &ws_msg,
2145 &emitter,
2146 &state,
2147 test_account_id(),
2148 &instruments,
2149 clock,
2150 );
2151
2152 let event = rx.try_recv().unwrap();
2154 match event {
2155 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
2156 assert_eq!(report.client_order_id, None);
2157 assert_eq!(report.venue_position_id, None);
2158 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
2159 assert_eq!(report.commission.as_f64(), 0.0);
2160 }
2161 other => panic!("Expected FillReport, found {other:?}"),
2162 }
2163 }
2164
2165 #[rstest]
2166 fn test_dispatch_untracked_execution_emits_fill_report() {
2167 let instrument = linear_instrument();
2168 let instruments = build_instruments(std::slice::from_ref(&instrument));
2169 let (emitter, mut rx) = create_emitter();
2170 let clock = get_atomic_clock_realtime();
2171 let state = WsDispatchState::default();
2172
2173 let json = load_test_json("ws_account_execution.json");
2174 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
2175 serde_json::from_str(&json).unwrap();
2176
2177 let ws_msg = BybitWsMessage::AccountExecution(msg);
2179 dispatch_ws_message(
2180 &ws_msg,
2181 &emitter,
2182 &state,
2183 test_account_id(),
2184 &instruments,
2185 clock,
2186 );
2187
2188 let event = rx.try_recv().unwrap();
2189 assert!(matches!(
2190 event,
2191 ExecutionEvent::Report(ExecutionReport::Fill(_))
2192 ));
2193 }
2194
2195 #[rstest]
2196 #[case::untracked("")]
2197 #[case::tracked("test-order-link-001")]
2198 fn test_dispatch_funding_execution_emits_no_event(#[case] order_link_id: &str) {
2199 let instrument = linear_instrument();
2200 let instruments = build_instruments(std::slice::from_ref(&instrument));
2201 let (emitter, mut rx) = create_emitter();
2202 let clock = get_atomic_clock_realtime();
2203 let state = WsDispatchState::default();
2204
2205 let json = load_test_json("ws_account_execution_funding.json");
2206 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2207 value["data"][0]["orderLinkId"] = serde_json::Value::String(order_link_id.to_string());
2208 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
2209 serde_json::from_value(value).unwrap();
2210 let execution = &msg.data[0];
2211
2212 assert_eq!(execution.exec_type, BybitExecType::Funding);
2213
2214 if !execution.order_link_id.is_empty() {
2215 state.order_identities.insert(
2216 ClientOrderId::new(execution.order_link_id),
2217 default_identity(),
2218 );
2219 }
2220
2221 dispatch_ws_message(
2222 &BybitWsMessage::AccountExecution(msg),
2223 &emitter,
2224 &state,
2225 test_account_id(),
2226 &instruments,
2227 clock,
2228 );
2229
2230 assert!(matches!(
2231 rx.try_recv(),
2232 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2233 ));
2234 }
2235
2236 #[rstest]
2237 #[case::corporate_action("CorporateAction")]
2238 #[case::forward_split_settle("ForwardSplitSettle")]
2239 #[case::reverse_split_settle("ReverseSplitSettle")]
2240 #[case::dividend("Dividend")]
2241 #[case::unrecognized("StockMerger")]
2242 fn test_dispatch_venue_initiated_execution_emits_only_fill_report(#[case] exec_type: &str) {
2243 let instrument = linear_instrument();
2244 let instruments = build_instruments(std::slice::from_ref(&instrument));
2245 let (emitter, mut rx) = create_emitter();
2246 let clock = get_atomic_clock_realtime();
2247 let state = WsDispatchState::default();
2248
2249 let json = load_test_json("ws_account_execution_adl.json");
2250 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2251 value["data"][0]["execType"] = serde_json::Value::String(exec_type.to_string());
2252 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
2253 serde_json::from_value(value).unwrap();
2254 let execution = &msg.data[0];
2255
2256 assert!(execution.order_link_id.is_empty());
2257
2258 dispatch_ws_message(
2259 &BybitWsMessage::AccountExecution(msg),
2260 &emitter,
2261 &state,
2262 test_account_id(),
2263 &instruments,
2264 clock,
2265 );
2266
2267 let event = rx.try_recv().unwrap();
2268 match event {
2269 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
2270 assert_eq!(report.client_order_id, None);
2271 assert_eq!(
2272 report.venue_order_id,
2273 VenueOrderId::from("9aac161b-8ed6-450d-9cab-c5cc67c21785")
2274 );
2275 }
2276 other => panic!("Expected FillReport, found {other:?}"),
2277 }
2278 assert!(rx.try_recv().is_err());
2279 }
2280
2281 #[rstest]
2282 fn test_dispatch_wallet_emits_account_state() {
2283 let instruments = AHashMap::new();
2284 let (emitter, mut rx) = create_emitter();
2285 let clock = get_atomic_clock_realtime();
2286 let state = WsDispatchState::default();
2287
2288 let json = load_test_json("ws_account_wallet.json");
2289 let msg: crate::websocket::messages::BybitWsAccountWalletMsg =
2290 serde_json::from_str(&json).unwrap();
2291 let ws_msg = BybitWsMessage::AccountWallet(msg);
2292
2293 dispatch_ws_message(
2294 &ws_msg,
2295 &emitter,
2296 &state,
2297 test_account_id(),
2298 &instruments,
2299 clock,
2300 );
2301
2302 let event = rx.try_recv().unwrap();
2303 assert!(matches!(event, ExecutionEvent::Account(_)));
2304 }
2305
2306 #[rstest]
2307 fn test_dispatch_data_message_ignored() {
2308 let instruments = AHashMap::new();
2309 let (emitter, mut rx) = create_emitter();
2310 let clock = get_atomic_clock_realtime();
2311 let state = WsDispatchState::default();
2312
2313 let json = load_test_json("ws_public_trade.json");
2314 let msg: crate::websocket::messages::BybitWsTradeMsg = serde_json::from_str(&json).unwrap();
2315 let ws_msg = BybitWsMessage::Trade(msg);
2316
2317 dispatch_ws_message(
2318 &ws_msg,
2319 &emitter,
2320 &state,
2321 test_account_id(),
2322 &instruments,
2323 clock,
2324 );
2325
2326 rx.try_recv().unwrap_err();
2327 }
2328
2329 #[rstest]
2330 fn test_accepted_dedup_prevents_duplicate() {
2331 let instrument = linear_instrument();
2332 let instruments = build_instruments(std::slice::from_ref(&instrument));
2333 let (emitter, mut rx) = create_emitter();
2334 let clock = get_atomic_clock_realtime();
2335 let state = WsDispatchState::default();
2336
2337 let json = load_test_json("ws_account_order.json");
2339 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2340 value["data"][0]["orderStatus"] = serde_json::Value::String("New".to_string());
2341 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
2342 serde_json::from_value(value).unwrap();
2343
2344 if let Some(order) = msg.data.first()
2345 && !order.order_link_id.is_empty()
2346 {
2347 let cid = ClientOrderId::new(order.order_link_id);
2348 state.order_identities.insert(cid, default_identity());
2349 }
2350
2351 let ws_msg = BybitWsMessage::AccountOrder(msg.clone());
2352 dispatch_ws_message(
2353 &ws_msg,
2354 &emitter,
2355 &state,
2356 test_account_id(),
2357 &instruments,
2358 clock,
2359 );
2360
2361 let event = rx.try_recv().unwrap();
2362 assert!(matches!(
2363 event,
2364 ExecutionEvent::Order(OrderEventAny::Accepted(_))
2365 ));
2366
2367 let ws_msg2 = BybitWsMessage::AccountOrder(msg);
2369 dispatch_ws_message(
2370 &ws_msg2,
2371 &emitter,
2372 &state,
2373 test_account_id(),
2374 &instruments,
2375 clock,
2376 );
2377
2378 rx.try_recv().unwrap_err();
2379 }
2380
2381 fn new_order_value() -> serde_json::Value {
2382 let json = load_test_json("ws_account_order.json");
2383 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
2384 value["data"][0]["orderStatus"] = serde_json::Value::String("New".to_string());
2385 value
2386 }
2387
2388 struct DispatchTestContext {
2389 instruments: AHashMap<Ustr, InstrumentAny>,
2390 emitter: ExecutionEventEmitter,
2391 rx: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2392 clock: &'static AtomicTime,
2393 state: WsDispatchState,
2394 }
2395
2396 impl DispatchTestContext {
2397 fn new() -> Self {
2398 let instrument = linear_instrument();
2399 let instruments = build_instruments(std::slice::from_ref(&instrument));
2400 let (emitter, rx) = create_emitter();
2401 let clock = get_atomic_clock_realtime();
2402 let state = WsDispatchState::default();
2403 Self {
2404 instruments,
2405 emitter,
2406 rx,
2407 clock,
2408 state,
2409 }
2410 }
2411
2412 fn accept_order(&mut self, value: &serde_json::Value) {
2413 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
2414 serde_json::from_value(value.clone()).unwrap();
2415
2416 if let Some(order) = msg.data.first()
2417 && !order.order_link_id.is_empty()
2418 && !self
2419 .state
2420 .order_identities
2421 .contains_key(&ClientOrderId::new(order.order_link_id))
2422 {
2423 let cid = ClientOrderId::new(order.order_link_id);
2424 self.state.order_identities.insert(cid, default_identity());
2425 }
2426
2427 self.dispatch_value(value);
2428
2429 let event = self.rx.try_recv().unwrap();
2430 assert!(
2431 matches!(event, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
2432 "Expected Accepted, found {event:?}"
2433 );
2434 }
2435
2436 fn dispatch_value(&self, value: &serde_json::Value) {
2437 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
2438 serde_json::from_value(value.clone()).unwrap();
2439 let ws_msg = BybitWsMessage::AccountOrder(msg);
2440 dispatch_ws_message(
2441 &ws_msg,
2442 &self.emitter,
2443 &self.state,
2444 test_account_id(),
2445 &self.instruments,
2446 self.clock,
2447 );
2448 }
2449
2450 fn recv_updated(&mut self) -> OrderUpdated {
2451 let event = self.rx.try_recv().unwrap();
2452 match event {
2453 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => updated,
2454 other => panic!("Expected Updated event, found {other:?}"),
2455 }
2456 }
2457 }
2458
2459 #[rstest]
2460 fn test_dispatch_order_updated_on_price_change() {
2461 let mut ctx = DispatchTestContext::new();
2462 let value = new_order_value();
2463 ctx.accept_order(&value);
2464
2465 let mut amended = value;
2466 amended["data"][0]["price"] = serde_json::Value::String("31000".to_string());
2467 ctx.dispatch_value(&amended);
2468
2469 let updated = ctx.recv_updated();
2470 assert_eq!(updated.client_order_id, ClientOrderId::from("client-1"));
2471 assert_eq!(updated.price, Some(Price::from("31000.00")));
2472 assert_eq!(updated.quantity, Quantity::from("0.010"));
2473 assert_eq!(updated.trigger_price, None);
2474 assert!(updated.venue_order_id.is_some());
2475 }
2476
2477 #[rstest]
2478 fn test_dispatch_order_updated_on_quantity_change() {
2479 let mut ctx = DispatchTestContext::new();
2480 let value = new_order_value();
2481 ctx.accept_order(&value);
2482
2483 let mut amended = value;
2484 amended["data"][0]["qty"] = serde_json::Value::String("0.020".to_string());
2485 ctx.dispatch_value(&amended);
2486
2487 let updated = ctx.recv_updated();
2488 assert_eq!(updated.quantity, Quantity::from("0.020"));
2489 assert_eq!(updated.price, Some(Price::from("30000.00")));
2490 }
2491
2492 #[rstest]
2493 fn test_dispatch_order_updated_on_trigger_price_change() {
2494 let mut ctx = DispatchTestContext::new();
2495 let mut value = new_order_value();
2496 value["data"][0]["triggerPrice"] = serde_json::Value::String("29000".to_string());
2497 ctx.accept_order(&value);
2498
2499 let mut amended = value;
2500 amended["data"][0]["triggerPrice"] = serde_json::Value::String("28000".to_string());
2501 ctx.dispatch_value(&amended);
2502
2503 let updated = ctx.recv_updated();
2504 assert_eq!(updated.trigger_price, Some(Price::from("28000.00")));
2505 assert_eq!(updated.price, Some(Price::from("30000.00")));
2506 }
2507
2508 #[rstest]
2509 fn test_dispatch_dedup_suppresses_identical_after_snapshot() {
2510 let mut ctx = DispatchTestContext::new();
2511 let value = new_order_value();
2512 ctx.accept_order(&value);
2513
2514 ctx.dispatch_value(&value);
2515
2516 assert!(
2517 ctx.rx.try_recv().is_err(),
2518 "Expected no event for identical redelivery"
2519 );
2520 }
2521
2522 #[rstest]
2523 fn test_dispatch_order_updated_stores_snapshot_for_subsequent_change() {
2524 let mut ctx = DispatchTestContext::new();
2525 let value = new_order_value();
2526 ctx.accept_order(&value);
2527
2528 let mut amended1 = value.clone();
2529 amended1["data"][0]["price"] = serde_json::Value::String("31000".to_string());
2530 ctx.dispatch_value(&amended1);
2531 let _result = ctx.recv_updated();
2532
2533 let mut amended2 = value;
2534 amended2["data"][0]["price"] = serde_json::Value::String("32000".to_string());
2535 ctx.dispatch_value(&amended2);
2536
2537 let updated = ctx.recv_updated();
2538 assert_eq!(updated.price, Some(Price::from("32000.00")));
2539 }
2540
2541 #[rstest]
2542 fn test_dispatch_accepted_with_seeded_snapshot_emits_updated_for_bbo() {
2543 let mut ctx = DispatchTestContext::new();
2544 let value = new_order_value();
2545 let cid = ClientOrderId::from("client-1");
2546 ctx.state.order_identities.insert(cid, default_identity());
2547 ctx.state.order_snapshots.insert(
2548 cid,
2549 OrderStateSnapshot {
2550 quantity: Quantity::from("0.010"),
2551 price: Some(Price::from("29000.00")),
2552 trigger_price: None,
2553 },
2554 );
2555
2556 ctx.dispatch_value(&value);
2557
2558 let accepted = ctx.rx.try_recv().unwrap();
2559 assert!(
2560 matches!(accepted, ExecutionEvent::Order(OrderEventAny::Accepted(_))),
2561 "Expected Accepted first, found {accepted:?}"
2562 );
2563
2564 let updated = ctx.recv_updated();
2565 assert_eq!(updated.client_order_id, cid);
2566 assert_eq!(updated.price, Some(Price::from("30000.00")));
2567 assert_eq!(updated.quantity, Quantity::from("0.010"));
2568 assert!(updated.venue_order_id.is_some());
2569 }
2570
2571 #[rstest]
2572 fn test_emit_rejection_for_place_clears_snapshot() {
2573 let ctx = DispatchTestContext::new();
2574 let cid = ClientOrderId::from("client-1");
2575 let identity = default_identity();
2576 ctx.state.order_identities.insert(cid, identity.clone());
2577 ctx.state.order_snapshots.insert(
2578 cid,
2579 OrderStateSnapshot {
2580 quantity: Quantity::from("0.010"),
2581 price: Some(Price::from("30000.00")),
2582 trigger_price: None,
2583 },
2584 );
2585
2586 emit_rejection_for_op(
2587 &PendingOperation::Place,
2588 cid,
2589 &identity,
2590 None,
2591 "rejected",
2592 &ctx.emitter,
2593 &ctx.state,
2594 UnixNanos::from(1u64),
2595 );
2596
2597 assert!(!ctx.state.order_identities.contains_key(&cid));
2598 assert!(!ctx.state.order_snapshots.contains_key(&cid));
2599 }
2600
2601 fn order_response(
2602 op: &str,
2603 ret_code: i64,
2604 ret_msg: &str,
2605 req_id: &str,
2606 data: serde_json::Value,
2607 ret_ext_info: Option<serde_json::Value>,
2608 ) -> BybitWsOrderResponse {
2609 BybitWsOrderResponse {
2610 op: Ustr::from(op),
2611 conn_id: Some("test-conn-id".to_string()),
2612 ret_code,
2613 ret_msg: ret_msg.to_string(),
2614 data,
2615 req_id: Some(req_id.to_string()),
2616 header: None,
2617 ret_ext_info,
2618 }
2619 }
2620
2621 #[rstest]
2622 fn test_dispatch_local_not_sent_emits_order_rejected() {
2623 let mut ctx = DispatchTestContext::new();
2624 let cid = ClientOrderId::from("not-sent-1");
2625 ctx.state.order_identities.insert(cid, default_identity());
2626 ctx.state.pending_requests.insert(
2627 "req-not-sent".to_string(),
2628 (vec![cid], vec![None], PendingOperation::Place),
2629 );
2630 let response = order_response(
2631 BYBIT_OP_ORDER_CREATE,
2632 -1,
2633 "Order command was not written",
2634 "req-not-sent",
2635 serde_json::json!({}),
2636 None,
2637 );
2638
2639 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2640
2641 let event = ctx.rx.try_recv().expect("expected OrderRejected event");
2642 assert!(
2643 matches!(event, ExecutionEvent::Order(OrderEventAny::Rejected(ref rejected))
2644 if rejected.client_order_id == cid
2645 && rejected.reason == "Order command was not written"),
2646 "Expected OrderRejected for {cid}, found {event:?}"
2647 );
2648 assert!(!ctx.state.order_identities.contains_key(&cid));
2649 assert!(!ctx.state.pending_requests.contains_key("req-not-sent"));
2650 }
2651
2652 #[rstest]
2653 fn test_dispatch_single_cancel_rejection_emits_cancel_rejected() {
2654 let mut ctx = DispatchTestContext::new();
2655 let cid = ClientOrderId::from("cancel-reject-1");
2656 let venue_order_id = VenueOrderId::from("venue-cancel-1");
2657 ctx.state.order_identities.insert(cid, default_identity());
2658 ctx.state.pending_requests.insert(
2659 "req-cancel".to_string(),
2660 (
2661 vec![cid],
2662 vec![Some(venue_order_id)],
2663 PendingOperation::Cancel,
2664 ),
2665 );
2666
2667 let response = order_response(
2668 BYBIT_OP_ORDER_CANCEL,
2669 110001,
2670 "Order does not exist.",
2671 "req-cancel",
2672 serde_json::json!({
2673 "orderId": venue_order_id.to_string(),
2674 "orderLinkId": cid.to_string(),
2675 }),
2676 None,
2677 );
2678
2679 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2680
2681 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
2682 assert!(
2683 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected)) if rejected.client_order_id == cid),
2684 "Expected CancelRejected for {cid}, found {event:?}"
2685 );
2686 assert!(
2687 ctx.rx.try_recv().is_err(),
2688 "Expected no extra events for single cancel rejection"
2689 );
2690 }
2691
2692 #[rstest]
2693 fn test_dispatch_single_modify_rejection_emits_modify_rejected() {
2694 let mut ctx = DispatchTestContext::new();
2695 let cid = ClientOrderId::from("modify-reject-1");
2696 let venue_order_id = VenueOrderId::from("venue-modify-1");
2697 ctx.state.order_identities.insert(cid, default_identity());
2698 ctx.state.pending_requests.insert(
2699 "req-modify".to_string(),
2700 (
2701 vec![cid],
2702 vec![Some(venue_order_id)],
2703 PendingOperation::Amend,
2704 ),
2705 );
2706
2707 let response = order_response(
2708 BYBIT_OP_ORDER_AMEND,
2709 110003,
2710 "Order price exceeds allowable range.",
2711 "req-modify",
2712 serde_json::json!({
2713 "orderId": venue_order_id.to_string(),
2714 "orderLinkId": cid.to_string(),
2715 }),
2716 None,
2717 );
2718
2719 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2720
2721 let event = ctx.rx.try_recv().expect("expected ModifyRejected event");
2722 assert!(
2723 matches!(event, ExecutionEvent::Order(OrderEventAny::ModifyRejected(ref rejected)) if rejected.client_order_id == cid),
2724 "Expected ModifyRejected for {cid}, found {event:?}"
2725 );
2726 assert!(
2727 ctx.rx.try_recv().is_err(),
2728 "Expected no extra events for single modify rejection"
2729 );
2730 }
2731
2732 #[rstest]
2733 fn test_dispatch_single_cancel_rate_limit_emits_cancel_rejected() {
2734 let mut ctx = DispatchTestContext::new();
2735 let cid = ClientOrderId::from("cancel-rate-limit-1");
2736 let venue_order_id = VenueOrderId::from("venue-cancel-rate-limit-1");
2737 ctx.state.order_identities.insert(cid, default_identity());
2738 ctx.state.pending_requests.insert(
2739 "req-cancel-rate-limit".to_string(),
2740 (
2741 vec![cid],
2742 vec![Some(venue_order_id)],
2743 PendingOperation::Cancel,
2744 ),
2745 );
2746
2747 let response = order_response(
2748 BYBIT_OP_ORDER_CANCEL,
2749 10429,
2750 "System level frequency protection.",
2751 "req-cancel-rate-limit",
2752 serde_json::json!({
2753 "orderId": venue_order_id.to_string(),
2754 "orderLinkId": cid.to_string(),
2755 }),
2756 None,
2757 );
2758
2759 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2760
2761 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
2762 assert!(
2763 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected))
2764 if rejected.client_order_id == cid && rejected.venue_order_id == Some(venue_order_id)),
2765 "Expected CancelRejected for {cid}, found {event:?}"
2766 );
2767 assert!(ctx.state.order_identities.contains_key(&cid));
2768 }
2769
2770 #[rstest]
2771 fn test_dispatch_single_modify_server_error_keeps_outcome_unresolved() {
2772 let mut ctx = DispatchTestContext::new();
2773 let cid = ClientOrderId::from("modify-server-error-1");
2774 let venue_order_id = VenueOrderId::from("venue-modify-server-error-1");
2775 ctx.state.order_identities.insert(cid, default_identity());
2776 ctx.state.pending_requests.insert(
2777 "req-modify-server-error".to_string(),
2778 (
2779 vec![cid],
2780 vec![Some(venue_order_id)],
2781 PendingOperation::Amend,
2782 ),
2783 );
2784
2785 let response = order_response(
2786 BYBIT_OP_ORDER_AMEND,
2787 10016,
2788 "Internal server error.",
2789 "req-modify-server-error",
2790 serde_json::json!({
2791 "orderId": venue_order_id.to_string(),
2792 "orderLinkId": cid.to_string(),
2793 }),
2794 None,
2795 );
2796
2797 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2798
2799 assert!(
2800 ctx.rx.try_recv().is_err(),
2801 "Expected no ModifyRejected event for server error response"
2802 );
2803 assert!(ctx.state.order_identities.contains_key(&cid));
2804 }
2805
2806 #[rstest]
2807 fn test_dispatch_batch_cancel_top_level_failure_does_not_emit_cancel_rejected() {
2808 let mut ctx = DispatchTestContext::new();
2809 let cid_1 = ClientOrderId::from("batch-cancel-1");
2810 let cid_2 = ClientOrderId::from("batch-cancel-2");
2811 ctx.state.order_identities.insert(cid_1, default_identity());
2812 ctx.state.order_identities.insert(cid_2, default_identity());
2813 ctx.state.pending_requests.insert(
2814 "req-batch-cancel".to_string(),
2815 (
2816 vec![cid_1, cid_2],
2817 vec![
2818 Some(VenueOrderId::from("venue-batch-1")),
2819 Some(VenueOrderId::from("venue-batch-2")),
2820 ],
2821 PendingOperation::Cancel,
2822 ),
2823 );
2824
2825 let response = order_response(
2826 BYBIT_OP_ORDER_CANCEL_BATCH,
2827 10016,
2828 "Internal server error.",
2829 "req-batch-cancel",
2830 serde_json::json!({}),
2831 None,
2832 );
2833
2834 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2835
2836 assert!(
2837 ctx.rx.try_recv().is_err(),
2838 "Expected no CancelRejected events for ambiguous batch failure"
2839 );
2840 assert!(ctx.state.order_identities.contains_key(&cid_1));
2841 assert!(ctx.state.order_identities.contains_key(&cid_2));
2842 }
2843
2844 #[rstest]
2845 fn test_dispatch_single_item_batch_cancel_top_level_failure_does_not_emit_cancel_rejected() {
2846 let mut ctx = DispatchTestContext::new();
2847 let cid = ClientOrderId::from("batch-cancel-single");
2848 ctx.state.order_identities.insert(cid, default_identity());
2849 ctx.state.pending_requests.insert(
2850 "req-batch-cancel-single".to_string(),
2851 (
2852 vec![cid],
2853 vec![Some(VenueOrderId::from("venue-batch-single"))],
2854 PendingOperation::Cancel,
2855 ),
2856 );
2857
2858 let response = order_response(
2859 BYBIT_OP_ORDER_CANCEL_BATCH,
2860 10016,
2861 "Internal server error.",
2862 "req-batch-cancel-single",
2863 serde_json::json!({}),
2864 None,
2865 );
2866
2867 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2868
2869 assert!(
2870 ctx.rx.try_recv().is_err(),
2871 "Expected no CancelRejected event for ambiguous one-item batch failure"
2872 );
2873 assert!(ctx.state.order_identities.contains_key(&cid));
2874 }
2875
2876 #[rstest]
2877 fn test_dispatch_batch_cancel_top_level_rate_limit_emits_cancel_rejected() {
2878 let mut ctx = DispatchTestContext::new();
2879 let cid_1 = ClientOrderId::from("batch-rate-1");
2880 let cid_2 = ClientOrderId::from("batch-rate-2");
2881 let venue_1 = VenueOrderId::from("venue-rate-1");
2882 let venue_2 = VenueOrderId::from("venue-rate-2");
2883 ctx.state.order_identities.insert(cid_1, default_identity());
2884 ctx.state.order_identities.insert(cid_2, default_identity());
2885 ctx.state.pending_requests.insert(
2886 "req-batch-rate".to_string(),
2887 (
2888 vec![cid_1, cid_2],
2889 vec![Some(venue_1), Some(venue_2)],
2890 PendingOperation::Cancel,
2891 ),
2892 );
2893
2894 let response = order_response(
2895 BYBIT_OP_ORDER_CANCEL_BATCH,
2896 10006,
2897 "Too many visits.",
2898 "req-batch-rate",
2899 serde_json::json!({}),
2900 None,
2901 );
2902
2903 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2904
2905 let first = ctx.rx.try_recv().expect("expected first CancelRejected");
2906 let second = ctx.rx.try_recv().expect("expected second CancelRejected");
2907 assert!(
2908 matches!(first, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected))
2909 if rejected.client_order_id == cid_1 && rejected.venue_order_id == Some(venue_1))
2910 );
2911 assert!(
2912 matches!(second, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected))
2913 if rejected.client_order_id == cid_2 && rejected.venue_order_id == Some(venue_2))
2914 );
2915 }
2916
2917 #[rstest]
2918 fn test_dispatch_batch_cancel_per_item_error_emits_only_failed_item() {
2919 let mut ctx = DispatchTestContext::new();
2920 let cid_1 = ClientOrderId::from("batch-cancel-ok");
2921 let cid_2 = ClientOrderId::from("batch-cancel-reject");
2922 ctx.state.order_identities.insert(cid_1, default_identity());
2923 ctx.state.order_identities.insert(cid_2, default_identity());
2924 ctx.state.pending_requests.insert(
2925 "req-batch-mixed".to_string(),
2926 (
2927 vec![cid_1, cid_2],
2928 vec![
2929 Some(VenueOrderId::from("venue-batch-ok")),
2930 Some(VenueOrderId::from("venue-batch-reject")),
2931 ],
2932 PendingOperation::Cancel,
2933 ),
2934 );
2935
2936 let response = order_response(
2937 BYBIT_OP_ORDER_CANCEL_BATCH,
2938 0,
2939 "OK",
2940 "req-batch-mixed",
2941 serde_json::json!({
2942 "list": [
2943 {"orderId": "venue-batch-ok", "orderLinkId": cid_1.to_string()},
2944 {"orderId": "venue-batch-reject", "orderLinkId": cid_2.to_string()}
2945 ]
2946 }),
2947 Some(serde_json::json!({
2948 "list": [
2949 {"code": 0, "msg": "OK"},
2950 {"code": 170213, "msg": "Order does not exist."}
2951 ]
2952 })),
2953 );
2954
2955 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
2956
2957 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
2958 assert!(
2959 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected)) if rejected.client_order_id == cid_2),
2960 "Expected CancelRejected for failed batch item {cid_2}, found {event:?}"
2961 );
2962 assert!(
2963 ctx.rx.try_recv().is_err(),
2964 "Expected no CancelRejected event for successful batch item"
2965 );
2966 }
2967
2968 #[rstest]
2969 fn test_dispatch_batch_cancel_per_item_rate_limit_emits_cancel_rejected() {
2970 let mut ctx = DispatchTestContext::new();
2971 let cid = ClientOrderId::from("batch-cancel-rate-limit");
2972 let venue_order_id = VenueOrderId::from("venue-batch-rate-limit");
2973 ctx.state.order_identities.insert(cid, default_identity());
2974 ctx.state.pending_requests.insert(
2975 "req-batch-rate-limit".to_string(),
2976 (
2977 vec![cid],
2978 vec![Some(venue_order_id)],
2979 PendingOperation::Cancel,
2980 ),
2981 );
2982
2983 let response = order_response(
2984 BYBIT_OP_ORDER_CANCEL_BATCH,
2985 0,
2986 "OK",
2987 "req-batch-rate-limit",
2988 serde_json::json!({
2989 "list": [
2990 {"orderId": venue_order_id.to_string(), "orderLinkId": cid.to_string()}
2991 ]
2992 }),
2993 Some(serde_json::json!({
2994 "list": [
2995 {"code": 10006, "msg": "Too many visits."}
2996 ]
2997 })),
2998 );
2999
3000 dispatch_order_response(&response, &ctx.emitter, &ctx.state, UnixNanos::from(1u64));
3001
3002 let event = ctx.rx.try_recv().expect("expected CancelRejected event");
3003 assert!(
3004 matches!(event, ExecutionEvent::Order(OrderEventAny::CancelRejected(ref rejected))
3005 if rejected.client_order_id == cid && rejected.venue_order_id == Some(venue_order_id)),
3006 "Expected CancelRejected for {cid}, found {event:?}"
3007 );
3008 assert!(ctx.state.order_identities.contains_key(&cid));
3009 }
3010
3011 #[rstest]
3012 fn test_dispatch_filled_with_seeded_snapshot_emits_updated_for_bbo() {
3013 let mut ctx = DispatchTestContext::new();
3014 let mut value = new_order_value();
3015 value["data"][0]["orderStatus"] = serde_json::Value::String("Filled".to_string());
3016 value["data"][0]["cumExecQty"] = serde_json::Value::String("0.010".to_string());
3017 let cid = ClientOrderId::from("client-1");
3018 ctx.state.order_identities.insert(cid, default_identity());
3019 ctx.state.order_snapshots.insert(
3020 cid,
3021 OrderStateSnapshot {
3022 quantity: Quantity::from("0.010"),
3023 price: Some(Price::from("29000.00")),
3024 trigger_price: None,
3025 },
3026 );
3027
3028 ctx.dispatch_value(&value);
3029
3030 let accepted_event = ctx.rx.try_recv().unwrap();
3031 let accepted = match accepted_event {
3032 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => accepted,
3033 other => panic!("Expected Accepted first, found {other:?}"),
3034 };
3035 assert_eq!(accepted.client_order_id, cid);
3036 assert!(!accepted.venue_order_id.to_string().is_empty());
3037
3038 let updated = ctx.recv_updated();
3039 assert_eq!(updated.client_order_id, cid);
3040 assert_eq!(updated.price, Some(Price::from("30000.00")));
3041 assert_eq!(updated.quantity, Quantity::from("0.010"));
3042 assert_eq!(updated.trigger_price, None);
3043 assert_eq!(updated.venue_order_id, Some(accepted.venue_order_id));
3044
3045 let stored = ctx.state.order_snapshots.get(&cid).unwrap();
3046 assert_eq!(stored.price, Some(Price::from("30000.00")));
3047 }
3048
3049 #[rstest]
3050 fn test_dispatch_accepted_with_matching_seeded_snapshot_no_updated() {
3051 let mut ctx = DispatchTestContext::new();
3052 let value = new_order_value();
3053 let cid = ClientOrderId::from("client-1");
3054 ctx.state.order_identities.insert(cid, default_identity());
3055 ctx.state.order_snapshots.insert(
3056 cid,
3057 OrderStateSnapshot {
3058 quantity: Quantity::from("0.010"),
3059 price: Some(Price::from("30000.00")),
3060 trigger_price: None,
3061 },
3062 );
3063
3064 ctx.dispatch_value(&value);
3065
3066 let accepted = ctx.rx.try_recv().unwrap();
3067 assert!(matches!(
3068 accepted,
3069 ExecutionEvent::Order(OrderEventAny::Accepted(_))
3070 ));
3071
3072 assert!(
3073 ctx.rx.try_recv().is_err(),
3074 "Expected no OrderUpdated when seed matches venue snapshot"
3075 );
3076 }
3077
3078 #[rstest]
3079 #[case::price_changed(
3080 Some(Price::from("100.00")),
3081 None,
3082 Quantity::from("1.000"),
3083 Some(Price::from("200.00")),
3084 None,
3085 Quantity::from("1.000"),
3086 true
3087 )]
3088 #[case::trigger_changed(
3089 None,
3090 Some(Price::from("100.00")),
3091 Quantity::from("1.000"),
3092 None,
3093 Some(Price::from("90.00")),
3094 Quantity::from("1.000"),
3095 true
3096 )]
3097 #[case::qty_changed(
3098 Some(Price::from("100.00")),
3099 None,
3100 Quantity::from("1.000"),
3101 Some(Price::from("100.00")),
3102 None,
3103 Quantity::from("2.000"),
3104 true
3105 )]
3106 #[case::no_change(
3107 Some(Price::from("100.00")),
3108 None,
3109 Quantity::from("1.000"),
3110 Some(Price::from("100.00")),
3111 None,
3112 Quantity::from("1.000"),
3113 false
3114 )]
3115 fn test_is_snapshot_updated(
3116 #[case] prev_price: Option<Price>,
3117 #[case] prev_trigger: Option<Price>,
3118 #[case] prev_qty: Quantity,
3119 #[case] new_price: Option<Price>,
3120 #[case] new_trigger: Option<Price>,
3121 #[case] new_qty: Quantity,
3122 #[case] expected: bool,
3123 ) {
3124 let state = WsDispatchState::default();
3125 let cid = ClientOrderId::from("test-1");
3126 state.order_snapshots.insert(
3127 cid,
3128 OrderStateSnapshot {
3129 quantity: prev_qty,
3130 price: prev_price,
3131 trigger_price: prev_trigger,
3132 },
3133 );
3134
3135 let new_snapshot = OrderStateSnapshot {
3136 quantity: new_qty,
3137 price: new_price,
3138 trigger_price: new_trigger,
3139 };
3140 assert_eq!(is_snapshot_updated(&new_snapshot, &cid, &state), expected);
3141 }
3142
3143 #[rstest]
3144 fn test_is_snapshot_updated_no_previous() {
3145 let state = WsDispatchState::default();
3146 let cid = ClientOrderId::from("test-1");
3147
3148 let new_snapshot = OrderStateSnapshot {
3149 quantity: Quantity::from("1.000"),
3150 price: Some(Price::from("100.00")),
3151 trigger_price: None,
3152 };
3153 assert!(!is_snapshot_updated(&new_snapshot, &cid, &state));
3154 }
3155
3156 #[rstest]
3157 #[case::limit_order("30000", "0", Some(Price::from("30000.00")), None)]
3158 #[case::conditional("0", "29000", None, Some(Price::from("29000.00")))]
3159 #[case::both(
3160 "30000",
3161 "29000",
3162 Some(Price::from("30000.00")),
3163 Some(Price::from("29000.00"))
3164 )]
3165 fn test_parse_order_snapshot(
3166 #[case] price: &str,
3167 #[case] trigger: &str,
3168 #[case] expected_price: Option<Price>,
3169 #[case] expected_trigger: Option<Price>,
3170 ) {
3171 let instrument = linear_instrument();
3172 let json = load_test_json("ws_account_order.json");
3173 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
3174 value["data"][0]["price"] = serde_json::Value::String(price.to_string());
3175 value["data"][0]["triggerPrice"] = serde_json::Value::String(trigger.to_string());
3176 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
3177 serde_json::from_value(value).unwrap();
3178
3179 let snapshot = parse_order_snapshot(&msg.data[0], &instrument).unwrap();
3180 assert_eq!(snapshot.price, expected_price);
3181 assert_eq!(snapshot.trigger_price, expected_trigger);
3182 assert_eq!(snapshot.quantity, Quantity::from("0.010"));
3183 }
3184
3185 #[rstest]
3186 fn test_parse_order_snapshot_invalid_qty_returns_none() {
3187 let instrument = linear_instrument();
3188 let json = load_test_json("ws_account_order.json");
3189 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
3190 value["data"][0]["qty"] = serde_json::Value::String(String::new());
3191 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
3192 serde_json::from_value(value).unwrap();
3193
3194 assert!(parse_order_snapshot(&msg.data[0], &instrument).is_none());
3195 }
3196
3197 #[rstest]
3198 fn test_dispatch_order_updated_on_partially_filled_price_change() {
3199 let mut ctx = DispatchTestContext::new();
3200 let value = new_order_value();
3201 ctx.accept_order(&value);
3202
3203 let mut amended = value;
3204 amended["data"][0]["orderStatus"] =
3205 serde_json::Value::String("PartiallyFilled".to_string());
3206 amended["data"][0]["cumExecQty"] = serde_json::Value::String("0.005".to_string());
3207 amended["data"][0]["price"] = serde_json::Value::String("31000".to_string());
3208 ctx.dispatch_value(&amended);
3209
3210 let updated = ctx.recv_updated();
3211 assert_eq!(updated.client_order_id, ClientOrderId::from("client-1"));
3212 assert_eq!(updated.price, Some(Price::from("31000.00")));
3213 }
3214}