Skip to main content

nautilus_binance/futures/websocket/streams/
parse_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//! Parsing for Binance Futures WebSocket JSON messages.
17
18use std::str::FromStr;
19
20use nautilus_core::nanos::UnixNanos;
21use nautilus_model::{
22    data::{
23        BarSpecification, BarType, BookOrder, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate,
24        OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick,
25    },
26    enums::{
27        AggregationSource, AggressorSide, BarAggregation, BookAction, OrderSide, PriceType,
28        RecordFlag,
29    },
30    identifiers::TradeId,
31    instruments::{Instrument, InstrumentAny},
32    types::Price,
33};
34use rust_decimal::Decimal;
35use ustr::Ustr;
36
37use super::{
38    error::{BinanceWsError, BinanceWsResult},
39    messages::{
40        BinanceFuturesAggTradeMsg, BinanceFuturesBookTickerMsg, BinanceFuturesDepthUpdateMsg,
41        BinanceFuturesKlineMsg, BinanceFuturesMarkPriceMsg, BinanceFuturesTickerMsg,
42        BinanceFuturesTradeMsg,
43    },
44};
45use crate::{
46    common::{
47        bar::BinanceBar,
48        enums::{BinanceKlineInterval, BinanceWsEventType},
49        parse::{
50            parse_millis, parse_millis_or_init, parse_required_price_at_precision,
51            parse_required_quantity_at_precision,
52        },
53    },
54    data_types::{BinanceFuturesMarkPriceUpdate, BinanceFuturesTicker},
55};
56
57/// Parses an aggregate trade message into a `TradeTick`.
58///
59/// # Errors
60///
61/// Returns an error if parsing fails.
62pub fn parse_agg_trade(
63    msg: &BinanceFuturesAggTradeMsg,
64    instrument: &InstrumentAny,
65    ts_init: UnixNanos,
66) -> BinanceWsResult<TradeTick> {
67    let instrument_id = instrument.id();
68    let price_precision = instrument.price_precision();
69    let size_precision = instrument.size_precision();
70
71    let price = parse_required_price_at_precision(&msg.price, price_precision, "price")
72        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
73    let size = parse_required_quantity_at_precision(&msg.quantity, size_precision, "quantity")
74        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
75
76    let aggressor_side = if msg.is_buyer_maker {
77        AggressorSide::Sell
78    } else {
79        AggressorSide::Buy
80    };
81
82    let ts_event = parse_millis_or_init(msg.trade_time, "Futures aggregate trade time", ts_init);
83    let trade_id = TradeId::new(msg.agg_trade_id.to_string());
84
85    Ok(TradeTick::new(
86        instrument_id,
87        price,
88        size,
89        aggressor_side,
90        trade_id,
91        ts_event,
92        ts_init,
93    ))
94}
95
96/// Parses a trade message into a `TradeTick`.
97///
98/// # Errors
99///
100/// Returns an error if parsing fails.
101pub fn parse_trade(
102    msg: &BinanceFuturesTradeMsg,
103    instrument: &InstrumentAny,
104    ts_init: UnixNanos,
105) -> BinanceWsResult<TradeTick> {
106    let instrument_id = instrument.id();
107    let price_precision = instrument.price_precision();
108    let size_precision = instrument.size_precision();
109
110    let price = parse_required_price_at_precision(&msg.price, price_precision, "price")
111        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
112    let size = parse_required_quantity_at_precision(&msg.quantity, size_precision, "quantity")
113        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
114
115    let aggressor_side = if msg.is_buyer_maker {
116        AggressorSide::Sell
117    } else {
118        AggressorSide::Buy
119    };
120
121    let ts_event = parse_millis_or_init(msg.trade_time, "Futures trade time", ts_init);
122    let trade_id = TradeId::new(msg.trade_id.to_string());
123
124    Ok(TradeTick::new(
125        instrument_id,
126        price,
127        size,
128        aggressor_side,
129        trade_id,
130        ts_event,
131        ts_init,
132    ))
133}
134
135/// Parses a book ticker message into a `QuoteTick`.
136///
137/// # Errors
138///
139/// Returns an error if parsing fails.
140pub fn parse_book_ticker(
141    msg: &BinanceFuturesBookTickerMsg,
142    instrument: &InstrumentAny,
143    ts_init: UnixNanos,
144) -> BinanceWsResult<QuoteTick> {
145    let instrument_id = instrument.id();
146    let price_precision = instrument.price_precision();
147    let size_precision = instrument.size_precision();
148
149    let bid_price =
150        parse_required_price_at_precision(&msg.best_bid_price, price_precision, "best_bid_price")
151            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
152    let bid_size =
153        parse_required_quantity_at_precision(&msg.best_bid_qty, size_precision, "best_bid_qty")
154            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
155    let ask_price =
156        parse_required_price_at_precision(&msg.best_ask_price, price_precision, "best_ask_price")
157            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
158    let ask_size =
159        parse_required_quantity_at_precision(&msg.best_ask_qty, size_precision, "best_ask_qty")
160            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
161
162    let ts_event = parse_millis_or_init(
163        msg.transaction_time,
164        "Futures book ticker transaction time",
165        ts_init,
166    );
167
168    Ok(QuoteTick::new(
169        instrument_id,
170        bid_price,
171        ask_price,
172        bid_size,
173        ask_size,
174        ts_event,
175        ts_init,
176    ))
177}
178
179/// Parses a depth update message into `OrderBookDeltas`.
180///
181/// # Errors
182///
183/// Returns an error if parsing fails.
184pub fn parse_depth_update(
185    msg: &BinanceFuturesDepthUpdateMsg,
186    instrument: &InstrumentAny,
187    ts_init: UnixNanos,
188) -> BinanceWsResult<OrderBookDeltas> {
189    parse_book_depth(msg, instrument, ts_init, false)
190}
191
192pub(crate) fn parse_depth_snapshot(
193    msg: &BinanceFuturesDepthUpdateMsg,
194    instrument: &InstrumentAny,
195    ts_init: UnixNanos,
196) -> BinanceWsResult<OrderBookDeltas> {
197    parse_book_depth(msg, instrument, ts_init, true)
198}
199
200fn parse_book_depth(
201    msg: &BinanceFuturesDepthUpdateMsg,
202    instrument: &InstrumentAny,
203    ts_init: UnixNanos,
204    snapshot: bool,
205) -> BinanceWsResult<OrderBookDeltas> {
206    let instrument_id = instrument.id();
207    let price_precision = instrument.price_precision();
208    let size_precision = instrument.size_precision();
209
210    let ts_event = parse_millis_or_init(
211        msg.transaction_time,
212        "Futures depth update transaction time",
213        ts_init,
214    );
215
216    let mut deltas = Vec::with_capacity(msg.bids.len() + msg.asks.len() + usize::from(snapshot));
217    if snapshot {
218        deltas.push(OrderBookDelta::clear(
219            instrument_id,
220            msg.final_update_id,
221            ts_event,
222            ts_init,
223        ));
224    }
225
226    // Process bids
227    for (i, bid) in msg.bids.iter().enumerate() {
228        let price = parse_required_price_at_precision(&bid[0], price_precision, "bid_price")
229            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
230        let size = parse_required_quantity_at_precision(&bid[1], size_precision, "bid_quantity")
231            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
232
233        let action = if size.is_zero() {
234            BookAction::Delete
235        } else if snapshot {
236            BookAction::Add
237        } else {
238            BookAction::Update
239        };
240
241        let is_last = i == msg.bids.len() - 1 && msg.asks.is_empty();
242        let flags = if is_last { RecordFlag::F_LAST as u8 } else { 0 };
243
244        let order = BookOrder::new(OrderSide::Buy, price, size, 0);
245
246        deltas.push(OrderBookDelta::new(
247            instrument_id,
248            action,
249            order,
250            flags,
251            msg.final_update_id,
252            ts_event,
253            ts_init,
254        ));
255    }
256
257    // Process asks
258    for (i, ask) in msg.asks.iter().enumerate() {
259        let price = parse_required_price_at_precision(&ask[0], price_precision, "ask_price")
260            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
261        let size = parse_required_quantity_at_precision(&ask[1], size_precision, "ask_quantity")
262            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
263
264        let action = if size.is_zero() {
265            BookAction::Delete
266        } else if snapshot {
267            BookAction::Add
268        } else {
269            BookAction::Update
270        };
271
272        let is_last = i == msg.asks.len() - 1;
273        let flags = if is_last { RecordFlag::F_LAST as u8 } else { 0 };
274
275        let order = BookOrder::new(OrderSide::Sell, price, size, 0);
276
277        deltas.push(OrderBookDelta::new(
278            instrument_id,
279            action,
280            order,
281            flags,
282            msg.final_update_id,
283            ts_event,
284            ts_init,
285        ));
286    }
287
288    if snapshot && let Some(last) = deltas.last_mut() {
289        last.flags |= RecordFlag::F_LAST as u8;
290    }
291
292    Ok(OrderBookDeltas::new(instrument_id, deltas))
293}
294
295/// Parses a mark price message into `MarkPriceUpdate`, `IndexPriceUpdate`, and `FundingRateUpdate`.
296///
297/// # Errors
298///
299/// Returns an error if parsing fails.
300pub fn parse_mark_price(
301    msg: &BinanceFuturesMarkPriceMsg,
302    instrument: &InstrumentAny,
303    ts_init: UnixNanos,
304) -> BinanceWsResult<(
305    MarkPriceUpdate,
306    IndexPriceUpdate,
307    FundingRateUpdate,
308    BinanceFuturesMarkPriceUpdate,
309)> {
310    let instrument_id = instrument.id();
311    let price_precision = instrument.price_precision();
312
313    let mark_price =
314        parse_required_price_at_precision(&msg.mark_price, price_precision, "mark_price")
315            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
316    let index_price =
317        parse_required_price_at_precision(&msg.index_price, price_precision, "index_price")
318            .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
319    let estimated_settle_price = msg
320        .estimated_settle_price
321        .parse::<Decimal>()
322        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
323    let estimated_settle_price = Price::from_decimal_dp(estimated_settle_price, price_precision)
324        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
325    let funding_rate = msg
326        .funding_rate
327        .parse::<Decimal>()
328        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
329
330    let ts_event = parse_millis_or_init(msg.event_time, "Futures mark price event time", ts_init);
331    let next_funding_ns = if msg.next_funding_time > 0 {
332        match parse_millis(
333            msg.next_funding_time,
334            "Futures mark price next funding time",
335        ) {
336            Ok(timestamp) => Some(timestamp),
337            Err(e) => {
338                log::warn!("{e}; omitting next funding time");
339                None
340            }
341        }
342    } else {
343        None
344    };
345
346    let mark_update = MarkPriceUpdate::new(instrument_id, mark_price, ts_event, ts_init);
347
348    let index_update = IndexPriceUpdate::new(instrument_id, index_price, ts_event, ts_init);
349
350    let funding_update = FundingRateUpdate::new(
351        instrument_id,
352        funding_rate,
353        None, // Binance does not provide the funding interval through WebSocket API
354        next_funding_ns,
355        ts_event,
356        ts_init,
357    );
358
359    let custom_update = BinanceFuturesMarkPriceUpdate {
360        instrument_id,
361        mark_price,
362        index_price,
363        estimated_settle_price,
364        funding_rate,
365        next_funding_time: next_funding_ns,
366        ts_event,
367        ts_init,
368    };
369
370    Ok((mark_update, index_update, funding_update, custom_update))
371}
372
373/// Parses a 24-hour ticker message into `BinanceFuturesTicker` custom data.
374///
375/// # Errors
376///
377/// Returns an error if parsing fails.
378pub fn parse_ticker(
379    msg: &BinanceFuturesTickerMsg,
380    instrument: &InstrumentAny,
381    ts_init: UnixNanos,
382) -> BinanceWsResult<BinanceFuturesTicker> {
383    Ok(BinanceFuturesTicker::new(
384        instrument.id(),
385        parse_ticker_decimal("price_change", &msg.price_change)?,
386        parse_ticker_decimal("price_change_percent", &msg.price_change_percent)?,
387        parse_ticker_decimal("weighted_avg_price", &msg.weighted_avg_price)?,
388        parse_ticker_decimal("last_price", &msg.last_price)?,
389        parse_ticker_decimal("last_qty", &msg.last_qty)?,
390        parse_ticker_decimal("open_price", &msg.open_price)?,
391        parse_ticker_decimal("high_price", &msg.high_price)?,
392        parse_ticker_decimal("low_price", &msg.low_price)?,
393        parse_ticker_decimal("volume", &msg.volume)?,
394        parse_ticker_decimal("quote_volume", &msg.quote_volume)?,
395        parse_millis_or_init(msg.open_time, "Futures ticker open time", ts_init),
396        parse_millis_or_init(msg.close_time, "Futures ticker close time", ts_init),
397        msg.first_trade_id,
398        msg.last_trade_id,
399        msg.num_trades,
400        parse_millis_or_init(msg.event_time, "Futures ticker event time", ts_init),
401        ts_init,
402    ))
403}
404
405fn parse_ticker_decimal(field: &str, value: &str) -> BinanceWsResult<Decimal> {
406    Decimal::from_str(value).map_err(|e| {
407        BinanceWsError::ParseError(format!("invalid Binance ticker {field}='{value}': {e}"))
408    })
409}
410
411/// Converts a Binance kline interval to a Nautilus `BarSpecification`.
412fn interval_to_bar_spec(interval: BinanceKlineInterval) -> BarSpecification {
413    match interval {
414        BinanceKlineInterval::Second1 => {
415            BarSpecification::new(1, BarAggregation::Second, PriceType::Last)
416        }
417        BinanceKlineInterval::Minute1 => {
418            BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
419        }
420        BinanceKlineInterval::Minute3 => {
421            BarSpecification::new(3, BarAggregation::Minute, PriceType::Last)
422        }
423        BinanceKlineInterval::Minute5 => {
424            BarSpecification::new(5, BarAggregation::Minute, PriceType::Last)
425        }
426        BinanceKlineInterval::Minute15 => {
427            BarSpecification::new(15, BarAggregation::Minute, PriceType::Last)
428        }
429        BinanceKlineInterval::Minute30 => {
430            BarSpecification::new(30, BarAggregation::Minute, PriceType::Last)
431        }
432        BinanceKlineInterval::Hour1 => {
433            BarSpecification::new(1, BarAggregation::Hour, PriceType::Last)
434        }
435        BinanceKlineInterval::Hour2 => {
436            BarSpecification::new(2, BarAggregation::Hour, PriceType::Last)
437        }
438        BinanceKlineInterval::Hour4 => {
439            BarSpecification::new(4, BarAggregation::Hour, PriceType::Last)
440        }
441        BinanceKlineInterval::Hour6 => {
442            BarSpecification::new(6, BarAggregation::Hour, PriceType::Last)
443        }
444        BinanceKlineInterval::Hour8 => {
445            BarSpecification::new(8, BarAggregation::Hour, PriceType::Last)
446        }
447        BinanceKlineInterval::Hour12 => {
448            BarSpecification::new(12, BarAggregation::Hour, PriceType::Last)
449        }
450        BinanceKlineInterval::Day1 => {
451            BarSpecification::new(1, BarAggregation::Day, PriceType::Last)
452        }
453        BinanceKlineInterval::Day3 => {
454            BarSpecification::new(3, BarAggregation::Day, PriceType::Last)
455        }
456        BinanceKlineInterval::Week1 => {
457            BarSpecification::new(1, BarAggregation::Week, PriceType::Last)
458        }
459        BinanceKlineInterval::Month1 => {
460            BarSpecification::new(1, BarAggregation::Month, PriceType::Last)
461        }
462    }
463}
464
465/// Parses a kline message into a `Bar`.
466///
467/// Returns `None` if the kline is not closed yet.
468///
469/// # Errors
470///
471/// Returns an error if parsing fails.
472pub fn parse_kline(
473    msg: &BinanceFuturesKlineMsg,
474    instrument: &InstrumentAny,
475    ts_init: UnixNanos,
476) -> BinanceWsResult<Option<BinanceBar>> {
477    // Only emit bars when the kline is closed
478    if !msg.kline.is_closed {
479        return Ok(None);
480    }
481
482    let instrument_id = instrument.id();
483    let price_precision = instrument.price_precision();
484    let size_precision = instrument.size_precision();
485
486    let spec = interval_to_bar_spec(msg.kline.interval);
487    let bar_type = BarType::new(instrument_id, spec, AggregationSource::External);
488
489    let price = |field: &str, value: &str| {
490        parse_required_price_at_precision(value, price_precision, field)
491            .map_err(|e| BinanceWsError::ParseError(e.to_string()))
492    };
493    let quantity = |field: &str, value: &str| {
494        parse_required_quantity_at_precision(value, size_precision, field)
495            .map_err(|e| BinanceWsError::ParseError(e.to_string()))
496    };
497    let decimal = |field: &str, value: &str| {
498        Decimal::from_str(value)
499            .map_err(|e| BinanceWsError::ParseError(format!("invalid {field} `{value}`: {e}")))
500    };
501    let count = u64::try_from(msg.kline.num_trades)
502        .map_err(|e| BinanceWsError::ParseError(e.to_string()))?;
503
504    // Use the kline close time as the event timestamp
505    let ts_event = parse_millis_or_init(msg.kline.close_time, "Futures kline close time", ts_init);
506
507    let bar = BinanceBar::new(
508        bar_type,
509        price("open", &msg.kline.open)?,
510        price("high", &msg.kline.high)?,
511        price("low", &msg.kline.low)?,
512        price("close", &msg.kline.close)?,
513        quantity("volume", &msg.kline.volume)?,
514        decimal("quote volume", &msg.kline.quote_volume)?,
515        count,
516        decimal("taker buy base volume", &msg.kline.taker_buy_volume)?,
517        decimal("taker buy quote volume", &msg.kline.taker_buy_quote_volume)?,
518        ts_event,
519        ts_init,
520    );
521
522    Ok(Some(bar))
523}
524
525/// Extracts the symbol from a raw JSON message.
526pub fn extract_symbol(json: &serde_json::Value) -> Option<Ustr> {
527    json.get("s").and_then(|v| v.as_str()).map(Ustr::from)
528}
529
530/// Extracts the event type from a raw JSON message.
531pub fn extract_event_type(json: &serde_json::Value) -> Option<BinanceWsEventType> {
532    json.get("e")
533        .and_then(|v| serde_json::from_value(v.clone()).ok())
534}
535
536#[cfg(test)]
537mod tests {
538    use nautilus_model::{enums::BookType, orderbook::OrderBook, types::Quantity};
539    use rstest::rstest;
540    use rust_decimal_macros::dec;
541    use serde::de::DeserializeOwned;
542    use serde_json::json;
543
544    use super::*;
545    use crate::{
546        common::{
547            enums::{BinanceOrderStatus, BinanceSide, BinanceTradingStatus},
548            parse::parse_usdm_instrument,
549            testing::{load_fixture_string, load_json_fixture},
550        },
551        futures::{
552            http::models::BinanceFuturesUsdSymbol,
553            websocket::streams::messages::{BinanceFuturesLiquidationMsg, BinanceFuturesTickerMsg},
554        },
555    };
556
557    const PRICE_PRECISION: u8 = 8;
558    const SIZE_PRECISION: u8 = 3;
559
560    fn sample_futures_symbol() -> BinanceFuturesUsdSymbol {
561        BinanceFuturesUsdSymbol {
562            symbol: Ustr::from("BTCUSDT"),
563            pair: Ustr::from("BTCUSDT"),
564            contract_type: "PERPETUAL".to_string(),
565            delivery_date: 4_133_404_800_000,
566            onboard_date: 1_569_398_400_000,
567            status: BinanceTradingStatus::Trading,
568            maint_margin_percent: "2.5000".to_string(),
569            required_margin_percent: "5.0000".to_string(),
570            base_asset: Ustr::from("BTC"),
571            quote_asset: Ustr::from("USDT"),
572            margin_asset: Ustr::from("USDT"),
573            price_precision: PRICE_PRECISION as i32,
574            quantity_precision: SIZE_PRECISION as i32,
575            base_asset_precision: 8,
576            quote_precision: 8,
577            underlying_type: Some("COIN".to_string()),
578            underlying_sub_type: vec!["PoW".to_string()],
579            settle_plan: None,
580            trigger_protect: Some("0.0500".to_string()),
581            liquidation_fee: Some("0.012500".to_string()),
582            market_take_bound: Some("0.05".to_string()),
583            order_types: vec!["LIMIT".to_string(), "MARKET".to_string()],
584            time_in_force: vec!["GTC".to_string(), "IOC".to_string()],
585            filters: vec![
586                json!({
587                    "filterType": "PRICE_FILTER",
588                    "tickSize": "0.00000001",
589                    "maxPrice": "1000000",
590                    "minPrice": "0.00000001"
591                }),
592                json!({
593                    "filterType": "LOT_SIZE",
594                    "stepSize": "0.001",
595                    "maxQty": "1000",
596                    "minQty": "0.001"
597                }),
598            ],
599        }
600    }
601
602    fn sample_instrument() -> InstrumentAny {
603        let ts = UnixNanos::from(1_700_000_000_000_000_000u64);
604        parse_usdm_instrument(&sample_futures_symbol(), ts, ts).unwrap()
605    }
606
607    fn load_market_fixture<T: DeserializeOwned>(filename: &str) -> T {
608        let path = format!("futures/market_data_json/{filename}");
609        serde_json::from_str(&load_fixture_string(&path))
610            .unwrap_or_else(|e| panic!("Failed to parse fixture {path}: {e}"))
611    }
612
613    #[rstest]
614    fn test_parse_agg_trade() {
615        let instrument = sample_instrument();
616        let msg: BinanceFuturesAggTradeMsg = load_market_fixture("agg_trade_stream.json");
617        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
618
619        let trade = parse_agg_trade(&msg, &instrument, ts_init).unwrap();
620
621        assert_eq!(trade.instrument_id, instrument.id());
622        assert_eq!(trade.price, Price::new(0.001, PRICE_PRECISION));
623        assert_eq!(trade.size, Quantity::new(100.0, SIZE_PRECISION));
624        assert_eq!(trade.aggressor_side, AggressorSide::Sell);
625        assert_eq!(trade.trade_id, TradeId::new("5933014"));
626        assert_eq!(trade.ts_event, UnixNanos::from(123_456_785_000_000u64));
627        assert_eq!(trade.ts_init, ts_init);
628    }
629
630    #[rstest]
631    #[case::negative(-1)]
632    #[case::overflow(i64::MAX)]
633    fn test_parse_agg_trade_falls_back_for_invalid_timestamp(#[case] trade_time: i64) {
634        let instrument = sample_instrument();
635        let mut msg: BinanceFuturesAggTradeMsg = load_market_fixture("agg_trade_stream.json");
636        msg.trade_time = trade_time;
637
638        let ts_init = UnixNanos::from(1);
639        let trade = parse_agg_trade(&msg, &instrument, ts_init).unwrap();
640
641        assert_eq!(trade.ts_event, ts_init);
642        assert_eq!(trade.ts_init, ts_init);
643    }
644
645    #[rstest]
646    fn test_parse_trade() {
647        let instrument = sample_instrument();
648        let msg: BinanceFuturesTradeMsg = load_market_fixture("trade_stream.json");
649        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
650
651        let trade = parse_trade(&msg, &instrument, ts_init).unwrap();
652
653        assert_eq!(trade.instrument_id, instrument.id());
654        assert_eq!(trade.price, Price::new(0.001, PRICE_PRECISION));
655        assert_eq!(trade.size, Quantity::new(100.0, SIZE_PRECISION));
656        assert_eq!(trade.aggressor_side, AggressorSide::Sell);
657        assert_eq!(trade.trade_id, TradeId::new("5933014"));
658        assert_eq!(trade.ts_event, UnixNanos::from(123_456_785_000_000u64));
659        assert_eq!(trade.ts_init, ts_init);
660    }
661
662    #[rstest]
663    fn test_parse_book_ticker() {
664        let instrument = sample_instrument();
665        let msg: BinanceFuturesBookTickerMsg = load_market_fixture("book_ticker_stream.json");
666        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
667
668        let quote = parse_book_ticker(&msg, &instrument, ts_init).unwrap();
669
670        assert_eq!(quote.instrument_id, instrument.id());
671        assert_eq!(quote.bid_price, Price::new(25.3519, PRICE_PRECISION));
672        assert_eq!(quote.ask_price, Price::new(25.3652, PRICE_PRECISION));
673        assert_eq!(quote.bid_size, Quantity::new(31.21, SIZE_PRECISION));
674        assert_eq!(quote.ask_size, Quantity::new(40.66, SIZE_PRECISION));
675        assert_eq!(
676            quote.ts_event,
677            UnixNanos::from(1_568_014_460_891_000_000u64)
678        );
679        assert_eq!(quote.ts_init, ts_init);
680    }
681
682    #[rstest]
683    fn test_parse_depth_update() {
684        let instrument = sample_instrument();
685        let msg: BinanceFuturesDepthUpdateMsg = load_market_fixture("depth_update_stream.json");
686        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
687
688        let deltas = parse_depth_update(&msg, &instrument, ts_init).unwrap();
689
690        assert_eq!(deltas.instrument_id, instrument.id());
691        assert_eq!(deltas.deltas.len(), 2);
692        assert_eq!(deltas.sequence, 160);
693        assert_eq!(deltas.ts_event, UnixNanos::from(123_456_788_000_000u64));
694        assert_eq!(deltas.ts_init, ts_init);
695        assert_eq!(deltas.deltas[0].action, BookAction::Update);
696        assert_eq!(deltas.deltas[0].order.side, OrderSide::Buy.into());
697        assert_eq!(
698            deltas.deltas[0].order.price,
699            Price::new(0.0024, PRICE_PRECISION)
700        );
701        assert_eq!(
702            deltas.deltas[0].order.size,
703            Quantity::new(10.0, SIZE_PRECISION)
704        );
705        assert_eq!(deltas.deltas[1].action, BookAction::Update);
706        assert_eq!(deltas.deltas[1].order.side, OrderSide::Sell.into());
707        assert_eq!(
708            deltas.deltas[1].order.price,
709            Price::new(0.0026, PRICE_PRECISION)
710        );
711        assert_eq!(
712            deltas.deltas[1].order.size,
713            Quantity::new(100.0, SIZE_PRECISION)
714        );
715        assert_eq!(deltas.deltas[1].flags, RecordFlag::F_LAST as u8);
716    }
717
718    #[rstest]
719    #[case::five(5)]
720    #[case::ten(10)]
721    #[case::twenty(20)]
722    fn test_parse_depth_snapshot_replaces_levels(#[case] depth: usize) {
723        let instrument = sample_instrument();
724        let mut msg: BinanceFuturesDepthUpdateMsg = load_market_fixture("depth_update_stream.json");
725        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
726        let mut book = OrderBook::new(instrument.id(), BookType::L2_MBP);
727
728        for offset in [0, 100] {
729            msg.bids = (0..depth)
730                .map(|i| [(1000 - offset - i).to_string(), "2.000".into()])
731                .collect();
732            msg.asks = (0..depth)
733                .map(|i| [(2000 + offset + i).to_string(), "3.000".into()])
734                .collect();
735            let snapshot = parse_depth_snapshot(&msg, &instrument, ts_init).unwrap();
736            let mut expected = vec![OrderBookDelta::clear(
737                instrument.id(),
738                msg.final_update_id,
739                snapshot.ts_event,
740                ts_init,
741            )];
742
743            for (side, levels) in [(OrderSide::Buy, &msg.bids), (OrderSide::Sell, &msg.asks)] {
744                for level in levels {
745                    expected.push(OrderBookDelta::new(
746                        instrument.id(),
747                        BookAction::Add,
748                        BookOrder::new(
749                            side,
750                            Price::from_str(&format!("{}.00000000", level[0])).unwrap(),
751                            Quantity::from_str(&level[1]).unwrap(),
752                            0,
753                        ),
754                        0,
755                        msg.final_update_id,
756                        UnixNanos::from(123_456_788_000_000u64),
757                        ts_init,
758                    ));
759                }
760            }
761            expected.last_mut().unwrap().flags = RecordFlag::F_LAST as u8;
762            assert_eq!(snapshot.instrument_id, instrument.id());
763            assert_eq!(snapshot.sequence, msg.final_update_id);
764            assert_eq!(snapshot.deltas, expected);
765            book.apply_deltas(&snapshot).unwrap();
766            assert_eq!(book.bids(None).count(), depth);
767            assert_eq!(book.asks(None).count(), depth);
768            for (actual, input) in book.bids(None).zip(&msg.bids) {
769                assert_eq!(
770                    actual.price.value.as_decimal(),
771                    Decimal::from_str(&input[0]).unwrap()
772                );
773                assert_eq!(actual.size_decimal(), dec!(2));
774            }
775
776            for (actual, input) in book.asks(None).zip(&msg.asks) {
777                assert_eq!(
778                    actual.price.value.as_decimal(),
779                    Decimal::from_str(&input[0]).unwrap()
780                );
781                assert_eq!(actual.size_decimal(), dec!(3));
782            }
783        }
784
785        msg.bids.clear();
786        msg.asks.clear();
787        let empty = parse_depth_snapshot(&msg, &instrument, ts_init).unwrap();
788        book.apply_deltas(&empty).unwrap();
789        assert_eq!(empty.deltas.len(), 1);
790        assert_eq!(
791            empty.deltas[0].flags,
792            RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
793        );
794        assert_eq!(book.bids(None).count(), 0);
795        assert_eq!(book.asks(None).count(), 0);
796    }
797
798    #[rstest]
799    fn test_market_data_parsers_preserve_decimal_prices() {
800        let instrument = sample_instrument();
801        let ts_init = UnixNanos::from(1);
802        let price = "123456789.12345678";
803        let mut agg: BinanceFuturesAggTradeMsg = load_market_fixture("agg_trade_stream.json");
804        agg.price = price.to_string();
805        let mut trade: BinanceFuturesTradeMsg = load_market_fixture("trade_stream.json");
806        trade.price = price.to_string();
807        let mut book: BinanceFuturesBookTickerMsg = load_market_fixture("book_ticker_stream.json");
808        book.best_bid_price = price.to_string();
809        book.best_ask_price = "123456789.87654321".to_string();
810        let mut depth: BinanceFuturesDepthUpdateMsg =
811            load_market_fixture("depth_update_stream.json");
812        depth.bids[0][0] = price.to_string();
813        depth.asks[0][0] = book.best_ask_price.clone();
814        depth.asks[0][1] = "0.000".to_string();
815
816        let agg = parse_agg_trade(&agg, &instrument, ts_init).unwrap();
817        let trade = parse_trade(&trade, &instrument, ts_init).unwrap();
818        let book = parse_book_ticker(&book, &instrument, ts_init).unwrap();
819        let depth = parse_depth_update(&depth, &instrument, ts_init).unwrap();
820
821        assert_eq!(agg.price, Price::from(price));
822        assert_eq!(trade.price, Price::from(price));
823        assert_eq!(book.bid_price, Price::from(price));
824        assert_eq!(book.ask_price, Price::from("123456789.87654321"));
825        assert_eq!(depth.deltas[0].order.price, Price::from(price));
826        assert_eq!(
827            depth.deltas[1].order.price,
828            Price::from("123456789.87654321")
829        );
830        assert_eq!(depth.deltas[1].order.size, Quantity::from("0.000"));
831        assert_eq!(depth.deltas[1].action, BookAction::Delete);
832    }
833
834    #[rstest]
835    fn test_parse_mark_price() {
836        let instrument = sample_instrument();
837        let msg: BinanceFuturesMarkPriceMsg = load_market_fixture("mark_price_stream.json");
838        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
839
840        let (mark, index, funding, custom) = parse_mark_price(&msg, &instrument, ts_init).unwrap();
841
842        assert_eq!(mark.instrument_id, instrument.id());
843        assert_eq!(mark.value, Price::new(11794.15, PRICE_PRECISION));
844        assert_eq!(index.value, Price::new(11784.62659091, PRICE_PRECISION));
845        assert_eq!(mark.ts_event, UnixNanos::from(1_562_305_380_000_000_000u64));
846        assert_eq!(funding.instrument_id, instrument.id());
847        assert_eq!(funding.rate.to_string(), "0.00038167");
848        assert_eq!(
849            funding.next_funding_ns,
850            Some(UnixNanos::from(1_562_306_400_000_000_000u64))
851        );
852        assert_eq!(
853            funding.ts_event,
854            UnixNanos::from(1_562_305_380_000_000_000u64)
855        );
856        assert_eq!(funding.ts_init, ts_init);
857        assert_eq!(custom.instrument_id, instrument.id());
858        assert_eq!(custom.mark_price, Price::from("11794.15000000"));
859        assert_eq!(custom.index_price, Price::from("11784.62659091"));
860        assert_eq!(custom.estimated_settle_price, Price::from("11784.25641265"));
861        assert_eq!(custom.funding_rate, dec!(0.00038167));
862        assert_eq!(custom.next_funding_time, funding.next_funding_ns);
863        assert_eq!(custom.ts_event, mark.ts_event);
864        assert_eq!(custom.ts_init, ts_init);
865    }
866
867    #[rstest]
868    #[case::zero(0)]
869    #[case::negative(-1)]
870    fn test_parse_mark_price_preserves_missing_funding_time(#[case] next_funding_time: i64) {
871        let instrument = sample_instrument();
872        let mut msg: BinanceFuturesMarkPriceMsg = load_market_fixture("mark_price_stream.json");
873        msg.next_funding_time = next_funding_time;
874
875        let (_, _, funding, custom) =
876            parse_mark_price(&msg, &instrument, UnixNanos::from(1)).unwrap();
877
878        assert_eq!(funding.next_funding_ns, None);
879        assert_eq!(custom.next_funding_time, None);
880    }
881
882    #[rstest]
883    fn test_parse_kline_closed() {
884        let instrument = sample_instrument();
885        let msg: BinanceFuturesKlineMsg = load_market_fixture("kline_stream_closed.json");
886        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
887
888        let bar = parse_kline(&msg, &instrument, ts_init).unwrap().unwrap();
889
890        assert_eq!(bar.bar_type.instrument_id(), instrument.id());
891        assert_eq!(bar.open, Price::new(0.001, PRICE_PRECISION));
892        assert_eq!(bar.high, Price::new(0.0025, PRICE_PRECISION));
893        assert_eq!(bar.low, Price::new(0.001, PRICE_PRECISION));
894        assert_eq!(bar.close, Price::new(0.002, PRICE_PRECISION));
895        assert_eq!(bar.volume, Quantity::new(1000.0, SIZE_PRECISION));
896        assert_eq!(bar.quote_volume, dec!(1.0000));
897        assert_eq!(bar.count, 100);
898        assert_eq!(bar.taker_buy_base_volume, dec!(500));
899        assert_eq!(bar.taker_buy_quote_volume, dec!(0.500));
900        assert_eq!(bar.ts_event, UnixNanos::from(1_638_747_719_999_000_000u64));
901        assert_eq!(bar.ts_init, ts_init);
902    }
903
904    #[rstest]
905    fn test_parse_kline_open_returns_none() {
906        let instrument = sample_instrument();
907        let msg: BinanceFuturesKlineMsg = load_market_fixture("kline_stream_open.json");
908
909        let bar = parse_kline(&msg, &instrument, UnixNanos::default()).unwrap();
910
911        assert!(bar.is_none());
912    }
913
914    #[rstest]
915    fn test_mark_price_msg_deserializes_optional_ap() {
916        let json = r#"{
917            "e": "markPriceUpdate",
918            "E": 1562305380000,
919            "s": "BTCUSDT",
920            "p": "11794.15000000",
921            "ap": "11792.85000000",
922            "i": "11784.62659091",
923            "P": "11784.25641265",
924            "r": "0.00038167",
925            "T": 1562306400000
926        }"#;
927
928        let msg: BinanceFuturesMarkPriceMsg = serde_json::from_str(json).unwrap();
929        assert_eq!(msg.mark_price_moving_avg.as_deref(), Some("11792.85000000"));
930
931        let legacy: BinanceFuturesMarkPriceMsg = load_market_fixture("mark_price_stream.json");
932        assert!(legacy.mark_price_moving_avg.is_none());
933    }
934
935    #[rstest]
936    fn test_parse_mark_price_funding_rate_fields() {
937        let instrument = sample_instrument();
938        let msg: BinanceFuturesMarkPriceMsg = load_market_fixture("mark_price_stream.json");
939        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
940
941        let (_mark, _index, funding, _custom) =
942            parse_mark_price(&msg, &instrument, ts_init).unwrap();
943
944        assert_eq!(funding.instrument_id, instrument.id());
945        assert_eq!(funding.rate.to_string(), "0.00038167");
946        assert!(funding.interval.is_none());
947        assert_eq!(
948            funding.next_funding_ns,
949            Some(UnixNanos::from(1_562_306_400_000_000_000u64))
950        );
951        assert_eq!(
952            funding.ts_event,
953            UnixNanos::from(1_562_305_380_000_000_000u64)
954        );
955        assert_eq!(funding.ts_init, ts_init);
956    }
957
958    #[rstest]
959    fn test_deserialize_liquidation_msg() {
960        let msg: BinanceFuturesLiquidationMsg = load_market_fixture("liquidation_stream.json");
961
962        assert_eq!(msg.event_type, "forceOrder");
963        assert_eq!(msg.event_time, 1_568_014_460_893);
964        assert_eq!(msg.order.symbol, Ustr::from("BTCUSDT"));
965        assert_eq!(msg.order.side, BinanceSide::Sell);
966        assert_eq!(msg.order.original_qty, "0.014");
967        assert_eq!(msg.order.average_price, "9910.12345678");
968        assert_eq!(msg.order.status, BinanceOrderStatus::Filled);
969        assert_eq!(msg.order.accumulated_qty, "0.014");
970        assert_eq!(msg.order.trade_time, 1_568_014_460_893);
971    }
972
973    #[rstest]
974    fn test_deserialize_ticker_msg() {
975        let msg: BinanceFuturesTickerMsg = load_market_fixture("ticker_stream.json");
976
977        assert_eq!(msg.event_type, "24hrTicker");
978        assert_eq!(msg.symbol, Ustr::from("BTCUSDT"));
979        assert_eq!(msg.price_change, "-131.40000000");
980        assert_eq!(msg.price_change_percent, "-0.786");
981        assert_eq!(msg.weighted_avg_price, "16628.97377498");
982        assert_eq!(msg.last_price, "16584.60000000");
983        assert_eq!(msg.open_price, "16716.00000000");
984        assert_eq!(msg.high_price, "16764.89000000");
985        assert_eq!(msg.low_price, "16456.51000000");
986        assert_eq!(msg.volume, "122474.816");
987        assert_eq!(msg.quote_volume, "2036102085.69746400");
988        assert_eq!(msg.num_trades, 142853);
989    }
990
991    #[rstest]
992    fn test_parse_ticker() {
993        let instrument = sample_instrument();
994        let msg: BinanceFuturesTickerMsg = load_market_fixture("ticker_stream.json");
995        let ts_init = UnixNanos::from(1_700_000_001_000_000_000u64);
996
997        let ticker = parse_ticker(&msg, &instrument, ts_init).unwrap();
998
999        assert_eq!(ticker.instrument_id, instrument.id());
1000        assert_eq!(ticker.price_change, dec!(-131.40000000));
1001        assert_eq!(ticker.price_change_percent, dec!(-0.786));
1002        assert_eq!(ticker.weighted_avg_price, dec!(16628.97377498));
1003        assert_eq!(ticker.last_price, dec!(16584.60000000));
1004        assert_eq!(ticker.last_qty, dec!(0.002));
1005        assert_eq!(ticker.open_price, dec!(16716.00000000));
1006        assert_eq!(ticker.high_price, dec!(16764.89000000));
1007        assert_eq!(ticker.low_price, dec!(16456.51000000));
1008        assert_eq!(ticker.volume, dec!(122474.816));
1009        assert_eq!(ticker.quote_volume, dec!(2036102085.69746400));
1010        assert_eq!(ticker.open_time, UnixNanos::from_millis(1_672_429_382_136));
1011        assert_eq!(ticker.close_time, UnixNanos::from_millis(1_672_515_782_136));
1012        assert_eq!(ticker.first_trade_id, 2_289_691);
1013        assert_eq!(ticker.last_trade_id, 2_432_543);
1014        assert_eq!(ticker.num_trades, 142_853);
1015        assert_eq!(ticker.ts_event, UnixNanos::from_millis(1_672_515_782_136));
1016        assert_eq!(ticker.ts_init, ts_init);
1017    }
1018
1019    #[rstest]
1020    fn test_parse_ticker_rejects_invalid_numeric_field() {
1021        let instrument = sample_instrument();
1022        let mut msg: BinanceFuturesTickerMsg = load_market_fixture("ticker_stream.json");
1023        msg.last_price = "not-a-decimal".to_string();
1024
1025        let result = parse_ticker(&msg, &instrument, UnixNanos::default());
1026
1027        assert!(result.is_err());
1028    }
1029
1030    #[rstest]
1031    fn test_extract_symbol() {
1032        let json = load_json_fixture("futures/market_data_json/book_ticker_stream.json");
1033
1034        let symbol = extract_symbol(&json);
1035
1036        assert_eq!(symbol, Some(Ustr::from("BNBUSDT")));
1037    }
1038
1039    #[rstest]
1040    fn test_extract_event_type() {
1041        let json = load_json_fixture("futures/market_data_json/mark_price_stream.json");
1042
1043        let event_type = extract_event_type(&json);
1044
1045        assert_eq!(event_type, Some(BinanceWsEventType::MarkPriceUpdate));
1046    }
1047
1048    #[rstest]
1049    fn test_extract_event_type_force_order() {
1050        let json = load_json_fixture("futures/market_data_json/liquidation_stream.json");
1051
1052        let event_type = extract_event_type(&json);
1053
1054        assert_eq!(event_type, Some(BinanceWsEventType::ForceOrder));
1055    }
1056
1057    #[rstest]
1058    fn test_extract_event_type_ticker() {
1059        let json = load_json_fixture("futures/market_data_json/ticker_stream.json");
1060
1061        let event_type = extract_event_type(&json);
1062
1063        assert_eq!(event_type, Some(BinanceWsEventType::Ticker24Hr));
1064    }
1065}