REST endpoints
Run backtests and read results over HTTP.
Base URL: https://www.texttoquant.com/api. All endpoints require authentication
and return the standard { success, data } envelope. The whole surface is described in OpenAPI 3.1 at
GET /v1/openapi.json (public). Generate typed clients straight from it.
Endpoints#
Account & usage
| Method | Path | Description |
|---|---|---|
GET | /v1/me | Verify a key and see who it belongs to, a quick connection test |
GET | /v1/usage | Plan tier and backtest credits: limit, used, remaining, period end |
GET | /v1/me/notification-channels | Which alert channels will deliver: Telegram linked? email? |
Parse & run
| Method | Path | Description |
|---|---|---|
POST | /v1/parse | Plain English → structured query. Send { "query": "…" } (+ optional customIndicatorHints); get a parsedQuery. Plan gated (LLM) |
POST | /v1/backtests | Run a backtest from a parsedQuery. Supports Idempotency-Key and savedIndicatorMapping. Bills 1 |
GET | /v1/backtests | Your run history, newest first, filterable & sortable (see below) |
GET | /v1/backtests/:id | One run in full: summary metrics, grade, edit lineage, exact parameters |
GET | /v1/backtests/:id/status | Cheap progress poll: completed, running (stage, percent), failed, cancelled. Use after a 202 |
POST | /v1/backtests/:id/cancel | Cancel a run that is still queued and refund its credit. A run already computing is not stopped (returns running). Never refunds a completed run |
POST | /v1/backtests/:id/sweep | Robustness suite (async → poll GET …/:jobId): /sweep, /grid, /walk-forward, /joint-sweep, /multi-asset. Bills |
GET | /v1/backtests/:id/context | Why trades won/lost by regime: /context/trades, /context/reanalyse, POST …/context-run, POST …/context/bucket-trades |
GET | /v1/backtests/:id/overfit | Overfitting verdict: Deflated Sharpe + PBO, deflated by the configs tried on the strategy session |
POST | /v1/condition-insights | Forward-outcome base rates for one entry condition, a filter probe, not a backtest |
GET | /v1/gallery | Showcase strategies to start from, ranked by sharpe, return, dsr. Each card carries author — {handle, kind} where kind is username, email or anonymous — or null |
POST | /v1/portfolio/sweep | Re-run a whole book across 2-5 values of one knob. Bills K×N, Idempotency-Key supported, poll GET …/:jobId, POST …/:jobId/cancel refunds unrun books |
GET | /v1/portfolio/runs/:id/analysis | A completed book's analysis: metrics, grade, attribution, benchmark, Monte Carlo, walk-forward, out-of-sample |
POST | /v1/portfolio/parse | Split a multi-asset prompt into per-asset strategies |
POST | /v1/portfolio/execute | Run a shared-capital portfolio (async → poll GET …/:jobId). Bills 1 per asset |
GET | /v1/portfolio/execute/:jobId | Poll an async portfolio run |
POST | /v1/portfolio/runs/:id/share | Mint a portfolio run's public share link (DELETE the same path revokes it) |
Data & results
| Method | Path | Description |
|---|---|---|
GET | /v1/runs/:runId/equity | Raw datasets: equity, trades, candles, monte-carlo, markers, condition logs (paginated) |
GET | /v1/runs/:runId/indicators/:name | Plotted values of one indicator on a run |
POST | /v1/backtests/:id/share | Mint the public share link (DELETE the same path revokes it) |
GET | /v1/backtests/:id/chart | Fresh chart payload (series + markers) for a saved run |
GET | /v1/backtests/:id/sweep/options | The sweepable knobs available for a run |
GET | /v1/strategies/:id/iterations | Edit/version history of a strategy session |
PATCH | /v1/backtests/:id/tags | Replace a run's organization tags |
PATCH | /v1/strategies/iterations/:id/label | Rename one iteration |
PATCH | /v1/strategies/:id/session-label | Rename a strategy session |
Custom indicators
| Method | Path | Description |
|---|---|---|
GET | /v1/indicators | Your saved custom indicators (name, source, columns, range) |
GET | /v1/indicators/:name | One saved indicator in full, including its Pine source |
DELETE | /v1/indicators/:name | Delete a saved indicator |
POST | /v1/indicators | Compile a Pine script against real data and save it: { "name", "pine_code" }. Plan gated (Pine) |
POST | /v1/indicators/validate | Compile-check a Pine script, no run, no save |
POST | /v1/indicators/lint | Offline Pine diagnostics (quota-free) |
POST | /v1/indicators/preview | Compile and run a Pine indicator against real data (async → poll GET …/preview/:jobId) |
POST | /v1/export/pine | A parsedQuery, or an existing backtestId, → a TradingView Pine v6 strategy() script |
POST | /v1/rephrase | Rewrite a query into clearer, parser friendly phrasing (metered; identical repeats free) |
Market screener (crypto)
| Method | Path | Description |
|---|---|---|
GET | /v1/screener/scan | The tradable crypto universe with per-token strength scores, price, 24h move and volume (cache-fresh) |
GET | /v1/screener/rsps | Market regime verdict per timeframe: RUN, REVIEW, SKIP |
GET | /v1/screener/token-stats | Per-token forward-outcome base rates for a tf + signal condition |
GET | /v1/screener/sectors | Crypto sector performance (optional date) |
GET | /v1/screener/rsps/matrix | Pairwise dominance for the top-k tokens |
GET | /v1/screener/saved-scans | Your saved screener filters: POST saves, DELETE /:id removes |
GET | /v1/screener/scan-alerts | Your scan alerts: POST creates, DELETE /:id removes; notified by Telegram/email. Paid |
GET | /v1/screener/custom-benchmark | Correlation + beta of the universe vs a benchmark you choose |
GET | /v1/screener/token-multitf | One token's recent OHLCV + multi-TF screener rows (capped) |
POST | /v1/screener/profiler | Start the Perfect Token Profiler (async → poll GET …/profiler/:jobId) |
Webhooks
| Method | Path | Description |
|---|---|---|
POST | /v1/webhooks | Register an HTTPS receiver for backtest.completed / backtest.failed. GET lists, DELETE /:id removes |
POST | /v1/webhooks/:id/test | Fire one signed webhook.test event to verify your receiver |
GET /v1/backtests: query parameters#
Mix and match freely. Anything invalid comes back as a 400 invalid_query that names the bad
parameter. Nothing is ever silently ignored.
| Parameter | Type (default) | Notes |
|---|---|---|
limit | int (20) | Page size, 1-100 |
offset | int (0) | Rows to skip (offset paging) |
sort | enum (created_at) | created_at, total_return, win_rate, total_trades, sharpe_ratio, max_drawdown, profit_factor, execution_time_ms |
order | enum (desc) | asc or desc |
asset | list (none) | One symbol or comma list, e.g. BTCUSDT,ETHUSDT |
timeframe | list (none) | One value or comma list, e.g. 4h,1d |
tag | list (none) | One tag or comma list of your tags |
search | string (none) | Free text over your original query text (≤ 200 chars) |
from / to | ISO date (none) | Inclusive created_at window |
status | enum (completed) | completed, failed, running, pending, cancelled |
include_stats | bool (off) | Aggregate stats block (off by default for API keys) |
include_facets | bool (off) | Distinct assets/timeframes, for filter menus |
# newest first (default)
/v1/backtests?limit=20
# best Sharpe on BTC or ETH, 4h, first half of 2025
/v1/backtests?asset=BTCUSDT,ETHUSDT&timeframe=4h&sort=sharpe_ratio&order=desc&from=2025-01-01&to=2025-06-30
# one tag, highest return first
/v1/backtests?tag=breakout&sort=total_return&order=descREST quick start#
Three calls, end to end, parse, run, then read your history:
# 1. parse plain English
curl -X POST https://www.texttoquant.com/api/v1/parse \
-H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \
-d '{"query":"Buy BTCUSDT on the 4h when RSI(14) crosses above 30, sell at 6% profit or 3% stop loss, last 2 years"}'
# 2. run it (bills one backtest)
curl -X POST https://www.texttoquant.com/api/v1/backtests \
-H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \
-d '{"parsedQuery": <the object from step 1>}'
# 3. read your results
curl "https://www.texttoquant.com/api/v1/backtests?limit=10" \
-H "Authorization: Bearer ttq_..."A successful run#
POST /v1/backtests returns summary metrics and a grade. Percentages are already scaled: 42.3
means 42.3%.
{
"success": true,
"data": {
"analysisId": "a1b2c3d4-e5f6-…",
"query": "Buy BTCUSDT on the 4h when RSI(14) …",
"backtestResults": {
"summary": {
"totalReturnPct": 42.3,
"winRate": 60.4,
"sharpeRatio": 1.82,
"maxDrawdownPct": 11.7,
"profitFactor": 1.9,
"totalTrades": 48
},
"grading": { "grade": "B+", "score": 78 }
}
}
}Probability scan#
GET /v1/backtests/:id carries the full signal base rate at metrics.probabilityScan
the entry trigger measured across every time it fired, including the ones the run could not
trade because a position was already open.
{
"metrics": {
"probabilityScan": {
"totalSignals": 647,
"resolvedSignals": 631,
"pendingSignals": 16,
"successRate": 27.7,
"edge": {
"breakevenRatePct": 26.1,
"edgePts": 1.6,
"expectancyPct": 0.026,
"payoffRatio": 2.83,
"ci95": { "low": 24.3, "high": 31.2 },
"verdict": "inconclusive",
"minSampleForVerdict": 30
},
"execution": {
"takenSignals": 544,
"skippedSignals": 87,
"skippedEdgePts": -1.4,
"modelFidelity": { "compared": 544, "agreementPct": 91.2, "medianAbsDeltaPts": 0.31 }
},
"successDefinition": "exit_targets",
"unmodelledExits": ["indicator_signal"],
"signalCount": 647
}
}
}Reading it correctly matters more than reading it at all:
successRatedivides byresolvedSignals, never the total. A pending signal is unknown, not unsuccessful.- Never quote the rate without
edge.breakevenRatePct. 27% is excellent at a 3:1 payoff and fatal at 1:1;edge.edgePtsis the difference and the actual claim. edge.verdictis decided by the interval, not the point estimate:edgeonly whenci95.lowclears break-even, andinconclusivebelowminSampleForVerdictregardless.execution.skippedEdgePtsis only a finding when the two intervals separate. They usually overlap, and then the sample cannot say whether the position filter helped.unmodelledExitslists exit types the scan did not simulate;execution.modelFidelitymeasures what that actually cost against the real trades.
Everything here is gross of fees and position-independent, a diagnostic of the trigger, not
account P&L. The per-signal rows are not in this payload; fetch them from the run's
chart-markers dataset.
probabilityScan is omitted on runs saved before 2026-08-22, and on heavy (chunked)
backtests, which stream the series in slices and cannot run the scan.
Cancel a queued run#
Start a run without waiting (wait: false) and you get a 202 with an analysisId to poll. Change your
mind before it starts computing and you can cancel it, the credit is refunded:
# start async → 202 { "analysisId": "…", "status": "running" }
curl -X POST https://www.texttoquant.com/api/v1/backtests \
-H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \
-d '{"parsedQuery": <the object from /v1/parse>, "wait": false}'
# cancel it, refunds the credit only if the run is still queued
curl -X POST https://www.texttoquant.com/api/v1/backtests/<analysisId>/cancel \
-H "Authorization: Bearer ttq_..."The response says which case applied:
{ "success": true,
"data": { "analysisId": "…", "status": "cancelled", "refunded": true, "reason": "queued_removed" } }status is cancelled (removed + refunded: true), running (already computing, not stopped, no
refund), completed (already saved, nothing to cancel), or not_found (no cancelable job for you). A run
that has already started keeps going server-side and lands in your history.
Custom indicators over the API#
Strategies can reference your saved custom indicators (from the Pine editor, CSV uploads, or
POST /v1/indicators) in three steps:
# 1. discover your saved indicator names
GET /v1/indicators
# 2. parse with hints so conditions are tagged custom
POST /v1/parse
{ "query": "Buy BTCUSDT 1d when my_osc crosses above 0, from 2023-01-01 to 2025-01-01",
"customIndicatorHints": ["my_osc"] }
# 3. run with the name mapping
POST /v1/backtests
{ "parsedQuery": { }, "savedIndicatorMapping": { "my_osc": "my_osc" } }Webhooks: push instead of poll#
Register a receiver once and get an HTTPS POST the moment a run finishes (or fails). Registration
returns the signing secret once; every delivery is signed so you can verify it came from us.
# register (max 3 active; https + public host only)
POST /v1/webhooks
{ "url": "https://yourapp.com/ttq-hook", "events": ["backtest.completed"] }
→ { "id": "…", "secret": "whsec_…" } # store the secret NOW
# every delivery is signed:
# X-TTQ-Signature: t=<unix>,v1=<hex hmac-sha256(secret, `${t}.${rawBody}`)>
# verify: recompute the HMAC over `${t}.${rawBody}` and compare constant-time
# payload
{ "id": "evt_…", "event": "backtest.completed",
"data": { "analysisId": "…", "status": "completed", "resultUrl": "/v1/backtests/…" } }Deliveries retry 3× (0s / 30s / 120s). A receiver failing 20 times in a row is auto disabled. Reregister to resume.
Prefer an AI agent? See the MCP server.