Orion Tasking — Directional Drift Guard¶
To: Orion (he/him)
From: Vesper (she/her)
CC: Katja (Captain), Atlas (he/him)
Date: 2026-04-19
Branch: feat/directional-drift-guard
Priority: HIGH — second guard in Phase 7.3 sequence, follows Anchor Saturation Guard (merged)
Mission¶
Implement the Directional Drift Guard. This guard protects against fill-flow imbalance — rapid same-side fills or sustained one-sided activity — not price movement. It is a different layer from the momentum filter, which gates quote placement. This guard responds to what actually fills.
Context: S41 showed 3 buy fills in 24 seconds. The momentum filter caught some suppressions but couldn't stop the burst. This guard would have triggered DEGRADED on that pattern and stopped the session before further one-sided exposure accumulated.
Spec (Atlas-locked Apr 19)¶
Trigger DEGRADED if ANY ONE of these three conditions fires:
Condition A — Fill burst:
≥ 3 same-side fills within ~30s
Condition B — Net notional imbalance: Net notional imbalance (buy RLUSD − sell RLUSD, cumulative within session) exceeds threshold quickly — specific threshold TBD, propose a sensible default and flag for Atlas calibration post-first-session.
Condition C — No opposing fills: No opposing fills after N ticks (~15 ticks default, ~60s at 4s cadence)
This is OR logic — any single condition triggers DEGRADED. Unlike the anchor guard (which required all three conditions simultaneously), one condition is sufficient here.
Behavior on trigger: DEGRADED — cancel all orders, stop quoting, continue observation. Same pattern as anchor saturation guard.
Key principle (Atlas): We are protecting against FLOW, not price. This is explicitly separate from the momentum filter.
YAML Config Block¶
Add under strategy: in all three config files:
directional_drift_guard:
enabled: true
# Condition A — burst
burst_fill_count: 3 # same-side fills to trigger
burst_window_seconds: 30.0 # rolling window for burst detection
# Condition B — net notional
net_notional_threshold_rlusd: 50.0 # cumulative imbalance cap (propose, flag for calibration)
net_notional_window_seconds: 120.0 # window over which to measure imbalance
# Condition C — no opposing fills
no_opposing_fill_ticks: 15 # ticks without an opposing fill after first fill
no_opposing_fill_min_fills: 1 # must have at least this many fills before C activates
Pre-Code Investigation Required¶
Before writing any code, investigate and report on the following:
Q1 — Fill event surface in the tick loop. Where in the main loop does the engine learn that a fill occurred on the current tick? Is there a list of fills detected this tick? What fields are available (side, quantity_rlusd, timestamp, order_id)? Identify the exact variable name and location in main_loop.py.
Q2 — Fill side convention. Confirm: what does side='buy' mean in the fills table and in the tick-loop fill event — buy XRP (pay RLUSD) or buy RLUSD (pay XRP)? Confirm with reference to the fills table and any in-loop fill struct. S41 showed all 3 fills as side='buy' and the engine gained XRP / lost RLUSD — confirm this matches.
Q3 — Insertion point. The anchor saturation guard evaluates at Step 8.5 (after anchor compute, before Step 9 submit). Where should the drift guard evaluate? It needs to see fills from the current tick before deciding whether to suppress intents. Identify whether fill detection happens before or after Step 8.5, and confirm the correct insertion point so both guards can coexist without order-of-evaluation conflicts.
Q4 — Time source. For Condition A's rolling 30s window, what time source should the guard use? datetime.utcnow(), the tick's snapshot timestamp, or something else? Confirm what's available at the fill detection point and what Orion used in the reconciler for detected_at.
Q5 — circuit_breaker_events write pattern. C5a added session_id to circuit_breaker_events and the record_circuit_breaker_event writer was established in the anchor guard branch. Confirm this writer is available and suitable for the drift guard, or whether a new writer method is needed. The drift guard should use breaker="directional_drift_guard" and log which condition (A, B, or C) triggered, plus the relevant metrics at trigger time.
Report findings before writing any code.
Implementation Guidance¶
Condition A — Burst detection: Maintain a deque of (timestamp, side) tuples for recent fills. On each tick, append any fills from this tick, then purge entries older than burst_window_seconds. If count(side == last_side) >= burst_fill_count after the purge, Condition A fires. The "last side" is the side of the most recent fill.
Condition B — Net notional: Track cumulative buy_rlusd - sell_rlusd within a rolling net_notional_window_seconds. If abs(net_notional) >= net_notional_threshold_rlusd, Condition B fires. This catches sustained one-sided accumulation even if fills don't cluster in time.
Condition C — No opposing fills: After the first fill in a session, start a tick counter. Reset the counter whenever a fill occurs on the OPPOSITE side. If the counter reaches no_opposing_fill_ticks without an opposing fill, Condition C fires. Condition C is inactive until no_opposing_fill_min_fills fills have occurred (prevents false trigger on session open before any fills).
Guard state resets: On session start, all three condition states reset (deques cleared, counters zeroed). The one-shot flag (_drift_guard_triggered_this_session) follows the same pattern as the anchor guard — log and persist once, then stay silent while DEGRADED holds.
circuit_breaker_events context payload should include:
- condition_triggered: "A", "B", or "C"
- For A: burst_count, burst_window_seconds, fill_sides_in_window
- For B: net_notional_rlusd, threshold, window_seconds
- For C: ticks_since_opposing_fill, threshold_ticks
Console WARNING format:
Test Requirements¶
Minimum 10 tests for this branch:
- Guard inactive before any fills occur
- Condition A fires on ≥3 same-side fills within window
- Condition A does NOT fire if fills are spread beyond window
- Condition A does NOT fire on alternating sides
- Condition B fires on net notional exceeding threshold
- Condition B does NOT fire if imbalance stays below threshold
- Condition C fires after N ticks with no opposing fill (after first fill)
- Condition C does NOT fire before minimum fills threshold
- Trigger enters DEGRADED, clears intents, one-shot dedup
enabled: falsedisables all three conditions- (Bonus)
circuit_breaker_eventsrow written with correct condition label and session_id - (Bonus) Windows teardown fix — use
addCleanuppattern from C5a for any integration tests using realStateManager
Commit Plan (suggested)¶
feat(config): DirectionalDriftGuardConfig dataclass + YAML defaultsfeat(main_loop): drift guard state initialization + fill tracking (Conditions A/B/C)feat(main_loop): drift guard evaluation + DEGRADED triggerfeat(db): drift guard circuit_breaker_events persistence + WARNING logtest(guard): directional drift guard — 10+ tests
Constraints¶
- OR logic — any single condition triggers; do not require all three
- Do not modify the momentum filter — these are separate layers
- Parameters configurable in YAML — no hardcoded thresholds
- Use existing
_enter_degraded_modeandrecord_circuit_breaker_eventinfrastructure - Windows teardown fix required in any integration tests using real
StateManager(useaddCleanuppattern from C5a) - 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