Skip to main content

nautilus_bitmex/
data.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 market data client implementation for the BitMEX adapter.
17
18use std::{
19    future::Future,
20    sync::{
21        Arc,
22        atomic::{AtomicBool, Ordering},
23    },
24};
25
26use ahash::AHashMap;
27use anyhow::Context;
28use futures_util::StreamExt;
29use nautilus_common::{
30    cache::quote::QuoteCache,
31    clients::DataClient,
32    live::{runner::get_data_event_sender, runtime::get_runtime},
33    messages::{
34        DataEvent,
35        data::{
36            BarsResponse, BookResponse, DataResponse, FundingRatesResponse, InstrumentResponse,
37            InstrumentsResponse, RequestBars, RequestBookSnapshot, RequestFundingRates,
38            RequestInstrument, RequestInstruments, RequestTrades, SubscribeBars,
39            SubscribeBookDeltas, SubscribeBookDepth10, SubscribeFundingRates, SubscribeIndexPrices,
40            SubscribeInstrument, SubscribeInstrumentStatus, SubscribeInstruments,
41            SubscribeMarkPrices, SubscribeQuotes, SubscribeTrades, TradesResponse, UnsubscribeBars,
42            UnsubscribeBookDeltas, UnsubscribeBookDepth10, UnsubscribeFundingRates,
43            UnsubscribeIndexPrices, UnsubscribeInstrumentStatus, UnsubscribeMarkPrices,
44            UnsubscribeQuotes, UnsubscribeTrades,
45        },
46    },
47};
48use nautilus_core::{
49    AtomicMap, UnixNanos,
50    datetime::datetime_to_unix_nanos,
51    time::{AtomicTime, get_atomic_clock_realtime},
52};
53use nautilus_model::{
54    data::{Data, InstrumentStatus},
55    enums::{BookType, MarketStatusAction},
56    identifiers::{ClientId, InstrumentId, Venue},
57    instruments::{Instrument, InstrumentAny},
58    types::Price,
59};
60use tokio::{task::JoinHandle, time::Duration};
61use tokio_util::sync::CancellationToken;
62use ustr::Ustr;
63
64use crate::{
65    common::{
66        consts::BITMEX_VENUE,
67        enums::BitmexInstrumentState,
68        parse::{
69            parse_contracts_quantity, parse_instrument_id, parse_optional_datetime_to_unix_nanos,
70        },
71    },
72    config::BitmexDataClientConfig,
73    http::{
74        client::BitmexHttpClient,
75        parse::{InstrumentParseResult, parse_instrument_any},
76    },
77    websocket::{
78        client::BitmexWebSocketClient,
79        enums::{BitmexAction, BitmexBookChannel, BitmexWsTopic},
80        messages::{BitmexQuoteMsg, BitmexTableMessage, BitmexWsMessage},
81        parse::{
82            parse_book_msg_vec, parse_book10_msg_vec, parse_funding_msg, parse_instrument_msg,
83            parse_trade_bin_msg_vec, parse_trade_msg_vec,
84        },
85    },
86};
87
88#[derive(Debug)]
89pub struct BitmexDataClient {
90    client_id: ClientId,
91    clock: &'static AtomicTime,
92    config: BitmexDataClientConfig,
93    http_client: BitmexHttpClient,
94    ws_client: Option<BitmexWebSocketClient>,
95    is_connected: AtomicBool,
96    cancellation_token: CancellationToken,
97    tasks: Vec<JoinHandle<()>>,
98    data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
99    instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
100    book_channels: Arc<AtomicMap<InstrumentId, BitmexBookChannel>>,
101    instrument_refresh_active: bool,
102}
103
104impl BitmexDataClient {
105    /// Creates a new [`BitmexDataClient`] instance.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the HTTP client cannot be constructed.
110    pub fn new(client_id: ClientId, config: BitmexDataClientConfig) -> anyhow::Result<Self> {
111        let clock = get_atomic_clock_realtime();
112        let data_sender = get_data_event_sender();
113
114        let http_client = BitmexHttpClient::new(
115            Some(config.http_base_url()),
116            config.api_key.clone(),
117            config.api_secret.clone(),
118            config.environment,
119            config.http_timeout_secs,
120            config.max_retries,
121            config.retry_delay_initial_ms,
122            config.retry_delay_max_ms,
123            config.recv_window_ms,
124            config.max_requests_per_second,
125            config.max_requests_per_minute,
126            config.proxy_url.clone(),
127        )
128        .context("failed to construct BitMEX HTTP client")?;
129
130        Ok(Self {
131            client_id,
132            clock,
133            config,
134            http_client,
135            ws_client: None,
136            is_connected: AtomicBool::new(false),
137            cancellation_token: CancellationToken::new(),
138            tasks: Vec::new(),
139            data_sender,
140            instruments: Arc::new(AtomicMap::new()),
141            book_channels: Arc::new(AtomicMap::new()),
142            instrument_refresh_active: false,
143        })
144    }
145
146    fn venue(&self) -> Venue {
147        *BITMEX_VENUE
148    }
149
150    fn ws_client(&self) -> anyhow::Result<&BitmexWebSocketClient> {
151        self.ws_client
152            .as_ref()
153            .context("websocket client not initialized; call connect first")
154    }
155
156    fn ws_client_mut(&mut self) -> anyhow::Result<&mut BitmexWebSocketClient> {
157        self.ws_client
158            .as_mut()
159            .context("websocket client not initialized; call connect first")
160    }
161
162    fn send_data(sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>, data: Data) {
163        if let Err(e) = sender.send(DataEvent::Data(data)) {
164            log::error!("Failed to emit data event: {e}");
165        }
166    }
167
168    fn spawn_ws<F>(&self, fut: F, context: &'static str)
169    where
170        F: Future<Output = anyhow::Result<()>> + Send + 'static,
171    {
172        get_runtime().spawn(async move {
173            if let Err(e) = fut.await {
174                log::error!("{context}: {e:?}");
175            }
176        });
177    }
178
179    fn spawn_stream_task(
180        &mut self,
181        stream: impl futures_util::Stream<Item = BitmexWsMessage> + Send + 'static,
182    ) {
183        let data_sender = self.data_sender.clone();
184        let instruments = Arc::clone(&self.instruments);
185        let cancellation = self.cancellation_token.clone();
186        let clock = self.clock;
187
188        let instruments_by_symbol: AHashMap<Ustr, InstrumentAny> = {
189            let guard = instruments.load();
190            guard
191                .values()
192                .map(|inst| (inst.symbol().inner(), inst.clone()))
193                .collect()
194        };
195
196        let handle = get_runtime().spawn(async move {
197            tokio::pin!(stream);
198            let mut quote_cache = QuoteCache::new();
199            let mut insts_by_symbol = instruments_by_symbol;
200
201            loop {
202                tokio::select! {
203                    maybe_msg = stream.next() => {
204                        match maybe_msg {
205                            Some(msg) => Self::handle_ws_message(
206                                clock.get_time_ns(),
207                                msg,
208                                &data_sender,
209                                &instruments,
210                                &mut insts_by_symbol,
211                                &mut quote_cache,
212                            ),
213                            None => {
214                                log::debug!("BitMEX websocket stream ended");
215                                break;
216                            }
217                        }
218                    }
219                    () = cancellation.cancelled() => {
220                        log::debug!("BitMEX websocket stream task cancelled");
221                        break;
222                    }
223                }
224            }
225        });
226
227        self.tasks.push(handle);
228    }
229
230    fn handle_ws_message(
231        ts_init: UnixNanos,
232        message: BitmexWsMessage,
233        sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
234        instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
235        instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
236        quote_cache: &mut QuoteCache,
237    ) {
238        match message {
239            BitmexWsMessage::Table(table_msg) => {
240                match table_msg {
241                    BitmexTableMessage::OrderBookL2 { action, data }
242                    | BitmexTableMessage::OrderBookL2_25 { action, data } => {
243                        if !data.is_empty() {
244                            let parsed =
245                                parse_book_msg_vec(data, action, instruments_by_symbol, ts_init);
246
247                            for d in parsed {
248                                Self::send_data(sender, d);
249                            }
250                        }
251                    }
252                    BitmexTableMessage::OrderBook10 { data, .. } => {
253                        if !data.is_empty() {
254                            let parsed = parse_book10_msg_vec(data, instruments_by_symbol, ts_init);
255                            for d in parsed {
256                                Self::send_data(sender, d);
257                            }
258                        }
259                    }
260                    BitmexTableMessage::Quote { data, .. } => {
261                        handle_quote_messages(
262                            data,
263                            instruments_by_symbol,
264                            quote_cache,
265                            ts_init,
266                            sender,
267                        );
268                    }
269                    BitmexTableMessage::Trade { data, .. } => {
270                        if !data.is_empty() {
271                            let parsed = parse_trade_msg_vec(data, instruments_by_symbol, ts_init);
272                            for d in parsed {
273                                Self::send_data(sender, d);
274                            }
275                        }
276                    }
277                    BitmexTableMessage::TradeBin1m { action, data } => {
278                        if action != BitmexAction::Partial && !data.is_empty() {
279                            let parsed = parse_trade_bin_msg_vec(
280                                data,
281                                &BitmexWsTopic::TradeBin1m,
282                                instruments_by_symbol,
283                                ts_init,
284                            );
285
286                            for d in parsed {
287                                Self::send_data(sender, d);
288                            }
289                        }
290                    }
291                    BitmexTableMessage::TradeBin5m { action, data } => {
292                        if action != BitmexAction::Partial && !data.is_empty() {
293                            let parsed = parse_trade_bin_msg_vec(
294                                data,
295                                &BitmexWsTopic::TradeBin5m,
296                                instruments_by_symbol,
297                                ts_init,
298                            );
299
300                            for d in parsed {
301                                Self::send_data(sender, d);
302                            }
303                        }
304                    }
305                    BitmexTableMessage::TradeBin1h { action, data } => {
306                        if action != BitmexAction::Partial && !data.is_empty() {
307                            let parsed = parse_trade_bin_msg_vec(
308                                data,
309                                &BitmexWsTopic::TradeBin1h,
310                                instruments_by_symbol,
311                                ts_init,
312                            );
313
314                            for d in parsed {
315                                Self::send_data(sender, d);
316                            }
317                        }
318                    }
319                    BitmexTableMessage::TradeBin1d { action, data } => {
320                        if action != BitmexAction::Partial && !data.is_empty() {
321                            let parsed = parse_trade_bin_msg_vec(
322                                data,
323                                &BitmexWsTopic::TradeBin1d,
324                                instruments_by_symbol,
325                                ts_init,
326                            );
327
328                            for d in parsed {
329                                Self::send_data(sender, d);
330                            }
331                        }
332                    }
333                    BitmexTableMessage::Instrument { action, data } => {
334                        Self::handle_instrument_msg(
335                            action,
336                            data,
337                            ts_init,
338                            sender,
339                            instruments,
340                            instruments_by_symbol,
341                        );
342                    }
343                    BitmexTableMessage::Funding { data, .. } => {
344                        for msg in data {
345                            let update = parse_funding_msg(&msg, ts_init);
346                            log::debug!(
347                                "Funding rate update: instrument={}, rate={}",
348                                update.instrument_id,
349                                update.rate,
350                            );
351
352                            if let Err(e) = sender.send(DataEvent::FundingRate(update)) {
353                                log::error!("Failed to emit funding rate event: {e}");
354                            }
355                        }
356                    }
357                    // Ignore execution-only tables on data client
358                    BitmexTableMessage::Order { .. }
359                    | BitmexTableMessage::Execution { .. }
360                    | BitmexTableMessage::Position { .. }
361                    | BitmexTableMessage::Wallet { .. }
362                    | BitmexTableMessage::Margin { .. } => {
363                        log::debug!("Ignoring trading message on data client");
364                    }
365                    _ => {
366                        log::warn!("Unhandled table message type on data client");
367                    }
368                }
369            }
370            BitmexWsMessage::Reconnected => {
371                quote_cache.clear();
372                log::info!("BitMEX websocket reconnected");
373            }
374            BitmexWsMessage::Authenticated => {
375                log::debug!("BitMEX websocket authenticated");
376            }
377        }
378    }
379
380    fn handle_instrument_msg(
381        action: BitmexAction,
382        data: Vec<crate::websocket::messages::BitmexInstrumentMsg>,
383        ts_init: UnixNanos,
384        sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
385        instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
386        instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
387    ) {
388        match action {
389            BitmexAction::Partial | BitmexAction::Insert => {
390                let mut new_instruments = Vec::with_capacity(data.len());
391                let mut temp_cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
392
393                let data_for_prices = data.clone();
394
395                for msg in data {
396                    match msg.try_into() {
397                        Ok(http_inst) => match parse_instrument_any(&http_inst, ts_init) {
398                            InstrumentParseResult::Ok(boxed) => {
399                                let instrument_any = *boxed;
400                                let symbol = instrument_any.symbol().inner();
401                                temp_cache.insert(symbol, instrument_any.clone());
402                                new_instruments.push(instrument_any);
403                            }
404                            InstrumentParseResult::Unsupported { .. }
405                            | InstrumentParseResult::Inactive { .. } => {}
406                            InstrumentParseResult::Failed {
407                                symbol,
408                                instrument_type,
409                                error,
410                            } => {
411                                log::warn!(
412                                    "Failed to parse instrument {symbol} ({instrument_type:?}): {error}"
413                                );
414                            }
415                        },
416                        Err(e) => {
417                            log::debug!("Skipping instrument (missing required fields): {e}");
418                        }
419                    }
420                }
421
422                instruments.rcu(|m| {
423                    for inst in &new_instruments {
424                        m.insert(inst.id(), inst.clone());
425                    }
426                });
427
428                for (symbol, inst) in &temp_cache {
429                    instruments_by_symbol.insert(*symbol, inst.clone());
430                }
431
432                for inst in new_instruments {
433                    if let Err(e) = sender.send(DataEvent::Instrument(inst)) {
434                        log::error!("Failed to send instrument event: {e}");
435                    }
436                }
437
438                for msg in data_for_prices {
439                    for d in parse_instrument_msg(&msg, &temp_cache, ts_init) {
440                        Self::send_data(sender, d);
441                    }
442                }
443            }
444            BitmexAction::Update => {
445                for msg in &data {
446                    if let Some(state_str) = &msg.state
447                        && let Ok(state) = serde_json::from_str::<BitmexInstrumentState>(&format!(
448                            "\"{state_str}\""
449                        ))
450                    {
451                        let instrument_id = parse_instrument_id(msg.symbol);
452                        let action = MarketStatusAction::from(&state);
453                        let is_trading = Some(state == BitmexInstrumentState::Open);
454                        let ts_event = parse_optional_datetime_to_unix_nanos(
455                            &Some(msg.timestamp),
456                            "timestamp",
457                        );
458                        let status = InstrumentStatus::new(
459                            instrument_id,
460                            action,
461                            ts_event,
462                            ts_init,
463                            None,
464                            None,
465                            is_trading,
466                            None,
467                            None,
468                        );
469
470                        if let Err(e) = sender.send(DataEvent::InstrumentStatus(status)) {
471                            log::error!("Failed to send instrument status: {e}");
472                        }
473                    }
474                }
475
476                // Parse mark/index price data
477                for msg in data {
478                    for d in parse_instrument_msg(&msg, instruments_by_symbol, ts_init) {
479                        Self::send_data(sender, d);
480                    }
481                }
482            }
483            BitmexAction::Delete => {
484                log::debug!(
485                    "Received instrument delete action for {} instrument(s)",
486                    data.len(),
487                );
488            }
489        }
490    }
491
492    async fn bootstrap_instruments(&self) -> anyhow::Result<Vec<InstrumentAny>> {
493        let http = self.http_client.clone();
494        let mut instruments = http
495            .request_instruments(self.config.active_only)
496            .await
497            .context("failed to request BitMEX instruments")?;
498
499        instruments.sort_by_key(|instrument| instrument.id());
500
501        self.instruments.rcu(|m| {
502            m.clear();
503            for instrument in &instruments {
504                m.insert(instrument.id(), instrument.clone());
505            }
506        });
507
508        self.http_client.cache_instruments(&instruments);
509
510        if let Some(ws) = &self.ws_client {
511            ws.cache_instruments(&instruments);
512        }
513
514        for instrument in &instruments {
515            if let Err(e) = self
516                .data_sender
517                .send(DataEvent::Instrument(instrument.clone()))
518            {
519                log::warn!(
520                    "Failed to send instrument event for {}: {e}",
521                    instrument.id()
522                );
523            }
524        }
525
526        Ok(instruments)
527    }
528
529    fn is_connected(&self) -> bool {
530        self.is_connected.load(Ordering::Relaxed)
531    }
532
533    fn is_disconnected(&self) -> bool {
534        !self.is_connected()
535    }
536
537    fn maybe_spawn_instrument_refresh(&mut self) {
538        let Some(minutes) = self.config.update_instruments_interval_mins else {
539            return;
540        };
541
542        if minutes == 0 || self.instrument_refresh_active {
543            return;
544        }
545
546        let interval_secs = minutes.saturating_mul(60);
547        if interval_secs == 0 {
548            return;
549        }
550
551        let interval = Duration::from_secs(interval_secs);
552        let cancellation = self.cancellation_token.clone();
553        let instruments_cache = Arc::clone(&self.instruments);
554        let active_only = self.config.active_only;
555        let client_id = self.client_id;
556        let http_client = self.http_client.clone();
557
558        let handle = get_runtime().spawn(async move {
559            let http_client = http_client;
560
561            loop {
562                let sleep = tokio::time::sleep(interval);
563                tokio::pin!(sleep);
564                tokio::select! {
565                    () = cancellation.cancelled() => {
566                        log::debug!("BitMEX instrument refresh task cancelled");
567                        break;
568                    }
569                    () = &mut sleep => {
570                        match http_client.request_instruments(active_only).await {
571                            Ok(mut instruments) => {
572                                instruments.sort_by_key(|instrument| instrument.id());
573
574                                instruments_cache.rcu(|m| {
575                                    m.clear();
576                                    for instrument in &instruments {
577                                        m.insert(instrument.id(), instrument.clone());
578                                    }
579                                });
580
581                                http_client.cache_instruments(&instruments);
582
583                                log::debug!("BitMEX instruments refreshed: client_id={client_id}");
584                            }
585                            Err(e) => {
586                                log::warn!("Failed to refresh BitMEX instruments: client_id={client_id}, error={e:?}");
587                            }
588                        }
589                    }
590                }
591            }
592        });
593
594        self.tasks.push(handle);
595        self.instrument_refresh_active = true;
596    }
597}
598
599#[async_trait::async_trait(?Send)]
600impl DataClient for BitmexDataClient {
601    fn client_id(&self) -> ClientId {
602        self.client_id
603    }
604
605    fn venue(&self) -> Option<Venue> {
606        Some(self.venue())
607    }
608
609    fn start(&mut self) -> anyhow::Result<()> {
610        log::info!(
611            "Starting BitMEX data client: client_id={}, environment={}, proxy_url={:?}",
612            self.client_id,
613            self.config.environment,
614            self.config.proxy_url,
615        );
616        Ok(())
617    }
618
619    fn stop(&mut self) -> anyhow::Result<()> {
620        log::info!("Stopping BitMEX data client {id}", id = self.client_id);
621        self.cancellation_token.cancel();
622        self.is_connected.store(false, Ordering::Relaxed);
623        self.instrument_refresh_active = false;
624        Ok(())
625    }
626
627    fn reset(&mut self) -> anyhow::Result<()> {
628        log::debug!("Resetting BitMEX data client {id}", id = self.client_id);
629        self.is_connected.store(false, Ordering::Relaxed);
630        self.cancellation_token = CancellationToken::new();
631        self.tasks.clear();
632        self.book_channels.store(AHashMap::new());
633        self.instrument_refresh_active = false;
634        Ok(())
635    }
636
637    fn dispose(&mut self) -> anyhow::Result<()> {
638        self.stop()
639    }
640
641    async fn connect(&mut self) -> anyhow::Result<()> {
642        if self.is_connected() {
643            return Ok(());
644        }
645
646        if self.ws_client.is_none() {
647            let ws = BitmexWebSocketClient::new_with_env(
648                Some(self.config.ws_url()),
649                self.config.api_key.clone(),
650                self.config.api_secret.clone(),
651                None,
652                self.config.heartbeat_interval_secs.unwrap_or(5),
653                self.config.environment,
654                self.config.transport_backend,
655                self.config.proxy_url.clone(),
656            )
657            .context("failed to construct BitMEX websocket client")?;
658            self.ws_client = Some(ws);
659        }
660
661        self.bootstrap_instruments().await?;
662
663        let ws = self.ws_client_mut()?;
664        ws.connect()
665            .await
666            .context("failed to connect BitMEX websocket")?;
667        ws.wait_until_active(10.0)
668            .await
669            .context("BitMEX websocket did not become active")?;
670
671        let stream = ws.stream();
672        self.spawn_stream_task(stream);
673        self.maybe_spawn_instrument_refresh();
674
675        self.is_connected.store(true, Ordering::Relaxed);
676        log::info!("Connected");
677        Ok(())
678    }
679
680    async fn disconnect(&mut self) -> anyhow::Result<()> {
681        if self.is_disconnected() {
682            return Ok(());
683        }
684
685        self.cancellation_token.cancel();
686
687        if let Some(ws) = self.ws_client.as_mut()
688            && let Err(e) = ws.close().await
689        {
690            log::warn!("Error while closing BitMEX websocket: {e:?}");
691        }
692
693        for handle in self.tasks.drain(..) {
694            if let Err(e) = handle.await {
695                log::error!("Error joining websocket task: {e:?}");
696            }
697        }
698
699        self.cancellation_token = CancellationToken::new();
700        self.is_connected.store(false, Ordering::Relaxed);
701        self.book_channels.store(AHashMap::new());
702        self.instrument_refresh_active = false;
703
704        log::info!("Disconnected");
705        Ok(())
706    }
707
708    fn is_connected(&self) -> bool {
709        self.is_connected()
710    }
711
712    fn is_disconnected(&self) -> bool {
713        self.is_disconnected()
714    }
715
716    fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
717        let ws = self.ws_client()?.clone();
718
719        self.spawn_ws(
720            async move {
721                ws.subscribe_instruments()
722                    .await
723                    .map_err(|e| anyhow::anyhow!(e))
724            },
725            "BitMEX instruments subscription",
726        );
727        Ok(())
728    }
729
730    fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
731        let instrument_id = cmd.instrument_id;
732
733        if let Some(instrument) = self.instruments.load().get(&instrument_id).cloned() {
734            if let Err(e) = self.data_sender.send(DataEvent::Instrument(instrument)) {
735                log::error!("Failed to send instrument event for {instrument_id}: {e}");
736            }
737            return Ok(());
738        }
739
740        log::warn!("Instrument {instrument_id} not found in BitMEX cache");
741
742        let ws = self.ws_client()?.clone();
743        self.spawn_ws(
744            async move {
745                ws.subscribe_instrument(instrument_id)
746                    .await
747                    .map_err(|e| anyhow::anyhow!(e))
748            },
749            "BitMEX instrument subscription",
750        );
751
752        Ok(())
753    }
754
755    fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
756        if cmd.book_type != BookType::L2_MBP {
757            anyhow::bail!("BitMEX only supports L2_MBP order book deltas");
758        }
759
760        let instrument_id = cmd.instrument_id;
761        let depth = cmd.depth.map_or(0, |d| d.get());
762        let channel = if depth > 0 && depth <= 25 {
763            if depth != 25 {
764                log::debug!(
765                    "BitMEX only supports depth 25 for L2 deltas, using L2_25 for requested depth {depth}"
766                );
767            }
768            BitmexBookChannel::OrderBookL2_25
769        } else {
770            BitmexBookChannel::OrderBookL2
771        };
772
773        let ws = self.ws_client()?.clone();
774        let book_channels = Arc::clone(&self.book_channels);
775
776        self.spawn_ws(
777            async move {
778                match channel {
779                    BitmexBookChannel::OrderBookL2 => ws
780                        .subscribe_book(instrument_id)
781                        .await
782                        .map_err(|e| anyhow::anyhow!(e))?,
783                    BitmexBookChannel::OrderBookL2_25 => ws
784                        .subscribe_book_25(instrument_id)
785                        .await
786                        .map_err(|e| anyhow::anyhow!(e))?,
787                    BitmexBookChannel::OrderBook10 => unreachable!(),
788                }
789                book_channels.insert(instrument_id, channel);
790                Ok(())
791            },
792            "BitMEX book delta subscription",
793        );
794
795        Ok(())
796    }
797
798    fn subscribe_book_depth10(&mut self, cmd: SubscribeBookDepth10) -> anyhow::Result<()> {
799        let instrument_id = cmd.instrument_id;
800        let ws = self.ws_client()?.clone();
801        let book_channels = Arc::clone(&self.book_channels);
802
803        self.spawn_ws(
804            async move {
805                ws.subscribe_book_depth10(instrument_id)
806                    .await
807                    .map_err(|e| anyhow::anyhow!(e))?;
808                book_channels.insert(instrument_id, BitmexBookChannel::OrderBook10);
809                Ok(())
810            },
811            "BitMEX book depth10 subscription",
812        );
813        Ok(())
814    }
815
816    fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
817        let instrument_id = cmd.instrument_id;
818        let ws = self.ws_client()?.clone();
819
820        self.spawn_ws(
821            async move {
822                ws.subscribe_quotes(instrument_id)
823                    .await
824                    .map_err(|e| anyhow::anyhow!(e))
825            },
826            "BitMEX quote subscription",
827        );
828        Ok(())
829    }
830
831    fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
832        let instrument_id = cmd.instrument_id;
833        let ws = self.ws_client()?.clone();
834
835        self.spawn_ws(
836            async move {
837                ws.subscribe_trades(instrument_id)
838                    .await
839                    .map_err(|e| anyhow::anyhow!(e))
840            },
841            "BitMEX trade subscription",
842        );
843        Ok(())
844    }
845
846    fn subscribe_mark_prices(&mut self, cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
847        let instrument_id = cmd.instrument_id;
848        let ws = self.ws_client()?.clone();
849
850        self.spawn_ws(
851            async move {
852                ws.subscribe_mark_prices(instrument_id)
853                    .await
854                    .map_err(|e| anyhow::anyhow!(e))
855            },
856            "BitMEX mark price subscription",
857        );
858        Ok(())
859    }
860
861    fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
862        let instrument_id = cmd.instrument_id;
863        let ws = self.ws_client()?.clone();
864
865        self.spawn_ws(
866            async move {
867                ws.subscribe_index_prices(instrument_id)
868                    .await
869                    .map_err(|e| anyhow::anyhow!(e))
870            },
871            "BitMEX index price subscription",
872        );
873        Ok(())
874    }
875
876    fn subscribe_funding_rates(&mut self, cmd: SubscribeFundingRates) -> anyhow::Result<()> {
877        let instrument_id = cmd.instrument_id;
878        let ws = self.ws_client()?.clone();
879
880        self.spawn_ws(
881            async move {
882                ws.subscribe_funding_rates(instrument_id)
883                    .await
884                    .map_err(|e| anyhow::anyhow!(e))
885            },
886            "BitMEX funding rate subscription",
887        );
888        Ok(())
889    }
890
891    fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
892        let bar_type = cmd.bar_type;
893        let ws = self.ws_client()?.clone();
894
895        self.spawn_ws(
896            async move {
897                ws.subscribe_bars(bar_type)
898                    .await
899                    .map_err(|e| anyhow::anyhow!(e))
900            },
901            "BitMEX bar subscription",
902        );
903        Ok(())
904    }
905
906    fn subscribe_instrument_status(
907        &mut self,
908        cmd: SubscribeInstrumentStatus,
909    ) -> anyhow::Result<()> {
910        let instrument_id = cmd.instrument_id;
911        let ws = self.ws_client()?.clone();
912
913        self.spawn_ws(
914            async move {
915                ws.subscribe_instrument(instrument_id)
916                    .await
917                    .map_err(|e| anyhow::anyhow!(e))
918            },
919            "BitMEX instrument status subscription",
920        );
921        Ok(())
922    }
923
924    fn unsubscribe_instrument_status(
925        &mut self,
926        cmd: &UnsubscribeInstrumentStatus,
927    ) -> anyhow::Result<()> {
928        let instrument_id = cmd.instrument_id;
929        let ws = self.ws_client()?.clone();
930
931        self.spawn_ws(
932            async move {
933                ws.unsubscribe_instrument(instrument_id)
934                    .await
935                    .map_err(|e| anyhow::anyhow!(e))
936            },
937            "BitMEX instrument status unsubscribe",
938        );
939        Ok(())
940    }
941
942    fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
943        let instrument_id = cmd.instrument_id;
944        let ws = self.ws_client()?.clone();
945        let book_channels = Arc::clone(&self.book_channels);
946
947        self.spawn_ws(
948            async move {
949                let channel = book_channels.load().get(&instrument_id).copied();
950                book_channels.remove(&instrument_id);
951
952                match channel {
953                    Some(BitmexBookChannel::OrderBookL2) => ws
954                        .unsubscribe_book(instrument_id)
955                        .await
956                        .map_err(|e| anyhow::anyhow!(e))?,
957                    Some(BitmexBookChannel::OrderBookL2_25) => ws
958                        .unsubscribe_book_25(instrument_id)
959                        .await
960                        .map_err(|e| anyhow::anyhow!(e))?,
961                    Some(BitmexBookChannel::OrderBook10) => ws
962                        .unsubscribe_book_depth10(instrument_id)
963                        .await
964                        .map_err(|e| anyhow::anyhow!(e))?,
965                    None => ws
966                        .unsubscribe_book(instrument_id)
967                        .await
968                        .map_err(|e| anyhow::anyhow!(e))?,
969                }
970                Ok(())
971            },
972            "BitMEX book delta unsubscribe",
973        );
974        Ok(())
975    }
976
977    fn unsubscribe_book_depth10(&mut self, cmd: &UnsubscribeBookDepth10) -> anyhow::Result<()> {
978        let instrument_id = cmd.instrument_id;
979        let ws = self.ws_client()?.clone();
980        let book_channels = Arc::clone(&self.book_channels);
981
982        self.spawn_ws(
983            async move {
984                book_channels.remove(&instrument_id);
985                ws.unsubscribe_book_depth10(instrument_id)
986                    .await
987                    .map_err(|e| anyhow::anyhow!(e))
988            },
989            "BitMEX book depth10 unsubscribe",
990        );
991        Ok(())
992    }
993
994    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
995        let instrument_id = cmd.instrument_id;
996        let ws = self.ws_client()?.clone();
997
998        self.spawn_ws(
999            async move {
1000                ws.unsubscribe_quotes(instrument_id)
1001                    .await
1002                    .map_err(|e| anyhow::anyhow!(e))
1003            },
1004            "BitMEX quote unsubscribe",
1005        );
1006        Ok(())
1007    }
1008
1009    fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1010        let instrument_id = cmd.instrument_id;
1011        let ws = self.ws_client()?.clone();
1012
1013        self.spawn_ws(
1014            async move {
1015                ws.unsubscribe_trades(instrument_id)
1016                    .await
1017                    .map_err(|e| anyhow::anyhow!(e))
1018            },
1019            "BitMEX trade unsubscribe",
1020        );
1021        Ok(())
1022    }
1023
1024    fn unsubscribe_mark_prices(&mut self, cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
1025        let ws = self.ws_client()?.clone();
1026        let instrument_id = cmd.instrument_id;
1027
1028        self.spawn_ws(
1029            async move {
1030                ws.unsubscribe_mark_prices(instrument_id)
1031                    .await
1032                    .map_err(|e| anyhow::anyhow!(e))
1033            },
1034            "BitMEX mark price unsubscribe",
1035        );
1036        Ok(())
1037    }
1038
1039    fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
1040        let ws = self.ws_client()?.clone();
1041        let instrument_id = cmd.instrument_id;
1042
1043        self.spawn_ws(
1044            async move {
1045                ws.unsubscribe_index_prices(instrument_id)
1046                    .await
1047                    .map_err(|e| anyhow::anyhow!(e))
1048            },
1049            "BitMEX index price unsubscribe",
1050        );
1051        Ok(())
1052    }
1053
1054    fn unsubscribe_funding_rates(&mut self, cmd: &UnsubscribeFundingRates) -> anyhow::Result<()> {
1055        let ws = self.ws_client()?.clone();
1056        let instrument_id = cmd.instrument_id;
1057
1058        self.spawn_ws(
1059            async move {
1060                ws.unsubscribe_funding_rates(instrument_id)
1061                    .await
1062                    .map_err(|e| anyhow::anyhow!(e))
1063            },
1064            "BitMEX funding rate unsubscribe",
1065        );
1066        Ok(())
1067    }
1068
1069    fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1070        let bar_type = cmd.bar_type;
1071        let ws = self.ws_client()?.clone();
1072
1073        self.spawn_ws(
1074            async move {
1075                ws.unsubscribe_bars(bar_type)
1076                    .await
1077                    .map_err(|e| anyhow::anyhow!(e))
1078            },
1079            "BitMEX bar unsubscribe",
1080        );
1081        Ok(())
1082    }
1083
1084    fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
1085        if let Some(req_venue) = request.venue
1086            && req_venue != self.venue()
1087        {
1088            log::warn!("Ignoring mismatched venue in instruments request: {req_venue}");
1089        }
1090        let venue = self.venue();
1091
1092        let http = self.http_client.clone();
1093        let instruments_cache = Arc::clone(&self.instruments);
1094        let sender = self.data_sender.clone();
1095        let request_id = request.request_id;
1096        let client_id = request.client_id.unwrap_or(self.client_id);
1097        let params = request.params;
1098        let start_nanos = datetime_to_unix_nanos(request.start);
1099        let end_nanos = datetime_to_unix_nanos(request.end);
1100        let clock = self.clock;
1101        let active_only = self.config.active_only;
1102
1103        get_runtime().spawn(async move {
1104            let http_client = http;
1105            match http_client
1106                .request_instruments(active_only)
1107                .await
1108                .context("failed to request instruments from BitMEX")
1109            {
1110                Ok(instruments) => {
1111                    instruments_cache.rcu(|m| {
1112                        m.clear();
1113                        for instrument in &instruments {
1114                            m.insert(instrument.id(), instrument.clone());
1115                        }
1116                    });
1117                    http_client.cache_instruments(&instruments);
1118
1119                    let response = DataResponse::Instruments(InstrumentsResponse::new(
1120                        request_id,
1121                        client_id,
1122                        venue,
1123                        instruments,
1124                        start_nanos,
1125                        end_nanos,
1126                        clock.get_time_ns(),
1127                        params,
1128                    ));
1129
1130                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1131                        log::error!("Failed to send instruments response: {e}");
1132                    }
1133                }
1134                Err(e) => log::error!("Instrument request failed: {e:?}"),
1135            }
1136        });
1137
1138        Ok(())
1139    }
1140
1141    fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
1142        let http_client = self.http_client.clone();
1143        let instruments_cache = Arc::clone(&self.instruments);
1144        let sender = self.data_sender.clone();
1145        let instrument_id = request.instrument_id;
1146        let request_id = request.request_id;
1147        let client_id = request.client_id.unwrap_or(self.client_id);
1148        let start = request.start;
1149        let end = request.end;
1150        let params = request.params;
1151        let clock = self.clock;
1152
1153        get_runtime().spawn(async move {
1154            match http_client
1155                .request_instrument(instrument_id)
1156                .await
1157                .context("failed to request instrument from BitMEX")
1158            {
1159                Ok(Some(instrument)) => {
1160                    http_client.cache_instrument(instrument.clone());
1161                    instruments_cache.insert(instrument.id(), instrument.clone());
1162
1163                    let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1164                        request_id,
1165                        client_id,
1166                        instrument.id(),
1167                        instrument,
1168                        datetime_to_unix_nanos(start),
1169                        datetime_to_unix_nanos(end),
1170                        clock.get_time_ns(),
1171                        params,
1172                    )));
1173
1174                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1175                        log::error!("Failed to send instrument response: {e}");
1176                    }
1177                }
1178                Ok(None) => log::warn!("BitMEX instrument {instrument_id} not found"),
1179                Err(e) => log::error!("Instrument request failed: {e:?}"),
1180            }
1181        });
1182
1183        Ok(())
1184    }
1185
1186    fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
1187        let http = self.http_client.clone();
1188        let sender = self.data_sender.clone();
1189        let instrument_id = request.instrument_id;
1190        let depth = request.depth.map(|n| n.get().min(u32::MAX as usize) as u32);
1191        let request_id = request.request_id;
1192        let client_id = request.client_id.unwrap_or(self.client_id);
1193        let params = request.params;
1194        let clock = self.clock;
1195
1196        get_runtime().spawn(async move {
1197            match http
1198                .request_book_snapshot(instrument_id, depth)
1199                .await
1200                .context("failed to request book snapshot from BitMEX")
1201            {
1202                Ok(book) => {
1203                    let response = DataResponse::Book(BookResponse::new(
1204                        request_id,
1205                        client_id,
1206                        instrument_id,
1207                        book,
1208                        None,
1209                        None,
1210                        clock.get_time_ns(),
1211                        params,
1212                    ));
1213
1214                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1215                        log::error!("Failed to send book snapshot response: {e}");
1216                    }
1217                }
1218                Err(e) => log::error!("Book snapshot request failed: {e:?}"),
1219            }
1220        });
1221
1222        Ok(())
1223    }
1224
1225    fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
1226        let http = self.http_client.clone();
1227        let sender = self.data_sender.clone();
1228        let instrument_id = request.instrument_id;
1229        let start = request.start;
1230        let end = request.end;
1231        let limit = request.limit.map(|n| n.get() as u32);
1232        let request_id = request.request_id;
1233        let client_id = request.client_id.unwrap_or(self.client_id);
1234        let params = request.params;
1235        let clock = self.clock;
1236        let start_nanos = datetime_to_unix_nanos(start);
1237        let end_nanos = datetime_to_unix_nanos(end);
1238
1239        get_runtime().spawn(async move {
1240            match http
1241                .request_trades(instrument_id, start, end, limit)
1242                .await
1243                .context("failed to request trades from BitMEX")
1244            {
1245                Ok(trades) => {
1246                    let response = DataResponse::Trades(TradesResponse::new(
1247                        request_id,
1248                        client_id,
1249                        instrument_id,
1250                        trades,
1251                        start_nanos,
1252                        end_nanos,
1253                        clock.get_time_ns(),
1254                        params,
1255                    ));
1256
1257                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1258                        log::error!("Failed to send trades response: {e}");
1259                    }
1260                }
1261                Err(e) => log::error!("Trade request failed: {e:?}"),
1262            }
1263        });
1264
1265        Ok(())
1266    }
1267
1268    fn request_funding_rates(&self, request: RequestFundingRates) -> anyhow::Result<()> {
1269        let http = self.http_client.clone();
1270        let sender = self.data_sender.clone();
1271        let instrument_id = request.instrument_id;
1272        let start = request.start;
1273        let end = request.end;
1274        let limit = request.limit.map(|n| n.get().min(u32::MAX as usize) as u32);
1275        let request_id = request.request_id;
1276        let client_id = request.client_id.unwrap_or(self.client_id);
1277        let params = request.params;
1278        let clock = self.clock;
1279        let start_nanos = datetime_to_unix_nanos(start);
1280        let end_nanos = datetime_to_unix_nanos(end);
1281
1282        get_runtime().spawn(async move {
1283            match http
1284                .request_funding_rates(instrument_id, start, end, limit)
1285                .await
1286                .context("failed to request funding rates from BitMEX")
1287            {
1288                Ok(rates) => {
1289                    let response = DataResponse::FundingRates(FundingRatesResponse::new(
1290                        request_id,
1291                        client_id,
1292                        instrument_id,
1293                        rates,
1294                        start_nanos,
1295                        end_nanos,
1296                        clock.get_time_ns(),
1297                        params,
1298                    ));
1299
1300                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1301                        log::error!("Failed to send funding rates response: {e}");
1302                    }
1303                }
1304                Err(e) => log::error!("Funding rates request failed: {e:?}"),
1305            }
1306        });
1307
1308        Ok(())
1309    }
1310
1311    fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1312        let http = self.http_client.clone();
1313        let sender = self.data_sender.clone();
1314        let bar_type = request.bar_type;
1315        let start = request.start;
1316        let end = request.end;
1317        let limit = request.limit.map(|n| n.get() as u32);
1318        let request_id = request.request_id;
1319        let client_id = request.client_id.unwrap_or(self.client_id);
1320        let params = request.params;
1321        let clock = self.clock;
1322        let start_nanos = datetime_to_unix_nanos(start);
1323        let end_nanos = datetime_to_unix_nanos(end);
1324
1325        get_runtime().spawn(async move {
1326            match http
1327                .request_bars(bar_type, start, end, limit, false)
1328                .await
1329                .context("failed to request bars from BitMEX")
1330            {
1331                Ok(bars) => {
1332                    let response = DataResponse::Bars(BarsResponse::new(
1333                        request_id,
1334                        client_id,
1335                        bar_type,
1336                        bars,
1337                        start_nanos,
1338                        end_nanos,
1339                        clock.get_time_ns(),
1340                        params,
1341                    ));
1342
1343                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1344                        log::error!("Failed to send bars response: {e}");
1345                    }
1346                }
1347                Err(e) => log::error!("Bar request failed: {e:?}"),
1348            }
1349        });
1350
1351        Ok(())
1352    }
1353}
1354
1355fn handle_quote_messages(
1356    data: Vec<BitmexQuoteMsg>,
1357    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
1358    quote_cache: &mut QuoteCache,
1359    ts_init: UnixNanos,
1360    sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
1361) {
1362    for msg in data {
1363        let Some(instrument) = instruments_by_symbol.get(&msg.symbol) else {
1364            log::error!(
1365                "Instrument cache miss: quote dropped for symbol={}",
1366                msg.symbol,
1367            );
1368            continue;
1369        };
1370
1371        let instrument_id = instrument.id();
1372        let price_precision = instrument.price_precision();
1373
1374        let bid_price = msg.bid_price.map(|p| Price::new(p, price_precision));
1375        let ask_price = msg.ask_price.map(|p| Price::new(p, price_precision));
1376        let bid_size = msg
1377            .bid_size
1378            .map(|s| parse_contracts_quantity(s, instrument));
1379        let ask_size = msg
1380            .ask_size
1381            .map(|s| parse_contracts_quantity(s, instrument));
1382        let ts_event = UnixNanos::from(msg.timestamp);
1383
1384        match quote_cache.process(
1385            instrument_id,
1386            bid_price,
1387            ask_price,
1388            bid_size,
1389            ask_size,
1390            ts_event,
1391            ts_init,
1392        ) {
1393            Ok(quote) => {
1394                if let Err(e) = sender.send(DataEvent::Data(Data::Quote(quote))) {
1395                    log::error!("Failed to emit data event: {e}");
1396                }
1397            }
1398            Err(e) => {
1399                log::warn!("Failed to process quote for {}: {e}", msg.symbol);
1400            }
1401        }
1402    }
1403}