quantwave_core/indicators/
system_evaluator.rs1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4#[derive(Debug, Clone, Default)]
9pub struct SystemEvaluator {
10 gross_winnings: f64,
11 gross_losses: f64,
12 num_wins: usize,
13 num_losses: usize,
14 count: usize,
15}
16
17#[derive(Debug, Clone, Copy, Default)]
18pub struct SystemEvaluationResults {
19 pub average_win_loss_ratio: f64,
20 pub average_trade: f64,
21 pub profit_factor: f64,
22 pub percent_winners: f64,
23 pub breakeven_profit_factor: f64,
24 pub weighted_average_trade: f64,
25 pub theoretical_consecutive_losers: f64,
26}
27
28impl SystemEvaluator {
29 pub fn new() -> Self {
30 Self::default()
31 }
32}
33
34impl Next<f64> for SystemEvaluator {
35 type Output = SystemEvaluationResults;
36
37 fn next(&mut self, trade_profit: f64) -> Self::Output {
38 self.count += 1;
39 if trade_profit > 0.0 {
40 self.gross_winnings += trade_profit;
41 self.num_wins += 1;
42 } else if trade_profit < 0.0 {
43 self.gross_losses += trade_profit.abs();
44 self.num_losses += 1;
45 }
46
47 let total_trades = (self.num_wins + self.num_losses) as f64;
48 if total_trades == 0.0 {
49 return SystemEvaluationResults::default();
50 }
51
52 let win_ratio = self.num_wins as f64 / total_trades;
53 let loss_ratio = 1.0 - win_ratio;
54 let pf = if self.gross_losses > 0.0 {
55 self.gross_winnings / self.gross_losses
56 } else if self.gross_winnings > 0.0 {
57 100.0 } else {
59 0.0
60 };
61
62 let ave_win = if self.num_wins > 0 { self.gross_winnings / self.num_wins as f64 } else { 0.0 };
63 let ave_loss = if self.num_losses > 0 { self.gross_losses / self.num_losses as f64 } else { 0.0 };
64
65 let ave_win_loss_ratio = if ave_loss > 0.0 { ave_win / ave_loss } else { 0.0 };
66 let average_trade = (self.gross_winnings - self.gross_losses) / total_trades;
67
68 let breakeven_pf = if win_ratio > 0.0 { loss_ratio / win_ratio } else { 100.0 };
69
70 let weighted_average_trade = average_trade * ave_win_loss_ratio;
73
74 let theoretical_consecutive_losers = if win_ratio < 1.0 {
77 (0.0027f64.ln()) / (1.0 - win_ratio).ln()
78 } else {
79 0.0
80 };
81
82 SystemEvaluationResults {
83 average_win_loss_ratio: ave_win_loss_ratio,
84 average_trade,
85 profit_factor: pf,
86 percent_winners: win_ratio,
87 breakeven_profit_factor: breakeven_pf,
88 weighted_average_trade,
89 theoretical_consecutive_losers,
90 }
91 }
92}
93
94pub const SYSTEM_EVALUATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
95 name: "System Evaluator",
96 description: "Calculates robust statistical performance metrics for a trading system based on a stream of trade profits.",
97 params: &[],
98 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/SystemEvaluation.pdf",
99 formula_latex: r#"
100\[
101AveTrade = \% \cdot (PF + 1) - 1
102\]
103\[
104PF_{breakeven} = \frac{1 - \%}{\%}
105\]
106\[
107N_{losers} = \frac{\ln(0.0027)}{\ln(1 - \%)}
108\]
109"#,
110 gold_standard_file: "system_evaluation.json",
111 category: "Statistics",
112};
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::traits::Next;
118 use proptest::prelude::*;
119
120 #[test]
121 fn test_system_evaluator() {
122 let mut evaluator = SystemEvaluator::new();
123 evaluator.next(200.0);
128 evaluator.next(-100.0);
129 let res = evaluator.next(200.0);
130
131 approx::assert_relative_eq!(res.profit_factor, 4.0);
132 approx::assert_relative_eq!(res.percent_winners, 0.6666666666666666);
133 approx::assert_relative_eq!(res.average_trade, 100.0);
134 assert!(res.weighted_average_trade > 0.0);
135 }
136
137 proptest! {
138 #[test]
139 fn test_system_evaluator_parity(
140 inputs in prop::collection::vec(-100.0..100.0, 10..100),
141 ) {
142 let mut evaluator = SystemEvaluator::new();
143 let streaming_results: Vec<SystemEvaluationResults> = inputs.iter().map(|&x| evaluator.next(x)).collect();
144
145 let mut evaluator_batch = SystemEvaluator::new();
146 let batch_results: Vec<SystemEvaluationResults> = inputs.iter().map(|&x| evaluator_batch.next(x)).collect();
147
148 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
149 approx::assert_relative_eq!(s.profit_factor, b.profit_factor, epsilon = 1e-10);
150 approx::assert_relative_eq!(s.average_trade, b.average_trade, epsilon = 1e-10);
151 approx::assert_relative_eq!(s.percent_winners, b.percent_winners, epsilon = 1e-10);
152 }
153 }
154 }
155}