Skip to main content

quantwave_core/indicators/
system_evaluator.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4/// System Evaluation Metrics
5///
6/// Based on John Ehlers and Ric Way's "Evaluating Trading Systems".
7/// Provides a robust set of statistical performance descriptors for a trading system.
8#[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 // Cap at 100 for no losses
58        } else {
59            0.0
60        };
61
62        let ave_win = if self.num_wins > 0 {
63            self.gross_winnings / self.num_wins as f64
64        } else {
65            0.0
66        };
67        let ave_loss = if self.num_losses > 0 {
68            self.gross_losses / self.num_losses as f64
69        } else {
70            0.0
71        };
72
73        let ave_win_loss_ratio = if ave_loss > 0.0 {
74            ave_win / ave_loss
75        } else {
76            0.0
77        };
78        let average_trade = (self.gross_winnings - self.gross_losses) / total_trades;
79
80        let breakeven_pf = if win_ratio > 0.0 {
81            loss_ratio / win_ratio
82        } else {
83            100.0
84        };
85
86        // Weighted Average Trade = AverageTrade * (AveWin / AveLoss)
87        // Note: The paper derives it as T * (AveWin / AveLoss)
88        let weighted_average_trade = average_trade * ave_win_loss_ratio;
89
90        // N = Log(0.0027) / Log(1 - %)
91        // Where % is the probability of a win.
92        let theoretical_consecutive_losers = if win_ratio < 1.0 {
93            (0.0027f64.ln()) / (1.0 - win_ratio).ln()
94        } else {
95            0.0
96        };
97
98        SystemEvaluationResults {
99            average_win_loss_ratio: ave_win_loss_ratio,
100            average_trade,
101            profit_factor: pf,
102            percent_winners: win_ratio,
103            breakeven_profit_factor: breakeven_pf,
104            weighted_average_trade,
105            theoretical_consecutive_losers,
106        }
107    }
108}
109
110pub const SYSTEM_EVALUATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
111    name: "System Evaluator",
112    description: "Calculates robust statistical performance metrics for a trading system based on a stream of trade profits.",
113    usage: "Use to assess the performance quality of a trading system output using signal processing metrics. Helps distinguish systems with genuine edge from those that merely overfit.",
114    keywords: &["system", "performance", "ehlers", "statistics"],
115    ehlers_summary: "Ehlers applies signal processing metrics to evaluate trading system quality in Cybernetic Analysis. Metrics such as the Signal-to-Noise Ratio of the equity curve quantify whether a system is generating genuine signal above the noise floor of random entry and exit.",
116    params: &[],
117    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/SystemEvaluation.pdf",
118    formula_latex: r#"
119\[
120AveTrade = \% \cdot (PF + 1) - 1
121\]
122\[
123PF_{breakeven} = \frac{1 - \%}{\%}
124\]
125\[
126N_{losers} = \frac{\ln(0.0027)}{\ln(1 - \%)}
127\]
128"#,
129    gold_standard_file: "system_evaluation.json",
130    category: "Statistics",
131};
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::traits::Next;
137    use proptest::prelude::*;
138
139    #[test]
140    fn test_system_evaluator() {
141        let mut evaluator = SystemEvaluator::new();
142        // A simple system: 2 wins of 200, 1 loss of 100
143        // PF = 400 / 100 = 4.0
144        // % Win = 2 / 3 = 0.666
145        // AveTrade = (400 - 100) / 3 = 100.0
146        evaluator.next(200.0);
147        evaluator.next(-100.0);
148        let res = evaluator.next(200.0);
149
150        approx::assert_relative_eq!(res.profit_factor, 4.0);
151        approx::assert_relative_eq!(res.percent_winners, 0.6666666666666666);
152        approx::assert_relative_eq!(res.average_trade, 100.0);
153        assert!(res.weighted_average_trade > 0.0);
154    }
155
156    proptest! {
157        #[test]
158        fn test_system_evaluator_parity(
159            inputs in prop::collection::vec(-100.0..100.0, 10..100),
160        ) {
161            let mut evaluator = SystemEvaluator::new();
162            let streaming_results: Vec<SystemEvaluationResults> = inputs.iter().map(|&x| evaluator.next(x)).collect();
163
164            let mut evaluator_batch = SystemEvaluator::new();
165            let batch_results: Vec<SystemEvaluationResults> = inputs.iter().map(|&x| evaluator_batch.next(x)).collect();
166
167            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
168                approx::assert_relative_eq!(s.profit_factor, b.profit_factor, epsilon = 1e-10);
169                approx::assert_relative_eq!(s.average_trade, b.average_trade, epsilon = 1e-10);
170                approx::assert_relative_eq!(s.percent_winners, b.percent_winners, epsilon = 1e-10);
171            }
172        }
173    }
174}