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 instruments::{Instrument, any::InstrumentAny},
24 types::{Price, Quantity},
25};
26use rust_decimal::Decimal;
27
28use crate::{
29 common::parse::{ax_timestamp_stn_to_unix_nanos, create_architect_trade_id},
30 http::parse::candle_width_to_bar_spec,
31 websocket::messages::{
32 AxBookLevel, AxBookLevelL3, AxMdBookL1, AxMdBookL2, AxMdBookL3, AxMdCandle, AxMdTrade,
33 },
34};
35
36fn decimal_to_price_dp(value: Decimal, precision: u8, field: &str) -> anyhow::Result<Price> {
38 Price::from_decimal_dp(value, precision).with_context(|| {
39 format!("Failed to construct Price for {field} with precision {precision}")
40 })
41}
42
43pub fn parse_book_l1_quote(
51 book: &AxMdBookL1,
52 instrument: &InstrumentAny,
53 ts_init: UnixNanos,
54) -> anyhow::Result<QuoteTick> {
55 parse_top_of_book_quote(
56 book.ts,
57 book.tn,
58 book.b.first().map(|level| (level.p, level.q)),
59 book.a.first().map(|level| (level.p, level.q)),
60 instrument,
61 ts_init,
62 )
63}
64
65pub fn parse_book_l2_quote(
71 book: &AxMdBookL2,
72 instrument: &InstrumentAny,
73 ts_init: UnixNanos,
74) -> anyhow::Result<QuoteTick> {
75 anyhow::ensure!(book.st, "AX incremental book frames are not supported");
76
77 parse_top_of_book_quote(
78 book.ts,
79 book.tn,
80 book.b
81 .iter()
82 .max_by_key(|level| level.p)
83 .map(|level| (level.p, level.q)),
84 book.a
85 .iter()
86 .min_by_key(|level| level.p)
87 .map(|level| (level.p, level.q)),
88 instrument,
89 ts_init,
90 )
91}
92
93pub fn parse_book_l3_quote(
99 book: &AxMdBookL3,
100 instrument: &InstrumentAny,
101 ts_init: UnixNanos,
102) -> anyhow::Result<QuoteTick> {
103 anyhow::ensure!(book.st, "AX incremental book frames are not supported");
104
105 parse_top_of_book_quote(
106 book.ts,
107 book.tn,
108 book.b
109 .iter()
110 .max_by_key(|level| level.p)
111 .map(|level| (level.p, level.q)),
112 book.a
113 .iter()
114 .min_by_key(|level| level.p)
115 .map(|level| (level.p, level.q)),
116 instrument,
117 ts_init,
118 )
119}
120
121fn parse_top_of_book_quote(
122 ts: i64,
123 tn: i64,
124 bid: Option<(Decimal, u64)>,
125 ask: Option<(Decimal, u64)>,
126 instrument: &InstrumentAny,
127 ts_init: UnixNanos,
128) -> anyhow::Result<QuoteTick> {
129 let price_precision = instrument.price_precision();
130 let size_precision = instrument.size_precision();
131
132 let (bid_price, bid_size) = if let Some((price, quantity)) = bid {
133 (
134 decimal_to_price_dp(price, price_precision, "book.bid.price")?,
135 Quantity::from_decimal_dp(Decimal::from(quantity), size_precision)?,
136 )
137 } else {
138 (Price::zero(price_precision), Quantity::zero(size_precision))
139 };
140
141 let (ask_price, ask_size) = if let Some((price, quantity)) = ask {
142 (
143 decimal_to_price_dp(price, price_precision, "book.ask.price")?,
144 Quantity::from_decimal_dp(Decimal::from(quantity), size_precision)?,
145 )
146 } else {
147 (Price::zero(price_precision), Quantity::zero(size_precision))
148 };
149
150 let ts_event = ax_timestamp_stn_to_unix_nanos(ts, tn)?;
151
152 QuoteTick::new_checked(
153 instrument.id(),
154 bid_price,
155 ask_price,
156 bid_size,
157 ask_size,
158 ts_event,
159 ts_init,
160 )
161 .context("Failed to construct QuoteTick from Ax L1 book")
162}
163
164fn parse_book_level(
166 level: &AxBookLevel,
167 price_precision: u8,
168 size_precision: u8,
169) -> anyhow::Result<(Price, Quantity)> {
170 let price = decimal_to_price_dp(level.p, price_precision, "book.level.price")?;
171 let size = Quantity::from_decimal_dp(Decimal::from(level.q), size_precision)?;
172 Ok((price, size))
173}
174
175pub fn parse_book_l2_deltas(
184 book: &AxMdBookL2,
185 instrument: &InstrumentAny,
186 sequence: u64,
187 ts_init: UnixNanos,
188) -> anyhow::Result<OrderBookDeltas> {
189 anyhow::ensure!(book.st, "AX incremental book frames are not supported");
190
191 let instrument_id = instrument.id();
192 let price_precision = instrument.price_precision();
193 let size_precision = instrument.size_precision();
194
195 let ts_event = ax_timestamp_stn_to_unix_nanos(book.ts, book.tn)?;
196
197 let total_levels = book.b.len() + book.a.len();
198 let capacity = total_levels + 1;
199
200 let mut deltas = Vec::with_capacity(capacity);
201
202 deltas.push(OrderBookDelta::clear(
203 instrument_id,
204 sequence,
205 ts_event,
206 ts_init,
207 ));
208
209 let mut processed = 0_usize;
210
211 for level in &book.b {
212 let (price, size) = parse_book_level(level, price_precision, size_precision)?;
213 processed += 1;
214
215 let mut flags = RecordFlag::F_MBP as u8 | RecordFlag::F_SNAPSHOT as u8;
216
217 if processed == total_levels {
218 flags |= RecordFlag::F_LAST as u8;
219 }
220
221 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
222 let delta = OrderBookDelta::new_checked(
223 instrument_id,
224 BookAction::Add,
225 order,
226 flags,
227 sequence,
228 ts_event,
229 ts_init,
230 )
231 .context("Failed to construct OrderBookDelta from Ax L2 bid level")?;
232
233 deltas.push(delta);
234 }
235
236 for level in &book.a {
237 let (price, size) = parse_book_level(level, price_precision, size_precision)?;
238 processed += 1;
239
240 let mut flags = RecordFlag::F_MBP as u8 | RecordFlag::F_SNAPSHOT as u8;
241
242 if processed == total_levels {
243 flags |= RecordFlag::F_LAST as u8;
244 }
245
246 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
247 let delta = OrderBookDelta::new_checked(
248 instrument_id,
249 BookAction::Add,
250 order,
251 flags,
252 sequence,
253 ts_event,
254 ts_init,
255 )
256 .context("Failed to construct OrderBookDelta from Ax L2 ask level")?;
257
258 deltas.push(delta);
259 }
260
261 if total_levels == 0
262 && let Some(first) = deltas.first_mut()
263 {
264 first.flags |= RecordFlag::F_LAST as u8;
265 }
266
267 OrderBookDeltas::new_checked(instrument_id, deltas)
268 .context("Failed to assemble OrderBookDeltas from Ax L2 message")
269}
270
271fn parse_book_level_l3(
273 level: &AxBookLevelL3,
274 price_precision: u8,
275 size_precision: u8,
276) -> anyhow::Result<(Price, Quantity)> {
277 let price = decimal_to_price_dp(level.p, price_precision, "book.level.price")?;
278 let size = Quantity::from_decimal_dp(Decimal::from(level.q), size_precision)?;
279 Ok((price, size))
280}
281
282pub fn parse_book_l3_deltas(
291 book: &AxMdBookL3,
292 instrument: &InstrumentAny,
293 sequence: u64,
294 ts_init: UnixNanos,
295) -> anyhow::Result<OrderBookDeltas> {
296 anyhow::ensure!(book.st, "AX incremental book frames are not supported");
297
298 let instrument_id = instrument.id();
299 let price_precision = instrument.price_precision();
300 let size_precision = instrument.size_precision();
301
302 let ts_event = ax_timestamp_stn_to_unix_nanos(book.ts, book.tn)?;
303
304 let total_orders: usize = book.b.iter().map(|l| l.o.len()).sum::<usize>()
305 + book.a.iter().map(|l| l.o.len()).sum::<usize>();
306 let capacity = total_orders + 1;
307
308 let mut deltas = Vec::with_capacity(capacity);
309
310 deltas.push(OrderBookDelta::clear(
311 instrument_id,
312 sequence,
313 ts_event,
314 ts_init,
315 ));
316
317 let mut processed = 0_usize;
318 let mut order_id_counter = 1_u64;
319
320 for level in &book.b {
321 let (price, _) = parse_book_level_l3(level, price_precision, size_precision)?;
322
323 for &order_qty in &level.o {
324 processed += 1;
325
326 let mut flags = RecordFlag::F_SNAPSHOT as u8;
327
328 if processed == total_orders {
329 flags |= RecordFlag::F_LAST as u8;
330 }
331
332 let size = Quantity::from_decimal_dp(Decimal::from(order_qty), size_precision)?;
333 let order = BookOrder::new(OrderSide::Buy, price, size, order_id_counter);
334 order_id_counter += 1;
335
336 let delta = OrderBookDelta::new_checked(
337 instrument_id,
338 BookAction::Add,
339 order,
340 flags,
341 sequence,
342 ts_event,
343 ts_init,
344 )
345 .context("Failed to construct OrderBookDelta from Ax L3 bid order")?;
346
347 deltas.push(delta);
348 }
349 }
350
351 for level in &book.a {
352 let (price, _) = parse_book_level_l3(level, price_precision, size_precision)?;
353
354 for &order_qty in &level.o {
355 processed += 1;
356
357 let mut flags = RecordFlag::F_SNAPSHOT as u8;
358
359 if processed == total_orders {
360 flags |= RecordFlag::F_LAST as u8;
361 }
362
363 let size = Quantity::from_decimal_dp(Decimal::from(order_qty), size_precision)?;
364 let order = BookOrder::new(OrderSide::Sell, price, size, order_id_counter);
365 order_id_counter += 1;
366
367 let delta = OrderBookDelta::new_checked(
368 instrument_id,
369 BookAction::Add,
370 order,
371 flags,
372 sequence,
373 ts_event,
374 ts_init,
375 )
376 .context("Failed to construct OrderBookDelta from Ax L3 ask order")?;
377
378 deltas.push(delta);
379 }
380 }
381
382 if total_orders == 0
383 && let Some(first) = deltas.first_mut()
384 {
385 first.flags |= RecordFlag::F_LAST as u8;
386 }
387
388 OrderBookDeltas::new_checked(instrument_id, deltas)
389 .context("Failed to assemble OrderBookDeltas from Ax L3 message")
390}
391
392pub fn parse_trade_tick(
398 trade: &AxMdTrade,
399 instrument: &InstrumentAny,
400 ts_init: UnixNanos,
401) -> anyhow::Result<TradeTick> {
402 let price_precision = instrument.price_precision();
403 let size_precision = instrument.size_precision();
404
405 let price = decimal_to_price_dp(trade.p, price_precision, "trade.price")?;
406 let size = Quantity::from_decimal_dp(Decimal::from(trade.q), size_precision)?;
407 let aggressor_side: AggressorSide = trade.d.map_or(AggressorSide::NoAggressor, |d| d.into());
408
409 let ts_event = ax_timestamp_stn_to_unix_nanos(trade.ts, trade.tn)?;
410 let trade_id = create_architect_trade_id(ts_event, price, size, aggressor_side)?;
411
412 TradeTick::new_checked(
413 instrument.id(),
414 price,
415 size,
416 aggressor_side,
417 trade_id,
418 ts_event,
419 ts_init,
420 )
421 .context("Failed to construct TradeTick from Ax trade message")
422}
423
424pub fn parse_candle_bar(
430 candle: &AxMdCandle,
431 instrument: &InstrumentAny,
432 ts_init: UnixNanos,
433) -> anyhow::Result<Bar> {
434 let price_precision = instrument.price_precision();
435 let size_precision = instrument.size_precision();
436
437 let open = decimal_to_price_dp(candle.open, price_precision, "candle.open")?;
438 let high = decimal_to_price_dp(candle.high, price_precision, "candle.high")?;
439 let low = decimal_to_price_dp(candle.low, price_precision, "candle.low")?;
440 let close = decimal_to_price_dp(candle.close, price_precision, "candle.close")?;
441 let volume = Quantity::from_decimal_dp(Decimal::from(candle.volume), size_precision)?;
442
443 let ts_event = ax_timestamp_stn_to_unix_nanos(candle.ts, 0)?;
444
445 let bar_spec = candle_width_to_bar_spec(candle.width);
446 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::External);
447
448 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
449 .context("Failed to construct Bar from Ax candle message")
450}
451
452#[cfg(test)]
453mod tests {
454 use nautilus_model::{
455 enums::AssetClass,
456 identifiers::{InstrumentId, Symbol},
457 instruments::PerpetualContract,
458 types::Currency,
459 };
460 use rstest::rstest;
461 use rust_decimal::Decimal;
462 use rust_decimal_macros::dec;
463 use ustr::Ustr;
464
465 use super::*;
466 use crate::{
467 common::{consts::AX_VENUE, enums::AxOrderSide},
468 http::{models::AxRestTrade, parse::parse_trade_tick as parse_rest_trade_tick},
469 websocket::messages::{AxMdBookL1, AxMdBookL2, AxMdBookL3, AxMdCandle, AxMdTrade},
470 };
471
472 fn create_test_instrument() -> InstrumentAny {
473 create_instrument_with_precision("BTC-PERP", 2, 3)
474 }
475
476 fn create_eurusd_instrument() -> InstrumentAny {
477 create_instrument_with_precision("EURUSD-PERP", 4, 0)
478 }
479
480 fn create_instrument_with_precision(
481 symbol: &str,
482 price_precision: u8,
483 size_precision: u8,
484 ) -> InstrumentAny {
485 let underlying = Ustr::from(symbol.split('-').next().unwrap_or(symbol));
486 let price_increment =
487 Price::from_decimal_dp(Decimal::new(1, price_precision as u32), price_precision)
488 .unwrap();
489 let size_increment =
490 Quantity::from_decimal_dp(Decimal::new(1, size_precision as u32), size_precision)
491 .unwrap();
492
493 let instrument = PerpetualContract::builder()
494 .instrument_id(InstrumentId::new(Symbol::new(symbol), *AX_VENUE))
495 .raw_symbol(Symbol::new(symbol))
496 .underlying(underlying)
497 .asset_class(AssetClass::Cryptocurrency)
498 .quote_currency(Currency::USD())
499 .settlement_currency(Currency::USD())
500 .is_inverse(false)
501 .price_precision(price_precision)
502 .size_precision(size_precision)
503 .price_increment(price_increment)
504 .size_increment(size_increment)
505 .lot_size(size_increment)
506 .min_quantity(size_increment)
507 .margin_init(Decimal::new(1, 2))
508 .margin_maint(Decimal::new(5, 3))
509 .ts_event(UnixNanos::default())
510 .ts_init(UnixNanos::default())
511 .build()
512 .unwrap();
513 InstrumentAny::PerpetualContract(instrument)
514 }
515
516 #[rstest]
517 fn test_parse_book_l1_quote() {
518 let book = AxMdBookL1 {
519 ts: 1700000000,
520 tn: 12345,
521 s: Ustr::from("BTC-PERP"),
522 b: vec![AxBookLevel {
523 p: dec!(50000.50),
524 q: 100,
525 }],
526 a: vec![AxBookLevel {
527 p: dec!(50001.00),
528 q: 150,
529 }],
530 };
531
532 let instrument = create_test_instrument();
533 let ts_init = UnixNanos::default();
534
535 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
536
537 assert_eq!(quote.bid_price.as_f64(), 50000.50);
538 assert_eq!(quote.ask_price.as_f64(), 50001.00);
539 assert_eq!(quote.bid_size.as_f64(), 100.0);
540 assert_eq!(quote.ask_size.as_f64(), 150.0);
541 }
542
543 #[rstest]
544 fn test_parse_book_quotes_select_best_unsorted_levels() {
545 let l2 = AxMdBookL2 {
546 ts: 1700000000,
547 tn: 12345,
548 s: Ustr::from("BTC-PERP"),
549 b: vec![
550 AxBookLevel {
551 p: dec!(50000.00),
552 q: 200,
553 },
554 AxBookLevel {
555 p: dec!(50000.50),
556 q: 100,
557 },
558 ],
559 a: vec![
560 AxBookLevel {
561 p: dec!(50001.50),
562 q: 250,
563 },
564 AxBookLevel {
565 p: dec!(50001.00),
566 q: 150,
567 },
568 ],
569 st: true,
570 };
571 let l3 = AxMdBookL3 {
572 ts: 1700000000,
573 tn: 12345,
574 s: Ustr::from("BTC-PERP"),
575 b: vec![
576 AxBookLevelL3 {
577 p: dec!(50000.00),
578 q: 200,
579 o: vec![200],
580 },
581 AxBookLevelL3 {
582 p: dec!(50000.50),
583 q: 100,
584 o: vec![100],
585 },
586 ],
587 a: vec![
588 AxBookLevelL3 {
589 p: dec!(50001.50),
590 q: 250,
591 o: vec![250],
592 },
593 AxBookLevelL3 {
594 p: dec!(50001.00),
595 q: 150,
596 o: vec![150],
597 },
598 ],
599 st: true,
600 };
601 let instrument = create_test_instrument();
602 let ts_init = UnixNanos::default();
603
604 let l2_quote = parse_book_l2_quote(&l2, &instrument, ts_init).unwrap();
605 let l3_quote = parse_book_l3_quote(&l3, &instrument, ts_init).unwrap();
606
607 assert_eq!(l2_quote.bid_price.as_f64(), 50000.50);
608 assert_eq!(l2_quote.bid_size.as_f64(), 100.0);
609 assert_eq!(l2_quote.ask_price.as_f64(), 50001.00);
610 assert_eq!(l2_quote.ask_size.as_f64(), 150.0);
611 assert_eq!(l3_quote.bid_price.as_f64(), 50000.50);
612 assert_eq!(l3_quote.bid_size.as_f64(), 100.0);
613 assert_eq!(l3_quote.ask_price.as_f64(), 50001.00);
614 assert_eq!(l3_quote.ask_size.as_f64(), 150.0);
615 }
616
617 #[rstest]
618 fn test_parse_book_l2_deltas() {
619 let book = AxMdBookL2 {
620 ts: 1700000000,
621 tn: 12345,
622 s: Ustr::from("BTC-PERP"),
623 b: vec![
624 AxBookLevel {
625 p: dec!(50000.50),
626 q: 100,
627 },
628 AxBookLevel {
629 p: dec!(50000.00),
630 q: 200,
631 },
632 ],
633 a: vec![
634 AxBookLevel {
635 p: dec!(50001.00),
636 q: 150,
637 },
638 AxBookLevel {
639 p: dec!(50001.50),
640 q: 250,
641 },
642 ],
643 st: true,
644 };
645
646 let instrument = create_test_instrument();
647 let ts_init = UnixNanos::default();
648
649 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
650
651 assert_eq!(deltas.deltas.len(), 5);
653 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
654 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy.into());
655 assert_eq!(deltas.deltas[3].order.side, OrderSide::Sell.into());
656
657 for delta in &deltas.deltas {
659 assert_ne!(delta.flags & RecordFlag::F_SNAPSHOT as u8, 0);
660 }
661
662 for delta in &deltas.deltas[..deltas.deltas.len() - 1] {
663 assert_eq!(delta.flags & RecordFlag::F_LAST as u8, 0);
664 }
665
666 assert_ne!(
667 deltas.deltas.last().unwrap().flags & RecordFlag::F_LAST as u8,
668 0
669 );
670 }
671
672 #[rstest]
673 fn test_parse_book_l3_deltas() {
674 let book = AxMdBookL3 {
675 ts: 1700000000,
676 tn: 12345,
677 s: Ustr::from("BTC-PERP"),
678 b: vec![AxBookLevelL3 {
679 p: dec!(50000.50),
680 q: 300,
681 o: vec![100, 200],
682 }],
683 a: vec![AxBookLevelL3 {
684 p: dec!(50001.00),
685 q: 250,
686 o: vec![150, 100],
687 }],
688 st: true,
689 };
690
691 let instrument = create_test_instrument();
692 let ts_init = UnixNanos::default();
693
694 let deltas = parse_book_l3_deltas(&book, &instrument, 1, ts_init).unwrap();
695
696 assert_eq!(deltas.deltas.len(), 5);
698 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
699
700 for delta in &deltas.deltas {
702 assert_ne!(delta.flags & RecordFlag::F_SNAPSHOT as u8, 0);
703 }
704
705 for delta in &deltas.deltas[1..] {
706 assert_eq!(delta.flags & RecordFlag::F_MBP as u8, 0);
707 }
708 }
709
710 #[rstest]
711 fn test_parse_trade_tick() {
712 let trade = AxMdTrade {
713 ts: 1700000000,
714 tn: 12345,
715 s: Ustr::from("BTC-PERP"),
716 p: dec!(50000.50),
717 q: 100,
718 d: Some(AxOrderSide::Buy),
719 };
720
721 let instrument = create_test_instrument();
722 let ts_init = UnixNanos::default();
723
724 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
725
726 assert_eq!(tick.price.as_f64(), 50000.50);
727 assert_eq!(tick.size.as_f64(), 100.0);
728 assert_eq!(tick.aggressor_side, AggressorSide::Buy);
729 }
730
731 #[rstest]
732 fn test_parse_book_l1_from_captured_data() {
733 let json = include_str!("../../../test_data/ws_md_book_l1_captured.json");
734 let book: AxMdBookL1 = serde_json::from_str(json).unwrap();
735
736 assert_eq!(book.s, "EURUSD-PERP");
737 assert_eq!(book.b.len(), 1);
738 assert_eq!(book.a.len(), 1);
739
740 let instrument = create_eurusd_instrument();
741 let ts_init = UnixNanos::default();
742
743 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
744
745 assert_eq!(quote.instrument_id.symbol.as_str(), "EURUSD-PERP");
746 assert_eq!(quote.bid_price.as_f64(), 1.1712);
747 assert_eq!(quote.ask_price.as_f64(), 1.1717);
748 assert_eq!(quote.bid_size.as_f64(), 300.0);
749 assert_eq!(quote.ask_size.as_f64(), 100.0);
750 }
751
752 #[rstest]
753 fn test_parse_book_l2_from_captured_data() {
754 let json = include_str!("../../../test_data/ws_md_book_l2_captured.json");
755 let book: AxMdBookL2 = serde_json::from_str(json).unwrap();
756
757 assert_eq!(book.s, "EURUSD-PERP");
758 assert_eq!(book.b.len(), 13);
759 assert_eq!(book.a.len(), 12);
760
761 let instrument = create_eurusd_instrument();
762 let ts_init = UnixNanos::default();
763
764 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
765
766 assert_eq!(deltas.deltas.len(), 26);
768 assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
769
770 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
772
773 let first_bid = &deltas.deltas[1];
775 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
776 assert_eq!(first_bid.order.price.as_f64(), 1.1712);
777 assert_eq!(first_bid.order.size.as_f64(), 300.0);
778
779 let first_ask = &deltas.deltas[14];
781 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
782 assert_eq!(first_ask.order.price.as_f64(), 1.1719);
783 assert_eq!(first_ask.order.size.as_f64(), 400.0);
784
785 let last_delta = deltas.deltas.last().unwrap();
787 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
788 }
789
790 #[rstest]
791 fn test_parse_book_l3_from_captured_data() {
792 let json = include_str!("../../../test_data/ws_md_book_l3_captured.json");
793 let book: AxMdBookL3 = serde_json::from_str(json).unwrap();
794
795 assert_eq!(book.s, "EURUSD-PERP");
796 assert_eq!(book.b.len(), 15);
797 assert_eq!(book.a.len(), 14);
798
799 let instrument = create_eurusd_instrument();
800 let ts_init = UnixNanos::default();
801
802 let deltas = parse_book_l3_deltas(&book, &instrument, 1, ts_init).unwrap();
803
804 assert_eq!(deltas.deltas.len(), 30); assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
808
809 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
811
812 let first_bid = &deltas.deltas[1];
814 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
815 assert_eq!(first_bid.order.price.as_f64(), 1.1714);
816 assert_eq!(first_bid.order.size.as_f64(), 100.0);
817
818 let last_delta = deltas.deltas.last().unwrap();
820 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
821 }
822
823 #[rstest]
824 fn test_parse_trade_from_captured_data() {
825 let json = include_str!("../../../test_data/ws_md_trade_captured.json");
826 let trade: AxMdTrade = serde_json::from_str(json).unwrap();
827
828 assert_eq!(trade.s, "EURUSD-PERP");
829 assert_eq!(trade.p, dec!(1.1719));
830 assert_eq!(trade.q, 400);
831 assert_eq!(trade.d, Some(AxOrderSide::Buy));
832
833 let instrument = create_eurusd_instrument();
834 let ts_init = UnixNanos::default();
835
836 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
837
838 assert_eq!(tick.instrument_id.symbol.as_str(), "EURUSD-PERP");
839 assert_eq!(tick.price.as_f64(), 1.1719);
840 assert_eq!(tick.size.as_f64(), 400.0);
841 assert_eq!(tick.aggressor_side, AggressorSide::Buy);
842 assert_eq!(
843 tick.trade_id.to_string(),
844 "1766193240334589144-38b4fe5a94a253d0"
845 );
846 }
847
848 #[rstest]
849 fn test_parse_trade_tick_matches_rest_trade_id_for_the_same_trade() {
850 let json = include_str!("../../../test_data/ws_md_trade_captured.json");
853 let ws_trade: AxMdTrade = serde_json::from_str(json).unwrap();
854 let rest_trade = AxRestTrade {
855 ts: ws_trade.ts,
856 tn: ws_trade.tn,
857 p: ws_trade.p,
858 q: ws_trade.q as i64,
859 s: ws_trade.s,
860 d: ws_trade.d.unwrap(),
861 };
862 let instrument = create_eurusd_instrument();
863 let ts_init = UnixNanos::default();
864
865 let ws_tick = parse_trade_tick(&ws_trade, &instrument, ts_init).unwrap();
866 let rest_tick = parse_rest_trade_tick(&rest_trade, &instrument, ts_init).unwrap();
867
868 assert_eq!(ws_tick.trade_id, rest_tick.trade_id);
869 assert_eq!(ws_tick.ts_event, rest_tick.ts_event);
870 assert_eq!(ws_tick.price, rest_tick.price);
871 assert_eq!(ws_tick.size, rest_tick.size);
872 assert_eq!(ws_tick.aggressor_side, rest_tick.aggressor_side);
873 }
874
875 #[rstest]
876 fn test_parse_book_l1_empty_sides() {
877 let book = AxMdBookL1 {
878 ts: 1700000000,
879 tn: 12345,
880 s: Ustr::from("TEST-PERP"),
881 b: vec![],
882 a: vec![],
883 };
884
885 let instrument = create_test_instrument();
886 let ts_init = UnixNanos::default();
887
888 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
889
890 assert_eq!(quote.bid_price.as_f64(), 0.0);
891 assert_eq!(quote.ask_price.as_f64(), 0.0);
892 assert_eq!(quote.bid_size.as_f64(), 0.0);
893 assert_eq!(quote.ask_size.as_f64(), 0.0);
894 }
895
896 #[rstest]
897 fn test_parse_book_l2_empty_book() {
898 let book = AxMdBookL2 {
899 ts: 1700000000,
900 tn: 12345,
901 s: Ustr::from("TEST-PERP"),
902 b: vec![],
903 a: vec![],
904 st: true,
905 };
906
907 let instrument = create_test_instrument();
908 let ts_init = UnixNanos::default();
909
910 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
911
912 assert_eq!(deltas.deltas.len(), 1);
914 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
915 assert!(deltas.deltas[0].flags & RecordFlag::F_LAST as u8 != 0);
916 }
917
918 #[rstest]
919 fn test_parse_candle_bar() {
920 use crate::common::enums::AxCandleWidth;
921
922 let candle = AxMdCandle {
923 symbol: Ustr::from("BTC-PERP"),
924 ts: 1700000000,
925 open: dec!(50000.00),
926 high: dec!(51000.00),
927 low: dec!(49500.00),
928 close: dec!(50500.00),
929 volume: 1000,
930 buy_volume: 600,
931 sell_volume: 400,
932 width: AxCandleWidth::Minutes1,
933 };
934
935 let instrument = create_test_instrument();
936 let ts_init = UnixNanos::default();
937
938 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
939
940 assert_eq!(bar.open.as_f64(), 50000.00);
941 assert_eq!(bar.high.as_f64(), 51000.00);
942 assert_eq!(bar.low.as_f64(), 49500.00);
943 assert_eq!(bar.close.as_f64(), 50500.00);
944 assert_eq!(bar.volume.as_f64(), 1000.0);
945 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "BTC-PERP");
946 }
947
948 #[rstest]
949 fn test_parse_candle_from_test_data() {
950 let json = include_str!("../../../test_data/ws_md_candle.json");
951 let candle: AxMdCandle = serde_json::from_str(json).unwrap();
952
953 assert_eq!(candle.symbol, "EURUSD-PERP");
954 assert_eq!(candle.open, dec!(49500.00));
955 assert_eq!(candle.close, dec!(50000.00));
956
957 let instrument = create_instrument_with_precision("EURUSD-PERP", 2, 3);
958 let ts_init = UnixNanos::default();
959
960 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
961
962 assert_eq!(bar.open.as_f64(), 49500.00);
963 assert_eq!(bar.high.as_f64(), 50500.00);
964 assert_eq!(bar.low.as_f64(), 49000.00);
965 assert_eq!(bar.close.as_f64(), 50000.00);
966 assert_eq!(bar.volume.as_f64(), 5000.0);
967 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "EURUSD-PERP");
968 }
969
970 #[rstest]
971 fn test_incremental_books_cannot_clear_snapshots_or_supply_quotes() {
972 let mut l2: AxMdBookL2 =
973 serde_json::from_str(include_str!("../../../test_data/captured/ws-md-2.json")).unwrap();
974 let mut l3: AxMdBookL3 =
975 serde_json::from_str(include_str!("../../../test_data/captured/ws-md-3.json")).unwrap();
976 l2.st = false;
977 l3.st = false;
978 let instrument = create_test_instrument();
979 let ts_init = UnixNanos::default();
980
981 for result in [
982 parse_book_l2_quote(&l2, &instrument, ts_init).map(|_| ()),
983 parse_book_l3_quote(&l3, &instrument, ts_init).map(|_| ()),
984 parse_book_l2_deltas(&l2, &instrument, 1, ts_init).map(|_| ()),
985 parse_book_l3_deltas(&l3, &instrument, 1, ts_init).map(|_| ()),
986 ] {
987 assert_eq!(
988 result.unwrap_err().to_string(),
989 "AX incremental book frames are not supported"
990 );
991 }
992 }
993}