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}