diff --git a/docs/superpowers/plans/2026-07-21-portfolio-backtester.md b/docs/superpowers/plans/2026-07-21-portfolio-backtester.md new file mode 100644 index 0000000..8dfff5a --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-portfolio-backtester.md @@ -0,0 +1,750 @@ +# Portfolio Backtester Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** A new `/paper/portfolio` cockpit surface where the operator selects an arbitrary set of edges, a weighting method, and a date range, and gets a portfolio equity curve + Sharpe/CAGR/maxDD **plus** the full/left-tail correlation matrix and each edge's marginal-Sharpe contribution — reusing the existing combiner/metrics/diversification engine end-to-end. + +**Architecture:** Three thin layers on top of a production-tested engine. (1) A cross-store **series aggregator** unions `bybit_sleeve_returns()` + `sim_curve_returns()` into the one `series_by_edge: dict[str, dict[int,float]]` shape the engine already consumes. (2) A pure **report bundler** slices to a date range, runs `combine_book` (default) or `combine_returns` (explicit-weights what-if), then `compute_stats`, `full_correlation`/`left_tail_correlation`, and `marginal_sharpe` on the aligned arrays, returning one report dict. (3) A FastAPI **route + template** (`/paper/portfolio` page + `/paper/portfolio/run` HTMX partial) rendering the curve + heatmap via Plotly (CDN) and the marginal-Sharpe table as HTML. Pure additive + read-only. + +**Tech Stack:** Python 3.12, FastAPI + Jinja2 + HTMX, NumPy, existing `fxhnt` domain/application engine, Plotly (browser-side, CDN). + +## Global Constraints + +- **Read-only + additive.** No task may touch any live-fund path, the execution CLI, `/paper/sim`, or any existing route/template/service method. Only NEW files + NEW route handlers + NEW nav links + one NEW ` +{% endif %} +``` + +- [ ] **Step 6: Add Plotly script + nav links to base.html** + +In `src/fxhnt/adapters/web/templates/base.html`, directly after the vendored htmx ` +``` + +**SRI hash (required — do not ship a bare CDN tag).** Compute the `sha384` for the exact pinned version before adding the tag: + +```bash +curl -sS https://cdn.plot.ly/plotly-2.35.2.min.js | openssl dgst -sha384 -binary | openssl base64 -A +``` + +Paste the result into `integrity="sha384-<...>"`. Without a matching hash the browser refuses to run the script, so this is verified the moment the chart renders (Task 4). If the operator prefers zero external dependency later, vendoring Plotly to `/static` (like `htmx.min.js`) is the drop-in alternative — out of scope for this slice, noted for the charting-migration slice. + +In the desktop `.bar` nav (after the Backtest link, ~line 165): + +```html + Portfolio +``` + +In the mobile `.tabs` nav (after the Backtest tab, ~line 173): + +```html + Portfolio +``` + +- [ ] **Step 7: Run the web tests** + +Run: `pytest tests/integration/test_portfolio_web.py -v` +Expected: PASS (4 passed). If `create_app`'s kwargs differ, fix the test's construction (Step 2 note). If `display_name` or `_now` aren't in scope in the new handlers, import/reference them as the neighboring handlers do. + +- [ ] **Step 8: Run the full suite to confirm nothing else broke** + +Run: `pytest -q` +Expected: all green (the change is purely additive; no existing test should change). + +- [ ] **Step 9: Commit** + +```bash +git add src/fxhnt/adapters/web/app.py src/fxhnt/adapters/web/templates/portfolio.html \ + src/fxhnt/adapters/web/templates/_portfolio_result.html \ + src/fxhnt/adapters/web/templates/base.html tests/integration/test_portfolio_web.py +git commit -m "feat(portfolio): /paper/portfolio route + Plotly curve/heatmap + marginal-Sharpe table" +``` + +--- + +## Task 4: Local browser verification + +**Files:** none (verification only — per `feedback_verify_cockpit_locally_not_prod`). + +- [ ] **Step 1: Run the cockpit locally** against a DB that has the precomputed series (or the seeded in-memory fixture), following `reference_fxhnt_local_cockpit_dev_loop`. Open `/paper/portfolio` in a real browser. + +- [ ] **Step 2: Verify by reading the numbers** (not just "it renders"): + - Select `crypto_tstrend` + `unlock` + `multistrat`, method = Book, full range → the equity curve draws, the metrics row shows plausible Sharpe/CAGR/maxDD, the heatmap shows a 3×3 with the Full/Left-tail toggle working, the marginal-Sharpe table lists all three. + - Switch to Explicit weights, set 0.7/0.3 on two edges → curve/metrics change. + - Narrow the date range → fewer days, metrics update. + - Deselect everything and run → the "select at least one edge" note, NOT a 500. + - Check mobile width (≤640px): the form + tables reflow (the `data-label` stacked-card pattern), charts stay within the viewport (no horizontal body scroll). + +- [ ] **Step 3:** If any number looks wrong, STOP and use `superpowers:systematic-debugging` (root cause before fix). Do NOT patch the template to hide a bad number. + +--- + +## Self-Review Notes (for the executor) + +- **Spec coverage:** Success criteria 1–4 map to Tasks 1–3 (aggregator + book/explicit report + corr/marginal + read-only). Criterion 5 (Plotly renders over tailscale) maps to Task 4. +- **`BacktestStats` fields:** Task 2 assumes `cagr`/`sharpe`/`max_drawdown`/`vol`. The executor MUST read `src/fxhnt/domain/backtest.py` and use the real attribute names — this is the one place the plan cannot verify without the file open. This is called out explicitly in Task 2 Step 3/4. +- **`create_app` kwargs:** Task 3 assumes `create_app(repo=, bybit_paper_repo=, paper_repo=)`. The executor confirms the real signature (Task 3 Step 2 note) and adjusts the test; the `_paper_repo()` helper inside `app.py` is the source of truth for which repo the report reads. +- **Two correlation axes:** `combine_book` → epoch-day dict; `combine_returns` → date-string tuples. The bundler bridges them (Task 2). Diagnostics (correlation, marginal) always run on the epoch-day-aligned `arrs` from `align_edges`, independent of the chosen method — matching the spec ("weight-independent properties of the edges"). diff --git a/docs/superpowers/specs/2026-07-21-portfolio-backtester-design.md b/docs/superpowers/specs/2026-07-21-portfolio-backtester-design.md new file mode 100644 index 0000000..34a40c4 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-portfolio-backtester-design.md @@ -0,0 +1,90 @@ +# Portfolio Backtester — Design Spec (2026-07-21) + +**Status:** design approved (all decisions made with operator). Implementation plan follows in a fresh session. +Slice 1 of the "cockpit = the platform" sequence (agreed order: portfolio-backtester → trade register → +cards-to-tables/IA polish). + +## Goal + +A new cockpit surface where you assemble an arbitrary set of edges, choose a weighting method + date range, and +get portfolio-level Sharpe/CAGR/maxDD **plus** the diversification view the research actually cares about — the +pairwise correlation matrix (full AND left-tail) and each edge's marginal-Sharpe contribution. Reuses the +existing, production-tested combiner/metrics/diversification engine end-to-end; new code is glue + a page. + +## Why this shape + +The three-explorer map (2026-07-21) established: the portfolio-backtest ENGINE already exists and is battle-tested +(`combine_book`, `combine_returns`, `compute_stats`, `marginal_sharpe`, `full_correlation`, +`left_tail_correlation`), and per-edge daily return series are already MATERIALIZED and queryable (three tables). +So a portfolio backtester is glue + surface, not a new engine. It exposes the exact machinery the research arc +already trusts (inverse-vol book, marginal-Sharpe LOO gate, co-crash / left-tail lens) as an interactive tool. +Pure additive + read-only — cannot break the live fund. + +## Decisions (all made with the operator) + +1. **Both combiners, default `combine_book`.** Default = the live inverse-vol → cap → optional marginal-prune → + vol-target + Kelly method (`book_allocator.combine_book`, book_allocator.py:117) — "what does my real + allocation do with these edges?". Optional **explicit-weights what-if** mode via `combine_returns` + (domain/portfolio/combine.py:10) — "what if positioning were 40%?". +2. **Three-part report:** (a) equity curve + headline metrics (Sharpe/CAGR/maxDD/vol/worst-year) via + `compute_stats`; (b) **correlation heatmap showing BOTH full and left-tail** correlation per pair + (`full_correlation` + `left_tail_correlation`, tail_frac 0.10) — the honest co-crash lens; (c) per-edge + **marginal-Sharpe** table (leave-one-out contribution, `marginal_sharpe`) — the same accept/reject gate. + Correlation + marginal-Sharpe are computed on the RAW edge series (properties of the edges, weight-independent); + the curve + metrics reflect the chosen weighting. Both lenses shown. +3. **New page `/paper/portfolio`** — a dedicated first-class cockpit section (the first IA step toward + cockpit-as-platform), NOT an extension of `/paper/sim` (which stays "the 4 canonical books"). Reuses existing + dashboard styling + the table pattern (with `data-label` mobile reflow). +4. **Charting = Plotly via CDN.** Adopt Plotly as the new platform charting layer, loaded from CDN (operator's + choice; correctly noted the cockpit renders in the operator's browser over tailscale serve, so browser fetches + are NOT blocked by the pod NetworkPolicy — the earlier "CDN is blocked" reasoning was wrong). Accepted + trade-off: CDN is a browser-side dependency (small SPOF/telemetry cost) for a private single-user cockpit. + The existing server-SVG charts (charts.py sparklines/equity/overlay) migrate to Plotly LATER, in a dedicated + charting-migration slice — NOT in this slice (keeps slice 1 focused). + +## Available edges (the multiselect options) + +- **Bybit 4 sleeves** from `bybit_sleeve_ret` (paper_repo.bybit_sleeve_returns() → {sleeve:{epoch_day:ret}}): + `crypto_tstrend`, `unlock`, `xsfunding`, `positioning`. +- **IBKR recompute-replay books** from `sim_curve_ret` (paper_repo.sim_curve_returns(sid) → {epoch_day:ret}): + `multistrat` (+ `multistrat_levered`). +- (Optionally `paper_sleeve_ret` live-paper series — a later addition; slice 1 covers the two stored-backtest sources.) + +## New code (glue + surface only) + +1. **Cross-store series aggregator** — a function that unions the three stores into one + `series_by_edge: dict[str, dict[int,float]]` for an arbitrary selected edge set, regardless of venue. This is + the ONE missing data primitive; `combine_book`/`combine_returns` both already consume exactly this shape. +2. **Report bundler** — takes selected edges + weighting method (+ optional explicit weights) + date range → + slices the series to the range → calls `combine_book` (or `combine_returns`) for the curve, `compute_stats` + for metrics, `full_correlation`+`left_tail_correlation` for the N×N matrix, `marginal_sharpe` per edge → one + report object. Never called together on a user-defined portfolio today (only em_asia_marginal_eval does a + fixed candidate-vs-book version) — this is the new assembly. +3. **Route + template + Plotly rendering** — `GET /paper/portfolio` (page) + `GET /paper/portfolio/run` (HTMX + partial: edge-multiselect + weighting toggle + date range → the three-part report). Plotly ` +{% endif %} diff --git a/src/fxhnt/adapters/web/templates/base.html b/src/fxhnt/adapters/web/templates/base.html index e822735..fcb41ae 100644 --- a/src/fxhnt/adapters/web/templates/base.html +++ b/src/fxhnt/adapters/web/templates/base.html @@ -5,6 +5,7 @@