HeyTraders Documentation

Backtest API

Backtests are asynchronous research jobs. Every operation below requires the research scope and is owned by the authenticated principal.

Operations

MethodPathRequired scopePurpose
GET/v1/backtest/strategiesresearchList available strategy types
GET/v1/backtest/strategies/{strategy_type}/schemaresearchRead the selected strategy's JSON Schema
POST/v1/backtest/validateresearchValidate Signal DSL and its universe
POST/v1/backtest/executeresearchAdmit and dispatch an asynchronous backtest
GET/v1/backtest/jobsresearchDiscover the authenticated owner's active jobs across sessions
GET/v1/backtest/status/{job_id}researchPoll status, progress, liveness, and result identity
POST/v1/backtest/cancel/{job_id}researchRequest cancellation while the stage is cancellable
GET/v1/backtest/results/{backtest_id}researchRead the completed summary
GET/v1/backtest/results/{backtest_id}/metricsresearchRead detailed metrics
GET/v1/backtest/results/{backtest_id}/equityresearchRead the equity curve
GET/v1/backtest/results/{backtest_id}/equity-with-tradesresearchRead equity with trade-time-pinned points
GET/v1/backtest/results/{backtest_id}/tradesresearchRead persisted execution fills
GET/v1/backtest/results/{backtest_id}/per-tickerresearchRead per-ticker performance
GET/v1/backtest/results/{backtest_id}/presentationresearchRead the result-bound chart payload
POST/v1/strategy-lineagesresearchCreate a strategy lineage tag
POST/v1/strategy-lineages/attachresearchAttach a version/result to a lineage
PATCH/v1/strategy-lineages/headresearchSelect 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:

TimeframeMaximum range
1m183 days
3m, 5m, 15m365 days
30m, 1h, 2h, 4h1095 days
6h, 8h, 12h, 1d, 3d, 1w, 1MLimited 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 stateHTTPError codeCaller action
queued or running404BACKTEST_NOT_READYPoll the status endpoint using error.details.job_id; keep the existing run and identifier.
failed, including a timeout404The status's error_category, such as SCRIPT_RUNTIME_ERROR or RESOURCE_TIMEOUT (INTERNAL_ERROR if absent)Read the terminal message and error context.
cancelled404cancelledThe job stopped without a result; cancellation has no separate error category.
Unknown or unowned identifier, or missing result with no known job404BACKTEST_NOT_FOUNDCheck the identifier and caller ownership.
Status read fails while resolving a missing result404BACKTEST_NOT_FOUNDRetry 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.