# Paper book: correct (compounding) equity accounting — Design (Phase 2F-A3f)

**Date:** 2026-06-22
**Status:** Design (approved)
**Repo:** fxhnt
**Follows:** A3e. Fixes the load-bearing P&L-accounting bug that made the cockpit curve show a loss while the
edges are strongly profitable.

## Motivation (root cause, confirmed)

The cockpit equity curve showed ≈−8% while the two replayable edges are intrinsically **+92.6%
(crypto_tstrend, Sharpe 2.39)** and **+195.7% (unlock, Sharpe 1.73)** over 2025-06→2026-06 — a 50/50 blend of
the clean per-sleeve signals compounds to **+153.5%**. Two sleeves at +92%/+195% cannot blend to a loss, so
the equity curve is wrong. Root cause in `persist_paper_book`:
- `target_positions` re-derives every position each day at that day's close → `entry_price` = today's close.
- `unrealized = Σ qty·(price − entry_price)` with `price` = today's close = `entry_price` → **`unrealized` ≡ 0
  every day.**
- `equity = capital + realized` then comes only from `realized_delta` (rebalance slivers, cost basis reset
  daily), which **discards the entire mark-to-market appreciation of held positions.**
The risk overlays (A3c/A3d/A3e: −29%→−10%→−9% DD) were therefore tuned against a meaningless curve; they are
valid machinery but must be re-judged on the corrected equity.

## Decisions (from brainstorming, approved)

- **Equity = mark-to-market of the held book, compounding.** The book's true daily P&L = the prior day's
  throttled book marked to today's close: `gain_d = Σ positions_before(d).qty·(close_d − entry_price)`
  (`entry_price` = prior close). `equity_d = equity_{d-1} + gain_d`.
- **Compounding (user choice):** size each date's throttled book off the **running equity**, not fixed
  `capital`. Then `gain_d = equity_{d-1}·r_book_d` and `equity_d = equity_{d-1}·(1 + r_book_d)` — clean
  compounding falls out. The fixed `capital` ($100k) is only the initial seed + the nav `realized` baseline.
- **Killed/flat days handled for free:** a kill makes today's book empty → `positions_before(next)` empty →
  `gain = 0` → equity flat while in cash. Correct.
- **Both equity consumers fixed consistently:** the replay curve (`persist_paper_book` → `paper_nav`) and the
  live `/paper` (`PaperBookService.view`).
- **Trades ledger unchanged** (`diff_trades`/`replace_trades` for display); the `realized_delta`-based equity
  contribution is dropped (superseded by MTM). The now-unused realized-pnl repo methods are left in place
  (out-of-scope cleanup) to bound blast radius.
- **Frictionless gross** (idealized daily rebalance to `current_weights`, no fees/slippage) — a cost model is
  a separate later refinement; noted, not in scope.

## Components

1. **`PaperRepo.nav_equity_before(run_date) -> float | None`** — the most recent `paper_nav.equity` strictly
   before `run_date` (the prior equity to compound from; `None` → caller seeds with `capital`).
2. **`PaperRepo.latest_nav_equity() -> float | None`** — the most recent `paper_nav.equity` (for the live view).
3. **`PaperBookService.derive_and_persist(..., capital_base: float)`** — size the throttled book off
   `capital_base` (the running equity) instead of `self._capital`: `target_positions(...,
   capital=capital_base · leverage ...)`. Drop the `realized_delta`/`set_realized_for_run_date` equity calls;
   keep `diff_trades`/`replace_trades` (display) + `replace_positions`.
4. **`persist_paper_book`** — the orchestrator computes the compounded equity then sizes off it:
   ```
   prior = repo.positions_before(run_date)                       # prior throttled book (entry = prior close)
   gain  = Σ prior.qty·(prices.get(sym, entry) − entry)          # prior book marked to today's close
   e_prev = repo.nav_equity_before(run_date) or capital          # equity_{d-1} (seed = capital on day 0)
   equity = e_prev + gain                                        # = e_prev·(1 + r_book) under this sizing
   svc.derive_and_persist(... capital_base=equity, leverage=leverage)   # size today's book off the new equity
   repo.upsert_nav(run_date, capital=capital, realized=e_prev−capital, unrealized=gain, equity=equity, at)
   ```
   `realized = e_prev − capital` (cumulative through prior day), `unrealized = gain` (today's MTM) →
   `capital + realized + unrealized = equity` keeps the schema meaningful. Idempotent (ascending recompute /
   upsert per run_date; `positions_before` + `nav_equity_before` are both strictly-prior, unaffected by
   rewriting today's rows).
5. **`PaperBookService.view`** — `equity = (latest_nav_equity or capital) + Σ current_positions.qty·(live_price
   − entry)`: the curve through last close plus the live intraday move. Drop the `realized_pnl()` term.

## Data flow

Per date (both backfill + snapshot, unchanged callers — the change is internal to `persist_paper_book`):
prior throttled book marked to today's close → `gain_d` → `equity_d = equity_{d-1} + gain_d` → size today's
book off `equity_d` → persist nav. The shadow signal (`paper_sleeve_ret`), `paper_risk_overlay`
(weights/leverage/kill), and `unit_shadow_positions` are all UNCHANGED — only the throttled-book sizing base
(capital → running equity) and the nav equity computation change.

## Risks / safety

- **Read-only/additive w.r.t. strategy:** no change to `book_allocator`, sleeve `run()`/`advance()`,
  `combine_book`, the gate record, `paper_risk_overlay`, the shadow signal, or any broker/live path. Only the
  paper-book equity accounting + sizing base.
- **Causality:** `gain_d` uses the strictly-prior book marked to today's close; equity compounds from
  strictly-prior equity. No look-ahead.
- **Idempotency:** ascending backfill recompute is deterministic; `nav_equity_before`/`positions_before` are
  strictly-prior; `upsert_nav`/`replace_positions` per run_date. Re-run reproduces identical equity.
- **No runaway:** leverage stays capped at 3.0; notional = weight·equity·leverage scales with equity (a
  growing book takes proportionally larger positions — intended for compounding). A drawdown shrinks equity →
  smaller positions (de-risking), which is correct compounding behavior.
- **Day 0 / empty prior:** `nav_equity_before` None → seed `capital`; `positions_before` empty → gain 0 →
  equity = capital. Clean start.
- **Test churn:** existing tests asserting the old equity/realized semantics (e.g. realized-monotone backfill,
  equity==capital-with-no-pnl) will be updated to the compounding semantics, preserving intent.

## Out of scope

Fees/slippage cost model; removing the now-dead realized-pnl repo methods; re-tuning the A3d/A3e risk-overlay
constants against the corrected curve (separate follow-up once the real curve is visible); the live-cache vs
warehouse price-source coupling.

## Acceptance criteria

- `nav_equity_before`/`latest_nav_equity` return the right strictly-prior / latest equity; `None`-when-empty;
  tested.
- `persist_paper_book` compounds: a rising sleeve grows equity multiplicatively off the prior equity (a
  two-day rising series gives `equity_2 ≈ equity_1·(1+r_2)`, not `capital + sliver`); a killed day → flat
  equity; day 0 → equity = capital; idempotent re-run; unit/integration-tested.
- `derive_and_persist` sizes off `capital_base` (notional = weight·capital_base·leverage); `leverage=0` →
  flat; default behavior covered.
- `PaperBookService.view` equity = latest nav equity + live intraday MTM of the current book; tested.
- Full suite green (old equity/realized assertions updated to compounding semantics, intent preserved).
- Re-run backfill in-cluster → `/paper/replay` curve is now strongly **profitable** (reflecting the +92%/+195%
  edges, compounded, after the risk overlay — not −8%); `/paper` live equity matches; verified post-deploy.
  Report the new final equity + max drawdown on the corrected curve.
- Read-only/additive (strategy); no `*.dbn` staged.
