Results Reference
When you download a completed scenario, you get up to three payloads: a small metadata.json summary (the headline figures — final capital, return, trade counts — also shown by scenario get and in the Read Workflow), the full timeseries.json equity curve, and the per-trade trades.json log. This page documents what's inside timeseries.json and trades.json and how every headline metric is computed.
Result files exist only for a scenario that reached COMPLETE — a FAILED or CANCELLED run produces no downloadable results.
A few conventions apply throughout:
- Timestamps are Unix epoch milliseconds in UTC — divide by 1000 for seconds.
- Decimal money and price values are encoded as JSON strings (e.g.
"1.08500"), not numbers, so exact scale survives the round-trip. Parse them as decimals, not floats. - Amounts are in the account's denomination currency (the currency all subscribed instruments settle in).
Timeseries file
timeseries.json is the equity curve. The engine stores it as four parallel arrays — timestamps, units, nav, and margin_used — with one value per data point in each, aligned by index: element i of every array describes the same moment. The three decimal columns are arrays of decimal strings; timestamps is an array of integers.
| Field | Type | Meaning |
|---|---|---|
timestamps | array of integers | Sample time as Unix epoch milliseconds (UTC), one per data point. |
units | array of decimal strings | Account balance (capital): starting capital adjusted by realized P&L and commissions, excluding the unrealized P&L of open positions. |
nav | array of decimal strings | Net asset value (equity): the account balance plus the unrealized P&L of any open positions. Equals units when flat. This is the curve every return, drawdown, and Sharpe figure is computed on. |
margin_used | array of decimal strings | Margin currently locked by open positions at that point; 0 when flat. |
In this file, units is an account-capital amount, not a position size. The identically named units field in the trades file is a position size (quantity). They are unrelated.
The number of data points is adaptive, not fixed-interval — the engine samples on market activity — so don't assume a uniform time step between consecutive timestamps.
A three-point example (element i of each array is the same moment):
{
"timestamps": [1704067200000, 1704067260000, 1704067320000],
"units": ["100000.00", "100000.00", "100120.50"],
"nav": ["100000.00", "100085.00", "100120.50"],
"margin_used": ["0", "2000.00", "0"]
}
Trades file
trades.json is an envelope around one array — { "trades": [ ... ] } — with one object per trade lot, sorted by id for deterministic output. A trade that is still open when the run ends omits every close-side field, so open trades have no close_price, closed_at, realized_pnl, or close_reason. Optional fields below are also absent on legacy trades recorded before the field existed, or on zero-fee configurations for the commission fields.
| Field | Type | Meaning |
|---|---|---|
id | string | Unique identifier for this trade lot. |
state | string enum | Open or Closed. |
instrument_symbol | string | Platform symbol of the traded instrument (e.g. F:EURUSD). |
open_price | decimal string | Entry fill price, after slippage was applied. |
base_open_price | decimal string (optional) | Market price before slippage at entry (the ask for buys, the bid for sells). Omitted for legacy trades. |
open_slippage_bps | decimal string (optional) | Slippage applied at entry, in basis points. Omitted for legacy trades. |
close_price | decimal string (optional) | Exit fill price, after slippage. Omitted while the trade is open. |
base_close_price | decimal string (optional) | Market price before slippage at exit. Omitted while open or for legacy trades. |
close_slippage_bps | decimal string (optional) | Slippage applied at exit, in basis points. Omitted while open or for legacy trades. |
entry_commission | decimal string (optional) | Commission paid at entry. Omitted for legacy trades or zero-fee configs. |
exit_commission | decimal string (optional) | Commission paid at exit. Omitted while open, for legacy trades, or for zero-fee configs. |
units | decimal string | Position size (quantity) of the lot. |
direction | string enum | Long or Short. |
opened_at | integer | Entry time as Unix epoch milliseconds (UTC). |
opened_by_order_id | string | ID of the order that opened this trade. |
closed_at | integer (optional) | Exit time (epoch ms, UTC). Omitted while open. |
closed_by_order_id | string (optional) | ID of the order that closed this trade. Omitted while open. |
realized_pnl | decimal string (optional) | Realized P&L in account currency. Omitted while open. |
close_reason | string enum (optional) | Why the trade closed: Normal (strategy closed it), Liquidation (margin stop-out), or EndOfBacktest (force-closed when the window ended). Omitted while open. |
Realized P&L is notional: units × (close_price − open_price) for a Long, and units × (open_price − close_price) for a Short. Because open_price and close_price are the post-slippage fill prices, slippage and spread are already reflected in the P&L; commissions are reported separately in entry_commission / exit_commission.
An example with one closed trade and one still-open trade — note the open trade omits every close-side field:
{
"trades": [
{
"id": "trade-000001",
"state": "Closed",
"instrument_symbol": "F:EURUSD",
"open_price": "1.08525",
"base_open_price": "1.08500",
"open_slippage_bps": "2.3",
"close_price": "1.09010",
"base_close_price": "1.09000",
"close_slippage_bps": "0.9",
"entry_commission": "2.50",
"exit_commission": "2.50",
"units": "10000",
"direction": "Long",
"opened_at": 1704067200000,
"opened_by_order_id": "ord_a1b2",
"closed_at": 1704153600000,
"closed_by_order_id": "ord_c3d4",
"realized_pnl": "48.50",
"close_reason": "Normal"
},
{
"id": "trade-000002",
"state": "Open",
"instrument_symbol": "F:GBPUSD",
"open_price": "1.26010",
"base_open_price": "1.26000",
"open_slippage_bps": "0.8",
"units": "5000",
"direction": "Short",
"opened_at": 1704070800000,
"opened_by_order_id": "ord_e5f6"
}
]
}
Headline metrics
These are the top-line performance figures reported for a completed scenario (see Write Workflow → Read the results for the scenario get shape). All are derived from the nav (equity) curve and the closed trades — you don't compute them yourself.
Percentage metrics are expressed as percentages, not fractions — 12.45 means 12.45%, not 1245%. Monetary metrics (netPnl, grossPnl) are in the account's denomination currency.
| Field | Definition & computation basis |
|---|---|
totalReturn | Total return over the run: (final NAV − initial NAV) / initial NAV × 100. A percentage. |
maxDrawdownPct | Deepest peak-to-trough decline of the NAV (equity) curve across the whole run: (peak − trough) / peak × 100. A percentage. |
sharpeRatio | Mean ÷ sample standard deviation of daily returns, annualized by × √(trading days per year), at a 0% risk-free rate. NAV is first resampled to one UTC daily close before returns are taken. Trading days per year is 252 (or 365 if every subscribed instrument is crypto). |
winRatePct | Winning trades ÷ total counted trades × 100, where a winner has realized P&L > 0. A percentage. |
totalTrades | Number of closed trades counted for the statistics above. |
netPnl | final NAV − initial NAV, in account currency — already net of commissions, since the engine deducts them from equity as the run proceeds. |
grossPnl | netPnl + totalCommission — P&L before commission (it adds commission back). Spread and slippage stay embedded in fill prices and are not separated out. |
Additional metrics
The full metrics object carries more detail. Percentages follow the same convention; the risk-adjusted and per-trade fields are omitted, each with a companion …Reason, when there isn't enough data to compute them (e.g. a flat curve, or a run with no losing trades). The …Reason value is one of insufficient_data_points, zero_standard_deviation, no_negative_returns, no_winning_trades, no_losing_trades, or no_closed_trades.
| Field | Definition & computation basis |
|---|---|
annualizedReturnPct | totalReturn geometrically annualized: ((1 + totalReturn/100) ^ (365 / durationDays) − 1) × 100, on a 365-calendar-day year. A percentage. |
maxDrawdownDurationDays | Longest span between successive equity highs (peak to recovery), in days; extends to the end of the run if NAV never recovers its last peak. |
sortinoRatio | Like sharpeRatio but the denominator is the downside deviation of daily returns (negative days only, divided by the total sample count), annualized the same way. Omitted when there are no negative daily returns. |
winningTrades | Count of trades with realized P&L > 0. |
losingTrades | Count of trades with realized P&L < 0. |
profitFactor | Gross profit ÷ gross loss (sum of winners' P&L ÷ absolute sum of losers' P&L). Omitted when there are no losing trades. |
avgWin | Mean P&L of winning trades. |
avgLoss | Mean absolute P&L of losing trades. |
largestWin | Largest single winning trade. |
largestLoss | Largest single losing trade (absolute). |
avgTradeDurationSeconds | Mean of closed_at − opened_at across trades, in seconds. |
totalCommission | Total commission across all trades, including entry fees on still-open trades. Spread and slippage are embedded in fill prices and are not part of this figure; 0 for zero-fee brokers. |
Related
- Read Workflow — how to download these files.
- Execution & Costs — how spread, slippage, commission, and funding enter the fill prices these files record.
- Available Instruments — the symbols that appear in
instrument_symbol.