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