Backtests

Validate a strategy against real history before it touches capital.

A backtest takes the same payload as a deployment plus a date range, runs it against the immutable datasets, and returns the result inline. The symmetry is deliberate: the strategy that backtested is byte-identical to the one you deploy.

GET/runtime/backtests/dataset

Resolve the one latest immutable dataset release that can serve an exact market before creating a Nautilus backtest. Both parameters are required:

/runtime/backtests/dataset?venue=lighter&market=SOL-PERP.LIGHTER

json
{
  "venue": "lighter",
  "market": "SOL-PERP.LIGHTER",
  "backtest_ready": true,
  "dataset": {
    "manifest_uri": "gs://bucket/backtesting-data/lighter/releases/lighter-20260808T084050Z/catalog.json",
    "dataset_version": "lighter-20260808T084050Z",
    "bars": {
      "timeframe": "1m",
      "coverage_start": "2025-01-17T08:44:00Z",
      "coverage_end": "2025-02-14T00:00:00Z"
    },
    "funding": {
      "coverage_start": "2025-01-17T08:00:00Z",
      "coverage_end": "2026-07-31T00:00:00Z"
    }
  }
}

Only the latest published release is considered. The endpoint never falls back to an older release or returns a venue-wide catalog. When the current release cannot serve the requested market, it returns backtest_ready: false and an unavailability_reason. Use the returned manifest_uri, dataset_version, and bar coverage to create a reproducible backtest. venue supports lighter and polymarket; Polymarket market is its outcome-token instrument ID.

POST/runtime/backtests

Request

json
{
  "framework": "freqtrade",
  "venue": "hyperliquid",
  "symbols": ["BTC/USDC:USDC"],
  "code": "class BtcRange(IStrategy):\n    …",
  "config": {
    "timeframe": "4h",
    "stake_amount": 100,
    "exchange": { "name": "hyperliquid", "pair_whitelist": ["BTC/USDC:USDC"] }
  },
  "range": { "from": "2026-01-01", "to": "2026-06-30" }
}

Response — 202

json
{ "id": "bt_01j8xw…", "status": "queued" }

Backtests are asynchronous — queue, poll, read. Typical runs finish in one to ten minutes depending on range and pair count. For Nautilus Lighter and Polymarket runs, check the exact-market dataset endpoint first; a range outside its bar coverage fails fast with data_unavailable. /context/datasets remains the broad catalog reference. Creates accept an Idempotency-Key header (kept 24h) — a retried create returns the original backtest instead of burning a second concurrency slot.

GET/runtime/backtests

List: ?status=, ?venue=, limit/cursor — same { "backtests": [...], "next_cursor": null } envelope as deployments.

GET/runtime/backtests/:id

Status and, once finished, the embedded result — one endpoint to poll:

json
{
  "id": "bt_01j8xw…",
  "status": "finished",
  "release": "rel_2026-07-27T02",
  "range": { "from": "2026-01-01", "to": "2026-06-30" },
  "result": {
    "profit_total_pct": 6.7,
    "market_change_pct": -4.2,
    "max_drawdown_pct": 8.1,
    "trades": 41,
    "win_rate": 0.56,
    "profit_factor": 1.31,
    "sharpe": 0.9,
    "per_pair": [
      { "symbol": "BTC/USDC:USDC", "profit_pct": 6.7, "trades": 41 }
    ],
    "equity_curve": [
      { "t": "2026-01-01", "equity": 1.0 },
      { "t": "2026-06-30", "equity": 1.067 }
    ]
  }
}

market_change_pct — what buy-and-hold did over the same window — ships in every result. A strategy that made 6.7% while the market fell 4.2% and one that made 6.7% while the market rose 30% are different findings; the response won't let you confuse them.

release pins the dataset snapshot where the venue has release machinery (Lighter, Polymarket): the run is reproducible, and the exact history it saw is auditable via /context/candles. On venues whose release pipeline is pending (Hyperliquid, Binance — see datasets), release is null: the backtest ran on the live backfill store, and a re-run after backfills extend may see more history.

equity_curve is daily-resolution (one point per UTC day, plus the final point); per-trade detail lives in the full result's trade list.

For an account-owned legacy v2 Freqtrade backtest on Hyperliquid, this known-ID GET can return the persisted record only after it has reached the terminal finished or error state. A terminal legacy result contains only the summary fields that the previous API originally persisted; the Unified API does not synthesize missing canonical result fields. This compatibility is read-only and does not expand list behavior or adopt active runtime ownership: legacy queued or running status, logs, deletion, cancellation, and lifecycle actions remain unavailable through the Unified API.

DELETE/runtime/backtests/:id

Delete a backtest and its runtime resources. The response returns the deleted backtest record; retrying the same delete is safe.

Cost model

What a result's numbers do and don't include — read this before trusting any profit_total_pct:

CostModeled?How
Venue trading feesyesTaker rate per venue, applied to every fill (freqtrade fee config / Nautilus fill model)
Superior builder fee (Hyperliquid)yesThe same 0.01% your live deployment pays — backtest and live see identical costs
SlippagenoFills at candle prices; thin markets and large sizes will do worse live
Funding payments (perps)noNot applied to holding periods — a long-hold perp strategy's real carry cost is invisible here; check /context/funding yourself

A strategy that only survives on zero slippage and free funding is a backtest artifact. Paper mode is the next filter.

GET/runtime/backtests/:id/logs

The engine's run logs. Each logs entry uses the canonical <RFC3339Nano UTC timestamp> <message> format — this is where a strategy that produced zero trades explains itself (entry condition never met, pair data missing, indicator error at startup).

Statuses

queued → running → finished, or error with a structured reason. Results are retained for 12 months and listable. A backtest is a record you can cite (the leaderboard does exactly that).