openpine_vm/strategy/report.rs
1//! Strategy report data structures for exporting backtest results.
2//!
3//! After executing a strategy script, call [`Instance::strategy_report()`] to
4//! obtain a [`StrategyReport`] — a complete snapshot of performance metrics,
5//! trade lists, equity curves, and daily returns sufficient for rendering a
6//! TradingView-style Strategy Tester UI.
7//!
8//! All types derive [`serde::Serialize`] with `camelCase` field names.
9//!
10//! [`Instance::strategy_report()`]: crate::Instance::strategy_report
11
12use serde::Serialize;
13
14use crate::strategy::{CloseEntriesRule, CommissionType, Direction};
15
16/// Convert NaN / Infinity to `None`; finite values to `Some(v)`.
17///
18/// `serde_json` does not support NaN by default, so any metric that can
19/// produce NaN or Infinity (e.g. `profit_factor` when there are zero
20/// losing trades) must go through this before being stored in an
21/// `Option<f64>` report field.
22#[inline]
23pub(crate) fn finite_or_none(v: f64) -> Option<f64> {
24 if v.is_finite() { Some(v) } else { None }
25}
26
27/// Complete strategy backtest report.
28///
29/// Contains three dimensions of performance metrics (All / Long / Short),
30/// per-trade lists, equity curve, drawdown curve, and daily returns.
31#[derive(Debug, Clone, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct StrategyReport {
34 /// Strategy configuration snapshot.
35 pub config: StrategyConfigReport,
36
37 /// Performance metrics for **all** trades.
38 pub performance_all: PerformanceMetrics,
39
40 /// Performance metrics for **long** trades only.
41 pub performance_long: PerformanceMetrics,
42
43 /// Performance metrics for **short** trades only.
44 pub performance_short: PerformanceMetrics,
45
46 /// Closed trades ordered by exit time.
47 pub closed_trades: Vec<ClosedTradeReport>,
48
49 /// Currently open trades.
50 pub open_trades: Vec<OpenTradeReport>,
51
52 /// Per-bar equity value (index 0 = first bar).
53 ///
54 /// `equity = initial_capital + net_profit + open_profit`
55 pub equity_curve: Vec<f64>,
56
57 /// Per-bar drawdown value (`peak_equity_at_fill - equity`, >= 0).
58 pub drawdown_curve: Vec<f64>,
59
60 /// Per-bar buy-and-hold equity (`initial_capital * open / first_open`).
61 pub buy_hold_curve: Vec<f64>,
62
63 /// Daily returns used for Sharpe / Sortino calculation.
64 pub daily_returns: Vec<DailyReturnReport>,
65
66 /// Time range of the first entry to the last exit.
67 pub trading_range: Option<TradingRangeReport>,
68}
69
70/// Aggregated performance metrics for one slice of trades
71/// (All, Long-only, or Short-only).
72///
73/// Mirrors TradingView's Performance Summary columns.
74#[derive(Debug, Clone, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct PerformanceMetrics {
77 /// Net profit in account currency.
78 pub net_profit: f64,
79 /// Net profit as percentage of initial capital.
80 pub net_profit_percent: f64,
81 /// Sum of profits from winning trades.
82 pub gross_profit: f64,
83 /// Gross profit as percentage of initial capital.
84 pub gross_profit_percent: f64,
85 /// Sum of losses from losing trades (positive number).
86 pub gross_loss: f64,
87 /// Gross loss as percentage of initial capital.
88 pub gross_loss_percent: f64,
89
90 /// Maximum equity drawdown in account currency.
91 pub max_drawdown: f64,
92 /// Maximum equity drawdown as percentage.
93 pub max_drawdown_percent: f64,
94 /// Maximum equity run-up in account currency.
95 pub max_runup: f64,
96 /// Maximum equity run-up as percentage.
97 pub max_runup_percent: f64,
98
99 /// Buy & hold return in account currency.
100 pub buy_hold_return: Option<f64>,
101 /// Buy & hold return as percentage of initial capital.
102 pub buy_hold_return_percent: Option<f64>,
103 /// Annualized Sharpe ratio.
104 pub sharpe_ratio: Option<f64>,
105 /// Annualized Sortino ratio.
106 pub sortino_ratio: Option<f64>,
107 /// Gross profit / gross loss.
108 pub profit_factor: Option<f64>,
109
110 /// Total number of closed trades.
111 pub total_closed_trades: usize,
112 /// Total number of currently open trades.
113 pub total_open_trades: usize,
114 /// Number of winning (profit > 0) closed trades.
115 pub num_winning_trades: usize,
116 /// Number of losing (profit < 0) closed trades.
117 pub num_losing_trades: usize,
118 /// Number of break-even (profit == 0) closed trades.
119 pub num_even_trades: usize,
120 /// Winning trades / total closed trades × 100.
121 pub percent_profitable: Option<f64>,
122
123 /// Average P&L per closed trade.
124 pub avg_trade: f64,
125 /// Average P&L percentage per closed trade.
126 pub avg_trade_percent: f64,
127 /// Average profit per winning trade.
128 pub avg_winning_trade: f64,
129 /// Average profit percentage per winning trade.
130 pub avg_winning_trade_percent: f64,
131 /// Average loss per losing trade.
132 pub avg_losing_trade: f64,
133 /// Average loss percentage per losing trade.
134 pub avg_losing_trade_percent: f64,
135
136 /// Average winning trade / average losing trade (absolute).
137 pub ratio_avg_win_loss: Option<f64>,
138
139 /// Largest single winning trade profit.
140 pub largest_winning_trade: f64,
141 /// Largest single winning trade profit percentage.
142 pub largest_winning_trade_percent: f64,
143 /// Largest single losing trade loss.
144 pub largest_losing_trade: f64,
145 /// Largest single losing trade loss percentage.
146 pub largest_losing_trade_percent: f64,
147
148 /// Average number of bars held across all closed trades.
149 pub avg_bars_in_trades: usize,
150 /// Average number of bars held across winning trades.
151 pub avg_bars_in_winning_trades: usize,
152 /// Average number of bars held across losing trades.
153 pub avg_bars_in_losing_trades: usize,
154
155 /// Total commission paid.
156 pub commission_paid: f64,
157 /// Peak number of contracts/shares held simultaneously.
158 pub max_contracts_held: f64,
159 /// Number of margin calls triggered.
160 pub margin_calls: usize,
161}
162
163/// A single closed (completed) trade.
164///
165/// Corresponds to one row in the "List of Trades" tab.
166#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct ClosedTradeReport {
169 /// Trade number (0-based).
170 pub trade_num: usize,
171
172 /// Entry order ID (e.g. `"Long Entry"`).
173 pub entry_id: String,
174 /// Entry order comment.
175 pub entry_comment: String,
176 /// Entry direction.
177 pub entry_side: Direction,
178 /// Entry fill price.
179 pub entry_price: f64,
180 /// Bar index at entry.
181 pub entry_bar: usize,
182 /// Entry time as UNIX milliseconds.
183 pub entry_time: i64,
184
185 /// Exit order ID.
186 pub exit_id: String,
187 /// Exit order comment.
188 pub exit_comment: String,
189 /// Exit fill price.
190 pub exit_price: f64,
191 /// Bar index at exit.
192 pub exit_bar: usize,
193 /// Exit time as UNIX milliseconds.
194 pub exit_time: i64,
195
196 /// Number of contracts / shares.
197 pub quantity: f64,
198 /// Realized profit (after commission).
199 pub profit: f64,
200 /// Realized profit as percentage of entry value.
201 pub profit_percent: f64,
202 /// Running sum of profit up to and including this trade.
203 pub cumulative_profit: f64,
204 /// Running sum of profit percent (of initial capital) up to and
205 /// including this trade.
206 pub cumulative_profit_percent: f64,
207
208 /// Maximum favorable excursion during the trade.
209 pub max_runup: f64,
210 /// Maximum favorable excursion as percentage.
211 pub max_runup_percent: f64,
212 /// Maximum adverse excursion during the trade.
213 pub max_drawdown: f64,
214 /// Maximum adverse excursion as percentage.
215 pub max_drawdown_percent: f64,
216
217 /// Total commission (entry + exit).
218 pub commission: f64,
219}
220
221/// A single currently-open trade.
222#[derive(Debug, Clone, Serialize)]
223#[serde(rename_all = "camelCase")]
224pub struct OpenTradeReport {
225 /// Trade number (0-based within open trades).
226 pub trade_num: usize,
227 /// Entry order ID.
228 pub entry_id: String,
229 /// Entry order comment.
230 pub entry_comment: String,
231 /// Entry direction.
232 pub entry_side: Direction,
233 /// Entry fill price.
234 pub entry_price: f64,
235 /// Bar index at entry.
236 pub entry_bar: usize,
237 /// Entry time as UNIX milliseconds.
238 pub entry_time: i64,
239 /// Number of contracts / shares.
240 pub quantity: f64,
241 /// Current unrealized profit (with commission).
242 pub profit: f64,
243 /// Current unrealized profit as percentage.
244 pub profit_percent: f64,
245 /// Maximum favorable excursion so far.
246 pub max_runup: f64,
247 /// Maximum favorable excursion as percentage.
248 pub max_runup_percent: f64,
249 /// Maximum adverse excursion so far.
250 pub max_drawdown: f64,
251 /// Maximum adverse excursion as percentage.
252 pub max_drawdown_percent: f64,
253 /// Commission paid (entry-side).
254 pub commission: f64,
255}
256
257/// Strategy configuration snapshot (public-facing subset).
258#[derive(Debug, Clone, Serialize)]
259#[serde(rename_all = "camelCase")]
260pub struct StrategyConfigReport {
261 /// Starting cash.
262 pub initial_capital: f64,
263 /// Commission mode.
264 pub commission_type: CommissionType,
265 /// Commission amount.
266 pub commission_value: f64,
267 /// Long margin percentage.
268 pub margin_long: f64,
269 /// Short margin percentage.
270 pub margin_short: f64,
271 /// Slippage in ticks.
272 pub slippage: f64,
273 /// Maximum entries in the same direction.
274 pub pyramiding: u32,
275 /// Annual risk-free rate for Sharpe / Sortino.
276 pub risk_free_rate: f64,
277 /// Fill orders at bar close instead of next open.
278 pub process_orders_on_close: bool,
279 /// Re-execute script on each order fill.
280 pub calc_on_order_fills: bool,
281 /// Entry close rule.
282 pub close_entries_rule: CloseEntriesRule,
283}
284
285/// One entry in the daily returns series.
286#[derive(Debug, Clone, Serialize)]
287#[serde(rename_all = "camelCase")]
288pub struct DailyReturnReport {
289 /// Date string in `YYYY-MM-DD` format.
290 pub date: String,
291 /// Net profit percentage as of this date.
292 pub return_percent: f64,
293}
294
295/// Trading time range (first entry → last exit).
296#[derive(Debug, Clone, Serialize)]
297#[serde(rename_all = "camelCase")]
298pub struct TradingRangeReport {
299 /// Start time as UNIX milliseconds.
300 pub start_time: i64,
301 /// End time as UNIX milliseconds.
302 pub end_time: i64,
303}