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 in members with has_results: false.
  • results.equity_curve, results.benchmark, results.metrics, results.days — the combined folds, net of your fee schedule and slippage. Each day slice carries slippage and a member_slices breakdown (per-member status and net P&L; status missing marks a session a member hasn't computed).
  • members — each member in the exact GET /strategies list-item shape plus date_to, has_results, and failed_days, so per-member staleness is visible.
  • exec_status is derived: running if any member is running. new_sessions_total / stale_members / update_allowed summarize 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} with privacy: "public" returns 409 private_members while any member strategy is private (the payload lists them). Make the members public first.
  • PATCH /strategies/{id} with privacy: "private" returns 409 strategy_in_public_portfolio while 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.