Backtest API
Backtests are asynchronous research jobs. Every operation below requires the
research scope and is owned by the authenticated principal.
Operations
| Method | Path | Required scope | Purpose |
|---|---|---|---|
GET | /v1/backtest/strategies | research | List available strategy types |
GET | /v1/backtest/strategies/{strategy_type}/schema | research | Read the selected strategy's JSON Schema |
POST | /v1/backtest/validate | research | Validate Signal DSL and its universe |
POST | /v1/backtest/execute | research | Admit and dispatch an asynchronous backtest |
GET | /v1/backtest/jobs | research | Discover the authenticated owner's active jobs across sessions |
GET | /v1/backtest/status/{job_id} | research | Poll status, progress, liveness, and result identity |
POST | /v1/backtest/cancel/{job_id} | research | Request cancellation while the stage is cancellable |
GET | /v1/backtest/results/{backtest_id} | research | Read the completed summary |
GET | /v1/backtest/results/{backtest_id}/metrics | research | Read detailed metrics |
GET | /v1/backtest/results/{backtest_id}/equity | research | Read the equity curve |
GET | /v1/backtest/results/{backtest_id}/equity-with-trades | research | Read equity with trade-time-pinned points |
GET | /v1/backtest/results/{backtest_id}/trades | research | Read persisted execution fills |
GET | /v1/backtest/results/{backtest_id}/per-ticker | research | Read per-ticker performance |
GET | /v1/backtest/results/{backtest_id}/presentation | research | Read the result-bound chart payload |
POST | /v1/strategy-lineages | research | Create a strategy lineage tag |
POST | /v1/strategy-lineages/attach | research | Attach a version/result to a lineage |
PATCH | /v1/strategy-lineages/head | research | Select the lineage head version |
Discovery and validation
Call strategies, then the selected strategy's schema, before constructing an
execute body. Signal strategies can also be checked with validate. Universes
use canonical EXCHANGE:BASE/QUOTE[:SETTLE] tickers and must exist in the
tradable exchange catalog. The current supported strategy types and exact
timeframe/date bounds come from OpenAPI, not this prose.
Timeframe range limits
The current global backtest date-range limits are:
| Timeframe | Maximum range |
|---|---|
1m | 183 days |
3m, 5m, 15m | 365 days |
30m, 1h, 2h, 4h | 1095 days |
6h, 8h, 12h, 1d, 3d, 1w, 1M | Limited by available data |
Historical backtests use the preloaded candle/funding data for the requested range. A range is executable only when the required local coverage exists for every ticker in the universe.
Idempotent admission
POST /v1/backtest/execute accepts an optional caller-supplied backtest_id in
the body. It is the idempotency identity for this workflow:
- replaying the same authenticated request and ID returns the existing job with
idempotent_replay=true; - reusing the ID for different input or a different owner returns
409; - omitting it lets the server create an ID.
This is not the order API's Idempotency-Key header contract.
Plan quota
Every accepted POST /v1/backtest/execute consumes the authenticated user's
daily and weekly plan quota. Browser sessions and all user-owned API keys share
one hashed user quota identity; historical provisional keys retain their own
key quota. Daily windows reset at 00:00 UTC and weekly windows reset Monday at
00:00 UTC.
Quota exhaustion returns HTTP 429 with current usage, limit, window, and retry
timing. If the distributed quota store or browser plan lookup is unavailable,
execution fails closed with HTTP 503 QUOTA_UNAVAILABLE rather than dispatching
an unmetered backtest.
Lifecycle
GET /v1/backtest/jobs?limit=20 returns a plain array of queued, running and
cancellation-pending jobs, newest first. Each entry uses the same status
projection as the single-job endpoint and includes strategy_name, universe,
timeframe, started_at and the exact backtest_id. Older retained jobs may
lack their submit-time name or universe; their IDs remain discoverable.
Follow the Link header's next cursor for additional pages (maximum 100 jobs
per page). A cursor preserves the start-time/ID boundary when earlier jobs
complete between pages. Refresh without a cursor to find newly submitted jobs.
Only the authenticated owner's jobs are returned; a storage failure returns
503 BACKTEST_STATUS_UNAVAILABLE, never an empty array.
An accepted execute response is not a completed backtest. Poll
GET /v1/backtest/status/{backtest_id} until status is completed, failed,
or cancelled. Intermediate states include queued and running stages. The
liveness field distinguishes active work, slow-but-alive work, and a stalled
worker; it is separate from percent progress.
All non-terminal queued or running stages accept cancellation, including
simulation. A running job remains running with stage cancel_requested and
liveness stopping until its worker confirms termination; it remains in the
active list during that interval. Use the response and subsequent status rather
than assuming a request immediately stopped computation. A concurrent completion
may return already_done.
When completed, preserve result_id if the status response provides one. The
accepted backtest ID remains the lifecycle identity, while result endpoints
resolve the durable result owned by that job.
Results and interpretation
Read the summary first, then request detailed metrics, equity, trades, or
per-ticker data only as needed. The summary reports canonical metrics only:
total_pnl is an account-currency amount, *_pct fields are percentage points
(0.6646 means 0.6646%), and metrics.metric_units names each unit. Trades are
execution fills, not pre-grouped round trips: entry_time and entry_price are
the fill's execution time and price for entry and exit fills alike,
round_trip_id groups one position cycle, and null fields are omitted.
Result availability errors
Before a job persists its settings, the summary, presentation, metrics,
per-ticker, trades, equity, and equity-with-trades readers cannot resolve its
accepted backtest ID to settings. They check that identifier as a job ID through
the same ownership-enforcing status reader as GET /v1/backtest/status/{job_id}.
They retain HTTP 404 and distinguish the cause in the error body only for a
caller-owned job. Unknown jobs, another user's jobs, and failed status reads
retain the plain BACKTEST_NOT_FOUND body for the supplied identifier, without
job details.
The summary, presentation, metrics, and per-ticker readers also use these errors when owned settings exist but the required result has not been saved:
| Job/result state | HTTP | Error code | Caller action |
|---|---|---|---|
queued or running | 404 | BACKTEST_NOT_READY | Poll the status endpoint using error.details.job_id; keep the existing run and identifier. |
failed, including a timeout | 404 | The status's error_category, such as SCRIPT_RUNTIME_ERROR or RESOURCE_TIMEOUT (INTERNAL_ERROR if absent) | Read the terminal message and error context. |
cancelled | 404 | cancelled | The job stopped without a result; cancellation has no separate error category. |
| Unknown or unowned identifier, or missing result with no known job | 404 | BACKTEST_NOT_FOUND | Check the identifier and caller ownership. |
| Status read fails while resolving a missing result | 404 | BACKTEST_NOT_FOUND | Retry the status read; do not submit another run. The status endpoint itself may return 503 BACKTEST_STATUS_UNAVAILABLE. |
For example, a caller-owned queued job with no settings row returns HTTP 404 from all seven readers:
{
"type": "error",
"error": {
"code": "BACKTEST_NOT_READY",
"message": "Backtest is still running; results are not ready yet.",
"suggestion": "Check this job's backtest status until it completes, then read the result again.",
"details": {
"status": "queued",
"job_id": "22222222-2222-4222-8222-222222222222"
}
}
}
The polling job_id can differ from the settings ID supplied to a result reader.
Terminal errors also include status, job_id, and the status's available
error_category, error_line, error_context, and error_reference. Persisted
results remain readable with HTTP 200 even if the transient status has expired
or its store is unavailable. A completed job whose result is missing still
returns 404 BACKTEST_NOT_FOUND.
Once an identifier resolves to owned settings, the trades, equity, and
equity-with-trades readers keep their existing behavior without requiring a
result row or reading job status. Trades return HTTP 200
with the available fills, including an empty list. Both equity readers return
HTTP 200 when persisted performance history is available. The equity reader
also falls back to the summary when history is empty; equity-with-trades
requires performance history. Missing equity data still returns
404 BACKTEST_NOT_FOUND.
A completed pipeline proves that the simulation ran; it does not prove data quality, executable fills, live profitability, or promotion readiness. Preserve fee, slippage, date range, universe, and strategy inputs alongside any reported metric.
Lineages
Lineages provide optional version ownership around strategy/backtest artifacts. Create one stable tag, attach immutable versions/results, and move the head only after the caller has selected a version. Lineage mutation does not rerun or promote a backtest.