Skip to main content

nautilus_bitmex/
execution.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Live execution client implementation for the BitMEX adapter.
17
18use 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    /// Creates a new [`BitmexExecutionClient`].
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if either the HTTP or WebSocket client fail to construct.
117    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        // Remove completed tasks to prevent unbounded growth
246        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    /// Populates `order_identities` for an order if not already present.
262    ///
263    /// Needed for cancel/modify commands on orders loaded via reconciliation
264    /// (which bypass `submit_order` and therefore have no identity entry).
265    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        // Abort and await loop shutdown so disconnect does not block on sleep/HTTP timeout.
335        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        // Build symbol-keyed instrument map, preferring core cache then HTTP client cache
453        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                    // The WS dispatch handles all lifecycle events for tracked orders.
598                    // Forwarding the HTTP response as a report would cause the ExecEngine
599                    // to generate inferred fills that conflict with real fills from the
600                    // Execution table WS stream.
601                }
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        // Reset cancellation token so HTTP requests succeed after reconnect
704        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        // Start submitter/canceller after WS connection succeeds
715        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        // Disarm DMS before cancelling requests (needs working HTTP)
740        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    // A duplicate clOrdID can mean the original success response was lost
1221    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}