1use std::{
19 str::FromStr,
20 sync::{
21 Arc, RwLock,
22 atomic::{AtomicBool, AtomicU32, Ordering},
23 },
24 time::Duration,
25};
26
27use ahash::AHashMap;
28use anyhow::Context;
29use futures_util::{StreamExt, pin_mut};
30use nautilus_common::{
31 clients::DataClient,
32 live::{runner::get_data_event_sender, runtime::get_runtime},
33 messages::{
34 DataEvent,
35 data::{
36 BarsResponse, CustomDataResponse, DataResponse, FundingRatesResponse,
37 InstrumentResponse, InstrumentsResponse, RequestBars, RequestCustomData,
38 RequestFundingRates, RequestInstrument, RequestInstruments, RequestTrades,
39 SubscribeBars, SubscribeBookDeltas, SubscribeCustomData, SubscribeFundingRates,
40 SubscribeIndexPrices, SubscribeInstrument, SubscribeInstruments, SubscribeMarkPrices,
41 SubscribeQuotes, SubscribeTrades, TradesResponse, UnsubscribeBars,
42 UnsubscribeBookDeltas, UnsubscribeCustomData, UnsubscribeFundingRates,
43 UnsubscribeIndexPrices, UnsubscribeMarkPrices, UnsubscribeQuotes, UnsubscribeTrades,
44 subscribe::SubscribeInstrumentStatus, unsubscribe::UnsubscribeInstrumentStatus,
45 },
46 },
47};
48use nautilus_core::{
49 AtomicMap, MUTEX_POISONED, Params,
50 datetime::{NANOSECONDS_IN_MILLISECOND, datetime_to_unix_nanos},
51 nanos::UnixNanos,
52 time::{AtomicTime, get_atomic_clock_realtime},
53};
54use nautilus_model::{
55 data::{
56 BookOrder, CustomData, Data, DataType, OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API,
57 },
58 enums::{BookAction, BookType, MarketStatusAction, OrderSide, RecordFlag},
59 identifiers::{ClientId, InstrumentId, Venue},
60 instruments::{Instrument, InstrumentAny},
61 types::{Price, Quantity},
62};
63use rust_decimal::Decimal;
64use tokio::task::JoinHandle;
65use tokio_util::sync::CancellationToken;
66use ustr::Ustr;
67
68use crate::{
69 common::{
70 consts::{BINANCE_BOOK_DEPTHS, BINANCE_VENUE},
71 enums::{BinanceEnvironment, BinanceProductType},
72 parse::{
73 bar_spec_to_binance_interval, parse_price_at_precision, parse_quantity_at_precision,
74 parse_required_price_at_precision, parse_required_quantity_at_precision,
75 },
76 status::diff_and_emit_statuses,
77 symbol::{format_binance_stream_symbol, format_binance_symbol},
78 urls::{get_usdm_ws_route_base_url, get_ws_public_base_url},
79 },
80 config::BinanceDataClientConfig,
81 data_types::{
82 BinanceFuturesLiquidation, BinanceFuturesOpenInterest, BinanceFuturesOpenInterestHist,
83 BinanceFuturesOpenInterestHistPoint, register_binance_custom_data,
84 },
85 futures::{
86 http::{
87 client::BinanceFuturesHttpClient,
88 models::BinanceOrderBook,
89 query::{BinanceDepthParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams},
90 },
91 websocket::streams::{
92 client::BinanceFuturesWebSocketClient,
93 messages::BinanceFuturesWsStreamsMessage,
94 parse_data::{
95 parse_agg_trade, parse_book_ticker, parse_depth_update, parse_kline,
96 parse_mark_price, parse_ticker, parse_trade,
97 },
98 },
99 },
100};
101
102const MAX_SNAPSHOT_RETRIES: u32 = 5;
103const MAX_BUFFERED_DEPTH_UPDATES: usize = 10_000;
104const SNAPSHOT_RETRY_BACKOFF_BASE_MS: u64 = 250;
105const SNAPSHOT_RETRY_BACKOFF_CAP_MS: u64 = 3_000;
106
107#[derive(Debug, Clone)]
108struct BufferedDepthUpdate {
109 deltas: OrderBookDeltas,
110 first_update_id: u64,
111 final_update_id: u64,
112 prev_final_update_id: u64,
113}
114
115#[derive(Debug, Clone)]
116struct BookBuffer {
117 updates: Vec<BufferedDepthUpdate>,
118 epoch: u64,
119}
120
121impl BookBuffer {
122 fn new(epoch: u64) -> Self {
123 Self {
124 updates: Vec::new(),
125 epoch,
126 }
127 }
128}
129
130#[derive(Debug)]
132pub struct BinanceFuturesDataClient {
133 clock: &'static AtomicTime,
134 client_id: ClientId,
135 config: BinanceDataClientConfig,
136 product_type: BinanceProductType,
137 http_client: BinanceFuturesHttpClient,
138 ws_client: BinanceFuturesWebSocketClient,
139 ws_public_client: BinanceFuturesWebSocketClient,
140 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
141 is_connected: AtomicBool,
142 cancellation_token: CancellationToken,
143 tasks: Vec<JoinHandle<()>>,
144 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
145 status_cache: Arc<AtomicMap<InstrumentId, MarketStatusAction>>,
146 book_buffers: Arc<AtomicMap<InstrumentId, BookBuffer>>,
147 book_subscriptions: Arc<AtomicMap<InstrumentId, u32>>,
148 mark_price_refs: Arc<AtomicMap<InstrumentId, u32>>,
149 ticker_refs: Arc<AtomicMap<InstrumentId, u32>>,
150 force_order_refs: Arc<AtomicMap<InstrumentId, u32>>,
151 force_order_all_market_refs: Arc<AtomicU32>,
152 force_order_all_market_stream_active: Arc<AtomicBool>,
153 force_order_ws_lock: Arc<tokio::sync::Mutex<()>>,
154 book_epoch: Arc<RwLock<u64>>,
155}
156
157impl BinanceFuturesDataClient {
158 pub fn new(
165 client_id: ClientId,
166 config: BinanceDataClientConfig,
167 product_type: BinanceProductType,
168 ) -> anyhow::Result<Self> {
169 match product_type {
170 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
171 _ => {
172 anyhow::bail!(
173 "BinanceFuturesDataClient requires UsdM or CoinM product type, was {product_type:?}"
174 );
175 }
176 }
177
178 let clock = get_atomic_clock_realtime();
179 let data_sender = get_data_event_sender();
180
181 let http_client = BinanceFuturesHttpClient::new(
182 product_type,
183 config.environment,
184 clock,
185 config.api_key.clone(),
186 config.api_secret.clone(),
187 config.base_url_http.clone(),
188 None, None, None, false, )?;
193
194 let market_url = config.base_url_ws.clone().map(|url| {
195 if product_type == BinanceProductType::UsdM
196 && config.environment == BinanceEnvironment::Live
197 {
198 get_usdm_ws_route_base_url(&url, "market")
199 } else {
200 url
201 }
202 });
203
204 let ws_client = BinanceFuturesWebSocketClient::new(
205 product_type,
206 config.environment,
207 config.api_key.clone(),
208 config.api_secret.clone(),
209 market_url,
210 Some(20), config.transport_backend,
212 )?;
213
214 let public_url = config.base_url_ws.clone().map_or_else(
215 || get_ws_public_base_url(product_type, config.environment).to_string(),
216 |url| {
217 if product_type == BinanceProductType::UsdM
218 && config.environment == BinanceEnvironment::Live
219 {
220 get_usdm_ws_route_base_url(&url, "public")
221 } else {
222 url
223 }
224 },
225 );
226 let ws_public_client = BinanceFuturesWebSocketClient::new(
227 product_type,
228 config.environment,
229 None,
230 None,
231 Some(public_url),
232 Some(20),
233 config.transport_backend,
234 )?;
235
236 Ok(Self {
237 clock,
238 client_id,
239 config,
240 product_type,
241 http_client,
242 ws_client,
243 ws_public_client,
244 data_sender,
245 is_connected: AtomicBool::new(false),
246 cancellation_token: CancellationToken::new(),
247 tasks: Vec::new(),
248 instruments: Arc::new(AtomicMap::new()),
249 status_cache: Arc::new(AtomicMap::new()),
250 book_buffers: Arc::new(AtomicMap::new()),
251 book_subscriptions: Arc::new(AtomicMap::new()),
252 mark_price_refs: Arc::new(AtomicMap::new()),
253 ticker_refs: Arc::new(AtomicMap::new()),
254 force_order_refs: Arc::new(AtomicMap::new()),
255 force_order_all_market_refs: Arc::new(AtomicU32::new(0)),
256 force_order_all_market_stream_active: Arc::new(AtomicBool::new(false)),
257 force_order_ws_lock: Arc::new(tokio::sync::Mutex::new(())),
258 book_epoch: Arc::new(RwLock::new(0)),
259 })
260 }
261
262 fn venue(&self) -> Venue {
263 *BINANCE_VENUE
264 }
265
266 fn send_data(sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>, data: Data) {
267 if let Err(e) = sender.send(DataEvent::Data(data)) {
268 log::error!("Failed to emit data event: {e}");
269 }
270 }
271
272 fn spawn_ws<F>(&self, fut: F, context: &'static str)
273 where
274 F: Future<Output = anyhow::Result<()>> + Send + 'static,
275 {
276 get_runtime().spawn(async move {
277 if let Err(e) = fut.await {
278 log::error!("{context}: {e:?}");
279 }
280 });
281 }
282
283 fn custom_liquidation_instrument_id(
284 data_type: &DataType,
285 ) -> anyhow::Result<Option<InstrumentId>> {
286 let Some(raw_instrument_id) = data_type
287 .metadata()
288 .as_ref()
289 .and_then(|m| m.get("instrument_id"))
290 .and_then(|v| v.as_str())
291 .map(str::trim)
292 .filter(|value| !value.is_empty())
293 else {
294 return Ok(None);
295 };
296
297 let instrument_id = InstrumentId::from_str(raw_instrument_id)
298 .with_context(|| format!("invalid instrument_id metadata `{raw_instrument_id}`"))?;
299
300 Ok(Some(instrument_id))
301 }
302
303 fn required_instrument_id_metadata(data_type: &DataType) -> anyhow::Result<InstrumentId> {
304 let Some(raw_instrument_id) = data_type
305 .metadata()
306 .as_ref()
307 .and_then(|m| m.get("instrument_id"))
308 .and_then(|v| v.as_str())
309 .map(str::trim)
310 .filter(|value| !value.is_empty())
311 else {
312 anyhow::bail!("custom data request requires `instrument_id` metadata");
313 };
314
315 InstrumentId::from_str(raw_instrument_id)
316 .with_context(|| format!("invalid instrument_id metadata `{raw_instrument_id}`"))
317 }
318
319 fn required_period_metadata(data_type: &DataType) -> anyhow::Result<String> {
320 let Some(period) = data_type
321 .metadata()
322 .as_ref()
323 .and_then(|m| m.get("period"))
324 .and_then(|v| v.as_str())
325 .map(str::trim)
326 .filter(|value| !value.is_empty())
327 else {
328 anyhow::bail!("historical open interest request requires `period` metadata");
329 };
330
331 Ok(period.to_string())
332 }
333
334 fn coinm_open_interest_hist_params(
337 instrument_id: &InstrumentId,
338 ) -> anyhow::Result<(String, String)> {
339 let symbol = format_binance_symbol(instrument_id);
340 let Some(pair) = symbol.strip_suffix("_PERP") else {
341 anyhow::bail!(
342 "COIN-M open interest history requires a perpetual instrument, received {instrument_id}"
343 );
344 };
345
346 Ok((pair.to_string(), "PERPETUAL".to_string()))
347 }
348
349 fn parse_open_interest_decimal(field: &str, value: &str) -> anyhow::Result<Decimal> {
350 Decimal::from_str_exact(value)
351 .with_context(|| format!("invalid Binance open interest `{field}` value `{value}`"))
352 }
353
354 fn unix_nanos_from_millis_i64(field: &str, value: i64) -> anyhow::Result<UnixNanos> {
355 let millis = u64::try_from(value)
356 .with_context(|| format!("invalid Binance open interest `{field}` value `{value}`"))?;
357 Ok(UnixNanos::from_millis(millis))
358 }
359
360 fn liquidation_data_type(instrument_id: InstrumentId) -> DataType {
361 let mut metadata = Params::new();
362 metadata.insert(
363 "instrument_id".to_string(),
364 serde_json::Value::String(instrument_id.to_string()),
365 );
366 DataType::new(
367 "BinanceFuturesLiquidation",
368 Some(metadata),
369 Some(instrument_id.to_string()),
370 )
371 }
372
373 fn liquidation_stream(instrument_id: &InstrumentId) -> String {
374 format!("{}@forceOrder", format_binance_stream_symbol(instrument_id))
375 }
376
377 fn spawn_liquidation_stream_reconcile(&self, context: &'static str) {
378 let ws = self.ws_client.clone();
379 let refs = self.force_order_refs.clone();
380 let all_market_refs = self.force_order_all_market_refs.clone();
381 let all_market_stream_active = self.force_order_all_market_stream_active.clone();
382 let ws_lock = self.force_order_ws_lock.clone();
383
384 self.spawn_ws(
385 async move {
386 let _guard = ws_lock.lock().await;
387 let wants_all_market = all_market_refs.load(Ordering::Relaxed) > 0;
388 let all_market_active = all_market_stream_active.load(Ordering::Acquire);
389
390 if wants_all_market {
391 if all_market_active {
392 return Ok(());
393 }
394
395 let specific_streams = refs
396 .load()
397 .keys()
398 .map(Self::liquidation_stream)
399 .collect::<Vec<_>>();
400
401 if !specific_streams.is_empty() {
402 ws.unsubscribe(specific_streams).await.context(
403 "specific forceOrder unsubscribe while enabling all-market",
404 )?;
405 }
406
407 if all_market_refs.load(Ordering::Relaxed) == 0 {
408 let restored_streams = refs
409 .load()
410 .keys()
411 .map(Self::liquidation_stream)
412 .collect::<Vec<_>>();
413
414 if !restored_streams.is_empty() {
415 ws.subscribe(restored_streams).await.context(
416 "specific forceOrder restore after canceled all-market subscription",
417 )?;
418 }
419 all_market_stream_active.store(false, Ordering::Release);
420 return Ok(());
421 }
422
423 all_market_stream_active.store(true, Ordering::Release);
424
425 if let Err(e) = ws
426 .subscribe(vec!["!forceOrder@arr".to_string()])
427 .await
428 .context("all-market forceOrder subscription")
429 {
430 all_market_stream_active.store(false, Ordering::Release);
431 return Err(e);
432 }
433 } else {
434 if !all_market_active {
435 return Ok(());
436 }
437
438 ws.unsubscribe(vec!["!forceOrder@arr".to_string()])
439 .await
440 .context("all-market forceOrder unsubscribe")?;
441
442 let specific_streams = refs
443 .load()
444 .keys()
445 .map(Self::liquidation_stream)
446 .collect::<Vec<_>>();
447
448 if !specific_streams.is_empty() {
449 ws.subscribe(specific_streams).await.context(
450 "specific forceOrder resubscribe after all-market unsubscribe",
451 )?;
452 }
453 all_market_stream_active.store(false, Ordering::Release);
454 }
455
456 Ok(())
457 },
458 context,
459 );
460 }
461
462 #[expect(clippy::too_many_arguments)]
463 fn handle_ws_message(
464 msg: BinanceFuturesWsStreamsMessage,
465 data_sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
466 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
467 ws_instruments: &Arc<AtomicMap<Ustr, InstrumentAny>>,
468 book_buffers: &Arc<AtomicMap<InstrumentId, BookBuffer>>,
469 book_subscriptions: &Arc<AtomicMap<InstrumentId, u32>>,
470 force_order_refs: &Arc<AtomicMap<InstrumentId, u32>>,
471 ticker_refs: &Arc<AtomicMap<InstrumentId, u32>>,
472 force_order_all_market_refs: &Arc<AtomicU32>,
473 force_order_all_market_stream_active: &Arc<AtomicBool>,
474 book_epoch: &Arc<RwLock<u64>>,
475 http_client: &BinanceFuturesHttpClient,
476 clock: &'static AtomicTime,
477 ) {
478 let ts_init = clock.get_time_ns();
479 let cache = ws_instruments.load();
480
481 match msg {
482 BinanceFuturesWsStreamsMessage::AggTrade(ref trade_msg) => {
483 if let Some(instrument) = cache.get(&trade_msg.symbol) {
484 match parse_agg_trade(trade_msg, instrument, ts_init) {
485 Ok(trade) => Self::send_data(data_sender, Data::Trade(trade)),
486 Err(e) => log::warn!("Failed to parse aggregate trade: {e}"),
487 }
488 }
489 }
490 BinanceFuturesWsStreamsMessage::Trade(ref trade_msg) => {
491 if let Some(instrument) = cache.get(&trade_msg.symbol) {
492 match parse_trade(trade_msg, instrument, ts_init) {
493 Ok(trade) => Self::send_data(data_sender, Data::Trade(trade)),
494 Err(e) => log::warn!("Failed to parse trade: {e}"),
495 }
496 }
497 }
498 BinanceFuturesWsStreamsMessage::BookTicker(ref ticker_msg) => {
499 if let Some(instrument) = cache.get(&ticker_msg.symbol) {
500 match parse_book_ticker(ticker_msg, instrument, ts_init) {
501 Ok(quote) => Self::send_data(data_sender, Data::Quote(quote)),
502 Err(e) => log::warn!("Failed to parse book ticker: {e}"),
503 }
504 }
505 }
506 BinanceFuturesWsStreamsMessage::DepthUpdate(ref depth_msg) => {
507 if let Some(instrument) = cache.get(&depth_msg.symbol) {
508 match parse_depth_update(depth_msg, instrument, ts_init) {
509 Ok(deltas) => {
510 let instrument_id = deltas.instrument_id;
511 let final_update_id = deltas.sequence;
512 let first_update_id = depth_msg.first_update_id;
513 let prev_final_update_id = depth_msg.prev_final_update_id;
514
515 if book_buffers.contains_key(&instrument_id) {
516 let mut was_buffered = false;
517 book_buffers.rcu(|m| {
518 was_buffered = false;
519
520 if let Some(buffer) = m.get_mut(&instrument_id) {
521 buffer.updates.push(BufferedDepthUpdate {
522 deltas: deltas.clone(),
523 first_update_id,
524 final_update_id,
525 prev_final_update_id,
526 });
527 trim_buffered_depth_updates(&mut buffer.updates);
528 was_buffered = true;
529 }
530 });
531
532 if was_buffered {
533 return;
534 }
535 }
536
537 Self::send_data(
538 data_sender,
539 Data::Deltas(OrderBookDeltas_API::new(deltas)),
540 );
541 }
542 Err(e) => log::warn!("Failed to parse depth update: {e}"),
543 }
544 }
545 }
546 BinanceFuturesWsStreamsMessage::MarkPrice(ref mark_msg) => {
547 if let Some(instrument) = cache.get(&mark_msg.symbol) {
548 match parse_mark_price(mark_msg, instrument, ts_init) {
549 Ok((mark_update, index_update, funding_update)) => {
550 Self::send_data(data_sender, Data::MarkPriceUpdate(mark_update));
551 Self::send_data(data_sender, Data::IndexPriceUpdate(index_update));
552 if let Err(e) = data_sender.send(DataEvent::FundingRate(funding_update))
553 {
554 log::error!("Failed to emit funding rate: {e}");
555 }
556 }
557 Err(e) => log::warn!("Failed to parse mark price: {e}"),
558 }
559 }
560 }
561 BinanceFuturesWsStreamsMessage::Kline(ref kline_msg) => {
562 if let Some(instrument) = cache.get(&kline_msg.symbol) {
563 match parse_kline(kline_msg, instrument, ts_init) {
564 Ok(Some(bar)) => Self::send_data(data_sender, Data::Bar(bar)),
565 Ok(None) => {} Err(e) => log::warn!("Failed to parse kline: {e}"),
567 }
568 }
569 }
570 BinanceFuturesWsStreamsMessage::ForceOrder(ref liq_msg) => {
571 if let Some(instrument) = cache.get(&liq_msg.order.symbol) {
572 let parse_price = |value: &str, field: &str| -> anyhow::Result<Price> {
573 parse_required_price_at_precision(
574 value,
575 instrument.price_precision(),
576 field,
577 )
578 };
579
580 let parse_quantity = |value: &str, field: &str| -> anyhow::Result<Quantity> {
581 parse_required_quantity_at_precision(
582 value,
583 instrument.size_precision(),
584 field,
585 )
586 };
587
588 match (
589 parse_price(&liq_msg.order.price, "price"),
590 parse_price(&liq_msg.order.average_price, "average_price"),
591 parse_quantity(&liq_msg.order.last_filled_qty, "last_filled_qty"),
592 parse_quantity(&liq_msg.order.accumulated_qty, "accumulated_qty"),
593 ) {
594 (
595 Ok(price),
596 Ok(average_price),
597 Ok(last_filled_qty),
598 Ok(accumulated_qty),
599 ) => {
600 let liquidation = Arc::new(BinanceFuturesLiquidation::new(
601 instrument.id(),
602 OrderSide::from(liq_msg.order.side),
603 price,
604 average_price,
605 last_filled_qty,
606 accumulated_qty,
607 UnixNanos::from_millis(liq_msg.event_time as u64),
608 ts_init,
609 ));
610
611 let has_all_market_subscription =
612 force_order_all_market_refs.load(Ordering::Relaxed) > 0;
613 let has_all_market_stream =
614 force_order_all_market_stream_active.load(Ordering::Acquire);
615 let has_specific_subscription =
616 force_order_refs.load().contains_key(&instrument.id());
617
618 if has_all_market_subscription || has_all_market_stream {
619 let data_type =
620 DataType::new("BinanceFuturesLiquidation", None, None);
621 Self::send_data(
622 data_sender,
623 Data::Custom(CustomData::new(liquidation, data_type)),
624 );
625 } else if has_specific_subscription {
626 let data_type = Self::liquidation_data_type(instrument.id());
627 Self::send_data(
628 data_sender,
629 Data::Custom(CustomData::new(liquidation, data_type)),
630 );
631 }
632 }
633 (p, ap, lq, aq) => {
634 log::warn!(
635 "Failed to parse Binance liquidation {}: price={:?} avg={:?} \
636 last_qty={:?} accumulated_qty={:?}",
637 liq_msg.order.symbol,
638 p.err(),
639 ap.err(),
640 lq.err(),
641 aq.err(),
642 );
643 }
644 }
645 } else {
646 log::warn!(
647 "Received Binance liquidation for uncached symbol {}",
648 liq_msg.order.symbol
649 );
650 }
651 }
652 BinanceFuturesWsStreamsMessage::Ticker(ref ticker_msg) => {
653 if let Some(instrument) = cache.get(&ticker_msg.symbol) {
654 let instrument_id = instrument.id();
655 if !ticker_refs.load().contains_key(&instrument_id) {
656 return;
657 }
658
659 match parse_ticker(ticker_msg, instrument, ts_init) {
660 Ok(ticker) => {
661 let data_type = ticker_data_type(instrument_id);
662 Self::send_data(
663 data_sender,
664 Data::Custom(CustomData::new(Arc::new(ticker), data_type)),
665 );
666 }
667 Err(e) => log::warn!("Failed to parse ticker: {e}"),
668 }
669 }
670 }
671 BinanceFuturesWsStreamsMessage::AccountUpdate(_)
673 | BinanceFuturesWsStreamsMessage::OrderUpdate(_)
674 | BinanceFuturesWsStreamsMessage::TradeLite(_)
675 | BinanceFuturesWsStreamsMessage::AlgoUpdate(_)
676 | BinanceFuturesWsStreamsMessage::MarginCall(_)
677 | BinanceFuturesWsStreamsMessage::AccountConfigUpdate(_)
678 | BinanceFuturesWsStreamsMessage::ListenKeyExpired => {}
679 BinanceFuturesWsStreamsMessage::Error(e) => {
680 log::warn!(
681 "Binance Futures WebSocket error: code={}, msg={}",
682 e.code,
683 e.msg
684 );
685 }
686 BinanceFuturesWsStreamsMessage::Reconnected => {
687 log::info!("WebSocket reconnected, rebuilding order book snapshots");
688
689 let epoch = {
690 let mut guard = book_epoch.write().expect(MUTEX_POISONED);
691 *guard = guard.wrapping_add(1);
692 *guard
693 };
694
695 let subs: Vec<(InstrumentId, u32)> = {
696 let guard = book_subscriptions.load();
697 guard.iter().map(|(k, v)| (*k, *v)).collect()
698 };
699
700 for (instrument_id, depth) in subs {
701 book_buffers.insert(instrument_id, BookBuffer::new(epoch));
702
703 log::debug!(
704 "OrderBook snapshot rebuild for {instrument_id} @ depth {depth} \
705 starting (reconnect, epoch={epoch})"
706 );
707
708 let http = http_client.clone();
709 let sender = data_sender.clone();
710 let buffers = book_buffers.clone();
711 let insts = instruments.clone();
712
713 get_runtime().spawn(async move {
714 Self::fetch_and_emit_snapshot(
715 http,
716 sender,
717 buffers,
718 insts,
719 instrument_id,
720 depth,
721 epoch,
722 clock,
723 )
724 .await;
725 });
726 }
727 }
728 }
729 }
730
731 #[expect(clippy::too_many_arguments)]
732 async fn fetch_and_emit_snapshot(
733 http: BinanceFuturesHttpClient,
734 sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
735 buffers: Arc<AtomicMap<InstrumentId, BookBuffer>>,
736 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
737 instrument_id: InstrumentId,
738 depth: u32,
739 epoch: u64,
740 clock: &'static AtomicTime,
741 ) {
742 Self::fetch_and_emit_snapshot_inner(
743 http,
744 sender,
745 buffers,
746 instruments,
747 instrument_id,
748 depth,
749 epoch,
750 clock,
751 0,
752 )
753 .await;
754 }
755
756 #[expect(clippy::too_many_arguments)]
757 async fn fetch_and_emit_snapshot_inner(
758 http: BinanceFuturesHttpClient,
759 sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
760 buffers: Arc<AtomicMap<InstrumentId, BookBuffer>>,
761 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
762 instrument_id: InstrumentId,
763 depth: u32,
764 epoch: u64,
765 clock: &'static AtomicTime,
766 retry_count: u32,
767 ) {
768 if wait_for_buffered_update(&buffers, instrument_id, epoch)
769 .await
770 .is_none()
771 {
772 return;
773 }
774
775 let symbol = format_binance_stream_symbol(&instrument_id).to_uppercase();
776 let params = BinanceDepthParams {
777 symbol,
778 limit: Some(depth),
779 };
780
781 match http.depth(¶ms).await {
782 Ok(order_book) => {
783 let ts_init = clock.get_time_ns();
784 let last_update_id = order_book.last_update_id as u64;
785
786 {
788 let guard = buffers.load();
789 match guard.get(&instrument_id) {
790 None => {
791 log::debug!(
792 "OrderBook subscription for {instrument_id} was cancelled, \
793 discarding snapshot"
794 );
795 return;
796 }
797 Some(buffer) if buffer.epoch != epoch => {
798 log::debug!(
799 "OrderBook snapshot for {instrument_id} is stale \
800 (epoch {epoch} != {}), discarding",
801 buffer.epoch
802 );
803 return;
804 }
805 _ => {}
806 }
807 }
808
809 let (price_precision, size_precision) = {
811 let guard = instruments.load();
812 match guard.get(&instrument_id) {
813 Some(inst) => (inst.price_precision(), inst.size_precision()),
814 None => {
815 log::error!("No instrument in cache for snapshot: {instrument_id}");
816 buffers.remove(&instrument_id);
817 return;
818 }
819 }
820 };
821
822 let Some(first) = wait_for_first_applicable_update(
823 &buffers,
824 instrument_id,
825 epoch,
826 last_update_id,
827 )
828 .await
829 else {
830 return;
831 };
832
833 let target = last_update_id;
836 let valid_overlap =
837 first.first_update_id <= target && first.final_update_id >= target;
838
839 if !valid_overlap {
840 if retry_count < MAX_SNAPSHOT_RETRIES {
841 log::warn!(
842 "OrderBook overlap validation failed for {instrument_id}: \
843 lastUpdateId={last_update_id}, first_update_id={}, \
844 final_update_id={} (need U <= {} <= u), \
845 retrying snapshot (attempt {}/{})",
846 first.first_update_id,
847 first.final_update_id,
848 target,
849 retry_count + 1,
850 MAX_SNAPSHOT_RETRIES
851 );
852
853 tokio::time::sleep(futures_snapshot_retry_backoff(retry_count)).await;
854
855 Box::pin(Self::fetch_and_emit_snapshot_inner(
856 http,
857 sender,
858 buffers,
859 instruments,
860 instrument_id,
861 depth,
862 epoch,
863 clock,
864 retry_count + 1,
865 ))
866 .await;
867 return;
868 }
869 log::error!(
870 "OrderBook overlap validation failed for {instrument_id} after \
871 {MAX_SNAPSHOT_RETRIES} retries; book may be inconsistent"
872 );
873 }
874
875 let snapshot_deltas = parse_order_book_snapshot(
876 &order_book,
877 instrument_id,
878 price_precision,
879 size_precision,
880 ts_init,
881 );
882
883 let buffered = {
885 let mut taken = Vec::new();
886 let mut should_return = false;
887 buffers.rcu(|m| {
888 taken = Vec::new();
889 should_return = false;
890
891 match m.get_mut(&instrument_id) {
892 Some(buffer) if buffer.epoch == epoch => {
893 taken = std::mem::take(&mut buffer.updates);
894 }
895 _ => should_return = true,
896 }
897 });
898
899 if should_return {
900 return;
901 }
902 taken
903 };
904
905 let mut replayed = 0;
907 let mut last_final_update_id = last_update_id;
908 let mut is_first = true;
909 let mut replay_ready = Vec::with_capacity(buffered.len());
910
911 for update in buffered {
912 if update.final_update_id < last_update_id {
913 continue;
914 }
915
916 if update.final_update_id == last_update_id {
917 last_final_update_id = update.final_update_id;
918 is_first = false;
919 continue;
920 }
921
922 if !is_first && update.prev_final_update_id != last_final_update_id {
925 if retry_count < MAX_SNAPSHOT_RETRIES {
926 log::warn!(
927 "OrderBook continuity break for {instrument_id}: \
928 expected pu={last_final_update_id}, was pu={}, \
929 triggering resync (attempt {}/{})",
930 update.prev_final_update_id,
931 retry_count + 1,
932 MAX_SNAPSHOT_RETRIES
933 );
934
935 reset_book_sync_buffer(&buffers, instrument_id, epoch);
936 tokio::time::sleep(futures_snapshot_retry_backoff(retry_count)).await;
937
938 Box::pin(Self::fetch_and_emit_snapshot_inner(
939 http,
940 sender,
941 buffers,
942 instruments,
943 instrument_id,
944 depth,
945 epoch,
946 clock,
947 retry_count + 1,
948 ))
949 .await;
950 return;
951 }
952 log::error!(
953 "OrderBook continuity break for {instrument_id} after \
954 {MAX_SNAPSHOT_RETRIES} retries: expected pu={last_final_update_id}, \
955 was pu={}; book may be inconsistent",
956 update.prev_final_update_id
957 );
958 }
959
960 last_final_update_id = update.final_update_id;
961 is_first = false;
962 replayed += 1;
963 replay_ready.push(update);
964 }
965
966 if let Err(e) = sender.send(DataEvent::Data(Data::Deltas(
967 OrderBookDeltas_API::new(snapshot_deltas),
968 ))) {
969 log::error!("Failed to send snapshot: {e}");
970 }
971
972 for update in replay_ready {
973 if let Err(e) = sender.send(DataEvent::Data(Data::Deltas(
974 OrderBookDeltas_API::new(update.deltas),
975 ))) {
976 log::error!("Failed to send replayed deltas: {e}");
977 }
978 }
979
980 loop {
982 let more = {
983 let mut taken = Vec::new();
984 let mut should_break = false;
985 buffers.rcu(|m| {
986 taken = Vec::new();
987 should_break = false;
988
989 match m.get_mut(&instrument_id) {
990 Some(buffer) if buffer.epoch == epoch => {
991 if buffer.updates.is_empty() {
992 m.remove(&instrument_id);
993 should_break = true;
994 } else {
995 taken = std::mem::take(&mut buffer.updates);
996 }
997 }
998 _ => should_break = true,
999 }
1000 });
1001
1002 if should_break {
1003 break;
1004 }
1005 taken
1006 };
1007
1008 for update in more {
1009 if update.final_update_id <= last_update_id {
1010 continue;
1011 }
1012
1013 if update.prev_final_update_id != last_final_update_id {
1014 if retry_count < MAX_SNAPSHOT_RETRIES {
1015 log::warn!(
1016 "OrderBook continuity break for {instrument_id}: \
1017 expected pu={last_final_update_id}, was pu={}, \
1018 triggering resync (attempt {}/{})",
1019 update.prev_final_update_id,
1020 retry_count + 1,
1021 MAX_SNAPSHOT_RETRIES
1022 );
1023
1024 reset_book_sync_buffer(&buffers, instrument_id, epoch);
1025 tokio::time::sleep(futures_snapshot_retry_backoff(retry_count))
1026 .await;
1027
1028 Box::pin(Self::fetch_and_emit_snapshot_inner(
1029 http,
1030 sender,
1031 buffers,
1032 instruments,
1033 instrument_id,
1034 depth,
1035 epoch,
1036 clock,
1037 retry_count + 1,
1038 ))
1039 .await;
1040 return;
1041 }
1042 log::error!(
1043 "OrderBook continuity break for {instrument_id} after \
1044 {MAX_SNAPSHOT_RETRIES} retries; book may be inconsistent"
1045 );
1046 }
1047
1048 last_final_update_id = update.final_update_id;
1049 replayed += 1;
1050
1051 if let Err(e) = sender.send(DataEvent::Data(Data::Deltas(
1052 OrderBookDeltas_API::new(update.deltas),
1053 ))) {
1054 log::error!("Failed to send replayed deltas: {e}");
1055 }
1056 }
1057 }
1058
1059 log::debug!(
1060 "OrderBook snapshot rebuild for {instrument_id} completed \
1061 (lastUpdateId={last_update_id}, replayed={replayed})"
1062 );
1063 }
1064 Err(e) => {
1065 if retry_count < MAX_SNAPSHOT_RETRIES {
1066 log::warn!(
1067 "Failed to request order book snapshot for {instrument_id}: {e}; \
1068 retrying snapshot (attempt {}/{})",
1069 retry_count + 1,
1070 MAX_SNAPSHOT_RETRIES
1071 );
1072
1073 tokio::time::sleep(futures_snapshot_retry_backoff(retry_count)).await;
1074
1075 Box::pin(Self::fetch_and_emit_snapshot_inner(
1076 http,
1077 sender,
1078 buffers,
1079 instruments,
1080 instrument_id,
1081 depth,
1082 epoch,
1083 clock,
1084 retry_count + 1,
1085 ))
1086 .await;
1087 return;
1088 }
1089
1090 log::error!(
1091 "Failed to request order book snapshot for {instrument_id} after \
1092 {MAX_SNAPSHOT_RETRIES} retries: {e}"
1093 );
1094 buffers.remove(&instrument_id);
1095 }
1096 }
1097 }
1098}
1099
1100fn upsert_instrument(
1101 cache: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
1102 instrument: InstrumentAny,
1103) {
1104 cache.insert(instrument.id(), instrument);
1105}
1106
1107fn reset_book_sync_buffer(
1108 buffers: &Arc<AtomicMap<InstrumentId, BookBuffer>>,
1109 instrument_id: InstrumentId,
1110 epoch: u64,
1111) {
1112 buffers.rcu(|m| {
1113 if let Some(buffer) = m.get_mut(&instrument_id)
1114 && buffer.epoch == epoch
1115 {
1116 buffer.updates.clear();
1117 }
1118 });
1119}
1120
1121fn trim_buffered_depth_updates(updates: &mut Vec<BufferedDepthUpdate>) {
1122 let excess = updates.len().saturating_sub(MAX_BUFFERED_DEPTH_UPDATES);
1123 if excess > 0 {
1124 updates.drain(..excess);
1125 }
1126}
1127
1128async fn wait_for_buffered_update(
1129 buffers: &Arc<AtomicMap<InstrumentId, BookBuffer>>,
1130 instrument_id: InstrumentId,
1131 epoch: u64,
1132) -> Option<()> {
1133 loop {
1134 let guard = buffers.load();
1135 match guard.get(&instrument_id) {
1136 Some(buffer) if buffer.epoch == epoch && !buffer.updates.is_empty() => return Some(()),
1137 Some(buffer) if buffer.epoch == epoch => {}
1138 _ => return None,
1139 }
1140
1141 drop(guard);
1142 tokio::time::sleep(Duration::from_millis(100)).await;
1143 }
1144}
1145
1146async fn wait_for_first_applicable_update(
1147 buffers: &Arc<AtomicMap<InstrumentId, BookBuffer>>,
1148 instrument_id: InstrumentId,
1149 epoch: u64,
1150 last_update_id: u64,
1151) -> Option<BufferedDepthUpdate> {
1152 loop {
1153 let mut first = None;
1154 let mut waiting = false;
1155 buffers.rcu(|m| {
1156 first = None;
1157 waiting = false;
1158
1159 if let Some(buffer) = m.get_mut(&instrument_id)
1160 && buffer.epoch == epoch
1161 {
1162 buffer
1163 .updates
1164 .retain(|update| update.final_update_id >= last_update_id);
1165 first = buffer
1166 .updates
1167 .iter()
1168 .find(|update| update.final_update_id >= last_update_id)
1169 .cloned();
1170 waiting = first.is_none();
1171 }
1172 });
1173
1174 if first.is_some() {
1175 return first;
1176 }
1177
1178 if !waiting {
1179 return None;
1180 }
1181
1182 tokio::time::sleep(Duration::from_millis(100)).await;
1183 }
1184}
1185
1186fn futures_snapshot_retry_backoff(retry_count: u32) -> Duration {
1187 let multiplier = 1_u64 << retry_count.min(4);
1188 let millis = SNAPSHOT_RETRY_BACKOFF_BASE_MS
1189 .saturating_mul(multiplier)
1190 .min(SNAPSHOT_RETRY_BACKOFF_CAP_MS);
1191 Duration::from_millis(millis)
1192}
1193
1194fn parse_order_book_snapshot(
1195 order_book: &BinanceOrderBook,
1196 instrument_id: InstrumentId,
1197 price_precision: u8,
1198 size_precision: u8,
1199 ts_init: UnixNanos,
1200) -> OrderBookDeltas {
1201 let sequence = order_book.last_update_id as u64;
1202 let ts_event = order_book.transaction_time.map_or(ts_init, |t| {
1203 UnixNanos::from((t as u64) * NANOSECONDS_IN_MILLISECOND)
1204 });
1205
1206 let total_levels = order_book.bids.len() + order_book.asks.len();
1207 let mut deltas = Vec::with_capacity(total_levels + 1);
1208
1209 deltas.push(OrderBookDelta::clear(
1211 instrument_id,
1212 sequence,
1213 ts_event,
1214 ts_init,
1215 ));
1216
1217 for (price_str, qty_str) in &order_book.bids {
1218 let Some(price) = parse_price_at_precision(price_str, price_precision) else {
1219 log::warn!(
1220 "Skipping Futures order book bid level for {instrument_id}: invalid or \
1221 non-positive price='{price_str}'"
1222 );
1223 continue;
1224 };
1225 let Some(size) = parse_quantity_at_precision(qty_str, size_precision) else {
1226 log::warn!(
1227 "Skipping Futures order book bid level for {instrument_id}: invalid or \
1228 non-positive quantity='{qty_str}'"
1229 );
1230 continue;
1231 };
1232
1233 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
1234
1235 deltas.push(OrderBookDelta::new(
1236 instrument_id,
1237 BookAction::Add,
1238 order,
1239 0,
1240 sequence,
1241 ts_event,
1242 ts_init,
1243 ));
1244 }
1245
1246 for (price_str, qty_str) in &order_book.asks {
1247 let Some(price) = parse_price_at_precision(price_str, price_precision) else {
1248 log::warn!(
1249 "Skipping Futures order book ask level for {instrument_id}: invalid or \
1250 non-positive price='{price_str}'"
1251 );
1252 continue;
1253 };
1254 let Some(size) = parse_quantity_at_precision(qty_str, size_precision) else {
1255 log::warn!(
1256 "Skipping Futures order book ask level for {instrument_id}: invalid or \
1257 non-positive quantity='{qty_str}'"
1258 );
1259 continue;
1260 };
1261
1262 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
1263
1264 deltas.push(OrderBookDelta::new(
1265 instrument_id,
1266 BookAction::Add,
1267 order,
1268 0,
1269 sequence,
1270 ts_event,
1271 ts_init,
1272 ));
1273 }
1274
1275 if let Some(delta) = deltas.last_mut() {
1276 delta.flags |= RecordFlag::F_LAST as u8;
1277 }
1278
1279 OrderBookDeltas::new(instrument_id, deltas)
1280}
1281
1282#[async_trait::async_trait(?Send)]
1283impl DataClient for BinanceFuturesDataClient {
1284 fn client_id(&self) -> ClientId {
1285 self.client_id
1286 }
1287
1288 fn venue(&self) -> Option<Venue> {
1289 Some(self.venue())
1290 }
1291
1292 fn start(&mut self) -> anyhow::Result<()> {
1293 log::info!(
1294 "Started: client_id={}, product_type={:?}, environment={:?}",
1295 self.client_id,
1296 self.product_type,
1297 self.config.environment,
1298 );
1299 Ok(())
1300 }
1301
1302 fn stop(&mut self) -> anyhow::Result<()> {
1303 log::info!("Stopping {id}", id = self.client_id);
1304 self.cancellation_token.cancel();
1305 self.is_connected.store(false, Ordering::Relaxed);
1306 Ok(())
1307 }
1308
1309 fn reset(&mut self) -> anyhow::Result<()> {
1310 log::debug!("Resetting {id}", id = self.client_id);
1311
1312 self.cancellation_token.cancel();
1313
1314 for task in self.tasks.drain(..) {
1315 task.abort();
1316 }
1317
1318 let mut ws = self.ws_client.clone();
1319 get_runtime().spawn(async move {
1320 let _ = ws.close().await;
1321 });
1322
1323 self.mark_price_refs.store(AHashMap::new());
1325 self.ticker_refs.store(AHashMap::new());
1326 self.force_order_refs.store(AHashMap::new());
1327 self.force_order_all_market_refs.store(0, Ordering::Relaxed);
1328 self.force_order_all_market_stream_active
1329 .store(false, Ordering::Release);
1330 self.book_subscriptions.store(AHashMap::new());
1331 self.book_buffers.store(AHashMap::new());
1332
1333 self.is_connected.store(false, Ordering::Relaxed);
1334 self.cancellation_token = CancellationToken::new();
1335 Ok(())
1336 }
1337
1338 fn dispose(&mut self) -> anyhow::Result<()> {
1339 log::debug!("Disposing {id}", id = self.client_id);
1340 self.stop()
1341 }
1342
1343 async fn connect(&mut self) -> anyhow::Result<()> {
1344 if self.is_connected() {
1345 return Ok(());
1346 }
1347
1348 register_binance_custom_data();
1349
1350 self.cancellation_token = CancellationToken::new();
1352
1353 let instruments = self
1354 .http_client
1355 .request_instruments()
1356 .await
1357 .context("failed to request Binance Futures instruments")?;
1358
1359 {
1361 let mut inst_map = AHashMap::new();
1362 let mut status_map = AHashMap::new();
1363
1364 for instrument in &instruments {
1365 inst_map.insert(instrument.id(), instrument.clone());
1366 }
1367
1368 let http_instruments = self.http_client.instruments_cache();
1369 for entry in http_instruments.iter() {
1370 let raw_symbol = entry.key();
1371 let action = match entry.value() {
1372 crate::futures::http::client::BinanceFuturesInstrument::UsdM(s) => {
1373 MarketStatusAction::from(s.status)
1374 }
1375 crate::futures::http::client::BinanceFuturesInstrument::CoinM(s) => s
1376 .contract_status
1377 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into),
1378 };
1379
1380 for instrument in &instruments {
1381 if instrument.raw_symbol().as_str() == raw_symbol.as_str() {
1382 status_map.insert(instrument.id(), action);
1383 break;
1384 }
1385 }
1386 }
1387
1388 self.instruments.store(inst_map);
1389 self.status_cache.store(status_map);
1390 }
1391
1392 for instrument in instruments.clone() {
1393 if let Err(e) = self.data_sender.send(DataEvent::Instrument(instrument)) {
1394 log::warn!("Failed to send instrument: {e}");
1395 }
1396 }
1397
1398 self.ws_client.cache_instruments(&instruments);
1399 self.ws_public_client.cache_instruments(&instruments);
1400
1401 log::info!("Connecting to Binance Futures market WebSocket...");
1402 self.ws_client.connect().await.map_err(|e| {
1403 log::error!("Binance Futures market WebSocket connection failed: {e:?}");
1404 anyhow::anyhow!("failed to connect Binance Futures market WebSocket: {e}")
1405 })?;
1406 log::info!("Binance Futures market WebSocket connected");
1407
1408 log::info!("Connecting to Binance Futures public WebSocket...");
1409 self.ws_public_client.connect().await.map_err(|e| {
1410 log::error!("Binance Futures public WebSocket connection failed: {e:?}");
1411 anyhow::anyhow!("failed to connect Binance Futures public WebSocket: {e}")
1412 })?;
1413 log::info!("Binance Futures public WebSocket connected");
1414
1415 let stream = self.ws_client.stream();
1417 let sender = self.data_sender.clone();
1418 let insts = self.instruments.clone();
1419 let ws_insts = self.ws_client.instruments_cache();
1420 let buffers = self.book_buffers.clone();
1421 let book_subs = self.book_subscriptions.clone();
1422 let force_order_refs = self.force_order_refs.clone();
1423 let ticker_refs = self.ticker_refs.clone();
1424 let force_order_all_market_refs = self.force_order_all_market_refs.clone();
1425 let force_order_all_market_stream_active =
1426 self.force_order_all_market_stream_active.clone();
1427 let book_epoch = self.book_epoch.clone();
1428 let http = self.http_client.clone();
1429 let clock = self.clock;
1430 let cancel = self.cancellation_token.clone();
1431
1432 let handle = get_runtime().spawn(async move {
1433 pin_mut!(stream);
1434
1435 loop {
1436 tokio::select! {
1437 Some(message) = stream.next() => {
1438 Self::handle_ws_message(
1439 message,
1440 &sender,
1441 &insts,
1442 &ws_insts,
1443 &buffers,
1444 &book_subs,
1445 &force_order_refs,
1446 &ticker_refs,
1447 &force_order_all_market_refs,
1448 &force_order_all_market_stream_active,
1449 &book_epoch,
1450 &http,
1451 clock,
1452 );
1453 }
1454 () = cancel.cancelled() => {
1455 log::debug!("Market WebSocket stream task cancelled");
1456 break;
1457 }
1458 }
1459 }
1460 });
1461 self.tasks.push(handle);
1462
1463 let pub_stream = self.ws_public_client.stream();
1465 let pub_sender = self.data_sender.clone();
1466 let pub_insts = self.instruments.clone();
1467 let pub_ws_insts = self.ws_public_client.instruments_cache();
1468 let pub_buffers = self.book_buffers.clone();
1469 let pub_book_subs = self.book_subscriptions.clone();
1470 let pub_force_order_refs = self.force_order_refs.clone();
1471 let pub_ticker_refs = self.ticker_refs.clone();
1472 let pub_force_order_all_market_refs = self.force_order_all_market_refs.clone();
1473 let pub_force_order_all_market_stream_active =
1474 self.force_order_all_market_stream_active.clone();
1475 let pub_book_epoch = self.book_epoch.clone();
1476 let pub_http = self.http_client.clone();
1477 let pub_cancel = self.cancellation_token.clone();
1478
1479 let pub_handle = get_runtime().spawn(async move {
1480 pin_mut!(pub_stream);
1481
1482 loop {
1483 tokio::select! {
1484 Some(message) = pub_stream.next() => {
1485 Self::handle_ws_message(
1486 message,
1487 &pub_sender,
1488 &pub_insts,
1489 &pub_ws_insts,
1490 &pub_buffers,
1491 &pub_book_subs,
1492 &pub_force_order_refs,
1493 &pub_ticker_refs,
1494 &pub_force_order_all_market_refs,
1495 &pub_force_order_all_market_stream_active,
1496 &pub_book_epoch,
1497 &pub_http,
1498 clock,
1499 );
1500 }
1501 () = pub_cancel.cancelled() => {
1502 log::debug!("Public WebSocket stream task cancelled");
1503 break;
1504 }
1505 }
1506 }
1507 });
1508 self.tasks.push(pub_handle);
1509
1510 let poll_secs = self.config.instrument_status_poll_secs;
1512 if poll_secs > 0 {
1513 let poll_http = self.http_client.clone();
1514 let poll_sender = self.data_sender.clone();
1515 let poll_instruments = self.instruments.clone();
1516 let poll_status_cache = self.status_cache.clone();
1517 let poll_cancel = self.cancellation_token.clone();
1518 let poll_clock = self.clock;
1519
1520 let poll_handle = get_runtime().spawn(async move {
1521 let mut interval =
1522 tokio::time::interval(tokio::time::Duration::from_secs(poll_secs));
1523 interval.tick().await; loop {
1526 tokio::select! {
1527 _ = interval.tick() => {
1528 match poll_http.request_symbol_statuses().await {
1529 Ok(symbol_statuses) => {
1530 let ts = poll_clock.get_time_ns();
1531 let inst_guard = poll_instruments.load();
1532
1533 let raw_to_id: AHashMap<Ustr, InstrumentId> = inst_guard
1535 .values()
1536 .map(|inst| (inst.raw_symbol().inner(), inst.id()))
1537 .collect();
1538
1539 let mut new_statuses = AHashMap::new();
1540
1541 for (raw_symbol, action) in &symbol_statuses {
1542 if let Some(&id) = raw_to_id.get(raw_symbol) {
1543 new_statuses.insert(id, *action);
1544 }
1545 }
1546 drop(inst_guard);
1547
1548 let mut cache = (**poll_status_cache.load()).clone();
1549 diff_and_emit_statuses(
1550 &new_statuses, &mut cache, &poll_sender, ts, ts,
1551 );
1552 poll_status_cache.store(cache);
1553 }
1554 Err(e) => {
1555 log::warn!("Futures instrument status poll failed: {e}");
1556 }
1557 }
1558 }
1559 () = poll_cancel.cancelled() => {
1560 log::debug!("Futures instrument status polling task cancelled");
1561 break;
1562 }
1563 }
1564 }
1565 });
1566 self.tasks.push(poll_handle);
1567 log::debug!("Futures instrument status polling started: interval={poll_secs}s");
1568 }
1569
1570 self.is_connected.store(true, Ordering::Release);
1571 log::info!("Connected: client_id={}", self.client_id);
1572 Ok(())
1573 }
1574
1575 async fn disconnect(&mut self) -> anyhow::Result<()> {
1576 if self.is_disconnected() {
1577 return Ok(());
1578 }
1579
1580 self.cancellation_token.cancel();
1581
1582 let _ = self.ws_client.close().await;
1583 let _ = self.ws_public_client.close().await;
1584
1585 let handles: Vec<_> = self.tasks.drain(..).collect();
1586 for handle in handles {
1587 if let Err(e) = handle.await {
1588 log::error!("Error joining WebSocket task: {e}");
1589 }
1590 }
1591
1592 self.mark_price_refs.store(AHashMap::new());
1594 self.ticker_refs.store(AHashMap::new());
1595 self.force_order_refs.store(AHashMap::new());
1596 self.force_order_all_market_refs.store(0, Ordering::Relaxed);
1597 self.force_order_all_market_stream_active
1598 .store(false, Ordering::Release);
1599 self.book_subscriptions.store(AHashMap::new());
1600 self.book_buffers.store(AHashMap::new());
1601
1602 self.is_connected.store(false, Ordering::Release);
1603 log::info!("Disconnected: client_id={}", self.client_id);
1604 Ok(())
1605 }
1606
1607 fn is_connected(&self) -> bool {
1608 self.is_connected.load(Ordering::Relaxed)
1609 }
1610
1611 fn is_disconnected(&self) -> bool {
1612 !self.is_connected()
1613 }
1614
1615 fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
1616 let data_type = cmd.data_type.type_name();
1617 if data_type == "BinanceFuturesTicker" {
1618 return subscribe_ticker(self, &cmd.data_type);
1619 }
1620
1621 if data_type != "BinanceFuturesLiquidation" {
1622 log::warn!("Unsupported custom data subscription: {data_type}");
1623 return Ok(());
1624 }
1625
1626 let instrument_id = Self::custom_liquidation_instrument_id(&cmd.data_type)?;
1627 if let Some(instrument_id) = instrument_id {
1628 if instrument_id.venue != self.venue() {
1629 anyhow::bail!(
1630 "Binance liquidation custom data requires BINANCE venue instrument, received {instrument_id}"
1631 );
1632 }
1633
1634 let should_subscribe = {
1635 let prev = self
1636 .force_order_refs
1637 .load()
1638 .get(&instrument_id)
1639 .copied()
1640 .unwrap_or(0);
1641 self.force_order_refs.rcu(|m| {
1642 let count = m.entry(instrument_id).or_insert(0);
1643 *count += 1;
1644 });
1645 prev == 0
1646 };
1647
1648 let has_all_market_subscription =
1649 self.force_order_all_market_refs.load(Ordering::Relaxed) > 0;
1650 let has_all_market_stream = self
1651 .force_order_all_market_stream_active
1652 .load(Ordering::Acquire);
1653
1654 if should_subscribe && !has_all_market_subscription && !has_all_market_stream {
1655 let ws = self.ws_client.clone();
1656 let stream = Self::liquidation_stream(&instrument_id);
1657 self.spawn_ws(
1658 async move {
1659 ws.subscribe(vec![stream])
1660 .await
1661 .context("forceOrder subscription")
1662 },
1663 "forceOrder subscription",
1664 );
1665 } else if should_subscribe && !has_all_market_subscription {
1666 self.spawn_liquidation_stream_reconcile("forceOrder subscription restore");
1667 }
1668
1669 return Ok(());
1670 }
1671
1672 let should_subscribe = self
1673 .force_order_all_market_refs
1674 .fetch_add(1, Ordering::Relaxed)
1675 == 0;
1676
1677 if should_subscribe {
1678 self.spawn_liquidation_stream_reconcile("all-market forceOrder subscription");
1679 }
1680
1681 Ok(())
1682 }
1683
1684 fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
1685 log::debug!(
1686 "subscribe_instruments: Binance Futures instruments are fetched via HTTP on connect"
1687 );
1688 Ok(())
1689 }
1690
1691 fn subscribe_instrument(&mut self, _cmd: SubscribeInstrument) -> anyhow::Result<()> {
1692 log::debug!(
1693 "subscribe_instrument: Binance Futures instruments are fetched via HTTP on connect"
1694 );
1695 Ok(())
1696 }
1697
1698 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
1699 if cmd.book_type != BookType::L2_MBP {
1700 anyhow::bail!("Binance Futures only supports L2_MBP order book deltas");
1701 }
1702
1703 let instrument_id = cmd.instrument_id;
1704 let depth = cmd.depth.map_or(1000, |d| d.get() as u32);
1705
1706 if !BINANCE_BOOK_DEPTHS.contains(&depth) {
1707 anyhow::bail!(
1708 "Invalid depth {depth} for Binance Futures order book. \
1709 Valid values: {BINANCE_BOOK_DEPTHS:?}"
1710 );
1711 }
1712
1713 self.book_subscriptions.insert(instrument_id, depth);
1715
1716 let epoch = {
1718 let mut guard = self.book_epoch.write().expect(MUTEX_POISONED);
1719 *guard = guard.wrapping_add(1);
1720 *guard
1721 };
1722
1723 self.book_buffers
1725 .insert(instrument_id, BookBuffer::new(epoch));
1726
1727 log::debug!("OrderBook snapshot rebuild for {instrument_id} @ depth {depth} starting");
1728
1729 let ws = self.ws_public_client.clone();
1731 let stream = format!("{}@depth@0ms", format_binance_stream_symbol(&instrument_id));
1732
1733 self.spawn_ws(
1734 async move {
1735 ws.subscribe(vec![stream])
1736 .await
1737 .context("book deltas subscription")
1738 },
1739 "order book subscription",
1740 );
1741
1742 let http = self.http_client.clone();
1744 let sender = self.data_sender.clone();
1745 let buffers = self.book_buffers.clone();
1746 let instruments = self.instruments.clone();
1747 let clock = self.clock;
1748
1749 get_runtime().spawn(async move {
1750 Self::fetch_and_emit_snapshot(
1751 http,
1752 sender,
1753 buffers,
1754 instruments,
1755 instrument_id,
1756 depth,
1757 epoch,
1758 clock,
1759 )
1760 .await;
1761 });
1762
1763 Ok(())
1764 }
1765
1766 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
1767 let instrument_id = cmd.instrument_id;
1768 let ws = self.ws_public_client.clone();
1769
1770 let stream = format!(
1772 "{}@bookTicker",
1773 format_binance_stream_symbol(&instrument_id)
1774 );
1775
1776 self.spawn_ws(
1777 async move {
1778 ws.subscribe(vec![stream])
1779 .await
1780 .context("quotes subscription")
1781 },
1782 "quote subscription",
1783 );
1784 Ok(())
1785 }
1786
1787 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
1788 let instrument_id = cmd.instrument_id;
1789 let ws = self.ws_client.clone();
1790
1791 let stream = format!("{}@aggTrade", format_binance_stream_symbol(&instrument_id));
1793
1794 self.spawn_ws(
1795 async move {
1796 ws.subscribe(vec![stream])
1797 .await
1798 .context("trades subscription")
1799 },
1800 "trade subscription",
1801 );
1802 Ok(())
1803 }
1804
1805 fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
1806 let bar_type = cmd.bar_type;
1807 let ws = self.ws_client.clone();
1808 let interval = bar_spec_to_binance_interval(bar_type.spec())?;
1809
1810 let stream = format!(
1811 "{}@kline_{}",
1812 format_binance_stream_symbol(&bar_type.instrument_id()),
1813 interval.as_str()
1814 );
1815
1816 self.spawn_ws(
1817 async move {
1818 ws.subscribe(vec![stream])
1819 .await
1820 .context("bars subscription")
1821 },
1822 "bar subscription",
1823 );
1824 Ok(())
1825 }
1826
1827 fn subscribe_mark_prices(&mut self, cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
1828 let instrument_id = cmd.instrument_id;
1829
1830 let should_subscribe = {
1832 let prev = self
1833 .mark_price_refs
1834 .load()
1835 .get(&instrument_id)
1836 .copied()
1837 .unwrap_or(0);
1838 self.mark_price_refs.rcu(|m| {
1839 let count = m.entry(instrument_id).or_insert(0);
1840 *count += 1;
1841 });
1842 prev == 0
1843 };
1844
1845 if should_subscribe {
1846 let ws = self.ws_client.clone();
1847 let stream = format!(
1848 "{}@markPrice@1s",
1849 format_binance_stream_symbol(&instrument_id)
1850 );
1851
1852 self.spawn_ws(
1853 async move {
1854 ws.subscribe(vec![stream])
1855 .await
1856 .context("mark prices subscription")
1857 },
1858 "mark prices subscription",
1859 );
1860 }
1861 Ok(())
1862 }
1863
1864 fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
1865 let instrument_id = cmd.instrument_id;
1866
1867 let should_subscribe = {
1869 let prev = self
1870 .mark_price_refs
1871 .load()
1872 .get(&instrument_id)
1873 .copied()
1874 .unwrap_or(0);
1875 self.mark_price_refs.rcu(|m| {
1876 let count = m.entry(instrument_id).or_insert(0);
1877 *count += 1;
1878 });
1879 prev == 0
1880 };
1881
1882 if should_subscribe {
1883 let ws = self.ws_client.clone();
1884 let stream = format!(
1885 "{}@markPrice@1s",
1886 format_binance_stream_symbol(&instrument_id)
1887 );
1888
1889 self.spawn_ws(
1890 async move {
1891 ws.subscribe(vec![stream])
1892 .await
1893 .context("index prices subscription")
1894 },
1895 "index prices subscription",
1896 );
1897 }
1898 Ok(())
1899 }
1900
1901 fn subscribe_funding_rates(&mut self, cmd: SubscribeFundingRates) -> anyhow::Result<()> {
1902 let instrument_id = cmd.instrument_id;
1903
1904 let should_subscribe = {
1905 let prev = self
1906 .mark_price_refs
1907 .load()
1908 .get(&instrument_id)
1909 .copied()
1910 .unwrap_or(0);
1911 self.mark_price_refs.rcu(|m| {
1912 let count = m.entry(instrument_id).or_insert(0);
1913 *count += 1;
1914 });
1915 prev == 0
1916 };
1917
1918 if should_subscribe {
1919 let ws = self.ws_client.clone();
1920 let stream = format!(
1921 "{}@markPrice@1s",
1922 format_binance_stream_symbol(&instrument_id)
1923 );
1924
1925 self.spawn_ws(
1926 async move {
1927 ws.subscribe(vec![stream])
1928 .await
1929 .context("funding rates subscription")
1930 },
1931 "funding rates subscription",
1932 );
1933 }
1934 Ok(())
1935 }
1936
1937 fn subscribe_instrument_status(
1938 &mut self,
1939 cmd: SubscribeInstrumentStatus,
1940 ) -> anyhow::Result<()> {
1941 log::debug!(
1942 "subscribe_instrument_status: {id} (status changes detected via periodic exchange info polling)",
1943 id = cmd.instrument_id,
1944 );
1945 Ok(())
1946 }
1947
1948 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
1949 let instrument_id = cmd.instrument_id;
1950 let ws = self.ws_public_client.clone();
1951
1952 self.book_subscriptions.remove(&instrument_id);
1954
1955 self.book_buffers.remove(&instrument_id);
1957
1958 let symbol_lower = format_binance_stream_symbol(&instrument_id);
1959 let streams = vec![
1960 format!("{symbol_lower}@depth"),
1961 format!("{symbol_lower}@depth@0ms"),
1962 format!("{symbol_lower}@depth@100ms"),
1963 format!("{symbol_lower}@depth@500ms"),
1964 ];
1965
1966 self.spawn_ws(
1967 async move {
1968 ws.unsubscribe(streams)
1969 .await
1970 .context("book deltas unsubscribe")
1971 },
1972 "order book unsubscribe",
1973 );
1974 Ok(())
1975 }
1976
1977 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
1978 let instrument_id = cmd.instrument_id;
1979 let ws = self.ws_public_client.clone();
1980
1981 let stream = format!(
1982 "{}@bookTicker",
1983 format_binance_stream_symbol(&instrument_id)
1984 );
1985
1986 self.spawn_ws(
1987 async move {
1988 ws.unsubscribe(vec![stream])
1989 .await
1990 .context("quotes unsubscribe")
1991 },
1992 "quote unsubscribe",
1993 );
1994 Ok(())
1995 }
1996
1997 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1998 let instrument_id = cmd.instrument_id;
1999 let ws = self.ws_client.clone();
2000
2001 let stream = format!("{}@aggTrade", format_binance_stream_symbol(&instrument_id));
2002
2003 self.spawn_ws(
2004 async move {
2005 ws.unsubscribe(vec![stream])
2006 .await
2007 .context("trades unsubscribe")
2008 },
2009 "trade unsubscribe",
2010 );
2011 Ok(())
2012 }
2013
2014 fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
2015 let data_type = cmd.data_type.type_name();
2016 if data_type == "BinanceFuturesTicker" {
2017 return unsubscribe_ticker(self, &cmd.data_type);
2018 }
2019
2020 if data_type != "BinanceFuturesLiquidation" {
2021 log::warn!("Unsupported custom data unsubscription: {data_type}");
2022 return Ok(());
2023 }
2024
2025 let instrument_id = Self::custom_liquidation_instrument_id(&cmd.data_type)?;
2026 if let Some(instrument_id) = instrument_id {
2027 if instrument_id.venue != self.venue() {
2028 anyhow::bail!(
2029 "Binance liquidation custom data requires BINANCE venue instrument, received {instrument_id}"
2030 );
2031 }
2032
2033 let should_unsubscribe = {
2034 let prev = self.force_order_refs.load().get(&instrument_id).copied();
2035 match prev {
2036 Some(1) => {
2037 self.force_order_refs.remove(&instrument_id);
2038 true
2039 }
2040 Some(count) if count > 1 => {
2041 self.force_order_refs.rcu(|m| {
2042 if let Some(existing) = m.get_mut(&instrument_id) {
2043 *existing -= 1;
2044 }
2045 });
2046 false
2047 }
2048 _ => false,
2049 }
2050 };
2051
2052 let has_all_market_subscription =
2053 self.force_order_all_market_refs.load(Ordering::Relaxed) > 0;
2054 let has_all_market_stream = self
2055 .force_order_all_market_stream_active
2056 .load(Ordering::Acquire);
2057
2058 if should_unsubscribe && !has_all_market_subscription {
2059 let ws = self.ws_client.clone();
2060 let stream = Self::liquidation_stream(&instrument_id);
2061 let ws_lock = self.force_order_ws_lock.clone();
2062 self.spawn_ws(
2063 async move {
2064 let _guard = if has_all_market_stream {
2065 Some(ws_lock.lock().await)
2066 } else {
2067 None
2068 };
2069 ws.unsubscribe(vec![stream])
2070 .await
2071 .context("forceOrder unsubscribe")
2072 },
2073 "forceOrder unsubscribe",
2074 );
2075 }
2076
2077 return Ok(());
2078 }
2079
2080 let should_unsubscribe = self
2081 .force_order_all_market_refs
2082 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
2083 if current == 0 {
2084 None
2085 } else {
2086 Some(current - 1)
2087 }
2088 })
2089 .is_ok_and(|prev| prev == 1);
2090 if should_unsubscribe {
2091 self.spawn_liquidation_stream_reconcile("all-market forceOrder unsubscribe");
2092 }
2093
2094 Ok(())
2095 }
2096
2097 fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
2098 let bar_type = cmd.bar_type;
2099 let ws = self.ws_client.clone();
2100 let interval = bar_spec_to_binance_interval(bar_type.spec())?;
2101
2102 let stream = format!(
2103 "{}@kline_{}",
2104 format_binance_stream_symbol(&bar_type.instrument_id()),
2105 interval.as_str()
2106 );
2107
2108 self.spawn_ws(
2109 async move {
2110 ws.unsubscribe(vec![stream])
2111 .await
2112 .context("bars unsubscribe")
2113 },
2114 "bar unsubscribe",
2115 );
2116 Ok(())
2117 }
2118
2119 fn unsubscribe_mark_prices(&mut self, cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
2120 let instrument_id = cmd.instrument_id;
2121
2122 let should_unsubscribe = {
2124 let prev = self.mark_price_refs.load().get(&instrument_id).copied();
2125 match prev {
2126 Some(count) if count <= 1 => {
2127 self.mark_price_refs.remove(&instrument_id);
2128 true
2129 }
2130 Some(_) => {
2131 self.mark_price_refs.rcu(|m| {
2132 if let Some(count) = m.get_mut(&instrument_id) {
2133 *count = count.saturating_sub(1);
2134 }
2135 });
2136 false
2137 }
2138 None => false,
2139 }
2140 };
2141
2142 if should_unsubscribe {
2143 let ws = self.ws_client.clone();
2144 let symbol_lower = format_binance_stream_symbol(&instrument_id);
2145 let streams = vec![
2146 format!("{symbol_lower}@markPrice"),
2147 format!("{symbol_lower}@markPrice@1s"),
2148 format!("{symbol_lower}@markPrice@3s"),
2149 ];
2150
2151 self.spawn_ws(
2152 async move {
2153 ws.unsubscribe(streams)
2154 .await
2155 .context("mark prices unsubscribe")
2156 },
2157 "mark prices unsubscribe",
2158 );
2159 }
2160 Ok(())
2161 }
2162
2163 fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
2164 let instrument_id = cmd.instrument_id;
2165
2166 let should_unsubscribe = {
2168 let prev = self.mark_price_refs.load().get(&instrument_id).copied();
2169 match prev {
2170 Some(count) if count <= 1 => {
2171 self.mark_price_refs.remove(&instrument_id);
2172 true
2173 }
2174 Some(_) => {
2175 self.mark_price_refs.rcu(|m| {
2176 if let Some(count) = m.get_mut(&instrument_id) {
2177 *count = count.saturating_sub(1);
2178 }
2179 });
2180 false
2181 }
2182 None => false,
2183 }
2184 };
2185
2186 if should_unsubscribe {
2187 let ws = self.ws_client.clone();
2188 let symbol_lower = format_binance_stream_symbol(&instrument_id);
2189 let streams = vec![
2190 format!("{symbol_lower}@markPrice"),
2191 format!("{symbol_lower}@markPrice@1s"),
2192 format!("{symbol_lower}@markPrice@3s"),
2193 ];
2194
2195 self.spawn_ws(
2196 async move {
2197 ws.unsubscribe(streams)
2198 .await
2199 .context("index prices unsubscribe")
2200 },
2201 "index prices unsubscribe",
2202 );
2203 }
2204 Ok(())
2205 }
2206
2207 fn unsubscribe_funding_rates(&mut self, cmd: &UnsubscribeFundingRates) -> anyhow::Result<()> {
2208 let instrument_id = cmd.instrument_id;
2209
2210 let should_unsubscribe = {
2211 let prev = self.mark_price_refs.load().get(&instrument_id).copied();
2212 match prev {
2213 Some(count) if count <= 1 => {
2214 self.mark_price_refs.remove(&instrument_id);
2215 true
2216 }
2217 Some(_) => {
2218 self.mark_price_refs.rcu(|m| {
2219 if let Some(count) = m.get_mut(&instrument_id) {
2220 *count = count.saturating_sub(1);
2221 }
2222 });
2223 false
2224 }
2225 None => false,
2226 }
2227 };
2228
2229 if should_unsubscribe {
2230 let ws = self.ws_client.clone();
2231 let symbol_lower = format_binance_stream_symbol(&instrument_id);
2232 let streams = vec![
2233 format!("{symbol_lower}@markPrice"),
2234 format!("{symbol_lower}@markPrice@1s"),
2235 format!("{symbol_lower}@markPrice@3s"),
2236 ];
2237
2238 self.spawn_ws(
2239 async move {
2240 ws.unsubscribe(streams)
2241 .await
2242 .context("funding rates unsubscribe")
2243 },
2244 "funding rates unsubscribe",
2245 );
2246 }
2247 Ok(())
2248 }
2249
2250 fn unsubscribe_instrument_status(
2251 &mut self,
2252 cmd: &UnsubscribeInstrumentStatus,
2253 ) -> anyhow::Result<()> {
2254 log::debug!(
2255 "unsubscribe_instrument_status: {id}",
2256 id = cmd.instrument_id,
2257 );
2258 Ok(())
2259 }
2260
2261 fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
2262 let http = self.http_client.clone();
2263 let sender = self.data_sender.clone();
2264 let instruments_cache = self.instruments.clone();
2265 let request_id = request.request_id;
2266 let client_id = request.client_id.unwrap_or(self.client_id);
2267 let venue = self.venue();
2268 let start = request.start;
2269 let end = request.end;
2270 let params = request.params;
2271 let clock = self.clock;
2272 let start_nanos = datetime_to_unix_nanos(start);
2273 let end_nanos = datetime_to_unix_nanos(end);
2274
2275 get_runtime().spawn(async move {
2276 match http.request_instruments().await {
2277 Ok(instruments) => {
2278 for instrument in &instruments {
2279 upsert_instrument(&instruments_cache, instrument.clone());
2280 }
2281
2282 let response = DataResponse::Instruments(InstrumentsResponse::new(
2283 request_id,
2284 client_id,
2285 venue,
2286 instruments,
2287 start_nanos,
2288 end_nanos,
2289 clock.get_time_ns(),
2290 params,
2291 ));
2292
2293 if let Err(e) = sender.send(DataEvent::Response(response)) {
2294 log::error!("Failed to send instruments response: {e}");
2295 }
2296 }
2297 Err(e) => log::error!("Instruments request failed: {e:?}"),
2298 }
2299 });
2300
2301 Ok(())
2302 }
2303
2304 fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
2305 let http = self.http_client.clone();
2306 let sender = self.data_sender.clone();
2307 let instruments = self.instruments.clone();
2308 let instrument_id = request.instrument_id;
2309 let request_id = request.request_id;
2310 let client_id = request.client_id.unwrap_or(self.client_id);
2311 let start = request.start;
2312 let end = request.end;
2313 let params = request.params;
2314 let clock = self.clock;
2315 let start_nanos = datetime_to_unix_nanos(start);
2316 let end_nanos = datetime_to_unix_nanos(end);
2317
2318 get_runtime().spawn(async move {
2319 match http.request_instruments().await {
2320 Ok(all_instruments) => {
2321 for instrument in &all_instruments {
2322 upsert_instrument(&instruments, instrument.clone());
2323 }
2324
2325 let instrument = all_instruments
2326 .into_iter()
2327 .find(|i| i.id() == instrument_id);
2328
2329 if let Some(instrument) = instrument {
2330 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
2331 request_id,
2332 client_id,
2333 instrument.id(),
2334 instrument,
2335 start_nanos,
2336 end_nanos,
2337 clock.get_time_ns(),
2338 params,
2339 )));
2340
2341 if let Err(e) = sender.send(DataEvent::Response(response)) {
2342 log::error!("Failed to send instrument response: {e}");
2343 }
2344 } else {
2345 log::error!("Instrument not found: {instrument_id}");
2346 }
2347 }
2348 Err(e) => log::error!("Instrument request failed: {e:?}"),
2349 }
2350 });
2351
2352 Ok(())
2353 }
2354
2355 fn request_data(&self, request: RequestCustomData) -> anyhow::Result<()> {
2360 let data_type = request.data_type.clone();
2361 let data_type_name = data_type.type_name().to_string();
2362
2363 if data_type_name != "BinanceFuturesOpenInterest"
2364 && data_type_name != "BinanceFuturesOpenInterestHist"
2365 {
2366 log::warn!("Unsupported custom data request: {data_type_name}");
2367 return Ok(());
2368 }
2369
2370 let instrument_id = Self::required_instrument_id_metadata(&data_type)?;
2371
2372 if instrument_id.venue != self.venue() {
2373 anyhow::bail!(
2374 "Binance Futures custom data requires BINANCE venue instrument, received {instrument_id}"
2375 );
2376 }
2377
2378 let period = if data_type_name == "BinanceFuturesOpenInterestHist" {
2379 Some(Self::required_period_metadata(&data_type)?)
2380 } else {
2381 None
2382 };
2383
2384 let http = self.http_client.clone();
2385 let sender = self.data_sender.clone();
2386 let request_id = request.request_id;
2387 let client_id = request.client_id;
2388 let params = request.params;
2389 let clock = self.clock;
2390 let venue = self.venue();
2391 let limit = request.limit.map(|n| n.get() as u32);
2392 let start_nanos = datetime_to_unix_nanos(request.start);
2393 let end_nanos = datetime_to_unix_nanos(request.end);
2394 let start_ms = request.start.map(|dt| dt.timestamp_millis());
2395 let end_ms = request.end.map(|dt| dt.timestamp_millis());
2396
2397 get_runtime().spawn(async move {
2398 let response = if data_type_name == "BinanceFuturesOpenInterest" {
2399 let response_data_type = data_type.clone();
2400 let query = BinanceOpenInterestParams {
2401 symbol: format_binance_symbol(&instrument_id),
2402 };
2403
2404 match http
2405 .open_interest(&query)
2406 .await
2407 .context("failed to request current open interest from Binance Futures")
2408 {
2409 Ok(open_interest) => {
2410 let ts_init = clock.get_time_ns();
2411 let open_interest_value = match Self::parse_open_interest_decimal(
2412 "open_interest",
2413 &open_interest.open_interest,
2414 ) {
2415 Ok(value) => value,
2416 Err(e) => {
2417 log::error!(
2418 "Current open interest request failed for {instrument_id}: {e:?}"
2419 );
2420 return;
2421 }
2422 };
2423 let ts_event =
2424 match Self::unix_nanos_from_millis_i64("time", open_interest.time) {
2425 Ok(value) => value,
2426 Err(e) => {
2427 log::error!(
2428 "Current open interest request failed for {instrument_id}: {e:?}"
2429 );
2430 return;
2431 }
2432 };
2433 let payload = Arc::new(BinanceFuturesOpenInterest::new(
2434 instrument_id,
2435 open_interest_value,
2436 ts_event,
2437 ts_init,
2438 ));
2439 let custom = CustomData::new(payload, response_data_type.clone());
2440
2441 Some(DataResponse::Data(CustomDataResponse::new(
2442 request_id,
2443 client_id,
2444 Some(venue),
2445 response_data_type,
2446 custom,
2447 start_nanos,
2448 end_nanos,
2449 ts_init,
2450 params,
2451 )))
2452 }
2453 Err(e) => {
2454 log::error!("Current open interest request failed for {instrument_id}: {e:?}");
2455 None
2456 }
2457 }
2458 } else {
2459 let response_data_type = data_type.clone();
2460 let period = period.expect("period required for historical open interest");
2461 let query = match http.product_type() {
2462 BinanceProductType::UsdM => BinanceOpenInterestHistParams {
2463 symbol: Some(format_binance_symbol(&instrument_id)),
2464 pair: None,
2465 contract_type: None,
2466 period: period.clone(),
2467 start_time: start_ms,
2468 end_time: end_ms,
2469 limit,
2470 },
2471 BinanceProductType::CoinM => {
2472 let (pair, contract_type) =
2473 match Self::coinm_open_interest_hist_params(&instrument_id) {
2474 Ok(values) => values,
2475 Err(e) => {
2476 log::error!(
2477 "Historical open interest request failed for {instrument_id}: {e:?}"
2478 );
2479 return;
2480 }
2481 };
2482 BinanceOpenInterestHistParams {
2483 symbol: None,
2484 pair: Some(pair),
2485 contract_type: Some(contract_type),
2486 period: period.clone(),
2487 start_time: start_ms,
2488 end_time: end_ms,
2489 limit,
2490 }
2491 }
2492 product_type => {
2493 log::error!(
2494 "Historical open interest request failed for {instrument_id}: unsupported product type {product_type:?}"
2495 );
2496 return;
2497 }
2498 };
2499
2500 match http
2501 .open_interest_hist(&query)
2502 .await
2503 .context("failed to request historical open interest from Binance Futures")
2504 {
2505 Ok(history) => {
2506 let ts_init = clock.get_time_ns();
2507 let points: Vec<BinanceFuturesOpenInterestHistPoint> = match history
2508 .into_iter()
2509 .map(|point| -> anyhow::Result<_> {
2510 Ok(BinanceFuturesOpenInterestHistPoint::new(
2511 Self::parse_open_interest_decimal(
2512 "sum_open_interest",
2513 &point.sum_open_interest,
2514 )?,
2515 Self::parse_open_interest_decimal(
2516 "sum_open_interest_value",
2517 &point.sum_open_interest_value,
2518 )?,
2519 Self::unix_nanos_from_millis_i64(
2520 "timestamp",
2521 point.timestamp,
2522 )?,
2523 ))
2524 })
2525 .collect()
2526 {
2527 Ok(points) => points,
2528 Err(e) => {
2529 log::error!(
2530 "Historical open interest request failed for {instrument_id}: {e:?}"
2531 );
2532 return;
2533 }
2534 };
2535 let ts_event = points.last().map_or(ts_init, |point| point.ts_event);
2536 let payload = Arc::new(BinanceFuturesOpenInterestHist::new(
2537 instrument_id,
2538 period,
2539 points,
2540 ts_event,
2541 ts_init,
2542 ));
2543 let custom = CustomData::new(payload, response_data_type.clone());
2544
2545 Some(DataResponse::Data(CustomDataResponse::new(
2546 request_id,
2547 client_id,
2548 Some(venue),
2549 response_data_type,
2550 custom,
2551 start_nanos,
2552 end_nanos,
2553 ts_init,
2554 params,
2555 )))
2556 }
2557 Err(e) => {
2558 log::error!(
2559 "Historical open interest request failed for {instrument_id}: {e:?}"
2560 );
2561 None
2562 }
2563 }
2564 };
2565
2566 if let Some(response) = response
2567 && let Err(e) = sender.send(DataEvent::Response(response))
2568 {
2569 log::error!("Failed to send custom data response: {e}");
2570 }
2571 });
2572
2573 Ok(())
2574 }
2575
2576 fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
2577 let http = self.http_client.clone();
2578 let sender = self.data_sender.clone();
2579 let instrument_id = request.instrument_id;
2580 let limit = request.limit.map(|n| n.get() as u32);
2581 let request_id = request.request_id;
2582 let client_id = request.client_id.unwrap_or(self.client_id);
2583 let params = request.params;
2584 let clock = self.clock;
2585 let start_nanos = datetime_to_unix_nanos(request.start);
2586 let end_nanos = datetime_to_unix_nanos(request.end);
2587
2588 get_runtime().spawn(async move {
2589 match http
2590 .request_trades(instrument_id, limit)
2591 .await
2592 .context("failed to request trades from Binance Futures")
2593 {
2594 Ok(trades) => {
2595 let response = DataResponse::Trades(TradesResponse::new(
2596 request_id,
2597 client_id,
2598 instrument_id,
2599 trades,
2600 start_nanos,
2601 end_nanos,
2602 clock.get_time_ns(),
2603 params,
2604 ));
2605
2606 if let Err(e) = sender.send(DataEvent::Response(response)) {
2607 log::error!("Failed to send trades response: {e}");
2608 }
2609 }
2610 Err(e) => log::error!("Trade request failed: {e:?}"),
2611 }
2612 });
2613
2614 Ok(())
2615 }
2616
2617 fn request_funding_rates(&self, request: RequestFundingRates) -> anyhow::Result<()> {
2618 let http = self.http_client.clone();
2619 let sender = self.data_sender.clone();
2620 let instrument_id = request.instrument_id;
2621 let start = request.start;
2622 let end = request.end;
2623 let limit = request.limit.map(|n| n.get() as u32);
2624 let request_id = request.request_id;
2625 let client_id = request.client_id.unwrap_or(self.client_id);
2626 let params = request.params;
2627 let clock = self.clock;
2628 let start_nanos = datetime_to_unix_nanos(start);
2629 let end_nanos = datetime_to_unix_nanos(end);
2630
2631 get_runtime().spawn(async move {
2632 match http
2633 .request_funding_rates(instrument_id, start, end, limit)
2634 .await
2635 .context("failed to request funding rates from Binance Futures")
2636 {
2637 Ok(funding_rates) => {
2638 let response = DataResponse::FundingRates(FundingRatesResponse::new(
2639 request_id,
2640 client_id,
2641 instrument_id,
2642 funding_rates,
2643 start_nanos,
2644 end_nanos,
2645 clock.get_time_ns(),
2646 params,
2647 ));
2648
2649 if let Err(e) = sender.send(DataEvent::Response(response)) {
2650 log::error!("Failed to send funding rates response: {e}");
2651 }
2652 }
2653 Err(e) => log::error!("Funding rates request failed for {instrument_id}: {e:?}"),
2654 }
2655 });
2656
2657 Ok(())
2658 }
2659
2660 fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
2661 let http = self.http_client.clone();
2662 let sender = self.data_sender.clone();
2663 let bar_type = request.bar_type;
2664 let start = request.start;
2665 let end = request.end;
2666 let limit = request.limit.map(|n| n.get() as u32);
2667 let request_id = request.request_id;
2668 let client_id = request.client_id.unwrap_or(self.client_id);
2669 let params = request.params;
2670 let clock = self.clock;
2671 let start_nanos = datetime_to_unix_nanos(start);
2672 let end_nanos = datetime_to_unix_nanos(end);
2673
2674 get_runtime().spawn(async move {
2675 match http
2676 .request_bars(bar_type, start, end, limit)
2677 .await
2678 .context("failed to request bars from Binance Futures")
2679 {
2680 Ok(bars) => {
2681 let response = DataResponse::Bars(BarsResponse::new(
2682 request_id,
2683 client_id,
2684 bar_type,
2685 bars,
2686 start_nanos,
2687 end_nanos,
2688 clock.get_time_ns(),
2689 params,
2690 ));
2691
2692 if let Err(e) = sender.send(DataEvent::Response(response)) {
2693 log::error!("Failed to send bars response: {e}");
2694 }
2695 }
2696 Err(e) => log::error!("Bar request failed: {e:?}"),
2697 }
2698 });
2699
2700 Ok(())
2701 }
2702}
2703
2704fn subscribe_ticker(client: &BinanceFuturesDataClient, data_type: &DataType) -> anyhow::Result<()> {
2705 let instrument_id = BinanceFuturesDataClient::required_instrument_id_metadata(data_type)?;
2706 if instrument_id.venue != client.venue() {
2707 anyhow::bail!(
2708 "Binance Futures ticker custom data requires BINANCE venue instrument, received {instrument_id}"
2709 );
2710 }
2711
2712 let should_subscribe = {
2713 let prev = client
2714 .ticker_refs
2715 .load()
2716 .get(&instrument_id)
2717 .copied()
2718 .unwrap_or(0);
2719 client.ticker_refs.rcu(|m| {
2720 let count = m.entry(instrument_id).or_insert(0);
2721 *count += 1;
2722 });
2723 prev == 0
2724 };
2725
2726 if should_subscribe {
2727 let ws = client.ws_client.clone();
2728 let stream = ticker_stream(&instrument_id);
2729 client.spawn_ws(
2730 async move {
2731 ws.subscribe(vec![stream])
2732 .await
2733 .context("ticker subscription")
2734 },
2735 "ticker subscription",
2736 );
2737 }
2738
2739 Ok(())
2740}
2741
2742fn unsubscribe_ticker(
2743 client: &BinanceFuturesDataClient,
2744 data_type: &DataType,
2745) -> anyhow::Result<()> {
2746 let instrument_id = BinanceFuturesDataClient::required_instrument_id_metadata(data_type)?;
2747 if instrument_id.venue != client.venue() {
2748 anyhow::bail!(
2749 "Binance Futures ticker custom data requires BINANCE venue instrument, received {instrument_id}"
2750 );
2751 }
2752
2753 let should_unsubscribe = {
2754 let prev = client.ticker_refs.load().get(&instrument_id).copied();
2755 match prev {
2756 Some(count) if count <= 1 => {
2757 client.ticker_refs.remove(&instrument_id);
2758 true
2759 }
2760 Some(_) => {
2761 client.ticker_refs.rcu(|m| {
2762 if let Some(count) = m.get_mut(&instrument_id) {
2763 *count = count.saturating_sub(1);
2764 }
2765 });
2766 false
2767 }
2768 None => false,
2769 }
2770 };
2771
2772 if should_unsubscribe {
2773 let ws = client.ws_client.clone();
2774 let stream = ticker_stream(&instrument_id);
2775 client.spawn_ws(
2776 async move {
2777 ws.unsubscribe(vec![stream])
2778 .await
2779 .context("ticker unsubscribe")
2780 },
2781 "ticker unsubscribe",
2782 );
2783 }
2784
2785 Ok(())
2786}
2787
2788fn ticker_data_type(instrument_id: InstrumentId) -> DataType {
2789 let mut metadata = Params::new();
2790 metadata.insert(
2791 "instrument_id".to_string(),
2792 serde_json::Value::String(instrument_id.to_string()),
2793 );
2794 DataType::new(
2795 "BinanceFuturesTicker",
2796 Some(metadata),
2797 Some(instrument_id.to_string()),
2798 )
2799}
2800
2801fn ticker_stream(instrument_id: &InstrumentId) -> String {
2802 format!("{}@ticker", format_binance_stream_symbol(instrument_id))
2803}
2804
2805#[cfg(test)]
2806mod tests {
2807 use rstest::rstest;
2808 use rust_decimal_macros::dec;
2809
2810 use super::*;
2811
2812 #[rstest]
2813 #[case(0, 250)]
2814 #[case(1, 500)]
2815 #[case(2, 1_000)]
2816 #[case(3, 2_000)]
2817 #[case(4, 3_000)]
2818 #[case(5, 3_000)]
2819 fn test_snapshot_retry_backoff_exponentially_increases_then_caps(
2820 #[case] retry_count: u32,
2821 #[case] expected_ms: u64,
2822 ) {
2823 assert_eq!(
2824 futures_snapshot_retry_backoff(retry_count),
2825 Duration::from_millis(expected_ms)
2826 );
2827 }
2828
2829 #[rstest]
2830 fn test_parse_order_book_snapshot_skips_invalid_levels() {
2831 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2832 let order_book = BinanceOrderBook {
2833 last_update_id: 10,
2834 bids: vec![
2835 ("not-a-price".to_string(), "1.0".to_string()),
2836 ("100.00".to_string(), "0.5".to_string()),
2837 ],
2838 asks: vec![
2839 ("101.00".to_string(), "not-a-quantity".to_string()),
2840 ("102.00".to_string(), "0.7".to_string()),
2841 ],
2842 event_time: None,
2843 transaction_time: None,
2844 };
2845
2846 let deltas =
2847 parse_order_book_snapshot(&order_book, instrument_id, 2, 3, UnixNanos::from(1));
2848
2849 assert_eq!(deltas.deltas.len(), 3);
2850 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy);
2851 assert_eq!(deltas.deltas[1].order.price.as_decimal(), dec!(100.00));
2852 assert_eq!(deltas.deltas[1].order.size.as_decimal(), dec!(0.500));
2853 assert_eq!(deltas.deltas[2].order.side, OrderSide::Sell);
2854 assert_eq!(deltas.deltas[2].order.price.as_decimal(), dec!(102.00));
2855 assert_eq!(deltas.deltas[2].order.size.as_decimal(), dec!(0.700));
2856 assert_eq!(deltas.deltas[2].flags, RecordFlag::F_LAST as u8);
2857 }
2858
2859 #[rstest]
2860 fn test_parse_order_book_snapshot_all_invalid_levels_marks_clear_last() {
2861 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
2862 let order_book = BinanceOrderBook {
2863 last_update_id: 10,
2864 bids: vec![("not-a-price".to_string(), "1.0".to_string())],
2865 asks: vec![("101.00".to_string(), "not-a-quantity".to_string())],
2866 event_time: None,
2867 transaction_time: None,
2868 };
2869
2870 let deltas =
2871 parse_order_book_snapshot(&order_book, instrument_id, 2, 3, UnixNanos::from(1));
2872
2873 assert_eq!(deltas.deltas.len(), 1);
2874 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
2875 assert_eq!(
2876 deltas.deltas[0].flags,
2877 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
2878 );
2879 }
2880}