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
{
"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
{
"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
{ "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:
{
"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:
| Cost | Modeled? | How |
|---|---|---|
| Venue trading fees | yes | Taker rate per venue, applied to every fill (freqtrade fee config / Nautilus fill model) |
| Superior builder fee (Hyperliquid) | yes | The same 0.01% your live deployment pays — backtest and live see identical costs |
| Slippage | no | Fills at candle prices; thin markets and large sizes will do worse live |
| Funding payments (perps) | no | Not 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).