---
title: "Run a backtest (bills one credit; idempotency supported)"
method: POST
path: "/v1/backtests"
---

# Run a backtest (bills one credit; idempotency supported)

`POST /v1/backtests`

## Request

- Base URL: `https://app.texttoquant.com/api`
- URL: `https://app.texttoquant.com/api/v1/backtests`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `parsedQuery` ParsedQuery, required: The structured strategy returned by POST /v1/parse: pass verbatim to POST /v1/backtests.
  - `savedIndicatorMapping` object: parsedName → saved indicator name (explicit timeConstraint dates required).
  - `customIndicatorHints` string[]
  - `idempotency_key` string
  - `wait` boolean: false → 202 immediately after enqueue; poll /status.

## Response `200`

Run finished within the sync window

- object
  - `success` true, required
  - `data` BacktestRow, required: A saved run: scalar metrics (total_return, sharpe_ratio, max_drawdown, win_rate, …), grade, effective params (timeframe, stop_loss, fees_bps, …), edit lineage (strategy_session_id, iteration_label). winning_trades / losing_trades: on runs saved before 2026-08-30 the stored counts can be 0 beside a real win_rate; such rows are served with counts approximated as round(win_rate × total_trades / 100) and carry trade_counts_approximated: true. Rows with persisted counts never carry the field.

## Other responses

- `202`: Still running — poll GET /v1/backtests/{id}/status; the credit stands and the result is saved
- `400`: SYMBOL_UNRESOLVED (no market and no company name match) or SYMBOL_AMBIGUOUS (a crypto token and a listed share use the same symbol, e.g. CAT). Refused before billing, nothing is charged. details.blockers[0] is the repair card. For the run's own asset each candidates[].patch.asset is a one tap fix (the near company listing, or the token pair and the share listings): resend with the chosen asset. With scope "condition" the symbol is read by a condition (another market's indicator, a ratio, a correlation), not the run's asset: never change asset. Write the chosen symbol in that condition. When one entry or exit condition holds it, the card has a locator {section, index} and each candidates[].condition is that whole condition with its symbol field rewritten: replace the condition at the locator and resend. On a rerun of a saved backtest (storedRun true) a condition card has no candidates: the saved run cannot be changed in place, so edit that condition in the strategy and run it as a new backtest.
- `409`: idempotency_key_reuse
- `422`: cannot_execute | custom_indicator_required
- `503`: QUEUE_BACKPRESSURE | queue_unavailable — retryable; credit refunded

---

[API](https://skmtc.dev/texttoquant/apis/texttoquant-api.md) · [All operations](https://skmtc.dev/texttoquant/apis/texttoquant-api/llms.txt) · [OpenAPI document](https://skmtc.dev/texttoquant/apis/texttoquant-api/revisions/d417fc3f4a7d?raw)
