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