Skip to content

[C] Orion Proposal — FLAG 053 Implementation Plan

Read your prework package. Three proposals below, tight scope. No branch created yet. Waiting on your confirmation before I touch code.


Q1 — Option A: exit-eligible residual threshold

Proposal: introduce a preview residual window with its own shorter warmup + lookback. Production window unchanged.

Mechanism

Two new config parameters on AnchorDualSignalConfig:

  • residual_exit_preview_warmup_ticks: int = 20 (vs. production warmup_ticks = 50)
  • residual_exit_preview_lookback_ticks: int = 10 (vs. production residual_hysteresis_lookback_ticks = 20)

New accessor on AnchorDualSignalCalculator: preview_baseline (property returning self._baseline, or None if not yet seeded). The calculator does NOT change how it folds observations — preview is a READ operation against state the calculator already holds.

New deque on the engine: _anchor_residual_exit_preview_window with maxlen = residual_exit_preview_lookback_ticks. Fed each valid-snapshot tick, right next to the existing residual-window append (main_loop.py:4339-4345): once _observations_since_seed >= residual_exit_preview_warmup_ticks and preview_baseline is not None, compute preview_residual = structural_basis_bps - preview_baseline and append.

Extend _select_anchor_guard_window with a for_exit: bool = False kwarg. Exit evaluator calls with for_exit=True. Selection priority becomes:

  1. Full residual window if len >= maxlen (production — unchanged)
  2. Preview window if for_exit=True AND ads_cfg.residual_exit_preview_enabled AND len(preview) >= preview_lookback (NEW)
  3. Legacy capped window (fallback — unchanged)

Entry evaluator still calls with for_exit=False, never gets the preview window, entry hysteresis unchanged.

Timing impact

  • Current minimum exit: 70 ticks (warmup + full residual window) + 30 stability = 100 ticks
  • With preview path: 20 (preview warmup) + 10 (preview window fill) + 30 stability = 60 ticks
  • 40-tick reduction in hostile-then-improving regimes. Production confidence path untouched.

Why not shorter

Going below 20 warmup ticks erodes EMA sample validity. At alpha ≈ 0.01325 (N=150), the baseline at 20 observations is ~24% influenced by seed and ~76% by subsequent samples — enough to express regime improvement, not so raw that noise dominates. The preview-lookback of 10 mirrors the 10-of-20 ratio of the production window — same structural proportion, half the samples.

New config parameter yes/no

Yes — two new parameters. Shared reduction (lowering the production warmup/lookback) is not acceptable; it weakens the saturation guard's entry threshold too. Preview-specific parameters are the minimum-blast-radius option.


Q2 — Option B: structural-basis benign-regime early exit

Proposal: single-tick structural threshold with the existing 30-tick stability counter. No rolling mean.

Mechanism

New config on AnchorSaturationGuardConfig:

  • recovery_structural_early_exit_enabled: bool = True

Threshold reuses the existing recovery_exit_bias_threshold_bps = 4.0 (no new number). Stability tick count reuses recovery_stability_ticks = 30.

In _evaluate_anchor_idle_exit (main_loop.py:2887-2997), during the legacy-window phase ONLY (i.e., when _select_anchor_guard_window(for_exit=True) returns source "last_anchor_divergence_bps" — preview path not yet available), add a parallel stability-tick rule:

  • If self._strategy.last_structural_basis_bps is not None AND abs(structural_basis_bps) < recovery_exit_bias_threshold_bps, treat the tick as stability-eligible (increment the counter).
  • If legacy window's bias+prevalence test ALSO passes, that also counts (existing behavior).
  • If BOTH fail → reset counter to 0 (unchanged hysteresis).

This is additive: in the legacy phase, the stability counter can be driven by EITHER the legacy window test (currently impossible in saturated regimes) OR the structural per-tick test. Once the preview or full residual source takes over, this path naturally deactivates (exit evaluator no longer enters the legacy branch).

Why raw structural, not rolling mean

Three reasons:

  1. The 30-tick stability counter IS the rolling filter. A per-tick structural below 4 bps that happens to be an outlier gets cleaned up by the next tick that crosses back above — counter resets, no false exit.
  2. Adding a separate rolling mean on top introduces a second filter with its own tuning knobs. Keep it simple.
  3. Structural is already derived from raw CLOB-AMM; it doesn't need additional smoothing to be meaningful as an improving-regime signal.

Stability tick count

30, unchanged. The structural early-exit path uses the same safeguard as the residual path. Symmetric design, single tuning knob.

Timing impact

In a benign regime where structural drops below 4 bps at tick K, exit fires at tick K+30. For regimes that improve within the first ~30 ticks, this is significantly faster than the preview path (which still needs 60 ticks minimum). For regimes that stay hostile for a long time, this path never activates and the preview path (Option A) takes over.


Q3 — Sign convention standardization

Proposal: full recompute of last_anchor_divergence_bps from the canonical formula. Operator-facing sign flip, not a display-only band-aid.

Canonical formula (confirmed from your prework)

anchor_divergence_bps = ((clob_mid − amm_price) / amm_price) × 10000

Negative = AMM above CLOB (same convention as clob_vs_amm_divergence_bps and structural_basis_bps).

Implementation

In strategy_engine.py:220-223, replace:

if mid_price > 0:
    self.last_anchor_divergence_bps = ((quote_anchor_price - mid_price) / mid_price) * 10000.0
else:
    self.last_anchor_divergence_bps = None

with:

if mid_price > 0 and amm_price is not None and amm_price > 0:
    self.last_anchor_divergence_bps = ((mid_price - amm_price) / amm_price) * 10000.0
else:
    self.last_anchor_divergence_bps = None

This changes: - Computed from quote_anchor_price (capped) → computed from raw amm_price (uncapped). - (quote_anchor − mid)/mid(mid − amm)/amm. Sign flips. Magnitude no longer clamped at ±cap.

Downstream impact audit

Grepped last_anchor_divergence_bps across the repo. Three consumers:

Site Uses sign? Uses magnitude? Impact of sign flip + uncap
_anchor_error_window feed (main_loop.py:4288) Entry/exit tests use abs(mean) and abs(x) > threshold Yes Behavior unchanged — guard math is sign-neutral
_anchor_divergence_obs summary (main_loop.py:4286) Signed statistics written to engine_state (anchor.mean_bps, min, max) Yes Sign of stored summary flips. Correct — now matches canonical convention.
anchor_error_bps per-tick telemetry (main_loop.py:4988 → DB column) Signed value stored Yes Sign of stored column flips. DB schema unchanged; changelog note required.

One subtlety: the stored magnitude is no longer clamped at ±10. That means the system_metrics.anchor_error_bps column will carry the full uncapped value. This is an improvement — the column name is a misnomer either way (it's a divergence metric, not an error), but now it carries more information and matches the tick log's clob_vs_amm_divergence_bps.

What stays the same

  • last_cap_applied — still True when amm_price is beyond the cap bounds. Cap-lock percentage metric is unchanged.
  • quote_anchor_price — still capped at ±10 bps for intent generation. Trading behavior does NOT change.
  • Saturation guard entry thresholds (bias_threshold_bps, prevalence_threshold_bps, prevalence_pct) — applied via abs(), so sign-neutral. Unchanged.

Why full recompute over display flip

Four reasons:

  1. The DB value survives across sessions. A display-only flip leaves a permanent opposite-sign artifact in the system_metrics table that any SQL query or external analysis will trip over.
  2. Magnitude uncap is a free win: the dashboard currently reports a clamped value that under-represents regime stress. Uncapped magnitude is more informative.
  3. Guard behavior is provably unchanged (all tests use abs()).
  4. Display flip would leave the "anchor_error_bps" column opposite-signed from structural_basis_bps — which is exactly the legibility gap this fix is meant to close.

Operator notice

Changelog entry required for the sign flip. Dashboard widget label should read anchor_divergence_bps (or similar) with the canonical convention clearly noted.


Proposed branch + commit plan (for your confirmation)

Branch name: fix/flag-053-anchor-idle-exit-lockout

Four commits:

  1. feat(config): FLAG-053 preview residual + structural early-exit config — adds residual_exit_preview_* fields to AnchorDualSignalConfig, adds recovery_structural_early_exit_enabled to AnchorSaturationGuardConfig, updates config_live_stage1.yaml, config.yaml, config.example.yaml.
  2. feat(dual_signal): FLAG-053 preview_baseline accessor — adds preview_baseline property on AnchorDualSignalCalculator. No state-mutation changes.
  3. feat(main_loop+strategy): FLAG-053 exit-evaluator preview window + structural early exit + sign flip — Option A preview window, Option B structural branch in exit evaluator, sign flip on last_anchor_divergence_bps. Exit evaluator updated to accept for_exit=True. Source selector extended.
  4. test: FLAG-053 validation suite — four tests per your validation criteria, plus 3-4 unit tests for preview baseline + sign-flip round-trip.

Validation tests (≥ 8 total, per your criteria)

  • T1 — Early exit in improving regime. Session enters ANCHOR_IDLE at tick 3, regime hostile for 20 ticks (structural −12 bps), then drops to −2 bps from tick 21. Expect: exit fires at tick 51 (21 + 30 stability on structural early-exit path). Confirms Option B.
  • T2 — Residual handover still works. Long session, regime improves after tick 70. Expect exit via residual window. No regression. Confirms production path untouched.
  • T3 — No false exit in persistently hostile regime. 100 ticks at structural −12 bps. Expect engine stays in ANCHOR_IDLE; stability counter never reaches 30.
  • T4 — Sign convention round-trip. Given CLOB-AMM divergence = −12 bps: clob_vs_amm_divergence_bps = −12, structural_basis_bps ≈ −12, last_anchor_divergence_bps ≈ −12 (post-flip). All three negative.
  • T5 — Preview window fills on schedule. Structural converges by tick 15, preview warmup=20, preview lookback=10 → preview window full at tick 30.
  • T6 — Preview window selected when appropriate. Exit evaluator with for_exit=True picks preview window over legacy when both available; picks full residual when available.
  • T7 — Preview window not selected for entry. Saturation guard entry evaluator (_evaluate_anchor_residual_guard) calls with for_exit=False and never gets the preview. Entry hysteresis unchanged.
  • T8 — Guard entry unaffected by sign flip. Saturation guard entry fires at same tick given same regime, pre-flip vs. post-flip (abs-based math).

Three questions back to you before I start

  1. Preview config defaults — OK with residual_exit_preview_warmup_ticks=20, residual_exit_preview_lookback_ticks=10? Or tighter/looser?
  2. Structural early-exit default state — Enabled (True) by default? Or ship disabled and enable by config flip after a session confirms?
  3. Dashboard label rename — In scope for this branch (rename anchor.mean_bpsanchor_divergence.mean_bps in engine_state), or save for a follow-up widget pass? Leaning save-for-follow-up to keep the fix branch contained.

Standing apply rules will be honored: no pre-created branch, Get-ChildItem ... ForEach-Object apply loop, defensive git branch -D preamble.

Ready on your confirm.

— Orion