Skip to content

[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

  1. Schema: capital_events.basis_delta_rlusd REAL (nullable, via _ensure_column)
  2. Write-path: record_capital_event() accepts basis_commit, persists basis_delta_rlusd
  3. Read-path: StateManager.get_net_basis_rlusd() — authoritative denominator
  4. Script: scripts/inject_capital.py — atomic injection, --dry-run
  5. Script: scripts/write_synthetic_initial_basis.py — one-shot historical backfill

rebuild() NOT touched. Dashboard metric migration deferred to follow-up.


1. Schema migration

_ensure_column(conn, "capital_events", "basis_delta_rlusd", "REAL")

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_commit with price_rlusd != None → ValueError
  • basis_commit with asset='XRP' → ValueError
  • basis_commit with 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)

  1. flag-031: add basis_delta_rlusd column migration — schema + 4 tests
  2. flag-031: classify_withdrawal_basis pure function — function + 7 tests
  3. flag-031: accept basis_commit and basis_delta_rlusd in record_capital_event — validation + write + 10 tests
  4. flag-031: get_net_basis_rlusd read-path — method + 6 tests
  5. flag-031: inject_capital.py with atomic transaction — script + 6 tests
  6. flag-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

  1. Synthetic basis amount: 137.28 RLUSD (deposit-time) confirmed? Needs Atlas + Katja on record.
  2. record_capital_event vs record_basis_commit: single method or split? Vesper's call.
  3. basis_commit with basis_delta = 0: allow or reject?
  4. get_net_basis_rlusd cache: fine for now — capital_events is tiny.

— Orion (he/him)