Skip to main content

opendeviationbar_core/
trade.rs

1//! Trade and data source types
2//!
3//! Extracted from types.rs (Phase 2c refactoring)
4
5use crate::fixed_point::FixedPoint;
6use serde::{Deserialize, Serialize};
7
8/// Data source for market data (future-proofing for multi-exchange support)
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
10#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
11pub enum DataSource {
12    /// Binance Spot Market (8 fields including is_best_match)
13    BinanceSpot,
14    /// Binance USD-Margined Futures (7 fields without is_best_match)
15    #[default]
16    BinanceFuturesUM,
17    /// Binance Coin-Margined Futures
18    BinanceFuturesCM,
19}
20
21/// Aggregate trade data from Binance markets
22///
23/// Represents a single AggTrade record which aggregates multiple individual
24/// exchange trades that occurred at the same price within ~100ms timeframe.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
27pub struct AggTrade {
28    /// Aggregate trade ID (unique per AggTrade record)
29    pub agg_trade_id: i64,
30
31    /// Price as fixed-point integer
32    pub price: FixedPoint,
33
34    /// Volume as fixed-point integer (total quantity across all individual trades)
35    pub volume: FixedPoint,
36
37    /// First individual trade ID in this aggregation
38    pub first_trade_id: i64,
39
40    /// Last individual trade ID in this aggregation
41    pub last_trade_id: i64,
42
43    /// Timestamp in microseconds (preserves maximum precision)
44    pub timestamp: i64,
45
46    /// Whether buyer is market maker (true = sell pressure, false = buy pressure)
47    /// Critical for order flow analysis and market microstructure
48    pub is_buyer_maker: bool,
49
50    /// Whether trade was best price match (Spot market only)
51    /// None for futures markets, Some(bool) for spot markets
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub is_best_match: Option<bool>,
54}
55
56impl AggTrade {
57    /// Number of individual exchange trades in this aggregated record
58    ///
59    /// Each AggTrade record represents multiple individual trades that occurred
60    /// at the same price within the same ~100ms window on the exchange.
61    /// Issue #96: #[inline] for per-trade hot path (called in OpenDeviationBar::new + update_with_trade)
62    #[inline]
63    pub fn individual_trade_count(&self) -> i64 {
64        self.last_trade_id - self.first_trade_id + 1
65    }
66
67    /// Turnover (price * volume) as i128 to prevent overflow
68    /// Issue #96: #[inline] for per-trade hot path (called in OpenDeviationBar::new + update_with_trade)
69    #[inline]
70    pub fn turnover(&self) -> i128 {
71        (self.price.0 as i128) * (self.volume.0 as i128)
72    }
73}
74
75// Issue #96: Test coverage for AggTrade methods
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn make_trade(price: &str, volume: &str, first_id: i64, last_id: i64) -> AggTrade {
81        AggTrade {
82            agg_trade_id: 1,
83            price: FixedPoint::from_str(price).unwrap(),
84            volume: FixedPoint::from_str(volume).unwrap(),
85            first_trade_id: first_id,
86            last_trade_id: last_id,
87            timestamp: 1000,
88            is_buyer_maker: false,
89            is_best_match: None,
90        }
91    }
92
93    #[test]
94    fn test_individual_trade_count_single() {
95        let trade = make_trade("100.0", "1.0", 5, 5);
96        assert_eq!(trade.individual_trade_count(), 1);
97    }
98
99    #[test]
100    fn test_individual_trade_count_multiple() {
101        let trade = make_trade("100.0", "1.0", 100, 199);
102        assert_eq!(trade.individual_trade_count(), 100);
103    }
104
105    #[test]
106    fn test_individual_trade_count_large_range() {
107        let trade = make_trade("100.0", "1.0", 0, 999_999);
108        assert_eq!(trade.individual_trade_count(), 1_000_000);
109    }
110
111    #[test]
112    fn test_turnover_basic() {
113        // price=100.0 (FixedPoint=10_000_000_000), volume=2.0 (FixedPoint=200_000_000)
114        let trade = make_trade("100.0", "2.0", 1, 1);
115        let expected = 10_000_000_000i128 * 200_000_000i128;
116        assert_eq!(trade.turnover(), expected);
117    }
118
119    #[test]
120    fn test_turnover_zero_volume() {
121        let trade = make_trade("100.0", "0.0", 1, 1);
122        assert_eq!(trade.turnover(), 0);
123    }
124
125    #[test]
126    fn test_turnover_large_values_no_overflow() {
127        // Simulate high-volume token: price * volume would overflow i64 but fits i128
128        // SHIBUSDT: price=0.00002, volume=10_000_000_000
129        let trade = make_trade("0.00002", "10000000000.0", 1, 1);
130        let turnover = trade.turnover();
131        assert!(turnover > 0, "Turnover should be positive for valid trade");
132        // Verify it's computable without panic
133        let _as_f64 = turnover as f64;
134    }
135
136    #[test]
137    fn test_turnover_tiny_price() {
138        let trade = make_trade("0.00000001", "1.0", 1, 1);
139        // price.0 = 1 (minimum non-zero FixedPoint), volume.0 = 100_000_000
140        assert_eq!(trade.turnover(), 100_000_000);
141    }
142}