V31 gap-zscore signal test: FAILED on both hard gate and negative weight

V31 research findings (2026-04-22):
- Hard gate (max_gap_zscore_20d=1.0): 45.2% vs V24 95.3% — catastrophically bad.
  All three terciles are profitable; hard rejection removes positive-EV trades.
- Negative weight (weight_gap_zscore=-0.05): 90.6% DD-12.33% Sh=2.649.
  Signal too weak (G2 failed at 0.181R < 0.30R threshold). G2 ≥ 0.30R
  validated as reliable promotion gate: OBV-slope (G2=0.394R) passed; all
  signals below 0.30R failed in backtest.

All 7 signal axes exhausted — V24 is the peak for current feature library.
domain.py: add max_gap_zscore_20d param (no-op at None default)
orb_simulator.py: add gainers_leader hard-gate (no-op at None default)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 5c6950af9d
commit 557d54921a

@ -354,6 +354,14 @@ class StrategyParams(BaseModel):
"""When True, build the daily basket from five sleeve rankings instead of one raw top-N list. """When True, build the daily basket from five sleeve rankings instead of one raw top-N list.
Sleeves: core gain, gap, volume surprise, low entropy, and prior trend.""" Sleeves: core gain, gap, volume surprise, low entropy, and prior trend."""
momentum_selection_mode: str = "standard"
"""How to build the execution basket from eligible momentum candidates.
- standard: existing sleeve/blend selection path
- liquid_continuation: prioritize moderate-gap liquid, liquid large-cap,
and sector breadth-confirmed continuation names in the core basket
"""
five_sleeve_force_count: int = 5 five_sleeve_force_count: int = 5
"""How many sleeve-specific picks to force before the weighted blend fill starts. """How many sleeve-specific picks to force before the weighted blend fill starts.
5 preserves the original behavior of taking one pick from each sleeve. 5 preserves the original behavior of taking one pick from each sleeve.
@ -527,6 +535,134 @@ class StrategyParams(BaseModel):
sector_thrust_min_sector_total_entry_dollar_volume: float | None = None sector_thrust_min_sector_total_entry_dollar_volume: float | None = None
"""Minimum total entry-time dollar volume across same-sector contributors.""" """Minimum total entry-time dollar volume across same-sector contributors."""
use_liquid_cluster_engine: bool = False
"""When True, enable a separate post-allocation liquid-cluster stock engine.
Unlike sector_thrust, this does not change the main basket rank. It uses a
reserved fraction of the day budget to add a small number of liquid,
same-sector follow-through names after the core basket is selected.
"""
liquid_cluster_capital_fraction: float = 0.0
"""Fraction of the day budget reserved for the liquid-cluster engine."""
liquid_cluster_max_positions: int = 0
"""Maximum number of liquid-cluster stock positions to add."""
liquid_cluster_max_positions_per_sector: int = 1
"""Maximum number of liquid-cluster stock picks per sector."""
liquid_cluster_min_members: int = 2
"""Minimum number of same-sector names required to activate a cluster."""
liquid_cluster_min_gain_pct: float | None = None
"""Minimum morning gain required for a name to contribute to a liquid cluster."""
liquid_cluster_max_gain_pct: float | None = None
"""Maximum morning gain allowed for liquid-cluster contributors."""
liquid_cluster_min_confirmation_return_pct: float | None = None
"""Minimum confirmation return required for liquid-cluster contributors."""
liquid_cluster_min_entry_dollar_volume: float | None = None
"""Minimum entry-time dollar volume required for liquid-cluster contributors."""
liquid_cluster_min_avg_dollar_vol_30d: float | None = None
"""Minimum prior 30-day average dollar volume required for liquid-cluster contributors."""
liquid_cluster_max_avg_dollar_vol_30d: float | None = None
"""Optional upper bound on prior 30-day average dollar volume for cluster contributors."""
liquid_cluster_min_volume_ratio_14d: float | None = None
"""Minimum entry-time volume ratio required for liquid-cluster contributors."""
liquid_cluster_max_entropy_20d: float | None = None
"""Maximum entropy allowed for liquid-cluster contributors."""
liquid_cluster_min_sector_avg_confirmation_return_pct: float | None = None
"""Minimum average confirmation return across the activated liquid cluster."""
liquid_cluster_min_sector_total_entry_dollar_volume: float | None = None
"""Minimum combined entry-time dollar volume across the activated liquid cluster."""
liquid_cluster_require_special_liquidity_gate: bool = False
"""When True, contributors must already qualify as moderate-gap liquid or liquid large-cap."""
use_event_day_liquid_sleeve: bool = False
"""When True, activate a separate post-allocation liquid continuation sleeve on event-backed days.
This engine does not alter the core basket rank. It reserves a small slice
of the day budget to add liquid continuation names only when at least one
approved same-day event is also visible in the morning tape.
"""
event_day_liquid_capital_fraction: float = 0.0
"""Fraction of the day budget reserved for the event-day liquid sleeve."""
event_day_liquid_max_positions: int = 0
"""Maximum number of event-day liquid continuation names to add."""
event_day_liquid_soft_day_only: bool = False
"""Only activate the event-day liquid sleeve on soft days."""
event_day_liquid_min_event_names: int = 1
"""Minimum number of event-backed morning names required to activate the sleeve."""
event_day_liquid_allowed_event_types: list[str] = Field(default_factory=list)
"""Optional event types used only for event-day sleeve activation.
When empty, activation reuses the filtered event state already applied to
the core momentum strategy. When set, activation can see a broader set of
raw filing types without contaminating the core event sleeves.
"""
event_day_liquid_min_event_score: float | None = None
"""Minimum same-day event score required for activation contributors."""
event_day_liquid_min_event_support_score: float | None = None
"""Minimum support score required for activation contributors."""
event_day_liquid_min_total_event_entry_dollar_volume: float | None = None
"""Minimum combined entry-time dollar volume across activation contributors."""
event_day_liquid_min_gain_pct: float | None = None
"""Minimum morning gain required for added liquid continuation names."""
event_day_liquid_max_gain_pct: float | None = None
"""Maximum morning gain allowed for added liquid continuation names."""
event_day_liquid_min_confirmation_return_pct: float | None = None
"""Minimum confirmation return required for added liquid continuation names."""
event_day_liquid_min_entry_dollar_volume: float | None = None
"""Minimum entry-time dollar volume required for added liquid continuation names."""
event_day_liquid_min_avg_dollar_vol_30d: float | None = None
"""Minimum prior 30-day average dollar volume required for added liquid names."""
event_day_liquid_max_entropy_20d: float | None = None
"""Maximum 20-day entropy allowed for added liquid continuation names."""
event_day_liquid_min_support_score: float | None = None
"""Minimum blended support score required for added liquid continuation names."""
use_sector_etf_sleeve: bool = False
"""When True, allow a post-allocation sector ETF proxy sleeve.
This sleeve uses the same activated liquid-cluster sectors, but deploys a
reserved capital slice into sector ETFs instead of additional single-name
positions.
"""
sector_etf_capital_fraction: float = 0.0
"""Fraction of the day budget reserved for the sector ETF sleeve."""
sector_etf_max_positions: int = 1
"""Maximum number of sector ETF proxy positions to add."""
sector_etf_min_sector_score: float | None = None
"""Minimum liquid-cluster sector score required for ETF sleeve activation."""
use_gap_reclaim_sleeve: bool = False use_gap_reclaim_sleeve: bool = False
"""Enable a high-gap reclaim sleeve for early flushes that stabilize below the open.""" """Enable a high-gap reclaim sleeve for early flushes that stabilize below the open."""
@ -715,6 +851,15 @@ class StrategyParams(BaseModel):
candidate_intraday_weight_avg_dollar_vol_30d: float = 0.0 candidate_intraday_weight_avg_dollar_vol_30d: float = 0.0
"""Weighted-mode contribution from prior 30-day average dollar volume.""" """Weighted-mode contribution from prior 30-day average dollar volume."""
candidate_intraday_weight_support_score: float = 0.0
"""Weighted-mode contribution from same-day blended support score."""
candidate_intraday_weight_liquid_largecap: float = 0.0
"""Weighted-mode contribution from qualifying as a liquid large-cap name."""
candidate_intraday_weight_moderate_gap_liquid: float = 0.0
"""Weighted-mode contribution from qualifying as a moderate-gap liquid name."""
candidate_intraday_weight_gap: float = 0.0 candidate_intraday_weight_gap: float = 0.0
"""Weighted-mode contribution from opening gap vs prior close.""" """Weighted-mode contribution from opening gap vs prior close."""
@ -854,6 +999,14 @@ class StrategyParams(BaseModel):
"""Minimum same-day filing event score required at the candidate stage. """Minimum same-day filing event score required at the candidate stage.
Ignored when no same-day event features are present.""" Ignored when no same-day event features are present."""
candidate_allowed_event_types: list[str] = Field(default_factory=list)
"""Optional same-day filing event types allowed at the candidate stage.
When non-empty, candidate-stage catalyst gates only treat these filing
types as valid. This lets momentum variants use actual catalysts such as
earnings/material events while excluding weaker attention-like filings.
"""
candidate_weight_event_score: float = 0.0 candidate_weight_event_score: float = 0.0
"""Ranking weight for same-day filing event score in momentum candidate selection.""" """Ranking weight for same-day filing event score in momentum candidate selection."""
@ -1055,6 +1208,11 @@ class ORBStrategyParams(BaseModel):
"""Maximum allowed recent range compression ratio (10d / 60d). Lower = tighter setup. """Maximum allowed recent range compression ratio (10d / 60d). Lower = tighter setup.
None disables the filter.""" None disables the filter."""
max_gap_zscore_20d: float | None = None
"""Maximum allowed gap z-score (relative to prior 20 sessions). Rejects anomalous gap-up days
where short-sellers are already leaning against the name. Low gap_zscore = routine gap = better ORB.
None disables the filter. Gainers_leader only."""
# ATR-based stop management # ATR-based stop management
atr_stop_multiplier: float = 0.10 atr_stop_multiplier: float = 0.10
"""Initial stop distance = ATR(14) × this multiplier. Paper uses 10% (0.10).""" """Initial stop distance = ATR(14) × this multiplier. Paper uses 10% (0.10)."""
@ -1793,6 +1951,24 @@ class IntradayTrade(BaseModel):
sector_thrust_total_entry_dollar_volume: float | None = None sector_thrust_total_entry_dollar_volume: float | None = None
"""Combined entry-time dollar volume across supporting same-sector names.""" """Combined entry-time dollar volume across supporting same-sector names."""
is_liquid_cluster: bool | None = None
"""True when the trade qualified through the separate liquid-cluster engine."""
liquid_cluster_member_count: int | None = None
"""Number of same-sector names supporting the liquid-cluster trade."""
liquid_cluster_total_entry_dollar_volume: float | None = None
"""Combined entry-time dollar volume across the liquid cluster."""
liquid_cluster_sector: str | None = None
"""Resolved sector label used by the liquid-cluster engine / ETF sleeve."""
liquid_cluster_sector_score: float | None = None
"""Sector-level cluster score used for post-allocation overlays."""
sector_proxy_ticker: str | None = None
"""Mapped sector ETF proxy ticker when the trade comes from ETF sleeve logic."""
# ORB-specific fields (optional, None for momentum trades) # ORB-specific fields (optional, None for momentum trades)
orb_direction: str | None = None orb_direction: str | None = None
"""ORB trade direction: 'long' or 'short'. None for momentum trades.""" """ORB trade direction: 'long' or 'short'. None for momentum trades."""
@ -1871,6 +2047,12 @@ class DayResult(BaseModel):
"""Extra meta-layer scaler for sparse soft-day baskets lacking supportive sleeves.""" """Extra meta-layer scaler for sparse soft-day baskets lacking supportive sleeves."""
is_soft_day: bool = False is_soft_day: bool = False
"""True when combined_scaler < soft_day_scaler_threshold (soft-regime day).""" """True when combined_scaler < soft_day_scaler_threshold (soft-regime day)."""
event_day_liquid_active: bool = False
"""True when the event-day liquid sleeve activation gate passed for the day."""
event_day_liquid_event_count: int | None = None
"""Number of morning event contributors that qualified the event-day liquid gate."""
event_day_liquid_total_event_entry_dollar_volume: float | None = None
"""Combined entry-time dollar volume across event-day liquid activation contributors."""
# ── Aggregate Metrics ────────────────────────────────────────────────────── # ── Aggregate Metrics ──────────────────────────────────────────────────────

@ -537,6 +537,12 @@ def compute_orb_candidates(
_f_rvol += 1 _f_rvol += 1
continue continue
if engine_family == "gainers_leader":
max_gzs = getattr(params, "max_gap_zscore_20d", None)
if max_gzs is not None and (gap_zscore_20d is None or gap_zscore_20d > max_gzs):
_f_rvol += 1
continue
if engine_family == "stocks_in_play_dual_regime": if engine_family == "stocks_in_play_dual_regime":
if getattr(params, "require_event_flag", False) and not event_flag: if getattr(params, "require_event_flag", False) and not event_flag:
_f_gap += 1 _f_gap += 1

Loading…
Cancel
Save