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.auth_timeout_secs,
654                self.config.environment,
655                self.config.transport_backend,
656                self.config.proxy_url.clone(),
657            )
658            .context("failed to construct BitMEX websocket client")?;
659            self.ws_client = Some(ws);
660        }
661
662        self.bootstrap_instruments().await?;
663
664        let ws = self.ws_client_mut()?;
665        ws.connect()
666            .await
667            .context("failed to connect BitMEX websocket")?;
668        ws.wait_until_active(10.0)
669            .await
670            .context("BitMEX websocket did not become active")?;
671
672        let stream = ws.stream();
673        self.spawn_stream_task(stream);
674        self.maybe_spawn_instrument_refresh();
675
676        self.is_connected.store(true, Ordering::Relaxed);
677        log::info!("Connected");
678        Ok(())
679    }
680
681    async fn disconnect(&mut self) -> anyhow::Result<()> {
682        if self.is_disconnected() {
683            return Ok(());
684        }
685
686        self.cancellation_token.cancel();
687
688        if let Some(ws) = self.ws_client.as_mut()
689            && let Err(e) = ws.close().await
690        {
691            log::warn!("Error while closing BitMEX websocket: {e:?}");
692        }
693
694        for handle in self.tasks.drain(..) {
695            if let Err(e) = handle.await {
696                log::error!("Error joining websocket task: {e:?}");
697            }
698        }
699
700        self.cancellation_token = CancellationToken::new();
701        self.is_connected.store(false, Ordering::Relaxed);
702        self.book_channels.store(AHashMap::new());
703        self.instrument_refresh_active = false;
704
705        log::info!("Disconnected");
706        Ok(())
707    }
708
709    fn is_connected(&self) -> bool {
710        self.is_connected()
711    }
712
713    fn is_disconnected(&self) -> bool {
714        self.is_disconnected()
715    }
716
717    fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
718        let ws = self.ws_client()?.clone();
719
720        self.spawn_ws(
721            async move {
722                ws.subscribe_instruments()
723                    .await
724                    .map_err(|e| anyhow::anyhow!(e))
725            },
726            "BitMEX instruments subscription",
727        );
728        Ok(())
729    }
730
731    fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
732        let instrument_id = cmd.instrument_id;
733
734        if let Some(instrument) = self.instruments.load().get(&instrument_id).cloned() {
735            if let Err(e) = self.data_sender.send(DataEvent::Instrument(instrument)) {
736                log::error!("Failed to send instrument event for {instrument_id}: {e}");
737            }
738            return Ok(());
739        }
740
741        log::warn!("Instrument {instrument_id} not found in BitMEX cache");
742
743        let ws = self.ws_client()?.clone();
744        self.spawn_ws(
745            async move {
746                ws.subscribe_instrument(instrument_id)
747                    .await
748                    .map_err(|e| anyhow::anyhow!(e))
749            },
750            "BitMEX instrument subscription",
751        );
752
753        Ok(())
754    }
755
756    fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
757        if cmd.book_type != BookType::L2_MBP {
758            anyhow::bail!("BitMEX only supports L2_MBP order book deltas");
759        }
760
761        let instrument_id = cmd.instrument_id;
762        let depth = cmd.depth.map_or(0, |d| d.get());
763        let channel = if depth > 0 && depth <= 25 {
764            if depth != 25 {
765                log::debug!(
766                    "BitMEX only supports depth 25 for L2 deltas, using L2_25 for requested depth {depth}"
767                );
768            }
769            BitmexBookChannel::OrderBookL2_25
770        } else {
771            BitmexBookChannel::OrderBookL2
772        };
773
774        let ws = self.ws_client()?.clone();
775        let book_channels = Arc::clone(&self.book_channels);
776
777        self.spawn_ws(
778            async move {
779                match channel {
780                    BitmexBookChannel::OrderBookL2 => ws
781                        .subscribe_book(instrument_id)
782                        .await
783                        .map_err(|e| anyhow::anyhow!(e))?,
784                    BitmexBookChannel::OrderBookL2_25 => ws
785                        .subscribe_book_25(instrument_id)
786                        .await
787                        .map_err(|e| anyhow::anyhow!(e))?,
788                    BitmexBookChannel::OrderBook10 => unreachable!(),
789                }
790                book_channels.insert(instrument_id, channel);
791                Ok(())
792            },
793            "BitMEX book delta subscription",
794        );
795
796        Ok(())
797    }
798
799    fn subscribe_book_depth10(&mut self, cmd: SubscribeBookDepth10) -> anyhow::Result<()> {
800        let instrument_id = cmd.instrument_id;
801        let ws = self.ws_client()?.clone();
802        let book_channels = Arc::clone(&self.book_channels);
803
804        self.spawn_ws(
805            async move {
806                ws.subscribe_book_depth10(instrument_id)
807                    .await
808                    .map_err(|e| anyhow::anyhow!(e))?;
809                book_channels.insert(instrument_id, BitmexBookChannel::OrderBook10);
810                Ok(())
811            },
812            "BitMEX book depth10 subscription",
813        );
814        Ok(())
815    }
816
817    fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
818        let instrument_id = cmd.instrument_id;
819        let ws = self.ws_client()?.clone();
820
821        self.spawn_ws(
822            async move {
823                ws.subscribe_quotes(instrument_id)
824                    .await
825                    .map_err(|e| anyhow::anyhow!(e))
826            },
827            "BitMEX quote subscription",
828        );
829        Ok(())
830    }
831
832    fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
833        let instrument_id = cmd.instrument_id;
834        let ws = self.ws_client()?.clone();
835
836        self.spawn_ws(
837            async move {
838                ws.subscribe_trades(instrument_id)
839                    .await
840                    .map_err(|e| anyhow::anyhow!(e))
841            },
842            "BitMEX trade subscription",
843        );
844        Ok(())
845    }
846
847    fn subscribe_mark_prices(&mut self, cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
848        let instrument_id = cmd.instrument_id;
849        let ws = self.ws_client()?.clone();
850
851        self.spawn_ws(
852            async move {
853                ws.subscribe_mark_prices(instrument_id)
854                    .await
855                    .map_err(|e| anyhow::anyhow!(e))
856            },
857            "BitMEX mark price subscription",
858        );
859        Ok(())
860    }
861
862    fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
863        let instrument_id = cmd.instrument_id;
864        let ws = self.ws_client()?.clone();
865
866        self.spawn_ws(
867            async move {
868                ws.subscribe_index_prices(instrument_id)
869                    .await
870                    .map_err(|e| anyhow::anyhow!(e))
871            },
872            "BitMEX index price subscription",
873        );
874        Ok(())
875    }
876
877    fn subscribe_funding_rates(&mut self, cmd: SubscribeFundingRates) -> anyhow::Result<()> {
878        let instrument_id = cmd.instrument_id;
879        let ws = self.ws_client()?.clone();
880
881        self.spawn_ws(
882            async move {
883                ws.subscribe_funding_rates(instrument_id)
884                    .await
885                    .map_err(|e| anyhow::anyhow!(e))
886            },
887            "BitMEX funding rate subscription",
888        );
889        Ok(())
890    }
891
892    fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
893        let bar_type = cmd.bar_type;
894        let ws = self.ws_client()?.clone();
895
896        self.spawn_ws(
897            async move {
898                ws.subscribe_bars(bar_type)
899                    .await
900                    .map_err(|e| anyhow::anyhow!(e))
901            },
902            "BitMEX bar subscription",
903        );
904        Ok(())
905    }
906
907    fn subscribe_instrument_status(
908        &mut self,
909        cmd: SubscribeInstrumentStatus,
910    ) -> anyhow::Result<()> {
911        let instrument_id = cmd.instrument_id;
912        let ws = self.ws_client()?.clone();
913
914        self.spawn_ws(
915            async move {
916                ws.subscribe_instrument(instrument_id)
917                    .await
918                    .map_err(|e| anyhow::anyhow!(e))
919            },
920            "BitMEX instrument status subscription",
921        );
922        Ok(())
923    }
924
925    fn unsubscribe_instrument_status(
926        &mut self,
927        cmd: &UnsubscribeInstrumentStatus,
928    ) -> anyhow::Result<()> {
929        let instrument_id = cmd.instrument_id;
930        let ws = self.ws_client()?.clone();
931
932        self.spawn_ws(
933            async move {
934                ws.unsubscribe_instrument(instrument_id)
935                    .await
936                    .map_err(|e| anyhow::anyhow!(e))
937            },
938            "BitMEX instrument status unsubscribe",
939        );
940        Ok(())
941    }
942
943    fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
944        let instrument_id = cmd.instrument_id;
945        let ws = self.ws_client()?.clone();
946        let book_channels = Arc::clone(&self.book_channels);
947
948        self.spawn_ws(
949            async move {
950                let channel = book_channels.load().get(&instrument_id).copied();
951                book_channels.remove(&instrument_id);
952
953                match channel {
954                    Some(BitmexBookChannel::OrderBookL2) => ws
955                        .unsubscribe_book(instrument_id)
956                        .await
957                        .map_err(|e| anyhow::anyhow!(e))?,
958                    Some(BitmexBookChannel::OrderBookL2_25) => ws
959                        .unsubscribe_book_25(instrument_id)
960                        .await
961                        .map_err(|e| anyhow::anyhow!(e))?,
962                    Some(BitmexBookChannel::OrderBook10) => ws
963                        .unsubscribe_book_depth10(instrument_id)
964                        .await
965                        .map_err(|e| anyhow::anyhow!(e))?,
966                    None => ws
967                        .unsubscribe_book(instrument_id)
968                        .await
969                        .map_err(|e| anyhow::anyhow!(e))?,
970                }
971                Ok(())
972            },
973            "BitMEX book delta unsubscribe",
974        );
975        Ok(())
976    }
977
978    fn unsubscribe_book_depth10(&mut self, cmd: &UnsubscribeBookDepth10) -> anyhow::Result<()> {
979        let instrument_id = cmd.instrument_id;
980        let ws = self.ws_client()?.clone();
981        let book_channels = Arc::clone(&self.book_channels);
982
983        self.spawn_ws(
984            async move {
985                book_channels.remove(&instrument_id);
986                ws.unsubscribe_book_depth10(instrument_id)
987                    .await
988                    .map_err(|e| anyhow::anyhow!(e))
989            },
990            "BitMEX book depth10 unsubscribe",
991        );
992        Ok(())
993    }
994
995    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
996        let instrument_id = cmd.instrument_id;
997        let ws = self.ws_client()?.clone();
998
999        self.spawn_ws(
1000            async move {
1001                ws.unsubscribe_quotes(instrument_id)
1002                    .await
1003                    .map_err(|e| anyhow::anyhow!(e))
1004            },
1005            "BitMEX quote unsubscribe",
1006        );
1007        Ok(())
1008    }
1009
1010    fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1011        let instrument_id = cmd.instrument_id;
1012        let ws = self.ws_client()?.clone();
1013
1014        self.spawn_ws(
1015            async move {
1016                ws.unsubscribe_trades(instrument_id)
1017                    .await
1018                    .map_err(|e| anyhow::anyhow!(e))
1019            },
1020            "BitMEX trade unsubscribe",
1021        );
1022        Ok(())
1023    }
1024
1025    fn unsubscribe_mark_prices(&mut self, cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
1026        let ws = self.ws_client()?.clone();
1027        let instrument_id = cmd.instrument_id;
1028
1029        self.spawn_ws(
1030            async move {
1031                ws.unsubscribe_mark_prices(instrument_id)
1032                    .await
1033                    .map_err(|e| anyhow::anyhow!(e))
1034            },
1035            "BitMEX mark price unsubscribe",
1036        );
1037        Ok(())
1038    }
1039
1040    fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
1041        let ws = self.ws_client()?.clone();
1042        let instrument_id = cmd.instrument_id;
1043
1044        self.spawn_ws(
1045            async move {
1046                ws.unsubscribe_index_prices(instrument_id)
1047                    .await
1048                    .map_err(|e| anyhow::anyhow!(e))
1049            },
1050            "BitMEX index price unsubscribe",
1051        );
1052        Ok(())
1053    }
1054
1055    fn unsubscribe_funding_rates(&mut self, cmd: &UnsubscribeFundingRates) -> anyhow::Result<()> {
1056        let ws = self.ws_client()?.clone();
1057        let instrument_id = cmd.instrument_id;
1058
1059        self.spawn_ws(
1060            async move {
1061                ws.unsubscribe_funding_rates(instrument_id)
1062                    .await
1063                    .map_err(|e| anyhow::anyhow!(e))
1064            },
1065            "BitMEX funding rate unsubscribe",
1066        );
1067        Ok(())
1068    }
1069
1070    fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1071        let bar_type = cmd.bar_type;
1072        let ws = self.ws_client()?.clone();
1073
1074        self.spawn_ws(
1075            async move {
1076                ws.unsubscribe_bars(bar_type)
1077                    .await
1078                    .map_err(|e| anyhow::anyhow!(e))
1079            },
1080            "BitMEX bar unsubscribe",
1081        );
1082        Ok(())
1083    }
1084
1085    fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
1086        if let Some(req_venue) = request.venue
1087            && req_venue != self.venue()
1088        {
1089            log::warn!("Ignoring mismatched venue in instruments request: {req_venue}");
1090        }
1091        let venue = self.venue();
1092
1093        let http = self.http_client.clone();
1094        let instruments_cache = Arc::clone(&self.instruments);
1095        let sender = self.data_sender.clone();
1096        let request_id = request.request_id;
1097        let client_id = request.client_id.unwrap_or(self.client_id);
1098        let params = request.params;
1099        let start_nanos = datetime_to_unix_nanos(request.start);
1100        let end_nanos = datetime_to_unix_nanos(request.end);
1101        let clock = self.clock;
1102        let active_only = self.config.active_only;
1103
1104        get_runtime().spawn(async move {
1105            let http_client = http;
1106            match http_client
1107                .request_instruments(active_only)
1108                .await
1109                .context("failed to request instruments from BitMEX")
1110            {
1111                Ok(instruments) => {
1112                    instruments_cache.rcu(|m| {
1113                        m.clear();
1114                        for instrument in &instruments {
1115                            m.insert(instrument.id(), instrument.clone());
1116                        }
1117                    });
1118                    http_client.cache_instruments(&instruments);
1119
1120                    let response = DataResponse::Instruments(InstrumentsResponse::new(
1121                        request_id,
1122                        client_id,
1123                        venue,
1124                        instruments,
1125                        start_nanos,
1126                        end_nanos,
1127                        clock.get_time_ns(),
1128                        params,
1129                    ));
1130
1131                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1132                        log::error!("Failed to send instruments response: {e}");
1133                    }
1134                }
1135                Err(e) => log::error!("Instrument request failed: {e:?}"),
1136            }
1137        });
1138
1139        Ok(())
1140    }
1141
1142    fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
1143        let http_client = self.http_client.clone();
1144        let instruments_cache = Arc::clone(&self.instruments);
1145        let sender = self.data_sender.clone();
1146        let instrument_id = request.instrument_id;
1147        let request_id = request.request_id;
1148        let client_id = request.client_id.unwrap_or(self.client_id);
1149        let start = request.start;
1150        let end = request.end;
1151        let params = request.params;
1152        let clock = self.clock;
1153
1154        get_runtime().spawn(async move {
1155            match http_client
1156                .request_instrument(instrument_id)
1157                .await
1158                .context("failed to request instrument from BitMEX")
1159            {
1160                Ok(Some(instrument)) => {
1161                    http_client.cache_instrument(instrument.clone());
1162                    instruments_cache.insert(instrument.id(), instrument.clone());
1163
1164                    let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1165                        request_id,
1166                        client_id,
1167                        instrument.id(),
1168                        instrument,
1169                        datetime_to_unix_nanos(start),
1170                        datetime_to_unix_nanos(end),
1171                        clock.get_time_ns(),
1172                        params,
1173                    )));
1174
1175                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1176                        log::error!("Failed to send instrument response: {e}");
1177                    }
1178                }
1179                Ok(None) => log::warn!("BitMEX instrument {instrument_id} not found"),
1180                Err(e) => log::error!("Instrument request failed: {e:?}"),
1181            }
1182        });
1183
1184        Ok(())
1185    }
1186
1187    fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
1188        let http = self.http_client.clone();
1189        let sender = self.data_sender.clone();
1190        let instrument_id = request.instrument_id;
1191        let depth = request.depth.map(|n| n.get().min(u32::MAX as usize) as u32);
1192        let request_id = request.request_id;
1193        let client_id = request.client_id.unwrap_or(self.client_id);
1194        let params = request.params;
1195        let clock = self.clock;
1196
1197        get_runtime().spawn(async move {
1198            match http
1199                .request_book_snapshot(instrument_id, depth)
1200                .await
1201                .context("failed to request book snapshot from BitMEX")
1202            {
1203                Ok(book) => {
1204                    let response = DataResponse::Book(BookResponse::new(
1205                        request_id,
1206                        client_id,
1207                        instrument_id,
1208                        book,
1209                        None,
1210                        None,
1211                        clock.get_time_ns(),
1212                        params,
1213                    ));
1214
1215                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1216                        log::error!("Failed to send book snapshot response: {e}");
1217                    }
1218                }
1219                Err(e) => log::error!("Book snapshot request failed: {e:?}"),
1220            }
1221        });
1222
1223        Ok(())
1224    }
1225
1226    fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
1227        let http = self.http_client.clone();
1228        let sender = self.data_sender.clone();
1229        let instrument_id = request.instrument_id;
1230        let start = request.start;
1231        let end = request.end;
1232        let limit = request.limit.map(|n| n.get() as u32);
1233        let request_id = request.request_id;
1234        let client_id = request.client_id.unwrap_or(self.client_id);
1235        let params = request.params;
1236        let clock = self.clock;
1237        let start_nanos = datetime_to_unix_nanos(start);
1238        let end_nanos = datetime_to_unix_nanos(end);
1239
1240        get_runtime().spawn(async move {
1241            match http
1242                .request_trades(instrument_id, start, end, limit)
1243                .await
1244                .context("failed to request trades from BitMEX")
1245            {
1246                Ok(trades) => {
1247                    let response = DataResponse::Trades(TradesResponse::new(
1248                        request_id,
1249                        client_id,
1250                        instrument_id,
1251                        trades,
1252                        start_nanos,
1253                        end_nanos,
1254                        clock.get_time_ns(),
1255                        params,
1256                    ));
1257
1258                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1259                        log::error!("Failed to send trades response: {e}");
1260                    }
1261                }
1262                Err(e) => log::error!("Trade request failed: {e:?}"),
1263            }
1264        });
1265
1266        Ok(())
1267    }
1268
1269    fn request_funding_rates(&self, request: RequestFundingRates) -> anyhow::Result<()> {
1270        let http = self.http_client.clone();
1271        let sender = self.data_sender.clone();
1272        let instrument_id = request.instrument_id;
1273        let start = request.start;
1274        let end = request.end;
1275        let limit = request.limit.map(|n| n.get().min(u32::MAX as usize) as u32);
1276        let request_id = request.request_id;
1277        let client_id = request.client_id.unwrap_or(self.client_id);
1278        let params = request.params;
1279        let clock = self.clock;
1280        let start_nanos = datetime_to_unix_nanos(start);
1281        let end_nanos = datetime_to_unix_nanos(end);
1282
1283        get_runtime().spawn(async move {
1284            match http
1285                .request_funding_rates(instrument_id, start, end, limit)
1286                .await
1287                .context("failed to request funding rates from BitMEX")
1288            {
1289                Ok(rates) => {
1290                    let response = DataResponse::FundingRates(FundingRatesResponse::new(
1291                        request_id,
1292                        client_id,
1293                        instrument_id,
1294                        rates,
1295                        start_nanos,
1296                        end_nanos,
1297                        clock.get_time_ns(),
1298                        params,
1299                    ));
1300
1301                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1302                        log::error!("Failed to send funding rates response: {e}");
1303                    }
1304                }
1305                Err(e) => log::error!("Funding rates request failed: {e:?}"),
1306            }
1307        });
1308
1309        Ok(())
1310    }
1311
1312    fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1313        let http = self.http_client.clone();
1314        let sender = self.data_sender.clone();
1315        let bar_type = request.bar_type;
1316        let start = request.start;
1317        let end = request.end;
1318        let limit = request.limit.map(|n| n.get() as u32);
1319        let request_id = request.request_id;
1320        let client_id = request.client_id.unwrap_or(self.client_id);
1321        let params = request.params;
1322        let clock = self.clock;
1323        let start_nanos = datetime_to_unix_nanos(start);
1324        let end_nanos = datetime_to_unix_nanos(end);
1325
1326        get_runtime().spawn(async move {
1327            match http
1328                .request_bars(bar_type, start, end, limit, false)
1329                .await
1330                .context("failed to request bars from BitMEX")
1331            {
1332                Ok(bars) => {
1333                    let response = DataResponse::Bars(BarsResponse::new(
1334                        request_id,
1335                        client_id,
1336                        bar_type,
1337                        bars,
1338                        start_nanos,
1339                        end_nanos,
1340                        clock.get_time_ns(),
1341                        params,
1342                    ));
1343
1344                    if let Err(e) = sender.send(DataEvent::Response(response)) {
1345                        log::error!("Failed to send bars response: {e}");
1346                    }
1347                }
1348                Err(e) => log::error!("Bar request failed: {e:?}"),
1349            }
1350        });
1351
1352        Ok(())
1353    }
1354}
1355
1356fn handle_quote_messages(
1357    data: Vec<BitmexQuoteMsg>,
1358    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
1359    quote_cache: &mut QuoteCache,
1360    ts_init: UnixNanos,
1361    sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
1362) {
1363    for msg in data {
1364        let Some(instrument) = instruments_by_symbol.get(&msg.symbol) else {
1365            log::error!(
1366                "Instrument cache miss: quote dropped for symbol={}",
1367                msg.symbol,
1368            );
1369            continue;
1370        };
1371
1372        let instrument_id = instrument.id();
1373        let price_precision = instrument.price_precision();
1374
1375        let bid_price = msg.bid_price.map(|p| Price::new(p, price_precision));
1376        let ask_price = msg.ask_price.map(|p| Price::new(p, price_precision));
1377        let bid_size = msg
1378            .bid_size
1379            .map(|s| parse_contracts_quantity(s, instrument));
1380        let ask_size = msg
1381            .ask_size
1382            .map(|s| parse_contracts_quantity(s, instrument));
1383        let ts_event = UnixNanos::from(msg.timestamp);
1384
1385        match quote_cache.process(
1386            instrument_id,
1387            bid_price,
1388            ask_price,
1389            bid_size,
1390            ask_size,
1391            ts_event,
1392            ts_init,
1393        ) {
1394            Ok(quote) => {
1395                if let Err(e) = sender.send(DataEvent::Data(Data::Quote(quote))) {
1396                    log::error!("Failed to emit data event: {e}");
1397                }
1398            }
1399            Err(e) => {
1400                log::warn!("Failed to process quote for {}: {e}", msg.symbol);
1401            }
1402        }
1403    }
1404}