Skip to content

[C] Orion Implementation — Cleanup + anchor error bps

To: Orion CC: Katja (Captain), Atlas From: Vesper Date: 2026-04-18


Context

Current main state (as of Apr 17): - Merged: fix/flag-008fix/flag-030feat/flag-031main - FLAG-032 verified and closed - Config pushed: base_size_rlusd: 15.0, max_xrp_exposure: 150.0, max_rlusd_exposure: 150.0, anchor_max_divergence_bps: 10.0 - S36 completed overnight — 19 fills, VW +0.65 bps, engine healthy

Your previous cleanup branch (fix/post-injection-cleanup) no longer exists. It was built in a temporary sandbox (/tmp/neo_git_work) that has been cleared. All design work is intact and described below — rebuild from current main.


Git Rule — MANDATORY

All git commands go to Katja's VS Code terminal via copy-paste. Do NOT attempt to run git via the filesystem mount — that path is unreliable for git operations and will show a broken HEAD state. Provide every git command as a copy-paste block. You do not push anything yourself.


Branch

git checkout main
git pull origin main
git checkout -b fix/cleanup-and-anchor-instrumentation

Items

Item 1 — FLAG-034: Session summary shows fills-only XRP balance, not total

File: summarize_paper_run.py

Problem: _get_inventory_balance() reads inventory_ledger.new_balance for XRP and RLUSD. Post-injection, this is fills-only. The capital overlay (35.21 XRP injected Apr 17) is not included. Session summary shows ~36 XRP instead of the actual ~71 XRP (post-session balance).

Fix: After reading fills_only from inventory_ledger, add the net capital delta from capital_events:

def _get_inventory_balance(conn, asset, fallback_key) -> float:
    row = conn.execute(
        "SELECT new_balance FROM inventory_ledger WHERE asset = ? ORDER BY id DESC LIMIT 1",
        (asset,),
    ).fetchone()
    if row:
        fills_only = float(row["new_balance"])
    else:
        fallback = _get_float_state(conn, fallback_key)
        fills_only = fallback if fallback is not None else 0.0
    # FLAG-034: add capital-events overlay so displayed balance matches
    # the live total held by InventoryManager (fills + capital deltas).
    overlay = conn.execute(
        "SELECT COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount "
        "WHEN event_type='withdrawal' THEN -amount ELSE 0 END), 0.0) AS o "
        "FROM capital_events WHERE asset = ?",
        (asset,),
    ).fetchone()["o"]
    return fills_only + float(overlay)

Note: basis_commit event type is correctly excluded by the CASE WHEN (only deposit/withdrawal affect asset balance).

Required tests (5): 1. No capital events → returns fills_only unchanged 2. XRP deposit → fills_only + deposit amount 3. RLUSD withdrawal → fills_only - withdrawal amount 4. basis_commit event → balance unchanged (excluded from overlay) 5. No ledger rows → uses engine_state fallback + overlay

File to create: tests/test_flag_034_display_overlay.py


Item 2 — max_inventory_usd retirement: remove dead config

Context: max_inventory_usd in StrategyConfig has no effect on engine behavior. It lives only in a log.info diagnostic block in main_loop.py:1102 with no branch, no gate, no return. It blocks nothing. The buy_inventory_guard_blocked log label was misleading — it never blocked any buys. Atlas-approved for full removal.

14 files to touch:

File Change
neo_engine/config.py Remove max_inventory_usd field from StrategyConfig and its loader kwarg
neo_engine/strategy_engine.py Remove from __init__ log dict (line ~116)
neo_engine/main_loop.py Remove buy_inventory_guard_blocked from _log_no_intents_reason() diagnostic block
config/config.yaml Remove max_inventory_usd line
config/config_live_stage1.yaml Remove max_inventory_usd: 20.0 line (line 78)
config/config_live_session1.yaml Remove if present
config/config.example.yaml Remove if present
tests/test_task5.py Remove from _make_config helper default and all StrategyConfig(...) constructor calls
tests/test_phase3d.py Remove all kwargs and mock assignments
tests/test_phase4a.py Remove all kwargs and mock assignments
tests/test_main_loop.py Remove all kwargs and mock assignments
neo_simulator/simulation_runner.py Remove kwarg
neo_engine/main_loop_Old.py Remove for grep hygiene (archive file)
neo_engine/strategy_engine_old.py Remove for grep hygiene (archive file)

Smoke test required: All four configs must load cleanly after removal. StrategyConfig must not have the field. Run: python -c "from neo_engine.config import StrategyConfig; print('ok')" and confirm each config file loads without KeyError.


Item 3 — FLAG-033: Startup DB integrity check

File: run_paper_session.py

Problem: FLAG-027 protects against kill-during-WAL-checkpoint via pre-run backup. It does NOT protect against OS-level truncation at clean shutdown. Session 32 completed normally but the post-exit checkpoint was interrupted, truncating 311 pages. Current startup sequence does not detect a corrupt DB before running.

Fix: Add _startup_integrity_check(db_path) called BEFORE _create_pre_run_backup():

def _startup_integrity_check(db_path: Path) -> None:
    """FLAG-033: Fail fast on corrupt DB before creating backup or starting engine."""
    if not db_path.exists() or db_path.stat().st_size == 0:
        return  # fresh run or in-memory — no-op
    import sqlite3
    try:
        uri = f"file:{db_path}?mode=ro"
        with sqlite3.connect(uri, uri=True) as conn:
            result = conn.execute("PRAGMA quick_check").fetchone()
        if result is None or result[0] != "ok":
            raise RuntimeError(
                f"[FLAG-033] DB integrity check FAILED: {db_path}\n"
                f"Result: {result}\n"
                f"Restore from the most recent backup before running."
            )
    except sqlite3.DatabaseError as e:
        raise RuntimeError(
            f"[FLAG-033] DB integrity check raised DatabaseError (file may be truncated): {e}\n"
            f"Restore from the most recent backup before running."
        ) from e
    log.info("[FLAG-033] DB integrity check passed (PRAGMA quick_check = ok)")

Ordering: _startup_integrity_check BEFORE _create_pre_run_backup. Corrupt files must not get quietly backed up.

Required tests (5): 1. :memory: path → no-op (skip check) 2. Non-existent path → no-op 3. Empty file → no-op 4. Healthy DB → passes, logs OK 5. Truncated/corrupt DB → raises RuntimeError with clear restore instructions

File to create: tests/test_flag_033_startup_integrity.py


Item 4 — FLAG-028: Add idx_fills_session_id index

File: neo_engine/state_manager.py

Problem: fills table has no index on session_id. All session-scoped fill queries (session summary, dashboard session metrics, Segment B analysis) do full table scans.

Fix: Add to the migration block, alongside other post-schema column additions:

CREATE INDEX IF NOT EXISTS idx_fills_session_id ON fills (session_id);

Must be IF NOT EXISTS — idempotent across existing DBs.

Required tests (2): 1. Index exists on fills table after migration 2. Index covers the session_id column

File to create: tests/test_flag_028_fills_session_index.py


Item 5 — anchor_error_bps: add |error| > 5 bps reliability stat (NEW — Katja priority)

Background: Katja's operating principle as of Apr 18: if |anchor_error_bps| > 5 bps, results from that tick are in unreliable territory. She needs the % of session ticks where this threshold is exceeded to assess session reliability.

What already exists: - strategy_engine.py:204: self.last_anchor_divergence_bps = (anchor_mid - clob_mid) / clob_mid * 10000 — this IS anchor_error_bps - main_loop.py: per-tick collection into self._anchor_divergence_obs (list of floats) - _log_anchor_divergence_summary(): computes mean, median, min, max, cap%, and one-sided buckets (<=0, 0-5, 5-10, 10-12, 12-14, 14-15, >15) - Session terminal summary already shows: Anchor: mean=X | median=X | range=[X, X] | bias=X

What's missing: The |error| > 5 bps stat (absolute value threshold).

Changes needed:

File: neo_engine/main_loop.py_log_anchor_divergence_summary()

Add to the per-observation loop:

abs_above_5 = 0
for v in obs:
    if abs(v) > 5:
        abs_above_5 += 1
    # ... existing bucket logic unchanged ...

Compute stat:

pct_error_above_5 = round(abs_above_5 / valid_count * 100, 1)

Add to log.info extra dict:

"pct_error_above_5bps": pct_error_above_5,

Add to engine_state persistence block:

self._state.set_engine_state("anchor.pct_error_above_5bps", str(pct_error_above_5))

File: summarize_paper_run.py

Read from engine_state in _collect_summary():

"anchor_pct_error_above_5bps": _get_float_engine_state(conn, "anchor.pct_error_above_5bps"),

Update the anchor display line (~line 421):

_anc_err5 = summary.get("anchor_pct_error_above_5bps")
err5_str = f" | |err|>5bps: {_anc_err5:.1f}%" if _anc_err5 is not None else ""
lines.append(
    f"Anchor: mean={_anc_mean:+.2f}bps | median={_anc_median:+.2f}bps"
    f" | range=[{_anc_min:+.1f}, {_anc_max:+.1f}] | bias={_bias}{err5_str}"
)

Example output:

Anchor: mean=+2.70bps | median=+5.45bps | range=[-10.0, +10.0] | bias=positive | |err|>5bps: 47.3%

Required tests (3): 1. All errors within ±5 bps → pct_error_above_5bps = 0.0 2. Mixed: half outside → pct_error_above_5bps = 50.0 3. All at cap (±10 bps) → pct_error_above_5bps = 100.0

Add to existing anchor test file or create tests/test_anchor_error_stat.py.


Commit Ordering

One commit per item — do not bundle across items. Tests travel with the code change they cover.

# Commit subject
1 fix(flag-034): session summary displays total XRP balance (fills + capital overlay)
2 chore: retire max_inventory_usd dead config (14 files)
3 fix(flag-033): startup DB integrity check before backup and engine init
4 fix(flag-028): add idx_fills_session_id index on fills table
5 feat: anchor_error_bps reliability stat — pct ticks where |error| > 5 bps

What NOT to Touch

  • anchor_max_divergence_bps: 10.0 — hold, no change
  • base_size_rlusd: 15.0 — hold
  • No other strategy parameters
  • No schema changes beyond the index in Item 4
  • Do not touch risk_engine.py or any fill calculation paths

Git Commands for Katja

After each commit passes tests, provide this for Katja's terminal:

# After all commits are ready:
git push origin fix/cleanup-and-anchor-instrumentation

Then provide the PR creation command. Katja runs all git commands. You provide the copy-paste blocks.


— Vesper