finance_query/backtesting/result/mod.rs
1//! Backtest results and performance metrics.
2
3mod benchmark;
4#[cfg(test)]
5mod fixtures;
6mod metrics;
7mod periods;
8mod rolling;
9mod stats;
10mod tags;
11
12pub use benchmark::BenchmarkMetrics;
13pub use metrics::PerformanceMetrics;
14
15use serde::{Deserialize, Serialize};
16
17use super::config::BacktestConfig;
18use super::position::{Position, Trade};
19use super::signal::SignalDirection;
20
21/// Point on the equity curve
22#[non_exhaustive]
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct EquityPoint {
25 /// Timestamp
26 pub timestamp: i64,
27 /// Portfolio equity at this point
28 pub equity: f64,
29 /// Current drawdown from peak as a **fraction** (0.0–1.0, not a percentage).
30 ///
31 /// `0.0` = equity is at its running all-time high; `0.2` = 20% below peak.
32 /// Multiply by 100 to convert to a conventional percentage.
33 pub drawdown_pct: f64,
34}
35
36/// Record of a generated signal (for analysis)
37#[non_exhaustive]
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SignalRecord {
40 /// Timestamp when signal was generated
41 pub timestamp: i64,
42 /// Price at signal time
43 pub price: f64,
44 /// Signal direction
45 pub direction: SignalDirection,
46 /// Signal strength (0.0-1.0)
47 pub strength: f64,
48 /// Signal reason/description
49 pub reason: Option<String>,
50 /// Whether the signal was executed
51 pub executed: bool,
52 /// Tags copied from the originating [`Signal`](crate::backtesting::Signal).
53 ///
54 /// Enables `BacktestResult::signals` to be filtered by tag so callers
55 /// can compare total generated vs. executed signal counts per tag.
56 #[serde(default)]
57 pub tags: Vec<String>,
58}
59
60/// Complete backtest result
61#[non_exhaustive]
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BacktestResult {
64 /// Symbol that was backtested
65 pub symbol: String,
66
67 /// Strategy name
68 pub strategy_name: String,
69
70 /// Configuration used
71 pub config: BacktestConfig,
72
73 /// Start timestamp
74 pub start_timestamp: i64,
75
76 /// End timestamp
77 pub end_timestamp: i64,
78
79 /// Initial capital
80 pub initial_capital: f64,
81
82 /// Final equity
83 pub final_equity: f64,
84
85 /// Performance metrics
86 pub metrics: PerformanceMetrics,
87
88 /// Complete trade log
89 pub trades: Vec<Trade>,
90
91 /// Equity curve (portfolio value at each bar)
92 pub equity_curve: Vec<EquityPoint>,
93
94 /// All signals generated (including non-executed)
95 pub signals: Vec<SignalRecord>,
96
97 /// Current open position (if any at end)
98 pub open_position: Option<Position>,
99
100 /// Benchmark comparison metrics (set when a benchmark is provided)
101 pub benchmark: Option<BenchmarkMetrics>,
102
103 /// Diagnostic messages (e.g. why zero trades were produced).
104 ///
105 /// Empty when the backtest ran without issues. Populated with actionable
106 /// hints when the engine detects likely misconfiguration.
107 #[serde(default)]
108 pub diagnostics: Vec<String>,
109
110 /// Highest gross exposure divided by equity reached on any bar.
111 ///
112 /// `0.0` for a run that never held a position, `1.0` for one that never
113 /// borrowed. Compare against `BacktestConfig::max_leverage` to see how much
114 /// of the allowance a strategy actually used. Per-symbol results from a
115 /// portfolio backtest measure the same ratio against portfolio equity, since
116 /// their entries draw on the shared cash pool.
117 #[serde(default)]
118 pub max_leverage_used: f64,
119}
120
121impl BacktestResult {
122 /// Get a formatted summary string
123 pub fn summary(&self) -> String {
124 format!(
125 "Backtest: {} on {}\n\
126 Period: {} bars\n\
127 Initial: ${:.2} -> Final: ${:.2}\n\
128 Return: {:.2}% | Sharpe: {:.2} | Max DD: {:.2}%\n\
129 Trades: {} | Win Rate: {:.1}% | Profit Factor: {:.2}",
130 self.strategy_name,
131 self.symbol,
132 self.equity_curve.len(),
133 self.initial_capital,
134 self.final_equity,
135 self.metrics.total_return_pct,
136 self.metrics.sharpe_ratio,
137 self.metrics.max_drawdown_pct * 100.0,
138 self.metrics.total_trades,
139 self.metrics.win_rate * 100.0,
140 self.metrics.profit_factor,
141 )
142 }
143
144 /// Check if the backtest was profitable
145 pub fn is_profitable(&self) -> bool {
146 self.final_equity > self.initial_capital
147 }
148
149 /// Get total P&L
150 pub fn total_pnl(&self) -> f64 {
151 self.final_equity - self.initial_capital
152 }
153
154 /// Get the number of bars in the backtest
155 pub fn num_bars(&self) -> usize {
156 self.equity_curve.len()
157 }
158}