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