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