use super::*;
use chrono::{DateTime, FixedOffset, TimeZone};
fn create_test_datetime(timestamp: i64) -> DateTime<FixedOffset> {
FixedOffset::east_opt(0).unwrap().timestamp_opt(timestamp, 0).unwrap()
}
fn create_valid_ohlc_data() -> (Vec<DateTime<FixedOffset>>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
let datetime = vec![
create_test_datetime(1640995200), create_test_datetime(1640995260), create_test_datetime(1640995320), ];
let open = vec![100.0, 101.0, 102.0];
let high = vec![105.0, 106.0, 107.0];
let low = vec![95.0, 96.0, 97.0];
let close = vec![103.0, 104.0, 105.0];
let volume = vec![1000.0, 1100.0, 1200.0];
(datetime, open, high, low, close, volume)
}
fn create_mock_candle_response(time_open: u64, coin: &str, interval: &str) -> hyperliquid_rust_sdk::CandlesSnapshotResponse {
hyperliquid_rust_sdk::CandlesSnapshotResponse {
time_open,
time_close: time_open + 60, coin: coin.to_string(),
candle_interval: interval.to_string(),
open: "100.0".to_string(),
close: "101.0".to_string(),
high: "102.0".to_string(),
low: "99.0".to_string(),
vlm: "1000.0".to_string(),
num_trades: 10,
}
}
fn create_mock_funding_response(time: u64, coin: &str) -> hyperliquid_rust_sdk::FundingHistoryResponse {
hyperliquid_rust_sdk::FundingHistoryResponse {
coin: coin.to_string(),
funding_rate: "0.0001".to_string(),
premium: "0.0".to_string(),
time,
}
}
struct MockHyperliquidDataFetcher;
impl MockHyperliquidDataFetcher {
fn validate_fetch_params(&self, coin: &str, interval: &str, start_time: u64, end_time: u64) -> Result<()> {
if coin.is_empty() {
return Err(HyperliquidBacktestError::validation("Coin cannot be empty"));
}
let valid_intervals = ["1m", "5m", "15m", "1h", "4h", "1d"];
if !valid_intervals.contains(&interval) {
return Err(HyperliquidBacktestError::unsupported_interval(interval));
}
if start_time >= end_time {
return Err(HyperliquidBacktestError::invalid_time_range(start_time, end_time));
}
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
if start_time > current_time + 86400 { return Err(HyperliquidBacktestError::validation("Start time cannot be in the future"));
}
if end_time > current_time + 86400 { return Err(HyperliquidBacktestError::validation("End time cannot be in the future"));
}
let max_range_seconds = match interval {
"1m" => 7 * 24 * 3600, "5m" => 30 * 24 * 3600, "15m" => 90 * 24 * 3600, "1h" => 365 * 24 * 3600, "4h" => 2 * 365 * 24 * 3600, "1d" => 5 * 365 * 24 * 3600, _ => 365 * 24 * 3600, };
if end_time - start_time > max_range_seconds {
return Err(HyperliquidBacktestError::validation(
format!("Time range too large for interval {}. Maximum range: {} days",
interval, max_range_seconds / 86400)
));
}
Ok(())
}
fn validate_ohlc_response(&self, candles: &[hyperliquid_rust_sdk::CandlesSnapshotResponse]) -> Result<()> {
if candles.is_empty() {
return Err(HyperliquidBacktestError::validation("No OHLC data returned from API"));
}
for (i, candle) in candles.iter().enumerate() {
candle.open.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid open price '{}' at index {}", candle.open, i)
))?;
candle.high.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid high price '{}' at index {}", candle.high, i)
))?;
candle.low.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid low price '{}' at index {}", candle.low, i)
))?;
candle.close.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid close price '{}' at index {}", candle.close, i)
))?;
candle.vlm.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid volume '{}' at index {}", candle.vlm, i)
))?;
if candle.time_open >= candle.time_close {
return Err(HyperliquidBacktestError::validation(
format!("Invalid candle timestamps: open {} >= close {} at index {}",
candle.time_open, candle.time_close, i)
));
}
}
for i in 1..candles.len() {
if candles[i].time_open <= candles[i - 1].time_open {
return Err(HyperliquidBacktestError::validation(
format!("Candles not in chronological order at indices {} and {}", i - 1, i)
));
}
}
Ok(())
}
fn validate_funding_response(&self, funding_history: &[hyperliquid_rust_sdk::FundingHistoryResponse]) -> Result<()> {
if funding_history.is_empty() {
return Ok(()); }
for (i, entry) in funding_history.iter().enumerate() {
entry.funding_rate.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid funding rate '{}' at index {}", entry.funding_rate, i)
))?;
entry.premium.parse::<f64>()
.map_err(|_| HyperliquidBacktestError::data_conversion(
format!("Invalid premium '{}' at index {}", entry.premium, i)
))?;
}
for i in 1..funding_history.len() {
if funding_history[i].time <= funding_history[i - 1].time {
return Err(HyperliquidBacktestError::validation(
format!("Funding history not in chronological order at indices {} and {}", i - 1, i)
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_rs_backtester_data() {
let (datetime, open, high, low, close, volume) = create_valid_ohlc_data();
let data = HyperliquidData::with_ohlc_data(
"BTC".to_string(),
datetime.clone(),
open.clone(),
high.clone(),
low.clone(),
close.clone(),
volume.clone(),
).unwrap();
let rs_data = data.to_rs_backtester_data();
assert_eq!(rs_data.ticker, "BTC");
assert_eq!(rs_data.datetime, datetime);
assert_eq!(rs_data.open, open);
assert_eq!(rs_data.high, high);
assert_eq!(rs_data.low, low);
assert_eq!(rs_data.close, close);
}
#[test]
fn test_hyperliquid_data_fetcher_supported_intervals() {
let intervals = HyperliquidDataFetcher::supported_intervals();
assert_eq!(intervals, &["1m", "5m", "15m", "1h", "4h", "1d"]);
}
#[test]
fn test_hyperliquid_data_fetcher_is_interval_supported() {
assert!(HyperliquidDataFetcher::is_interval_supported("1m"));
assert!(HyperliquidDataFetcher::is_interval_supported("5m"));
assert!(HyperliquidDataFetcher::is_interval_supported("15m"));
assert!(HyperliquidDataFetcher::is_interval_supported("1h"));
assert!(HyperliquidDataFetcher::is_interval_supported("4h"));
assert!(HyperliquidDataFetcher::is_interval_supported("1d"));
assert!(!HyperliquidDataFetcher::is_interval_supported("30s"));
assert!(!HyperliquidDataFetcher::is_interval_supported("2h"));
assert!(!HyperliquidDataFetcher::is_interval_supported("1w"));
}
#[test]
fn test_hyperliquid_data_fetcher_max_time_range() {
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("1m"), 7 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("5m"), 30 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("15m"), 90 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("1h"), 365 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("4h"), 2 * 365 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("1d"), 5 * 365 * 24 * 3600);
assert_eq!(HyperliquidDataFetcher::max_time_range_for_interval("unknown"), 365 * 24 * 3600);
}
#[test]
fn test_validate_fetch_params_empty_coin() {
let fetcher = MockHyperliquidDataFetcher;
let result = fetcher.validate_fetch_params("", "1h", 1640995200, 1640995260);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_fetch_params_unsupported_interval() {
let fetcher = MockHyperliquidDataFetcher;
let result = fetcher.validate_fetch_params("BTC", "30s", 1640995200, 1640995260);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::UnsupportedInterval(_)));
}
#[test]
fn test_validate_fetch_params_invalid_time_range() {
let fetcher = MockHyperliquidDataFetcher;
let result = fetcher.validate_fetch_params("BTC", "1h", 1640995260, 1640995200);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::InvalidTimeRange { .. }));
}
#[test]
fn test_validate_fetch_params_future_time() {
let fetcher = MockHyperliquidDataFetcher;
let future_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + 2 * 86400;
let result = fetcher.validate_fetch_params("BTC", "1h", future_time, future_time + 3600);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_fetch_params_time_range_too_large() {
let fetcher = MockHyperliquidDataFetcher;
let start_time = 1640995200;
let end_time = start_time + 8 * 24 * 3600;
let result = fetcher.validate_fetch_params("BTC", "1m", start_time, end_time);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_fetch_params_valid() {
let fetcher = MockHyperliquidDataFetcher;
let start_time = 1640995200;
let end_time = start_time + 3600;
let result = fetcher.validate_fetch_params("BTC", "1h", start_time, end_time);
assert!(result.is_ok());
}
#[test]
fn test_validate_ohlc_response_empty() {
let fetcher = MockHyperliquidDataFetcher;
let candles = vec![];
let result = fetcher.validate_ohlc_response(&candles);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_ohlc_response_invalid_price() {
let fetcher = MockHyperliquidDataFetcher;
let mut candle = create_mock_candle_response(1640995200, "BTC", "1h");
candle.open = "invalid".to_string();
let candles = vec![candle];
let result = fetcher.validate_ohlc_response(&candles);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::DataConversion(_)));
}
#[test]
fn test_validate_ohlc_response_invalid_timestamps() {
let fetcher = MockHyperliquidDataFetcher;
let mut candle = create_mock_candle_response(1640995200, "BTC", "1h");
candle.time_close = candle.time_open - 1; let candles = vec![candle];
let result = fetcher.validate_ohlc_response(&candles);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_ohlc_response_not_chronological() {
let fetcher = MockHyperliquidDataFetcher;
let candle1 = create_mock_candle_response(1640995260, "BTC", "1h");
let candle2 = create_mock_candle_response(1640995200, "BTC", "1h"); let candles = vec![candle1, candle2];
let result = fetcher.validate_ohlc_response(&candles);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_ohlc_response_valid() {
let fetcher = MockHyperliquidDataFetcher;
let candle1 = create_mock_candle_response(1640995200, "BTC", "1h");
let candle2 = create_mock_candle_response(1640995260, "BTC", "1h");
let candles = vec![candle1, candle2];
let result = fetcher.validate_ohlc_response(&candles);
assert!(result.is_ok());
}
#[test]
fn test_validate_funding_response_empty() {
let fetcher = MockHyperliquidDataFetcher;
let funding_history = vec![];
let result = fetcher.validate_funding_response(&funding_history);
assert!(result.is_ok()); }
#[test]
fn test_validate_funding_response_invalid_rate() {
let fetcher = MockHyperliquidDataFetcher;
let mut funding = create_mock_funding_response(1640995200, "BTC");
funding.funding_rate = "invalid".to_string();
let funding_history = vec![funding];
let result = fetcher.validate_funding_response(&funding_history);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::DataConversion(_)));
}
#[test]
fn test_validate_funding_response_not_chronological() {
let fetcher = MockHyperliquidDataFetcher;
let funding1 = create_mock_funding_response(1640995260, "BTC");
let funding2 = create_mock_funding_response(1640995200, "BTC"); let funding_history = vec![funding1, funding2];
let result = fetcher.validate_funding_response(&funding_history);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_funding_response_valid() {
let fetcher = MockHyperliquidDataFetcher;
let funding1 = create_mock_funding_response(1640995200, "BTC");
let funding2 = create_mock_funding_response(1640995260, "BTC");
let funding_history = vec![funding1, funding2];
let result = fetcher.validate_funding_response(&funding_history);
assert!(result.is_ok());
}
#[test]
fn test_cacheable_funding_history_conversion() {
let original = create_mock_funding_response(1640995200, "BTC");
let cacheable = CacheableFundingHistory::from(&original);
let converted_back: hyperliquid_rust_sdk::FundingHistoryResponse = cacheable.into();
assert_eq!(original.coin, converted_back.coin);
assert_eq!(original.funding_rate, converted_back.funding_rate);
assert_eq!(original.premium, converted_back.premium);
assert_eq!(original.time, converted_back.time);
}
#[test]
fn test_find_funding_rate_for_timestamp() {
let fetcher = MockHyperliquidDataFetcher;
let mut funding_map = std::collections::HashMap::new();
funding_map.insert(1640995200, 0.0001); funding_map.insert(1640995800, 0.0002); funding_map.insert(1640996400, -0.0001);
let rate = fetcher.find_funding_rate_for_timestamp(1640995800, &funding_map);
assert_eq!(rate, 0.0002);
let rate = fetcher.find_funding_rate_for_timestamp(1640996000, &funding_map); assert_eq!(rate, 0.0002);
let rate = fetcher.find_funding_rate_for_timestamp(1640995000, &funding_map); assert_eq!(rate, 0.0001); }
#[test]
fn test_align_ohlc_and_funding_data() {
let fetcher = MockHyperliquidDataFetcher;
let ohlc_data = vec![
create_mock_candle_response(1640995200, "BTC", "1h"), create_mock_candle_response(1640995800, "BTC", "1h"), create_mock_candle_response(1640996400, "BTC", "1h"), ];
let funding_data = vec![
create_mock_funding_response(1640995200, "BTC"), create_mock_funding_response(1640996400, "BTC"), ];
let result = fetcher.align_ohlc_and_funding_data(&ohlc_data, &funding_data);
assert!(result.is_ok());
let (timestamps, rates) = result.unwrap();
assert_eq!(timestamps.len(), 3);
assert_eq!(rates.len(), 3);
assert_eq!(rates[0], 0.0001);
assert_eq!(rates[1], 0.0001);
assert_eq!(rates[2], 0.0001);
}
#[test]
fn test_align_ohlc_and_funding_data_empty() {
let fetcher = MockHyperliquidDataFetcher;
let ohlc_data = vec![];
let funding_data = vec![];
let result = fetcher.align_ohlc_and_funding_data(&ohlc_data, &funding_data);
assert!(result.is_ok());
let (timestamps, rates) = result.unwrap();
assert!(timestamps.is_empty());
assert!(rates.is_empty());
}
}
impl MockHyperliquidDataFetcher {
fn find_funding_rate_for_timestamp(
&self,
timestamp: u64,
funding_map: &std::collections::HashMap<u64, f64>,
) -> f64 {
if let Some(&rate) = funding_map.get(×tamp) {
return rate;
}
let mut best_timestamp = 0;
let mut best_rate = 0.0;
for (&funding_timestamp, &rate) in funding_map.iter() {
if funding_timestamp <= timestamp && funding_timestamp > best_timestamp {
best_timestamp = funding_timestamp;
best_rate = rate;
}
}
if best_timestamp == 0 {
let mut closest_timestamp = u64::MAX;
for (&funding_timestamp, &rate) in funding_map.iter() {
if funding_timestamp > timestamp && funding_timestamp < closest_timestamp {
closest_timestamp = funding_timestamp;
best_rate = rate;
}
}
}
best_rate
}
fn align_ohlc_and_funding_data(
&self,
ohlc_data: &[hyperliquid_rust_sdk::CandlesSnapshotResponse],
funding_data: &[hyperliquid_rust_sdk::FundingHistoryResponse],
) -> Result<(Vec<DateTime<FixedOffset>>, Vec<f64>)> {
if ohlc_data.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let mut aligned_timestamps = Vec::new();
let mut aligned_funding_rates = Vec::new();
let funding_map: std::collections::HashMap<u64, f64> = funding_data
.iter()
.map(|entry| {
let rate = entry.funding_rate.parse::<f64>()
.unwrap_or(0.0); (entry.time, rate)
})
.collect();
for candle in ohlc_data {
let ohlc_timestamp = candle.time_open;
let datetime = FixedOffset::east_opt(0)
.ok_or_else(|| HyperliquidBacktestError::data_conversion(
"Failed to create UTC timezone offset".to_string()
))?
.timestamp_opt(ohlc_timestamp as i64, 0)
.single()
.ok_or_else(|| HyperliquidBacktestError::data_conversion(
format!("Invalid timestamp {}", ohlc_timestamp)
))?;
let funding_rate = self.find_funding_rate_for_timestamp(ohlc_timestamp, &funding_map);
aligned_timestamps.push(datetime);
aligned_funding_rates.push(funding_rate);
}
Ok((aligned_timestamps, aligned_funding_rates))
}
}
#[cfg(test)]
mod unified_fetch_tests {
use super::*;
#[test]
fn test_validate_fetch_parameters_valid() {
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", 1640995200, 1640998800);
assert!(result.is_ok());
}
#[test]
fn test_validate_fetch_parameters_empty_coin() {
let result = HyperliquidData::validate_fetch_parameters("", "1h", 1640995200, 1640998800);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_fetch_parameters_unsupported_interval() {
let result = HyperliquidData::validate_fetch_parameters("BTC", "30s", 1640995200, 1640998800);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::UnsupportedInterval(_)));
}
#[test]
fn test_validate_fetch_parameters_invalid_time_range() {
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", 1640998800, 1640995200);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::InvalidTimeRange { .. }));
}
#[test]
fn test_validate_fetch_parameters_future_time() {
let future_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + 2 * 86400;
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", future_time, future_time + 3600);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_validate_fetch_parameters_time_range_too_large() {
let start_time = 1640995200;
let end_time = start_time + 8 * 24 * 3600;
let result = HyperliquidData::validate_fetch_parameters("BTC", "1m", start_time, end_time);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HyperliquidBacktestError::Validation(_)));
}
#[test]
fn test_popular_trading_pairs() {
let pairs = HyperliquidData::popular_trading_pairs();
assert!(pairs.contains(&"BTC"));
assert!(pairs.contains(&"ETH"));
assert!(pairs.contains(&"SOL"));
assert!(pairs.contains(&"AVAX"));
assert!(pairs.contains(&"MATIC"));
assert!(pairs.contains(&"ARB"));
assert!(pairs.contains(&"OP"));
assert!(pairs.contains(&"DOGE"));
assert!(pairs.contains(&"LINK"));
assert!(pairs.contains(&"UNI"));
}
#[test]
fn test_is_popular_pair() {
assert!(HyperliquidData::is_popular_pair("BTC"));
assert!(HyperliquidData::is_popular_pair("ETH"));
assert!(HyperliquidData::is_popular_pair("SOL"));
assert!(!HyperliquidData::is_popular_pair("UNKNOWN"));
assert!(!HyperliquidData::is_popular_pair(""));
}
#[test]
fn test_fetch_last_hours_time_calculation() {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let hours = 24;
let expected_start = current_time - (hours * 3600);
assert!(expected_start <= current_time);
assert!(current_time - expected_start >= hours * 3600 - 10); }
#[test]
fn test_fetch_last_days_time_calculation() {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let days = 7;
let expected_start = current_time - (days * 24 * 3600);
assert!(expected_start <= current_time);
assert!(current_time - expected_start >= days * 24 * 3600 - 10); }
#[test]
fn test_fetch_date_range_time_conversion() {
use chrono::{TimeZone, FixedOffset};
let start_date = FixedOffset::east_opt(0).unwrap().timestamp_opt(1640995200, 0).unwrap();
let end_date = FixedOffset::east_opt(0).unwrap().timestamp_opt(1640998800, 0).unwrap();
let start_time = start_date.timestamp() as u64;
let end_time = end_date.timestamp() as u64;
assert_eq!(start_time, 1640995200);
assert_eq!(end_time, 1640998800);
assert!(end_time > start_time);
}
#[test]
fn test_convenience_methods_parameters() {
let start_time = 1640995200;
let end_time = 1640998800;
let interval = "1h";
assert!(HyperliquidData::validate_fetch_parameters("BTC", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("ETH", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("SOL", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("AVAX", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("MATIC", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("ARB", interval, start_time, end_time).is_ok());
assert!(HyperliquidData::validate_fetch_parameters("OP", interval, start_time, end_time).is_ok());
}
#[test]
fn test_supported_intervals_comprehensive() {
let supported_intervals = HyperliquidDataFetcher::supported_intervals();
for &interval in supported_intervals {
assert!(HyperliquidDataFetcher::is_interval_supported(interval));
let result = HyperliquidData::validate_fetch_parameters("BTC", interval, 1640995200, 1640998800);
assert!(result.is_ok(), "Interval {} should be supported", interval);
}
let unsupported = ["30s", "2m", "3m", "6m", "12m", "30m", "2h", "3h", "6h", "8h", "12h", "2d", "3d", "1w", "1M"];
for &interval in &unsupported {
assert!(!HyperliquidDataFetcher::is_interval_supported(interval));
let result = HyperliquidData::validate_fetch_parameters("BTC", interval, 1640995200, 1640998800);
assert!(result.is_err(), "Interval {} should not be supported", interval);
}
}
#[test]
fn test_max_time_ranges_for_intervals() {
let test_cases = [
("1m", 7 * 24 * 3600), ("5m", 30 * 24 * 3600), ("15m", 90 * 24 * 3600), ("1h", 365 * 24 * 3600), ("4h", 2 * 365 * 24 * 3600), ("1d", 5 * 365 * 24 * 3600), ];
for (interval, expected_max) in test_cases.iter() {
let max_range = HyperliquidDataFetcher::max_time_range_for_interval(interval);
assert_eq!(max_range, *expected_max, "Max range for {} should be {} seconds", interval, expected_max);
let start_time = 1609459200;
let test_range = if *expected_max > 2 * 365 * 24 * 3600 {
365 * 24 * 3600
} else {
max_range - 1
};
let end_time = start_time + test_range;
let result = HyperliquidData::validate_fetch_parameters("BTC", interval, start_time, end_time);
assert!(result.is_ok(), "Range should be valid for {} (range: {} days)", interval, test_range / 86400);
let end_time_over = start_time + max_range + 1;
let result = HyperliquidData::validate_fetch_parameters("BTC", interval, start_time, end_time_over);
assert!(result.is_err(), "Range over limit should be invalid for {}", interval);
}
}
#[test]
fn test_edge_cases_for_time_validation() {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", current_time - 3600, current_time);
assert!(result.is_ok());
let future_time = current_time + 86400;
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", current_time, future_time);
assert!(result.is_ok());
let future_time_over = current_time + 86400 + 1;
let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", current_time, future_time_over);
assert!(result.is_err());
let old_start = 1000000000; let old_end = old_start + 3600; let result = HyperliquidData::validate_fetch_parameters("BTC", "1h", old_start, old_end);
assert!(result.is_ok());
}
#[test]
fn test_comprehensive_parameter_combinations() {
let coins = ["BTC", "ETH", "SOL"];
let intervals = ["1m", "5m", "15m", "1h", "4h", "1d"];
let base_time = 1640995200;
for coin in &coins {
for interval in &intervals {
let max_range = HyperliquidDataFetcher::max_time_range_for_interval(interval);
let safe_range = max_range / 2; let end_time = base_time + safe_range;
let result = HyperliquidData::validate_fetch_parameters(coin, interval, base_time, end_time);
assert!(result.is_ok(), "Valid combination should pass: coin={}, interval={}", coin, interval);
}
}
}
}