Orion Delivery — feat/anchor-error-per-tick-telemetry¶
To: Vesper (she/her)
From: Orion (he/him)
CC: Katja (Captain), Atlas (he/him)
Date: 2026-04-21
Branch: feat/anchor-error-per-tick-telemetry
Base: main at d7c84cc (post-corridor-guard merge)
Phase 7.3 gate: 6 — required for live-session calibration interpretation
Summary¶
Per-tick anchor_error_bps REAL column on system_metrics, written on every tick from NEOEngine._persist_tick_telemetry. Value source is self._strategy.last_anchor_divergence_bps — the same source the anchor saturation guard's rolling window reads from. On invalid-snapshot ticks the write is gated to NULL so telemetry and the guard observe identical samples (Vesper Q1 Option A ruling 2026-04-21). Telemetry only — no guard logic, no config changes, no strategy tuning.
Commits (3)¶
847f926 feat(db): add anchor_error_bps column to system_metrics (per-tick anchor telemetry)
965d6e8 feat(main_loop): write anchor_error_bps to system_metrics on each tick
d4f1467 test(telemetry): anchor error per-tick telemetry — 7 tests
Diff: 3 files changed, 506 insertions(+), 3 deletions(-)
| Commit | Files | +/- |
|---|---|---|
847f926 |
neo_engine/state_manager.py |
+31 / −3 |
965d6e8 |
neo_engine/main_loop.py |
+16 / −0 |
d4f1467 |
tests/test_anchor_error_telemetry.py |
+459 / −0 |
Test Count & Pass Rate¶
7 new tests, all passing. Total regression: 219 / 219 green across state_manager + all Phase 7.3 protection branches + this branch:
tests/test_anchor_error_telemetry.py 7 passed
tests/test_state_manager.py 117 passed
tests/test_anchor_error_stat.py 3 passed
tests/test_anchor_saturation_guard.py 16 passed
tests/test_distance_to_touch_summary.py 18 passed
tests/test_directional_drift_guard.py 24 passed
tests/test_inventory_corridor_guard.py 24 passed
tests/test_reconciler_conservative.py 10 passed
tests/test_flag_036_wallet_truth_reconciliation.py (included in group)
tests/test_reconciler_anomaly_log.py 11 passed
------------------------------------------------------
TOTAL 219 passed
Verification command (run on Katja's VS Code Windows terminal from the repo root):
python -m pytest tests/test_anchor_error_telemetry.py tests/test_state_manager.py tests/test_anchor_error_stat.py tests/test_anchor_saturation_guard.py tests/test_distance_to_touch_summary.py tests/test_directional_drift_guard.py tests/test_inventory_corridor_guard.py tests/test_reconciler_conservative.py tests/test_flag_036_wallet_truth_reconciliation.py tests/test_reconciler_anomaly_log.py -v
Broader out-of-scope failures (test_xrpl_gateway.py, test_main_loop.py::TestComputeFillRealism, test_run_paper_session.py, test_paper_launch.py) are pre-existing on main and unrelated to this branch — confirmed unchanged by these commits.
Pre-Code Investigation — Findings Recap¶
Investigation memo delivered and ratified at [C] Orion Investigation — Anchor Error Per-Tick Telemetry.md. Vesper green-lit Option A 2026-04-21. Summary:
Q1 — system_metrics write location + staleness risk. Written inside NEOEngine._persist_tick_telemetry at main_loop.py:3191. Order within the tick: Step 3E anchor diagnostics → Step 8.5 saturation guard → Step 8.6 telemetry persistence. By the telemetry call, self._strategy.last_anchor_divergence_bps has been updated by calculate_quote for this tick — iff the snapshot was valid. On invalid-snapshot ticks calculate_quote early-returns at strategy_engine.py:154 before the assignment, leaving the attribute at the previous valid tick's value. Resolution: Option A gate — in _persist_tick_telemetry, sample the attribute only when snapshot.is_valid() is True; write None otherwise. This mirrors the saturation guard's own observation gate at main_loop.py:2555-2563.
Q2 — _ensure_column migration pattern. Confirmed precedent from Branch #6 (distance_to_touch_bid_bps, distance_to_touch_ask_bps — both added with the CREATE TABLE IF NOT EXISTS body update + _ensure_column call). Used the same two-part pattern here. Vesper confirmed in the ruling that CREATE TABLE body updates are in scope — the constraint in the tasking memo is about manual ALTER TABLE / DROP/RECREATE, not the canonical schema declaration.
Q3 — NULL vs 0.0 handling. Branch #6 precedent: distance_to_touch_bid_bps and distance_to_touch_ask_bps are nullable REAL, populated only in live mode or when the quote exists. Passing Python None through sqlite3 parameter binding produces SQL NULL directly — no cast error, no substitution. Confirmed the same binding behaviour in the new column via test #4.
Q4 — Existing anchor_error_bps references. Grep across neo_engine/, tests/, docs/, and config.yaml — the name appears only in comments, docstrings, the anchor.pct_error_above_5bps aggregate stat, and as an alias in docs. No existing column, dataclass field, or function parameter named anchor_error_bps. Clean to introduce.
Implementation¶
C1 — feat(db): add anchor_error_bps column to system_metrics (847f926)¶
neo_engine/state_manager.py (+31 / −3):
- Added
anchor_error_bps REALto theCREATE TABLE IF NOT EXISTS system_metricsbody (lines 595–603) with a doc block explaining: Phase 7.3 gate 6, same source as the saturation guard's rolling window, NULL on invalid-snapshot ticks per Option A. - Added
_ensure_column(conn, "system_metrics", "anchor_error_bps", "REAL")at line 798, following the Branch #6 distance-to-touch calls, with comment pointing at[C] Orion Investigation — Anchor Error Per-Tick Telemetry.mdQ1 Option A for the gate rationale. - Extended
record_system_metricsignature with a new keyword-only parameteranchor_error_bps: Optional[float] = None(line 2009) with a doc comment explicitly warning against 0.0 substitution. - Added
anchor_error_bpsto the INSERT column list (16 columns now) and the bind tuple.VALUESclause extended to 16 placeholders.
C2 — feat(main_loop): write anchor_error_bps to system_metrics on each tick (965d6e8)¶
neo_engine/main_loop.py (+16):
- In
_persist_tick_telemetry, after the Branch #6dist_to_touchlog block, inserted the Option A gate:
# Phase 7.3 gate 6 — anchor_error_bps per-tick telemetry.
# Mirror the anchor saturation guard's own observation gate at
# main_loop.py:2555-2563 — only sample the strategy attribute on
# valid-snapshot ticks. On invalid ticks calculate_quote()
# early-returns without updating last_anchor_divergence_bps
# (strategy_engine.py:154), so the attribute would otherwise
# carry the previous valid tick's value. Writing None here
# produces SQL NULL and keeps the column aligned with what the
# guard actually observes. Vesper Q1 Option A ruling 2026-04-21.
# Telemetry only; never influences trading.
if snapshot.is_valid():
anchor_err_bps = self._strategy.last_anchor_divergence_bps
else:
anchor_err_bps = None
- Passed
anchor_error_bps=anchor_err_bpsto therecord_system_metriccall.
C3 — test(telemetry): anchor error per-tick telemetry — 7 tests (d4f1467)¶
tests/test_anchor_error_telemetry.py (+459). Structured in four parts matching the invariants:
| # | Part | Invariant |
|---|---|---|
| 1 | Schema migration | Fresh DB has anchor_error_bps column (PRAGMA + round-trip) |
| 2 | Schema migration | initialize_database() is idempotent (no-op on re-run; exactly one column) |
| 3 | Write contract | Non-None float round-trips unchanged |
| 4 | Write contract | None → SQL NULL (not 0.0); default-arg path also NULL |
| 5 | Behavioural gate | Invalid snapshot forces NULL despite stale strategy value; valid-snapshot control opens the gate |
| 6 | Integration | Multi-tick writes preserve row order + session_id on every row |
| 7 | Integration | tick_latency_ms, engine_status, and Branch #6 distance_to_touch_* columns unaffected by the new field |
Windows teardown: LIFO addCleanup — TemporaryDirectory registered first, StateManager.close registered second. Applied in a shared _TempDBTest base class so every integration test that uses a real StateManager inherits it.
Test #5 specifically ratifies Option A — builds a MagicMock(spec=NEOEngine) with _strategy.last_anchor_divergence_bps=-4.75 pre-populated (the "stale" state), calls NEOEngine._persist_tick_telemetry with an invalid snapshot, and asserts the row shows NULL. Then calls again with a valid snapshot and asserts the same -4.75 now lands on the row — proving the gate is directional (closes on invalid, opens on valid) rather than unconditional.
Deviations¶
None. Branch hewed to the tasking memo and Vesper's Option A ruling:
_ensure_columnused for the migration (notALTER TABLE/DROP/RECREATE).CREATE TABLE IF NOT EXISTSbody also updated — confirmed in scope per Vesper's ruling.- No guard logic, no config changes, no strategy tuning touched.
- NULL preserved for invalid-snapshot and source-None ticks — never 0.0.
- No branch pre-creation — branch was created only when ready to commit C1 (investigation and Option A ruling completed on
main). - 7 tests delivered (tasking minimum was 6; the dedicated invalid-snapshot gate test #5 was added per Vesper's explicit "not optional" ratification of Option A).
Apply Instructions (PowerShell, copy-paste from VS Code terminal)¶
Patches live at:
C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\patches\feat-anchor-error-per-tick-telemetry\
From the repo root (C:\Users\Katja\Documents\NEO GitHub\neo-2026\):
# Ensure a clean main tip
git checkout main
git pull
# Defensive branch delete (no-op if the branch doesn't exist; silent if it does)
git branch -D feat/anchor-error-per-tick-telemetry 2>$null
# Create the branch
git checkout -b feat/anchor-error-per-tick-telemetry
# Apply all 3 patches in order (Get-ChildItem form — NOT glob)
Get-ChildItem "C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\patches\feat-anchor-error-per-tick-telemetry\" -Filter "*.patch" | Sort-Object Name | ForEach-Object { git am $_.FullName }
# Verify
git log --oneline main..HEAD
# Expected 3 commits:
# d4f1467 test(telemetry): anchor error per-tick telemetry — 7 tests
# 965d6e8 feat(main_loop): write anchor_error_bps to system_metrics on each tick
# 847f926 feat(db): add anchor_error_bps column to system_metrics (per-tick anchor telemetry)
# Run the targeted regression subsuite
python -m pytest tests/test_anchor_error_telemetry.py tests/test_state_manager.py tests/test_anchor_error_stat.py tests/test_anchor_saturation_guard.py tests/test_distance_to_touch_summary.py tests/test_directional_drift_guard.py tests/test_inventory_corridor_guard.py tests/test_reconciler_conservative.py tests/test_flag_036_wallet_truth_reconciliation.py tests/test_reconciler_anomaly_log.py -v
Expected: 219 passed.
What This Unblocks¶
Post-session calibration of the anchor saturation guard thresholds (6–8 bps mean, 40 % prevalence) — previously guesswork from the session-level anchor.pct_error_above_5bps aggregate, now grounded in the full per-tick distribution tagged by session_id. With this merged, the two Katja-agreed clean live sessions required as a Phase 7.4 SR-AUDIT precondition can be evaluated against real data: threshold too sensitive, not sensitive enough, at what tick the bias first crossed, what the distribution shape looked like.
Vesper reviews before merge.
— Orion