1use anyhow::Context;
19use nautilus_core::nanos::UnixNanos;
20use nautilus_model::{
21 data::{Bar, BarType, BookOrder, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick},
22 enums::{AggregationSource, AggressorSide, BookAction, OrderSide, RecordFlag},
23 identifiers::TradeId,
24 instruments::{Instrument, any::InstrumentAny},
25 types::{Price, Quantity},
26};
27use rust_decimal::Decimal;
28
29use crate::{
30 common::parse::ax_timestamp_s_to_unix_nanos,
31 http::parse::candle_width_to_bar_spec,
32 websocket::messages::{
33 AxBookLevel, AxBookLevelL3, AxMdBookL1, AxMdBookL2, AxMdBookL3, AxMdCandle, AxMdTrade,
34 },
35};
36
37fn decimal_to_price_dp(value: Decimal, precision: u8, field: &str) -> anyhow::Result<Price> {
39 Price::from_decimal_dp(value, precision).with_context(|| {
40 format!("Failed to construct Price for {field} with precision {precision}")
41 })
42}
43
44pub fn parse_book_l1_quote(
52 book: &AxMdBookL1,
53 instrument: &InstrumentAny,
54 ts_init: UnixNanos,
55) -> anyhow::Result<QuoteTick> {
56 let price_precision = instrument.price_precision();
57 let size_precision = instrument.size_precision();
58
59 let (bid_price, bid_size) = if let Some(bid) = book.b.first() {
60 (
61 decimal_to_price_dp(bid.p, price_precision, "book.bid.price")?,
62 Quantity::new(bid.q as f64, size_precision),
63 )
64 } else {
65 (Price::zero(price_precision), Quantity::zero(size_precision))
66 };
67
68 let (ask_price, ask_size) = if let Some(ask) = book.a.first() {
69 (
70 decimal_to_price_dp(ask.p, price_precision, "book.ask.price")?,
71 Quantity::new(ask.q as f64, size_precision),
72 )
73 } else {
74 (Price::zero(price_precision), Quantity::zero(size_precision))
75 };
76
77 let ts_event = ax_timestamp_s_to_unix_nanos(book.ts);
78
79 QuoteTick::new_checked(
80 instrument.id(),
81 bid_price,
82 ask_price,
83 bid_size,
84 ask_size,
85 ts_event,
86 ts_init,
87 )
88 .context("Failed to construct QuoteTick from Ax L1 book")
89}
90
91fn parse_book_level(
93 level: &AxBookLevel,
94 price_precision: u8,
95 size_precision: u8,
96) -> anyhow::Result<(Price, Quantity)> {
97 let price = decimal_to_price_dp(level.p, price_precision, "book.level.price")?;
98 let size = Quantity::new(level.q as f64, size_precision);
99 Ok((price, size))
100}
101
102pub fn parse_book_l2_deltas(
111 book: &AxMdBookL2,
112 instrument: &InstrumentAny,
113 sequence: u64,
114 ts_init: UnixNanos,
115) -> anyhow::Result<OrderBookDeltas> {
116 let instrument_id = instrument.id();
117 let price_precision = instrument.price_precision();
118 let size_precision = instrument.size_precision();
119
120 let ts_event = ax_timestamp_s_to_unix_nanos(book.ts);
121
122 let total_levels = book.b.len() + book.a.len();
123 let capacity = total_levels + 1;
124
125 let mut deltas = Vec::with_capacity(capacity);
126
127 deltas.push(OrderBookDelta::clear(
128 instrument_id,
129 sequence,
130 ts_event,
131 ts_init,
132 ));
133
134 let mut processed = 0_usize;
135
136 for level in &book.b {
137 let (price, size) = parse_book_level(level, price_precision, size_precision)?;
138 processed += 1;
139
140 let mut flags = RecordFlag::F_MBP as u8;
141 if processed == total_levels {
142 flags |= RecordFlag::F_LAST as u8;
143 }
144
145 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
146 let delta = OrderBookDelta::new_checked(
147 instrument_id,
148 BookAction::Add,
149 order,
150 flags,
151 sequence,
152 ts_event,
153 ts_init,
154 )
155 .context("Failed to construct OrderBookDelta from Ax L2 bid level")?;
156
157 deltas.push(delta);
158 }
159
160 for level in &book.a {
161 let (price, size) = parse_book_level(level, price_precision, size_precision)?;
162 processed += 1;
163
164 let mut flags = RecordFlag::F_MBP as u8;
165 if processed == total_levels {
166 flags |= RecordFlag::F_LAST as u8;
167 }
168
169 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
170 let delta = OrderBookDelta::new_checked(
171 instrument_id,
172 BookAction::Add,
173 order,
174 flags,
175 sequence,
176 ts_event,
177 ts_init,
178 )
179 .context("Failed to construct OrderBookDelta from Ax L2 ask level")?;
180
181 deltas.push(delta);
182 }
183
184 if total_levels == 0
185 && let Some(first) = deltas.first_mut()
186 {
187 first.flags |= RecordFlag::F_LAST as u8;
188 }
189
190 OrderBookDeltas::new_checked(instrument_id, deltas)
191 .context("Failed to assemble OrderBookDeltas from Ax L2 message")
192}
193
194fn parse_book_level_l3(
196 level: &AxBookLevelL3,
197 price_precision: u8,
198 size_precision: u8,
199) -> anyhow::Result<(Price, Quantity)> {
200 let price = decimal_to_price_dp(level.p, price_precision, "book.level.price")?;
201 let size = Quantity::new(level.q as f64, size_precision);
202 Ok((price, size))
203}
204
205pub fn parse_book_l3_deltas(
214 book: &AxMdBookL3,
215 instrument: &InstrumentAny,
216 sequence: u64,
217 ts_init: UnixNanos,
218) -> anyhow::Result<OrderBookDeltas> {
219 let instrument_id = instrument.id();
220 let price_precision = instrument.price_precision();
221 let size_precision = instrument.size_precision();
222
223 let ts_event = ax_timestamp_s_to_unix_nanos(book.ts);
224
225 let total_orders: usize = book.b.iter().map(|l| l.o.len()).sum::<usize>()
226 + book.a.iter().map(|l| l.o.len()).sum::<usize>();
227 let capacity = total_orders + 1;
228
229 let mut deltas = Vec::with_capacity(capacity);
230
231 deltas.push(OrderBookDelta::clear(
232 instrument_id,
233 sequence,
234 ts_event,
235 ts_init,
236 ));
237
238 let mut processed = 0_usize;
239 let mut order_id_counter = 1_u64;
240
241 for level in &book.b {
242 let (price, _) = parse_book_level_l3(level, price_precision, size_precision)?;
243
244 for &order_qty in &level.o {
245 processed += 1;
246
247 let mut flags = 0_u8;
248 if processed == total_orders {
249 flags |= RecordFlag::F_LAST as u8;
250 }
251
252 let size = Quantity::new(order_qty as f64, size_precision);
253 let order = BookOrder::new(OrderSide::Buy, price, size, order_id_counter);
254 order_id_counter += 1;
255
256 let delta = OrderBookDelta::new_checked(
257 instrument_id,
258 BookAction::Add,
259 order,
260 flags,
261 sequence,
262 ts_event,
263 ts_init,
264 )
265 .context("Failed to construct OrderBookDelta from Ax L3 bid order")?;
266
267 deltas.push(delta);
268 }
269 }
270
271 for level in &book.a {
272 let (price, _) = parse_book_level_l3(level, price_precision, size_precision)?;
273
274 for &order_qty in &level.o {
275 processed += 1;
276
277 let mut flags = 0_u8;
278 if processed == total_orders {
279 flags |= RecordFlag::F_LAST as u8;
280 }
281
282 let size = Quantity::new(order_qty as f64, size_precision);
283 let order = BookOrder::new(OrderSide::Sell, price, size, order_id_counter);
284 order_id_counter += 1;
285
286 let delta = OrderBookDelta::new_checked(
287 instrument_id,
288 BookAction::Add,
289 order,
290 flags,
291 sequence,
292 ts_event,
293 ts_init,
294 )
295 .context("Failed to construct OrderBookDelta from Ax L3 ask order")?;
296
297 deltas.push(delta);
298 }
299 }
300
301 if total_orders == 0
302 && let Some(first) = deltas.first_mut()
303 {
304 first.flags |= RecordFlag::F_LAST as u8;
305 }
306
307 OrderBookDeltas::new_checked(instrument_id, deltas)
308 .context("Failed to assemble OrderBookDeltas from Ax L3 message")
309}
310
311pub fn parse_trade_tick(
317 trade: &AxMdTrade,
318 instrument: &InstrumentAny,
319 ts_init: UnixNanos,
320) -> anyhow::Result<TradeTick> {
321 let price_precision = instrument.price_precision();
322 let size_precision = instrument.size_precision();
323
324 let price = decimal_to_price_dp(trade.p, price_precision, "trade.price")?;
325 let size = Quantity::new(trade.q as f64, size_precision);
326 let aggressor_side: AggressorSide = trade.d.map_or(AggressorSide::NoAggressor, |d| d.into());
327
328 let mut buf = itoa::Buffer::new();
330 let trade_id = TradeId::new_checked(buf.format(trade.tn))
331 .context("Failed to create TradeId from transaction number")?;
332
333 let ts_event = ax_timestamp_s_to_unix_nanos(trade.ts);
334
335 TradeTick::new_checked(
336 instrument.id(),
337 price,
338 size,
339 aggressor_side,
340 trade_id,
341 ts_event,
342 ts_init,
343 )
344 .context("Failed to construct TradeTick from Ax trade message")
345}
346
347pub fn parse_candle_bar(
353 candle: &AxMdCandle,
354 instrument: &InstrumentAny,
355 ts_init: UnixNanos,
356) -> anyhow::Result<Bar> {
357 let price_precision = instrument.price_precision();
358 let size_precision = instrument.size_precision();
359
360 let open = decimal_to_price_dp(candle.open, price_precision, "candle.open")?;
361 let high = decimal_to_price_dp(candle.high, price_precision, "candle.high")?;
362 let low = decimal_to_price_dp(candle.low, price_precision, "candle.low")?;
363 let close = decimal_to_price_dp(candle.close, price_precision, "candle.close")?;
364 let volume = Quantity::new(candle.volume as f64, size_precision);
365
366 let ts_event = ax_timestamp_s_to_unix_nanos(candle.ts);
367
368 let bar_spec = candle_width_to_bar_spec(candle.width);
369 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::External);
370
371 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
372 .context("Failed to construct Bar from Ax candle message")
373}
374
375#[cfg(test)]
376mod tests {
377 use nautilus_model::{
378 identifiers::{InstrumentId, Symbol},
379 instruments::CryptoPerpetual,
380 types::Currency,
381 };
382 use rstest::rstest;
383 use rust_decimal::Decimal;
384 use rust_decimal_macros::dec;
385 use ustr::Ustr;
386
387 use super::*;
388 use crate::{
389 common::{consts::AX_VENUE, enums::AxOrderSide},
390 websocket::messages::{AxMdBookL1, AxMdBookL2, AxMdBookL3, AxMdCandle, AxMdTrade},
391 };
392
393 fn create_test_instrument() -> InstrumentAny {
394 create_instrument_with_precision("BTC-PERP", 2, 3)
395 }
396
397 fn create_eurusd_instrument() -> InstrumentAny {
398 create_instrument_with_precision("EURUSD-PERP", 4, 0)
399 }
400
401 fn create_instrument_with_precision(
402 symbol: &str,
403 price_precision: u8,
404 size_precision: u8,
405 ) -> InstrumentAny {
406 let price_increment =
407 Price::from_decimal_dp(Decimal::new(1, price_precision as u32), price_precision)
408 .unwrap();
409 let size_increment =
410 Quantity::from_decimal_dp(Decimal::new(1, size_precision as u32), size_precision)
411 .unwrap();
412
413 let instrument = CryptoPerpetual::new(
414 InstrumentId::new(Symbol::new(symbol), *AX_VENUE),
415 Symbol::new(symbol),
416 Currency::USD(),
417 Currency::USD(),
418 Currency::USD(),
419 false,
420 price_precision,
421 size_precision,
422 price_increment,
423 size_increment,
424 None,
425 Some(size_increment),
426 None,
427 Some(size_increment),
428 None,
429 None,
430 None,
431 None,
432 Some(Decimal::new(1, 2)),
433 Some(Decimal::new(5, 3)),
434 Some(Decimal::new(2, 4)),
435 Some(Decimal::new(5, 4)),
436 UnixNanos::default(),
437 UnixNanos::default(),
438 );
439 InstrumentAny::CryptoPerpetual(instrument)
440 }
441
442 #[rstest]
443 fn test_parse_book_l1_quote() {
444 let book = AxMdBookL1 {
445 ts: 1700000000,
446 tn: 12345,
447 s: Ustr::from("BTC-PERP"),
448 b: vec![AxBookLevel {
449 p: dec!(50000.50),
450 q: 100,
451 }],
452 a: vec![AxBookLevel {
453 p: dec!(50001.00),
454 q: 150,
455 }],
456 };
457
458 let instrument = create_test_instrument();
459 let ts_init = UnixNanos::default();
460
461 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
462
463 assert_eq!(quote.bid_price.as_f64(), 50000.50);
464 assert_eq!(quote.ask_price.as_f64(), 50001.00);
465 assert_eq!(quote.bid_size.as_f64(), 100.0);
466 assert_eq!(quote.ask_size.as_f64(), 150.0);
467 }
468
469 #[rstest]
470 fn test_parse_book_l2_deltas() {
471 let book = AxMdBookL2 {
472 ts: 1700000000,
473 tn: 12345,
474 s: Ustr::from("BTC-PERP"),
475 b: vec![
476 AxBookLevel {
477 p: dec!(50000.50),
478 q: 100,
479 },
480 AxBookLevel {
481 p: dec!(50000.00),
482 q: 200,
483 },
484 ],
485 a: vec![
486 AxBookLevel {
487 p: dec!(50001.00),
488 q: 150,
489 },
490 AxBookLevel {
491 p: dec!(50001.50),
492 q: 250,
493 },
494 ],
495 };
496
497 let instrument = create_test_instrument();
498 let ts_init = UnixNanos::default();
499
500 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
501
502 assert_eq!(deltas.deltas.len(), 5);
504 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
505 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy);
506 assert_eq!(deltas.deltas[3].order.side, OrderSide::Sell);
507 }
508
509 #[rstest]
510 fn test_parse_book_l3_deltas() {
511 let book = AxMdBookL3 {
512 ts: 1700000000,
513 tn: 12345,
514 s: Ustr::from("BTC-PERP"),
515 b: vec![AxBookLevelL3 {
516 p: dec!(50000.50),
517 q: 300,
518 o: vec![100, 200],
519 }],
520 a: vec![AxBookLevelL3 {
521 p: dec!(50001.00),
522 q: 250,
523 o: vec![150, 100],
524 }],
525 };
526
527 let instrument = create_test_instrument();
528 let ts_init = UnixNanos::default();
529
530 let deltas = parse_book_l3_deltas(&book, &instrument, 1, ts_init).unwrap();
531
532 assert_eq!(deltas.deltas.len(), 5);
534 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
535 }
536
537 #[rstest]
538 fn test_parse_trade_tick() {
539 let trade = AxMdTrade {
540 ts: 1700000000,
541 tn: 12345,
542 s: Ustr::from("BTC-PERP"),
543 p: dec!(50000.50),
544 q: 100,
545 d: Some(AxOrderSide::Buy),
546 };
547
548 let instrument = create_test_instrument();
549 let ts_init = UnixNanos::default();
550
551 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
552
553 assert_eq!(tick.price.as_f64(), 50000.50);
554 assert_eq!(tick.size.as_f64(), 100.0);
555 assert_eq!(tick.aggressor_side, AggressorSide::Buyer);
556 }
557
558 #[rstest]
559 fn test_parse_book_l1_from_captured_data() {
560 let json = include_str!("../../../test_data/ws_md_book_l1_captured.json");
561 let book: AxMdBookL1 = serde_json::from_str(json).unwrap();
562
563 assert_eq!(book.s.as_str(), "EURUSD-PERP");
564 assert_eq!(book.b.len(), 1);
565 assert_eq!(book.a.len(), 1);
566
567 let instrument = create_eurusd_instrument();
568 let ts_init = UnixNanos::default();
569
570 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
571
572 assert_eq!(quote.instrument_id.symbol.as_str(), "EURUSD-PERP");
573 assert_eq!(quote.bid_price.as_f64(), 1.1712);
574 assert_eq!(quote.ask_price.as_f64(), 1.1717);
575 assert_eq!(quote.bid_size.as_f64(), 300.0);
576 assert_eq!(quote.ask_size.as_f64(), 100.0);
577 }
578
579 #[rstest]
580 fn test_parse_book_l2_from_captured_data() {
581 let json = include_str!("../../../test_data/ws_md_book_l2_captured.json");
582 let book: AxMdBookL2 = serde_json::from_str(json).unwrap();
583
584 assert_eq!(book.s.as_str(), "EURUSD-PERP");
585 assert_eq!(book.b.len(), 13);
586 assert_eq!(book.a.len(), 12);
587
588 let instrument = create_eurusd_instrument();
589 let ts_init = UnixNanos::default();
590
591 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
592
593 assert_eq!(deltas.deltas.len(), 26);
595 assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
596
597 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
599
600 let first_bid = &deltas.deltas[1];
602 assert_eq!(first_bid.order.side, OrderSide::Buy);
603 assert_eq!(first_bid.order.price.as_f64(), 1.1712);
604 assert_eq!(first_bid.order.size.as_f64(), 300.0);
605
606 let first_ask = &deltas.deltas[14];
608 assert_eq!(first_ask.order.side, OrderSide::Sell);
609 assert_eq!(first_ask.order.price.as_f64(), 1.1719);
610 assert_eq!(first_ask.order.size.as_f64(), 400.0);
611
612 let last_delta = deltas.deltas.last().unwrap();
614 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
615 }
616
617 #[rstest]
618 fn test_parse_book_l3_from_captured_data() {
619 let json = include_str!("../../../test_data/ws_md_book_l3_captured.json");
620 let book: AxMdBookL3 = serde_json::from_str(json).unwrap();
621
622 assert_eq!(book.s.as_str(), "EURUSD-PERP");
623 assert_eq!(book.b.len(), 15);
624 assert_eq!(book.a.len(), 14);
625
626 let instrument = create_eurusd_instrument();
627 let ts_init = UnixNanos::default();
628
629 let deltas = parse_book_l3_deltas(&book, &instrument, 1, ts_init).unwrap();
630
631 assert_eq!(deltas.deltas.len(), 30); assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
635
636 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
638
639 let first_bid = &deltas.deltas[1];
641 assert_eq!(first_bid.order.side, OrderSide::Buy);
642 assert_eq!(first_bid.order.price.as_f64(), 1.1714);
643 assert_eq!(first_bid.order.size.as_f64(), 100.0);
644
645 let last_delta = deltas.deltas.last().unwrap();
647 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
648 }
649
650 #[rstest]
651 fn test_parse_trade_from_captured_data() {
652 let json = include_str!("../../../test_data/ws_md_trade_captured.json");
653 let trade: AxMdTrade = serde_json::from_str(json).unwrap();
654
655 assert_eq!(trade.s.as_str(), "EURUSD-PERP");
656 assert_eq!(trade.p, dec!(1.1719));
657 assert_eq!(trade.q, 400);
658 assert_eq!(trade.d, Some(AxOrderSide::Buy));
659
660 let instrument = create_eurusd_instrument();
661 let ts_init = UnixNanos::default();
662
663 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
664
665 assert_eq!(tick.instrument_id.symbol.as_str(), "EURUSD-PERP");
666 assert_eq!(tick.price.as_f64(), 1.1719);
667 assert_eq!(tick.size.as_f64(), 400.0);
668 assert_eq!(tick.aggressor_side, AggressorSide::Buyer);
669 assert_eq!(tick.trade_id.to_string(), "334589144");
670 }
671
672 #[rstest]
673 fn test_parse_book_l1_empty_sides() {
674 let book = AxMdBookL1 {
675 ts: 1700000000,
676 tn: 12345,
677 s: Ustr::from("TEST-PERP"),
678 b: vec![],
679 a: vec![],
680 };
681
682 let instrument = create_test_instrument();
683 let ts_init = UnixNanos::default();
684
685 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
686
687 assert_eq!(quote.bid_price.as_f64(), 0.0);
688 assert_eq!(quote.ask_price.as_f64(), 0.0);
689 assert_eq!(quote.bid_size.as_f64(), 0.0);
690 assert_eq!(quote.ask_size.as_f64(), 0.0);
691 }
692
693 #[rstest]
694 fn test_parse_book_l2_empty_book() {
695 let book = AxMdBookL2 {
696 ts: 1700000000,
697 tn: 12345,
698 s: Ustr::from("TEST-PERP"),
699 b: vec![],
700 a: vec![],
701 };
702
703 let instrument = create_test_instrument();
704 let ts_init = UnixNanos::default();
705
706 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
707
708 assert_eq!(deltas.deltas.len(), 1);
710 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
711 assert!(deltas.deltas[0].flags & RecordFlag::F_LAST as u8 != 0);
712 }
713
714 #[rstest]
715 fn test_parse_candle_bar() {
716 use crate::common::enums::AxCandleWidth;
717
718 let candle = AxMdCandle {
719 symbol: Ustr::from("BTC-PERP"),
720 ts: 1700000000,
721 open: dec!(50000.00),
722 high: dec!(51000.00),
723 low: dec!(49500.00),
724 close: dec!(50500.00),
725 volume: 1000,
726 buy_volume: 600,
727 sell_volume: 400,
728 width: AxCandleWidth::Minutes1,
729 };
730
731 let instrument = create_test_instrument();
732 let ts_init = UnixNanos::default();
733
734 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
735
736 assert_eq!(bar.open.as_f64(), 50000.00);
737 assert_eq!(bar.high.as_f64(), 51000.00);
738 assert_eq!(bar.low.as_f64(), 49500.00);
739 assert_eq!(bar.close.as_f64(), 50500.00);
740 assert_eq!(bar.volume.as_f64(), 1000.0);
741 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "BTC-PERP");
742 }
743
744 #[rstest]
745 fn test_parse_candle_from_test_data() {
746 let json = include_str!("../../../test_data/ws_md_candle.json");
747 let candle: AxMdCandle = serde_json::from_str(json).unwrap();
748
749 assert_eq!(candle.symbol.as_str(), "BTCUSD-PERP");
750 assert_eq!(candle.open, dec!(49500.00));
751 assert_eq!(candle.close, dec!(50000.00));
752
753 let instrument = create_instrument_with_precision("BTCUSD-PERP", 2, 3);
754 let ts_init = UnixNanos::default();
755
756 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
757
758 assert_eq!(bar.open.as_f64(), 49500.00);
759 assert_eq!(bar.high.as_f64(), 50500.00);
760 assert_eq!(bar.low.as_f64(), 49000.00);
761 assert_eq!(bar.close.as_f64(), 50000.00);
762 assert_eq!(bar.volume.as_f64(), 5000.0);
763 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "BTCUSD-PERP");
764 }
765}