1use crate::enums::OrderSide;
7use crate::instruments::InstrumentId;
8use crate::simd::SimdOps;
9use quanta::{Clock, Instant};
10use rust_decimal::Decimal;
11use rust_decimal::prelude::FromPrimitive;
12use smallvec::SmallVec;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19#[repr(align(64))] pub struct MarketTrade {
21 pub timestamp: Instant,
23 pub exchange_time_ns: u64,
25 pub price: Decimal,
27 pub quantity: Decimal,
29 pub direction: OrderSide,
31 pub instrument_id: InstrumentId,
33}
34
35#[derive(Debug)]
38#[repr(align(64))]
39pub struct TradeBatch {
40 trades: SmallVec<[MarketTrade; 128]>,
42
43 clock: Clock,
45
46 instrument_id: InstrumentId,
48
49 last_update: u64,
51}
52
53impl MarketTrade {
54 #[inline]
56 #[must_use]
57 pub fn new(
58 price: Decimal,
59 quantity: Decimal,
60 direction: OrderSide,
61 instrument_id: InstrumentId,
62 clock: &Clock,
63 exchange_time_ns: u64,
64 ) -> Self {
65 Self {
66 timestamp: clock.now(),
67 exchange_time_ns,
68 price,
69 quantity,
70 direction,
71 instrument_id,
72 }
73 }
74
75 #[inline]
77 #[must_use]
78 pub fn latency(&self) -> Option<u64> {
79 if self.exchange_time_ns == 0 {
80 return None;
81 }
82
83 let local_time = u64::try_from(self.timestamp.duration_since(Instant::recent()).as_nanos())
84 .unwrap_or(u64::MAX);
85 Some(local_time.saturating_sub(self.exchange_time_ns))
86 }
87
88 #[inline]
90 #[must_use]
91 pub fn notional_value(&self) -> Decimal {
92 self.price * self.quantity
93 }
94}
95
96impl TradeBatch {
97 #[inline]
99 #[must_use]
100 pub fn new(instrument_id: InstrumentId) -> Self {
101 Self {
102 trades: SmallVec::with_capacity(16),
103 clock: Clock::new(),
104 instrument_id,
105 last_update: 0,
106 }
107 }
108
109 #[inline]
111 #[must_use]
112 pub fn with_clock(instrument_id: InstrumentId, clock: Clock) -> Self {
113 Self {
114 trades: SmallVec::with_capacity(16),
115 clock,
116 instrument_id,
117 last_update: 0,
118 }
119 }
120
121 #[inline]
132 pub fn add_trade(
133 &mut self,
134 price: Decimal,
135 quantity: Decimal,
136 direction: OrderSide,
137 exchange_time_ns: u64,
138 ) {
139 let trade = MarketTrade::new(
140 price,
141 quantity,
142 direction,
143 self.instrument_id.clone(),
144 &self.clock,
145 exchange_time_ns,
146 );
147
148 let insert_pos = match self
151 .trades
152 .binary_search_by(|existing| existing.timestamp.cmp(&trade.timestamp))
153 {
154 Ok(pos) | Err(pos) => pos,
156 };
157
158 self.trades.insert(insert_pos, trade);
159 self.last_update = self.clock.raw();
160 }
161
162 #[inline]
164 #[must_use]
165 pub fn trades(&self) -> &[MarketTrade] {
166 &self.trades
167 }
168
169 #[inline]
171 #[must_use]
172 pub fn len(&self) -> usize {
173 self.trades.len()
174 }
175
176 #[inline]
178 #[must_use]
179 pub fn is_empty(&self) -> bool {
180 self.trades.is_empty()
181 }
182
183 #[inline]
185 #[must_use]
186 pub const fn instrument_id(&self) -> &InstrumentId {
187 &self.instrument_id
188 }
189
190 #[inline]
192 #[must_use]
193 pub const fn last_update(&self) -> u64 {
194 self.last_update
195 }
196
197 #[inline]
200 #[must_use]
201 pub fn count_by_direction(&self, direction: OrderSide) -> usize {
202 self.trades
203 .iter()
204 .filter(|t| t.direction == direction)
205 .count()
206 }
207
208 #[inline]
210 pub fn iter_by_direction(
211 &self,
212 direction: OrderSide,
213 ) -> impl Iterator<Item = &MarketTrade> + '_ {
214 self.trades.iter().filter(move |t| t.direction == direction)
215 }
216
217 #[inline]
220 pub fn iter_after(&self, timestamp: Instant) -> impl Iterator<Item = &MarketTrade> + '_ {
221 self.trades.iter().filter(move |t| t.timestamp > timestamp)
222 }
223
224 #[inline]
227 pub fn iter_before(&self, timestamp: Instant) -> impl Iterator<Item = &MarketTrade> + '_ {
228 self.trades.iter().filter(move |t| t.timestamp < timestamp)
229 }
230
231 #[inline]
243 #[must_use]
244 pub fn total_volume(&self) -> Decimal {
245 if self.trades.is_empty() {
246 return Decimal::ZERO;
247 }
248
249 if self.trades.len() == 1 {
250 return self.trades[0].quantity;
251 }
252
253 if self.trades.len() <= 3 {
255 return self
256 .trades
257 .iter()
258 .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
259 }
260
261 if self.trades.len() <= 64 {
263 let mut quantities = [Decimal::ZERO; 64];
264 for (i, trade) in self.trades.iter().enumerate() {
265 if i >= 64 {
266 break;
267 }
268 quantities[i] = trade.quantity;
269 }
270
271 return SimdOps::sum_decimal(&quantities[..self.trades.len()]);
273 }
274
275 let quantities: Vec<Decimal> = self.trades.iter().map(|t| t.quantity).collect();
277 SimdOps::sum_decimal(&quantities)
278 }
279
280 #[inline]
292 #[must_use]
293 pub fn buy_volume(&self) -> Decimal {
294 if self.trades.is_empty() {
295 return Decimal::ZERO;
296 }
297
298 let buy_count = self.count_by_direction(OrderSide::Buy);
300
301 if buy_count == 0 {
302 return Decimal::ZERO;
303 }
304
305 if buy_count == 1 {
306 return self
308 .trades
309 .iter()
310 .find(|t| t.direction == OrderSide::Buy)
311 .map_or(Decimal::ZERO, |t| t.quantity);
312 }
313
314 if buy_count <= 3 {
316 return self
317 .iter_by_direction(OrderSide::Buy)
318 .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
319 }
320
321 if buy_count <= 64 {
323 let mut quantities = [Decimal::ZERO; 64];
324
325 for (index, trade) in self.iter_by_direction(OrderSide::Buy).enumerate() {
326 if index >= 64 {
327 break;
328 }
329 quantities[index] = trade.quantity;
330 }
331
332 return SimdOps::sum_decimal(&quantities[..buy_count]);
334 }
335
336 let quantities: Vec<Decimal> = self
338 .iter_by_direction(OrderSide::Buy)
339 .map(|t| t.quantity)
340 .collect();
341
342 SimdOps::sum_decimal(&quantities)
343 }
344
345 #[inline]
357 #[must_use]
358 pub fn sell_volume(&self) -> Decimal {
359 if self.trades.is_empty() {
360 return Decimal::ZERO;
361 }
362
363 let sell_count = self.count_by_direction(OrderSide::Sell);
365
366 if sell_count == 0 {
367 return Decimal::ZERO;
368 }
369
370 if sell_count == 1 {
371 return self
373 .trades
374 .iter()
375 .find(|t| t.direction == OrderSide::Sell)
376 .map_or(Decimal::ZERO, |t| t.quantity);
377 }
378
379 if sell_count <= 3 {
381 return self
382 .iter_by_direction(OrderSide::Sell)
383 .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
384 }
385
386 if sell_count <= 64 {
388 let mut quantities = [Decimal::ZERO; 64];
389
390 for (index, trade) in self.iter_by_direction(OrderSide::Sell).enumerate() {
391 if index >= 64 {
392 break;
393 }
394 quantities[index] = trade.quantity;
395 }
396
397 return SimdOps::sum_decimal(&quantities[..sell_count]);
399 }
400
401 let quantities: Vec<Decimal> = self
403 .iter_by_direction(OrderSide::Sell)
404 .map(|t| t.quantity)
405 .collect();
406
407 SimdOps::sum_decimal(&quantities)
408 }
409
410 #[inline]
427 #[must_use]
428 pub fn volume_imbalance(&self) -> Option<f64> {
429 if self.trades.is_empty() {
430 return None;
431 }
432
433 let buy_vol = self.buy_volume();
435 let sell_vol = self.sell_volume();
436
437 let total = buy_vol + sell_vol;
439
440 if total.is_zero() {
442 return None;
443 }
444
445 let imbalance = (buy_vol - sell_vol) / total;
448
449 Some(rusty_common::decimal_utils::decimal_to_f64_or_nan(
451 imbalance,
452 ))
453 }
454
455 #[inline]
472 #[must_use]
473 pub fn vwap(&self) -> Option<Decimal> {
474 if self.trades.is_empty() {
475 return None;
476 }
477
478 let total_quantity = self.total_volume();
480 if total_quantity.is_zero() {
481 return None;
482 }
483
484 if self.trades.len() == 1 {
485 return Some(self.trades[0].price);
487 }
488
489 if self.trades.len() <= 3 {
491 let total_price_quantity = self
492 .trades
493 .iter()
494 .fold(Decimal::ZERO, |acc, t| acc + (t.price * t.quantity));
495 return Some((total_price_quantity / total_quantity).round_dp(2));
497 }
498
499 if self.trades.len() <= 64 {
501 let mut prices = [0.0f64; 64];
502 let mut quantities = [0.0f64; 64];
503
504 for (i, trade) in self.trades.iter().enumerate() {
505 if i >= 64 {
506 break;
507 }
508 prices[i] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.price);
509 quantities[i] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.quantity);
510 }
511
512 let dot_product = SimdOps::dot_product_f64(
514 &prices[..self.trades.len()],
515 &quantities[..self.trades.len()],
516 );
517
518 let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
520 return Some((total_price_quantity / total_quantity).round_dp(2));
522 }
523
524 let prices: Vec<f64> = self
526 .trades
527 .iter()
528 .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.price))
529 .collect();
530
531 let quantities: Vec<f64> = self
532 .trades
533 .iter()
534 .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.quantity))
535 .collect();
536
537 let dot_product = SimdOps::dot_product_f64(&prices, &quantities);
539
540 let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
542 Some((total_price_quantity / total_quantity).round_dp(2))
544 }
545
546 #[inline]
566 #[must_use]
567 pub fn vwap_by_direction(&self, direction: OrderSide) -> Option<Decimal> {
568 if self.trades.is_empty() {
569 return None;
570 }
571
572 let count = self.count_by_direction(direction);
574
575 if count == 0 {
576 return None;
577 }
578
579 if count == 1 {
580 return self
582 .trades
583 .iter()
584 .find(|t| t.direction == direction)
585 .map(|t| t.price);
586 }
587
588 if count <= 3 {
590 let mut total_quantity = Decimal::ZERO;
591 let mut total_price_quantity = Decimal::ZERO;
592
593 for trade in self.iter_by_direction(direction) {
594 total_quantity += trade.quantity;
595 total_price_quantity += trade.price * trade.quantity;
596 }
597
598 if total_quantity.is_zero() {
599 return None;
600 }
601
602 return Some((total_price_quantity / total_quantity).round_dp(2));
604 }
605
606 if count <= 64 {
608 let mut prices = [0.0f64; 64];
609 let mut quantities = [0.0f64; 64];
610 let mut total_quantity = Decimal::ZERO;
611
612 for (index, trade) in self.iter_by_direction(direction).enumerate() {
613 if index >= 64 {
614 break;
615 }
616 prices[index] = rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.price);
617 quantities[index] =
618 rusty_common::decimal_utils::decimal_to_f64_or_nan(trade.quantity);
619 total_quantity += trade.quantity;
620 }
621
622 if total_quantity.is_zero() {
623 return None;
624 }
625
626 let dot_product = SimdOps::dot_product_f64(&prices[..count], &quantities[..count]);
628
629 let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
631 return Some((total_price_quantity / total_quantity).round_dp(2));
633 }
634
635 let filtered_trades: Vec<&MarketTrade> = self.iter_by_direction(direction).collect();
637
638 let prices: Vec<f64> = filtered_trades
639 .iter()
640 .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.price))
641 .collect();
642
643 let quantities: Vec<f64> = filtered_trades
644 .iter()
645 .map(|t| rusty_common::decimal_utils::decimal_to_f64_or_nan(t.quantity))
646 .collect();
647
648 let total_quantity = filtered_trades
650 .iter()
651 .fold(Decimal::ZERO, |acc, t| acc + t.quantity);
652
653 if total_quantity.is_zero() {
654 return None;
655 }
656
657 let dot_product = SimdOps::dot_product_f64(&prices, &quantities);
659
660 let total_price_quantity = Decimal::from_f64(dot_product).unwrap_or(Decimal::ZERO);
662 Some((total_price_quantity / total_quantity).round_dp(2))
664 }
665
666 #[inline]
668 pub fn clear(&mut self) {
669 self.trades.clear();
670 self.last_update = self.clock.raw();
671 }
672
673 #[inline]
688 pub fn truncate(&mut self, size: usize) {
689 if self.trades.len() <= size {
690 return;
692 }
693
694 let current_len = self.trades.len();
695 let start_idx = current_len - size;
696
697 if current_len <= 128 || start_idx <= 32 {
699 self.trades.drain(0..start_idx);
700 } else {
701 let mut new_trades = SmallVec::with_capacity(size);
704
705 for i in start_idx..current_len {
708 new_trades.push(self.trades[i].clone());
709 }
710
711 self.trades = new_trades;
713 }
714
715 self.last_update = self.clock.raw();
717 }
718
719 #[inline]
721 #[must_use]
722 pub fn latest_trade(&self) -> Option<&MarketTrade> {
723 self.trades.last()
724 }
725
726 #[inline]
728 #[must_use]
729 pub fn average_trade_size(&self) -> Option<Decimal> {
730 if self.trades.is_empty() {
731 return None;
732 }
733
734 Some(self.total_volume() / Decimal::from(self.trades.len()))
735 }
736
737 #[inline]
754 #[must_use]
755 pub fn median_price(&self) -> Option<Decimal> {
756 if self.trades.is_empty() {
757 return None;
758 }
759
760 if self.trades.len() == 1 {
761 return Some(self.trades[0].price);
762 }
763
764 if self.trades.len() <= 5 {
766 let mut prices = [Decimal::ZERO; 5];
767 for (i, trade) in self.trades.iter().enumerate() {
768 prices[i] = trade.price;
769 }
770
771 prices[..self.trades.len()].sort();
773
774 let mid = self.trades.len() / 2;
775 if self.trades.len().is_multiple_of(2) {
776 return Some((prices[mid - 1] + prices[mid]) / Decimal::TWO);
778 }
779 return Some(prices[mid]);
781 }
782
783 if self.trades.len() <= 64 {
785 let mut prices = [Decimal::ZERO; 64];
786 for (i, trade) in self.trades.iter().enumerate() {
787 if i >= 64 {
788 break;
789 }
790 prices[i] = trade.price;
791 }
792
793 prices[..self.trades.len()].sort();
795
796 let mid = self.trades.len() / 2;
797 if self.trades.len().is_multiple_of(2) {
798 return Some((prices[mid - 1] + prices[mid]) / Decimal::TWO);
800 }
801 return Some(prices[mid]);
803 }
804
805 let mut prices: Vec<Decimal> = self.trades.iter().map(|t| t.price).collect();
808
809 let mid = prices.len() / 2;
811
812 if prices.len().is_multiple_of(2) {
813 let (left, _, _) = prices.select_nth_unstable(mid - 1);
816 let median_left = left[mid - 1];
817
818 let (_, median_right, _) = prices.select_nth_unstable(mid);
819
820 Some((median_left + *median_right) / Decimal::TWO)
822 } else {
823 let (_, median, _) = prices.select_nth_unstable(mid);
826 Some(*median)
827 }
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use crate::venues::Venue;
835 use rust_decimal_macros::dec;
836 use std::thread;
837 use std::time::Duration;
838
839 fn create_instrument_id() -> InstrumentId {
840 InstrumentId::new("BTCUSDT", Venue::Binance)
841 }
842
843 #[test]
844 fn test_add_trade() {
845 let mut batch = TradeBatch::new(create_instrument_id());
846
847 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
848 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
849
850 assert_eq!(batch.len(), 2);
851 assert_eq!(batch.trades()[0].price, dec!(100.5));
852 assert_eq!(batch.trades()[0].quantity, dec!(10.0));
853 assert_eq!(batch.trades()[0].direction, OrderSide::Buy);
854 assert_eq!(batch.trades()[1].price, dec!(101.0));
855 assert_eq!(batch.trades()[1].quantity, dec!(5.0));
856 assert_eq!(batch.trades()[1].direction, OrderSide::Sell);
857 }
858
859 #[test]
860 fn test_count_by_direction() {
861 let mut batch = TradeBatch::new(create_instrument_id());
862
863 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
864 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
865 batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
866
867 assert_eq!(batch.count_by_direction(OrderSide::Buy), 2);
868 assert_eq!(batch.count_by_direction(OrderSide::Sell), 1);
869 }
870
871 #[test]
872 fn test_total_volume() {
873 let mut batch = TradeBatch::new(create_instrument_id());
874
875 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
876 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
877 batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
878
879 let total_volume = batch.total_volume();
880 assert_eq!(total_volume, dec!(22.5));
881 }
882
883 #[test]
884 fn test_vwap() {
885 let mut batch = TradeBatch::new(create_instrument_id());
886
887 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
888 batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
889
890 let vwap = batch.vwap();
891 assert!(vwap.is_some());
892 assert_eq!(vwap.unwrap(), dec!(100.67));
893 }
894
895 #[test]
896 fn test_vwap_with_no_trades() {
897 let batch = TradeBatch::new(create_instrument_id());
898
899 let vwap = batch.vwap();
900 assert!(vwap.is_none());
901 }
902
903 #[test]
904 fn test_iter_after() {
905 let clock = Clock::new();
906 let mut batch = TradeBatch::with_clock(create_instrument_id(), clock.clone());
907
908 let timestamp1 = clock.now();
909 thread::sleep(Duration::from_millis(10));
910 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
911 let timestamp2 = clock.now();
912 thread::sleep(Duration::from_millis(10));
913 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
914
915 assert_eq!(batch.iter_after(timestamp1).count(), 2);
916
917 let filtered_trades: Vec<_> = batch.iter_after(timestamp2).collect();
918 assert_eq!(filtered_trades.len(), 1);
919 assert_eq!(filtered_trades[0].price, dec!(101.0));
920 }
921
922 #[test]
923 fn test_iter_before() {
924 let clock = Clock::new();
925 let mut batch = TradeBatch::with_clock(create_instrument_id(), clock.clone());
926
927 let timestamp1 = clock.now();
928 thread::sleep(Duration::from_millis(10));
929 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
930 let timestamp2 = clock.now();
931 thread::sleep(Duration::from_millis(10));
932 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
933
934 assert_eq!(batch.iter_before(timestamp1).count(), 0);
935
936 let filtered_trades: Vec<_> = batch.iter_before(timestamp2).collect();
937 assert_eq!(filtered_trades.len(), 1);
938 assert_eq!(filtered_trades[0].price, dec!(100.5));
939 }
940
941 #[test]
942 fn test_clear() {
943 let mut batch = TradeBatch::new(create_instrument_id());
944
945 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
946 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
947
948 assert_eq!(batch.len(), 2);
949
950 batch.clear();
951
952 assert_eq!(batch.len(), 0);
953 }
954
955 #[test]
956 fn test_truncate() {
957 let mut batch = TradeBatch::new(create_instrument_id());
958
959 batch.add_trade(dec!(100.5), dec!(10.0), OrderSide::Buy, 0);
960 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
961 batch.add_trade(dec!(102.5), dec!(7.5), OrderSide::Buy, 0);
962
963 assert_eq!(batch.len(), 3);
964
965 batch.truncate(2);
966
967 assert_eq!(batch.len(), 2);
968 assert_eq!(batch.trades()[0].price, dec!(101.0));
969 assert_eq!(batch.trades()[1].price, dec!(102.5));
970 }
971
972 #[test]
973 fn test_volume_imbalance() {
974 let mut batch = TradeBatch::new(create_instrument_id());
975
976 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
978 batch.add_trade(dec!(102.0), dec!(10.0), OrderSide::Sell, 0);
979
980 let imbalance = batch.volume_imbalance();
981 assert!(imbalance.is_some());
982 assert!((imbalance.unwrap() - 0.0).abs() < f64::EPSILON);
983
984 batch.clear();
986 batch.add_trade(dec!(100.0), dec!(15.0), OrderSide::Buy, 0);
987 batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
988
989 let imbalance = batch.volume_imbalance();
990 assert!(imbalance.is_some());
991 assert!((imbalance.unwrap() - 0.5).abs() < f64::EPSILON);
993 }
994
995 #[test]
996 fn test_buy_sell_volume() {
997 let mut batch = TradeBatch::new(create_instrument_id());
998
999 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1000 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1001 batch.add_trade(dec!(102.0), dec!(7.5), OrderSide::Sell, 0);
1002
1003 assert_eq!(batch.buy_volume(), dec!(15.0));
1004 assert_eq!(batch.sell_volume(), dec!(7.5));
1005 }
1006
1007 #[test]
1008 fn test_vwap_by_direction() {
1009 let mut batch = TradeBatch::new(create_instrument_id());
1010
1011 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1012 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1013 batch.add_trade(dec!(102.0), dec!(10.0), OrderSide::Sell, 0);
1014
1015 let buy_vwap = batch.vwap_by_direction(OrderSide::Buy);
1017 assert!(buy_vwap.is_some());
1018 assert_eq!(buy_vwap.unwrap().round_dp(2), dec!(100.33));
1019
1020 let sell_vwap = batch.vwap_by_direction(OrderSide::Sell);
1022 assert!(sell_vwap.is_some());
1023 assert_eq!(sell_vwap.unwrap(), dec!(102.0));
1024 }
1025
1026 #[test]
1027 fn test_latest_trade() {
1028 let mut batch = TradeBatch::new(create_instrument_id());
1029
1030 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1031 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Buy, 0);
1032
1033 let latest = batch.latest_trade();
1034 assert!(latest.is_some());
1035 assert_eq!(latest.unwrap().price, dec!(101.0));
1036 }
1037
1038 #[test]
1039 fn test_average_trade_size() {
1040 let mut batch = TradeBatch::new(create_instrument_id());
1041
1042 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1043 batch.add_trade(dec!(101.0), dec!(5.0), OrderSide::Sell, 0);
1044
1045 let avg_size = batch.average_trade_size();
1046 assert!(avg_size.is_some());
1047 assert_eq!(avg_size.unwrap(), dec!(7.5));
1048 }
1049
1050 #[test]
1051 fn test_trade_notional_value() {
1052 let clock = Clock::new();
1053 let instrument_id = create_instrument_id();
1054
1055 let trade = MarketTrade::new(
1056 dec!(100.5),
1057 dec!(10.0),
1058 OrderSide::Buy,
1059 instrument_id,
1060 &clock,
1061 0,
1062 );
1063
1064 assert_eq!(trade.notional_value(), dec!(1005.0));
1065 }
1066
1067 #[test]
1068 fn test_trade_latency() {
1069 let clock = Clock::new();
1070 let instrument_id = create_instrument_id();
1071
1072 let trade1 = MarketTrade::new(
1074 dec!(100.0),
1075 dec!(10.0),
1076 OrderSide::Buy,
1077 instrument_id.clone(),
1078 &clock,
1079 0,
1080 );
1081 assert_eq!(trade1.latency(), None);
1082
1083 let now_ns = clock.raw();
1086 let trade2 = MarketTrade::new(
1087 dec!(100.0),
1088 dec!(10.0),
1089 OrderSide::Buy,
1090 instrument_id,
1091 &clock,
1092 now_ns - 1_000_000, );
1094 assert!(trade2.latency().is_some());
1095 }
1096
1097 #[test]
1098 fn test_median_price() {
1099 let mut batch = TradeBatch::new(create_instrument_id());
1100
1101 assert_eq!(batch.median_price(), None);
1103
1104 batch.add_trade(dec!(100.0), dec!(10.0), OrderSide::Buy, 0);
1106 batch.add_trade(dec!(102.0), dec!(5.0), OrderSide::Sell, 0);
1107 batch.add_trade(dec!(101.0), dec!(7.5), OrderSide::Buy, 0);
1108
1109 let median = batch.median_price();
1110 assert!(median.is_some());
1111 assert_eq!(median.unwrap(), dec!(101.0));
1112
1113 batch.add_trade(dec!(103.0), dec!(3.0), OrderSide::Sell, 0);
1115
1116 let median = batch.median_price();
1117 assert!(median.is_some());
1118 assert_eq!(median.unwrap(), dec!(101.5)); }
1120}