# Paper Replay (history scrubber) — Design (Phase 2F-A3)

**Date:** 2026-06-21
**Status:** Design (approved)
**Repo:** fxhnt
**Follows:** 2F MVP (paper view) + 2F-2 (nightly snapshot). Adds a scrubbable history.

## Motivation

`/paper` shows the live paper book (current positions, live MTM). The user wants to also *replay* — scrub
through history and watch the paper equity curve + book evolve. The snapshot persists only forward, per
run_date, with no equity-per-date series. This adds a backfill (recompute the paper book over past dates)
+ a `paper_nav` equity series + a `/paper/replay` scrubber.

## Decisions (from brainstorming)

- **Backfill from warehouse history** (capped ~1yr / warehouse availability) for the **as_of-aware** sleeves
  (stablecoin, unlock, ts-trend). **xsfunding is EXCLUDED** (its `current_weights` reads a live funding
  snapshot with no historical replay — back-computing it with today's funding would be false). Flagged in UI.
- **Persist a `paper_nav` equity-per-date table** (forward snapshot + backfill both write it).
- **Replay = the snapshot derivation pointed at the past**: reuse `current_weights(as_of)` + warehouse
  closes per date; mark each date's book at *that date's* close.

## Components

1. **`paper_nav` table** (`adapters/persistence/cockpit_models.py`): `(run_date PK, capital, realized,
   unrealized, equity, src_updated_at)`. Timescale-friendly like `forward_nav`.
2. **Equity-per-date write**: extend `persist_paper_book` (used by both nightly snapshot + backfill) to also
   compute + upsert the `paper_nav` row for `run_date`: `unrealized = Σ qty·(close@run_date − entry)` over
   that date's positions, `realized = cumulative realized ≤ run_date` (already tracked), `equity = capital +
   realized + unrealized`. Idempotent per run_date.
3. **Backfill** (`application/paper_backfill.py` + `fxt paper-backfill [--start] [--days N]` CLI in the
   existing CLI, and a manually-triggerable Dagster op): for each date in the range, build the as_of-aware
   sleeves' `current_weights(as_of=date)` + allocator weights + that date's warehouse closes → `snapshot(
   as_of=date, prices=closes@date)`. Per-date/per-sleeve try-except (skip missing data); **skip xsfunding**.
   Cap to `min(warehouse_start, today − cap_days)`. Re-runnable (idempotent per run_date).
4. **PaperRepo reads**: `nav_history() -> [(date, equity, realized, unrealized)]`, `positions_at(run_date)`,
   `trades_on(run_date)`.
5. **`/paper/replay` view** (`adapters/web/app.py` + `templates/replay.html` + `_replay_at.html`):
   - a **date scrubber** (`<input type=range>` over the `paper_nav` dates) + a **Play/Pause** control,
   - the **equity curve** (SVG of `paper_nav` up to the selected date — reuse/extend `nav_sparkline`),
   - the **book@date**: positions (`positions_at`) marked at that date's close + the day's trades
     (`trades_on`).
   - Scrub/play fires `GET /paper/replay/at?date=<d>` (HTMX) returning the `_replay_at.html` partial.
   - Empty history → "no replay history yet — run `fxt paper-backfill`". A small note: "funding sleeve is
     live-only, excluded from replay."
   - Nav link from `/paper` ↔ `/paper/replay`.

## Data flow

Backfill (one-shot CLI/op) recomputes positions + `paper_nav` per historical date → `/paper/replay` reads
`nav_history` (curve) + `positions_at`/`trades_on` (book@scrubbed-date). The nightly snapshot keeps
appending `paper_nav` so replay extends forward automatically.

## Risks / safety

- **Read-only**: backfill recomputes from warehouse + domain weight fns; no broker, no live-trading change.
- **xsfunding fidelity**: excluded from backfill (documented + UI-flagged) rather than back-computed wrongly.
- **Missing warehouse data** for a date/symbol → that date/sleeve skipped (no nav row), logged; replay just
  shows the dates that exist.
- **Idempotent**: backfill re-run replaces per run_date (positions/trades/nav all keyed by run_date).
- **Performance**: backfill over ~1yr × few sleeves is a one-shot batch; `/paper/replay` reads are indexed
  by run_date. paper_nav keeps the curve cheap (no per-render recompute).

## Out of scope
- B alerts panel (separate). Live *equity* feed for equities (marks at close, as MVP). Back-computing
  xsfunding history.

## Acceptance criteria
- `paper_nav` table; `persist_paper_book` writes the equity row per run_date; unit-tested.
- `fxt paper-backfill` recomputes positions + paper_nav over a date range from the warehouse for the
  as_of-aware sleeves, skips xsfunding + missing data; integration-tested (fake warehouse → multiple dates).
- `PaperRepo.nav_history/positions_at/trades_on` tested.
- `/paper/replay` renders the curve + scrubber + book@date; `/paper/replay/at?date=` returns the right
  book; empty-history + xsfunding-excluded notes shown; web-tested with seeded data.
- Re-running backfill is idempotent (no dup rows).
- Nightly snapshot unchanged except it now also writes paper_nav.
