|
| 1 | +# Trading metrics reference {#metrics} |
| 2 | + |
| 3 | +@tableofcontents |
| 4 | + |
| 5 | +Every backtest report (`pf_report_t::metrics`, ABI v2) carries a computed |
| 6 | +trading-metrics suite plus the raw per-script-bar equity curve. This page is |
| 7 | +the complete metric list with definitions, units, NaN rules, and validation |
| 8 | +status. The per-field doxygen in `<pineforge/pineforge.h>` is the canonical |
| 9 | +contract; this page is the readable index of it. |
| 10 | + |
| 11 | +@see @ref report_schema for where the blocks sit inside `pf_report_t`. |
| 12 | + |
| 13 | +## Blocks |
| 14 | + |
| 15 | +| Block | Type | Scope | |
| 16 | +| --- | --- | --- | |
| 17 | +| `metrics.all` | #pf_trade_stats_t | All closed trades | |
| 18 | +| `metrics.longs` | #pf_trade_stats_t | Long trades only | |
| 19 | +| `metrics.shorts` | #pf_trade_stats_t | Short trades only | |
| 20 | +| `metrics.equity` | #pf_equity_stats_t | Equity-curve stats (all-trades only, like TradingView) | |
| 21 | +| `equity_curve[]` | #pf_equity_point_t | One point per script bar: `{time_ms, equity, open_profit}` | |
| 22 | + |
| 23 | +## Trade statistics (per block: All / Long / Short) |
| 24 | + |
| 25 | +Conventions: loss-side currency fields are **positive magnitudes**; percent |
| 26 | +fields are on the 0–100 scale; per-trade `%` derives from |
| 27 | +`pf_trade_t::pnl_pct` = **net return-on-cost** (`net pnl / (entry_price × |
| 28 | +qty × pointvalue) × 100`, TradingView-arbitrated). |
| 29 | + |
| 30 | +| Field | Definition | Undefined → NaN when | |
| 31 | +| --- | --- | --- | |
| 32 | +| `num_trades` | Closed trades in the block | — | |
| 33 | +| `num_wins` / `num_losses` | Trades with `pnl > 0` / `pnl < 0` | — | |
| 34 | +| `num_even` | Trades with `pnl == 0.0` exactly; break both streaks, excluded from win/loss averages | — | |
| 35 | +| `percent_profitable` | `100 · num_wins / num_trades` | no trades | |
| 36 | +| `net_profit` | Σ pnl (account currency, net of commission) | — | |
| 37 | +| `net_profit_pct` | `net_profit / initial_capital · 100` | capital ≤ 0 | |
| 38 | +| `gross_profit` (+`_pct`) | Σ winning pnl (pct vs initial capital) | capital ≤ 0 (pct) | |
| 39 | +| `gross_loss` (+`_pct`) | Σ \|losing pnl\| — positive magnitude | capital ≤ 0 (pct) | |
| 40 | +| `profit_factor` | `gross_profit / gross_loss` | zero gross loss | |
| 41 | +| `avg_trade` (+`_pct`) | `net_profit / num_trades`; pct = mean `pnl_pct` | no trades | |
| 42 | +| `avg_win` (+`_pct`) | `gross_profit / num_wins`; pct = mean win `pnl_pct` | no wins | |
| 43 | +| `avg_loss` (+`_pct`) | `gross_loss / num_losses` (positive); pct = mean negated loss `pnl_pct` | no losses | |
| 44 | +| `ratio_avg_win_avg_loss` | `avg_win / avg_loss` | either side empty | |
| 45 | +| `largest_win` (+`_pct`) | Largest win in **currency**; `_pct` = **independent max** of per-trade % (TV convention — may come from a different trade) | no wins | |
| 46 | +| `largest_loss` (+`_pct`) | Same, loss side, positive magnitudes | no losses | |
| 47 | +| `commission_paid` | Σ `pf_trade_t::commission` (captured at close from actual deductions) | — | |
| 48 | +| `expectancy` | `(num_wins/n)·avg_win − (num_losses/n)·avg_loss`, currency per trade | no trades | |
| 49 | +| `max_consecutive_wins` / `_losses` | Longest run; even trades reset both | — | |
| 50 | +| `avg_bars_in_trade` / `_wins` / `_losses` | Mean of `exit_bar − entry_bar + 1` (**inclusive** of the entry bar, TV convention), script bars | empty set | |
| 51 | + |
| 52 | +## Equity statistics (`metrics.equity`, all-trades only) |
| 53 | + |
| 54 | +The equity curve samples `initial_capital + net_profit + open_profit` at |
| 55 | +every **script-bar close**, timestamped with the script-bar **open** time |
| 56 | +(magnifier-invariant). |
| 57 | + |
| 58 | +| Field | Definition | Undefined → NaN when | |
| 59 | +| --- | --- | --- | |
| 60 | +| `max_equity_drawdown` (+`_pct`) | Peak-to-trough equity drop, positive currency; pct vs the peak in effect | — (0 when flat) | |
| 61 | +| `max_equity_runup` (+`_pct`) | Trough-to-peak rise, **trough resets on each new equity peak** (mirrors the engine's intra-run extremes); pct vs that trough | — | |
| 62 | +| `buy_hold_return` (+`_pct`) | `initial_capital · (last_close/first_open − 1)` | first open non-finite or ≤ 0 | |
| 63 | +| `sharpe_tv` / `sortino_tv` | Month-end-resampled equity simple returns (chart tz, open-time bucketing), risk-free 2 %/yr (2/12 per month), annualized ×√12. Sharpe: sample (N−1) stddev. Sortino: population downside deviation vs the monthly risk-free | < 2 monthly returns, or zero deviation | |
| 64 | +| `sharpe_bar` / `sortino_bar` | Same construction over per-script-bar returns, annualized by **observed bar density** (`bars/yr = (len−1)/calendar span`) — not a fixed calendar formula | < 2 returns, or zero deviation | |
| 65 | +| `cagr` | `100 · ((final_equity/initial_capital)^(1/years) − 1)`, calendar span | span ≤ 0, or either side ≤ 0 | |
| 66 | +| `calmar` | `cagr / max_equity_drawdown_pct` (both percent → dimensionless) | zero drawdown | |
| 67 | +| `recovery_factor` | `net_profit / max_equity_drawdown` | zero drawdown | |
| 68 | +| `time_in_market_pct` | `100 ·` script bars with an open position at close `/ total script bars` | empty curve | |
| 69 | +| `open_pl` | Mark-to-market open profit at the final bar | — | |
| 70 | + |
| 71 | +@note Metrics are only meaningful when `strategy_get_last_error()` returns |
| 72 | +an empty string: `run()` captures exceptions, so a failed run yields a |
| 73 | +truncated curve and metrics over the truncated prefix. |
| 74 | + |
| 75 | +## Validation status |
| 76 | + |
| 77 | +| Surface | Validated against | Result | |
| 78 | +| --- | --- | --- | |
| 79 | +| Trade statistics (counts, PF, percent bases, averages, largest-%, bars) | Real TradingView Strategy Tester export (`composite-4emarsi-integration-01`, 336 trades, All/Long/Short panels) | Match within TV 2-dp rounding; three TV conventions arbitrated and adopted (net return-on-cost `pnl_pct`, independent largest-%, inclusive bar counts) | |
| 80 | +| Equity statistics (max DD ±%, Sharpe/Sortino both variants, CAGR, Calmar, recovery) | quantstats 0.0.81 + empyrical-reloaded 0.5.12 (`scripts/crossvalidate_metrics.py`) | ≤ 1e-11 relative on two corpus strategies; residual deltas proven to be library conventions | |
| 81 | +| Closed-form unit oracles | `tests/test_metrics.cpp` (e.g. monthly Sharpe 19/20, Sortino 114/61 exact rationals) | Bit-level | |
| 82 | + |
| 83 | +Known open deltas: TradingView's "Max run-up (close-to-close)" uses a |
| 84 | +different run-up definition than the engine's trough-reset semantics; TV's |
| 85 | +own Sharpe/Sortino/max-DD panel values have not yet been exported for |
| 86 | +comparison (engine values are library-validated meanwhile). TV-only fields |
| 87 | +not computed: outliers, average run-up/duration, intrabar excursion |
| 88 | +variants. |
| 89 | + |
| 90 | +## Consuming from Python |
| 91 | + |
| 92 | +```python |
| 93 | +report.metrics.all.profit_factor # ctypes mirror, see FFI page |
| 94 | +report.metrics.equity.sharpe_tv |
| 95 | +curve = report.equity_curve[:report.equity_curve_len] |
| 96 | +``` |
| 97 | + |
| 98 | +Mirror classes and the mandatory `pf_abi_version()` guard: @ref ffi_python. |
0 commit comments