Skip to main content

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}