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