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