Tektii just went live. We're shipping fixes daily — spot a bug? Let us know

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 arraystimestamps, 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.

FieldTypeMeaning
timestampsarray of integersSample time as Unix epoch milliseconds (UTC), one per data point.
unitsarray of decimal stringsAccount balance (capital): starting capital adjusted by realized P&L and commissions, excluding the unrealized P&L of open positions.
navarray of decimal stringsNet 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_usedarray of decimal stringsMargin currently locked by open positions at that point; 0 when flat.

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.

FieldTypeMeaning
idstringUnique identifier for this trade lot.
statestring enumOpen or Closed.
instrument_symbolstringPlatform symbol of the traded instrument (e.g. F:EURUSD).
open_pricedecimal stringEntry fill price, after slippage was applied.
base_open_pricedecimal string (optional)Market price before slippage at entry (the ask for buys, the bid for sells). Omitted for legacy trades.
open_slippage_bpsdecimal string (optional)Slippage applied at entry, in basis points. Omitted for legacy trades.
close_pricedecimal string (optional)Exit fill price, after slippage. Omitted while the trade is open.
base_close_pricedecimal string (optional)Market price before slippage at exit. Omitted while open or for legacy trades.
close_slippage_bpsdecimal string (optional)Slippage applied at exit, in basis points. Omitted while open or for legacy trades.
entry_commissiondecimal string (optional)Commission paid at entry. Omitted for legacy trades or zero-fee configs.
exit_commissiondecimal string (optional)Commission paid at exit. Omitted while open, for legacy trades, or for zero-fee configs.
unitsdecimal stringPosition size (quantity) of the lot.
directionstring enumLong or Short.
opened_atintegerEntry time as Unix epoch milliseconds (UTC).
opened_by_order_idstringID of the order that opened this trade.
closed_atinteger (optional)Exit time (epoch ms, UTC). Omitted while open.
closed_by_order_idstring (optional)ID of the order that closed this trade. Omitted while open.
realized_pnldecimal string (optional)Realized P&L in account currency. Omitted while open.
close_reasonstring 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 fractions12.45 means 12.45%, not 1245%. Monetary metrics (netPnl, grossPnl) are in the account's denomination currency.

FieldDefinition & computation basis
totalReturnTotal return over the run: (final NAV − initial NAV) / initial NAV × 100. A percentage.
maxDrawdownPctDeepest peak-to-trough decline of the NAV (equity) curve across the whole run: (peak − trough) / peak × 100. A percentage.
sharpeRatioMean ÷ 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).
winRatePctWinning trades ÷ total counted trades × 100, where a winner has realized P&L > 0. A percentage.
totalTradesNumber of closed trades counted for the statistics above.
netPnlfinal NAV − initial NAV, in account currency — already net of commissions, since the engine deducts them from equity as the run proceeds.
grossPnlnetPnl + 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.

FieldDefinition & computation basis
annualizedReturnPcttotalReturn geometrically annualized: ((1 + totalReturn/100) ^ (365 / durationDays) − 1) × 100, on a 365-calendar-day year. A percentage.
maxDrawdownDurationDaysLongest span between successive equity highs (peak to recovery), in days; extends to the end of the run if NAV never recovers its last peak.
sortinoRatioLike 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.
winningTradesCount of trades with realized P&L > 0.
losingTradesCount of trades with realized P&L < 0.
profitFactorGross profit ÷ gross loss (sum of winners' P&L ÷ absolute sum of losers' P&L). Omitted when there are no losing trades.
avgWinMean P&L of winning trades.
avgLossMean absolute P&L of losing trades.
largestWinLargest single winning trade.
largestLossLargest single losing trade (absolute).
avgTradeDurationSecondsMean of closed_at − opened_at across trades, in seconds.
totalCommissionTotal 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.