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 pub fn cumulative_volume(ticks: &[NormalizedTick]) -> Vec<Decimal> {
2203 let mut acc = Decimal::ZERO;
2204 ticks
2205 .iter()
2206 .map(|t| {
2207 acc += t.quantity;
2208 acc
2209 })
2210 .collect()
2211 }
2212
2213 pub fn price_volatility_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2218 use rust_decimal::prelude::ToPrimitive;
2219 let range = Self::price_range(ticks)?;
2220 let mean = Self::average_price(ticks)?;
2221 if mean.is_zero() {
2222 return None;
2223 }
2224 (range / mean).to_f64()
2225 }
2226
2227 pub fn notional_per_tick(ticks: &[NormalizedTick]) -> Option<f64> {
2234 use rust_decimal::prelude::ToPrimitive;
2235 Self::average_notional(ticks)?.to_f64()
2236 }
2237
2238 pub fn buy_to_total_volume_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2242 use rust_decimal::prelude::ToPrimitive;
2243 if ticks.is_empty() {
2244 return None;
2245 }
2246 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
2247 if total.is_zero() {
2248 return None;
2249 }
2250 (Self::buy_volume(ticks) / total).to_f64()
2251 }
2252
2253 pub fn avg_latency_ms(ticks: &[NormalizedTick]) -> Option<f64> {
2258 let latencies: Vec<i64> = ticks.iter().filter_map(|t| t.latency_ms()).collect();
2259 if latencies.is_empty() {
2260 return None;
2261 }
2262 Some(latencies.iter().sum::<i64>() as f64 / latencies.len() as f64)
2263 }
2264
2265 pub fn price_gini(ticks: &[NormalizedTick]) -> Option<f64> {
2271 use rust_decimal::prelude::ToPrimitive;
2272 if ticks.is_empty() {
2273 return None;
2274 }
2275 let mut prices: Vec<f64> = ticks.iter().filter_map(|t| t.price.to_f64()).collect();
2276 if prices.is_empty() {
2277 return None;
2278 }
2279 prices.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2280 let n = prices.len() as f64;
2281 let sum: f64 = prices.iter().sum();
2282 if sum == 0.0 {
2283 return None;
2284 }
2285 let weighted_sum: f64 = prices
2286 .iter()
2287 .enumerate()
2288 .map(|(i, &p)| (2.0 * (i + 1) as f64 - n - 1.0) * p)
2289 .sum();
2290 Some(weighted_sum / (n * sum))
2291 }
2292
2293 pub fn trade_velocity(ticks: &[NormalizedTick]) -> Option<f64> {
2298 let span_ms = Self::time_span_ms(ticks)?;
2299 if span_ms == 0 {
2300 return None;
2301 }
2302 Some(ticks.len() as f64 / span_ms as f64)
2303 }
2304
2305 pub fn floor_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
2312 Self::min_price(ticks)
2313 }
2314
2315 pub fn price_momentum_score(ticks: &[NormalizedTick]) -> Option<f64> {
2319 use rust_decimal::prelude::ToPrimitive;
2320 if ticks.len() < 2 {
2321 return None;
2322 }
2323 let mut num = 0f64;
2324 let mut den = 0f64;
2325 for w in ticks.windows(2) {
2326 let dp = (w[1].price - w[0].price).to_f64()?;
2327 let q = w[1].quantity.to_f64()?;
2328 num += dp * q;
2329 den += q;
2330 }
2331 if den == 0.0 { None } else { Some(num / den) }
2332 }
2333
2334 pub fn vwap_std(ticks: &[NormalizedTick]) -> Option<f64> {
2336 use rust_decimal::prelude::ToPrimitive;
2337 if ticks.len() < 2 {
2338 return None;
2339 }
2340 let vwap = Self::vwap(ticks)?.to_f64()?;
2341 let total_vol: f64 = ticks.iter().filter_map(|t| t.quantity.to_f64()).sum();
2342 if total_vol == 0.0 {
2343 return None;
2344 }
2345 let var: f64 = ticks
2346 .iter()
2347 .filter_map(|t| {
2348 let p = t.price.to_f64()?;
2349 let q = t.quantity.to_f64()?;
2350 Some((p - vwap).powi(2) * q)
2351 })
2352 .sum::<f64>()
2353 / total_vol;
2354 Some(var.sqrt())
2355 }
2356
2357 pub fn price_range_expansion(ticks: &[NormalizedTick]) -> Option<f64> {
2359 if ticks.is_empty() {
2360 return None;
2361 }
2362 let mut hi = ticks[0].price;
2363 let mut lo = ticks[0].price;
2364 let mut count = 0usize;
2365 for t in ticks.iter().skip(1) {
2366 if t.price > hi {
2367 hi = t.price;
2368 count += 1;
2369 } else if t.price < lo {
2370 lo = t.price;
2371 count += 1;
2372 }
2373 }
2374 Some(count as f64 / ticks.len() as f64)
2375 }
2376
2377 pub fn sell_to_total_volume_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2379 use rust_decimal::prelude::ToPrimitive;
2380 if ticks.is_empty() {
2381 return None;
2382 }
2383 let sell_vol: Decimal = ticks
2384 .iter()
2385 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
2386 .map(|t| t.quantity)
2387 .sum();
2388 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
2389 if total.is_zero() {
2390 return Some(0.0);
2391 }
2392 sell_vol.to_f64().zip(total.to_f64()).map(|(s, tot)| s / tot)
2393 }
2394
2395 pub fn notional_std(ticks: &[NormalizedTick]) -> Option<f64> {
2397 use rust_decimal::prelude::ToPrimitive;
2398 if ticks.len() < 2 {
2399 return None;
2400 }
2401 let notionals: Vec<f64> = ticks
2402 .iter()
2403 .filter_map(|t| (t.price * t.quantity).to_f64())
2404 .collect();
2405 let n = notionals.len() as f64;
2406 if n < 2.0 {
2407 return None;
2408 }
2409 let mean = notionals.iter().sum::<f64>() / n;
2410 let var = notionals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2411 Some(var.sqrt())
2412 }
2413
2414 pub fn quantity_autocorrelation(ticks: &[NormalizedTick]) -> Option<f64> {
2421 use rust_decimal::prelude::ToPrimitive;
2422 if ticks.len() < 3 {
2423 return None;
2424 }
2425 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
2426 let n = vals.len() as f64;
2427 let mean = vals.iter().sum::<f64>() / n;
2428 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
2429 if var == 0.0 {
2430 return None;
2431 }
2432 let cov = vals.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>() / n;
2433 Some(cov / var)
2434 }
2435
2436 pub fn fraction_above_vwap(ticks: &[NormalizedTick]) -> Option<f64> {
2440 let vwap = Self::vwap(ticks)?;
2441 if ticks.is_empty() {
2442 return None;
2443 }
2444 let above = ticks.iter().filter(|t| t.price > vwap).count();
2445 Some(above as f64 / ticks.len() as f64)
2446 }
2447
2448 pub fn max_buy_streak(ticks: &[NormalizedTick]) -> usize {
2452 let mut max = 0usize;
2453 let mut current = 0usize;
2454 for t in ticks {
2455 if t.side == Some(TradeSide::Buy) {
2456 current += 1;
2457 if current > max {
2458 max = current;
2459 }
2460 } else {
2461 current = 0;
2462 }
2463 }
2464 max
2465 }
2466
2467 pub fn max_sell_streak(ticks: &[NormalizedTick]) -> usize {
2471 let mut max = 0usize;
2472 let mut current = 0usize;
2473 for t in ticks {
2474 if t.side == Some(TradeSide::Sell) {
2475 current += 1;
2476 if current > max {
2477 max = current;
2478 }
2479 } else {
2480 current = 0;
2481 }
2482 }
2483 max
2484 }
2485
2486 pub fn side_entropy(ticks: &[NormalizedTick]) -> Option<f64> {
2492 if ticks.is_empty() {
2493 return None;
2494 }
2495 let n = ticks.len() as f64;
2496 let buys = Self::buy_count(ticks) as f64;
2497 let sells = Self::sell_count(ticks) as f64;
2498 let neutrals = Self::count_neutral(ticks) as f64;
2499 let entropy = [buys, sells, neutrals]
2500 .iter()
2501 .filter(|&&c| c > 0.0)
2502 .map(|&c| {
2503 let p = c / n;
2504 -p * p.ln()
2505 })
2506 .sum::<f64>();
2507 Some(entropy)
2508 }
2509
2510 pub fn mean_inter_tick_gap_ms(ticks: &[NormalizedTick]) -> Option<f64> {
2515 if ticks.len() < 2 {
2516 return None;
2517 }
2518 let gaps: Vec<f64> = ticks
2519 .windows(2)
2520 .map(|w| w[1].received_at_ms.saturating_sub(w[0].received_at_ms) as f64)
2521 .collect();
2522 let mean = gaps.iter().sum::<f64>() / gaps.len() as f64;
2523 Some(mean)
2524 }
2525
2526 pub fn round_number_fraction(ticks: &[NormalizedTick], step: Decimal) -> Option<f64> {
2530 if ticks.is_empty() || step.is_zero() {
2531 return None;
2532 }
2533 let round = ticks.iter().filter(|t| (t.price % step).is_zero()).count();
2534 Some(round as f64 / ticks.len() as f64)
2535 }
2536
2537 pub fn geometric_mean_quantity(ticks: &[NormalizedTick]) -> Option<f64> {
2542 use rust_decimal::prelude::ToPrimitive;
2543 if ticks.is_empty() {
2544 return None;
2545 }
2546 let log_sum: f64 = ticks
2547 .iter()
2548 .map(|t| {
2549 let q = t.quantity.to_f64()?;
2550 if q <= 0.0 { None } else { Some(q.ln()) }
2551 })
2552 .try_fold(0.0f64, |acc, v| v.map(|x| acc + x))?;
2553 Some((log_sum / ticks.len() as f64).exp())
2554 }
2555
2556 pub fn max_tick_return(ticks: &[NormalizedTick]) -> Option<f64> {
2560 use rust_decimal::prelude::ToPrimitive;
2561 if ticks.len() < 2 {
2562 return None;
2563 }
2564 ticks
2565 .windows(2)
2566 .filter_map(|w| {
2567 let prev = w[0].price.to_f64()?;
2568 if prev == 0.0 { return None; }
2569 let curr = w[1].price.to_f64()?;
2570 Some((curr - prev) / prev)
2571 })
2572 .reduce(f64::max)
2573 }
2574
2575 pub fn min_tick_return(ticks: &[NormalizedTick]) -> Option<f64> {
2579 use rust_decimal::prelude::ToPrimitive;
2580 if ticks.len() < 2 {
2581 return None;
2582 }
2583 ticks
2584 .windows(2)
2585 .filter_map(|w| {
2586 let prev = w[0].price.to_f64()?;
2587 if prev == 0.0 { return None; }
2588 let curr = w[1].price.to_f64()?;
2589 Some((curr - prev) / prev)
2590 })
2591 .reduce(f64::min)
2592 }
2593
2594 pub fn buy_price_mean(ticks: &[NormalizedTick]) -> Option<Decimal> {
2598 let buys: Vec<Decimal> = ticks
2599 .iter()
2600 .filter(|t| t.side == Some(crate::tick::TradeSide::Buy))
2601 .map(|t| t.price)
2602 .collect();
2603 if buys.is_empty() {
2604 return None;
2605 }
2606 Some(buys.iter().copied().sum::<Decimal>() / Decimal::from(buys.len()))
2607 }
2608
2609 pub fn sell_price_mean(ticks: &[NormalizedTick]) -> Option<Decimal> {
2611 let sells: Vec<Decimal> = ticks
2612 .iter()
2613 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
2614 .map(|t| t.price)
2615 .collect();
2616 if sells.is_empty() {
2617 return None;
2618 }
2619 Some(sells.iter().copied().sum::<Decimal>() / Decimal::from(sells.len()))
2620 }
2621
2622 pub fn price_efficiency(ticks: &[NormalizedTick]) -> Option<f64> {
2624 use rust_decimal::prelude::ToPrimitive;
2625 if ticks.len() < 2 {
2626 return None;
2627 }
2628 let total_path: Decimal = ticks
2629 .windows(2)
2630 .map(|w| (w[1].price - w[0].price).abs())
2631 .sum();
2632 if total_path.is_zero() {
2633 return None;
2634 }
2635 let net = (ticks.last()?.price - ticks.first()?.price).abs();
2636 (net / total_path).to_f64()
2637 }
2638
2639 pub fn price_return_skewness(ticks: &[NormalizedTick]) -> Option<f64> {
2641 use rust_decimal::prelude::ToPrimitive;
2642 if ticks.len() < 5 {
2643 return None;
2644 }
2645 let returns: Vec<f64> = ticks
2646 .windows(2)
2647 .filter_map(|w| {
2648 let prev = w[0].price.to_f64()?;
2649 if prev <= 0.0 { return None; }
2650 let curr = w[1].price.to_f64()?;
2651 Some((curr / prev).ln())
2652 })
2653 .collect();
2654 let n = returns.len() as f64;
2655 if n < 4.0 {
2656 return None;
2657 }
2658 let mean = returns.iter().sum::<f64>() / n;
2659 let var = returns.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
2660 let std = var.sqrt();
2661 if std == 0.0 {
2662 return None;
2663 }
2664 let skew = returns.iter().map(|v| ((v - mean) / std).powi(3)).sum::<f64>() / n;
2665 Some(skew)
2666 }
2667
2668 pub fn buy_sell_vwap_spread(ticks: &[NormalizedTick]) -> Option<f64> {
2670 use rust_decimal::prelude::ToPrimitive;
2671 let (buy_pv, buy_v): (Decimal, Decimal) = ticks
2672 .iter()
2673 .filter(|t| t.side == Some(crate::tick::TradeSide::Buy))
2674 .fold((Decimal::ZERO, Decimal::ZERO), |(pv, v), t| {
2675 (pv + t.price * t.quantity, v + t.quantity)
2676 });
2677 let (sell_pv, sell_v): (Decimal, Decimal) = ticks
2678 .iter()
2679 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
2680 .fold((Decimal::ZERO, Decimal::ZERO), |(pv, v), t| {
2681 (pv + t.price * t.quantity, v + t.quantity)
2682 });
2683 if buy_v.is_zero() || sell_v.is_zero() {
2684 return None;
2685 }
2686 let buy_vwap = (buy_pv / buy_v).to_f64()?;
2687 let sell_vwap = (sell_pv / sell_v).to_f64()?;
2688 Some(buy_vwap - sell_vwap)
2689 }
2690
2691 pub fn above_mean_quantity_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2693 if ticks.is_empty() {
2694 return None;
2695 }
2696 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
2697 let mean = total / Decimal::from(ticks.len());
2698 let count = ticks.iter().filter(|t| t.quantity > mean).count();
2699 Some(count as f64 / ticks.len() as f64)
2700 }
2701
2702 pub fn price_unchanged_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2704 if ticks.len() < 2 {
2705 return None;
2706 }
2707 let unchanged = ticks.windows(2).filter(|w| w[0].price == w[1].price).count();
2708 Some(unchanged as f64 / (ticks.len() - 1) as f64)
2709 }
2710
2711 pub fn qty_weighted_range(ticks: &[NormalizedTick]) -> Option<f64> {
2714 use rust_decimal::prelude::ToPrimitive;
2715 if ticks.is_empty() {
2716 return None;
2717 }
2718 let hi = ticks.iter().map(|t| t.price).max()?;
2719 let lo = ticks.iter().map(|t| t.price).min()?;
2720 (hi - lo).to_f64()
2721 }
2722
2723 pub fn sell_notional_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2727 use rust_decimal::prelude::ToPrimitive;
2728 if ticks.is_empty() {
2729 return None;
2730 }
2731 let total: Decimal = ticks.iter().map(|t| t.price * t.quantity).sum();
2732 if total.is_zero() {
2733 return Some(0.0);
2734 }
2735 let sell_notional: Decimal = ticks
2736 .iter()
2737 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
2738 .map(|t| t.price * t.quantity)
2739 .sum();
2740 sell_notional.to_f64().zip(total.to_f64()).map(|(s, t)| s / t)
2741 }
2742
2743 pub fn max_price_gap(ticks: &[NormalizedTick]) -> Option<Decimal> {
2745 if ticks.len() < 2 {
2746 return None;
2747 }
2748 ticks.windows(2).map(|w| (w[1].price - w[0].price).abs()).max()
2749 }
2750
2751 pub fn price_range_velocity(ticks: &[NormalizedTick]) -> Option<f64> {
2753 use rust_decimal::prelude::ToPrimitive;
2754 if ticks.len() < 2 {
2755 return None;
2756 }
2757 let time_span = ticks.last()?.received_at_ms.saturating_sub(ticks.first()?.received_at_ms);
2758 if time_span == 0 {
2759 return None;
2760 }
2761 let hi = ticks.iter().map(|t| t.price).max()?;
2762 let lo = ticks.iter().map(|t| t.price).min()?;
2763 let range = (hi - lo).to_f64()?;
2764 Some(range / time_span as f64)
2765 }
2766
2767 pub fn tick_count_per_ms(ticks: &[NormalizedTick]) -> Option<f64> {
2769 if ticks.len() < 2 {
2770 return None;
2771 }
2772 let span = ticks.last()?.received_at_ms.saturating_sub(ticks.first()?.received_at_ms);
2773 if span == 0 {
2774 return None;
2775 }
2776 Some(ticks.len() as f64 / span as f64)
2777 }
2778
2779 pub fn buy_quantity_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2781 use rust_decimal::prelude::ToPrimitive;
2782 if ticks.is_empty() {
2783 return None;
2784 }
2785 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
2786 if total.is_zero() {
2787 return Some(0.0);
2788 }
2789 let buy_qty: Decimal = ticks
2790 .iter()
2791 .filter(|t| t.side == Some(crate::tick::TradeSide::Buy))
2792 .map(|t| t.quantity)
2793 .sum();
2794 buy_qty.to_f64().zip(total.to_f64()).map(|(b, tot)| b / tot)
2795 }
2796
2797 pub fn sell_quantity_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2799 use rust_decimal::prelude::ToPrimitive;
2800 if ticks.is_empty() {
2801 return None;
2802 }
2803 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
2804 if total.is_zero() {
2805 return Some(0.0);
2806 }
2807 let sell_qty: Decimal = ticks
2808 .iter()
2809 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
2810 .map(|t| t.quantity)
2811 .sum();
2812 sell_qty.to_f64().zip(total.to_f64()).map(|(s, tot)| s / tot)
2813 }
2814
2815 pub fn price_mean_crossover_count(ticks: &[NormalizedTick]) -> Option<usize> {
2817 use rust_decimal::prelude::ToPrimitive;
2818 if ticks.len() < 2 {
2819 return None;
2820 }
2821 let sum: Decimal = ticks.iter().map(|t| t.price).sum();
2822 let mean = sum / Decimal::from(ticks.len() as i64);
2823 let crossovers = ticks
2824 .windows(2)
2825 .filter(|w| {
2826 let prev = w[0].price - mean;
2827 let curr = w[1].price - mean;
2828 prev.is_sign_negative() != curr.is_sign_negative()
2829 })
2830 .count();
2831 let _ = mean.to_f64(); Some(crossovers)
2833 }
2834
2835 pub fn notional_skewness(ticks: &[NormalizedTick]) -> Option<f64> {
2837 use rust_decimal::prelude::ToPrimitive;
2838 if ticks.len() < 3 {
2839 return None;
2840 }
2841 let vals: Vec<f64> = ticks
2842 .iter()
2843 .filter_map(|t| (t.price * t.quantity).to_f64())
2844 .collect();
2845 let n = vals.len() as f64;
2846 if n < 3.0 {
2847 return None;
2848 }
2849 let mean = vals.iter().sum::<f64>() / n;
2850 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
2851 let std = var.sqrt();
2852 if std < 1e-12 {
2853 return None;
2854 }
2855 let skew = vals.iter().map(|v| ((v - mean) / std).powi(3)).sum::<f64>() / n;
2856 Some(skew)
2857 }
2858
2859 pub fn volume_weighted_mid_price(ticks: &[NormalizedTick]) -> Option<Decimal> {
2862 if ticks.is_empty() {
2863 return None;
2864 }
2865 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
2866 if total_qty.is_zero() {
2867 return None;
2868 }
2869 let pv: Decimal = ticks.iter().map(|t| t.price * t.quantity).sum();
2870 Some(pv / total_qty)
2871 }
2872
2873 pub fn neutral_count(ticks: &[NormalizedTick]) -> usize {
2877 ticks.iter().filter(|t| t.side.is_none()).count()
2878 }
2879
2880 pub fn price_dispersion(ticks: &[NormalizedTick]) -> Option<Decimal> {
2882 if ticks.is_empty() {
2883 return None;
2884 }
2885 let hi = ticks.iter().map(|t| t.price).max()?;
2886 let lo = ticks.iter().map(|t| t.price).min()?;
2887 Some(hi - lo)
2888 }
2889
2890 pub fn max_notional(ticks: &[NormalizedTick]) -> Option<Decimal> {
2892 ticks.iter().map(|t| t.price * t.quantity).max()
2893 }
2894
2895 pub fn min_notional(ticks: &[NormalizedTick]) -> Option<Decimal> {
2897 ticks.iter().map(|t| t.price * t.quantity).min()
2898 }
2899
2900 pub fn below_vwap_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
2902 if ticks.is_empty() {
2903 return None;
2904 }
2905 let vwap = Self::vwap(ticks)?;
2906 let count = ticks.iter().filter(|t| t.price < vwap).count();
2907 Some(count as f64 / ticks.len() as f64)
2908 }
2909
2910 pub fn trade_notional_std(ticks: &[NormalizedTick]) -> Option<f64> {
2915 use rust_decimal::prelude::ToPrimitive;
2916 if ticks.len() < 2 {
2917 return None;
2918 }
2919 let vals: Vec<f64> = ticks
2920 .iter()
2921 .filter_map(|t| (t.price * t.quantity).to_f64())
2922 .collect();
2923 let n = vals.len() as f64;
2924 if n < 2.0 {
2925 return None;
2926 }
2927 let mean = vals.iter().sum::<f64>() / n;
2928 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2929 Some(var.sqrt())
2930 }
2931
2932 pub fn buy_sell_count_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
2934 let sells = Self::sell_count(ticks);
2935 if sells == 0 {
2936 return None;
2937 }
2938 Some(Self::buy_count(ticks) as f64 / sells as f64)
2939 }
2940
2941 pub fn price_mad(ticks: &[NormalizedTick]) -> Option<f64> {
2943 use rust_decimal::prelude::ToPrimitive;
2944 if ticks.is_empty() {
2945 return None;
2946 }
2947 let sum: Decimal = ticks.iter().map(|t| t.price).sum();
2948 let mean = sum / Decimal::from(ticks.len() as i64);
2949 let mad: f64 = ticks
2950 .iter()
2951 .filter_map(|t| (t.price - mean).abs().to_f64())
2952 .sum::<f64>() / ticks.len() as f64;
2953 Some(mad)
2954 }
2955
2956 pub fn price_range_pct_of_open(ticks: &[NormalizedTick]) -> Option<f64> {
2958 use rust_decimal::prelude::ToPrimitive;
2959 if ticks.is_empty() {
2960 return None;
2961 }
2962 let first_price = ticks.first()?.price;
2963 if first_price.is_zero() {
2964 return None;
2965 }
2966 let hi = ticks.iter().map(|t| t.price).max()?;
2967 let lo = ticks.iter().map(|t| t.price).min()?;
2968 ((hi - lo) / first_price).to_f64()
2969 }
2970
2971 pub fn price_mean(ticks: &[NormalizedTick]) -> Option<Decimal> {
2975 if ticks.is_empty() {
2976 return None;
2977 }
2978 let sum: Decimal = ticks.iter().map(|t| t.price).sum();
2979 Some(sum / Decimal::from(ticks.len() as i64))
2980 }
2981
2982 pub fn uptick_count(ticks: &[NormalizedTick]) -> usize {
2984 if ticks.len() < 2 {
2985 return 0;
2986 }
2987 ticks.windows(2).filter(|w| w[1].price > w[0].price).count()
2988 }
2989
2990 pub fn downtick_count(ticks: &[NormalizedTick]) -> usize {
2992 if ticks.len() < 2 {
2993 return 0;
2994 }
2995 ticks.windows(2).filter(|w| w[1].price < w[0].price).count()
2996 }
2997
2998 pub fn uptick_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
3000 if ticks.len() < 2 {
3001 return None;
3002 }
3003 let intervals = (ticks.len() - 1) as f64;
3004 Some(Self::uptick_count(ticks) as f64 / intervals)
3005 }
3006
3007 pub fn quantity_std(ticks: &[NormalizedTick]) -> Option<f64> {
3009 use rust_decimal::prelude::ToPrimitive;
3010 if ticks.len() < 2 {
3011 return None;
3012 }
3013 let vals: Vec<f64> = ticks.iter().filter_map(|t| t.quantity.to_f64()).collect();
3014 let n = vals.len() as f64;
3015 if n < 2.0 {
3016 return None;
3017 }
3018 let mean = vals.iter().sum::<f64>() / n;
3019 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
3020 Some(var.sqrt())
3021 }
3022
3023 pub fn vwap_deviation_std(ticks: &[NormalizedTick]) -> Option<f64> {
3029 use rust_decimal::prelude::ToPrimitive;
3030 if ticks.len() < 2 {
3031 return None;
3032 }
3033 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
3034 if total_qty.is_zero() {
3035 return None;
3036 }
3037 let vwap = ticks
3038 .iter()
3039 .map(|t| t.price * t.quantity)
3040 .sum::<Decimal>()
3041 / total_qty;
3042 let deviations: Vec<f64> = ticks
3043 .iter()
3044 .filter_map(|t| (t.price - vwap).to_f64())
3045 .collect();
3046 let n = deviations.len() as f64;
3047 if n < 2.0 {
3048 return None;
3049 }
3050 let mean_dev = deviations.iter().sum::<f64>() / n;
3051 let var = deviations
3052 .iter()
3053 .map(|d| (d - mean_dev).powi(2))
3054 .sum::<f64>()
3055 / (n - 1.0);
3056 Some(var.sqrt())
3057 }
3058
3059 pub fn max_consecutive_side_run(ticks: &[NormalizedTick]) -> usize {
3062 let mut max_run = 0usize;
3063 let mut current_run = 0usize;
3064 let mut last_side: Option<TradeSide> = None;
3065 for t in ticks {
3066 if let Some(side) = t.side {
3067 if Some(side) == last_side {
3068 current_run += 1;
3069 } else {
3070 current_run = 1;
3071 last_side = Some(side);
3072 }
3073 if current_run > max_run {
3074 max_run = current_run;
3075 }
3076 }
3077 }
3078 max_run
3079 }
3080
3081 pub fn inter_arrival_cv(ticks: &[NormalizedTick]) -> Option<f64> {
3085 if ticks.len() < 2 {
3086 return None;
3087 }
3088 let intervals: Vec<f64> = ticks
3089 .windows(2)
3090 .filter_map(|w| {
3091 let dt = w[1].received_at_ms.checked_sub(w[0].received_at_ms)?;
3092 Some(dt as f64)
3093 })
3094 .collect();
3095 if intervals.len() < 2 {
3096 return None;
3097 }
3098 let n = intervals.len() as f64;
3099 let mean = intervals.iter().sum::<f64>() / n;
3100 if mean == 0.0 {
3101 return None;
3102 }
3103 let var = intervals
3104 .iter()
3105 .map(|v| (v - mean).powi(2))
3106 .sum::<f64>()
3107 / (n - 1.0);
3108 Some(var.sqrt() / mean)
3109 }
3110
3111 pub fn volume_per_ms(ticks: &[NormalizedTick]) -> Option<f64> {
3114 use rust_decimal::prelude::ToPrimitive;
3115 if ticks.len() < 2 {
3116 return None;
3117 }
3118 let first_ms = ticks.first()?.received_at_ms;
3119 let last_ms = ticks.last()?.received_at_ms;
3120 let elapsed = last_ms.checked_sub(first_ms)? as f64;
3121 if elapsed == 0.0 {
3122 return None;
3123 }
3124 let total_qty: f64 = ticks
3125 .iter()
3126 .filter_map(|t| t.quantity.to_f64())
3127 .sum();
3128 Some(total_qty / elapsed)
3129 }
3130
3131 pub fn notional_per_second(ticks: &[NormalizedTick]) -> Option<f64> {
3134 use rust_decimal::prelude::ToPrimitive;
3135 if ticks.len() < 2 {
3136 return None;
3137 }
3138 let first_ms = ticks.first()?.received_at_ms;
3139 let last_ms = ticks.last()?.received_at_ms;
3140 let elapsed_sec = last_ms.checked_sub(first_ms)? as f64 / 1000.0;
3141 if elapsed_sec == 0.0 {
3142 return None;
3143 }
3144 let total_notional: f64 = ticks
3145 .iter()
3146 .filter_map(|t| (t.price * t.quantity).to_f64())
3147 .sum();
3148 Some(total_notional / elapsed_sec)
3149 }
3150
3151 pub fn order_flow_imbalance(ticks: &[NormalizedTick]) -> Option<f64> {
3158 use rust_decimal::prelude::ToPrimitive;
3159 if ticks.is_empty() {
3160 return None;
3161 }
3162 let buy_qty: Decimal = ticks
3163 .iter()
3164 .filter(|t| t.side == Some(crate::tick::TradeSide::Buy))
3165 .map(|t| t.quantity)
3166 .sum();
3167 let sell_qty: Decimal = ticks
3168 .iter()
3169 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
3170 .map(|t| t.quantity)
3171 .sum();
3172 let total: Decimal = ticks.iter().map(|t| t.quantity).sum();
3173 if total.is_zero() {
3174 return None;
3175 }
3176 (buy_qty - sell_qty).to_f64().zip(total.to_f64()).map(|(n, d)| n / d)
3177 }
3178
3179 pub fn price_qty_up_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
3183 if ticks.len() < 2 {
3184 return None;
3185 }
3186 let count = ticks
3187 .windows(2)
3188 .filter(|w| w[1].price > w[0].price && w[1].quantity > w[0].quantity)
3189 .count();
3190 Some(count as f64 / (ticks.len() - 1) as f64)
3191 }
3192
3193 pub fn running_high_count(ticks: &[NormalizedTick]) -> usize {
3195 if ticks.is_empty() {
3196 return 0;
3197 }
3198 let mut hi = ticks[0].price;
3199 let mut count = 1usize;
3200 for t in ticks.iter().skip(1) {
3201 if t.price >= hi {
3202 hi = t.price;
3203 count += 1;
3204 }
3205 }
3206 count
3207 }
3208
3209 pub fn running_low_count(ticks: &[NormalizedTick]) -> usize {
3211 if ticks.is_empty() {
3212 return 0;
3213 }
3214 let mut lo = ticks[0].price;
3215 let mut count = 1usize;
3216 for t in ticks.iter().skip(1) {
3217 if t.price <= lo {
3218 lo = t.price;
3219 count += 1;
3220 }
3221 }
3222 count
3223 }
3224
3225 pub fn buy_sell_avg_qty_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
3229 use rust_decimal::prelude::ToPrimitive;
3230 let buys: Vec<Decimal> = ticks
3231 .iter()
3232 .filter(|t| t.side == Some(crate::tick::TradeSide::Buy))
3233 .map(|t| t.quantity)
3234 .collect();
3235 let sells: Vec<Decimal> = ticks
3236 .iter()
3237 .filter(|t| t.side == Some(crate::tick::TradeSide::Sell))
3238 .map(|t| t.quantity)
3239 .collect();
3240 if buys.is_empty() || sells.is_empty() {
3241 return None;
3242 }
3243 let buy_mean = buys.iter().copied().sum::<Decimal>() / Decimal::from(buys.len() as i64);
3244 let sell_mean = sells.iter().copied().sum::<Decimal>() / Decimal::from(sells.len() as i64);
3245 if sell_mean.is_zero() {
3246 return None;
3247 }
3248 (buy_mean / sell_mean).to_f64()
3249 }
3250
3251 pub fn max_price_drop(ticks: &[NormalizedTick]) -> Option<Decimal> {
3255 if ticks.len() < 2 {
3256 return None;
3257 }
3258 ticks
3259 .windows(2)
3260 .map(|w| (w[0].price - w[1].price).max(Decimal::ZERO))
3261 .max()
3262 }
3263
3264 pub fn max_price_rise(ticks: &[NormalizedTick]) -> Option<Decimal> {
3268 if ticks.len() < 2 {
3269 return None;
3270 }
3271 ticks
3272 .windows(2)
3273 .map(|w| (w[1].price - w[0].price).max(Decimal::ZERO))
3274 .max()
3275 }
3276
3277 pub fn buy_trade_count(ticks: &[NormalizedTick]) -> usize {
3279 ticks
3280 .iter()
3281 .filter(|t| t.side == Some(TradeSide::Buy))
3282 .count()
3283 }
3284
3285 pub fn sell_trade_count(ticks: &[NormalizedTick]) -> usize {
3287 ticks
3288 .iter()
3289 .filter(|t| t.side == Some(TradeSide::Sell))
3290 .count()
3291 }
3292
3293 pub fn price_reversal_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
3297 if ticks.len() < 3 {
3298 return None;
3299 }
3300 let reversals = ticks
3301 .windows(3)
3302 .filter(|w| {
3303 let up1 = w[1].price > w[0].price;
3304 let up2 = w[2].price > w[1].price;
3305 up1 != up2
3306 })
3307 .count();
3308 Some(reversals as f64 / (ticks.len() - 2) as f64)
3309 }
3310
3311 pub fn near_vwap_fraction(ticks: &[NormalizedTick], band: Decimal) -> Option<f64> {
3317 if ticks.is_empty() {
3318 return None;
3319 }
3320 let vwap = Self::vwap(ticks)?;
3321 let count = ticks.iter().filter(|t| (t.price - vwap).abs() <= band).count();
3322 Some(count as f64 / ticks.len() as f64)
3323 }
3324
3325 pub fn mean_tick_return(ticks: &[NormalizedTick]) -> Option<f64> {
3329 use rust_decimal::prelude::ToPrimitive;
3330 if ticks.len() < 2 {
3331 return None;
3332 }
3333 let returns: Vec<f64> = ticks
3334 .windows(2)
3335 .filter_map(|w| {
3336 let prev = w[0].price.to_f64()?;
3337 if prev == 0.0 { return None; }
3338 let curr = w[1].price.to_f64()?;
3339 Some((curr - prev) / prev)
3340 })
3341 .collect();
3342 if returns.is_empty() {
3343 return None;
3344 }
3345 Some(returns.iter().sum::<f64>() / returns.len() as f64)
3346 }
3347
3348 pub fn passive_buy_count(ticks: &[NormalizedTick]) -> usize {
3352 let vwap = match Self::vwap(ticks) {
3353 Some(v) => v,
3354 None => return 0,
3355 };
3356 ticks
3357 .iter()
3358 .filter(|t| t.side == Some(TradeSide::Buy) && t.price < vwap)
3359 .count()
3360 }
3361
3362 pub fn passive_sell_count(ticks: &[NormalizedTick]) -> usize {
3366 let vwap = match Self::vwap(ticks) {
3367 Some(v) => v,
3368 None => return 0,
3369 };
3370 ticks
3371 .iter()
3372 .filter(|t| t.side == Some(TradeSide::Sell) && t.price > vwap)
3373 .count()
3374 }
3375
3376 pub fn quantity_iqr(ticks: &[NormalizedTick]) -> Option<Decimal> {
3380 if ticks.len() < 4 {
3381 return None;
3382 }
3383 let mut qtys: Vec<Decimal> = ticks.iter().map(|t| t.quantity).collect();
3384 qtys.sort();
3385 let n = qtys.len();
3386 let q1 = qtys[n / 4];
3387 let q3 = qtys[(3 * n) / 4];
3388 Some(q3 - q1)
3389 }
3390
3391 pub fn top_quartile_price_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
3395 if ticks.len() < 4 {
3396 return None;
3397 }
3398 let mut prices: Vec<Decimal> = ticks.iter().map(|t| t.price).collect();
3399 prices.sort();
3400 let q3 = prices[(3 * prices.len()) / 4];
3401 let count = ticks.iter().filter(|t| t.price > q3).count();
3402 Some(count as f64 / ticks.len() as f64)
3403 }
3404
3405 pub fn buy_notional_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
3408 use rust_decimal::prelude::ToPrimitive;
3409 if ticks.is_empty() {
3410 return None;
3411 }
3412 let total_notional: Decimal = ticks.iter().map(|t| t.price * t.quantity).sum();
3413 if total_notional.is_zero() {
3414 return None;
3415 }
3416 let buy_notional: Decimal = ticks
3417 .iter()
3418 .filter(|t| t.side == Some(TradeSide::Buy))
3419 .map(|t| t.price * t.quantity)
3420 .sum();
3421 (buy_notional / total_notional).to_f64()
3422 }
3423
3424 pub fn return_std(ticks: &[NormalizedTick]) -> Option<f64> {
3427 use rust_decimal::prelude::ToPrimitive;
3428 if ticks.len() < 3 {
3429 return None;
3430 }
3431 let returns: Vec<f64> = ticks
3432 .windows(2)
3433 .filter_map(|w| {
3434 let prev = w[0].price.to_f64()?;
3435 if prev == 0.0 { return None; }
3436 Some((w[1].price.to_f64()? - prev) / prev)
3437 })
3438 .collect();
3439 if returns.len() < 2 {
3440 return None;
3441 }
3442 let n = returns.len() as f64;
3443 let mean = returns.iter().sum::<f64>() / n;
3444 let var = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
3445 Some(var.sqrt())
3446 }
3447
3448 pub fn max_drawdown(ticks: &[NormalizedTick]) -> Option<f64> {
3454 use rust_decimal::prelude::ToPrimitive;
3455 if ticks.is_empty() {
3456 return None;
3457 }
3458 let mut peak = ticks[0].price;
3459 let mut max_dd = Decimal::ZERO;
3460 for t in ticks {
3461 if t.price > peak {
3462 peak = t.price;
3463 }
3464 let dd = peak - t.price;
3465 if dd > max_dd {
3466 max_dd = dd;
3467 }
3468 }
3469 if peak.is_zero() {
3470 return None;
3471 }
3472 (max_dd / peak).to_f64()
3473 }
3474
3475 pub fn high_to_low_ratio(ticks: &[NormalizedTick]) -> Option<f64> {
3479 use rust_decimal::prelude::ToPrimitive;
3480 if ticks.is_empty() {
3481 return None;
3482 }
3483 let high = ticks.iter().map(|t| t.price).max()?;
3484 let low = ticks.iter().map(|t| t.price).min()?;
3485 if low.is_zero() {
3486 return None;
3487 }
3488 (high / low).to_f64()
3489 }
3490
3491 pub fn tick_velocity(ticks: &[NormalizedTick]) -> Option<f64> {
3495 if ticks.len() < 2 {
3496 return None;
3497 }
3498 let first_ms = ticks.first()?.received_at_ms;
3499 let last_ms = ticks.last()?.received_at_ms;
3500 let span = last_ms.saturating_sub(first_ms);
3501 if span == 0 {
3502 return None;
3503 }
3504 Some(ticks.len() as f64 / span as f64)
3505 }
3506
3507 pub fn notional_decay(ticks: &[NormalizedTick]) -> Option<f64> {
3512 use rust_decimal::prelude::ToPrimitive;
3513 if ticks.len() < 2 {
3514 return None;
3515 }
3516 let mid = ticks.len() / 2;
3517 let first_half: Decimal = ticks[..mid].iter().map(|t| t.price * t.quantity).sum();
3518 let second_half: Decimal = ticks[mid..].iter().map(|t| t.price * t.quantity).sum();
3519 if first_half.is_zero() {
3520 return None;
3521 }
3522 (second_half / first_half).to_f64()
3523 }
3524
3525 pub fn late_price_momentum(ticks: &[NormalizedTick]) -> Option<f64> {
3529 use rust_decimal::prelude::ToPrimitive;
3530 if ticks.len() < 2 {
3531 return None;
3532 }
3533 let mid = ticks.len() / 2;
3534 let n1 = mid as u32;
3535 let n2 = (ticks.len() - mid) as u32;
3536 if n1 == 0 || n2 == 0 {
3537 return None;
3538 }
3539 let mean1: Decimal = ticks[..mid].iter().map(|t| t.price).sum::<Decimal>()
3540 / Decimal::from(n1);
3541 let mean2: Decimal = ticks[mid..].iter().map(|t| t.price).sum::<Decimal>()
3542 / Decimal::from(n2);
3543 if mean1.is_zero() {
3544 return None;
3545 }
3546 ((mean2 - mean1) / mean1).to_f64()
3547 }
3548
3549 pub fn consecutive_buys_max(ticks: &[NormalizedTick]) -> usize {
3553 let mut max_run = 0usize;
3554 let mut run = 0usize;
3555 for t in ticks {
3556 if t.side == Some(TradeSide::Buy) {
3557 run += 1;
3558 if run > max_run {
3559 max_run = run;
3560 }
3561 } else {
3562 run = 0;
3563 }
3564 }
3565 max_run
3566 }
3567
3568 pub fn above_mean_qty_fraction(ticks: &[NormalizedTick]) -> Option<f64> {
3574 if ticks.is_empty() {
3575 return None;
3576 }
3577 let n = ticks.len() as u32;
3578 let mean_qty: Decimal =
3579 ticks.iter().map(|t| t.quantity).sum::<Decimal>() / Decimal::from(n);
3580 let count = ticks.iter().filter(|t| t.quantity > mean_qty).count();
3581 Some(count as f64 / ticks.len() as f64)
3582 }
3583
3584 pub fn side_alternation_rate(ticks: &[NormalizedTick]) -> Option<f64> {
3589 let sided: Vec<TradeSide> = ticks.iter().filter_map(|t| t.side).collect();
3590 if sided.len() < 2 {
3591 return None;
3592 }
3593 let alternations = sided.windows(2).filter(|w| w[0] != w[1]).count();
3594 Some(alternations as f64 / (sided.len() - 1) as f64)
3595 }
3596
3597 pub fn price_range_per_tick(ticks: &[NormalizedTick]) -> Option<f64> {
3601 use rust_decimal::prelude::ToPrimitive;
3602 if ticks.is_empty() {
3603 return None;
3604 }
3605 let high = ticks.iter().map(|t| t.price).max()?;
3606 let low = ticks.iter().map(|t| t.price).min()?;
3607 let range = (high - low).to_f64()?;
3608 Some(range / ticks.len() as f64)
3609 }
3610
3611 pub fn qty_weighted_price_std(ticks: &[NormalizedTick]) -> Option<f64> {
3615 use rust_decimal::prelude::ToPrimitive;
3616 if ticks.is_empty() {
3617 return None;
3618 }
3619 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
3620 if total_qty.is_zero() {
3621 return None;
3622 }
3623 let vwap: Decimal =
3624 ticks.iter().map(|t| t.price * t.quantity).sum::<Decimal>() / total_qty;
3625 let total_qty_f = total_qty.to_f64()?;
3626 let variance: f64 = ticks
3627 .iter()
3628 .filter_map(|t| {
3629 let diff = (t.price - vwap).to_f64()?;
3630 let w = t.quantity.to_f64()?;
3631 Some(w * diff * diff)
3632 })
3633 .sum::<f64>()
3634 / total_qty_f;
3635 Some(variance.sqrt())
3636 }
3637
3638}
3639
3640
3641impl std::fmt::Display for NormalizedTick {
3642 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3643 let side = match self.side {
3644 Some(s) => s.to_string(),
3645 None => "?".to_string(),
3646 };
3647 write!(
3648 f,
3649 "{} {} {} x {} {} @{}ms",
3650 self.exchange, self.symbol, self.price, self.quantity, side, self.received_at_ms
3651 )
3652 }
3653}
3654
3655pub struct TickNormalizer;
3660
3661impl TickNormalizer {
3662 pub fn new() -> Self {
3664 Self
3665 }
3666
3667 pub fn normalize(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3675 let tick = match raw.exchange {
3676 Exchange::Binance => self.normalize_binance(raw),
3677 Exchange::Coinbase => self.normalize_coinbase(raw),
3678 Exchange::Alpaca => self.normalize_alpaca(raw),
3679 Exchange::Polygon => self.normalize_polygon(raw),
3680 }?;
3681 if tick.price <= Decimal::ZERO {
3682 return Err(StreamError::InvalidTick {
3683 reason: format!("price must be positive, got {}", tick.price),
3684 });
3685 }
3686 if tick.quantity < Decimal::ZERO {
3687 return Err(StreamError::InvalidTick {
3688 reason: format!("quantity must be non-negative, got {}", tick.quantity),
3689 });
3690 }
3691 trace!(
3692 exchange = %tick.exchange,
3693 symbol = %tick.symbol,
3694 price = %tick.price,
3695 exchange_ts_ms = ?tick.exchange_ts_ms,
3696 "tick normalized"
3697 );
3698 Ok(tick)
3699 }
3700
3701 fn normalize_binance(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3702 let p = &raw.payload;
3703 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3704 let qty = parse_decimal_field(p, "q", &raw.exchange.to_string())?;
3705 let side = p.get("m").and_then(|v| v.as_bool()).map(|maker| {
3706 if maker {
3707 TradeSide::Sell
3708 } else {
3709 TradeSide::Buy
3710 }
3711 });
3712 let trade_id = p.get("t").and_then(|v| v.as_u64()).map(|id| id.to_string());
3713 let exchange_ts = p.get("T").and_then(|v| v.as_u64());
3714 Ok(NormalizedTick {
3715 exchange: raw.exchange,
3716 symbol: raw.symbol,
3717 price,
3718 quantity: qty,
3719 side,
3720 trade_id,
3721 exchange_ts_ms: exchange_ts,
3722 received_at_ms: raw.received_at_ms,
3723 })
3724 }
3725
3726 fn normalize_coinbase(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3727 let p = &raw.payload;
3728 let price = parse_decimal_field(p, "price", &raw.exchange.to_string())?;
3729 let qty = parse_decimal_field(p, "size", &raw.exchange.to_string())?;
3730 let side = p.get("side").and_then(|v| v.as_str()).map(|s| {
3731 if s == "buy" {
3732 TradeSide::Buy
3733 } else {
3734 TradeSide::Sell
3735 }
3736 });
3737 let trade_id = p
3738 .get("trade_id")
3739 .and_then(|v| v.as_str())
3740 .map(str::to_string);
3741 let exchange_ts_ms = p
3743 .get("time")
3744 .and_then(|v| v.as_str())
3745 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
3746 .map(|dt| dt.timestamp_millis() as u64);
3747 Ok(NormalizedTick {
3748 exchange: raw.exchange,
3749 symbol: raw.symbol,
3750 price,
3751 quantity: qty,
3752 side,
3753 trade_id,
3754 exchange_ts_ms,
3755 received_at_ms: raw.received_at_ms,
3756 })
3757 }
3758
3759 fn normalize_alpaca(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3760 let p = &raw.payload;
3761 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3762 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
3763 let trade_id = p.get("i").and_then(|v| v.as_u64()).map(|id| id.to_string());
3764 let exchange_ts_ms = p
3766 .get("t")
3767 .and_then(|v| v.as_str())
3768 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
3769 .map(|dt| dt.timestamp_millis() as u64);
3770 Ok(NormalizedTick {
3771 exchange: raw.exchange,
3772 symbol: raw.symbol,
3773 price,
3774 quantity: qty,
3775 side: None,
3776 trade_id,
3777 exchange_ts_ms,
3778 received_at_ms: raw.received_at_ms,
3779 })
3780 }
3781
3782 fn normalize_polygon(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3783 let p = &raw.payload;
3784 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3785 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
3786 let trade_id = p.get("i").and_then(|v| v.as_str()).map(str::to_string);
3787 let exchange_ts = p
3789 .get("t")
3790 .and_then(|v| v.as_u64())
3791 .map(|t_ns| t_ns / 1_000_000);
3792 Ok(NormalizedTick {
3793 exchange: raw.exchange,
3794 symbol: raw.symbol,
3795 price,
3796 quantity: qty,
3797 side: None,
3798 trade_id,
3799 exchange_ts_ms: exchange_ts,
3800 received_at_ms: raw.received_at_ms,
3801 })
3802 }
3803}
3804
3805impl Default for TickNormalizer {
3806 fn default() -> Self {
3807 Self::new()
3808 }
3809}
3810
3811fn parse_decimal_field(
3812 v: &serde_json::Value,
3813 field: &str,
3814 exchange: &str,
3815) -> Result<Decimal, StreamError> {
3816 let raw = v.get(field).ok_or_else(|| StreamError::ParseError {
3817 exchange: exchange.to_string(),
3818 reason: format!("missing field '{}'", field),
3819 })?;
3820 let s: String = match raw {
3826 serde_json::Value::String(s) => s.clone(),
3827 serde_json::Value::Number(n) => n.to_string(),
3828 _ => {
3829 return Err(StreamError::ParseError {
3830 exchange: exchange.to_string(),
3831 reason: format!("field '{}' is not a string or number", field),
3832 });
3833 }
3834 };
3835 Decimal::from_str(&s).map_err(|e| StreamError::ParseError {
3836 exchange: exchange.to_string(),
3837 reason: format!("field '{}' parse error: {}", field, e),
3838 })
3839}
3840
3841fn now_ms() -> u64 {
3842 std::time::SystemTime::now()
3843 .duration_since(std::time::UNIX_EPOCH)
3844 .map(|d| d.as_millis() as u64)
3845 .unwrap_or(0)
3846}
3847
3848#[cfg(test)]
3849mod tests {
3850 use super::*;
3851 use serde_json::json;
3852
3853 fn normalizer() -> TickNormalizer {
3854 TickNormalizer::new()
3855 }
3856
3857 fn binance_tick(symbol: &str) -> RawTick {
3858 RawTick {
3859 exchange: Exchange::Binance,
3860 symbol: symbol.to_string(),
3861 payload: json!({ "p": "50000.12", "q": "0.001", "m": false, "t": 12345, "T": 1700000000000u64 }),
3862 received_at_ms: 1700000000001,
3863 }
3864 }
3865
3866 fn coinbase_tick(symbol: &str) -> RawTick {
3867 RawTick {
3868 exchange: Exchange::Coinbase,
3869 symbol: symbol.to_string(),
3870 payload: json!({ "price": "50001.00", "size": "0.5", "side": "buy", "trade_id": "abc123" }),
3871 received_at_ms: 1700000000002,
3872 }
3873 }
3874
3875 fn alpaca_tick(symbol: &str) -> RawTick {
3876 RawTick {
3877 exchange: Exchange::Alpaca,
3878 symbol: symbol.to_string(),
3879 payload: json!({ "p": "180.50", "s": "10", "i": 99 }),
3880 received_at_ms: 1700000000003,
3881 }
3882 }
3883
3884 fn polygon_tick(symbol: &str) -> RawTick {
3885 RawTick {
3886 exchange: Exchange::Polygon,
3887 symbol: symbol.to_string(),
3888 payload: json!({ "p": "180.51", "s": "5", "i": "XYZ-001", "t": 1_700_000_000_000_000_000u64 }),
3890 received_at_ms: 1700000000005,
3891 }
3892 }
3893
3894 #[test]
3895 fn test_exchange_from_str_valid() {
3896 assert_eq!("binance".parse::<Exchange>().unwrap(), Exchange::Binance);
3897 assert_eq!("Coinbase".parse::<Exchange>().unwrap(), Exchange::Coinbase);
3898 assert_eq!("ALPACA".parse::<Exchange>().unwrap(), Exchange::Alpaca);
3899 assert_eq!("polygon".parse::<Exchange>().unwrap(), Exchange::Polygon);
3900 }
3901
3902 #[test]
3903 fn test_exchange_from_str_unknown_returns_error() {
3904 let result = "Kraken".parse::<Exchange>();
3905 assert!(matches!(result, Err(StreamError::UnknownExchange(_))));
3906 }
3907
3908 #[test]
3909 fn test_exchange_display() {
3910 assert_eq!(Exchange::Binance.to_string(), "Binance");
3911 assert_eq!(Exchange::Coinbase.to_string(), "Coinbase");
3912 }
3913
3914 #[test]
3915 fn test_normalize_binance_tick_price_and_qty() {
3916 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3917 assert_eq!(tick.price, Decimal::from_str("50000.12").unwrap());
3918 assert_eq!(tick.quantity, Decimal::from_str("0.001").unwrap());
3919 assert_eq!(tick.exchange, Exchange::Binance);
3920 assert_eq!(tick.symbol, "BTCUSDT");
3921 }
3922
3923 #[test]
3924 fn test_normalize_binance_side_maker_false_is_buy() {
3925 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3926 assert_eq!(tick.side, Some(TradeSide::Buy));
3927 }
3928
3929 #[test]
3930 fn test_normalize_binance_side_maker_true_is_sell() {
3931 let raw = RawTick {
3932 exchange: Exchange::Binance,
3933 symbol: "BTCUSDT".into(),
3934 payload: json!({ "p": "50000", "q": "1", "m": true }),
3935 received_at_ms: 0,
3936 };
3937 let tick = normalizer().normalize(raw).unwrap();
3938 assert_eq!(tick.side, Some(TradeSide::Sell));
3939 }
3940
3941 #[test]
3942 fn test_normalize_binance_trade_id_and_ts() {
3943 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3944 assert_eq!(tick.trade_id, Some("12345".to_string()));
3945 assert_eq!(tick.exchange_ts_ms, Some(1700000000000));
3946 }
3947
3948 #[test]
3949 fn test_normalize_coinbase_tick() {
3950 let tick = normalizer().normalize(coinbase_tick("BTC-USD")).unwrap();
3951 assert_eq!(tick.price, Decimal::from_str("50001.00").unwrap());
3952 assert_eq!(tick.quantity, Decimal::from_str("0.5").unwrap());
3953 assert_eq!(tick.side, Some(TradeSide::Buy));
3954 assert_eq!(tick.trade_id, Some("abc123".to_string()));
3955 }
3956
3957 #[test]
3958 fn test_normalize_coinbase_sell_side() {
3959 let raw = RawTick {
3960 exchange: Exchange::Coinbase,
3961 symbol: "BTC-USD".into(),
3962 payload: json!({ "price": "50000", "size": "1", "side": "sell" }),
3963 received_at_ms: 0,
3964 };
3965 let tick = normalizer().normalize(raw).unwrap();
3966 assert_eq!(tick.side, Some(TradeSide::Sell));
3967 }
3968
3969 #[test]
3970 fn test_normalize_alpaca_tick() {
3971 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
3972 assert_eq!(tick.price, Decimal::from_str("180.50").unwrap());
3973 assert_eq!(tick.quantity, Decimal::from_str("10").unwrap());
3974 assert_eq!(tick.trade_id, Some("99".to_string()));
3975 assert_eq!(tick.side, None);
3976 }
3977
3978 #[test]
3979 fn test_normalize_polygon_tick() {
3980 let tick = normalizer().normalize(polygon_tick("AAPL")).unwrap();
3981 assert_eq!(tick.price, Decimal::from_str("180.51").unwrap());
3982 assert_eq!(tick.exchange_ts_ms, Some(1_700_000_000_000u64));
3984 assert_eq!(tick.trade_id, Some("XYZ-001".to_string()));
3985 }
3986
3987 #[test]
3988 fn test_normalize_alpaca_rfc3339_timestamp() {
3989 let raw = RawTick {
3990 exchange: Exchange::Alpaca,
3991 symbol: "AAPL".into(),
3992 payload: json!({ "p": "180.50", "s": "10", "i": 99, "t": "2023-11-15T00:00:00Z" }),
3993 received_at_ms: 1700000000003,
3994 };
3995 let tick = normalizer().normalize(raw).unwrap();
3996 assert!(tick.exchange_ts_ms.is_some(), "Alpaca 't' field should be parsed");
3997 assert_eq!(tick.exchange_ts_ms, Some(1700006400000u64));
3999 }
4000
4001 #[test]
4002 fn test_normalize_alpaca_no_timestamp_field() {
4003 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
4004 assert_eq!(tick.exchange_ts_ms, None, "missing 't' field means no exchange_ts_ms");
4005 }
4006
4007 #[test]
4008 fn test_normalize_missing_price_field_returns_parse_error() {
4009 let raw = RawTick {
4010 exchange: Exchange::Binance,
4011 symbol: "BTCUSDT".into(),
4012 payload: json!({ "q": "1" }),
4013 received_at_ms: 0,
4014 };
4015 let result = normalizer().normalize(raw);
4016 assert!(matches!(result, Err(StreamError::ParseError { .. })));
4017 }
4018
4019 #[test]
4020 fn test_normalize_invalid_decimal_returns_parse_error() {
4021 let raw = RawTick {
4022 exchange: Exchange::Coinbase,
4023 symbol: "BTC-USD".into(),
4024 payload: json!({ "price": "not-a-number", "size": "1" }),
4025 received_at_ms: 0,
4026 };
4027 let result = normalizer().normalize(raw);
4028 assert!(matches!(result, Err(StreamError::ParseError { .. })));
4029 }
4030
4031 #[test]
4032 fn test_raw_tick_new_sets_received_at() {
4033 let raw = RawTick::new(Exchange::Binance, "BTCUSDT", json!({}));
4034 assert!(raw.received_at_ms > 0);
4035 }
4036
4037 #[test]
4038 fn test_normalize_numeric_price_field() {
4039 let raw = RawTick {
4040 exchange: Exchange::Binance,
4041 symbol: "BTCUSDT".into(),
4042 payload: json!({ "p": 50000.0, "q": 1.0 }),
4043 received_at_ms: 0,
4044 };
4045 let tick = normalizer().normalize(raw).unwrap();
4046 assert!(tick.price > Decimal::ZERO);
4047 }
4048
4049 #[test]
4050 fn test_trade_side_from_str_buy() {
4051 assert_eq!("buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
4052 assert_eq!("Buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
4053 assert_eq!("BUY".parse::<TradeSide>().unwrap(), TradeSide::Buy);
4054 }
4055
4056 #[test]
4057 fn test_trade_side_from_str_sell() {
4058 assert_eq!("sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
4059 assert_eq!("Sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
4060 assert_eq!("SELL".parse::<TradeSide>().unwrap(), TradeSide::Sell);
4061 }
4062
4063 #[test]
4064 fn test_trade_side_from_str_invalid() {
4065 let err = "long".parse::<TradeSide>().unwrap_err();
4066 assert!(matches!(err, StreamError::ParseError { .. }));
4067 }
4068
4069 #[test]
4070 fn test_trade_side_display() {
4071 assert_eq!(TradeSide::Buy.to_string(), "buy");
4072 assert_eq!(TradeSide::Sell.to_string(), "sell");
4073 }
4074
4075 #[test]
4076 fn test_normalize_zero_price_returns_invalid_tick() {
4077 let raw = RawTick {
4078 exchange: Exchange::Binance,
4079 symbol: "BTCUSDT".into(),
4080 payload: json!({ "p": "0", "q": "1" }),
4081 received_at_ms: 0,
4082 };
4083 let err = normalizer().normalize(raw).unwrap_err();
4084 assert!(matches!(err, StreamError::InvalidTick { .. }));
4085 }
4086
4087 #[test]
4088 fn test_normalize_negative_price_returns_invalid_tick() {
4089 let raw = RawTick {
4090 exchange: Exchange::Binance,
4091 symbol: "BTCUSDT".into(),
4092 payload: json!({ "p": "-1", "q": "1" }),
4093 received_at_ms: 0,
4094 };
4095 let err = normalizer().normalize(raw).unwrap_err();
4096 assert!(matches!(err, StreamError::InvalidTick { .. }));
4097 }
4098
4099 #[test]
4100 fn test_normalize_negative_quantity_returns_invalid_tick() {
4101 let raw = RawTick {
4102 exchange: Exchange::Binance,
4103 symbol: "BTCUSDT".into(),
4104 payload: json!({ "p": "100", "q": "-1" }),
4105 received_at_ms: 0,
4106 };
4107 let err = normalizer().normalize(raw).unwrap_err();
4108 assert!(matches!(err, StreamError::InvalidTick { .. }));
4109 }
4110
4111 #[test]
4112 fn test_normalize_zero_quantity_is_valid() {
4113 let raw = RawTick {
4115 exchange: Exchange::Binance,
4116 symbol: "BTCUSDT".into(),
4117 payload: json!({ "p": "100", "q": "0" }),
4118 received_at_ms: 0,
4119 };
4120 let tick = normalizer().normalize(raw).unwrap();
4121 assert_eq!(tick.quantity, Decimal::ZERO);
4122 }
4123
4124 #[test]
4125 fn test_trade_side_is_buy() {
4126 assert!(TradeSide::Buy.is_buy());
4127 assert!(!TradeSide::Buy.is_sell());
4128 }
4129
4130 #[test]
4131 fn test_trade_side_is_sell() {
4132 assert!(TradeSide::Sell.is_sell());
4133 assert!(!TradeSide::Sell.is_buy());
4134 }
4135
4136 #[test]
4137 fn test_normalized_tick_display() {
4138 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4139 let s = tick.to_string();
4140 assert!(s.contains("Binance"));
4141 assert!(s.contains("BTCUSDT"));
4142 assert!(s.contains("50000"));
4143 }
4144
4145 #[test]
4146 fn test_normalized_tick_value_is_price_times_qty() {
4147 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4148 let expected = tick.price * tick.quantity;
4150 assert_eq!(tick.volume_notional(), expected);
4151 }
4152
4153 #[test]
4154 fn test_normalized_tick_age_ms_positive() {
4155 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4156 let raw = RawTick {
4159 exchange: Exchange::Binance,
4160 symbol: "BTCUSDT".into(),
4161 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
4162 received_at_ms: 1_000_000,
4163 };
4164 let tick = normalizer().normalize(raw).unwrap();
4165 assert_eq!(tick.age_ms(1_001_000), 1_000);
4166 }
4167
4168 #[test]
4169 fn test_normalized_tick_age_ms_zero_when_now_equals_received() {
4170 let raw = RawTick {
4171 exchange: Exchange::Binance,
4172 symbol: "BTCUSDT".into(),
4173 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
4174 received_at_ms: 5_000,
4175 };
4176 let tick = normalizer().normalize(raw).unwrap();
4177 assert_eq!(tick.age_ms(5_000), 0);
4178 assert_eq!(tick.age_ms(4_000), 0);
4180 }
4181
4182 #[test]
4183 fn test_normalized_tick_value_zero_qty_is_zero() {
4184 use rust_decimal_macros::dec;
4185 let raw = RawTick {
4186 exchange: Exchange::Binance,
4187 symbol: "BTCUSDT".into(),
4188 payload: serde_json::json!({
4189 "p": "50000",
4190 "q": "0",
4191 "m": false,
4192 }),
4193 received_at_ms: 1000,
4194 };
4195 let tick = normalizer().normalize(raw).unwrap();
4196 assert_eq!(tick.value(), dec!(0));
4197 }
4198
4199 fn make_tick_at(received_at_ms: u64) -> NormalizedTick {
4202 NormalizedTick {
4203 exchange: Exchange::Binance,
4204 symbol: "BTCUSDT".into(),
4205 price: rust_decimal_macros::dec!(100),
4206 quantity: rust_decimal_macros::dec!(1),
4207 side: None,
4208 trade_id: None,
4209 exchange_ts_ms: None,
4210 received_at_ms,
4211 }
4212 }
4213
4214 #[test]
4215 fn test_is_stale_true_when_age_exceeds_threshold() {
4216 let tick = make_tick_at(1_000);
4217 assert!(tick.is_stale(6_000, 4_000));
4219 }
4220
4221 #[test]
4222 fn test_is_stale_false_when_age_equals_threshold() {
4223 let tick = make_tick_at(1_000);
4224 assert!(!tick.is_stale(5_000, 4_000));
4226 }
4227
4228 #[test]
4229 fn test_is_stale_false_for_fresh_tick() {
4230 let tick = make_tick_at(10_000);
4231 assert!(!tick.is_stale(10_500, 1_000));
4232 }
4233
4234 #[test]
4237 fn test_is_buy_true_for_buy_side() {
4238 let mut tick = make_tick_at(1_000);
4239 tick.side = Some(TradeSide::Buy);
4240 assert!(tick.is_buy());
4241 assert!(!tick.is_sell());
4242 }
4243
4244 #[test]
4245 fn test_is_sell_true_for_sell_side() {
4246 let mut tick = make_tick_at(1_000);
4247 tick.side = Some(TradeSide::Sell);
4248 assert!(tick.is_sell());
4249 assert!(!tick.is_buy());
4250 }
4251
4252 #[test]
4253 fn test_is_buy_false_for_unknown_side() {
4254 let mut tick = make_tick_at(1_000);
4255 tick.side = None;
4256 assert!(!tick.is_buy());
4257 assert!(!tick.is_sell());
4258 }
4259
4260 #[test]
4263 fn test_with_exchange_ts_sets_field() {
4264 let tick = make_tick_at(5_000).with_exchange_ts(3_000);
4265 assert_eq!(tick.exchange_ts_ms, Some(3_000));
4266 assert_eq!(tick.received_at_ms, 5_000); }
4268
4269 #[test]
4270 fn test_with_exchange_ts_overrides_existing() {
4271 let tick = make_tick_at(1_000).with_exchange_ts(999).with_exchange_ts(888);
4272 assert_eq!(tick.exchange_ts_ms, Some(888));
4273 }
4274
4275 #[test]
4278 fn test_price_move_from_positive() {
4279 let prev = make_tick_at(1_000);
4280 let mut curr = make_tick_at(2_000);
4281 curr.price = prev.price + rust_decimal_macros::dec!(5);
4282 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(5));
4283 }
4284
4285 #[test]
4286 fn test_price_move_from_negative() {
4287 let prev = make_tick_at(1_000);
4288 let mut curr = make_tick_at(2_000);
4289 curr.price = prev.price - rust_decimal_macros::dec!(3);
4290 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(-3));
4291 }
4292
4293 #[test]
4294 fn test_price_move_from_zero_when_same() {
4295 let tick = make_tick_at(1_000);
4296 assert_eq!(tick.price_move_from(&tick), rust_decimal_macros::dec!(0));
4297 }
4298
4299 #[test]
4300 fn test_is_more_recent_than_true() {
4301 let older = make_tick_at(1_000);
4302 let newer = make_tick_at(2_000);
4303 assert!(newer.is_more_recent_than(&older));
4304 }
4305
4306 #[test]
4307 fn test_is_more_recent_than_false_when_older() {
4308 let older = make_tick_at(1_000);
4309 let newer = make_tick_at(2_000);
4310 assert!(!older.is_more_recent_than(&newer));
4311 }
4312
4313 #[test]
4314 fn test_is_more_recent_than_false_when_equal() {
4315 let tick = make_tick_at(1_000);
4316 assert!(!tick.is_more_recent_than(&tick));
4317 }
4318
4319 #[test]
4322 fn test_with_side_sets_buy() {
4323 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
4324 assert_eq!(tick.side, Some(TradeSide::Buy));
4325 }
4326
4327 #[test]
4328 fn test_with_side_sets_sell() {
4329 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
4330 assert_eq!(tick.side, Some(TradeSide::Sell));
4331 }
4332
4333 #[test]
4334 fn test_with_side_overrides_existing() {
4335 let tick = make_tick_at(1_000).with_side(TradeSide::Buy).with_side(TradeSide::Sell);
4336 assert_eq!(tick.side, Some(TradeSide::Sell));
4337 }
4338
4339 #[test]
4342 fn test_is_neutral_true_when_no_side() {
4343 let mut tick = make_tick_at(1_000);
4344 tick.side = None;
4345 assert!(tick.is_neutral());
4346 }
4347
4348 #[test]
4349 fn test_is_neutral_false_when_buy() {
4350 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
4351 assert!(!tick.is_neutral());
4352 }
4353
4354 #[test]
4355 fn test_is_neutral_false_when_sell() {
4356 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
4357 assert!(!tick.is_neutral());
4358 }
4359
4360 #[test]
4363 fn test_is_large_trade_above_threshold() {
4364 let mut tick = make_tick_at(1_000);
4365 tick.quantity = rust_decimal_macros::dec!(100);
4366 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
4367 }
4368
4369 #[test]
4370 fn test_is_large_trade_at_threshold() {
4371 let mut tick = make_tick_at(1_000);
4372 tick.quantity = rust_decimal_macros::dec!(50);
4373 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
4374 }
4375
4376 #[test]
4377 fn test_is_large_trade_below_threshold() {
4378 let mut tick = make_tick_at(1_000);
4379 tick.quantity = rust_decimal_macros::dec!(10);
4380 assert!(!tick.is_large_trade(rust_decimal_macros::dec!(50)));
4381 }
4382
4383 #[test]
4384 fn test_volume_notional_is_price_times_quantity() {
4385 let mut tick = make_tick_at(1_000);
4386 tick.price = rust_decimal_macros::dec!(200);
4387 tick.quantity = rust_decimal_macros::dec!(3);
4388 assert_eq!(tick.volume_notional(), rust_decimal_macros::dec!(600));
4389 }
4390
4391 #[test]
4394 fn test_is_above_returns_true_when_price_higher() {
4395 let mut tick = make_tick_at(1_000);
4396 tick.price = rust_decimal_macros::dec!(200);
4397 assert!(tick.is_above(rust_decimal_macros::dec!(150)));
4398 }
4399
4400 #[test]
4401 fn test_is_above_returns_false_when_price_equal() {
4402 let mut tick = make_tick_at(1_000);
4403 tick.price = rust_decimal_macros::dec!(200);
4404 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
4405 }
4406
4407 #[test]
4408 fn test_is_above_returns_false_when_price_lower() {
4409 let mut tick = make_tick_at(1_000);
4410 tick.price = rust_decimal_macros::dec!(100);
4411 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
4412 }
4413
4414 #[test]
4417 fn test_is_below_returns_true_when_price_lower() {
4418 let mut tick = make_tick_at(1_000);
4419 tick.price = rust_decimal_macros::dec!(100);
4420 assert!(tick.is_below(rust_decimal_macros::dec!(150)));
4421 }
4422
4423 #[test]
4424 fn test_is_below_returns_false_when_price_equal() {
4425 let mut tick = make_tick_at(1_000);
4426 tick.price = rust_decimal_macros::dec!(100);
4427 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
4428 }
4429
4430 #[test]
4431 fn test_is_below_returns_false_when_price_higher() {
4432 let mut tick = make_tick_at(1_000);
4433 tick.price = rust_decimal_macros::dec!(200);
4434 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
4435 }
4436
4437 #[test]
4440 fn test_has_exchange_ts_false_when_none() {
4441 let tick = make_tick_at(1_000);
4442 assert!(!tick.has_exchange_ts());
4443 }
4444
4445 #[test]
4446 fn test_has_exchange_ts_true_when_some() {
4447 let tick = make_tick_at(1_000).with_exchange_ts(900);
4448 assert!(tick.has_exchange_ts());
4449 }
4450
4451 #[test]
4454 fn test_is_at_returns_true_when_equal() {
4455 let mut tick = make_tick_at(1_000);
4456 tick.price = rust_decimal_macros::dec!(100);
4457 assert!(tick.is_at(rust_decimal_macros::dec!(100)));
4458 }
4459
4460 #[test]
4461 fn test_is_at_returns_false_when_higher() {
4462 let mut tick = make_tick_at(1_000);
4463 tick.price = rust_decimal_macros::dec!(101);
4464 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
4465 }
4466
4467 #[test]
4468 fn test_is_at_returns_false_when_lower() {
4469 let mut tick = make_tick_at(1_000);
4470 tick.price = rust_decimal_macros::dec!(99);
4471 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
4472 }
4473
4474 #[test]
4477 fn test_is_buy_true_when_side_is_buy() {
4478 let mut tick = make_tick_at(1_000);
4479 tick.side = Some(TradeSide::Buy);
4480 assert!(tick.is_buy());
4481 }
4482
4483 #[test]
4484 fn test_is_buy_false_when_side_is_sell() {
4485 let mut tick = make_tick_at(1_000);
4486 tick.side = Some(TradeSide::Sell);
4487 assert!(!tick.is_buy());
4488 }
4489
4490 #[test]
4491 fn test_is_buy_false_when_side_is_none() {
4492 let mut tick = make_tick_at(1_000);
4493 tick.side = None;
4494 assert!(!tick.is_buy());
4495 }
4496
4497 #[test]
4500 fn test_side_str_buy() {
4501 let mut tick = make_tick_at(1_000);
4502 tick.side = Some(TradeSide::Buy);
4503 assert_eq!(tick.side_str(), "buy");
4504 }
4505
4506 #[test]
4507 fn test_side_str_sell() {
4508 let mut tick = make_tick_at(1_000);
4509 tick.side = Some(TradeSide::Sell);
4510 assert_eq!(tick.side_str(), "sell");
4511 }
4512
4513 #[test]
4514 fn test_side_str_unknown_when_none() {
4515 let mut tick = make_tick_at(1_000);
4516 tick.side = None;
4517 assert_eq!(tick.side_str(), "unknown");
4518 }
4519
4520 #[test]
4521 fn test_is_round_lot_true_for_integer_quantity() {
4522 let mut tick = make_tick_at(1_000);
4523 tick.quantity = rust_decimal_macros::dec!(100);
4524 assert!(tick.is_round_lot());
4525 }
4526
4527 #[test]
4528 fn test_is_round_lot_false_for_fractional_quantity() {
4529 let mut tick = make_tick_at(1_000);
4530 tick.quantity = rust_decimal_macros::dec!(0.5);
4531 assert!(!tick.is_round_lot());
4532 }
4533
4534 #[test]
4537 fn test_is_same_symbol_as_true_when_symbols_match() {
4538 let t1 = make_tick_at(1_000);
4539 let t2 = make_tick_at(2_000);
4540 assert!(t1.is_same_symbol_as(&t2));
4541 }
4542
4543 #[test]
4544 fn test_is_same_symbol_as_false_when_symbols_differ() {
4545 let t1 = make_tick_at(1_000);
4546 let mut t2 = make_tick_at(2_000);
4547 t2.symbol = "ETH-USD".to_string();
4548 assert!(!t1.is_same_symbol_as(&t2));
4549 }
4550
4551 #[test]
4552 fn test_price_distance_from_is_absolute() {
4553 let mut t1 = make_tick_at(1_000);
4554 let mut t2 = make_tick_at(2_000);
4555 t1.price = rust_decimal_macros::dec!(100);
4556 t2.price = rust_decimal_macros::dec!(110);
4557 assert_eq!(t1.price_distance_from(&t2), rust_decimal_macros::dec!(10));
4558 assert_eq!(t2.price_distance_from(&t1), rust_decimal_macros::dec!(10));
4559 }
4560
4561 #[test]
4562 fn test_price_distance_from_zero_when_equal() {
4563 let t1 = make_tick_at(1_000);
4564 let t2 = make_tick_at(2_000);
4565 assert!(t1.price_distance_from(&t2).is_zero());
4566 }
4567
4568 #[test]
4571 fn test_is_sell_true_when_side_is_sell() {
4572 let mut tick = make_tick_at(1_000);
4573 tick.side = Some(TradeSide::Sell);
4574 assert!(tick.is_sell());
4575 }
4576
4577 #[test]
4578 fn test_is_sell_false_when_side_is_buy() {
4579 let mut tick = make_tick_at(1_000);
4580 tick.side = Some(TradeSide::Buy);
4581 assert!(!tick.is_sell());
4582 }
4583
4584 #[test]
4585 fn test_is_sell_false_when_side_is_none() {
4586 let mut tick = make_tick_at(1_000);
4587 tick.side = None;
4588 assert!(!tick.is_sell());
4589 }
4590
4591 #[test]
4594 fn test_exchange_latency_ms_positive_for_normal_delivery() {
4595 let mut tick = make_tick_at(1_100);
4596 tick.exchange_ts_ms = Some(1_000);
4597 assert_eq!(tick.exchange_latency_ms(), Some(100));
4598 }
4599
4600 #[test]
4601 fn test_exchange_latency_ms_negative_for_clock_skew() {
4602 let mut tick = make_tick_at(1_000);
4603 tick.exchange_ts_ms = Some(1_100);
4604 assert_eq!(tick.exchange_latency_ms(), Some(-100));
4605 }
4606
4607 #[test]
4608 fn test_exchange_latency_ms_none_when_no_exchange_ts() {
4609 let mut tick = make_tick_at(1_000);
4610 tick.exchange_ts_ms = None;
4611 assert!(tick.exchange_latency_ms().is_none());
4612 }
4613
4614 #[test]
4615 fn test_is_notional_large_trade_true_when_above_threshold() {
4616 let mut tick = make_tick_at(1_000);
4617 tick.price = rust_decimal_macros::dec!(100);
4618 tick.quantity = rust_decimal_macros::dec!(10);
4619 assert!(tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
4621 }
4622
4623 #[test]
4624 fn test_is_notional_large_trade_false_when_at_or_below_threshold() {
4625 let mut tick = make_tick_at(1_000);
4626 tick.price = rust_decimal_macros::dec!(100);
4627 tick.quantity = rust_decimal_macros::dec!(5);
4628 assert!(!tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
4630 }
4631
4632 #[test]
4633 fn test_is_aggressive_true_when_buy() {
4634 let mut tick = make_tick_at(1_000);
4635 tick.side = Some(TradeSide::Buy);
4636 assert!(tick.is_aggressive());
4637 }
4638
4639 #[test]
4640 fn test_is_aggressive_true_when_sell() {
4641 let mut tick = make_tick_at(1_000);
4642 tick.side = Some(TradeSide::Sell);
4643 assert!(tick.is_aggressive());
4644 }
4645
4646 #[test]
4647 fn test_is_aggressive_false_when_neutral() {
4648 let tick = make_tick_at(1_000); assert!(!tick.is_aggressive());
4650 }
4651
4652 #[test]
4653 fn test_price_diff_from_positive_when_higher() {
4654 let mut t1 = make_tick_at(1_000);
4655 let mut t2 = make_tick_at(1_000);
4656 t1.price = rust_decimal_macros::dec!(105);
4657 t2.price = rust_decimal_macros::dec!(100);
4658 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(5));
4659 }
4660
4661 #[test]
4662 fn test_price_diff_from_negative_when_lower() {
4663 let mut t1 = make_tick_at(1_000);
4664 let mut t2 = make_tick_at(1_000);
4665 t1.price = rust_decimal_macros::dec!(95);
4666 t2.price = rust_decimal_macros::dec!(100);
4667 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(-5));
4668 }
4669
4670 #[test]
4671 fn test_is_micro_trade_true_when_below_threshold() {
4672 let mut tick = make_tick_at(1_000);
4673 tick.quantity = rust_decimal_macros::dec!(0.5);
4674 assert!(tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4675 }
4676
4677 #[test]
4678 fn test_is_micro_trade_false_when_equal_threshold() {
4679 let mut tick = make_tick_at(1_000);
4680 tick.quantity = rust_decimal_macros::dec!(1);
4681 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4682 }
4683
4684 #[test]
4685 fn test_is_micro_trade_false_when_above_threshold() {
4686 let mut tick = make_tick_at(1_000);
4687 tick.quantity = rust_decimal_macros::dec!(2);
4688 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4689 }
4690
4691 #[test]
4694 fn test_is_zero_price_true_for_zero() {
4695 let mut tick = make_tick_at(1_000);
4696 tick.price = rust_decimal_macros::dec!(0);
4697 assert!(tick.is_zero_price());
4698 }
4699
4700 #[test]
4701 fn test_is_zero_price_false_for_nonzero() {
4702 let tick = make_tick_at(1_000); assert!(!tick.is_zero_price());
4704 }
4705
4706 #[test]
4707 fn test_is_fresh_true_when_within_age() {
4708 let tick = make_tick_at(1_000);
4709 assert!(tick.is_fresh(2_000, 1_500));
4711 }
4712
4713 #[test]
4714 fn test_is_fresh_false_when_too_old() {
4715 let tick = make_tick_at(1_000);
4716 assert!(!tick.is_fresh(5_000, 2_000));
4718 }
4719
4720 #[test]
4721 fn test_is_fresh_true_when_now_less_than_received() {
4722 let tick = make_tick_at(5_000);
4724 assert!(tick.is_fresh(3_000, 100));
4725 }
4726
4727 #[test]
4729 fn test_age_ms_correct_elapsed() {
4730 let tick = make_tick_at(10_000);
4731 assert_eq!(tick.age_ms(10_500), 500);
4732 }
4733
4734 #[test]
4735 fn test_age_ms_zero_when_now_equals_received() {
4736 let tick = make_tick_at(10_000);
4737 assert_eq!(tick.age_ms(10_000), 0);
4738 }
4739
4740 #[test]
4741 fn test_age_ms_zero_when_now_before_received() {
4742 let tick = make_tick_at(10_000);
4743 assert_eq!(tick.age_ms(9_000), 0);
4744 }
4745
4746 #[test]
4748 fn test_is_buying_pressure_true_above_midpoint() {
4749 use rust_decimal_macros::dec;
4750 let mut tick = make_tick_at(0);
4751 tick.price = dec!(100.50);
4752 assert!(tick.is_buying_pressure(dec!(100)));
4753 }
4754
4755 #[test]
4756 fn test_is_buying_pressure_false_below_midpoint() {
4757 use rust_decimal_macros::dec;
4758 let mut tick = make_tick_at(0);
4759 tick.price = dec!(99.50);
4760 assert!(!tick.is_buying_pressure(dec!(100)));
4761 }
4762
4763 #[test]
4764 fn test_is_buying_pressure_false_at_midpoint() {
4765 use rust_decimal_macros::dec;
4766 let mut tick = make_tick_at(0);
4767 tick.price = dec!(100);
4768 assert!(!tick.is_buying_pressure(dec!(100)));
4769 }
4770
4771 #[test]
4773 fn test_rounded_price_rounds_to_nearest_tick() {
4774 use rust_decimal_macros::dec;
4775 let mut tick = make_tick_at(0);
4776 tick.price = dec!(100.37);
4777 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.25));
4779 }
4780
4781 #[test]
4782 fn test_rounded_price_unchanged_when_already_aligned() {
4783 use rust_decimal_macros::dec;
4784 let mut tick = make_tick_at(0);
4785 tick.price = dec!(100.50);
4786 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.50));
4787 }
4788
4789 #[test]
4790 fn test_rounded_price_returns_original_for_zero_tick_size() {
4791 use rust_decimal_macros::dec;
4792 let mut tick = make_tick_at(0);
4793 tick.price = dec!(99.99);
4794 assert_eq!(tick.rounded_price(dec!(0)), dec!(99.99));
4795 }
4796
4797 #[test]
4799 fn test_is_large_spread_from_true_when_large() {
4800 use rust_decimal_macros::dec;
4801 let mut t1 = make_tick_at(0);
4802 let mut t2 = make_tick_at(0);
4803 t1.price = dec!(100);
4804 t2.price = dec!(110);
4805 assert!(t1.is_large_spread_from(&t2, dec!(5)));
4806 }
4807
4808 #[test]
4809 fn test_is_large_spread_from_false_when_small() {
4810 use rust_decimal_macros::dec;
4811 let mut t1 = make_tick_at(0);
4812 let mut t2 = make_tick_at(0);
4813 t1.price = dec!(100);
4814 t2.price = dec!(101);
4815 assert!(!t1.is_large_spread_from(&t2, dec!(5)));
4816 }
4817
4818 #[test]
4821 fn test_age_secs_correct() {
4822 let tick = make_tick_at(1_000);
4823 assert!((tick.age_secs(3_000) - 2.0).abs() < 1e-9);
4824 }
4825
4826 #[test]
4827 fn test_age_secs_zero_when_now_equals_received() {
4828 let tick = make_tick_at(5_000);
4829 assert_eq!(tick.age_secs(5_000), 0.0);
4830 }
4831
4832 #[test]
4833 fn test_age_secs_zero_when_now_before_received() {
4834 let tick = make_tick_at(5_000);
4835 assert_eq!(tick.age_secs(1_000), 0.0);
4836 }
4837
4838 #[test]
4841 fn test_is_same_exchange_as_true_when_matching() {
4842 let t1 = make_tick_at(1_000); let t2 = make_tick_at(2_000); assert!(t1.is_same_exchange_as(&t2));
4845 }
4846
4847 #[test]
4848 fn test_is_same_exchange_as_false_when_different() {
4849 let t1 = make_tick_at(1_000); let mut t2 = make_tick_at(2_000);
4851 t2.exchange = Exchange::Coinbase;
4852 assert!(!t1.is_same_exchange_as(&t2));
4853 }
4854
4855 #[test]
4858 fn test_quote_age_ms_correct() {
4859 let tick = make_tick_at(1_000);
4860 assert_eq!(tick.quote_age_ms(3_000), 2_000);
4861 }
4862
4863 #[test]
4864 fn test_quote_age_ms_zero_when_now_before_received() {
4865 let tick = make_tick_at(5_000);
4866 assert_eq!(tick.quote_age_ms(1_000), 0);
4867 }
4868
4869 #[test]
4870 fn test_notional_value_correct() {
4871 use rust_decimal_macros::dec;
4872 let mut tick = make_tick_at(0);
4873 tick.price = dec!(100);
4874 tick.quantity = dec!(5);
4875 assert_eq!(tick.notional_value(), dec!(500));
4876 }
4877
4878 #[test]
4879 fn test_is_high_value_tick_true_when_above_threshold() {
4880 use rust_decimal_macros::dec;
4881 let mut tick = make_tick_at(0);
4882 tick.price = dec!(100);
4883 tick.quantity = dec!(10);
4884 assert!(tick.is_high_value_tick(dec!(500)));
4886 }
4887
4888 #[test]
4889 fn test_is_high_value_tick_false_when_below_threshold() {
4890 use rust_decimal_macros::dec;
4891 let mut tick = make_tick_at(0);
4892 tick.price = dec!(10);
4893 tick.quantity = dec!(2);
4894 assert!(!tick.is_high_value_tick(dec!(100)));
4896 }
4897
4898 #[test]
4901 fn test_is_buy_side_true_when_buy() {
4902 let mut tick = make_tick_at(0);
4903 tick.side = Some(TradeSide::Buy);
4904 assert!(tick.is_buy_side());
4905 }
4906
4907 #[test]
4908 fn test_is_buy_side_false_when_sell() {
4909 let mut tick = make_tick_at(0);
4910 tick.side = Some(TradeSide::Sell);
4911 assert!(!tick.is_buy_side());
4912 }
4913
4914 #[test]
4915 fn test_is_buy_side_false_when_none() {
4916 let mut tick = make_tick_at(0);
4917 tick.side = None;
4918 assert!(!tick.is_buy_side());
4919 }
4920
4921 #[test]
4922 fn test_is_sell_side_true_when_sell() {
4923 let mut tick = make_tick_at(0);
4924 tick.side = Some(TradeSide::Sell);
4925 assert!(tick.is_sell_side());
4926 }
4927
4928 #[test]
4929 fn test_price_in_range_true_when_within() {
4930 use rust_decimal_macros::dec;
4931 let mut tick = make_tick_at(0);
4932 tick.price = dec!(100);
4933 assert!(tick.price_in_range(dec!(90), dec!(110)));
4934 }
4935
4936 #[test]
4937 fn test_price_in_range_false_when_below() {
4938 use rust_decimal_macros::dec;
4939 let mut tick = make_tick_at(0);
4940 tick.price = dec!(80);
4941 assert!(!tick.price_in_range(dec!(90), dec!(110)));
4942 }
4943
4944 #[test]
4945 fn test_price_in_range_true_at_boundary() {
4946 use rust_decimal_macros::dec;
4947 let mut tick = make_tick_at(0);
4948 tick.price = dec!(90);
4949 assert!(tick.price_in_range(dec!(90), dec!(110)));
4950 }
4951
4952 #[test]
4955 fn test_is_zero_quantity_true_when_zero() {
4956 let mut tick = make_tick_at(0);
4957 tick.quantity = Decimal::ZERO;
4958 assert!(tick.is_zero_quantity());
4959 }
4960
4961 #[test]
4962 fn test_is_zero_quantity_false_when_nonzero() {
4963 let mut tick = make_tick_at(0);
4964 tick.quantity = Decimal::ONE;
4965 assert!(!tick.is_zero_quantity());
4966 }
4967
4968 #[test]
4971 fn test_is_large_tick_true_when_above_threshold() {
4972 let mut tick = make_tick_at(0);
4973 tick.quantity = Decimal::from(10u32);
4974 assert!(tick.is_large_tick(Decimal::from(5u32)));
4975 }
4976
4977 #[test]
4978 fn test_is_large_tick_false_when_at_threshold() {
4979 let mut tick = make_tick_at(0);
4980 tick.quantity = Decimal::from(5u32);
4981 assert!(!tick.is_large_tick(Decimal::from(5u32)));
4982 }
4983
4984 #[test]
4985 fn test_is_large_tick_false_when_below_threshold() {
4986 let mut tick = make_tick_at(0);
4987 tick.quantity = Decimal::from(1u32);
4988 assert!(!tick.is_large_tick(Decimal::from(5u32)));
4989 }
4990
4991 #[test]
4994 fn test_is_away_from_price_true_when_beyond_threshold() {
4995 let mut tick = make_tick_at(0);
4996 tick.price = Decimal::from(110u32);
4997 assert!(tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
4999 }
5000
5001 #[test]
5002 fn test_is_away_from_price_false_when_at_threshold() {
5003 let mut tick = make_tick_at(0);
5004 tick.price = Decimal::from(105u32);
5005 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
5007 }
5008
5009 #[test]
5010 fn test_is_away_from_price_false_when_equal() {
5011 let mut tick = make_tick_at(0);
5012 tick.price = Decimal::from(100u32);
5013 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(1u32)));
5014 }
5015
5016 #[test]
5019 fn test_is_within_spread_true_when_between() {
5020 let mut tick = make_tick_at(0);
5021 tick.price = Decimal::from(100u32);
5022 assert!(tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
5023 }
5024
5025 #[test]
5026 fn test_is_within_spread_false_when_at_bid() {
5027 let mut tick = make_tick_at(0);
5028 tick.price = Decimal::from(99u32);
5029 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
5030 }
5031
5032 #[test]
5033 fn test_is_within_spread_false_when_above_ask() {
5034 let mut tick = make_tick_at(0);
5035 tick.price = Decimal::from(102u32);
5036 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
5037 }
5038
5039 #[test]
5042 fn test_is_recent_true_when_within_threshold() {
5043 let tick = make_tick_at(9_500);
5044 assert!(tick.is_recent(1_000, 10_000));
5046 }
5047
5048 #[test]
5049 fn test_is_recent_false_when_beyond_threshold() {
5050 let tick = make_tick_at(8_000);
5051 assert!(!tick.is_recent(1_000, 10_000));
5053 }
5054
5055 #[test]
5056 fn test_is_recent_true_at_exact_threshold() {
5057 let tick = make_tick_at(9_000);
5058 assert!(tick.is_recent(1_000, 10_000));
5060 }
5061
5062 #[test]
5065 fn test_side_as_str_buy() {
5066 let mut tick = make_tick_at(0);
5067 tick.side = Some(TradeSide::Buy);
5068 assert_eq!(tick.side_as_str(), Some("buy"));
5069 }
5070
5071 #[test]
5072 fn test_side_as_str_sell() {
5073 let mut tick = make_tick_at(0);
5074 tick.side = Some(TradeSide::Sell);
5075 assert_eq!(tick.side_as_str(), Some("sell"));
5076 }
5077
5078 #[test]
5079 fn test_side_as_str_none_when_unknown() {
5080 let mut tick = make_tick_at(0);
5081 tick.side = None;
5082 assert!(tick.side_as_str().is_none());
5083 }
5084
5085 #[test]
5088 fn test_is_above_price_true_when_strictly_above() {
5089 let tick = make_tick_at(0); assert!(tick.is_above_price(rust_decimal_macros::dec!(99)));
5091 }
5092
5093 #[test]
5094 fn test_is_above_price_false_when_equal() {
5095 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(100)));
5097 }
5098
5099 #[test]
5100 fn test_is_above_price_false_when_below() {
5101 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(101)));
5103 }
5104
5105 #[test]
5108 fn test_price_change_from_positive_when_above_reference() {
5109 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(90)), rust_decimal_macros::dec!(10));
5111 }
5112
5113 #[test]
5114 fn test_price_change_from_negative_when_below_reference() {
5115 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(110)), rust_decimal_macros::dec!(-10));
5117 }
5118
5119 #[test]
5120 fn test_price_change_from_zero_when_equal() {
5121 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
5123 }
5124
5125 #[test]
5128 fn test_is_below_price_true_when_strictly_below() {
5129 let tick = make_tick_at(0); assert!(tick.is_below_price(rust_decimal_macros::dec!(101)));
5131 }
5132
5133 #[test]
5134 fn test_is_below_price_false_when_equal() {
5135 let tick = make_tick_at(0); assert!(!tick.is_below_price(rust_decimal_macros::dec!(100)));
5137 }
5138
5139 #[test]
5142 fn test_quantity_above_true_when_quantity_exceeds_threshold() {
5143 let tick = make_tick_at(0); assert!(tick.quantity_above(rust_decimal_macros::dec!(0)));
5145 }
5146
5147 #[test]
5148 fn test_quantity_above_false_when_quantity_equals_threshold() {
5149 let tick = make_tick_at(0); assert!(!tick.quantity_above(rust_decimal_macros::dec!(1)));
5151 }
5152
5153 #[test]
5156 fn test_is_at_price_true_when_equal() {
5157 let tick = make_tick_at(0); assert!(tick.is_at_price(rust_decimal_macros::dec!(100)));
5159 }
5160
5161 #[test]
5162 fn test_is_at_price_false_when_different() {
5163 let tick = make_tick_at(0); assert!(!tick.is_at_price(rust_decimal_macros::dec!(101)));
5165 }
5166
5167 #[test]
5170 fn test_is_round_number_true_when_divisible() {
5171 let tick = make_tick_at(0); assert!(tick.is_round_number(rust_decimal_macros::dec!(10)));
5173 assert!(tick.is_round_number(rust_decimal_macros::dec!(100)));
5174 }
5175
5176 #[test]
5177 fn test_is_round_number_false_when_not_divisible() {
5178 let tick = make_tick_at(0); assert!(!tick.is_round_number(rust_decimal_macros::dec!(3)));
5180 }
5181
5182 #[test]
5183 fn test_is_round_number_false_when_step_zero() {
5184 let tick = make_tick_at(0);
5185 assert!(!tick.is_round_number(rust_decimal_macros::dec!(0)));
5186 }
5187
5188 #[test]
5191 fn test_is_market_open_tick_true_when_within_session() {
5192 let tick = make_tick_at(500); assert!(tick.is_market_open_tick(100, 1_000));
5194 }
5195
5196 #[test]
5197 fn test_is_market_open_tick_false_when_before_session() {
5198 let tick = make_tick_at(50);
5199 assert!(!tick.is_market_open_tick(100, 1_000));
5200 }
5201
5202 #[test]
5203 fn test_is_market_open_tick_false_when_at_session_end() {
5204 let tick = make_tick_at(1_000);
5205 assert!(!tick.is_market_open_tick(100, 1_000)); }
5207
5208 #[test]
5211 fn test_signed_quantity_positive_for_buy() {
5212 let mut tick = make_tick_at(0);
5213 tick.side = Some(TradeSide::Buy);
5214 assert!(tick.signed_quantity() > rust_decimal::Decimal::ZERO);
5215 }
5216
5217 #[test]
5218 fn test_signed_quantity_negative_for_sell() {
5219 let mut tick = make_tick_at(0);
5220 tick.side = Some(TradeSide::Sell);
5221 assert!(tick.signed_quantity() < rust_decimal::Decimal::ZERO);
5222 }
5223
5224 #[test]
5225 fn test_signed_quantity_zero_for_unknown() {
5226 let tick = make_tick_at(0); assert_eq!(tick.signed_quantity(), rust_decimal::Decimal::ZERO);
5228 }
5229
5230 #[test]
5233 fn test_as_price_level_returns_price_and_quantity() {
5234 let tick = make_tick_at(0); let (p, q) = tick.as_price_level();
5236 assert_eq!(p, rust_decimal_macros::dec!(100));
5237 assert_eq!(q, rust_decimal_macros::dec!(1));
5238 }
5239
5240 fn make_sided_tick(qty: rust_decimal::Decimal, side: Option<TradeSide>) -> NormalizedTick {
5243 NormalizedTick {
5244 exchange: Exchange::Binance,
5245 symbol: "BTCUSDT".into(),
5246 price: rust_decimal_macros::dec!(100),
5247 quantity: qty,
5248 side,
5249 trade_id: None,
5250 exchange_ts_ms: None,
5251 received_at_ms: 0,
5252 }
5253 }
5254
5255 #[test]
5256 fn test_buy_volume_zero_for_empty_slice() {
5257 assert_eq!(NormalizedTick::buy_volume(&[]), rust_decimal::Decimal::ZERO);
5258 }
5259
5260 #[test]
5261 fn test_buy_volume_sums_only_buy_ticks() {
5262 let buy1 = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
5263 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5264 let buy2 = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
5265 let unknown = make_sided_tick(rust_decimal_macros::dec!(10), None);
5266 assert_eq!(
5267 NormalizedTick::buy_volume(&[buy1, sell, buy2, unknown]),
5268 rust_decimal_macros::dec!(7)
5269 );
5270 }
5271
5272 #[test]
5273 fn test_sell_volume_zero_for_empty_slice() {
5274 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal::Decimal::ZERO);
5275 }
5276
5277 #[test]
5278 fn test_sell_volume_sums_only_sell_ticks() {
5279 let buy = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
5280 let sell1 = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5281 let sell2 = make_sided_tick(rust_decimal_macros::dec!(4), Some(TradeSide::Sell));
5282 assert_eq!(
5283 NormalizedTick::sell_volume(&[buy, sell1, sell2]),
5284 rust_decimal_macros::dec!(7)
5285 );
5286 }
5287
5288 #[test]
5289 fn test_buy_sell_volumes_dont_include_unknown_side() {
5290 let buy = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
5291 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5292 let unknown = make_sided_tick(rust_decimal_macros::dec!(2), None);
5293 let ticks = [buy, sell, unknown];
5294 let total: rust_decimal::Decimal = ticks.iter().map(|t| t.quantity).sum();
5295 let accounted = NormalizedTick::buy_volume(&ticks) + NormalizedTick::sell_volume(&ticks);
5296 assert_eq!(accounted, rust_decimal_macros::dec!(8));
5298 assert!(accounted < total);
5299 }
5300
5301 fn make_tick_with_price(price: rust_decimal::Decimal) -> NormalizedTick {
5304 NormalizedTick {
5305 exchange: Exchange::Binance,
5306 symbol: "BTCUSDT".into(),
5307 price,
5308 quantity: rust_decimal_macros::dec!(1),
5309 side: None,
5310 trade_id: None,
5311 exchange_ts_ms: None,
5312 received_at_ms: 0,
5313 }
5314 }
5315
5316 #[test]
5317 fn test_price_range_none_for_empty_slice() {
5318 assert!(NormalizedTick::price_range(&[]).is_none());
5319 }
5320
5321 #[test]
5322 fn test_price_range_zero_for_single_tick() {
5323 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5324 assert_eq!(NormalizedTick::price_range(&[tick]), Some(rust_decimal_macros::dec!(0)));
5325 }
5326
5327 #[test]
5328 fn test_price_range_correct_for_multiple_ticks() {
5329 let t1 = make_tick_with_price(rust_decimal_macros::dec!(95));
5330 let t2 = make_tick_with_price(rust_decimal_macros::dec!(105));
5331 let t3 = make_tick_with_price(rust_decimal_macros::dec!(100));
5332 assert_eq!(NormalizedTick::price_range(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
5333 }
5334
5335 #[test]
5336 fn test_average_price_none_for_empty_slice() {
5337 assert!(NormalizedTick::average_price(&[]).is_none());
5338 }
5339
5340 #[test]
5341 fn test_average_price_equals_price_for_single_tick() {
5342 let tick = make_tick_with_price(rust_decimal_macros::dec!(200));
5343 assert_eq!(NormalizedTick::average_price(&[tick]), Some(rust_decimal_macros::dec!(200)));
5344 }
5345
5346 #[test]
5347 fn test_average_price_correct_for_multiple_ticks() {
5348 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5349 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5350 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5351 assert_eq!(NormalizedTick::average_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
5353 }
5354
5355 fn make_tick_pq(price: rust_decimal::Decimal, qty: rust_decimal::Decimal) -> NormalizedTick {
5358 NormalizedTick {
5359 exchange: Exchange::Binance,
5360 symbol: "BTCUSDT".into(),
5361 price,
5362 quantity: qty,
5363 side: None,
5364 trade_id: None,
5365 exchange_ts_ms: None,
5366 received_at_ms: 0,
5367 }
5368 }
5369
5370 #[test]
5371 fn test_vwap_none_for_empty_slice() {
5372 assert!(NormalizedTick::vwap(&[]).is_none());
5373 }
5374
5375 #[test]
5376 fn test_vwap_equals_price_for_single_tick() {
5377 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5378 assert_eq!(NormalizedTick::vwap(&[tick]), Some(rust_decimal_macros::dec!(100)));
5379 }
5380
5381 #[test]
5382 fn test_vwap_weighted_correctly() {
5383 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5385 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5386 assert_eq!(NormalizedTick::vwap(&[t1, t2]), Some(rust_decimal_macros::dec!(175)));
5387 }
5388
5389 #[test]
5390 fn test_vwap_none_for_zero_total_volume() {
5391 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(0));
5392 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(0));
5393 assert!(NormalizedTick::vwap(&[t1, t2]).is_none());
5394 }
5395
5396 #[test]
5399 fn test_count_above_price_zero_for_empty_slice() {
5400 assert_eq!(NormalizedTick::count_above_price(&[], rust_decimal_macros::dec!(100)), 0);
5401 }
5402
5403 #[test]
5404 fn test_count_above_price_correct() {
5405 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5406 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5407 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5408 assert_eq!(NormalizedTick::count_above_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
5409 }
5410
5411 #[test]
5412 fn test_count_below_price_correct() {
5413 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5414 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5415 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5416 assert_eq!(NormalizedTick::count_below_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
5417 }
5418
5419 #[test]
5420 fn test_count_above_at_threshold_excluded() {
5421 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5422 assert_eq!(NormalizedTick::count_above_price(&[tick], rust_decimal_macros::dec!(100)), 0);
5423 }
5424
5425 #[test]
5426 fn test_count_below_at_threshold_excluded() {
5427 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5428 assert_eq!(NormalizedTick::count_below_price(&[tick], rust_decimal_macros::dec!(100)), 0);
5429 }
5430
5431 #[test]
5434 fn test_total_notional_zero_for_empty_slice() {
5435 assert_eq!(NormalizedTick::total_notional(&[]), rust_decimal::Decimal::ZERO);
5436 }
5437
5438 #[test]
5439 fn test_total_notional_sums_all_ticks() {
5440 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5442 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5443 assert_eq!(NormalizedTick::total_notional(&[t1, t2]), rust_decimal_macros::dec!(800));
5444 }
5445
5446 #[test]
5447 fn test_buy_notional_only_includes_buy_side() {
5448 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5449 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5450 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
5451 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
5452 assert_eq!(NormalizedTick::buy_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(200));
5454 }
5455
5456 #[test]
5457 fn test_sell_notional_only_includes_sell_side() {
5458 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5459 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5460 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
5461 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
5462 assert_eq!(NormalizedTick::sell_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(600));
5464 }
5465
5466 #[test]
5469 fn test_median_price_none_for_empty_slice() {
5470 assert!(NormalizedTick::median_price(&[]).is_none());
5471 }
5472
5473 #[test]
5474 fn test_median_price_single_tick() {
5475 let tick = make_tick_with_price(rust_decimal_macros::dec!(150));
5476 assert_eq!(NormalizedTick::median_price(&[tick]), Some(rust_decimal_macros::dec!(150)));
5477 }
5478
5479 #[test]
5480 fn test_median_price_odd_count() {
5481 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5482 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5483 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5484 assert_eq!(NormalizedTick::median_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
5485 }
5486
5487 #[test]
5488 fn test_median_price_even_count() {
5489 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5490 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5491 assert_eq!(NormalizedTick::median_price(&[t1, t2]), Some(rust_decimal_macros::dec!(95)));
5493 }
5494
5495 #[test]
5498 fn test_net_volume_zero_for_empty_slice() {
5499 assert_eq!(NormalizedTick::net_volume(&[]), rust_decimal::Decimal::ZERO);
5500 }
5501
5502 #[test]
5503 fn test_net_volume_positive_when_more_buys() {
5504 let buy = NormalizedTick {
5505 side: Some(TradeSide::Buy),
5506 quantity: rust_decimal_macros::dec!(5),
5507 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5))
5508 };
5509 let sell = NormalizedTick {
5510 side: Some(TradeSide::Sell),
5511 quantity: rust_decimal_macros::dec!(3),
5512 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3))
5513 };
5514 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(2));
5515 }
5516
5517 #[test]
5518 fn test_net_volume_negative_when_more_sells() {
5519 let buy = NormalizedTick {
5520 side: Some(TradeSide::Buy),
5521 quantity: rust_decimal_macros::dec!(2),
5522 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2))
5523 };
5524 let sell = NormalizedTick {
5525 side: Some(TradeSide::Sell),
5526 quantity: rust_decimal_macros::dec!(7),
5527 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(7))
5528 };
5529 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(-5));
5530 }
5531
5532 #[test]
5535 fn test_average_quantity_none_for_empty_slice() {
5536 assert!(NormalizedTick::average_quantity(&[]).is_none());
5537 }
5538
5539 #[test]
5540 fn test_average_quantity_single_tick() {
5541 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5542 assert_eq!(NormalizedTick::average_quantity(&[tick]), Some(rust_decimal_macros::dec!(5)));
5543 }
5544
5545 #[test]
5546 fn test_average_quantity_multiple_ticks() {
5547 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5548 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(4));
5549 assert_eq!(NormalizedTick::average_quantity(&[t1, t2]), Some(rust_decimal_macros::dec!(3)));
5551 }
5552
5553 #[test]
5554 fn test_max_quantity_none_for_empty_slice() {
5555 assert!(NormalizedTick::max_quantity(&[]).is_none());
5556 }
5557
5558 #[test]
5559 fn test_max_quantity_returns_largest() {
5560 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5561 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(10));
5562 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5563 assert_eq!(NormalizedTick::max_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
5564 }
5565
5566 #[test]
5567 fn test_min_quantity_none_for_empty_slice() {
5568 assert!(NormalizedTick::min_quantity(&[]).is_none());
5569 }
5570
5571 #[test]
5572 fn test_min_quantity_returns_smallest() {
5573 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5574 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5575 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3));
5576 assert_eq!(NormalizedTick::min_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(1)));
5577 }
5578
5579 #[test]
5580 fn test_buy_count_zero_for_empty_slice() {
5581 assert_eq!(NormalizedTick::buy_count(&[]), 0);
5582 }
5583
5584 #[test]
5585 fn test_buy_count_counts_only_buys() {
5586 use rust_decimal_macros::dec;
5587 let mut buy = make_tick_pq(dec!(100), dec!(1));
5588 buy.side = Some(TradeSide::Buy);
5589 let mut sell = make_tick_pq(dec!(100), dec!(1));
5590 sell.side = Some(TradeSide::Sell);
5591 let neutral = make_tick_pq(dec!(100), dec!(1));
5592 assert_eq!(NormalizedTick::buy_count(&[buy, sell, neutral]), 1);
5593 }
5594
5595 #[test]
5596 fn test_sell_count_zero_for_empty_slice() {
5597 assert_eq!(NormalizedTick::sell_count(&[]), 0);
5598 }
5599
5600 #[test]
5601 fn test_sell_count_counts_only_sells() {
5602 use rust_decimal_macros::dec;
5603 let mut buy = make_tick_pq(dec!(100), dec!(1));
5604 buy.side = Some(TradeSide::Buy);
5605 let mut sell1 = make_tick_pq(dec!(100), dec!(1));
5606 sell1.side = Some(TradeSide::Sell);
5607 let mut sell2 = make_tick_pq(dec!(100), dec!(1));
5608 sell2.side = Some(TradeSide::Sell);
5609 assert_eq!(NormalizedTick::sell_count(&[buy, sell1, sell2]), 2);
5610 }
5611
5612 #[test]
5613 fn test_price_momentum_none_for_empty_slice() {
5614 assert!(NormalizedTick::price_momentum(&[]).is_none());
5615 }
5616
5617 #[test]
5618 fn test_price_momentum_none_for_single_tick() {
5619 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5620 assert!(NormalizedTick::price_momentum(&[t]).is_none());
5621 }
5622
5623 #[test]
5624 fn test_price_momentum_positive_when_price_rises() {
5625 use rust_decimal_macros::dec;
5626 let t1 = make_tick_pq(dec!(100), dec!(1));
5627 let t2 = make_tick_pq(dec!(110), dec!(1));
5628 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
5629 assert!((mom - 0.1).abs() < 1e-9);
5630 }
5631
5632 #[test]
5633 fn test_price_momentum_negative_when_price_falls() {
5634 use rust_decimal_macros::dec;
5635 let t1 = make_tick_pq(dec!(100), dec!(1));
5636 let t2 = make_tick_pq(dec!(90), dec!(1));
5637 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
5638 assert!(mom < 0.0);
5639 }
5640
5641 #[test]
5642 fn test_min_price_none_for_empty_slice() {
5643 assert!(NormalizedTick::min_price(&[]).is_none());
5644 }
5645
5646 #[test]
5647 fn test_min_price_returns_lowest() {
5648 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5649 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5650 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5651 assert_eq!(NormalizedTick::min_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(90)));
5652 }
5653
5654 #[test]
5655 fn test_max_price_none_for_empty_slice() {
5656 assert!(NormalizedTick::max_price(&[]).is_none());
5657 }
5658
5659 #[test]
5660 fn test_max_price_returns_highest() {
5661 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5662 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5663 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5664 assert_eq!(NormalizedTick::max_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(110)));
5665 }
5666
5667 #[test]
5668 fn test_price_std_dev_none_for_empty_slice() {
5669 assert!(NormalizedTick::price_std_dev(&[]).is_none());
5670 }
5671
5672 #[test]
5673 fn test_price_std_dev_none_for_single_tick() {
5674 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5675 assert!(NormalizedTick::price_std_dev(&[t]).is_none());
5676 }
5677
5678 #[test]
5679 fn test_price_std_dev_two_equal_prices_is_zero() {
5680 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5681 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5682 assert_eq!(NormalizedTick::price_std_dev(&[t1, t2]), Some(0.0));
5683 }
5684
5685 #[test]
5686 fn test_price_std_dev_positive_for_varying_prices() {
5687 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5688 let t2 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5689 let t3 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5690 let std = NormalizedTick::price_std_dev(&[t1, t2, t3]).unwrap();
5691 assert!(std > 0.0);
5692 }
5693
5694 #[test]
5695 fn test_buy_sell_ratio_none_for_empty_slice() {
5696 assert!(NormalizedTick::buy_sell_ratio(&[]).is_none());
5697 }
5698
5699 #[test]
5700 fn test_buy_sell_ratio_none_when_no_sells() {
5701 let mut t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5702 t.side = Some(TradeSide::Buy);
5703 assert!(NormalizedTick::buy_sell_ratio(&[t]).is_none());
5704 }
5705
5706 #[test]
5707 fn test_buy_sell_ratio_two_to_one() {
5708 use rust_decimal_macros::dec;
5709 let mut buy1 = make_tick_pq(dec!(100), dec!(2));
5710 buy1.side = Some(TradeSide::Buy);
5711 let mut buy2 = make_tick_pq(dec!(100), dec!(2));
5712 buy2.side = Some(TradeSide::Buy);
5713 let mut sell = make_tick_pq(dec!(100), dec!(2));
5714 sell.side = Some(TradeSide::Sell);
5715 let ratio = NormalizedTick::buy_sell_ratio(&[buy1, buy2, sell]).unwrap();
5716 assert!((ratio - 2.0).abs() < 1e-9);
5717 }
5718
5719 #[test]
5720 fn test_largest_trade_none_for_empty_slice() {
5721 assert!(NormalizedTick::largest_trade(&[]).is_none());
5722 }
5723
5724 #[test]
5725 fn test_largest_trade_returns_max_quantity_tick() {
5726 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5727 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(10));
5728 let t3 = make_tick_pq(rust_decimal_macros::dec!(150), rust_decimal_macros::dec!(5));
5729 let ticks = [t1, t2, t3];
5730 let largest = NormalizedTick::largest_trade(&ticks).unwrap();
5731 assert_eq!(largest.quantity, rust_decimal_macros::dec!(10));
5732 }
5733
5734 #[test]
5735 fn test_large_trade_count_zero_for_empty_slice() {
5736 assert_eq!(NormalizedTick::large_trade_count(&[], rust_decimal_macros::dec!(1)), 0);
5737 }
5738
5739 #[test]
5740 fn test_large_trade_count_counts_trades_above_threshold() {
5741 use rust_decimal_macros::dec;
5742 let t1 = make_tick_pq(dec!(100), dec!(0.5));
5743 let t2 = make_tick_pq(dec!(100), dec!(5));
5744 let t3 = make_tick_pq(dec!(100), dec!(10));
5745 assert_eq!(NormalizedTick::large_trade_count(&[t1, t2, t3], dec!(1)), 2);
5746 }
5747
5748 #[test]
5749 fn test_large_trade_count_strict_greater_than() {
5750 use rust_decimal_macros::dec;
5751 let t = make_tick_pq(dec!(100), dec!(1));
5752 assert_eq!(NormalizedTick::large_trade_count(&[t], dec!(1)), 0);
5754 }
5755
5756 #[test]
5757 fn test_price_iqr_none_for_small_slice() {
5758 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5759 assert!(NormalizedTick::price_iqr(&[t.clone(), t.clone(), t]).is_none());
5760 }
5761
5762 #[test]
5763 fn test_price_iqr_positive_for_varied_prices() {
5764 use rust_decimal_macros::dec;
5765 let ticks: Vec<_> = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)]
5766 .iter()
5767 .map(|&p| make_tick_pq(p, dec!(1)))
5768 .collect();
5769 let iqr = NormalizedTick::price_iqr(&ticks).unwrap();
5770 assert!(iqr > dec!(0));
5771 }
5772
5773 #[test]
5774 fn test_fraction_buy_none_for_empty_slice() {
5775 assert!(NormalizedTick::fraction_buy(&[]).is_none());
5776 }
5777
5778 #[test]
5779 fn test_fraction_buy_zero_when_no_buys() {
5780 use rust_decimal_macros::dec;
5781 let mut t = make_tick_pq(dec!(100), dec!(1));
5782 t.side = Some(TradeSide::Sell);
5783 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(0.0));
5784 }
5785
5786 #[test]
5787 fn test_fraction_buy_one_when_all_buys() {
5788 use rust_decimal_macros::dec;
5789 let mut t = make_tick_pq(dec!(100), dec!(1));
5790 t.side = Some(TradeSide::Buy);
5791 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(1.0));
5792 }
5793
5794 #[test]
5795 fn test_fraction_buy_half_for_equal_mix() {
5796 use rust_decimal_macros::dec;
5797 let mut buy = make_tick_pq(dec!(100), dec!(1));
5798 buy.side = Some(TradeSide::Buy);
5799 let mut sell = make_tick_pq(dec!(100), dec!(1));
5800 sell.side = Some(TradeSide::Sell);
5801 let frac = NormalizedTick::fraction_buy(&[buy, sell]).unwrap();
5802 assert!((frac - 0.5).abs() < 1e-9);
5803 }
5804
5805 #[test]
5806 fn test_std_quantity_none_for_empty_slice() {
5807 assert!(NormalizedTick::std_quantity(&[]).is_none());
5808 }
5809
5810 #[test]
5811 fn test_std_quantity_none_for_single_tick() {
5812 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5813 assert!(NormalizedTick::std_quantity(&[t]).is_none());
5814 }
5815
5816 #[test]
5817 fn test_std_quantity_zero_for_identical_quantities() {
5818 use rust_decimal_macros::dec;
5819 let t1 = make_tick_pq(dec!(100), dec!(5));
5820 let t2 = make_tick_pq(dec!(100), dec!(5));
5821 assert_eq!(NormalizedTick::std_quantity(&[t1, t2]), Some(0.0));
5822 }
5823
5824 #[test]
5825 fn test_std_quantity_positive_for_varied_quantities() {
5826 use rust_decimal_macros::dec;
5827 let t1 = make_tick_pq(dec!(100), dec!(1));
5828 let t2 = make_tick_pq(dec!(100), dec!(10));
5829 let std = NormalizedTick::std_quantity(&[t1, t2]).unwrap();
5830 assert!(std > 0.0);
5831 }
5832
5833 #[test]
5834 fn test_buy_pressure_none_for_empty_slice() {
5835 assert!(NormalizedTick::buy_pressure(&[]).is_none());
5836 }
5837
5838 #[test]
5839 fn test_buy_pressure_none_for_unsided_ticks() {
5840 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5841 assert!(NormalizedTick::buy_pressure(&[t]).is_none());
5842 }
5843
5844 #[test]
5845 fn test_buy_pressure_one_for_all_buys() {
5846 use rust_decimal_macros::dec;
5847 let mut t = make_tick_pq(dec!(100), dec!(1));
5848 t.side = Some(TradeSide::Buy);
5849 let bp = NormalizedTick::buy_pressure(&[t]).unwrap();
5850 assert!((bp - 1.0).abs() < 1e-9);
5851 }
5852
5853 #[test]
5854 fn test_buy_pressure_half_for_equal_volume() {
5855 use rust_decimal_macros::dec;
5856 let mut buy = make_tick_pq(dec!(100), dec!(5));
5857 buy.side = Some(TradeSide::Buy);
5858 let mut sell = make_tick_pq(dec!(100), dec!(5));
5859 sell.side = Some(TradeSide::Sell);
5860 let bp = NormalizedTick::buy_pressure(&[buy, sell]).unwrap();
5861 assert!((bp - 0.5).abs() < 1e-9);
5862 }
5863
5864 #[test]
5865 fn test_average_notional_none_for_empty_slice() {
5866 assert!(NormalizedTick::average_notional(&[]).is_none());
5867 }
5868
5869 #[test]
5870 fn test_average_notional_single_tick() {
5871 use rust_decimal_macros::dec;
5872 let t = make_tick_pq(dec!(100), dec!(2));
5873 assert_eq!(NormalizedTick::average_notional(&[t]), Some(dec!(200)));
5874 }
5875
5876 #[test]
5877 fn test_average_notional_multiple_ticks() {
5878 use rust_decimal_macros::dec;
5879 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)));
5883 }
5884
5885 #[test]
5886 fn test_count_neutral_zero_for_empty_slice() {
5887 assert_eq!(NormalizedTick::count_neutral(&[]), 0);
5888 }
5889
5890 #[test]
5891 fn test_count_neutral_counts_sideless_ticks() {
5892 use rust_decimal_macros::dec;
5893 let neutral = make_tick_pq(dec!(100), dec!(1)); let mut buy = make_tick_pq(dec!(100), dec!(1));
5895 buy.side = Some(TradeSide::Buy);
5896 assert_eq!(NormalizedTick::count_neutral(&[neutral, buy]), 1);
5897 }
5898
5899 #[test]
5900 fn test_recent_returns_all_when_n_exceeds_len() {
5901 use rust_decimal_macros::dec;
5902 let ticks = vec![
5903 make_tick_pq(dec!(100), dec!(1)),
5904 make_tick_pq(dec!(110), dec!(1)),
5905 ];
5906 assert_eq!(NormalizedTick::recent(&ticks, 10).len(), 2);
5907 }
5908
5909 #[test]
5910 fn test_recent_returns_last_n() {
5911 use rust_decimal_macros::dec;
5912 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120), dec!(130)]
5913 .iter()
5914 .map(|&p| make_tick_pq(p, dec!(1)))
5915 .collect();
5916 let recent = NormalizedTick::recent(&ticks, 2);
5917 assert_eq!(recent.len(), 2);
5918 assert_eq!(recent[0].price, dec!(120));
5919 assert_eq!(recent[1].price, dec!(130));
5920 }
5921
5922 #[test]
5923 fn test_price_linear_slope_none_for_single_tick() {
5924 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5925 assert!(NormalizedTick::price_linear_slope(&[t]).is_none());
5926 }
5927
5928 #[test]
5929 fn test_price_linear_slope_positive_for_rising_prices() {
5930 use rust_decimal_macros::dec;
5931 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120)]
5932 .iter()
5933 .map(|&p| make_tick_pq(p, dec!(1)))
5934 .collect();
5935 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
5936 assert!(slope > 0.0);
5937 }
5938
5939 #[test]
5940 fn test_price_linear_slope_negative_for_falling_prices() {
5941 use rust_decimal_macros::dec;
5942 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(100)]
5943 .iter()
5944 .map(|&p| make_tick_pq(p, dec!(1)))
5945 .collect();
5946 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
5947 assert!(slope < 0.0);
5948 }
5949
5950 #[test]
5951 fn test_notional_std_dev_none_for_single_tick() {
5952 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5953 assert!(NormalizedTick::notional_std_dev(&[t]).is_none());
5954 }
5955
5956 #[test]
5957 fn test_notional_std_dev_zero_for_identical_notionals() {
5958 use rust_decimal_macros::dec;
5959 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));
5962 }
5963
5964 #[test]
5965 fn test_notional_std_dev_positive_for_varied_notionals() {
5966 use rust_decimal_macros::dec;
5967 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();
5970 assert!(std > 0.0);
5971 }
5972
5973 #[test]
5974 fn test_monotone_up_true_for_empty_slice() {
5975 assert!(NormalizedTick::monotone_up(&[]));
5976 }
5977
5978 #[test]
5979 fn test_monotone_up_true_for_non_decreasing_prices() {
5980 use rust_decimal_macros::dec;
5981 let ticks: Vec<_> = [dec!(100), dec!(100), dec!(110), dec!(120)]
5982 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5983 assert!(NormalizedTick::monotone_up(&ticks));
5984 }
5985
5986 #[test]
5987 fn test_monotone_up_false_for_any_decrease() {
5988 use rust_decimal_macros::dec;
5989 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(105)]
5990 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5991 assert!(!NormalizedTick::monotone_up(&ticks));
5992 }
5993
5994 #[test]
5995 fn test_monotone_down_true_for_non_increasing_prices() {
5996 use rust_decimal_macros::dec;
5997 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(110), dec!(100)]
5998 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5999 assert!(NormalizedTick::monotone_down(&ticks));
6000 }
6001
6002 #[test]
6003 fn test_monotone_down_false_for_any_increase() {
6004 use rust_decimal_macros::dec;
6005 let ticks: Vec<_> = [dec!(100), dec!(90), dec!(95)]
6006 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
6007 assert!(!NormalizedTick::monotone_down(&ticks));
6008 }
6009
6010 #[test]
6011 fn test_volume_at_price_zero_for_empty_slice() {
6012 assert_eq!(NormalizedTick::volume_at_price(&[], rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
6013 }
6014
6015 #[test]
6016 fn test_volume_at_price_sums_matching_ticks() {
6017 use rust_decimal_macros::dec;
6018 let t1 = make_tick_pq(dec!(100), dec!(2));
6019 let t2 = make_tick_pq(dec!(100), dec!(3));
6020 let t3 = make_tick_pq(dec!(110), dec!(5));
6021 assert_eq!(NormalizedTick::volume_at_price(&[t1, t2, t3], dec!(100)), dec!(5));
6022 }
6023
6024 #[test]
6025 fn test_last_price_none_for_empty_slice() {
6026 assert!(NormalizedTick::last_price(&[]).is_none());
6027 }
6028
6029 #[test]
6030 fn test_last_price_returns_last_tick_price() {
6031 use rust_decimal_macros::dec;
6032 let t1 = make_tick_pq(dec!(100), dec!(1));
6033 let t2 = make_tick_pq(dec!(110), dec!(1));
6034 assert_eq!(NormalizedTick::last_price(&[t1, t2]), Some(dec!(110)));
6035 }
6036
6037 #[test]
6038 fn test_longest_buy_streak_zero_for_empty() {
6039 assert_eq!(NormalizedTick::longest_buy_streak(&[]), 0);
6040 }
6041
6042 #[test]
6043 fn test_longest_buy_streak_counts_consecutive_buys() {
6044 use rust_decimal_macros::dec;
6045 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
6046 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
6047 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
6048 let mut b3 = make_tick_pq(dec!(100), dec!(1)); b3.side = Some(TradeSide::Buy);
6049 assert_eq!(NormalizedTick::longest_buy_streak(&[b1, b2, s, b3]), 2);
6051 }
6052
6053 #[test]
6054 fn test_longest_sell_streak_zero_for_no_sells() {
6055 use rust_decimal_macros::dec;
6056 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
6057 assert_eq!(NormalizedTick::longest_sell_streak(&[b]), 0);
6058 }
6059
6060 #[test]
6061 fn test_longest_sell_streak_correct() {
6062 use rust_decimal_macros::dec;
6063 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
6064 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
6065 let mut s2 = make_tick_pq(dec!(100), dec!(1)); s2.side = Some(TradeSide::Sell);
6066 let mut s3 = make_tick_pq(dec!(100), dec!(1)); s3.side = Some(TradeSide::Sell);
6067 assert_eq!(NormalizedTick::longest_sell_streak(&[b, s1, s2, s3]), 3);
6068 }
6069
6070 #[test]
6071 fn test_price_at_max_volume_none_for_empty() {
6072 assert!(NormalizedTick::price_at_max_volume(&[]).is_none());
6073 }
6074
6075 #[test]
6076 fn test_price_at_max_volume_returns_dominant_price() {
6077 use rust_decimal_macros::dec;
6078 let t1 = make_tick_pq(dec!(100), dec!(1));
6079 let t2 = make_tick_pq(dec!(200), dec!(5));
6080 let t3 = make_tick_pq(dec!(200), dec!(3));
6081 assert_eq!(NormalizedTick::price_at_max_volume(&[t1, t2, t3]), Some(dec!(200)));
6083 }
6084
6085 #[test]
6086 fn test_recent_volume_zero_for_empty_slice() {
6087 assert_eq!(NormalizedTick::recent_volume(&[], 5), rust_decimal_macros::dec!(0));
6088 }
6089
6090 #[test]
6091 fn test_recent_volume_sums_last_n_ticks() {
6092 use rust_decimal_macros::dec;
6093 let ticks: Vec<_> = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)]
6094 .iter().map(|&q| make_tick_pq(dec!(100), q)).collect();
6095 assert_eq!(NormalizedTick::recent_volume(&ticks, 3), dec!(12));
6097 }
6098
6099 #[test]
6102 fn test_first_price_none_for_empty_slice() {
6103 assert!(NormalizedTick::first_price(&[]).is_none());
6104 }
6105
6106 #[test]
6107 fn test_first_price_returns_first_tick_price() {
6108 use rust_decimal_macros::dec;
6109 let ticks = vec![make_tick_pq(dec!(50), dec!(1)), make_tick_pq(dec!(60), dec!(1))];
6110 assert_eq!(NormalizedTick::first_price(&ticks), Some(dec!(50)));
6111 }
6112
6113 #[test]
6116 fn test_price_return_pct_none_for_single_tick() {
6117 use rust_decimal_macros::dec;
6118 assert!(NormalizedTick::price_return_pct(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6119 }
6120
6121 #[test]
6122 fn test_price_return_pct_positive_for_rising_price() {
6123 use rust_decimal_macros::dec;
6124 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(110), dec!(1))];
6125 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
6126 assert!((pct - 0.1).abs() < 1e-9);
6127 }
6128
6129 #[test]
6130 fn test_price_return_pct_negative_for_falling_price() {
6131 use rust_decimal_macros::dec;
6132 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(90), dec!(1))];
6133 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
6134 assert!((pct - (-0.1)).abs() < 1e-9);
6135 }
6136
6137 #[test]
6140 fn test_volume_above_price_zero_for_empty_slice() {
6141 use rust_decimal_macros::dec;
6142 assert_eq!(NormalizedTick::volume_above_price(&[], dec!(100)), dec!(0));
6143 }
6144
6145 #[test]
6146 fn test_volume_above_price_sums_above_threshold() {
6147 use rust_decimal_macros::dec;
6148 let ticks = vec![
6149 make_tick_pq(dec!(90), dec!(5)),
6150 make_tick_pq(dec!(100), dec!(10)),
6151 make_tick_pq(dec!(110), dec!(3)),
6152 ];
6153 assert_eq!(NormalizedTick::volume_above_price(&ticks, dec!(100)), dec!(3));
6155 }
6156
6157 #[test]
6158 fn test_volume_below_price_sums_below_threshold() {
6159 use rust_decimal_macros::dec;
6160 let ticks = vec![
6161 make_tick_pq(dec!(90), dec!(5)),
6162 make_tick_pq(dec!(100), dec!(10)),
6163 make_tick_pq(dec!(110), dec!(3)),
6164 ];
6165 assert_eq!(NormalizedTick::volume_below_price(&ticks, dec!(100)), dec!(5));
6167 }
6168
6169 #[test]
6172 fn test_qwap_none_for_empty_slice() {
6173 assert!(NormalizedTick::quantity_weighted_avg_price(&[]).is_none());
6174 }
6175
6176 #[test]
6177 fn test_qwap_correct_for_equal_quantities() {
6178 use rust_decimal_macros::dec;
6179 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(1))];
6181 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(150)));
6182 }
6183
6184 #[test]
6185 fn test_qwap_weighted_towards_higher_volume() {
6186 use rust_decimal_macros::dec;
6187 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(3))];
6189 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(175)));
6190 }
6191
6192 #[test]
6195 fn test_tick_count_above_price_zero_for_empty_slice() {
6196 use rust_decimal_macros::dec;
6197 assert_eq!(NormalizedTick::tick_count_above_price(&[], dec!(100)), 0);
6198 }
6199
6200 #[test]
6201 fn test_tick_count_above_price_correct() {
6202 use rust_decimal_macros::dec;
6203 let ticks = vec![
6204 make_tick_pq(dec!(90), dec!(1)),
6205 make_tick_pq(dec!(100), dec!(1)),
6206 make_tick_pq(dec!(110), dec!(1)),
6207 make_tick_pq(dec!(120), dec!(1)),
6208 ];
6209 assert_eq!(NormalizedTick::tick_count_above_price(&ticks, dec!(100)), 2);
6210 }
6211
6212 #[test]
6213 fn test_tick_count_below_price_correct() {
6214 use rust_decimal_macros::dec;
6215 let ticks = vec![
6216 make_tick_pq(dec!(90), dec!(1)),
6217 make_tick_pq(dec!(100), dec!(1)),
6218 make_tick_pq(dec!(110), dec!(1)),
6219 ];
6220 assert_eq!(NormalizedTick::tick_count_below_price(&ticks, dec!(100)), 1);
6221 }
6222
6223 #[test]
6226 fn test_price_at_percentile_none_for_empty_slice() {
6227 use rust_decimal_macros::dec;
6228 assert!(NormalizedTick::price_at_percentile(&[], 0.5).is_none());
6229 }
6230
6231 #[test]
6232 fn test_price_at_percentile_none_for_out_of_range() {
6233 use rust_decimal_macros::dec;
6234 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
6235 assert!(NormalizedTick::price_at_percentile(&ticks, 1.5).is_none());
6236 }
6237
6238 #[test]
6239 fn test_price_at_percentile_median_for_sorted_prices() {
6240 use rust_decimal_macros::dec;
6241 let ticks = vec![
6242 make_tick_pq(dec!(10), dec!(1)),
6243 make_tick_pq(dec!(20), dec!(1)),
6244 make_tick_pq(dec!(30), dec!(1)),
6245 make_tick_pq(dec!(40), dec!(1)),
6246 make_tick_pq(dec!(50), dec!(1)),
6247 ];
6248 assert_eq!(NormalizedTick::price_at_percentile(&ticks, 0.5), Some(dec!(30)));
6250 }
6251
6252 #[test]
6255 fn test_unique_price_count_zero_for_empty() {
6256 assert_eq!(NormalizedTick::unique_price_count(&[]), 0);
6257 }
6258
6259 #[test]
6260 fn test_unique_price_count_counts_distinct_prices() {
6261 use rust_decimal_macros::dec;
6262 let ticks = vec![
6263 make_tick_pq(dec!(100), dec!(1)),
6264 make_tick_pq(dec!(100), dec!(2)),
6265 make_tick_pq(dec!(110), dec!(1)),
6266 make_tick_pq(dec!(120), dec!(1)),
6267 ];
6268 assert_eq!(NormalizedTick::unique_price_count(&ticks), 3);
6269 }
6270
6271 #[test]
6274 fn test_sell_volume_zero_for_empty() {
6275 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal_macros::dec!(0));
6276 }
6277
6278 #[test]
6279 fn test_sell_volume_sums_sell_side_only() {
6280 use rust_decimal_macros::dec;
6281 let mut buy_tick = make_tick_pq(dec!(100), dec!(5));
6282 buy_tick.side = Some(TradeSide::Buy);
6283 let mut sell_tick = make_tick_pq(dec!(100), dec!(3));
6284 sell_tick.side = Some(TradeSide::Sell);
6285 let no_side_tick = make_tick_pq(dec!(100), dec!(10));
6286 let ticks = [buy_tick, sell_tick, no_side_tick];
6287 assert_eq!(NormalizedTick::sell_volume(&ticks), dec!(3));
6288 assert_eq!(NormalizedTick::buy_volume(&ticks), dec!(5));
6289 }
6290
6291 #[test]
6294 fn test_avg_inter_tick_spread_none_for_single_tick() {
6295 use rust_decimal_macros::dec;
6296 assert!(NormalizedTick::avg_inter_tick_spread(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6297 }
6298
6299 #[test]
6300 fn test_avg_inter_tick_spread_correct_for_uniform_moves() {
6301 use rust_decimal_macros::dec;
6302 let ticks = vec![
6304 make_tick_pq(dec!(100), dec!(1)),
6305 make_tick_pq(dec!(102), dec!(1)),
6306 make_tick_pq(dec!(104), dec!(1)),
6307 ];
6308 let spread = NormalizedTick::avg_inter_tick_spread(&ticks).unwrap();
6309 assert!((spread - 2.0).abs() < 1e-9);
6310 }
6311
6312 #[test]
6315 fn test_price_range_none_for_empty() {
6316 assert!(NormalizedTick::price_range(&[]).is_none());
6317 }
6318
6319 #[test]
6320 fn test_price_range_correct() {
6321 use rust_decimal_macros::dec;
6322 let ticks = vec![
6323 make_tick_pq(dec!(90), dec!(1)),
6324 make_tick_pq(dec!(110), dec!(1)),
6325 make_tick_pq(dec!(100), dec!(1)),
6326 ];
6327 assert_eq!(NormalizedTick::price_range(&ticks), Some(dec!(20)));
6328 }
6329
6330 #[test]
6333 fn test_median_price_none_for_empty() {
6334 assert!(NormalizedTick::median_price(&[]).is_none());
6335 }
6336
6337 #[test]
6338 fn test_median_price_returns_middle_value() {
6339 use rust_decimal_macros::dec;
6340 let ticks = vec![
6341 make_tick_pq(dec!(10), dec!(1)),
6342 make_tick_pq(dec!(30), dec!(1)),
6343 make_tick_pq(dec!(20), dec!(1)),
6344 ];
6345 assert_eq!(NormalizedTick::median_price(&ticks), Some(dec!(20)));
6347 }
6348
6349 #[test]
6352 fn test_largest_sell_none_for_no_sell_ticks() {
6353 use rust_decimal_macros::dec;
6354 let mut t = make_tick_pq(dec!(100), dec!(5));
6355 t.side = Some(TradeSide::Buy);
6356 assert!(NormalizedTick::largest_sell(&[t]).is_none());
6357 }
6358
6359 #[test]
6360 fn test_largest_sell_returns_max_sell_qty() {
6361 use rust_decimal_macros::dec;
6362 let mut t1 = make_tick_pq(dec!(100), dec!(3));
6363 t1.side = Some(TradeSide::Sell);
6364 let mut t2 = make_tick_pq(dec!(100), dec!(7));
6365 t2.side = Some(TradeSide::Sell);
6366 assert_eq!(NormalizedTick::largest_sell(&[t1, t2]), Some(dec!(7)));
6367 }
6368
6369 #[test]
6370 fn test_largest_buy_returns_max_buy_qty() {
6371 use rust_decimal_macros::dec;
6372 let mut t1 = make_tick_pq(dec!(100), dec!(2));
6373 t1.side = Some(TradeSide::Buy);
6374 let mut t2 = make_tick_pq(dec!(100), dec!(9));
6375 t2.side = Some(TradeSide::Buy);
6376 assert_eq!(NormalizedTick::largest_buy(&[t1, t2]), Some(dec!(9)));
6377 }
6378
6379 #[test]
6382 fn test_trade_count_zero_for_empty() {
6383 assert_eq!(NormalizedTick::trade_count(&[]), 0);
6384 }
6385
6386 #[test]
6387 fn test_trade_count_matches_slice_length() {
6388 use rust_decimal_macros::dec;
6389 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
6390 assert_eq!(NormalizedTick::trade_count(&ticks), 2);
6391 }
6392
6393 #[test]
6396 fn test_price_acceleration_none_for_fewer_than_3() {
6397 use rust_decimal_macros::dec;
6398 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6399 assert!(NormalizedTick::price_acceleration(&ticks).is_none());
6400 }
6401
6402 #[test]
6403 fn test_price_acceleration_zero_for_constant_velocity() {
6404 use rust_decimal_macros::dec;
6405 let ticks = vec![
6407 make_tick_pq(dec!(100), dec!(1)),
6408 make_tick_pq(dec!(102), dec!(1)),
6409 make_tick_pq(dec!(104), dec!(1)),
6410 ];
6411 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
6412 assert!((acc - 0.0).abs() < 1e-9);
6413 }
6414
6415 #[test]
6416 fn test_price_acceleration_positive_when_speeding_up() {
6417 use rust_decimal_macros::dec;
6418 let ticks = vec![
6420 make_tick_pq(dec!(100), dec!(1)),
6421 make_tick_pq(dec!(101), dec!(1)),
6422 make_tick_pq(dec!(103), dec!(1)),
6423 ];
6424 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
6425 assert!((acc - 1.0).abs() < 1e-9);
6426 }
6427
6428 #[test]
6431 fn test_buy_sell_diff_zero_for_empty() {
6432 assert_eq!(NormalizedTick::buy_sell_diff(&[]), rust_decimal_macros::dec!(0));
6433 }
6434
6435 #[test]
6436 fn test_buy_sell_diff_positive_for_net_buying() {
6437 use rust_decimal_macros::dec;
6438 let mut t1 = make_tick_pq(dec!(100), dec!(10));
6439 t1.side = Some(TradeSide::Buy);
6440 let mut t2 = make_tick_pq(dec!(100), dec!(3));
6441 t2.side = Some(TradeSide::Sell);
6442 assert_eq!(NormalizedTick::buy_sell_diff(&[t1, t2]), dec!(7));
6443 }
6444
6445 #[test]
6448 fn test_is_aggressive_buy_true_when_exceeds_avg() {
6449 use rust_decimal_macros::dec;
6450 let mut t = make_tick_pq(dec!(100), dec!(15));
6451 t.side = Some(TradeSide::Buy);
6452 assert!(NormalizedTick::is_aggressive_buy(&t, dec!(10)));
6453 }
6454
6455 #[test]
6456 fn test_is_aggressive_buy_false_when_not_buy_side() {
6457 use rust_decimal_macros::dec;
6458 let mut t = make_tick_pq(dec!(100), dec!(15));
6459 t.side = Some(TradeSide::Sell);
6460 assert!(!NormalizedTick::is_aggressive_buy(&t, dec!(10)));
6461 }
6462
6463 #[test]
6464 fn test_is_aggressive_sell_true_when_exceeds_avg() {
6465 use rust_decimal_macros::dec;
6466 let mut t = make_tick_pq(dec!(100), dec!(20));
6467 t.side = Some(TradeSide::Sell);
6468 assert!(NormalizedTick::is_aggressive_sell(&t, dec!(10)));
6469 }
6470
6471 #[test]
6474 fn test_notional_volume_zero_for_empty() {
6475 assert_eq!(NormalizedTick::notional_volume(&[]), rust_decimal_macros::dec!(0));
6476 }
6477
6478 #[test]
6479 fn test_notional_volume_correct() {
6480 use rust_decimal_macros::dec;
6481 let ticks = vec![
6482 make_tick_pq(dec!(100), dec!(2)), make_tick_pq(dec!(50), dec!(4)), ];
6485 assert_eq!(NormalizedTick::notional_volume(&ticks), dec!(400));
6486 }
6487
6488 #[test]
6491 fn test_weighted_side_score_none_for_empty() {
6492 assert!(NormalizedTick::weighted_side_score(&[]).is_none());
6493 }
6494
6495 #[test]
6496 fn test_weighted_side_score_correct_for_all_buys() {
6497 use rust_decimal_macros::dec;
6498 let mut t = make_tick_pq(dec!(100), dec!(10));
6499 t.side = Some(TradeSide::Buy);
6500 let score = NormalizedTick::weighted_side_score(&[t]).unwrap();
6502 assert!((score - 1.0).abs() < 1e-9);
6503 }
6504
6505 #[test]
6508 fn test_time_span_none_for_single_tick() {
6509 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
6510 assert!(NormalizedTick::time_span_ms(&[t]).is_none());
6511 }
6512
6513 #[test]
6514 fn test_time_span_correct_for_two_ticks() {
6515 use rust_decimal_macros::dec;
6516 let mut t1 = make_tick_pq(dec!(100), dec!(1));
6517 t1.received_at_ms = 1000;
6518 let mut t2 = make_tick_pq(dec!(101), dec!(1));
6519 t2.received_at_ms = 5000;
6520 assert_eq!(NormalizedTick::time_span_ms(&[t1, t2]), Some(4000));
6521 }
6522
6523 #[test]
6526 fn test_price_above_vwap_count_none_for_empty() {
6527 assert!(NormalizedTick::price_above_vwap_count(&[]).is_none());
6528 }
6529
6530 #[test]
6531 fn test_price_above_vwap_count_correct() {
6532 use rust_decimal_macros::dec;
6533 let ticks = vec![
6535 make_tick_pq(dec!(90), dec!(1)),
6536 make_tick_pq(dec!(100), dec!(1)),
6537 make_tick_pq(dec!(110), dec!(1)),
6538 ];
6539 assert_eq!(NormalizedTick::price_above_vwap_count(&ticks), Some(1));
6540 }
6541
6542 #[test]
6545 fn test_avg_trade_size_none_for_empty() {
6546 assert!(NormalizedTick::avg_trade_size(&[]).is_none());
6547 }
6548
6549 #[test]
6550 fn test_avg_trade_size_correct() {
6551 use rust_decimal_macros::dec;
6552 let ticks = vec![
6553 make_tick_pq(dec!(100), dec!(2)),
6554 make_tick_pq(dec!(101), dec!(4)),
6555 ];
6556 assert_eq!(NormalizedTick::avg_trade_size(&ticks), Some(dec!(3)));
6557 }
6558
6559 #[test]
6562 fn test_volume_concentration_none_for_empty() {
6563 assert!(NormalizedTick::volume_concentration(&[]).is_none());
6564 }
6565
6566 #[test]
6567 fn test_volume_concentration_is_one_for_single_tick() {
6568 use rust_decimal_macros::dec;
6569 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
6570 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
6571 assert!((c - 1.0).abs() < 1e-9);
6572 }
6573
6574 #[test]
6575 fn test_volume_concentration_in_range() {
6576 use rust_decimal_macros::dec;
6577 let ticks = vec![
6578 make_tick_pq(dec!(100), dec!(1)),
6579 make_tick_pq(dec!(101), dec!(1)),
6580 make_tick_pq(dec!(102), dec!(1)),
6581 make_tick_pq(dec!(103), dec!(10)),
6582 ];
6583 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
6584 assert!(c > 0.0 && c <= 1.0, "expected value in (0,1], got {}", c);
6585 }
6586
6587 #[test]
6590 fn test_trade_imbalance_score_none_for_empty() {
6591 assert!(NormalizedTick::trade_imbalance_score(&[]).is_none());
6592 }
6593
6594 #[test]
6595 fn test_trade_imbalance_score_positive_for_all_buys() {
6596 use rust_decimal_macros::dec;
6597 let mut t = make_tick_pq(dec!(100), dec!(1));
6598 t.side = Some(TradeSide::Buy);
6599 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
6600 assert!(score > 0.0);
6601 }
6602
6603 #[test]
6604 fn test_trade_imbalance_score_negative_for_all_sells() {
6605 use rust_decimal_macros::dec;
6606 let mut t = make_tick_pq(dec!(100), dec!(1));
6607 t.side = Some(TradeSide::Sell);
6608 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
6609 assert!(score < 0.0);
6610 }
6611
6612 #[test]
6615 fn test_price_entropy_none_for_empty() {
6616 assert!(NormalizedTick::price_entropy(&[]).is_none());
6617 }
6618
6619 #[test]
6620 fn test_price_entropy_zero_for_single_price() {
6621 use rust_decimal_macros::dec;
6622 let ticks = vec![
6623 make_tick_pq(dec!(100), dec!(1)),
6624 make_tick_pq(dec!(100), dec!(2)),
6625 ];
6626 let e = NormalizedTick::price_entropy(&ticks).unwrap();
6627 assert!((e - 0.0).abs() < 1e-9, "identical prices should have zero entropy, got {}", e);
6628 }
6629
6630 #[test]
6631 fn test_price_entropy_positive_for_varied_prices() {
6632 use rust_decimal_macros::dec;
6633 let ticks = vec![
6634 make_tick_pq(dec!(100), dec!(1)),
6635 make_tick_pq(dec!(101), dec!(1)),
6636 make_tick_pq(dec!(102), dec!(1)),
6637 ];
6638 let e = NormalizedTick::price_entropy(&ticks).unwrap();
6639 assert!(e > 0.0, "varied prices should have positive entropy, got {}", e);
6640 }
6641
6642 #[test]
6645 fn test_buy_avg_price_none_for_no_buys() {
6646 use rust_decimal_macros::dec;
6647 let mut t = make_tick_pq(dec!(100), dec!(1));
6648 t.side = Some(TradeSide::Sell);
6649 assert!(NormalizedTick::buy_avg_price(&[t]).is_none());
6650 }
6651
6652 #[test]
6653 fn test_buy_avg_price_correct() {
6654 use rust_decimal_macros::dec;
6655 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.side = Some(TradeSide::Buy);
6656 let mut t2 = make_tick_pq(dec!(110), dec!(1)); t2.side = Some(TradeSide::Buy);
6657 assert_eq!(NormalizedTick::buy_avg_price(&[t1, t2]), Some(dec!(105)));
6658 }
6659
6660 #[test]
6661 fn test_sell_avg_price_none_for_no_sells() {
6662 use rust_decimal_macros::dec;
6663 let mut t = make_tick_pq(dec!(100), dec!(1));
6664 t.side = Some(TradeSide::Buy);
6665 assert!(NormalizedTick::sell_avg_price(&[t]).is_none());
6666 }
6667
6668 #[test]
6669 fn test_sell_avg_price_correct() {
6670 use rust_decimal_macros::dec;
6671 let mut t1 = make_tick_pq(dec!(90), dec!(1)); t1.side = Some(TradeSide::Sell);
6672 let mut t2 = make_tick_pq(dec!(100), dec!(1)); t2.side = Some(TradeSide::Sell);
6673 assert_eq!(NormalizedTick::sell_avg_price(&[t1, t2]), Some(dec!(95)));
6674 }
6675
6676 #[test]
6679 fn test_price_skewness_none_for_fewer_than_3() {
6680 use rust_decimal_macros::dec;
6681 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6682 assert!(NormalizedTick::price_skewness(&ticks).is_none());
6683 }
6684
6685 #[test]
6686 fn test_price_skewness_zero_for_symmetric() {
6687 use rust_decimal_macros::dec;
6688 let ticks = vec![
6690 make_tick_pq(dec!(1), dec!(1)),
6691 make_tick_pq(dec!(2), dec!(1)),
6692 make_tick_pq(dec!(3), dec!(1)),
6693 ];
6694 let s = NormalizedTick::price_skewness(&ticks).unwrap();
6695 assert!(s.abs() < 1e-9, "symmetric should have near-zero skew, got {}", s);
6696 }
6697
6698 #[test]
6701 fn test_quantity_skewness_none_for_fewer_than_3() {
6702 use rust_decimal_macros::dec;
6703 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
6704 assert!(NormalizedTick::quantity_skewness(&ticks).is_none());
6705 }
6706
6707 #[test]
6708 fn test_quantity_skewness_positive_for_right_skewed() {
6709 use rust_decimal_macros::dec;
6710 let ticks = vec![
6712 make_tick_pq(dec!(100), dec!(1)),
6713 make_tick_pq(dec!(101), dec!(1)),
6714 make_tick_pq(dec!(102), dec!(100)),
6715 ];
6716 let s = NormalizedTick::quantity_skewness(&ticks).unwrap();
6717 assert!(s > 0.0, "right-skewed distribution should have positive skewness, got {}", s);
6718 }
6719
6720 #[test]
6723 fn test_price_kurtosis_none_for_fewer_than_4() {
6724 use rust_decimal_macros::dec;
6725 let ticks = vec![
6726 make_tick_pq(dec!(1), dec!(1)),
6727 make_tick_pq(dec!(2), dec!(1)),
6728 make_tick_pq(dec!(3), dec!(1)),
6729 ];
6730 assert!(NormalizedTick::price_kurtosis(&ticks).is_none());
6731 }
6732
6733 #[test]
6734 fn test_price_kurtosis_returns_some_for_varied_prices() {
6735 use rust_decimal_macros::dec;
6736 let ticks = vec![
6737 make_tick_pq(dec!(1), dec!(1)),
6738 make_tick_pq(dec!(2), dec!(1)),
6739 make_tick_pq(dec!(3), dec!(1)),
6740 make_tick_pq(dec!(4), dec!(1)),
6741 ];
6742 assert!(NormalizedTick::price_kurtosis(&ticks).is_some());
6743 }
6744
6745 #[test]
6748 fn test_high_volume_tick_count_zero_for_empty() {
6749 use rust_decimal_macros::dec;
6750 assert_eq!(NormalizedTick::high_volume_tick_count(&[], dec!(1)), 0);
6751 }
6752
6753 #[test]
6754 fn test_high_volume_tick_count_correct() {
6755 use rust_decimal_macros::dec;
6756 let ticks = vec![
6757 make_tick_pq(dec!(100), dec!(1)),
6758 make_tick_pq(dec!(101), dec!(5)),
6759 make_tick_pq(dec!(102), dec!(10)),
6760 ];
6761 assert_eq!(NormalizedTick::high_volume_tick_count(&ticks, dec!(4)), 2);
6762 }
6763
6764 #[test]
6767 fn test_vwap_spread_none_when_no_buys_or_sells() {
6768 use rust_decimal_macros::dec;
6769 let t = make_tick_pq(dec!(100), dec!(1));
6770 assert!(NormalizedTick::vwap_spread(&[t]).is_none());
6771 }
6772
6773 #[test]
6774 fn test_vwap_spread_positive_when_buys_priced_higher() {
6775 use rust_decimal_macros::dec;
6776 let mut buy = make_tick_pq(dec!(105), dec!(1)); buy.side = Some(TradeSide::Buy);
6777 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
6778 let spread = NormalizedTick::vwap_spread(&[buy, sell]).unwrap();
6779 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
6780 }
6781
6782 #[test]
6785 fn test_avg_buy_quantity_none_for_no_buys() {
6786 use rust_decimal_macros::dec;
6787 let mut t = make_tick_pq(dec!(100), dec!(2)); t.side = Some(TradeSide::Sell);
6788 assert!(NormalizedTick::avg_buy_quantity(&[t]).is_none());
6789 }
6790
6791 #[test]
6792 fn test_avg_buy_quantity_correct() {
6793 use rust_decimal_macros::dec;
6794 let mut t1 = make_tick_pq(dec!(100), dec!(2)); t1.side = Some(TradeSide::Buy);
6795 let mut t2 = make_tick_pq(dec!(101), dec!(4)); t2.side = Some(TradeSide::Buy);
6796 assert_eq!(NormalizedTick::avg_buy_quantity(&[t1, t2]), Some(dec!(3)));
6797 }
6798
6799 #[test]
6800 fn test_avg_sell_quantity_correct() {
6801 use rust_decimal_macros::dec;
6802 let mut t1 = make_tick_pq(dec!(100), dec!(6)); t1.side = Some(TradeSide::Sell);
6803 let mut t2 = make_tick_pq(dec!(101), dec!(2)); t2.side = Some(TradeSide::Sell);
6804 assert_eq!(NormalizedTick::avg_sell_quantity(&[t1, t2]), Some(dec!(4)));
6805 }
6806
6807 #[test]
6810 fn test_price_mean_reversion_score_none_for_empty() {
6811 assert!(NormalizedTick::price_mean_reversion_score(&[]).is_none());
6812 }
6813
6814 #[test]
6815 fn test_price_mean_reversion_score_in_range() {
6816 use rust_decimal_macros::dec;
6817 let ticks = vec![
6818 make_tick_pq(dec!(90), dec!(1)),
6819 make_tick_pq(dec!(100), dec!(1)),
6820 make_tick_pq(dec!(110), dec!(1)),
6821 ];
6822 let score = NormalizedTick::price_mean_reversion_score(&ticks).unwrap();
6823 assert!(score >= 0.0 && score <= 1.0, "score should be in [0, 1], got {}", score);
6824 }
6825
6826 #[test]
6829 fn test_largest_price_move_none_for_single_tick() {
6830 use rust_decimal_macros::dec;
6831 let t = make_tick_pq(dec!(100), dec!(1));
6832 assert!(NormalizedTick::largest_price_move(&[t]).is_none());
6833 }
6834
6835 #[test]
6836 fn test_largest_price_move_correct() {
6837 use rust_decimal_macros::dec;
6838 let ticks = vec![
6839 make_tick_pq(dec!(100), dec!(1)),
6840 make_tick_pq(dec!(105), dec!(1)), make_tick_pq(dec!(102), dec!(1)), ];
6843 assert_eq!(NormalizedTick::largest_price_move(&ticks), Some(dec!(5)));
6844 }
6845
6846 #[test]
6849 fn test_tick_rate_none_for_single_tick() {
6850 use rust_decimal_macros::dec;
6851 let t = make_tick_pq(dec!(100), dec!(1));
6852 assert!(NormalizedTick::tick_rate(&[t]).is_none());
6853 }
6854
6855 #[test]
6856 fn test_tick_rate_correct() {
6857 use rust_decimal_macros::dec;
6858 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.received_at_ms = 0;
6859 let mut t2 = make_tick_pq(dec!(101), dec!(1)); t2.received_at_ms = 2;
6860 let mut t3 = make_tick_pq(dec!(102), dec!(1)); t3.received_at_ms = 4;
6861 let rate = NormalizedTick::tick_rate(&[t1, t2, t3]).unwrap();
6863 assert!((rate - 0.75).abs() < 1e-9, "expected 0.75 ticks/ms, got {}", rate);
6864 }
6865
6866 #[test]
6869 fn test_buy_notional_fraction_none_for_empty() {
6870 assert!(NormalizedTick::buy_notional_fraction(&[]).is_none());
6871 }
6872
6873 #[test]
6874 fn test_buy_notional_fraction_one_when_all_buys() {
6875 use rust_decimal_macros::dec;
6876 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
6877 let frac = NormalizedTick::buy_notional_fraction(&[t]).unwrap();
6878 assert!((frac - 1.0).abs() < 1e-9, "all buys should give fraction=1.0, got {}", frac);
6879 }
6880
6881 #[test]
6882 fn test_buy_notional_fraction_in_range_for_mixed() {
6883 use rust_decimal_macros::dec;
6884 let mut buy = make_tick_pq(dec!(100), dec!(3)); buy.side = Some(TradeSide::Buy);
6885 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
6886 let frac = NormalizedTick::buy_notional_fraction(&[buy, sell]).unwrap();
6887 assert!(frac > 0.0 && frac < 1.0, "mixed ticks should be in (0,1), got {}", frac);
6888 }
6889
6890 #[test]
6893 fn test_price_range_pct_none_for_empty() {
6894 assert!(NormalizedTick::price_range_pct(&[]).is_none());
6895 }
6896
6897 #[test]
6898 fn test_price_range_pct_correct() {
6899 use rust_decimal_macros::dec;
6900 let ticks = vec![
6901 make_tick_pq(dec!(100), dec!(1)),
6902 make_tick_pq(dec!(110), dec!(1)),
6903 ];
6904 let pct = NormalizedTick::price_range_pct(&ticks).unwrap();
6906 assert!((pct - 10.0).abs() < 1e-6, "expected 10.0%, got {}", pct);
6907 }
6908
6909 #[test]
6912 fn test_buy_side_dominance_none_when_no_sides() {
6913 use rust_decimal_macros::dec;
6914 let t = make_tick_pq(dec!(100), dec!(1)); assert!(NormalizedTick::buy_side_dominance(&[t]).is_none());
6916 }
6917
6918 #[test]
6919 fn test_buy_side_dominance_one_when_all_buys() {
6920 use rust_decimal_macros::dec;
6921 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
6922 let d = NormalizedTick::buy_side_dominance(&[t]).unwrap();
6923 assert!((d - 1.0).abs() < 1e-9, "all buys should give 1.0, got {}", d);
6924 }
6925
6926 #[test]
6929 fn test_volume_weighted_price_std_none_for_empty() {
6930 assert!(NormalizedTick::volume_weighted_price_std(&[]).is_none());
6931 }
6932
6933 #[test]
6934 fn test_volume_weighted_price_std_zero_for_same_price() {
6935 use rust_decimal_macros::dec;
6936 let ticks = vec![
6937 make_tick_pq(dec!(100), dec!(2)),
6938 make_tick_pq(dec!(100), dec!(3)),
6939 ];
6940 let std = NormalizedTick::volume_weighted_price_std(&ticks).unwrap();
6941 assert!((std - 0.0).abs() < 1e-9, "same price should give 0 std, got {}", std);
6942 }
6943
6944 #[test]
6947 fn test_last_n_vwap_none_for_zero_n() {
6948 use rust_decimal_macros::dec;
6949 let t = make_tick_pq(dec!(100), dec!(1));
6950 assert!(NormalizedTick::last_n_vwap(&[t], 0).is_none());
6951 }
6952
6953 #[test]
6954 fn test_last_n_vwap_uses_last_n_ticks() {
6955 use rust_decimal_macros::dec;
6956 let ticks = vec![
6958 make_tick_pq(dec!(50), dec!(10)),
6959 make_tick_pq(dec!(100), dec!(5)),
6960 make_tick_pq(dec!(100), dec!(5)),
6961 ];
6962 let v = NormalizedTick::last_n_vwap(&ticks, 2).unwrap();
6963 assert_eq!(v, dec!(100));
6964 }
6965
6966 #[test]
6969 fn test_price_autocorrelation_none_for_fewer_than_3() {
6970 use rust_decimal_macros::dec;
6971 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6972 assert!(NormalizedTick::price_autocorrelation(&ticks).is_none());
6973 }
6974
6975 #[test]
6976 fn test_price_autocorrelation_positive_for_trending_prices() {
6977 use rust_decimal_macros::dec;
6978 let ticks = vec![
6979 make_tick_pq(dec!(100), dec!(1)),
6980 make_tick_pq(dec!(102), dec!(1)),
6981 make_tick_pq(dec!(104), dec!(1)),
6982 make_tick_pq(dec!(106), dec!(1)),
6983 ];
6984 let ac = NormalizedTick::price_autocorrelation(&ticks).unwrap();
6985 assert!(ac > 0.0, "trending prices should have positive AC, got {}", ac);
6986 }
6987
6988 #[test]
6991 fn test_net_trade_direction_zero_for_empty() {
6992 assert_eq!(NormalizedTick::net_trade_direction(&[]), 0);
6993 }
6994
6995 #[test]
6996 fn test_net_trade_direction_positive_for_more_buys() {
6997 use rust_decimal_macros::dec;
6998 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
6999 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
7000 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
7001 assert_eq!(NormalizedTick::net_trade_direction(&[b1, b2, s1]), 1);
7002 }
7003
7004 #[test]
7007 fn test_sell_side_notional_fraction_none_for_empty() {
7008 assert!(NormalizedTick::sell_side_notional_fraction(&[]).is_none());
7009 }
7010
7011 #[test]
7012 fn test_sell_side_notional_fraction_one_when_all_sells() {
7013 use rust_decimal_macros::dec;
7014 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Sell);
7015 let f = NormalizedTick::sell_side_notional_fraction(&[t]).unwrap();
7016 assert!((f - 1.0).abs() < 1e-9, "all sells should give 1.0, got {}", f);
7017 }
7018
7019 #[test]
7022 fn test_price_oscillation_count_zero_for_monotone() {
7023 use rust_decimal_macros::dec;
7024 let ticks = vec![
7025 make_tick_pq(dec!(100), dec!(1)),
7026 make_tick_pq(dec!(101), dec!(1)),
7027 make_tick_pq(dec!(102), dec!(1)),
7028 ];
7029 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 0);
7030 }
7031
7032 #[test]
7033 fn test_price_oscillation_count_detects_reversals() {
7034 use rust_decimal_macros::dec;
7035 let ticks = vec![
7038 make_tick_pq(dec!(100), dec!(1)),
7039 make_tick_pq(dec!(105), dec!(1)),
7040 make_tick_pq(dec!(102), dec!(1)),
7041 make_tick_pq(dec!(107), dec!(1)),
7042 ];
7043 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 2);
7044 }
7045
7046 #[test]
7049 fn test_realized_spread_none_when_no_sides() {
7050 use rust_decimal_macros::dec;
7051 let t = make_tick_pq(dec!(100), dec!(1));
7052 assert!(NormalizedTick::realized_spread(&[t]).is_none());
7053 }
7054
7055 #[test]
7056 fn test_realized_spread_positive_when_buys_higher() {
7057 use rust_decimal_macros::dec;
7058 let mut b = make_tick_pq(dec!(105), dec!(1)); b.side = Some(TradeSide::Buy);
7059 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
7060 let spread = NormalizedTick::realized_spread(&[b, s]).unwrap();
7061 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
7062 }
7063
7064 #[test]
7067 fn test_price_impact_per_unit_none_for_single_tick() {
7068 use rust_decimal_macros::dec;
7069 let t = make_tick_pq(dec!(100), dec!(1));
7070 assert!(NormalizedTick::price_impact_per_unit(&[t]).is_none());
7071 }
7072
7073 #[test]
7076 fn test_volume_weighted_return_none_for_single_tick() {
7077 use rust_decimal_macros::dec;
7078 let t = make_tick_pq(dec!(100), dec!(1));
7079 assert!(NormalizedTick::volume_weighted_return(&[t]).is_none());
7080 }
7081
7082 #[test]
7083 fn test_volume_weighted_return_zero_for_constant_price() {
7084 use rust_decimal_macros::dec;
7085 let ticks = vec![
7086 make_tick_pq(dec!(100), dec!(5)),
7087 make_tick_pq(dec!(100), dec!(5)),
7088 ];
7089 let r = NormalizedTick::volume_weighted_return(&ticks).unwrap();
7090 assert!((r - 0.0).abs() < 1e-9, "constant price should give 0 return, got {}", r);
7091 }
7092
7093 #[test]
7096 fn test_quantity_concentration_none_for_empty() {
7097 assert!(NormalizedTick::quantity_concentration(&[]).is_none());
7098 }
7099
7100 #[test]
7101 fn test_quantity_concentration_zero_for_identical_quantities() {
7102 use rust_decimal_macros::dec;
7103 let ticks = vec![
7104 make_tick_pq(dec!(100), dec!(5)),
7105 make_tick_pq(dec!(101), dec!(5)),
7106 ];
7107 let c = NormalizedTick::quantity_concentration(&ticks).unwrap();
7108 assert!((c - 0.0).abs() < 1e-9, "identical quantities should give 0 concentration, got {}", c);
7109 }
7110
7111 #[test]
7114 fn test_price_level_volume_zero_for_no_match() {
7115 use rust_decimal_macros::dec;
7116 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7117 let v = NormalizedTick::price_level_volume(&ticks, dec!(200));
7118 assert_eq!(v, dec!(0));
7119 }
7120
7121 #[test]
7122 fn test_price_level_volume_sums_matching_ticks() {
7123 use rust_decimal_macros::dec;
7124 let ticks = vec![
7125 make_tick_pq(dec!(100), dec!(3)),
7126 make_tick_pq(dec!(101), dec!(7)),
7127 make_tick_pq(dec!(100), dec!(2)),
7128 ];
7129 assert_eq!(NormalizedTick::price_level_volume(&ticks, dec!(100)), dec!(5));
7130 }
7131
7132 #[test]
7135 fn test_mid_price_drift_none_for_single_tick() {
7136 use rust_decimal_macros::dec;
7137 let t = make_tick_pq(dec!(100), dec!(1));
7138 assert!(NormalizedTick::mid_price_drift(&[t]).is_none());
7139 }
7140
7141 #[test]
7144 fn test_tick_direction_bias_none_for_fewer_than_3() {
7145 use rust_decimal_macros::dec;
7146 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
7147 assert!(NormalizedTick::tick_direction_bias(&ticks).is_none());
7148 }
7149
7150 #[test]
7151 fn test_tick_direction_bias_one_for_monotone() {
7152 use rust_decimal_macros::dec;
7153 let ticks = vec![
7154 make_tick_pq(dec!(100), dec!(1)),
7155 make_tick_pq(dec!(101), dec!(1)),
7156 make_tick_pq(dec!(102), dec!(1)),
7157 make_tick_pq(dec!(103), dec!(1)),
7158 ];
7159 let bias = NormalizedTick::tick_direction_bias(&ticks).unwrap();
7160 assert!((bias - 1.0).abs() < 1e-9, "monotone should give bias=1.0, got {}", bias);
7161 }
7162
7163 #[test]
7164 fn test_buy_sell_size_ratio_none_for_empty() {
7165 assert!(NormalizedTick::buy_sell_size_ratio(&[]).is_none());
7166 }
7167
7168 #[test]
7169 fn test_buy_sell_size_ratio_positive() {
7170 use rust_decimal_macros::dec;
7171 let buy = NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) };
7172 let sell = NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(2)) };
7173 let r = NormalizedTick::buy_sell_size_ratio(&[buy, sell]).unwrap();
7174 assert!((r - 2.0).abs() < 1e-6, "ratio should be 2.0, got {}", r);
7175 }
7176
7177 #[test]
7178 fn test_trade_size_dispersion_none_for_single_tick() {
7179 use rust_decimal_macros::dec;
7180 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7181 assert!(NormalizedTick::trade_size_dispersion(&ticks).is_none());
7182 }
7183
7184 #[test]
7185 fn test_trade_size_dispersion_zero_for_identical() {
7186 use rust_decimal_macros::dec;
7187 let ticks = vec![
7188 make_tick_pq(dec!(100), dec!(5)),
7189 make_tick_pq(dec!(101), dec!(5)),
7190 make_tick_pq(dec!(102), dec!(5)),
7191 ];
7192 let d = NormalizedTick::trade_size_dispersion(&ticks).unwrap();
7193 assert!(d.abs() < 1e-9, "identical sizes → dispersion=0, got {}", d);
7194 }
7195
7196 #[test]
7197 fn test_first_last_price_none_for_empty() {
7198 assert!(NormalizedTick::first_price(&[]).is_none());
7199 assert!(NormalizedTick::last_price(&[]).is_none());
7200 }
7201
7202 #[test]
7203 fn test_first_last_price_correct() {
7204 use rust_decimal_macros::dec;
7205 let ticks = vec![
7206 make_tick_pq(dec!(100), dec!(1)),
7207 make_tick_pq(dec!(105), dec!(1)),
7208 make_tick_pq(dec!(110), dec!(1)),
7209 ];
7210 assert_eq!(NormalizedTick::first_price(&ticks).unwrap(), dec!(100));
7211 assert_eq!(NormalizedTick::last_price(&ticks).unwrap(), dec!(110));
7212 }
7213
7214 #[test]
7215 fn test_median_quantity_none_for_empty() {
7216 assert!(NormalizedTick::median_quantity(&[]).is_none());
7217 }
7218
7219 #[test]
7220 fn test_median_quantity_odd_count() {
7221 use rust_decimal_macros::dec;
7222 let ticks = vec![
7223 make_tick_pq(dec!(100), dec!(3)),
7224 make_tick_pq(dec!(101), dec!(1)),
7225 make_tick_pq(dec!(102), dec!(5)),
7226 ];
7227 assert_eq!(NormalizedTick::median_quantity(&ticks).unwrap(), dec!(3));
7229 }
7230
7231 #[test]
7232 fn test_volume_above_vwap_none_for_empty() {
7233 assert!(NormalizedTick::volume_above_vwap(&[]).is_none());
7234 }
7235
7236 #[test]
7237 fn test_volume_above_vwap_none_when_all_at_vwap() {
7238 use rust_decimal_macros::dec;
7239 let ticks = vec![
7241 make_tick_pq(dec!(100), dec!(5)),
7242 make_tick_pq(dec!(100), dec!(5)),
7243 ];
7244 let v = NormalizedTick::volume_above_vwap(&ticks).unwrap();
7245 assert_eq!(v, dec!(0));
7246 }
7247
7248 #[test]
7249 fn test_inter_arrival_variance_none_for_fewer_than_3() {
7250 use rust_decimal_macros::dec;
7251 let t = make_tick_pq(dec!(100), dec!(1));
7252 assert!(NormalizedTick::inter_arrival_variance(&[t]).is_none());
7253 }
7254
7255 #[test]
7256 fn test_spread_efficiency_none_for_single_tick() {
7257 use rust_decimal_macros::dec;
7258 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7259 assert!(NormalizedTick::spread_efficiency(&ticks).is_none());
7260 }
7261
7262 #[test]
7263 fn test_spread_efficiency_one_for_monotone() {
7264 use rust_decimal_macros::dec;
7265 let ticks = vec![
7266 make_tick_pq(dec!(100), dec!(1)),
7267 make_tick_pq(dec!(101), dec!(1)),
7268 make_tick_pq(dec!(102), dec!(1)),
7269 ];
7270 let e = NormalizedTick::spread_efficiency(&ticks).unwrap();
7272 assert!((e - 1.0).abs() < 1e-9, "expected 1.0, got {}", e);
7273 }
7274
7275 #[test]
7280 fn test_aggressor_fraction_none_for_empty() {
7281 assert!(NormalizedTick::aggressor_fraction(&[]).is_none());
7282 }
7283
7284 #[test]
7285 fn test_aggressor_fraction_zero_when_all_neutral() {
7286 use rust_decimal_macros::dec;
7287 let ticks = vec![
7288 make_tick_pq(dec!(100), dec!(1)),
7289 make_tick_pq(dec!(101), dec!(1)),
7290 ];
7291 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
7292 assert!((f - 0.0).abs() < 1e-9, "all neutral → fraction=0, got {}", f);
7293 }
7294
7295 #[test]
7296 fn test_aggressor_fraction_one_when_all_known() {
7297 use rust_decimal_macros::dec;
7298 let ticks = vec![
7299 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(1)) },
7300 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(101), dec!(1)) },
7301 ];
7302 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
7303 assert!((f - 1.0).abs() < 1e-9, "all known → fraction=1, got {}", f);
7304 }
7305
7306 #[test]
7309 fn test_volume_imbalance_ratio_none_for_neutral_ticks() {
7310 use rust_decimal_macros::dec;
7311 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7312 assert!(NormalizedTick::volume_imbalance_ratio(&ticks).is_none());
7313 }
7314
7315 #[test]
7316 fn test_volume_imbalance_ratio_positive_for_all_buys() {
7317 use rust_decimal_macros::dec;
7318 let ticks = vec![
7319 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) },
7320 ];
7321 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
7322 assert!((r - 1.0).abs() < 1e-9, "all buys → ratio=1.0, got {}", r);
7323 }
7324
7325 #[test]
7326 fn test_volume_imbalance_ratio_zero_for_equal_sides() {
7327 use rust_decimal_macros::dec;
7328 let ticks = vec![
7329 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7330 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
7331 ];
7332 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
7333 assert!(r.abs() < 1e-9, "equal buy/sell → ratio=0, got {}", r);
7334 }
7335
7336 #[test]
7339 fn test_price_quantity_covariance_none_for_single_tick() {
7340 use rust_decimal_macros::dec;
7341 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7342 assert!(NormalizedTick::price_quantity_covariance(&ticks).is_none());
7343 }
7344
7345 #[test]
7346 fn test_price_quantity_covariance_positive_when_correlated() {
7347 use rust_decimal_macros::dec;
7348 let ticks = vec![
7349 make_tick_pq(dec!(100), dec!(1)),
7350 make_tick_pq(dec!(200), dec!(2)),
7351 make_tick_pq(dec!(300), dec!(3)),
7352 ];
7353 let c = NormalizedTick::price_quantity_covariance(&ticks).unwrap();
7354 assert!(c > 0.0, "price and qty both rise → positive cov, got {}", c);
7355 }
7356
7357 #[test]
7360 fn test_large_trade_fraction_none_for_empty() {
7361 use rust_decimal_macros::dec;
7362 assert!(NormalizedTick::large_trade_fraction(&[], dec!(10)).is_none());
7363 }
7364
7365 #[test]
7366 fn test_large_trade_fraction_zero_when_all_small() {
7367 use rust_decimal_macros::dec;
7368 let ticks = vec![
7369 make_tick_pq(dec!(100), dec!(1)),
7370 make_tick_pq(dec!(101), dec!(2)),
7371 ];
7372 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
7373 assert!((f - 0.0).abs() < 1e-9, "all small → fraction=0, got {}", f);
7374 }
7375
7376 #[test]
7377 fn test_large_trade_fraction_one_when_all_large() {
7378 use rust_decimal_macros::dec;
7379 let ticks = vec![
7380 make_tick_pq(dec!(100), dec!(20)),
7381 make_tick_pq(dec!(101), dec!(30)),
7382 ];
7383 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
7384 assert!((f - 1.0).abs() < 1e-9, "all large → fraction=1, got {}", f);
7385 }
7386
7387 #[test]
7390 fn test_price_level_density_none_for_empty() {
7391 assert!(NormalizedTick::price_level_density(&[]).is_none());
7392 }
7393
7394 #[test]
7395 fn test_price_level_density_none_when_range_zero() {
7396 use rust_decimal_macros::dec;
7397 let ticks = vec![
7398 make_tick_pq(dec!(100), dec!(1)),
7399 make_tick_pq(dec!(100), dec!(2)),
7400 ];
7401 assert!(NormalizedTick::price_level_density(&ticks).is_none());
7402 }
7403
7404 #[test]
7405 fn test_price_level_density_positive_for_varied_prices() {
7406 use rust_decimal_macros::dec;
7407 let ticks = vec![
7408 make_tick_pq(dec!(100), dec!(1)),
7409 make_tick_pq(dec!(110), dec!(1)),
7410 make_tick_pq(dec!(120), dec!(1)),
7411 ];
7412 let d = NormalizedTick::price_level_density(&ticks).unwrap();
7413 assert!(d > 0.0, "should be positive, got {}", d);
7414 }
7415
7416 #[test]
7419 fn test_notional_buy_sell_ratio_none_when_no_sells() {
7420 use rust_decimal_macros::dec;
7421 let ticks = vec![
7422 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7423 ];
7424 assert!(NormalizedTick::notional_buy_sell_ratio(&ticks).is_none());
7425 }
7426
7427 #[test]
7428 fn test_notional_buy_sell_ratio_one_for_equal_notional() {
7429 use rust_decimal_macros::dec;
7430 let ticks = vec![
7431 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7432 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
7433 ];
7434 let r = NormalizedTick::notional_buy_sell_ratio(&ticks).unwrap();
7435 assert!((r - 1.0).abs() < 1e-9, "equal notional → ratio=1, got {}", r);
7436 }
7437
7438 #[test]
7441 fn test_log_return_mean_none_for_single_tick() {
7442 use rust_decimal_macros::dec;
7443 assert!(NormalizedTick::log_return_mean(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
7444 }
7445
7446 #[test]
7447 fn test_log_return_mean_zero_for_constant_price() {
7448 use rust_decimal_macros::dec;
7449 let ticks = vec![
7450 make_tick_pq(dec!(100), dec!(1)),
7451 make_tick_pq(dec!(100), dec!(1)),
7452 make_tick_pq(dec!(100), dec!(1)),
7453 ];
7454 let m = NormalizedTick::log_return_mean(&ticks).unwrap();
7455 assert!(m.abs() < 1e-9, "constant price → mean log return=0, got {}", m);
7456 }
7457
7458 #[test]
7461 fn test_log_return_std_none_for_fewer_than_3_ticks() {
7462 use rust_decimal_macros::dec;
7463 let ticks = vec![
7464 make_tick_pq(dec!(100), dec!(1)),
7465 make_tick_pq(dec!(101), dec!(1)),
7466 ];
7467 assert!(NormalizedTick::log_return_std(&ticks).is_none());
7468 }
7469
7470 #[test]
7471 fn test_log_return_std_zero_for_constant_price() {
7472 use rust_decimal_macros::dec;
7473 let ticks = vec![
7474 make_tick_pq(dec!(100), dec!(1)),
7475 make_tick_pq(dec!(100), dec!(1)),
7476 make_tick_pq(dec!(100), dec!(1)),
7477 make_tick_pq(dec!(100), dec!(1)),
7478 ];
7479 let s = NormalizedTick::log_return_std(&ticks).unwrap();
7480 assert!(s.abs() < 1e-9, "constant price → std=0, got {}", s);
7481 }
7482
7483 #[test]
7486 fn test_price_overshoot_ratio_none_for_empty() {
7487 assert!(NormalizedTick::price_overshoot_ratio(&[]).is_none());
7488 }
7489
7490 #[test]
7491 fn test_price_overshoot_ratio_one_for_monotone_up() {
7492 use rust_decimal_macros::dec;
7493 let ticks = vec![
7494 make_tick_pq(dec!(100), dec!(1)),
7495 make_tick_pq(dec!(105), dec!(1)),
7496 make_tick_pq(dec!(110), dec!(1)),
7497 ];
7498 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
7500 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
7501 }
7502
7503 #[test]
7504 fn test_price_overshoot_ratio_above_one_when_price_retreats() {
7505 use rust_decimal_macros::dec;
7506 let ticks = vec![
7507 make_tick_pq(dec!(100), dec!(1)),
7508 make_tick_pq(dec!(120), dec!(1)),
7509 make_tick_pq(dec!(110), dec!(1)),
7510 ];
7511 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
7513 assert!(r > 1.0, "price retreated → ratio>1, got {}", r);
7514 }
7515
7516 #[test]
7519 fn test_price_undershoot_ratio_none_for_empty() {
7520 assert!(NormalizedTick::price_undershoot_ratio(&[]).is_none());
7521 }
7522
7523 #[test]
7524 fn test_price_undershoot_ratio_one_for_monotone_down() {
7525 use rust_decimal_macros::dec;
7526 let ticks = vec![
7527 make_tick_pq(dec!(110), dec!(1)),
7528 make_tick_pq(dec!(105), dec!(1)),
7529 make_tick_pq(dec!(100), dec!(1)),
7530 ];
7531 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
7533 assert!(r > 1.0, "monotone down → ratio>1, got {}", r);
7534 }
7535
7536 #[test]
7537 fn test_price_undershoot_ratio_one_for_monotone_up() {
7538 use rust_decimal_macros::dec;
7539 let ticks = vec![
7540 make_tick_pq(dec!(100), dec!(1)),
7541 make_tick_pq(dec!(105), dec!(1)),
7542 make_tick_pq(dec!(110), dec!(1)),
7543 ];
7544 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
7546 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
7547 }
7548
7549 #[test]
7552 fn test_net_notional_empty_is_zero() {
7553 assert_eq!(NormalizedTick::net_notional(&[]), Decimal::ZERO);
7554 }
7555
7556 #[test]
7557 fn test_net_notional_positive_buy() {
7558 use rust_decimal_macros::dec;
7559 let ticks = vec![
7560 make_tick_pq(dec!(100), dec!(5)).with_side(TradeSide::Buy),
7561 make_tick_pq(dec!(100), dec!(2)).with_side(TradeSide::Sell),
7562 ];
7563 assert_eq!(NormalizedTick::net_notional(&ticks), dec!(300));
7564 }
7565
7566 #[test]
7567 fn test_price_reversal_count_empty_is_zero() {
7568 assert_eq!(NormalizedTick::price_reversal_count(&[]), 0);
7569 }
7570
7571 #[test]
7572 fn test_price_reversal_count_monotone_is_zero() {
7573 use rust_decimal_macros::dec;
7574 let ticks = vec![
7575 make_tick_pq(dec!(100), dec!(1)),
7576 make_tick_pq(dec!(101), dec!(1)),
7577 make_tick_pq(dec!(102), dec!(1)),
7578 ];
7579 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 0);
7580 }
7581
7582 #[test]
7583 fn test_price_reversal_count_zigzag() {
7584 use rust_decimal_macros::dec;
7585 let ticks = vec![
7586 make_tick_pq(dec!(100), dec!(1)),
7587 make_tick_pq(dec!(105), dec!(1)),
7588 make_tick_pq(dec!(100), dec!(1)),
7589 make_tick_pq(dec!(105), dec!(1)),
7590 ];
7591 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 2);
7592 }
7593
7594 #[test]
7595 fn test_quantity_kurtosis_none_for_few_ticks() {
7596 use rust_decimal_macros::dec;
7597 let t = make_tick_pq(dec!(100), dec!(1));
7598 assert!(NormalizedTick::quantity_kurtosis(&[t]).is_none());
7599 }
7600
7601 #[test]
7602 fn test_quantity_kurtosis_some_for_sufficient() {
7603 use rust_decimal_macros::dec;
7604 let ticks = vec![
7605 make_tick_pq(dec!(100), dec!(1)),
7606 make_tick_pq(dec!(101), dec!(2)),
7607 make_tick_pq(dec!(102), dec!(3)),
7608 make_tick_pq(dec!(103), dec!(4)),
7609 ];
7610 assert!(NormalizedTick::quantity_kurtosis(&ticks).is_some());
7611 }
7612
7613 #[test]
7614 fn test_largest_notional_trade_none_for_empty() {
7615 assert!(NormalizedTick::largest_notional_trade(&[]).is_none());
7616 }
7617
7618 #[test]
7619 fn test_largest_notional_trade_correct() {
7620 use rust_decimal_macros::dec;
7621 let ticks = vec![
7622 make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(50), dec!(10)), make_tick_pq(dec!(200), dec!(1)), ];
7626 let t = NormalizedTick::largest_notional_trade(&ticks).unwrap();
7627 assert_eq!(t.price, dec!(50));
7628 }
7629
7630 #[test]
7631 fn test_twap_none_for_single_tick() {
7632 use rust_decimal_macros::dec;
7633 assert!(NormalizedTick::twap(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
7634 }
7635
7636 #[test]
7637 fn test_twap_two_equal_intervals() {
7638 use rust_decimal_macros::dec;
7639 let mut t1 = make_tick_pq(dec!(100), dec!(1));
7640 t1.received_at_ms = 0;
7641 let mut t2 = make_tick_pq(dec!(200), dec!(1));
7642 t2.received_at_ms = 1000;
7643 let mut t3 = make_tick_pq(dec!(300), dec!(1));
7644 t3.received_at_ms = 2000;
7645 let twap = NormalizedTick::twap(&[t1, t2, t3]).unwrap();
7647 assert_eq!(twap, dec!(150));
7648 }
7649
7650 #[test]
7651 fn test_neutral_fraction_all_neutral() {
7652 use rust_decimal_macros::dec;
7653 let ticks = vec![
7654 make_tick_pq(dec!(100), dec!(1)),
7655 make_tick_pq(dec!(101), dec!(1)),
7656 ];
7657 let f = NormalizedTick::neutral_fraction(&ticks).unwrap();
7658 assert!((f - 1.0).abs() < 1e-9, "all neutral → fraction=1, got {}", f);
7659 }
7660
7661 #[test]
7662 fn test_log_return_variance_none_for_few_ticks() {
7663 use rust_decimal_macros::dec;
7664 let t = make_tick_pq(dec!(100), dec!(1));
7665 assert!(NormalizedTick::log_return_variance(&[t]).is_none());
7666 }
7667
7668 #[test]
7669 fn test_log_return_variance_zero_for_flat_prices() {
7670 use rust_decimal_macros::dec;
7671 let ticks = vec![
7672 make_tick_pq(dec!(100), dec!(1)),
7673 make_tick_pq(dec!(100), dec!(1)),
7674 make_tick_pq(dec!(100), dec!(1)),
7675 ];
7676 let v = NormalizedTick::log_return_variance(&ticks).unwrap();
7677 assert!(v.abs() < 1e-9, "flat prices → variance=0, got {}", v);
7678 }
7679
7680 #[test]
7681 fn test_volume_at_vwap_zero_for_empty() {
7682 assert_eq!(
7683 NormalizedTick::volume_at_vwap(&[], rust_decimal_macros::dec!(1)),
7684 Decimal::ZERO
7685 );
7686 }
7687
7688 #[test]
7691 fn test_cumulative_volume_empty_for_empty_slice() {
7692 assert!(NormalizedTick::cumulative_volume(&[]).is_empty());
7693 }
7694
7695 #[test]
7696 fn test_cumulative_volume_last_equals_total() {
7697 use rust_decimal_macros::dec;
7698 let ticks = vec![
7699 make_tick_pq(dec!(100), dec!(2)),
7700 make_tick_pq(dec!(101), dec!(3)),
7701 make_tick_pq(dec!(102), dec!(5)),
7702 ];
7703 let cv = NormalizedTick::cumulative_volume(&ticks);
7704 assert_eq!(cv.last().copied().unwrap(), dec!(10));
7705 assert_eq!(cv[0], dec!(2));
7706 }
7707
7708 #[test]
7711 fn test_price_volatility_ratio_none_for_empty() {
7712 assert!(NormalizedTick::price_volatility_ratio(&[]).is_none());
7713 }
7714
7715 #[test]
7716 fn test_price_volatility_ratio_zero_for_constant_price() {
7717 use rust_decimal_macros::dec;
7718 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(100), dec!(1))];
7719 let r = NormalizedTick::price_volatility_ratio(&ticks).unwrap();
7720 assert!(r.abs() < 1e-9, "constant price → ratio=0, got {}", r);
7721 }
7722
7723 #[test]
7726 fn test_notional_per_tick_none_for_empty() {
7727 assert!(NormalizedTick::notional_per_tick(&[]).is_none());
7728 }
7729
7730 #[test]
7731 fn test_notional_per_tick_equals_single_tick_notional() {
7732 use rust_decimal_macros::dec;
7733 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7734 let n = NormalizedTick::notional_per_tick(&ticks).unwrap();
7735 assert!((n - 500.0).abs() < 1e-6, "100×5=500, got {}", n);
7736 }
7737
7738 #[test]
7741 fn test_buy_to_total_volume_ratio_none_for_empty() {
7742 assert!(NormalizedTick::buy_to_total_volume_ratio(&[]).is_none());
7743 }
7744
7745 #[test]
7746 fn test_buy_to_total_volume_ratio_zero_for_all_neutral() {
7747 use rust_decimal_macros::dec;
7748 let ticks = vec![make_tick_pq(dec!(100), dec!(5)), make_tick_pq(dec!(101), dec!(3))];
7749 let r = NormalizedTick::buy_to_total_volume_ratio(&ticks).unwrap();
7750 assert!(r.abs() < 1e-9, "neutral ticks → buy ratio=0, got {}", r);
7751 }
7752
7753 #[test]
7756 fn test_avg_latency_ms_none_when_no_exchange_ts() {
7757 use rust_decimal_macros::dec;
7758 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7759 assert!(NormalizedTick::avg_latency_ms(&ticks).is_none());
7760 }
7761
7762 #[test]
7765 fn test_price_gini_none_for_empty() {
7766 assert!(NormalizedTick::price_gini(&[]).is_none());
7767 }
7768
7769 #[test]
7770 fn test_price_gini_zero_for_uniform_prices() {
7771 use rust_decimal_macros::dec;
7772 let ticks = vec![
7773 make_tick_pq(dec!(100), dec!(1)),
7774 make_tick_pq(dec!(100), dec!(1)),
7775 make_tick_pq(dec!(100), dec!(1)),
7776 ];
7777 let g = NormalizedTick::price_gini(&ticks).unwrap();
7778 assert!(g.abs() < 1e-9, "uniform prices → gini=0, got {}", g);
7779 }
7780
7781 #[test]
7784 fn test_trade_velocity_none_for_same_timestamp() {
7785 use rust_decimal_macros::dec;
7786 let ticks = vec![
7787 make_tick_pq(dec!(100), dec!(1)),
7788 make_tick_pq(dec!(101), dec!(1)),
7789 ];
7790 assert!(NormalizedTick::trade_velocity(&ticks).is_none());
7791 }
7792
7793 #[test]
7796 fn test_floor_price_none_for_empty() {
7797 assert!(NormalizedTick::floor_price(&[]).is_none());
7798 }
7799
7800 #[test]
7801 fn test_floor_price_equals_min_price() {
7802 use rust_decimal_macros::dec;
7803 let ticks = vec![
7804 make_tick_pq(dec!(105), dec!(1)),
7805 make_tick_pq(dec!(100), dec!(1)),
7806 make_tick_pq(dec!(103), dec!(1)),
7807 ];
7808 assert_eq!(NormalizedTick::floor_price(&ticks), NormalizedTick::min_price(&ticks));
7809 }
7810
7811 #[test]
7814 fn test_price_momentum_score_none_for_single_tick() {
7815 use rust_decimal_macros::dec;
7816 let t = make_tick_pq(dec!(100), dec!(1));
7817 assert!(NormalizedTick::price_momentum_score(&[t]).is_none());
7818 }
7819
7820 #[test]
7821 fn test_price_momentum_score_positive_for_rising_prices() {
7822 use rust_decimal_macros::dec;
7823 let ticks = vec![
7824 make_tick_pq(dec!(100), dec!(1)),
7825 make_tick_pq(dec!(102), dec!(2)),
7826 make_tick_pq(dec!(104), dec!(2)),
7827 ];
7828 let s = NormalizedTick::price_momentum_score(&ticks).unwrap();
7829 assert!(s > 0.0, "rising prices → positive momentum, got {}", s);
7830 }
7831
7832 #[test]
7833 fn test_vwap_std_none_for_single_tick() {
7834 use rust_decimal_macros::dec;
7835 let t = make_tick_pq(dec!(100), dec!(1));
7836 assert!(NormalizedTick::vwap_std(&[t]).is_none());
7837 }
7838
7839 #[test]
7840 fn test_vwap_std_zero_for_constant_price() {
7841 use rust_decimal_macros::dec;
7842 let ticks = vec![
7843 make_tick_pq(dec!(100), dec!(1)),
7844 make_tick_pq(dec!(100), dec!(2)),
7845 make_tick_pq(dec!(100), dec!(3)),
7846 ];
7847 let s = NormalizedTick::vwap_std(&ticks).unwrap();
7848 assert!(s.abs() < 1e-9, "constant price → vwap_std=0, got {}", s);
7849 }
7850
7851 #[test]
7852 fn test_price_range_expansion_none_for_empty() {
7853 assert!(NormalizedTick::price_range_expansion(&[]).is_none());
7854 }
7855
7856 #[test]
7857 fn test_price_range_expansion_monotone_rising() {
7858 use rust_decimal_macros::dec;
7859 let ticks = vec![
7860 make_tick_pq(dec!(100), dec!(1)),
7861 make_tick_pq(dec!(101), dec!(1)),
7862 make_tick_pq(dec!(102), dec!(1)),
7863 make_tick_pq(dec!(103), dec!(1)),
7864 ];
7865 let f = NormalizedTick::price_range_expansion(&ticks).unwrap();
7866 assert!((f - 0.75).abs() < 1e-9, "expected 0.75, got {}", f);
7868 }
7869
7870 #[test]
7871 fn test_sell_to_total_volume_ratio_none_for_empty() {
7872 assert!(NormalizedTick::sell_to_total_volume_ratio(&[]).is_none());
7873 }
7874
7875 #[test]
7876 fn test_sell_to_total_volume_ratio_zero_for_all_buys() {
7877 use rust_decimal_macros::dec;
7878 let mut t1 = make_tick_pq(dec!(100), dec!(5));
7879 t1.side = Some(crate::tick::TradeSide::Buy);
7880 let mut t2 = make_tick_pq(dec!(101), dec!(3));
7881 t2.side = Some(crate::tick::TradeSide::Buy);
7882 let r = NormalizedTick::sell_to_total_volume_ratio(&[t1, t2]).unwrap();
7883 assert!(r.abs() < 1e-9, "all buys → sell ratio=0, got {}", r);
7884 }
7885
7886 #[test]
7887 fn test_notional_std_none_for_single_tick() {
7888 use rust_decimal_macros::dec;
7889 let t = make_tick_pq(dec!(100), dec!(1));
7890 assert!(NormalizedTick::notional_std(&[t]).is_none());
7891 }
7892
7893 #[test]
7894 fn test_notional_std_zero_for_identical_notionals() {
7895 use rust_decimal_macros::dec;
7896 let t1 = make_tick_pq(dec!(100), dec!(2));
7897 let t2 = make_tick_pq(dec!(100), dec!(2));
7898 let s = NormalizedTick::notional_std(&[t1, t2]).unwrap();
7899 assert!(s.abs() < 1e-9, "identical notionals → std=0, got {}", s);
7900 }
7901
7902 #[test]
7905 fn test_buy_price_mean_none_when_no_buys() {
7906 use rust_decimal_macros::dec;
7907 let t = make_tick_pq(dec!(100), dec!(1)); assert!(NormalizedTick::buy_price_mean(&[t]).is_none());
7909 }
7910
7911 #[test]
7912 fn test_buy_price_mean_correct_value() {
7913 use rust_decimal_macros::dec;
7914 let mut t1 = make_tick_pq(dec!(100), dec!(1));
7915 t1.side = Some(crate::tick::TradeSide::Buy);
7916 let mut t2 = make_tick_pq(dec!(102), dec!(1));
7917 t2.side = Some(crate::tick::TradeSide::Buy);
7918 let mean = NormalizedTick::buy_price_mean(&[t1, t2]).unwrap();
7919 assert_eq!(mean, dec!(101));
7920 }
7921
7922 #[test]
7923 fn test_sell_price_mean_none_when_no_sells() {
7924 use rust_decimal_macros::dec;
7925 let t = make_tick_pq(dec!(100), dec!(1));
7926 assert!(NormalizedTick::sell_price_mean(&[t]).is_none());
7927 }
7928
7929 #[test]
7930 fn test_price_efficiency_none_for_single_tick() {
7931 use rust_decimal_macros::dec;
7932 let t = make_tick_pq(dec!(100), dec!(1));
7933 assert!(NormalizedTick::price_efficiency(&[t]).is_none());
7934 }
7935
7936 #[test]
7937 fn test_price_efficiency_one_for_directional() {
7938 use rust_decimal_macros::dec;
7939 let ticks = vec![
7940 make_tick_pq(dec!(100), dec!(1)),
7941 make_tick_pq(dec!(102), dec!(1)),
7942 make_tick_pq(dec!(104), dec!(1)),
7943 ];
7944 let e = NormalizedTick::price_efficiency(&ticks).unwrap();
7945 assert!((e - 1.0).abs() < 1e-9, "monotone rising → efficiency=1, got {}", e);
7946 }
7947
7948 #[test]
7949 fn test_price_return_skewness_none_for_few_ticks() {
7950 use rust_decimal_macros::dec;
7951 let ticks = vec![
7952 make_tick_pq(dec!(100), dec!(1)),
7953 make_tick_pq(dec!(101), dec!(1)),
7954 make_tick_pq(dec!(102), dec!(1)),
7955 ];
7956 assert!(NormalizedTick::price_return_skewness(&ticks).is_none());
7957 }
7958
7959 #[test]
7960 fn test_buy_sell_vwap_spread_none_when_no_sides() {
7961 use rust_decimal_macros::dec;
7962 let ticks = vec![
7963 make_tick_pq(dec!(100), dec!(1)),
7964 make_tick_pq(dec!(101), dec!(1)),
7965 ];
7966 assert!(NormalizedTick::buy_sell_vwap_spread(&ticks).is_none());
7967 }
7968
7969 #[test]
7970 fn test_above_mean_quantity_fraction_none_for_empty() {
7971 assert!(NormalizedTick::above_mean_quantity_fraction(&[]).is_none());
7972 }
7973
7974 #[test]
7975 fn test_above_mean_quantity_fraction_in_range() {
7976 use rust_decimal_macros::dec;
7977 let ticks = vec![
7978 make_tick_pq(dec!(100), dec!(1)),
7979 make_tick_pq(dec!(100), dec!(5)),
7980 make_tick_pq(dec!(100), dec!(3)),
7981 ];
7982 let f = NormalizedTick::above_mean_quantity_fraction(&ticks).unwrap();
7983 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
7984 }
7985
7986 #[test]
7987 fn test_price_unchanged_fraction_none_for_single_tick() {
7988 use rust_decimal_macros::dec;
7989 let t = make_tick_pq(dec!(100), dec!(1));
7990 assert!(NormalizedTick::price_unchanged_fraction(&[t]).is_none());
7991 }
7992
7993 #[test]
7994 fn test_price_unchanged_fraction_zero_for_all_changing() {
7995 use rust_decimal_macros::dec;
7996 let ticks = vec![
7997 make_tick_pq(dec!(100), dec!(1)),
7998 make_tick_pq(dec!(101), dec!(1)),
7999 make_tick_pq(dec!(102), dec!(1)),
8000 ];
8001 let f = NormalizedTick::price_unchanged_fraction(&ticks).unwrap();
8002 assert!(f.abs() < 1e-9, "all prices different → unchanged=0, got {}", f);
8003 }
8004
8005 #[test]
8006 fn test_qty_weighted_range_none_for_empty() {
8007 assert!(NormalizedTick::qty_weighted_range(&[]).is_none());
8008 }
8009
8010 #[test]
8011 fn test_qty_weighted_range_zero_for_single_tick() {
8012 use rust_decimal_macros::dec;
8013 let t = make_tick_pq(dec!(100), dec!(2));
8014 let r = NormalizedTick::qty_weighted_range(&[t]).unwrap();
8015 assert!(r.abs() < 1e-9, "single tick → range=0, got {}", r);
8016 }
8017
8018 #[test]
8021 fn test_sell_notional_fraction_none_for_empty() {
8022 assert!(NormalizedTick::sell_notional_fraction(&[]).is_none());
8023 }
8024
8025 #[test]
8026 fn test_sell_notional_fraction_zero_for_all_buys() {
8027 use rust_decimal_macros::dec;
8028 let mut t1 = make_tick_pq(dec!(100), dec!(3));
8029 t1.side = Some(crate::tick::TradeSide::Buy);
8030 let r = NormalizedTick::sell_notional_fraction(&[t1]).unwrap();
8031 assert!(r.abs() < 1e-9, "all buys → sell fraction=0, got {}", r);
8032 }
8033
8034 #[test]
8035 fn test_max_price_gap_none_for_single_tick() {
8036 use rust_decimal_macros::dec;
8037 let t = make_tick_pq(dec!(100), dec!(1));
8038 assert!(NormalizedTick::max_price_gap(&[t]).is_none());
8039 }
8040
8041 #[test]
8042 fn test_max_price_gap_correct_value() {
8043 use rust_decimal_macros::dec;
8044 let ticks = vec![
8045 make_tick_pq(dec!(100), dec!(1)),
8046 make_tick_pq(dec!(105), dec!(1)),
8047 make_tick_pq(dec!(103), dec!(1)),
8048 ];
8049 assert_eq!(NormalizedTick::max_price_gap(&ticks).unwrap(), dec!(5));
8050 }
8051
8052 #[test]
8053 fn test_price_range_velocity_none_for_single_tick() {
8054 use rust_decimal_macros::dec;
8055 let t = make_tick_pq(dec!(100), dec!(1));
8056 assert!(NormalizedTick::price_range_velocity(&[t]).is_none());
8057 }
8058
8059 #[test]
8060 fn test_tick_count_per_ms_none_for_single_tick() {
8061 use rust_decimal_macros::dec;
8062 let t = make_tick_pq(dec!(100), dec!(1));
8063 assert!(NormalizedTick::tick_count_per_ms(&[t]).is_none());
8064 }
8065
8066 #[test]
8067 fn test_buy_quantity_fraction_none_for_empty() {
8068 assert!(NormalizedTick::buy_quantity_fraction(&[]).is_none());
8069 }
8070
8071 #[test]
8072 fn test_buy_quantity_fraction_one_for_all_buys() {
8073 use rust_decimal_macros::dec;
8074 let mut t = make_tick_pq(dec!(100), dec!(5));
8075 t.side = Some(crate::tick::TradeSide::Buy);
8076 let f = NormalizedTick::buy_quantity_fraction(&[t]).unwrap();
8077 assert!((f - 1.0).abs() < 1e-9, "all buys → buy fraction=1, got {}", f);
8078 }
8079
8080 #[test]
8081 fn test_sell_quantity_fraction_none_for_empty() {
8082 assert!(NormalizedTick::sell_quantity_fraction(&[]).is_none());
8083 }
8084
8085 #[test]
8086 fn test_sell_quantity_fraction_one_for_all_sells() {
8087 use rust_decimal_macros::dec;
8088 let mut t = make_tick_pq(dec!(100), dec!(5));
8089 t.side = Some(crate::tick::TradeSide::Sell);
8090 let f = NormalizedTick::sell_quantity_fraction(&[t]).unwrap();
8091 assert!((f - 1.0).abs() < 1e-9, "all sells → sell fraction=1, got {}", f);
8092 }
8093
8094 #[test]
8095 fn test_price_mean_crossover_count_none_for_single_tick() {
8096 use rust_decimal_macros::dec;
8097 let t = make_tick_pq(dec!(100), dec!(1));
8098 assert!(NormalizedTick::price_mean_crossover_count(&[t]).is_none());
8099 }
8100
8101 #[test]
8102 fn test_price_mean_crossover_count_in_range() {
8103 use rust_decimal_macros::dec;
8104 let ticks = vec![
8105 make_tick_pq(dec!(90), dec!(1)),
8106 make_tick_pq(dec!(110), dec!(1)),
8107 make_tick_pq(dec!(90), dec!(1)),
8108 ];
8109 let c = NormalizedTick::price_mean_crossover_count(&ticks).unwrap();
8110 assert!(c >= 1, "expect at least 1 crossover, got {}", c);
8111 }
8112
8113 #[test]
8114 fn test_notional_skewness_none_for_two_ticks() {
8115 use rust_decimal_macros::dec;
8116 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
8117 assert!(NormalizedTick::notional_skewness(&ticks).is_none());
8118 }
8119
8120 #[test]
8121 fn test_volume_weighted_mid_price_none_for_empty() {
8122 assert!(NormalizedTick::volume_weighted_mid_price(&[]).is_none());
8123 }
8124
8125 #[test]
8126 fn test_volume_weighted_mid_price_equals_price_for_single_tick() {
8127 use rust_decimal_macros::dec;
8128 let t = make_tick_pq(dec!(123), dec!(5));
8129 let mid = NormalizedTick::volume_weighted_mid_price(&[t]).unwrap();
8130 assert_eq!(mid, dec!(123));
8131 }
8132
8133 #[test]
8136 fn test_neutral_count_zero_when_all_sided() {
8137 use rust_decimal_macros::dec;
8138 let mut t = make_tick_pq(dec!(100), dec!(1));
8139 t.side = Some(crate::tick::TradeSide::Buy);
8140 assert_eq!(NormalizedTick::neutral_count(&[t]), 0);
8141 }
8142
8143 #[test]
8144 fn test_neutral_count_all_when_no_side() {
8145 use rust_decimal_macros::dec;
8146 let t1 = make_tick_pq(dec!(100), dec!(1));
8147 let t2 = make_tick_pq(dec!(101), dec!(1));
8148 assert_eq!(NormalizedTick::neutral_count(&[t1, t2]), 2);
8149 }
8150
8151 #[test]
8152 fn test_price_dispersion_none_for_empty() {
8153 assert!(NormalizedTick::price_dispersion(&[]).is_none());
8154 }
8155
8156 #[test]
8157 fn test_price_dispersion_zero_for_single() {
8158 use rust_decimal_macros::dec;
8159 let t = make_tick_pq(dec!(100), dec!(1));
8160 assert_eq!(NormalizedTick::price_dispersion(&[t]).unwrap(), dec!(0));
8161 }
8162
8163 #[test]
8164 fn test_max_notional_none_for_empty() {
8165 assert!(NormalizedTick::max_notional(&[]).is_none());
8166 }
8167
8168 #[test]
8169 fn test_max_notional_selects_largest() {
8170 use rust_decimal_macros::dec;
8171 let t1 = make_tick_pq(dec!(100), dec!(2)); let t2 = make_tick_pq(dec!(50), dec!(5)); assert_eq!(NormalizedTick::max_notional(&[t1, t2]).unwrap(), dec!(250));
8174 }
8175
8176 #[test]
8177 fn test_min_notional_none_for_empty() {
8178 assert!(NormalizedTick::min_notional(&[]).is_none());
8179 }
8180
8181 #[test]
8182 fn test_below_vwap_fraction_none_for_empty() {
8183 assert!(NormalizedTick::below_vwap_fraction(&[]).is_none());
8184 }
8185
8186 #[test]
8187 fn test_trade_notional_std_none_for_single() {
8188 use rust_decimal_macros::dec;
8189 let t = make_tick_pq(dec!(100), dec!(1));
8190 assert!(NormalizedTick::trade_notional_std(&[t]).is_none());
8191 }
8192
8193 #[test]
8194 fn test_buy_sell_count_ratio_none_for_no_sells() {
8195 use rust_decimal_macros::dec;
8196 let mut t = make_tick_pq(dec!(100), dec!(1));
8197 t.side = Some(crate::tick::TradeSide::Buy);
8198 assert!(NormalizedTick::buy_sell_count_ratio(&[t]).is_none());
8199 }
8200
8201 #[test]
8202 fn test_buy_sell_count_ratio_correct() {
8203 use rust_decimal_macros::dec;
8204 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8205 t1.side = Some(crate::tick::TradeSide::Buy);
8206 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8207 t2.side = Some(crate::tick::TradeSide::Sell);
8208 let r = NormalizedTick::buy_sell_count_ratio(&[t1, t2]).unwrap();
8209 assert!((r - 1.0).abs() < 1e-9, "1 buy / 1 sell = 1.0, got {}", r);
8210 }
8211
8212 #[test]
8213 fn test_price_mad_none_for_empty() {
8214 assert!(NormalizedTick::price_mad(&[]).is_none());
8215 }
8216
8217 #[test]
8218 fn test_price_mad_zero_for_constant_price() {
8219 use rust_decimal_macros::dec;
8220 let ticks = vec![
8221 make_tick_pq(dec!(100), dec!(1)),
8222 make_tick_pq(dec!(100), dec!(2)),
8223 ];
8224 let m = NormalizedTick::price_mad(&ticks).unwrap();
8225 assert!(m.abs() < 1e-9, "constant price → MAD=0, got {}", m);
8226 }
8227
8228 #[test]
8229 fn test_price_range_pct_of_open_none_for_empty() {
8230 assert!(NormalizedTick::price_range_pct_of_open(&[]).is_none());
8231 }
8232
8233 #[test]
8234 fn test_price_range_pct_of_open_zero_for_constant() {
8235 use rust_decimal_macros::dec;
8236 let ticks = vec![
8237 make_tick_pq(dec!(100), dec!(1)),
8238 make_tick_pq(dec!(100), dec!(1)),
8239 ];
8240 let p = NormalizedTick::price_range_pct_of_open(&ticks).unwrap();
8241 assert!(p.abs() < 1e-9, "constant → range_pct=0, got {}", p);
8242 }
8243
8244 #[test]
8247 fn test_price_mean_none_for_empty() {
8248 assert!(NormalizedTick::price_mean(&[]).is_none());
8249 }
8250
8251 #[test]
8252 fn test_price_mean_correct() {
8253 use rust_decimal_macros::dec;
8254 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(1))];
8255 assert_eq!(NormalizedTick::price_mean(&ticks).unwrap(), dec!(150));
8256 }
8257
8258 #[test]
8259 fn test_uptick_count_zero_for_single() {
8260 use rust_decimal_macros::dec;
8261 let t = make_tick_pq(dec!(100), dec!(1));
8262 assert_eq!(NormalizedTick::uptick_count(&[t]), 0);
8263 }
8264
8265 #[test]
8266 fn test_uptick_count_correct() {
8267 use rust_decimal_macros::dec;
8268 let ticks = vec![
8269 make_tick_pq(dec!(100), dec!(1)),
8270 make_tick_pq(dec!(101), dec!(1)),
8271 make_tick_pq(dec!(100), dec!(1)),
8272 ];
8273 assert_eq!(NormalizedTick::uptick_count(&ticks), 1);
8274 }
8275
8276 #[test]
8277 fn test_downtick_count_zero_for_all_up() {
8278 use rust_decimal_macros::dec;
8279 let ticks = vec![
8280 make_tick_pq(dec!(100), dec!(1)),
8281 make_tick_pq(dec!(101), dec!(1)),
8282 make_tick_pq(dec!(102), dec!(1)),
8283 ];
8284 assert_eq!(NormalizedTick::downtick_count(&ticks), 0);
8285 }
8286
8287 #[test]
8288 fn test_uptick_fraction_none_for_single() {
8289 use rust_decimal_macros::dec;
8290 let t = make_tick_pq(dec!(100), dec!(1));
8291 assert!(NormalizedTick::uptick_fraction(&[t]).is_none());
8292 }
8293
8294 #[test]
8295 fn test_quantity_std_none_for_single() {
8296 use rust_decimal_macros::dec;
8297 let t = make_tick_pq(dec!(100), dec!(1));
8298 assert!(NormalizedTick::quantity_std(&[t]).is_none());
8299 }
8300
8301 #[test]
8302 fn test_quantity_std_zero_for_constant_qty() {
8303 use rust_decimal_macros::dec;
8304 let ticks = vec![
8305 make_tick_pq(dec!(100), dec!(5)),
8306 make_tick_pq(dec!(101), dec!(5)),
8307 ];
8308 let s = NormalizedTick::quantity_std(&ticks).unwrap();
8309 assert!(s.abs() < 1e-9, "constant quantity → std=0, got {}", s);
8310 }
8311
8312 #[test]
8315 fn test_vwap_deviation_std_none_for_single() {
8316 use rust_decimal_macros::dec;
8317 let t = make_tick_pq(dec!(100), dec!(1));
8318 assert!(NormalizedTick::vwap_deviation_std(&[t]).is_none());
8319 }
8320
8321 #[test]
8322 fn test_vwap_deviation_std_zero_for_single_price() {
8323 use rust_decimal_macros::dec;
8324 let ticks = vec![
8325 make_tick_pq(dec!(100), dec!(1)),
8326 make_tick_pq(dec!(100), dec!(2)),
8327 ];
8328 let s = NormalizedTick::vwap_deviation_std(&ticks).unwrap();
8330 assert!(s.abs() < 1e-9, "all at VWAP → std=0, got {}", s);
8331 }
8332
8333 #[test]
8334 fn test_vwap_deviation_std_positive_for_varied_prices() {
8335 use rust_decimal_macros::dec;
8336 let ticks = vec![
8337 make_tick_pq(dec!(100), dec!(1)),
8338 make_tick_pq(dec!(110), dec!(1)),
8339 make_tick_pq(dec!(90), dec!(1)),
8340 ];
8341 let s = NormalizedTick::vwap_deviation_std(&ticks).unwrap();
8342 assert!(s > 0.0, "varied prices → std > 0, got {}", s);
8343 }
8344
8345 #[test]
8346 fn test_max_consecutive_side_run_zero_for_no_side() {
8347 use rust_decimal_macros::dec;
8348 let ticks = vec![
8349 make_tick_pq(dec!(100), dec!(1)),
8350 make_tick_pq(dec!(101), dec!(1)),
8351 ];
8352 assert_eq!(NormalizedTick::max_consecutive_side_run(&ticks), 0);
8353 }
8354
8355 #[test]
8356 fn test_max_consecutive_side_run_with_sides() {
8357 use rust_decimal_macros::dec;
8358 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8359 t1.side = Some(TradeSide::Buy);
8360 let mut t2 = make_tick_pq(dec!(101), dec!(1));
8361 t2.side = Some(TradeSide::Buy);
8362 let mut t3 = make_tick_pq(dec!(102), dec!(1));
8363 t3.side = Some(TradeSide::Sell);
8364 assert_eq!(NormalizedTick::max_consecutive_side_run(&[t1, t2, t3]), 2);
8365 }
8366
8367 #[test]
8368 fn test_inter_arrival_cv_none_for_single() {
8369 use rust_decimal_macros::dec;
8370 let t = make_tick_pq(dec!(100), dec!(1));
8371 assert!(NormalizedTick::inter_arrival_cv(&[t]).is_none());
8372 }
8373
8374 #[test]
8375 fn test_inter_arrival_cv_zero_for_uniform_spacing() {
8376 use rust_decimal_macros::dec;
8377 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8378 t1.received_at_ms = 1000;
8379 let mut t2 = make_tick_pq(dec!(101), dec!(1));
8380 t2.received_at_ms = 2000;
8381 let mut t3 = make_tick_pq(dec!(102), dec!(1));
8382 t3.received_at_ms = 3000;
8383 let cv = NormalizedTick::inter_arrival_cv(&[t1, t2, t3]).unwrap();
8385 assert!(cv.abs() < 1e-9, "uniform spacing → cv=0, got {}", cv);
8386 }
8387
8388 #[test]
8389 fn test_volume_per_ms_none_for_single() {
8390 use rust_decimal_macros::dec;
8391 let t = make_tick_pq(dec!(100), dec!(5));
8392 assert!(NormalizedTick::volume_per_ms(&[t]).is_none());
8393 }
8394
8395 #[test]
8396 fn test_volume_per_ms_correct() {
8397 use rust_decimal_macros::dec;
8398 let mut t1 = make_tick_pq(dec!(100), dec!(5));
8399 t1.received_at_ms = 1000;
8400 let mut t2 = make_tick_pq(dec!(101), dec!(5));
8401 t2.received_at_ms = 2000;
8402 let r = NormalizedTick::volume_per_ms(&[t1, t2]).unwrap();
8404 assert!((r - 0.01).abs() < 1e-9, "expected 0.01, got {}", r);
8405 }
8406
8407 #[test]
8408 fn test_notional_per_second_none_for_single() {
8409 use rust_decimal_macros::dec;
8410 let t = make_tick_pq(dec!(100), dec!(1));
8411 assert!(NormalizedTick::notional_per_second(&[t]).is_none());
8412 }
8413
8414 #[test]
8415 fn test_notional_per_second_positive() {
8416 use rust_decimal_macros::dec;
8417 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8418 t1.received_at_ms = 0;
8419 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8420 t2.received_at_ms = 1000; let r = NormalizedTick::notional_per_second(&[t1, t2]).unwrap();
8423 assert!((r - 200.0).abs() < 1e-9, "expected 200, got {}", r);
8424 }
8425
8426 #[test]
8429 fn test_order_flow_imbalance_none_for_empty() {
8430 assert!(NormalizedTick::order_flow_imbalance(&[]).is_none());
8431 }
8432
8433 #[test]
8434 fn test_order_flow_imbalance_pos_one_for_all_buys() {
8435 use rust_decimal_macros::dec;
8436 let mut t = make_tick_pq(dec!(100), dec!(5));
8437 t.side = Some(crate::tick::TradeSide::Buy);
8438 let r = NormalizedTick::order_flow_imbalance(&[t]).unwrap();
8439 assert!((r - 1.0).abs() < 1e-9, "all buys → OFI=+1, got {}", r);
8440 }
8441
8442 #[test]
8443 fn test_order_flow_imbalance_neg_one_for_all_sells() {
8444 use rust_decimal_macros::dec;
8445 let mut t = make_tick_pq(dec!(100), dec!(5));
8446 t.side = Some(crate::tick::TradeSide::Sell);
8447 let r = NormalizedTick::order_flow_imbalance(&[t]).unwrap();
8448 assert!((r + 1.0).abs() < 1e-9, "all sells → OFI=-1, got {}", r);
8449 }
8450
8451 #[test]
8452 fn test_price_qty_up_fraction_none_for_single() {
8453 use rust_decimal_macros::dec;
8454 let t = make_tick_pq(dec!(100), dec!(1));
8455 assert!(NormalizedTick::price_qty_up_fraction(&[t]).is_none());
8456 }
8457
8458 #[test]
8459 fn test_running_high_count_single_tick() {
8460 use rust_decimal_macros::dec;
8461 let t = make_tick_pq(dec!(100), dec!(1));
8462 assert_eq!(NormalizedTick::running_high_count(&[t]), 1);
8463 }
8464
8465 #[test]
8466 fn test_running_low_count_single_tick() {
8467 use rust_decimal_macros::dec;
8468 let t = make_tick_pq(dec!(100), dec!(1));
8469 assert_eq!(NormalizedTick::running_low_count(&[t]), 1);
8470 }
8471
8472 #[test]
8473 fn test_buy_sell_avg_qty_ratio_none_for_no_sells() {
8474 use rust_decimal_macros::dec;
8475 let mut t = make_tick_pq(dec!(100), dec!(5));
8476 t.side = Some(crate::tick::TradeSide::Buy);
8477 assert!(NormalizedTick::buy_sell_avg_qty_ratio(&[t]).is_none());
8478 }
8479
8480 #[test]
8481 fn test_max_price_drop_none_for_single() {
8482 use rust_decimal_macros::dec;
8483 let t = make_tick_pq(dec!(100), dec!(1));
8484 assert!(NormalizedTick::max_price_drop(&[t]).is_none());
8485 }
8486
8487 #[test]
8488 fn test_max_price_rise_none_for_single() {
8489 use rust_decimal_macros::dec;
8490 let t = make_tick_pq(dec!(100), dec!(1));
8491 assert!(NormalizedTick::max_price_rise(&[t]).is_none());
8492 }
8493
8494 #[test]
8495 fn test_max_price_drop_correct() {
8496 use rust_decimal_macros::dec;
8497 let ticks = vec![
8498 make_tick_pq(dec!(100), dec!(1)),
8499 make_tick_pq(dec!(90), dec!(1)), make_tick_pq(dec!(95), dec!(1)), ];
8502 assert_eq!(NormalizedTick::max_price_drop(&ticks).unwrap(), dec!(10));
8503 }
8504
8505 #[test]
8506 fn test_max_price_rise_correct() {
8507 use rust_decimal_macros::dec;
8508 let ticks = vec![
8509 make_tick_pq(dec!(90), dec!(1)),
8510 make_tick_pq(dec!(105), dec!(1)), make_tick_pq(dec!(100), dec!(1)),
8512 ];
8513 assert_eq!(NormalizedTick::max_price_rise(&ticks).unwrap(), dec!(15));
8514 }
8515
8516 #[test]
8517 fn test_buy_trade_count_zero_for_no_sides() {
8518 use rust_decimal_macros::dec;
8519 let t = make_tick_pq(dec!(100), dec!(1));
8520 assert_eq!(NormalizedTick::buy_trade_count(&[t]), 0);
8521 }
8522
8523 #[test]
8524 fn test_buy_trade_count_correct() {
8525 use rust_decimal_macros::dec;
8526 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8527 t1.side = Some(TradeSide::Buy);
8528 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8529 t2.side = Some(TradeSide::Sell);
8530 assert_eq!(NormalizedTick::buy_trade_count(&[t1, t2]), 1);
8531 }
8532
8533 #[test]
8534 fn test_sell_trade_count_correct() {
8535 use rust_decimal_macros::dec;
8536 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8537 t1.side = Some(TradeSide::Buy);
8538 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8539 t2.side = Some(TradeSide::Sell);
8540 assert_eq!(NormalizedTick::sell_trade_count(&[t1, t2]), 1);
8541 }
8542
8543 #[test]
8544 fn test_price_reversal_fraction_none_for_two_ticks() {
8545 use rust_decimal_macros::dec;
8546 let t1 = make_tick_pq(dec!(100), dec!(1));
8547 let t2 = make_tick_pq(dec!(101), dec!(1));
8548 assert!(NormalizedTick::price_reversal_fraction(&[t1, t2]).is_none());
8549 }
8550
8551 #[test]
8552 fn test_price_reversal_fraction_one_for_zigzag() {
8553 use rust_decimal_macros::dec;
8554 let ticks = vec![
8556 make_tick_pq(dec!(100), dec!(1)),
8557 make_tick_pq(dec!(110), dec!(1)),
8558 make_tick_pq(dec!(105), dec!(1)),
8559 make_tick_pq(dec!(115), dec!(1)),
8560 ];
8561 let f = NormalizedTick::price_reversal_fraction(&ticks).unwrap();
8562 assert!((f - 1.0).abs() < 1e-9, "perfect zigzag → 1.0, got {}", f);
8563 }
8564
8565 #[test]
8568 fn test_near_vwap_fraction_none_for_empty() {
8569 use rust_decimal_macros::dec;
8570 assert!(NormalizedTick::near_vwap_fraction(&[], dec!(1)).is_none());
8571 }
8572
8573 #[test]
8574 fn test_near_vwap_fraction_one_for_all_at_vwap() {
8575 use rust_decimal_macros::dec;
8576 let ticks = vec![
8578 make_tick_pq(dec!(100), dec!(1)),
8579 make_tick_pq(dec!(100), dec!(1)),
8580 ];
8581 let f = NormalizedTick::near_vwap_fraction(&ticks, dec!(0)).unwrap();
8582 assert!((f - 1.0).abs() < 1e-9, "all at VWAP → 1.0, got {}", f);
8583 }
8584
8585 #[test]
8586 fn test_mean_tick_return_none_for_single() {
8587 use rust_decimal_macros::dec;
8588 let t = make_tick_pq(dec!(100), dec!(1));
8589 assert!(NormalizedTick::mean_tick_return(&[t]).is_none());
8590 }
8591
8592 #[test]
8593 fn test_mean_tick_return_zero_for_constant_price() {
8594 use rust_decimal_macros::dec;
8595 let ticks = vec![
8596 make_tick_pq(dec!(100), dec!(1)),
8597 make_tick_pq(dec!(100), dec!(1)),
8598 make_tick_pq(dec!(100), dec!(1)),
8599 ];
8600 let r = NormalizedTick::mean_tick_return(&ticks).unwrap();
8601 assert!(r.abs() < 1e-9, "constant price → mean_return=0, got {}", r);
8602 }
8603
8604 #[test]
8605 fn test_passive_buy_count_zero_for_no_sides() {
8606 use rust_decimal_macros::dec;
8607 let t = make_tick_pq(dec!(100), dec!(1));
8608 assert_eq!(NormalizedTick::passive_buy_count(&[t]), 0);
8609 }
8610
8611 #[test]
8612 fn test_quantity_iqr_none_for_small_slice() {
8613 use rust_decimal_macros::dec;
8614 let ticks = vec![
8615 make_tick_pq(dec!(100), dec!(1)),
8616 make_tick_pq(dec!(101), dec!(2)),
8617 ];
8618 assert!(NormalizedTick::quantity_iqr(&ticks).is_none());
8619 }
8620
8621 #[test]
8622 fn test_quantity_iqr_positive_for_varied_quantities() {
8623 use rust_decimal_macros::dec;
8624 let ticks: Vec<_> = [dec!(1), dec!(2), dec!(8), dec!(16), dec!(32), dec!(64), dec!(128), dec!(256)]
8625 .iter()
8626 .map(|&q| make_tick_pq(dec!(100), q))
8627 .collect();
8628 let iqr = NormalizedTick::quantity_iqr(&ticks).unwrap();
8629 assert!(iqr > dec!(0));
8630 }
8631
8632 #[test]
8633 fn test_top_quartile_price_fraction_none_for_small_slice() {
8634 use rust_decimal_macros::dec;
8635 let ticks = vec![
8636 make_tick_pq(dec!(100), dec!(1)),
8637 make_tick_pq(dec!(101), dec!(1)),
8638 ];
8639 assert!(NormalizedTick::top_quartile_price_fraction(&ticks).is_none());
8640 }
8641
8642 #[test]
8643 fn test_buy_notional_ratio_none_for_empty() {
8644 assert!(NormalizedTick::buy_notional_ratio(&[]).is_none());
8645 }
8646
8647 #[test]
8648 fn test_buy_notional_ratio_one_for_all_buys() {
8649 use rust_decimal_macros::dec;
8650 let mut t = make_tick_pq(dec!(100), dec!(1));
8651 t.side = Some(TradeSide::Buy);
8652 let r = NormalizedTick::buy_notional_ratio(&[t]).unwrap();
8653 assert!((r - 1.0).abs() < 1e-9, "all buys → ratio=1, got {}", r);
8654 }
8655
8656 #[test]
8657 fn test_return_std_none_for_two_ticks() {
8658 use rust_decimal_macros::dec;
8659 let t1 = make_tick_pq(dec!(100), dec!(1));
8660 let t2 = make_tick_pq(dec!(101), dec!(1));
8661 assert!(NormalizedTick::return_std(&[t1, t2]).is_none());
8662 }
8663
8664 #[test]
8665 fn test_return_std_zero_for_constant_price() {
8666 use rust_decimal_macros::dec;
8667 let ticks = vec![
8668 make_tick_pq(dec!(100), dec!(1)),
8669 make_tick_pq(dec!(100), dec!(1)),
8670 make_tick_pq(dec!(100), dec!(1)),
8671 ];
8672 let s = NormalizedTick::return_std(&ticks).unwrap();
8673 assert!(s.abs() < 1e-9, "constant price → return_std=0, got {}", s);
8674 }
8675
8676 #[test]
8679 fn test_max_drawdown_none_for_empty() {
8680 assert!(NormalizedTick::max_drawdown(&[]).is_none());
8681 }
8682
8683 #[test]
8684 fn test_max_drawdown_zero_for_rising_prices() {
8685 use rust_decimal_macros::dec;
8686 let ticks = vec![
8687 make_tick_pq(dec!(100), dec!(1)),
8688 make_tick_pq(dec!(110), dec!(1)),
8689 make_tick_pq(dec!(120), dec!(1)),
8690 ];
8691 let dd = NormalizedTick::max_drawdown(&ticks).unwrap();
8692 assert!(dd.abs() < 1e-9, "monotone rise → drawdown=0, got {}", dd);
8693 }
8694
8695 #[test]
8696 fn test_max_drawdown_positive_after_peak() {
8697 use rust_decimal_macros::dec;
8698 let ticks = vec![
8699 make_tick_pq(dec!(100), dec!(1)),
8700 make_tick_pq(dec!(120), dec!(1)),
8701 make_tick_pq(dec!(90), dec!(1)),
8702 ];
8703 let dd = NormalizedTick::max_drawdown(&ticks).unwrap();
8704 assert!((dd - 0.25).abs() < 1e-6, "expected 0.25, got {}", dd);
8706 }
8707
8708 #[test]
8709 fn test_high_to_low_ratio_none_for_empty() {
8710 assert!(NormalizedTick::high_to_low_ratio(&[]).is_none());
8711 }
8712
8713 #[test]
8714 fn test_high_to_low_ratio_one_for_constant_price() {
8715 use rust_decimal_macros::dec;
8716 let ticks = vec![
8717 make_tick_pq(dec!(100), dec!(1)),
8718 make_tick_pq(dec!(100), dec!(1)),
8719 ];
8720 let r = NormalizedTick::high_to_low_ratio(&ticks).unwrap();
8721 assert!((r - 1.0).abs() < 1e-9, "constant price → ratio=1, got {}", r);
8722 }
8723
8724 #[test]
8725 fn test_tick_velocity_none_for_single_tick() {
8726 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8727 assert!(NormalizedTick::tick_velocity(&[t]).is_none());
8728 }
8729
8730 #[test]
8731 fn test_notional_decay_none_for_single_tick() {
8732 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8733 assert!(NormalizedTick::notional_decay(&[t]).is_none());
8734 }
8735
8736 #[test]
8737 fn test_notional_decay_one_for_balanced_halves() {
8738 use rust_decimal_macros::dec;
8739 let t1 = make_tick_pq(dec!(100), dec!(1));
8740 let t2 = make_tick_pq(dec!(100), dec!(1));
8741 let r = NormalizedTick::notional_decay(&[t1, t2]).unwrap();
8742 assert!((r - 1.0).abs() < 1e-9, "equal halves → ratio=1, got {}", r);
8743 }
8744
8745 #[test]
8746 fn test_late_price_momentum_none_for_single_tick() {
8747 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8748 assert!(NormalizedTick::late_price_momentum(&[t]).is_none());
8749 }
8750
8751 #[test]
8752 fn test_consecutive_buys_max_zero_for_empty() {
8753 assert_eq!(NormalizedTick::consecutive_buys_max(&[]), 0);
8754 }
8755
8756 #[test]
8757 fn test_consecutive_buys_max_two_for_run_of_two() {
8758 use rust_decimal_macros::dec;
8759 let mut buy1 = make_tick_pq(dec!(100), dec!(1));
8760 buy1.side = Some(TradeSide::Buy);
8761 let mut buy2 = make_tick_pq(dec!(101), dec!(1));
8762 buy2.side = Some(TradeSide::Buy);
8763 let mut sell = make_tick_pq(dec!(102), dec!(1));
8764 sell.side = Some(TradeSide::Sell);
8765 assert_eq!(NormalizedTick::consecutive_buys_max(&[buy1, buy2, sell]), 2);
8766 }
8767
8768 #[test]
8771 fn test_price_acceleration_none_for_two_ticks() {
8772 use rust_decimal_macros::dec;
8773 let t1 = make_tick_pq(dec!(100), dec!(1));
8774 let t2 = make_tick_pq(dec!(101), dec!(1));
8775 assert!(NormalizedTick::price_acceleration(&[t1, t2]).is_none());
8776 }
8777
8778 #[test]
8779 fn test_price_acceleration_zero_for_linear_price() {
8780 use rust_decimal_macros::dec;
8781 let ticks = vec![
8783 make_tick_pq(dec!(100), dec!(1)),
8784 make_tick_pq(dec!(101), dec!(1)),
8785 make_tick_pq(dec!(102), dec!(1)),
8786 ];
8787 let a = NormalizedTick::price_acceleration(&ticks).unwrap();
8788 assert!(a.abs() < 1e-9, "linear price → acceleration=0, got {}", a);
8789 }
8790
8791 #[test]
8792 fn test_large_trade_fraction_none_for_empty() {
8793 assert!(NormalizedTick::large_trade_fraction(&[]).is_none());
8794 }
8795
8796 #[test]
8797 fn test_large_trade_fraction_half_for_one_above_one_below() {
8798 use rust_decimal_macros::dec;
8799 let t1 = make_tick_pq(dec!(100), dec!(1)); let t2 = make_tick_pq(dec!(100), dec!(3)); let f = NormalizedTick::large_trade_fraction(&[t1, t2]).unwrap();
8802 assert!((f - 0.5).abs() < 1e-9, "one above, one below → 0.5, got {}", f);
8803 }
8804
8805 #[test]
8806 fn test_side_alternation_rate_none_for_no_sided_ticks() {
8807 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8808 assert!(NormalizedTick::side_alternation_rate(&[t]).is_none());
8809 }
8810
8811 #[test]
8812 fn test_side_alternation_rate_one_for_full_alternation() {
8813 use rust_decimal_macros::dec;
8814 let mut b = make_tick_pq(dec!(100), dec!(1));
8815 b.side = Some(TradeSide::Buy);
8816 let mut s = make_tick_pq(dec!(101), dec!(1));
8817 s.side = Some(TradeSide::Sell);
8818 let mut b2 = make_tick_pq(dec!(102), dec!(1));
8819 b2.side = Some(TradeSide::Buy);
8820 let r = NormalizedTick::side_alternation_rate(&[b, s, b2]).unwrap();
8821 assert!((r - 1.0).abs() < 1e-9, "B-S-B → rate=1.0, got {}", r);
8822 }
8823
8824 #[test]
8825 fn test_price_range_per_tick_none_for_empty() {
8826 assert!(NormalizedTick::price_range_per_tick(&[]).is_none());
8827 }
8828
8829 #[test]
8830 fn test_price_range_per_tick_zero_for_constant_price() {
8831 use rust_decimal_macros::dec;
8832 let ticks = vec![
8833 make_tick_pq(dec!(100), dec!(1)),
8834 make_tick_pq(dec!(100), dec!(1)),
8835 ];
8836 let r = NormalizedTick::price_range_per_tick(&ticks).unwrap();
8837 assert!(r.abs() < 1e-9, "constant price → range_per_tick=0, got {}", r);
8838 }
8839
8840 #[test]
8841 fn test_qty_weighted_price_std_none_for_empty() {
8842 assert!(NormalizedTick::qty_weighted_price_std(&[]).is_none());
8843 }
8844
8845 #[test]
8846 fn test_qty_weighted_price_std_zero_for_constant_price() {
8847 use rust_decimal_macros::dec;
8848 let ticks = vec![
8849 make_tick_pq(dec!(100), dec!(2)),
8850 make_tick_pq(dec!(100), dec!(3)),
8851 ];
8852 let s = NormalizedTick::qty_weighted_price_std(&ticks).unwrap();
8853 assert!(s.abs() < 1e-9, "constant price → std=0, got {}", s);
8854 }
8855}