[C] Orion Findings — FLAG 032 net deposits + TotalPnL Baseline Audit
To: Vesper (she/her) CC: Katja (Captain), Atlas (he/him) From: Orion (he/him) Date: 2026-04-17
TL;DR¶
The dashboard's NET DEPOSITS tile IS already sourced from capital_events (via get_net_deposits_rlusd() → written into valuation_snapshots.net_deposits_rlusd_cum every snapshot). That path is correct. However, the TOTAL PNL tile uses a different baseline (paper.pnl_starting_value_rlusd engine_state key), and that key is not updated when capital_events land. Post-injection, TOTAL PNL will jump by the injection amount — visible, wrong, and avoidable with a small change to the injection script. NET DEPOSITS and TRADING VALUE are fine.
Ground truth from the post-S32 DB snapshot¶
Fresh read-only copy of neo_live_stage1.db.bak.20260417T180137Z (integrity ok).
capital_events has 2 rows. Both written Apr 13 00:18:19 UTC, source_note = 'stage1 opening baseline':
| id | type | asset | amount | price_rlusd | RLUSD-equiv |
|---|---|---|---|---|---|
| f9588349… | deposit | XRP | 39.27 | 1.3314 | 52.284078 |
| 192fdaae… | deposit | RLUSD | 85.00 | (None) | 85.000000 |
Sum = 137.284078 RLUSD — matches the NET DEPOSITS tile exactly.
valuation_snapshots.net_deposits_rlusd_cum is constant at 137.284078 across all 19,270 snapshots. Written by main_loop.py:1378 which calls self._state.get_net_deposits_rlusd() on every snapshot tick — correctly driven by capital_events.
First valuation_snapshot (40 minutes after capital_events insert):
| Column | Value |
|---|---|
| created_at | 2026-04-13T00:58:40 (40min after deposits at 00:18:19) |
| xrp_balance | 40.472253 (deposit was 39.27 — delta +1.20) |
| rlusd_balance | 85.569024 (deposit was 85.00 — delta +0.57) |
| total_value_rlusd | 139.46301667714764 |
| net_deposits_rlusd_cum | 137.284078 |
The 2.18 RLUSD gap is trading P&L accrued in the 40 minutes between capital deposits and first snapshot write. Engine was live and filling orders during that window but the accounting layer hadn't started writing snapshots yet.
The bug — two baselines, only one tracks capital¶
From engine_state:
paper.pnl_starting_value_rlusd = 139.46301667714764 ← exactly first snapshot TV
paper.pnl_starting_mid_price = 1.331628187573129
live.starting_balance_xrp = 40.472253 ← post-trading state, not deposit amount
live.starting_balance_rlusd = 85.569024 ← post-trading state, not deposit amount
paper.pnl_starting_value_rlusd is set once at engine first-start and never updated. TOTAL PNL is:
# dashboard.py:1666
sv = _sf(es, "paper.pnl_starting_value_rlusd")
if sv is not None and ending_total_value is not None:
total_pnl = ending_total_value - sv
So total_pnl = ending_total_value - 139.46 with 139.46 hardcoded forever.
Post-injection projection:
| Tile | Formula | Pre-inject | Post-inject ($50 RLUSD) |
|---|---|---|---|
| TOTAL VALUE | inventory | 147.38 | ~197.38 |
| NET DEPOSITS | SUM(capital_events) | 137.28 | 187.28 ✓ |
| TRADING VALUE | TV − NET_DEPOSITS | 10.10 | 10.10 ✓ |
| TOTAL PNL | TV − paper.pnl_starting_value_rlusd | 7.91 | 57.92 ✗ |
TOTAL PNL jumps by exactly the injection amount. Not a subtle rounding error — the whole injection shows up as profit.
Options¶
Option A — minimum pre-injection fix (recommended): Add to FLAG-031 injection script. When the script records a capital event, it also updates paper.pnl_starting_value_rlusd atomically in the same transaction:
rlusd_equiv = (amount if asset == 'RLUSD' else amount * price_rlusd) * sign
current_sv = state.get_engine_state('paper.pnl_starting_value_rlusd')
new_sv = float(current_sv) + rlusd_equiv
state.set_engine_state('paper.pnl_starting_value_rlusd', str(new_sv))
For withdrawals, use basis_delta_rlusd (principal portion only, not total withdrawal). Net effect: after $50 deposit, paper.pnl_starting_value_rlusd goes 139.46 → 189.46. TOTAL PNL stays at ~7.91. ~10 LOC, no dashboard changes, no schema changes.
Option B — comprehensive (post-gate, Phase 7): Deprecate paper.pnl_starting_value_rlusd entirely. Redefine TOTAL PNL to use net_basis from FLAG-031. Drop one of TRADING VALUE or TOTAL PNL — they become the same quantity. Higher blast radius, not a pre-injection blocker.
Recommendation for the gate¶
Ship Option A inside FLAG-031's injection script. FLAG-032's pre-injection scope becomes: verify the injection script correctly updates paper.pnl_starting_value_rlusd alongside capital_events, and assert TOTAL PNL doesn't drift post-injection on the dry-run.
Label fix, TradingValue-vs-TotalPnL reconciliation, and broader dashboard baseline audit defer to FLAG-032 post-injection.
Revised pre-injection gate (no new steps):
1. ask=14 confirmation — PASS ✓
2. FLAG-008
3. FLAG-030
4. FLAG-031 (basis_delta_rlusd + injection script including Option A baseline update)
5. FLAG-032 (verification only — dry-run assertions pass)
6. Dry-run on DB copy
7. Inject $50
8. Size 10 → 15 RLUSD
Ask for ruling¶
- Agree with folding Option A into FLAG-031?
- Agree with narrowing FLAG-032 pre-injection scope to verification-only?
Standing by. FLAG-008 branch held pending acknowledgment.
— Orion (he/him)