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