1use crate::error::StreamError;
14use chrono::DateTime;
15use rust_decimal::Decimal;
16use std::str::FromStr;
17use tracing::trace;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
21pub enum Exchange {
22 Binance,
24 Coinbase,
26 Alpaca,
28 Polygon,
30}
31
32impl Exchange {
33 pub fn all() -> &'static [Exchange] {
38 &[
39 Exchange::Binance,
40 Exchange::Coinbase,
41 Exchange::Alpaca,
42 Exchange::Polygon,
43 ]
44 }
45
46 pub fn is_crypto(self) -> bool {
52 matches!(self, Exchange::Binance | Exchange::Coinbase)
53 }
54}
55
56impl std::fmt::Display for Exchange {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Exchange::Binance => write!(f, "Binance"),
60 Exchange::Coinbase => write!(f, "Coinbase"),
61 Exchange::Alpaca => write!(f, "Alpaca"),
62 Exchange::Polygon => write!(f, "Polygon"),
63 }
64 }
65}
66
67impl FromStr for Exchange {
68 type Err = StreamError;
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 match s.to_lowercase().as_str() {
71 "binance" => Ok(Exchange::Binance),
72 "coinbase" => Ok(Exchange::Coinbase),
73 "alpaca" => Ok(Exchange::Alpaca),
74 "polygon" => Ok(Exchange::Polygon),
75 _ => Err(StreamError::UnknownExchange(s.to_string())),
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct RawTick {
83 pub exchange: Exchange,
85 pub symbol: String,
87 pub payload: serde_json::Value,
89 pub received_at_ms: u64,
91}
92
93impl RawTick {
94 pub fn new(exchange: Exchange, symbol: impl Into<String>, payload: serde_json::Value) -> Self {
96 Self {
97 exchange,
98 symbol: symbol.into(),
99 payload,
100 received_at_ms: now_ms(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
107pub struct NormalizedTick {
108 pub exchange: Exchange,
110 pub symbol: String,
112 pub price: Decimal,
114 pub quantity: Decimal,
116 pub side: Option<TradeSide>,
118 pub trade_id: Option<String>,
120 pub exchange_ts_ms: Option<u64>,
122 pub received_at_ms: u64,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
128pub enum TradeSide {
129 Buy,
131 Sell,
133}
134
135impl FromStr for TradeSide {
136 type Err = StreamError;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 match s.to_lowercase().as_str() {
140 "buy" => Ok(TradeSide::Buy),
141 "sell" => Ok(TradeSide::Sell),
142 _ => Err(StreamError::ParseError {
143 exchange: "TradeSide".into(),
144 reason: format!("unknown trade side '{s}'"),
145 }),
146 }
147 }
148}
149
150impl std::fmt::Display for TradeSide {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 TradeSide::Buy => write!(f, "buy"),
154 TradeSide::Sell => write!(f, "sell"),
155 }
156 }
157}
158
159impl TradeSide {
160 pub fn is_buy(self) -> bool {
162 self == TradeSide::Buy
163 }
164
165 pub fn is_sell(self) -> bool {
167 self == TradeSide::Sell
168 }
169}
170
171impl NormalizedTick {
172 pub fn value(&self) -> Decimal {
181 self.price * self.quantity
182 }
183
184 pub fn age_ms(&self, now_ms: u64) -> u64 {
191 now_ms.saturating_sub(self.received_at_ms)
192 }
193
194 pub fn is_stale(&self, now_ms: u64, threshold_ms: u64) -> bool {
199 self.age_ms(now_ms) > threshold_ms
200 }
201
202 pub fn is_buy(&self) -> bool {
206 self.side == Some(TradeSide::Buy)
207 }
208
209 pub fn is_sell(&self) -> bool {
213 self.side == Some(TradeSide::Sell)
214 }
215
216 pub fn is_neutral(&self) -> bool {
222 self.side.is_none()
223 }
224
225 pub fn is_large_trade(&self, threshold: Decimal) -> bool {
230 self.quantity >= threshold
231 }
232
233 pub fn with_side(mut self, side: TradeSide) -> Self {
238 self.side = Some(side);
239 self
240 }
241
242 pub fn with_exchange_ts(mut self, ts_ms: u64) -> Self {
247 self.exchange_ts_ms = Some(ts_ms);
248 self
249 }
250
251 pub fn price_move_from(&self, prev: &NormalizedTick) -> Decimal {
256 self.price - prev.price
257 }
258
259 pub fn is_more_recent_than(&self, other: &NormalizedTick) -> bool {
263 self.received_at_ms > other.received_at_ms
264 }
265
266 pub fn latency_ms(&self) -> Option<i64> {
274 let exchange_ts = self.exchange_ts_ms? as i64;
275 Some(self.received_at_ms as i64 - exchange_ts)
276 }
277
278 pub fn volume_notional(&self) -> rust_decimal::Decimal {
282 self.value()
283 }
284
285 pub fn has_exchange_ts(&self) -> bool {
291 self.exchange_ts_ms.is_some()
292 }
293
294 pub fn side_str(&self) -> &'static str {
296 match self.side {
297 Some(TradeSide::Buy) => "buy",
298 Some(TradeSide::Sell) => "sell",
299 None => "unknown",
300 }
301 }
302
303 pub fn is_round_lot(&self) -> bool {
308 self.quantity.fract().is_zero()
309 }
310
311 pub fn is_same_symbol_as(&self, other: &NormalizedTick) -> bool {
313 self.symbol == other.symbol
314 }
315
316 pub fn price_distance_from(&self, other: &NormalizedTick) -> Decimal {
321 (self.price - other.price).abs()
322 }
323
324 pub fn exchange_latency_ms(&self) -> Option<i64> {
329 self.latency_ms()
330 }
331
332 pub fn is_notional_large_trade(&self, threshold: Decimal) -> bool {
339 self.volume_notional() > threshold
340 }
341
342 pub fn is_zero_price(&self) -> bool {
346 self.price.is_zero()
347 }
348
349 pub fn is_fresh(&self, now_ms: u64, max_age_ms: u64) -> bool {
354 now_ms.saturating_sub(self.received_at_ms) <= max_age_ms
355 }
356
357 pub fn is_above(&self, price: Decimal) -> bool {
359 self.price > price
360 }
361
362 pub fn is_below(&self, price: Decimal) -> bool {
364 self.price < price
365 }
366
367 pub fn is_at(&self, price: Decimal) -> bool {
369 self.price == price
370 }
371
372 pub fn is_aggressive(&self) -> bool {
376 self.side.is_some()
377 }
378
379 #[deprecated(since = "2.2.0", note = "Use `price_move_from` instead")]
383 pub fn price_diff_from(&self, other: &NormalizedTick) -> Decimal {
384 self.price_move_from(other)
385 }
386
387 pub fn is_micro_trade(&self, threshold: Decimal) -> bool {
391 self.quantity < threshold
392 }
393
394 pub fn is_buying_pressure(&self, midpoint: Decimal) -> bool {
398 self.price > midpoint
399 }
400
401 pub fn age_secs(&self, now_ms: u64) -> f64 {
405 now_ms.saturating_sub(self.received_at_ms) as f64 / 1_000.0
406 }
407
408 pub fn is_same_exchange_as(&self, other: &NormalizedTick) -> bool {
410 self.exchange == other.exchange
411 }
412
413 #[deprecated(since = "2.2.0", note = "Use `age_ms` instead")]
417 pub fn quote_age_ms(&self, now_ms: u64) -> u64 {
418 self.age_ms(now_ms)
419 }
420
421 #[deprecated(since = "2.2.0", note = "Use `value` instead")]
425 pub fn notional_value(&self) -> Decimal {
426 self.value()
427 }
428
429 #[deprecated(since = "2.2.0", note = "Use `is_notional_large_trade` instead")]
433 pub fn is_high_value_tick(&self, threshold: Decimal) -> bool {
434 self.is_notional_large_trade(threshold)
435 }
436
437 pub fn side_as_str(&self) -> Option<&'static str> {
439 match self.side {
440 Some(TradeSide::Buy) => Some("buy"),
441 Some(TradeSide::Sell) => Some("sell"),
442 None => None,
443 }
444 }
445
446 #[deprecated(since = "2.2.0", note = "Use `is_above` instead")]
450 pub fn is_above_price(&self, reference: Decimal) -> bool {
451 self.is_above(reference)
452 }
453
454 pub fn price_change_from(&self, reference: Decimal) -> Decimal {
456 self.price - reference
457 }
458
459 pub fn is_market_open_tick(&self, session_start_ms: u64, session_end_ms: u64) -> bool {
461 self.received_at_ms >= session_start_ms && self.received_at_ms < session_end_ms
462 }
463
464 #[deprecated(since = "2.2.0", note = "Use `is_at` instead")]
468 pub fn is_at_price(&self, target: Decimal) -> bool {
469 self.is_at(target)
470 }
471
472 #[deprecated(since = "2.2.0", note = "Use `is_below` instead")]
476 pub fn is_below_price(&self, reference: Decimal) -> bool {
477 self.is_below(reference)
478 }
479
480 pub fn is_round_number(&self, step: Decimal) -> bool {
485 if step.is_zero() {
486 return false;
487 }
488 (self.price % step).is_zero()
489 }
490
491 pub fn signed_quantity(&self) -> Decimal {
493 match self.side {
494 Some(TradeSide::Buy) => self.quantity,
495 Some(TradeSide::Sell) => -self.quantity,
496 None => Decimal::ZERO,
497 }
498 }
499
500 pub fn as_price_level(&self) -> (Decimal, Decimal) {
502 (self.price, self.quantity)
503 }
504
505 pub fn quantity_above(&self, threshold: Decimal) -> bool {
507 self.quantity > threshold
508 }
509
510 #[deprecated(since = "2.2.0", note = "Use `is_fresh(now_ms, threshold_ms)` instead")]
514 pub fn is_recent(&self, threshold_ms: u64, now_ms: u64) -> bool {
515 self.is_fresh(now_ms, threshold_ms)
516 }
517
518 #[deprecated(since = "2.2.0", note = "Use `is_buy` instead")]
522 pub fn is_buy_side(&self) -> bool {
523 self.is_buy()
524 }
525
526 #[deprecated(since = "2.2.0", note = "Use `is_sell` instead")]
530 pub fn is_sell_side(&self) -> bool {
531 self.is_sell()
532 }
533
534 pub fn is_zero_quantity(&self) -> bool {
536 self.quantity.is_zero()
537 }
538
539 pub fn is_within_spread(&self, bid: Decimal, ask: Decimal) -> bool {
541 self.price > bid && self.price < ask
542 }
543
544 pub fn is_away_from_price(&self, reference: Decimal, threshold: Decimal) -> bool {
546 (self.price - reference).abs() > threshold
547 }
548
549 #[deprecated(since = "2.2.0", note = "Use `is_large_trade` instead")]
554 pub fn is_large_tick(&self, threshold: Decimal) -> bool {
555 self.quantity > threshold
556 }
557
558 pub fn price_in_range(&self, low: Decimal, high: Decimal) -> bool {
560 self.price >= low && self.price <= high
561 }
562
563 pub fn rounded_price(&self, tick_size: Decimal) -> Decimal {
567 if tick_size.is_zero() {
568 return self.price;
569 }
570 (self.price / tick_size).floor() * tick_size
571 }
572
573 pub fn is_large_spread_from(&self, other: &NormalizedTick, threshold: Decimal) -> bool {
575 (self.price - other.price).abs() > threshold
576 }
577
578 pub fn volume_notional_f64(&self) -> f64 {
580 use rust_decimal::prelude::ToPrimitive;
581 self.volume_notional().to_f64().unwrap_or(0.0)
582 }
583
584 pub fn price_velocity(&self, prev: &NormalizedTick, dt_ms: u64) -> Option<Decimal> {
588 if dt_ms == 0 { return None; }
589 Some((self.price - prev.price) / Decimal::from(dt_ms))
590 }
591
592 pub fn is_reversal(&self, prev: &NormalizedTick, min_move: Decimal) -> bool {
598 let move_size = (self.price - prev.price).abs();
599 move_size >= min_move
600 }
601
602 pub fn spread_crossed(bid_tick: &NormalizedTick, ask_tick: &NormalizedTick) -> bool {
607 bid_tick.price >= ask_tick.price
608 }
609
610 pub fn dollar_value(&self) -> Decimal {
614 self.value()
615 }
616
617 pub fn contract_value(&self, multiplier: Decimal) -> Decimal {
619 self.value() * multiplier
620 }
621
622 pub fn tick_imbalance(ticks: &[NormalizedTick]) -> Option<f64> {
627 use rust_decimal::prelude::ToPrimitive;
628 let buy_qty: Decimal = ticks.iter()
629 .filter(|t| matches!(t.side, Some(TradeSide::Buy)))
630 .map(|t| t.quantity)
631 .sum();
632 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
633 if total_qty.is_zero() { return None; }
634 let sell_qty = total_qty - buy_qty;
635 ((buy_qty - sell_qty) / total_qty).to_f64()
636 }
637
638 pub fn quote_midpoint(bid: &NormalizedTick, ask: &NormalizedTick) -> Option<Decimal> {
643 if bid.price <= Decimal::ZERO || ask.price <= Decimal::ZERO {
644 return None;
645 }
646 if bid.price > ask.price {
647 return None;
648 }
649 Some((bid.price + ask.price) / Decimal::TWO)
650 }
651
652 pub fn buy_volume(ticks: &[NormalizedTick]) -> Decimal {
657 ticks
658 .iter()
659 .filter(|t| t.side == Some(TradeSide::Buy))
660 .map(|t| t.quantity)
661 .sum()
662 }
663
664 pub fn sell_volume(ticks: &[NormalizedTick]) -> Decimal {
669 ticks
670 .iter()
671 .filter(|t| t.side == Some(TradeSide::Sell))
672 .map(|t| t.quantity)
673 .sum()
674 }
675
676 pub fn price_range(ticks: &[NormalizedTick]) -> Option<Decimal> {
680 if ticks.is_empty() {
681 return None;
682 }
683 let max = ticks.iter().map(|t| t.price).max()?;
684 let min = ticks.iter().map(|t| t.price).min()?;
685 Some(max - min)
686 }
687
688 pub fn average_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
692 if ticks.is_empty() {
693 return None;
694 }
695 let sum: Decimal = ticks.iter().map(|t| t.price).sum();
696 Some(sum / Decimal::from(ticks.len() as u64))
697 }
698
699 pub fn vwap(ticks: &[NormalizedTick]) -> Option<Decimal> {
704 let volume: Decimal = ticks.iter().map(|t| t.quantity).sum();
705 if volume.is_zero() {
706 return None;
707 }
708 Some(Self::total_notional(ticks) / volume)
709 }
710
711 pub fn count_above_price(ticks: &[NormalizedTick], threshold: Decimal) -> usize {
713 ticks.iter().filter(|t| t.price > threshold).count()
714 }
715
716 pub fn count_below_price(ticks: &[NormalizedTick], threshold: Decimal) -> usize {
718 ticks.iter().filter(|t| t.price < threshold).count()
719 }
720
721 pub fn total_notional(ticks: &[NormalizedTick]) -> Decimal {
723 ticks.iter().map(|t| t.value()).sum()
724 }
725
726 pub fn buy_notional(ticks: &[NormalizedTick]) -> Decimal {
728 ticks.iter()
729 .filter(|t| t.side == Some(TradeSide::Buy))
730 .map(|t| t.value())
731 .sum()
732 }
733
734 pub fn sell_notional(ticks: &[NormalizedTick]) -> Decimal {
736 ticks.iter()
737 .filter(|t| t.side == Some(TradeSide::Sell))
738 .map(|t| t.value())
739 .sum()
740 }
741
742 pub fn median_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
747 if ticks.is_empty() {
748 return None;
749 }
750 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price).collect();
751 prices.sort();
752 let n = prices.len();
753 if n % 2 == 1 {
754 Some(prices[n / 2])
755 } else {
756 Some((prices[n / 2 - 1] + prices[n / 2]) / Decimal::from(2u64))
757 }
758 }
759
760 pub fn net_volume(ticks: &[NormalizedTick]) -> Decimal {
764 Self::buy_volume(ticks) - Self::sell_volume(ticks)
765 }
766
767 pub fn average_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
771 if ticks.is_empty() {
772 return None;
773 }
774 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
775 Some(total / Decimal::from(ticks.len() as u64))
776 }
777
778 pub fn max_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
782 ticks.iter().map(|t| t.quantity).reduce(Decimal::max)
783 }
784
785 pub fn min_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
789 ticks.iter().map(|t| t.quantity).reduce(Decimal::min)
790 }
791
792 pub fn buy_count(ticks: &[NormalizedTick]) -> usize {
794 ticks.iter().filter(|t| t.is_buy()).count()
795 }
796
797 pub fn sell_count(ticks: &[NormalizedTick]) -> usize {
799 ticks.iter().filter(|t| t.is_sell()).count()
800 }
801
802 pub fn price_momentum(ticks: &[NormalizedTick]) -> Option<f64> {
807 use rust_decimal::prelude::ToPrimitive;
808 let n = ticks.len();
809 if n < 2 {
810 return None;
811 }
812 let first = ticks[0].price;
813 let last = ticks[n - 1].price;
814 if first.is_zero() {
815 return None;
816 }
817 ((last - first) / first).to_f64()
818 }
819
820 pub fn min_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
824 ticks.iter().map(|t| t.price).reduce(Decimal::min)
825 }
826
827 pub fn max_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
831 ticks.iter().map(|t| t.price).reduce(Decimal::max)
832 }
833
834 pub fn price_std_dev(ticks: &[NormalizedTick]) -> Option<f64> {
838 use rust_decimal::prelude::ToPrimitive;
839 let n = ticks.len();
840 if n < 2 { return None; }
841 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
842 if vals.len() < 2 { return None; }
843 let mean = vals.iter().sum::<f64>() / vals.len() as f64;
844 let variance = vals.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / (vals.len() - 1) as f64;
845 Some(variance.sqrt())
846 }
847
848 pub fn buy_sell_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
852 use rust_decimal::prelude::ToPrimitive;
853 let sell = Self::sell_volume(ticks);
854 if sell.is_zero() {
855 return None;
856 }
857 (Self::buy_volume(ticks) / sell).to_f64()
858 }
859
860 pub fn largest_trade(ticks: &[NormalizedTick]) -> Option<&NormalizedTick> {
864 ticks.iter().max_by(|a, b| a.quantity.cmp(&b.quantity))
865 }
866
867 pub fn large_trade_count(ticks: &[NormalizedTick], threshold: Decimal) -> usize {
869 ticks.iter().filter(|t| t.quantity > threshold).count()
870 }
871
872 pub fn price_iqr(ticks: &[NormalizedTick]) -> Option<Decimal> {
876 let n = ticks.len();
877 if n < 4 {
878 return None;
879 }
880 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price).collect();
881 prices.sort();
882 let q1_idx = n / 4;
883 let q3_idx = 3 * n / 4;
884 Some(prices[q3_idx] - prices[q1_idx])
885 }
886
887 pub fn fraction_buy(ticks: &[NormalizedTick]) -> Option<f64> {
891 if ticks.is_empty() {
892 return None;
893 }
894 Some(Self::buy_count(ticks) as f64 / ticks.len() as f64)
895 }
896
897 pub fn std_quantity(ticks: &[NormalizedTick]) -> Option<f64> {
901 use rust_decimal::prelude::ToPrimitive;
902 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
903 Self::sample_std_dev_f64(&vals)
904 }
905
906 pub fn buy_pressure(ticks: &[NormalizedTick]) -> Option<f64> {
910 use rust_decimal::prelude::ToPrimitive;
911 let buy = Self::buy_volume(ticks);
912 let sell = Self::sell_volume(ticks);
913 let total = buy + sell;
914 if total.is_zero() {
915 return None;
916 }
917 (buy / total).to_f64()
918 }
919
920 pub fn average_notional(ticks: &[NormalizedTick]) -> Option<Decimal> {
924 if ticks.is_empty() {
925 return None;
926 }
927 Some(Self::total_notional(ticks) / Decimal::from(ticks.len() as u64))
928 }
929
930 pub fn count_neutral(ticks: &[NormalizedTick]) -> usize {
932 ticks.iter().filter(|t| t.is_neutral()).count()
933 }
934
935 pub fn recent(ticks: &[NormalizedTick], n: usize) -> &[NormalizedTick] {
939 let len = ticks.len();
940 if n >= len { ticks } else { &ticks[len - n..] }
941 }
942
943 pub fn price_linear_slope(ticks: &[NormalizedTick]) -> Option<f64> {
948 use rust_decimal::prelude::ToPrimitive;
949 let n = ticks.len();
950 if n < 2 {
951 return None;
952 }
953 let n_f = n as f64;
954 let xs: Vec<f64> = (0..n).map(|i| i as f64).collect();
955 let ys: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
956 if ys.len() < 2 {
957 return None;
958 }
959 let x_mean = xs.iter().sum::<f64>() / n_f;
960 let y_mean = ys.iter().sum::<f64>() / ys.len() as f64;
961 let numerator: f64 = xs.iter().zip(ys.iter()).map(|(&x, &y)| (x - x_mean) * (y - y_mean)).sum();
962 let denominator: f64 = xs.iter().map(|&x| (x - x_mean).powi(2)).sum();
963 if denominator == 0.0 {
964 return None;
965 }
966 Some(numerator / denominator)
967 }
968
969 pub fn notional_std_dev(ticks: &[NormalizedTick]) -> Option<f64> {
973 use rust_decimal::prelude::ToPrimitive;
974 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.value().to_f64()).collect();
975 Self::sample_std_dev_f64(&vals)
976 }
977
978 pub fn monotone_up(ticks: &[NormalizedTick]) -> bool {
982 ticks.windows(2).all(|w| w[1].price >= w[0].price)
983 }
984
985 pub fn monotone_down(ticks: &[NormalizedTick]) -> bool {
989 ticks.windows(2).all(|w| w[1].price <= w[0].price)
990 }
991
992 pub fn volume_at_price(ticks: &[NormalizedTick], price: Decimal) -> Decimal {
994 ticks.iter().filter(|t| t.price == price).map(|t| t.quantity).sum()
995 }
996
997 pub fn last_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
1001 ticks.last().map(|t| t.price)
1002 }
1003
1004 pub fn longest_buy_streak(ticks: &[NormalizedTick]) -> usize {
1006 let mut max = 0usize;
1007 let mut current = 0usize;
1008 for t in ticks {
1009 if t.is_buy() {
1010 current += 1;
1011 max = max.max(current);
1012 } else {
1013 current = 0;
1014 }
1015 }
1016 max
1017 }
1018
1019 pub fn longest_sell_streak(ticks: &[NormalizedTick]) -> usize {
1021 let mut max = 0usize;
1022 let mut current = 0usize;
1023 for t in ticks {
1024 if t.is_sell() {
1025 current += 1;
1026 max = max.max(current);
1027 } else {
1028 current = 0;
1029 }
1030 }
1031 max
1032 }
1033
1034 pub fn price_at_max_volume(ticks: &[NormalizedTick]) -> Option<Decimal> {
1038 use std::collections::HashMap;
1039 if ticks.is_empty() {
1040 return None;
1041 }
1042 let mut volume_by_price: HashMap<String, (Decimal, Decimal)> = HashMap::new();
1043 for t in ticks {
1044 let key = t.price.to_string();
1045 let entry = volume_by_price.entry(key).or_insert((t.price, Decimal::ZERO));
1046 entry.1 += t.quantity;
1047 }
1048 volume_by_price
1049 .values()
1050 .max_by(|a, b| a.1.cmp(&b.1))
1051 .map(|(price, _)| *price)
1052 }
1053
1054 pub fn recent_volume(ticks: &[NormalizedTick], n: usize) -> Decimal {
1058 Self::recent(ticks, n).iter().map(|t| t.quantity).sum()
1059 }
1060
1061 fn sample_std_dev_f64(vals: &[f64]) -> Option<f64> {
1065 let n = vals.len();
1066 if n < 2 {
1067 return None;
1068 }
1069 let mean = vals.iter().sum::<f64>() / n as f64;
1070 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
1071 Some(variance.sqrt())
1072 }
1073
1074 pub fn first_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
1078 ticks.first().map(|t| t.price)
1079 }
1080
1081 pub fn price_return_pct(ticks: &[NormalizedTick]) -> Option<f64> {
1085 use rust_decimal::prelude::ToPrimitive;
1086 let n = ticks.len();
1087 if n < 2 { return None; }
1088 let first = ticks[0].price;
1089 if first.is_zero() { return None; }
1090 ((ticks[n - 1].price - first) / first).to_f64()
1091 }
1092
1093 pub fn volume_above_price(ticks: &[NormalizedTick], price: Decimal) -> Decimal {
1095 ticks.iter().filter(|t| t.price > price).map(|t| t.quantity).sum()
1096 }
1097
1098 pub fn volume_below_price(ticks: &[NormalizedTick], price: Decimal) -> Decimal {
1100 ticks.iter().filter(|t| t.price < price).map(|t| t.quantity).sum()
1101 }
1102
1103 pub fn quantity_weighted_avg_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
1107 if ticks.is_empty() {
1108 return None;
1109 }
1110 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
1111 if total_qty.is_zero() {
1112 return None;
1113 }
1114 let weighted: Decimal = ticks.iter().map(|t| t.price * t.quantity).sum();
1115 Some(weighted / total_qty)
1116 }
1117
1118 pub fn tick_count_above_price(ticks: &[NormalizedTick], price: Decimal) -> usize {
1120 ticks.iter().filter(|t| t.price > price).count()
1121 }
1122
1123 pub fn tick_count_below_price(ticks: &[NormalizedTick], price: Decimal) -> usize {
1125 ticks.iter().filter(|t| t.price < price).count()
1126 }
1127
1128 pub fn price_at_percentile(ticks: &[NormalizedTick], percentile: f64) -> Option<Decimal> {
1132 if ticks.is_empty() || !(0.0..=1.0).contains(&percentile) {
1133 return None;
1134 }
1135 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price).collect();
1136 prices.sort();
1137 let idx = ((prices.len() - 1) as f64 * percentile).round() as usize;
1138 Some(prices[idx])
1139 }
1140
1141 pub fn unique_price_count(ticks: &[NormalizedTick]) -> usize {
1143 use std::collections::HashSet;
1144 ticks.iter().map(|t| t.price.to_string()).collect::<HashSet<_>>().len()
1145 }
1146
1147 pub fn avg_inter_tick_spread(ticks: &[NormalizedTick]) -> Option<f64> {
1151 use rust_decimal::prelude::ToPrimitive;
1152 if ticks.len() < 2 {
1153 return None;
1154 }
1155 let sum: Decimal = ticks.windows(2).map(|w| (w[1].price - w[0].price).abs()).sum();
1156 (sum / Decimal::from((ticks.len() - 1) as u32)).to_f64()
1157 }
1158
1159 pub fn largest_sell(ticks: &[NormalizedTick]) -> Option<Decimal> {
1163 ticks.iter().filter(|t| t.is_sell()).map(|t| t.quantity).reduce(Decimal::max)
1164 }
1165
1166 pub fn largest_buy(ticks: &[NormalizedTick]) -> Option<Decimal> {
1170 ticks.iter().filter(|t| t.is_buy()).map(|t| t.quantity).reduce(Decimal::max)
1171 }
1172
1173 pub fn trade_count(ticks: &[NormalizedTick]) -> usize {
1175 ticks.len()
1176 }
1177
1178 pub fn price_acceleration(ticks: &[NormalizedTick]) -> Option<f64> {
1183 use rust_decimal::prelude::ToPrimitive;
1184 let n = ticks.len();
1185 if n < 3 {
1186 return None;
1187 }
1188 let v1 = (ticks[n - 2].price - ticks[n - 3].price).to_f64()?;
1189 let v2 = (ticks[n - 1].price - ticks[n - 2].price).to_f64()?;
1190 Some(v2 - v1)
1191 }
1192
1193 pub fn buy_sell_diff(ticks: &[NormalizedTick]) -> Decimal {
1197 Self::buy_volume(ticks) - Self::sell_volume(ticks)
1198 }
1199
1200 pub fn is_aggressive_buy(tick: &NormalizedTick, avg_buy_qty: Decimal) -> bool {
1202 tick.is_buy() && tick.quantity > avg_buy_qty
1203 }
1204
1205 pub fn is_aggressive_sell(tick: &NormalizedTick, avg_sell_qty: Decimal) -> bool {
1207 tick.is_sell() && tick.quantity > avg_sell_qty
1208 }
1209
1210 pub fn notional_volume(ticks: &[NormalizedTick]) -> Decimal {
1212 ticks.iter().map(|t| t.price * t.quantity).sum()
1213 }
1214
1215 pub fn weighted_side_score(ticks: &[NormalizedTick]) -> Option<f64> {
1219 use rust_decimal::prelude::ToPrimitive;
1220 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
1221 if total.is_zero() {
1222 return None;
1223 }
1224 let diff = Self::buy_volume(ticks) - Self::sell_volume(ticks);
1225 (diff / total).to_f64()
1226 }
1227
1228 pub fn time_span_ms(ticks: &[NormalizedTick]) -> Option<u64> {
1232 if ticks.len() < 2 {
1233 return None;
1234 }
1235 Some(ticks.last()?.received_at_ms.saturating_sub(ticks.first()?.received_at_ms))
1236 }
1237
1238 pub fn price_above_vwap_count(ticks: &[NormalizedTick]) -> Option<usize> {
1242 let vwap = Self::vwap(ticks)?;
1243 Some(ticks.iter().filter(|t| t.price > vwap).count())
1244 }
1245
1246 pub fn avg_trade_size(ticks: &[NormalizedTick]) -> Option<Decimal> {
1250 if ticks.is_empty() {
1251 return None;
1252 }
1253 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
1254 Some(total / Decimal::from(ticks.len() as u32))
1255 }
1256
1257 pub fn volume_concentration(ticks: &[NormalizedTick]) -> Option<f64> {
1263 use rust_decimal::prelude::ToPrimitive;
1264 if ticks.is_empty() {
1265 return None;
1266 }
1267 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
1268 if total.is_zero() {
1269 return None;
1270 }
1271 let mut qtys: Vec<Decimal> = ticks.iter().map(|t| t.quantity).collect();
1272 qtys.sort_by(|a, b| b.cmp(a));
1273 let top_n = ((ticks.len() + 3) / 4).max(1);
1274 let top_vol: Decimal = qtys.iter().take(top_n).copied().sum();
1275 (top_vol / total).to_f64()
1276 }
1277
1278 pub fn trade_imbalance_score(ticks: &[NormalizedTick]) -> Option<f64> {
1282 if ticks.is_empty() {
1283 return None;
1284 }
1285 let n = ticks.len() as f64;
1286 let buys = Self::buy_count(ticks) as f64;
1287 let sells = Self::sell_count(ticks) as f64;
1288 Some((buys - sells) / n)
1289 }
1290
1291 pub fn buy_avg_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
1295 let buys: Vec<_> = ticks.iter().filter(|t| t.is_buy()).collect();
1296 if buys.is_empty() {
1297 return None;
1298 }
1299 let sum: Decimal = buys.iter().map(|t| t.price).sum();
1300 Some(sum / Decimal::from(buys.len() as u32))
1301 }
1302
1303 pub fn sell_avg_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
1307 let sells: Vec<_> = ticks.iter().filter(|t| t.is_sell()).collect();
1308 if sells.is_empty() {
1309 return None;
1310 }
1311 let sum: Decimal = sells.iter().map(|t| t.price).sum();
1312 Some(sum / Decimal::from(sells.len() as u32))
1313 }
1314
1315 pub fn price_skewness(ticks: &[NormalizedTick]) -> Option<f64> {
1320 use rust_decimal::prelude::ToPrimitive;
1321 let n = ticks.len();
1322 if n < 3 {
1323 return None;
1324 }
1325 let prices: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
1326 if prices.len() != n {
1327 return None;
1328 }
1329 let nf = n as f64;
1330 let mean = prices.iter().sum::<f64>() / nf;
1331 let variance = prices.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / nf;
1332 if variance == 0.0 {
1333 return None;
1334 }
1335 let std_dev = variance.sqrt();
1336 let skew = prices.iter().map(|p| ((p - mean) / std_dev).powi(3)).sum::<f64>() / nf;
1337 Some(skew)
1338 }
1339
1340 pub fn quantity_skewness(ticks: &[NormalizedTick]) -> Option<f64> {
1345 use rust_decimal::prelude::ToPrimitive;
1346 let n = ticks.len();
1347 if n < 3 {
1348 return None;
1349 }
1350 let qtys: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
1351 if qtys.len() != n {
1352 return None;
1353 }
1354 let nf = n as f64;
1355 let mean = qtys.iter().sum::<f64>() / nf;
1356 let variance = qtys.iter().map(|q| (q - mean).powi(2)).sum::<f64>() / nf;
1357 if variance == 0.0 {
1358 return None;
1359 }
1360 let std_dev = variance.sqrt();
1361 let skew = qtys.iter().map(|q| ((q - mean) / std_dev).powi(3)).sum::<f64>() / nf;
1362 Some(skew)
1363 }
1364
1365 pub fn price_entropy(ticks: &[NormalizedTick]) -> Option<f64> {
1371 if ticks.is_empty() {
1372 return None;
1373 }
1374 let n = ticks.len() as f64;
1375 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1376 for t in ticks {
1377 *counts.entry(t.price.to_string()).or_insert(0) += 1;
1378 }
1379 let entropy = counts.values().map(|&c| {
1380 let p = c as f64 / n;
1381 -p * p.ln()
1382 }).sum();
1383 Some(entropy)
1384 }
1385
1386 pub fn price_kurtosis(ticks: &[NormalizedTick]) -> Option<f64> {
1391 use rust_decimal::prelude::ToPrimitive;
1392 let n = ticks.len();
1393 if n < 4 {
1394 return None;
1395 }
1396 let prices: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
1397 if prices.len() != n {
1398 return None;
1399 }
1400 let nf = n as f64;
1401 let mean = prices.iter().sum::<f64>() / nf;
1402 let variance = prices.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / nf;
1403 if variance == 0.0 {
1404 return None;
1405 }
1406 let std_dev = variance.sqrt();
1407 let kurt = prices.iter().map(|p| ((p - mean) / std_dev).powi(4)).sum::<f64>() / nf - 3.0;
1408 Some(kurt)
1409 }
1410
1411 pub fn high_volume_tick_count(ticks: &[NormalizedTick], threshold: Decimal) -> usize {
1413 ticks.iter().filter(|t| t.quantity > threshold).count()
1414 }
1415
1416 pub fn vwap_spread(ticks: &[NormalizedTick]) -> Option<Decimal> {
1423 let buy = Self::buy_avg_price(ticks)?;
1424 let sell = Self::sell_avg_price(ticks)?;
1425 Some(buy - sell)
1426 }
1427
1428 pub fn avg_buy_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
1432 let buys: Vec<_> = ticks.iter().filter(|t| t.is_buy()).collect();
1433 if buys.is_empty() {
1434 return None;
1435 }
1436 let total: Decimal = buys.iter().map(|t| t.quantity).sum();
1437 Some(total / Decimal::from(buys.len() as u32))
1438 }
1439
1440 pub fn avg_sell_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
1444 let sells: Vec<_> = ticks.iter().filter(|t| t.is_sell()).collect();
1445 if sells.is_empty() {
1446 return None;
1447 }
1448 let total: Decimal = sells.iter().map(|t| t.quantity).sum();
1449 Some(total / Decimal::from(sells.len() as u32))
1450 }
1451
1452 pub fn price_mean_reversion_score(ticks: &[NormalizedTick]) -> Option<f64> {
1457 let vwap = Self::vwap(ticks)?;
1458 let below = ticks.iter().filter(|t| t.price < vwap).count();
1459 Some(below as f64 / ticks.len() as f64)
1460 }
1461
1462 pub fn largest_price_move(ticks: &[NormalizedTick]) -> Option<Decimal> {
1466 if ticks.len() < 2 {
1467 return None;
1468 }
1469 ticks.windows(2).map(|w| (w[1].price - w[0].price).abs()).reduce(Decimal::max)
1470 }
1471
1472 pub fn tick_rate(ticks: &[NormalizedTick]) -> Option<f64> {
1476 let span = Self::time_span_ms(ticks)? as f64;
1477 if span == 0.0 {
1478 return None;
1479 }
1480 Some(ticks.len() as f64 / span)
1481 }
1482
1483 pub fn buy_notional_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
1487 use rust_decimal::prelude::ToPrimitive;
1488 let total = Self::total_notional(ticks);
1489 if total.is_zero() {
1490 return None;
1491 }
1492 let buy = Self::buy_notional(ticks);
1493 (buy / total).to_f64()
1494 }
1495
1496 pub fn price_range_pct(ticks: &[NormalizedTick]) -> Option<f64> {
1501 use rust_decimal::prelude::ToPrimitive;
1502 let min = Self::min_price(ticks)?;
1503 let max = Self::max_price(ticks)?;
1504 if min.is_zero() {
1505 return None;
1506 }
1507 ((max - min) / min * Decimal::ONE_HUNDRED).to_f64()
1508 }
1509
1510 pub fn buy_side_dominance(ticks: &[NormalizedTick]) -> Option<f64> {
1514 use rust_decimal::prelude::ToPrimitive;
1515 let buy = Self::buy_volume(ticks);
1516 let sell = Self::sell_volume(ticks);
1517 let total = buy + sell;
1518 if total.is_zero() {
1519 return None;
1520 }
1521 (buy / total).to_f64()
1522 }
1523
1524 pub fn volume_weighted_price_std(ticks: &[NormalizedTick]) -> Option<f64> {
1529 use rust_decimal::prelude::ToPrimitive;
1530 let vwap = Self::vwap(ticks)?;
1531 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
1532 if total_qty.is_zero() {
1533 return None;
1534 }
1535 let variance: Decimal = ticks.iter()
1536 .map(|t| {
1537 let diff = t.price - vwap;
1538 diff * diff * t.quantity
1539 })
1540 .sum::<Decimal>() / total_qty;
1541 variance.to_f64().map(f64::sqrt)
1542 }
1543
1544 pub fn last_n_vwap(ticks: &[NormalizedTick], n: usize) -> Option<Decimal> {
1549 if n == 0 || ticks.is_empty() {
1550 return None;
1551 }
1552 let window = &ticks[ticks.len().saturating_sub(n)..];
1553 Self::vwap(window)
1554 }
1555
1556 pub fn price_autocorrelation(ticks: &[NormalizedTick]) -> Option<f64> {
1561 use rust_decimal::prelude::ToPrimitive;
1562 let n = ticks.len();
1563 if n < 3 {
1564 return None;
1565 }
1566 let prices: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
1567 if prices.len() != n {
1568 return None;
1569 }
1570 let nf = (n - 1) as f64;
1571 let mean = prices.iter().sum::<f64>() / n as f64;
1572 let var = prices.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / n as f64;
1573 if var == 0.0 {
1574 return None;
1575 }
1576 let cov: f64 = prices.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>() / nf;
1577 Some(cov / var)
1578 }
1579
1580 pub fn net_trade_direction(ticks: &[NormalizedTick]) -> i64 {
1582 Self::buy_count(ticks) as i64 - Self::sell_count(ticks) as i64
1583 }
1584
1585 pub fn sell_side_notional_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
1589 use rust_decimal::prelude::ToPrimitive;
1590 let total = Self::total_notional(ticks);
1591 if total.is_zero() {
1592 return None;
1593 }
1594 let sell = Self::sell_notional(ticks);
1595 (sell / total).to_f64()
1596 }
1597
1598 pub fn price_oscillation_count(ticks: &[NormalizedTick]) -> usize {
1602 if ticks.len() < 3 {
1603 return 0;
1604 }
1605 ticks.windows(3).filter(|w| {
1606 let d1 = w[1].price.cmp(&w[0].price);
1607 let d2 = w[2].price.cmp(&w[1].price);
1608 use std::cmp::Ordering::*;
1609 matches!((d1, d2), (Greater, Less) | (Less, Greater))
1610 }).count()
1611 }
1612
1613 pub fn realized_spread(ticks: &[NormalizedTick]) -> Option<Decimal> {
1619 let buy_avg = Self::buy_avg_price(ticks)?;
1620 let sell_avg = Self::sell_avg_price(ticks)?;
1621 Some(buy_avg - sell_avg)
1622 }
1623
1624 pub fn adverse_selection_score(ticks: &[NormalizedTick]) -> Option<f64> {
1630 if ticks.len() < 3 {
1631 return None;
1632 }
1633 let median_qty = Self::median_price(
1634 &ticks.iter().map(|t| {
1635 let mut cloned = t.clone();
1636 cloned.price = t.quantity;
1637 cloned
1638 }).collect::<Vec<_>>()
1639 )?;
1640 if median_qty.is_zero() {
1641 return None;
1642 }
1643 let large_trades: Vec<_> = ticks.windows(2)
1644 .filter(|w| w[0].quantity > median_qty)
1645 .collect();
1646 if large_trades.is_empty() {
1647 return None;
1648 }
1649 let adverse = large_trades.iter().filter(|w| {
1651 let price_moved_up = w[1].price > w[0].price;
1652 match w[0].side {
1653 Some(TradeSide::Buy) => !price_moved_up, Some(TradeSide::Sell) => price_moved_up, None => false,
1656 }
1657 }).count();
1658 Some(adverse as f64 / large_trades.len() as f64)
1659 }
1660
1661 pub fn price_impact_per_unit(ticks: &[NormalizedTick]) -> Option<f64> {
1665 use rust_decimal::prelude::ToPrimitive;
1666 let ret = (Self::price_return_pct(ticks)?.abs()) as f64;
1667 let vol = Self::buy_volume(ticks) + Self::sell_volume(ticks);
1668 if vol.is_zero() {
1669 return None;
1670 }
1671 vol.to_f64().map(|v| ret / v)
1672 }
1673
1674 pub fn volume_weighted_return(ticks: &[NormalizedTick]) -> Option<f64> {
1680 use rust_decimal::prelude::ToPrimitive;
1681 if ticks.len() < 2 {
1682 return None;
1683 }
1684 let total_qty: Decimal = ticks[1..].iter().map(|t| t.quantity).sum();
1685 if total_qty.is_zero() {
1686 return None;
1687 }
1688 let weighted: f64 = ticks.windows(2).filter_map(|w| {
1689 if w[0].price.is_zero() { return None; }
1690 let ret = ((w[1].price - w[0].price) / w[0].price).to_f64()?;
1691 let qty = w[1].quantity.to_f64()?;
1692 Some(ret * qty)
1693 }).sum::<f64>();
1694 total_qty.to_f64().map(|tq| weighted / tq)
1695 }
1696
1697 pub fn quantity_concentration(ticks: &[NormalizedTick]) -> Option<f64> {
1702 use rust_decimal::prelude::ToPrimitive;
1703 let n = ticks.len();
1704 if n == 0 {
1705 return None;
1706 }
1707 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
1708 if total.is_zero() {
1709 return None;
1710 }
1711 let mean = total / Decimal::from(n as u32);
1712 let mut sum = Decimal::ZERO;
1713 for i in 0..n {
1714 for j in 0..n {
1715 sum += (ticks[i].quantity - ticks[j].quantity).abs();
1716 }
1717 }
1718 let denom = mean * Decimal::from((2 * n * n) as u32);
1719 if denom.is_zero() {
1720 return None;
1721 }
1722 (sum / denom).to_f64()
1723 }
1724
1725 pub fn price_level_volume(ticks: &[NormalizedTick], price: Decimal) -> Decimal {
1727 ticks.iter().filter(|t| t.price == price).map(|t| t.quantity).sum()
1728 }
1729
1730 pub fn mid_price_drift(ticks: &[NormalizedTick]) -> Option<f64> {
1735 use rust_decimal::prelude::ToPrimitive;
1736 let first = Self::first_price(ticks)?;
1737 let last = Self::last_price(ticks)?;
1738 let span = Self::time_span_ms(ticks)? as f64;
1739 if span == 0.0 {
1740 return None;
1741 }
1742 (last - first).to_f64().map(|d| d / span)
1743 }
1744
1745 pub fn tick_direction_bias(ticks: &[NormalizedTick]) -> Option<f64> {
1750 if ticks.len() < 3 {
1751 return None;
1752 }
1753 let total = ticks.len() - 2;
1754 let same = ticks.windows(3).filter(|w| {
1755 let d1 = w[1].price.cmp(&w[0].price);
1756 let d2 = w[2].price.cmp(&w[1].price);
1757 d1 == d2 && d1 != std::cmp::Ordering::Equal
1758 }).count();
1759 Some(same as f64 / total as f64)
1760 }
1761
1762 pub fn median_quantity(ticks: &[NormalizedTick]) -> Option<Decimal> {
1766 if ticks.is_empty() {
1767 return None;
1768 }
1769 let mut qtys: Vec<Decimal> = ticks.iter().map(|t| t.quantity).collect();
1770 qtys.sort();
1771 let n = qtys.len();
1772 if n % 2 == 1 {
1773 Some(qtys[n / 2])
1774 } else {
1775 Some((qtys[n / 2 - 1] + qtys[n / 2]) / Decimal::TWO)
1776 }
1777 }
1778
1779 pub fn volume_above_vwap(ticks: &[NormalizedTick]) -> Option<Decimal> {
1783 let vwap = Self::vwap(ticks)?;
1784 Some(ticks.iter().filter(|t| t.price > vwap).map(|t| t.quantity).sum())
1785 }
1786
1787 pub fn inter_arrival_variance(ticks: &[NormalizedTick]) -> Option<f64> {
1791 if ticks.len() < 3 {
1792 return None;
1793 }
1794 let intervals: Vec<f64> = ticks.windows(2)
1795 .filter_map(|w| {
1796 let dt = w[1].received_at_ms.checked_sub(w[0].received_at_ms)?;
1797 Some(dt as f64)
1798 })
1799 .collect();
1800 if intervals.len() < 2 {
1801 return None;
1802 }
1803 let n = intervals.len() as f64;
1804 let mean = intervals.iter().sum::<f64>() / n;
1805 let variance = intervals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
1806 Some(variance)
1807 }
1808
1809 pub fn spread_efficiency(ticks: &[NormalizedTick]) -> Option<f64> {
1815 use rust_decimal::prelude::ToPrimitive;
1816 if ticks.len() < 2 {
1817 return None;
1818 }
1819 let path: Decimal = ticks.windows(2)
1820 .map(|w| (w[1].price - w[0].price).abs())
1821 .sum();
1822 if path.is_zero() {
1823 return None;
1824 }
1825 let net = (ticks.last()?.price - ticks.first()?.price).abs();
1826 (net / path).to_f64()
1827 }
1828
1829 pub fn buy_sell_size_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
1834 use rust_decimal::prelude::ToPrimitive;
1835 let avg_buy = Self::avg_buy_quantity(ticks)?;
1836 let avg_sell = Self::avg_sell_quantity(ticks)?;
1837 if avg_sell.is_zero() {
1838 return None;
1839 }
1840 (avg_buy / avg_sell).to_f64()
1841 }
1842
1843 pub fn trade_size_dispersion(ticks: &[NormalizedTick]) -> Option<f64> {
1847 use rust_decimal::prelude::ToPrimitive;
1848 if ticks.len() < 2 {
1849 return None;
1850 }
1851 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
1852 if vals.len() < 2 {
1853 return None;
1854 }
1855 let n = vals.len() as f64;
1856 let mean = vals.iter().sum::<f64>() / n;
1857 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
1858 Some(variance.sqrt())
1859 }
1860
1861 pub fn aggressor_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
1870 if ticks.is_empty() {
1871 return None;
1872 }
1873 let known = ticks.iter().filter(|t| t.side.is_some()).count();
1874 Some(known as f64 / ticks.len() as f64)
1875 }
1876
1877 pub fn volume_imbalance_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
1883 use rust_decimal::prelude::ToPrimitive;
1884 let buy = Self::buy_volume(ticks);
1885 let sell = Self::sell_volume(ticks);
1886 let total = buy + sell;
1887 if total.is_zero() {
1888 return None;
1889 }
1890 ((buy - sell) / total).to_f64()
1891 }
1892
1893 pub fn price_quantity_covariance(ticks: &[NormalizedTick]) -> Option<f64> {
1901 use rust_decimal::prelude::ToPrimitive;
1902 if ticks.len() < 2 {
1903 return None;
1904 }
1905 let prices: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
1906 let qtys: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
1907 if prices.len() != ticks.len() || qtys.len() != ticks.len() {
1908 return None;
1909 }
1910 let n = prices.len() as f64;
1911 let mean_p = prices.iter().sum::<f64>() / n;
1912 let mean_q = qtys.iter().sum::<f64>() / n;
1913 let cov = prices
1914 .iter()
1915 .zip(qtys.iter())
1916 .map(|(p, q)| (p - mean_p) * (q - mean_q))
1917 .sum::<f64>()
1918 / (n - 1.0);
1919 Some(cov)
1920 }
1921
1922 pub fn large_trade_fraction(ticks: &[NormalizedTick], threshold: Decimal) -> Option<f64> {
1927 if ticks.is_empty() {
1928 return None;
1929 }
1930 let count = Self::large_trade_count(ticks, threshold);
1931 Some(count as f64 / ticks.len() as f64)
1932 }
1933
1934 pub fn price_level_density(ticks: &[NormalizedTick]) -> Option<f64> {
1942 use rust_decimal::prelude::ToPrimitive;
1943 let range = Self::price_range(ticks)?;
1944 if range.is_zero() {
1945 return None;
1946 }
1947 let unique = Self::unique_price_count(ticks) as f64;
1948 (Decimal::from(Self::unique_price_count(ticks) as i64) / range).to_f64()
1949 .or_else(|| Some(unique / range.to_f64()?))
1950 }
1951
1952 pub fn notional_buy_sell_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
1957 use rust_decimal::prelude::ToPrimitive;
1958 let buy_n = Self::buy_notional(ticks);
1959 let sell_n = Self::sell_notional(ticks);
1960 if sell_n.is_zero() {
1961 return None;
1962 }
1963 (buy_n / sell_n).to_f64()
1964 }
1965
1966 pub fn log_return_mean(ticks: &[NormalizedTick]) -> Option<f64> {
1971 if ticks.len() < 2 {
1972 return None;
1973 }
1974 let returns: Vec<f64> = ticks
1975 .windows(2)
1976 .filter_map(|w| {
1977 use rust_decimal::prelude::ToPrimitive;
1978 let prev = w[0].price.to_f64()?;
1979 let curr = w[1].price.to_f64()?;
1980 if prev <= 0.0 || curr <= 0.0 {
1981 return None;
1982 }
1983 Some((curr / prev).ln())
1984 })
1985 .collect();
1986 if returns.is_empty() {
1987 return None;
1988 }
1989 Some(returns.iter().sum::<f64>() / returns.len() as f64)
1990 }
1991
1992 pub fn log_return_std(ticks: &[NormalizedTick]) -> Option<f64> {
1997 if ticks.len() < 3 {
1998 return None;
1999 }
2000 let returns: Vec<f64> = ticks
2001 .windows(2)
2002 .filter_map(|w| {
2003 use rust_decimal::prelude::ToPrimitive;
2004 let prev = w[0].price.to_f64()?;
2005 let curr = w[1].price.to_f64()?;
2006 if prev <= 0.0 || curr <= 0.0 {
2007 return None;
2008 }
2009 Some((curr / prev).ln())
2010 })
2011 .collect();
2012 if returns.len() < 2 {
2013 return None;
2014 }
2015 let n = returns.len() as f64;
2016 let mean = returns.iter().sum::<f64>() / n;
2017 let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
2018 Some(variance.sqrt())
2019 }
2020
2021 pub fn price_overshoot_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2028 use rust_decimal::prelude::ToPrimitive;
2029 let max_p = Self::max_price(ticks)?;
2030 let last_p = Self::last_price(ticks)?;
2031 if last_p.is_zero() {
2032 return None;
2033 }
2034 (max_p / last_p).to_f64()
2035 }
2036
2037 pub fn price_undershoot_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2044 use rust_decimal::prelude::ToPrimitive;
2045 let first_p = Self::first_price(ticks)?;
2046 let min_p = Self::min_price(ticks)?;
2047 if min_p.is_zero() {
2048 return None;
2049 }
2050 (first_p / min_p).to_f64()
2051 }
2052
2053 pub fn net_notional(ticks: &[NormalizedTick]) -> Decimal {
2061 Self::buy_notional(ticks) - Self::sell_notional(ticks)
2062 }
2063
2064 pub fn price_reversal_count(ticks: &[NormalizedTick]) -> usize {
2069 if ticks.len() < 3 {
2070 return 0;
2071 }
2072 let mut count = 0usize;
2073 for w in ticks.windows(3) {
2074 let d1 = w[1].price - w[0].price;
2075 let d2 = w[2].price - w[1].price;
2076 if (d1 > Decimal::ZERO && d2 < Decimal::ZERO)
2077 || (d1 < Decimal::ZERO && d2 > Decimal::ZERO)
2078 {
2079 count += 1;
2080 }
2081 }
2082 count
2083 }
2084
2085 pub fn quantity_kurtosis(ticks: &[NormalizedTick]) -> Option<f64> {
2091 use rust_decimal::prelude::ToPrimitive;
2092 if ticks.len() < 4 {
2093 return None;
2094 }
2095 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
2096 if vals.len() < 4 {
2097 return None;
2098 }
2099 let n_f = vals.len() as f64;
2100 let mean = vals.iter().sum::<f64>() / n_f;
2101 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
2102 let std_dev = variance.sqrt();
2103 if std_dev == 0.0 {
2104 return None;
2105 }
2106 Some(vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0)
2107 }
2108
2109 pub fn largest_notional_trade(ticks: &[NormalizedTick]) -> Option<&NormalizedTick> {
2117 ticks.iter().max_by(|a, b| a.value().cmp(&b.value()))
2118 }
2119
2120 pub fn twap(ticks: &[NormalizedTick]) -> Option<Decimal> {
2126 if ticks.len() < 2 {
2127 return None;
2128 }
2129 let mut weighted_sum = Decimal::ZERO;
2130 let mut total_time = 0u64;
2131 for w in ticks.windows(2) {
2132 let dt = w[1].received_at_ms.saturating_sub(w[0].received_at_ms);
2133 weighted_sum += w[0].price * Decimal::from(dt);
2134 total_time += dt;
2135 }
2136 if total_time == 0 {
2137 return None;
2138 }
2139 Some(weighted_sum / Decimal::from(total_time))
2140 }
2141
2142 pub fn neutral_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2147 if ticks.is_empty() {
2148 return None;
2149 }
2150 Some(Self::count_neutral(ticks) as f64 / ticks.len() as f64)
2151 }
2152
2153 pub fn log_return_variance(ticks: &[NormalizedTick]) -> Option<f64> {
2157 if ticks.len() < 3 {
2158 return None;
2159 }
2160 let returns: Vec<f64> = ticks
2161 .windows(2)
2162 .filter_map(|w| {
2163 use rust_decimal::prelude::ToPrimitive;
2164 let prev = w[0].price.to_f64()?;
2165 let curr = w[1].price.to_f64()?;
2166 if prev <= 0.0 || curr <= 0.0 {
2167 return None;
2168 }
2169 Some((curr / prev).ln())
2170 })
2171 .collect();
2172 if returns.len() < 2 {
2173 return None;
2174 }
2175 let n = returns.len() as f64;
2176 let mean = returns.iter().sum::<f64>() / n;
2177 Some(returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0))
2178 }
2179
2180 pub fn volume_at_vwap(ticks: &[NormalizedTick], tolerance: Decimal) -> Decimal {
2185 let vwap = match Self::vwap(ticks) {
2186 Some(v) => v,
2187 None => return Decimal::ZERO,
2188 };
2189 ticks
2190 .iter()
2191 .filter(|t| (t.price - vwap).abs() <= tolerance)
2192 .map(|t| t.quantity)
2193 .sum()
2194 }
2195
2196}
2197
2198
2199impl std::fmt::Display for NormalizedTick {
2200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2201 let side = match self.side {
2202 Some(s) => s.to_string(),
2203 None => "?".to_string(),
2204 };
2205 write!(
2206 f,
2207 "{} {} {} x {} {} @{}ms",
2208 self.exchange, self.symbol, self.price, self.quantity, side, self.received_at_ms
2209 )
2210 }
2211}
2212
2213pub struct TickNormalizer;
2218
2219impl TickNormalizer {
2220 pub fn new() -> Self {
2222 Self
2223 }
2224
2225 pub fn normalize(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
2233 let tick = match raw.exchange {
2234 Exchange::Binance => self.normalize_binance(raw),
2235 Exchange::Coinbase => self.normalize_coinbase(raw),
2236 Exchange::Alpaca => self.normalize_alpaca(raw),
2237 Exchange::Polygon => self.normalize_polygon(raw),
2238 }?;
2239 if tick.price <= Decimal::ZERO {
2240 return Err(StreamError::InvalidTick {
2241 reason: format!("price must be positive, got {}", tick.price),
2242 });
2243 }
2244 if tick.quantity < Decimal::ZERO {
2245 return Err(StreamError::InvalidTick {
2246 reason: format!("quantity must be non-negative, got {}", tick.quantity),
2247 });
2248 }
2249 trace!(
2250 exchange = %tick.exchange,
2251 symbol = %tick.symbol,
2252 price = %tick.price,
2253 exchange_ts_ms = ?tick.exchange_ts_ms,
2254 "tick normalized"
2255 );
2256 Ok(tick)
2257 }
2258
2259 fn normalize_binance(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
2260 let p = &raw.payload;
2261 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
2262 let qty = parse_decimal_field(p, "q", &raw.exchange.to_string())?;
2263 let side = p.get("m").and_then(|v| v.as_bool()).map(|maker| {
2264 if maker {
2265 TradeSide::Sell
2266 } else {
2267 TradeSide::Buy
2268 }
2269 });
2270 let trade_id = p.get("t").and_then(|v| v.as_u64()).map(|id| id.to_string());
2271 let exchange_ts = p.get("T").and_then(|v| v.as_u64());
2272 Ok(NormalizedTick {
2273 exchange: raw.exchange,
2274 symbol: raw.symbol,
2275 price,
2276 quantity: qty,
2277 side,
2278 trade_id,
2279 exchange_ts_ms: exchange_ts,
2280 received_at_ms: raw.received_at_ms,
2281 })
2282 }
2283
2284 fn normalize_coinbase(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
2285 let p = &raw.payload;
2286 let price = parse_decimal_field(p, "price", &raw.exchange.to_string())?;
2287 let qty = parse_decimal_field(p, "size", &raw.exchange.to_string())?;
2288 let side = p.get("side").and_then(|v| v.as_str()).map(|s| {
2289 if s == "buy" {
2290 TradeSide::Buy
2291 } else {
2292 TradeSide::Sell
2293 }
2294 });
2295 let trade_id = p
2296 .get("trade_id")
2297 .and_then(|v| v.as_str())
2298 .map(str::to_string);
2299 let exchange_ts_ms = p
2301 .get("time")
2302 .and_then(|v| v.as_str())
2303 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
2304 .map(|dt| dt.timestamp_millis() as u64);
2305 Ok(NormalizedTick {
2306 exchange: raw.exchange,
2307 symbol: raw.symbol,
2308 price,
2309 quantity: qty,
2310 side,
2311 trade_id,
2312 exchange_ts_ms,
2313 received_at_ms: raw.received_at_ms,
2314 })
2315 }
2316
2317 fn normalize_alpaca(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
2318 let p = &raw.payload;
2319 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
2320 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
2321 let trade_id = p.get("i").and_then(|v| v.as_u64()).map(|id| id.to_string());
2322 let exchange_ts_ms = p
2324 .get("t")
2325 .and_then(|v| v.as_str())
2326 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
2327 .map(|dt| dt.timestamp_millis() as u64);
2328 Ok(NormalizedTick {
2329 exchange: raw.exchange,
2330 symbol: raw.symbol,
2331 price,
2332 quantity: qty,
2333 side: None,
2334 trade_id,
2335 exchange_ts_ms,
2336 received_at_ms: raw.received_at_ms,
2337 })
2338 }
2339
2340 fn normalize_polygon(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
2341 let p = &raw.payload;
2342 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
2343 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
2344 let trade_id = p.get("i").and_then(|v| v.as_str()).map(str::to_string);
2345 let exchange_ts = p
2347 .get("t")
2348 .and_then(|v| v.as_u64())
2349 .map(|t_ns| t_ns / 1_000_000);
2350 Ok(NormalizedTick {
2351 exchange: raw.exchange,
2352 symbol: raw.symbol,
2353 price,
2354 quantity: qty,
2355 side: None,
2356 trade_id,
2357 exchange_ts_ms: exchange_ts,
2358 received_at_ms: raw.received_at_ms,
2359 })
2360 }
2361}
2362
2363impl Default for TickNormalizer {
2364 fn default() -> Self {
2365 Self::new()
2366 }
2367}
2368
2369fn parse_decimal_field(
2370 v: &serde_json::Value,
2371 field: &str,
2372 exchange: &str,
2373) -> Result<Decimal, StreamError> {
2374 let raw = v.get(field).ok_or_else(|| StreamError::ParseError {
2375 exchange: exchange.to_string(),
2376 reason: format!("missing field '{}'", field),
2377 })?;
2378 let s: String = match raw {
2384 serde_json::Value::String(s) => s.clone(),
2385 serde_json::Value::Number(n) => n.to_string(),
2386 _ => {
2387 return Err(StreamError::ParseError {
2388 exchange: exchange.to_string(),
2389 reason: format!("field '{}' is not a string or number", field),
2390 });
2391 }
2392 };
2393 Decimal::from_str(&s).map_err(|e| StreamError::ParseError {
2394 exchange: exchange.to_string(),
2395 reason: format!("field '{}' parse error: {}", field, e),
2396 })
2397}
2398
2399fn now_ms() -> u64 {
2400 std::time::SystemTime::now()
2401 .duration_since(std::time::UNIX_EPOCH)
2402 .map(|d| d.as_millis() as u64)
2403 .unwrap_or(0)
2404}
2405
2406#[cfg(test)]
2407mod tests {
2408 use super::*;
2409 use serde_json::json;
2410
2411 fn normalizer() -> TickNormalizer {
2412 TickNormalizer::new()
2413 }
2414
2415 fn binance_tick(symbol: &str) -> RawTick {
2416 RawTick {
2417 exchange: Exchange::Binance,
2418 symbol: symbol.to_string(),
2419 payload: json!({ "p": "50000.12", "q": "0.001", "m": false, "t": 12345, "T": 1700000000000u64 }),
2420 received_at_ms: 1700000000001,
2421 }
2422 }
2423
2424 fn coinbase_tick(symbol: &str) -> RawTick {
2425 RawTick {
2426 exchange: Exchange::Coinbase,
2427 symbol: symbol.to_string(),
2428 payload: json!({ "price": "50001.00", "size": "0.5", "side": "buy", "trade_id": "abc123" }),
2429 received_at_ms: 1700000000002,
2430 }
2431 }
2432
2433 fn alpaca_tick(symbol: &str) -> RawTick {
2434 RawTick {
2435 exchange: Exchange::Alpaca,
2436 symbol: symbol.to_string(),
2437 payload: json!({ "p": "180.50", "s": "10", "i": 99 }),
2438 received_at_ms: 1700000000003,
2439 }
2440 }
2441
2442 fn polygon_tick(symbol: &str) -> RawTick {
2443 RawTick {
2444 exchange: Exchange::Polygon,
2445 symbol: symbol.to_string(),
2446 payload: json!({ "p": "180.51", "s": "5", "i": "XYZ-001", "t": 1_700_000_000_000_000_000u64 }),
2448 received_at_ms: 1700000000005,
2449 }
2450 }
2451
2452 #[test]
2453 fn test_exchange_from_str_valid() {
2454 assert_eq!("binance".parse::<Exchange>().unwrap(), Exchange::Binance);
2455 assert_eq!("Coinbase".parse::<Exchange>().unwrap(), Exchange::Coinbase);
2456 assert_eq!("ALPACA".parse::<Exchange>().unwrap(), Exchange::Alpaca);
2457 assert_eq!("polygon".parse::<Exchange>().unwrap(), Exchange::Polygon);
2458 }
2459
2460 #[test]
2461 fn test_exchange_from_str_unknown_returns_error() {
2462 let result = "Kraken".parse::<Exchange>();
2463 assert!(matches!(result, Err(StreamError::UnknownExchange(_))));
2464 }
2465
2466 #[test]
2467 fn test_exchange_display() {
2468 assert_eq!(Exchange::Binance.to_string(), "Binance");
2469 assert_eq!(Exchange::Coinbase.to_string(), "Coinbase");
2470 }
2471
2472 #[test]
2473 fn test_normalize_binance_tick_price_and_qty() {
2474 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2475 assert_eq!(tick.price, Decimal::from_str("50000.12").unwrap());
2476 assert_eq!(tick.quantity, Decimal::from_str("0.001").unwrap());
2477 assert_eq!(tick.exchange, Exchange::Binance);
2478 assert_eq!(tick.symbol, "BTCUSDT");
2479 }
2480
2481 #[test]
2482 fn test_normalize_binance_side_maker_false_is_buy() {
2483 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2484 assert_eq!(tick.side, Some(TradeSide::Buy));
2485 }
2486
2487 #[test]
2488 fn test_normalize_binance_side_maker_true_is_sell() {
2489 let raw = RawTick {
2490 exchange: Exchange::Binance,
2491 symbol: "BTCUSDT".into(),
2492 payload: json!({ "p": "50000", "q": "1", "m": true }),
2493 received_at_ms: 0,
2494 };
2495 let tick = normalizer().normalize(raw).unwrap();
2496 assert_eq!(tick.side, Some(TradeSide::Sell));
2497 }
2498
2499 #[test]
2500 fn test_normalize_binance_trade_id_and_ts() {
2501 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2502 assert_eq!(tick.trade_id, Some("12345".to_string()));
2503 assert_eq!(tick.exchange_ts_ms, Some(1700000000000));
2504 }
2505
2506 #[test]
2507 fn test_normalize_coinbase_tick() {
2508 let tick = normalizer().normalize(coinbase_tick("BTC-USD")).unwrap();
2509 assert_eq!(tick.price, Decimal::from_str("50001.00").unwrap());
2510 assert_eq!(tick.quantity, Decimal::from_str("0.5").unwrap());
2511 assert_eq!(tick.side, Some(TradeSide::Buy));
2512 assert_eq!(tick.trade_id, Some("abc123".to_string()));
2513 }
2514
2515 #[test]
2516 fn test_normalize_coinbase_sell_side() {
2517 let raw = RawTick {
2518 exchange: Exchange::Coinbase,
2519 symbol: "BTC-USD".into(),
2520 payload: json!({ "price": "50000", "size": "1", "side": "sell" }),
2521 received_at_ms: 0,
2522 };
2523 let tick = normalizer().normalize(raw).unwrap();
2524 assert_eq!(tick.side, Some(TradeSide::Sell));
2525 }
2526
2527 #[test]
2528 fn test_normalize_alpaca_tick() {
2529 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
2530 assert_eq!(tick.price, Decimal::from_str("180.50").unwrap());
2531 assert_eq!(tick.quantity, Decimal::from_str("10").unwrap());
2532 assert_eq!(tick.trade_id, Some("99".to_string()));
2533 assert_eq!(tick.side, None);
2534 }
2535
2536 #[test]
2537 fn test_normalize_polygon_tick() {
2538 let tick = normalizer().normalize(polygon_tick("AAPL")).unwrap();
2539 assert_eq!(tick.price, Decimal::from_str("180.51").unwrap());
2540 assert_eq!(tick.exchange_ts_ms, Some(1_700_000_000_000u64));
2542 assert_eq!(tick.trade_id, Some("XYZ-001".to_string()));
2543 }
2544
2545 #[test]
2546 fn test_normalize_alpaca_rfc3339_timestamp() {
2547 let raw = RawTick {
2548 exchange: Exchange::Alpaca,
2549 symbol: "AAPL".into(),
2550 payload: json!({ "p": "180.50", "s": "10", "i": 99, "t": "2023-11-15T00:00:00Z" }),
2551 received_at_ms: 1700000000003,
2552 };
2553 let tick = normalizer().normalize(raw).unwrap();
2554 assert!(tick.exchange_ts_ms.is_some(), "Alpaca 't' field should be parsed");
2555 assert_eq!(tick.exchange_ts_ms, Some(1700006400000u64));
2557 }
2558
2559 #[test]
2560 fn test_normalize_alpaca_no_timestamp_field() {
2561 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
2562 assert_eq!(tick.exchange_ts_ms, None, "missing 't' field means no exchange_ts_ms");
2563 }
2564
2565 #[test]
2566 fn test_normalize_missing_price_field_returns_parse_error() {
2567 let raw = RawTick {
2568 exchange: Exchange::Binance,
2569 symbol: "BTCUSDT".into(),
2570 payload: json!({ "q": "1" }),
2571 received_at_ms: 0,
2572 };
2573 let result = normalizer().normalize(raw);
2574 assert!(matches!(result, Err(StreamError::ParseError { .. })));
2575 }
2576
2577 #[test]
2578 fn test_normalize_invalid_decimal_returns_parse_error() {
2579 let raw = RawTick {
2580 exchange: Exchange::Coinbase,
2581 symbol: "BTC-USD".into(),
2582 payload: json!({ "price": "not-a-number", "size": "1" }),
2583 received_at_ms: 0,
2584 };
2585 let result = normalizer().normalize(raw);
2586 assert!(matches!(result, Err(StreamError::ParseError { .. })));
2587 }
2588
2589 #[test]
2590 fn test_raw_tick_new_sets_received_at() {
2591 let raw = RawTick::new(Exchange::Binance, "BTCUSDT", json!({}));
2592 assert!(raw.received_at_ms > 0);
2593 }
2594
2595 #[test]
2596 fn test_normalize_numeric_price_field() {
2597 let raw = RawTick {
2598 exchange: Exchange::Binance,
2599 symbol: "BTCUSDT".into(),
2600 payload: json!({ "p": 50000.0, "q": 1.0 }),
2601 received_at_ms: 0,
2602 };
2603 let tick = normalizer().normalize(raw).unwrap();
2604 assert!(tick.price > Decimal::ZERO);
2605 }
2606
2607 #[test]
2608 fn test_trade_side_from_str_buy() {
2609 assert_eq!("buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
2610 assert_eq!("Buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
2611 assert_eq!("BUY".parse::<TradeSide>().unwrap(), TradeSide::Buy);
2612 }
2613
2614 #[test]
2615 fn test_trade_side_from_str_sell() {
2616 assert_eq!("sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
2617 assert_eq!("Sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
2618 assert_eq!("SELL".parse::<TradeSide>().unwrap(), TradeSide::Sell);
2619 }
2620
2621 #[test]
2622 fn test_trade_side_from_str_invalid() {
2623 let err = "long".parse::<TradeSide>().unwrap_err();
2624 assert!(matches!(err, StreamError::ParseError { .. }));
2625 }
2626
2627 #[test]
2628 fn test_trade_side_display() {
2629 assert_eq!(TradeSide::Buy.to_string(), "buy");
2630 assert_eq!(TradeSide::Sell.to_string(), "sell");
2631 }
2632
2633 #[test]
2634 fn test_normalize_zero_price_returns_invalid_tick() {
2635 let raw = RawTick {
2636 exchange: Exchange::Binance,
2637 symbol: "BTCUSDT".into(),
2638 payload: json!({ "p": "0", "q": "1" }),
2639 received_at_ms: 0,
2640 };
2641 let err = normalizer().normalize(raw).unwrap_err();
2642 assert!(matches!(err, StreamError::InvalidTick { .. }));
2643 }
2644
2645 #[test]
2646 fn test_normalize_negative_price_returns_invalid_tick() {
2647 let raw = RawTick {
2648 exchange: Exchange::Binance,
2649 symbol: "BTCUSDT".into(),
2650 payload: json!({ "p": "-1", "q": "1" }),
2651 received_at_ms: 0,
2652 };
2653 let err = normalizer().normalize(raw).unwrap_err();
2654 assert!(matches!(err, StreamError::InvalidTick { .. }));
2655 }
2656
2657 #[test]
2658 fn test_normalize_negative_quantity_returns_invalid_tick() {
2659 let raw = RawTick {
2660 exchange: Exchange::Binance,
2661 symbol: "BTCUSDT".into(),
2662 payload: json!({ "p": "100", "q": "-1" }),
2663 received_at_ms: 0,
2664 };
2665 let err = normalizer().normalize(raw).unwrap_err();
2666 assert!(matches!(err, StreamError::InvalidTick { .. }));
2667 }
2668
2669 #[test]
2670 fn test_normalize_zero_quantity_is_valid() {
2671 let raw = RawTick {
2673 exchange: Exchange::Binance,
2674 symbol: "BTCUSDT".into(),
2675 payload: json!({ "p": "100", "q": "0" }),
2676 received_at_ms: 0,
2677 };
2678 let tick = normalizer().normalize(raw).unwrap();
2679 assert_eq!(tick.quantity, Decimal::ZERO);
2680 }
2681
2682 #[test]
2683 fn test_trade_side_is_buy() {
2684 assert!(TradeSide::Buy.is_buy());
2685 assert!(!TradeSide::Buy.is_sell());
2686 }
2687
2688 #[test]
2689 fn test_trade_side_is_sell() {
2690 assert!(TradeSide::Sell.is_sell());
2691 assert!(!TradeSide::Sell.is_buy());
2692 }
2693
2694 #[test]
2695 fn test_normalized_tick_display() {
2696 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2697 let s = tick.to_string();
2698 assert!(s.contains("Binance"));
2699 assert!(s.contains("BTCUSDT"));
2700 assert!(s.contains("50000"));
2701 }
2702
2703 #[test]
2704 fn test_normalized_tick_value_is_price_times_qty() {
2705 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2706 let expected = tick.price * tick.quantity;
2708 assert_eq!(tick.volume_notional(), expected);
2709 }
2710
2711 #[test]
2712 fn test_normalized_tick_age_ms_positive() {
2713 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
2714 let raw = RawTick {
2717 exchange: Exchange::Binance,
2718 symbol: "BTCUSDT".into(),
2719 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
2720 received_at_ms: 1_000_000,
2721 };
2722 let tick = normalizer().normalize(raw).unwrap();
2723 assert_eq!(tick.age_ms(1_001_000), 1_000);
2724 }
2725
2726 #[test]
2727 fn test_normalized_tick_age_ms_zero_when_now_equals_received() {
2728 let raw = RawTick {
2729 exchange: Exchange::Binance,
2730 symbol: "BTCUSDT".into(),
2731 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
2732 received_at_ms: 5_000,
2733 };
2734 let tick = normalizer().normalize(raw).unwrap();
2735 assert_eq!(tick.age_ms(5_000), 0);
2736 assert_eq!(tick.age_ms(4_000), 0);
2738 }
2739
2740 #[test]
2741 fn test_normalized_tick_value_zero_qty_is_zero() {
2742 use rust_decimal_macros::dec;
2743 let raw = RawTick {
2744 exchange: Exchange::Binance,
2745 symbol: "BTCUSDT".into(),
2746 payload: serde_json::json!({
2747 "p": "50000",
2748 "q": "0",
2749 "m": false,
2750 }),
2751 received_at_ms: 1000,
2752 };
2753 let tick = normalizer().normalize(raw).unwrap();
2754 assert_eq!(tick.value(), dec!(0));
2755 }
2756
2757 fn make_tick_at(received_at_ms: u64) -> NormalizedTick {
2760 NormalizedTick {
2761 exchange: Exchange::Binance,
2762 symbol: "BTCUSDT".into(),
2763 price: rust_decimal_macros::dec!(100),
2764 quantity: rust_decimal_macros::dec!(1),
2765 side: None,
2766 trade_id: None,
2767 exchange_ts_ms: None,
2768 received_at_ms,
2769 }
2770 }
2771
2772 #[test]
2773 fn test_is_stale_true_when_age_exceeds_threshold() {
2774 let tick = make_tick_at(1_000);
2775 assert!(tick.is_stale(6_000, 4_000));
2777 }
2778
2779 #[test]
2780 fn test_is_stale_false_when_age_equals_threshold() {
2781 let tick = make_tick_at(1_000);
2782 assert!(!tick.is_stale(5_000, 4_000));
2784 }
2785
2786 #[test]
2787 fn test_is_stale_false_for_fresh_tick() {
2788 let tick = make_tick_at(10_000);
2789 assert!(!tick.is_stale(10_500, 1_000));
2790 }
2791
2792 #[test]
2795 fn test_is_buy_true_for_buy_side() {
2796 let mut tick = make_tick_at(1_000);
2797 tick.side = Some(TradeSide::Buy);
2798 assert!(tick.is_buy());
2799 assert!(!tick.is_sell());
2800 }
2801
2802 #[test]
2803 fn test_is_sell_true_for_sell_side() {
2804 let mut tick = make_tick_at(1_000);
2805 tick.side = Some(TradeSide::Sell);
2806 assert!(tick.is_sell());
2807 assert!(!tick.is_buy());
2808 }
2809
2810 #[test]
2811 fn test_is_buy_false_for_unknown_side() {
2812 let mut tick = make_tick_at(1_000);
2813 tick.side = None;
2814 assert!(!tick.is_buy());
2815 assert!(!tick.is_sell());
2816 }
2817
2818 #[test]
2821 fn test_with_exchange_ts_sets_field() {
2822 let tick = make_tick_at(5_000).with_exchange_ts(3_000);
2823 assert_eq!(tick.exchange_ts_ms, Some(3_000));
2824 assert_eq!(tick.received_at_ms, 5_000); }
2826
2827 #[test]
2828 fn test_with_exchange_ts_overrides_existing() {
2829 let tick = make_tick_at(1_000).with_exchange_ts(999).with_exchange_ts(888);
2830 assert_eq!(tick.exchange_ts_ms, Some(888));
2831 }
2832
2833 #[test]
2836 fn test_price_move_from_positive() {
2837 let prev = make_tick_at(1_000);
2838 let mut curr = make_tick_at(2_000);
2839 curr.price = prev.price + rust_decimal_macros::dec!(5);
2840 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(5));
2841 }
2842
2843 #[test]
2844 fn test_price_move_from_negative() {
2845 let prev = make_tick_at(1_000);
2846 let mut curr = make_tick_at(2_000);
2847 curr.price = prev.price - rust_decimal_macros::dec!(3);
2848 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(-3));
2849 }
2850
2851 #[test]
2852 fn test_price_move_from_zero_when_same() {
2853 let tick = make_tick_at(1_000);
2854 assert_eq!(tick.price_move_from(&tick), rust_decimal_macros::dec!(0));
2855 }
2856
2857 #[test]
2858 fn test_is_more_recent_than_true() {
2859 let older = make_tick_at(1_000);
2860 let newer = make_tick_at(2_000);
2861 assert!(newer.is_more_recent_than(&older));
2862 }
2863
2864 #[test]
2865 fn test_is_more_recent_than_false_when_older() {
2866 let older = make_tick_at(1_000);
2867 let newer = make_tick_at(2_000);
2868 assert!(!older.is_more_recent_than(&newer));
2869 }
2870
2871 #[test]
2872 fn test_is_more_recent_than_false_when_equal() {
2873 let tick = make_tick_at(1_000);
2874 assert!(!tick.is_more_recent_than(&tick));
2875 }
2876
2877 #[test]
2880 fn test_with_side_sets_buy() {
2881 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
2882 assert_eq!(tick.side, Some(TradeSide::Buy));
2883 }
2884
2885 #[test]
2886 fn test_with_side_sets_sell() {
2887 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
2888 assert_eq!(tick.side, Some(TradeSide::Sell));
2889 }
2890
2891 #[test]
2892 fn test_with_side_overrides_existing() {
2893 let tick = make_tick_at(1_000).with_side(TradeSide::Buy).with_side(TradeSide::Sell);
2894 assert_eq!(tick.side, Some(TradeSide::Sell));
2895 }
2896
2897 #[test]
2900 fn test_is_neutral_true_when_no_side() {
2901 let mut tick = make_tick_at(1_000);
2902 tick.side = None;
2903 assert!(tick.is_neutral());
2904 }
2905
2906 #[test]
2907 fn test_is_neutral_false_when_buy() {
2908 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
2909 assert!(!tick.is_neutral());
2910 }
2911
2912 #[test]
2913 fn test_is_neutral_false_when_sell() {
2914 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
2915 assert!(!tick.is_neutral());
2916 }
2917
2918 #[test]
2921 fn test_is_large_trade_above_threshold() {
2922 let mut tick = make_tick_at(1_000);
2923 tick.quantity = rust_decimal_macros::dec!(100);
2924 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
2925 }
2926
2927 #[test]
2928 fn test_is_large_trade_at_threshold() {
2929 let mut tick = make_tick_at(1_000);
2930 tick.quantity = rust_decimal_macros::dec!(50);
2931 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
2932 }
2933
2934 #[test]
2935 fn test_is_large_trade_below_threshold() {
2936 let mut tick = make_tick_at(1_000);
2937 tick.quantity = rust_decimal_macros::dec!(10);
2938 assert!(!tick.is_large_trade(rust_decimal_macros::dec!(50)));
2939 }
2940
2941 #[test]
2942 fn test_volume_notional_is_price_times_quantity() {
2943 let mut tick = make_tick_at(1_000);
2944 tick.price = rust_decimal_macros::dec!(200);
2945 tick.quantity = rust_decimal_macros::dec!(3);
2946 assert_eq!(tick.volume_notional(), rust_decimal_macros::dec!(600));
2947 }
2948
2949 #[test]
2952 fn test_is_above_returns_true_when_price_higher() {
2953 let mut tick = make_tick_at(1_000);
2954 tick.price = rust_decimal_macros::dec!(200);
2955 assert!(tick.is_above(rust_decimal_macros::dec!(150)));
2956 }
2957
2958 #[test]
2959 fn test_is_above_returns_false_when_price_equal() {
2960 let mut tick = make_tick_at(1_000);
2961 tick.price = rust_decimal_macros::dec!(200);
2962 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
2963 }
2964
2965 #[test]
2966 fn test_is_above_returns_false_when_price_lower() {
2967 let mut tick = make_tick_at(1_000);
2968 tick.price = rust_decimal_macros::dec!(100);
2969 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
2970 }
2971
2972 #[test]
2975 fn test_is_below_returns_true_when_price_lower() {
2976 let mut tick = make_tick_at(1_000);
2977 tick.price = rust_decimal_macros::dec!(100);
2978 assert!(tick.is_below(rust_decimal_macros::dec!(150)));
2979 }
2980
2981 #[test]
2982 fn test_is_below_returns_false_when_price_equal() {
2983 let mut tick = make_tick_at(1_000);
2984 tick.price = rust_decimal_macros::dec!(100);
2985 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
2986 }
2987
2988 #[test]
2989 fn test_is_below_returns_false_when_price_higher() {
2990 let mut tick = make_tick_at(1_000);
2991 tick.price = rust_decimal_macros::dec!(200);
2992 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
2993 }
2994
2995 #[test]
2998 fn test_has_exchange_ts_false_when_none() {
2999 let tick = make_tick_at(1_000);
3000 assert!(!tick.has_exchange_ts());
3001 }
3002
3003 #[test]
3004 fn test_has_exchange_ts_true_when_some() {
3005 let tick = make_tick_at(1_000).with_exchange_ts(900);
3006 assert!(tick.has_exchange_ts());
3007 }
3008
3009 #[test]
3012 fn test_is_at_returns_true_when_equal() {
3013 let mut tick = make_tick_at(1_000);
3014 tick.price = rust_decimal_macros::dec!(100);
3015 assert!(tick.is_at(rust_decimal_macros::dec!(100)));
3016 }
3017
3018 #[test]
3019 fn test_is_at_returns_false_when_higher() {
3020 let mut tick = make_tick_at(1_000);
3021 tick.price = rust_decimal_macros::dec!(101);
3022 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
3023 }
3024
3025 #[test]
3026 fn test_is_at_returns_false_when_lower() {
3027 let mut tick = make_tick_at(1_000);
3028 tick.price = rust_decimal_macros::dec!(99);
3029 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
3030 }
3031
3032 #[test]
3035 fn test_is_buy_true_when_side_is_buy() {
3036 let mut tick = make_tick_at(1_000);
3037 tick.side = Some(TradeSide::Buy);
3038 assert!(tick.is_buy());
3039 }
3040
3041 #[test]
3042 fn test_is_buy_false_when_side_is_sell() {
3043 let mut tick = make_tick_at(1_000);
3044 tick.side = Some(TradeSide::Sell);
3045 assert!(!tick.is_buy());
3046 }
3047
3048 #[test]
3049 fn test_is_buy_false_when_side_is_none() {
3050 let mut tick = make_tick_at(1_000);
3051 tick.side = None;
3052 assert!(!tick.is_buy());
3053 }
3054
3055 #[test]
3058 fn test_side_str_buy() {
3059 let mut tick = make_tick_at(1_000);
3060 tick.side = Some(TradeSide::Buy);
3061 assert_eq!(tick.side_str(), "buy");
3062 }
3063
3064 #[test]
3065 fn test_side_str_sell() {
3066 let mut tick = make_tick_at(1_000);
3067 tick.side = Some(TradeSide::Sell);
3068 assert_eq!(tick.side_str(), "sell");
3069 }
3070
3071 #[test]
3072 fn test_side_str_unknown_when_none() {
3073 let mut tick = make_tick_at(1_000);
3074 tick.side = None;
3075 assert_eq!(tick.side_str(), "unknown");
3076 }
3077
3078 #[test]
3079 fn test_is_round_lot_true_for_integer_quantity() {
3080 let mut tick = make_tick_at(1_000);
3081 tick.quantity = rust_decimal_macros::dec!(100);
3082 assert!(tick.is_round_lot());
3083 }
3084
3085 #[test]
3086 fn test_is_round_lot_false_for_fractional_quantity() {
3087 let mut tick = make_tick_at(1_000);
3088 tick.quantity = rust_decimal_macros::dec!(0.5);
3089 assert!(!tick.is_round_lot());
3090 }
3091
3092 #[test]
3095 fn test_is_same_symbol_as_true_when_symbols_match() {
3096 let t1 = make_tick_at(1_000);
3097 let t2 = make_tick_at(2_000);
3098 assert!(t1.is_same_symbol_as(&t2));
3099 }
3100
3101 #[test]
3102 fn test_is_same_symbol_as_false_when_symbols_differ() {
3103 let t1 = make_tick_at(1_000);
3104 let mut t2 = make_tick_at(2_000);
3105 t2.symbol = "ETH-USD".to_string();
3106 assert!(!t1.is_same_symbol_as(&t2));
3107 }
3108
3109 #[test]
3110 fn test_price_distance_from_is_absolute() {
3111 let mut t1 = make_tick_at(1_000);
3112 let mut t2 = make_tick_at(2_000);
3113 t1.price = rust_decimal_macros::dec!(100);
3114 t2.price = rust_decimal_macros::dec!(110);
3115 assert_eq!(t1.price_distance_from(&t2), rust_decimal_macros::dec!(10));
3116 assert_eq!(t2.price_distance_from(&t1), rust_decimal_macros::dec!(10));
3117 }
3118
3119 #[test]
3120 fn test_price_distance_from_zero_when_equal() {
3121 let t1 = make_tick_at(1_000);
3122 let t2 = make_tick_at(2_000);
3123 assert!(t1.price_distance_from(&t2).is_zero());
3124 }
3125
3126 #[test]
3129 fn test_is_sell_true_when_side_is_sell() {
3130 let mut tick = make_tick_at(1_000);
3131 tick.side = Some(TradeSide::Sell);
3132 assert!(tick.is_sell());
3133 }
3134
3135 #[test]
3136 fn test_is_sell_false_when_side_is_buy() {
3137 let mut tick = make_tick_at(1_000);
3138 tick.side = Some(TradeSide::Buy);
3139 assert!(!tick.is_sell());
3140 }
3141
3142 #[test]
3143 fn test_is_sell_false_when_side_is_none() {
3144 let mut tick = make_tick_at(1_000);
3145 tick.side = None;
3146 assert!(!tick.is_sell());
3147 }
3148
3149 #[test]
3152 fn test_exchange_latency_ms_positive_for_normal_delivery() {
3153 let mut tick = make_tick_at(1_100);
3154 tick.exchange_ts_ms = Some(1_000);
3155 assert_eq!(tick.exchange_latency_ms(), Some(100));
3156 }
3157
3158 #[test]
3159 fn test_exchange_latency_ms_negative_for_clock_skew() {
3160 let mut tick = make_tick_at(1_000);
3161 tick.exchange_ts_ms = Some(1_100);
3162 assert_eq!(tick.exchange_latency_ms(), Some(-100));
3163 }
3164
3165 #[test]
3166 fn test_exchange_latency_ms_none_when_no_exchange_ts() {
3167 let mut tick = make_tick_at(1_000);
3168 tick.exchange_ts_ms = None;
3169 assert!(tick.exchange_latency_ms().is_none());
3170 }
3171
3172 #[test]
3173 fn test_is_notional_large_trade_true_when_above_threshold() {
3174 let mut tick = make_tick_at(1_000);
3175 tick.price = rust_decimal_macros::dec!(100);
3176 tick.quantity = rust_decimal_macros::dec!(10);
3177 assert!(tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
3179 }
3180
3181 #[test]
3182 fn test_is_notional_large_trade_false_when_at_or_below_threshold() {
3183 let mut tick = make_tick_at(1_000);
3184 tick.price = rust_decimal_macros::dec!(100);
3185 tick.quantity = rust_decimal_macros::dec!(5);
3186 assert!(!tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
3188 }
3189
3190 #[test]
3191 fn test_is_aggressive_true_when_buy() {
3192 let mut tick = make_tick_at(1_000);
3193 tick.side = Some(TradeSide::Buy);
3194 assert!(tick.is_aggressive());
3195 }
3196
3197 #[test]
3198 fn test_is_aggressive_true_when_sell() {
3199 let mut tick = make_tick_at(1_000);
3200 tick.side = Some(TradeSide::Sell);
3201 assert!(tick.is_aggressive());
3202 }
3203
3204 #[test]
3205 fn test_is_aggressive_false_when_neutral() {
3206 let tick = make_tick_at(1_000); assert!(!tick.is_aggressive());
3208 }
3209
3210 #[test]
3211 fn test_price_diff_from_positive_when_higher() {
3212 let mut t1 = make_tick_at(1_000);
3213 let mut t2 = make_tick_at(1_000);
3214 t1.price = rust_decimal_macros::dec!(105);
3215 t2.price = rust_decimal_macros::dec!(100);
3216 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(5));
3217 }
3218
3219 #[test]
3220 fn test_price_diff_from_negative_when_lower() {
3221 let mut t1 = make_tick_at(1_000);
3222 let mut t2 = make_tick_at(1_000);
3223 t1.price = rust_decimal_macros::dec!(95);
3224 t2.price = rust_decimal_macros::dec!(100);
3225 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(-5));
3226 }
3227
3228 #[test]
3229 fn test_is_micro_trade_true_when_below_threshold() {
3230 let mut tick = make_tick_at(1_000);
3231 tick.quantity = rust_decimal_macros::dec!(0.5);
3232 assert!(tick.is_micro_trade(rust_decimal_macros::dec!(1)));
3233 }
3234
3235 #[test]
3236 fn test_is_micro_trade_false_when_equal_threshold() {
3237 let mut tick = make_tick_at(1_000);
3238 tick.quantity = rust_decimal_macros::dec!(1);
3239 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
3240 }
3241
3242 #[test]
3243 fn test_is_micro_trade_false_when_above_threshold() {
3244 let mut tick = make_tick_at(1_000);
3245 tick.quantity = rust_decimal_macros::dec!(2);
3246 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
3247 }
3248
3249 #[test]
3252 fn test_is_zero_price_true_for_zero() {
3253 let mut tick = make_tick_at(1_000);
3254 tick.price = rust_decimal_macros::dec!(0);
3255 assert!(tick.is_zero_price());
3256 }
3257
3258 #[test]
3259 fn test_is_zero_price_false_for_nonzero() {
3260 let tick = make_tick_at(1_000); assert!(!tick.is_zero_price());
3262 }
3263
3264 #[test]
3265 fn test_is_fresh_true_when_within_age() {
3266 let tick = make_tick_at(1_000);
3267 assert!(tick.is_fresh(2_000, 1_500));
3269 }
3270
3271 #[test]
3272 fn test_is_fresh_false_when_too_old() {
3273 let tick = make_tick_at(1_000);
3274 assert!(!tick.is_fresh(5_000, 2_000));
3276 }
3277
3278 #[test]
3279 fn test_is_fresh_true_when_now_less_than_received() {
3280 let tick = make_tick_at(5_000);
3282 assert!(tick.is_fresh(3_000, 100));
3283 }
3284
3285 #[test]
3287 fn test_age_ms_correct_elapsed() {
3288 let tick = make_tick_at(10_000);
3289 assert_eq!(tick.age_ms(10_500), 500);
3290 }
3291
3292 #[test]
3293 fn test_age_ms_zero_when_now_equals_received() {
3294 let tick = make_tick_at(10_000);
3295 assert_eq!(tick.age_ms(10_000), 0);
3296 }
3297
3298 #[test]
3299 fn test_age_ms_zero_when_now_before_received() {
3300 let tick = make_tick_at(10_000);
3301 assert_eq!(tick.age_ms(9_000), 0);
3302 }
3303
3304 #[test]
3306 fn test_is_buying_pressure_true_above_midpoint() {
3307 use rust_decimal_macros::dec;
3308 let mut tick = make_tick_at(0);
3309 tick.price = dec!(100.50);
3310 assert!(tick.is_buying_pressure(dec!(100)));
3311 }
3312
3313 #[test]
3314 fn test_is_buying_pressure_false_below_midpoint() {
3315 use rust_decimal_macros::dec;
3316 let mut tick = make_tick_at(0);
3317 tick.price = dec!(99.50);
3318 assert!(!tick.is_buying_pressure(dec!(100)));
3319 }
3320
3321 #[test]
3322 fn test_is_buying_pressure_false_at_midpoint() {
3323 use rust_decimal_macros::dec;
3324 let mut tick = make_tick_at(0);
3325 tick.price = dec!(100);
3326 assert!(!tick.is_buying_pressure(dec!(100)));
3327 }
3328
3329 #[test]
3331 fn test_rounded_price_rounds_to_nearest_tick() {
3332 use rust_decimal_macros::dec;
3333 let mut tick = make_tick_at(0);
3334 tick.price = dec!(100.37);
3335 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.25));
3337 }
3338
3339 #[test]
3340 fn test_rounded_price_unchanged_when_already_aligned() {
3341 use rust_decimal_macros::dec;
3342 let mut tick = make_tick_at(0);
3343 tick.price = dec!(100.50);
3344 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.50));
3345 }
3346
3347 #[test]
3348 fn test_rounded_price_returns_original_for_zero_tick_size() {
3349 use rust_decimal_macros::dec;
3350 let mut tick = make_tick_at(0);
3351 tick.price = dec!(99.99);
3352 assert_eq!(tick.rounded_price(dec!(0)), dec!(99.99));
3353 }
3354
3355 #[test]
3357 fn test_is_large_spread_from_true_when_large() {
3358 use rust_decimal_macros::dec;
3359 let mut t1 = make_tick_at(0);
3360 let mut t2 = make_tick_at(0);
3361 t1.price = dec!(100);
3362 t2.price = dec!(110);
3363 assert!(t1.is_large_spread_from(&t2, dec!(5)));
3364 }
3365
3366 #[test]
3367 fn test_is_large_spread_from_false_when_small() {
3368 use rust_decimal_macros::dec;
3369 let mut t1 = make_tick_at(0);
3370 let mut t2 = make_tick_at(0);
3371 t1.price = dec!(100);
3372 t2.price = dec!(101);
3373 assert!(!t1.is_large_spread_from(&t2, dec!(5)));
3374 }
3375
3376 #[test]
3379 fn test_age_secs_correct() {
3380 let tick = make_tick_at(1_000);
3381 assert!((tick.age_secs(3_000) - 2.0).abs() < 1e-9);
3382 }
3383
3384 #[test]
3385 fn test_age_secs_zero_when_now_equals_received() {
3386 let tick = make_tick_at(5_000);
3387 assert_eq!(tick.age_secs(5_000), 0.0);
3388 }
3389
3390 #[test]
3391 fn test_age_secs_zero_when_now_before_received() {
3392 let tick = make_tick_at(5_000);
3393 assert_eq!(tick.age_secs(1_000), 0.0);
3394 }
3395
3396 #[test]
3399 fn test_is_same_exchange_as_true_when_matching() {
3400 let t1 = make_tick_at(1_000); let t2 = make_tick_at(2_000); assert!(t1.is_same_exchange_as(&t2));
3403 }
3404
3405 #[test]
3406 fn test_is_same_exchange_as_false_when_different() {
3407 let t1 = make_tick_at(1_000); let mut t2 = make_tick_at(2_000);
3409 t2.exchange = Exchange::Coinbase;
3410 assert!(!t1.is_same_exchange_as(&t2));
3411 }
3412
3413 #[test]
3416 fn test_quote_age_ms_correct() {
3417 let tick = make_tick_at(1_000);
3418 assert_eq!(tick.quote_age_ms(3_000), 2_000);
3419 }
3420
3421 #[test]
3422 fn test_quote_age_ms_zero_when_now_before_received() {
3423 let tick = make_tick_at(5_000);
3424 assert_eq!(tick.quote_age_ms(1_000), 0);
3425 }
3426
3427 #[test]
3428 fn test_notional_value_correct() {
3429 use rust_decimal_macros::dec;
3430 let mut tick = make_tick_at(0);
3431 tick.price = dec!(100);
3432 tick.quantity = dec!(5);
3433 assert_eq!(tick.notional_value(), dec!(500));
3434 }
3435
3436 #[test]
3437 fn test_is_high_value_tick_true_when_above_threshold() {
3438 use rust_decimal_macros::dec;
3439 let mut tick = make_tick_at(0);
3440 tick.price = dec!(100);
3441 tick.quantity = dec!(10);
3442 assert!(tick.is_high_value_tick(dec!(500)));
3444 }
3445
3446 #[test]
3447 fn test_is_high_value_tick_false_when_below_threshold() {
3448 use rust_decimal_macros::dec;
3449 let mut tick = make_tick_at(0);
3450 tick.price = dec!(10);
3451 tick.quantity = dec!(2);
3452 assert!(!tick.is_high_value_tick(dec!(100)));
3454 }
3455
3456 #[test]
3459 fn test_is_buy_side_true_when_buy() {
3460 let mut tick = make_tick_at(0);
3461 tick.side = Some(TradeSide::Buy);
3462 assert!(tick.is_buy_side());
3463 }
3464
3465 #[test]
3466 fn test_is_buy_side_false_when_sell() {
3467 let mut tick = make_tick_at(0);
3468 tick.side = Some(TradeSide::Sell);
3469 assert!(!tick.is_buy_side());
3470 }
3471
3472 #[test]
3473 fn test_is_buy_side_false_when_none() {
3474 let mut tick = make_tick_at(0);
3475 tick.side = None;
3476 assert!(!tick.is_buy_side());
3477 }
3478
3479 #[test]
3480 fn test_is_sell_side_true_when_sell() {
3481 let mut tick = make_tick_at(0);
3482 tick.side = Some(TradeSide::Sell);
3483 assert!(tick.is_sell_side());
3484 }
3485
3486 #[test]
3487 fn test_price_in_range_true_when_within() {
3488 use rust_decimal_macros::dec;
3489 let mut tick = make_tick_at(0);
3490 tick.price = dec!(100);
3491 assert!(tick.price_in_range(dec!(90), dec!(110)));
3492 }
3493
3494 #[test]
3495 fn test_price_in_range_false_when_below() {
3496 use rust_decimal_macros::dec;
3497 let mut tick = make_tick_at(0);
3498 tick.price = dec!(80);
3499 assert!(!tick.price_in_range(dec!(90), dec!(110)));
3500 }
3501
3502 #[test]
3503 fn test_price_in_range_true_at_boundary() {
3504 use rust_decimal_macros::dec;
3505 let mut tick = make_tick_at(0);
3506 tick.price = dec!(90);
3507 assert!(tick.price_in_range(dec!(90), dec!(110)));
3508 }
3509
3510 #[test]
3513 fn test_is_zero_quantity_true_when_zero() {
3514 let mut tick = make_tick_at(0);
3515 tick.quantity = Decimal::ZERO;
3516 assert!(tick.is_zero_quantity());
3517 }
3518
3519 #[test]
3520 fn test_is_zero_quantity_false_when_nonzero() {
3521 let mut tick = make_tick_at(0);
3522 tick.quantity = Decimal::ONE;
3523 assert!(!tick.is_zero_quantity());
3524 }
3525
3526 #[test]
3529 fn test_is_large_tick_true_when_above_threshold() {
3530 let mut tick = make_tick_at(0);
3531 tick.quantity = Decimal::from(10u32);
3532 assert!(tick.is_large_tick(Decimal::from(5u32)));
3533 }
3534
3535 #[test]
3536 fn test_is_large_tick_false_when_at_threshold() {
3537 let mut tick = make_tick_at(0);
3538 tick.quantity = Decimal::from(5u32);
3539 assert!(!tick.is_large_tick(Decimal::from(5u32)));
3540 }
3541
3542 #[test]
3543 fn test_is_large_tick_false_when_below_threshold() {
3544 let mut tick = make_tick_at(0);
3545 tick.quantity = Decimal::from(1u32);
3546 assert!(!tick.is_large_tick(Decimal::from(5u32)));
3547 }
3548
3549 #[test]
3552 fn test_is_away_from_price_true_when_beyond_threshold() {
3553 let mut tick = make_tick_at(0);
3554 tick.price = Decimal::from(110u32);
3555 assert!(tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
3557 }
3558
3559 #[test]
3560 fn test_is_away_from_price_false_when_at_threshold() {
3561 let mut tick = make_tick_at(0);
3562 tick.price = Decimal::from(105u32);
3563 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
3565 }
3566
3567 #[test]
3568 fn test_is_away_from_price_false_when_equal() {
3569 let mut tick = make_tick_at(0);
3570 tick.price = Decimal::from(100u32);
3571 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(1u32)));
3572 }
3573
3574 #[test]
3577 fn test_is_within_spread_true_when_between() {
3578 let mut tick = make_tick_at(0);
3579 tick.price = Decimal::from(100u32);
3580 assert!(tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
3581 }
3582
3583 #[test]
3584 fn test_is_within_spread_false_when_at_bid() {
3585 let mut tick = make_tick_at(0);
3586 tick.price = Decimal::from(99u32);
3587 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
3588 }
3589
3590 #[test]
3591 fn test_is_within_spread_false_when_above_ask() {
3592 let mut tick = make_tick_at(0);
3593 tick.price = Decimal::from(102u32);
3594 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
3595 }
3596
3597 #[test]
3600 fn test_is_recent_true_when_within_threshold() {
3601 let tick = make_tick_at(9_500);
3602 assert!(tick.is_recent(1_000, 10_000));
3604 }
3605
3606 #[test]
3607 fn test_is_recent_false_when_beyond_threshold() {
3608 let tick = make_tick_at(8_000);
3609 assert!(!tick.is_recent(1_000, 10_000));
3611 }
3612
3613 #[test]
3614 fn test_is_recent_true_at_exact_threshold() {
3615 let tick = make_tick_at(9_000);
3616 assert!(tick.is_recent(1_000, 10_000));
3618 }
3619
3620 #[test]
3623 fn test_side_as_str_buy() {
3624 let mut tick = make_tick_at(0);
3625 tick.side = Some(TradeSide::Buy);
3626 assert_eq!(tick.side_as_str(), Some("buy"));
3627 }
3628
3629 #[test]
3630 fn test_side_as_str_sell() {
3631 let mut tick = make_tick_at(0);
3632 tick.side = Some(TradeSide::Sell);
3633 assert_eq!(tick.side_as_str(), Some("sell"));
3634 }
3635
3636 #[test]
3637 fn test_side_as_str_none_when_unknown() {
3638 let mut tick = make_tick_at(0);
3639 tick.side = None;
3640 assert!(tick.side_as_str().is_none());
3641 }
3642
3643 #[test]
3646 fn test_is_above_price_true_when_strictly_above() {
3647 let tick = make_tick_at(0); assert!(tick.is_above_price(rust_decimal_macros::dec!(99)));
3649 }
3650
3651 #[test]
3652 fn test_is_above_price_false_when_equal() {
3653 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(100)));
3655 }
3656
3657 #[test]
3658 fn test_is_above_price_false_when_below() {
3659 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(101)));
3661 }
3662
3663 #[test]
3666 fn test_price_change_from_positive_when_above_reference() {
3667 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(90)), rust_decimal_macros::dec!(10));
3669 }
3670
3671 #[test]
3672 fn test_price_change_from_negative_when_below_reference() {
3673 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(110)), rust_decimal_macros::dec!(-10));
3675 }
3676
3677 #[test]
3678 fn test_price_change_from_zero_when_equal() {
3679 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
3681 }
3682
3683 #[test]
3686 fn test_is_below_price_true_when_strictly_below() {
3687 let tick = make_tick_at(0); assert!(tick.is_below_price(rust_decimal_macros::dec!(101)));
3689 }
3690
3691 #[test]
3692 fn test_is_below_price_false_when_equal() {
3693 let tick = make_tick_at(0); assert!(!tick.is_below_price(rust_decimal_macros::dec!(100)));
3695 }
3696
3697 #[test]
3700 fn test_quantity_above_true_when_quantity_exceeds_threshold() {
3701 let tick = make_tick_at(0); assert!(tick.quantity_above(rust_decimal_macros::dec!(0)));
3703 }
3704
3705 #[test]
3706 fn test_quantity_above_false_when_quantity_equals_threshold() {
3707 let tick = make_tick_at(0); assert!(!tick.quantity_above(rust_decimal_macros::dec!(1)));
3709 }
3710
3711 #[test]
3714 fn test_is_at_price_true_when_equal() {
3715 let tick = make_tick_at(0); assert!(tick.is_at_price(rust_decimal_macros::dec!(100)));
3717 }
3718
3719 #[test]
3720 fn test_is_at_price_false_when_different() {
3721 let tick = make_tick_at(0); assert!(!tick.is_at_price(rust_decimal_macros::dec!(101)));
3723 }
3724
3725 #[test]
3728 fn test_is_round_number_true_when_divisible() {
3729 let tick = make_tick_at(0); assert!(tick.is_round_number(rust_decimal_macros::dec!(10)));
3731 assert!(tick.is_round_number(rust_decimal_macros::dec!(100)));
3732 }
3733
3734 #[test]
3735 fn test_is_round_number_false_when_not_divisible() {
3736 let tick = make_tick_at(0); assert!(!tick.is_round_number(rust_decimal_macros::dec!(3)));
3738 }
3739
3740 #[test]
3741 fn test_is_round_number_false_when_step_zero() {
3742 let tick = make_tick_at(0);
3743 assert!(!tick.is_round_number(rust_decimal_macros::dec!(0)));
3744 }
3745
3746 #[test]
3749 fn test_is_market_open_tick_true_when_within_session() {
3750 let tick = make_tick_at(500); assert!(tick.is_market_open_tick(100, 1_000));
3752 }
3753
3754 #[test]
3755 fn test_is_market_open_tick_false_when_before_session() {
3756 let tick = make_tick_at(50);
3757 assert!(!tick.is_market_open_tick(100, 1_000));
3758 }
3759
3760 #[test]
3761 fn test_is_market_open_tick_false_when_at_session_end() {
3762 let tick = make_tick_at(1_000);
3763 assert!(!tick.is_market_open_tick(100, 1_000)); }
3765
3766 #[test]
3769 fn test_signed_quantity_positive_for_buy() {
3770 let mut tick = make_tick_at(0);
3771 tick.side = Some(TradeSide::Buy);
3772 assert!(tick.signed_quantity() > rust_decimal::Decimal::ZERO);
3773 }
3774
3775 #[test]
3776 fn test_signed_quantity_negative_for_sell() {
3777 let mut tick = make_tick_at(0);
3778 tick.side = Some(TradeSide::Sell);
3779 assert!(tick.signed_quantity() < rust_decimal::Decimal::ZERO);
3780 }
3781
3782 #[test]
3783 fn test_signed_quantity_zero_for_unknown() {
3784 let tick = make_tick_at(0); assert_eq!(tick.signed_quantity(), rust_decimal::Decimal::ZERO);
3786 }
3787
3788 #[test]
3791 fn test_as_price_level_returns_price_and_quantity() {
3792 let tick = make_tick_at(0); let (p, q) = tick.as_price_level();
3794 assert_eq!(p, rust_decimal_macros::dec!(100));
3795 assert_eq!(q, rust_decimal_macros::dec!(1));
3796 }
3797
3798 fn make_sided_tick(qty: rust_decimal::Decimal, side: Option<TradeSide>) -> NormalizedTick {
3801 NormalizedTick {
3802 exchange: Exchange::Binance,
3803 symbol: "BTCUSDT".into(),
3804 price: rust_decimal_macros::dec!(100),
3805 quantity: qty,
3806 side,
3807 trade_id: None,
3808 exchange_ts_ms: None,
3809 received_at_ms: 0,
3810 }
3811 }
3812
3813 #[test]
3814 fn test_buy_volume_zero_for_empty_slice() {
3815 assert_eq!(NormalizedTick::buy_volume(&[]), rust_decimal::Decimal::ZERO);
3816 }
3817
3818 #[test]
3819 fn test_buy_volume_sums_only_buy_ticks() {
3820 let buy1 = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
3821 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
3822 let buy2 = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
3823 let unknown = make_sided_tick(rust_decimal_macros::dec!(10), None);
3824 assert_eq!(
3825 NormalizedTick::buy_volume(&[buy1, sell, buy2, unknown]),
3826 rust_decimal_macros::dec!(7)
3827 );
3828 }
3829
3830 #[test]
3831 fn test_sell_volume_zero_for_empty_slice() {
3832 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal::Decimal::ZERO);
3833 }
3834
3835 #[test]
3836 fn test_sell_volume_sums_only_sell_ticks() {
3837 let buy = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
3838 let sell1 = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
3839 let sell2 = make_sided_tick(rust_decimal_macros::dec!(4), Some(TradeSide::Sell));
3840 assert_eq!(
3841 NormalizedTick::sell_volume(&[buy, sell1, sell2]),
3842 rust_decimal_macros::dec!(7)
3843 );
3844 }
3845
3846 #[test]
3847 fn test_buy_sell_volumes_dont_include_unknown_side() {
3848 let buy = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
3849 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
3850 let unknown = make_sided_tick(rust_decimal_macros::dec!(2), None);
3851 let ticks = [buy, sell, unknown];
3852 let total: rust_decimal::Decimal = ticks.iter().map(|t| t.quantity).sum();
3853 let accounted = NormalizedTick::buy_volume(&ticks) + NormalizedTick::sell_volume(&ticks);
3854 assert_eq!(accounted, rust_decimal_macros::dec!(8));
3856 assert!(accounted < total);
3857 }
3858
3859 fn make_tick_with_price(price: rust_decimal::Decimal) -> NormalizedTick {
3862 NormalizedTick {
3863 exchange: Exchange::Binance,
3864 symbol: "BTCUSDT".into(),
3865 price,
3866 quantity: rust_decimal_macros::dec!(1),
3867 side: None,
3868 trade_id: None,
3869 exchange_ts_ms: None,
3870 received_at_ms: 0,
3871 }
3872 }
3873
3874 #[test]
3875 fn test_price_range_none_for_empty_slice() {
3876 assert!(NormalizedTick::price_range(&[]).is_none());
3877 }
3878
3879 #[test]
3880 fn test_price_range_zero_for_single_tick() {
3881 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
3882 assert_eq!(NormalizedTick::price_range(&[tick]), Some(rust_decimal_macros::dec!(0)));
3883 }
3884
3885 #[test]
3886 fn test_price_range_correct_for_multiple_ticks() {
3887 let t1 = make_tick_with_price(rust_decimal_macros::dec!(95));
3888 let t2 = make_tick_with_price(rust_decimal_macros::dec!(105));
3889 let t3 = make_tick_with_price(rust_decimal_macros::dec!(100));
3890 assert_eq!(NormalizedTick::price_range(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
3891 }
3892
3893 #[test]
3894 fn test_average_price_none_for_empty_slice() {
3895 assert!(NormalizedTick::average_price(&[]).is_none());
3896 }
3897
3898 #[test]
3899 fn test_average_price_equals_price_for_single_tick() {
3900 let tick = make_tick_with_price(rust_decimal_macros::dec!(200));
3901 assert_eq!(NormalizedTick::average_price(&[tick]), Some(rust_decimal_macros::dec!(200)));
3902 }
3903
3904 #[test]
3905 fn test_average_price_correct_for_multiple_ticks() {
3906 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
3907 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
3908 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
3909 assert_eq!(NormalizedTick::average_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
3911 }
3912
3913 fn make_tick_pq(price: rust_decimal::Decimal, qty: rust_decimal::Decimal) -> NormalizedTick {
3916 NormalizedTick {
3917 exchange: Exchange::Binance,
3918 symbol: "BTCUSDT".into(),
3919 price,
3920 quantity: qty,
3921 side: None,
3922 trade_id: None,
3923 exchange_ts_ms: None,
3924 received_at_ms: 0,
3925 }
3926 }
3927
3928 #[test]
3929 fn test_vwap_none_for_empty_slice() {
3930 assert!(NormalizedTick::vwap(&[]).is_none());
3931 }
3932
3933 #[test]
3934 fn test_vwap_equals_price_for_single_tick() {
3935 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
3936 assert_eq!(NormalizedTick::vwap(&[tick]), Some(rust_decimal_macros::dec!(100)));
3937 }
3938
3939 #[test]
3940 fn test_vwap_weighted_correctly() {
3941 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
3943 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
3944 assert_eq!(NormalizedTick::vwap(&[t1, t2]), Some(rust_decimal_macros::dec!(175)));
3945 }
3946
3947 #[test]
3948 fn test_vwap_none_for_zero_total_volume() {
3949 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(0));
3950 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(0));
3951 assert!(NormalizedTick::vwap(&[t1, t2]).is_none());
3952 }
3953
3954 #[test]
3957 fn test_count_above_price_zero_for_empty_slice() {
3958 assert_eq!(NormalizedTick::count_above_price(&[], rust_decimal_macros::dec!(100)), 0);
3959 }
3960
3961 #[test]
3962 fn test_count_above_price_correct() {
3963 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
3964 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
3965 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
3966 assert_eq!(NormalizedTick::count_above_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
3967 }
3968
3969 #[test]
3970 fn test_count_below_price_correct() {
3971 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
3972 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
3973 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
3974 assert_eq!(NormalizedTick::count_below_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
3975 }
3976
3977 #[test]
3978 fn test_count_above_at_threshold_excluded() {
3979 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
3980 assert_eq!(NormalizedTick::count_above_price(&[tick], rust_decimal_macros::dec!(100)), 0);
3981 }
3982
3983 #[test]
3984 fn test_count_below_at_threshold_excluded() {
3985 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
3986 assert_eq!(NormalizedTick::count_below_price(&[tick], rust_decimal_macros::dec!(100)), 0);
3987 }
3988
3989 #[test]
3992 fn test_total_notional_zero_for_empty_slice() {
3993 assert_eq!(NormalizedTick::total_notional(&[]), rust_decimal::Decimal::ZERO);
3994 }
3995
3996 #[test]
3997 fn test_total_notional_sums_all_ticks() {
3998 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4000 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
4001 assert_eq!(NormalizedTick::total_notional(&[t1, t2]), rust_decimal_macros::dec!(800));
4002 }
4003
4004 #[test]
4005 fn test_buy_notional_only_includes_buy_side() {
4006 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4007 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
4008 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
4009 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
4010 assert_eq!(NormalizedTick::buy_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(200));
4012 }
4013
4014 #[test]
4015 fn test_sell_notional_only_includes_sell_side() {
4016 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4017 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
4018 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
4019 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
4020 assert_eq!(NormalizedTick::sell_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(600));
4022 }
4023
4024 #[test]
4027 fn test_median_price_none_for_empty_slice() {
4028 assert!(NormalizedTick::median_price(&[]).is_none());
4029 }
4030
4031 #[test]
4032 fn test_median_price_single_tick() {
4033 let tick = make_tick_with_price(rust_decimal_macros::dec!(150));
4034 assert_eq!(NormalizedTick::median_price(&[tick]), Some(rust_decimal_macros::dec!(150)));
4035 }
4036
4037 #[test]
4038 fn test_median_price_odd_count() {
4039 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
4040 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
4041 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
4042 assert_eq!(NormalizedTick::median_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
4043 }
4044
4045 #[test]
4046 fn test_median_price_even_count() {
4047 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
4048 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
4049 assert_eq!(NormalizedTick::median_price(&[t1, t2]), Some(rust_decimal_macros::dec!(95)));
4051 }
4052
4053 #[test]
4056 fn test_net_volume_zero_for_empty_slice() {
4057 assert_eq!(NormalizedTick::net_volume(&[]), rust_decimal::Decimal::ZERO);
4058 }
4059
4060 #[test]
4061 fn test_net_volume_positive_when_more_buys() {
4062 let buy = NormalizedTick {
4063 side: Some(TradeSide::Buy),
4064 quantity: rust_decimal_macros::dec!(5),
4065 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5))
4066 };
4067 let sell = NormalizedTick {
4068 side: Some(TradeSide::Sell),
4069 quantity: rust_decimal_macros::dec!(3),
4070 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3))
4071 };
4072 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(2));
4073 }
4074
4075 #[test]
4076 fn test_net_volume_negative_when_more_sells() {
4077 let buy = NormalizedTick {
4078 side: Some(TradeSide::Buy),
4079 quantity: rust_decimal_macros::dec!(2),
4080 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2))
4081 };
4082 let sell = NormalizedTick {
4083 side: Some(TradeSide::Sell),
4084 quantity: rust_decimal_macros::dec!(7),
4085 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(7))
4086 };
4087 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(-5));
4088 }
4089
4090 #[test]
4093 fn test_average_quantity_none_for_empty_slice() {
4094 assert!(NormalizedTick::average_quantity(&[]).is_none());
4095 }
4096
4097 #[test]
4098 fn test_average_quantity_single_tick() {
4099 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
4100 assert_eq!(NormalizedTick::average_quantity(&[tick]), Some(rust_decimal_macros::dec!(5)));
4101 }
4102
4103 #[test]
4104 fn test_average_quantity_multiple_ticks() {
4105 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4106 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(4));
4107 assert_eq!(NormalizedTick::average_quantity(&[t1, t2]), Some(rust_decimal_macros::dec!(3)));
4109 }
4110
4111 #[test]
4112 fn test_max_quantity_none_for_empty_slice() {
4113 assert!(NormalizedTick::max_quantity(&[]).is_none());
4114 }
4115
4116 #[test]
4117 fn test_max_quantity_returns_largest() {
4118 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4119 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(10));
4120 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
4121 assert_eq!(NormalizedTick::max_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
4122 }
4123
4124 #[test]
4125 fn test_min_quantity_none_for_empty_slice() {
4126 assert!(NormalizedTick::min_quantity(&[]).is_none());
4127 }
4128
4129 #[test]
4130 fn test_min_quantity_returns_smallest() {
4131 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
4132 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4133 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3));
4134 assert_eq!(NormalizedTick::min_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(1)));
4135 }
4136
4137 #[test]
4138 fn test_buy_count_zero_for_empty_slice() {
4139 assert_eq!(NormalizedTick::buy_count(&[]), 0);
4140 }
4141
4142 #[test]
4143 fn test_buy_count_counts_only_buys() {
4144 use rust_decimal_macros::dec;
4145 let mut buy = make_tick_pq(dec!(100), dec!(1));
4146 buy.side = Some(TradeSide::Buy);
4147 let mut sell = make_tick_pq(dec!(100), dec!(1));
4148 sell.side = Some(TradeSide::Sell);
4149 let neutral = make_tick_pq(dec!(100), dec!(1));
4150 assert_eq!(NormalizedTick::buy_count(&[buy, sell, neutral]), 1);
4151 }
4152
4153 #[test]
4154 fn test_sell_count_zero_for_empty_slice() {
4155 assert_eq!(NormalizedTick::sell_count(&[]), 0);
4156 }
4157
4158 #[test]
4159 fn test_sell_count_counts_only_sells() {
4160 use rust_decimal_macros::dec;
4161 let mut buy = make_tick_pq(dec!(100), dec!(1));
4162 buy.side = Some(TradeSide::Buy);
4163 let mut sell1 = make_tick_pq(dec!(100), dec!(1));
4164 sell1.side = Some(TradeSide::Sell);
4165 let mut sell2 = make_tick_pq(dec!(100), dec!(1));
4166 sell2.side = Some(TradeSide::Sell);
4167 assert_eq!(NormalizedTick::sell_count(&[buy, sell1, sell2]), 2);
4168 }
4169
4170 #[test]
4171 fn test_price_momentum_none_for_empty_slice() {
4172 assert!(NormalizedTick::price_momentum(&[]).is_none());
4173 }
4174
4175 #[test]
4176 fn test_price_momentum_none_for_single_tick() {
4177 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4178 assert!(NormalizedTick::price_momentum(&[t]).is_none());
4179 }
4180
4181 #[test]
4182 fn test_price_momentum_positive_when_price_rises() {
4183 use rust_decimal_macros::dec;
4184 let t1 = make_tick_pq(dec!(100), dec!(1));
4185 let t2 = make_tick_pq(dec!(110), dec!(1));
4186 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
4187 assert!((mom - 0.1).abs() < 1e-9);
4188 }
4189
4190 #[test]
4191 fn test_price_momentum_negative_when_price_falls() {
4192 use rust_decimal_macros::dec;
4193 let t1 = make_tick_pq(dec!(100), dec!(1));
4194 let t2 = make_tick_pq(dec!(90), dec!(1));
4195 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
4196 assert!(mom < 0.0);
4197 }
4198
4199 #[test]
4200 fn test_min_price_none_for_empty_slice() {
4201 assert!(NormalizedTick::min_price(&[]).is_none());
4202 }
4203
4204 #[test]
4205 fn test_min_price_returns_lowest() {
4206 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4207 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
4208 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
4209 assert_eq!(NormalizedTick::min_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(90)));
4210 }
4211
4212 #[test]
4213 fn test_max_price_none_for_empty_slice() {
4214 assert!(NormalizedTick::max_price(&[]).is_none());
4215 }
4216
4217 #[test]
4218 fn test_max_price_returns_highest() {
4219 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4220 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
4221 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
4222 assert_eq!(NormalizedTick::max_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(110)));
4223 }
4224
4225 #[test]
4226 fn test_price_std_dev_none_for_empty_slice() {
4227 assert!(NormalizedTick::price_std_dev(&[]).is_none());
4228 }
4229
4230 #[test]
4231 fn test_price_std_dev_none_for_single_tick() {
4232 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4233 assert!(NormalizedTick::price_std_dev(&[t]).is_none());
4234 }
4235
4236 #[test]
4237 fn test_price_std_dev_two_equal_prices_is_zero() {
4238 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4239 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4240 assert_eq!(NormalizedTick::price_std_dev(&[t1, t2]), Some(0.0));
4241 }
4242
4243 #[test]
4244 fn test_price_std_dev_positive_for_varying_prices() {
4245 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4246 let t2 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
4247 let t3 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
4248 let std = NormalizedTick::price_std_dev(&[t1, t2, t3]).unwrap();
4249 assert!(std > 0.0);
4250 }
4251
4252 #[test]
4253 fn test_buy_sell_ratio_none_for_empty_slice() {
4254 assert!(NormalizedTick::buy_sell_ratio(&[]).is_none());
4255 }
4256
4257 #[test]
4258 fn test_buy_sell_ratio_none_when_no_sells() {
4259 let mut t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4260 t.side = Some(TradeSide::Buy);
4261 assert!(NormalizedTick::buy_sell_ratio(&[t]).is_none());
4262 }
4263
4264 #[test]
4265 fn test_buy_sell_ratio_two_to_one() {
4266 use rust_decimal_macros::dec;
4267 let mut buy1 = make_tick_pq(dec!(100), dec!(2));
4268 buy1.side = Some(TradeSide::Buy);
4269 let mut buy2 = make_tick_pq(dec!(100), dec!(2));
4270 buy2.side = Some(TradeSide::Buy);
4271 let mut sell = make_tick_pq(dec!(100), dec!(2));
4272 sell.side = Some(TradeSide::Sell);
4273 let ratio = NormalizedTick::buy_sell_ratio(&[buy1, buy2, sell]).unwrap();
4274 assert!((ratio - 2.0).abs() < 1e-9);
4275 }
4276
4277 #[test]
4278 fn test_largest_trade_none_for_empty_slice() {
4279 assert!(NormalizedTick::largest_trade(&[]).is_none());
4280 }
4281
4282 #[test]
4283 fn test_largest_trade_returns_max_quantity_tick() {
4284 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
4285 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(10));
4286 let t3 = make_tick_pq(rust_decimal_macros::dec!(150), rust_decimal_macros::dec!(5));
4287 let ticks = [t1, t2, t3];
4288 let largest = NormalizedTick::largest_trade(&ticks).unwrap();
4289 assert_eq!(largest.quantity, rust_decimal_macros::dec!(10));
4290 }
4291
4292 #[test]
4293 fn test_large_trade_count_zero_for_empty_slice() {
4294 assert_eq!(NormalizedTick::large_trade_count(&[], rust_decimal_macros::dec!(1)), 0);
4295 }
4296
4297 #[test]
4298 fn test_large_trade_count_counts_trades_above_threshold() {
4299 use rust_decimal_macros::dec;
4300 let t1 = make_tick_pq(dec!(100), dec!(0.5));
4301 let t2 = make_tick_pq(dec!(100), dec!(5));
4302 let t3 = make_tick_pq(dec!(100), dec!(10));
4303 assert_eq!(NormalizedTick::large_trade_count(&[t1, t2, t3], dec!(1)), 2);
4304 }
4305
4306 #[test]
4307 fn test_large_trade_count_strict_greater_than() {
4308 use rust_decimal_macros::dec;
4309 let t = make_tick_pq(dec!(100), dec!(1));
4310 assert_eq!(NormalizedTick::large_trade_count(&[t], dec!(1)), 0);
4312 }
4313
4314 #[test]
4315 fn test_price_iqr_none_for_small_slice() {
4316 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4317 assert!(NormalizedTick::price_iqr(&[t.clone(), t.clone(), t]).is_none());
4318 }
4319
4320 #[test]
4321 fn test_price_iqr_positive_for_varied_prices() {
4322 use rust_decimal_macros::dec;
4323 let ticks: Vec<_> = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)]
4324 .iter()
4325 .map(|&p| make_tick_pq(p, dec!(1)))
4326 .collect();
4327 let iqr = NormalizedTick::price_iqr(&ticks).unwrap();
4328 assert!(iqr > dec!(0));
4329 }
4330
4331 #[test]
4332 fn test_fraction_buy_none_for_empty_slice() {
4333 assert!(NormalizedTick::fraction_buy(&[]).is_none());
4334 }
4335
4336 #[test]
4337 fn test_fraction_buy_zero_when_no_buys() {
4338 use rust_decimal_macros::dec;
4339 let mut t = make_tick_pq(dec!(100), dec!(1));
4340 t.side = Some(TradeSide::Sell);
4341 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(0.0));
4342 }
4343
4344 #[test]
4345 fn test_fraction_buy_one_when_all_buys() {
4346 use rust_decimal_macros::dec;
4347 let mut t = make_tick_pq(dec!(100), dec!(1));
4348 t.side = Some(TradeSide::Buy);
4349 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(1.0));
4350 }
4351
4352 #[test]
4353 fn test_fraction_buy_half_for_equal_mix() {
4354 use rust_decimal_macros::dec;
4355 let mut buy = make_tick_pq(dec!(100), dec!(1));
4356 buy.side = Some(TradeSide::Buy);
4357 let mut sell = make_tick_pq(dec!(100), dec!(1));
4358 sell.side = Some(TradeSide::Sell);
4359 let frac = NormalizedTick::fraction_buy(&[buy, sell]).unwrap();
4360 assert!((frac - 0.5).abs() < 1e-9);
4361 }
4362
4363 #[test]
4364 fn test_std_quantity_none_for_empty_slice() {
4365 assert!(NormalizedTick::std_quantity(&[]).is_none());
4366 }
4367
4368 #[test]
4369 fn test_std_quantity_none_for_single_tick() {
4370 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
4371 assert!(NormalizedTick::std_quantity(&[t]).is_none());
4372 }
4373
4374 #[test]
4375 fn test_std_quantity_zero_for_identical_quantities() {
4376 use rust_decimal_macros::dec;
4377 let t1 = make_tick_pq(dec!(100), dec!(5));
4378 let t2 = make_tick_pq(dec!(100), dec!(5));
4379 assert_eq!(NormalizedTick::std_quantity(&[t1, t2]), Some(0.0));
4380 }
4381
4382 #[test]
4383 fn test_std_quantity_positive_for_varied_quantities() {
4384 use rust_decimal_macros::dec;
4385 let t1 = make_tick_pq(dec!(100), dec!(1));
4386 let t2 = make_tick_pq(dec!(100), dec!(10));
4387 let std = NormalizedTick::std_quantity(&[t1, t2]).unwrap();
4388 assert!(std > 0.0);
4389 }
4390
4391 #[test]
4392 fn test_buy_pressure_none_for_empty_slice() {
4393 assert!(NormalizedTick::buy_pressure(&[]).is_none());
4394 }
4395
4396 #[test]
4397 fn test_buy_pressure_none_for_unsided_ticks() {
4398 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4399 assert!(NormalizedTick::buy_pressure(&[t]).is_none());
4400 }
4401
4402 #[test]
4403 fn test_buy_pressure_one_for_all_buys() {
4404 use rust_decimal_macros::dec;
4405 let mut t = make_tick_pq(dec!(100), dec!(1));
4406 t.side = Some(TradeSide::Buy);
4407 let bp = NormalizedTick::buy_pressure(&[t]).unwrap();
4408 assert!((bp - 1.0).abs() < 1e-9);
4409 }
4410
4411 #[test]
4412 fn test_buy_pressure_half_for_equal_volume() {
4413 use rust_decimal_macros::dec;
4414 let mut buy = make_tick_pq(dec!(100), dec!(5));
4415 buy.side = Some(TradeSide::Buy);
4416 let mut sell = make_tick_pq(dec!(100), dec!(5));
4417 sell.side = Some(TradeSide::Sell);
4418 let bp = NormalizedTick::buy_pressure(&[buy, sell]).unwrap();
4419 assert!((bp - 0.5).abs() < 1e-9);
4420 }
4421
4422 #[test]
4423 fn test_average_notional_none_for_empty_slice() {
4424 assert!(NormalizedTick::average_notional(&[]).is_none());
4425 }
4426
4427 #[test]
4428 fn test_average_notional_single_tick() {
4429 use rust_decimal_macros::dec;
4430 let t = make_tick_pq(dec!(100), dec!(2));
4431 assert_eq!(NormalizedTick::average_notional(&[t]), Some(dec!(200)));
4432 }
4433
4434 #[test]
4435 fn test_average_notional_multiple_ticks() {
4436 use rust_decimal_macros::dec;
4437 let t1 = make_tick_pq(dec!(100), dec!(1)); let t2 = make_tick_pq(dec!(200), dec!(1)); assert_eq!(NormalizedTick::average_notional(&[t1, t2]), Some(dec!(150)));
4441 }
4442
4443 #[test]
4444 fn test_count_neutral_zero_for_empty_slice() {
4445 assert_eq!(NormalizedTick::count_neutral(&[]), 0);
4446 }
4447
4448 #[test]
4449 fn test_count_neutral_counts_sideless_ticks() {
4450 use rust_decimal_macros::dec;
4451 let neutral = make_tick_pq(dec!(100), dec!(1)); let mut buy = make_tick_pq(dec!(100), dec!(1));
4453 buy.side = Some(TradeSide::Buy);
4454 assert_eq!(NormalizedTick::count_neutral(&[neutral, buy]), 1);
4455 }
4456
4457 #[test]
4458 fn test_recent_returns_all_when_n_exceeds_len() {
4459 use rust_decimal_macros::dec;
4460 let ticks = vec![
4461 make_tick_pq(dec!(100), dec!(1)),
4462 make_tick_pq(dec!(110), dec!(1)),
4463 ];
4464 assert_eq!(NormalizedTick::recent(&ticks, 10).len(), 2);
4465 }
4466
4467 #[test]
4468 fn test_recent_returns_last_n() {
4469 use rust_decimal_macros::dec;
4470 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120), dec!(130)]
4471 .iter()
4472 .map(|&p| make_tick_pq(p, dec!(1)))
4473 .collect();
4474 let recent = NormalizedTick::recent(&ticks, 2);
4475 assert_eq!(recent.len(), 2);
4476 assert_eq!(recent[0].price, dec!(120));
4477 assert_eq!(recent[1].price, dec!(130));
4478 }
4479
4480 #[test]
4481 fn test_price_linear_slope_none_for_single_tick() {
4482 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4483 assert!(NormalizedTick::price_linear_slope(&[t]).is_none());
4484 }
4485
4486 #[test]
4487 fn test_price_linear_slope_positive_for_rising_prices() {
4488 use rust_decimal_macros::dec;
4489 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120)]
4490 .iter()
4491 .map(|&p| make_tick_pq(p, dec!(1)))
4492 .collect();
4493 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
4494 assert!(slope > 0.0);
4495 }
4496
4497 #[test]
4498 fn test_price_linear_slope_negative_for_falling_prices() {
4499 use rust_decimal_macros::dec;
4500 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(100)]
4501 .iter()
4502 .map(|&p| make_tick_pq(p, dec!(1)))
4503 .collect();
4504 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
4505 assert!(slope < 0.0);
4506 }
4507
4508 #[test]
4509 fn test_notional_std_dev_none_for_single_tick() {
4510 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
4511 assert!(NormalizedTick::notional_std_dev(&[t]).is_none());
4512 }
4513
4514 #[test]
4515 fn test_notional_std_dev_zero_for_identical_notionals() {
4516 use rust_decimal_macros::dec;
4517 let t1 = make_tick_pq(dec!(100), dec!(1)); let t2 = make_tick_pq(dec!(100), dec!(1)); assert_eq!(NormalizedTick::notional_std_dev(&[t1, t2]), Some(0.0));
4520 }
4521
4522 #[test]
4523 fn test_notional_std_dev_positive_for_varied_notionals() {
4524 use rust_decimal_macros::dec;
4525 let t1 = make_tick_pq(dec!(100), dec!(1)); let t2 = make_tick_pq(dec!(200), dec!(2)); let std = NormalizedTick::notional_std_dev(&[t1, t2]).unwrap();
4528 assert!(std > 0.0);
4529 }
4530
4531 #[test]
4532 fn test_monotone_up_true_for_empty_slice() {
4533 assert!(NormalizedTick::monotone_up(&[]));
4534 }
4535
4536 #[test]
4537 fn test_monotone_up_true_for_non_decreasing_prices() {
4538 use rust_decimal_macros::dec;
4539 let ticks: Vec<_> = [dec!(100), dec!(100), dec!(110), dec!(120)]
4540 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
4541 assert!(NormalizedTick::monotone_up(&ticks));
4542 }
4543
4544 #[test]
4545 fn test_monotone_up_false_for_any_decrease() {
4546 use rust_decimal_macros::dec;
4547 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(105)]
4548 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
4549 assert!(!NormalizedTick::monotone_up(&ticks));
4550 }
4551
4552 #[test]
4553 fn test_monotone_down_true_for_non_increasing_prices() {
4554 use rust_decimal_macros::dec;
4555 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(110), dec!(100)]
4556 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
4557 assert!(NormalizedTick::monotone_down(&ticks));
4558 }
4559
4560 #[test]
4561 fn test_monotone_down_false_for_any_increase() {
4562 use rust_decimal_macros::dec;
4563 let ticks: Vec<_> = [dec!(100), dec!(90), dec!(95)]
4564 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
4565 assert!(!NormalizedTick::monotone_down(&ticks));
4566 }
4567
4568 #[test]
4569 fn test_volume_at_price_zero_for_empty_slice() {
4570 assert_eq!(NormalizedTick::volume_at_price(&[], rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
4571 }
4572
4573 #[test]
4574 fn test_volume_at_price_sums_matching_ticks() {
4575 use rust_decimal_macros::dec;
4576 let t1 = make_tick_pq(dec!(100), dec!(2));
4577 let t2 = make_tick_pq(dec!(100), dec!(3));
4578 let t3 = make_tick_pq(dec!(110), dec!(5));
4579 assert_eq!(NormalizedTick::volume_at_price(&[t1, t2, t3], dec!(100)), dec!(5));
4580 }
4581
4582 #[test]
4583 fn test_last_price_none_for_empty_slice() {
4584 assert!(NormalizedTick::last_price(&[]).is_none());
4585 }
4586
4587 #[test]
4588 fn test_last_price_returns_last_tick_price() {
4589 use rust_decimal_macros::dec;
4590 let t1 = make_tick_pq(dec!(100), dec!(1));
4591 let t2 = make_tick_pq(dec!(110), dec!(1));
4592 assert_eq!(NormalizedTick::last_price(&[t1, t2]), Some(dec!(110)));
4593 }
4594
4595 #[test]
4596 fn test_longest_buy_streak_zero_for_empty() {
4597 assert_eq!(NormalizedTick::longest_buy_streak(&[]), 0);
4598 }
4599
4600 #[test]
4601 fn test_longest_buy_streak_counts_consecutive_buys() {
4602 use rust_decimal_macros::dec;
4603 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
4604 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
4605 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
4606 let mut b3 = make_tick_pq(dec!(100), dec!(1)); b3.side = Some(TradeSide::Buy);
4607 assert_eq!(NormalizedTick::longest_buy_streak(&[b1, b2, s, b3]), 2);
4609 }
4610
4611 #[test]
4612 fn test_longest_sell_streak_zero_for_no_sells() {
4613 use rust_decimal_macros::dec;
4614 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
4615 assert_eq!(NormalizedTick::longest_sell_streak(&[b]), 0);
4616 }
4617
4618 #[test]
4619 fn test_longest_sell_streak_correct() {
4620 use rust_decimal_macros::dec;
4621 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
4622 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
4623 let mut s2 = make_tick_pq(dec!(100), dec!(1)); s2.side = Some(TradeSide::Sell);
4624 let mut s3 = make_tick_pq(dec!(100), dec!(1)); s3.side = Some(TradeSide::Sell);
4625 assert_eq!(NormalizedTick::longest_sell_streak(&[b, s1, s2, s3]), 3);
4626 }
4627
4628 #[test]
4629 fn test_price_at_max_volume_none_for_empty() {
4630 assert!(NormalizedTick::price_at_max_volume(&[]).is_none());
4631 }
4632
4633 #[test]
4634 fn test_price_at_max_volume_returns_dominant_price() {
4635 use rust_decimal_macros::dec;
4636 let t1 = make_tick_pq(dec!(100), dec!(1));
4637 let t2 = make_tick_pq(dec!(200), dec!(5));
4638 let t3 = make_tick_pq(dec!(200), dec!(3));
4639 assert_eq!(NormalizedTick::price_at_max_volume(&[t1, t2, t3]), Some(dec!(200)));
4641 }
4642
4643 #[test]
4644 fn test_recent_volume_zero_for_empty_slice() {
4645 assert_eq!(NormalizedTick::recent_volume(&[], 5), rust_decimal_macros::dec!(0));
4646 }
4647
4648 #[test]
4649 fn test_recent_volume_sums_last_n_ticks() {
4650 use rust_decimal_macros::dec;
4651 let ticks: Vec<_> = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)]
4652 .iter().map(|&q| make_tick_pq(dec!(100), q)).collect();
4653 assert_eq!(NormalizedTick::recent_volume(&ticks, 3), dec!(12));
4655 }
4656
4657 #[test]
4660 fn test_first_price_none_for_empty_slice() {
4661 assert!(NormalizedTick::first_price(&[]).is_none());
4662 }
4663
4664 #[test]
4665 fn test_first_price_returns_first_tick_price() {
4666 use rust_decimal_macros::dec;
4667 let ticks = vec![make_tick_pq(dec!(50), dec!(1)), make_tick_pq(dec!(60), dec!(1))];
4668 assert_eq!(NormalizedTick::first_price(&ticks), Some(dec!(50)));
4669 }
4670
4671 #[test]
4674 fn test_price_return_pct_none_for_single_tick() {
4675 use rust_decimal_macros::dec;
4676 assert!(NormalizedTick::price_return_pct(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
4677 }
4678
4679 #[test]
4680 fn test_price_return_pct_positive_for_rising_price() {
4681 use rust_decimal_macros::dec;
4682 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(110), dec!(1))];
4683 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
4684 assert!((pct - 0.1).abs() < 1e-9);
4685 }
4686
4687 #[test]
4688 fn test_price_return_pct_negative_for_falling_price() {
4689 use rust_decimal_macros::dec;
4690 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(90), dec!(1))];
4691 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
4692 assert!((pct - (-0.1)).abs() < 1e-9);
4693 }
4694
4695 #[test]
4698 fn test_volume_above_price_zero_for_empty_slice() {
4699 use rust_decimal_macros::dec;
4700 assert_eq!(NormalizedTick::volume_above_price(&[], dec!(100)), dec!(0));
4701 }
4702
4703 #[test]
4704 fn test_volume_above_price_sums_above_threshold() {
4705 use rust_decimal_macros::dec;
4706 let ticks = vec![
4707 make_tick_pq(dec!(90), dec!(5)),
4708 make_tick_pq(dec!(100), dec!(10)),
4709 make_tick_pq(dec!(110), dec!(3)),
4710 ];
4711 assert_eq!(NormalizedTick::volume_above_price(&ticks, dec!(100)), dec!(3));
4713 }
4714
4715 #[test]
4716 fn test_volume_below_price_sums_below_threshold() {
4717 use rust_decimal_macros::dec;
4718 let ticks = vec![
4719 make_tick_pq(dec!(90), dec!(5)),
4720 make_tick_pq(dec!(100), dec!(10)),
4721 make_tick_pq(dec!(110), dec!(3)),
4722 ];
4723 assert_eq!(NormalizedTick::volume_below_price(&ticks, dec!(100)), dec!(5));
4725 }
4726
4727 #[test]
4730 fn test_qwap_none_for_empty_slice() {
4731 assert!(NormalizedTick::quantity_weighted_avg_price(&[]).is_none());
4732 }
4733
4734 #[test]
4735 fn test_qwap_correct_for_equal_quantities() {
4736 use rust_decimal_macros::dec;
4737 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(1))];
4739 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(150)));
4740 }
4741
4742 #[test]
4743 fn test_qwap_weighted_towards_higher_volume() {
4744 use rust_decimal_macros::dec;
4745 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(3))];
4747 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(175)));
4748 }
4749
4750 #[test]
4753 fn test_tick_count_above_price_zero_for_empty_slice() {
4754 use rust_decimal_macros::dec;
4755 assert_eq!(NormalizedTick::tick_count_above_price(&[], dec!(100)), 0);
4756 }
4757
4758 #[test]
4759 fn test_tick_count_above_price_correct() {
4760 use rust_decimal_macros::dec;
4761 let ticks = vec![
4762 make_tick_pq(dec!(90), dec!(1)),
4763 make_tick_pq(dec!(100), dec!(1)),
4764 make_tick_pq(dec!(110), dec!(1)),
4765 make_tick_pq(dec!(120), dec!(1)),
4766 ];
4767 assert_eq!(NormalizedTick::tick_count_above_price(&ticks, dec!(100)), 2);
4768 }
4769
4770 #[test]
4771 fn test_tick_count_below_price_correct() {
4772 use rust_decimal_macros::dec;
4773 let ticks = vec![
4774 make_tick_pq(dec!(90), dec!(1)),
4775 make_tick_pq(dec!(100), dec!(1)),
4776 make_tick_pq(dec!(110), dec!(1)),
4777 ];
4778 assert_eq!(NormalizedTick::tick_count_below_price(&ticks, dec!(100)), 1);
4779 }
4780
4781 #[test]
4784 fn test_price_at_percentile_none_for_empty_slice() {
4785 use rust_decimal_macros::dec;
4786 assert!(NormalizedTick::price_at_percentile(&[], 0.5).is_none());
4787 }
4788
4789 #[test]
4790 fn test_price_at_percentile_none_for_out_of_range() {
4791 use rust_decimal_macros::dec;
4792 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
4793 assert!(NormalizedTick::price_at_percentile(&ticks, 1.5).is_none());
4794 }
4795
4796 #[test]
4797 fn test_price_at_percentile_median_for_sorted_prices() {
4798 use rust_decimal_macros::dec;
4799 let ticks = vec![
4800 make_tick_pq(dec!(10), dec!(1)),
4801 make_tick_pq(dec!(20), dec!(1)),
4802 make_tick_pq(dec!(30), dec!(1)),
4803 make_tick_pq(dec!(40), dec!(1)),
4804 make_tick_pq(dec!(50), dec!(1)),
4805 ];
4806 assert_eq!(NormalizedTick::price_at_percentile(&ticks, 0.5), Some(dec!(30)));
4808 }
4809
4810 #[test]
4813 fn test_unique_price_count_zero_for_empty() {
4814 assert_eq!(NormalizedTick::unique_price_count(&[]), 0);
4815 }
4816
4817 #[test]
4818 fn test_unique_price_count_counts_distinct_prices() {
4819 use rust_decimal_macros::dec;
4820 let ticks = vec![
4821 make_tick_pq(dec!(100), dec!(1)),
4822 make_tick_pq(dec!(100), dec!(2)),
4823 make_tick_pq(dec!(110), dec!(1)),
4824 make_tick_pq(dec!(120), dec!(1)),
4825 ];
4826 assert_eq!(NormalizedTick::unique_price_count(&ticks), 3);
4827 }
4828
4829 #[test]
4832 fn test_sell_volume_zero_for_empty() {
4833 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal_macros::dec!(0));
4834 }
4835
4836 #[test]
4837 fn test_sell_volume_sums_sell_side_only() {
4838 use rust_decimal_macros::dec;
4839 let mut buy_tick = make_tick_pq(dec!(100), dec!(5));
4840 buy_tick.side = Some(TradeSide::Buy);
4841 let mut sell_tick = make_tick_pq(dec!(100), dec!(3));
4842 sell_tick.side = Some(TradeSide::Sell);
4843 let no_side_tick = make_tick_pq(dec!(100), dec!(10));
4844 let ticks = [buy_tick, sell_tick, no_side_tick];
4845 assert_eq!(NormalizedTick::sell_volume(&ticks), dec!(3));
4846 assert_eq!(NormalizedTick::buy_volume(&ticks), dec!(5));
4847 }
4848
4849 #[test]
4852 fn test_avg_inter_tick_spread_none_for_single_tick() {
4853 use rust_decimal_macros::dec;
4854 assert!(NormalizedTick::avg_inter_tick_spread(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
4855 }
4856
4857 #[test]
4858 fn test_avg_inter_tick_spread_correct_for_uniform_moves() {
4859 use rust_decimal_macros::dec;
4860 let ticks = vec![
4862 make_tick_pq(dec!(100), dec!(1)),
4863 make_tick_pq(dec!(102), dec!(1)),
4864 make_tick_pq(dec!(104), dec!(1)),
4865 ];
4866 let spread = NormalizedTick::avg_inter_tick_spread(&ticks).unwrap();
4867 assert!((spread - 2.0).abs() < 1e-9);
4868 }
4869
4870 #[test]
4873 fn test_price_range_none_for_empty() {
4874 assert!(NormalizedTick::price_range(&[]).is_none());
4875 }
4876
4877 #[test]
4878 fn test_price_range_correct() {
4879 use rust_decimal_macros::dec;
4880 let ticks = vec![
4881 make_tick_pq(dec!(90), dec!(1)),
4882 make_tick_pq(dec!(110), dec!(1)),
4883 make_tick_pq(dec!(100), dec!(1)),
4884 ];
4885 assert_eq!(NormalizedTick::price_range(&ticks), Some(dec!(20)));
4886 }
4887
4888 #[test]
4891 fn test_median_price_none_for_empty() {
4892 assert!(NormalizedTick::median_price(&[]).is_none());
4893 }
4894
4895 #[test]
4896 fn test_median_price_returns_middle_value() {
4897 use rust_decimal_macros::dec;
4898 let ticks = vec![
4899 make_tick_pq(dec!(10), dec!(1)),
4900 make_tick_pq(dec!(30), dec!(1)),
4901 make_tick_pq(dec!(20), dec!(1)),
4902 ];
4903 assert_eq!(NormalizedTick::median_price(&ticks), Some(dec!(20)));
4905 }
4906
4907 #[test]
4910 fn test_largest_sell_none_for_no_sell_ticks() {
4911 use rust_decimal_macros::dec;
4912 let mut t = make_tick_pq(dec!(100), dec!(5));
4913 t.side = Some(TradeSide::Buy);
4914 assert!(NormalizedTick::largest_sell(&[t]).is_none());
4915 }
4916
4917 #[test]
4918 fn test_largest_sell_returns_max_sell_qty() {
4919 use rust_decimal_macros::dec;
4920 let mut t1 = make_tick_pq(dec!(100), dec!(3));
4921 t1.side = Some(TradeSide::Sell);
4922 let mut t2 = make_tick_pq(dec!(100), dec!(7));
4923 t2.side = Some(TradeSide::Sell);
4924 assert_eq!(NormalizedTick::largest_sell(&[t1, t2]), Some(dec!(7)));
4925 }
4926
4927 #[test]
4928 fn test_largest_buy_returns_max_buy_qty() {
4929 use rust_decimal_macros::dec;
4930 let mut t1 = make_tick_pq(dec!(100), dec!(2));
4931 t1.side = Some(TradeSide::Buy);
4932 let mut t2 = make_tick_pq(dec!(100), dec!(9));
4933 t2.side = Some(TradeSide::Buy);
4934 assert_eq!(NormalizedTick::largest_buy(&[t1, t2]), Some(dec!(9)));
4935 }
4936
4937 #[test]
4940 fn test_trade_count_zero_for_empty() {
4941 assert_eq!(NormalizedTick::trade_count(&[]), 0);
4942 }
4943
4944 #[test]
4945 fn test_trade_count_matches_slice_length() {
4946 use rust_decimal_macros::dec;
4947 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
4948 assert_eq!(NormalizedTick::trade_count(&ticks), 2);
4949 }
4950
4951 #[test]
4954 fn test_price_acceleration_none_for_fewer_than_3() {
4955 use rust_decimal_macros::dec;
4956 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
4957 assert!(NormalizedTick::price_acceleration(&ticks).is_none());
4958 }
4959
4960 #[test]
4961 fn test_price_acceleration_zero_for_constant_velocity() {
4962 use rust_decimal_macros::dec;
4963 let ticks = vec![
4965 make_tick_pq(dec!(100), dec!(1)),
4966 make_tick_pq(dec!(102), dec!(1)),
4967 make_tick_pq(dec!(104), dec!(1)),
4968 ];
4969 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
4970 assert!((acc - 0.0).abs() < 1e-9);
4971 }
4972
4973 #[test]
4974 fn test_price_acceleration_positive_when_speeding_up() {
4975 use rust_decimal_macros::dec;
4976 let ticks = vec![
4978 make_tick_pq(dec!(100), dec!(1)),
4979 make_tick_pq(dec!(101), dec!(1)),
4980 make_tick_pq(dec!(103), dec!(1)),
4981 ];
4982 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
4983 assert!((acc - 1.0).abs() < 1e-9);
4984 }
4985
4986 #[test]
4989 fn test_buy_sell_diff_zero_for_empty() {
4990 assert_eq!(NormalizedTick::buy_sell_diff(&[]), rust_decimal_macros::dec!(0));
4991 }
4992
4993 #[test]
4994 fn test_buy_sell_diff_positive_for_net_buying() {
4995 use rust_decimal_macros::dec;
4996 let mut t1 = make_tick_pq(dec!(100), dec!(10));
4997 t1.side = Some(TradeSide::Buy);
4998 let mut t2 = make_tick_pq(dec!(100), dec!(3));
4999 t2.side = Some(TradeSide::Sell);
5000 assert_eq!(NormalizedTick::buy_sell_diff(&[t1, t2]), dec!(7));
5001 }
5002
5003 #[test]
5006 fn test_is_aggressive_buy_true_when_exceeds_avg() {
5007 use rust_decimal_macros::dec;
5008 let mut t = make_tick_pq(dec!(100), dec!(15));
5009 t.side = Some(TradeSide::Buy);
5010 assert!(NormalizedTick::is_aggressive_buy(&t, dec!(10)));
5011 }
5012
5013 #[test]
5014 fn test_is_aggressive_buy_false_when_not_buy_side() {
5015 use rust_decimal_macros::dec;
5016 let mut t = make_tick_pq(dec!(100), dec!(15));
5017 t.side = Some(TradeSide::Sell);
5018 assert!(!NormalizedTick::is_aggressive_buy(&t, dec!(10)));
5019 }
5020
5021 #[test]
5022 fn test_is_aggressive_sell_true_when_exceeds_avg() {
5023 use rust_decimal_macros::dec;
5024 let mut t = make_tick_pq(dec!(100), dec!(20));
5025 t.side = Some(TradeSide::Sell);
5026 assert!(NormalizedTick::is_aggressive_sell(&t, dec!(10)));
5027 }
5028
5029 #[test]
5032 fn test_notional_volume_zero_for_empty() {
5033 assert_eq!(NormalizedTick::notional_volume(&[]), rust_decimal_macros::dec!(0));
5034 }
5035
5036 #[test]
5037 fn test_notional_volume_correct() {
5038 use rust_decimal_macros::dec;
5039 let ticks = vec![
5040 make_tick_pq(dec!(100), dec!(2)), make_tick_pq(dec!(50), dec!(4)), ];
5043 assert_eq!(NormalizedTick::notional_volume(&ticks), dec!(400));
5044 }
5045
5046 #[test]
5049 fn test_weighted_side_score_none_for_empty() {
5050 assert!(NormalizedTick::weighted_side_score(&[]).is_none());
5051 }
5052
5053 #[test]
5054 fn test_weighted_side_score_correct_for_all_buys() {
5055 use rust_decimal_macros::dec;
5056 let mut t = make_tick_pq(dec!(100), dec!(10));
5057 t.side = Some(TradeSide::Buy);
5058 let score = NormalizedTick::weighted_side_score(&[t]).unwrap();
5060 assert!((score - 1.0).abs() < 1e-9);
5061 }
5062
5063 #[test]
5066 fn test_time_span_none_for_single_tick() {
5067 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5068 assert!(NormalizedTick::time_span_ms(&[t]).is_none());
5069 }
5070
5071 #[test]
5072 fn test_time_span_correct_for_two_ticks() {
5073 use rust_decimal_macros::dec;
5074 let mut t1 = make_tick_pq(dec!(100), dec!(1));
5075 t1.received_at_ms = 1000;
5076 let mut t2 = make_tick_pq(dec!(101), dec!(1));
5077 t2.received_at_ms = 5000;
5078 assert_eq!(NormalizedTick::time_span_ms(&[t1, t2]), Some(4000));
5079 }
5080
5081 #[test]
5084 fn test_price_above_vwap_count_none_for_empty() {
5085 assert!(NormalizedTick::price_above_vwap_count(&[]).is_none());
5086 }
5087
5088 #[test]
5089 fn test_price_above_vwap_count_correct() {
5090 use rust_decimal_macros::dec;
5091 let ticks = vec![
5093 make_tick_pq(dec!(90), dec!(1)),
5094 make_tick_pq(dec!(100), dec!(1)),
5095 make_tick_pq(dec!(110), dec!(1)),
5096 ];
5097 assert_eq!(NormalizedTick::price_above_vwap_count(&ticks), Some(1));
5098 }
5099
5100 #[test]
5103 fn test_avg_trade_size_none_for_empty() {
5104 assert!(NormalizedTick::avg_trade_size(&[]).is_none());
5105 }
5106
5107 #[test]
5108 fn test_avg_trade_size_correct() {
5109 use rust_decimal_macros::dec;
5110 let ticks = vec![
5111 make_tick_pq(dec!(100), dec!(2)),
5112 make_tick_pq(dec!(101), dec!(4)),
5113 ];
5114 assert_eq!(NormalizedTick::avg_trade_size(&ticks), Some(dec!(3)));
5115 }
5116
5117 #[test]
5120 fn test_volume_concentration_none_for_empty() {
5121 assert!(NormalizedTick::volume_concentration(&[]).is_none());
5122 }
5123
5124 #[test]
5125 fn test_volume_concentration_is_one_for_single_tick() {
5126 use rust_decimal_macros::dec;
5127 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
5128 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
5129 assert!((c - 1.0).abs() < 1e-9);
5130 }
5131
5132 #[test]
5133 fn test_volume_concentration_in_range() {
5134 use rust_decimal_macros::dec;
5135 let ticks = vec![
5136 make_tick_pq(dec!(100), dec!(1)),
5137 make_tick_pq(dec!(101), dec!(1)),
5138 make_tick_pq(dec!(102), dec!(1)),
5139 make_tick_pq(dec!(103), dec!(10)),
5140 ];
5141 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
5142 assert!(c > 0.0 && c <= 1.0, "expected value in (0,1], got {}", c);
5143 }
5144
5145 #[test]
5148 fn test_trade_imbalance_score_none_for_empty() {
5149 assert!(NormalizedTick::trade_imbalance_score(&[]).is_none());
5150 }
5151
5152 #[test]
5153 fn test_trade_imbalance_score_positive_for_all_buys() {
5154 use rust_decimal_macros::dec;
5155 let mut t = make_tick_pq(dec!(100), dec!(1));
5156 t.side = Some(TradeSide::Buy);
5157 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
5158 assert!(score > 0.0);
5159 }
5160
5161 #[test]
5162 fn test_trade_imbalance_score_negative_for_all_sells() {
5163 use rust_decimal_macros::dec;
5164 let mut t = make_tick_pq(dec!(100), dec!(1));
5165 t.side = Some(TradeSide::Sell);
5166 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
5167 assert!(score < 0.0);
5168 }
5169
5170 #[test]
5173 fn test_price_entropy_none_for_empty() {
5174 assert!(NormalizedTick::price_entropy(&[]).is_none());
5175 }
5176
5177 #[test]
5178 fn test_price_entropy_zero_for_single_price() {
5179 use rust_decimal_macros::dec;
5180 let ticks = vec![
5181 make_tick_pq(dec!(100), dec!(1)),
5182 make_tick_pq(dec!(100), dec!(2)),
5183 ];
5184 let e = NormalizedTick::price_entropy(&ticks).unwrap();
5185 assert!((e - 0.0).abs() < 1e-9, "identical prices should have zero entropy, got {}", e);
5186 }
5187
5188 #[test]
5189 fn test_price_entropy_positive_for_varied_prices() {
5190 use rust_decimal_macros::dec;
5191 let ticks = vec![
5192 make_tick_pq(dec!(100), dec!(1)),
5193 make_tick_pq(dec!(101), dec!(1)),
5194 make_tick_pq(dec!(102), dec!(1)),
5195 ];
5196 let e = NormalizedTick::price_entropy(&ticks).unwrap();
5197 assert!(e > 0.0, "varied prices should have positive entropy, got {}", e);
5198 }
5199
5200 #[test]
5203 fn test_buy_avg_price_none_for_no_buys() {
5204 use rust_decimal_macros::dec;
5205 let mut t = make_tick_pq(dec!(100), dec!(1));
5206 t.side = Some(TradeSide::Sell);
5207 assert!(NormalizedTick::buy_avg_price(&[t]).is_none());
5208 }
5209
5210 #[test]
5211 fn test_buy_avg_price_correct() {
5212 use rust_decimal_macros::dec;
5213 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.side = Some(TradeSide::Buy);
5214 let mut t2 = make_tick_pq(dec!(110), dec!(1)); t2.side = Some(TradeSide::Buy);
5215 assert_eq!(NormalizedTick::buy_avg_price(&[t1, t2]), Some(dec!(105)));
5216 }
5217
5218 #[test]
5219 fn test_sell_avg_price_none_for_no_sells() {
5220 use rust_decimal_macros::dec;
5221 let mut t = make_tick_pq(dec!(100), dec!(1));
5222 t.side = Some(TradeSide::Buy);
5223 assert!(NormalizedTick::sell_avg_price(&[t]).is_none());
5224 }
5225
5226 #[test]
5227 fn test_sell_avg_price_correct() {
5228 use rust_decimal_macros::dec;
5229 let mut t1 = make_tick_pq(dec!(90), dec!(1)); t1.side = Some(TradeSide::Sell);
5230 let mut t2 = make_tick_pq(dec!(100), dec!(1)); t2.side = Some(TradeSide::Sell);
5231 assert_eq!(NormalizedTick::sell_avg_price(&[t1, t2]), Some(dec!(95)));
5232 }
5233
5234 #[test]
5237 fn test_price_skewness_none_for_fewer_than_3() {
5238 use rust_decimal_macros::dec;
5239 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
5240 assert!(NormalizedTick::price_skewness(&ticks).is_none());
5241 }
5242
5243 #[test]
5244 fn test_price_skewness_zero_for_symmetric() {
5245 use rust_decimal_macros::dec;
5246 let ticks = vec![
5248 make_tick_pq(dec!(1), dec!(1)),
5249 make_tick_pq(dec!(2), dec!(1)),
5250 make_tick_pq(dec!(3), dec!(1)),
5251 ];
5252 let s = NormalizedTick::price_skewness(&ticks).unwrap();
5253 assert!(s.abs() < 1e-9, "symmetric should have near-zero skew, got {}", s);
5254 }
5255
5256 #[test]
5259 fn test_quantity_skewness_none_for_fewer_than_3() {
5260 use rust_decimal_macros::dec;
5261 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
5262 assert!(NormalizedTick::quantity_skewness(&ticks).is_none());
5263 }
5264
5265 #[test]
5266 fn test_quantity_skewness_positive_for_right_skewed() {
5267 use rust_decimal_macros::dec;
5268 let ticks = vec![
5270 make_tick_pq(dec!(100), dec!(1)),
5271 make_tick_pq(dec!(101), dec!(1)),
5272 make_tick_pq(dec!(102), dec!(100)),
5273 ];
5274 let s = NormalizedTick::quantity_skewness(&ticks).unwrap();
5275 assert!(s > 0.0, "right-skewed distribution should have positive skewness, got {}", s);
5276 }
5277
5278 #[test]
5281 fn test_price_kurtosis_none_for_fewer_than_4() {
5282 use rust_decimal_macros::dec;
5283 let ticks = vec![
5284 make_tick_pq(dec!(1), dec!(1)),
5285 make_tick_pq(dec!(2), dec!(1)),
5286 make_tick_pq(dec!(3), dec!(1)),
5287 ];
5288 assert!(NormalizedTick::price_kurtosis(&ticks).is_none());
5289 }
5290
5291 #[test]
5292 fn test_price_kurtosis_returns_some_for_varied_prices() {
5293 use rust_decimal_macros::dec;
5294 let ticks = vec![
5295 make_tick_pq(dec!(1), dec!(1)),
5296 make_tick_pq(dec!(2), dec!(1)),
5297 make_tick_pq(dec!(3), dec!(1)),
5298 make_tick_pq(dec!(4), dec!(1)),
5299 ];
5300 assert!(NormalizedTick::price_kurtosis(&ticks).is_some());
5301 }
5302
5303 #[test]
5306 fn test_high_volume_tick_count_zero_for_empty() {
5307 use rust_decimal_macros::dec;
5308 assert_eq!(NormalizedTick::high_volume_tick_count(&[], dec!(1)), 0);
5309 }
5310
5311 #[test]
5312 fn test_high_volume_tick_count_correct() {
5313 use rust_decimal_macros::dec;
5314 let ticks = vec![
5315 make_tick_pq(dec!(100), dec!(1)),
5316 make_tick_pq(dec!(101), dec!(5)),
5317 make_tick_pq(dec!(102), dec!(10)),
5318 ];
5319 assert_eq!(NormalizedTick::high_volume_tick_count(&ticks, dec!(4)), 2);
5320 }
5321
5322 #[test]
5325 fn test_vwap_spread_none_when_no_buys_or_sells() {
5326 use rust_decimal_macros::dec;
5327 let t = make_tick_pq(dec!(100), dec!(1));
5328 assert!(NormalizedTick::vwap_spread(&[t]).is_none());
5329 }
5330
5331 #[test]
5332 fn test_vwap_spread_positive_when_buys_priced_higher() {
5333 use rust_decimal_macros::dec;
5334 let mut buy = make_tick_pq(dec!(105), dec!(1)); buy.side = Some(TradeSide::Buy);
5335 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
5336 let spread = NormalizedTick::vwap_spread(&[buy, sell]).unwrap();
5337 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
5338 }
5339
5340 #[test]
5343 fn test_avg_buy_quantity_none_for_no_buys() {
5344 use rust_decimal_macros::dec;
5345 let mut t = make_tick_pq(dec!(100), dec!(2)); t.side = Some(TradeSide::Sell);
5346 assert!(NormalizedTick::avg_buy_quantity(&[t]).is_none());
5347 }
5348
5349 #[test]
5350 fn test_avg_buy_quantity_correct() {
5351 use rust_decimal_macros::dec;
5352 let mut t1 = make_tick_pq(dec!(100), dec!(2)); t1.side = Some(TradeSide::Buy);
5353 let mut t2 = make_tick_pq(dec!(101), dec!(4)); t2.side = Some(TradeSide::Buy);
5354 assert_eq!(NormalizedTick::avg_buy_quantity(&[t1, t2]), Some(dec!(3)));
5355 }
5356
5357 #[test]
5358 fn test_avg_sell_quantity_correct() {
5359 use rust_decimal_macros::dec;
5360 let mut t1 = make_tick_pq(dec!(100), dec!(6)); t1.side = Some(TradeSide::Sell);
5361 let mut t2 = make_tick_pq(dec!(101), dec!(2)); t2.side = Some(TradeSide::Sell);
5362 assert_eq!(NormalizedTick::avg_sell_quantity(&[t1, t2]), Some(dec!(4)));
5363 }
5364
5365 #[test]
5368 fn test_price_mean_reversion_score_none_for_empty() {
5369 assert!(NormalizedTick::price_mean_reversion_score(&[]).is_none());
5370 }
5371
5372 #[test]
5373 fn test_price_mean_reversion_score_in_range() {
5374 use rust_decimal_macros::dec;
5375 let ticks = vec![
5376 make_tick_pq(dec!(90), dec!(1)),
5377 make_tick_pq(dec!(100), dec!(1)),
5378 make_tick_pq(dec!(110), dec!(1)),
5379 ];
5380 let score = NormalizedTick::price_mean_reversion_score(&ticks).unwrap();
5381 assert!(score >= 0.0 && score <= 1.0, "score should be in [0, 1], got {}", score);
5382 }
5383
5384 #[test]
5387 fn test_largest_price_move_none_for_single_tick() {
5388 use rust_decimal_macros::dec;
5389 let t = make_tick_pq(dec!(100), dec!(1));
5390 assert!(NormalizedTick::largest_price_move(&[t]).is_none());
5391 }
5392
5393 #[test]
5394 fn test_largest_price_move_correct() {
5395 use rust_decimal_macros::dec;
5396 let ticks = vec![
5397 make_tick_pq(dec!(100), dec!(1)),
5398 make_tick_pq(dec!(105), dec!(1)), make_tick_pq(dec!(102), dec!(1)), ];
5401 assert_eq!(NormalizedTick::largest_price_move(&ticks), Some(dec!(5)));
5402 }
5403
5404 #[test]
5407 fn test_tick_rate_none_for_single_tick() {
5408 use rust_decimal_macros::dec;
5409 let t = make_tick_pq(dec!(100), dec!(1));
5410 assert!(NormalizedTick::tick_rate(&[t]).is_none());
5411 }
5412
5413 #[test]
5414 fn test_tick_rate_correct() {
5415 use rust_decimal_macros::dec;
5416 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.received_at_ms = 0;
5417 let mut t2 = make_tick_pq(dec!(101), dec!(1)); t2.received_at_ms = 2;
5418 let mut t3 = make_tick_pq(dec!(102), dec!(1)); t3.received_at_ms = 4;
5419 let rate = NormalizedTick::tick_rate(&[t1, t2, t3]).unwrap();
5421 assert!((rate - 0.75).abs() < 1e-9, "expected 0.75 ticks/ms, got {}", rate);
5422 }
5423
5424 #[test]
5427 fn test_buy_notional_fraction_none_for_empty() {
5428 assert!(NormalizedTick::buy_notional_fraction(&[]).is_none());
5429 }
5430
5431 #[test]
5432 fn test_buy_notional_fraction_one_when_all_buys() {
5433 use rust_decimal_macros::dec;
5434 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
5435 let frac = NormalizedTick::buy_notional_fraction(&[t]).unwrap();
5436 assert!((frac - 1.0).abs() < 1e-9, "all buys should give fraction=1.0, got {}", frac);
5437 }
5438
5439 #[test]
5440 fn test_buy_notional_fraction_in_range_for_mixed() {
5441 use rust_decimal_macros::dec;
5442 let mut buy = make_tick_pq(dec!(100), dec!(3)); buy.side = Some(TradeSide::Buy);
5443 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
5444 let frac = NormalizedTick::buy_notional_fraction(&[buy, sell]).unwrap();
5445 assert!(frac > 0.0 && frac < 1.0, "mixed ticks should be in (0,1), got {}", frac);
5446 }
5447
5448 #[test]
5451 fn test_price_range_pct_none_for_empty() {
5452 assert!(NormalizedTick::price_range_pct(&[]).is_none());
5453 }
5454
5455 #[test]
5456 fn test_price_range_pct_correct() {
5457 use rust_decimal_macros::dec;
5458 let ticks = vec![
5459 make_tick_pq(dec!(100), dec!(1)),
5460 make_tick_pq(dec!(110), dec!(1)),
5461 ];
5462 let pct = NormalizedTick::price_range_pct(&ticks).unwrap();
5464 assert!((pct - 10.0).abs() < 1e-6, "expected 10.0%, got {}", pct);
5465 }
5466
5467 #[test]
5470 fn test_buy_side_dominance_none_when_no_sides() {
5471 use rust_decimal_macros::dec;
5472 let t = make_tick_pq(dec!(100), dec!(1)); assert!(NormalizedTick::buy_side_dominance(&[t]).is_none());
5474 }
5475
5476 #[test]
5477 fn test_buy_side_dominance_one_when_all_buys() {
5478 use rust_decimal_macros::dec;
5479 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
5480 let d = NormalizedTick::buy_side_dominance(&[t]).unwrap();
5481 assert!((d - 1.0).abs() < 1e-9, "all buys should give 1.0, got {}", d);
5482 }
5483
5484 #[test]
5487 fn test_volume_weighted_price_std_none_for_empty() {
5488 assert!(NormalizedTick::volume_weighted_price_std(&[]).is_none());
5489 }
5490
5491 #[test]
5492 fn test_volume_weighted_price_std_zero_for_same_price() {
5493 use rust_decimal_macros::dec;
5494 let ticks = vec![
5495 make_tick_pq(dec!(100), dec!(2)),
5496 make_tick_pq(dec!(100), dec!(3)),
5497 ];
5498 let std = NormalizedTick::volume_weighted_price_std(&ticks).unwrap();
5499 assert!((std - 0.0).abs() < 1e-9, "same price should give 0 std, got {}", std);
5500 }
5501
5502 #[test]
5505 fn test_last_n_vwap_none_for_zero_n() {
5506 use rust_decimal_macros::dec;
5507 let t = make_tick_pq(dec!(100), dec!(1));
5508 assert!(NormalizedTick::last_n_vwap(&[t], 0).is_none());
5509 }
5510
5511 #[test]
5512 fn test_last_n_vwap_uses_last_n_ticks() {
5513 use rust_decimal_macros::dec;
5514 let ticks = vec![
5516 make_tick_pq(dec!(50), dec!(10)),
5517 make_tick_pq(dec!(100), dec!(5)),
5518 make_tick_pq(dec!(100), dec!(5)),
5519 ];
5520 let v = NormalizedTick::last_n_vwap(&ticks, 2).unwrap();
5521 assert_eq!(v, dec!(100));
5522 }
5523
5524 #[test]
5527 fn test_price_autocorrelation_none_for_fewer_than_3() {
5528 use rust_decimal_macros::dec;
5529 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
5530 assert!(NormalizedTick::price_autocorrelation(&ticks).is_none());
5531 }
5532
5533 #[test]
5534 fn test_price_autocorrelation_positive_for_trending_prices() {
5535 use rust_decimal_macros::dec;
5536 let ticks = vec![
5537 make_tick_pq(dec!(100), dec!(1)),
5538 make_tick_pq(dec!(102), dec!(1)),
5539 make_tick_pq(dec!(104), dec!(1)),
5540 make_tick_pq(dec!(106), dec!(1)),
5541 ];
5542 let ac = NormalizedTick::price_autocorrelation(&ticks).unwrap();
5543 assert!(ac > 0.0, "trending prices should have positive AC, got {}", ac);
5544 }
5545
5546 #[test]
5549 fn test_net_trade_direction_zero_for_empty() {
5550 assert_eq!(NormalizedTick::net_trade_direction(&[]), 0);
5551 }
5552
5553 #[test]
5554 fn test_net_trade_direction_positive_for_more_buys() {
5555 use rust_decimal_macros::dec;
5556 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
5557 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
5558 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
5559 assert_eq!(NormalizedTick::net_trade_direction(&[b1, b2, s1]), 1);
5560 }
5561
5562 #[test]
5565 fn test_sell_side_notional_fraction_none_for_empty() {
5566 assert!(NormalizedTick::sell_side_notional_fraction(&[]).is_none());
5567 }
5568
5569 #[test]
5570 fn test_sell_side_notional_fraction_one_when_all_sells() {
5571 use rust_decimal_macros::dec;
5572 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Sell);
5573 let f = NormalizedTick::sell_side_notional_fraction(&[t]).unwrap();
5574 assert!((f - 1.0).abs() < 1e-9, "all sells should give 1.0, got {}", f);
5575 }
5576
5577 #[test]
5580 fn test_price_oscillation_count_zero_for_monotone() {
5581 use rust_decimal_macros::dec;
5582 let ticks = vec![
5583 make_tick_pq(dec!(100), dec!(1)),
5584 make_tick_pq(dec!(101), dec!(1)),
5585 make_tick_pq(dec!(102), dec!(1)),
5586 ];
5587 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 0);
5588 }
5589
5590 #[test]
5591 fn test_price_oscillation_count_detects_reversals() {
5592 use rust_decimal_macros::dec;
5593 let ticks = vec![
5596 make_tick_pq(dec!(100), dec!(1)),
5597 make_tick_pq(dec!(105), dec!(1)),
5598 make_tick_pq(dec!(102), dec!(1)),
5599 make_tick_pq(dec!(107), dec!(1)),
5600 ];
5601 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 2);
5602 }
5603
5604 #[test]
5607 fn test_realized_spread_none_when_no_sides() {
5608 use rust_decimal_macros::dec;
5609 let t = make_tick_pq(dec!(100), dec!(1));
5610 assert!(NormalizedTick::realized_spread(&[t]).is_none());
5611 }
5612
5613 #[test]
5614 fn test_realized_spread_positive_when_buys_higher() {
5615 use rust_decimal_macros::dec;
5616 let mut b = make_tick_pq(dec!(105), dec!(1)); b.side = Some(TradeSide::Buy);
5617 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
5618 let spread = NormalizedTick::realized_spread(&[b, s]).unwrap();
5619 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
5620 }
5621
5622 #[test]
5625 fn test_price_impact_per_unit_none_for_single_tick() {
5626 use rust_decimal_macros::dec;
5627 let t = make_tick_pq(dec!(100), dec!(1));
5628 assert!(NormalizedTick::price_impact_per_unit(&[t]).is_none());
5629 }
5630
5631 #[test]
5634 fn test_volume_weighted_return_none_for_single_tick() {
5635 use rust_decimal_macros::dec;
5636 let t = make_tick_pq(dec!(100), dec!(1));
5637 assert!(NormalizedTick::volume_weighted_return(&[t]).is_none());
5638 }
5639
5640 #[test]
5641 fn test_volume_weighted_return_zero_for_constant_price() {
5642 use rust_decimal_macros::dec;
5643 let ticks = vec![
5644 make_tick_pq(dec!(100), dec!(5)),
5645 make_tick_pq(dec!(100), dec!(5)),
5646 ];
5647 let r = NormalizedTick::volume_weighted_return(&ticks).unwrap();
5648 assert!((r - 0.0).abs() < 1e-9, "constant price should give 0 return, got {}", r);
5649 }
5650
5651 #[test]
5654 fn test_quantity_concentration_none_for_empty() {
5655 assert!(NormalizedTick::quantity_concentration(&[]).is_none());
5656 }
5657
5658 #[test]
5659 fn test_quantity_concentration_zero_for_identical_quantities() {
5660 use rust_decimal_macros::dec;
5661 let ticks = vec![
5662 make_tick_pq(dec!(100), dec!(5)),
5663 make_tick_pq(dec!(101), dec!(5)),
5664 ];
5665 let c = NormalizedTick::quantity_concentration(&ticks).unwrap();
5666 assert!((c - 0.0).abs() < 1e-9, "identical quantities should give 0 concentration, got {}", c);
5667 }
5668
5669 #[test]
5672 fn test_price_level_volume_zero_for_no_match() {
5673 use rust_decimal_macros::dec;
5674 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
5675 let v = NormalizedTick::price_level_volume(&ticks, dec!(200));
5676 assert_eq!(v, dec!(0));
5677 }
5678
5679 #[test]
5680 fn test_price_level_volume_sums_matching_ticks() {
5681 use rust_decimal_macros::dec;
5682 let ticks = vec![
5683 make_tick_pq(dec!(100), dec!(3)),
5684 make_tick_pq(dec!(101), dec!(7)),
5685 make_tick_pq(dec!(100), dec!(2)),
5686 ];
5687 assert_eq!(NormalizedTick::price_level_volume(&ticks, dec!(100)), dec!(5));
5688 }
5689
5690 #[test]
5693 fn test_mid_price_drift_none_for_single_tick() {
5694 use rust_decimal_macros::dec;
5695 let t = make_tick_pq(dec!(100), dec!(1));
5696 assert!(NormalizedTick::mid_price_drift(&[t]).is_none());
5697 }
5698
5699 #[test]
5702 fn test_tick_direction_bias_none_for_fewer_than_3() {
5703 use rust_decimal_macros::dec;
5704 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
5705 assert!(NormalizedTick::tick_direction_bias(&ticks).is_none());
5706 }
5707
5708 #[test]
5709 fn test_tick_direction_bias_one_for_monotone() {
5710 use rust_decimal_macros::dec;
5711 let ticks = vec![
5712 make_tick_pq(dec!(100), dec!(1)),
5713 make_tick_pq(dec!(101), dec!(1)),
5714 make_tick_pq(dec!(102), dec!(1)),
5715 make_tick_pq(dec!(103), dec!(1)),
5716 ];
5717 let bias = NormalizedTick::tick_direction_bias(&ticks).unwrap();
5718 assert!((bias - 1.0).abs() < 1e-9, "monotone should give bias=1.0, got {}", bias);
5719 }
5720
5721 #[test]
5722 fn test_buy_sell_size_ratio_none_for_empty() {
5723 assert!(NormalizedTick::buy_sell_size_ratio(&[]).is_none());
5724 }
5725
5726 #[test]
5727 fn test_buy_sell_size_ratio_positive() {
5728 use rust_decimal_macros::dec;
5729 let buy = NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) };
5730 let sell = NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(2)) };
5731 let r = NormalizedTick::buy_sell_size_ratio(&[buy, sell]).unwrap();
5732 assert!((r - 2.0).abs() < 1e-6, "ratio should be 2.0, got {}", r);
5733 }
5734
5735 #[test]
5736 fn test_trade_size_dispersion_none_for_single_tick() {
5737 use rust_decimal_macros::dec;
5738 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
5739 assert!(NormalizedTick::trade_size_dispersion(&ticks).is_none());
5740 }
5741
5742 #[test]
5743 fn test_trade_size_dispersion_zero_for_identical() {
5744 use rust_decimal_macros::dec;
5745 let ticks = vec![
5746 make_tick_pq(dec!(100), dec!(5)),
5747 make_tick_pq(dec!(101), dec!(5)),
5748 make_tick_pq(dec!(102), dec!(5)),
5749 ];
5750 let d = NormalizedTick::trade_size_dispersion(&ticks).unwrap();
5751 assert!(d.abs() < 1e-9, "identical sizes → dispersion=0, got {}", d);
5752 }
5753
5754 #[test]
5755 fn test_first_last_price_none_for_empty() {
5756 assert!(NormalizedTick::first_price(&[]).is_none());
5757 assert!(NormalizedTick::last_price(&[]).is_none());
5758 }
5759
5760 #[test]
5761 fn test_first_last_price_correct() {
5762 use rust_decimal_macros::dec;
5763 let ticks = vec![
5764 make_tick_pq(dec!(100), dec!(1)),
5765 make_tick_pq(dec!(105), dec!(1)),
5766 make_tick_pq(dec!(110), dec!(1)),
5767 ];
5768 assert_eq!(NormalizedTick::first_price(&ticks).unwrap(), dec!(100));
5769 assert_eq!(NormalizedTick::last_price(&ticks).unwrap(), dec!(110));
5770 }
5771
5772 #[test]
5773 fn test_median_quantity_none_for_empty() {
5774 assert!(NormalizedTick::median_quantity(&[]).is_none());
5775 }
5776
5777 #[test]
5778 fn test_median_quantity_odd_count() {
5779 use rust_decimal_macros::dec;
5780 let ticks = vec![
5781 make_tick_pq(dec!(100), dec!(3)),
5782 make_tick_pq(dec!(101), dec!(1)),
5783 make_tick_pq(dec!(102), dec!(5)),
5784 ];
5785 assert_eq!(NormalizedTick::median_quantity(&ticks).unwrap(), dec!(3));
5787 }
5788
5789 #[test]
5790 fn test_volume_above_vwap_none_for_empty() {
5791 assert!(NormalizedTick::volume_above_vwap(&[]).is_none());
5792 }
5793
5794 #[test]
5795 fn test_volume_above_vwap_none_when_all_at_vwap() {
5796 use rust_decimal_macros::dec;
5797 let ticks = vec![
5799 make_tick_pq(dec!(100), dec!(5)),
5800 make_tick_pq(dec!(100), dec!(5)),
5801 ];
5802 let v = NormalizedTick::volume_above_vwap(&ticks).unwrap();
5803 assert_eq!(v, dec!(0));
5804 }
5805
5806 #[test]
5807 fn test_inter_arrival_variance_none_for_fewer_than_3() {
5808 use rust_decimal_macros::dec;
5809 let t = make_tick_pq(dec!(100), dec!(1));
5810 assert!(NormalizedTick::inter_arrival_variance(&[t]).is_none());
5811 }
5812
5813 #[test]
5814 fn test_spread_efficiency_none_for_single_tick() {
5815 use rust_decimal_macros::dec;
5816 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
5817 assert!(NormalizedTick::spread_efficiency(&ticks).is_none());
5818 }
5819
5820 #[test]
5821 fn test_spread_efficiency_one_for_monotone() {
5822 use rust_decimal_macros::dec;
5823 let ticks = vec![
5824 make_tick_pq(dec!(100), dec!(1)),
5825 make_tick_pq(dec!(101), dec!(1)),
5826 make_tick_pq(dec!(102), dec!(1)),
5827 ];
5828 let e = NormalizedTick::spread_efficiency(&ticks).unwrap();
5830 assert!((e - 1.0).abs() < 1e-9, "expected 1.0, got {}", e);
5831 }
5832
5833 #[test]
5838 fn test_aggressor_fraction_none_for_empty() {
5839 assert!(NormalizedTick::aggressor_fraction(&[]).is_none());
5840 }
5841
5842 #[test]
5843 fn test_aggressor_fraction_zero_when_all_neutral() {
5844 use rust_decimal_macros::dec;
5845 let ticks = vec![
5846 make_tick_pq(dec!(100), dec!(1)),
5847 make_tick_pq(dec!(101), dec!(1)),
5848 ];
5849 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
5850 assert!((f - 0.0).abs() < 1e-9, "all neutral → fraction=0, got {}", f);
5851 }
5852
5853 #[test]
5854 fn test_aggressor_fraction_one_when_all_known() {
5855 use rust_decimal_macros::dec;
5856 let ticks = vec![
5857 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(1)) },
5858 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(101), dec!(1)) },
5859 ];
5860 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
5861 assert!((f - 1.0).abs() < 1e-9, "all known → fraction=1, got {}", f);
5862 }
5863
5864 #[test]
5867 fn test_volume_imbalance_ratio_none_for_neutral_ticks() {
5868 use rust_decimal_macros::dec;
5869 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
5870 assert!(NormalizedTick::volume_imbalance_ratio(&ticks).is_none());
5871 }
5872
5873 #[test]
5874 fn test_volume_imbalance_ratio_positive_for_all_buys() {
5875 use rust_decimal_macros::dec;
5876 let ticks = vec![
5877 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) },
5878 ];
5879 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
5880 assert!((r - 1.0).abs() < 1e-9, "all buys → ratio=1.0, got {}", r);
5881 }
5882
5883 #[test]
5884 fn test_volume_imbalance_ratio_zero_for_equal_sides() {
5885 use rust_decimal_macros::dec;
5886 let ticks = vec![
5887 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
5888 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
5889 ];
5890 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
5891 assert!(r.abs() < 1e-9, "equal buy/sell → ratio=0, got {}", r);
5892 }
5893
5894 #[test]
5897 fn test_price_quantity_covariance_none_for_single_tick() {
5898 use rust_decimal_macros::dec;
5899 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
5900 assert!(NormalizedTick::price_quantity_covariance(&ticks).is_none());
5901 }
5902
5903 #[test]
5904 fn test_price_quantity_covariance_positive_when_correlated() {
5905 use rust_decimal_macros::dec;
5906 let ticks = vec![
5907 make_tick_pq(dec!(100), dec!(1)),
5908 make_tick_pq(dec!(200), dec!(2)),
5909 make_tick_pq(dec!(300), dec!(3)),
5910 ];
5911 let c = NormalizedTick::price_quantity_covariance(&ticks).unwrap();
5912 assert!(c > 0.0, "price and qty both rise → positive cov, got {}", c);
5913 }
5914
5915 #[test]
5918 fn test_large_trade_fraction_none_for_empty() {
5919 use rust_decimal_macros::dec;
5920 assert!(NormalizedTick::large_trade_fraction(&[], dec!(10)).is_none());
5921 }
5922
5923 #[test]
5924 fn test_large_trade_fraction_zero_when_all_small() {
5925 use rust_decimal_macros::dec;
5926 let ticks = vec![
5927 make_tick_pq(dec!(100), dec!(1)),
5928 make_tick_pq(dec!(101), dec!(2)),
5929 ];
5930 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
5931 assert!((f - 0.0).abs() < 1e-9, "all small → fraction=0, got {}", f);
5932 }
5933
5934 #[test]
5935 fn test_large_trade_fraction_one_when_all_large() {
5936 use rust_decimal_macros::dec;
5937 let ticks = vec![
5938 make_tick_pq(dec!(100), dec!(20)),
5939 make_tick_pq(dec!(101), dec!(30)),
5940 ];
5941 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
5942 assert!((f - 1.0).abs() < 1e-9, "all large → fraction=1, got {}", f);
5943 }
5944
5945 #[test]
5948 fn test_price_level_density_none_for_empty() {
5949 assert!(NormalizedTick::price_level_density(&[]).is_none());
5950 }
5951
5952 #[test]
5953 fn test_price_level_density_none_when_range_zero() {
5954 use rust_decimal_macros::dec;
5955 let ticks = vec![
5956 make_tick_pq(dec!(100), dec!(1)),
5957 make_tick_pq(dec!(100), dec!(2)),
5958 ];
5959 assert!(NormalizedTick::price_level_density(&ticks).is_none());
5960 }
5961
5962 #[test]
5963 fn test_price_level_density_positive_for_varied_prices() {
5964 use rust_decimal_macros::dec;
5965 let ticks = vec![
5966 make_tick_pq(dec!(100), dec!(1)),
5967 make_tick_pq(dec!(110), dec!(1)),
5968 make_tick_pq(dec!(120), dec!(1)),
5969 ];
5970 let d = NormalizedTick::price_level_density(&ticks).unwrap();
5971 assert!(d > 0.0, "should be positive, got {}", d);
5972 }
5973
5974 #[test]
5977 fn test_notional_buy_sell_ratio_none_when_no_sells() {
5978 use rust_decimal_macros::dec;
5979 let ticks = vec![
5980 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
5981 ];
5982 assert!(NormalizedTick::notional_buy_sell_ratio(&ticks).is_none());
5983 }
5984
5985 #[test]
5986 fn test_notional_buy_sell_ratio_one_for_equal_notional() {
5987 use rust_decimal_macros::dec;
5988 let ticks = vec![
5989 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
5990 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
5991 ];
5992 let r = NormalizedTick::notional_buy_sell_ratio(&ticks).unwrap();
5993 assert!((r - 1.0).abs() < 1e-9, "equal notional → ratio=1, got {}", r);
5994 }
5995
5996 #[test]
5999 fn test_log_return_mean_none_for_single_tick() {
6000 use rust_decimal_macros::dec;
6001 assert!(NormalizedTick::log_return_mean(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6002 }
6003
6004 #[test]
6005 fn test_log_return_mean_zero_for_constant_price() {
6006 use rust_decimal_macros::dec;
6007 let ticks = vec![
6008 make_tick_pq(dec!(100), dec!(1)),
6009 make_tick_pq(dec!(100), dec!(1)),
6010 make_tick_pq(dec!(100), dec!(1)),
6011 ];
6012 let m = NormalizedTick::log_return_mean(&ticks).unwrap();
6013 assert!(m.abs() < 1e-9, "constant price → mean log return=0, got {}", m);
6014 }
6015
6016 #[test]
6019 fn test_log_return_std_none_for_fewer_than_3_ticks() {
6020 use rust_decimal_macros::dec;
6021 let ticks = vec![
6022 make_tick_pq(dec!(100), dec!(1)),
6023 make_tick_pq(dec!(101), dec!(1)),
6024 ];
6025 assert!(NormalizedTick::log_return_std(&ticks).is_none());
6026 }
6027
6028 #[test]
6029 fn test_log_return_std_zero_for_constant_price() {
6030 use rust_decimal_macros::dec;
6031 let ticks = vec![
6032 make_tick_pq(dec!(100), dec!(1)),
6033 make_tick_pq(dec!(100), dec!(1)),
6034 make_tick_pq(dec!(100), dec!(1)),
6035 make_tick_pq(dec!(100), dec!(1)),
6036 ];
6037 let s = NormalizedTick::log_return_std(&ticks).unwrap();
6038 assert!(s.abs() < 1e-9, "constant price → std=0, got {}", s);
6039 }
6040
6041 #[test]
6044 fn test_price_overshoot_ratio_none_for_empty() {
6045 assert!(NormalizedTick::price_overshoot_ratio(&[]).is_none());
6046 }
6047
6048 #[test]
6049 fn test_price_overshoot_ratio_one_for_monotone_up() {
6050 use rust_decimal_macros::dec;
6051 let ticks = vec![
6052 make_tick_pq(dec!(100), dec!(1)),
6053 make_tick_pq(dec!(105), dec!(1)),
6054 make_tick_pq(dec!(110), dec!(1)),
6055 ];
6056 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
6058 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
6059 }
6060
6061 #[test]
6062 fn test_price_overshoot_ratio_above_one_when_price_retreats() {
6063 use rust_decimal_macros::dec;
6064 let ticks = vec![
6065 make_tick_pq(dec!(100), dec!(1)),
6066 make_tick_pq(dec!(120), dec!(1)),
6067 make_tick_pq(dec!(110), dec!(1)),
6068 ];
6069 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
6071 assert!(r > 1.0, "price retreated → ratio>1, got {}", r);
6072 }
6073
6074 #[test]
6077 fn test_price_undershoot_ratio_none_for_empty() {
6078 assert!(NormalizedTick::price_undershoot_ratio(&[]).is_none());
6079 }
6080
6081 #[test]
6082 fn test_price_undershoot_ratio_one_for_monotone_down() {
6083 use rust_decimal_macros::dec;
6084 let ticks = vec![
6085 make_tick_pq(dec!(110), dec!(1)),
6086 make_tick_pq(dec!(105), dec!(1)),
6087 make_tick_pq(dec!(100), dec!(1)),
6088 ];
6089 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
6091 assert!(r > 1.0, "monotone down → ratio>1, got {}", r);
6092 }
6093
6094 #[test]
6095 fn test_price_undershoot_ratio_one_for_monotone_up() {
6096 use rust_decimal_macros::dec;
6097 let ticks = vec![
6098 make_tick_pq(dec!(100), dec!(1)),
6099 make_tick_pq(dec!(105), dec!(1)),
6100 make_tick_pq(dec!(110), dec!(1)),
6101 ];
6102 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
6104 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
6105 }
6106
6107 #[test]
6110 fn test_net_notional_empty_is_zero() {
6111 assert_eq!(NormalizedTick::net_notional(&[]), Decimal::ZERO);
6112 }
6113
6114 #[test]
6115 fn test_net_notional_positive_buy() {
6116 use rust_decimal_macros::dec;
6117 let ticks = vec![
6118 make_tick_pq(dec!(100), dec!(5)).with_side(TradeSide::Buy),
6119 make_tick_pq(dec!(100), dec!(2)).with_side(TradeSide::Sell),
6120 ];
6121 assert_eq!(NormalizedTick::net_notional(&ticks), dec!(300));
6122 }
6123
6124 #[test]
6125 fn test_price_reversal_count_empty_is_zero() {
6126 assert_eq!(NormalizedTick::price_reversal_count(&[]), 0);
6127 }
6128
6129 #[test]
6130 fn test_price_reversal_count_monotone_is_zero() {
6131 use rust_decimal_macros::dec;
6132 let ticks = vec![
6133 make_tick_pq(dec!(100), dec!(1)),
6134 make_tick_pq(dec!(101), dec!(1)),
6135 make_tick_pq(dec!(102), dec!(1)),
6136 ];
6137 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 0);
6138 }
6139
6140 #[test]
6141 fn test_price_reversal_count_zigzag() {
6142 use rust_decimal_macros::dec;
6143 let ticks = vec![
6144 make_tick_pq(dec!(100), dec!(1)),
6145 make_tick_pq(dec!(105), dec!(1)),
6146 make_tick_pq(dec!(100), dec!(1)),
6147 make_tick_pq(dec!(105), dec!(1)),
6148 ];
6149 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 2);
6150 }
6151
6152 #[test]
6153 fn test_quantity_kurtosis_none_for_few_ticks() {
6154 use rust_decimal_macros::dec;
6155 let t = make_tick_pq(dec!(100), dec!(1));
6156 assert!(NormalizedTick::quantity_kurtosis(&[t]).is_none());
6157 }
6158
6159 #[test]
6160 fn test_quantity_kurtosis_some_for_sufficient() {
6161 use rust_decimal_macros::dec;
6162 let ticks = vec![
6163 make_tick_pq(dec!(100), dec!(1)),
6164 make_tick_pq(dec!(101), dec!(2)),
6165 make_tick_pq(dec!(102), dec!(3)),
6166 make_tick_pq(dec!(103), dec!(4)),
6167 ];
6168 assert!(NormalizedTick::quantity_kurtosis(&ticks).is_some());
6169 }
6170
6171 #[test]
6172 fn test_largest_notional_trade_none_for_empty() {
6173 assert!(NormalizedTick::largest_notional_trade(&[]).is_none());
6174 }
6175
6176 #[test]
6177 fn test_largest_notional_trade_correct() {
6178 use rust_decimal_macros::dec;
6179 let ticks = vec![
6180 make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(50), dec!(10)), make_tick_pq(dec!(200), dec!(1)), ];
6184 let t = NormalizedTick::largest_notional_trade(&ticks).unwrap();
6185 assert_eq!(t.price, dec!(50));
6186 }
6187
6188 #[test]
6189 fn test_twap_none_for_single_tick() {
6190 use rust_decimal_macros::dec;
6191 assert!(NormalizedTick::twap(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6192 }
6193
6194 #[test]
6195 fn test_twap_two_equal_intervals() {
6196 use rust_decimal_macros::dec;
6197 let mut t1 = make_tick_pq(dec!(100), dec!(1));
6198 t1.received_at_ms = 0;
6199 let mut t2 = make_tick_pq(dec!(200), dec!(1));
6200 t2.received_at_ms = 1000;
6201 let mut t3 = make_tick_pq(dec!(300), dec!(1));
6202 t3.received_at_ms = 2000;
6203 let twap = NormalizedTick::twap(&[t1, t2, t3]).unwrap();
6205 assert_eq!(twap, dec!(150));
6206 }
6207
6208 #[test]
6209 fn test_neutral_fraction_all_neutral() {
6210 use rust_decimal_macros::dec;
6211 let ticks = vec![
6212 make_tick_pq(dec!(100), dec!(1)),
6213 make_tick_pq(dec!(101), dec!(1)),
6214 ];
6215 let f = NormalizedTick::neutral_fraction(&ticks).unwrap();
6216 assert!((f - 1.0).abs() < 1e-9, "all neutral → fraction=1, got {}", f);
6217 }
6218
6219 #[test]
6220 fn test_log_return_variance_none_for_few_ticks() {
6221 use rust_decimal_macros::dec;
6222 let t = make_tick_pq(dec!(100), dec!(1));
6223 assert!(NormalizedTick::log_return_variance(&[t]).is_none());
6224 }
6225
6226 #[test]
6227 fn test_log_return_variance_zero_for_flat_prices() {
6228 use rust_decimal_macros::dec;
6229 let ticks = vec![
6230 make_tick_pq(dec!(100), dec!(1)),
6231 make_tick_pq(dec!(100), dec!(1)),
6232 make_tick_pq(dec!(100), dec!(1)),
6233 ];
6234 let v = NormalizedTick::log_return_variance(&ticks).unwrap();
6235 assert!(v.abs() < 1e-9, "flat prices → variance=0, got {}", v);
6236 }
6237
6238 #[test]
6239 fn test_volume_at_vwap_zero_for_empty() {
6240 assert_eq!(
6241 NormalizedTick::volume_at_vwap(&[], rust_decimal_macros::dec!(1)),
6242 Decimal::ZERO
6243 );
6244 }
6245}