# IBKR Paper Cockpit Parity — Design

**Date:** 2026-07-14
**Status:** Approved (brainstorming) → ready for implementation plan
**Branch:** `feat/ibkr-paper-cockpit-parity`

## Goal

Bring the IBKR paper strategies to cockpit parity with the Bybit book: show their **backtest** in the sim
and their **real paper-account trading** (actual DU-account fills / positions / NAV) across the cockpit —
overview (at-a-glance), strategy detail (the slice), and an account-level view (the whole DU pot) — so the
paper trading that is already working is *visible and verifiable* at every level, as the honest pre-live
confidence path. UX is a first-class requirement, and the design scales to multiple executing strategies by
adding a row, not a redesign.

## Scope

**In:** (information architecture VALIDATED via UX mockup — consolidated, plain-language, click-through-to-holdings)
- D1 — the IBKR strategies' honest-cost backtest curve on the **Backtests** page (both venues, one place).
- D2 — capture the **real** IBKR DU paper-account state (fills, positions, NAV), render a **holdings detail
  page** (what it's holding: assets + shares + value + %; recent trades; value-over-time), and reconcile it
  against the recomputed (idealized) sim ("how closely the real trades follow the plan").
- D3 — surface the executed reality on the **overview**: a "real trades" sub-line under an executing strategy
  (reusing the console's existing exec-twin sub-row) with a "behind/ahead of plan" pill.
- D4 — a **consolidated "Paper books" page** (`/paper`) showing BOTH books side by side — the crypto 4-edge
  book (simulated) and the IBKR paper account (real) — each a clickable card leading to its holdings detail.
  The IBKR book's detail carries the per-strategy breakdown that scales by row as more strategies trade.

**UX rules (VALIDATED, binding):**
- **Plain language, no dev jargon** in ANY user-facing copy. Human names only: "Crypto 4-edge book",
  "Multi-asset ETF portfolio", "Options income (put spreads)", "real trades", "0.3% behind plan", "cost to
  trade 0.04%", "building", "not trading — options access pending". NEVER surface internal IDs/terms
  (`bybit_4edge`, `multistrat`, `vrp`, the DU account number, `bps`, "sim", "recompute-replay", "exec-twin").
  Keep only the fund's own vocabulary (Sharpe, backtest, worst drop). A single `display_name` mapping is the
  source of every rendered name.
- **`REAL PAPER`** badge (blue) marks the one real-execution surface (the IBKR paper account); **`PAPER`**
  (grey) marks the simulated crypto book. Reuse the existing `.badge-exec` style.
- The crypto book and the IBKR account are the SAME kind of thing (paper books you watch) — consolidate onto
  one Paper-books page; click either for its holdings.

Designed for scale (UX is a first-class requirement): today only `multistrat` executes on DU (vrp exec is
suspended on IBKR options permission; sixtyforty is a benchmark), so the account currently *is* the multistrat
book. But ALL captured data is tagged by strategy and the views are built at both the per-strategy AND the
account level, so a second executor arming is a new ROW, never a redesign.

**Out (explicit):**
- No live / real-capital execution, no arm step, no promotion→allocation lifecycle changes ("still on paper,
  no live yet"). Gate/promotion/allocation semantics are UNCHANGED.
- Bybit stays exactly as-is (a pure simulated book — no EU Bybit-testnet API keys are available, so there is
  no real Bybit account to ingest). This design does not touch the Bybit `/paper` book or its sim.

## Current state (grounded)

- **Backtesting IS integrated:** `multistrat` has a real 0a `backtest_ref` (Sharpe ~1.67), `vrp` has one; both
  reconcile against the gate. The `/paper/sim` honest-cost curve, however, reads ONLY `bybit_sleeve_ret`
  (`app.py` `_SIM_BOOKS = ("bybit_4edge", "bybit_4edge_levered")`, `_sim_returns` →
  `_paper_repo().bybit_sleeve_returns()`) — the IBKR backtests exist but are not shown there.
- **Real paper execution happens but is not captured:** `IbkrBroker` (`adapters/broker/ibkr.py:39-40`) already
  reads real `accountSummary()` (NLV/cash/gross) + `ib.positions()`. `execute-multistrat` (`cli.py:248`) →
  `ExecutionService.rebalance` → `multistrat_exec_record.plan_and_record` books an *observed* return for the
  `multistrat_exec` track. BUT it records the **intended** target book (`weights·nlv/price`), not the actual
  fills; the `exec_fills` table (`ExecFillRow`: client_id, rebalance_id, symbol, side, qty, price, fee, status,
  shortfall_bps, at) is **empty**; and there is **no cockpit view** of the DU account's real positions/NAV.

So this is a faithful EXTENSION of existing machinery, not greenfield: populate `exec_fills`, snapshot the real
account NAV, and add the cockpit view + reconciliation.

## D1 — IBKR backtest curve in `/paper/sim`

Generalize the sim's data source so a book draws its honest-cost backtest curve from its registry
`backtest.kind` engine (the 0a `backtest_ref` dispatch: `recompute-replay` for `multistrat`/`vrp`) instead of
only `bybit_sleeve_ret`.

- **Book list:** replace the hardcoded Bybit-only `_SIM_BOOKS` with a source that includes the IBKR recompute
  books (`multistrat`, `vrp`) alongside the Bybit books. Each book declares (via the registry) which engine
  produces its curve.
- **Curve source:** `_sim_returns(book)` dispatches on the book's `backtest.kind`: Bybit books keep reading
  `bybit_sleeve_ret` (unchanged); `recompute-replay` books source their series from the 0a
  `backtest_ref(sid, ...)` / the recompute engine that already produces their backtest. NO new backtest
  compute on the request path — read precomputed/cached series as the Bybit path does (persist the IBKR
  recompute-replay curve where the sim can read it cheaply, mirroring `bybit_sleeve_ret`; the
  `fxhnt-backtest-refs` Job / the nightly asset is the natural writer).
- **UI:** the existing `/paper/sim` book selector renders the added IBKR books; the honest-cost curve + summary
  stats render identically. No template redesign — a data-source generalization.

**Boundary:** D1 changes only the sim's book list + curve-source dispatch. It does not touch the gate,
`forward_summary`, or the Bybit sim behavior.

## D2 — Real DU paper-account book + reconciliation

### D2.1 Capture (extend the exec path)
At each `execute-multistrat` run (post-execution, when not blocked by a safety gate), capture the REAL account:
- **Fills:** collect the actual fills from the placed trades (ib-async trade `.fills` / `ib.reqExecutions`) and
  persist to the existing `exec_fills` table — one row per fill (symbol, side, qty, filled price, fee/commission,
  status, `shortfall_bps` = filled-vs-intended slippage, `rebalance_id`, `at`). This populates the currently-empty
  table.
- **Account snapshot:** persist a per-run snapshot of the real account — `NetLiquidation`, cash, gross position
  value, and the real held positions (`ib.positions()`) — into a NEW idempotent table `ibkr_account_nav`
  (run_date PK; nlv, cash, gross, positions_json, at). One row per run_date (idempotent replace), Timescale/
  Postgres-safe migrate following the `forward_nav`/`strategy_health` create-if-not-exists pattern.
- **Reuse:** the broker already reads NLV+positions; add fill collection + the two persistence calls. Do NOT
  change the `multistrat_exec` return basis (it is gate-relevant, C1-designed) — the capture is additive.

### D2.2 Holdings detail page
Clicking a book (from Paper books or the overview) opens its detail page. For the IBKR paper account book:
- **What it's holding right now** — a holdings table: asset (SPY/IEF/…), a plain "what it is" description
  (US stocks / US Treasuries / Gold / Commodities / Managed futures), shares, value, % of book (from the
  latest `ibkr_account_nav` positions snapshot).
- **Recent trades** — from `exec_fills`: time, asset, action (Buy/Sell), shares, price, "cost to trade"
  (slippage as a %, NOT bps), fee.
- **Value over time** — the **real** account NAV curve (from `ibkr_account_nav`) with the plan it's trying to
  follow overlaid as a dashed line.
Empty/absent data → "no trades recorded yet" (never a 500, never a fabricated number). The crypto book's detail
renders its (simulated) holdings the same way.

### D2.3 Reconciliation — "how closely the real trades follow the plan"
The one shared signal, in plain language. Compute the divergence between the **real DU account NAV**
(`ibkr_account_nav`, new) and the recomputed idealized sim (`forward_summary`) → a "X% behind/ahead of plan"
pill (reuse the existing amber "decaying"/green pill vocabulary). Surface it: a pill on the overview real-trades
line, the "Following the plan" card + headline on the book detail, and the per-strategy column on the account
detail. This gap — real fill prices, whole-share rounding, cost-to-trade, fees — is the pre-live verification.
`multistrat_exec` (the observed envelope-return track) remains the gated exec twin, unchanged.

## D3 — Overview page: the executed reality at a glance

The overview (`/`, `dashboard_service.fleet()`/`overview()`) already renders each strategy with gate/health/
backtest badges and — for a modeled edge with an executed twin — a nested sub-row. For a strategy executing on
the real paper account, render that sub-row as a **"real trades"** line (reuse the existing `.exec-tag` +
`tr.exrow` style): what actually filled (real return, trade count, holdings), a **"X% behind/ahead of plan"**
pill (the shared D2.3 signal), and a plain "cost to trade / fees" note. Mark the strategy's row with the
**`REAL PAPER`** badge. Clicking the strategy (or "N trades · M holdings →") opens its holdings detail (D2.2).
No separate rollup card — the consolidated Paper-books page (D4) is the account home.

## D4 — Consolidated "Paper books" page (`/paper`)

Replace the current Bybit-only `/paper` landing with a page that shows BOTH books side by side, each a
clickable card:
- **Crypto 4-edge book** (`PAPER`, simulated) — value / since / Sharpe / worst drop / status, a mini curve.
- **IBKR paper account** (`REAL PAPER`, real) — account value / strategies trading / holdings count / the
  "X% behind plan" pill / a mini curve with the plan overlaid.
Both read identically (value, track record, status). Clicking a card opens that book's holdings detail (D2.2).

The IBKR book's detail page carries the **per-strategy breakdown** (one row per executing strategy: its real
return, "behind/ahead of plan", holdings + trade counts) — one row today, N as strategies arm ("scales by row,
not redesign"). The account is one pot; the per-strategy rows are its slices, driven by the strategy-tagged
capture. A dimmed "not started yet" row illustrates the scale path.

## UX & information architecture

Progressive disclosure, one consistent "following the plan" signal at every level, plain language throughout:

```
Overview  /              -> fleet scan; an executing strategy shows a "real trades" sub-line
  |                          + a "X% behind plan" pill (REAL PAPER badge on the row)
  |
Paper books  /paper      -> BOTH books as clickable cards: Crypto 4-edge (PAPER, simulated)
  |                          + IBKR paper account (REAL PAPER, real). Same layout for each.
  |-- Book detail        -> click a book: WHAT IT'S HOLDING (assets + shares + value + %),
  |                          recent trades (cost-to-trade %), value-over-time (real vs plan),
  |                          and — IBKR book — the per-strategy breakdown (1 row now, N later)
  |
Backtests  /paper/sim    -> both venues' honest-cost curves in one place (human book names)
```

UX principles (professional ops UI):
- **At-a-glance first:** the overview must answer "which strats are actually paper-trading, and is execution
  tracking the model?" from badges/chips alone — no drill-in required.
- **Progressive disclosure:** overview (fleet) → strategy detail (one slice) → account view (the pot). Each
  level adds resolution, none duplicates another's job.
- **One divergence signal, rendered consistently** at all three levels (chip on overview, headline on detail,
  breakdown on account) — the same computation, the same color language.
- **Reuse the existing visual system:** the gate/health badge palette + `exec_badge` macro; do NOT invent a new
  palette. The EXEC badge and divergence chip are new *states* in the existing badge vocabulary.
- **Scale by row, not redesign:** every view renders a *set* of executing strats (size 1 today); adding a strat
  never changes the layout, only adds a row/chip.
- **Honest empty/degraded states:** no captured account yet, or ib-gateway unreachable, renders a clear "no
  executed data yet" state — never a 500, never a fabricated number. Consistent with the health axis (a stalled
  exec track already surfaces as STALE).

## Data model

- **Reuse** `exec_fills` (`ExecFillRow`) — populate it (currently empty). It already carries `rebalance_id`;
  ensure each rebalance's `rebalance_id` maps to its `strategy_id` (a `strategy_id` column or a rebalance→strat
  mapping) so fills are attributable per strategy for D4's breakdown. No breaking schema change (add the tag).
- **New** `ibkr_account_nav` (per-run real account snapshot): `run_date` PK, `nlv`, `cash`, `gross`,
  `positions_json`, `at`. Account-level (the whole DU pot). Idempotent create + replace-per-run; migrate follows
  the existing `forward_nav`/`paper_repo`/`strategy_health` pattern (no heavy startup DDL).
- **Per-strategy executed return** already exists as the `*_exec` observed forward tracks (`multistrat_exec`
  today; each future executor gets its own, same pattern) — the account view's per-strategy breakdown reads
  these + the strategy-tagged `exec_fills`. The account NAV rolls up the account; per-strat return stays on the
  `*_exec` tracks. No new per-strat NAV store.
- **No change** to `forward_summary`, `backtest_summary`, `strategy_health`, gate/promotion/allocation.

## Architecture / components

- `adapters/broker/ibkr.py` — add fill collection from placed trades; expose fills + the account snapshot to the
  caller (keep the broker a thin adapter; no persistence here).
- `application/multistrat_exec_record.py` (or a small sibling) — persist the captured fills → `exec_fills` and the
  account snapshot → `ibkr_account_nav`. Additive to `plan_and_record`; gated behind `account_is_paper` (matches
  the existing paper-only guard). Does not alter the observed-return basis.
- `adapters/persistence/` — the `ibkr_account_nav` repo (create/replace/read a NAV series + latest positions),
  following `forward_nav`/`paper_repo` conventions.
- `adapters/web/app.py` + templates — D1 sim book-list/curve-source dispatch; D2 the multistrat-detail "IBKR
  paper account" section (real NAV curve + positions + fills + reconciliation headline); D3 the overview EXEC
  badge + divergence chip + account-rollup card (via `dashboard_service.fleet()`/`overview()` + the badge
  macros); D4 the `/paper/ibkr` account view (total NAV + all positions/fills + per-strategy breakdown table).
- `application/dashboard_service.py` — compute the per-strategy exec status + real-vs-sim divergence (one shared
  function feeding overview chip, detail headline, account breakdown — the "one signal, three places" rule) and
  the account rollup.
- `application/backtest_refs*` / the nightly asset — persist the IBKR recompute-replay curve where the sim can
  read it cheaply (mirrors `bybit_sleeve_ret`).

## Error handling

- Capture is BEST-EFFORT and paper-only: a fills/snapshot persistence failure logs (does not fail the rebalance
  or block execution) — the rebalance already succeeded; the capture is observability. Follows the existing
  "ref persist is best-effort" pattern.
- Empty/absent data degrades to an empty section (never 500), matching the Bybit `/paper` "missing precomputed
  table → []" guard.
- ib-gateway unreachable at ingest → no snapshot that run; the health axis already surfaces a stalled exec track.

## Testing

- **D1:** `/paper/sim` book list includes `multistrat`; `_sim_returns("multistrat")` sources the recompute-replay
  curve (not `bybit_sleeve_ret`); the sim curve + stats render for an IBKR book (TestClient); Bybit sim
  unchanged.
- **D2.1:** captured fills persist to `exec_fills` with correct fields incl. `shortfall_bps`; the account snapshot
  persists to `ibkr_account_nav`; capture is paper-only + best-effort (a persistence error does not fail the
  rebalance; verified via a raising fake).
- **D2.2/2.3:** the multistrat detail page renders the real NAV curve, positions, fills; the sim-vs-executed
  reconciliation computes the divergence on a fixture (known sim series vs known executed fills → expected
  divergence + median slippage); empty data → empty section, no 500.
- **D3:** the shared divergence function returns the expected value on a fixture; the overview renders the EXEC
  badge + divergence chip for an executing strat and NOT for a pure-sim strat; the account-rollup card shows
  total NAV + strat count; no-executed-data → no badge/empty card, no 500.
- **D4:** `/paper/ibkr` renders total NAV + all positions/fills + a per-strategy breakdown row for `multistrat`;
  the breakdown is a SET (assert it renders correctly for one strat and would render N — e.g. a two-strat
  fixture yields two rows); empty account → clean empty state, no 500.
- **Scale test:** a fixture with a second (synthetic) executing strat proves overview/account views render both
  without layout change — the "new row not redesign" invariant.

## Constraints

- **Plain language, no dev jargon** in any user-facing copy (see UX rules). A single `display_name` mapping
  (internal `strategy_id`/book id → human name) is the source of every rendered name — no raw ids, no `bps`,
  no "sim"/"recompute-replay". `REAL PAPER`/`PAPER` badges as specified.
- Paper only — no live/real-capital path, no arm, no gate/promotion/allocation change.
- The consolidated `/paper` page shows BOTH books; the Bybit book's underlying data/behavior is untouched
  (only its landing is now shared with the IBKR book). Bybit stays a simulated book.
- DB migrations idempotent + Postgres-safe (no heavy startup DDL — crashloops the cockpit).
- Verify the cockpit LOCALLY in a real browser before deploy (read the rendered numbers; never claim "works"
  off synthetic events).
- No silent degradation: capture failures log loudly; a stalled exec track already surfaces via the health axis.

## Sequencing

1. **D1** — sim curve for IBKR (low-risk, backtests exist, immediate parity).
2. **D2.1** — capture the real account (fills → `exec_fills` tagged by strategy; snapshot → `ibkr_account_nav`).
   The data foundation everything else reads.
3. **D2.2 + D2.3** — the multistrat detail "IBKR paper account" section + the sim-vs-executed reconciliation
   (build the ONE shared divergence function here).
4. **D3** — overview EXEC badge + divergence chip (reusing the shared function) + account-rollup card.
5. **D4** — the `/paper/ibkr` account view (total NAV + all positions/fills + per-strategy breakdown).

Each step is independently testable and shippable; D2.1 is the pivot (it fills `exec_fills` + the account
snapshot that D2.2/D3/D4 all render). The shared divergence function (built in step 3) is reused by D3 and D4
so the signal is computed once and rendered three places.
