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