hyperliquid_backtest/
utils.rs

1//! Utility functions and helpers for Hyperliquid backtesting
2
3use chrono::{DateTime, FixedOffset, TimeZone, Utc};
4use crate::errors::{HyperliquidBacktestError, Result};
5
6/// Time conversion utilities
7pub mod time {
8    use super::*;
9    
10    /// Convert Unix timestamp to DateTime<FixedOffset>
11    pub fn unix_to_datetime(timestamp: u64) -> Result<DateTime<FixedOffset>> {
12        let datetime = DateTime::from_timestamp(timestamp as i64, 0)
13            .ok_or_else(|| HyperliquidBacktestError::DataConversion(
14                format!("Invalid timestamp: {}", timestamp)
15            ))?;
16        
17        Ok(datetime.with_timezone(&FixedOffset::east_opt(0).unwrap()))
18    }
19    
20    /// Convert DateTime to Unix timestamp
21    pub fn datetime_to_unix(dt: DateTime<FixedOffset>) -> u64 {
22        dt.timestamp() as u64
23    }
24    
25    /// Validate time range
26    pub fn validate_time_range(start: u64, end: u64) -> Result<()> {
27        if start >= end {
28            return Err(HyperliquidBacktestError::InvalidTimeRange { start, end });
29        }
30        Ok(())
31    }
32    
33    /// Get current Unix timestamp
34    pub fn current_timestamp() -> u64 {
35        Utc::now().timestamp() as u64
36    }
37}
38
39/// Data validation utilities
40pub mod validation {
41    use super::*;
42    
43    /// Validate OHLC data consistency
44    pub fn validate_ohlc_data(
45        open: &[f64],
46        high: &[f64],
47        low: &[f64],
48        close: &[f64],
49    ) -> Result<()> {
50        let len = open.len();
51        if high.len() != len || low.len() != len || close.len() != len {
52            return Err(HyperliquidBacktestError::DataConversion(
53                "OHLC arrays have different lengths".to_string()
54            ));
55        }
56        
57        for i in 0..len {
58            if high[i] < low[i] {
59                return Err(HyperliquidBacktestError::DataConversion(
60                    format!("High price {} is less than low price {} at index {}", 
61                           high[i], low[i], i)
62                ));
63            }
64            
65            if open[i] < 0.0 || high[i] < 0.0 || low[i] < 0.0 || close[i] < 0.0 {
66                return Err(HyperliquidBacktestError::DataConversion(
67                    format!("Negative price found at index {}", i)
68                ));
69            }
70        }
71        
72        Ok(())
73    }
74    
75    /// Validate funding rate data
76    pub fn validate_funding_rates(rates: &[f64]) -> Result<()> {
77        for (i, &rate) in rates.iter().enumerate() {
78            if rate.is_nan() || rate.is_infinite() {
79                return Err(HyperliquidBacktestError::DataConversion(
80                    format!("Invalid funding rate at index {}: {}", i, rate)
81                ));
82            }
83            
84            // Reasonable bounds check for funding rates (typically between -1% and 1%)
85            if rate.abs() > 0.01 {
86                eprintln!("Warning: Unusually high funding rate at index {}: {:.4}%", 
87                         i, rate * 100.0);
88            }
89        }
90        
91        Ok(())
92    }
93    
94    /// Validate time interval string
95    pub fn validate_interval(interval: &str) -> Result<()> {
96        match interval {
97            "1m" | "5m" | "15m" | "30m" | "1h" | "4h" | "1d" => Ok(()),
98            _ => Err(HyperliquidBacktestError::UnsupportedInterval(interval.to_string())),
99        }
100    }
101}
102
103/// String conversion utilities
104pub mod conversion {
105    use super::*;
106    
107    /// Convert string to f64 with error handling
108    pub fn string_to_f64(s: &str) -> Result<f64> {
109        s.parse::<f64>()
110            .map_err(|_| HyperliquidBacktestError::DataConversion(
111                format!("Failed to parse '{}' as f64", s)
112            ))
113    }
114    
115    /// Convert string to u64 with error handling
116    pub fn string_to_u64(s: &str) -> Result<u64> {
117        s.parse::<u64>()
118            .map_err(|_| HyperliquidBacktestError::DataConversion(
119                format!("Failed to parse '{}' as u64", s)
120            ))
121    }
122    
123    /// Safely convert f64 to string with precision
124    pub fn f64_to_string(value: f64, precision: usize) -> String {
125        format!("{:.prec$}", value, prec = precision)
126    }
127}
128
129/// Mathematical utilities
130pub mod math {
131    /// Calculate simple moving average
132    pub fn sma(data: &[f64], period: usize) -> Vec<f64> {
133        if period == 0 || data.len() < period {
134            return Vec::new();
135        }
136        
137        let mut result = Vec::with_capacity(data.len() - period + 1);
138        
139        for i in period - 1..data.len() {
140            let sum: f64 = data[i - period + 1..=i].iter().sum();
141            result.push(sum / period as f64);
142        }
143        
144        result
145    }
146    
147    /// Calculate exponential moving average
148    pub fn ema(data: &[f64], period: usize) -> Vec<f64> {
149        if data.is_empty() || period == 0 {
150            return Vec::new();
151        }
152        
153        let alpha = 2.0 / (period as f64 + 1.0);
154        let mut result = Vec::with_capacity(data.len());
155        
156        // First value is just the first data point
157        result.push(data[0]);
158        
159        for i in 1..data.len() {
160            let ema_value = alpha * data[i] + (1.0 - alpha) * result[i - 1];
161            result.push(ema_value);
162        }
163        
164        result
165    }
166    
167    /// Calculate standard deviation
168    pub fn std_dev(data: &[f64]) -> f64 {
169        if data.len() < 2 {
170            return 0.0;
171        }
172        
173        let mean = data.iter().sum::<f64>() / data.len() as f64;
174        let variance = data.iter()
175            .map(|x| (x - mean).powi(2))
176            .sum::<f64>() / (data.len() - 1) as f64;
177        
178        variance.sqrt()
179    }
180    
181    /// Linear interpolation between two points
182    pub fn lerp(x0: f64, y0: f64, x1: f64, y1: f64, x: f64) -> f64 {
183        if (x1 - x0).abs() < f64::EPSILON {
184            return y0;
185        }
186        
187        y0 + (y1 - y0) * (x - x0) / (x1 - x0)
188    }
189}
190
191/// CSV utilities for data export
192pub mod csv_utils {
193    use super::*;
194    use std::path::Path;
195    
196    /// Write data to CSV file
197    pub fn write_csv<P: AsRef<Path>>(
198        path: P,
199        headers: &[&str],
200        data: &[Vec<String>],
201    ) -> Result<()> {
202        let mut writer = csv::Writer::from_path(path)?;
203        
204        // Write headers
205        writer.write_record(headers)?;
206        
207        // Write data rows
208        for row in data {
209            writer.write_record(row)?;
210        }
211        
212        writer.flush()?;
213        Ok(())
214    }
215    
216    /// Read CSV file into vectors
217    pub fn read_csv<P: AsRef<Path>>(path: P) -> Result<(Vec<String>, Vec<Vec<String>>)> {
218        let mut reader = csv::Reader::from_path(path)?;
219        
220        // Get headers
221        let headers = reader.headers()?.iter().map(|s| s.to_string()).collect();
222        
223        // Read data rows
224        let mut data = Vec::new();
225        for result in reader.records() {
226            let record = result?;
227            let row: Vec<String> = record.iter().map(|s| s.to_string()).collect();
228            data.push(row);
229        }
230        
231        Ok((headers, data))
232    }
233}