Portfolios API
Combine saved strategies into portfolios with read-time aggregated results.
A portfolio is a named collection of 1–20 of your saved strategies whose combined backtest results are computed at read time — nothing aggregated is ever stored. See Portfolios for the concepts: equal $100k sleeves (starting capital = N × $100k), the intersection window, and the per-session status combination.
Most endpoints require authentication; reading a public portfolio does not.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/portfolios |
List your portfolios with folded headline stats |
POST |
/portfolios |
Create a portfolio |
POST |
/portfolios/preview |
Preview a member set without creating anything |
GET |
/portfolios/{id} |
Get the combined results snapshot |
PATCH |
/portfolios/{id} |
Rename, change privacy, or replace the members |
DELETE |
/portfolios/{id} |
Delete the portfolio (members untouched) |
POST |
/portfolios/{id}/results/update |
Update every stale member (fan-out) |
GET |
/portfolios/{id}/results/days.csv |
Combined daily CSV, one net column per member |
GET |
/portfolios/{id}/results/days/{date} |
Aggregated session drill-in (recomputed) |
Create
curl -s https://api.0dtespx.com/portfolios \
-X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Overnight income mix","strategy_ids":["'$STRAT_A'","'$STRAT_B'"]}'
strategy_ids are your saved-strategy ids (the same ids /strategies returns). name is optional and defaults to Portfolio N. Duplicate ids, strategies you don't own, and counts outside 1–20 return 422. The response is the full detail payload (below) — 201.
There is no clone endpoint: to duplicate a portfolio, POST /portfolios with the source portfolio's member ids.
Preview before creating
curl -s https://api.0dtespx.com/portfolios/preview \
-X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"strategy_ids":["'$STRAT_A'","'$STRAT_B'"]}'
Folds an ad-hoc member set exactly like the detail read would — same intersection window, $100k sleeves, and fee/slippage overlay — without creating anything: no portfolio row, no backtest jobs, nothing persisted. This is what powers the live preview on the create page.
The 200 response is the detail payload minus identity: member_count / folded_members, the window, starting_capital (folded members × $100k — a selected strategy with no results contributes no sleeve), the same results block, and a lean members list (id, description, has_results, date_to, new_sessions, exec_status). A selection where nothing folds returns 200 with empty results and no window — run the members' backtests first. Validation matches create: 1–20 ids, no duplicates, all your own saved strategies (422 otherwise).
Read
curl -s https://api.0dtespx.com/portfolios -H "Authorization: $TOKEN"
curl -s https://api.0dtespx.com/portfolios/$PORTFOLIO_ID -H "Authorization: $TOKEN"
The detail payload mirrors a strategy's results snapshot, so the same client code renders both:
window_from/window_to— the intersection window ([max member start, min member watermark]); absent while no member has results.starting_capital—"200000"for two folded members. A member with no results is excluded from the fold (and doesn't add capital) — it appears inmemberswithhas_results: false.results.equity_curve,results.benchmark,results.metrics,results.days— the combined folds, net of your fee schedule and slippage. Each day slice carriesslippageand amember_slicesbreakdown (per-member status and net P&L; statusmissingmarks a session a member hasn't computed).members— each member in the exactGET /strategieslist-item shape plusdate_to,has_results, andfailed_days, so per-member staleness is visible.exec_statusis derived:runningif any member is running.new_sessions_total/stale_members/update_allowedsummarize what the update fan-out would do.
Auth is optional on the {id} reads — the public-strategy access pattern: a public portfolio is readable by anyone holding the link (unlisted, undiscoverable); a private one you don't own returns 404. On a public portfolio viewed by a non-owner, any member that is no longer public is dropped from the payload.
Update every stale member
curl -s -X POST https://api.0dtespx.com/portfolios/$PORTFOLIO_ID/results/update \
-H "Authorization: $TOKEN"
Fans out to every member with missing sessions, retryable failed sessions, or a reset backtest. Returns 202 with a per-member outcome:
{
"members": [
{ "id": "…", "source_hash": "…", "outcome": "queued" },
{ "id": "…", "source_hash": "…", "outcome": "up_to_date" },
{ "id": "…", "source_hash": "…", "outcome": "deferred" }
]
}
Unlike the per-strategy update, the fan-out never returns 429: members past your 3-active-runs cap are parked as deferred and start automatically as your earlier runs finish. The flip side: every started job is attributed to you, so while a large fan-out drains, your other backtest requests (builder previews, per-strategy updates) will hit the cap. already_running means the member's configuration already has an active run — nothing new was queued.
As each member's coverage advances, the portfolio's window extends member by member — re-fetch the snapshot (or watch each member's source_hash on the backtest_events WebSocket channel) to see it fill in.
Combined daily CSV
curl -sOJ https://api.0dtespx.com/portfolios/$PORTFOLIO_ID/results/days.csv -H "Authorization: $TOKEN"
One row per session in the window: date, status, the portfolio's summed gross_pnl / fees / slippage / net_pnl, then one net-P&L column per member (the header is the member description's first line). Member cells are empty — not 0.00 — on sessions excluded from the metrics fold.
Aggregated session drill-in
curl -s https://api.0dtespx.com/portfolios/$PORTFOLIO_ID/results/days/2026-03-10 \
-H "Authorization: $TOKEN"
The combined session detail: summed totals against the N×$100k baseline, every member's orders / transactions / decision-log rows merged in time order (each tagged strategy_id + strategy), the step-summed combined intraday_curve, and a per-member members summary (including per-member reconciliation_warning and strategy_errors).
Each traded member is recomputed by re-running that session through the engine (cached per member+date; skipped and zero-order member-days skip the engine). The cold path runs members through the engine two at a time — expect tens of seconds at high member counts; warm views return instantly. Costs member_count × 10 rate-limit credits. The date must lie inside the intersection window (404 otherwise).
Manage
curl -s -X PATCH https://api.0dtespx.com/portfolios/$PORTFOLIO_ID \
-H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Renamed","strategy_ids":["'$STRAT_A'"]}'
curl -s -X DELETE https://api.0dtespx.com/portfolios/$PORTFOLIO_ID -H "Authorization: $TOKEN"
PATCH (owner only) mutates name, privacy, and/or the full membership (strategy_ids replaces the list, validated like create) and returns the refolded snapshot. DELETE removes only the portfolio — member strategies stay in your library.
Privacy — enforced in both directions
The portfolio page lists its members, so visibility is guarded on both sides:
PATCH /portfolios/{id}withprivacy: "public"returns409 private_memberswhile any member strategy is private (the payload lists them). Make the members public first.PATCH /strategies/{id}withprivacy: "private"returns409 strategy_in_public_portfoliowhile that strategy is inside one of your public portfolios (the payload names them). Make the portfolio private first.
The delete guard
DELETE /strategies/{id} returns 409 strategy_in_portfolio while the strategy is a member of any of your portfolios, naming them in the payload — deleting a member would silently change those portfolios' composition and results. Remove the membership (via PATCH /portfolios/{id} with the reduced strategy_ids) and delete again.
Next: the walkthrough builds a two-strategy portfolio end to end.