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