[C] Orion Plan — FLAG 031 Basis Model Implementation
From: Orion (he/him)
Date: 2026-04-17
Prereq: FLAG-030 branch fix/flag-030-capital-overlay @ 37692b0 — landed, push pending.
Scope — five surfaces, no runtime behavior change until injection script is used¶
- Schema:
capital_events.basis_delta_rlusd REAL(nullable, via_ensure_column) - Write-path:
record_capital_event()acceptsbasis_commit, persistsbasis_delta_rlusd - Read-path:
StateManager.get_net_basis_rlusd()— authoritative denominator - Script:
scripts/inject_capital.py— atomic injection,--dry-run - Script:
scripts/write_synthetic_initial_basis.py— one-shot historical backfill
rebuild() NOT touched. Dashboard metric migration deferred to follow-up.
1. Schema migration¶
Nullable. Existing rows stay NULL; get_net_basis_rlusd() treats NULL as 0 via COALESCE. Idempotent. No index (net_basis is a full-table sum, capital_events is small forever).
2. Write-path changes¶
2a. record_capital_event() — validation table¶
| event_type | asset allowed | price_rlusd | basis_delta_rlusd | amount |
|---|---|---|---|---|
| deposit | XRP or RLUSD | XRP required, positive finite | auto-computed if None; override if caller provides. RLUSD: +amount. XRP: +amount × price_rlusd | positive finite |
| withdrawal | XRP or RLUSD | XRP required | MUST be provided by caller (negative or zero) | positive finite |
| basis_commit | RLUSD only | must be None | MUST be provided by caller (any finite sign); amount = | basis_delta_rlusd |
basis_commitwith price_rlusd != None → ValueErrorbasis_commitwith asset='XRP' → ValueErrorbasis_commitwith amount != |basis_delta_rlusd| → ValueError- Withdrawal without basis_delta_rlusd → ValueError (forces caller to classify first)
Rationale: Caller-owns-classification on withdrawal keeps the pre-event boundary crisp — if record_capital_event read state internally to classify, the read would race the write.
2b. classify_withdrawal_basis — pure function¶
def classify_withdrawal_basis(
withdrawal_rlusd: float,
equity_before_rlusd: float,
basis_before_rlusd: float,
) -> tuple[float, float]:
"""
Returns (profit_portion, principal_portion).
profit_portion = min(withdrawal, max(equity_before - basis_before, 0))
principal_portion = withdrawal - profit_portion
Caller applies sign: basis_delta_rlusd = -principal_portion
"""
7 pure tests: pure profit, pure principal, drawdown (equity < basis), mixed split, zero withdrawal, withdrawal > equity (cap), precision edge values.
2c. record_capital_event tests (10 cases)¶
deposit RLUSD auto-basis, deposit XRP auto-basis, caller-override, withdrawal with basis_delta, withdrawal without → ValueError, basis_commit RLUSD valid, basis_commit with price_rlusd → ValueError, basis_commit asset=XRP → ValueError, basis_commit amount≠|basis_delta| → ValueError, basis_commit with basis_delta=0 → allowed.
3. Read-path: get_net_basis_rlusd()¶
def get_net_basis_rlusd(self) -> float:
row = self._conn.execute(
"SELECT COALESCE(SUM(basis_delta_rlusd), 0.0) AS nb FROM capital_events"
).fetchone()
return float(row["nb"])
No boundary filter — net_basis is global. 6 tests: empty→0.0, one deposit, deposit+withdrawal, basis_commit, mixed, NULL rows→0.0.
4. scripts/inject_capital.py¶
CLI: python scripts/inject_capital.py --db <path> --amount <rlusd> [--dry-run] [--source-note <str>] [--tx-hash <str>]
RLUSD only, positive amount only (first cut).
Dry-run output (exact labels per FLAG-031 spec):
=== FLAG-031 injection dry-run ===
DB: /tmp/live_copy.db
Amount: +50.00 RLUSD
net_basis_before = <current>
net_basis_after = <current + 50.00>
paper.pnl_starting_value_rlusd (before) = <current>
paper.pnl_starting_value_rlusd (after) = <current + 50.00>
capital_events row that WOULD be written:
event_type = deposit
asset = RLUSD
amount = 50.00
basis_delta_rlusd = +50.00
source_note = <source-note or 'manual injection'>
tx_hash = <tx-hash or NULL>
No changes made.
Real run — single transaction, locked order: 1. write capital_event 2. update paper.pnl_starting_value_rlusd 3. commit
6 tests including forced-failure rollback (monkey-patched engine_state write → capital_event NOT persisted).
5. scripts/write_synthetic_initial_basis.py¶
CLI: python scripts/write_synthetic_initial_basis.py --db <path> --amount <rlusd> [--dry-run]
- One row: event_type='basis_commit', source_note='synthetic_initial_basis'
- Idempotency guard: check
source_note = 'synthetic_initial_basis'before inserting - Does NOT touch engine_state (historical reconstruction, not a new live event)
- 5 tests: first run writes, second run no-op, dry-run no writes, negative amount rejected, synthetic row does NOT count into
get_capital_delta_total(FLAG-030 regression)
Open question (Q1): Amount = 137.28 RLUSD (deposit-time valuation of the two pre-engine deposits: 39.27 XRP × 1.3314 + 85.00 = 137.28) vs mark-to-market. Orion recommends 137.28 — needs Atlas + Katja sign-off.
Commit sequence (branch: feat/flag-031-basis-model, stacked on FLAG-030)¶
flag-031: add basis_delta_rlusd column migration— schema + 4 testsflag-031: classify_withdrawal_basis pure function— function + 7 testsflag-031: accept basis_commit and basis_delta_rlusd in record_capital_event— validation + write + 10 testsflag-031: get_net_basis_rlusd read-path— method + 6 testsflag-031: inject_capital.py with atomic transaction— script + 6 testsflag-031: write_synthetic_initial_basis.py one-shot migration— script + 5 tests
Live-DB dry-run verification (before any live run): Run inject_capital.py --dry-run --amount 50 against copy of live DB with synthetic_initial_basis pre-applied. Assert four FLAG-032 invariants: Δ NET DEPOSITS = +50, Δ TOTAL VALUE = +50, Δ TOTAL PNL = 0, Δ TRADING VALUE = 0. Any fail → STOP.
What is NOT in this flag¶
rebuild()not touched- Dashboard metric migration deferred (follow-up commit)
- No withdrawal CLI (no current use case)
- No automated basis_commit (manual only per spec)
Open questions¶
- Synthetic basis amount: 137.28 RLUSD (deposit-time) confirmed? Needs Atlas + Katja on record.
- record_capital_event vs record_basis_commit: single method or split? Vesper's call.
- basis_commit with basis_delta = 0: allow or reject?
- get_net_basis_rlusd cache: fine for now — capital_events is tiny.
— Orion (he/him)