Skip to main content

nautilus_binance/futures/
data.rs

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