1use 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 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 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 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}