Files
momentum-system/runtime/halt_guard.py
momentum devandClaude Opus 4.8 87186be949 Entry guards: harden after adversarial review
- Reject a non-positive last_price ("bad_price") instead of `last_price or
  trigger` coercing 0.0 -> trigger, which silently skipped the extension guard
  and sized on a phantom price. (latent: fires() guarantees price>=trigger live,
  but submit_breakout_now is public.)
- Extract the halt-gap decision to runtime/halt_guard.is_halt_gap() — pure and
  unit-tested (the riskiest heuristic had no coverage). Encodes the fail-safe:
  None ages (query glitch / session-date mismatch) -> inert, not mass-block;
  global-health gate -> a feed-wide lag can't flag the whole book. 7 cases.
- Honest rationale on the extension ceiling: it measures distance above the ORB
  trigger (catches the intraday chase / gap-through / halt re-open); overnight
  +100% gappers are already excluded upstream by the trigger book's gap_max. It
  is not a day-extension metric (a fuller day-move guard is a possible follow-up).
- Fix stale test docstring (15% not 6%); add bad_price test.

tests/unit: 154 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 07:31:39 -04:00

35 lines
1.5 KiB
Python

"""Halt detection for the entry guard — pure decision, separately testable.
A real LULD trading halt isn't directly readable on the paper/IEX feed, so we
infer it from a gap in a symbol's minute bars: during RTH a liquid gapper that
goes silent for minutes while the rest of the book keeps printing is almost
certainly halted (you can't exit during a halt, and it gaps through the stop on
resume). The bar-age math lives in the loop (it queries the research DB); this
module is just the decision so its edge cases are unit-tested.
"""
from __future__ import annotations
def is_halt_gap(
sym_age_s: float | None,
global_age_s: float | None,
gap_seconds: float,
) -> bool:
"""True iff this symbol looks halted: its freshest bar is older than
``gap_seconds`` WHILE ingestion is globally healthy (the freshest bar across
ALL names is within ``gap_seconds``). The global-health gate is what stops a
feed-wide lag from flagging the entire book at once.
``None`` ages (no data) deliberately return False — NOT halted. This fails
SAFE: a query error or a session-date mismatch makes the guard inert (entries
proceed under the other gates) rather than mass-blocking every entry. The
cost of the rare missed halt is bounded by the extension ceiling, which still
blocks the gap-through on resume.
"""
if gap_seconds <= 0:
return False
if sym_age_s is None or sym_age_s <= gap_seconds:
return False
return global_age_s is not None and global_age_s <= gap_seconds