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