hyperliquid_backtest/
data.rs

1//! # Data Structures and Utilities for Hyperliquid Market Data
2//!
3//! This module provides the core data structures and fetching utilities for working with
4//! Hyperliquid market data, including OHLC price data and funding rates for perpetual futures.
5//!
6//! ## Key Features
7//!
8//! - **Async Data Fetching**: Efficient retrieval of historical market data from Hyperliquid API
9//! - **Funding Rate Integration**: Complete funding rate data for perpetual futures analysis
10//! - **Multiple Time Intervals**: Support for 1m, 5m, 15m, 1h, 4h, and 1d intervals
11//! - **Data Validation**: Comprehensive validation and error handling for data integrity
12//! - **rs-backtester Compatibility**: Seamless conversion to rs-backtester Data format
13//!
14//! ## Usage Examples
15//!
16//! ### Basic Data Fetching
17//!
18//! ```rust,no_run
19//! use hyperliquid_backtest::prelude::*;
20//! use chrono::Utc;
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<(), HyperliquidBacktestError> {
24//!     let end_time = Utc::now().timestamp() as u64;
25//!     let start_time = end_time - (7 * 24 * 60 * 60); // 7 days ago
26//!     
27//!     let data = HyperliquidData::fetch("BTC", "1h", start_time, end_time).await?;
28//!     
29//!     println!("Fetched {} data points for {}", data.len(), data.symbol);
30//!     println!("Price range: ${:.2} - ${:.2}", data.price_range().0, data.price_range().1);
31//!     
32//!     Ok(())
33//! }
34//! ```
35//!
36//! ### Working with Funding Rates
37//!
38//! ```rust,no_run
39//! use hyperliquid_backtest::prelude::*;
40//!
41//! #[tokio::main]
42//! async fn main() -> Result<(), HyperliquidBacktestError> {
43//!     let data = HyperliquidData::fetch("ETH", "1h", start_time, end_time).await?;
44//!     
45//!     // Get funding statistics
46//!     let funding_stats = data.funding_statistics()?;
47//!     println!("Average funding rate: {:.4}%", funding_stats.average_rate * 100.0);
48//!     println!("Funding volatility: {:.4}%", funding_stats.volatility * 100.0);
49//!     
50//!     // Get funding rate at specific time
51//!     if let Some(rate) = data.get_funding_rate_at(data.datetime[100]) {
52//!         println!("Funding rate at {}: {:.4}%", data.datetime[100], rate * 100.0);
53//!     }
54//!     
55//!     Ok(())
56//! }
57//! ```
58
59use crate::errors::{HyperliquidBacktestError, Result};
60use chrono::{DateTime, FixedOffset, TimeZone};
61use serde::{Deserialize, Serialize};
62use std::collections::HashMap;
63
64/// Main data structure for Hyperliquid market data
65///
66/// This structure contains OHLC price data along with funding rates for perpetual futures.
67/// It provides a comprehensive view of market conditions including both price action and
68/// funding dynamics.
69///
70/// ## Fields
71///
72/// - `symbol`: Trading pair symbol (e.g., "BTC", "ETH")
73/// - `datetime`: Timestamps for each data point (UTC with timezone info)
74/// - `open`, `high`, `low`, `close`: OHLC price data
75/// - `volume`: Trading volume for each period
76/// - `funding_rates`: Funding rates (NaN for non-funding periods)
77///
78/// ## Data Alignment
79///
80/// All arrays have the same length and are aligned by index. The funding_rates array
81/// contains NaN values for periods where funding is not applied (typically every 8 hours).
82///
83/// ## Example
84///
85/// ```rust,no_run
86/// use hyperliquid_backtest::prelude::*;
87///
88/// #[tokio::main]
89/// async fn main() -> Result<(), HyperliquidBacktestError> {
90///     let data = HyperliquidData::fetch("BTC", "1h", start_time, end_time).await?;
91///     
92///     // Access price data
93///     println!("Latest close price: ${:.2}", data.close.last().unwrap());
94///     
95///     // Check data integrity
96///     assert_eq!(data.datetime.len(), data.close.len());
97///     assert_eq!(data.close.len(), data.funding_rates.len());
98///     
99///     Ok(())
100/// }
101/// ```
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct HyperliquidData {
104    /// Trading pair symbol (e.g., "BTC", "ETH", "SOL")
105    pub symbol: String,
106    /// Array of timestamps for each data point (UTC with timezone information)
107    pub datetime: Vec<DateTime<FixedOffset>>,
108    /// Array of opening prices for each period
109    pub open: Vec<f64>,
110    /// Array of highest prices for each period
111    pub high: Vec<f64>,
112    /// Array of lowest prices for each period
113    pub low: Vec<f64>,
114    /// Array of closing prices for each period
115    pub close: Vec<f64>,
116    /// Array of trading volumes for each period
117    pub volume: Vec<f64>,
118    /// Array of funding rates (NaN for non-funding periods, typically every 8 hours)
119    pub funding_rates: Vec<f64>,
120}
121
122/// Statistics about funding rates
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct FundingStatistics {
125    /// Average funding rate over the period
126    pub average_rate: f64,
127    /// Volatility (standard deviation) of funding rates
128    pub volatility: f64,
129    /// Minimum funding rate observed
130    pub min_rate: f64,
131    /// Maximum funding rate observed
132    pub max_rate: f64,
133    /// Number of positive funding periods
134    pub positive_periods: usize,
135    /// Number of negative funding periods
136    pub negative_periods: usize,
137    /// Total funding periods
138    pub total_periods: usize,
139}
140
141/// Cacheable version of funding history for storage
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct CacheableFundingHistory {
144    /// Trading pair symbol
145    pub coin: String,
146    /// Funding rate as string
147    pub funding_rate: String,
148    /// Premium as string
149    pub premium: String,
150    /// Timestamp
151    pub time: u64,
152}
153
154impl From<&hyperliquid_rust_sdk::FundingHistoryResponse> for CacheableFundingHistory {
155    fn from(response: &hyperliquid_rust_sdk::FundingHistoryResponse) -> Self {
156        Self {
157            coin: response.coin.clone(),
158            funding_rate: response.funding_rate.clone(),
159            premium: response.premium.clone(),
160            time: response.time,
161        }
162    }
163}
164
165impl From<CacheableFundingHistory> for hyperliquid_rust_sdk::FundingHistoryResponse {
166    fn from(cacheable: CacheableFundingHistory) -> Self {
167        Self {
168            coin: cacheable.coin,
169            funding_rate: cacheable.funding_rate,
170            premium: cacheable.premium,
171            time: cacheable.time,
172        }
173    }
174}
175
176/// Data fetcher for Hyperliquid market data
177pub struct HyperliquidDataFetcher {
178    /// Hyperliquid info client
179    info_client: hyperliquid_rust_sdk::InfoClient,
180}
181
182impl HyperliquidDataFetcher {
183    /// Create a new HyperliquidDataFetcher
184    pub async fn new() -> std::result::Result<Self, hyperliquid_rust_sdk::Error> {
185        let info_client = hyperliquid_rust_sdk::InfoClient::new(None, Some(hyperliquid_rust_sdk::BaseUrl::Mainnet)).await?;
186        
187        Ok(Self {
188            info_client,
189        })
190    }
191    
192    /// Get supported time intervals
193    pub fn supported_intervals() -> &'static [&'static str] {
194        &["1m", "5m", "15m", "1h", "4h", "1d"]
195    }
196    
197    /// Check if a time interval is supported
198    pub fn is_interval_supported(interval: &str) -> bool {
199        Self::supported_intervals().contains(&interval)
200    }
201    
202    /// Get maximum time range for a given interval
203    pub fn max_time_range_for_interval(interval: &str) -> u64 {
204        match interval {
205            "1m" => 7 * 24 * 3600,      // 1 week for 1-minute data
206            "5m" => 30 * 24 * 3600,     // 1 month for 5-minute data
207            "15m" => 90 * 24 * 3600,    // 3 months for 15-minute data
208            "1h" => 365 * 24 * 3600,    // 1 year for 1-hour data
209            "4h" => 2 * 365 * 24 * 3600, // 2 years for 4-hour data
210            "1d" => 5 * 365 * 24 * 3600, // 5 years for daily data
211            _ => 365 * 24 * 3600,       // Default to 1 year
212        }
213    }
214    
215    /// Fetch OHLC data from Hyperliquid API
216    pub async fn fetch_ohlc_data(
217        &self,
218        coin: &str,
219        interval: &str,
220        start_time: u64,
221        end_time: u64,
222    ) -> Result<Vec<hyperliquid_rust_sdk::CandlesSnapshotResponse>> {
223        // Validate parameters
224        HyperliquidData::validate_fetch_parameters(coin, interval, start_time, end_time)?;
225        
226        // Fetch data from API
227        let candles = self.info_client
228            .candles_snapshot(coin.to_string(), interval.to_string(), start_time, end_time)
229            .await
230            .map_err(|e| HyperliquidBacktestError::from(e))?;
231        
232        // Validate response
233        self.validate_ohlc_response(&candles)?;
234        
235        Ok(candles)
236    }
237    
238    /// Fetch funding history from Hyperliquid API
239    pub async fn fetch_funding_history(
240        &self,
241        coin: &str,
242        start_time: u64,
243        end_time: u64,
244    ) -> Result<Vec<hyperliquid_rust_sdk::FundingHistoryResponse>> {
245        // Validate parameters
246        if coin.is_empty() {
247            return Err(HyperliquidBacktestError::validation("Coin cannot be empty"));
248        }
249        
250        if start_time >= end_time {
251            return Err(HyperliquidBacktestError::invalid_time_range(start_time, end_time));
252        }
253        
254        // Fetch data from API
255        let funding_history = self.info_client
256            .funding_history(coin.to_string(), start_time, Some(end_time))
257            .await
258            .map_err(|e| HyperliquidBacktestError::from(e))?;
259        
260        // Validate response
261        self.validate_funding_response(&funding_history)?;
262        
263        Ok(funding_history)
264    }
265    
266    /// Validate OHLC response
267    fn validate_ohlc_response(&self, candles: &[hyperliquid_rust_sdk::CandlesSnapshotResponse]) -> Result<()> {
268        if candles.is_empty() {
269            return Err(HyperliquidBacktestError::validation("No OHLC data returned from API"));
270        }
271
272        // Validate each candle
273        for (i, candle) in candles.iter().enumerate() {
274            // Check that OHLC values can be parsed as floats
275            candle.open.parse::<f64>()
276                .map_err(|_| HyperliquidBacktestError::data_conversion(
277                    format!("Invalid open price '{}' at index {}", candle.open, i)
278                ))?;
279            
280            candle.high.parse::<f64>()
281                .map_err(|_| HyperliquidBacktestError::data_conversion(
282                    format!("Invalid high price '{}' at index {}", candle.high, i)
283                ))?;
284            
285            candle.low.parse::<f64>()
286                .map_err(|_| HyperliquidBacktestError::data_conversion(
287                    format!("Invalid low price '{}' at index {}", candle.low, i)
288                ))?;
289            
290            candle.close.parse::<f64>()
291                .map_err(|_| HyperliquidBacktestError::data_conversion(
292                    format!("Invalid close price '{}' at index {}", candle.close, i)
293                ))?;
294            
295            candle.vlm.parse::<f64>()
296                .map_err(|_| HyperliquidBacktestError::data_conversion(
297                    format!("Invalid volume '{}' at index {}", candle.vlm, i)
298                ))?;
299
300            // Validate timestamp
301            if candle.time_open >= candle.time_close {
302                return Err(HyperliquidBacktestError::validation(
303                    format!("Invalid candle timestamps: open {} >= close {} at index {}", 
304                        candle.time_open, candle.time_close, i)
305                ));
306            }
307        }
308
309        // Check chronological order
310        for i in 1..candles.len() {
311            if candles[i].time_open <= candles[i - 1].time_open {
312                return Err(HyperliquidBacktestError::validation(
313                    format!("Candles not in chronological order at indices {} and {}", i - 1, i)
314                ));
315            }
316        }
317
318        Ok(())
319    }
320    
321    /// Validate funding response
322    fn validate_funding_response(&self, funding_history: &[hyperliquid_rust_sdk::FundingHistoryResponse]) -> Result<()> {
323        if funding_history.is_empty() {
324            return Ok(()); // Empty funding history is valid
325        }
326
327        // Validate each funding entry
328        for (i, entry) in funding_history.iter().enumerate() {
329            // Check that funding rate can be parsed as float
330            entry.funding_rate.parse::<f64>()
331                .map_err(|_| HyperliquidBacktestError::data_conversion(
332                    format!("Invalid funding rate '{}' at index {}", entry.funding_rate, i)
333                ))?;
334            
335            // Check that premium can be parsed as float
336            entry.premium.parse::<f64>()
337                .map_err(|_| HyperliquidBacktestError::data_conversion(
338                    format!("Invalid premium '{}' at index {}", entry.premium, i)
339                ))?;
340        }
341
342        // Check chronological order
343        for i in 1..funding_history.len() {
344            if funding_history[i].time <= funding_history[i - 1].time {
345                return Err(HyperliquidBacktestError::validation(
346                    format!("Funding history not in chronological order at indices {} and {}", i - 1, i)
347                ));
348            }
349        }
350
351        Ok(())
352    }
353    
354    /// Align OHLC and funding data
355    pub fn align_ohlc_and_funding_data(
356        &self,
357        ohlc_data: &[hyperliquid_rust_sdk::CandlesSnapshotResponse],
358        funding_data: &[hyperliquid_rust_sdk::FundingHistoryResponse],
359    ) -> Result<(Vec<DateTime<FixedOffset>>, Vec<f64>)> {
360        if ohlc_data.is_empty() {
361            return Ok((Vec::new(), Vec::new()));
362        }
363
364        let mut aligned_timestamps = Vec::new();
365        let mut aligned_funding_rates = Vec::new();
366
367        // Convert funding data to a more searchable format
368        let funding_map: HashMap<u64, f64> = funding_data
369            .iter()
370            .map(|entry| {
371                let rate = entry.funding_rate.parse::<f64>()
372                    .unwrap_or(0.0); // Default to 0 if parsing fails
373                (entry.time, rate)
374            })
375            .collect();
376
377        // For each OHLC timestamp, find the corresponding or nearest funding rate
378        for candle in ohlc_data {
379            let ohlc_timestamp = candle.time_open;
380            let datetime = FixedOffset::east_opt(0)
381                .ok_or_else(|| HyperliquidBacktestError::data_conversion(
382                    "Failed to create UTC timezone offset".to_string()
383                ))?
384                .timestamp_opt(ohlc_timestamp as i64, 0)
385                .single()
386                .ok_or_else(|| HyperliquidBacktestError::data_conversion(
387                    format!("Invalid timestamp {}", ohlc_timestamp)
388                ))?;
389
390            // Find the funding rate for this timestamp
391            let funding_rate = self.find_funding_rate_for_timestamp(ohlc_timestamp, &funding_map);
392            
393            aligned_timestamps.push(datetime);
394            aligned_funding_rates.push(funding_rate);
395        }
396
397        Ok((aligned_timestamps, aligned_funding_rates))
398    }
399    
400    /// Find funding rate for a specific timestamp
401    fn find_funding_rate_for_timestamp(
402        &self,
403        timestamp: u64,
404        funding_map: &HashMap<u64, f64>,
405    ) -> f64 {
406        // First, try exact match
407        if let Some(&rate) = funding_map.get(&timestamp) {
408            return rate;
409        }
410
411        // If no exact match, find the closest funding rate before this timestamp
412        let mut best_timestamp = 0;
413        let mut best_rate = 0.0;
414
415        for (&funding_timestamp, &rate) in funding_map.iter() {
416            if funding_timestamp <= timestamp && funding_timestamp > best_timestamp {
417                best_timestamp = funding_timestamp;
418                best_rate = rate;
419            }
420        }
421
422        // If no funding rate found before this timestamp, try to find one after
423        if best_timestamp == 0 {
424            let mut closest_timestamp = u64::MAX;
425            for (&funding_timestamp, &rate) in funding_map.iter() {
426                if funding_timestamp > timestamp && funding_timestamp < closest_timestamp {
427                    closest_timestamp = funding_timestamp;
428                    best_rate = rate;
429                }
430            }
431        }
432
433        best_rate
434    }
435}
436
437impl HyperliquidDataFetcher {
438    /// Create a new HyperliquidDataFetcher with custom error type
439    pub async fn new_with_custom_error() -> std::result::Result<Self, Box<dyn std::error::Error>> {
440        let info_client = hyperliquid_rust_sdk::InfoClient::new(None, Some(hyperliquid_rust_sdk::BaseUrl::Mainnet)).await?;
441        Ok(Self { info_client })
442    }
443}
444
445impl HyperliquidData {
446    /// Fetch historical market data from Hyperliquid API
447    ///
448    /// This is the primary method for obtaining market data for backtesting. It fetches both
449    /// OHLC price data and funding rate information from the Hyperliquid API.
450    ///
451    /// # Arguments
452    ///
453    /// * `coin` - Trading pair symbol (e.g., "BTC", "ETH", "SOL")
454    /// * `interval` - Time interval for candles ("1m", "5m", "15m", "1h", "4h", "1d")
455    /// * `start_time` - Start timestamp in Unix seconds
456    /// * `end_time` - End timestamp in Unix seconds
457    ///
458    /// # Returns
459    ///
460    /// Returns a `Result<HyperliquidData, HyperliquidBacktestError>` containing the market data
461    /// or an error if the fetch operation fails.
462    ///
463    /// # Errors
464    ///
465    /// This method can return several types of errors:
466    /// - `UnsupportedInterval` - If the interval is not supported
467    /// - `InvalidTimeRange` - If start_time >= end_time
468    /// - `HyperliquidApi` - If the API request fails
469    /// - `DataConversion` - If the response data is invalid
470    /// - `Network` - If there are network connectivity issues
471    ///
472    /// # Examples
473    ///
474    /// ```rust,no_run
475    /// use hyperliquid_backtest::prelude::*;
476    /// use chrono::Utc;
477    ///
478    /// #[tokio::main]
479    /// async fn main() -> Result<(), HyperliquidBacktestError> {
480    ///     let end_time = Utc::now().timestamp() as u64;
481    ///     let start_time = end_time - (24 * 60 * 60); // 24 hours ago
482    ///     
483    ///     // Fetch BTC data with 1-hour intervals
484    ///     let data = HyperliquidData::fetch("BTC", "1h", start_time, end_time).await?;
485    ///     
486    ///     println!("Fetched {} data points", data.len());
487    ///     println!("Latest price: ${:.2}", data.close.last().unwrap());
488    ///     
489    ///     Ok(())
490    /// }
491    /// ```
492    ///
493    /// # Performance Notes
494    ///
495    /// - Larger time ranges will take longer to fetch and process
496    /// - Consider caching data locally for repeated backtests
497    /// - Use appropriate intervals for your analysis (higher frequency = more data)
498    pub async fn fetch(
499        coin: &str,
500        interval: &str,
501        start_time: u64,
502        end_time: u64,
503    ) -> Result<Self> {
504        let fetcher = HyperliquidDataFetcher::new().await
505            .map_err(|e| HyperliquidBacktestError::HyperliquidApi(e.to_string()))?;
506        
507        // Fetch OHLC data
508        let ohlc_data = fetcher.fetch_ohlc_data(coin, interval, start_time, end_time).await?;
509        
510        // Fetch funding data
511        let funding_data = fetcher.fetch_funding_history(coin, start_time, end_time).await?;
512        
513        // Align and convert data
514        let (aligned_timestamps, aligned_funding_rates) = 
515            fetcher.align_ohlc_and_funding_data(&ohlc_data, &funding_data)?;
516        
517        // Convert OHLC data
518        let mut open = Vec::new();
519        let mut high = Vec::new();
520        let mut low = Vec::new();
521        let mut close = Vec::new();
522        let mut volume = Vec::new();
523        
524        for candle in &ohlc_data {
525            open.push(candle.open.parse::<f64>()?);
526            high.push(candle.high.parse::<f64>()?);
527            low.push(candle.low.parse::<f64>()?);
528            close.push(candle.close.parse::<f64>()?);
529            volume.push(candle.vlm.parse::<f64>()?);
530        }
531        
532        let data = Self::with_ohlc_and_funding_data(
533            coin.to_string(),
534            aligned_timestamps,
535            open,
536            high,
537            low,
538            close,
539            volume,
540            aligned_funding_rates,
541        )?;
542        
543        // Validate the final data
544        data.validate_all_data()?;
545        
546        Ok(data)
547    }
548
549    /// Create a new HyperliquidData instance with OHLC data only
550    ///
551    /// This constructor creates a HyperliquidData instance with OHLC price data but no funding
552    /// rate information. Funding rates will be set to NaN for all periods.
553    ///
554    /// # Arguments
555    ///
556    /// * `symbol` - Trading pair symbol
557    /// * `datetime` - Vector of timestamps
558    /// * `open` - Vector of opening prices
559    /// * `high` - Vector of high prices
560    /// * `low` - Vector of low prices
561    /// * `close` - Vector of closing prices
562    /// * `volume` - Vector of trading volumes
563    ///
564    /// # Returns
565    ///
566    /// Returns a `Result<HyperliquidData, HyperliquidBacktestError>` or a validation error
567    /// if the input arrays have different lengths.
568    ///
569    /// # Examples
570    ///
571    /// ```rust,no_run
572    /// use hyperliquid_backtest::prelude::*;
573    /// use chrono::{DateTime, FixedOffset, Utc};
574    ///
575    /// let timestamps = vec![Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap())];
576    /// let prices = vec![50000.0];
577    /// let volumes = vec![1000.0];
578    ///
579    /// let data = HyperliquidData::with_ohlc_data(
580    ///     "BTC".to_string(),
581    ///     timestamps,
582    ///     prices.clone(), // open
583    ///     prices.clone(), // high
584    ///     prices.clone(), // low
585    ///     prices.clone(), // close
586    ///     volumes,
587    /// )?;
588    /// # Ok::<(), HyperliquidBacktestError>(())
589    /// ```
590    pub fn with_ohlc_data(
591        symbol: String,
592        datetime: Vec<DateTime<FixedOffset>>,
593        open: Vec<f64>,
594        high: Vec<f64>,
595        low: Vec<f64>,
596        close: Vec<f64>,
597        volume: Vec<f64>,
598    ) -> Result<Self> {
599        // Validate data arrays have the same length
600        let len = datetime.len();
601        if open.len() != len || high.len() != len || low.len() != len || close.len() != len || volume.len() != len {
602            return Err(HyperliquidBacktestError::validation(
603                "All data arrays must have the same length"
604            ));
605        }
606        
607        // Create instance with empty funding rates
608        let funding_rates = vec![f64::NAN; len];
609        
610        Ok(Self {
611            symbol,
612            datetime,
613            open,
614            high,
615            low,
616            close,
617            volume,
618            funding_rates,
619        })
620    }
621    
622    /// Create a new HyperliquidData instance with OHLC and funding data
623    pub fn with_ohlc_and_funding_data(
624        symbol: String,
625        datetime: Vec<DateTime<FixedOffset>>,
626        open: Vec<f64>,
627        high: Vec<f64>,
628        low: Vec<f64>,
629        close: Vec<f64>,
630        volume: Vec<f64>,
631        funding_rates: Vec<f64>,
632    ) -> Result<Self> {
633        // Validate data arrays have the same length
634        let len = datetime.len();
635        if open.len() != len || high.len() != len || low.len() != len || close.len() != len || 
636           volume.len() != len || funding_rates.len() != len {
637            return Err(HyperliquidBacktestError::validation(
638                "All data arrays must have the same length"
639            ));
640        }
641        
642        Ok(Self {
643            symbol,
644            datetime,
645            open,
646            high,
647            low,
648            close,
649            volume,
650            funding_rates,
651        })
652    }
653    
654    /// Get the number of data points
655    pub fn len(&self) -> usize {
656        self.datetime.len()
657    }
658    
659    /// Check if the data is empty
660    pub fn is_empty(&self) -> bool {
661        self.datetime.is_empty()
662    }
663    
664    /// Validate all data for consistency
665    pub fn validate_all_data(&self) -> Result<()> {
666        // Check that all arrays have the same length
667        let len = self.datetime.len();
668        if self.open.len() != len || self.high.len() != len || self.low.len() != len || 
669           self.close.len() != len || self.volume.len() != len || self.funding_rates.len() != len {
670            return Err(HyperliquidBacktestError::validation(
671                "All data arrays must have the same length"
672            ));
673        }
674        
675        // Check that high >= low for all candles
676        for i in 0..len {
677            if self.high[i] < self.low[i] {
678                return Err(HyperliquidBacktestError::validation(
679                    format!("High price {} is less than low price {} at index {}", 
680                        self.high[i], self.low[i], i)
681                ));
682            }
683        }
684        
685        // Check that timestamps are in chronological order
686        for i in 1..len {
687            if self.datetime[i] <= self.datetime[i - 1] {
688                return Err(HyperliquidBacktestError::validation(
689                    format!("Timestamps not in chronological order at indices {} and {}", 
690                        i - 1, i)
691                ));
692            }
693        }
694        
695        Ok(())
696    }
697    
698    /// Convert to rs-backtester Data format
699    pub fn to_rs_backtester_data(&self) -> rs_backtester::datas::Data {
700        // Create a new Data struct using the load method pattern
701        // Since the fields might be private in the version we're using,
702        // let's create a temporary CSV and load it
703        use std::io::Write;
704        use tempfile::NamedTempFile;
705        
706        // Create a temporary CSV file
707        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
708        writeln!(temp_file, "DATE,OPEN,HIGH,LOW,CLOSE").expect("Failed to write header");
709        
710        for i in 0..self.datetime.len() {
711            writeln!(
712                temp_file,
713                "{},{},{},{},{}",
714                self.datetime[i].to_rfc3339(),
715                self.open[i],
716                self.high[i],
717                self.low[i],
718                self.close[i]
719            ).expect("Failed to write data");
720        }
721        
722        temp_file.flush().expect("Failed to flush temp file");
723        
724        // Load the data using the rs-backtester load method
725        rs_backtester::datas::Data::load(
726            temp_file.path().to_str().unwrap(),
727            &self.symbol
728        ).expect("Failed to load data")
729    }
730    
731    /// Get funding rate at a specific timestamp
732    pub fn get_funding_rate_at(&self, timestamp: DateTime<FixedOffset>) -> Option<f64> {
733        // Find the index of the timestamp
734        if let Some(index) = self.datetime.iter().position(|&t| t == timestamp) {
735            let rate = self.funding_rates[index];
736            if !rate.is_nan() {
737                return Some(rate);
738            }
739        }
740        
741        // If not found or NaN, return None
742        None
743    }
744    
745    /// Get the price (close) at or near a specific timestamp
746    pub fn get_price_at_or_near(&self, timestamp: DateTime<FixedOffset>) -> Option<f64> {
747        if self.datetime.is_empty() {
748            return None;
749        }
750
751        // Find exact match first
752        if let Some(index) = self.datetime.iter().position(|&t| t == timestamp) {
753            return Some(self.close[index]);
754        }
755
756        // Find the closest timestamp
757        let mut closest_index = 0;
758        let mut min_diff = i64::MAX;
759
760        for (i, &dt) in self.datetime.iter().enumerate() {
761            let diff = (dt.timestamp() - timestamp.timestamp()).abs();
762            if diff < min_diff {
763                min_diff = diff;
764                closest_index = i;
765            }
766        }
767
768        // Return the price at the closest timestamp
769        // Only return if within a reasonable time window (e.g., 24 hours)
770        if min_diff <= 24 * 3600 {
771            Some(self.close[closest_index])
772        } else {
773            None
774        }
775    }
776    
777    /// Calculate funding statistics
778    pub fn calculate_funding_statistics(&self) -> FundingStatistics {
779        let mut valid_rates = Vec::new();
780        let mut positive_periods = 0;
781        let mut negative_periods = 0;
782        
783        // Collect valid funding rates
784        for &rate in &self.funding_rates {
785            if !rate.is_nan() {
786                valid_rates.push(rate);
787                
788                if rate > 0.0 {
789                    positive_periods += 1;
790                } else if rate < 0.0 {
791                    negative_periods += 1;
792                }
793            }
794        }
795        
796        // Calculate statistics
797        let total_periods = valid_rates.len();
798        let average_rate = if total_periods > 0 {
799            valid_rates.iter().sum::<f64>() / total_periods as f64
800        } else {
801            0.0
802        };
803        
804        let min_rate = valid_rates.iter().fold(f64::INFINITY, |a, &b| a.min(b));
805        let max_rate = valid_rates.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
806        
807        // Calculate volatility (standard deviation)
808        let volatility = if total_periods > 1 {
809            let variance = valid_rates.iter()
810                .map(|&r| (r - average_rate).powi(2))
811                .sum::<f64>() / (total_periods - 1) as f64;
812            variance.sqrt()
813        } else {
814            0.0
815        };
816        
817        FundingStatistics {
818            average_rate,
819            volatility,
820            min_rate: if min_rate.is_finite() { min_rate } else { 0.0 },
821            max_rate: if max_rate.is_finite() { max_rate } else { 0.0 },
822            positive_periods,
823            negative_periods,
824            total_periods,
825        }
826    }
827    
828    /// Validate fetch parameters
829    pub fn validate_fetch_parameters(
830        coin: &str,
831        interval: &str,
832        start_time: u64,
833        end_time: u64,
834    ) -> Result<()> {
835        // Validate coin parameter
836        if coin.is_empty() {
837            return Err(HyperliquidBacktestError::validation("Coin cannot be empty"));
838        }
839
840        // Validate interval parameter
841        if !HyperliquidDataFetcher::is_interval_supported(interval) {
842            return Err(HyperliquidBacktestError::unsupported_interval(interval));
843        }
844
845        // Validate time range
846        if start_time >= end_time {
847            return Err(HyperliquidBacktestError::invalid_time_range(start_time, end_time));
848        }
849
850        // Validate that times are reasonable (not too far in the past or future)
851        let current_time = std::time::SystemTime::now()
852            .duration_since(std::time::UNIX_EPOCH)
853            .unwrap()
854            .as_secs();
855
856        if start_time > current_time + 86400 { // Not more than 1 day in the future
857            return Err(HyperliquidBacktestError::validation("Start time cannot be in the future"));
858        }
859
860        if end_time > current_time + 86400 { // Not more than 1 day in the future
861            return Err(HyperliquidBacktestError::validation("End time cannot be in the future"));
862        }
863
864        // Validate that the time range is not too large (to prevent excessive API calls)
865        let max_range_seconds = HyperliquidDataFetcher::max_time_range_for_interval(interval);
866
867        if end_time - start_time > max_range_seconds {
868            return Err(HyperliquidBacktestError::validation(
869                format!("Time range too large for interval {}. Maximum range: {} days", 
870                    interval, max_range_seconds / 86400)
871            ));
872        }
873
874        Ok(())
875    }
876    
877    /// Get list of popular trading pairs
878    pub fn popular_trading_pairs() -> &'static [&'static str] {
879        &["BTC", "ETH", "ATOM", "MATIC", "DYDX", "SOL", "AVAX", "BNB", "APE", "OP"]
880    }
881    
882    /// Check if a trading pair is popular
883    pub fn is_popular_pair(coin: &str) -> bool {
884        Self::popular_trading_pairs().contains(&coin)
885    }
886}