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.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 #[case(AxOrderSide::Buy, AggressorSide::Buy)]
712 #[case(AxOrderSide::Sell, AggressorSide::Sell)]
713 fn test_parse_trade_tick(#[case] side: AxOrderSide, #[case] expected_side: AggressorSide) {
714 let trade = AxMdTrade {
715 ts: 1700000000,
716 tn: 12345,
717 s: Ustr::from("BTC-PERP"),
718 p: dec!(50000.50),
719 q: 100,
720 d: side,
721 };
722
723 let instrument = create_test_instrument();
724 let ts_init = UnixNanos::from(1700000001000000000);
725
726 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
727
728 assert_eq!(tick.instrument_id, instrument.id());
729 assert_eq!(tick.price.as_decimal(), dec!(50000.50));
730 assert_eq!(tick.size.as_decimal(), dec!(100));
731 assert_eq!(tick.aggressor_side, expected_side);
732 assert_eq!(tick.ts_event, UnixNanos::from(1700000000000012345));
733 assert_eq!(tick.ts_init, ts_init);
734 }
735
736 #[rstest]
737 fn test_parse_book_l1_from_captured_data() {
738 let json = include_str!("../../../test_data/ws_md_book_l1_captured.json");
739 let book: AxMdBookL1 = serde_json::from_str(json).unwrap();
740
741 assert_eq!(book.s, "EURUSD-PERP");
742 assert_eq!(book.b.len(), 1);
743 assert_eq!(book.a.len(), 1);
744
745 let instrument = create_eurusd_instrument();
746 let ts_init = UnixNanos::default();
747
748 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
749
750 assert_eq!(quote.instrument_id.symbol.as_str(), "EURUSD-PERP");
751 assert_eq!(quote.bid_price.as_f64(), 1.1712);
752 assert_eq!(quote.ask_price.as_f64(), 1.1717);
753 assert_eq!(quote.bid_size.as_f64(), 300.0);
754 assert_eq!(quote.ask_size.as_f64(), 100.0);
755 }
756
757 #[rstest]
758 fn test_parse_book_l2_from_captured_data() {
759 let json = include_str!("../../../test_data/ws_md_book_l2_captured.json");
760 let book: AxMdBookL2 = serde_json::from_str(json).unwrap();
761
762 assert_eq!(book.s, "EURUSD-PERP");
763 assert_eq!(book.b.len(), 13);
764 assert_eq!(book.a.len(), 12);
765
766 let instrument = create_eurusd_instrument();
767 let ts_init = UnixNanos::default();
768
769 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
770
771 assert_eq!(deltas.deltas.len(), 26);
773 assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
774
775 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
777
778 let first_bid = &deltas.deltas[1];
780 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
781 assert_eq!(first_bid.order.price.as_f64(), 1.1712);
782 assert_eq!(first_bid.order.size.as_f64(), 300.0);
783
784 let first_ask = &deltas.deltas[14];
786 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
787 assert_eq!(first_ask.order.price.as_f64(), 1.1719);
788 assert_eq!(first_ask.order.size.as_f64(), 400.0);
789
790 let last_delta = deltas.deltas.last().unwrap();
792 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
793 }
794
795 #[rstest]
796 fn test_parse_book_l3_from_captured_data() {
797 let json = include_str!("../../../test_data/ws_md_book_l3_captured.json");
798 let book: AxMdBookL3 = serde_json::from_str(json).unwrap();
799
800 assert_eq!(book.s, "EURUSD-PERP");
801 assert_eq!(book.b.len(), 15);
802 assert_eq!(book.a.len(), 14);
803
804 let instrument = create_eurusd_instrument();
805 let ts_init = UnixNanos::default();
806
807 let deltas = parse_book_l3_deltas(&book, &instrument, 1, ts_init).unwrap();
808
809 assert_eq!(deltas.deltas.len(), 30); assert_eq!(deltas.instrument_id.symbol.as_str(), "EURUSD-PERP");
813
814 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
816
817 let first_bid = &deltas.deltas[1];
819 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
820 assert_eq!(first_bid.order.price.as_f64(), 1.1714);
821 assert_eq!(first_bid.order.size.as_f64(), 100.0);
822
823 let last_delta = deltas.deltas.last().unwrap();
825 assert!(last_delta.flags & RecordFlag::F_LAST as u8 != 0);
826 }
827
828 #[rstest]
829 fn test_parse_trade_from_captured_data() {
830 let json = include_str!("../../../test_data/ws_md_trade_captured.json");
831 let trade: AxMdTrade = serde_json::from_str(json).unwrap();
832
833 assert_eq!(trade.s, "EURUSD-PERP");
834 assert_eq!(trade.p, dec!(1.1719));
835 assert_eq!(trade.q, 400);
836 assert_eq!(trade.d, AxOrderSide::Buy);
837
838 let instrument = create_eurusd_instrument();
839 let ts_init = UnixNanos::default();
840
841 let tick = parse_trade_tick(&trade, &instrument, ts_init).unwrap();
842
843 assert_eq!(tick.instrument_id.symbol.as_str(), "EURUSD-PERP");
844 assert_eq!(tick.price.as_f64(), 1.1719);
845 assert_eq!(tick.size.as_f64(), 400.0);
846 assert_eq!(tick.aggressor_side, AggressorSide::Buy);
847 assert_eq!(
848 tick.trade_id.to_string(),
849 "1766193240334589144-38b4fe5a94a253d0"
850 );
851 }
852
853 #[rstest]
854 #[case(AxOrderSide::Buy)]
855 #[case(AxOrderSide::Sell)]
856 fn test_parse_trade_tick_matches_rest_trade_id_for_the_same_trade(#[case] side: AxOrderSide) {
857 let json = include_str!("../../../test_data/ws_md_trade_captured.json");
860 let mut ws_trade: AxMdTrade = serde_json::from_str(json).unwrap();
861 ws_trade.d = side;
862
863 let rest_trade = AxRestTrade {
864 ts: ws_trade.ts,
865 tn: ws_trade.tn,
866 p: ws_trade.p,
867 q: ws_trade.q as i64,
868 s: ws_trade.s,
869 d: ws_trade.d,
870 };
871 let instrument = create_eurusd_instrument();
872 let ts_init = UnixNanos::default();
873
874 let ws_tick = parse_trade_tick(&ws_trade, &instrument, ts_init).unwrap();
875 let rest_tick = parse_rest_trade_tick(&rest_trade, &instrument, ts_init).unwrap();
876
877 assert_eq!(ws_tick, rest_tick);
878 }
879
880 #[rstest]
881 fn test_parse_book_l1_empty_sides() {
882 let book = AxMdBookL1 {
883 ts: 1700000000,
884 tn: 12345,
885 s: Ustr::from("TEST-PERP"),
886 b: vec![],
887 a: vec![],
888 };
889
890 let instrument = create_test_instrument();
891 let ts_init = UnixNanos::default();
892
893 let quote = parse_book_l1_quote(&book, &instrument, ts_init).unwrap();
894
895 assert_eq!(quote.bid_price.as_f64(), 0.0);
896 assert_eq!(quote.ask_price.as_f64(), 0.0);
897 assert_eq!(quote.bid_size.as_f64(), 0.0);
898 assert_eq!(quote.ask_size.as_f64(), 0.0);
899 }
900
901 #[rstest]
902 fn test_parse_book_l2_empty_book() {
903 let book = AxMdBookL2 {
904 ts: 1700000000,
905 tn: 12345,
906 s: Ustr::from("TEST-PERP"),
907 b: vec![],
908 a: vec![],
909 st: true,
910 };
911
912 let instrument = create_test_instrument();
913 let ts_init = UnixNanos::default();
914
915 let deltas = parse_book_l2_deltas(&book, &instrument, 1, ts_init).unwrap();
916
917 assert_eq!(deltas.deltas.len(), 1);
919 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
920 assert!(deltas.deltas[0].flags & RecordFlag::F_LAST as u8 != 0);
921 }
922
923 #[rstest]
924 fn test_parse_candle_bar() {
925 use crate::common::enums::AxCandleWidth;
926
927 let candle = AxMdCandle {
928 symbol: Ustr::from("BTC-PERP"),
929 ts: 1700000000,
930 open: dec!(50000.00),
931 high: dec!(51000.00),
932 low: dec!(49500.00),
933 close: dec!(50500.00),
934 volume: 1000,
935 buy_volume: 600,
936 sell_volume: 400,
937 width: AxCandleWidth::Minutes1,
938 };
939
940 let instrument = create_test_instrument();
941 let ts_init = UnixNanos::default();
942
943 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
944
945 assert_eq!(bar.open.as_f64(), 50000.00);
946 assert_eq!(bar.high.as_f64(), 51000.00);
947 assert_eq!(bar.low.as_f64(), 49500.00);
948 assert_eq!(bar.close.as_f64(), 50500.00);
949 assert_eq!(bar.volume.as_f64(), 1000.0);
950 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "BTC-PERP");
951 }
952
953 #[rstest]
954 fn test_parse_candle_from_test_data() {
955 let json = include_str!("../../../test_data/ws_md_candle.json");
956 let candle: AxMdCandle = serde_json::from_str(json).unwrap();
957
958 assert_eq!(candle.symbol, "EURUSD-PERP");
959 assert_eq!(candle.open, dec!(49500.00));
960 assert_eq!(candle.close, dec!(50000.00));
961
962 let instrument = create_instrument_with_precision("EURUSD-PERP", 2, 3);
963 let ts_init = UnixNanos::default();
964
965 let bar = parse_candle_bar(&candle, &instrument, ts_init).unwrap();
966
967 assert_eq!(bar.open.as_f64(), 49500.00);
968 assert_eq!(bar.high.as_f64(), 50500.00);
969 assert_eq!(bar.low.as_f64(), 49000.00);
970 assert_eq!(bar.close.as_f64(), 50000.00);
971 assert_eq!(bar.volume.as_f64(), 5000.0);
972 assert_eq!(bar.bar_type.instrument_id().symbol.as_str(), "EURUSD-PERP");
973 }
974
975 #[rstest]
976 fn test_incremental_books_cannot_clear_snapshots_or_supply_quotes() {
977 let mut l2: AxMdBookL2 =
978 serde_json::from_str(include_str!("../../../test_data/captured/ws-md-2.json")).unwrap();
979 let mut l3: AxMdBookL3 =
980 serde_json::from_str(include_str!("../../../test_data/captured/ws-md-3.json")).unwrap();
981 l2.st = false;
982 l3.st = false;
983 let instrument = create_test_instrument();
984 let ts_init = UnixNanos::default();
985
986 for result in [
987 parse_book_l2_quote(&l2, &instrument, ts_init).map(|_| ()),
988 parse_book_l3_quote(&l3, &instrument, ts_init).map(|_| ()),
989 parse_book_l2_deltas(&l2, &instrument, 1, ts_init).map(|_| ()),
990 parse_book_l3_deltas(&l3, &instrument, 1, ts_init).map(|_| ()),
991 ] {
992 assert_eq!(
993 result.unwrap_err().to_string(),
994 "AX incremental book frames are not supported"
995 );
996 }
997 }
998}