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}
3569
3570
3571impl std::fmt::Display for NormalizedTick {
3572 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3573 let side = match self.side {
3574 Some(s) => s.to_string(),
3575 None => "?".to_string(),
3576 };
3577 write!(
3578 f,
3579 "{} {} {} x {} {} @{}ms",
3580 self.exchange, self.symbol, self.price, self.quantity, side, self.received_at_ms
3581 )
3582 }
3583}
3584
3585pub struct TickNormalizer;
3590
3591impl TickNormalizer {
3592 pub fn new() -> Self {
3594 Self
3595 }
3596
3597 pub fn normalize(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3605 let tick = match raw.exchange {
3606 Exchange::Binance => self.normalize_binance(raw),
3607 Exchange::Coinbase => self.normalize_coinbase(raw),
3608 Exchange::Alpaca => self.normalize_alpaca(raw),
3609 Exchange::Polygon => self.normalize_polygon(raw),
3610 }?;
3611 if tick.price <= Decimal::ZERO {
3612 return Err(StreamError::InvalidTick {
3613 reason: format!("price must be positive, got {}", tick.price),
3614 });
3615 }
3616 if tick.quantity < Decimal::ZERO {
3617 return Err(StreamError::InvalidTick {
3618 reason: format!("quantity must be non-negative, got {}", tick.quantity),
3619 });
3620 }
3621 trace!(
3622 exchange = %tick.exchange,
3623 symbol = %tick.symbol,
3624 price = %tick.price,
3625 exchange_ts_ms = ?tick.exchange_ts_ms,
3626 "tick normalized"
3627 );
3628 Ok(tick)
3629 }
3630
3631 fn normalize_binance(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3632 let p = &raw.payload;
3633 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3634 let qty = parse_decimal_field(p, "q", &raw.exchange.to_string())?;
3635 let side = p.get("m").and_then(|v| v.as_bool()).map(|maker| {
3636 if maker {
3637 TradeSide::Sell
3638 } else {
3639 TradeSide::Buy
3640 }
3641 });
3642 let trade_id = p.get("t").and_then(|v| v.as_u64()).map(|id| id.to_string());
3643 let exchange_ts = p.get("T").and_then(|v| v.as_u64());
3644 Ok(NormalizedTick {
3645 exchange: raw.exchange,
3646 symbol: raw.symbol,
3647 price,
3648 quantity: qty,
3649 side,
3650 trade_id,
3651 exchange_ts_ms: exchange_ts,
3652 received_at_ms: raw.received_at_ms,
3653 })
3654 }
3655
3656 fn normalize_coinbase(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3657 let p = &raw.payload;
3658 let price = parse_decimal_field(p, "price", &raw.exchange.to_string())?;
3659 let qty = parse_decimal_field(p, "size", &raw.exchange.to_string())?;
3660 let side = p.get("side").and_then(|v| v.as_str()).map(|s| {
3661 if s == "buy" {
3662 TradeSide::Buy
3663 } else {
3664 TradeSide::Sell
3665 }
3666 });
3667 let trade_id = p
3668 .get("trade_id")
3669 .and_then(|v| v.as_str())
3670 .map(str::to_string);
3671 let exchange_ts_ms = p
3673 .get("time")
3674 .and_then(|v| v.as_str())
3675 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
3676 .map(|dt| dt.timestamp_millis() as u64);
3677 Ok(NormalizedTick {
3678 exchange: raw.exchange,
3679 symbol: raw.symbol,
3680 price,
3681 quantity: qty,
3682 side,
3683 trade_id,
3684 exchange_ts_ms,
3685 received_at_ms: raw.received_at_ms,
3686 })
3687 }
3688
3689 fn normalize_alpaca(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3690 let p = &raw.payload;
3691 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3692 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
3693 let trade_id = p.get("i").and_then(|v| v.as_u64()).map(|id| id.to_string());
3694 let exchange_ts_ms = p
3696 .get("t")
3697 .and_then(|v| v.as_str())
3698 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
3699 .map(|dt| dt.timestamp_millis() as u64);
3700 Ok(NormalizedTick {
3701 exchange: raw.exchange,
3702 symbol: raw.symbol,
3703 price,
3704 quantity: qty,
3705 side: None,
3706 trade_id,
3707 exchange_ts_ms,
3708 received_at_ms: raw.received_at_ms,
3709 })
3710 }
3711
3712 fn normalize_polygon(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
3713 let p = &raw.payload;
3714 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
3715 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
3716 let trade_id = p.get("i").and_then(|v| v.as_str()).map(str::to_string);
3717 let exchange_ts = p
3719 .get("t")
3720 .and_then(|v| v.as_u64())
3721 .map(|t_ns| t_ns / 1_000_000);
3722 Ok(NormalizedTick {
3723 exchange: raw.exchange,
3724 symbol: raw.symbol,
3725 price,
3726 quantity: qty,
3727 side: None,
3728 trade_id,
3729 exchange_ts_ms: exchange_ts,
3730 received_at_ms: raw.received_at_ms,
3731 })
3732 }
3733}
3734
3735impl Default for TickNormalizer {
3736 fn default() -> Self {
3737 Self::new()
3738 }
3739}
3740
3741fn parse_decimal_field(
3742 v: &serde_json::Value,
3743 field: &str,
3744 exchange: &str,
3745) -> Result<Decimal, StreamError> {
3746 let raw = v.get(field).ok_or_else(|| StreamError::ParseError {
3747 exchange: exchange.to_string(),
3748 reason: format!("missing field '{}'", field),
3749 })?;
3750 let s: String = match raw {
3756 serde_json::Value::String(s) => s.clone(),
3757 serde_json::Value::Number(n) => n.to_string(),
3758 _ => {
3759 return Err(StreamError::ParseError {
3760 exchange: exchange.to_string(),
3761 reason: format!("field '{}' is not a string or number", field),
3762 });
3763 }
3764 };
3765 Decimal::from_str(&s).map_err(|e| StreamError::ParseError {
3766 exchange: exchange.to_string(),
3767 reason: format!("field '{}' parse error: {}", field, e),
3768 })
3769}
3770
3771fn now_ms() -> u64 {
3772 std::time::SystemTime::now()
3773 .duration_since(std::time::UNIX_EPOCH)
3774 .map(|d| d.as_millis() as u64)
3775 .unwrap_or(0)
3776}
3777
3778#[cfg(test)]
3779mod tests {
3780 use super::*;
3781 use serde_json::json;
3782
3783 fn normalizer() -> TickNormalizer {
3784 TickNormalizer::new()
3785 }
3786
3787 fn binance_tick(symbol: &str) -> RawTick {
3788 RawTick {
3789 exchange: Exchange::Binance,
3790 symbol: symbol.to_string(),
3791 payload: json!({ "p": "50000.12", "q": "0.001", "m": false, "t": 12345, "T": 1700000000000u64 }),
3792 received_at_ms: 1700000000001,
3793 }
3794 }
3795
3796 fn coinbase_tick(symbol: &str) -> RawTick {
3797 RawTick {
3798 exchange: Exchange::Coinbase,
3799 symbol: symbol.to_string(),
3800 payload: json!({ "price": "50001.00", "size": "0.5", "side": "buy", "trade_id": "abc123" }),
3801 received_at_ms: 1700000000002,
3802 }
3803 }
3804
3805 fn alpaca_tick(symbol: &str) -> RawTick {
3806 RawTick {
3807 exchange: Exchange::Alpaca,
3808 symbol: symbol.to_string(),
3809 payload: json!({ "p": "180.50", "s": "10", "i": 99 }),
3810 received_at_ms: 1700000000003,
3811 }
3812 }
3813
3814 fn polygon_tick(symbol: &str) -> RawTick {
3815 RawTick {
3816 exchange: Exchange::Polygon,
3817 symbol: symbol.to_string(),
3818 payload: json!({ "p": "180.51", "s": "5", "i": "XYZ-001", "t": 1_700_000_000_000_000_000u64 }),
3820 received_at_ms: 1700000000005,
3821 }
3822 }
3823
3824 #[test]
3825 fn test_exchange_from_str_valid() {
3826 assert_eq!("binance".parse::<Exchange>().unwrap(), Exchange::Binance);
3827 assert_eq!("Coinbase".parse::<Exchange>().unwrap(), Exchange::Coinbase);
3828 assert_eq!("ALPACA".parse::<Exchange>().unwrap(), Exchange::Alpaca);
3829 assert_eq!("polygon".parse::<Exchange>().unwrap(), Exchange::Polygon);
3830 }
3831
3832 #[test]
3833 fn test_exchange_from_str_unknown_returns_error() {
3834 let result = "Kraken".parse::<Exchange>();
3835 assert!(matches!(result, Err(StreamError::UnknownExchange(_))));
3836 }
3837
3838 #[test]
3839 fn test_exchange_display() {
3840 assert_eq!(Exchange::Binance.to_string(), "Binance");
3841 assert_eq!(Exchange::Coinbase.to_string(), "Coinbase");
3842 }
3843
3844 #[test]
3845 fn test_normalize_binance_tick_price_and_qty() {
3846 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3847 assert_eq!(tick.price, Decimal::from_str("50000.12").unwrap());
3848 assert_eq!(tick.quantity, Decimal::from_str("0.001").unwrap());
3849 assert_eq!(tick.exchange, Exchange::Binance);
3850 assert_eq!(tick.symbol, "BTCUSDT");
3851 }
3852
3853 #[test]
3854 fn test_normalize_binance_side_maker_false_is_buy() {
3855 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3856 assert_eq!(tick.side, Some(TradeSide::Buy));
3857 }
3858
3859 #[test]
3860 fn test_normalize_binance_side_maker_true_is_sell() {
3861 let raw = RawTick {
3862 exchange: Exchange::Binance,
3863 symbol: "BTCUSDT".into(),
3864 payload: json!({ "p": "50000", "q": "1", "m": true }),
3865 received_at_ms: 0,
3866 };
3867 let tick = normalizer().normalize(raw).unwrap();
3868 assert_eq!(tick.side, Some(TradeSide::Sell));
3869 }
3870
3871 #[test]
3872 fn test_normalize_binance_trade_id_and_ts() {
3873 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
3874 assert_eq!(tick.trade_id, Some("12345".to_string()));
3875 assert_eq!(tick.exchange_ts_ms, Some(1700000000000));
3876 }
3877
3878 #[test]
3879 fn test_normalize_coinbase_tick() {
3880 let tick = normalizer().normalize(coinbase_tick("BTC-USD")).unwrap();
3881 assert_eq!(tick.price, Decimal::from_str("50001.00").unwrap());
3882 assert_eq!(tick.quantity, Decimal::from_str("0.5").unwrap());
3883 assert_eq!(tick.side, Some(TradeSide::Buy));
3884 assert_eq!(tick.trade_id, Some("abc123".to_string()));
3885 }
3886
3887 #[test]
3888 fn test_normalize_coinbase_sell_side() {
3889 let raw = RawTick {
3890 exchange: Exchange::Coinbase,
3891 symbol: "BTC-USD".into(),
3892 payload: json!({ "price": "50000", "size": "1", "side": "sell" }),
3893 received_at_ms: 0,
3894 };
3895 let tick = normalizer().normalize(raw).unwrap();
3896 assert_eq!(tick.side, Some(TradeSide::Sell));
3897 }
3898
3899 #[test]
3900 fn test_normalize_alpaca_tick() {
3901 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
3902 assert_eq!(tick.price, Decimal::from_str("180.50").unwrap());
3903 assert_eq!(tick.quantity, Decimal::from_str("10").unwrap());
3904 assert_eq!(tick.trade_id, Some("99".to_string()));
3905 assert_eq!(tick.side, None);
3906 }
3907
3908 #[test]
3909 fn test_normalize_polygon_tick() {
3910 let tick = normalizer().normalize(polygon_tick("AAPL")).unwrap();
3911 assert_eq!(tick.price, Decimal::from_str("180.51").unwrap());
3912 assert_eq!(tick.exchange_ts_ms, Some(1_700_000_000_000u64));
3914 assert_eq!(tick.trade_id, Some("XYZ-001".to_string()));
3915 }
3916
3917 #[test]
3918 fn test_normalize_alpaca_rfc3339_timestamp() {
3919 let raw = RawTick {
3920 exchange: Exchange::Alpaca,
3921 symbol: "AAPL".into(),
3922 payload: json!({ "p": "180.50", "s": "10", "i": 99, "t": "2023-11-15T00:00:00Z" }),
3923 received_at_ms: 1700000000003,
3924 };
3925 let tick = normalizer().normalize(raw).unwrap();
3926 assert!(tick.exchange_ts_ms.is_some(), "Alpaca 't' field should be parsed");
3927 assert_eq!(tick.exchange_ts_ms, Some(1700006400000u64));
3929 }
3930
3931 #[test]
3932 fn test_normalize_alpaca_no_timestamp_field() {
3933 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
3934 assert_eq!(tick.exchange_ts_ms, None, "missing 't' field means no exchange_ts_ms");
3935 }
3936
3937 #[test]
3938 fn test_normalize_missing_price_field_returns_parse_error() {
3939 let raw = RawTick {
3940 exchange: Exchange::Binance,
3941 symbol: "BTCUSDT".into(),
3942 payload: json!({ "q": "1" }),
3943 received_at_ms: 0,
3944 };
3945 let result = normalizer().normalize(raw);
3946 assert!(matches!(result, Err(StreamError::ParseError { .. })));
3947 }
3948
3949 #[test]
3950 fn test_normalize_invalid_decimal_returns_parse_error() {
3951 let raw = RawTick {
3952 exchange: Exchange::Coinbase,
3953 symbol: "BTC-USD".into(),
3954 payload: json!({ "price": "not-a-number", "size": "1" }),
3955 received_at_ms: 0,
3956 };
3957 let result = normalizer().normalize(raw);
3958 assert!(matches!(result, Err(StreamError::ParseError { .. })));
3959 }
3960
3961 #[test]
3962 fn test_raw_tick_new_sets_received_at() {
3963 let raw = RawTick::new(Exchange::Binance, "BTCUSDT", json!({}));
3964 assert!(raw.received_at_ms > 0);
3965 }
3966
3967 #[test]
3968 fn test_normalize_numeric_price_field() {
3969 let raw = RawTick {
3970 exchange: Exchange::Binance,
3971 symbol: "BTCUSDT".into(),
3972 payload: json!({ "p": 50000.0, "q": 1.0 }),
3973 received_at_ms: 0,
3974 };
3975 let tick = normalizer().normalize(raw).unwrap();
3976 assert!(tick.price > Decimal::ZERO);
3977 }
3978
3979 #[test]
3980 fn test_trade_side_from_str_buy() {
3981 assert_eq!("buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
3982 assert_eq!("Buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
3983 assert_eq!("BUY".parse::<TradeSide>().unwrap(), TradeSide::Buy);
3984 }
3985
3986 #[test]
3987 fn test_trade_side_from_str_sell() {
3988 assert_eq!("sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
3989 assert_eq!("Sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
3990 assert_eq!("SELL".parse::<TradeSide>().unwrap(), TradeSide::Sell);
3991 }
3992
3993 #[test]
3994 fn test_trade_side_from_str_invalid() {
3995 let err = "long".parse::<TradeSide>().unwrap_err();
3996 assert!(matches!(err, StreamError::ParseError { .. }));
3997 }
3998
3999 #[test]
4000 fn test_trade_side_display() {
4001 assert_eq!(TradeSide::Buy.to_string(), "buy");
4002 assert_eq!(TradeSide::Sell.to_string(), "sell");
4003 }
4004
4005 #[test]
4006 fn test_normalize_zero_price_returns_invalid_tick() {
4007 let raw = RawTick {
4008 exchange: Exchange::Binance,
4009 symbol: "BTCUSDT".into(),
4010 payload: json!({ "p": "0", "q": "1" }),
4011 received_at_ms: 0,
4012 };
4013 let err = normalizer().normalize(raw).unwrap_err();
4014 assert!(matches!(err, StreamError::InvalidTick { .. }));
4015 }
4016
4017 #[test]
4018 fn test_normalize_negative_price_returns_invalid_tick() {
4019 let raw = RawTick {
4020 exchange: Exchange::Binance,
4021 symbol: "BTCUSDT".into(),
4022 payload: json!({ "p": "-1", "q": "1" }),
4023 received_at_ms: 0,
4024 };
4025 let err = normalizer().normalize(raw).unwrap_err();
4026 assert!(matches!(err, StreamError::InvalidTick { .. }));
4027 }
4028
4029 #[test]
4030 fn test_normalize_negative_quantity_returns_invalid_tick() {
4031 let raw = RawTick {
4032 exchange: Exchange::Binance,
4033 symbol: "BTCUSDT".into(),
4034 payload: json!({ "p": "100", "q": "-1" }),
4035 received_at_ms: 0,
4036 };
4037 let err = normalizer().normalize(raw).unwrap_err();
4038 assert!(matches!(err, StreamError::InvalidTick { .. }));
4039 }
4040
4041 #[test]
4042 fn test_normalize_zero_quantity_is_valid() {
4043 let raw = RawTick {
4045 exchange: Exchange::Binance,
4046 symbol: "BTCUSDT".into(),
4047 payload: json!({ "p": "100", "q": "0" }),
4048 received_at_ms: 0,
4049 };
4050 let tick = normalizer().normalize(raw).unwrap();
4051 assert_eq!(tick.quantity, Decimal::ZERO);
4052 }
4053
4054 #[test]
4055 fn test_trade_side_is_buy() {
4056 assert!(TradeSide::Buy.is_buy());
4057 assert!(!TradeSide::Buy.is_sell());
4058 }
4059
4060 #[test]
4061 fn test_trade_side_is_sell() {
4062 assert!(TradeSide::Sell.is_sell());
4063 assert!(!TradeSide::Sell.is_buy());
4064 }
4065
4066 #[test]
4067 fn test_normalized_tick_display() {
4068 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4069 let s = tick.to_string();
4070 assert!(s.contains("Binance"));
4071 assert!(s.contains("BTCUSDT"));
4072 assert!(s.contains("50000"));
4073 }
4074
4075 #[test]
4076 fn test_normalized_tick_value_is_price_times_qty() {
4077 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4078 let expected = tick.price * tick.quantity;
4080 assert_eq!(tick.volume_notional(), expected);
4081 }
4082
4083 #[test]
4084 fn test_normalized_tick_age_ms_positive() {
4085 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
4086 let raw = RawTick {
4089 exchange: Exchange::Binance,
4090 symbol: "BTCUSDT".into(),
4091 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
4092 received_at_ms: 1_000_000,
4093 };
4094 let tick = normalizer().normalize(raw).unwrap();
4095 assert_eq!(tick.age_ms(1_001_000), 1_000);
4096 }
4097
4098 #[test]
4099 fn test_normalized_tick_age_ms_zero_when_now_equals_received() {
4100 let raw = RawTick {
4101 exchange: Exchange::Binance,
4102 symbol: "BTCUSDT".into(),
4103 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
4104 received_at_ms: 5_000,
4105 };
4106 let tick = normalizer().normalize(raw).unwrap();
4107 assert_eq!(tick.age_ms(5_000), 0);
4108 assert_eq!(tick.age_ms(4_000), 0);
4110 }
4111
4112 #[test]
4113 fn test_normalized_tick_value_zero_qty_is_zero() {
4114 use rust_decimal_macros::dec;
4115 let raw = RawTick {
4116 exchange: Exchange::Binance,
4117 symbol: "BTCUSDT".into(),
4118 payload: serde_json::json!({
4119 "p": "50000",
4120 "q": "0",
4121 "m": false,
4122 }),
4123 received_at_ms: 1000,
4124 };
4125 let tick = normalizer().normalize(raw).unwrap();
4126 assert_eq!(tick.value(), dec!(0));
4127 }
4128
4129 fn make_tick_at(received_at_ms: u64) -> NormalizedTick {
4132 NormalizedTick {
4133 exchange: Exchange::Binance,
4134 symbol: "BTCUSDT".into(),
4135 price: rust_decimal_macros::dec!(100),
4136 quantity: rust_decimal_macros::dec!(1),
4137 side: None,
4138 trade_id: None,
4139 exchange_ts_ms: None,
4140 received_at_ms,
4141 }
4142 }
4143
4144 #[test]
4145 fn test_is_stale_true_when_age_exceeds_threshold() {
4146 let tick = make_tick_at(1_000);
4147 assert!(tick.is_stale(6_000, 4_000));
4149 }
4150
4151 #[test]
4152 fn test_is_stale_false_when_age_equals_threshold() {
4153 let tick = make_tick_at(1_000);
4154 assert!(!tick.is_stale(5_000, 4_000));
4156 }
4157
4158 #[test]
4159 fn test_is_stale_false_for_fresh_tick() {
4160 let tick = make_tick_at(10_000);
4161 assert!(!tick.is_stale(10_500, 1_000));
4162 }
4163
4164 #[test]
4167 fn test_is_buy_true_for_buy_side() {
4168 let mut tick = make_tick_at(1_000);
4169 tick.side = Some(TradeSide::Buy);
4170 assert!(tick.is_buy());
4171 assert!(!tick.is_sell());
4172 }
4173
4174 #[test]
4175 fn test_is_sell_true_for_sell_side() {
4176 let mut tick = make_tick_at(1_000);
4177 tick.side = Some(TradeSide::Sell);
4178 assert!(tick.is_sell());
4179 assert!(!tick.is_buy());
4180 }
4181
4182 #[test]
4183 fn test_is_buy_false_for_unknown_side() {
4184 let mut tick = make_tick_at(1_000);
4185 tick.side = None;
4186 assert!(!tick.is_buy());
4187 assert!(!tick.is_sell());
4188 }
4189
4190 #[test]
4193 fn test_with_exchange_ts_sets_field() {
4194 let tick = make_tick_at(5_000).with_exchange_ts(3_000);
4195 assert_eq!(tick.exchange_ts_ms, Some(3_000));
4196 assert_eq!(tick.received_at_ms, 5_000); }
4198
4199 #[test]
4200 fn test_with_exchange_ts_overrides_existing() {
4201 let tick = make_tick_at(1_000).with_exchange_ts(999).with_exchange_ts(888);
4202 assert_eq!(tick.exchange_ts_ms, Some(888));
4203 }
4204
4205 #[test]
4208 fn test_price_move_from_positive() {
4209 let prev = make_tick_at(1_000);
4210 let mut curr = make_tick_at(2_000);
4211 curr.price = prev.price + rust_decimal_macros::dec!(5);
4212 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(5));
4213 }
4214
4215 #[test]
4216 fn test_price_move_from_negative() {
4217 let prev = make_tick_at(1_000);
4218 let mut curr = make_tick_at(2_000);
4219 curr.price = prev.price - rust_decimal_macros::dec!(3);
4220 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(-3));
4221 }
4222
4223 #[test]
4224 fn test_price_move_from_zero_when_same() {
4225 let tick = make_tick_at(1_000);
4226 assert_eq!(tick.price_move_from(&tick), rust_decimal_macros::dec!(0));
4227 }
4228
4229 #[test]
4230 fn test_is_more_recent_than_true() {
4231 let older = make_tick_at(1_000);
4232 let newer = make_tick_at(2_000);
4233 assert!(newer.is_more_recent_than(&older));
4234 }
4235
4236 #[test]
4237 fn test_is_more_recent_than_false_when_older() {
4238 let older = make_tick_at(1_000);
4239 let newer = make_tick_at(2_000);
4240 assert!(!older.is_more_recent_than(&newer));
4241 }
4242
4243 #[test]
4244 fn test_is_more_recent_than_false_when_equal() {
4245 let tick = make_tick_at(1_000);
4246 assert!(!tick.is_more_recent_than(&tick));
4247 }
4248
4249 #[test]
4252 fn test_with_side_sets_buy() {
4253 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
4254 assert_eq!(tick.side, Some(TradeSide::Buy));
4255 }
4256
4257 #[test]
4258 fn test_with_side_sets_sell() {
4259 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
4260 assert_eq!(tick.side, Some(TradeSide::Sell));
4261 }
4262
4263 #[test]
4264 fn test_with_side_overrides_existing() {
4265 let tick = make_tick_at(1_000).with_side(TradeSide::Buy).with_side(TradeSide::Sell);
4266 assert_eq!(tick.side, Some(TradeSide::Sell));
4267 }
4268
4269 #[test]
4272 fn test_is_neutral_true_when_no_side() {
4273 let mut tick = make_tick_at(1_000);
4274 tick.side = None;
4275 assert!(tick.is_neutral());
4276 }
4277
4278 #[test]
4279 fn test_is_neutral_false_when_buy() {
4280 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
4281 assert!(!tick.is_neutral());
4282 }
4283
4284 #[test]
4285 fn test_is_neutral_false_when_sell() {
4286 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
4287 assert!(!tick.is_neutral());
4288 }
4289
4290 #[test]
4293 fn test_is_large_trade_above_threshold() {
4294 let mut tick = make_tick_at(1_000);
4295 tick.quantity = rust_decimal_macros::dec!(100);
4296 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
4297 }
4298
4299 #[test]
4300 fn test_is_large_trade_at_threshold() {
4301 let mut tick = make_tick_at(1_000);
4302 tick.quantity = rust_decimal_macros::dec!(50);
4303 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
4304 }
4305
4306 #[test]
4307 fn test_is_large_trade_below_threshold() {
4308 let mut tick = make_tick_at(1_000);
4309 tick.quantity = rust_decimal_macros::dec!(10);
4310 assert!(!tick.is_large_trade(rust_decimal_macros::dec!(50)));
4311 }
4312
4313 #[test]
4314 fn test_volume_notional_is_price_times_quantity() {
4315 let mut tick = make_tick_at(1_000);
4316 tick.price = rust_decimal_macros::dec!(200);
4317 tick.quantity = rust_decimal_macros::dec!(3);
4318 assert_eq!(tick.volume_notional(), rust_decimal_macros::dec!(600));
4319 }
4320
4321 #[test]
4324 fn test_is_above_returns_true_when_price_higher() {
4325 let mut tick = make_tick_at(1_000);
4326 tick.price = rust_decimal_macros::dec!(200);
4327 assert!(tick.is_above(rust_decimal_macros::dec!(150)));
4328 }
4329
4330 #[test]
4331 fn test_is_above_returns_false_when_price_equal() {
4332 let mut tick = make_tick_at(1_000);
4333 tick.price = rust_decimal_macros::dec!(200);
4334 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
4335 }
4336
4337 #[test]
4338 fn test_is_above_returns_false_when_price_lower() {
4339 let mut tick = make_tick_at(1_000);
4340 tick.price = rust_decimal_macros::dec!(100);
4341 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
4342 }
4343
4344 #[test]
4347 fn test_is_below_returns_true_when_price_lower() {
4348 let mut tick = make_tick_at(1_000);
4349 tick.price = rust_decimal_macros::dec!(100);
4350 assert!(tick.is_below(rust_decimal_macros::dec!(150)));
4351 }
4352
4353 #[test]
4354 fn test_is_below_returns_false_when_price_equal() {
4355 let mut tick = make_tick_at(1_000);
4356 tick.price = rust_decimal_macros::dec!(100);
4357 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
4358 }
4359
4360 #[test]
4361 fn test_is_below_returns_false_when_price_higher() {
4362 let mut tick = make_tick_at(1_000);
4363 tick.price = rust_decimal_macros::dec!(200);
4364 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
4365 }
4366
4367 #[test]
4370 fn test_has_exchange_ts_false_when_none() {
4371 let tick = make_tick_at(1_000);
4372 assert!(!tick.has_exchange_ts());
4373 }
4374
4375 #[test]
4376 fn test_has_exchange_ts_true_when_some() {
4377 let tick = make_tick_at(1_000).with_exchange_ts(900);
4378 assert!(tick.has_exchange_ts());
4379 }
4380
4381 #[test]
4384 fn test_is_at_returns_true_when_equal() {
4385 let mut tick = make_tick_at(1_000);
4386 tick.price = rust_decimal_macros::dec!(100);
4387 assert!(tick.is_at(rust_decimal_macros::dec!(100)));
4388 }
4389
4390 #[test]
4391 fn test_is_at_returns_false_when_higher() {
4392 let mut tick = make_tick_at(1_000);
4393 tick.price = rust_decimal_macros::dec!(101);
4394 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
4395 }
4396
4397 #[test]
4398 fn test_is_at_returns_false_when_lower() {
4399 let mut tick = make_tick_at(1_000);
4400 tick.price = rust_decimal_macros::dec!(99);
4401 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
4402 }
4403
4404 #[test]
4407 fn test_is_buy_true_when_side_is_buy() {
4408 let mut tick = make_tick_at(1_000);
4409 tick.side = Some(TradeSide::Buy);
4410 assert!(tick.is_buy());
4411 }
4412
4413 #[test]
4414 fn test_is_buy_false_when_side_is_sell() {
4415 let mut tick = make_tick_at(1_000);
4416 tick.side = Some(TradeSide::Sell);
4417 assert!(!tick.is_buy());
4418 }
4419
4420 #[test]
4421 fn test_is_buy_false_when_side_is_none() {
4422 let mut tick = make_tick_at(1_000);
4423 tick.side = None;
4424 assert!(!tick.is_buy());
4425 }
4426
4427 #[test]
4430 fn test_side_str_buy() {
4431 let mut tick = make_tick_at(1_000);
4432 tick.side = Some(TradeSide::Buy);
4433 assert_eq!(tick.side_str(), "buy");
4434 }
4435
4436 #[test]
4437 fn test_side_str_sell() {
4438 let mut tick = make_tick_at(1_000);
4439 tick.side = Some(TradeSide::Sell);
4440 assert_eq!(tick.side_str(), "sell");
4441 }
4442
4443 #[test]
4444 fn test_side_str_unknown_when_none() {
4445 let mut tick = make_tick_at(1_000);
4446 tick.side = None;
4447 assert_eq!(tick.side_str(), "unknown");
4448 }
4449
4450 #[test]
4451 fn test_is_round_lot_true_for_integer_quantity() {
4452 let mut tick = make_tick_at(1_000);
4453 tick.quantity = rust_decimal_macros::dec!(100);
4454 assert!(tick.is_round_lot());
4455 }
4456
4457 #[test]
4458 fn test_is_round_lot_false_for_fractional_quantity() {
4459 let mut tick = make_tick_at(1_000);
4460 tick.quantity = rust_decimal_macros::dec!(0.5);
4461 assert!(!tick.is_round_lot());
4462 }
4463
4464 #[test]
4467 fn test_is_same_symbol_as_true_when_symbols_match() {
4468 let t1 = make_tick_at(1_000);
4469 let t2 = make_tick_at(2_000);
4470 assert!(t1.is_same_symbol_as(&t2));
4471 }
4472
4473 #[test]
4474 fn test_is_same_symbol_as_false_when_symbols_differ() {
4475 let t1 = make_tick_at(1_000);
4476 let mut t2 = make_tick_at(2_000);
4477 t2.symbol = "ETH-USD".to_string();
4478 assert!(!t1.is_same_symbol_as(&t2));
4479 }
4480
4481 #[test]
4482 fn test_price_distance_from_is_absolute() {
4483 let mut t1 = make_tick_at(1_000);
4484 let mut t2 = make_tick_at(2_000);
4485 t1.price = rust_decimal_macros::dec!(100);
4486 t2.price = rust_decimal_macros::dec!(110);
4487 assert_eq!(t1.price_distance_from(&t2), rust_decimal_macros::dec!(10));
4488 assert_eq!(t2.price_distance_from(&t1), rust_decimal_macros::dec!(10));
4489 }
4490
4491 #[test]
4492 fn test_price_distance_from_zero_when_equal() {
4493 let t1 = make_tick_at(1_000);
4494 let t2 = make_tick_at(2_000);
4495 assert!(t1.price_distance_from(&t2).is_zero());
4496 }
4497
4498 #[test]
4501 fn test_is_sell_true_when_side_is_sell() {
4502 let mut tick = make_tick_at(1_000);
4503 tick.side = Some(TradeSide::Sell);
4504 assert!(tick.is_sell());
4505 }
4506
4507 #[test]
4508 fn test_is_sell_false_when_side_is_buy() {
4509 let mut tick = make_tick_at(1_000);
4510 tick.side = Some(TradeSide::Buy);
4511 assert!(!tick.is_sell());
4512 }
4513
4514 #[test]
4515 fn test_is_sell_false_when_side_is_none() {
4516 let mut tick = make_tick_at(1_000);
4517 tick.side = None;
4518 assert!(!tick.is_sell());
4519 }
4520
4521 #[test]
4524 fn test_exchange_latency_ms_positive_for_normal_delivery() {
4525 let mut tick = make_tick_at(1_100);
4526 tick.exchange_ts_ms = Some(1_000);
4527 assert_eq!(tick.exchange_latency_ms(), Some(100));
4528 }
4529
4530 #[test]
4531 fn test_exchange_latency_ms_negative_for_clock_skew() {
4532 let mut tick = make_tick_at(1_000);
4533 tick.exchange_ts_ms = Some(1_100);
4534 assert_eq!(tick.exchange_latency_ms(), Some(-100));
4535 }
4536
4537 #[test]
4538 fn test_exchange_latency_ms_none_when_no_exchange_ts() {
4539 let mut tick = make_tick_at(1_000);
4540 tick.exchange_ts_ms = None;
4541 assert!(tick.exchange_latency_ms().is_none());
4542 }
4543
4544 #[test]
4545 fn test_is_notional_large_trade_true_when_above_threshold() {
4546 let mut tick = make_tick_at(1_000);
4547 tick.price = rust_decimal_macros::dec!(100);
4548 tick.quantity = rust_decimal_macros::dec!(10);
4549 assert!(tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
4551 }
4552
4553 #[test]
4554 fn test_is_notional_large_trade_false_when_at_or_below_threshold() {
4555 let mut tick = make_tick_at(1_000);
4556 tick.price = rust_decimal_macros::dec!(100);
4557 tick.quantity = rust_decimal_macros::dec!(5);
4558 assert!(!tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
4560 }
4561
4562 #[test]
4563 fn test_is_aggressive_true_when_buy() {
4564 let mut tick = make_tick_at(1_000);
4565 tick.side = Some(TradeSide::Buy);
4566 assert!(tick.is_aggressive());
4567 }
4568
4569 #[test]
4570 fn test_is_aggressive_true_when_sell() {
4571 let mut tick = make_tick_at(1_000);
4572 tick.side = Some(TradeSide::Sell);
4573 assert!(tick.is_aggressive());
4574 }
4575
4576 #[test]
4577 fn test_is_aggressive_false_when_neutral() {
4578 let tick = make_tick_at(1_000); assert!(!tick.is_aggressive());
4580 }
4581
4582 #[test]
4583 fn test_price_diff_from_positive_when_higher() {
4584 let mut t1 = make_tick_at(1_000);
4585 let mut t2 = make_tick_at(1_000);
4586 t1.price = rust_decimal_macros::dec!(105);
4587 t2.price = rust_decimal_macros::dec!(100);
4588 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(5));
4589 }
4590
4591 #[test]
4592 fn test_price_diff_from_negative_when_lower() {
4593 let mut t1 = make_tick_at(1_000);
4594 let mut t2 = make_tick_at(1_000);
4595 t1.price = rust_decimal_macros::dec!(95);
4596 t2.price = rust_decimal_macros::dec!(100);
4597 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(-5));
4598 }
4599
4600 #[test]
4601 fn test_is_micro_trade_true_when_below_threshold() {
4602 let mut tick = make_tick_at(1_000);
4603 tick.quantity = rust_decimal_macros::dec!(0.5);
4604 assert!(tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4605 }
4606
4607 #[test]
4608 fn test_is_micro_trade_false_when_equal_threshold() {
4609 let mut tick = make_tick_at(1_000);
4610 tick.quantity = rust_decimal_macros::dec!(1);
4611 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4612 }
4613
4614 #[test]
4615 fn test_is_micro_trade_false_when_above_threshold() {
4616 let mut tick = make_tick_at(1_000);
4617 tick.quantity = rust_decimal_macros::dec!(2);
4618 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
4619 }
4620
4621 #[test]
4624 fn test_is_zero_price_true_for_zero() {
4625 let mut tick = make_tick_at(1_000);
4626 tick.price = rust_decimal_macros::dec!(0);
4627 assert!(tick.is_zero_price());
4628 }
4629
4630 #[test]
4631 fn test_is_zero_price_false_for_nonzero() {
4632 let tick = make_tick_at(1_000); assert!(!tick.is_zero_price());
4634 }
4635
4636 #[test]
4637 fn test_is_fresh_true_when_within_age() {
4638 let tick = make_tick_at(1_000);
4639 assert!(tick.is_fresh(2_000, 1_500));
4641 }
4642
4643 #[test]
4644 fn test_is_fresh_false_when_too_old() {
4645 let tick = make_tick_at(1_000);
4646 assert!(!tick.is_fresh(5_000, 2_000));
4648 }
4649
4650 #[test]
4651 fn test_is_fresh_true_when_now_less_than_received() {
4652 let tick = make_tick_at(5_000);
4654 assert!(tick.is_fresh(3_000, 100));
4655 }
4656
4657 #[test]
4659 fn test_age_ms_correct_elapsed() {
4660 let tick = make_tick_at(10_000);
4661 assert_eq!(tick.age_ms(10_500), 500);
4662 }
4663
4664 #[test]
4665 fn test_age_ms_zero_when_now_equals_received() {
4666 let tick = make_tick_at(10_000);
4667 assert_eq!(tick.age_ms(10_000), 0);
4668 }
4669
4670 #[test]
4671 fn test_age_ms_zero_when_now_before_received() {
4672 let tick = make_tick_at(10_000);
4673 assert_eq!(tick.age_ms(9_000), 0);
4674 }
4675
4676 #[test]
4678 fn test_is_buying_pressure_true_above_midpoint() {
4679 use rust_decimal_macros::dec;
4680 let mut tick = make_tick_at(0);
4681 tick.price = dec!(100.50);
4682 assert!(tick.is_buying_pressure(dec!(100)));
4683 }
4684
4685 #[test]
4686 fn test_is_buying_pressure_false_below_midpoint() {
4687 use rust_decimal_macros::dec;
4688 let mut tick = make_tick_at(0);
4689 tick.price = dec!(99.50);
4690 assert!(!tick.is_buying_pressure(dec!(100)));
4691 }
4692
4693 #[test]
4694 fn test_is_buying_pressure_false_at_midpoint() {
4695 use rust_decimal_macros::dec;
4696 let mut tick = make_tick_at(0);
4697 tick.price = dec!(100);
4698 assert!(!tick.is_buying_pressure(dec!(100)));
4699 }
4700
4701 #[test]
4703 fn test_rounded_price_rounds_to_nearest_tick() {
4704 use rust_decimal_macros::dec;
4705 let mut tick = make_tick_at(0);
4706 tick.price = dec!(100.37);
4707 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.25));
4709 }
4710
4711 #[test]
4712 fn test_rounded_price_unchanged_when_already_aligned() {
4713 use rust_decimal_macros::dec;
4714 let mut tick = make_tick_at(0);
4715 tick.price = dec!(100.50);
4716 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.50));
4717 }
4718
4719 #[test]
4720 fn test_rounded_price_returns_original_for_zero_tick_size() {
4721 use rust_decimal_macros::dec;
4722 let mut tick = make_tick_at(0);
4723 tick.price = dec!(99.99);
4724 assert_eq!(tick.rounded_price(dec!(0)), dec!(99.99));
4725 }
4726
4727 #[test]
4729 fn test_is_large_spread_from_true_when_large() {
4730 use rust_decimal_macros::dec;
4731 let mut t1 = make_tick_at(0);
4732 let mut t2 = make_tick_at(0);
4733 t1.price = dec!(100);
4734 t2.price = dec!(110);
4735 assert!(t1.is_large_spread_from(&t2, dec!(5)));
4736 }
4737
4738 #[test]
4739 fn test_is_large_spread_from_false_when_small() {
4740 use rust_decimal_macros::dec;
4741 let mut t1 = make_tick_at(0);
4742 let mut t2 = make_tick_at(0);
4743 t1.price = dec!(100);
4744 t2.price = dec!(101);
4745 assert!(!t1.is_large_spread_from(&t2, dec!(5)));
4746 }
4747
4748 #[test]
4751 fn test_age_secs_correct() {
4752 let tick = make_tick_at(1_000);
4753 assert!((tick.age_secs(3_000) - 2.0).abs() < 1e-9);
4754 }
4755
4756 #[test]
4757 fn test_age_secs_zero_when_now_equals_received() {
4758 let tick = make_tick_at(5_000);
4759 assert_eq!(tick.age_secs(5_000), 0.0);
4760 }
4761
4762 #[test]
4763 fn test_age_secs_zero_when_now_before_received() {
4764 let tick = make_tick_at(5_000);
4765 assert_eq!(tick.age_secs(1_000), 0.0);
4766 }
4767
4768 #[test]
4771 fn test_is_same_exchange_as_true_when_matching() {
4772 let t1 = make_tick_at(1_000); let t2 = make_tick_at(2_000); assert!(t1.is_same_exchange_as(&t2));
4775 }
4776
4777 #[test]
4778 fn test_is_same_exchange_as_false_when_different() {
4779 let t1 = make_tick_at(1_000); let mut t2 = make_tick_at(2_000);
4781 t2.exchange = Exchange::Coinbase;
4782 assert!(!t1.is_same_exchange_as(&t2));
4783 }
4784
4785 #[test]
4788 fn test_quote_age_ms_correct() {
4789 let tick = make_tick_at(1_000);
4790 assert_eq!(tick.quote_age_ms(3_000), 2_000);
4791 }
4792
4793 #[test]
4794 fn test_quote_age_ms_zero_when_now_before_received() {
4795 let tick = make_tick_at(5_000);
4796 assert_eq!(tick.quote_age_ms(1_000), 0);
4797 }
4798
4799 #[test]
4800 fn test_notional_value_correct() {
4801 use rust_decimal_macros::dec;
4802 let mut tick = make_tick_at(0);
4803 tick.price = dec!(100);
4804 tick.quantity = dec!(5);
4805 assert_eq!(tick.notional_value(), dec!(500));
4806 }
4807
4808 #[test]
4809 fn test_is_high_value_tick_true_when_above_threshold() {
4810 use rust_decimal_macros::dec;
4811 let mut tick = make_tick_at(0);
4812 tick.price = dec!(100);
4813 tick.quantity = dec!(10);
4814 assert!(tick.is_high_value_tick(dec!(500)));
4816 }
4817
4818 #[test]
4819 fn test_is_high_value_tick_false_when_below_threshold() {
4820 use rust_decimal_macros::dec;
4821 let mut tick = make_tick_at(0);
4822 tick.price = dec!(10);
4823 tick.quantity = dec!(2);
4824 assert!(!tick.is_high_value_tick(dec!(100)));
4826 }
4827
4828 #[test]
4831 fn test_is_buy_side_true_when_buy() {
4832 let mut tick = make_tick_at(0);
4833 tick.side = Some(TradeSide::Buy);
4834 assert!(tick.is_buy_side());
4835 }
4836
4837 #[test]
4838 fn test_is_buy_side_false_when_sell() {
4839 let mut tick = make_tick_at(0);
4840 tick.side = Some(TradeSide::Sell);
4841 assert!(!tick.is_buy_side());
4842 }
4843
4844 #[test]
4845 fn test_is_buy_side_false_when_none() {
4846 let mut tick = make_tick_at(0);
4847 tick.side = None;
4848 assert!(!tick.is_buy_side());
4849 }
4850
4851 #[test]
4852 fn test_is_sell_side_true_when_sell() {
4853 let mut tick = make_tick_at(0);
4854 tick.side = Some(TradeSide::Sell);
4855 assert!(tick.is_sell_side());
4856 }
4857
4858 #[test]
4859 fn test_price_in_range_true_when_within() {
4860 use rust_decimal_macros::dec;
4861 let mut tick = make_tick_at(0);
4862 tick.price = dec!(100);
4863 assert!(tick.price_in_range(dec!(90), dec!(110)));
4864 }
4865
4866 #[test]
4867 fn test_price_in_range_false_when_below() {
4868 use rust_decimal_macros::dec;
4869 let mut tick = make_tick_at(0);
4870 tick.price = dec!(80);
4871 assert!(!tick.price_in_range(dec!(90), dec!(110)));
4872 }
4873
4874 #[test]
4875 fn test_price_in_range_true_at_boundary() {
4876 use rust_decimal_macros::dec;
4877 let mut tick = make_tick_at(0);
4878 tick.price = dec!(90);
4879 assert!(tick.price_in_range(dec!(90), dec!(110)));
4880 }
4881
4882 #[test]
4885 fn test_is_zero_quantity_true_when_zero() {
4886 let mut tick = make_tick_at(0);
4887 tick.quantity = Decimal::ZERO;
4888 assert!(tick.is_zero_quantity());
4889 }
4890
4891 #[test]
4892 fn test_is_zero_quantity_false_when_nonzero() {
4893 let mut tick = make_tick_at(0);
4894 tick.quantity = Decimal::ONE;
4895 assert!(!tick.is_zero_quantity());
4896 }
4897
4898 #[test]
4901 fn test_is_large_tick_true_when_above_threshold() {
4902 let mut tick = make_tick_at(0);
4903 tick.quantity = Decimal::from(10u32);
4904 assert!(tick.is_large_tick(Decimal::from(5u32)));
4905 }
4906
4907 #[test]
4908 fn test_is_large_tick_false_when_at_threshold() {
4909 let mut tick = make_tick_at(0);
4910 tick.quantity = Decimal::from(5u32);
4911 assert!(!tick.is_large_tick(Decimal::from(5u32)));
4912 }
4913
4914 #[test]
4915 fn test_is_large_tick_false_when_below_threshold() {
4916 let mut tick = make_tick_at(0);
4917 tick.quantity = Decimal::from(1u32);
4918 assert!(!tick.is_large_tick(Decimal::from(5u32)));
4919 }
4920
4921 #[test]
4924 fn test_is_away_from_price_true_when_beyond_threshold() {
4925 let mut tick = make_tick_at(0);
4926 tick.price = Decimal::from(110u32);
4927 assert!(tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
4929 }
4930
4931 #[test]
4932 fn test_is_away_from_price_false_when_at_threshold() {
4933 let mut tick = make_tick_at(0);
4934 tick.price = Decimal::from(105u32);
4935 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
4937 }
4938
4939 #[test]
4940 fn test_is_away_from_price_false_when_equal() {
4941 let mut tick = make_tick_at(0);
4942 tick.price = Decimal::from(100u32);
4943 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(1u32)));
4944 }
4945
4946 #[test]
4949 fn test_is_within_spread_true_when_between() {
4950 let mut tick = make_tick_at(0);
4951 tick.price = Decimal::from(100u32);
4952 assert!(tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
4953 }
4954
4955 #[test]
4956 fn test_is_within_spread_false_when_at_bid() {
4957 let mut tick = make_tick_at(0);
4958 tick.price = Decimal::from(99u32);
4959 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
4960 }
4961
4962 #[test]
4963 fn test_is_within_spread_false_when_above_ask() {
4964 let mut tick = make_tick_at(0);
4965 tick.price = Decimal::from(102u32);
4966 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
4967 }
4968
4969 #[test]
4972 fn test_is_recent_true_when_within_threshold() {
4973 let tick = make_tick_at(9_500);
4974 assert!(tick.is_recent(1_000, 10_000));
4976 }
4977
4978 #[test]
4979 fn test_is_recent_false_when_beyond_threshold() {
4980 let tick = make_tick_at(8_000);
4981 assert!(!tick.is_recent(1_000, 10_000));
4983 }
4984
4985 #[test]
4986 fn test_is_recent_true_at_exact_threshold() {
4987 let tick = make_tick_at(9_000);
4988 assert!(tick.is_recent(1_000, 10_000));
4990 }
4991
4992 #[test]
4995 fn test_side_as_str_buy() {
4996 let mut tick = make_tick_at(0);
4997 tick.side = Some(TradeSide::Buy);
4998 assert_eq!(tick.side_as_str(), Some("buy"));
4999 }
5000
5001 #[test]
5002 fn test_side_as_str_sell() {
5003 let mut tick = make_tick_at(0);
5004 tick.side = Some(TradeSide::Sell);
5005 assert_eq!(tick.side_as_str(), Some("sell"));
5006 }
5007
5008 #[test]
5009 fn test_side_as_str_none_when_unknown() {
5010 let mut tick = make_tick_at(0);
5011 tick.side = None;
5012 assert!(tick.side_as_str().is_none());
5013 }
5014
5015 #[test]
5018 fn test_is_above_price_true_when_strictly_above() {
5019 let tick = make_tick_at(0); assert!(tick.is_above_price(rust_decimal_macros::dec!(99)));
5021 }
5022
5023 #[test]
5024 fn test_is_above_price_false_when_equal() {
5025 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(100)));
5027 }
5028
5029 #[test]
5030 fn test_is_above_price_false_when_below() {
5031 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(101)));
5033 }
5034
5035 #[test]
5038 fn test_price_change_from_positive_when_above_reference() {
5039 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(90)), rust_decimal_macros::dec!(10));
5041 }
5042
5043 #[test]
5044 fn test_price_change_from_negative_when_below_reference() {
5045 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(110)), rust_decimal_macros::dec!(-10));
5047 }
5048
5049 #[test]
5050 fn test_price_change_from_zero_when_equal() {
5051 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
5053 }
5054
5055 #[test]
5058 fn test_is_below_price_true_when_strictly_below() {
5059 let tick = make_tick_at(0); assert!(tick.is_below_price(rust_decimal_macros::dec!(101)));
5061 }
5062
5063 #[test]
5064 fn test_is_below_price_false_when_equal() {
5065 let tick = make_tick_at(0); assert!(!tick.is_below_price(rust_decimal_macros::dec!(100)));
5067 }
5068
5069 #[test]
5072 fn test_quantity_above_true_when_quantity_exceeds_threshold() {
5073 let tick = make_tick_at(0); assert!(tick.quantity_above(rust_decimal_macros::dec!(0)));
5075 }
5076
5077 #[test]
5078 fn test_quantity_above_false_when_quantity_equals_threshold() {
5079 let tick = make_tick_at(0); assert!(!tick.quantity_above(rust_decimal_macros::dec!(1)));
5081 }
5082
5083 #[test]
5086 fn test_is_at_price_true_when_equal() {
5087 let tick = make_tick_at(0); assert!(tick.is_at_price(rust_decimal_macros::dec!(100)));
5089 }
5090
5091 #[test]
5092 fn test_is_at_price_false_when_different() {
5093 let tick = make_tick_at(0); assert!(!tick.is_at_price(rust_decimal_macros::dec!(101)));
5095 }
5096
5097 #[test]
5100 fn test_is_round_number_true_when_divisible() {
5101 let tick = make_tick_at(0); assert!(tick.is_round_number(rust_decimal_macros::dec!(10)));
5103 assert!(tick.is_round_number(rust_decimal_macros::dec!(100)));
5104 }
5105
5106 #[test]
5107 fn test_is_round_number_false_when_not_divisible() {
5108 let tick = make_tick_at(0); assert!(!tick.is_round_number(rust_decimal_macros::dec!(3)));
5110 }
5111
5112 #[test]
5113 fn test_is_round_number_false_when_step_zero() {
5114 let tick = make_tick_at(0);
5115 assert!(!tick.is_round_number(rust_decimal_macros::dec!(0)));
5116 }
5117
5118 #[test]
5121 fn test_is_market_open_tick_true_when_within_session() {
5122 let tick = make_tick_at(500); assert!(tick.is_market_open_tick(100, 1_000));
5124 }
5125
5126 #[test]
5127 fn test_is_market_open_tick_false_when_before_session() {
5128 let tick = make_tick_at(50);
5129 assert!(!tick.is_market_open_tick(100, 1_000));
5130 }
5131
5132 #[test]
5133 fn test_is_market_open_tick_false_when_at_session_end() {
5134 let tick = make_tick_at(1_000);
5135 assert!(!tick.is_market_open_tick(100, 1_000)); }
5137
5138 #[test]
5141 fn test_signed_quantity_positive_for_buy() {
5142 let mut tick = make_tick_at(0);
5143 tick.side = Some(TradeSide::Buy);
5144 assert!(tick.signed_quantity() > rust_decimal::Decimal::ZERO);
5145 }
5146
5147 #[test]
5148 fn test_signed_quantity_negative_for_sell() {
5149 let mut tick = make_tick_at(0);
5150 tick.side = Some(TradeSide::Sell);
5151 assert!(tick.signed_quantity() < rust_decimal::Decimal::ZERO);
5152 }
5153
5154 #[test]
5155 fn test_signed_quantity_zero_for_unknown() {
5156 let tick = make_tick_at(0); assert_eq!(tick.signed_quantity(), rust_decimal::Decimal::ZERO);
5158 }
5159
5160 #[test]
5163 fn test_as_price_level_returns_price_and_quantity() {
5164 let tick = make_tick_at(0); let (p, q) = tick.as_price_level();
5166 assert_eq!(p, rust_decimal_macros::dec!(100));
5167 assert_eq!(q, rust_decimal_macros::dec!(1));
5168 }
5169
5170 fn make_sided_tick(qty: rust_decimal::Decimal, side: Option<TradeSide>) -> NormalizedTick {
5173 NormalizedTick {
5174 exchange: Exchange::Binance,
5175 symbol: "BTCUSDT".into(),
5176 price: rust_decimal_macros::dec!(100),
5177 quantity: qty,
5178 side,
5179 trade_id: None,
5180 exchange_ts_ms: None,
5181 received_at_ms: 0,
5182 }
5183 }
5184
5185 #[test]
5186 fn test_buy_volume_zero_for_empty_slice() {
5187 assert_eq!(NormalizedTick::buy_volume(&[]), rust_decimal::Decimal::ZERO);
5188 }
5189
5190 #[test]
5191 fn test_buy_volume_sums_only_buy_ticks() {
5192 let buy1 = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
5193 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5194 let buy2 = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
5195 let unknown = make_sided_tick(rust_decimal_macros::dec!(10), None);
5196 assert_eq!(
5197 NormalizedTick::buy_volume(&[buy1, sell, buy2, unknown]),
5198 rust_decimal_macros::dec!(7)
5199 );
5200 }
5201
5202 #[test]
5203 fn test_sell_volume_zero_for_empty_slice() {
5204 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal::Decimal::ZERO);
5205 }
5206
5207 #[test]
5208 fn test_sell_volume_sums_only_sell_ticks() {
5209 let buy = make_sided_tick(rust_decimal_macros::dec!(2), Some(TradeSide::Buy));
5210 let sell1 = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5211 let sell2 = make_sided_tick(rust_decimal_macros::dec!(4), Some(TradeSide::Sell));
5212 assert_eq!(
5213 NormalizedTick::sell_volume(&[buy, sell1, sell2]),
5214 rust_decimal_macros::dec!(7)
5215 );
5216 }
5217
5218 #[test]
5219 fn test_buy_sell_volumes_dont_include_unknown_side() {
5220 let buy = make_sided_tick(rust_decimal_macros::dec!(5), Some(TradeSide::Buy));
5221 let sell = make_sided_tick(rust_decimal_macros::dec!(3), Some(TradeSide::Sell));
5222 let unknown = make_sided_tick(rust_decimal_macros::dec!(2), None);
5223 let ticks = [buy, sell, unknown];
5224 let total: rust_decimal::Decimal = ticks.iter().map(|t| t.quantity).sum();
5225 let accounted = NormalizedTick::buy_volume(&ticks) + NormalizedTick::sell_volume(&ticks);
5226 assert_eq!(accounted, rust_decimal_macros::dec!(8));
5228 assert!(accounted < total);
5229 }
5230
5231 fn make_tick_with_price(price: rust_decimal::Decimal) -> NormalizedTick {
5234 NormalizedTick {
5235 exchange: Exchange::Binance,
5236 symbol: "BTCUSDT".into(),
5237 price,
5238 quantity: rust_decimal_macros::dec!(1),
5239 side: None,
5240 trade_id: None,
5241 exchange_ts_ms: None,
5242 received_at_ms: 0,
5243 }
5244 }
5245
5246 #[test]
5247 fn test_price_range_none_for_empty_slice() {
5248 assert!(NormalizedTick::price_range(&[]).is_none());
5249 }
5250
5251 #[test]
5252 fn test_price_range_zero_for_single_tick() {
5253 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5254 assert_eq!(NormalizedTick::price_range(&[tick]), Some(rust_decimal_macros::dec!(0)));
5255 }
5256
5257 #[test]
5258 fn test_price_range_correct_for_multiple_ticks() {
5259 let t1 = make_tick_with_price(rust_decimal_macros::dec!(95));
5260 let t2 = make_tick_with_price(rust_decimal_macros::dec!(105));
5261 let t3 = make_tick_with_price(rust_decimal_macros::dec!(100));
5262 assert_eq!(NormalizedTick::price_range(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
5263 }
5264
5265 #[test]
5266 fn test_average_price_none_for_empty_slice() {
5267 assert!(NormalizedTick::average_price(&[]).is_none());
5268 }
5269
5270 #[test]
5271 fn test_average_price_equals_price_for_single_tick() {
5272 let tick = make_tick_with_price(rust_decimal_macros::dec!(200));
5273 assert_eq!(NormalizedTick::average_price(&[tick]), Some(rust_decimal_macros::dec!(200)));
5274 }
5275
5276 #[test]
5277 fn test_average_price_correct_for_multiple_ticks() {
5278 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5279 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5280 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5281 assert_eq!(NormalizedTick::average_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
5283 }
5284
5285 fn make_tick_pq(price: rust_decimal::Decimal, qty: rust_decimal::Decimal) -> NormalizedTick {
5288 NormalizedTick {
5289 exchange: Exchange::Binance,
5290 symbol: "BTCUSDT".into(),
5291 price,
5292 quantity: qty,
5293 side: None,
5294 trade_id: None,
5295 exchange_ts_ms: None,
5296 received_at_ms: 0,
5297 }
5298 }
5299
5300 #[test]
5301 fn test_vwap_none_for_empty_slice() {
5302 assert!(NormalizedTick::vwap(&[]).is_none());
5303 }
5304
5305 #[test]
5306 fn test_vwap_equals_price_for_single_tick() {
5307 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5308 assert_eq!(NormalizedTick::vwap(&[tick]), Some(rust_decimal_macros::dec!(100)));
5309 }
5310
5311 #[test]
5312 fn test_vwap_weighted_correctly() {
5313 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5315 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5316 assert_eq!(NormalizedTick::vwap(&[t1, t2]), Some(rust_decimal_macros::dec!(175)));
5317 }
5318
5319 #[test]
5320 fn test_vwap_none_for_zero_total_volume() {
5321 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(0));
5322 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(0));
5323 assert!(NormalizedTick::vwap(&[t1, t2]).is_none());
5324 }
5325
5326 #[test]
5329 fn test_count_above_price_zero_for_empty_slice() {
5330 assert_eq!(NormalizedTick::count_above_price(&[], rust_decimal_macros::dec!(100)), 0);
5331 }
5332
5333 #[test]
5334 fn test_count_above_price_correct() {
5335 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5336 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5337 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5338 assert_eq!(NormalizedTick::count_above_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
5339 }
5340
5341 #[test]
5342 fn test_count_below_price_correct() {
5343 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5344 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5345 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5346 assert_eq!(NormalizedTick::count_below_price(&[t1, t2, t3], rust_decimal_macros::dec!(100)), 1);
5347 }
5348
5349 #[test]
5350 fn test_count_above_at_threshold_excluded() {
5351 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5352 assert_eq!(NormalizedTick::count_above_price(&[tick], rust_decimal_macros::dec!(100)), 0);
5353 }
5354
5355 #[test]
5356 fn test_count_below_at_threshold_excluded() {
5357 let tick = make_tick_with_price(rust_decimal_macros::dec!(100));
5358 assert_eq!(NormalizedTick::count_below_price(&[tick], rust_decimal_macros::dec!(100)), 0);
5359 }
5360
5361 #[test]
5364 fn test_total_notional_zero_for_empty_slice() {
5365 assert_eq!(NormalizedTick::total_notional(&[]), rust_decimal::Decimal::ZERO);
5366 }
5367
5368 #[test]
5369 fn test_total_notional_sums_all_ticks() {
5370 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5372 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5373 assert_eq!(NormalizedTick::total_notional(&[t1, t2]), rust_decimal_macros::dec!(800));
5374 }
5375
5376 #[test]
5377 fn test_buy_notional_only_includes_buy_side() {
5378 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5379 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5380 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
5381 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
5382 assert_eq!(NormalizedTick::buy_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(200));
5384 }
5385
5386 #[test]
5387 fn test_sell_notional_only_includes_sell_side() {
5388 let buy = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5389 let sell = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(3));
5390 let buy_with_side = NormalizedTick { side: Some(TradeSide::Buy), ..buy };
5391 let sell_with_side = NormalizedTick { side: Some(TradeSide::Sell), ..sell };
5392 assert_eq!(NormalizedTick::sell_notional(&[buy_with_side, sell_with_side]), rust_decimal_macros::dec!(600));
5394 }
5395
5396 #[test]
5399 fn test_median_price_none_for_empty_slice() {
5400 assert!(NormalizedTick::median_price(&[]).is_none());
5401 }
5402
5403 #[test]
5404 fn test_median_price_single_tick() {
5405 let tick = make_tick_with_price(rust_decimal_macros::dec!(150));
5406 assert_eq!(NormalizedTick::median_price(&[tick]), Some(rust_decimal_macros::dec!(150)));
5407 }
5408
5409 #[test]
5410 fn test_median_price_odd_count() {
5411 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5412 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5413 let t3 = make_tick_with_price(rust_decimal_macros::dec!(110));
5414 assert_eq!(NormalizedTick::median_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(100)));
5415 }
5416
5417 #[test]
5418 fn test_median_price_even_count() {
5419 let t1 = make_tick_with_price(rust_decimal_macros::dec!(90));
5420 let t2 = make_tick_with_price(rust_decimal_macros::dec!(100));
5421 assert_eq!(NormalizedTick::median_price(&[t1, t2]), Some(rust_decimal_macros::dec!(95)));
5423 }
5424
5425 #[test]
5428 fn test_net_volume_zero_for_empty_slice() {
5429 assert_eq!(NormalizedTick::net_volume(&[]), rust_decimal::Decimal::ZERO);
5430 }
5431
5432 #[test]
5433 fn test_net_volume_positive_when_more_buys() {
5434 let buy = NormalizedTick {
5435 side: Some(TradeSide::Buy),
5436 quantity: rust_decimal_macros::dec!(5),
5437 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5))
5438 };
5439 let sell = NormalizedTick {
5440 side: Some(TradeSide::Sell),
5441 quantity: rust_decimal_macros::dec!(3),
5442 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3))
5443 };
5444 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(2));
5445 }
5446
5447 #[test]
5448 fn test_net_volume_negative_when_more_sells() {
5449 let buy = NormalizedTick {
5450 side: Some(TradeSide::Buy),
5451 quantity: rust_decimal_macros::dec!(2),
5452 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2))
5453 };
5454 let sell = NormalizedTick {
5455 side: Some(TradeSide::Sell),
5456 quantity: rust_decimal_macros::dec!(7),
5457 ..make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(7))
5458 };
5459 assert_eq!(NormalizedTick::net_volume(&[buy, sell]), rust_decimal_macros::dec!(-5));
5460 }
5461
5462 #[test]
5465 fn test_average_quantity_none_for_empty_slice() {
5466 assert!(NormalizedTick::average_quantity(&[]).is_none());
5467 }
5468
5469 #[test]
5470 fn test_average_quantity_single_tick() {
5471 let tick = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5472 assert_eq!(NormalizedTick::average_quantity(&[tick]), Some(rust_decimal_macros::dec!(5)));
5473 }
5474
5475 #[test]
5476 fn test_average_quantity_multiple_ticks() {
5477 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5478 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(4));
5479 assert_eq!(NormalizedTick::average_quantity(&[t1, t2]), Some(rust_decimal_macros::dec!(3)));
5481 }
5482
5483 #[test]
5484 fn test_max_quantity_none_for_empty_slice() {
5485 assert!(NormalizedTick::max_quantity(&[]).is_none());
5486 }
5487
5488 #[test]
5489 fn test_max_quantity_returns_largest() {
5490 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5491 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(10));
5492 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5493 assert_eq!(NormalizedTick::max_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(10)));
5494 }
5495
5496 #[test]
5497 fn test_min_quantity_none_for_empty_slice() {
5498 assert!(NormalizedTick::min_quantity(&[]).is_none());
5499 }
5500
5501 #[test]
5502 fn test_min_quantity_returns_smallest() {
5503 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5504 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5505 let t3 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(3));
5506 assert_eq!(NormalizedTick::min_quantity(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(1)));
5507 }
5508
5509 #[test]
5510 fn test_buy_count_zero_for_empty_slice() {
5511 assert_eq!(NormalizedTick::buy_count(&[]), 0);
5512 }
5513
5514 #[test]
5515 fn test_buy_count_counts_only_buys() {
5516 use rust_decimal_macros::dec;
5517 let mut buy = make_tick_pq(dec!(100), dec!(1));
5518 buy.side = Some(TradeSide::Buy);
5519 let mut sell = make_tick_pq(dec!(100), dec!(1));
5520 sell.side = Some(TradeSide::Sell);
5521 let neutral = make_tick_pq(dec!(100), dec!(1));
5522 assert_eq!(NormalizedTick::buy_count(&[buy, sell, neutral]), 1);
5523 }
5524
5525 #[test]
5526 fn test_sell_count_zero_for_empty_slice() {
5527 assert_eq!(NormalizedTick::sell_count(&[]), 0);
5528 }
5529
5530 #[test]
5531 fn test_sell_count_counts_only_sells() {
5532 use rust_decimal_macros::dec;
5533 let mut buy = make_tick_pq(dec!(100), dec!(1));
5534 buy.side = Some(TradeSide::Buy);
5535 let mut sell1 = make_tick_pq(dec!(100), dec!(1));
5536 sell1.side = Some(TradeSide::Sell);
5537 let mut sell2 = make_tick_pq(dec!(100), dec!(1));
5538 sell2.side = Some(TradeSide::Sell);
5539 assert_eq!(NormalizedTick::sell_count(&[buy, sell1, sell2]), 2);
5540 }
5541
5542 #[test]
5543 fn test_price_momentum_none_for_empty_slice() {
5544 assert!(NormalizedTick::price_momentum(&[]).is_none());
5545 }
5546
5547 #[test]
5548 fn test_price_momentum_none_for_single_tick() {
5549 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5550 assert!(NormalizedTick::price_momentum(&[t]).is_none());
5551 }
5552
5553 #[test]
5554 fn test_price_momentum_positive_when_price_rises() {
5555 use rust_decimal_macros::dec;
5556 let t1 = make_tick_pq(dec!(100), dec!(1));
5557 let t2 = make_tick_pq(dec!(110), dec!(1));
5558 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
5559 assert!((mom - 0.1).abs() < 1e-9);
5560 }
5561
5562 #[test]
5563 fn test_price_momentum_negative_when_price_falls() {
5564 use rust_decimal_macros::dec;
5565 let t1 = make_tick_pq(dec!(100), dec!(1));
5566 let t2 = make_tick_pq(dec!(90), dec!(1));
5567 let mom = NormalizedTick::price_momentum(&[t1, t2]).unwrap();
5568 assert!(mom < 0.0);
5569 }
5570
5571 #[test]
5572 fn test_min_price_none_for_empty_slice() {
5573 assert!(NormalizedTick::min_price(&[]).is_none());
5574 }
5575
5576 #[test]
5577 fn test_min_price_returns_lowest() {
5578 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5579 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5580 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5581 assert_eq!(NormalizedTick::min_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(90)));
5582 }
5583
5584 #[test]
5585 fn test_max_price_none_for_empty_slice() {
5586 assert!(NormalizedTick::max_price(&[]).is_none());
5587 }
5588
5589 #[test]
5590 fn test_max_price_returns_highest() {
5591 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5592 let t2 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5593 let t3 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5594 assert_eq!(NormalizedTick::max_price(&[t1, t2, t3]), Some(rust_decimal_macros::dec!(110)));
5595 }
5596
5597 #[test]
5598 fn test_price_std_dev_none_for_empty_slice() {
5599 assert!(NormalizedTick::price_std_dev(&[]).is_none());
5600 }
5601
5602 #[test]
5603 fn test_price_std_dev_none_for_single_tick() {
5604 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5605 assert!(NormalizedTick::price_std_dev(&[t]).is_none());
5606 }
5607
5608 #[test]
5609 fn test_price_std_dev_two_equal_prices_is_zero() {
5610 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5611 let t2 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5612 assert_eq!(NormalizedTick::price_std_dev(&[t1, t2]), Some(0.0));
5613 }
5614
5615 #[test]
5616 fn test_price_std_dev_positive_for_varying_prices() {
5617 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5618 let t2 = make_tick_pq(rust_decimal_macros::dec!(110), rust_decimal_macros::dec!(1));
5619 let t3 = make_tick_pq(rust_decimal_macros::dec!(90), rust_decimal_macros::dec!(1));
5620 let std = NormalizedTick::price_std_dev(&[t1, t2, t3]).unwrap();
5621 assert!(std > 0.0);
5622 }
5623
5624 #[test]
5625 fn test_buy_sell_ratio_none_for_empty_slice() {
5626 assert!(NormalizedTick::buy_sell_ratio(&[]).is_none());
5627 }
5628
5629 #[test]
5630 fn test_buy_sell_ratio_none_when_no_sells() {
5631 let mut t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5632 t.side = Some(TradeSide::Buy);
5633 assert!(NormalizedTick::buy_sell_ratio(&[t]).is_none());
5634 }
5635
5636 #[test]
5637 fn test_buy_sell_ratio_two_to_one() {
5638 use rust_decimal_macros::dec;
5639 let mut buy1 = make_tick_pq(dec!(100), dec!(2));
5640 buy1.side = Some(TradeSide::Buy);
5641 let mut buy2 = make_tick_pq(dec!(100), dec!(2));
5642 buy2.side = Some(TradeSide::Buy);
5643 let mut sell = make_tick_pq(dec!(100), dec!(2));
5644 sell.side = Some(TradeSide::Sell);
5645 let ratio = NormalizedTick::buy_sell_ratio(&[buy1, buy2, sell]).unwrap();
5646 assert!((ratio - 2.0).abs() < 1e-9);
5647 }
5648
5649 #[test]
5650 fn test_largest_trade_none_for_empty_slice() {
5651 assert!(NormalizedTick::largest_trade(&[]).is_none());
5652 }
5653
5654 #[test]
5655 fn test_largest_trade_returns_max_quantity_tick() {
5656 let t1 = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(2));
5657 let t2 = make_tick_pq(rust_decimal_macros::dec!(200), rust_decimal_macros::dec!(10));
5658 let t3 = make_tick_pq(rust_decimal_macros::dec!(150), rust_decimal_macros::dec!(5));
5659 let ticks = [t1, t2, t3];
5660 let largest = NormalizedTick::largest_trade(&ticks).unwrap();
5661 assert_eq!(largest.quantity, rust_decimal_macros::dec!(10));
5662 }
5663
5664 #[test]
5665 fn test_large_trade_count_zero_for_empty_slice() {
5666 assert_eq!(NormalizedTick::large_trade_count(&[], rust_decimal_macros::dec!(1)), 0);
5667 }
5668
5669 #[test]
5670 fn test_large_trade_count_counts_trades_above_threshold() {
5671 use rust_decimal_macros::dec;
5672 let t1 = make_tick_pq(dec!(100), dec!(0.5));
5673 let t2 = make_tick_pq(dec!(100), dec!(5));
5674 let t3 = make_tick_pq(dec!(100), dec!(10));
5675 assert_eq!(NormalizedTick::large_trade_count(&[t1, t2, t3], dec!(1)), 2);
5676 }
5677
5678 #[test]
5679 fn test_large_trade_count_strict_greater_than() {
5680 use rust_decimal_macros::dec;
5681 let t = make_tick_pq(dec!(100), dec!(1));
5682 assert_eq!(NormalizedTick::large_trade_count(&[t], dec!(1)), 0);
5684 }
5685
5686 #[test]
5687 fn test_price_iqr_none_for_small_slice() {
5688 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5689 assert!(NormalizedTick::price_iqr(&[t.clone(), t.clone(), t]).is_none());
5690 }
5691
5692 #[test]
5693 fn test_price_iqr_positive_for_varied_prices() {
5694 use rust_decimal_macros::dec;
5695 let ticks: Vec<_> = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)]
5696 .iter()
5697 .map(|&p| make_tick_pq(p, dec!(1)))
5698 .collect();
5699 let iqr = NormalizedTick::price_iqr(&ticks).unwrap();
5700 assert!(iqr > dec!(0));
5701 }
5702
5703 #[test]
5704 fn test_fraction_buy_none_for_empty_slice() {
5705 assert!(NormalizedTick::fraction_buy(&[]).is_none());
5706 }
5707
5708 #[test]
5709 fn test_fraction_buy_zero_when_no_buys() {
5710 use rust_decimal_macros::dec;
5711 let mut t = make_tick_pq(dec!(100), dec!(1));
5712 t.side = Some(TradeSide::Sell);
5713 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(0.0));
5714 }
5715
5716 #[test]
5717 fn test_fraction_buy_one_when_all_buys() {
5718 use rust_decimal_macros::dec;
5719 let mut t = make_tick_pq(dec!(100), dec!(1));
5720 t.side = Some(TradeSide::Buy);
5721 assert_eq!(NormalizedTick::fraction_buy(&[t]), Some(1.0));
5722 }
5723
5724 #[test]
5725 fn test_fraction_buy_half_for_equal_mix() {
5726 use rust_decimal_macros::dec;
5727 let mut buy = make_tick_pq(dec!(100), dec!(1));
5728 buy.side = Some(TradeSide::Buy);
5729 let mut sell = make_tick_pq(dec!(100), dec!(1));
5730 sell.side = Some(TradeSide::Sell);
5731 let frac = NormalizedTick::fraction_buy(&[buy, sell]).unwrap();
5732 assert!((frac - 0.5).abs() < 1e-9);
5733 }
5734
5735 #[test]
5736 fn test_std_quantity_none_for_empty_slice() {
5737 assert!(NormalizedTick::std_quantity(&[]).is_none());
5738 }
5739
5740 #[test]
5741 fn test_std_quantity_none_for_single_tick() {
5742 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(5));
5743 assert!(NormalizedTick::std_quantity(&[t]).is_none());
5744 }
5745
5746 #[test]
5747 fn test_std_quantity_zero_for_identical_quantities() {
5748 use rust_decimal_macros::dec;
5749 let t1 = make_tick_pq(dec!(100), dec!(5));
5750 let t2 = make_tick_pq(dec!(100), dec!(5));
5751 assert_eq!(NormalizedTick::std_quantity(&[t1, t2]), Some(0.0));
5752 }
5753
5754 #[test]
5755 fn test_std_quantity_positive_for_varied_quantities() {
5756 use rust_decimal_macros::dec;
5757 let t1 = make_tick_pq(dec!(100), dec!(1));
5758 let t2 = make_tick_pq(dec!(100), dec!(10));
5759 let std = NormalizedTick::std_quantity(&[t1, t2]).unwrap();
5760 assert!(std > 0.0);
5761 }
5762
5763 #[test]
5764 fn test_buy_pressure_none_for_empty_slice() {
5765 assert!(NormalizedTick::buy_pressure(&[]).is_none());
5766 }
5767
5768 #[test]
5769 fn test_buy_pressure_none_for_unsided_ticks() {
5770 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5771 assert!(NormalizedTick::buy_pressure(&[t]).is_none());
5772 }
5773
5774 #[test]
5775 fn test_buy_pressure_one_for_all_buys() {
5776 use rust_decimal_macros::dec;
5777 let mut t = make_tick_pq(dec!(100), dec!(1));
5778 t.side = Some(TradeSide::Buy);
5779 let bp = NormalizedTick::buy_pressure(&[t]).unwrap();
5780 assert!((bp - 1.0).abs() < 1e-9);
5781 }
5782
5783 #[test]
5784 fn test_buy_pressure_half_for_equal_volume() {
5785 use rust_decimal_macros::dec;
5786 let mut buy = make_tick_pq(dec!(100), dec!(5));
5787 buy.side = Some(TradeSide::Buy);
5788 let mut sell = make_tick_pq(dec!(100), dec!(5));
5789 sell.side = Some(TradeSide::Sell);
5790 let bp = NormalizedTick::buy_pressure(&[buy, sell]).unwrap();
5791 assert!((bp - 0.5).abs() < 1e-9);
5792 }
5793
5794 #[test]
5795 fn test_average_notional_none_for_empty_slice() {
5796 assert!(NormalizedTick::average_notional(&[]).is_none());
5797 }
5798
5799 #[test]
5800 fn test_average_notional_single_tick() {
5801 use rust_decimal_macros::dec;
5802 let t = make_tick_pq(dec!(100), dec!(2));
5803 assert_eq!(NormalizedTick::average_notional(&[t]), Some(dec!(200)));
5804 }
5805
5806 #[test]
5807 fn test_average_notional_multiple_ticks() {
5808 use rust_decimal_macros::dec;
5809 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)));
5813 }
5814
5815 #[test]
5816 fn test_count_neutral_zero_for_empty_slice() {
5817 assert_eq!(NormalizedTick::count_neutral(&[]), 0);
5818 }
5819
5820 #[test]
5821 fn test_count_neutral_counts_sideless_ticks() {
5822 use rust_decimal_macros::dec;
5823 let neutral = make_tick_pq(dec!(100), dec!(1)); let mut buy = make_tick_pq(dec!(100), dec!(1));
5825 buy.side = Some(TradeSide::Buy);
5826 assert_eq!(NormalizedTick::count_neutral(&[neutral, buy]), 1);
5827 }
5828
5829 #[test]
5830 fn test_recent_returns_all_when_n_exceeds_len() {
5831 use rust_decimal_macros::dec;
5832 let ticks = vec![
5833 make_tick_pq(dec!(100), dec!(1)),
5834 make_tick_pq(dec!(110), dec!(1)),
5835 ];
5836 assert_eq!(NormalizedTick::recent(&ticks, 10).len(), 2);
5837 }
5838
5839 #[test]
5840 fn test_recent_returns_last_n() {
5841 use rust_decimal_macros::dec;
5842 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120), dec!(130)]
5843 .iter()
5844 .map(|&p| make_tick_pq(p, dec!(1)))
5845 .collect();
5846 let recent = NormalizedTick::recent(&ticks, 2);
5847 assert_eq!(recent.len(), 2);
5848 assert_eq!(recent[0].price, dec!(120));
5849 assert_eq!(recent[1].price, dec!(130));
5850 }
5851
5852 #[test]
5853 fn test_price_linear_slope_none_for_single_tick() {
5854 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5855 assert!(NormalizedTick::price_linear_slope(&[t]).is_none());
5856 }
5857
5858 #[test]
5859 fn test_price_linear_slope_positive_for_rising_prices() {
5860 use rust_decimal_macros::dec;
5861 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(120)]
5862 .iter()
5863 .map(|&p| make_tick_pq(p, dec!(1)))
5864 .collect();
5865 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
5866 assert!(slope > 0.0);
5867 }
5868
5869 #[test]
5870 fn test_price_linear_slope_negative_for_falling_prices() {
5871 use rust_decimal_macros::dec;
5872 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(100)]
5873 .iter()
5874 .map(|&p| make_tick_pq(p, dec!(1)))
5875 .collect();
5876 let slope = NormalizedTick::price_linear_slope(&ticks).unwrap();
5877 assert!(slope < 0.0);
5878 }
5879
5880 #[test]
5881 fn test_notional_std_dev_none_for_single_tick() {
5882 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
5883 assert!(NormalizedTick::notional_std_dev(&[t]).is_none());
5884 }
5885
5886 #[test]
5887 fn test_notional_std_dev_zero_for_identical_notionals() {
5888 use rust_decimal_macros::dec;
5889 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));
5892 }
5893
5894 #[test]
5895 fn test_notional_std_dev_positive_for_varied_notionals() {
5896 use rust_decimal_macros::dec;
5897 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();
5900 assert!(std > 0.0);
5901 }
5902
5903 #[test]
5904 fn test_monotone_up_true_for_empty_slice() {
5905 assert!(NormalizedTick::monotone_up(&[]));
5906 }
5907
5908 #[test]
5909 fn test_monotone_up_true_for_non_decreasing_prices() {
5910 use rust_decimal_macros::dec;
5911 let ticks: Vec<_> = [dec!(100), dec!(100), dec!(110), dec!(120)]
5912 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5913 assert!(NormalizedTick::monotone_up(&ticks));
5914 }
5915
5916 #[test]
5917 fn test_monotone_up_false_for_any_decrease() {
5918 use rust_decimal_macros::dec;
5919 let ticks: Vec<_> = [dec!(100), dec!(110), dec!(105)]
5920 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5921 assert!(!NormalizedTick::monotone_up(&ticks));
5922 }
5923
5924 #[test]
5925 fn test_monotone_down_true_for_non_increasing_prices() {
5926 use rust_decimal_macros::dec;
5927 let ticks: Vec<_> = [dec!(120), dec!(110), dec!(110), dec!(100)]
5928 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5929 assert!(NormalizedTick::monotone_down(&ticks));
5930 }
5931
5932 #[test]
5933 fn test_monotone_down_false_for_any_increase() {
5934 use rust_decimal_macros::dec;
5935 let ticks: Vec<_> = [dec!(100), dec!(90), dec!(95)]
5936 .iter().map(|&p| make_tick_pq(p, dec!(1))).collect();
5937 assert!(!NormalizedTick::monotone_down(&ticks));
5938 }
5939
5940 #[test]
5941 fn test_volume_at_price_zero_for_empty_slice() {
5942 assert_eq!(NormalizedTick::volume_at_price(&[], rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
5943 }
5944
5945 #[test]
5946 fn test_volume_at_price_sums_matching_ticks() {
5947 use rust_decimal_macros::dec;
5948 let t1 = make_tick_pq(dec!(100), dec!(2));
5949 let t2 = make_tick_pq(dec!(100), dec!(3));
5950 let t3 = make_tick_pq(dec!(110), dec!(5));
5951 assert_eq!(NormalizedTick::volume_at_price(&[t1, t2, t3], dec!(100)), dec!(5));
5952 }
5953
5954 #[test]
5955 fn test_last_price_none_for_empty_slice() {
5956 assert!(NormalizedTick::last_price(&[]).is_none());
5957 }
5958
5959 #[test]
5960 fn test_last_price_returns_last_tick_price() {
5961 use rust_decimal_macros::dec;
5962 let t1 = make_tick_pq(dec!(100), dec!(1));
5963 let t2 = make_tick_pq(dec!(110), dec!(1));
5964 assert_eq!(NormalizedTick::last_price(&[t1, t2]), Some(dec!(110)));
5965 }
5966
5967 #[test]
5968 fn test_longest_buy_streak_zero_for_empty() {
5969 assert_eq!(NormalizedTick::longest_buy_streak(&[]), 0);
5970 }
5971
5972 #[test]
5973 fn test_longest_buy_streak_counts_consecutive_buys() {
5974 use rust_decimal_macros::dec;
5975 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
5976 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
5977 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
5978 let mut b3 = make_tick_pq(dec!(100), dec!(1)); b3.side = Some(TradeSide::Buy);
5979 assert_eq!(NormalizedTick::longest_buy_streak(&[b1, b2, s, b3]), 2);
5981 }
5982
5983 #[test]
5984 fn test_longest_sell_streak_zero_for_no_sells() {
5985 use rust_decimal_macros::dec;
5986 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
5987 assert_eq!(NormalizedTick::longest_sell_streak(&[b]), 0);
5988 }
5989
5990 #[test]
5991 fn test_longest_sell_streak_correct() {
5992 use rust_decimal_macros::dec;
5993 let mut b = make_tick_pq(dec!(100), dec!(1)); b.side = Some(TradeSide::Buy);
5994 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
5995 let mut s2 = make_tick_pq(dec!(100), dec!(1)); s2.side = Some(TradeSide::Sell);
5996 let mut s3 = make_tick_pq(dec!(100), dec!(1)); s3.side = Some(TradeSide::Sell);
5997 assert_eq!(NormalizedTick::longest_sell_streak(&[b, s1, s2, s3]), 3);
5998 }
5999
6000 #[test]
6001 fn test_price_at_max_volume_none_for_empty() {
6002 assert!(NormalizedTick::price_at_max_volume(&[]).is_none());
6003 }
6004
6005 #[test]
6006 fn test_price_at_max_volume_returns_dominant_price() {
6007 use rust_decimal_macros::dec;
6008 let t1 = make_tick_pq(dec!(100), dec!(1));
6009 let t2 = make_tick_pq(dec!(200), dec!(5));
6010 let t3 = make_tick_pq(dec!(200), dec!(3));
6011 assert_eq!(NormalizedTick::price_at_max_volume(&[t1, t2, t3]), Some(dec!(200)));
6013 }
6014
6015 #[test]
6016 fn test_recent_volume_zero_for_empty_slice() {
6017 assert_eq!(NormalizedTick::recent_volume(&[], 5), rust_decimal_macros::dec!(0));
6018 }
6019
6020 #[test]
6021 fn test_recent_volume_sums_last_n_ticks() {
6022 use rust_decimal_macros::dec;
6023 let ticks: Vec<_> = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)]
6024 .iter().map(|&q| make_tick_pq(dec!(100), q)).collect();
6025 assert_eq!(NormalizedTick::recent_volume(&ticks, 3), dec!(12));
6027 }
6028
6029 #[test]
6032 fn test_first_price_none_for_empty_slice() {
6033 assert!(NormalizedTick::first_price(&[]).is_none());
6034 }
6035
6036 #[test]
6037 fn test_first_price_returns_first_tick_price() {
6038 use rust_decimal_macros::dec;
6039 let ticks = vec![make_tick_pq(dec!(50), dec!(1)), make_tick_pq(dec!(60), dec!(1))];
6040 assert_eq!(NormalizedTick::first_price(&ticks), Some(dec!(50)));
6041 }
6042
6043 #[test]
6046 fn test_price_return_pct_none_for_single_tick() {
6047 use rust_decimal_macros::dec;
6048 assert!(NormalizedTick::price_return_pct(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6049 }
6050
6051 #[test]
6052 fn test_price_return_pct_positive_for_rising_price() {
6053 use rust_decimal_macros::dec;
6054 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(110), dec!(1))];
6055 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
6056 assert!((pct - 0.1).abs() < 1e-9);
6057 }
6058
6059 #[test]
6060 fn test_price_return_pct_negative_for_falling_price() {
6061 use rust_decimal_macros::dec;
6062 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(90), dec!(1))];
6063 let pct = NormalizedTick::price_return_pct(&ticks).unwrap();
6064 assert!((pct - (-0.1)).abs() < 1e-9);
6065 }
6066
6067 #[test]
6070 fn test_volume_above_price_zero_for_empty_slice() {
6071 use rust_decimal_macros::dec;
6072 assert_eq!(NormalizedTick::volume_above_price(&[], dec!(100)), dec!(0));
6073 }
6074
6075 #[test]
6076 fn test_volume_above_price_sums_above_threshold() {
6077 use rust_decimal_macros::dec;
6078 let ticks = vec![
6079 make_tick_pq(dec!(90), dec!(5)),
6080 make_tick_pq(dec!(100), dec!(10)),
6081 make_tick_pq(dec!(110), dec!(3)),
6082 ];
6083 assert_eq!(NormalizedTick::volume_above_price(&ticks, dec!(100)), dec!(3));
6085 }
6086
6087 #[test]
6088 fn test_volume_below_price_sums_below_threshold() {
6089 use rust_decimal_macros::dec;
6090 let ticks = vec![
6091 make_tick_pq(dec!(90), dec!(5)),
6092 make_tick_pq(dec!(100), dec!(10)),
6093 make_tick_pq(dec!(110), dec!(3)),
6094 ];
6095 assert_eq!(NormalizedTick::volume_below_price(&ticks, dec!(100)), dec!(5));
6097 }
6098
6099 #[test]
6102 fn test_qwap_none_for_empty_slice() {
6103 assert!(NormalizedTick::quantity_weighted_avg_price(&[]).is_none());
6104 }
6105
6106 #[test]
6107 fn test_qwap_correct_for_equal_quantities() {
6108 use rust_decimal_macros::dec;
6109 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(1))];
6111 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(150)));
6112 }
6113
6114 #[test]
6115 fn test_qwap_weighted_towards_higher_volume() {
6116 use rust_decimal_macros::dec;
6117 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(3))];
6119 assert_eq!(NormalizedTick::quantity_weighted_avg_price(&ticks), Some(dec!(175)));
6120 }
6121
6122 #[test]
6125 fn test_tick_count_above_price_zero_for_empty_slice() {
6126 use rust_decimal_macros::dec;
6127 assert_eq!(NormalizedTick::tick_count_above_price(&[], dec!(100)), 0);
6128 }
6129
6130 #[test]
6131 fn test_tick_count_above_price_correct() {
6132 use rust_decimal_macros::dec;
6133 let ticks = vec![
6134 make_tick_pq(dec!(90), dec!(1)),
6135 make_tick_pq(dec!(100), dec!(1)),
6136 make_tick_pq(dec!(110), dec!(1)),
6137 make_tick_pq(dec!(120), dec!(1)),
6138 ];
6139 assert_eq!(NormalizedTick::tick_count_above_price(&ticks, dec!(100)), 2);
6140 }
6141
6142 #[test]
6143 fn test_tick_count_below_price_correct() {
6144 use rust_decimal_macros::dec;
6145 let ticks = vec![
6146 make_tick_pq(dec!(90), dec!(1)),
6147 make_tick_pq(dec!(100), dec!(1)),
6148 make_tick_pq(dec!(110), dec!(1)),
6149 ];
6150 assert_eq!(NormalizedTick::tick_count_below_price(&ticks, dec!(100)), 1);
6151 }
6152
6153 #[test]
6156 fn test_price_at_percentile_none_for_empty_slice() {
6157 use rust_decimal_macros::dec;
6158 assert!(NormalizedTick::price_at_percentile(&[], 0.5).is_none());
6159 }
6160
6161 #[test]
6162 fn test_price_at_percentile_none_for_out_of_range() {
6163 use rust_decimal_macros::dec;
6164 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
6165 assert!(NormalizedTick::price_at_percentile(&ticks, 1.5).is_none());
6166 }
6167
6168 #[test]
6169 fn test_price_at_percentile_median_for_sorted_prices() {
6170 use rust_decimal_macros::dec;
6171 let ticks = vec![
6172 make_tick_pq(dec!(10), dec!(1)),
6173 make_tick_pq(dec!(20), dec!(1)),
6174 make_tick_pq(dec!(30), dec!(1)),
6175 make_tick_pq(dec!(40), dec!(1)),
6176 make_tick_pq(dec!(50), dec!(1)),
6177 ];
6178 assert_eq!(NormalizedTick::price_at_percentile(&ticks, 0.5), Some(dec!(30)));
6180 }
6181
6182 #[test]
6185 fn test_unique_price_count_zero_for_empty() {
6186 assert_eq!(NormalizedTick::unique_price_count(&[]), 0);
6187 }
6188
6189 #[test]
6190 fn test_unique_price_count_counts_distinct_prices() {
6191 use rust_decimal_macros::dec;
6192 let ticks = vec![
6193 make_tick_pq(dec!(100), dec!(1)),
6194 make_tick_pq(dec!(100), dec!(2)),
6195 make_tick_pq(dec!(110), dec!(1)),
6196 make_tick_pq(dec!(120), dec!(1)),
6197 ];
6198 assert_eq!(NormalizedTick::unique_price_count(&ticks), 3);
6199 }
6200
6201 #[test]
6204 fn test_sell_volume_zero_for_empty() {
6205 assert_eq!(NormalizedTick::sell_volume(&[]), rust_decimal_macros::dec!(0));
6206 }
6207
6208 #[test]
6209 fn test_sell_volume_sums_sell_side_only() {
6210 use rust_decimal_macros::dec;
6211 let mut buy_tick = make_tick_pq(dec!(100), dec!(5));
6212 buy_tick.side = Some(TradeSide::Buy);
6213 let mut sell_tick = make_tick_pq(dec!(100), dec!(3));
6214 sell_tick.side = Some(TradeSide::Sell);
6215 let no_side_tick = make_tick_pq(dec!(100), dec!(10));
6216 let ticks = [buy_tick, sell_tick, no_side_tick];
6217 assert_eq!(NormalizedTick::sell_volume(&ticks), dec!(3));
6218 assert_eq!(NormalizedTick::buy_volume(&ticks), dec!(5));
6219 }
6220
6221 #[test]
6224 fn test_avg_inter_tick_spread_none_for_single_tick() {
6225 use rust_decimal_macros::dec;
6226 assert!(NormalizedTick::avg_inter_tick_spread(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
6227 }
6228
6229 #[test]
6230 fn test_avg_inter_tick_spread_correct_for_uniform_moves() {
6231 use rust_decimal_macros::dec;
6232 let ticks = vec![
6234 make_tick_pq(dec!(100), dec!(1)),
6235 make_tick_pq(dec!(102), dec!(1)),
6236 make_tick_pq(dec!(104), dec!(1)),
6237 ];
6238 let spread = NormalizedTick::avg_inter_tick_spread(&ticks).unwrap();
6239 assert!((spread - 2.0).abs() < 1e-9);
6240 }
6241
6242 #[test]
6245 fn test_price_range_none_for_empty() {
6246 assert!(NormalizedTick::price_range(&[]).is_none());
6247 }
6248
6249 #[test]
6250 fn test_price_range_correct() {
6251 use rust_decimal_macros::dec;
6252 let ticks = vec![
6253 make_tick_pq(dec!(90), dec!(1)),
6254 make_tick_pq(dec!(110), dec!(1)),
6255 make_tick_pq(dec!(100), dec!(1)),
6256 ];
6257 assert_eq!(NormalizedTick::price_range(&ticks), Some(dec!(20)));
6258 }
6259
6260 #[test]
6263 fn test_median_price_none_for_empty() {
6264 assert!(NormalizedTick::median_price(&[]).is_none());
6265 }
6266
6267 #[test]
6268 fn test_median_price_returns_middle_value() {
6269 use rust_decimal_macros::dec;
6270 let ticks = vec![
6271 make_tick_pq(dec!(10), dec!(1)),
6272 make_tick_pq(dec!(30), dec!(1)),
6273 make_tick_pq(dec!(20), dec!(1)),
6274 ];
6275 assert_eq!(NormalizedTick::median_price(&ticks), Some(dec!(20)));
6277 }
6278
6279 #[test]
6282 fn test_largest_sell_none_for_no_sell_ticks() {
6283 use rust_decimal_macros::dec;
6284 let mut t = make_tick_pq(dec!(100), dec!(5));
6285 t.side = Some(TradeSide::Buy);
6286 assert!(NormalizedTick::largest_sell(&[t]).is_none());
6287 }
6288
6289 #[test]
6290 fn test_largest_sell_returns_max_sell_qty() {
6291 use rust_decimal_macros::dec;
6292 let mut t1 = make_tick_pq(dec!(100), dec!(3));
6293 t1.side = Some(TradeSide::Sell);
6294 let mut t2 = make_tick_pq(dec!(100), dec!(7));
6295 t2.side = Some(TradeSide::Sell);
6296 assert_eq!(NormalizedTick::largest_sell(&[t1, t2]), Some(dec!(7)));
6297 }
6298
6299 #[test]
6300 fn test_largest_buy_returns_max_buy_qty() {
6301 use rust_decimal_macros::dec;
6302 let mut t1 = make_tick_pq(dec!(100), dec!(2));
6303 t1.side = Some(TradeSide::Buy);
6304 let mut t2 = make_tick_pq(dec!(100), dec!(9));
6305 t2.side = Some(TradeSide::Buy);
6306 assert_eq!(NormalizedTick::largest_buy(&[t1, t2]), Some(dec!(9)));
6307 }
6308
6309 #[test]
6312 fn test_trade_count_zero_for_empty() {
6313 assert_eq!(NormalizedTick::trade_count(&[]), 0);
6314 }
6315
6316 #[test]
6317 fn test_trade_count_matches_slice_length() {
6318 use rust_decimal_macros::dec;
6319 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
6320 assert_eq!(NormalizedTick::trade_count(&ticks), 2);
6321 }
6322
6323 #[test]
6326 fn test_price_acceleration_none_for_fewer_than_3() {
6327 use rust_decimal_macros::dec;
6328 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6329 assert!(NormalizedTick::price_acceleration(&ticks).is_none());
6330 }
6331
6332 #[test]
6333 fn test_price_acceleration_zero_for_constant_velocity() {
6334 use rust_decimal_macros::dec;
6335 let ticks = vec![
6337 make_tick_pq(dec!(100), dec!(1)),
6338 make_tick_pq(dec!(102), dec!(1)),
6339 make_tick_pq(dec!(104), dec!(1)),
6340 ];
6341 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
6342 assert!((acc - 0.0).abs() < 1e-9);
6343 }
6344
6345 #[test]
6346 fn test_price_acceleration_positive_when_speeding_up() {
6347 use rust_decimal_macros::dec;
6348 let ticks = vec![
6350 make_tick_pq(dec!(100), dec!(1)),
6351 make_tick_pq(dec!(101), dec!(1)),
6352 make_tick_pq(dec!(103), dec!(1)),
6353 ];
6354 let acc = NormalizedTick::price_acceleration(&ticks).unwrap();
6355 assert!((acc - 1.0).abs() < 1e-9);
6356 }
6357
6358 #[test]
6361 fn test_buy_sell_diff_zero_for_empty() {
6362 assert_eq!(NormalizedTick::buy_sell_diff(&[]), rust_decimal_macros::dec!(0));
6363 }
6364
6365 #[test]
6366 fn test_buy_sell_diff_positive_for_net_buying() {
6367 use rust_decimal_macros::dec;
6368 let mut t1 = make_tick_pq(dec!(100), dec!(10));
6369 t1.side = Some(TradeSide::Buy);
6370 let mut t2 = make_tick_pq(dec!(100), dec!(3));
6371 t2.side = Some(TradeSide::Sell);
6372 assert_eq!(NormalizedTick::buy_sell_diff(&[t1, t2]), dec!(7));
6373 }
6374
6375 #[test]
6378 fn test_is_aggressive_buy_true_when_exceeds_avg() {
6379 use rust_decimal_macros::dec;
6380 let mut t = make_tick_pq(dec!(100), dec!(15));
6381 t.side = Some(TradeSide::Buy);
6382 assert!(NormalizedTick::is_aggressive_buy(&t, dec!(10)));
6383 }
6384
6385 #[test]
6386 fn test_is_aggressive_buy_false_when_not_buy_side() {
6387 use rust_decimal_macros::dec;
6388 let mut t = make_tick_pq(dec!(100), dec!(15));
6389 t.side = Some(TradeSide::Sell);
6390 assert!(!NormalizedTick::is_aggressive_buy(&t, dec!(10)));
6391 }
6392
6393 #[test]
6394 fn test_is_aggressive_sell_true_when_exceeds_avg() {
6395 use rust_decimal_macros::dec;
6396 let mut t = make_tick_pq(dec!(100), dec!(20));
6397 t.side = Some(TradeSide::Sell);
6398 assert!(NormalizedTick::is_aggressive_sell(&t, dec!(10)));
6399 }
6400
6401 #[test]
6404 fn test_notional_volume_zero_for_empty() {
6405 assert_eq!(NormalizedTick::notional_volume(&[]), rust_decimal_macros::dec!(0));
6406 }
6407
6408 #[test]
6409 fn test_notional_volume_correct() {
6410 use rust_decimal_macros::dec;
6411 let ticks = vec![
6412 make_tick_pq(dec!(100), dec!(2)), make_tick_pq(dec!(50), dec!(4)), ];
6415 assert_eq!(NormalizedTick::notional_volume(&ticks), dec!(400));
6416 }
6417
6418 #[test]
6421 fn test_weighted_side_score_none_for_empty() {
6422 assert!(NormalizedTick::weighted_side_score(&[]).is_none());
6423 }
6424
6425 #[test]
6426 fn test_weighted_side_score_correct_for_all_buys() {
6427 use rust_decimal_macros::dec;
6428 let mut t = make_tick_pq(dec!(100), dec!(10));
6429 t.side = Some(TradeSide::Buy);
6430 let score = NormalizedTick::weighted_side_score(&[t]).unwrap();
6432 assert!((score - 1.0).abs() < 1e-9);
6433 }
6434
6435 #[test]
6438 fn test_time_span_none_for_single_tick() {
6439 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
6440 assert!(NormalizedTick::time_span_ms(&[t]).is_none());
6441 }
6442
6443 #[test]
6444 fn test_time_span_correct_for_two_ticks() {
6445 use rust_decimal_macros::dec;
6446 let mut t1 = make_tick_pq(dec!(100), dec!(1));
6447 t1.received_at_ms = 1000;
6448 let mut t2 = make_tick_pq(dec!(101), dec!(1));
6449 t2.received_at_ms = 5000;
6450 assert_eq!(NormalizedTick::time_span_ms(&[t1, t2]), Some(4000));
6451 }
6452
6453 #[test]
6456 fn test_price_above_vwap_count_none_for_empty() {
6457 assert!(NormalizedTick::price_above_vwap_count(&[]).is_none());
6458 }
6459
6460 #[test]
6461 fn test_price_above_vwap_count_correct() {
6462 use rust_decimal_macros::dec;
6463 let ticks = vec![
6465 make_tick_pq(dec!(90), dec!(1)),
6466 make_tick_pq(dec!(100), dec!(1)),
6467 make_tick_pq(dec!(110), dec!(1)),
6468 ];
6469 assert_eq!(NormalizedTick::price_above_vwap_count(&ticks), Some(1));
6470 }
6471
6472 #[test]
6475 fn test_avg_trade_size_none_for_empty() {
6476 assert!(NormalizedTick::avg_trade_size(&[]).is_none());
6477 }
6478
6479 #[test]
6480 fn test_avg_trade_size_correct() {
6481 use rust_decimal_macros::dec;
6482 let ticks = vec![
6483 make_tick_pq(dec!(100), dec!(2)),
6484 make_tick_pq(dec!(101), dec!(4)),
6485 ];
6486 assert_eq!(NormalizedTick::avg_trade_size(&ticks), Some(dec!(3)));
6487 }
6488
6489 #[test]
6492 fn test_volume_concentration_none_for_empty() {
6493 assert!(NormalizedTick::volume_concentration(&[]).is_none());
6494 }
6495
6496 #[test]
6497 fn test_volume_concentration_is_one_for_single_tick() {
6498 use rust_decimal_macros::dec;
6499 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
6500 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
6501 assert!((c - 1.0).abs() < 1e-9);
6502 }
6503
6504 #[test]
6505 fn test_volume_concentration_in_range() {
6506 use rust_decimal_macros::dec;
6507 let ticks = vec![
6508 make_tick_pq(dec!(100), dec!(1)),
6509 make_tick_pq(dec!(101), dec!(1)),
6510 make_tick_pq(dec!(102), dec!(1)),
6511 make_tick_pq(dec!(103), dec!(10)),
6512 ];
6513 let c = NormalizedTick::volume_concentration(&ticks).unwrap();
6514 assert!(c > 0.0 && c <= 1.0, "expected value in (0,1], got {}", c);
6515 }
6516
6517 #[test]
6520 fn test_trade_imbalance_score_none_for_empty() {
6521 assert!(NormalizedTick::trade_imbalance_score(&[]).is_none());
6522 }
6523
6524 #[test]
6525 fn test_trade_imbalance_score_positive_for_all_buys() {
6526 use rust_decimal_macros::dec;
6527 let mut t = make_tick_pq(dec!(100), dec!(1));
6528 t.side = Some(TradeSide::Buy);
6529 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
6530 assert!(score > 0.0);
6531 }
6532
6533 #[test]
6534 fn test_trade_imbalance_score_negative_for_all_sells() {
6535 use rust_decimal_macros::dec;
6536 let mut t = make_tick_pq(dec!(100), dec!(1));
6537 t.side = Some(TradeSide::Sell);
6538 let score = NormalizedTick::trade_imbalance_score(&[t]).unwrap();
6539 assert!(score < 0.0);
6540 }
6541
6542 #[test]
6545 fn test_price_entropy_none_for_empty() {
6546 assert!(NormalizedTick::price_entropy(&[]).is_none());
6547 }
6548
6549 #[test]
6550 fn test_price_entropy_zero_for_single_price() {
6551 use rust_decimal_macros::dec;
6552 let ticks = vec![
6553 make_tick_pq(dec!(100), dec!(1)),
6554 make_tick_pq(dec!(100), dec!(2)),
6555 ];
6556 let e = NormalizedTick::price_entropy(&ticks).unwrap();
6557 assert!((e - 0.0).abs() < 1e-9, "identical prices should have zero entropy, got {}", e);
6558 }
6559
6560 #[test]
6561 fn test_price_entropy_positive_for_varied_prices() {
6562 use rust_decimal_macros::dec;
6563 let ticks = vec![
6564 make_tick_pq(dec!(100), dec!(1)),
6565 make_tick_pq(dec!(101), dec!(1)),
6566 make_tick_pq(dec!(102), dec!(1)),
6567 ];
6568 let e = NormalizedTick::price_entropy(&ticks).unwrap();
6569 assert!(e > 0.0, "varied prices should have positive entropy, got {}", e);
6570 }
6571
6572 #[test]
6575 fn test_buy_avg_price_none_for_no_buys() {
6576 use rust_decimal_macros::dec;
6577 let mut t = make_tick_pq(dec!(100), dec!(1));
6578 t.side = Some(TradeSide::Sell);
6579 assert!(NormalizedTick::buy_avg_price(&[t]).is_none());
6580 }
6581
6582 #[test]
6583 fn test_buy_avg_price_correct() {
6584 use rust_decimal_macros::dec;
6585 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.side = Some(TradeSide::Buy);
6586 let mut t2 = make_tick_pq(dec!(110), dec!(1)); t2.side = Some(TradeSide::Buy);
6587 assert_eq!(NormalizedTick::buy_avg_price(&[t1, t2]), Some(dec!(105)));
6588 }
6589
6590 #[test]
6591 fn test_sell_avg_price_none_for_no_sells() {
6592 use rust_decimal_macros::dec;
6593 let mut t = make_tick_pq(dec!(100), dec!(1));
6594 t.side = Some(TradeSide::Buy);
6595 assert!(NormalizedTick::sell_avg_price(&[t]).is_none());
6596 }
6597
6598 #[test]
6599 fn test_sell_avg_price_correct() {
6600 use rust_decimal_macros::dec;
6601 let mut t1 = make_tick_pq(dec!(90), dec!(1)); t1.side = Some(TradeSide::Sell);
6602 let mut t2 = make_tick_pq(dec!(100), dec!(1)); t2.side = Some(TradeSide::Sell);
6603 assert_eq!(NormalizedTick::sell_avg_price(&[t1, t2]), Some(dec!(95)));
6604 }
6605
6606 #[test]
6609 fn test_price_skewness_none_for_fewer_than_3() {
6610 use rust_decimal_macros::dec;
6611 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6612 assert!(NormalizedTick::price_skewness(&ticks).is_none());
6613 }
6614
6615 #[test]
6616 fn test_price_skewness_zero_for_symmetric() {
6617 use rust_decimal_macros::dec;
6618 let ticks = vec![
6620 make_tick_pq(dec!(1), dec!(1)),
6621 make_tick_pq(dec!(2), dec!(1)),
6622 make_tick_pq(dec!(3), dec!(1)),
6623 ];
6624 let s = NormalizedTick::price_skewness(&ticks).unwrap();
6625 assert!(s.abs() < 1e-9, "symmetric should have near-zero skew, got {}", s);
6626 }
6627
6628 #[test]
6631 fn test_quantity_skewness_none_for_fewer_than_3() {
6632 use rust_decimal_macros::dec;
6633 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(2))];
6634 assert!(NormalizedTick::quantity_skewness(&ticks).is_none());
6635 }
6636
6637 #[test]
6638 fn test_quantity_skewness_positive_for_right_skewed() {
6639 use rust_decimal_macros::dec;
6640 let ticks = vec![
6642 make_tick_pq(dec!(100), dec!(1)),
6643 make_tick_pq(dec!(101), dec!(1)),
6644 make_tick_pq(dec!(102), dec!(100)),
6645 ];
6646 let s = NormalizedTick::quantity_skewness(&ticks).unwrap();
6647 assert!(s > 0.0, "right-skewed distribution should have positive skewness, got {}", s);
6648 }
6649
6650 #[test]
6653 fn test_price_kurtosis_none_for_fewer_than_4() {
6654 use rust_decimal_macros::dec;
6655 let ticks = vec![
6656 make_tick_pq(dec!(1), dec!(1)),
6657 make_tick_pq(dec!(2), dec!(1)),
6658 make_tick_pq(dec!(3), dec!(1)),
6659 ];
6660 assert!(NormalizedTick::price_kurtosis(&ticks).is_none());
6661 }
6662
6663 #[test]
6664 fn test_price_kurtosis_returns_some_for_varied_prices() {
6665 use rust_decimal_macros::dec;
6666 let ticks = vec![
6667 make_tick_pq(dec!(1), dec!(1)),
6668 make_tick_pq(dec!(2), dec!(1)),
6669 make_tick_pq(dec!(3), dec!(1)),
6670 make_tick_pq(dec!(4), dec!(1)),
6671 ];
6672 assert!(NormalizedTick::price_kurtosis(&ticks).is_some());
6673 }
6674
6675 #[test]
6678 fn test_high_volume_tick_count_zero_for_empty() {
6679 use rust_decimal_macros::dec;
6680 assert_eq!(NormalizedTick::high_volume_tick_count(&[], dec!(1)), 0);
6681 }
6682
6683 #[test]
6684 fn test_high_volume_tick_count_correct() {
6685 use rust_decimal_macros::dec;
6686 let ticks = vec![
6687 make_tick_pq(dec!(100), dec!(1)),
6688 make_tick_pq(dec!(101), dec!(5)),
6689 make_tick_pq(dec!(102), dec!(10)),
6690 ];
6691 assert_eq!(NormalizedTick::high_volume_tick_count(&ticks, dec!(4)), 2);
6692 }
6693
6694 #[test]
6697 fn test_vwap_spread_none_when_no_buys_or_sells() {
6698 use rust_decimal_macros::dec;
6699 let t = make_tick_pq(dec!(100), dec!(1));
6700 assert!(NormalizedTick::vwap_spread(&[t]).is_none());
6701 }
6702
6703 #[test]
6704 fn test_vwap_spread_positive_when_buys_priced_higher() {
6705 use rust_decimal_macros::dec;
6706 let mut buy = make_tick_pq(dec!(105), dec!(1)); buy.side = Some(TradeSide::Buy);
6707 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
6708 let spread = NormalizedTick::vwap_spread(&[buy, sell]).unwrap();
6709 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
6710 }
6711
6712 #[test]
6715 fn test_avg_buy_quantity_none_for_no_buys() {
6716 use rust_decimal_macros::dec;
6717 let mut t = make_tick_pq(dec!(100), dec!(2)); t.side = Some(TradeSide::Sell);
6718 assert!(NormalizedTick::avg_buy_quantity(&[t]).is_none());
6719 }
6720
6721 #[test]
6722 fn test_avg_buy_quantity_correct() {
6723 use rust_decimal_macros::dec;
6724 let mut t1 = make_tick_pq(dec!(100), dec!(2)); t1.side = Some(TradeSide::Buy);
6725 let mut t2 = make_tick_pq(dec!(101), dec!(4)); t2.side = Some(TradeSide::Buy);
6726 assert_eq!(NormalizedTick::avg_buy_quantity(&[t1, t2]), Some(dec!(3)));
6727 }
6728
6729 #[test]
6730 fn test_avg_sell_quantity_correct() {
6731 use rust_decimal_macros::dec;
6732 let mut t1 = make_tick_pq(dec!(100), dec!(6)); t1.side = Some(TradeSide::Sell);
6733 let mut t2 = make_tick_pq(dec!(101), dec!(2)); t2.side = Some(TradeSide::Sell);
6734 assert_eq!(NormalizedTick::avg_sell_quantity(&[t1, t2]), Some(dec!(4)));
6735 }
6736
6737 #[test]
6740 fn test_price_mean_reversion_score_none_for_empty() {
6741 assert!(NormalizedTick::price_mean_reversion_score(&[]).is_none());
6742 }
6743
6744 #[test]
6745 fn test_price_mean_reversion_score_in_range() {
6746 use rust_decimal_macros::dec;
6747 let ticks = vec![
6748 make_tick_pq(dec!(90), dec!(1)),
6749 make_tick_pq(dec!(100), dec!(1)),
6750 make_tick_pq(dec!(110), dec!(1)),
6751 ];
6752 let score = NormalizedTick::price_mean_reversion_score(&ticks).unwrap();
6753 assert!(score >= 0.0 && score <= 1.0, "score should be in [0, 1], got {}", score);
6754 }
6755
6756 #[test]
6759 fn test_largest_price_move_none_for_single_tick() {
6760 use rust_decimal_macros::dec;
6761 let t = make_tick_pq(dec!(100), dec!(1));
6762 assert!(NormalizedTick::largest_price_move(&[t]).is_none());
6763 }
6764
6765 #[test]
6766 fn test_largest_price_move_correct() {
6767 use rust_decimal_macros::dec;
6768 let ticks = vec![
6769 make_tick_pq(dec!(100), dec!(1)),
6770 make_tick_pq(dec!(105), dec!(1)), make_tick_pq(dec!(102), dec!(1)), ];
6773 assert_eq!(NormalizedTick::largest_price_move(&ticks), Some(dec!(5)));
6774 }
6775
6776 #[test]
6779 fn test_tick_rate_none_for_single_tick() {
6780 use rust_decimal_macros::dec;
6781 let t = make_tick_pq(dec!(100), dec!(1));
6782 assert!(NormalizedTick::tick_rate(&[t]).is_none());
6783 }
6784
6785 #[test]
6786 fn test_tick_rate_correct() {
6787 use rust_decimal_macros::dec;
6788 let mut t1 = make_tick_pq(dec!(100), dec!(1)); t1.received_at_ms = 0;
6789 let mut t2 = make_tick_pq(dec!(101), dec!(1)); t2.received_at_ms = 2;
6790 let mut t3 = make_tick_pq(dec!(102), dec!(1)); t3.received_at_ms = 4;
6791 let rate = NormalizedTick::tick_rate(&[t1, t2, t3]).unwrap();
6793 assert!((rate - 0.75).abs() < 1e-9, "expected 0.75 ticks/ms, got {}", rate);
6794 }
6795
6796 #[test]
6799 fn test_buy_notional_fraction_none_for_empty() {
6800 assert!(NormalizedTick::buy_notional_fraction(&[]).is_none());
6801 }
6802
6803 #[test]
6804 fn test_buy_notional_fraction_one_when_all_buys() {
6805 use rust_decimal_macros::dec;
6806 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
6807 let frac = NormalizedTick::buy_notional_fraction(&[t]).unwrap();
6808 assert!((frac - 1.0).abs() < 1e-9, "all buys should give fraction=1.0, got {}", frac);
6809 }
6810
6811 #[test]
6812 fn test_buy_notional_fraction_in_range_for_mixed() {
6813 use rust_decimal_macros::dec;
6814 let mut buy = make_tick_pq(dec!(100), dec!(3)); buy.side = Some(TradeSide::Buy);
6815 let mut sell = make_tick_pq(dec!(100), dec!(1)); sell.side = Some(TradeSide::Sell);
6816 let frac = NormalizedTick::buy_notional_fraction(&[buy, sell]).unwrap();
6817 assert!(frac > 0.0 && frac < 1.0, "mixed ticks should be in (0,1), got {}", frac);
6818 }
6819
6820 #[test]
6823 fn test_price_range_pct_none_for_empty() {
6824 assert!(NormalizedTick::price_range_pct(&[]).is_none());
6825 }
6826
6827 #[test]
6828 fn test_price_range_pct_correct() {
6829 use rust_decimal_macros::dec;
6830 let ticks = vec![
6831 make_tick_pq(dec!(100), dec!(1)),
6832 make_tick_pq(dec!(110), dec!(1)),
6833 ];
6834 let pct = NormalizedTick::price_range_pct(&ticks).unwrap();
6836 assert!((pct - 10.0).abs() < 1e-6, "expected 10.0%, got {}", pct);
6837 }
6838
6839 #[test]
6842 fn test_buy_side_dominance_none_when_no_sides() {
6843 use rust_decimal_macros::dec;
6844 let t = make_tick_pq(dec!(100), dec!(1)); assert!(NormalizedTick::buy_side_dominance(&[t]).is_none());
6846 }
6847
6848 #[test]
6849 fn test_buy_side_dominance_one_when_all_buys() {
6850 use rust_decimal_macros::dec;
6851 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Buy);
6852 let d = NormalizedTick::buy_side_dominance(&[t]).unwrap();
6853 assert!((d - 1.0).abs() < 1e-9, "all buys should give 1.0, got {}", d);
6854 }
6855
6856 #[test]
6859 fn test_volume_weighted_price_std_none_for_empty() {
6860 assert!(NormalizedTick::volume_weighted_price_std(&[]).is_none());
6861 }
6862
6863 #[test]
6864 fn test_volume_weighted_price_std_zero_for_same_price() {
6865 use rust_decimal_macros::dec;
6866 let ticks = vec![
6867 make_tick_pq(dec!(100), dec!(2)),
6868 make_tick_pq(dec!(100), dec!(3)),
6869 ];
6870 let std = NormalizedTick::volume_weighted_price_std(&ticks).unwrap();
6871 assert!((std - 0.0).abs() < 1e-9, "same price should give 0 std, got {}", std);
6872 }
6873
6874 #[test]
6877 fn test_last_n_vwap_none_for_zero_n() {
6878 use rust_decimal_macros::dec;
6879 let t = make_tick_pq(dec!(100), dec!(1));
6880 assert!(NormalizedTick::last_n_vwap(&[t], 0).is_none());
6881 }
6882
6883 #[test]
6884 fn test_last_n_vwap_uses_last_n_ticks() {
6885 use rust_decimal_macros::dec;
6886 let ticks = vec![
6888 make_tick_pq(dec!(50), dec!(10)),
6889 make_tick_pq(dec!(100), dec!(5)),
6890 make_tick_pq(dec!(100), dec!(5)),
6891 ];
6892 let v = NormalizedTick::last_n_vwap(&ticks, 2).unwrap();
6893 assert_eq!(v, dec!(100));
6894 }
6895
6896 #[test]
6899 fn test_price_autocorrelation_none_for_fewer_than_3() {
6900 use rust_decimal_macros::dec;
6901 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
6902 assert!(NormalizedTick::price_autocorrelation(&ticks).is_none());
6903 }
6904
6905 #[test]
6906 fn test_price_autocorrelation_positive_for_trending_prices() {
6907 use rust_decimal_macros::dec;
6908 let ticks = vec![
6909 make_tick_pq(dec!(100), dec!(1)),
6910 make_tick_pq(dec!(102), dec!(1)),
6911 make_tick_pq(dec!(104), dec!(1)),
6912 make_tick_pq(dec!(106), dec!(1)),
6913 ];
6914 let ac = NormalizedTick::price_autocorrelation(&ticks).unwrap();
6915 assert!(ac > 0.0, "trending prices should have positive AC, got {}", ac);
6916 }
6917
6918 #[test]
6921 fn test_net_trade_direction_zero_for_empty() {
6922 assert_eq!(NormalizedTick::net_trade_direction(&[]), 0);
6923 }
6924
6925 #[test]
6926 fn test_net_trade_direction_positive_for_more_buys() {
6927 use rust_decimal_macros::dec;
6928 let mut b1 = make_tick_pq(dec!(100), dec!(1)); b1.side = Some(TradeSide::Buy);
6929 let mut b2 = make_tick_pq(dec!(100), dec!(1)); b2.side = Some(TradeSide::Buy);
6930 let mut s1 = make_tick_pq(dec!(100), dec!(1)); s1.side = Some(TradeSide::Sell);
6931 assert_eq!(NormalizedTick::net_trade_direction(&[b1, b2, s1]), 1);
6932 }
6933
6934 #[test]
6937 fn test_sell_side_notional_fraction_none_for_empty() {
6938 assert!(NormalizedTick::sell_side_notional_fraction(&[]).is_none());
6939 }
6940
6941 #[test]
6942 fn test_sell_side_notional_fraction_one_when_all_sells() {
6943 use rust_decimal_macros::dec;
6944 let mut t = make_tick_pq(dec!(100), dec!(5)); t.side = Some(TradeSide::Sell);
6945 let f = NormalizedTick::sell_side_notional_fraction(&[t]).unwrap();
6946 assert!((f - 1.0).abs() < 1e-9, "all sells should give 1.0, got {}", f);
6947 }
6948
6949 #[test]
6952 fn test_price_oscillation_count_zero_for_monotone() {
6953 use rust_decimal_macros::dec;
6954 let ticks = vec![
6955 make_tick_pq(dec!(100), dec!(1)),
6956 make_tick_pq(dec!(101), dec!(1)),
6957 make_tick_pq(dec!(102), dec!(1)),
6958 ];
6959 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 0);
6960 }
6961
6962 #[test]
6963 fn test_price_oscillation_count_detects_reversals() {
6964 use rust_decimal_macros::dec;
6965 let ticks = vec![
6968 make_tick_pq(dec!(100), dec!(1)),
6969 make_tick_pq(dec!(105), dec!(1)),
6970 make_tick_pq(dec!(102), dec!(1)),
6971 make_tick_pq(dec!(107), dec!(1)),
6972 ];
6973 assert_eq!(NormalizedTick::price_oscillation_count(&ticks), 2);
6974 }
6975
6976 #[test]
6979 fn test_realized_spread_none_when_no_sides() {
6980 use rust_decimal_macros::dec;
6981 let t = make_tick_pq(dec!(100), dec!(1));
6982 assert!(NormalizedTick::realized_spread(&[t]).is_none());
6983 }
6984
6985 #[test]
6986 fn test_realized_spread_positive_when_buys_higher() {
6987 use rust_decimal_macros::dec;
6988 let mut b = make_tick_pq(dec!(105), dec!(1)); b.side = Some(TradeSide::Buy);
6989 let mut s = make_tick_pq(dec!(100), dec!(1)); s.side = Some(TradeSide::Sell);
6990 let spread = NormalizedTick::realized_spread(&[b, s]).unwrap();
6991 assert!(spread > dec!(0), "expected positive spread, got {}", spread);
6992 }
6993
6994 #[test]
6997 fn test_price_impact_per_unit_none_for_single_tick() {
6998 use rust_decimal_macros::dec;
6999 let t = make_tick_pq(dec!(100), dec!(1));
7000 assert!(NormalizedTick::price_impact_per_unit(&[t]).is_none());
7001 }
7002
7003 #[test]
7006 fn test_volume_weighted_return_none_for_single_tick() {
7007 use rust_decimal_macros::dec;
7008 let t = make_tick_pq(dec!(100), dec!(1));
7009 assert!(NormalizedTick::volume_weighted_return(&[t]).is_none());
7010 }
7011
7012 #[test]
7013 fn test_volume_weighted_return_zero_for_constant_price() {
7014 use rust_decimal_macros::dec;
7015 let ticks = vec![
7016 make_tick_pq(dec!(100), dec!(5)),
7017 make_tick_pq(dec!(100), dec!(5)),
7018 ];
7019 let r = NormalizedTick::volume_weighted_return(&ticks).unwrap();
7020 assert!((r - 0.0).abs() < 1e-9, "constant price should give 0 return, got {}", r);
7021 }
7022
7023 #[test]
7026 fn test_quantity_concentration_none_for_empty() {
7027 assert!(NormalizedTick::quantity_concentration(&[]).is_none());
7028 }
7029
7030 #[test]
7031 fn test_quantity_concentration_zero_for_identical_quantities() {
7032 use rust_decimal_macros::dec;
7033 let ticks = vec![
7034 make_tick_pq(dec!(100), dec!(5)),
7035 make_tick_pq(dec!(101), dec!(5)),
7036 ];
7037 let c = NormalizedTick::quantity_concentration(&ticks).unwrap();
7038 assert!((c - 0.0).abs() < 1e-9, "identical quantities should give 0 concentration, got {}", c);
7039 }
7040
7041 #[test]
7044 fn test_price_level_volume_zero_for_no_match() {
7045 use rust_decimal_macros::dec;
7046 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7047 let v = NormalizedTick::price_level_volume(&ticks, dec!(200));
7048 assert_eq!(v, dec!(0));
7049 }
7050
7051 #[test]
7052 fn test_price_level_volume_sums_matching_ticks() {
7053 use rust_decimal_macros::dec;
7054 let ticks = vec![
7055 make_tick_pq(dec!(100), dec!(3)),
7056 make_tick_pq(dec!(101), dec!(7)),
7057 make_tick_pq(dec!(100), dec!(2)),
7058 ];
7059 assert_eq!(NormalizedTick::price_level_volume(&ticks, dec!(100)), dec!(5));
7060 }
7061
7062 #[test]
7065 fn test_mid_price_drift_none_for_single_tick() {
7066 use rust_decimal_macros::dec;
7067 let t = make_tick_pq(dec!(100), dec!(1));
7068 assert!(NormalizedTick::mid_price_drift(&[t]).is_none());
7069 }
7070
7071 #[test]
7074 fn test_tick_direction_bias_none_for_fewer_than_3() {
7075 use rust_decimal_macros::dec;
7076 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
7077 assert!(NormalizedTick::tick_direction_bias(&ticks).is_none());
7078 }
7079
7080 #[test]
7081 fn test_tick_direction_bias_one_for_monotone() {
7082 use rust_decimal_macros::dec;
7083 let ticks = vec![
7084 make_tick_pq(dec!(100), dec!(1)),
7085 make_tick_pq(dec!(101), dec!(1)),
7086 make_tick_pq(dec!(102), dec!(1)),
7087 make_tick_pq(dec!(103), dec!(1)),
7088 ];
7089 let bias = NormalizedTick::tick_direction_bias(&ticks).unwrap();
7090 assert!((bias - 1.0).abs() < 1e-9, "monotone should give bias=1.0, got {}", bias);
7091 }
7092
7093 #[test]
7094 fn test_buy_sell_size_ratio_none_for_empty() {
7095 assert!(NormalizedTick::buy_sell_size_ratio(&[]).is_none());
7096 }
7097
7098 #[test]
7099 fn test_buy_sell_size_ratio_positive() {
7100 use rust_decimal_macros::dec;
7101 let buy = NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) };
7102 let sell = NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(2)) };
7103 let r = NormalizedTick::buy_sell_size_ratio(&[buy, sell]).unwrap();
7104 assert!((r - 2.0).abs() < 1e-6, "ratio should be 2.0, got {}", r);
7105 }
7106
7107 #[test]
7108 fn test_trade_size_dispersion_none_for_single_tick() {
7109 use rust_decimal_macros::dec;
7110 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7111 assert!(NormalizedTick::trade_size_dispersion(&ticks).is_none());
7112 }
7113
7114 #[test]
7115 fn test_trade_size_dispersion_zero_for_identical() {
7116 use rust_decimal_macros::dec;
7117 let ticks = vec![
7118 make_tick_pq(dec!(100), dec!(5)),
7119 make_tick_pq(dec!(101), dec!(5)),
7120 make_tick_pq(dec!(102), dec!(5)),
7121 ];
7122 let d = NormalizedTick::trade_size_dispersion(&ticks).unwrap();
7123 assert!(d.abs() < 1e-9, "identical sizes → dispersion=0, got {}", d);
7124 }
7125
7126 #[test]
7127 fn test_first_last_price_none_for_empty() {
7128 assert!(NormalizedTick::first_price(&[]).is_none());
7129 assert!(NormalizedTick::last_price(&[]).is_none());
7130 }
7131
7132 #[test]
7133 fn test_first_last_price_correct() {
7134 use rust_decimal_macros::dec;
7135 let ticks = vec![
7136 make_tick_pq(dec!(100), dec!(1)),
7137 make_tick_pq(dec!(105), dec!(1)),
7138 make_tick_pq(dec!(110), dec!(1)),
7139 ];
7140 assert_eq!(NormalizedTick::first_price(&ticks).unwrap(), dec!(100));
7141 assert_eq!(NormalizedTick::last_price(&ticks).unwrap(), dec!(110));
7142 }
7143
7144 #[test]
7145 fn test_median_quantity_none_for_empty() {
7146 assert!(NormalizedTick::median_quantity(&[]).is_none());
7147 }
7148
7149 #[test]
7150 fn test_median_quantity_odd_count() {
7151 use rust_decimal_macros::dec;
7152 let ticks = vec![
7153 make_tick_pq(dec!(100), dec!(3)),
7154 make_tick_pq(dec!(101), dec!(1)),
7155 make_tick_pq(dec!(102), dec!(5)),
7156 ];
7157 assert_eq!(NormalizedTick::median_quantity(&ticks).unwrap(), dec!(3));
7159 }
7160
7161 #[test]
7162 fn test_volume_above_vwap_none_for_empty() {
7163 assert!(NormalizedTick::volume_above_vwap(&[]).is_none());
7164 }
7165
7166 #[test]
7167 fn test_volume_above_vwap_none_when_all_at_vwap() {
7168 use rust_decimal_macros::dec;
7169 let ticks = vec![
7171 make_tick_pq(dec!(100), dec!(5)),
7172 make_tick_pq(dec!(100), dec!(5)),
7173 ];
7174 let v = NormalizedTick::volume_above_vwap(&ticks).unwrap();
7175 assert_eq!(v, dec!(0));
7176 }
7177
7178 #[test]
7179 fn test_inter_arrival_variance_none_for_fewer_than_3() {
7180 use rust_decimal_macros::dec;
7181 let t = make_tick_pq(dec!(100), dec!(1));
7182 assert!(NormalizedTick::inter_arrival_variance(&[t]).is_none());
7183 }
7184
7185 #[test]
7186 fn test_spread_efficiency_none_for_single_tick() {
7187 use rust_decimal_macros::dec;
7188 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7189 assert!(NormalizedTick::spread_efficiency(&ticks).is_none());
7190 }
7191
7192 #[test]
7193 fn test_spread_efficiency_one_for_monotone() {
7194 use rust_decimal_macros::dec;
7195 let ticks = vec![
7196 make_tick_pq(dec!(100), dec!(1)),
7197 make_tick_pq(dec!(101), dec!(1)),
7198 make_tick_pq(dec!(102), dec!(1)),
7199 ];
7200 let e = NormalizedTick::spread_efficiency(&ticks).unwrap();
7202 assert!((e - 1.0).abs() < 1e-9, "expected 1.0, got {}", e);
7203 }
7204
7205 #[test]
7210 fn test_aggressor_fraction_none_for_empty() {
7211 assert!(NormalizedTick::aggressor_fraction(&[]).is_none());
7212 }
7213
7214 #[test]
7215 fn test_aggressor_fraction_zero_when_all_neutral() {
7216 use rust_decimal_macros::dec;
7217 let ticks = vec![
7218 make_tick_pq(dec!(100), dec!(1)),
7219 make_tick_pq(dec!(101), dec!(1)),
7220 ];
7221 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
7222 assert!((f - 0.0).abs() < 1e-9, "all neutral → fraction=0, got {}", f);
7223 }
7224
7225 #[test]
7226 fn test_aggressor_fraction_one_when_all_known() {
7227 use rust_decimal_macros::dec;
7228 let ticks = vec![
7229 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(1)) },
7230 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(101), dec!(1)) },
7231 ];
7232 let f = NormalizedTick::aggressor_fraction(&ticks).unwrap();
7233 assert!((f - 1.0).abs() < 1e-9, "all known → fraction=1, got {}", f);
7234 }
7235
7236 #[test]
7239 fn test_volume_imbalance_ratio_none_for_neutral_ticks() {
7240 use rust_decimal_macros::dec;
7241 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7242 assert!(NormalizedTick::volume_imbalance_ratio(&ticks).is_none());
7243 }
7244
7245 #[test]
7246 fn test_volume_imbalance_ratio_positive_for_all_buys() {
7247 use rust_decimal_macros::dec;
7248 let ticks = vec![
7249 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(4)) },
7250 ];
7251 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
7252 assert!((r - 1.0).abs() < 1e-9, "all buys → ratio=1.0, got {}", r);
7253 }
7254
7255 #[test]
7256 fn test_volume_imbalance_ratio_zero_for_equal_sides() {
7257 use rust_decimal_macros::dec;
7258 let ticks = vec![
7259 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7260 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
7261 ];
7262 let r = NormalizedTick::volume_imbalance_ratio(&ticks).unwrap();
7263 assert!(r.abs() < 1e-9, "equal buy/sell → ratio=0, got {}", r);
7264 }
7265
7266 #[test]
7269 fn test_price_quantity_covariance_none_for_single_tick() {
7270 use rust_decimal_macros::dec;
7271 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7272 assert!(NormalizedTick::price_quantity_covariance(&ticks).is_none());
7273 }
7274
7275 #[test]
7276 fn test_price_quantity_covariance_positive_when_correlated() {
7277 use rust_decimal_macros::dec;
7278 let ticks = vec![
7279 make_tick_pq(dec!(100), dec!(1)),
7280 make_tick_pq(dec!(200), dec!(2)),
7281 make_tick_pq(dec!(300), dec!(3)),
7282 ];
7283 let c = NormalizedTick::price_quantity_covariance(&ticks).unwrap();
7284 assert!(c > 0.0, "price and qty both rise → positive cov, got {}", c);
7285 }
7286
7287 #[test]
7290 fn test_large_trade_fraction_none_for_empty() {
7291 use rust_decimal_macros::dec;
7292 assert!(NormalizedTick::large_trade_fraction(&[], dec!(10)).is_none());
7293 }
7294
7295 #[test]
7296 fn test_large_trade_fraction_zero_when_all_small() {
7297 use rust_decimal_macros::dec;
7298 let ticks = vec![
7299 make_tick_pq(dec!(100), dec!(1)),
7300 make_tick_pq(dec!(101), dec!(2)),
7301 ];
7302 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
7303 assert!((f - 0.0).abs() < 1e-9, "all small → fraction=0, got {}", f);
7304 }
7305
7306 #[test]
7307 fn test_large_trade_fraction_one_when_all_large() {
7308 use rust_decimal_macros::dec;
7309 let ticks = vec![
7310 make_tick_pq(dec!(100), dec!(20)),
7311 make_tick_pq(dec!(101), dec!(30)),
7312 ];
7313 let f = NormalizedTick::large_trade_fraction(&ticks, dec!(10)).unwrap();
7314 assert!((f - 1.0).abs() < 1e-9, "all large → fraction=1, got {}", f);
7315 }
7316
7317 #[test]
7320 fn test_price_level_density_none_for_empty() {
7321 assert!(NormalizedTick::price_level_density(&[]).is_none());
7322 }
7323
7324 #[test]
7325 fn test_price_level_density_none_when_range_zero() {
7326 use rust_decimal_macros::dec;
7327 let ticks = vec![
7328 make_tick_pq(dec!(100), dec!(1)),
7329 make_tick_pq(dec!(100), dec!(2)),
7330 ];
7331 assert!(NormalizedTick::price_level_density(&ticks).is_none());
7332 }
7333
7334 #[test]
7335 fn test_price_level_density_positive_for_varied_prices() {
7336 use rust_decimal_macros::dec;
7337 let ticks = vec![
7338 make_tick_pq(dec!(100), dec!(1)),
7339 make_tick_pq(dec!(110), dec!(1)),
7340 make_tick_pq(dec!(120), dec!(1)),
7341 ];
7342 let d = NormalizedTick::price_level_density(&ticks).unwrap();
7343 assert!(d > 0.0, "should be positive, got {}", d);
7344 }
7345
7346 #[test]
7349 fn test_notional_buy_sell_ratio_none_when_no_sells() {
7350 use rust_decimal_macros::dec;
7351 let ticks = vec![
7352 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7353 ];
7354 assert!(NormalizedTick::notional_buy_sell_ratio(&ticks).is_none());
7355 }
7356
7357 #[test]
7358 fn test_notional_buy_sell_ratio_one_for_equal_notional() {
7359 use rust_decimal_macros::dec;
7360 let ticks = vec![
7361 NormalizedTick { side: Some(TradeSide::Buy), ..make_tick_pq(dec!(100), dec!(5)) },
7362 NormalizedTick { side: Some(TradeSide::Sell), ..make_tick_pq(dec!(100), dec!(5)) },
7363 ];
7364 let r = NormalizedTick::notional_buy_sell_ratio(&ticks).unwrap();
7365 assert!((r - 1.0).abs() < 1e-9, "equal notional → ratio=1, got {}", r);
7366 }
7367
7368 #[test]
7371 fn test_log_return_mean_none_for_single_tick() {
7372 use rust_decimal_macros::dec;
7373 assert!(NormalizedTick::log_return_mean(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
7374 }
7375
7376 #[test]
7377 fn test_log_return_mean_zero_for_constant_price() {
7378 use rust_decimal_macros::dec;
7379 let ticks = vec![
7380 make_tick_pq(dec!(100), dec!(1)),
7381 make_tick_pq(dec!(100), dec!(1)),
7382 make_tick_pq(dec!(100), dec!(1)),
7383 ];
7384 let m = NormalizedTick::log_return_mean(&ticks).unwrap();
7385 assert!(m.abs() < 1e-9, "constant price → mean log return=0, got {}", m);
7386 }
7387
7388 #[test]
7391 fn test_log_return_std_none_for_fewer_than_3_ticks() {
7392 use rust_decimal_macros::dec;
7393 let ticks = vec![
7394 make_tick_pq(dec!(100), dec!(1)),
7395 make_tick_pq(dec!(101), dec!(1)),
7396 ];
7397 assert!(NormalizedTick::log_return_std(&ticks).is_none());
7398 }
7399
7400 #[test]
7401 fn test_log_return_std_zero_for_constant_price() {
7402 use rust_decimal_macros::dec;
7403 let ticks = vec![
7404 make_tick_pq(dec!(100), dec!(1)),
7405 make_tick_pq(dec!(100), dec!(1)),
7406 make_tick_pq(dec!(100), dec!(1)),
7407 make_tick_pq(dec!(100), dec!(1)),
7408 ];
7409 let s = NormalizedTick::log_return_std(&ticks).unwrap();
7410 assert!(s.abs() < 1e-9, "constant price → std=0, got {}", s);
7411 }
7412
7413 #[test]
7416 fn test_price_overshoot_ratio_none_for_empty() {
7417 assert!(NormalizedTick::price_overshoot_ratio(&[]).is_none());
7418 }
7419
7420 #[test]
7421 fn test_price_overshoot_ratio_one_for_monotone_up() {
7422 use rust_decimal_macros::dec;
7423 let ticks = vec![
7424 make_tick_pq(dec!(100), dec!(1)),
7425 make_tick_pq(dec!(105), dec!(1)),
7426 make_tick_pq(dec!(110), dec!(1)),
7427 ];
7428 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
7430 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
7431 }
7432
7433 #[test]
7434 fn test_price_overshoot_ratio_above_one_when_price_retreats() {
7435 use rust_decimal_macros::dec;
7436 let ticks = vec![
7437 make_tick_pq(dec!(100), dec!(1)),
7438 make_tick_pq(dec!(120), dec!(1)),
7439 make_tick_pq(dec!(110), dec!(1)),
7440 ];
7441 let r = NormalizedTick::price_overshoot_ratio(&ticks).unwrap();
7443 assert!(r > 1.0, "price retreated → ratio>1, got {}", r);
7444 }
7445
7446 #[test]
7449 fn test_price_undershoot_ratio_none_for_empty() {
7450 assert!(NormalizedTick::price_undershoot_ratio(&[]).is_none());
7451 }
7452
7453 #[test]
7454 fn test_price_undershoot_ratio_one_for_monotone_down() {
7455 use rust_decimal_macros::dec;
7456 let ticks = vec![
7457 make_tick_pq(dec!(110), dec!(1)),
7458 make_tick_pq(dec!(105), dec!(1)),
7459 make_tick_pq(dec!(100), dec!(1)),
7460 ];
7461 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
7463 assert!(r > 1.0, "monotone down → ratio>1, got {}", r);
7464 }
7465
7466 #[test]
7467 fn test_price_undershoot_ratio_one_for_monotone_up() {
7468 use rust_decimal_macros::dec;
7469 let ticks = vec![
7470 make_tick_pq(dec!(100), dec!(1)),
7471 make_tick_pq(dec!(105), dec!(1)),
7472 make_tick_pq(dec!(110), dec!(1)),
7473 ];
7474 let r = NormalizedTick::price_undershoot_ratio(&ticks).unwrap();
7476 assert!((r - 1.0).abs() < 1e-9, "monotone up → ratio=1, got {}", r);
7477 }
7478
7479 #[test]
7482 fn test_net_notional_empty_is_zero() {
7483 assert_eq!(NormalizedTick::net_notional(&[]), Decimal::ZERO);
7484 }
7485
7486 #[test]
7487 fn test_net_notional_positive_buy() {
7488 use rust_decimal_macros::dec;
7489 let ticks = vec![
7490 make_tick_pq(dec!(100), dec!(5)).with_side(TradeSide::Buy),
7491 make_tick_pq(dec!(100), dec!(2)).with_side(TradeSide::Sell),
7492 ];
7493 assert_eq!(NormalizedTick::net_notional(&ticks), dec!(300));
7494 }
7495
7496 #[test]
7497 fn test_price_reversal_count_empty_is_zero() {
7498 assert_eq!(NormalizedTick::price_reversal_count(&[]), 0);
7499 }
7500
7501 #[test]
7502 fn test_price_reversal_count_monotone_is_zero() {
7503 use rust_decimal_macros::dec;
7504 let ticks = vec![
7505 make_tick_pq(dec!(100), dec!(1)),
7506 make_tick_pq(dec!(101), dec!(1)),
7507 make_tick_pq(dec!(102), dec!(1)),
7508 ];
7509 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 0);
7510 }
7511
7512 #[test]
7513 fn test_price_reversal_count_zigzag() {
7514 use rust_decimal_macros::dec;
7515 let ticks = vec![
7516 make_tick_pq(dec!(100), dec!(1)),
7517 make_tick_pq(dec!(105), dec!(1)),
7518 make_tick_pq(dec!(100), dec!(1)),
7519 make_tick_pq(dec!(105), dec!(1)),
7520 ];
7521 assert_eq!(NormalizedTick::price_reversal_count(&ticks), 2);
7522 }
7523
7524 #[test]
7525 fn test_quantity_kurtosis_none_for_few_ticks() {
7526 use rust_decimal_macros::dec;
7527 let t = make_tick_pq(dec!(100), dec!(1));
7528 assert!(NormalizedTick::quantity_kurtosis(&[t]).is_none());
7529 }
7530
7531 #[test]
7532 fn test_quantity_kurtosis_some_for_sufficient() {
7533 use rust_decimal_macros::dec;
7534 let ticks = vec![
7535 make_tick_pq(dec!(100), dec!(1)),
7536 make_tick_pq(dec!(101), dec!(2)),
7537 make_tick_pq(dec!(102), dec!(3)),
7538 make_tick_pq(dec!(103), dec!(4)),
7539 ];
7540 assert!(NormalizedTick::quantity_kurtosis(&ticks).is_some());
7541 }
7542
7543 #[test]
7544 fn test_largest_notional_trade_none_for_empty() {
7545 assert!(NormalizedTick::largest_notional_trade(&[]).is_none());
7546 }
7547
7548 #[test]
7549 fn test_largest_notional_trade_correct() {
7550 use rust_decimal_macros::dec;
7551 let ticks = vec![
7552 make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(50), dec!(10)), make_tick_pq(dec!(200), dec!(1)), ];
7556 let t = NormalizedTick::largest_notional_trade(&ticks).unwrap();
7557 assert_eq!(t.price, dec!(50));
7558 }
7559
7560 #[test]
7561 fn test_twap_none_for_single_tick() {
7562 use rust_decimal_macros::dec;
7563 assert!(NormalizedTick::twap(&[make_tick_pq(dec!(100), dec!(1))]).is_none());
7564 }
7565
7566 #[test]
7567 fn test_twap_two_equal_intervals() {
7568 use rust_decimal_macros::dec;
7569 let mut t1 = make_tick_pq(dec!(100), dec!(1));
7570 t1.received_at_ms = 0;
7571 let mut t2 = make_tick_pq(dec!(200), dec!(1));
7572 t2.received_at_ms = 1000;
7573 let mut t3 = make_tick_pq(dec!(300), dec!(1));
7574 t3.received_at_ms = 2000;
7575 let twap = NormalizedTick::twap(&[t1, t2, t3]).unwrap();
7577 assert_eq!(twap, dec!(150));
7578 }
7579
7580 #[test]
7581 fn test_neutral_fraction_all_neutral() {
7582 use rust_decimal_macros::dec;
7583 let ticks = vec![
7584 make_tick_pq(dec!(100), dec!(1)),
7585 make_tick_pq(dec!(101), dec!(1)),
7586 ];
7587 let f = NormalizedTick::neutral_fraction(&ticks).unwrap();
7588 assert!((f - 1.0).abs() < 1e-9, "all neutral → fraction=1, got {}", f);
7589 }
7590
7591 #[test]
7592 fn test_log_return_variance_none_for_few_ticks() {
7593 use rust_decimal_macros::dec;
7594 let t = make_tick_pq(dec!(100), dec!(1));
7595 assert!(NormalizedTick::log_return_variance(&[t]).is_none());
7596 }
7597
7598 #[test]
7599 fn test_log_return_variance_zero_for_flat_prices() {
7600 use rust_decimal_macros::dec;
7601 let ticks = vec![
7602 make_tick_pq(dec!(100), dec!(1)),
7603 make_tick_pq(dec!(100), dec!(1)),
7604 make_tick_pq(dec!(100), dec!(1)),
7605 ];
7606 let v = NormalizedTick::log_return_variance(&ticks).unwrap();
7607 assert!(v.abs() < 1e-9, "flat prices → variance=0, got {}", v);
7608 }
7609
7610 #[test]
7611 fn test_volume_at_vwap_zero_for_empty() {
7612 assert_eq!(
7613 NormalizedTick::volume_at_vwap(&[], rust_decimal_macros::dec!(1)),
7614 Decimal::ZERO
7615 );
7616 }
7617
7618 #[test]
7621 fn test_cumulative_volume_empty_for_empty_slice() {
7622 assert!(NormalizedTick::cumulative_volume(&[]).is_empty());
7623 }
7624
7625 #[test]
7626 fn test_cumulative_volume_last_equals_total() {
7627 use rust_decimal_macros::dec;
7628 let ticks = vec![
7629 make_tick_pq(dec!(100), dec!(2)),
7630 make_tick_pq(dec!(101), dec!(3)),
7631 make_tick_pq(dec!(102), dec!(5)),
7632 ];
7633 let cv = NormalizedTick::cumulative_volume(&ticks);
7634 assert_eq!(cv.last().copied().unwrap(), dec!(10));
7635 assert_eq!(cv[0], dec!(2));
7636 }
7637
7638 #[test]
7641 fn test_price_volatility_ratio_none_for_empty() {
7642 assert!(NormalizedTick::price_volatility_ratio(&[]).is_none());
7643 }
7644
7645 #[test]
7646 fn test_price_volatility_ratio_zero_for_constant_price() {
7647 use rust_decimal_macros::dec;
7648 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(100), dec!(1))];
7649 let r = NormalizedTick::price_volatility_ratio(&ticks).unwrap();
7650 assert!(r.abs() < 1e-9, "constant price → ratio=0, got {}", r);
7651 }
7652
7653 #[test]
7656 fn test_notional_per_tick_none_for_empty() {
7657 assert!(NormalizedTick::notional_per_tick(&[]).is_none());
7658 }
7659
7660 #[test]
7661 fn test_notional_per_tick_equals_single_tick_notional() {
7662 use rust_decimal_macros::dec;
7663 let ticks = vec![make_tick_pq(dec!(100), dec!(5))];
7664 let n = NormalizedTick::notional_per_tick(&ticks).unwrap();
7665 assert!((n - 500.0).abs() < 1e-6, "100×5=500, got {}", n);
7666 }
7667
7668 #[test]
7671 fn test_buy_to_total_volume_ratio_none_for_empty() {
7672 assert!(NormalizedTick::buy_to_total_volume_ratio(&[]).is_none());
7673 }
7674
7675 #[test]
7676 fn test_buy_to_total_volume_ratio_zero_for_all_neutral() {
7677 use rust_decimal_macros::dec;
7678 let ticks = vec![make_tick_pq(dec!(100), dec!(5)), make_tick_pq(dec!(101), dec!(3))];
7679 let r = NormalizedTick::buy_to_total_volume_ratio(&ticks).unwrap();
7680 assert!(r.abs() < 1e-9, "neutral ticks → buy ratio=0, got {}", r);
7681 }
7682
7683 #[test]
7686 fn test_avg_latency_ms_none_when_no_exchange_ts() {
7687 use rust_decimal_macros::dec;
7688 let ticks = vec![make_tick_pq(dec!(100), dec!(1))];
7689 assert!(NormalizedTick::avg_latency_ms(&ticks).is_none());
7690 }
7691
7692 #[test]
7695 fn test_price_gini_none_for_empty() {
7696 assert!(NormalizedTick::price_gini(&[]).is_none());
7697 }
7698
7699 #[test]
7700 fn test_price_gini_zero_for_uniform_prices() {
7701 use rust_decimal_macros::dec;
7702 let ticks = vec![
7703 make_tick_pq(dec!(100), dec!(1)),
7704 make_tick_pq(dec!(100), dec!(1)),
7705 make_tick_pq(dec!(100), dec!(1)),
7706 ];
7707 let g = NormalizedTick::price_gini(&ticks).unwrap();
7708 assert!(g.abs() < 1e-9, "uniform prices → gini=0, got {}", g);
7709 }
7710
7711 #[test]
7714 fn test_trade_velocity_none_for_same_timestamp() {
7715 use rust_decimal_macros::dec;
7716 let ticks = vec![
7717 make_tick_pq(dec!(100), dec!(1)),
7718 make_tick_pq(dec!(101), dec!(1)),
7719 ];
7720 assert!(NormalizedTick::trade_velocity(&ticks).is_none());
7721 }
7722
7723 #[test]
7726 fn test_floor_price_none_for_empty() {
7727 assert!(NormalizedTick::floor_price(&[]).is_none());
7728 }
7729
7730 #[test]
7731 fn test_floor_price_equals_min_price() {
7732 use rust_decimal_macros::dec;
7733 let ticks = vec![
7734 make_tick_pq(dec!(105), dec!(1)),
7735 make_tick_pq(dec!(100), dec!(1)),
7736 make_tick_pq(dec!(103), dec!(1)),
7737 ];
7738 assert_eq!(NormalizedTick::floor_price(&ticks), NormalizedTick::min_price(&ticks));
7739 }
7740
7741 #[test]
7744 fn test_price_momentum_score_none_for_single_tick() {
7745 use rust_decimal_macros::dec;
7746 let t = make_tick_pq(dec!(100), dec!(1));
7747 assert!(NormalizedTick::price_momentum_score(&[t]).is_none());
7748 }
7749
7750 #[test]
7751 fn test_price_momentum_score_positive_for_rising_prices() {
7752 use rust_decimal_macros::dec;
7753 let ticks = vec![
7754 make_tick_pq(dec!(100), dec!(1)),
7755 make_tick_pq(dec!(102), dec!(2)),
7756 make_tick_pq(dec!(104), dec!(2)),
7757 ];
7758 let s = NormalizedTick::price_momentum_score(&ticks).unwrap();
7759 assert!(s > 0.0, "rising prices → positive momentum, got {}", s);
7760 }
7761
7762 #[test]
7763 fn test_vwap_std_none_for_single_tick() {
7764 use rust_decimal_macros::dec;
7765 let t = make_tick_pq(dec!(100), dec!(1));
7766 assert!(NormalizedTick::vwap_std(&[t]).is_none());
7767 }
7768
7769 #[test]
7770 fn test_vwap_std_zero_for_constant_price() {
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!(2)),
7775 make_tick_pq(dec!(100), dec!(3)),
7776 ];
7777 let s = NormalizedTick::vwap_std(&ticks).unwrap();
7778 assert!(s.abs() < 1e-9, "constant price → vwap_std=0, got {}", s);
7779 }
7780
7781 #[test]
7782 fn test_price_range_expansion_none_for_empty() {
7783 assert!(NormalizedTick::price_range_expansion(&[]).is_none());
7784 }
7785
7786 #[test]
7787 fn test_price_range_expansion_monotone_rising() {
7788 use rust_decimal_macros::dec;
7789 let ticks = vec![
7790 make_tick_pq(dec!(100), dec!(1)),
7791 make_tick_pq(dec!(101), dec!(1)),
7792 make_tick_pq(dec!(102), dec!(1)),
7793 make_tick_pq(dec!(103), dec!(1)),
7794 ];
7795 let f = NormalizedTick::price_range_expansion(&ticks).unwrap();
7796 assert!((f - 0.75).abs() < 1e-9, "expected 0.75, got {}", f);
7798 }
7799
7800 #[test]
7801 fn test_sell_to_total_volume_ratio_none_for_empty() {
7802 assert!(NormalizedTick::sell_to_total_volume_ratio(&[]).is_none());
7803 }
7804
7805 #[test]
7806 fn test_sell_to_total_volume_ratio_zero_for_all_buys() {
7807 use rust_decimal_macros::dec;
7808 let mut t1 = make_tick_pq(dec!(100), dec!(5));
7809 t1.side = Some(crate::tick::TradeSide::Buy);
7810 let mut t2 = make_tick_pq(dec!(101), dec!(3));
7811 t2.side = Some(crate::tick::TradeSide::Buy);
7812 let r = NormalizedTick::sell_to_total_volume_ratio(&[t1, t2]).unwrap();
7813 assert!(r.abs() < 1e-9, "all buys → sell ratio=0, got {}", r);
7814 }
7815
7816 #[test]
7817 fn test_notional_std_none_for_single_tick() {
7818 use rust_decimal_macros::dec;
7819 let t = make_tick_pq(dec!(100), dec!(1));
7820 assert!(NormalizedTick::notional_std(&[t]).is_none());
7821 }
7822
7823 #[test]
7824 fn test_notional_std_zero_for_identical_notionals() {
7825 use rust_decimal_macros::dec;
7826 let t1 = make_tick_pq(dec!(100), dec!(2));
7827 let t2 = make_tick_pq(dec!(100), dec!(2));
7828 let s = NormalizedTick::notional_std(&[t1, t2]).unwrap();
7829 assert!(s.abs() < 1e-9, "identical notionals → std=0, got {}", s);
7830 }
7831
7832 #[test]
7835 fn test_buy_price_mean_none_when_no_buys() {
7836 use rust_decimal_macros::dec;
7837 let t = make_tick_pq(dec!(100), dec!(1)); assert!(NormalizedTick::buy_price_mean(&[t]).is_none());
7839 }
7840
7841 #[test]
7842 fn test_buy_price_mean_correct_value() {
7843 use rust_decimal_macros::dec;
7844 let mut t1 = make_tick_pq(dec!(100), dec!(1));
7845 t1.side = Some(crate::tick::TradeSide::Buy);
7846 let mut t2 = make_tick_pq(dec!(102), dec!(1));
7847 t2.side = Some(crate::tick::TradeSide::Buy);
7848 let mean = NormalizedTick::buy_price_mean(&[t1, t2]).unwrap();
7849 assert_eq!(mean, dec!(101));
7850 }
7851
7852 #[test]
7853 fn test_sell_price_mean_none_when_no_sells() {
7854 use rust_decimal_macros::dec;
7855 let t = make_tick_pq(dec!(100), dec!(1));
7856 assert!(NormalizedTick::sell_price_mean(&[t]).is_none());
7857 }
7858
7859 #[test]
7860 fn test_price_efficiency_none_for_single_tick() {
7861 use rust_decimal_macros::dec;
7862 let t = make_tick_pq(dec!(100), dec!(1));
7863 assert!(NormalizedTick::price_efficiency(&[t]).is_none());
7864 }
7865
7866 #[test]
7867 fn test_price_efficiency_one_for_directional() {
7868 use rust_decimal_macros::dec;
7869 let ticks = vec![
7870 make_tick_pq(dec!(100), dec!(1)),
7871 make_tick_pq(dec!(102), dec!(1)),
7872 make_tick_pq(dec!(104), dec!(1)),
7873 ];
7874 let e = NormalizedTick::price_efficiency(&ticks).unwrap();
7875 assert!((e - 1.0).abs() < 1e-9, "monotone rising → efficiency=1, got {}", e);
7876 }
7877
7878 #[test]
7879 fn test_price_return_skewness_none_for_few_ticks() {
7880 use rust_decimal_macros::dec;
7881 let ticks = vec![
7882 make_tick_pq(dec!(100), dec!(1)),
7883 make_tick_pq(dec!(101), dec!(1)),
7884 make_tick_pq(dec!(102), dec!(1)),
7885 ];
7886 assert!(NormalizedTick::price_return_skewness(&ticks).is_none());
7887 }
7888
7889 #[test]
7890 fn test_buy_sell_vwap_spread_none_when_no_sides() {
7891 use rust_decimal_macros::dec;
7892 let ticks = vec![
7893 make_tick_pq(dec!(100), dec!(1)),
7894 make_tick_pq(dec!(101), dec!(1)),
7895 ];
7896 assert!(NormalizedTick::buy_sell_vwap_spread(&ticks).is_none());
7897 }
7898
7899 #[test]
7900 fn test_above_mean_quantity_fraction_none_for_empty() {
7901 assert!(NormalizedTick::above_mean_quantity_fraction(&[]).is_none());
7902 }
7903
7904 #[test]
7905 fn test_above_mean_quantity_fraction_in_range() {
7906 use rust_decimal_macros::dec;
7907 let ticks = vec![
7908 make_tick_pq(dec!(100), dec!(1)),
7909 make_tick_pq(dec!(100), dec!(5)),
7910 make_tick_pq(dec!(100), dec!(3)),
7911 ];
7912 let f = NormalizedTick::above_mean_quantity_fraction(&ticks).unwrap();
7913 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
7914 }
7915
7916 #[test]
7917 fn test_price_unchanged_fraction_none_for_single_tick() {
7918 use rust_decimal_macros::dec;
7919 let t = make_tick_pq(dec!(100), dec!(1));
7920 assert!(NormalizedTick::price_unchanged_fraction(&[t]).is_none());
7921 }
7922
7923 #[test]
7924 fn test_price_unchanged_fraction_zero_for_all_changing() {
7925 use rust_decimal_macros::dec;
7926 let ticks = vec![
7927 make_tick_pq(dec!(100), dec!(1)),
7928 make_tick_pq(dec!(101), dec!(1)),
7929 make_tick_pq(dec!(102), dec!(1)),
7930 ];
7931 let f = NormalizedTick::price_unchanged_fraction(&ticks).unwrap();
7932 assert!(f.abs() < 1e-9, "all prices different → unchanged=0, got {}", f);
7933 }
7934
7935 #[test]
7936 fn test_qty_weighted_range_none_for_empty() {
7937 assert!(NormalizedTick::qty_weighted_range(&[]).is_none());
7938 }
7939
7940 #[test]
7941 fn test_qty_weighted_range_zero_for_single_tick() {
7942 use rust_decimal_macros::dec;
7943 let t = make_tick_pq(dec!(100), dec!(2));
7944 let r = NormalizedTick::qty_weighted_range(&[t]).unwrap();
7945 assert!(r.abs() < 1e-9, "single tick → range=0, got {}", r);
7946 }
7947
7948 #[test]
7951 fn test_sell_notional_fraction_none_for_empty() {
7952 assert!(NormalizedTick::sell_notional_fraction(&[]).is_none());
7953 }
7954
7955 #[test]
7956 fn test_sell_notional_fraction_zero_for_all_buys() {
7957 use rust_decimal_macros::dec;
7958 let mut t1 = make_tick_pq(dec!(100), dec!(3));
7959 t1.side = Some(crate::tick::TradeSide::Buy);
7960 let r = NormalizedTick::sell_notional_fraction(&[t1]).unwrap();
7961 assert!(r.abs() < 1e-9, "all buys → sell fraction=0, got {}", r);
7962 }
7963
7964 #[test]
7965 fn test_max_price_gap_none_for_single_tick() {
7966 use rust_decimal_macros::dec;
7967 let t = make_tick_pq(dec!(100), dec!(1));
7968 assert!(NormalizedTick::max_price_gap(&[t]).is_none());
7969 }
7970
7971 #[test]
7972 fn test_max_price_gap_correct_value() {
7973 use rust_decimal_macros::dec;
7974 let ticks = vec![
7975 make_tick_pq(dec!(100), dec!(1)),
7976 make_tick_pq(dec!(105), dec!(1)),
7977 make_tick_pq(dec!(103), dec!(1)),
7978 ];
7979 assert_eq!(NormalizedTick::max_price_gap(&ticks).unwrap(), dec!(5));
7980 }
7981
7982 #[test]
7983 fn test_price_range_velocity_none_for_single_tick() {
7984 use rust_decimal_macros::dec;
7985 let t = make_tick_pq(dec!(100), dec!(1));
7986 assert!(NormalizedTick::price_range_velocity(&[t]).is_none());
7987 }
7988
7989 #[test]
7990 fn test_tick_count_per_ms_none_for_single_tick() {
7991 use rust_decimal_macros::dec;
7992 let t = make_tick_pq(dec!(100), dec!(1));
7993 assert!(NormalizedTick::tick_count_per_ms(&[t]).is_none());
7994 }
7995
7996 #[test]
7997 fn test_buy_quantity_fraction_none_for_empty() {
7998 assert!(NormalizedTick::buy_quantity_fraction(&[]).is_none());
7999 }
8000
8001 #[test]
8002 fn test_buy_quantity_fraction_one_for_all_buys() {
8003 use rust_decimal_macros::dec;
8004 let mut t = make_tick_pq(dec!(100), dec!(5));
8005 t.side = Some(crate::tick::TradeSide::Buy);
8006 let f = NormalizedTick::buy_quantity_fraction(&[t]).unwrap();
8007 assert!((f - 1.0).abs() < 1e-9, "all buys → buy fraction=1, got {}", f);
8008 }
8009
8010 #[test]
8011 fn test_sell_quantity_fraction_none_for_empty() {
8012 assert!(NormalizedTick::sell_quantity_fraction(&[]).is_none());
8013 }
8014
8015 #[test]
8016 fn test_sell_quantity_fraction_one_for_all_sells() {
8017 use rust_decimal_macros::dec;
8018 let mut t = make_tick_pq(dec!(100), dec!(5));
8019 t.side = Some(crate::tick::TradeSide::Sell);
8020 let f = NormalizedTick::sell_quantity_fraction(&[t]).unwrap();
8021 assert!((f - 1.0).abs() < 1e-9, "all sells → sell fraction=1, got {}", f);
8022 }
8023
8024 #[test]
8025 fn test_price_mean_crossover_count_none_for_single_tick() {
8026 use rust_decimal_macros::dec;
8027 let t = make_tick_pq(dec!(100), dec!(1));
8028 assert!(NormalizedTick::price_mean_crossover_count(&[t]).is_none());
8029 }
8030
8031 #[test]
8032 fn test_price_mean_crossover_count_in_range() {
8033 use rust_decimal_macros::dec;
8034 let ticks = vec![
8035 make_tick_pq(dec!(90), dec!(1)),
8036 make_tick_pq(dec!(110), dec!(1)),
8037 make_tick_pq(dec!(90), dec!(1)),
8038 ];
8039 let c = NormalizedTick::price_mean_crossover_count(&ticks).unwrap();
8040 assert!(c >= 1, "expect at least 1 crossover, got {}", c);
8041 }
8042
8043 #[test]
8044 fn test_notional_skewness_none_for_two_ticks() {
8045 use rust_decimal_macros::dec;
8046 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(101), dec!(1))];
8047 assert!(NormalizedTick::notional_skewness(&ticks).is_none());
8048 }
8049
8050 #[test]
8051 fn test_volume_weighted_mid_price_none_for_empty() {
8052 assert!(NormalizedTick::volume_weighted_mid_price(&[]).is_none());
8053 }
8054
8055 #[test]
8056 fn test_volume_weighted_mid_price_equals_price_for_single_tick() {
8057 use rust_decimal_macros::dec;
8058 let t = make_tick_pq(dec!(123), dec!(5));
8059 let mid = NormalizedTick::volume_weighted_mid_price(&[t]).unwrap();
8060 assert_eq!(mid, dec!(123));
8061 }
8062
8063 #[test]
8066 fn test_neutral_count_zero_when_all_sided() {
8067 use rust_decimal_macros::dec;
8068 let mut t = make_tick_pq(dec!(100), dec!(1));
8069 t.side = Some(crate::tick::TradeSide::Buy);
8070 assert_eq!(NormalizedTick::neutral_count(&[t]), 0);
8071 }
8072
8073 #[test]
8074 fn test_neutral_count_all_when_no_side() {
8075 use rust_decimal_macros::dec;
8076 let t1 = make_tick_pq(dec!(100), dec!(1));
8077 let t2 = make_tick_pq(dec!(101), dec!(1));
8078 assert_eq!(NormalizedTick::neutral_count(&[t1, t2]), 2);
8079 }
8080
8081 #[test]
8082 fn test_price_dispersion_none_for_empty() {
8083 assert!(NormalizedTick::price_dispersion(&[]).is_none());
8084 }
8085
8086 #[test]
8087 fn test_price_dispersion_zero_for_single() {
8088 use rust_decimal_macros::dec;
8089 let t = make_tick_pq(dec!(100), dec!(1));
8090 assert_eq!(NormalizedTick::price_dispersion(&[t]).unwrap(), dec!(0));
8091 }
8092
8093 #[test]
8094 fn test_max_notional_none_for_empty() {
8095 assert!(NormalizedTick::max_notional(&[]).is_none());
8096 }
8097
8098 #[test]
8099 fn test_max_notional_selects_largest() {
8100 use rust_decimal_macros::dec;
8101 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));
8104 }
8105
8106 #[test]
8107 fn test_min_notional_none_for_empty() {
8108 assert!(NormalizedTick::min_notional(&[]).is_none());
8109 }
8110
8111 #[test]
8112 fn test_below_vwap_fraction_none_for_empty() {
8113 assert!(NormalizedTick::below_vwap_fraction(&[]).is_none());
8114 }
8115
8116 #[test]
8117 fn test_trade_notional_std_none_for_single() {
8118 use rust_decimal_macros::dec;
8119 let t = make_tick_pq(dec!(100), dec!(1));
8120 assert!(NormalizedTick::trade_notional_std(&[t]).is_none());
8121 }
8122
8123 #[test]
8124 fn test_buy_sell_count_ratio_none_for_no_sells() {
8125 use rust_decimal_macros::dec;
8126 let mut t = make_tick_pq(dec!(100), dec!(1));
8127 t.side = Some(crate::tick::TradeSide::Buy);
8128 assert!(NormalizedTick::buy_sell_count_ratio(&[t]).is_none());
8129 }
8130
8131 #[test]
8132 fn test_buy_sell_count_ratio_correct() {
8133 use rust_decimal_macros::dec;
8134 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8135 t1.side = Some(crate::tick::TradeSide::Buy);
8136 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8137 t2.side = Some(crate::tick::TradeSide::Sell);
8138 let r = NormalizedTick::buy_sell_count_ratio(&[t1, t2]).unwrap();
8139 assert!((r - 1.0).abs() < 1e-9, "1 buy / 1 sell = 1.0, got {}", r);
8140 }
8141
8142 #[test]
8143 fn test_price_mad_none_for_empty() {
8144 assert!(NormalizedTick::price_mad(&[]).is_none());
8145 }
8146
8147 #[test]
8148 fn test_price_mad_zero_for_constant_price() {
8149 use rust_decimal_macros::dec;
8150 let ticks = vec![
8151 make_tick_pq(dec!(100), dec!(1)),
8152 make_tick_pq(dec!(100), dec!(2)),
8153 ];
8154 let m = NormalizedTick::price_mad(&ticks).unwrap();
8155 assert!(m.abs() < 1e-9, "constant price → MAD=0, got {}", m);
8156 }
8157
8158 #[test]
8159 fn test_price_range_pct_of_open_none_for_empty() {
8160 assert!(NormalizedTick::price_range_pct_of_open(&[]).is_none());
8161 }
8162
8163 #[test]
8164 fn test_price_range_pct_of_open_zero_for_constant() {
8165 use rust_decimal_macros::dec;
8166 let ticks = vec![
8167 make_tick_pq(dec!(100), dec!(1)),
8168 make_tick_pq(dec!(100), dec!(1)),
8169 ];
8170 let p = NormalizedTick::price_range_pct_of_open(&ticks).unwrap();
8171 assert!(p.abs() < 1e-9, "constant → range_pct=0, got {}", p);
8172 }
8173
8174 #[test]
8177 fn test_price_mean_none_for_empty() {
8178 assert!(NormalizedTick::price_mean(&[]).is_none());
8179 }
8180
8181 #[test]
8182 fn test_price_mean_correct() {
8183 use rust_decimal_macros::dec;
8184 let ticks = vec![make_tick_pq(dec!(100), dec!(1)), make_tick_pq(dec!(200), dec!(1))];
8185 assert_eq!(NormalizedTick::price_mean(&ticks).unwrap(), dec!(150));
8186 }
8187
8188 #[test]
8189 fn test_uptick_count_zero_for_single() {
8190 use rust_decimal_macros::dec;
8191 let t = make_tick_pq(dec!(100), dec!(1));
8192 assert_eq!(NormalizedTick::uptick_count(&[t]), 0);
8193 }
8194
8195 #[test]
8196 fn test_uptick_count_correct() {
8197 use rust_decimal_macros::dec;
8198 let ticks = vec![
8199 make_tick_pq(dec!(100), dec!(1)),
8200 make_tick_pq(dec!(101), dec!(1)),
8201 make_tick_pq(dec!(100), dec!(1)),
8202 ];
8203 assert_eq!(NormalizedTick::uptick_count(&ticks), 1);
8204 }
8205
8206 #[test]
8207 fn test_downtick_count_zero_for_all_up() {
8208 use rust_decimal_macros::dec;
8209 let ticks = vec![
8210 make_tick_pq(dec!(100), dec!(1)),
8211 make_tick_pq(dec!(101), dec!(1)),
8212 make_tick_pq(dec!(102), dec!(1)),
8213 ];
8214 assert_eq!(NormalizedTick::downtick_count(&ticks), 0);
8215 }
8216
8217 #[test]
8218 fn test_uptick_fraction_none_for_single() {
8219 use rust_decimal_macros::dec;
8220 let t = make_tick_pq(dec!(100), dec!(1));
8221 assert!(NormalizedTick::uptick_fraction(&[t]).is_none());
8222 }
8223
8224 #[test]
8225 fn test_quantity_std_none_for_single() {
8226 use rust_decimal_macros::dec;
8227 let t = make_tick_pq(dec!(100), dec!(1));
8228 assert!(NormalizedTick::quantity_std(&[t]).is_none());
8229 }
8230
8231 #[test]
8232 fn test_quantity_std_zero_for_constant_qty() {
8233 use rust_decimal_macros::dec;
8234 let ticks = vec![
8235 make_tick_pq(dec!(100), dec!(5)),
8236 make_tick_pq(dec!(101), dec!(5)),
8237 ];
8238 let s = NormalizedTick::quantity_std(&ticks).unwrap();
8239 assert!(s.abs() < 1e-9, "constant quantity → std=0, got {}", s);
8240 }
8241
8242 #[test]
8245 fn test_vwap_deviation_std_none_for_single() {
8246 use rust_decimal_macros::dec;
8247 let t = make_tick_pq(dec!(100), dec!(1));
8248 assert!(NormalizedTick::vwap_deviation_std(&[t]).is_none());
8249 }
8250
8251 #[test]
8252 fn test_vwap_deviation_std_zero_for_single_price() {
8253 use rust_decimal_macros::dec;
8254 let ticks = vec![
8255 make_tick_pq(dec!(100), dec!(1)),
8256 make_tick_pq(dec!(100), dec!(2)),
8257 ];
8258 let s = NormalizedTick::vwap_deviation_std(&ticks).unwrap();
8260 assert!(s.abs() < 1e-9, "all at VWAP → std=0, got {}", s);
8261 }
8262
8263 #[test]
8264 fn test_vwap_deviation_std_positive_for_varied_prices() {
8265 use rust_decimal_macros::dec;
8266 let ticks = vec![
8267 make_tick_pq(dec!(100), dec!(1)),
8268 make_tick_pq(dec!(110), dec!(1)),
8269 make_tick_pq(dec!(90), dec!(1)),
8270 ];
8271 let s = NormalizedTick::vwap_deviation_std(&ticks).unwrap();
8272 assert!(s > 0.0, "varied prices → std > 0, got {}", s);
8273 }
8274
8275 #[test]
8276 fn test_max_consecutive_side_run_zero_for_no_side() {
8277 use rust_decimal_macros::dec;
8278 let ticks = vec![
8279 make_tick_pq(dec!(100), dec!(1)),
8280 make_tick_pq(dec!(101), dec!(1)),
8281 ];
8282 assert_eq!(NormalizedTick::max_consecutive_side_run(&ticks), 0);
8283 }
8284
8285 #[test]
8286 fn test_max_consecutive_side_run_with_sides() {
8287 use rust_decimal_macros::dec;
8288 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8289 t1.side = Some(TradeSide::Buy);
8290 let mut t2 = make_tick_pq(dec!(101), dec!(1));
8291 t2.side = Some(TradeSide::Buy);
8292 let mut t3 = make_tick_pq(dec!(102), dec!(1));
8293 t3.side = Some(TradeSide::Sell);
8294 assert_eq!(NormalizedTick::max_consecutive_side_run(&[t1, t2, t3]), 2);
8295 }
8296
8297 #[test]
8298 fn test_inter_arrival_cv_none_for_single() {
8299 use rust_decimal_macros::dec;
8300 let t = make_tick_pq(dec!(100), dec!(1));
8301 assert!(NormalizedTick::inter_arrival_cv(&[t]).is_none());
8302 }
8303
8304 #[test]
8305 fn test_inter_arrival_cv_zero_for_uniform_spacing() {
8306 use rust_decimal_macros::dec;
8307 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8308 t1.received_at_ms = 1000;
8309 let mut t2 = make_tick_pq(dec!(101), dec!(1));
8310 t2.received_at_ms = 2000;
8311 let mut t3 = make_tick_pq(dec!(102), dec!(1));
8312 t3.received_at_ms = 3000;
8313 let cv = NormalizedTick::inter_arrival_cv(&[t1, t2, t3]).unwrap();
8315 assert!(cv.abs() < 1e-9, "uniform spacing → cv=0, got {}", cv);
8316 }
8317
8318 #[test]
8319 fn test_volume_per_ms_none_for_single() {
8320 use rust_decimal_macros::dec;
8321 let t = make_tick_pq(dec!(100), dec!(5));
8322 assert!(NormalizedTick::volume_per_ms(&[t]).is_none());
8323 }
8324
8325 #[test]
8326 fn test_volume_per_ms_correct() {
8327 use rust_decimal_macros::dec;
8328 let mut t1 = make_tick_pq(dec!(100), dec!(5));
8329 t1.received_at_ms = 1000;
8330 let mut t2 = make_tick_pq(dec!(101), dec!(5));
8331 t2.received_at_ms = 2000;
8332 let r = NormalizedTick::volume_per_ms(&[t1, t2]).unwrap();
8334 assert!((r - 0.01).abs() < 1e-9, "expected 0.01, got {}", r);
8335 }
8336
8337 #[test]
8338 fn test_notional_per_second_none_for_single() {
8339 use rust_decimal_macros::dec;
8340 let t = make_tick_pq(dec!(100), dec!(1));
8341 assert!(NormalizedTick::notional_per_second(&[t]).is_none());
8342 }
8343
8344 #[test]
8345 fn test_notional_per_second_positive() {
8346 use rust_decimal_macros::dec;
8347 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8348 t1.received_at_ms = 0;
8349 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8350 t2.received_at_ms = 1000; let r = NormalizedTick::notional_per_second(&[t1, t2]).unwrap();
8353 assert!((r - 200.0).abs() < 1e-9, "expected 200, got {}", r);
8354 }
8355
8356 #[test]
8359 fn test_order_flow_imbalance_none_for_empty() {
8360 assert!(NormalizedTick::order_flow_imbalance(&[]).is_none());
8361 }
8362
8363 #[test]
8364 fn test_order_flow_imbalance_pos_one_for_all_buys() {
8365 use rust_decimal_macros::dec;
8366 let mut t = make_tick_pq(dec!(100), dec!(5));
8367 t.side = Some(crate::tick::TradeSide::Buy);
8368 let r = NormalizedTick::order_flow_imbalance(&[t]).unwrap();
8369 assert!((r - 1.0).abs() < 1e-9, "all buys → OFI=+1, got {}", r);
8370 }
8371
8372 #[test]
8373 fn test_order_flow_imbalance_neg_one_for_all_sells() {
8374 use rust_decimal_macros::dec;
8375 let mut t = make_tick_pq(dec!(100), dec!(5));
8376 t.side = Some(crate::tick::TradeSide::Sell);
8377 let r = NormalizedTick::order_flow_imbalance(&[t]).unwrap();
8378 assert!((r + 1.0).abs() < 1e-9, "all sells → OFI=-1, got {}", r);
8379 }
8380
8381 #[test]
8382 fn test_price_qty_up_fraction_none_for_single() {
8383 use rust_decimal_macros::dec;
8384 let t = make_tick_pq(dec!(100), dec!(1));
8385 assert!(NormalizedTick::price_qty_up_fraction(&[t]).is_none());
8386 }
8387
8388 #[test]
8389 fn test_running_high_count_single_tick() {
8390 use rust_decimal_macros::dec;
8391 let t = make_tick_pq(dec!(100), dec!(1));
8392 assert_eq!(NormalizedTick::running_high_count(&[t]), 1);
8393 }
8394
8395 #[test]
8396 fn test_running_low_count_single_tick() {
8397 use rust_decimal_macros::dec;
8398 let t = make_tick_pq(dec!(100), dec!(1));
8399 assert_eq!(NormalizedTick::running_low_count(&[t]), 1);
8400 }
8401
8402 #[test]
8403 fn test_buy_sell_avg_qty_ratio_none_for_no_sells() {
8404 use rust_decimal_macros::dec;
8405 let mut t = make_tick_pq(dec!(100), dec!(5));
8406 t.side = Some(crate::tick::TradeSide::Buy);
8407 assert!(NormalizedTick::buy_sell_avg_qty_ratio(&[t]).is_none());
8408 }
8409
8410 #[test]
8411 fn test_max_price_drop_none_for_single() {
8412 use rust_decimal_macros::dec;
8413 let t = make_tick_pq(dec!(100), dec!(1));
8414 assert!(NormalizedTick::max_price_drop(&[t]).is_none());
8415 }
8416
8417 #[test]
8418 fn test_max_price_rise_none_for_single() {
8419 use rust_decimal_macros::dec;
8420 let t = make_tick_pq(dec!(100), dec!(1));
8421 assert!(NormalizedTick::max_price_rise(&[t]).is_none());
8422 }
8423
8424 #[test]
8425 fn test_max_price_drop_correct() {
8426 use rust_decimal_macros::dec;
8427 let ticks = vec![
8428 make_tick_pq(dec!(100), dec!(1)),
8429 make_tick_pq(dec!(90), dec!(1)), make_tick_pq(dec!(95), dec!(1)), ];
8432 assert_eq!(NormalizedTick::max_price_drop(&ticks).unwrap(), dec!(10));
8433 }
8434
8435 #[test]
8436 fn test_max_price_rise_correct() {
8437 use rust_decimal_macros::dec;
8438 let ticks = vec![
8439 make_tick_pq(dec!(90), dec!(1)),
8440 make_tick_pq(dec!(105), dec!(1)), make_tick_pq(dec!(100), dec!(1)),
8442 ];
8443 assert_eq!(NormalizedTick::max_price_rise(&ticks).unwrap(), dec!(15));
8444 }
8445
8446 #[test]
8447 fn test_buy_trade_count_zero_for_no_sides() {
8448 use rust_decimal_macros::dec;
8449 let t = make_tick_pq(dec!(100), dec!(1));
8450 assert_eq!(NormalizedTick::buy_trade_count(&[t]), 0);
8451 }
8452
8453 #[test]
8454 fn test_buy_trade_count_correct() {
8455 use rust_decimal_macros::dec;
8456 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8457 t1.side = Some(TradeSide::Buy);
8458 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8459 t2.side = Some(TradeSide::Sell);
8460 assert_eq!(NormalizedTick::buy_trade_count(&[t1, t2]), 1);
8461 }
8462
8463 #[test]
8464 fn test_sell_trade_count_correct() {
8465 use rust_decimal_macros::dec;
8466 let mut t1 = make_tick_pq(dec!(100), dec!(1));
8467 t1.side = Some(TradeSide::Buy);
8468 let mut t2 = make_tick_pq(dec!(100), dec!(1));
8469 t2.side = Some(TradeSide::Sell);
8470 assert_eq!(NormalizedTick::sell_trade_count(&[t1, t2]), 1);
8471 }
8472
8473 #[test]
8474 fn test_price_reversal_fraction_none_for_two_ticks() {
8475 use rust_decimal_macros::dec;
8476 let t1 = make_tick_pq(dec!(100), dec!(1));
8477 let t2 = make_tick_pq(dec!(101), dec!(1));
8478 assert!(NormalizedTick::price_reversal_fraction(&[t1, t2]).is_none());
8479 }
8480
8481 #[test]
8482 fn test_price_reversal_fraction_one_for_zigzag() {
8483 use rust_decimal_macros::dec;
8484 let ticks = vec![
8486 make_tick_pq(dec!(100), dec!(1)),
8487 make_tick_pq(dec!(110), dec!(1)),
8488 make_tick_pq(dec!(105), dec!(1)),
8489 make_tick_pq(dec!(115), dec!(1)),
8490 ];
8491 let f = NormalizedTick::price_reversal_fraction(&ticks).unwrap();
8492 assert!((f - 1.0).abs() < 1e-9, "perfect zigzag → 1.0, got {}", f);
8493 }
8494
8495 #[test]
8498 fn test_near_vwap_fraction_none_for_empty() {
8499 use rust_decimal_macros::dec;
8500 assert!(NormalizedTick::near_vwap_fraction(&[], dec!(1)).is_none());
8501 }
8502
8503 #[test]
8504 fn test_near_vwap_fraction_one_for_all_at_vwap() {
8505 use rust_decimal_macros::dec;
8506 let ticks = vec![
8508 make_tick_pq(dec!(100), dec!(1)),
8509 make_tick_pq(dec!(100), dec!(1)),
8510 ];
8511 let f = NormalizedTick::near_vwap_fraction(&ticks, dec!(0)).unwrap();
8512 assert!((f - 1.0).abs() < 1e-9, "all at VWAP → 1.0, got {}", f);
8513 }
8514
8515 #[test]
8516 fn test_mean_tick_return_none_for_single() {
8517 use rust_decimal_macros::dec;
8518 let t = make_tick_pq(dec!(100), dec!(1));
8519 assert!(NormalizedTick::mean_tick_return(&[t]).is_none());
8520 }
8521
8522 #[test]
8523 fn test_mean_tick_return_zero_for_constant_price() {
8524 use rust_decimal_macros::dec;
8525 let ticks = vec![
8526 make_tick_pq(dec!(100), dec!(1)),
8527 make_tick_pq(dec!(100), dec!(1)),
8528 make_tick_pq(dec!(100), dec!(1)),
8529 ];
8530 let r = NormalizedTick::mean_tick_return(&ticks).unwrap();
8531 assert!(r.abs() < 1e-9, "constant price → mean_return=0, got {}", r);
8532 }
8533
8534 #[test]
8535 fn test_passive_buy_count_zero_for_no_sides() {
8536 use rust_decimal_macros::dec;
8537 let t = make_tick_pq(dec!(100), dec!(1));
8538 assert_eq!(NormalizedTick::passive_buy_count(&[t]), 0);
8539 }
8540
8541 #[test]
8542 fn test_quantity_iqr_none_for_small_slice() {
8543 use rust_decimal_macros::dec;
8544 let ticks = vec![
8545 make_tick_pq(dec!(100), dec!(1)),
8546 make_tick_pq(dec!(101), dec!(2)),
8547 ];
8548 assert!(NormalizedTick::quantity_iqr(&ticks).is_none());
8549 }
8550
8551 #[test]
8552 fn test_quantity_iqr_positive_for_varied_quantities() {
8553 use rust_decimal_macros::dec;
8554 let ticks: Vec<_> = [dec!(1), dec!(2), dec!(8), dec!(16), dec!(32), dec!(64), dec!(128), dec!(256)]
8555 .iter()
8556 .map(|&q| make_tick_pq(dec!(100), q))
8557 .collect();
8558 let iqr = NormalizedTick::quantity_iqr(&ticks).unwrap();
8559 assert!(iqr > dec!(0));
8560 }
8561
8562 #[test]
8563 fn test_top_quartile_price_fraction_none_for_small_slice() {
8564 use rust_decimal_macros::dec;
8565 let ticks = vec![
8566 make_tick_pq(dec!(100), dec!(1)),
8567 make_tick_pq(dec!(101), dec!(1)),
8568 ];
8569 assert!(NormalizedTick::top_quartile_price_fraction(&ticks).is_none());
8570 }
8571
8572 #[test]
8573 fn test_buy_notional_ratio_none_for_empty() {
8574 assert!(NormalizedTick::buy_notional_ratio(&[]).is_none());
8575 }
8576
8577 #[test]
8578 fn test_buy_notional_ratio_one_for_all_buys() {
8579 use rust_decimal_macros::dec;
8580 let mut t = make_tick_pq(dec!(100), dec!(1));
8581 t.side = Some(TradeSide::Buy);
8582 let r = NormalizedTick::buy_notional_ratio(&[t]).unwrap();
8583 assert!((r - 1.0).abs() < 1e-9, "all buys → ratio=1, got {}", r);
8584 }
8585
8586 #[test]
8587 fn test_return_std_none_for_two_ticks() {
8588 use rust_decimal_macros::dec;
8589 let t1 = make_tick_pq(dec!(100), dec!(1));
8590 let t2 = make_tick_pq(dec!(101), dec!(1));
8591 assert!(NormalizedTick::return_std(&[t1, t2]).is_none());
8592 }
8593
8594 #[test]
8595 fn test_return_std_zero_for_constant_price() {
8596 use rust_decimal_macros::dec;
8597 let ticks = vec![
8598 make_tick_pq(dec!(100), dec!(1)),
8599 make_tick_pq(dec!(100), dec!(1)),
8600 make_tick_pq(dec!(100), dec!(1)),
8601 ];
8602 let s = NormalizedTick::return_std(&ticks).unwrap();
8603 assert!(s.abs() < 1e-9, "constant price → return_std=0, got {}", s);
8604 }
8605
8606 #[test]
8609 fn test_max_drawdown_none_for_empty() {
8610 assert!(NormalizedTick::max_drawdown(&[]).is_none());
8611 }
8612
8613 #[test]
8614 fn test_max_drawdown_zero_for_rising_prices() {
8615 use rust_decimal_macros::dec;
8616 let ticks = vec![
8617 make_tick_pq(dec!(100), dec!(1)),
8618 make_tick_pq(dec!(110), dec!(1)),
8619 make_tick_pq(dec!(120), dec!(1)),
8620 ];
8621 let dd = NormalizedTick::max_drawdown(&ticks).unwrap();
8622 assert!(dd.abs() < 1e-9, "monotone rise → drawdown=0, got {}", dd);
8623 }
8624
8625 #[test]
8626 fn test_max_drawdown_positive_after_peak() {
8627 use rust_decimal_macros::dec;
8628 let ticks = vec![
8629 make_tick_pq(dec!(100), dec!(1)),
8630 make_tick_pq(dec!(120), dec!(1)),
8631 make_tick_pq(dec!(90), dec!(1)),
8632 ];
8633 let dd = NormalizedTick::max_drawdown(&ticks).unwrap();
8634 assert!((dd - 0.25).abs() < 1e-6, "expected 0.25, got {}", dd);
8636 }
8637
8638 #[test]
8639 fn test_high_to_low_ratio_none_for_empty() {
8640 assert!(NormalizedTick::high_to_low_ratio(&[]).is_none());
8641 }
8642
8643 #[test]
8644 fn test_high_to_low_ratio_one_for_constant_price() {
8645 use rust_decimal_macros::dec;
8646 let ticks = vec![
8647 make_tick_pq(dec!(100), dec!(1)),
8648 make_tick_pq(dec!(100), dec!(1)),
8649 ];
8650 let r = NormalizedTick::high_to_low_ratio(&ticks).unwrap();
8651 assert!((r - 1.0).abs() < 1e-9, "constant price → ratio=1, got {}", r);
8652 }
8653
8654 #[test]
8655 fn test_tick_velocity_none_for_single_tick() {
8656 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8657 assert!(NormalizedTick::tick_velocity(&[t]).is_none());
8658 }
8659
8660 #[test]
8661 fn test_notional_decay_none_for_single_tick() {
8662 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8663 assert!(NormalizedTick::notional_decay(&[t]).is_none());
8664 }
8665
8666 #[test]
8667 fn test_notional_decay_one_for_balanced_halves() {
8668 use rust_decimal_macros::dec;
8669 let t1 = make_tick_pq(dec!(100), dec!(1));
8670 let t2 = make_tick_pq(dec!(100), dec!(1));
8671 let r = NormalizedTick::notional_decay(&[t1, t2]).unwrap();
8672 assert!((r - 1.0).abs() < 1e-9, "equal halves → ratio=1, got {}", r);
8673 }
8674
8675 #[test]
8676 fn test_late_price_momentum_none_for_single_tick() {
8677 let t = make_tick_pq(rust_decimal_macros::dec!(100), rust_decimal_macros::dec!(1));
8678 assert!(NormalizedTick::late_price_momentum(&[t]).is_none());
8679 }
8680
8681 #[test]
8682 fn test_consecutive_buys_max_zero_for_empty() {
8683 assert_eq!(NormalizedTick::consecutive_buys_max(&[]), 0);
8684 }
8685
8686 #[test]
8687 fn test_consecutive_buys_max_two_for_run_of_two() {
8688 use rust_decimal_macros::dec;
8689 let mut buy1 = make_tick_pq(dec!(100), dec!(1));
8690 buy1.side = Some(TradeSide::Buy);
8691 let mut buy2 = make_tick_pq(dec!(101), dec!(1));
8692 buy2.side = Some(TradeSide::Buy);
8693 let mut sell = make_tick_pq(dec!(102), dec!(1));
8694 sell.side = Some(TradeSide::Sell);
8695 assert_eq!(NormalizedTick::consecutive_buys_max(&[buy1, buy2, sell]), 2);
8696 }
8697}