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
This commit is contained in:
momentum devandClaude Opus 5.5 committed 2026-09-28 09:45:51 -05:00
1 parent efde7c721d
commit 0c8a27e23f
5 files changed
+39 -1

No files matched your search

+5 -1
View File
@@ -483,7 +483,11 @@ def build_runtime(args: argparse.Namespace) -> dict:
max_armed=int(os.environ.get("TRIGGER_MAX_ARMED", "6")),
gap_min=float(os.environ.get("TRIGGER_GAP_MIN", "3.0")),
gap_max=float(os.environ.get("TRIGGER_GAP_MAX", "1e9")),
gap_squeeze_min=float(os.environ.get("TRIGGER_GAP_SQUEEZE_MIN", "0")),
# TRADING_GAP_SQUEEZE_ENABLED=0 turns the >=150% squeeze exception off here too, so
# one switch covers the trigger book, the watcher pre-filter and the entry gate.
gap_squeeze_min=(float(os.environ.get("TRIGGER_GAP_SQUEEZE_MIN", "0"))
if os.environ.get("TRADING_GAP_SQUEEZE_ENABLED", "1").strip().lower()
not in {"0", "false", "no", "off"} else 0.0),
gap_squeeze_rvol=float(os.environ.get("TRIGGER_GAP_SQUEEZE_RVOL", "5.0")),
rvol_min=float(os.environ.get("TRIGGER_RVOL_MIN", "2.0")),
min_range_pct=float(os.environ.get("TRIGGER_MIN_RANGE_PCT", "0.004")),
+11
View File
@@ -38,6 +38,13 @@ class PopulationConfig:
gap_dead_band_lo: float = 0.35
gap_dead_band_hi: float = 1.50
squeeze_rvol_min: float = 5.0
# The >=150% "verified squeeze" exception. Env TRADING_GAP_SQUEEZE_ENABLED (default on).
# 2026-09-28: under the current exits, live-band ORB breaks with a >=150% gap lost in both
# halves (Jun-Jul -0.21R n=65, Aug-Sep -0.30R n=53), while the 10-35% band made +0.27R /
# +0.13R (n=342 / 327). Our own squeeze fills were -0.31R / -0.33R realized (n=16).
# These are the day's top gainers by the time their opening range breaks: the move has
# already happened.
squeeze_enabled: bool = True
@classmethod
def from_env(cls, env: dict | None = None) -> "PopulationConfig":
@@ -60,6 +67,8 @@ class PopulationConfig:
price_max=float(v.get("TRADING_PRICE_MAX", "20.0")),
min_entry_gap_pct=_qfloat(v.get("TRADING_MIN_ENTRY_GAP_PCT", "0.0")),
min_day_volume=float(v.get("TRADING_MIN_DAY_VOLUME", "0.0")),
squeeze_enabled=str(v.get("TRADING_GAP_SQUEEZE_ENABLED", "1")).strip().lower()
not in {"0", "false", "no", "off"},
)
@@ -93,6 +102,8 @@ def population_reject(
# reverse-split fakes show rvol ~0.01).
if cfg.gap_dead_band_lo < g < cfg.gap_dead_band_hi:
return f"gap {g:.0%} in dead band 35-150%"
if g >= cfg.gap_dead_band_hi and not cfg.squeeze_enabled:
return f"gap {g:.0%} above band (squeeze exception off)"
if g >= cfg.gap_dead_band_hi and (rvol is None or float(rvol) < cfg.squeeze_rvol_min):
return f"gap {g:.0%} squeeze unverified (rvol {rvol})"
except (TypeError, ValueError):
+1
View File
@@ -87,3 +87,4 @@ def test_gap_gate_disabled_when_min_zero():
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
+19
View File
@@ -0,0 +1,19 @@
"""TRADING_GAP_SQUEEZE_ENABLED=0 turns the >=150% squeeze exception off everywhere."""
from strategy.population import PopulationConfig, population_reject
from trading_execution import ExecutionSettings
def test_population_config_switch():
on = PopulationConfig.from_env({"TRADING_MIN_ENTRY_GAP_PCT": "0.10"})
off = PopulationConfig.from_env({"TRADING_MIN_ENTRY_GAP_PCT": "0.10", "TRADING_GAP_SQUEEZE_ENABLED": "0"})
assert on.squeeze_enabled and not off.squeeze_enabled
assert population_reject(on, gap_pct=2.0, price=5.0, volume=None, rvol=20.0) is None
reason = population_reject(off, gap_pct=2.0, price=5.0, volume=None, rvol=20.0)
assert reason and "squeeze exception off" in reason
assert population_reject(off, gap_pct=0.20, price=5.0, volume=None, rvol=3.0) is None # core band untouched
def test_execution_settings_carry_the_switch():
assert ExecutionSettings.from_env({}).gap_squeeze_enabled is True
assert ExecutionSettings.from_env({"TRADING_GAP_SQUEEZE_ENABLED": "0"}).gap_squeeze_enabled is False
+3
View File
@@ -122,6 +122,7 @@ class ExecutionSettings:
# 0 = disabled. Fail-open: a signal missing gap/volume data is never blocked.
min_entry_gap_pct: float = 0.0
min_day_volume: float = 0.0
gap_squeeze_enabled: bool = True # TRADING_GAP_SQUEEZE_ENABLED, see strategy/population.py
# entries only during the regular session (ET). SIP prints extended-hours
# trades, so without this gate after-hours price crosses fire real orders
# (TDTH/SUNE resting entries submitted 18:53-20:09 ET on 2026-07-09).
@@ -319,6 +320,7 @@ class ExecutionSettings:
entry_confirm_by_minute=int(values.get("TRADING_ENTRY_CONFIRM_BY_MINUTE", "0")),
min_entry_gap_pct=_qfloat(values.get("TRADING_MIN_ENTRY_GAP_PCT", "0.0")),
min_day_volume=float(values.get("TRADING_MIN_DAY_VOLUME", "0.0")),
gap_squeeze_enabled=flag("TRADING_GAP_SQUEEZE_ENABLED", "1"),
session_only=flag("TRADING_SESSION_ONLY", "1"),
session_start=values.get("TRADING_SESSION_START", "09:30"),
session_end=values.get("TRADING_SESSION_END", "15:55"),
@@ -957,6 +959,7 @@ class TradingExecutionService:
price_max=self.settings.price_band_max,
min_entry_gap_pct=self.settings.min_entry_gap_pct,
min_day_volume=self.settings.min_day_volume,
squeeze_enabled=self.settings.gap_squeeze_enabled,
)
return population_reject(
cfg, gap_pct=gap_pct, price=price, volume=cum_volume, rvol=rvol)