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:
1 parent
efde7c721d
commit
0c8a27e23f
5 files changed
+39
-1
No files matched your search
+5
-1
@@ -483,7 +483,11 @@ def build_runtime(args: argparse.Namespace) -> dict:
|
|||||||
max_armed=int(os.environ.get("TRIGGER_MAX_ARMED", "6")),
|
max_armed=int(os.environ.get("TRIGGER_MAX_ARMED", "6")),
|
||||||
gap_min=float(os.environ.get("TRIGGER_GAP_MIN", "3.0")),
|
gap_min=float(os.environ.get("TRIGGER_GAP_MIN", "3.0")),
|
||||||
gap_max=float(os.environ.get("TRIGGER_GAP_MAX", "1e9")),
|
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")),
|
gap_squeeze_rvol=float(os.environ.get("TRIGGER_GAP_SQUEEZE_RVOL", "5.0")),
|
||||||
rvol_min=float(os.environ.get("TRIGGER_RVOL_MIN", "2.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")),
|
min_range_pct=float(os.environ.get("TRIGGER_MIN_RANGE_PCT", "0.004")),
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ class PopulationConfig:
|
|||||||
gap_dead_band_lo: float = 0.35
|
gap_dead_band_lo: float = 0.35
|
||||||
gap_dead_band_hi: float = 1.50
|
gap_dead_band_hi: float = 1.50
|
||||||
squeeze_rvol_min: float = 5.0
|
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
|
@classmethod
|
||||||
def from_env(cls, env: dict | None = None) -> "PopulationConfig":
|
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")),
|
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_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")),
|
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).
|
# reverse-split fakes show rvol ~0.01).
|
||||||
if cfg.gap_dead_band_lo < g < cfg.gap_dead_band_hi:
|
if cfg.gap_dead_band_lo < g < cfg.gap_dead_band_hi:
|
||||||
return f"gap {g:.0%} in dead band 35-150%"
|
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):
|
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})"
|
return f"gap {g:.0%} squeeze unverified (rvol {rvol})"
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
|
|||||||
@@ -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.05) is None # below-floor not checked
|
||||||
assert g(gap_pct=0.40) is None # dead band 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
|
assert g(gap_pct=1.60, rvol=1.0) is None # squeeze not checked
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -122,6 +122,7 @@ class ExecutionSettings:
|
|||||||
# 0 = disabled. Fail-open: a signal missing gap/volume data is never blocked.
|
# 0 = disabled. Fail-open: a signal missing gap/volume data is never blocked.
|
||||||
min_entry_gap_pct: float = 0.0
|
min_entry_gap_pct: float = 0.0
|
||||||
min_day_volume: 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
|
# entries only during the regular session (ET). SIP prints extended-hours
|
||||||
# trades, so without this gate after-hours price crosses fire real orders
|
# 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).
|
# (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")),
|
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_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")),
|
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_only=flag("TRADING_SESSION_ONLY", "1"),
|
||||||
session_start=values.get("TRADING_SESSION_START", "09:30"),
|
session_start=values.get("TRADING_SESSION_START", "09:30"),
|
||||||
session_end=values.get("TRADING_SESSION_END", "15:55"),
|
session_end=values.get("TRADING_SESSION_END", "15:55"),
|
||||||
@@ -957,6 +959,7 @@ class TradingExecutionService:
|
|||||||
price_max=self.settings.price_band_max,
|
price_max=self.settings.price_band_max,
|
||||||
min_entry_gap_pct=self.settings.min_entry_gap_pct,
|
min_entry_gap_pct=self.settings.min_entry_gap_pct,
|
||||||
min_day_volume=self.settings.min_day_volume,
|
min_day_volume=self.settings.min_day_volume,
|
||||||
|
squeeze_enabled=self.settings.gap_squeeze_enabled,
|
||||||
)
|
)
|
||||||
return population_reject(
|
return population_reject(
|
||||||
cfg, gap_pct=gap_pct, price=price, volume=cum_volume, rvol=rvol)
|
cfg, gap_pct=gap_pct, price=price, volume=cum_volume, rvol=rvol)
|
||||||
|
|||||||
Reference in new issue
Block a user