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