---
title: Backtests
description: 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](/context/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" }
}
```

For Nautilus, copy the exact `framework_symbols.nautilus` alias into both top-level `symbols` and `config.instrument_ids`:

```json
{
  "framework": "nautilus",
  "venue": "hyperliquid",
  "symbols": ["BTC-PERP.HYPERLIQUID"],
  "code": "from nautilus_trader.trading.strategy import Strategy\n\nclass BtcRange(Strategy):\n    …",
  "config": {
    "instrument_ids": ["BTC-PERP.HYPERLIQUID"],
    "trade_size_usd": 50
  },
  "range": { "from": "2026-03-01T00:00:00Z", "to": "2026-06-30T00:00:00Z" }
}
```

Freqtrade follows the same identity rule: top-level `symbols` and `config.exchange.pair_whitelist` both use `framework_symbols.freqtrade`. Sending a venue's canonical top-level `symbol` where the selected framework requires a different alias returns 400 `invalid_request`; there is no canonical-symbol fallback. Context and exact dataset lookups still use the top-level canonical `symbol`.

**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`](/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](/reference/rate-limits).

### GET /runtime/backtests

List: `?status=`, `?venue=`, `limit`/`cursor` — same `{ "backtests": [...], "next_cursor": null }` envelope as [deployments](/runtime/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`](/context/market-data). On venues whose release pipeline is pending (Hyperliquid, Binance — see [datasets](/context/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 does not expand list behavior or adopt active runtime ownership: legacy `queued` or `running` status, logs, cancellation, and other 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.

For an account-owned legacy v2 Hyperliquid Freqtrade backtest in terminal `finished` or `error` status, deletion tombstones the persisted record without contacting the retired runtime workload. Active `queued` or `running` legacy records and ambiguous records remain protected and return `409 unsupported_runtime_record`.

## 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`](/context/market-data) yourself |

A strategy that only survives on zero slippage and free funding is a backtest artifact. [Paper mode](/runtime/paper-trading) 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](/context/leaderboard) does exactly that).
