A saved strategy is an immutable trading program authored with the Strategy Builder (or the AI assistant) that owns its backtest results — there is no separate backtest object and there are no /backtests* endpoints. See Strategy results for the concepts. Most endpoints require authentication; reading a public strategy (and its results) does not.
The model
- Keyed by source hash. A form
configcompiles to a deterministic program, and every distinct configuration is identified by itssource_hash— the key the preview and WebSocket endpoints use. Strategies are immutable — to change one, save a new configuration (a different hash). - Short title, generated automatically. Each saved strategy carries a short
titlefor display. It is generated automatically a moment after you save, so it may come back as an empty string ("") right after the save returns — pollGET /strategies/{id}and it fills in shortly. You can rename it withPATCH /strategies/{id}once generation has finished: while it is still running the detail response reportstitle_pending: trueand atitlepatch returns409 title_generating. - Private by default, public means "anyone with the link". Each saved strategy carries a
privacyflag —private(the default) orpublic. Public is unlisted, not discoverable: there is no directory of public strategies, nothing crawls or lists them, and the id is a random UUID that can't be guessed — so a public strategy is reachable only by someone you hand the link to. Once they have it, any user (or an unauthenticated visitor) can view and clone it; a private strategy is visible only to you. Flip it any time withPATCH /strategies/{id}. - Results are computed the same way for every strategy: every trading session, run independently at a fixed $100,000 per-session capital, fee-free. Fees are applied at read time from the viewer's fee schedule (the default schedule for unauthenticated visitors), so results always render with your costs.
- Full history. Results cover every trading session, and newly added sessions can be appended at any time. Every results response labels the window (
window_label— alwaysall trading sessions— pluswindow_from,window_to). - No run credits. The backtester is protected by a per-user cap of 3 concurrently-executing runs (
429 too_many_active_backtests— retry with backoff) and a priority queue. Only the session drill-in and AI-assistant turns draw rate-limit credits.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/strategies/preview |
Preview a config (a persisted backtest) |
GET |
/strategies/preview/{source_hash}/results |
Read preview results (pre-save, heartbeat) |
POST |
/strategies/preview/abandon |
Abandon a preview |
GET |
/strategies |
List your saved strategies (the leaderboard) |
POST |
/strategies |
Save (publish) a strategy |
GET |
/strategies/{id} |
Get the logic + results snapshot |
PATCH |
/strategies/{id} |
Update per-user metadata |
DELETE |
/strategies/{id} |
Remove the strategy from your list |
GET |
/strategies/{id}/results/days |
Per-session day list |
GET |
/strategies/{id}/results/days.csv |
The day list as a CSV attachment |
GET |
/strategies/{id}/results/days/{date} |
Session drill-in (recomputed on demand) |
POST |
/strategies/{id}/results/update |
Append newly added sessions |
POST |
/strategies/assistant/drafts |
Create an AI-assistant draft |
GET |
/strategies/assistant/drafts |
List your drafts |
GET |
/strategies/assistant/drafts/{id} |
Get a draft + chat transcript |
POST |
/strategies/assistant/drafts/{id}/messages |
Send a message (run one turn) |
POST |
/strategies/assistant/drafts/{id}/cancel |
Cancel the in-flight turn |
POST |
/strategies/assistant/drafts/{id}/fork |
Clone a draft into a new editable one |
DELETE |
/strategies/assistant/drafts/{id} |
Discard a draft |
Preview a configuration
POST /strategies/preview is the builder's per-change action. There is no ephemeral dry-run: every distinct configuration is a real, persisted backtest. The call validates the config, ensures the backtest for its hash exists, starts (or resumes) the coverage run when sessions are missing, registers your interest, and returns the current fee-overlaid snapshot — instant for a configuration that already has results (one you previewed or saved before).
curl -s https://api.0dtespx.com/strategies/preview \
-X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{
"config": {
"legs": [
{"direction":"sell","type":"put","qty":1,"strike":{"method":"delta","value":0.3}},
{"direction":"buy","type":"put","qty":1,"strike":{"method":"delta","value":0.2}}
],
"entry": {"time":"10:00"},
"exit": {"profit_target_pct":50,"time":"15:55"}
}
}'
POST /strategies/preview
Validates this config and returns its current results snapshot, starting a coverage run when sessions are still missing. Not a dry run: every distinct config IS a persisted backtest, and a configuration you haven't previewed before counts against your 3-run cap. Re-run this example as authored rather than tweaking a parameter on each send.
Interactive — run this request from the docs
entry.time is a single entry time — a strategy opens one trade per session. For several entries in a day, save one strategy per entry time and combine them in a portfolio. entry.days_of_week optionally restricts to specific weekdays.
Every saved strategy also exposes a derived, declarative entry window — entry: {start_time, end_time, days_of_week?} (ET; days_of_week uses Monday=0 … Sunday=6, absent = every trading day) — on GET /strategies/{id}. For a builder strategy it is the form's entry time plus one minute (10:00 → {"start_time":"10:00","end_time":"10:01"}); an AI-assistant strategy can declare either a specific time or a genuine range ("try to enter between 09:35 and 10:00"). Bots use it twice: to refuse session starts after the window has passed, and as the live give-up — at end_time a bot cancels any entry order that has not filled and places no further opening orders for that session.
The response carries the generated artifacts — the description, summary, risks, and params (the standardized {legs, entry, exit} summary described under List and read) — and a snapshot with source_hash, exec_status, coverage counters (covered_days, total_sessions), the labeled window, and the folded metrics so far. When you change a parameter, send the new config with abandon_hash set to the previous source_hash — the superseded run stops at the next session boundary (completed sessions are kept; returning to that configuration resumes where it left off).
Errors: a malformed config returns 400 invalid_config with a fields map keyed by dotted config path — nothing is created. 429 too_many_active_backtests means you already have 3 runs executing; retry with backoff (a steady builder session never trips this, since each change abandons the previous run first).
Read preview results
GET /strategies/preview/{source_hash}/results is the pre-save read path: the full day list and folded summary, net of your fee schedule. Calling it also refreshes your interest heartbeat — an unsaved preview keeps running only while you're watching (interest expires 15 minutes after the last fetch; saved strategies always run to completion).
curl -s https://api.0dtespx.com/strategies/preview/$HASH/results -H "Authorization: $TOKEN"
GET /strategies/preview/{{source_hash}}/results
Reads the hash captured by the preview above, and refreshes your interest heartbeat with every send. Send it again while a run is in flight to watch covered_days climb.
Interactive — run this request from the docs
Poll until exec_status is idle and covered_days == total_sessions — or subscribe to the backtest_events WebSocket channel with the source_hash and read each frame's data.snapshot (the full fee-overlaid results snapshot; the HTTP endpoint backs the initial load + reconnect). Access requires live interest in the hash (from a recent preview) or a saved strategy of yours with this source — otherwise 404.
When you leave the builder without saving, POST /strategies/preview/abandon with {"source_hash": "…"} drops your interest explicitly (best-effort, always 204 — the 15-minute TTL backstops it).
Save (publish)
POST /strategies publishes the immutable strategy for the hash and keeps the backtest already running or done for it — a config you just previewed publishes with its results intact, no recompute.
curl -s https://api.0dtespx.com/strategies \
-X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"config": { … }, "privacy": "public"}'
POST /strategies
Publishes the config above into your library (private by default) and keeps the backtest it already has. Saving the same configuration twice is idempotent — the same id comes back.
Interactive — run this request from the docs
Returns 201 with your saved-strategy id, the source_hash, the saved privacy, and the current results snapshot. Saving never trips the run cap — at the cap the coverage run is parked and starts automatically when one of your runs finishes. Re-saving a configuration you previously removed resurrects the same id with its results.
You can hold at most 500 saved strategies. Saving a new one while at the cap returns 409 strategy_limit_reached and creates nothing — delete one first. Re-saving a strategy you already have doesn't count against the cap.
privacy is optional and defaults to private. Pass "public" to publish on save, or leave it off and publish later from the strategy page (or via PATCH).
AI assistant drafts
The AI assistant authors strategies through a per-user draft instead of a form config. Create one with POST /strategies/assistant/drafts (it seeds a canonical short iron condor and returns the draft id + the initial snapshot + source_hash), then converse with POST /strategies/assistant/drafts/{id}/messages (returns 202 {turn_id}; the reply, the updated preview hash, and the refreshed description + risk scenarios + params stream over the assistant_draft_events WebSocket channel). The live preview is read through the same GET /strategies/preview/{source_hash}/results endpoint as the Builder, keyed by the draft's current source_hash.
GET /strategies/assistant/drafts/{id} returns the draft and its chat transcript, plus risks and params — the draft strategy's standardized {legs, entry, exit} summary, the same shape a saved strategy carries (see List and read), so a client can show the same three rows before and after saving. It is absent on an older draft that hasn't been edited since the field was introduced; the next edit supplies it.
To save a draft, call POST /strategies with { "draft_id": "<uuid>", "privacy": "private" } instead of {config} — it publishes the draft's current source as an origin: "assistant" strategy and returns the same {id, source_hash, privacy, snapshot} envelope.
To clone an assistant strategy, POST /strategies/assistant/drafts/{id}/fork: because a saved conversation is frozen, cloning duplicates that conversation (its strategy bundle and chat transcript) into a new editable draft you own, so you can keep chatting and save a variant while the original is untouched. It returns the same envelope as create; it's owner-only (a draft you don't own returns 404).
The three calls that start a turn — draft create, …/{id}/messages, and …/{id}/fork — return 503 {"error":"assistant_unavailable"} while the assistant is temporarily offline platform-wide; nothing is charged, reading, discarding, and canceling existing drafts keep working, and the calls succeed again once it is back. Retry later.
List and read
curl -s https://api.0dtespx.com/strategies -H "Authorization: $TOKEN"
curl -s https://api.0dtespx.com/strategies/$STRAT_ID -H "Authorization: $TOKEN"
GET /strategies
Your leaderboard, most recently updated first — the first row's id lands in {{strategy_id}} for the reads below.
Interactive — run this request from the docs
GET /strategies/{{strategy_id}}
The immutable logic plus the results snapshot, net of your fee schedule. Right after a save, `title` may still be an empty string and `title_pending` true (a rename is refused until it clears) — send again in a moment.
Interactive — run this request from the docs
GET /strategies is the leaderboard: every saved strategy with its short title (rename it via PATCH once its generation has finished), full-history headline metrics (return, Sharpe, max drawdown, win rate — net of your fee schedule and slippage), coverage counters, and staleness (new_sessions_available), ordered by updated_at descending. The generated source is never included.
Both the list and the detail carry params — the strategy's standardized summary, the same three plain-text fields whatever authored it:
{
"legs": "sell 1 put at 30 Δ; buy 1 put at 20 Δ",
"entry": "enter at 09:45 ET on Mon/Wed/Fri",
"exit": "take profit at 50% of premium, stop at 200% of premium, close by 15:50 ET"
}
legs names every leg in one phrase (separated by ; ), entry says when the strategy opens its trade, and exit says how the position closes — hold to the 16:00 ET cash settlement when nothing closes it early. Each is one line of plain text with no markdown, so a client can render the three as fixed labeled rows without parsing the description. Treat it as optional: on the list it is omitted, and on the detail it is null, for an older strategy whose summary hasn't been filled in yet — render the rest and re-read later.
GET /strategies/{id} returns the immutable logic (config, generated description, summary, risks, params) plus the short title (rename it via PATCH once its generation has finished) and the results snapshot over all trading sessions, net of the viewer's fee schedule, with the labeled window. Both endpoints always include title; it is an empty string until a title is generated shortly after a save — or you set one yourself (poll the detail endpoint if you need it). The detail response also carries title_pending: true while a title is still being generated for the strategy, which is when a title patch is refused with 409 title_generating. It clears when generation concludes (or shortly after, if generation keeps failing) — note it is about the generation, not about title itself, which can already be non-empty from a rename you made earlier. origin is "builder" or "assistant"; for an assistant-authored strategy config is null (it was written as source, not a form config, so it can't be opened in the Builder). The payload also carries privacy and is_owner (whether the caller owns this link) so a client can show owner-only controls. For your own assistant strategy it additionally carries draft_id — the originating conversation, used to route the Clone action back to it (owner-only; never returned to others). Auth is optional: a public strategy is readable by anyone holding the link (including unauthenticated visitors) — but it's unlisted, so they have to be given the link; a private strategy you don't own returns 404 — its existence is never revealed. Viewing as an authenticated user keeps an in-flight run's interest fresh.
Per-session results
curl -s https://api.0dtespx.com/strategies/$STRAT_ID/results/days -H "Authorization: $TOKEN"
curl -sOJ https://api.0dtespx.com/strategies/$STRAT_ID/results/days.csv -H "Authorization: $TOKEN"
GET /strategies/{{strategy_id}}/results/days
One row per trading session, plus the window bounds. Long response — the viewer caps what it draws and the Copy button still gives you all of it.
Interactive — run this request from the docs
/results/days returns the full day list — one row per trading session: date, status (completed | skipped | failed | halted), gross_pnl, fees (your schedule), net_pnl (gross minus fees and your slippage setting), and order counts — plus the window bounds. The .csv variant renders the same list as an attachment with two extra engine columns: intraday_max_dd (fee-free intraday max drawdown) and spx_close, plus error_message and halt_reason.
Drill into one session
curl -s https://api.0dtespx.com/strategies/$STRAT_ID/results/days/2025-01-15 -H "Authorization: $TOKEN"
The heavy per-session detail — orders, transactions (each with an overlay_fee from your schedule), decision log, and the intraday equity curve — is not stored. It is recomputed by re-running that single session through the engine. Expect a response time of seconds, not milliseconds; the result is cached server-side for a few minutes and costs 10 rate-limit credits.
Each order carries the class the platform priced it under — entry, take_profit or stop_loss (absent on a session computed before that was recorded). A strategy's orders are priced and worked toward a fill exactly as a bot's are, so a session lists every step an order took toward its fill: the step that was replaced and the replacement that followed it, each carrying the same class. The decision log carries the matching rows — order_start_priced (the price an order went in at, and how wide the spread was), order_repriced (one step: both order ids, both prices, and whether the step also re-selected the legs) and, at warn, reprice_failed (a step the platform declined, leaving the order resting where it was). Both pricing rows also report the market they were computed from: quotes_at maps each leg's option symbol to the timestamp of the quote used for that leg, and net_bid / net_ask are the order's net bid and ask at that moment — the same three keys a bot writes, so a strategy's prices and its bot's line up key for key.
The drill-in's headline net_pnl is gross minus both fees (your schedule) and slippage (your per-contract slippage applied to the day's option fills). The response always includes the intraday_curve (downsampled to at most 600 points across the session), cost-adjusted for fees + slippage. reconciliation_warning: true means the recomputed gross P&L no longer matches the stored result — the engine changed since the session was computed, and the stored results will be recomputed.
Update with new sessions
curl -s -X POST https://api.0dtespx.com/strategies/$STRAT_ID/results/update -H "Authorization: $TOKEN"
As new market days become available, results go stale (new_sessions_available > 0). An update runs only the missing sessions and refolds the summaries — nothing already computed is re-run. Returns 202 {"status":"queued"}. The snapshot's update_allowed simply flags whether new sessions exist — the call is accepted either way. Subject to the 3-active-runs cap (429). Failed sessions are retried by an explicit update.
Manage
curl -s -X PATCH https://api.0dtespx.com/strategies/$STRAT_ID \
-H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"My put credit spread","description":"my spread","privacy":"public"}'
curl -s -X DELETE https://api.0dtespx.com/strategies/$STRAT_ID -H "Authorization: $TOKEN"
PATCH (owner only) touches per-user metadata — a title, a description override and the privacy flag ("public" publishes, "private" unpublishes); each field is applied only when present, and the logic itself is immutable (clone to change it). A title is trimmed and must be 1–120 characters (422 invalid_title otherwise), and can only be set once the strategy's automatic title generation has finished: sent during that brief post-save window it returns 409 title_generating (watch title_pending on GET /strategies/{id}, and retry when it clears — a description- or privacy-only patch is never blocked by it). Publishing only makes the strategy reachable by link — it's never listed or discoverable — so to actually share it, send someone its URL (https://www.0dtespx.com/research/strategies/{id}); the strategy page has a share button that copies the link for you. DELETE removes the strategy from your list; re-saving the exact same configuration restores it with the same id and its results. Portfolio membership blocks both: PATCH with privacy: "private" returns 409 strategy_in_public_portfolio while a public portfolio of yours contains the strategy, and DELETE returns 409 strategy_in_portfolio while any of your portfolios does — both payloads name the blocking portfolios.
Clone someone else's strategy
For a Builder strategy there is no clone endpoint — cloning is just saving a configuration you read from a public strategy. Fetch a public strategy with GET /strategies/{id}, take its config, and POST /strategies it (optionally tweaked). You get your own private link and your own results overlay; the original owner is unaffected. Because config is only returned for strategies you can read, you can only clone public strategies (or your own).
An assistant strategy has no config (it was written as source), so it's cloned differently: POST /strategies/assistant/drafts/{id}/fork forks its conversation into a new editable draft (see AI assistant drafts above). This is owner-only — it copies your private chat, so it's never available to someone viewing a strategy you shared.
Next: track run progress over the WebSocket backtest_events channel.