Orion Tasking — Inventory Corridor Guard¶
To: Orion (he/him)
From: Vesper (she/her)
CC: Katja (Captain), Atlas (he/him)
Date: 2026-04-19
Branch: feat/inventory-corridor-guard
Priority: HIGH — Phase 7.3 gate 4, required before live sessions resume
Context¶
The inventory corridor guard was specified by Atlas on 2026-04-19 as Priority 2 in the Phase 7.3 guard sequence:
"Now that wallet truth is reliable: enforce composition bounds (XRP vs RLUSD) using real wallet state, not derived state. Trigger: DEGRADED (not immediate HALT unless extreme)."
The problem this addresses: S39 saw 52.11 XRP sold in 90 seconds immediately after capital injection, followed by sustained sell-side pressure — the engine ran deep into RLUSD-heavy territory with no guard to stop it. The drift guard catches fill-flow imbalance in real time. This guard catches the accumulated result — inventory composition drifting outside safe bounds.
Key distinction: The drift guard responds to FLOW. This guard responds to STATE. Both are required. A session could pass the drift guard (fills balanced in each window) while still drifting into unsafe composition over time.
Spec (Atlas-locked Apr 19, parameters proposed here for Atlas calibration)¶
What to measure: XRP value as a percentage of total portfolio value, computed using the current mid price from the market snapshot.
xrp_value_rlusd = xrp_balance × mid_price_rlusd_per_xrp
total_value_rlusd = xrp_value_rlusd + rlusd_balance
xrp_pct = 100 × xrp_value_rlusd / total_value_rlusd
Trigger DEGRADED if:
- xrp_pct < min_xrp_pct (too light in XRP — over-sold)
- OR xrp_pct > max_xrp_pct (too heavy in XRP — over-bought)
- AND condition persists for corridor_lookback_ticks consecutive ticks (prevents single-tick price-spike false positives)
Hard floor — trigger DEGRADED immediately (no lookback required) if:
- rlusd_balance < min_rlusd_floor (RLUSD critically low — engine cannot quote safely)
Guard inactive if:
- Total portfolio value < min_portfolio_rlusd (degenerate state — don't fire on near-empty portfolio)
- Mid price is None or unavailable (see Q4 ruling request below)
Behavior on trigger: DEGRADED — cancel all orders, stop quoting, continue observation. Same pattern as anchor and drift guards. One-shot flag per session.
YAML Config Block¶
Add under strategy: in all three config files:
inventory_corridor_guard:
enabled: true
# Composition bounds (XRP value as % of total portfolio)
min_xrp_pct: 25.0 # below this → DEGRADED (over-sold XRP)
max_xrp_pct: 75.0 # above this → DEGRADED (over-bought XRP)
corridor_lookback_ticks: 3 # consecutive ticks outside corridor before trigger
# Hard floor
min_rlusd_floor: 30.0 # below this RLUSD balance → immediate DEGRADED
# Guard activation gate
min_portfolio_rlusd: 50.0 # guard inactive below this total portfolio value
All parameters flagged for Atlas calibration post-first-live-session. Do not tune in this branch.
Rationale for proposed defaults:
- 25/75 corridor: S41 ending inventory is 70.79 XRP / 95.96 RLUSD ≈ 52% XRP. Corridor allows meaningful drift before firing. S39's severe sell-side would have put XRP below 25% well before the drain was complete.
- corridor_lookback_ticks: 3 (~12s at 4s cadence): eliminates single-tick price spike noise without meaningful lag on real composition drift.
- min_rlusd_floor: 30.0: below 30 RLUSD we cannot safely quote both sides.
- min_portfolio_rlusd: 50.0: degenerate sessions excluded.
Pre-Code Investigation Required¶
Before writing any code, investigate and report on the following:
Q1 — Inventory state in tick loop. Where in the main loop tick is the current XRP and RLUSD balance available? Is it engine.inventory_manager.get_snapshot() or another accessor? What fields does it expose — specifically, is the balance live (updated on each fill this session) or does it reflect the last reconciler snapshot? Confirm that the balance this guard reads is the same one the pre-trade gate uses.
Q2 — Mid price availability. At the proposed insertion point (Step 8.5c, after drift guard), what is the current mid price? Is it available as a field on the engine or market snapshot? What is the field name and type? Confirm it is the same mid price used for quote construction so the guard's valuation is consistent with the engine's own view.
Q3 — Insertion point. Confirm Step 8.5c (after drift guard at 8.5b, after anchor guard at 8.5) is the correct insertion. Both prior guards use _enter_degraded_mode (idempotent) and independent one-shot flags. Confirm no ordering conflict — this guard needs current inventory state and current mid price to both be available at that point.
Q4 — Mid price None handling. What should happen if mid price is None at evaluation time (e.g., no market data this tick)? Two options: (A) skip the corridor check this tick (fail open — don't trigger on missing data), or (B) treat as unknown → trigger DEGRADED (fail closed). Propose and flag for ruling.
Q5 — Existing inventory bounds. Is there any existing min_inventory / max_inventory configuration or enforcement in the engine that this guard overlaps with or should supersede? Check config.py, inventory_manager.py, and any existing inventory cap logic. The new guard must not double-fire with existing inventory enforcement.
Report findings before writing any code.
Implementation Guidance¶
Tick counter for lookback: Maintain _corridor_ticks_outside: int — increment each tick the composition is outside the corridor (above max or below min), reset to 0 when inside. Fire when _corridor_ticks_outside >= corridor_lookback_ticks. The hard min_rlusd_floor check bypasses this counter — it fires immediately.
Guard state resets: On session start, _corridor_ticks_outside = 0, _corridor_guard_triggered_this_session = False.
One-shot flag: _corridor_guard_triggered_this_session = True must be set as the first action inside the trigger block — before WARNING log, before record_circuit_breaker_event, before _enter_degraded_mode. Same pattern as anchor and drift guards.
_enter_degraded_mode reason strings:
- Corridor breach: "inventory_corridor_guard_composition"
- RLUSD floor breach: "inventory_corridor_guard_rlusd_floor"
Console WARNING format:
[CORRIDOR_GUARD] DEGRADED triggered — condition=<composition|rlusd_floor> | xrp_pct=<pct>% | xrp=<xrp> | rlusd=<rlusd> | mid=<mid>
circuit_breaker_events context payload:
- condition_triggered: "composition" or "rlusd_floor"
- For composition: xrp_pct, min_xrp_pct, max_xrp_pct, xrp_balance, rlusd_balance, mid_price, ticks_outside
- For rlusd_floor: rlusd_balance, min_rlusd_floor
Persist-failure safety: DB write failure swallowed, DEGRADED still entered. Same pattern as all prior guards.
Test Requirements¶
Minimum 10 tests for this branch:
- Guard inactive before
min_portfolio_rlusdthreshold - Composition inside corridor → no trigger
- Composition outside corridor for <
lookback_ticks→ no trigger (not yet) - Composition outside corridor for >=
lookback_ticks→ DEGRADED triggered - Composition returns inside corridor before lookback completes → counter resets, no trigger
- RLUSD below floor → immediate DEGRADED (no lookback wait)
- Mid price None → behavior per Q4 ruling
- One-shot dedup — no second trigger on subsequent ticks
enabled: false→ all conditions skippedcircuit_breaker_eventsrow written with correct condition label and session_id- (Bonus) Windows teardown fix —
addCleanuppattern for any integration tests using realStateManager
Commit Plan (suggested)¶
feat(config): InventoryCorridorGuardConfig dataclass + YAML defaults (Phase 7.3)feat(main_loop): inventory corridor guard state + evaluation (Step 8.5c)feat(db,main_loop): corridor guard circuit_breaker_events persistence + WARNING logtest(guard): inventory corridor guard — 10+ tests
Constraints¶
- Use real inventory state (same source as pre-trade gate), not derived state — per Atlas ruling
- DEGRADED only, not HALT — unless extreme (out of scope for this branch)
- Parameters configurable in YAML — no hardcoded thresholds
- Use existing
_enter_degraded_modeandrecord_circuit_breaker_eventinfrastructure - No pre-creating the branch during investigation
- No
*.patchglob in PowerShell apply instructions — useGet-ChildItemform - Always include defensive
git branch -Dbeforegit checkout -bin apply instructions - Windows teardown fix required in any integration tests using real
StateManager - No strategy tuning in this branch — guard only
Deliverable¶
Standard format: branch name, commit list with hashes and messages, test count and pass rate, pre-code investigation findings, any deviations from spec flagged explicitly. Vesper reviews before merge.
— Vesper