# Paper-Trading Cockpit (live MTM) — Design (Phase 2F, MVP = A1+A2)

**Date:** 2026-06-21
**Status:** Design (approved)
**Repo:** fxhnt

## Motivation

The forward-track sleeves are paper-trading on daily NAV (gate=WAIT, building a live record), but the
cockpit only shows NAV/return/gate per sleeve. The user wants to *watch the strategies trade with paper
money in (near) real-time* — see positions, trades, and live mark-to-market PnL — to build intuition
before micro-live. The engine already computes the trades (`book_allocator` weights + strategies'
`positions()` + `ExecutionService`); the forward track discards everything but the daily NAV.

## Decisions (from brainstorming)

- **Capital model:** ONE paper book, configurable `paper_capital` (default **$100,000**), allocated across
  sleeves by the existing `book_allocator` (inverse-vol + fractional-Kelly). Combined equity + per-sleeve PnL.
- **"Real-time" = live mark-to-market** of daily positions (prices poll ~45s; trades appear at the daily
  rebalance) — faithful to the daily edges (intraday trading them is a known fee-trap). A **replay** mode is
  a later sub-project (A3).
- **Scope = MVP (A1 persist + A2 live view).** Replay (A3) and an Alerts panel (B) are separate specs.
- **Read-only / paper-only:** no real orders. The persisted positions come from the rebalance computation
  on the `execute=False` path; existing broker gates already refuse live money.

## Components — A1: persist positions & trades

New persistence (mirrors `forward_nav.py`; Postgres TimescaleDB hypertable where applicable, SQLite for tests):
- **`paper_positions`** — `(run_date, sleeve, symbol, qty, entry_price, entry_date, weight, src_updated_at)`;
  the open book after each daily rebalance (latest run_date = current holdings).
- **`paper_trades`** — `(ts, sleeve, symbol, side {BUY|SELL}, qty, price, reason)`; one row per change
  (entry / exit / resize) derived by diffing today's target positions vs the prior held positions.

Derivation (in the daily rebalance path, alongside the existing NAV write):
1. `book_allocator` → per-sleeve weight; `× paper_capital` → sleeve notional.
2. Each sleeve's domain weights/`positions()` → per-symbol target weight; `× sleeve_notional / close` → target qty.
3. Diff target vs prior held qty per (sleeve, symbol) → BUY/SELL trades; persist trades + the new positions.
4. Realized PnL accrues on SELLs (vs entry_price); carried in the positions/trade rows.

## Components — A2: live view (`/paper`)

- **Endpoint `/paper`** (server-rendered page) + **`/paper/live`** (HTMX partial, polled ~45s).
- Loads current open positions (latest run_date), fetches **live prices** for held symbols via
  `binance_ccxt` (crypto); equities (SPY/IEF/QQQ…) marked at their **last daily close** for the MVP (no
  live equity feed). Resilient: cache last-good price; flag stale.
- Computes per-position unrealized PnL (`qty·(last−entry)`), per-sleeve PnL, and **combined equity =
  paper_capital + realized + unrealized**.
- **Renders:** combined live equity (number + sparkline), per-sleeve cards (alloc %, # positions, day/total
  PnL), open-positions table (sleeve, sym, qty, entry, last, PnL, %), recent-trades log (last ~25).

## Data flow

Daily cron rebalance → derive + persist positions/trades (A1). Browser → `/paper` → read latest positions
+ live prices → MTM → render; HTMX `/paper/live` poll refreshes the numbers. No writes from the web layer
(read-only cockpit, consistent with the existing dashboard).

## Architecture notes

- Follows the existing hexagonal layout: domain (pure position/trade diff + MTM math), application
  (`paper_book` service deriving + persisting), adapters (`paper_repo` persistence; `binance_ccxt` price
  port; web routes in `adapters/web/app.py` + a `paper.html` template), ports (a `LivePrice` port so tests
  inject a fake).
- Reuse `nav_sparkline` for the equity chart; reuse the `DashboardService`/templates patterns.

## Risks / safety

- **No real orders:** derivation runs on computed targets only; nothing touches the broker execute path.
- **Live price failures:** wrap the price fetch; on error show last-good + a "stale" badge (never crash the page).
- **Symbol mapping:** sleeves use various symbols (crypto pairs vs equity tickers); map to the right price
  source (crypto→binance_ccxt, equity→last close). Unknown symbol → mark at entry (0 PnL) + flag.
- **Capital/sizing fidelity:** the MVP mirrors `book_allocator`'s weights; it is a faithful *paper* view,
  not a guarantee of live fills/slippage (paper, by design).

## Out of scope (separate specs)
- **A3 — Replay scrubber** (animate persisted history).
- **B — Alerts panel** (surface Alertmanager alerts in the cockpit).
- Live *equity* price feed (MVP marks equities at last close); real order execution.

## Acceptance criteria
- `paper_positions` + `paper_trades` tables created + populated by a rebalance run (derived from
  book_allocator weights × $100k); a unit test covers the target-qty + trade-diff math.
- `/paper` renders combined equity, per-sleeve cards, open positions, and a trade log from seeded data
  (integration test with mocked live prices, mirroring `test_dashboard_service.py`).
- `/paper/live` returns updated MTM on poll; live crypto prices via `binance_ccxt` (equities last-close).
- Price-fetch failure degrades gracefully (last-good + stale flag), page never 500s.
- Read-only: no broker/order side effects.
