Skip to main content

opendeviationbar_core/
timestamp.rs

1//! Universal timestamp normalization utilities
2//!
3//! This module provides centralized timestamp handling to ensure all aggTrade data
4//! uses consistent 16-digit microsecond precision regardless of source format.
5
6/// Universal timestamp normalization threshold
7/// Values below this are treated as 13-digit milliseconds and converted to microseconds
8const MICROSECOND_THRESHOLD: u64 = 10_000_000_000_000;
9
10/// Declared timestamp unit for registry-driven conversion.
11///
12/// Replaces the heuristic `normalize_timestamp()` as the primary conversion path.
13/// The heuristic is preserved as a safety net for legacy code paths.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TimestampUnit {
16    /// 13-digit millisecond timestamps (pre-2025 Spot, all Futures)
17    Millisecond,
18    /// 16-digit microsecond timestamps (2025+ Spot with timeUnit=MICROSECOND)
19    Microsecond,
20}
21
22impl TimestampUnit {
23    /// Convert a raw timestamp to internal microseconds based on declared unit.
24    #[inline]
25    pub fn to_microseconds(self, raw: i64) -> i64 {
26        match self {
27            TimestampUnit::Millisecond => raw * 1_000,
28            TimestampUnit::Microsecond => raw,
29        }
30    }
31}
32
33/// Normalize any timestamp to 16-digit microseconds
34///
35/// # Arguments
36/// * `raw_timestamp` - Raw timestamp that could be 13-digit millis or 16-digit micros
37///
38/// # Returns
39/// * Normalized timestamp in microseconds (16-digit precision)
40///
41/// # Examples
42/// ```rust
43/// use opendeviationbar_core::normalize_timestamp;
44///
45/// // 13-digit millisecond timestamp -> 16-digit microseconds
46/// assert_eq!(normalize_timestamp(1609459200000), 1609459200000000);
47///
48/// // Already 16-digit microseconds -> unchanged
49/// assert_eq!(normalize_timestamp(1609459200000000), 1609459200000000);
50/// ```
51pub fn normalize_timestamp(raw_timestamp: u64) -> i64 {
52    if raw_timestamp < MICROSECOND_THRESHOLD {
53        // 13-digit milliseconds -> convert to microseconds
54        (raw_timestamp * 1_000) as i64
55    } else {
56        // Already microseconds (16+ digits)
57        raw_timestamp as i64
58    }
59}
60
61/// Validate timestamp is in expected microsecond range
62///
63/// Checks if timestamp falls within reasonable bounds for financial data.
64/// Expanded range (2000-2035) covers historical financial data (2003+)
65/// and cryptocurrency data (2009+) while rejecting obviously invalid timestamps.
66///
67/// # Arguments
68///
69/// * `timestamp` - Timestamp in microseconds (16-digit precision)
70///
71/// # Returns
72///
73/// `true` if timestamp is within valid range, `false` otherwise
74///
75/// # Validation Range (Q16)
76///
77/// - MIN: 2000-01-01 (covers historical data from 2003)
78/// - MAX: 2035-01-01 (future-proof for upcoming data)
79/// - Rejects: Unix epoch (1970), far future (2100+), negative timestamps
80pub fn validate_timestamp(timestamp: i64) -> bool {
81    // Expanded bounds: 2000-01-01 to 2035-01-01 in microseconds (Q16)
82    const MIN_TIMESTAMP: i64 = 946_684_800_000_000; // 2000-01-01 00:00:00 UTC
83    const MAX_TIMESTAMP: i64 = 2_051_222_400_000_000; // 2035-01-01 00:00:00 UTC
84
85    (MIN_TIMESTAMP..=MAX_TIMESTAMP).contains(&timestamp)
86}
87
88/// Create a normalized Tick with automatic timestamp conversion
89///
90/// This is the preferred way to create Tick instances to ensure
91/// timestamp consistency across all data sources.
92pub fn create_aggtrade_with_normalized_timestamp(
93    ref_id: i64,
94    price: crate::FixedPoint,
95    volume: crate::FixedPoint,
96    first_sub_id: i64,
97    last_sub_id: i64,
98    raw_timestamp: u64,
99    is_buyer_maker: bool,
100) -> crate::trade::Tick {
101    use crate::trade::Tick;
102
103    Tick {
104        ref_id,
105        price,
106        volume,
107        first_sub_id,
108        last_sub_id,
109        timestamp: normalize_timestamp(raw_timestamp),
110        is_buyer_maker,
111        is_best_match: None, // Not provided in this context
112        best_bid: None,
113        best_ask: None,
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_normalize_13_digit_milliseconds() {
123        // Common 13-digit timestamp (Jan 1, 2021 00:00:00 UTC)
124        let millis = 1609459200000u64;
125        let expected = 1609459200000000i64;
126        assert_eq!(normalize_timestamp(millis), expected);
127    }
128
129    #[test]
130    fn test_normalize_16_digit_microseconds() {
131        // Already 16-digit microseconds
132        let micros = 1609459200000000u64;
133        let expected = 1609459200000000i64;
134        assert_eq!(normalize_timestamp(micros), expected);
135    }
136
137    #[test]
138    fn test_threshold_boundary() {
139        // Right at the threshold
140        let threshold_minus_one = MICROSECOND_THRESHOLD - 1;
141        let threshold = MICROSECOND_THRESHOLD;
142
143        // Below threshold: convert
144        assert_eq!(
145            normalize_timestamp(threshold_minus_one),
146            (threshold_minus_one * 1000) as i64
147        );
148
149        // At threshold: no conversion
150        assert_eq!(normalize_timestamp(threshold), threshold as i64);
151    }
152
153    #[test]
154    fn test_validate_timestamp() {
155        // Valid: 2024 timestamp (crypto era)
156        assert!(validate_timestamp(1_704_067_200_000_000)); // 2024-01-01
157
158        // Valid: 2003 timestamp (historical data)
159        assert!(validate_timestamp(1_041_379_200_000_000)); // 2003-01-01
160
161        // Valid: 2000 timestamp (min boundary)
162        assert!(validate_timestamp(946_684_800_000_000)); // 2000-01-01
163
164        // Valid: 2034 timestamp (near max boundary)
165        assert!(validate_timestamp(2_019_686_400_000_000)); // 2034-01-01
166
167        // Invalid: 1999 (before historical data)
168        assert!(!validate_timestamp(915_148_800_000_000)); // 1999-01-01
169
170        // Invalid: Unix epoch era (1970s)
171        assert!(!validate_timestamp(1_000_000_000_000)); // 1970-01-12
172
173        // Invalid: Far future (2050+)
174        assert!(!validate_timestamp(2_524_608_000_000_000)); // 2050-01-01
175    }
176
177    // Issue #96: Additional edge case coverage
178
179    #[test]
180    fn test_validate_timestamp_negative() {
181        assert!(!validate_timestamp(-1));
182        assert!(!validate_timestamp(i64::MIN));
183    }
184
185    #[test]
186    fn test_validate_timestamp_zero() {
187        assert!(!validate_timestamp(0));
188    }
189
190    #[test]
191    fn test_validate_timestamp_exact_boundaries() {
192        // Exact min boundary (2000-01-01): valid
193        assert!(validate_timestamp(946_684_800_000_000));
194        // One microsecond before min: invalid
195        assert!(!validate_timestamp(946_684_800_000_000 - 1));
196        // Exact max boundary (2035-01-01): valid
197        assert!(validate_timestamp(2_051_222_400_000_000));
198        // One microsecond after max: invalid
199        assert!(!validate_timestamp(2_051_222_400_000_000 + 1));
200    }
201
202    #[test]
203    fn test_normalize_timestamp_zero() {
204        // Zero is below threshold → converted (0 * 1000 = 0)
205        assert_eq!(normalize_timestamp(0), 0);
206    }
207
208    #[test]
209    fn test_timestamp_unit_millisecond_conversion() {
210        assert_eq!(
211            TimestampUnit::Millisecond.to_microseconds(1609459200000),
212            1609459200000000
213        );
214    }
215
216    #[test]
217    fn test_timestamp_unit_microsecond_passthrough() {
218        assert_eq!(
219            TimestampUnit::Microsecond.to_microseconds(1609459200000000),
220            1609459200000000
221        );
222    }
223
224    #[test]
225    fn test_create_aggtrade_normalizes_timestamp() {
226        use crate::FixedPoint;
227        let trade = create_aggtrade_with_normalized_timestamp(
228            1,
229            FixedPoint::from_str("100.0").unwrap(),
230            FixedPoint::from_str("1.0").unwrap(),
231            10,
232            10,
233            1609459200000, // 13-digit millis
234            false,
235        );
236        assert_eq!(trade.timestamp, 1609459200000000); // Converted to 16-digit micros
237    }
238}