Population gate audit: static overnight gap everywhere + full band

The 2026-07-08 audit (owner: "figure out if there is some kind of logic
or audit issue") found BOTH entry paths feeding the population gate the
WRONG gap. The fast path used the trigger book's dynamic gap (latest
price vs prev close), so intraday rallies masqueraded as gappers: TVRD
(6.0% true gap), SKIN (2.2%, filled at $0.98), JLHL (4.3%) all traded
outside the validated population. The slow path used max(gap, intraday)
with "open" taken from the first lookback-window bar, and had no upper
band at all — dead-band names SEER (38.6%) and IOTR (40.7%) entered.

Fixes: candidates carry gap_open_pct (session open vs prev close);
trigger eligibility + submit_breakout_now use it exclusively once the
session is open; _population_reject now implements the full validated
band (10-35% OR volume-verified >=150% squeeze, $1-20 price band) for
both paths; evaluator gap uses the session open; signals carry
relative_volume for squeeze verification. Plus momentum-testpg.service
so the test-isolation Postgres survives reboots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-07-08 10:34:46 -04:00
1 parent 2e5346e035
commit 44ff36a669
8 files changed
+103 -13

No files matched your search

+21
View File
@@ -834,3 +834,24 @@ def test_breakout_now_respects_dilution_veto(store):
service = _service_with_catalyst(store, advisory, veto_enabled=True)
res = service.submit_breakout_now("DILUT", trigger=14.0, stop=13.45, last_price=14.0)
assert res["ok"] is False and res["skipped"] == "dilution_veto"
def test_population_gate_full_band_semantics():
"""2026-07-08 audit: the gate must implement the VALIDATED population — static
overnight gap in [10%, 35%] OR volume-verified squeeze >=150%; $1-20 price band.
TVRD (6%), SKIN (2.2%, $0.98), JLHL (4.3%) traded outside it; SEER (38.6%) and
IOTR (40.7%) came from the dead band."""
from trading_execution import ExecutionSettings, TradingExecutionService
svc = TradingExecutionService.__new__(TradingExecutionService)
svc.settings = ExecutionSettings(min_entry_gap_pct=0.10, min_day_volume=300_000)
ok = lambda **kw: svc._population_reject(**{"gap_pct": None, "cum_volume": 500_000, **kw})
assert ok(gap_pct=0.06) is not None # TVRD: below floor
assert ok(gap_pct=0.20) is None # core band trades
assert ok(gap_pct=0.40) is not None # SEER/IOTR: dead band
assert ok(gap_pct=2.78, rvol=40.0) is None # LHSW: verified squeeze
assert ok(gap_pct=2.78, rvol=0.4) is not None # split fake: rejected
assert ok(gap_pct=2.78) is not None # unverified squeeze: rejected
assert ok(gap_pct=0.20, price=0.98) is not None # SKIN: below $1
assert ok(gap_pct=0.20, price=21.5) is not None # above $20
assert ok(gap_pct=0.20, price=5.0) is None
assert ok(gap_pct=None) is None # fail-open on missing gap
+15
View File
@@ -386,3 +386,18 @@ def test_squeeze_exception_off_by_default():
b = ArmedTriggerBook(gap_min=10, gap_max=35, rvol_min=2)
b.arm([_cand("LHSW", gap=278.0, rvol=40.0)])
assert b.triggers["LHSW"].state == WEAK
def test_trigger_eligibility_uses_static_open_gap_not_dynamic():
"""2026-07-08 audit: TVRD's true overnight gap was 6% but its dynamic gap crossed
10% on an intraday rally and it traded as a 'gapper'. Once the session open is
known, eligibility must use the STATIC gap."""
b = ArmedTriggerBook(gap_min=10, gap_max=35, rvol_min=2)
c = _cand("TVRD", gap=13.2, rvol=6.0) # dynamic says 13.2%...
c["gap_open_pct"] = 6.0 # ...but the true overnight gap is 6%
b.arm([c])
assert b.triggers["TVRD"].state == WEAK
c2 = _cand("BABU", gap=25.0, rvol=6.0)
c2["gap_open_pct"] = 19.0 # true gapper stays armed
b.arm([c2])
assert b.triggers["BABU"].state == ARMED