Files
momentum-system/tests/unit/test_population_gate.py
T
momentum devandClaude Opus 5.5 0c8a27e23f feat: switch to turn off the >=150% gap squeeze exception (default on)
TRADING_GAP_SQUEEZE_ENABLED=0 rejects >=150% gappers in the population gate
(watcher pre-filter and entry gate) and zeroes the trigger book's squeeze
arm. One switch covers all three paths.

Why: while testing the trader's "trade the leading gainers" idea on 1,093
live-band v1 ORB breaks (Jun-Sep 2026, current exits), top-3 gainers at the
break did WORSE (diff -0.48R Jun-Jul, -0.40R Aug-Sep, 90% CIs excluding 0).
That turned out to be almost entirely the >=150% squeeze-exception names:

  gap 10-35%   Jun-Jul +0.27R (n=342)   Aug-Sep +0.13R (n=327)
  gap >=150%   Jun-Jul -0.21R (n=65)    Aug-Sep -0.30R (n=53)

Our own squeeze fills: -0.31R / -0.33R realized (n=16). By the time a squeeze
name breaks its opening range the move has happened; the trader catches these
names premarket, which the bot does not. This finding was post-hoc (not the
pre-registered direction), but it held in both halves and in the real fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
2026-09-28 09:45:51 -05:00

91 lines
2.9 KiB
Python

"""Characterization tests pinning the CURRENT behaviour of the live population
gate (``TradingExecutionService._population_reject``) BEFORE it is extracted to
``strategy/population.py`` (plan 004).
CONTRACT: ``gap_pct`` is a FRACTION (0.12 == 12%). These tests must stay green
byte-for-byte across the extraction — a change in any outcome means the move
altered live behaviour, which the plan forbids (it MOVES code, it does not
retune).
"""
from trading_execution import ExecutionSettings, TradingExecutionService
# production-shaped settings (deploy/docker/truenas-compose.yml env block)
PROD = dict(
min_entry_gap_pct=0.10,
min_day_volume=300_000,
price_band_min=2.0,
price_band_max=20.0,
)
def _gate(**overrides):
"""Return ``_population_reject`` bound to production-shaped settings.
Built the way the existing execution tests do it (``__new__`` + set
``.settings``) so nothing else in the service needs constructing.
"""
svc = TradingExecutionService.__new__(TradingExecutionService)
svc.settings = ExecutionSettings(**{**PROD, **overrides})
def call(gap_pct=0.12, cum_volume=1_000_000, price=10.0, rvol=None):
return svc._population_reject(gap_pct, cum_volume, price=price, rvol=rvol)
return call
def test_in_band_passes():
# $2-20, 12% open gap, healthy volume => tradeable
assert _gate()(gap_pct=0.12, price=10.0, cum_volume=1_000_000) is None
def test_below_gap_rejects():
assert _gate()(gap_pct=0.05) is not None
def test_dead_band_rejects():
assert _gate()(gap_pct=0.40) is not None
def test_squeeze_exception_passes():
# >=150% gap re-admitted ONLY with a verified squeeze (rvol >= 5)
assert _gate()(gap_pct=1.60, rvol=6.0) is None
def test_squeeze_rvol_fail_rejects():
assert _gate()(gap_pct=1.60, rvol=3.0) is not None
def test_price_band_low_rejects():
assert _gate()(price=1.50) is not None
def test_price_band_high_rejects():
assert _gate()(price=21.5) is not None
def test_volume_floor_rejects():
assert _gate()(cum_volume=100_000) is not None
def test_missing_data_fail_open():
# None gap / None volume / None price => never blocks
assert _gate()(gap_pct=None, cum_volume=None, price=None) is None
def test_gap_is_a_fraction_not_percent():
# 0.40 (== 40%) lands in the 35-150% dead band. A caller that wrongly passed
# 40.0 (percent) would trip the >=150% squeeze branch instead — a DIFFERENT
# reason. The distinct reason strings pin the fraction contract.
assert "dead band" in _gate()(gap_pct=0.40)
assert "squeeze" in _gate()(gap_pct=40.0)
def test_gap_gate_disabled_when_min_zero():
# The whole gap logic is nested under ``min_entry_gap_pct > 0``.
g = _gate(min_entry_gap_pct=0.0)
assert g(gap_pct=0.05) is None # below-floor not checked
assert g(gap_pct=0.40) is None # dead band not checked
assert g(gap_pct=1.60, rvol=1.0) is None # squeeze not checked