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(×tamp)
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}