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 {
178 self.price * self.quantity
179 }
180
181 pub fn age_ms(&self, now_ms: u64) -> u64 {
186 now_ms.saturating_sub(self.received_at_ms)
187 }
188
189 pub fn is_stale(&self, now_ms: u64, threshold_ms: u64) -> bool {
194 self.age_ms(now_ms) > threshold_ms
195 }
196
197 pub fn is_buy(&self) -> bool {
201 self.side == Some(TradeSide::Buy)
202 }
203
204 pub fn is_sell(&self) -> bool {
208 self.side == Some(TradeSide::Sell)
209 }
210
211 pub fn is_neutral(&self) -> bool {
217 self.side.is_none()
218 }
219
220 pub fn is_large_trade(&self, threshold: Decimal) -> bool {
225 self.quantity >= threshold
226 }
227
228 pub fn with_side(mut self, side: TradeSide) -> Self {
233 self.side = Some(side);
234 self
235 }
236
237 pub fn with_exchange_ts(mut self, ts_ms: u64) -> Self {
242 self.exchange_ts_ms = Some(ts_ms);
243 self
244 }
245
246 pub fn price_move_from(&self, prev: &NormalizedTick) -> Decimal {
251 self.price - prev.price
252 }
253
254 pub fn is_more_recent_than(&self, other: &NormalizedTick) -> bool {
258 self.received_at_ms > other.received_at_ms
259 }
260
261 pub fn latency_ms(&self) -> Option<i64> {
267 let exchange_ts = self.exchange_ts_ms? as i64;
268 Some(self.received_at_ms as i64 - exchange_ts)
269 }
270
271 pub fn volume_notional(&self) -> rust_decimal::Decimal {
273 self.price * self.quantity
274 }
275
276 pub fn has_exchange_ts(&self) -> bool {
282 self.exchange_ts_ms.is_some()
283 }
284
285 pub fn side_str(&self) -> &'static str {
287 match self.side {
288 Some(TradeSide::Buy) => "buy",
289 Some(TradeSide::Sell) => "sell",
290 None => "unknown",
291 }
292 }
293
294 pub fn is_round_lot(&self) -> bool {
299 self.quantity.fract().is_zero()
300 }
301
302 pub fn is_same_symbol_as(&self, other: &NormalizedTick) -> bool {
304 self.symbol == other.symbol
305 }
306
307 pub fn price_distance_from(&self, other: &NormalizedTick) -> Decimal {
312 (self.price - other.price).abs()
313 }
314
315 pub fn exchange_latency_ms(&self) -> Option<i64> {
324 self.exchange_ts_ms
325 .map(|e| self.received_at_ms as i64 - e as i64)
326 }
327
328 pub fn is_notional_large_trade(&self, threshold: Decimal) -> bool {
335 self.volume_notional() > threshold
336 }
337
338 pub fn is_zero_price(&self) -> bool {
342 self.price.is_zero()
343 }
344
345 pub fn is_fresh(&self, now_ms: u64, max_age_ms: u64) -> bool {
350 now_ms.saturating_sub(self.received_at_ms) <= max_age_ms
351 }
352
353 pub fn is_above(&self, price: Decimal) -> bool {
355 self.price > price
356 }
357
358 pub fn is_below(&self, price: Decimal) -> bool {
360 self.price < price
361 }
362
363 pub fn is_at(&self, price: Decimal) -> bool {
365 self.price == price
366 }
367
368 pub fn is_aggressive(&self) -> bool {
372 self.side.is_some()
373 }
374
375 pub fn price_diff_from(&self, other: &NormalizedTick) -> Decimal {
379 self.price - other.price
380 }
381
382 pub fn is_micro_trade(&self, threshold: Decimal) -> bool {
386 self.quantity < threshold
387 }
388
389 pub fn is_buying_pressure(&self, midpoint: Decimal) -> bool {
393 self.price > midpoint
394 }
395
396 pub fn age_secs(&self, now_ms: u64) -> f64 {
400 now_ms.saturating_sub(self.received_at_ms) as f64 / 1_000.0
401 }
402
403 pub fn is_same_exchange_as(&self, other: &NormalizedTick) -> bool {
405 self.exchange == other.exchange
406 }
407
408 pub fn quote_age_ms(&self, now_ms: u64) -> u64 {
412 now_ms.saturating_sub(self.received_at_ms)
413 }
414
415 pub fn notional_value(&self) -> Decimal {
417 self.price * self.quantity
418 }
419
420 pub fn is_high_value_tick(&self, threshold: Decimal) -> bool {
422 self.notional_value() > threshold
423 }
424
425 pub fn side_as_str(&self) -> Option<&'static str> {
427 match self.side {
428 Some(TradeSide::Buy) => Some("buy"),
429 Some(TradeSide::Sell) => Some("sell"),
430 None => None,
431 }
432 }
433
434 pub fn is_above_price(&self, reference: Decimal) -> bool {
436 self.price > reference
437 }
438
439 pub fn price_change_from(&self, reference: Decimal) -> Decimal {
441 self.price - reference
442 }
443
444 pub fn is_market_open_tick(&self, session_start_ms: u64, session_end_ms: u64) -> bool {
446 self.received_at_ms >= session_start_ms && self.received_at_ms < session_end_ms
447 }
448
449 pub fn is_at_price(&self, target: Decimal) -> bool {
451 self.price == target
452 }
453
454 pub fn is_below_price(&self, reference: Decimal) -> bool {
456 self.price < reference
457 }
458
459 pub fn is_round_number(&self, step: Decimal) -> bool {
464 if step.is_zero() {
465 return false;
466 }
467 (self.price % step).is_zero()
468 }
469
470 pub fn signed_quantity(&self) -> Decimal {
472 match self.side {
473 Some(TradeSide::Buy) => self.quantity,
474 Some(TradeSide::Sell) => -self.quantity,
475 None => Decimal::ZERO,
476 }
477 }
478
479 pub fn as_price_level(&self) -> (Decimal, Decimal) {
481 (self.price, self.quantity)
482 }
483
484 pub fn quantity_above(&self, threshold: Decimal) -> bool {
486 self.quantity > threshold
487 }
488
489 pub fn is_recent(&self, threshold_ms: u64, now_ms: u64) -> bool {
491 now_ms.saturating_sub(self.received_at_ms) <= threshold_ms
492 }
493
494 pub fn is_buy_side(&self) -> bool {
498 self.side == Some(TradeSide::Buy)
499 }
500
501 pub fn is_sell_side(&self) -> bool {
505 self.side == Some(TradeSide::Sell)
506 }
507
508 pub fn is_zero_quantity(&self) -> bool {
510 self.quantity.is_zero()
511 }
512
513 pub fn is_within_spread(&self, bid: Decimal, ask: Decimal) -> bool {
515 self.price > bid && self.price < ask
516 }
517
518 pub fn is_away_from_price(&self, reference: Decimal, threshold: Decimal) -> bool {
520 (self.price - reference).abs() > threshold
521 }
522
523 pub fn is_large_tick(&self, threshold: Decimal) -> bool {
525 self.quantity > threshold
526 }
527
528 pub fn price_in_range(&self, low: Decimal, high: Decimal) -> bool {
530 self.price >= low && self.price <= high
531 }
532
533 pub fn rounded_price(&self, tick_size: Decimal) -> Decimal {
537 if tick_size.is_zero() {
538 return self.price;
539 }
540 (self.price / tick_size).floor() * tick_size
541 }
542
543 pub fn is_large_spread_from(&self, other: &NormalizedTick, threshold: Decimal) -> bool {
545 (self.price - other.price).abs() > threshold
546 }
547
548 pub fn volume_notional_f64(&self) -> f64 {
550 use rust_decimal::prelude::ToPrimitive;
551 self.volume_notional().to_f64().unwrap_or(0.0)
552 }
553
554 pub fn price_velocity(&self, prev: &NormalizedTick, dt_ms: u64) -> Option<Decimal> {
558 if dt_ms == 0 { return None; }
559 Some((self.price - prev.price) / Decimal::from(dt_ms))
560 }
561
562 pub fn vwap(ticks: &[NormalizedTick]) -> Option<Decimal> {
566 if ticks.is_empty() { return None; }
567 let total_vol: Decimal = ticks.iter().map(|t| t.quantity).sum();
568 if total_vol.is_zero() { return None; }
569 let total_notional: Decimal = ticks.iter().map(|t| t.price * t.quantity).sum();
570 Some(total_notional / total_vol)
571 }
572
573 pub fn is_reversal(&self, prev: &NormalizedTick, min_move: Decimal) -> bool {
579 let move_size = (self.price - prev.price).abs();
580 move_size >= min_move
581 }
582
583 pub fn spread_crossed(bid_tick: &NormalizedTick, ask_tick: &NormalizedTick) -> bool {
588 bid_tick.price >= ask_tick.price
589 }
590
591 pub fn dollar_value(&self) -> Decimal {
593 self.price * self.quantity
594 }
595
596 pub fn contract_value(&self, multiplier: Decimal) -> Decimal {
598 self.price * self.quantity * multiplier
599 }
600
601 pub fn tick_imbalance(ticks: &[NormalizedTick]) -> Option<f64> {
606 use rust_decimal::prelude::ToPrimitive;
607 let buy_qty: Decimal = ticks.iter()
608 .filter(|t| matches!(t.side, Some(TradeSide::Buy)))
609 .map(|t| t.quantity)
610 .sum();
611 let total_qty: Decimal = ticks.iter().map(|t| t.quantity).sum();
612 if total_qty.is_zero() { return None; }
613 let sell_qty = total_qty - buy_qty;
614 ((buy_qty - sell_qty) / total_qty).to_f64()
615 }
616
617 pub fn quote_midpoint(bid: &NormalizedTick, ask: &NormalizedTick) -> Option<Decimal> {
622 if bid.price <= Decimal::ZERO || ask.price <= Decimal::ZERO {
623 return None;
624 }
625 if bid.price > ask.price {
626 return None;
627 }
628 Some((bid.price + ask.price) / Decimal::TWO)
629 }
630
631}
632
633impl std::fmt::Display for NormalizedTick {
634 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635 let side = match self.side {
636 Some(s) => s.to_string(),
637 None => "?".to_string(),
638 };
639 write!(
640 f,
641 "{} {} {} x {} {} @{}ms",
642 self.exchange, self.symbol, self.price, self.quantity, side, self.received_at_ms
643 )
644 }
645}
646
647pub struct TickNormalizer;
652
653impl TickNormalizer {
654 pub fn new() -> Self {
656 Self
657 }
658
659 pub fn normalize(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
667 let tick = match raw.exchange {
668 Exchange::Binance => self.normalize_binance(raw),
669 Exchange::Coinbase => self.normalize_coinbase(raw),
670 Exchange::Alpaca => self.normalize_alpaca(raw),
671 Exchange::Polygon => self.normalize_polygon(raw),
672 }?;
673 if tick.price <= Decimal::ZERO {
674 return Err(StreamError::InvalidTick {
675 reason: format!("price must be positive, got {}", tick.price),
676 });
677 }
678 if tick.quantity < Decimal::ZERO {
679 return Err(StreamError::InvalidTick {
680 reason: format!("quantity must be non-negative, got {}", tick.quantity),
681 });
682 }
683 trace!(
684 exchange = %tick.exchange,
685 symbol = %tick.symbol,
686 price = %tick.price,
687 exchange_ts_ms = ?tick.exchange_ts_ms,
688 "tick normalized"
689 );
690 Ok(tick)
691 }
692
693 fn normalize_binance(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
694 let p = &raw.payload;
695 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
696 let qty = parse_decimal_field(p, "q", &raw.exchange.to_string())?;
697 let side = p.get("m").and_then(|v| v.as_bool()).map(|maker| {
698 if maker {
699 TradeSide::Sell
700 } else {
701 TradeSide::Buy
702 }
703 });
704 let trade_id = p.get("t").and_then(|v| v.as_u64()).map(|id| id.to_string());
705 let exchange_ts = p.get("T").and_then(|v| v.as_u64());
706 Ok(NormalizedTick {
707 exchange: raw.exchange,
708 symbol: raw.symbol,
709 price,
710 quantity: qty,
711 side,
712 trade_id,
713 exchange_ts_ms: exchange_ts,
714 received_at_ms: raw.received_at_ms,
715 })
716 }
717
718 fn normalize_coinbase(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
719 let p = &raw.payload;
720 let price = parse_decimal_field(p, "price", &raw.exchange.to_string())?;
721 let qty = parse_decimal_field(p, "size", &raw.exchange.to_string())?;
722 let side = p.get("side").and_then(|v| v.as_str()).map(|s| {
723 if s == "buy" {
724 TradeSide::Buy
725 } else {
726 TradeSide::Sell
727 }
728 });
729 let trade_id = p
730 .get("trade_id")
731 .and_then(|v| v.as_str())
732 .map(str::to_string);
733 let exchange_ts_ms = p
735 .get("time")
736 .and_then(|v| v.as_str())
737 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
738 .map(|dt| dt.timestamp_millis() as u64);
739 Ok(NormalizedTick {
740 exchange: raw.exchange,
741 symbol: raw.symbol,
742 price,
743 quantity: qty,
744 side,
745 trade_id,
746 exchange_ts_ms,
747 received_at_ms: raw.received_at_ms,
748 })
749 }
750
751 fn normalize_alpaca(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
752 let p = &raw.payload;
753 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
754 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
755 let trade_id = p.get("i").and_then(|v| v.as_u64()).map(|id| id.to_string());
756 let exchange_ts_ms = p
758 .get("t")
759 .and_then(|v| v.as_str())
760 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
761 .map(|dt| dt.timestamp_millis() as u64);
762 Ok(NormalizedTick {
763 exchange: raw.exchange,
764 symbol: raw.symbol,
765 price,
766 quantity: qty,
767 side: None,
768 trade_id,
769 exchange_ts_ms,
770 received_at_ms: raw.received_at_ms,
771 })
772 }
773
774 fn normalize_polygon(&self, raw: RawTick) -> Result<NormalizedTick, StreamError> {
775 let p = &raw.payload;
776 let price = parse_decimal_field(p, "p", &raw.exchange.to_string())?;
777 let qty = parse_decimal_field(p, "s", &raw.exchange.to_string())?;
778 let trade_id = p.get("i").and_then(|v| v.as_str()).map(str::to_string);
779 let exchange_ts = p
781 .get("t")
782 .and_then(|v| v.as_u64())
783 .map(|t_ns| t_ns / 1_000_000);
784 Ok(NormalizedTick {
785 exchange: raw.exchange,
786 symbol: raw.symbol,
787 price,
788 quantity: qty,
789 side: None,
790 trade_id,
791 exchange_ts_ms: exchange_ts,
792 received_at_ms: raw.received_at_ms,
793 })
794 }
795}
796
797impl Default for TickNormalizer {
798 fn default() -> Self {
799 Self::new()
800 }
801}
802
803fn parse_decimal_field(
804 v: &serde_json::Value,
805 field: &str,
806 exchange: &str,
807) -> Result<Decimal, StreamError> {
808 let raw = v.get(field).ok_or_else(|| StreamError::ParseError {
809 exchange: exchange.to_string(),
810 reason: format!("missing field '{}'", field),
811 })?;
812 let s: String = match raw {
818 serde_json::Value::String(s) => s.clone(),
819 serde_json::Value::Number(n) => n.to_string(),
820 _ => {
821 return Err(StreamError::ParseError {
822 exchange: exchange.to_string(),
823 reason: format!("field '{}' is not a string or number", field),
824 });
825 }
826 };
827 Decimal::from_str(&s).map_err(|e| StreamError::ParseError {
828 exchange: exchange.to_string(),
829 reason: format!("field '{}' parse error: {}", field, e),
830 })
831}
832
833fn now_ms() -> u64 {
834 std::time::SystemTime::now()
835 .duration_since(std::time::UNIX_EPOCH)
836 .map(|d| d.as_millis() as u64)
837 .unwrap_or(0)
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use serde_json::json;
844
845 fn normalizer() -> TickNormalizer {
846 TickNormalizer::new()
847 }
848
849 fn binance_tick(symbol: &str) -> RawTick {
850 RawTick {
851 exchange: Exchange::Binance,
852 symbol: symbol.to_string(),
853 payload: json!({ "p": "50000.12", "q": "0.001", "m": false, "t": 12345, "T": 1700000000000u64 }),
854 received_at_ms: 1700000000001,
855 }
856 }
857
858 fn coinbase_tick(symbol: &str) -> RawTick {
859 RawTick {
860 exchange: Exchange::Coinbase,
861 symbol: symbol.to_string(),
862 payload: json!({ "price": "50001.00", "size": "0.5", "side": "buy", "trade_id": "abc123" }),
863 received_at_ms: 1700000000002,
864 }
865 }
866
867 fn alpaca_tick(symbol: &str) -> RawTick {
868 RawTick {
869 exchange: Exchange::Alpaca,
870 symbol: symbol.to_string(),
871 payload: json!({ "p": "180.50", "s": "10", "i": 99 }),
872 received_at_ms: 1700000000003,
873 }
874 }
875
876 fn polygon_tick(symbol: &str) -> RawTick {
877 RawTick {
878 exchange: Exchange::Polygon,
879 symbol: symbol.to_string(),
880 payload: json!({ "p": "180.51", "s": "5", "i": "XYZ-001", "t": 1_700_000_000_000_000_000u64 }),
882 received_at_ms: 1700000000005,
883 }
884 }
885
886 #[test]
887 fn test_exchange_from_str_valid() {
888 assert_eq!("binance".parse::<Exchange>().unwrap(), Exchange::Binance);
889 assert_eq!("Coinbase".parse::<Exchange>().unwrap(), Exchange::Coinbase);
890 assert_eq!("ALPACA".parse::<Exchange>().unwrap(), Exchange::Alpaca);
891 assert_eq!("polygon".parse::<Exchange>().unwrap(), Exchange::Polygon);
892 }
893
894 #[test]
895 fn test_exchange_from_str_unknown_returns_error() {
896 let result = "Kraken".parse::<Exchange>();
897 assert!(matches!(result, Err(StreamError::UnknownExchange(_))));
898 }
899
900 #[test]
901 fn test_exchange_display() {
902 assert_eq!(Exchange::Binance.to_string(), "Binance");
903 assert_eq!(Exchange::Coinbase.to_string(), "Coinbase");
904 }
905
906 #[test]
907 fn test_normalize_binance_tick_price_and_qty() {
908 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
909 assert_eq!(tick.price, Decimal::from_str("50000.12").unwrap());
910 assert_eq!(tick.quantity, Decimal::from_str("0.001").unwrap());
911 assert_eq!(tick.exchange, Exchange::Binance);
912 assert_eq!(tick.symbol, "BTCUSDT");
913 }
914
915 #[test]
916 fn test_normalize_binance_side_maker_false_is_buy() {
917 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
918 assert_eq!(tick.side, Some(TradeSide::Buy));
919 }
920
921 #[test]
922 fn test_normalize_binance_side_maker_true_is_sell() {
923 let raw = RawTick {
924 exchange: Exchange::Binance,
925 symbol: "BTCUSDT".into(),
926 payload: json!({ "p": "50000", "q": "1", "m": true }),
927 received_at_ms: 0,
928 };
929 let tick = normalizer().normalize(raw).unwrap();
930 assert_eq!(tick.side, Some(TradeSide::Sell));
931 }
932
933 #[test]
934 fn test_normalize_binance_trade_id_and_ts() {
935 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
936 assert_eq!(tick.trade_id, Some("12345".to_string()));
937 assert_eq!(tick.exchange_ts_ms, Some(1700000000000));
938 }
939
940 #[test]
941 fn test_normalize_coinbase_tick() {
942 let tick = normalizer().normalize(coinbase_tick("BTC-USD")).unwrap();
943 assert_eq!(tick.price, Decimal::from_str("50001.00").unwrap());
944 assert_eq!(tick.quantity, Decimal::from_str("0.5").unwrap());
945 assert_eq!(tick.side, Some(TradeSide::Buy));
946 assert_eq!(tick.trade_id, Some("abc123".to_string()));
947 }
948
949 #[test]
950 fn test_normalize_coinbase_sell_side() {
951 let raw = RawTick {
952 exchange: Exchange::Coinbase,
953 symbol: "BTC-USD".into(),
954 payload: json!({ "price": "50000", "size": "1", "side": "sell" }),
955 received_at_ms: 0,
956 };
957 let tick = normalizer().normalize(raw).unwrap();
958 assert_eq!(tick.side, Some(TradeSide::Sell));
959 }
960
961 #[test]
962 fn test_normalize_alpaca_tick() {
963 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
964 assert_eq!(tick.price, Decimal::from_str("180.50").unwrap());
965 assert_eq!(tick.quantity, Decimal::from_str("10").unwrap());
966 assert_eq!(tick.trade_id, Some("99".to_string()));
967 assert_eq!(tick.side, None);
968 }
969
970 #[test]
971 fn test_normalize_polygon_tick() {
972 let tick = normalizer().normalize(polygon_tick("AAPL")).unwrap();
973 assert_eq!(tick.price, Decimal::from_str("180.51").unwrap());
974 assert_eq!(tick.exchange_ts_ms, Some(1_700_000_000_000u64));
976 assert_eq!(tick.trade_id, Some("XYZ-001".to_string()));
977 }
978
979 #[test]
980 fn test_normalize_alpaca_rfc3339_timestamp() {
981 let raw = RawTick {
982 exchange: Exchange::Alpaca,
983 symbol: "AAPL".into(),
984 payload: json!({ "p": "180.50", "s": "10", "i": 99, "t": "2023-11-15T00:00:00Z" }),
985 received_at_ms: 1700000000003,
986 };
987 let tick = normalizer().normalize(raw).unwrap();
988 assert!(tick.exchange_ts_ms.is_some(), "Alpaca 't' field should be parsed");
989 assert_eq!(tick.exchange_ts_ms, Some(1700006400000u64));
991 }
992
993 #[test]
994 fn test_normalize_alpaca_no_timestamp_field() {
995 let tick = normalizer().normalize(alpaca_tick("AAPL")).unwrap();
996 assert_eq!(tick.exchange_ts_ms, None, "missing 't' field means no exchange_ts_ms");
997 }
998
999 #[test]
1000 fn test_normalize_missing_price_field_returns_parse_error() {
1001 let raw = RawTick {
1002 exchange: Exchange::Binance,
1003 symbol: "BTCUSDT".into(),
1004 payload: json!({ "q": "1" }),
1005 received_at_ms: 0,
1006 };
1007 let result = normalizer().normalize(raw);
1008 assert!(matches!(result, Err(StreamError::ParseError { .. })));
1009 }
1010
1011 #[test]
1012 fn test_normalize_invalid_decimal_returns_parse_error() {
1013 let raw = RawTick {
1014 exchange: Exchange::Coinbase,
1015 symbol: "BTC-USD".into(),
1016 payload: json!({ "price": "not-a-number", "size": "1" }),
1017 received_at_ms: 0,
1018 };
1019 let result = normalizer().normalize(raw);
1020 assert!(matches!(result, Err(StreamError::ParseError { .. })));
1021 }
1022
1023 #[test]
1024 fn test_raw_tick_new_sets_received_at() {
1025 let raw = RawTick::new(Exchange::Binance, "BTCUSDT", json!({}));
1026 assert!(raw.received_at_ms > 0);
1027 }
1028
1029 #[test]
1030 fn test_normalize_numeric_price_field() {
1031 let raw = RawTick {
1032 exchange: Exchange::Binance,
1033 symbol: "BTCUSDT".into(),
1034 payload: json!({ "p": 50000.0, "q": 1.0 }),
1035 received_at_ms: 0,
1036 };
1037 let tick = normalizer().normalize(raw).unwrap();
1038 assert!(tick.price > Decimal::ZERO);
1039 }
1040
1041 #[test]
1042 fn test_trade_side_from_str_buy() {
1043 assert_eq!("buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
1044 assert_eq!("Buy".parse::<TradeSide>().unwrap(), TradeSide::Buy);
1045 assert_eq!("BUY".parse::<TradeSide>().unwrap(), TradeSide::Buy);
1046 }
1047
1048 #[test]
1049 fn test_trade_side_from_str_sell() {
1050 assert_eq!("sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
1051 assert_eq!("Sell".parse::<TradeSide>().unwrap(), TradeSide::Sell);
1052 assert_eq!("SELL".parse::<TradeSide>().unwrap(), TradeSide::Sell);
1053 }
1054
1055 #[test]
1056 fn test_trade_side_from_str_invalid() {
1057 let err = "long".parse::<TradeSide>().unwrap_err();
1058 assert!(matches!(err, StreamError::ParseError { .. }));
1059 }
1060
1061 #[test]
1062 fn test_trade_side_display() {
1063 assert_eq!(TradeSide::Buy.to_string(), "buy");
1064 assert_eq!(TradeSide::Sell.to_string(), "sell");
1065 }
1066
1067 #[test]
1068 fn test_normalize_zero_price_returns_invalid_tick() {
1069 let raw = RawTick {
1070 exchange: Exchange::Binance,
1071 symbol: "BTCUSDT".into(),
1072 payload: json!({ "p": "0", "q": "1" }),
1073 received_at_ms: 0,
1074 };
1075 let err = normalizer().normalize(raw).unwrap_err();
1076 assert!(matches!(err, StreamError::InvalidTick { .. }));
1077 }
1078
1079 #[test]
1080 fn test_normalize_negative_price_returns_invalid_tick() {
1081 let raw = RawTick {
1082 exchange: Exchange::Binance,
1083 symbol: "BTCUSDT".into(),
1084 payload: json!({ "p": "-1", "q": "1" }),
1085 received_at_ms: 0,
1086 };
1087 let err = normalizer().normalize(raw).unwrap_err();
1088 assert!(matches!(err, StreamError::InvalidTick { .. }));
1089 }
1090
1091 #[test]
1092 fn test_normalize_negative_quantity_returns_invalid_tick() {
1093 let raw = RawTick {
1094 exchange: Exchange::Binance,
1095 symbol: "BTCUSDT".into(),
1096 payload: json!({ "p": "100", "q": "-1" }),
1097 received_at_ms: 0,
1098 };
1099 let err = normalizer().normalize(raw).unwrap_err();
1100 assert!(matches!(err, StreamError::InvalidTick { .. }));
1101 }
1102
1103 #[test]
1104 fn test_normalize_zero_quantity_is_valid() {
1105 let raw = RawTick {
1107 exchange: Exchange::Binance,
1108 symbol: "BTCUSDT".into(),
1109 payload: json!({ "p": "100", "q": "0" }),
1110 received_at_ms: 0,
1111 };
1112 let tick = normalizer().normalize(raw).unwrap();
1113 assert_eq!(tick.quantity, Decimal::ZERO);
1114 }
1115
1116 #[test]
1117 fn test_trade_side_is_buy() {
1118 assert!(TradeSide::Buy.is_buy());
1119 assert!(!TradeSide::Buy.is_sell());
1120 }
1121
1122 #[test]
1123 fn test_trade_side_is_sell() {
1124 assert!(TradeSide::Sell.is_sell());
1125 assert!(!TradeSide::Sell.is_buy());
1126 }
1127
1128 #[test]
1129 fn test_normalized_tick_display() {
1130 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
1131 let s = tick.to_string();
1132 assert!(s.contains("Binance"));
1133 assert!(s.contains("BTCUSDT"));
1134 assert!(s.contains("50000"));
1135 }
1136
1137 #[test]
1138 fn test_normalized_tick_value_is_price_times_qty() {
1139 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
1140 let expected = tick.price * tick.quantity;
1142 assert_eq!(tick.volume_notional(), expected);
1143 }
1144
1145 #[test]
1146 fn test_normalized_tick_age_ms_positive() {
1147 let tick = normalizer().normalize(binance_tick("BTCUSDT")).unwrap();
1148 let raw = RawTick {
1151 exchange: Exchange::Binance,
1152 symbol: "BTCUSDT".into(),
1153 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
1154 received_at_ms: 1_000_000,
1155 };
1156 let tick = normalizer().normalize(raw).unwrap();
1157 assert_eq!(tick.age_ms(1_001_000), 1_000);
1158 }
1159
1160 #[test]
1161 fn test_normalized_tick_age_ms_zero_when_now_equals_received() {
1162 let raw = RawTick {
1163 exchange: Exchange::Binance,
1164 symbol: "BTCUSDT".into(),
1165 payload: serde_json::json!({"p": "50000", "q": "0.001", "m": false}),
1166 received_at_ms: 5_000,
1167 };
1168 let tick = normalizer().normalize(raw).unwrap();
1169 assert_eq!(tick.age_ms(5_000), 0);
1170 assert_eq!(tick.age_ms(4_000), 0);
1172 }
1173
1174 #[test]
1175 fn test_normalized_tick_value_zero_qty_is_zero() {
1176 use rust_decimal_macros::dec;
1177 let raw = RawTick {
1178 exchange: Exchange::Binance,
1179 symbol: "BTCUSDT".into(),
1180 payload: serde_json::json!({
1181 "p": "50000",
1182 "q": "0",
1183 "m": false,
1184 }),
1185 received_at_ms: 1000,
1186 };
1187 let tick = normalizer().normalize(raw).unwrap();
1188 assert_eq!(tick.value(), dec!(0));
1189 }
1190
1191 fn make_tick_at(received_at_ms: u64) -> NormalizedTick {
1194 NormalizedTick {
1195 exchange: Exchange::Binance,
1196 symbol: "BTCUSDT".into(),
1197 price: rust_decimal_macros::dec!(100),
1198 quantity: rust_decimal_macros::dec!(1),
1199 side: None,
1200 trade_id: None,
1201 exchange_ts_ms: None,
1202 received_at_ms,
1203 }
1204 }
1205
1206 #[test]
1207 fn test_is_stale_true_when_age_exceeds_threshold() {
1208 let tick = make_tick_at(1_000);
1209 assert!(tick.is_stale(6_000, 4_000));
1211 }
1212
1213 #[test]
1214 fn test_is_stale_false_when_age_equals_threshold() {
1215 let tick = make_tick_at(1_000);
1216 assert!(!tick.is_stale(5_000, 4_000));
1218 }
1219
1220 #[test]
1221 fn test_is_stale_false_for_fresh_tick() {
1222 let tick = make_tick_at(10_000);
1223 assert!(!tick.is_stale(10_500, 1_000));
1224 }
1225
1226 #[test]
1229 fn test_is_buy_true_for_buy_side() {
1230 let mut tick = make_tick_at(1_000);
1231 tick.side = Some(TradeSide::Buy);
1232 assert!(tick.is_buy());
1233 assert!(!tick.is_sell());
1234 }
1235
1236 #[test]
1237 fn test_is_sell_true_for_sell_side() {
1238 let mut tick = make_tick_at(1_000);
1239 tick.side = Some(TradeSide::Sell);
1240 assert!(tick.is_sell());
1241 assert!(!tick.is_buy());
1242 }
1243
1244 #[test]
1245 fn test_is_buy_false_for_unknown_side() {
1246 let mut tick = make_tick_at(1_000);
1247 tick.side = None;
1248 assert!(!tick.is_buy());
1249 assert!(!tick.is_sell());
1250 }
1251
1252 #[test]
1255 fn test_with_exchange_ts_sets_field() {
1256 let tick = make_tick_at(5_000).with_exchange_ts(3_000);
1257 assert_eq!(tick.exchange_ts_ms, Some(3_000));
1258 assert_eq!(tick.received_at_ms, 5_000); }
1260
1261 #[test]
1262 fn test_with_exchange_ts_overrides_existing() {
1263 let tick = make_tick_at(1_000).with_exchange_ts(999).with_exchange_ts(888);
1264 assert_eq!(tick.exchange_ts_ms, Some(888));
1265 }
1266
1267 #[test]
1270 fn test_price_move_from_positive() {
1271 let prev = make_tick_at(1_000);
1272 let mut curr = make_tick_at(2_000);
1273 curr.price = prev.price + rust_decimal_macros::dec!(5);
1274 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(5));
1275 }
1276
1277 #[test]
1278 fn test_price_move_from_negative() {
1279 let prev = make_tick_at(1_000);
1280 let mut curr = make_tick_at(2_000);
1281 curr.price = prev.price - rust_decimal_macros::dec!(3);
1282 assert_eq!(curr.price_move_from(&prev), rust_decimal_macros::dec!(-3));
1283 }
1284
1285 #[test]
1286 fn test_price_move_from_zero_when_same() {
1287 let tick = make_tick_at(1_000);
1288 assert_eq!(tick.price_move_from(&tick), rust_decimal_macros::dec!(0));
1289 }
1290
1291 #[test]
1292 fn test_is_more_recent_than_true() {
1293 let older = make_tick_at(1_000);
1294 let newer = make_tick_at(2_000);
1295 assert!(newer.is_more_recent_than(&older));
1296 }
1297
1298 #[test]
1299 fn test_is_more_recent_than_false_when_older() {
1300 let older = make_tick_at(1_000);
1301 let newer = make_tick_at(2_000);
1302 assert!(!older.is_more_recent_than(&newer));
1303 }
1304
1305 #[test]
1306 fn test_is_more_recent_than_false_when_equal() {
1307 let tick = make_tick_at(1_000);
1308 assert!(!tick.is_more_recent_than(&tick));
1309 }
1310
1311 #[test]
1314 fn test_with_side_sets_buy() {
1315 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
1316 assert_eq!(tick.side, Some(TradeSide::Buy));
1317 }
1318
1319 #[test]
1320 fn test_with_side_sets_sell() {
1321 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
1322 assert_eq!(tick.side, Some(TradeSide::Sell));
1323 }
1324
1325 #[test]
1326 fn test_with_side_overrides_existing() {
1327 let tick = make_tick_at(1_000).with_side(TradeSide::Buy).with_side(TradeSide::Sell);
1328 assert_eq!(tick.side, Some(TradeSide::Sell));
1329 }
1330
1331 #[test]
1334 fn test_is_neutral_true_when_no_side() {
1335 let mut tick = make_tick_at(1_000);
1336 tick.side = None;
1337 assert!(tick.is_neutral());
1338 }
1339
1340 #[test]
1341 fn test_is_neutral_false_when_buy() {
1342 let tick = make_tick_at(1_000).with_side(TradeSide::Buy);
1343 assert!(!tick.is_neutral());
1344 }
1345
1346 #[test]
1347 fn test_is_neutral_false_when_sell() {
1348 let tick = make_tick_at(1_000).with_side(TradeSide::Sell);
1349 assert!(!tick.is_neutral());
1350 }
1351
1352 #[test]
1355 fn test_is_large_trade_above_threshold() {
1356 let mut tick = make_tick_at(1_000);
1357 tick.quantity = rust_decimal_macros::dec!(100);
1358 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
1359 }
1360
1361 #[test]
1362 fn test_is_large_trade_at_threshold() {
1363 let mut tick = make_tick_at(1_000);
1364 tick.quantity = rust_decimal_macros::dec!(50);
1365 assert!(tick.is_large_trade(rust_decimal_macros::dec!(50)));
1366 }
1367
1368 #[test]
1369 fn test_is_large_trade_below_threshold() {
1370 let mut tick = make_tick_at(1_000);
1371 tick.quantity = rust_decimal_macros::dec!(10);
1372 assert!(!tick.is_large_trade(rust_decimal_macros::dec!(50)));
1373 }
1374
1375 #[test]
1376 fn test_volume_notional_is_price_times_quantity() {
1377 let mut tick = make_tick_at(1_000);
1378 tick.price = rust_decimal_macros::dec!(200);
1379 tick.quantity = rust_decimal_macros::dec!(3);
1380 assert_eq!(tick.volume_notional(), rust_decimal_macros::dec!(600));
1381 }
1382
1383 #[test]
1386 fn test_is_above_returns_true_when_price_higher() {
1387 let mut tick = make_tick_at(1_000);
1388 tick.price = rust_decimal_macros::dec!(200);
1389 assert!(tick.is_above(rust_decimal_macros::dec!(150)));
1390 }
1391
1392 #[test]
1393 fn test_is_above_returns_false_when_price_equal() {
1394 let mut tick = make_tick_at(1_000);
1395 tick.price = rust_decimal_macros::dec!(200);
1396 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
1397 }
1398
1399 #[test]
1400 fn test_is_above_returns_false_when_price_lower() {
1401 let mut tick = make_tick_at(1_000);
1402 tick.price = rust_decimal_macros::dec!(100);
1403 assert!(!tick.is_above(rust_decimal_macros::dec!(200)));
1404 }
1405
1406 #[test]
1409 fn test_is_below_returns_true_when_price_lower() {
1410 let mut tick = make_tick_at(1_000);
1411 tick.price = rust_decimal_macros::dec!(100);
1412 assert!(tick.is_below(rust_decimal_macros::dec!(150)));
1413 }
1414
1415 #[test]
1416 fn test_is_below_returns_false_when_price_equal() {
1417 let mut tick = make_tick_at(1_000);
1418 tick.price = rust_decimal_macros::dec!(100);
1419 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
1420 }
1421
1422 #[test]
1423 fn test_is_below_returns_false_when_price_higher() {
1424 let mut tick = make_tick_at(1_000);
1425 tick.price = rust_decimal_macros::dec!(200);
1426 assert!(!tick.is_below(rust_decimal_macros::dec!(100)));
1427 }
1428
1429 #[test]
1432 fn test_has_exchange_ts_false_when_none() {
1433 let tick = make_tick_at(1_000);
1434 assert!(!tick.has_exchange_ts());
1435 }
1436
1437 #[test]
1438 fn test_has_exchange_ts_true_when_some() {
1439 let tick = make_tick_at(1_000).with_exchange_ts(900);
1440 assert!(tick.has_exchange_ts());
1441 }
1442
1443 #[test]
1446 fn test_is_at_returns_true_when_equal() {
1447 let mut tick = make_tick_at(1_000);
1448 tick.price = rust_decimal_macros::dec!(100);
1449 assert!(tick.is_at(rust_decimal_macros::dec!(100)));
1450 }
1451
1452 #[test]
1453 fn test_is_at_returns_false_when_higher() {
1454 let mut tick = make_tick_at(1_000);
1455 tick.price = rust_decimal_macros::dec!(101);
1456 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
1457 }
1458
1459 #[test]
1460 fn test_is_at_returns_false_when_lower() {
1461 let mut tick = make_tick_at(1_000);
1462 tick.price = rust_decimal_macros::dec!(99);
1463 assert!(!tick.is_at(rust_decimal_macros::dec!(100)));
1464 }
1465
1466 #[test]
1469 fn test_is_buy_true_when_side_is_buy() {
1470 let mut tick = make_tick_at(1_000);
1471 tick.side = Some(TradeSide::Buy);
1472 assert!(tick.is_buy());
1473 }
1474
1475 #[test]
1476 fn test_is_buy_false_when_side_is_sell() {
1477 let mut tick = make_tick_at(1_000);
1478 tick.side = Some(TradeSide::Sell);
1479 assert!(!tick.is_buy());
1480 }
1481
1482 #[test]
1483 fn test_is_buy_false_when_side_is_none() {
1484 let mut tick = make_tick_at(1_000);
1485 tick.side = None;
1486 assert!(!tick.is_buy());
1487 }
1488
1489 #[test]
1492 fn test_side_str_buy() {
1493 let mut tick = make_tick_at(1_000);
1494 tick.side = Some(TradeSide::Buy);
1495 assert_eq!(tick.side_str(), "buy");
1496 }
1497
1498 #[test]
1499 fn test_side_str_sell() {
1500 let mut tick = make_tick_at(1_000);
1501 tick.side = Some(TradeSide::Sell);
1502 assert_eq!(tick.side_str(), "sell");
1503 }
1504
1505 #[test]
1506 fn test_side_str_unknown_when_none() {
1507 let mut tick = make_tick_at(1_000);
1508 tick.side = None;
1509 assert_eq!(tick.side_str(), "unknown");
1510 }
1511
1512 #[test]
1513 fn test_is_round_lot_true_for_integer_quantity() {
1514 let mut tick = make_tick_at(1_000);
1515 tick.quantity = rust_decimal_macros::dec!(100);
1516 assert!(tick.is_round_lot());
1517 }
1518
1519 #[test]
1520 fn test_is_round_lot_false_for_fractional_quantity() {
1521 let mut tick = make_tick_at(1_000);
1522 tick.quantity = rust_decimal_macros::dec!(0.5);
1523 assert!(!tick.is_round_lot());
1524 }
1525
1526 #[test]
1529 fn test_is_same_symbol_as_true_when_symbols_match() {
1530 let t1 = make_tick_at(1_000);
1531 let t2 = make_tick_at(2_000);
1532 assert!(t1.is_same_symbol_as(&t2));
1533 }
1534
1535 #[test]
1536 fn test_is_same_symbol_as_false_when_symbols_differ() {
1537 let t1 = make_tick_at(1_000);
1538 let mut t2 = make_tick_at(2_000);
1539 t2.symbol = "ETH-USD".to_string();
1540 assert!(!t1.is_same_symbol_as(&t2));
1541 }
1542
1543 #[test]
1544 fn test_price_distance_from_is_absolute() {
1545 let mut t1 = make_tick_at(1_000);
1546 let mut t2 = make_tick_at(2_000);
1547 t1.price = rust_decimal_macros::dec!(100);
1548 t2.price = rust_decimal_macros::dec!(110);
1549 assert_eq!(t1.price_distance_from(&t2), rust_decimal_macros::dec!(10));
1550 assert_eq!(t2.price_distance_from(&t1), rust_decimal_macros::dec!(10));
1551 }
1552
1553 #[test]
1554 fn test_price_distance_from_zero_when_equal() {
1555 let t1 = make_tick_at(1_000);
1556 let t2 = make_tick_at(2_000);
1557 assert!(t1.price_distance_from(&t2).is_zero());
1558 }
1559
1560 #[test]
1563 fn test_is_sell_true_when_side_is_sell() {
1564 let mut tick = make_tick_at(1_000);
1565 tick.side = Some(TradeSide::Sell);
1566 assert!(tick.is_sell());
1567 }
1568
1569 #[test]
1570 fn test_is_sell_false_when_side_is_buy() {
1571 let mut tick = make_tick_at(1_000);
1572 tick.side = Some(TradeSide::Buy);
1573 assert!(!tick.is_sell());
1574 }
1575
1576 #[test]
1577 fn test_is_sell_false_when_side_is_none() {
1578 let mut tick = make_tick_at(1_000);
1579 tick.side = None;
1580 assert!(!tick.is_sell());
1581 }
1582
1583 #[test]
1586 fn test_exchange_latency_ms_positive_for_normal_delivery() {
1587 let mut tick = make_tick_at(1_100);
1588 tick.exchange_ts_ms = Some(1_000);
1589 assert_eq!(tick.exchange_latency_ms(), Some(100));
1590 }
1591
1592 #[test]
1593 fn test_exchange_latency_ms_negative_for_clock_skew() {
1594 let mut tick = make_tick_at(1_000);
1595 tick.exchange_ts_ms = Some(1_100);
1596 assert_eq!(tick.exchange_latency_ms(), Some(-100));
1597 }
1598
1599 #[test]
1600 fn test_exchange_latency_ms_none_when_no_exchange_ts() {
1601 let mut tick = make_tick_at(1_000);
1602 tick.exchange_ts_ms = None;
1603 assert!(tick.exchange_latency_ms().is_none());
1604 }
1605
1606 #[test]
1607 fn test_is_notional_large_trade_true_when_above_threshold() {
1608 let mut tick = make_tick_at(1_000);
1609 tick.price = rust_decimal_macros::dec!(100);
1610 tick.quantity = rust_decimal_macros::dec!(10);
1611 assert!(tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
1613 }
1614
1615 #[test]
1616 fn test_is_notional_large_trade_false_when_at_or_below_threshold() {
1617 let mut tick = make_tick_at(1_000);
1618 tick.price = rust_decimal_macros::dec!(100);
1619 tick.quantity = rust_decimal_macros::dec!(5);
1620 assert!(!tick.is_notional_large_trade(rust_decimal_macros::dec!(500)));
1622 }
1623
1624 #[test]
1625 fn test_is_aggressive_true_when_buy() {
1626 let mut tick = make_tick_at(1_000);
1627 tick.side = Some(TradeSide::Buy);
1628 assert!(tick.is_aggressive());
1629 }
1630
1631 #[test]
1632 fn test_is_aggressive_true_when_sell() {
1633 let mut tick = make_tick_at(1_000);
1634 tick.side = Some(TradeSide::Sell);
1635 assert!(tick.is_aggressive());
1636 }
1637
1638 #[test]
1639 fn test_is_aggressive_false_when_neutral() {
1640 let tick = make_tick_at(1_000); assert!(!tick.is_aggressive());
1642 }
1643
1644 #[test]
1645 fn test_price_diff_from_positive_when_higher() {
1646 let mut t1 = make_tick_at(1_000);
1647 let mut t2 = make_tick_at(1_000);
1648 t1.price = rust_decimal_macros::dec!(105);
1649 t2.price = rust_decimal_macros::dec!(100);
1650 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(5));
1651 }
1652
1653 #[test]
1654 fn test_price_diff_from_negative_when_lower() {
1655 let mut t1 = make_tick_at(1_000);
1656 let mut t2 = make_tick_at(1_000);
1657 t1.price = rust_decimal_macros::dec!(95);
1658 t2.price = rust_decimal_macros::dec!(100);
1659 assert_eq!(t1.price_diff_from(&t2), rust_decimal_macros::dec!(-5));
1660 }
1661
1662 #[test]
1663 fn test_is_micro_trade_true_when_below_threshold() {
1664 let mut tick = make_tick_at(1_000);
1665 tick.quantity = rust_decimal_macros::dec!(0.5);
1666 assert!(tick.is_micro_trade(rust_decimal_macros::dec!(1)));
1667 }
1668
1669 #[test]
1670 fn test_is_micro_trade_false_when_equal_threshold() {
1671 let mut tick = make_tick_at(1_000);
1672 tick.quantity = rust_decimal_macros::dec!(1);
1673 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
1674 }
1675
1676 #[test]
1677 fn test_is_micro_trade_false_when_above_threshold() {
1678 let mut tick = make_tick_at(1_000);
1679 tick.quantity = rust_decimal_macros::dec!(2);
1680 assert!(!tick.is_micro_trade(rust_decimal_macros::dec!(1)));
1681 }
1682
1683 #[test]
1686 fn test_is_zero_price_true_for_zero() {
1687 let mut tick = make_tick_at(1_000);
1688 tick.price = rust_decimal_macros::dec!(0);
1689 assert!(tick.is_zero_price());
1690 }
1691
1692 #[test]
1693 fn test_is_zero_price_false_for_nonzero() {
1694 let tick = make_tick_at(1_000); assert!(!tick.is_zero_price());
1696 }
1697
1698 #[test]
1699 fn test_is_fresh_true_when_within_age() {
1700 let tick = make_tick_at(1_000);
1701 assert!(tick.is_fresh(2_000, 1_500));
1703 }
1704
1705 #[test]
1706 fn test_is_fresh_false_when_too_old() {
1707 let tick = make_tick_at(1_000);
1708 assert!(!tick.is_fresh(5_000, 2_000));
1710 }
1711
1712 #[test]
1713 fn test_is_fresh_true_when_now_less_than_received() {
1714 let tick = make_tick_at(5_000);
1716 assert!(tick.is_fresh(3_000, 100));
1717 }
1718
1719 #[test]
1721 fn test_age_ms_correct_elapsed() {
1722 let tick = make_tick_at(10_000);
1723 assert_eq!(tick.age_ms(10_500), 500);
1724 }
1725
1726 #[test]
1727 fn test_age_ms_zero_when_now_equals_received() {
1728 let tick = make_tick_at(10_000);
1729 assert_eq!(tick.age_ms(10_000), 0);
1730 }
1731
1732 #[test]
1733 fn test_age_ms_zero_when_now_before_received() {
1734 let tick = make_tick_at(10_000);
1735 assert_eq!(tick.age_ms(9_000), 0);
1736 }
1737
1738 #[test]
1740 fn test_is_buying_pressure_true_above_midpoint() {
1741 use rust_decimal_macros::dec;
1742 let mut tick = make_tick_at(0);
1743 tick.price = dec!(100.50);
1744 assert!(tick.is_buying_pressure(dec!(100)));
1745 }
1746
1747 #[test]
1748 fn test_is_buying_pressure_false_below_midpoint() {
1749 use rust_decimal_macros::dec;
1750 let mut tick = make_tick_at(0);
1751 tick.price = dec!(99.50);
1752 assert!(!tick.is_buying_pressure(dec!(100)));
1753 }
1754
1755 #[test]
1756 fn test_is_buying_pressure_false_at_midpoint() {
1757 use rust_decimal_macros::dec;
1758 let mut tick = make_tick_at(0);
1759 tick.price = dec!(100);
1760 assert!(!tick.is_buying_pressure(dec!(100)));
1761 }
1762
1763 #[test]
1765 fn test_rounded_price_rounds_to_nearest_tick() {
1766 use rust_decimal_macros::dec;
1767 let mut tick = make_tick_at(0);
1768 tick.price = dec!(100.37);
1769 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.25));
1771 }
1772
1773 #[test]
1774 fn test_rounded_price_unchanged_when_already_aligned() {
1775 use rust_decimal_macros::dec;
1776 let mut tick = make_tick_at(0);
1777 tick.price = dec!(100.50);
1778 assert_eq!(tick.rounded_price(dec!(0.25)), dec!(100.50));
1779 }
1780
1781 #[test]
1782 fn test_rounded_price_returns_original_for_zero_tick_size() {
1783 use rust_decimal_macros::dec;
1784 let mut tick = make_tick_at(0);
1785 tick.price = dec!(99.99);
1786 assert_eq!(tick.rounded_price(dec!(0)), dec!(99.99));
1787 }
1788
1789 #[test]
1791 fn test_is_large_spread_from_true_when_large() {
1792 use rust_decimal_macros::dec;
1793 let mut t1 = make_tick_at(0);
1794 let mut t2 = make_tick_at(0);
1795 t1.price = dec!(100);
1796 t2.price = dec!(110);
1797 assert!(t1.is_large_spread_from(&t2, dec!(5)));
1798 }
1799
1800 #[test]
1801 fn test_is_large_spread_from_false_when_small() {
1802 use rust_decimal_macros::dec;
1803 let mut t1 = make_tick_at(0);
1804 let mut t2 = make_tick_at(0);
1805 t1.price = dec!(100);
1806 t2.price = dec!(101);
1807 assert!(!t1.is_large_spread_from(&t2, dec!(5)));
1808 }
1809
1810 #[test]
1813 fn test_age_secs_correct() {
1814 let tick = make_tick_at(1_000);
1815 assert!((tick.age_secs(3_000) - 2.0).abs() < 1e-9);
1816 }
1817
1818 #[test]
1819 fn test_age_secs_zero_when_now_equals_received() {
1820 let tick = make_tick_at(5_000);
1821 assert_eq!(tick.age_secs(5_000), 0.0);
1822 }
1823
1824 #[test]
1825 fn test_age_secs_zero_when_now_before_received() {
1826 let tick = make_tick_at(5_000);
1827 assert_eq!(tick.age_secs(1_000), 0.0);
1828 }
1829
1830 #[test]
1833 fn test_is_same_exchange_as_true_when_matching() {
1834 let t1 = make_tick_at(1_000); let t2 = make_tick_at(2_000); assert!(t1.is_same_exchange_as(&t2));
1837 }
1838
1839 #[test]
1840 fn test_is_same_exchange_as_false_when_different() {
1841 let t1 = make_tick_at(1_000); let mut t2 = make_tick_at(2_000);
1843 t2.exchange = Exchange::Coinbase;
1844 assert!(!t1.is_same_exchange_as(&t2));
1845 }
1846
1847 #[test]
1850 fn test_quote_age_ms_correct() {
1851 let tick = make_tick_at(1_000);
1852 assert_eq!(tick.quote_age_ms(3_000), 2_000);
1853 }
1854
1855 #[test]
1856 fn test_quote_age_ms_zero_when_now_before_received() {
1857 let tick = make_tick_at(5_000);
1858 assert_eq!(tick.quote_age_ms(1_000), 0);
1859 }
1860
1861 #[test]
1862 fn test_notional_value_correct() {
1863 use rust_decimal_macros::dec;
1864 let mut tick = make_tick_at(0);
1865 tick.price = dec!(100);
1866 tick.quantity = dec!(5);
1867 assert_eq!(tick.notional_value(), dec!(500));
1868 }
1869
1870 #[test]
1871 fn test_is_high_value_tick_true_when_above_threshold() {
1872 use rust_decimal_macros::dec;
1873 let mut tick = make_tick_at(0);
1874 tick.price = dec!(100);
1875 tick.quantity = dec!(10);
1876 assert!(tick.is_high_value_tick(dec!(500)));
1878 }
1879
1880 #[test]
1881 fn test_is_high_value_tick_false_when_below_threshold() {
1882 use rust_decimal_macros::dec;
1883 let mut tick = make_tick_at(0);
1884 tick.price = dec!(10);
1885 tick.quantity = dec!(2);
1886 assert!(!tick.is_high_value_tick(dec!(100)));
1888 }
1889
1890 #[test]
1893 fn test_is_buy_side_true_when_buy() {
1894 let mut tick = make_tick_at(0);
1895 tick.side = Some(TradeSide::Buy);
1896 assert!(tick.is_buy_side());
1897 }
1898
1899 #[test]
1900 fn test_is_buy_side_false_when_sell() {
1901 let mut tick = make_tick_at(0);
1902 tick.side = Some(TradeSide::Sell);
1903 assert!(!tick.is_buy_side());
1904 }
1905
1906 #[test]
1907 fn test_is_buy_side_false_when_none() {
1908 let mut tick = make_tick_at(0);
1909 tick.side = None;
1910 assert!(!tick.is_buy_side());
1911 }
1912
1913 #[test]
1914 fn test_is_sell_side_true_when_sell() {
1915 let mut tick = make_tick_at(0);
1916 tick.side = Some(TradeSide::Sell);
1917 assert!(tick.is_sell_side());
1918 }
1919
1920 #[test]
1921 fn test_price_in_range_true_when_within() {
1922 use rust_decimal_macros::dec;
1923 let mut tick = make_tick_at(0);
1924 tick.price = dec!(100);
1925 assert!(tick.price_in_range(dec!(90), dec!(110)));
1926 }
1927
1928 #[test]
1929 fn test_price_in_range_false_when_below() {
1930 use rust_decimal_macros::dec;
1931 let mut tick = make_tick_at(0);
1932 tick.price = dec!(80);
1933 assert!(!tick.price_in_range(dec!(90), dec!(110)));
1934 }
1935
1936 #[test]
1937 fn test_price_in_range_true_at_boundary() {
1938 use rust_decimal_macros::dec;
1939 let mut tick = make_tick_at(0);
1940 tick.price = dec!(90);
1941 assert!(tick.price_in_range(dec!(90), dec!(110)));
1942 }
1943
1944 #[test]
1947 fn test_is_zero_quantity_true_when_zero() {
1948 let mut tick = make_tick_at(0);
1949 tick.quantity = Decimal::ZERO;
1950 assert!(tick.is_zero_quantity());
1951 }
1952
1953 #[test]
1954 fn test_is_zero_quantity_false_when_nonzero() {
1955 let mut tick = make_tick_at(0);
1956 tick.quantity = Decimal::ONE;
1957 assert!(!tick.is_zero_quantity());
1958 }
1959
1960 #[test]
1963 fn test_is_large_tick_true_when_above_threshold() {
1964 let mut tick = make_tick_at(0);
1965 tick.quantity = Decimal::from(10u32);
1966 assert!(tick.is_large_tick(Decimal::from(5u32)));
1967 }
1968
1969 #[test]
1970 fn test_is_large_tick_false_when_at_threshold() {
1971 let mut tick = make_tick_at(0);
1972 tick.quantity = Decimal::from(5u32);
1973 assert!(!tick.is_large_tick(Decimal::from(5u32)));
1974 }
1975
1976 #[test]
1977 fn test_is_large_tick_false_when_below_threshold() {
1978 let mut tick = make_tick_at(0);
1979 tick.quantity = Decimal::from(1u32);
1980 assert!(!tick.is_large_tick(Decimal::from(5u32)));
1981 }
1982
1983 #[test]
1986 fn test_is_away_from_price_true_when_beyond_threshold() {
1987 let mut tick = make_tick_at(0);
1988 tick.price = Decimal::from(110u32);
1989 assert!(tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
1991 }
1992
1993 #[test]
1994 fn test_is_away_from_price_false_when_at_threshold() {
1995 let mut tick = make_tick_at(0);
1996 tick.price = Decimal::from(105u32);
1997 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(5u32)));
1999 }
2000
2001 #[test]
2002 fn test_is_away_from_price_false_when_equal() {
2003 let mut tick = make_tick_at(0);
2004 tick.price = Decimal::from(100u32);
2005 assert!(!tick.is_away_from_price(Decimal::from(100u32), Decimal::from(1u32)));
2006 }
2007
2008 #[test]
2011 fn test_is_within_spread_true_when_between() {
2012 let mut tick = make_tick_at(0);
2013 tick.price = Decimal::from(100u32);
2014 assert!(tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
2015 }
2016
2017 #[test]
2018 fn test_is_within_spread_false_when_at_bid() {
2019 let mut tick = make_tick_at(0);
2020 tick.price = Decimal::from(99u32);
2021 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
2022 }
2023
2024 #[test]
2025 fn test_is_within_spread_false_when_above_ask() {
2026 let mut tick = make_tick_at(0);
2027 tick.price = Decimal::from(102u32);
2028 assert!(!tick.is_within_spread(Decimal::from(99u32), Decimal::from(101u32)));
2029 }
2030
2031 #[test]
2034 fn test_is_recent_true_when_within_threshold() {
2035 let tick = make_tick_at(9_500);
2036 assert!(tick.is_recent(1_000, 10_000));
2038 }
2039
2040 #[test]
2041 fn test_is_recent_false_when_beyond_threshold() {
2042 let tick = make_tick_at(8_000);
2043 assert!(!tick.is_recent(1_000, 10_000));
2045 }
2046
2047 #[test]
2048 fn test_is_recent_true_at_exact_threshold() {
2049 let tick = make_tick_at(9_000);
2050 assert!(tick.is_recent(1_000, 10_000));
2052 }
2053
2054 #[test]
2057 fn test_side_as_str_buy() {
2058 let mut tick = make_tick_at(0);
2059 tick.side = Some(TradeSide::Buy);
2060 assert_eq!(tick.side_as_str(), Some("buy"));
2061 }
2062
2063 #[test]
2064 fn test_side_as_str_sell() {
2065 let mut tick = make_tick_at(0);
2066 tick.side = Some(TradeSide::Sell);
2067 assert_eq!(tick.side_as_str(), Some("sell"));
2068 }
2069
2070 #[test]
2071 fn test_side_as_str_none_when_unknown() {
2072 let mut tick = make_tick_at(0);
2073 tick.side = None;
2074 assert!(tick.side_as_str().is_none());
2075 }
2076
2077 #[test]
2080 fn test_is_above_price_true_when_strictly_above() {
2081 let tick = make_tick_at(0); assert!(tick.is_above_price(rust_decimal_macros::dec!(99)));
2083 }
2084
2085 #[test]
2086 fn test_is_above_price_false_when_equal() {
2087 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(100)));
2089 }
2090
2091 #[test]
2092 fn test_is_above_price_false_when_below() {
2093 let tick = make_tick_at(0); assert!(!tick.is_above_price(rust_decimal_macros::dec!(101)));
2095 }
2096
2097 #[test]
2100 fn test_price_change_from_positive_when_above_reference() {
2101 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(90)), rust_decimal_macros::dec!(10));
2103 }
2104
2105 #[test]
2106 fn test_price_change_from_negative_when_below_reference() {
2107 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(110)), rust_decimal_macros::dec!(-10));
2109 }
2110
2111 #[test]
2112 fn test_price_change_from_zero_when_equal() {
2113 let tick = make_tick_at(0); assert_eq!(tick.price_change_from(rust_decimal_macros::dec!(100)), rust_decimal_macros::dec!(0));
2115 }
2116
2117 #[test]
2120 fn test_is_below_price_true_when_strictly_below() {
2121 let tick = make_tick_at(0); assert!(tick.is_below_price(rust_decimal_macros::dec!(101)));
2123 }
2124
2125 #[test]
2126 fn test_is_below_price_false_when_equal() {
2127 let tick = make_tick_at(0); assert!(!tick.is_below_price(rust_decimal_macros::dec!(100)));
2129 }
2130
2131 #[test]
2134 fn test_quantity_above_true_when_quantity_exceeds_threshold() {
2135 let tick = make_tick_at(0); assert!(tick.quantity_above(rust_decimal_macros::dec!(0)));
2137 }
2138
2139 #[test]
2140 fn test_quantity_above_false_when_quantity_equals_threshold() {
2141 let tick = make_tick_at(0); assert!(!tick.quantity_above(rust_decimal_macros::dec!(1)));
2143 }
2144
2145 #[test]
2148 fn test_is_at_price_true_when_equal() {
2149 let tick = make_tick_at(0); assert!(tick.is_at_price(rust_decimal_macros::dec!(100)));
2151 }
2152
2153 #[test]
2154 fn test_is_at_price_false_when_different() {
2155 let tick = make_tick_at(0); assert!(!tick.is_at_price(rust_decimal_macros::dec!(101)));
2157 }
2158
2159 #[test]
2162 fn test_is_round_number_true_when_divisible() {
2163 let tick = make_tick_at(0); assert!(tick.is_round_number(rust_decimal_macros::dec!(10)));
2165 assert!(tick.is_round_number(rust_decimal_macros::dec!(100)));
2166 }
2167
2168 #[test]
2169 fn test_is_round_number_false_when_not_divisible() {
2170 let tick = make_tick_at(0); assert!(!tick.is_round_number(rust_decimal_macros::dec!(3)));
2172 }
2173
2174 #[test]
2175 fn test_is_round_number_false_when_step_zero() {
2176 let tick = make_tick_at(0);
2177 assert!(!tick.is_round_number(rust_decimal_macros::dec!(0)));
2178 }
2179
2180 #[test]
2183 fn test_is_market_open_tick_true_when_within_session() {
2184 let tick = make_tick_at(500); assert!(tick.is_market_open_tick(100, 1_000));
2186 }
2187
2188 #[test]
2189 fn test_is_market_open_tick_false_when_before_session() {
2190 let tick = make_tick_at(50);
2191 assert!(!tick.is_market_open_tick(100, 1_000));
2192 }
2193
2194 #[test]
2195 fn test_is_market_open_tick_false_when_at_session_end() {
2196 let tick = make_tick_at(1_000);
2197 assert!(!tick.is_market_open_tick(100, 1_000)); }
2199
2200 #[test]
2203 fn test_signed_quantity_positive_for_buy() {
2204 let mut tick = make_tick_at(0);
2205 tick.side = Some(TradeSide::Buy);
2206 assert!(tick.signed_quantity() > rust_decimal::Decimal::ZERO);
2207 }
2208
2209 #[test]
2210 fn test_signed_quantity_negative_for_sell() {
2211 let mut tick = make_tick_at(0);
2212 tick.side = Some(TradeSide::Sell);
2213 assert!(tick.signed_quantity() < rust_decimal::Decimal::ZERO);
2214 }
2215
2216 #[test]
2217 fn test_signed_quantity_zero_for_unknown() {
2218 let tick = make_tick_at(0); assert_eq!(tick.signed_quantity(), rust_decimal::Decimal::ZERO);
2220 }
2221
2222 #[test]
2225 fn test_as_price_level_returns_price_and_quantity() {
2226 let tick = make_tick_at(0); let (p, q) = tick.as_price_level();
2228 assert_eq!(p, rust_decimal_macros::dec!(100));
2229 assert_eq!(q, rust_decimal_macros::dec!(1));
2230 }
2231}