Skip to main content

nautilus_binance/futures/
data.rs

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