1use std::{
19 future::Future,
20 sync::{
21 Arc, Mutex,
22 atomic::{AtomicBool, Ordering},
23 },
24 time::{Duration, Instant},
25};
26
27use ahash::AHashMap;
28use anyhow::Context;
29use async_trait::async_trait;
30use futures_util::{StreamExt, pin_mut};
31use nautilus_common::{
32 clients::ExecutionClient,
33 enums::LogLevel,
34 live::{get_runtime, runner::get_exec_event_sender},
35 messages::execution::{
36 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
37 GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
38 GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
39 GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
40 SubmitOrderList,
41 },
42};
43use nautilus_core::{
44 UnixNanos,
45 time::{AtomicTime, get_atomic_clock_realtime},
46};
47use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter};
48use nautilus_model::{
49 accounts::AccountAny,
50 enums::{AccountType, OmsType, OrderSide, OrderType, TrailingOffsetType},
51 identifiers::{
52 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
53 },
54 instruments::{Instrument, InstrumentAny},
55 orders::{Order, OrderAny},
56 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
57 types::{AccountBalance, MarginBalance},
58};
59use rust_decimal::prelude::ToPrimitive;
60use tokio::task::JoinHandle;
61use ustr::Ustr;
62
63use crate::{
64 broadcast::{
65 canceller::{CancelBroadcaster, CancelBroadcasterConfig},
66 submitter::{DEFINITIVE_SUBMIT_REJECTION, SubmitBroadcaster, SubmitBroadcasterConfig},
67 },
68 common::{
69 enums::{BitmexContingencyType, BitmexOrderType, BitmexPegPriceType, BitmexTimeInForce},
70 parse::{parse_peg_offset_value, parse_peg_price_type},
71 },
72 config::BitmexExecClientConfig,
73 http::{client::BitmexHttpClient, error::BitmexHttpError},
74 websocket::{
75 client::BitmexWebSocketClient,
76 dispatch::{self, OrderIdentity, WsDispatchState},
77 },
78};
79
80#[derive(Debug)]
81pub struct BitmexExecutionClient {
82 core: ExecutionClientCore,
83 clock: &'static AtomicTime,
84 config: BitmexExecClientConfig,
85 emitter: ExecutionEventEmitter,
86 http_client: BitmexHttpClient,
87 ws_client: BitmexWebSocketClient,
88 ws_dispatch_state: Arc<WsDispatchState>,
89 _submitter: SubmitBroadcaster,
90 _canceller: CancelBroadcaster,
91 ws_stream_handle: Option<JoinHandle<()>>,
92 pending_tasks: Mutex<Vec<JoinHandle<()>>>,
93 dms_task_handle: Option<JoinHandle<()>>,
94 dms_running: Arc<AtomicBool>,
95}
96
97impl BitmexExecutionClient {
98 fn log_report_receipt(count: usize, report_type: &str, log_level: LogLevel) {
99 let plural = if count == 1 { "" } else { "s" };
100 let message = format!("Received {count} {report_type}{plural}");
101
102 match log_level {
103 LogLevel::Off => {}
104 LogLevel::Trace => log::trace!("{message}"),
105 LogLevel::Debug => log::debug!("{message}"),
106 LogLevel::Info => log::info!("{message}"),
107 LogLevel::Warning => log::warn!("{message}"),
108 LogLevel::Error => log::error!("{message}"),
109 }
110 }
111
112 pub fn new(
118 mut core: ExecutionClientCore,
119 config: BitmexExecClientConfig,
120 ) -> anyhow::Result<Self> {
121 if !config.has_api_credentials() {
122 anyhow::bail!("BitMEX execution client requires API key and secret");
123 }
124
125 if let Some(account_id) = config.account_id {
126 core.set_account_id(account_id);
127 }
128
129 let trader_id = core.trader_id;
130 let account_id = core.account_id;
131 let clock = get_atomic_clock_realtime();
132 let emitter =
133 ExecutionEventEmitter::new(clock, trader_id, account_id, AccountType::Margin, None);
134 let http_client = BitmexHttpClient::new(
135 Some(config.http_base_url()),
136 config.api_key.clone(),
137 config.api_secret.clone(),
138 config.environment,
139 config.http_timeout_secs,
140 config.max_retries,
141 config.retry_delay_initial_ms,
142 config.retry_delay_max_ms,
143 config.recv_window_ms,
144 config.max_requests_per_second,
145 config.max_requests_per_minute,
146 config.proxy_url.clone(),
147 )
148 .context("failed to construct BitMEX HTTP client")?;
149 let ws_client = BitmexWebSocketClient::new_with_env(
150 Some(config.ws_url()),
151 config.api_key.clone(),
152 config.api_secret.clone(),
153 Some(account_id),
154 config.heartbeat_interval_secs,
155 config.environment,
156 config.transport_backend,
157 config.proxy_url.clone(),
158 )
159 .context("failed to construct BitMEX execution websocket client")?;
160
161 let pool_size = config.submitter_pool_size.unwrap_or(1);
162 let submitter_proxy_urls = match &config.submitter_proxy_urls {
163 Some(urls) => urls.iter().map(|url| Some(url.clone())).collect(),
164 None => vec![config.proxy_url.clone(); pool_size],
165 };
166
167 let submitter_config = SubmitBroadcasterConfig {
168 pool_size,
169 api_key: config.api_key.clone(),
170 api_secret: config.api_secret.clone(),
171 base_url: config.base_url_http.clone(),
172 environment: config.environment,
173 timeout_secs: config.http_timeout_secs,
174 max_retries: config.max_retries,
175 retry_delay_ms: config.retry_delay_initial_ms,
176 retry_delay_max_ms: config.retry_delay_max_ms,
177 recv_window_ms: config.recv_window_ms,
178 max_requests_per_second: config.max_requests_per_second,
179 max_requests_per_minute: config.max_requests_per_minute,
180 proxy_urls: submitter_proxy_urls,
181 ..Default::default()
182 };
183
184 let _submitter = SubmitBroadcaster::new(submitter_config)
185 .context("failed to create SubmitBroadcaster")?;
186
187 let canceller_pool_size = config.canceller_pool_size.unwrap_or(1);
188 let canceller_proxy_urls = match &config.canceller_proxy_urls {
189 Some(urls) => urls.iter().map(|url| Some(url.clone())).collect(),
190 None => vec![config.proxy_url.clone(); canceller_pool_size],
191 };
192
193 let canceller_config = CancelBroadcasterConfig {
194 pool_size: canceller_pool_size,
195 api_key: config.api_key.clone(),
196 api_secret: config.api_secret.clone(),
197 base_url: config.base_url_http.clone(),
198 environment: config.environment,
199 timeout_secs: config.http_timeout_secs,
200 max_retries: config.max_retries,
201 retry_delay_ms: config.retry_delay_initial_ms,
202 retry_delay_max_ms: config.retry_delay_max_ms,
203 recv_window_ms: config.recv_window_ms,
204 max_requests_per_second: config.max_requests_per_second,
205 max_requests_per_minute: config.max_requests_per_minute,
206 proxy_urls: canceller_proxy_urls,
207 ..Default::default()
208 };
209
210 let _canceller = CancelBroadcaster::new(canceller_config)
211 .context("failed to create CancelBroadcaster")?;
212
213 Ok(Self {
214 core,
215 clock,
216 config,
217 emitter,
218 http_client,
219 ws_client,
220 ws_dispatch_state: Arc::new(WsDispatchState::default()),
221 _submitter,
222 _canceller,
223 ws_stream_handle: None,
224 pending_tasks: Mutex::new(Vec::new()),
225 dms_task_handle: None,
226 dms_running: Arc::new(AtomicBool::new(false)),
227 })
228 }
229
230 fn spawn_task<F>(&self, label: &'static str, fut: F)
231 where
232 F: Future<Output = anyhow::Result<()>> + Send + 'static,
233 {
234 let handle = get_runtime().spawn(async move {
235 if let Err(e) = fut.await {
236 log::error!("{label}: {e:?}");
237 }
238 });
239
240 let mut guard = self
241 .pending_tasks
242 .lock()
243 .expect("pending task lock poisoned");
244
245 guard.retain(|h| !h.is_finished());
247 guard.push(handle);
248 }
249
250 fn abort_pending_tasks(&self) {
251 let mut guard = self
252 .pending_tasks
253 .lock()
254 .expect("pending task lock poisoned");
255
256 for handle in guard.drain(..) {
257 handle.abort();
258 }
259 }
260
261 fn ensure_order_identity(
266 &self,
267 client_order_id: ClientOrderId,
268 strategy_id: StrategyId,
269 instrument_id: InstrumentId,
270 ) {
271 if self
272 .ws_dispatch_state
273 .order_identities
274 .contains_key(&client_order_id)
275 {
276 return;
277 }
278
279 let cache = self.core.cache();
280 let (order_side, order_type) = cache
281 .order(&client_order_id)
282 .map_or((OrderSide::NoOrderSide, OrderType::Market), |o| {
283 (o.order_side(), o.order_type())
284 });
285 drop(cache);
286
287 self.ws_dispatch_state.order_identities.insert(
288 client_order_id,
289 OrderIdentity {
290 instrument_id,
291 strategy_id,
292 order_side,
293 order_type,
294 },
295 );
296 self.ws_dispatch_state.insert_accepted(client_order_id);
297 }
298
299 fn start_deadmans_switch(&mut self) {
300 let Some(timeout_secs) = self.config.deadmans_switch_timeout_secs else {
301 return;
302 };
303
304 let timeout_ms = timeout_secs * 1000;
305 let interval_secs = (timeout_secs / 4).max(1);
306
307 log::info!(
308 "Starting dead man's switch: timeout={timeout_secs}s, refresh_interval={interval_secs}s",
309 );
310
311 self.dms_running.store(true, Ordering::SeqCst);
312 let running = self.dms_running.clone();
313 let http_client = self.http_client.clone();
314
315 let handle = get_runtime().spawn(async move {
316 while running.load(Ordering::SeqCst) {
317 if let Err(e) = http_client.cancel_all_after(timeout_ms).await {
318 log::warn!("Dead man's switch heartbeat failed: {e}");
319 }
320 tokio::time::sleep(Duration::from_secs(interval_secs)).await;
321 }
322 });
323
324 self.dms_task_handle = Some(handle);
325 }
326
327 async fn stop_deadmans_switch(&mut self) {
328 if self.config.deadmans_switch_timeout_secs.is_none() {
329 return;
330 }
331
332 self.dms_running.store(false, Ordering::SeqCst);
333
334 if let Some(handle) = self.dms_task_handle.take() {
336 handle.abort();
337 let _ = handle.await;
338 }
339
340 log::info!("Disarming dead man's switch");
341
342 if let Err(e) = self.http_client.cancel_all_after(0).await {
343 log::warn!("Failed to disarm dead man's switch: {e}");
344 }
345 }
346
347 async fn ensure_instruments_initialized_async(&self) -> anyhow::Result<()> {
348 if self.core.instruments_initialized() {
349 return Ok(());
350 }
351
352 let mut instruments: Vec<InstrumentAny> = {
353 let cache = self.core.cache();
354 cache
355 .instruments(&self.core.venue, None)
356 .into_iter()
357 .cloned()
358 .collect()
359 };
360
361 if instruments.is_empty() {
362 let http = self.http_client.clone();
363 instruments = http
364 .request_instruments(self.config.active_only)
365 .await
366 .context("failed to request BitMEX instruments")?;
367 } else {
368 log::debug!(
369 "Reusing {} cached BitMEX instruments for execution client initialization",
370 instruments.len()
371 );
372 }
373
374 instruments.sort_by_key(|instrument| instrument.id());
375
376 self.http_client.cache_instruments(&instruments);
377 self.ws_client.cache_instruments(&instruments);
378 for instrument in &instruments {
379 self._submitter.cache_instrument(instrument);
380 self._canceller.cache_instrument(instrument);
381 }
382
383 self.core.set_instruments_initialized();
384 Ok(())
385 }
386
387 async fn refresh_account_state(&mut self) -> anyhow::Result<()> {
388 let account_state = self
389 .http_client
390 .request_account_state(self.core.account_id)
391 .await
392 .context("failed to request BitMEX account state")?;
393
394 self.apply_account_id(account_state.account_id);
395 self.emitter.send_account_state(account_state);
396 Ok(())
397 }
398
399 fn apply_account_id(&mut self, account_id: AccountId) {
400 if self.core.account_id != account_id {
401 log::debug!(
402 "Discovered BitMEX account ID: account_id={} (was {})",
403 account_id,
404 self.core.account_id
405 );
406 }
407
408 self.core.set_account_id(account_id);
409 self.emitter.set_account_id(account_id);
410 self.ws_client.set_account_id(account_id);
411 }
412
413 async fn await_account_registered(&self, timeout_secs: f64) -> anyhow::Result<()> {
414 let account_id = self.core.account_id;
415
416 if self.core.cache().account(&account_id).is_some() {
417 log::info!("Account {account_id} registered");
418 return Ok(());
419 }
420
421 let start = Instant::now();
422 let timeout = Duration::from_secs_f64(timeout_secs);
423 let interval = Duration::from_millis(10);
424
425 loop {
426 tokio::time::sleep(interval).await;
427
428 if self.core.cache().account(&account_id).is_some() {
429 log::info!("Account {account_id} registered");
430 return Ok(());
431 }
432
433 if start.elapsed() >= timeout {
434 anyhow::bail!(
435 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
436 );
437 }
438 }
439 }
440
441 fn start_ws_stream(&mut self) {
442 if self.ws_stream_handle.is_some() {
443 return;
444 }
445
446 let stream = self.ws_client.stream();
447 let emitter = self.emitter.clone();
448 let state = Arc::clone(&self.ws_dispatch_state);
449 let account_id = self.core.account_id;
450 let clock = self.clock;
451
452 let mut instruments_by_symbol: AHashMap<Ustr, InstrumentAny> = self
454 .core
455 .cache()
456 .instruments(&self.core.venue, None)
457 .into_iter()
458 .map(|inst| (inst.symbol().inner(), inst.clone()))
459 .collect();
460
461 if instruments_by_symbol.is_empty() {
462 for (key, inst) in self.http_client.instruments_cache.load().iter() {
463 instruments_by_symbol.insert(*key, inst.clone());
464 }
465 }
466
467 let handle = get_runtime().spawn(async move {
468 pin_mut!(stream);
469 let mut order_type_cache: AHashMap<ClientOrderId, OrderType> = AHashMap::new();
470 let mut order_symbol_cache: AHashMap<ClientOrderId, Ustr> = AHashMap::new();
471 let mut insts_by_symbol = instruments_by_symbol;
472
473 while let Some(message) = stream.next().await {
474 dispatch::dispatch_ws_message(
475 clock.get_time_ns(),
476 message,
477 &emitter,
478 &state,
479 &mut insts_by_symbol,
480 &mut order_type_cache,
481 &mut order_symbol_cache,
482 account_id,
483 );
484 }
485 });
486
487 self.ws_stream_handle = Some(handle);
488 }
489
490 fn submit_cached_order(
491 &self,
492 order: &OrderAny,
493 submit_tries: Option<usize>,
494 peg_price_type: Option<BitmexPegPriceType>,
495 peg_offset_value: Option<f64>,
496 task_label: &'static str,
497 ) {
498 if order.is_closed() {
499 log::warn!("Cannot submit closed order {}", order.client_order_id());
500 return;
501 }
502
503 if let Err(e) = validate_order_for_bitmex_submit(order, peg_price_type, peg_offset_value) {
504 self.emitter.emit_order_denied(order, &e.to_string());
505 return;
506 }
507
508 self.emitter.emit_order_submitted(order);
509
510 let strategy_id = order.strategy_id();
511 let instrument_id = order.instrument_id();
512 let client_order_id = order.client_order_id();
513 let order_side = order.order_side();
514 let order_type = order.order_type();
515
516 self.ws_dispatch_state.order_identities.insert(
517 client_order_id,
518 OrderIdentity {
519 instrument_id,
520 strategy_id,
521 order_side,
522 order_type,
523 },
524 );
525
526 let use_broadcaster = submit_tries.is_some_and(|n| n > 1);
527 let http_client = self.http_client.clone();
528 let submitter = self._submitter.clone_for_async();
529 let ws_dispatch_state = self.ws_dispatch_state.clone();
530 let emitter = self.emitter.clone();
531 let clock = self.clock;
532 let quantity = order.quantity();
533 let time_in_force = order.time_in_force();
534 let price = order.price();
535 let trigger_price = order.trigger_price();
536 let trigger_type = order.trigger_type();
537 let trailing_offset = order.trailing_offset().and_then(|d| d.to_f64());
538 let trailing_offset_type = order.trailing_offset_type();
539 let display_qty = order.display_qty();
540 let post_only = order.is_post_only();
541 let reduce_only = order.is_reduce_only();
542 let order_list_id = order.order_list_id();
543 let contingency_type = order.contingency_type();
544
545 self.spawn_task(task_label, async move {
546 let result = if use_broadcaster {
547 submitter
548 .broadcast_submit(
549 instrument_id,
550 client_order_id,
551 order_side,
552 order_type,
553 quantity,
554 time_in_force,
555 price,
556 trigger_price,
557 trigger_type,
558 trailing_offset,
559 trailing_offset_type,
560 display_qty,
561 post_only,
562 reduce_only,
563 order_list_id,
564 contingency_type,
565 submit_tries,
566 peg_price_type,
567 peg_offset_value,
568 )
569 .await
570 } else {
571 http_client
572 .submit_order(
573 instrument_id,
574 client_order_id,
575 order_side,
576 order_type,
577 quantity,
578 time_in_force,
579 price,
580 trigger_price,
581 trigger_type,
582 trailing_offset,
583 trailing_offset_type,
584 display_qty,
585 post_only,
586 reduce_only,
587 order_list_id,
588 contingency_type,
589 peg_price_type,
590 peg_offset_value,
591 )
592 .await
593 };
594
595 match result {
596 Ok(_report) => {
597 }
602 Err(e) => handle_submit_failure(&SubmitFailure {
603 err: &e,
604 ws_dispatch_state: &ws_dispatch_state,
605 emitter: &emitter,
606 clock,
607 strategy_id,
608 instrument_id,
609 client_order_id,
610 post_only,
611 }),
612 }
613 Ok(())
614 });
615 }
616}
617
618#[async_trait(?Send)]
619impl ExecutionClient for BitmexExecutionClient {
620 fn is_connected(&self) -> bool {
621 self.core.is_connected()
622 }
623
624 fn client_id(&self) -> ClientId {
625 self.core.client_id
626 }
627
628 fn account_id(&self) -> AccountId {
629 self.core.account_id
630 }
631
632 fn venue(&self) -> Venue {
633 self.core.venue
634 }
635
636 fn oms_type(&self) -> OmsType {
637 self.core.oms_type
638 }
639
640 fn get_account(&self) -> Option<AccountAny> {
641 self.core.cache().account_owned(&self.core.account_id)
642 }
643
644 fn generate_account_state(
645 &self,
646 balances: Vec<AccountBalance>,
647 margins: Vec<MarginBalance>,
648 reported: bool,
649 ts_event: UnixNanos,
650 ) -> anyhow::Result<()> {
651 self.emitter
652 .emit_account_state(balances, margins, reported, ts_event);
653 Ok(())
654 }
655
656 fn start(&mut self) -> anyhow::Result<()> {
657 if self.core.is_started() {
658 return Ok(());
659 }
660
661 self.emitter.set_sender(get_exec_event_sender());
662 self.core.set_started();
663 log::info!(
664 "BitMEX execution client started: client_id={}, account_id={}, environment={}, submitter_pool_size={:?}, canceller_pool_size={:?}, proxy_url={:?}, submitter_proxy_urls={:?}, canceller_proxy_urls={:?}",
665 self.core.client_id,
666 self.core.account_id,
667 self.config.environment,
668 self.config.submitter_pool_size,
669 self.config.canceller_pool_size,
670 self.config.proxy_url,
671 self.config.submitter_proxy_urls,
672 self.config.canceller_proxy_urls,
673 );
674 Ok(())
675 }
676
677 fn stop(&mut self) -> anyhow::Result<()> {
678 if self.core.is_stopped() {
679 return Ok(());
680 }
681
682 self.core.set_stopped();
683 self.core.set_disconnected();
684
685 if let Some(handle) = self.ws_stream_handle.take() {
686 handle.abort();
687 }
688
689 if let Some(handle) = self.dms_task_handle.take() {
690 handle.abort();
691 }
692 self.dms_running.store(false, Ordering::SeqCst);
693 self.abort_pending_tasks();
694 log::info!("BitMEX execution client {} stopped", self.core.client_id);
695 Ok(())
696 }
697
698 async fn connect(&mut self) -> anyhow::Result<()> {
699 if self.core.is_connected() {
700 return Ok(());
701 }
702
703 self.http_client.reset_cancellation_token();
705
706 self.ensure_instruments_initialized_async().await?;
707
708 self.refresh_account_state().await?;
709 self.await_account_registered(30.0).await?;
710
711 self.ws_client.connect().await?;
712 self.ws_client.wait_until_active(10.0).await?;
713
714 self._submitter.start().await?;
716 self._canceller.start().await?;
717
718 self.ws_client.subscribe_orders().await?;
719 self.ws_client.subscribe_executions().await?;
720 self.ws_client.subscribe_positions().await?;
721 self.ws_client.subscribe_wallet().await?;
722 if let Err(e) = self.ws_client.subscribe_margin().await {
723 log::debug!("Margin subscription unavailable: {e:?}");
724 }
725
726 self.start_ws_stream();
727
728 self.core.set_connected();
729 self.start_deadmans_switch();
730 log::info!("Connected: client_id={}", self.core.client_id);
731 Ok(())
732 }
733
734 async fn disconnect(&mut self) -> anyhow::Result<()> {
735 if self.core.is_disconnected() {
736 return Ok(());
737 }
738
739 self.stop_deadmans_switch().await;
741
742 self.http_client.cancel_all_requests();
743 self._submitter.stop().await;
744 self._canceller.stop().await;
745
746 if let Err(e) = self.ws_client.close().await {
747 log::warn!("Error while closing BitMEX execution websocket: {e:?}");
748 }
749
750 if let Some(handle) = self.ws_stream_handle.take() {
751 handle.abort();
752 }
753
754 self.abort_pending_tasks();
755 self.core.set_disconnected();
756 log::info!("Disconnected: client_id={}", self.core.client_id);
757 Ok(())
758 }
759
760 async fn generate_order_status_report(
761 &self,
762 cmd: &GenerateOrderStatusReport,
763 ) -> anyhow::Result<Option<OrderStatusReport>> {
764 let instrument_id = cmd
765 .instrument_id
766 .context("BitMEX generate_order_status_report requires an instrument identifier")?;
767
768 self.http_client
769 .query_order(
770 instrument_id,
771 cmd.client_order_id,
772 cmd.venue_order_id.map(|id| VenueOrderId::from(id.as_str())),
773 )
774 .await
775 .context("failed to query BitMEX order status")
776 }
777
778 async fn generate_order_status_reports(
779 &self,
780 cmd: &GenerateOrderStatusReports,
781 ) -> anyhow::Result<Vec<OrderStatusReport>> {
782 let start_dt = cmd.start.map(|nanos| nanos.to_datetime_utc());
783 let end_dt = cmd.end.map(|nanos| nanos.to_datetime_utc());
784
785 let mut reports = self
786 .http_client
787 .request_order_status_reports(cmd.instrument_id, cmd.open_only, start_dt, end_dt, None)
788 .await
789 .context("failed to request BitMEX order status reports")?;
790
791 if let Some(start) = cmd.start {
792 reports.retain(|report| report.ts_last >= start);
793 }
794
795 if let Some(end) = cmd.end {
796 reports.retain(|report| report.ts_last <= end);
797 }
798
799 Self::log_report_receipt(reports.len(), "OrderStatusReport", cmd.log_receipt_level);
800
801 Ok(reports)
802 }
803
804 async fn generate_fill_reports(
805 &self,
806 cmd: GenerateFillReports,
807 ) -> anyhow::Result<Vec<FillReport>> {
808 let start_dt = cmd.start.map(|nanos| nanos.to_datetime_utc());
809 let end_dt = cmd.end.map(|nanos| nanos.to_datetime_utc());
810
811 let mut reports = self
812 .http_client
813 .request_fill_reports(cmd.instrument_id, start_dt, end_dt, None)
814 .await
815 .context("failed to request BitMEX fill reports")?;
816
817 if let Some(order_id) = cmd.venue_order_id {
818 reports.retain(|report| report.venue_order_id.as_str() == order_id.as_str());
819 }
820
821 if let Some(start) = cmd.start {
822 reports.retain(|report| report.ts_event >= start);
823 }
824
825 if let Some(end) = cmd.end {
826 reports.retain(|report| report.ts_event <= end);
827 }
828
829 Self::log_report_receipt(reports.len(), "FillReport", cmd.log_receipt_level);
830
831 Ok(reports)
832 }
833
834 async fn generate_position_status_reports(
835 &self,
836 cmd: &GeneratePositionStatusReports,
837 ) -> anyhow::Result<Vec<PositionStatusReport>> {
838 let mut reports = self
839 .http_client
840 .request_position_status_reports()
841 .await
842 .context("failed to request BitMEX position reports")?;
843
844 if let Some(instrument_id) = cmd.instrument_id {
845 reports.retain(|report| report.instrument_id == instrument_id);
846 }
847
848 if let Some(start) = cmd.start {
849 reports.retain(|report| report.ts_last >= start);
850 }
851
852 if let Some(end) = cmd.end {
853 reports.retain(|report| report.ts_last <= end);
854 }
855
856 Self::log_report_receipt(reports.len(), "PositionStatusReport", cmd.log_receipt_level);
857
858 Ok(reports)
859 }
860
861 async fn generate_mass_status(
862 &self,
863 lookback_mins: Option<u64>,
864 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
865 log::info!("Generating ExecutionMassStatus (lookback_mins={lookback_mins:?})");
866
867 let ts_now = self.clock.get_time_ns();
868 let start = lookback_mins.map(|mins| {
869 let lookback_ns = mins.saturating_mul(60).saturating_mul(1_000_000_000);
870 UnixNanos::from(ts_now.as_u64().saturating_sub(lookback_ns))
871 });
872
873 let order_cmd = GenerateOrderStatusReportsBuilder::default()
874 .ts_init(ts_now)
875 .open_only(false)
876 .start(start)
877 .build()
878 .map_err(|e| anyhow::anyhow!("{e}"))?;
879
880 let fill_cmd = GenerateFillReportsBuilder::default()
881 .ts_init(ts_now)
882 .start(start)
883 .build()
884 .map_err(|e| anyhow::anyhow!("{e}"))?;
885
886 let position_cmd = GeneratePositionStatusReportsBuilder::default()
887 .ts_init(ts_now)
888 .start(start)
889 .build()
890 .map_err(|e| anyhow::anyhow!("{e}"))?;
891
892 let (order_reports, fill_reports, position_reports) = tokio::try_join!(
893 self.generate_order_status_reports(&order_cmd),
894 self.generate_fill_reports(fill_cmd),
895 self.generate_position_status_reports(&position_cmd),
896 )?;
897
898 let mut mass_status = ExecutionMassStatus::new(
899 self.core.client_id,
900 self.core.account_id,
901 self.core.venue,
902 ts_now,
903 None,
904 );
905 mass_status.add_order_reports(order_reports);
906 mass_status.add_fill_reports(fill_reports);
907 mass_status.add_position_reports(position_reports);
908
909 Ok(Some(mass_status))
910 }
911
912 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
913 let http_client = self.http_client.clone();
914 let emitter = self.emitter.clone();
915 let account_id = self.core.account_id;
916
917 self.spawn_task("query_account", async move {
918 match http_client.request_account_state(account_id).await {
919 Ok(account_state) => emitter.send_account_state(account_state),
920 Err(e) => log::error!("BitMEX query account failed: {e:?}"),
921 }
922 Ok(())
923 });
924
925 Ok(())
926 }
927
928 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
929 let http_client = self.http_client.clone();
930 let instrument_id = cmd.instrument_id;
931 let client_order_id = Some(cmd.client_order_id);
932 let venue_order_id = cmd.venue_order_id;
933 let emitter = self.emitter.clone();
934
935 self.spawn_task("query_order", async move {
936 match http_client
937 .request_order_status_report(instrument_id, client_order_id, venue_order_id)
938 .await
939 {
940 Ok(report) => emitter.send_order_status_report(report),
941 Err(e) => log::error!("BitMEX query order failed: {e:?}"),
942 }
943 Ok(())
944 });
945
946 Ok(())
947 }
948
949 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
950 let submit_tries = cmd
951 .params
952 .as_ref()
953 .and_then(|p| p.get_usize("submit_tries"))
954 .filter(|&n| n > 0);
955
956 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
957
958 let peg_price_type = match parse_peg_price_type(cmd.params.as_ref()) {
959 Ok(value) => value,
960 Err(e) => {
961 self.emitter.emit_order_denied(&order, &e.to_string());
962 return Ok(());
963 }
964 };
965 let peg_offset_value = match parse_peg_offset_value(cmd.params.as_ref()) {
966 Ok(value) => value,
967 Err(e) => {
968 self.emitter.emit_order_denied(&order, &e.to_string());
969 return Ok(());
970 }
971 };
972
973 self.submit_cached_order(
974 &order,
975 submit_tries,
976 peg_price_type,
977 peg_offset_value,
978 "submit_order",
979 );
980 Ok(())
981 }
982
983 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
984 if cmd.order_list.client_order_ids.is_empty() {
985 log::debug!("submit_order_list called with empty order list");
986 return Ok(());
987 }
988
989 let submit_tries = cmd
990 .params
991 .as_ref()
992 .and_then(|p| p.get_usize("submit_tries"))
993 .filter(|&n| n > 0);
994
995 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
996
997 let peg_price_type = match parse_peg_price_type(cmd.params.as_ref()) {
998 Ok(value) => value,
999 Err(e) => {
1000 for order in &orders {
1001 self.emitter.emit_order_denied(order, &e.to_string());
1002 }
1003 return Ok(());
1004 }
1005 };
1006 let peg_offset_value = match parse_peg_offset_value(cmd.params.as_ref()) {
1007 Ok(value) => value,
1008 Err(e) => {
1009 for order in &orders {
1010 self.emitter.emit_order_denied(order, &e.to_string());
1011 }
1012 return Ok(());
1013 }
1014 };
1015
1016 log::debug!(
1017 "Submitting BitMEX order list: order_list_id={}, count={}",
1018 cmd.order_list.id,
1019 orders.len(),
1020 );
1021
1022 for order in orders {
1023 self.submit_cached_order(
1024 &order,
1025 submit_tries,
1026 peg_price_type,
1027 peg_offset_value,
1028 "submit_order_list_item",
1029 );
1030 }
1031
1032 Ok(())
1033 }
1034
1035 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1036 self.ensure_order_identity(cmd.client_order_id, cmd.strategy_id, cmd.instrument_id);
1037 let http_client = self.http_client.clone();
1038 let emitter = self.emitter.clone();
1039 let clock = self.clock;
1040 let instrument_id = cmd.instrument_id;
1041 let client_order_id = cmd.client_order_id;
1042 let client_order_id_opt = Some(client_order_id);
1043 let venue_order_id = cmd.venue_order_id;
1044 let quantity = cmd.quantity;
1045 let price = cmd.price;
1046 let trigger_price = cmd.trigger_price;
1047 let strategy_id = cmd.strategy_id;
1048
1049 self.spawn_task("modify_order", async move {
1050 match http_client
1051 .modify_order(
1052 instrument_id,
1053 client_order_id_opt,
1054 venue_order_id,
1055 quantity,
1056 price,
1057 trigger_price,
1058 )
1059 .await
1060 {
1061 Ok(_) => {
1062 log::debug!(
1063 "BitMEX modify accepted by REST, awaiting websocket confirmation: client_order_id={client_order_id}"
1064 );
1065 }
1066 Err(e) => handle_modify_failure(&ModifyFailure {
1067 err: &e,
1068 emitter: &emitter,
1069 clock,
1070 strategy_id,
1071 instrument_id,
1072 client_order_id,
1073 venue_order_id,
1074 }),
1075 }
1076 Ok(())
1077 });
1078
1079 Ok(())
1080 }
1081
1082 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1083 self.ensure_order_identity(cmd.client_order_id, cmd.strategy_id, cmd.instrument_id);
1084 let canceller = self._canceller.clone_for_async();
1085 let emitter = self.emitter.clone();
1086 let dispatch_state = Arc::clone(&self.ws_dispatch_state);
1087 let instrument_id = cmd.instrument_id;
1088 let client_order_id = Some(cmd.client_order_id);
1089 let venue_order_id = cmd.venue_order_id;
1090
1091 self.spawn_task("cancel_order", async move {
1092 match canceller
1093 .broadcast_cancel(instrument_id, client_order_id, venue_order_id)
1094 .await
1095 {
1096 Ok(Some(report)) => {
1097 if let Some(cid) = &report.client_order_id {
1098 dispatch_state.tombstone_order(cid);
1099 }
1100 emitter.send_order_status_report(report);
1101 }
1102 Ok(None) => {
1103 log::debug!("Order already cancelled: {client_order_id:?}");
1104 }
1105 Err(e) => log::error!("BitMEX cancel order failed: {e:?}"),
1106 }
1107 Ok(())
1108 });
1109
1110 Ok(())
1111 }
1112
1113 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1114 let canceller = self._canceller.clone_for_async();
1115 let emitter = self.emitter.clone();
1116 let dispatch_state = Arc::clone(&self.ws_dispatch_state);
1117 let instrument_id = cmd.instrument_id;
1118 let order_side = if cmd.order_side == OrderSide::NoOrderSide {
1119 log::debug!(
1120 "BitMEX cancel_all_orders received NoOrderSide for {instrument_id}, using unfiltered cancel-all",
1121 );
1122 None
1123 } else {
1124 Some(cmd.order_side)
1125 };
1126
1127 self.spawn_task("cancel_all_orders", async move {
1128 match canceller
1129 .broadcast_cancel_all(instrument_id, order_side)
1130 .await
1131 {
1132 Ok(reports) => {
1133 for report in &reports {
1134 if let Some(cid) = &report.client_order_id {
1135 dispatch_state.tombstone_order(cid);
1136 }
1137 }
1138
1139 for report in reports {
1140 emitter.send_order_status_report(report);
1141 }
1142 }
1143 Err(e) => log::error!("BitMEX cancel all failed: {e:?}"),
1144 }
1145 Ok(())
1146 });
1147
1148 Ok(())
1149 }
1150
1151 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1152 let canceller = self._canceller.clone_for_async();
1153 let emitter = self.emitter.clone();
1154 let dispatch_state = Arc::clone(&self.ws_dispatch_state);
1155 let instrument_id = cmd.instrument_id;
1156
1157 let client_ids: Vec<ClientOrderId> = cmd
1158 .cancels
1159 .iter()
1160 .map(|cancel| cancel.client_order_id)
1161 .collect();
1162
1163 let venue_ids: Vec<VenueOrderId> = cmd
1164 .cancels
1165 .iter()
1166 .filter_map(|cancel| cancel.venue_order_id)
1167 .collect();
1168
1169 let client_ids_opt = if client_ids.is_empty() {
1170 None
1171 } else {
1172 Some(client_ids)
1173 };
1174
1175 let venue_ids_opt = if venue_ids.is_empty() {
1176 None
1177 } else {
1178 Some(venue_ids)
1179 };
1180
1181 self.spawn_task("batch_cancel_orders", async move {
1182 match canceller
1183 .broadcast_batch_cancel(instrument_id, client_ids_opt, venue_ids_opt)
1184 .await
1185 {
1186 Ok(reports) => {
1187 for report in &reports {
1188 if let Some(cid) = &report.client_order_id {
1189 dispatch_state.tombstone_order(cid);
1190 }
1191 }
1192
1193 for report in reports {
1194 emitter.send_order_status_report(report);
1195 }
1196 }
1197 Err(e) => log::error!("BitMEX batch cancel failed: {e:?}"),
1198 }
1199 Ok(())
1200 });
1201
1202 Ok(())
1203 }
1204}
1205
1206struct SubmitFailure<'a> {
1207 err: &'a anyhow::Error,
1208 ws_dispatch_state: &'a Arc<WsDispatchState>,
1209 emitter: &'a ExecutionEventEmitter,
1210 clock: &'static AtomicTime,
1211 strategy_id: StrategyId,
1212 instrument_id: InstrumentId,
1213 client_order_id: ClientOrderId,
1214 post_only: bool,
1215}
1216
1217fn handle_submit_failure(failure: &SubmitFailure<'_>) {
1218 let error_msg = failure.err.to_string();
1219
1220 if is_bitmex_duplicate_clordid_submit_failure(failure.err) {
1222 log::warn!(
1223 "Order {} may exist (duplicate clOrdID), \
1224 awaiting WebSocket confirmation",
1225 failure.client_order_id,
1226 );
1227 return;
1228 }
1229
1230 if is_definitive_bitmex_submit_rejection(failure.err) {
1231 failure
1232 .ws_dispatch_state
1233 .order_identities
1234 .remove(&failure.client_order_id);
1235 let ts_event = failure.clock.get_time_ns();
1236 let rejection_reason = error_msg
1237 .strip_prefix(DEFINITIVE_SUBMIT_REJECTION)
1238 .map_or(error_msg.as_str(), |msg| {
1239 msg.trim_start_matches(':').trim_start()
1240 });
1241 failure.emitter.emit_order_rejected_event(
1242 failure.strategy_id,
1243 failure.instrument_id,
1244 failure.client_order_id,
1245 &format!("submit-order-error: {rejection_reason}"),
1246 ts_event,
1247 failure.post_only,
1248 );
1249 } else {
1250 log::warn!(
1251 "Ambiguous BitMEX submit failure for {}, awaiting reconciliation: {:?}",
1252 failure.client_order_id,
1253 failure.err,
1254 );
1255 }
1256}
1257
1258struct ModifyFailure<'a> {
1259 err: &'a anyhow::Error,
1260 emitter: &'a ExecutionEventEmitter,
1261 clock: &'static AtomicTime,
1262 strategy_id: StrategyId,
1263 instrument_id: InstrumentId,
1264 client_order_id: ClientOrderId,
1265 venue_order_id: Option<VenueOrderId>,
1266}
1267
1268fn handle_modify_failure(failure: &ModifyFailure<'_>) {
1269 if is_definitive_bitmex_modify_rejection(failure.err) {
1270 let ts_event = failure.clock.get_time_ns();
1271 failure.emitter.emit_order_modify_rejected_event(
1272 failure.strategy_id,
1273 failure.instrument_id,
1274 failure.client_order_id,
1275 failure.venue_order_id,
1276 &format!("modify-order-error: {}", failure.err),
1277 ts_event,
1278 );
1279 } else {
1280 log::warn!(
1281 "Ambiguous BitMEX modify failure for {}, awaiting reconciliation: {:?}",
1282 failure.client_order_id,
1283 failure.err,
1284 );
1285 }
1286}
1287
1288fn validate_order_for_bitmex_submit(
1289 order: &OrderAny,
1290 peg_price_type: Option<BitmexPegPriceType>,
1291 peg_offset_value: Option<f64>,
1292) -> anyhow::Result<()> {
1293 if order.order_side() == OrderSide::NoOrderSide {
1294 anyhow::bail!("Order side must be Buy or Sell");
1295 }
1296
1297 BitmexOrderType::try_from_order_type(order.order_type())?;
1298 BitmexTimeInForce::try_from_time_in_force(order.time_in_force())?;
1299
1300 let is_trailing_stop = matches!(
1301 order.order_type(),
1302 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
1303 );
1304
1305 if is_trailing_stop
1306 && let Some(offset_type) = order.trailing_offset_type()
1307 && offset_type != TrailingOffsetType::Price
1308 {
1309 anyhow::bail!("BitMEX only supports PRICE trailing offset type, was {offset_type:?}");
1310 }
1311
1312 if peg_price_type.is_none() && peg_offset_value.is_some() {
1313 anyhow::bail!("`peg_offset_value` requires `peg_price_type`");
1314 }
1315
1316 if peg_price_type.is_some() && order.order_type() != OrderType::Limit {
1317 let order_type = order.order_type();
1318 anyhow::bail!("Pegged orders only supported for LIMIT order type, was {order_type:?}");
1319 }
1320
1321 if let Some(contingency_type) = order.contingency_type() {
1322 BitmexContingencyType::try_from(contingency_type)?;
1323 }
1324
1325 Ok(())
1326}
1327
1328fn is_definitive_bitmex_submit_rejection(err: &anyhow::Error) -> bool {
1329 if is_bitmex_duplicate_clordid_submit_failure(err) {
1330 return false;
1331 }
1332
1333 if has_bitmex_api_refusal(err) {
1334 return true;
1335 }
1336
1337 let message = err.to_string();
1338 message.starts_with("Order rejected:") || message.starts_with(DEFINITIVE_SUBMIT_REJECTION)
1339}
1340
1341fn is_bitmex_duplicate_clordid_submit_failure(err: &anyhow::Error) -> bool {
1342 if err.to_string().contains("IDEMPOTENT_DUPLICATE") {
1343 return true;
1344 }
1345
1346 err.chain().any(|cause| {
1347 cause
1348 .downcast_ref::<BitmexHttpError>()
1349 .is_some_and(|e| {
1350 matches!(e, BitmexHttpError::BitmexError { message, .. } if message.contains("Duplicate clOrdID"))
1351 })
1352 })
1353}
1354
1355fn is_definitive_bitmex_modify_rejection(err: &anyhow::Error) -> bool {
1356 if has_bitmex_api_refusal(err) {
1357 return true;
1358 }
1359
1360 err.to_string().starts_with("Order modification rejected:")
1361}
1362
1363fn has_bitmex_api_refusal(err: &anyhow::Error) -> bool {
1364 err.chain().any(|cause| {
1365 cause
1366 .downcast_ref::<BitmexHttpError>()
1367 .is_some_and(|e| matches!(e, BitmexHttpError::BitmexError { .. }))
1368 })
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use std::{cell::RefCell, rc::Rc};
1374
1375 use nautilus_common::{
1376 cache::Cache,
1377 clients::ExecutionClient,
1378 messages::{ExecutionEvent, ExecutionReport},
1379 };
1380 use nautilus_core::{Params, UUID4};
1381 use nautilus_model::{
1382 enums::TimeInForce,
1383 events::OrderEventAny,
1384 identifiers::{Symbol, TraderId},
1385 instruments::crypto_perpetual::CryptoPerpetual,
1386 orders::builder::OrderTestBuilder,
1387 types::{Currency, Price, Quantity},
1388 };
1389 use nautilus_network::http::StatusCode;
1390 use rstest::rstest;
1391
1392 use super::*;
1393 use crate::{
1394 common::{
1395 consts::{BITMEX_CLIENT_ID, BITMEX_VENUE},
1396 testing::load_test_json,
1397 },
1398 websocket::{
1399 enums::BitmexAction,
1400 messages::{BitmexExecutionMsg, BitmexTableMessage, BitmexWalletMsg, BitmexWsMessage},
1401 },
1402 };
1403
1404 fn bitmex_api_error() -> anyhow::Error {
1405 anyhow::Error::new(BitmexHttpError::BitmexError {
1406 error_name: "HTTPError".to_string(),
1407 message: "Invalid price".to_string(),
1408 })
1409 }
1410
1411 fn test_execution_client() -> (BitmexExecutionClient, Rc<RefCell<Cache>>) {
1412 let cache = Rc::new(RefCell::new(Cache::default()));
1413 let core = ExecutionClientCore::new(
1414 TraderId::from("TESTER-001"),
1415 *BITMEX_CLIENT_ID,
1416 *BITMEX_VENUE,
1417 OmsType::Netting,
1418 AccountId::from("BITMEX-001"),
1419 AccountType::Margin,
1420 None,
1421 cache.clone(),
1422 );
1423 let config = BitmexExecClientConfig {
1424 api_key: Some("test_key".to_string()),
1425 api_secret: Some("test_secret".to_string()),
1426 base_url_http: Some("http://127.0.0.1:9/api/v1".to_string()),
1427 base_url_ws: Some("ws://127.0.0.1:9/realtime".to_string()),
1428 ..Default::default()
1429 };
1430
1431 (BitmexExecutionClient::new(core, config).unwrap(), cache)
1432 }
1433
1434 fn make_emitter() -> (
1435 ExecutionEventEmitter,
1436 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1437 ) {
1438 let mut emitter = ExecutionEventEmitter::new(
1439 get_atomic_clock_realtime(),
1440 TraderId::from("TESTER-001"),
1441 AccountId::from("BITMEX-001"),
1442 AccountType::Margin,
1443 None,
1444 );
1445 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1446 emitter.set_sender(tx);
1447 (emitter, rx)
1448 }
1449
1450 fn limit_order() -> OrderAny {
1451 limit_order_with_id(ClientOrderId::from("O-LIMIT"))
1452 }
1453
1454 fn limit_order_with_id(client_order_id: ClientOrderId) -> OrderAny {
1455 let mut builder = OrderTestBuilder::new(OrderType::Limit);
1456 builder
1457 .instrument_id(InstrumentId::from("XBTUSD.BITMEX"))
1458 .client_order_id(client_order_id)
1459 .side(OrderSide::Buy)
1460 .quantity(Quantity::from("1"))
1461 .price(Price::from("100.0"))
1462 .build()
1463 }
1464
1465 fn test_perpetual_instrument() -> InstrumentAny {
1466 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
1467 InstrumentId::from("XBTUSD.BITMEX"),
1468 Symbol::new("XBTUSD"),
1469 Currency::BTC(),
1470 Currency::USD(),
1471 Currency::BTC(),
1472 true,
1473 1,
1474 0,
1475 Price::new(0.5, 1),
1476 Quantity::new(1.0, 0),
1477 None,
1478 None,
1479 None,
1480 None,
1481 None,
1482 None,
1483 None,
1484 None,
1485 None,
1486 None,
1487 None,
1488 None,
1489 None,
1490 None,
1491 UnixNanos::default(),
1492 UnixNanos::default(),
1493 ))
1494 }
1495
1496 fn market_order() -> OrderAny {
1497 let mut builder = OrderTestBuilder::new(OrderType::Market);
1498 builder
1499 .instrument_id(InstrumentId::from("XBTUSD.BITMEX"))
1500 .quantity(Quantity::from("1"))
1501 .build()
1502 }
1503
1504 fn order_identity(order: &OrderAny) -> OrderIdentity {
1505 OrderIdentity {
1506 instrument_id: order.instrument_id(),
1507 strategy_id: order.strategy_id(),
1508 order_side: order.order_side(),
1509 order_type: order.order_type(),
1510 }
1511 }
1512
1513 fn submit_command(order: &OrderAny, params: Option<Params>) -> SubmitOrder {
1514 SubmitOrder::new(
1515 order.trader_id(),
1516 Some(*BITMEX_CLIENT_ID),
1517 order.strategy_id(),
1518 order.instrument_id(),
1519 order.client_order_id(),
1520 order.init_event().clone(),
1521 None,
1522 None,
1523 params,
1524 UUID4::new(),
1525 UnixNanos::default(),
1526 None,
1527 )
1528 }
1529
1530 fn drain_order_events(
1531 rx: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1532 ) -> Vec<OrderEventAny> {
1533 let mut events = Vec::new();
1534
1535 while let Ok(event) = rx.try_recv() {
1536 if let ExecutionEvent::Order(event) = event {
1537 events.push(event);
1538 }
1539 }
1540 events
1541 }
1542
1543 fn dispatch_execution_fixture(
1544 state: &WsDispatchState,
1545 emitter: &ExecutionEventEmitter,
1546 account_id: AccountId,
1547 ) {
1548 let exec_msg: BitmexExecutionMsg =
1549 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1550 let mut instruments_by_symbol = AHashMap::new();
1551 instruments_by_symbol.insert(Ustr::from("XBTUSD"), test_perpetual_instrument());
1552 let mut order_type_cache = AHashMap::new();
1553 let mut order_symbol_cache = AHashMap::new();
1554
1555 dispatch::dispatch_ws_message(
1556 UnixNanos::default(),
1557 BitmexWsMessage::Table(BitmexTableMessage::Execution {
1558 action: BitmexAction::Insert,
1559 data: vec![exec_msg],
1560 }),
1561 emitter,
1562 state,
1563 &mut instruments_by_symbol,
1564 &mut order_type_cache,
1565 &mut order_symbol_cache,
1566 account_id,
1567 );
1568 }
1569
1570 #[rstest]
1571 fn test_bitmex_api_error_is_definitive_submit_rejection() {
1572 let err = bitmex_api_error();
1573
1574 assert!(is_definitive_bitmex_submit_rejection(&err));
1575 }
1576
1577 #[rstest]
1578 fn test_config_account_id_seeds_core_account_id() {
1579 let cache = Rc::new(RefCell::new(Cache::default()));
1580 let core = ExecutionClientCore::new(
1581 TraderId::from("TESTER-001"),
1582 *BITMEX_CLIENT_ID,
1583 *BITMEX_VENUE,
1584 OmsType::Netting,
1585 AccountId::from("BITMEX-001"),
1586 AccountType::Margin,
1587 None,
1588 cache,
1589 );
1590 let config = BitmexExecClientConfig {
1591 api_key: Some("test_key".to_string()),
1592 api_secret: Some("test_secret".to_string()),
1593 account_id: Some(AccountId::from("BITMEX-319111")),
1594 base_url_http: Some("http://127.0.0.1:9/api/v1".to_string()),
1595 base_url_ws: Some("ws://127.0.0.1:9/realtime".to_string()),
1596 ..Default::default()
1597 };
1598
1599 let client = BitmexExecutionClient::new(core, config).unwrap();
1600
1601 assert_eq!(client.account_id(), AccountId::from("BITMEX-319111"));
1602 }
1603
1604 #[rstest]
1605 fn test_apply_account_id_updates_core_emitter_and_websocket_client() {
1606 let (mut client, _) = test_execution_client();
1607 let account_id = AccountId::from("BITMEX-319111");
1608
1609 client.apply_account_id(account_id);
1610
1611 assert_eq!(client.account_id(), account_id);
1612 assert_eq!(client.emitter.account_id(), account_id);
1613 assert_eq!(client.ws_client.account_id(), account_id);
1614 }
1615
1616 #[rstest]
1617 fn test_dispatch_tracked_fill_uses_bitmex_account_id() {
1618 let (emitter, mut rx) = make_emitter();
1619 let state = WsDispatchState::default();
1620 let account_id = AccountId::from("BITMEX-1234567");
1621 let client_order_id = ClientOrderId::from("mm_bitmex_2b/oemUeQ4CAJZgP3fjHsB");
1622 state.order_identities.insert(
1623 client_order_id,
1624 OrderIdentity {
1625 instrument_id: InstrumentId::from("XBTUSD.BITMEX"),
1626 strategy_id: StrategyId::from("S-001"),
1627 order_side: OrderSide::Sell,
1628 order_type: OrderType::Limit,
1629 },
1630 );
1631
1632 dispatch_execution_fixture(&state, &emitter, account_id);
1633
1634 let events = drain_order_events(&mut rx);
1635 assert_eq!(events.len(), 2);
1636 match &events[..] {
1637 [
1638 OrderEventAny::Accepted(accepted),
1639 OrderEventAny::Filled(filled),
1640 ] => {
1641 assert_eq!(accepted.account_id, account_id);
1642 assert_eq!(filled.account_id, account_id);
1643 }
1644 events => panic!("expected accepted and filled events, was {events:?}"),
1645 }
1646 }
1647
1648 #[rstest]
1649 fn test_dispatch_untracked_fill_report_uses_bitmex_account_id() {
1650 let (emitter, mut rx) = make_emitter();
1651 let state = WsDispatchState::default();
1652 let account_id = AccountId::from("BITMEX-1234567");
1653
1654 dispatch_execution_fixture(&state, &emitter, account_id);
1655
1656 match rx.try_recv().unwrap() {
1657 ExecutionEvent::Report(ExecutionReport::Fill(report)) => {
1658 assert_eq!(report.account_id, account_id);
1659 }
1660 event => panic!("expected fill report, was {event:?}"),
1661 }
1662 assert!(rx.try_recv().is_err());
1663 }
1664
1665 #[rstest]
1666 fn test_dispatch_wallet_account_state_uses_bitmex_account_id() {
1667 let (emitter, mut rx) = make_emitter();
1668 let state = WsDispatchState::default();
1669 let account_id = AccountId::from("BITMEX-1234567");
1670 let wallet_msg: BitmexWalletMsg =
1671 serde_json::from_str(&load_test_json("ws_wallet.json")).unwrap();
1672 let mut instruments_by_symbol = AHashMap::new();
1673 let mut order_type_cache = AHashMap::new();
1674 let mut order_symbol_cache = AHashMap::new();
1675
1676 dispatch::dispatch_ws_message(
1677 UnixNanos::default(),
1678 BitmexWsMessage::Table(BitmexTableMessage::Wallet {
1679 action: BitmexAction::Insert,
1680 data: vec![wallet_msg],
1681 }),
1682 &emitter,
1683 &state,
1684 &mut instruments_by_symbol,
1685 &mut order_type_cache,
1686 &mut order_symbol_cache,
1687 account_id,
1688 );
1689
1690 match rx.try_recv().unwrap() {
1691 ExecutionEvent::Account(state) => {
1692 assert_eq!(state.account_id, account_id);
1693 }
1694 event => panic!("expected account state, was {event:?}"),
1695 }
1696 assert!(rx.try_recv().is_err());
1697 }
1698
1699 #[rstest]
1700 fn test_bitmex_api_error_is_definitive_modify_rejection() {
1701 let err = bitmex_api_error();
1702
1703 assert!(is_definitive_bitmex_modify_rejection(&err));
1704 }
1705
1706 #[rstest]
1707 fn test_parsed_submit_reject_is_definitive_submit_rejection() {
1708 let err = anyhow::anyhow!("Order rejected: Price is invalid");
1709
1710 assert!(is_definitive_bitmex_submit_rejection(&err));
1711 assert!(!is_definitive_bitmex_modify_rejection(&err));
1712 }
1713
1714 #[rstest]
1715 fn test_broadcast_submit_refusal_is_definitive_submit_rejection() {
1716 let err =
1717 anyhow::anyhow!("{DEFINITIVE_SUBMIT_REJECTION}: All submit requests were refused");
1718
1719 assert!(is_definitive_bitmex_submit_rejection(&err));
1720 assert!(!is_definitive_bitmex_modify_rejection(&err));
1721 }
1722
1723 #[rstest]
1724 fn test_duplicate_clordid_is_ambiguous_submit_failure() {
1725 let err = anyhow::Error::new(BitmexHttpError::BitmexError {
1726 error_name: "HTTPError".to_string(),
1727 message: "Duplicate clOrdID".to_string(),
1728 });
1729
1730 assert!(is_bitmex_duplicate_clordid_submit_failure(&err));
1731 assert!(!is_definitive_bitmex_submit_rejection(&err));
1732 }
1733
1734 #[rstest]
1735 fn test_parsed_modify_reject_is_definitive_modify_rejection() {
1736 let err = anyhow::anyhow!("Order modification rejected: Price is invalid");
1737
1738 assert!(is_definitive_bitmex_modify_rejection(&err));
1739 assert!(!is_definitive_bitmex_submit_rejection(&err));
1740 }
1741
1742 #[rstest]
1743 fn test_network_error_is_ambiguous_command_failure() {
1744 let err = anyhow::Error::new(BitmexHttpError::NetworkError("timeout".to_string()));
1745
1746 assert!(!is_definitive_bitmex_submit_rejection(&err));
1747 assert!(!is_definitive_bitmex_modify_rejection(&err));
1748 }
1749
1750 #[rstest]
1751 fn test_canceled_request_is_ambiguous_command_failure() {
1752 let err = anyhow::Error::new(BitmexHttpError::Canceled("shutdown".to_string()));
1753
1754 assert!(!is_definitive_bitmex_submit_rejection(&err));
1755 assert!(!is_definitive_bitmex_modify_rejection(&err));
1756 }
1757
1758 #[rstest]
1759 fn test_unstructured_http_status_is_ambiguous_command_failure() {
1760 let err = anyhow::Error::new(BitmexHttpError::UnexpectedStatus {
1761 status: StatusCode::BAD_GATEWAY,
1762 body: "bad gateway".to_string(),
1763 });
1764
1765 assert!(!is_definitive_bitmex_submit_rejection(&err));
1766 assert!(!is_definitive_bitmex_modify_rejection(&err));
1767 }
1768
1769 #[rstest]
1770 fn test_validate_order_for_bitmex_submit_requires_peg_type_for_offset() {
1771 let order = limit_order();
1772 let err = validate_order_for_bitmex_submit(&order, None, Some(1.0)).unwrap_err();
1773
1774 assert!(err.to_string().contains("`peg_offset_value` requires"));
1775 }
1776
1777 #[rstest]
1778 fn test_validate_order_for_bitmex_submit_rejects_pegged_market_order() {
1779 let order = market_order();
1780 let err = validate_order_for_bitmex_submit(&order, Some(BitmexPegPriceType::LastPeg), None)
1781 .unwrap_err();
1782
1783 assert!(err.to_string().contains("Pegged orders only supported"));
1784 }
1785
1786 #[rstest]
1787 fn test_submit_order_invalid_peg_params_emits_denied_without_submitted() {
1788 let (mut client, cache) = test_execution_client();
1789 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1790 client.emitter.set_sender(tx);
1791
1792 let order = limit_order_with_id(ClientOrderId::from("O-INVALID-PEG"));
1793 cache
1794 .borrow_mut()
1795 .add_order(order.clone(), None, Some(*BITMEX_CLIENT_ID), false)
1796 .unwrap();
1797
1798 let mut params = Params::new();
1799 params.insert("peg_price_type".to_string(), serde_json::json!("BadPeg"));
1800
1801 client
1802 .submit_order(submit_command(&order, Some(params)))
1803 .unwrap();
1804
1805 let events = drain_order_events(&mut rx);
1806 assert_eq!(events.len(), 1);
1807 match &events[0] {
1808 OrderEventAny::Denied(denied) => {
1809 assert_eq!(denied.client_order_id, order.client_order_id());
1810 assert_eq!(denied.reason.to_string(), "Invalid peg_price_type: BadPeg");
1811 }
1812 event => panic!("expected OrderDenied event, was {event:?}"),
1813 }
1814 assert!(
1815 !client
1816 .ws_dispatch_state
1817 .order_identities
1818 .contains_key(&order.client_order_id())
1819 );
1820 }
1821
1822 #[rstest]
1823 fn test_submit_order_gtd_time_in_force_emits_denied_without_submitted() {
1824 let (mut client, cache) = test_execution_client();
1825 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1826 client.emitter.set_sender(tx);
1827
1828 let mut builder = OrderTestBuilder::new(OrderType::Limit);
1829 let order = builder
1830 .instrument_id(InstrumentId::from("XBTUSD.BITMEX"))
1831 .client_order_id(ClientOrderId::from("O-GTD"))
1832 .side(OrderSide::Buy)
1833 .quantity(Quantity::from("1"))
1834 .price(Price::from("100.0"))
1835 .time_in_force(TimeInForce::Gtd)
1836 .expire_time(UnixNanos::from(1_000_000_000_u64))
1837 .build();
1838 cache
1839 .borrow_mut()
1840 .add_order(order.clone(), None, Some(*BITMEX_CLIENT_ID), false)
1841 .unwrap();
1842
1843 client.submit_order(submit_command(&order, None)).unwrap();
1844
1845 let events = drain_order_events(&mut rx);
1846 assert_eq!(events.len(), 1);
1847 match &events[0] {
1848 OrderEventAny::Denied(denied) => {
1849 assert_eq!(denied.client_order_id, order.client_order_id());
1850 assert!(
1851 denied
1852 .reason
1853 .to_string()
1854 .contains("GTD time in force is not supported")
1855 );
1856 }
1857 event => panic!("expected OrderDenied event, was {event:?}"),
1858 }
1859 assert!(
1860 !client
1861 .ws_dispatch_state
1862 .order_identities
1863 .contains_key(&order.client_order_id())
1864 );
1865 }
1866
1867 #[rstest]
1868 fn test_submit_failure_definitive_refusal_removes_identity_and_emits_rejected() {
1869 let (emitter, mut rx) = make_emitter();
1870 let ws_dispatch_state = Arc::new(WsDispatchState::default());
1871 let order = limit_order_with_id(ClientOrderId::from("O-SUBMIT-REJECTED"));
1872 ws_dispatch_state
1873 .order_identities
1874 .insert(order.client_order_id(), order_identity(&order));
1875
1876 let err = anyhow::anyhow!(
1877 "{DEFINITIVE_SUBMIT_REJECTION}: All submit requests were refused by BitMEX"
1878 );
1879
1880 handle_submit_failure(&SubmitFailure {
1881 err: &err,
1882 ws_dispatch_state: &ws_dispatch_state,
1883 emitter: &emitter,
1884 clock: get_atomic_clock_realtime(),
1885 strategy_id: order.strategy_id(),
1886 instrument_id: order.instrument_id(),
1887 client_order_id: order.client_order_id(),
1888 post_only: false,
1889 });
1890
1891 assert!(
1892 !ws_dispatch_state
1893 .order_identities
1894 .contains_key(&order.client_order_id())
1895 );
1896
1897 let events = drain_order_events(&mut rx);
1898 assert_eq!(events.len(), 1);
1899 match &events[0] {
1900 OrderEventAny::Rejected(rejected) => {
1901 assert_eq!(rejected.client_order_id, order.client_order_id());
1902 assert_eq!(
1903 rejected.reason.to_string(),
1904 "submit-order-error: All submit requests were refused by BitMEX"
1905 );
1906 assert!(!rejected.due_post_only);
1907 }
1908 event => panic!("expected OrderRejected event, was {event:?}"),
1909 }
1910 }
1911
1912 #[rstest]
1913 fn test_submit_failure_duplicate_clordid_keeps_identity_and_emits_no_rejection() {
1914 let (emitter, mut rx) = make_emitter();
1915 let ws_dispatch_state = Arc::new(WsDispatchState::default());
1916 let order = limit_order_with_id(ClientOrderId::from("O-DUPLICATE"));
1917 ws_dispatch_state
1918 .order_identities
1919 .insert(order.client_order_id(), order_identity(&order));
1920 let err = anyhow::Error::new(BitmexHttpError::BitmexError {
1921 error_name: "HTTPError".to_string(),
1922 message: "Duplicate clOrdID".to_string(),
1923 });
1924
1925 handle_submit_failure(&SubmitFailure {
1926 err: &err,
1927 ws_dispatch_state: &ws_dispatch_state,
1928 emitter: &emitter,
1929 clock: get_atomic_clock_realtime(),
1930 strategy_id: order.strategy_id(),
1931 instrument_id: order.instrument_id(),
1932 client_order_id: order.client_order_id(),
1933 post_only: false,
1934 });
1935
1936 assert!(
1937 ws_dispatch_state
1938 .order_identities
1939 .contains_key(&order.client_order_id())
1940 );
1941 assert!(drain_order_events(&mut rx).is_empty());
1942 }
1943
1944 #[rstest]
1945 fn test_submit_failure_network_error_keeps_identity_and_emits_no_rejection() {
1946 let (emitter, mut rx) = make_emitter();
1947 let ws_dispatch_state = Arc::new(WsDispatchState::default());
1948 let order = limit_order_with_id(ClientOrderId::from("O-SUBMIT-NETWORK"));
1949 ws_dispatch_state
1950 .order_identities
1951 .insert(order.client_order_id(), order_identity(&order));
1952 let err = anyhow::Error::new(BitmexHttpError::NetworkError("timeout".to_string()));
1953
1954 handle_submit_failure(&SubmitFailure {
1955 err: &err,
1956 ws_dispatch_state: &ws_dispatch_state,
1957 emitter: &emitter,
1958 clock: get_atomic_clock_realtime(),
1959 strategy_id: order.strategy_id(),
1960 instrument_id: order.instrument_id(),
1961 client_order_id: order.client_order_id(),
1962 post_only: false,
1963 });
1964
1965 assert!(
1966 ws_dispatch_state
1967 .order_identities
1968 .contains_key(&order.client_order_id())
1969 );
1970 assert!(drain_order_events(&mut rx).is_empty());
1971 }
1972
1973 #[rstest]
1974 fn test_modify_failure_definitive_refusal_emits_modify_rejected() {
1975 let (emitter, mut rx) = make_emitter();
1976 let order = limit_order_with_id(ClientOrderId::from("O-MODIFY-REJECTED"));
1977 let venue_order_id = Some(VenueOrderId::from("V-001"));
1978 let err = bitmex_api_error();
1979
1980 handle_modify_failure(&ModifyFailure {
1981 err: &err,
1982 emitter: &emitter,
1983 clock: get_atomic_clock_realtime(),
1984 strategy_id: order.strategy_id(),
1985 instrument_id: order.instrument_id(),
1986 client_order_id: order.client_order_id(),
1987 venue_order_id,
1988 });
1989
1990 let events = drain_order_events(&mut rx);
1991 assert_eq!(events.len(), 1);
1992 match &events[0] {
1993 OrderEventAny::ModifyRejected(rejected) => {
1994 assert_eq!(rejected.client_order_id, order.client_order_id());
1995 assert_eq!(rejected.venue_order_id, venue_order_id);
1996 assert_eq!(
1997 rejected.reason.to_string(),
1998 "modify-order-error: BitMEX error HTTPError: Invalid price"
1999 );
2000 }
2001 event => panic!("expected OrderModifyRejected event, was {event:?}"),
2002 }
2003 }
2004
2005 #[rstest]
2006 fn test_modify_failure_network_error_emits_no_modify_rejected() {
2007 let (emitter, mut rx) = make_emitter();
2008 let order = limit_order_with_id(ClientOrderId::from("O-MODIFY-NETWORK"));
2009 let err = anyhow::Error::new(BitmexHttpError::NetworkError("timeout".to_string()));
2010
2011 handle_modify_failure(&ModifyFailure {
2012 err: &err,
2013 emitter: &emitter,
2014 clock: get_atomic_clock_realtime(),
2015 strategy_id: order.strategy_id(),
2016 instrument_id: order.instrument_id(),
2017 client_order_id: order.client_order_id(),
2018 venue_order_id: None,
2019 });
2020
2021 assert!(drain_order_events(&mut rx).is_empty());
2022 }
2023}