1use crate::data::HyperliquidData;
87use crate::errors::{HyperliquidBacktestError, Result};
88use chrono::{DateTime, FixedOffset, Timelike};
89use serde::{Deserialize, Serialize};
90use std::collections::hash_map::DefaultHasher;
91use std::hash::{Hash, Hasher};
92use std::path::Path;
93use std::fs::File;
94use std::io::Write;
95
96#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
98pub enum OrderType {
99 Market,
101 LimitMaker,
103 LimitTaker,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
109pub enum TradingScenario {
110 OpenPosition,
112 ClosePosition,
114 ReducePosition,
116 IncreasePosition,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct HyperliquidCommission {
123 pub maker_rate: f64,
125 pub taker_rate: f64,
127 pub funding_enabled: bool,
129}
130
131impl Default for HyperliquidCommission {
132 fn default() -> Self {
133 Self {
134 maker_rate: 0.0002, taker_rate: 0.0005, funding_enabled: true,
137 }
138 }
139}
140
141impl HyperliquidCommission {
142 pub fn new(maker_rate: f64, taker_rate: f64, funding_enabled: bool) -> Self {
144 Self {
145 maker_rate,
146 taker_rate,
147 funding_enabled,
148 }
149 }
150
151 pub fn calculate_fee(&self, order_type: OrderType, trade_value: f64) -> f64 {
153 let rate = match order_type {
154 OrderType::Market | OrderType::LimitTaker => self.taker_rate,
155 OrderType::LimitMaker => self.maker_rate,
156 };
157 trade_value * rate
158 }
159
160 pub fn calculate_scenario_fee(
162 &self,
163 scenario: TradingScenario,
164 order_type: OrderType,
165 trade_value: f64,
166 ) -> f64 {
167 let base_fee = self.calculate_fee(order_type, trade_value);
169
170 match scenario {
172 TradingScenario::OpenPosition => base_fee,
173 TradingScenario::ClosePosition => base_fee,
174 TradingScenario::ReducePosition => base_fee,
175 TradingScenario::IncreasePosition => base_fee,
176 }
177 }
178
179 pub fn to_rs_backtester_commission(&self) -> rs_backtester::backtester::Commission {
181 rs_backtester::backtester::Commission {
182 rate: self.taker_rate,
183 }
184 }
185
186 pub fn validate(&self) -> Result<()> {
188 if self.maker_rate < 0.0 || self.maker_rate > 1.0 {
189 return Err(HyperliquidBacktestError::validation(
190 format!("Invalid maker rate: {}. Must be between 0.0 and 1.0", self.maker_rate)
191 ));
192 }
193 if self.taker_rate < 0.0 || self.taker_rate > 1.0 {
194 return Err(HyperliquidBacktestError::validation(
195 format!("Invalid taker rate: {}. Must be between 0.0 and 1.0", self.taker_rate)
196 ));
197 }
198 if self.maker_rate > self.taker_rate {
199 return Err(HyperliquidBacktestError::validation(
200 "Maker rate should typically be lower than taker rate".to_string()
201 ));
202 }
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum OrderTypeStrategy {
210 AlwaysMarket,
212 AlwaysMaker,
214 Mixed { maker_percentage: f64 },
216 Adaptive,
218}
219
220impl OrderTypeStrategy {
221 pub fn get_order_type(&self, trade_index: usize) -> OrderType {
223 match self {
224 OrderTypeStrategy::AlwaysMarket => OrderType::Market,
225 OrderTypeStrategy::AlwaysMaker => OrderType::LimitMaker,
226 OrderTypeStrategy::Mixed { maker_percentage } => {
227 let mut hasher = DefaultHasher::new();
229 trade_index.hash(&mut hasher);
230 let hash_value = hasher.finish();
231 let normalized = (hash_value as f64) / (u64::MAX as f64);
232
233 if normalized < *maker_percentage {
234 OrderType::LimitMaker
235 } else {
236 OrderType::Market
237 }
238 }
239 OrderTypeStrategy::Adaptive => {
240 if trade_index % 2 == 0 {
242 OrderType::LimitMaker
243 } else {
244 OrderType::Market
245 }
246 }
247 }
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct CommissionStats {
254 pub total_commission: f64,
256 pub maker_fees: f64,
258 pub taker_fees: f64,
260 pub maker_orders: usize,
262 pub taker_orders: usize,
264 pub average_rate: f64,
266 pub maker_taker_ratio: f64,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct FundingPayment {
273 pub timestamp: DateTime<chrono::FixedOffset>,
275 pub position_size: f64,
277 pub funding_rate: f64,
279 pub payment_amount: f64,
281 pub mark_price: f64,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct EnhancedMetrics {
288 pub total_return_with_funding: f64,
290 pub trading_only_return: f64,
292 pub funding_only_return: f64,
294 pub sharpe_ratio_with_funding: f64,
296 pub max_drawdown_with_funding: f64,
298 pub funding_payments_received: usize,
300 pub funding_payments_paid: usize,
302 pub average_funding_rate: f64,
304 pub funding_rate_volatility: f64,
306}
307
308impl Default for EnhancedMetrics {
309 fn default() -> Self {
310 Self {
311 total_return_with_funding: 0.0,
312 trading_only_return: 0.0,
313 funding_only_return: 0.0,
314 sharpe_ratio_with_funding: 0.0,
315 max_drawdown_with_funding: 0.0,
316 funding_payments_received: 0,
317 funding_payments_paid: 0,
318 average_funding_rate: 0.0,
319 funding_rate_volatility: 0.0,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct CommissionTracker {
327 pub total_maker_fees: f64,
329 pub total_taker_fees: f64,
331 pub maker_order_count: usize,
333 pub taker_order_count: usize,
335}
336
337impl Default for CommissionTracker {
338 fn default() -> Self {
339 Self {
340 total_maker_fees: 0.0,
341 total_taker_fees: 0.0,
342 maker_order_count: 0,
343 taker_order_count: 0,
344 }
345 }
346}
347
348impl CommissionTracker {
349 pub fn add_commission(
351 &mut self,
352 _timestamp: chrono::DateTime<chrono::FixedOffset>,
353 order_type: OrderType,
354 _trade_value: f64,
355 commission_paid: f64,
356 _scenario: TradingScenario,
357 ) {
358 match order_type {
359 OrderType::LimitMaker => {
360 self.total_maker_fees += commission_paid;
361 self.maker_order_count += 1;
362 }
363 OrderType::Market | OrderType::LimitTaker => {
364 self.total_taker_fees += commission_paid;
365 self.taker_order_count += 1;
366 }
367 }
368 }
369
370 pub fn total_commission(&self) -> f64 {
372 self.total_maker_fees + self.total_taker_fees
373 }
374
375 pub fn average_commission_rate(&self) -> f64 {
377 let total_orders = self.maker_order_count + self.taker_order_count;
378 if total_orders > 0 {
379 self.total_commission() / total_orders as f64
380 } else {
381 0.0
382 }
383 }
384
385 pub fn maker_taker_ratio(&self) -> f64 {
387 let total_orders = self.maker_order_count + self.taker_order_count;
388 if total_orders > 0 {
389 self.maker_order_count as f64 / total_orders as f64
390 } else {
391 0.0
392 }
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct EnhancedReport {
399 pub strategy_name: String,
401 pub ticker: String,
403 pub initial_capital: f64,
405 pub final_equity: f64,
407 pub total_return: f64,
409 pub trade_count: usize,
411 pub win_rate: f64,
413 pub profit_factor: f64,
415 pub sharpe_ratio: f64,
417 pub max_drawdown: f64,
419 pub enhanced_metrics: EnhancedMetrics,
421 pub commission_stats: CommissionStats,
423 pub funding_summary: FundingSummary,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct FundingSummary {
430 pub total_funding_paid: f64,
432 pub total_funding_received: f64,
434 pub net_funding: f64,
436 pub funding_payment_count: usize,
438 pub average_funding_payment: f64,
440 pub average_funding_rate: f64,
442 pub funding_rate_volatility: f64,
444 pub funding_contribution_percentage: f64,
446}
447
448#[derive(Clone)]
450pub struct HyperliquidBacktest {
451 pub base_backtest: Option<rs_backtester::backtester::Backtest>,
453 pub data: HyperliquidData,
455 pub strategy_name: String,
457 pub initial_capital: f64,
459 pub commission_config: HyperliquidCommission,
461 pub commission_tracker: CommissionTracker,
463 pub order_type_strategy: OrderTypeStrategy,
465 pub funding_pnl: Vec<f64>,
467 pub trading_pnl: Vec<f64>,
469 pub total_pnl: Vec<f64>,
471 pub total_funding_paid: f64,
473 pub total_funding_received: f64,
475 pub funding_payments: Vec<FundingPayment>,
477 pub enhanced_metrics: EnhancedMetrics,
479}
480
481impl HyperliquidBacktest {
482 pub fn new(
484 data: HyperliquidData,
485 strategy_name: String,
486 initial_capital: f64,
487 commission: HyperliquidCommission,
488 ) -> Self {
489 Self {
490 base_backtest: None,
491 data,
492 strategy_name,
493 initial_capital,
494 commission_config: commission,
495 commission_tracker: CommissionTracker::default(),
496 order_type_strategy: OrderTypeStrategy::Mixed { maker_percentage: 0.5 },
497 funding_pnl: Vec::new(),
498 trading_pnl: Vec::new(),
499 total_pnl: Vec::new(),
500 total_funding_paid: 0.0,
501 total_funding_received: 0.0,
502 funding_payments: Vec::new(),
503 enhanced_metrics: EnhancedMetrics::default(),
504 }
505 }
506
507 pub fn base_backtest(&self) -> Option<&rs_backtester::backtester::Backtest> {
509 self.base_backtest.as_ref()
510 }
511
512 pub fn base_backtest_mut(&mut self) -> Option<&mut rs_backtester::backtester::Backtest> {
514 self.base_backtest.as_mut()
515 }
516
517 pub fn with_order_type_strategy(mut self, strategy: OrderTypeStrategy) -> Self {
519 self.order_type_strategy = strategy;
520 self
521 }
522
523 pub fn initialize_base_backtest(&mut self) -> Result<()> {
525 self.data.validate_all_data()?;
527
528 let rs_data = self.data.to_rs_backtester_data();
530
531 let rs_commission = self.commission_config.to_rs_backtester_commission();
533
534 let strategy = rs_backtester::strategies::do_nothing(rs_data.clone());
536
537 let backtest = rs_backtester::backtester::Backtest::new(
538 rs_data,
539 strategy,
540 self.initial_capital,
541 rs_commission,
542 );
543
544 self.base_backtest = Some(backtest);
545
546 self.funding_pnl = vec![0.0; self.data.len()];
548 self.trading_pnl = vec![0.0; self.data.len()];
549 self.total_pnl = vec![0.0; self.data.len()];
550
551 Ok(())
552 }
553
554 pub fn calculate_with_funding(&mut self) -> Result<()> {
557 if self.base_backtest.is_none() {
559 return Err(HyperliquidBacktestError::validation(
560 "Base backtest must be initialized before calculating funding"
561 ));
562 }
563
564 let data_len = self.data.len();
566
567 if self.funding_pnl.len() != data_len {
569 self.funding_pnl = vec![0.0; data_len];
570 }
571 if self.trading_pnl.len() != data_len {
572 self.trading_pnl = vec![0.0; data_len];
573 }
574
575 self.total_funding_paid = 0.0;
577 self.total_funding_received = 0.0;
578 self.funding_payments.clear();
579
580 let current_position = 0.0;
582 let mut cumulative_funding_pnl = 0.0;
583
584 for i in 0..data_len {
586 let timestamp = self.data.datetime[i];
587 let price = self.data.close[i];
588
589 if self.is_funding_time(timestamp) {
591 if let Some(funding_rate) = self.get_funding_rate_for_timestamp(timestamp) {
593 let funding_payment = self.calculate_funding_payment(
595 current_position,
596 funding_rate,
597 price,
598 );
599
600 cumulative_funding_pnl += funding_payment;
602
603 if funding_payment > 0.0 {
605 self.total_funding_received += funding_payment;
606 } else {
607 self.total_funding_paid += funding_payment.abs();
608 }
609
610 self.funding_payments.push(FundingPayment {
612 timestamp,
613 position_size: current_position,
614 funding_rate,
615 payment_amount: funding_payment,
616 mark_price: price,
617 });
618 }
619 }
620
621 self.funding_pnl[i] = cumulative_funding_pnl;
623
624 self.trading_pnl[i] = 0.0; }
628
629 self.update_enhanced_metrics()?;
631
632 Ok(())
633 }
634
635 pub fn calculate_with_funding_and_positions(&mut self, positions: &[f64]) -> Result<()> {
638 if self.base_backtest.is_none() {
640 return Err(HyperliquidBacktestError::validation(
641 "Base backtest must be initialized before calculating funding"
642 ));
643 }
644
645 let data_len = self.data.len();
646
647 if positions.len() != data_len {
649 return Err(HyperliquidBacktestError::validation(
650 "Positions array length must match data length"
651 ));
652 }
653
654 if self.funding_pnl.len() != data_len {
656 self.funding_pnl = vec![0.0; data_len];
657 }
658 if self.trading_pnl.len() != data_len {
659 self.trading_pnl = vec![0.0; data_len];
660 }
661
662 self.total_funding_paid = 0.0;
664 self.total_funding_received = 0.0;
665 self.funding_payments.clear();
666
667 let mut cumulative_funding_pnl = 0.0;
668
669 for i in 0..data_len {
671 let timestamp = self.data.datetime[i];
672 let price = self.data.close[i];
673 let position_size = positions[i];
674
675 if self.is_funding_time(timestamp) {
677 if let Some(funding_rate) = self.get_funding_rate_for_timestamp(timestamp) {
679 let funding_payment = self.calculate_funding_payment(
681 position_size,
682 funding_rate,
683 price,
684 );
685
686 cumulative_funding_pnl += funding_payment;
688
689 if funding_payment > 0.0 {
691 self.total_funding_received += funding_payment;
692 } else {
693 self.total_funding_paid += funding_payment.abs();
694 }
695
696 self.funding_payments.push(FundingPayment {
698 timestamp,
699 position_size,
700 funding_rate,
701 payment_amount: funding_payment,
702 mark_price: price,
703 });
704 }
705 }
706
707 self.funding_pnl[i] = cumulative_funding_pnl;
709 }
710
711 self.update_enhanced_metrics()?;
713
714 Ok(())
715 }
716
717 pub fn is_funding_time(&self, timestamp: DateTime<FixedOffset>) -> bool {
720 let hour = timestamp.hour();
721 hour % 8 == 0 && timestamp.minute() == 0 && timestamp.second() == 0
722 }
723
724 pub fn get_funding_rate_for_timestamp(&self, timestamp: DateTime<FixedOffset>) -> Option<f64> {
726 self.data.get_funding_rate_at(timestamp)
727 }
728
729 pub fn calculate_funding_payment(&self, position_size: f64, funding_rate: f64, mark_price: f64) -> f64 {
733 if position_size == 0.0 {
735 return 0.0;
736 }
737
738 let funding_payment = -position_size * funding_rate * mark_price;
742
743 funding_payment
744 }
745
746 fn update_enhanced_metrics(&mut self) -> Result<()> {
748 if self.funding_pnl.is_empty() {
749 return Ok(());
750 }
751
752 let final_funding_pnl = self.funding_pnl.last().unwrap_or(&0.0);
754 self.enhanced_metrics.funding_only_return = final_funding_pnl / self.initial_capital;
755
756 let final_trading_pnl = self.trading_pnl.last().unwrap_or(&0.0);
758 self.enhanced_metrics.trading_only_return = final_trading_pnl / self.initial_capital;
759
760 self.enhanced_metrics.total_return_with_funding =
762 self.enhanced_metrics.trading_only_return + self.enhanced_metrics.funding_only_return;
763
764 self.enhanced_metrics.funding_payments_received =
766 self.funding_payments.iter().filter(|p| p.payment_amount > 0.0).count();
767 self.enhanced_metrics.funding_payments_paid =
768 self.funding_payments.iter().filter(|p| p.payment_amount < 0.0).count();
769
770 if !self.funding_payments.is_empty() {
772 let total_funding_rate: f64 = self.funding_payments.iter().map(|p| p.funding_rate).sum();
773 self.enhanced_metrics.average_funding_rate = total_funding_rate / self.funding_payments.len() as f64;
774
775 let mean_rate = self.enhanced_metrics.average_funding_rate;
777 let variance: f64 = self.funding_payments.iter()
778 .map(|p| (p.funding_rate - mean_rate).powi(2))
779 .sum::<f64>() / self.funding_payments.len() as f64;
780 self.enhanced_metrics.funding_rate_volatility = variance.sqrt();
781 }
782
783 let mut peak = self.initial_capital;
785 let mut max_drawdown = 0.0;
786
787 for i in 0..self.funding_pnl.len() {
788 let total_value = self.initial_capital + self.trading_pnl[i] + self.funding_pnl[i];
789 if total_value > peak {
790 peak = total_value;
791 }
792 let drawdown = (peak - total_value) / peak;
793 if drawdown > max_drawdown {
794 max_drawdown = drawdown;
795 }
796 }
797 self.enhanced_metrics.max_drawdown_with_funding = -max_drawdown;
798
799 if self.funding_pnl.len() > 1 {
801 let returns: Vec<f64> = (1..self.funding_pnl.len())
802 .map(|i| {
803 let prev_total = self.initial_capital + self.trading_pnl[i-1] + self.funding_pnl[i-1];
804 let curr_total = self.initial_capital + self.trading_pnl[i] + self.funding_pnl[i];
805 if prev_total > 0.0 {
806 (curr_total - prev_total) / prev_total
807 } else {
808 0.0
809 }
810 })
811 .collect();
812
813 if !returns.is_empty() {
814 let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
815 let variance = returns.iter()
816 .map(|r| (r - mean_return).powi(2))
817 .sum::<f64>() / returns.len() as f64;
818 let std_dev = variance.sqrt();
819
820 if std_dev > 0.0 {
821 self.enhanced_metrics.sharpe_ratio_with_funding = mean_return / std_dev;
822 }
823 }
824 }
825
826 Ok(())
827 }
828
829 pub fn data(&self) -> &HyperliquidData { &self.data }
831 pub fn strategy_name(&self) -> &str { &self.strategy_name }
832 pub fn initial_capital(&self) -> f64 { self.initial_capital }
833 pub fn commission_config(&self) -> &HyperliquidCommission { &self.commission_config }
834 pub fn funding_pnl(&self) -> &[f64] { &self.funding_pnl }
835 pub fn trading_pnl(&self) -> &[f64] { &self.trading_pnl }
836 pub fn total_funding_paid(&self) -> f64 { self.total_funding_paid }
837 pub fn total_funding_received(&self) -> f64 { self.total_funding_received }
838 pub fn funding_payments(&self) -> &[FundingPayment] { &self.funding_payments }
839 pub fn enhanced_metrics(&self) -> &EnhancedMetrics { &self.enhanced_metrics }
840 pub fn is_initialized(&self) -> bool { self.base_backtest.is_some() }
841
842 pub fn validate(&self) -> Result<()> {
843 self.commission_config.validate()?;
844 self.data.validate_all_data()?;
845 if self.initial_capital <= 0.0 {
846 return Err(HyperliquidBacktestError::validation("Initial capital must be positive"));
847 }
848 if self.strategy_name.is_empty() {
849 return Err(HyperliquidBacktestError::validation("Strategy name cannot be empty"));
850 }
851 Ok(())
852 }
853
854 pub fn commission_stats(&self) -> CommissionStats {
856 CommissionStats {
857 total_commission: self.commission_tracker.total_commission(),
858 maker_fees: self.commission_tracker.total_maker_fees,
859 taker_fees: self.commission_tracker.total_taker_fees,
860 maker_orders: self.commission_tracker.maker_order_count,
861 taker_orders: self.commission_tracker.taker_order_count,
862 average_rate: self.commission_tracker.average_commission_rate(),
863 maker_taker_ratio: self.commission_tracker.maker_taker_ratio(),
864 }
865 }
866
867 pub fn calculate_trade_commission(
869 &self,
870 trade_value: f64,
871 trade_index: usize,
872 scenario: TradingScenario,
873 ) -> (OrderType, f64) {
874 let order_type = self.order_type_strategy.get_order_type(trade_index);
875 let commission = self.commission_config.calculate_scenario_fee(scenario, order_type, trade_value);
876 (order_type, commission)
877 }
878
879 pub fn track_commission(
881 &mut self,
882 timestamp: DateTime<chrono::FixedOffset>,
883 order_type: OrderType,
884 trade_value: f64,
885 commission_paid: f64,
886 scenario: TradingScenario,
887 ) {
888 self.commission_tracker.add_commission(
889 timestamp,
890 order_type,
891 trade_value,
892 commission_paid,
893 scenario,
894 );
895 }
896
897 pub fn funding_summary(&self) -> FundingSummary {
899 let net_funding = self.total_funding_received - self.total_funding_paid;
900 let funding_payment_count = self.funding_payments.len();
901
902 let average_funding_payment = if funding_payment_count > 0 {
903 let total_payments: f64 = self.funding_payments.iter()
904 .map(|p| p.payment_amount)
905 .sum();
906 total_payments / funding_payment_count as f64
907 } else {
908 0.0
909 };
910
911 let funding_contribution_percentage = if self.enhanced_metrics.total_return_with_funding != 0.0 {
912 (self.enhanced_metrics.funding_only_return / self.enhanced_metrics.total_return_with_funding) * 100.0
913 } else {
914 0.0
915 };
916
917 FundingSummary {
918 total_funding_paid: self.total_funding_paid,
919 total_funding_received: self.total_funding_received,
920 net_funding,
921 funding_payment_count,
922 average_funding_payment,
923 average_funding_rate: self.enhanced_metrics.average_funding_rate,
924 funding_rate_volatility: self.enhanced_metrics.funding_rate_volatility,
925 funding_contribution_percentage,
926 }
927 }
928
929 pub fn enhanced_report(&self) -> Result<EnhancedReport> {
931 let base_backtest = match &self.base_backtest {
933 Some(backtest) => backtest,
934 None => return Err(HyperliquidBacktestError::validation(
935 "Base backtest must be initialized before generating a report"
936 )),
937 };
938
939 let final_equity = if let Some(last_position) = base_backtest.position().last() {
941 if let Some(last_close) = self.data.close.last() {
942 if let Some(last_account) = base_backtest.account().last() {
943 last_position * last_close + last_account
944 } else {
945 self.initial_capital
946 }
947 } else {
948 self.initial_capital
949 }
950 } else {
951 self.initial_capital
952 };
953
954 let total_return = (final_equity - self.initial_capital) / self.initial_capital;
955
956 let mut trade_count = 0;
958 let mut win_count = 0;
959 let mut profit_sum = 0.0;
960 let mut loss_sum = 0.0;
961
962 let orders = base_backtest.orders();
964 if orders.len() > 1 {
965 for i in 1..orders.len() {
966 if orders[i] != orders[i-1] && orders[i] != rs_backtester::orders::Order::NULL {
967 trade_count += 1;
968
969 if i < self.data.close.len() - 1 {
971 let entry_price = self.data.close[i];
972 let exit_price = self.data.close[i+1];
973 let profit = match orders[i] {
974 rs_backtester::orders::Order::BUY => exit_price - entry_price,
975 rs_backtester::orders::Order::SHORTSELL => entry_price - exit_price,
976 _ => 0.0,
977 };
978
979 if profit > 0.0 {
980 win_count += 1;
981 profit_sum += profit;
982 } else {
983 loss_sum += profit.abs();
984 }
985 }
986 }
987 }
988 }
989
990 let win_rate = if trade_count > 0 {
991 win_count as f64 / trade_count as f64
992 } else {
993 0.0
994 };
995
996 let profit_factor = if loss_sum > 0.0 {
997 profit_sum / loss_sum
998 } else if profit_sum > 0.0 {
999 f64::INFINITY
1000 } else {
1001 0.0
1002 };
1003
1004 let sharpe_ratio = self.enhanced_metrics.sharpe_ratio_with_funding;
1006
1007 let max_drawdown = self.enhanced_metrics.max_drawdown_with_funding;
1009
1010 let report = EnhancedReport {
1012 strategy_name: self.strategy_name.clone(),
1013 ticker: self.data.symbol.clone(),
1014 initial_capital: self.initial_capital,
1015 final_equity,
1016 total_return,
1017 trade_count,
1018 win_rate,
1019 profit_factor,
1020 sharpe_ratio,
1021 max_drawdown,
1022 enhanced_metrics: self.enhanced_metrics.clone(),
1023 commission_stats: self.commission_stats(),
1024 funding_summary: self.funding_summary(),
1025 };
1026
1027 Ok(report)
1028 }
1029
1030 pub fn print_enhanced_report(&self) -> Result<()> {
1032 let report = self.enhanced_report()?;
1033
1034 println!("\n=== HYPERLIQUID BACKTEST REPORT ===");
1035 println!("Strategy: {}", report.strategy_name);
1036 println!("Symbol: {}", report.ticker);
1037 println!("Period: {} to {}",
1038 self.data.datetime.first().unwrap_or(&DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap()),
1039 self.data.datetime.last().unwrap_or(&DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap())
1040 );
1041 println!("Initial Capital: ${:.2}", report.initial_capital);
1042 println!("Final Equity: ${:.2}", report.final_equity);
1043
1044 println!("\n--- Base Performance Metrics ---");
1046 println!("Total Return: {:.2}%", report.total_return * 100.0);
1047 println!("Sharpe Ratio: {:.2}", report.sharpe_ratio);
1048 println!("Max Drawdown: {:.2}%", report.max_drawdown * 100.0);
1049 println!("Win Rate: {:.2}%", report.win_rate * 100.0);
1050 println!("Profit Factor: {:.2}", report.profit_factor);
1051 println!("Trade Count: {}", report.trade_count);
1052
1053 println!("\n--- Enhanced Performance Metrics (with Funding) ---");
1055 println!("Total Return (with Funding): {:.2}%", report.enhanced_metrics.total_return_with_funding * 100.0);
1056 println!("Trading-Only Return: {:.2}%", report.enhanced_metrics.trading_only_return * 100.0);
1057 println!("Funding-Only Return: {:.2}%", report.enhanced_metrics.funding_only_return * 100.0);
1058 println!("Sharpe Ratio (with Funding): {:.2}", report.enhanced_metrics.sharpe_ratio_with_funding);
1059 println!("Max Drawdown (with Funding): {:.2}%", report.enhanced_metrics.max_drawdown_with_funding * 100.0);
1060
1061 println!("\n--- Commission Statistics ---");
1063 println!("Total Commission: ${:.2}", report.commission_stats.total_commission);
1064 println!("Maker Fees: ${:.2} ({} orders)",
1065 report.commission_stats.maker_fees,
1066 report.commission_stats.maker_orders
1067 );
1068 println!("Taker Fees: ${:.2} ({} orders)",
1069 report.commission_stats.taker_fees,
1070 report.commission_stats.taker_orders
1071 );
1072 println!("Average Commission Rate: {:.4}%", report.commission_stats.average_rate * 100.0);
1073 println!("Maker/Taker Ratio: {:.2}", report.commission_stats.maker_taker_ratio);
1074
1075 println!("\n--- Funding Summary ---");
1077 println!("Total Funding Paid: ${:.2}", report.funding_summary.total_funding_paid);
1078 println!("Total Funding Received: ${:.2}", report.funding_summary.total_funding_received);
1079 println!("Net Funding: ${:.2}", report.funding_summary.net_funding);
1080 println!("Funding Payments: {}", report.funding_summary.funding_payment_count);
1081 println!("Average Funding Payment: ${:.2}", report.funding_summary.average_funding_payment);
1082 println!("Average Funding Rate: {:.6}%", report.funding_summary.average_funding_rate * 100.0);
1083 println!("Funding Rate Volatility: {:.6}%", report.funding_summary.funding_rate_volatility * 100.0);
1084 println!("Funding Contribution: {:.2}% of total return", report.funding_summary.funding_contribution_percentage);
1085
1086 println!("\n=== END OF REPORT ===\n");
1087
1088 Ok(())
1089 }
1090
1091 pub fn export_to_csv<P: AsRef<Path>>(&self, path: P) -> Result<()> {
1093 if self.base_backtest.is_none() {
1095 return Err(HyperliquidBacktestError::validation(
1096 "Base backtest must be initialized before exporting to CSV"
1097 ));
1098 }
1099
1100 let file = File::create(path)?;
1102 let mut wtr = csv::Writer::from_writer(file);
1103
1104 wtr.write_record(&[
1106 "Timestamp",
1107 "Open",
1108 "High",
1109 "Low",
1110 "Close",
1111 "Volume",
1112 "Funding Rate",
1113 "Position",
1114 "Trading PnL",
1115 "Funding PnL",
1116 "Total PnL",
1117 "Equity",
1118 ])?;
1119
1120 for i in 0..self.data.len() {
1122 let timestamp = self.data.datetime[i].to_rfc3339();
1123 let open = self.data.open[i].to_string();
1124 let high = self.data.high[i].to_string();
1125 let low = self.data.low[i].to_string();
1126 let close = self.data.close[i].to_string();
1127 let volume = self.data.volume[i].to_string();
1128
1129 let funding_rate = match self.get_funding_rate_for_timestamp(self.data.datetime[i]) {
1131 Some(rate) => rate.to_string(),
1132 None => "".to_string(),
1133 };
1134
1135 let position = "0.0".to_string(); let trading_pnl = if i < self.trading_pnl.len() {
1140 self.trading_pnl[i].to_string()
1141 } else {
1142 "0.0".to_string()
1143 };
1144
1145 let funding_pnl = if i < self.funding_pnl.len() {
1146 self.funding_pnl[i].to_string()
1147 } else {
1148 "0.0".to_string()
1149 };
1150
1151 let total_pnl = (
1153 self.trading_pnl.get(i).unwrap_or(&0.0) +
1154 self.funding_pnl.get(i).unwrap_or(&0.0)
1155 ).to_string();
1156
1157 let equity = (
1158 self.initial_capital +
1159 self.trading_pnl.get(i).unwrap_or(&0.0) +
1160 self.funding_pnl.get(i).unwrap_or(&0.0)
1161 ).to_string();
1162
1163 wtr.write_record(&[
1165 ×tamp,
1166 &open,
1167 &high,
1168 &low,
1169 &close,
1170 &volume,
1171 &funding_rate,
1172 &position,
1173 &trading_pnl,
1174 &funding_pnl,
1175 &total_pnl,
1176 &equity,
1177 ])?;
1178 }
1179
1180 wtr.flush()?;
1182
1183 Ok(())
1184 }
1185
1186 pub fn export_funding_to_csv<P: AsRef<Path>>(&self, path: P) -> Result<()> {
1188 let file = File::create(path)?;
1190 let mut wtr = csv::Writer::from_writer(file);
1191
1192 wtr.write_record(&[
1194 "Timestamp",
1195 "Position Size",
1196 "Funding Rate",
1197 "Mark Price",
1198 "Payment Amount",
1199 ])?;
1200
1201 for payment in &self.funding_payments {
1203 wtr.write_record(&[
1204 &payment.timestamp.to_rfc3339(),
1205 &payment.position_size.to_string(),
1206 &payment.funding_rate.to_string(),
1207 &payment.mark_price.to_string(),
1208 &payment.payment_amount.to_string(),
1209 ])?;
1210 }
1211
1212 wtr.flush()?;
1214
1215 Ok(())
1216 }
1217
1218 pub fn funding_report(&self) -> Result<crate::funding_report::FundingReport> {
1220 use crate::funding_report::FundingReport;
1221
1222 if self.base_backtest.is_none() {
1224 return Err(HyperliquidBacktestError::validation(
1225 "Base backtest must be initialized before generating a funding report"
1226 ));
1227 }
1228
1229 let mut position_sizes = Vec::with_capacity(self.data.len());
1231 let mut position_values = Vec::with_capacity(self.data.len());
1232
1233 if let Some(base_backtest) = &self.base_backtest {
1235 let positions = base_backtest.position();
1236
1237 for i in 0..self.data.len() {
1238 let position_size = if i < positions.len() {
1239 positions[i]
1240 } else {
1241 0.0
1242 };
1243
1244 position_sizes.push(position_size);
1245 position_values.push(position_size * self.data.close[i]);
1246 }
1247 } else {
1248 position_sizes = vec![0.0; self.data.len()];
1250 position_values = vec![0.0; self.data.len()];
1251 }
1252
1253 let trading_pnl = if let Some(last) = self.trading_pnl.last() {
1255 *last
1256 } else {
1257 0.0
1258 };
1259
1260 let funding_pnl = if let Some(last) = self.funding_pnl.last() {
1262 *last
1263 } else {
1264 0.0
1265 };
1266
1267 FundingReport::new(
1269 &self.data.symbol,
1270 &self.data,
1271 &position_values,
1272 self.funding_payments.clone(),
1273 funding_pnl,
1274 )
1275 }
1276
1277 pub fn export_report_to_csv<P: AsRef<Path>>(&self, path: P) -> Result<()> {
1279 let report = self.enhanced_report()?;
1280
1281 let file = File::create(path)?;
1283 let mut wtr = csv::Writer::from_writer(file);
1284
1285 wtr.write_record(&["Metric", "Value"])?;
1287
1288 wtr.write_record(&["Strategy", &report.strategy_name])?;
1290 wtr.write_record(&["Symbol", &report.ticker])?;
1291 wtr.write_record(&["Initial Capital", &report.initial_capital.to_string()])?;
1292 wtr.write_record(&["Final Equity", &report.final_equity.to_string()])?;
1293
1294 wtr.write_record(&["Total Return", &(report.total_return * 100.0).to_string()])?;
1296 wtr.write_record(&["Sharpe Ratio", &report.sharpe_ratio.to_string()])?;
1297 wtr.write_record(&["Max Drawdown", &(report.max_drawdown * 100.0).to_string()])?;
1298 wtr.write_record(&["Win Rate", &(report.win_rate * 100.0).to_string()])?;
1299 wtr.write_record(&["Profit Factor", &report.profit_factor.to_string()])?;
1300 wtr.write_record(&["Trade Count", &report.trade_count.to_string()])?;
1301
1302 wtr.write_record(&["Total Return (with Funding)", &(report.enhanced_metrics.total_return_with_funding * 100.0).to_string()])?;
1304 wtr.write_record(&["Trading-Only Return", &(report.enhanced_metrics.trading_only_return * 100.0).to_string()])?;
1305 wtr.write_record(&["Funding-Only Return", &(report.enhanced_metrics.funding_only_return * 100.0).to_string()])?;
1306 wtr.write_record(&["Sharpe Ratio (with Funding)", &report.enhanced_metrics.sharpe_ratio_with_funding.to_string()])?;
1307 wtr.write_record(&["Max Drawdown (with Funding)", &(report.enhanced_metrics.max_drawdown_with_funding * 100.0).to_string()])?;
1308
1309 wtr.write_record(&["Total Commission", &report.commission_stats.total_commission.to_string()])?;
1311 wtr.write_record(&["Maker Fees", &report.commission_stats.maker_fees.to_string()])?;
1312 wtr.write_record(&["Taker Fees", &report.commission_stats.taker_fees.to_string()])?;
1313 wtr.write_record(&["Maker Orders", &report.commission_stats.maker_orders.to_string()])?;
1314 wtr.write_record(&["Taker Orders", &report.commission_stats.taker_orders.to_string()])?;
1315 wtr.write_record(&["Average Commission Rate", &(report.commission_stats.average_rate * 100.0).to_string()])?;
1316 wtr.write_record(&["Maker/Taker Ratio", &report.commission_stats.maker_taker_ratio.to_string()])?;
1317
1318 wtr.write_record(&["Total Funding Paid", &report.funding_summary.total_funding_paid.to_string()])?;
1320 wtr.write_record(&["Total Funding Received", &report.funding_summary.total_funding_received.to_string()])?;
1321 wtr.write_record(&["Net Funding", &report.funding_summary.net_funding.to_string()])?;
1322 wtr.write_record(&["Funding Payments", &report.funding_summary.funding_payment_count.to_string()])?;
1323 wtr.write_record(&["Average Funding Payment", &report.funding_summary.average_funding_payment.to_string()])?;
1324 wtr.write_record(&["Average Funding Rate", &(report.funding_summary.average_funding_rate * 100.0).to_string()])?;
1325 wtr.write_record(&["Funding Rate Volatility", &(report.funding_summary.funding_rate_volatility * 100.0).to_string()])?;
1326 wtr.write_record(&["Funding Contribution", &report.funding_summary.funding_contribution_percentage.to_string()])?;
1327
1328 wtr.flush()?;
1330
1331 Ok(())
1332 }
1333}