1use 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
57pub 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
96pub 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
135pub 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
179pub 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 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 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
295pub 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, 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
373pub 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
411fn 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
465pub fn parse_kline(
473 msg: &BinanceFuturesKlineMsg,
474 instrument: &InstrumentAny,
475 ts_init: UnixNanos,
476) -> BinanceWsResult<Option<BinanceBar>> {
477 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 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
525pub fn extract_symbol(json: &serde_json::Value) -> Option<Ustr> {
527 json.get("s").and_then(|v| v.as_str()).map(Ustr::from)
528}
529
530pub 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}