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