use crate::backtest::*;
use crate::data::HyperliquidData;
use crate::errors::Result;
use chrono::{DateTime, FixedOffset, TimeZone};
use rs_backtester::strategies::Strategy;
fn create_test_data() -> HyperliquidData {
let mut datetime = Vec::new();
let mut open = Vec::new();
let mut high = Vec::new();
let mut low = Vec::new();
let mut close = Vec::new();
let mut volume = Vec::new();
let mut funding_rates = Vec::new();
let base_timestamp = 1640995200;
for i in 0..10*24 {
let timestamp = FixedOffset::east_opt(0).unwrap()
.timestamp_opt(base_timestamp + i * 3600, 0).unwrap();
datetime.push(timestamp);
let trend = (i as f64) * 0.01;
let cycle = ((i as f64) * 0.1).sin() * 5.0;
let price = 100.0 + trend + cycle;
open.push(price - 0.5);
high.push(price + 1.0);
low.push(price - 1.0);
close.push(price);
volume.push(1000.0 + (i as f64 % 24.0) * 100.0);
if timestamp.hour() % 8 == 0 {
let funding_cycle = ((i as f64) * 0.05).sin() * 0.0002;
funding_rates.push(funding_cycle);
} else {
funding_rates.push(f64::NAN);
}
}
HyperliquidData {
symbol: "BTC".to_string(),
datetime,
open,
high,
low,
close,
volume,
funding_rates,
}
}
fn create_test_strategy(data: rs_backtester::datas::Data) -> Strategy {
let mut strategy = Strategy::new();
strategy.next(Box::new(move |ctx, _| {
let index = ctx.index();
if index < 5 {
return; }
let price = ctx.data().close[index];
if price > 100.0 {
ctx.entry_qty(1.0);
} else if price < 100.0 {
ctx.entry_qty(-1.0);
} else {
ctx.exit();
}
}));
strategy
}
#[test]
fn test_hyperliquid_backtest_new() {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
);
assert_eq!(backtest.strategy_name(), &strategy_name);
assert_eq!(backtest.initial_capital(), initial_capital);
assert_eq!(backtest.data().symbol, data.symbol);
assert_eq!(backtest.data().len(), data.len());
assert!(!backtest.is_initialized()); }
#[test]
fn test_hyperliquid_backtest_with_order_type_strategy() {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
).with_order_type_strategy(OrderTypeStrategy::AlwaysMaker);
match backtest.order_type_strategy {
OrderTypeStrategy::AlwaysMaker => {}, _ => panic!("Order type strategy not set correctly"),
}
}
#[test]
fn test_initialize_base_backtest() -> Result<()> {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
);
backtest.initialize_base_backtest()?;
assert!(backtest.is_initialized());
assert!(backtest.base_backtest().is_some());
assert_eq!(backtest.funding_pnl().len(), data.len());
assert_eq!(backtest.trading_pnl().len(), data.len());
Ok(())
}
#[test]
fn test_calculate_with_funding() -> Result<()> {
let data = create_test_data();
let rs_data = data.to_rs_backtester_data();
let strategy = create_test_strategy(rs_data);
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
"Test Strategy".to_string(),
initial_capital,
commission.clone(),
);
backtest.base_backtest = Some(rs_backtester::backtester::Backtest::new(
rs_data,
strategy,
initial_capital,
commission.to_rs_backtester_commission(),
));
backtest.calculate_with_funding()?;
assert!(!backtest.funding_payments.is_empty());
assert_eq!(backtest.funding_pnl.len(), data.len());
assert!(backtest.enhanced_metrics.funding_payments_received >= 0);
assert!(backtest.enhanced_metrics.funding_payments_paid >= 0);
Ok(())
}
#[test]
fn test_calculate_with_funding_and_positions() -> Result<()> {
let data = create_test_data();
let rs_data = data.to_rs_backtester_data();
let strategy = create_test_strategy(rs_data);
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
"Test Strategy".to_string(),
initial_capital,
commission.clone(),
);
backtest.base_backtest = Some(rs_backtester::backtester::Backtest::new(
rs_data,
strategy,
initial_capital,
commission.to_rs_backtester_commission(),
));
let positions: Vec<f64> = (0..data.len())
.map(|i| if i % 16 < 8 { 1.0 } else { -1.0 })
.collect();
backtest.calculate_with_funding_and_positions(&positions)?;
assert!(!backtest.funding_payments.is_empty());
assert_eq!(backtest.funding_pnl.len(), data.len());
assert!(backtest.enhanced_metrics.funding_payments_received >= 0);
assert!(backtest.enhanced_metrics.funding_payments_paid >= 0);
Ok(())
}
#[test]
fn test_validate() -> Result<()> {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
);
let result = backtest.validate();
assert!(result.is_ok());
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
0.0, commission.clone(),
);
let result = backtest.validate();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("Initial capital must be positive"));
}
let backtest = HyperliquidBacktest::new(
data.clone(),
"".to_string(), initial_capital,
commission.clone(),
);
let result = backtest.validate();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("Strategy name cannot be empty"));
}
Ok(())
}
#[test]
fn test_commission_stats() -> Result<()> {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
);
let timestamp = FixedOffset::east_opt(0).unwrap()
.timestamp_opt(1640995200, 0).unwrap();
backtest.track_commission(
timestamp,
OrderType::LimitMaker,
10000.0,
2.0,
TradingScenario::OpenPosition
);
backtest.track_commission(
timestamp,
OrderType::Market,
20000.0,
10.0,
TradingScenario::ClosePosition
);
let stats = backtest.commission_stats();
assert_eq!(stats.total_commission, 12.0);
assert_eq!(stats.maker_fees, 2.0);
assert_eq!(stats.taker_fees, 10.0);
assert_eq!(stats.maker_orders, 1);
assert_eq!(stats.taker_orders, 1);
assert_eq!(stats.average_rate, 6.0); assert_eq!(stats.maker_taker_ratio, 0.5);
Ok(())
}
#[test]
fn test_calculate_trade_commission() {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
).with_order_type_strategy(OrderTypeStrategy::AlwaysMarket);
let (order_type, fee) = backtest.calculate_trade_commission(
10000.0,
0,
TradingScenario::OpenPosition
);
assert_eq!(order_type, OrderType::Market);
assert_eq!(fee, 10000.0 * 0.0005);
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
).with_order_type_strategy(OrderTypeStrategy::AlwaysMaker);
let (order_type, fee) = backtest.calculate_trade_commission(
10000.0,
0,
TradingScenario::OpenPosition
);
assert_eq!(order_type, OrderType::LimitMaker);
assert_eq!(fee, 10000.0 * 0.0002);
let backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
).with_order_type_strategy(OrderTypeStrategy::Mixed { maker_percentage: 0.0 });
let (order_type, fee) = backtest.calculate_trade_commission(
10000.0,
0,
TradingScenario::OpenPosition
);
assert_eq!(order_type, OrderType::Market);
assert_eq!(fee, 10000.0 * 0.0005); }
#[test]
fn test_funding_summary() -> Result<()> {
let data = create_test_data();
let strategy_name = "Test Strategy".to_string();
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
strategy_name.clone(),
initial_capital,
commission.clone(),
);
backtest.initialize_base_backtest()?;
let positions = vec![1.0; data.len()];
backtest.calculate_with_funding_and_positions(&positions)?;
let summary = backtest.funding_summary();
assert_eq!(summary.total_funding_paid, backtest.total_funding_paid);
assert_eq!(summary.total_funding_received, backtest.total_funding_received);
assert_eq!(summary.net_funding, backtest.total_funding_received - backtest.total_funding_paid);
assert_eq!(summary.funding_payment_count, backtest.funding_payments.len());
if !backtest.funding_payments.is_empty() {
let total_payments: f64 = backtest.funding_payments.iter()
.map(|p| p.payment_amount)
.sum();
let expected_avg = total_payments / backtest.funding_payments.len() as f64;
assert_eq!(summary.average_funding_payment, expected_avg);
}
Ok(())
}
#[test]
fn test_enhanced_report() -> Result<()> {
let data = create_test_data();
let rs_data = data.to_rs_backtester_data();
let strategy = create_test_strategy(rs_data);
let initial_capital = 10000.0;
let commission = HyperliquidCommission::default();
let mut backtest = HyperliquidBacktest::new(
data.clone(),
"Test Strategy".to_string(),
initial_capital,
commission.clone(),
);
backtest.base_backtest = Some(rs_backtester::backtester::Backtest::new(
rs_data,
strategy,
initial_capital,
commission.to_rs_backtester_commission(),
));
backtest.calculate_with_funding()?;
let report = backtest.enhanced_report();
assert_eq!(report.strategy_name, "Test Strategy");
assert_eq!(report.ticker, "BTC");
assert_eq!(report.initial_capital, initial_capital);
assert_eq!(report.enhanced_metrics.funding_only_return, backtest.enhanced_metrics.funding_only_return);
assert_eq!(report.enhanced_metrics.trading_only_return, backtest.enhanced_metrics.trading_only_return);
assert_eq!(report.enhanced_metrics.total_return_with_funding, backtest.enhanced_metrics.total_return_with_funding);
assert_eq!(report.commission_stats.total_commission, backtest.commission_tracker.total_commission());
assert_eq!(report.commission_stats.maker_fees, backtest.commission_tracker.total_maker_fees);
assert_eq!(report.commission_stats.taker_fees, backtest.commission_tracker.total_taker_fees);
assert_eq!(report.funding_summary.total_funding_paid, backtest.total_funding_paid);
assert_eq!(report.funding_summary.total_funding_received, backtest.total_funding_received);
assert_eq!(report.funding_summary.net_funding, backtest.total_funding_received - backtest.total_funding_paid);
Ok(())
}