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