rangebar_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/// Normalize any timestamp to 16-digit microseconds
11///
12/// # Arguments
13/// * `raw_timestamp` - Raw timestamp that could be 13-digit millis or 16-digit micros
14///
15/// # Returns
16/// * Normalized timestamp in microseconds (16-digit precision)
17///
18/// # Examples
19/// ```rust
20/// use rangebar_core::normalize_timestamp;
21///
22/// // 13-digit millisecond timestamp -> 16-digit microseconds
23/// assert_eq!(normalize_timestamp(1609459200000), 1609459200000000);
24///
25/// // Already 16-digit microseconds -> unchanged
26/// assert_eq!(normalize_timestamp(1609459200000000), 1609459200000000);
27/// ```
28pub fn normalize_timestamp(raw_timestamp: u64) -> i64 {
29    if raw_timestamp < MICROSECOND_THRESHOLD {
30        // 13-digit milliseconds -> convert to microseconds
31        (raw_timestamp * 1_000) as i64
32    } else {
33        // Already microseconds (16+ digits)
34        raw_timestamp as i64
35    }
36}
37
38/// Validate timestamp is in expected microsecond range
39///
40/// Checks if timestamp falls within reasonable bounds for financial data.
41/// Expanded range (2000-2035) covers historical Forex data (2003+)
42/// and cryptocurrency data (2009+) while rejecting obviously invalid timestamps.
43///
44/// # Arguments
45///
46/// * `timestamp` - Timestamp in microseconds (16-digit precision)
47///
48/// # Returns
49///
50/// `true` if timestamp is within valid range, `false` otherwise
51///
52/// # Validation Range (Q16)
53///
54/// - MIN: 2000-01-01 (covers historical Forex from 2003)
55/// - MAX: 2035-01-01 (future-proof for upcoming data)
56/// - Rejects: Unix epoch (1970), far future (2100+), negative timestamps
57pub fn validate_timestamp(timestamp: i64) -> bool {
58    // Expanded bounds: 2000-01-01 to 2035-01-01 in microseconds (Q16)
59    const MIN_TIMESTAMP: i64 = 946_684_800_000_000; // 2000-01-01 00:00:00 UTC
60    const MAX_TIMESTAMP: i64 = 2_051_222_400_000_000; // 2035-01-01 00:00:00 UTC
61
62    (MIN_TIMESTAMP..=MAX_TIMESTAMP).contains(&timestamp)
63}
64
65/// Create a normalized AggTrade with automatic timestamp conversion
66///
67/// This is the preferred way to create AggTrade instances to ensure
68/// timestamp consistency across all data sources.
69pub fn create_aggtrade_with_normalized_timestamp(
70    agg_trade_id: i64,
71    price: crate::FixedPoint,
72    volume: crate::FixedPoint,
73    first_trade_id: i64,
74    last_trade_id: i64,
75    raw_timestamp: u64,
76    is_buyer_maker: bool,
77) -> crate::types::AggTrade {
78    use crate::types::AggTrade;
79
80    AggTrade {
81        agg_trade_id,
82        price,
83        volume,
84        first_trade_id,
85        last_trade_id,
86        timestamp: normalize_timestamp(raw_timestamp),
87        is_buyer_maker,
88        is_best_match: None, // Not provided in this context
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_normalize_13_digit_milliseconds() {
98        // Common 13-digit timestamp (Jan 1, 2021 00:00:00 UTC)
99        let millis = 1609459200000u64;
100        let expected = 1609459200000000i64;
101        assert_eq!(normalize_timestamp(millis), expected);
102    }
103
104    #[test]
105    fn test_normalize_16_digit_microseconds() {
106        // Already 16-digit microseconds
107        let micros = 1609459200000000u64;
108        let expected = 1609459200000000i64;
109        assert_eq!(normalize_timestamp(micros), expected);
110    }
111
112    #[test]
113    fn test_threshold_boundary() {
114        // Right at the threshold
115        let threshold_minus_one = MICROSECOND_THRESHOLD - 1;
116        let threshold = MICROSECOND_THRESHOLD;
117
118        // Below threshold: convert
119        assert_eq!(
120            normalize_timestamp(threshold_minus_one),
121            (threshold_minus_one * 1000) as i64
122        );
123
124        // At threshold: no conversion
125        assert_eq!(normalize_timestamp(threshold), threshold as i64);
126    }
127
128    #[test]
129    fn test_validate_timestamp() {
130        // Valid: 2024 timestamp (crypto era)
131        assert!(validate_timestamp(1_704_067_200_000_000)); // 2024-01-01
132
133        // Valid: 2003 timestamp (Forex historical data)
134        assert!(validate_timestamp(1_041_379_200_000_000)); // 2003-01-01
135
136        // Valid: 2000 timestamp (min boundary)
137        assert!(validate_timestamp(946_684_800_000_000)); // 2000-01-01
138
139        // Valid: 2034 timestamp (near max boundary)
140        assert!(validate_timestamp(2_019_686_400_000_000)); // 2034-01-01
141
142        // Invalid: 1999 (before historical Forex data)
143        assert!(!validate_timestamp(915_148_800_000_000)); // 1999-01-01
144
145        // Invalid: Unix epoch era (1970s)
146        assert!(!validate_timestamp(1_000_000_000_000)); // 1970-01-12
147
148        // Invalid: Far future (2050+)
149        assert!(!validate_timestamp(2_524_608_000_000_000)); // 2050-01-01
150    }
151}