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