"""Core domain models for the ACE-F backtester.""" from __future__ import annotations import datetime as dt from enum import Enum from typing import Any from pydantic import BaseModel, ConfigDict, Field class PositionStatus(str, Enum): PLANNED = "PLANNED" ENTERED = "ENTERED" PARTIALLY_EXITED = "PARTIALLY_EXITED" OPEN = "OPEN" EXIT_PENDING = "EXIT_PENDING" CLOSED = "CLOSED" ARCHIVED = "ARCHIVED" class ExitReason(str, Enum): STOP = "STOP" TARGET = "TARGET" TIME = "TIME" DECAY = "DECAY" TRAILING = "TRAILING" KILL_SWITCH = "KILL_SWITCH" END_OF_BACKTEST = "END_OF_BACKTEST" MISSING_BAR = "MISSING_BAR" NO_FOLLOW_THROUGH = "NO_FOLLOW_THROUGH" EARLY_FAILURE = "EARLY_FAILURE" NO_PROGRESS = "NO_PROGRESS" GIVEBACK = "GIVEBACK" RECYCLE = "RECYCLE" ROTATION = "ROTATION" PARKING = "PARKING" DIVIDEND_CAPTURE = "DIVIDEND_CAPTURE" class BacktestMode(str, Enum): RESEARCH = "research" LIVE = "live" class LookaheadViolationError(RuntimeError): """Raised when a candidate's feature timestamp is not strictly before the decision date. The decision date is the trading day on which the candidate is *built* (T-1 close), so any feature timestamp >= decision_date 09:30 ET indicates information leakage from inside or after the entry day. """ class Candidate(BaseModel): """An eligible trade candidate derived from a Parquet snapshot row.""" model_config = ConfigDict(frozen=True) event_id: str symbol: str source_symbol: str | None = None issuer_id: str | None = None score: float sector: str # "UNKNOWN" if unavailable event_type: str event_timestamp: dt.datetime # must be timezone-aware event_date: dt.date | None = None filing_time_bucket: str timing_class: str = "unknown" # "same_day", "after_close", "unknown" reaction_date: dt.date execution_date: dt.date # mapped from Parquet entry_date at SnapshotStore boundary entry_price_est: float # reaction-day close price avg_dollar_volume: float # 20-day mean(volume * close) atr_14: float | None = None score_bucket: str engine_id: str = "default" entry_timing_policy: str = "next_open" shadow_only: bool = False engine_min_entry_price: float | None = None engine_max_entry_price: float | None = None engine_max_holding_days: int | None = None engine_max_positions_per_sector: int | None = None engine_max_position_value_pct: float | None = None engine_max_adv_fraction: float | None = None engine_risk_budget_pct: float = 1.0 engine_capital_bucket_id: str | None = None engine_capital_bucket_allocation_pct: float | None = None engine_per_trade_risk_pct: float | None = None engine_macro_vix_size_scaler_low: float | None = None engine_macro_vix_size_scaler_high: float | None = None engine_macro_vix_size_scaler_min: float | None = None engine_macro_hy_spread_size_scaler_low: float | None = None engine_macro_hy_spread_size_scaler_high: float | None = None engine_macro_hy_spread_size_scaler_min: float | None = None engine_score_size_scaler_low: float | None = None engine_score_size_scaler_high: float | None = None engine_score_size_scaler_min: float | None = None engine_entropy_size_scaler_low: float | None = None engine_entropy_size_scaler_high: float | None = None engine_entropy_size_scaler_min: float | None = None engine_target_atr_multiplier: float | None = None engine_stop_atr_multiplier: float | None = None engine_target_1_r: float | None = None engine_target_1_fraction: float | None = None engine_trailing_model: str | None = None engine_trailing_warmup_days: int | None = None engine_use_reaction_day_low_stop: bool | None = None engine_early_failure_close_below_entry_and_reaction_close: bool | None = None engine_early_failure_no_progress_days: int | None = None engine_early_failure_no_progress_r: float | None = None engine_early_failure_no_progress_fraction: float | None = None engine_dynamic_hold_checkpoints: list[list[float]] | None = None engine_dynamic_hold_extend_day: int | None = None engine_dynamic_hold_extend_r: float | None = None engine_dynamic_hold_extend_to: int | None = None engine_veto_oneoff_penalty: float | None = None engine_allow_oneoff_downsizing: bool | None = None engine_oneoff_downsize_floor: float | None = None engine_veto_parse_confidence_min: float | None = None engine_allow_unknown_direction: bool | None = None engine_next_open_gap_cap_pct: float | None = None engine_add_on_max_count: int | None = None engine_add_on_size_fraction: float | None = None trade_symbol_mode: str = "event" # "event", "sector_etf", "peer_proxy" trade_direction: str = "long" # "long" or "short" engine_forced_trade_direction: str | None = None # explicit engine override, e.g. contrarian long on bearish parent_position_id: str | None = None is_add_on: bool = False forced_shares: int | None = None features: dict[str, Any] = Field(default_factory=dict) class PlannedOrder(BaseModel): """A sized, gated order plan for a candidate.""" model_config = ConfigDict(frozen=True) candidate: Candidate shares: int entry_price_limit: float stop_price: float target_price: float risk_dollars: float event_date: dt.date | None = None timing_class: str = "unknown" engine_id: str = "default" entry_timing_policy: str = "next_open" shadow_only: bool = False parent_position_id: str | None = None is_add_on: bool = False skip_reason: str | None = None # non-None means the order was rejected class FilledTrade(BaseModel): """A completed (closed) trade leg.""" model_config = ConfigDict(frozen=True) trade_id: str position_id: str event_id: str symbol: str source_symbol: str | None = None event_date: dt.date | None = None event_type: str = "" score: float = 0.0 timing_class: str = "unknown" engine_id: str = "default" entry_timing_policy: str = "next_open" shadow_only: bool = False parent_position_id: str | None = None is_add_on: bool = False trade_symbol_mode: str = "event" entry_date: dt.date exit_date: dt.date entry_price: float exit_price: float exit_reason: ExitReason shares: int commission: float slippage_bps: float gross_pnl: float net_pnl: float pnl_pct: float r_multiple: float holding_days: int class OpenPosition(BaseModel): """A live open position (mutable throughout its lifetime).""" position_id: str plan: PlannedOrder entry_date: dt.date entry_price: float entry_fill_slippage_bps: float current_stop: float target_price: float peak_price: float shares_open: int shares_total: int parent_position_id: str | None = None is_add_on: bool = False days_held: int = 0 status: PositionStatus = PositionStatus.ENTERED partial_fills: list[FilledTrade] = Field(default_factory=list) class DailyPortfolioState(BaseModel): """Immutable snapshot of portfolio state at end of a trading day.""" model_config = ConfigDict(frozen=True) date: dt.date equity: float sizing_equity: float | None = None # equity used for position sizing; defaults to equity when None cash_available: float gross_exposure: float net_exposure: float reserved_risk_budget: float unrealized_pnl: float realized_pnl: float open_positions: list[str] = Field(default_factory=list) # position_ids daily_new_risk_used: float peak_equity: float current_drawdown_pct: float # Idle capital decomposition (optional; None when not instrumented) raw_cash: float | None = None # self._cash (actual uninvested cash) parking_value: float | None = None # market value of parked ETF positions idle_alpha_exposure: float | None = None # notional in idle-alpha-sleeve positions primary_exposure: float | None = None # notional in primary engine positions class MetricsBundle(BaseModel): """21 performance metrics for a completed backtest run.""" # Trade metrics (7) trade_count: int = 0 win_rate: float | None = None avg_win_pct: float | None = None avg_loss_pct: float | None = None profit_factor: float | None = None expectancy_r: float | None = None avg_r_multiple: float | None = None # Portfolio metrics (8) total_return_pct: float | None = None annualized_return_pct: float | None = None max_drawdown_pct: float | None = None calmar_ratio: float | None = None sharpe_ratio: float | None = None sortino_ratio: float | None = None avg_daily_pnl: float | None = None avg_positions_held: float | None = None avg_gross_exposure_pct: float | None = None avg_net_exposure_pct: float | None = None days_in_market_pct: float | None = None # Stability metrics (4) trade_skewness: float | None = None trade_kurtosis: float | None = None monthly_win_rate: float | None = None equity_curve_r_squared: float | None = None # Practicality metrics (4) avg_holding_days: float | None = None stop_exit_rate: float | None = None target_exit_rate: float | None = None no_follow_through_exit_rate: float | None = None score_bucket_hit_rate: dict[str, float] = Field(default_factory=dict) qqq_benchmark_return_pct: float | None = None excess_vs_qqq_pct: float | None = None long_net_pnl: float | None = None short_net_pnl: float | None = None long_pnl_contribution_pct: float | None = None short_pnl_contribution_pct: float | None = None # Simple (non-compounding) return simple_return_pct: float | None = None # sum(net_pnl) / initial_equity * 100 initial_equity: float | None = None # starting capital used for simple return calc # Bootstrap confidence intervals (95%) bootstrap_cis: dict[str, tuple[float, float] | None] = Field(default_factory=dict) # Idle capital decomposition (populated only when raw_cash/parking_value tracked) avg_idle_fraction_pct: float | None = None # mean((raw_cash + parking_value) / equity) avg_primary_utilization_pct: float | None = None # mean(primary_exposure / equity) avg_ia_utilization_pct: float | None = None # mean(idle_alpha_exposure / equity) # --------------------------------------------------------------------------- # Config models (mirror JSON Schema) # --------------------------------------------------------------------------- class UniverseConfig(BaseModel): min_price: float = 5.0 min_avg_dollar_volume: float = 1_000_000.0 min_market_cap_proxy: float | None = None exclude_asset_types: list[str] = Field(default_factory=list) allowed_exchanges: list[str] | None = None class SignalConfig(BaseModel): score_threshold: float = 0.5 max_candidates_per_day: int = 5 execution_timing: str = "next_open" decision_timing: str = "reaction_close" ranking_fields: list[str] = Field(default_factory=list) ranking_model_path: str | None = None scoring_model: str = "default" # "default" or "pead" pead_reaction_threshold: float = 0.05 pead_volume_threshold: float = 1.5 a_tier_score_threshold: float | None = None prior_drift_min: float | None = None pre_event_momentum_20d_max: float | None = None # reject if 20d pre-event return > this # --------------------------------------------------------------------------- # Named cash-parking presets — referenced by RiskConfig.apply_parking_preset() # --------------------------------------------------------------------------- PARKING_PRESETS: dict[str, dict] = { # ── Volatility Only (aggressive) ──────────────────────────────────────── "vol_20_24": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, }, "vol_20_25": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.25, }, "vol_30_24": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 30, "cash_parking_gate_vol_threshold": 0.24, }, # ── Vol + Momentum (recommended) ──────────────────────────────────────── "vm_24_m20": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, }, "vm_25_m20": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.25, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, }, "vm_24_m20_r1": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.01, }, "vm_24_m10": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 10, "cash_parking_trend_reentry_pct": 0.02, }, # ── Entropy (lowest DD) ───────────────────────────────────────────────── "ve_10_10": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_entropy_lookback": 10, "cash_parking_entropy_threshold": 1.0, }, "ve_10_12": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_entropy_lookback": 10, "cash_parking_entropy_threshold": 1.2, }, "vme_24_e14": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.4, }, # ── VRP — Volatility Risk Premium ──────────────────────────────────────── "vv_24_vrp8": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_vrp_threshold": 8.0, }, "vmv_24_m20_vrp8": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, "cash_parking_vrp_threshold": 8.0, }, # ── Temperature — Vol Acceleration ────────────────────────────────────── "vt_24_t13": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_temperature_threshold": 1.3, }, "vte_24_t12_e12_m20": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 10, "cash_parking_entropy_threshold": 1.2, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, }, # ── Hurst Exponent ─────────────────────────────────────────────────────── "vh_24_h50": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_hurst_threshold": 0.50, }, "vmh_24_m20_h50": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, "cash_parking_hurst_threshold": 0.50, }, # ── Multi-Signal ───────────────────────────────────────────────────────── "vmeh_24_e14_h50": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.02, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.4, "cash_parking_hurst_threshold": 0.50, }, "composite_v1": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 20, }, "composite_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 35, "cash_parking_composite_enter_score": 18, }, # ── Drawdown / Combo ───────────────────────────────────────────────────── "dd100_8": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "drawdown", "cash_parking_gate_drawdown_lookback": 100, "cash_parking_gate_drawdown_pct": 0.08, }, "dd100_10": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "drawdown", "cash_parking_gate_drawdown_lookback": 100, "cash_parking_gate_drawdown_pct": 0.10, }, "vd_24_dd10": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "vol_dd", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.24, "cash_parking_gate_drawdown_lookback": 100, "cash_parking_gate_drawdown_pct": 0.10, }, # ── Simple / Baseline ──────────────────────────────────────────────────── "sgov": { "cash_parking_enabled": True, "cash_parking_symbol": "sgov", }, "qqq_no_gate": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.99, # effectively never gates to SGOV }, "qqqm_low_dd": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.275, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, }, "qqqm_low_dd_risk25": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.275, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_topup_risk_score_max": 25.0, "cash_parking_reserve_pct": 0.0, }, "qqqm_low_dd_gld": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.275, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, }, "qqqm_low_dd_tqqq_calm": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.275, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.17, "cash_parking_low_vol_overlay_temperature_max": 0.92, "cash_parking_low_vol_overlay_entropy_max": 1.15, }, "qqqm_low_dd_tqqq_calm_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.0, "cash_parking_low_vol_overlay_entropy_max": 1.45, }, "qqqm_low_dd_tqqq_calm_v2_gld": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.0, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, }, "qqqm_low_dd_tqqq_active": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.275, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.30, }, "qqqm_low_dd_tqqq_active_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, }, "qqqm_low_dd_tqqq_active_v2_gld": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, }, "qqqm_low_dd_tqqq_active_v2_gld_brake_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, # Shock brake: Signal 4 (near-SMA buffer) ONLY — Signals 1-3 disabled # Signals 1-3 (rv_ratio, dd5_pct, sma_cross) over-trigger 2023-2024, compounding equity loss # Signal 4 correctly fires Dec 11 (SmaGap=0.37%, vol5/vol20=0.284>0.25) without false alarms "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, # Signal 1: effectively disabled "cash_parking_overlay_shock_brake_dd5_pct": 1.0, # Signal 3: effectively disabled "cash_parking_overlay_shock_brake_sma_cross": False, # Signal 2: disabled "cash_parking_overlay_shock_brake_cooldown_days": 2, # Signal 4: exit TQQQ when QQQ within 0.5% above SMA10 + vol5/vol20 ∈ (0.25, 0.45) # Upper bound 0.45 filters out high-vol days (regular gate handles those) and false alarms. # Targets "barely elevated" pre-crash vol signature; fires Dec 11 (0.284) not May 21 (0.529). # Same-day re-buy is skipped after brake fires; next day gate decides (SGOV on crash days) "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, # Dwell cap: disabled (max_hold=4 caused Sep-26 bottom-exit; default 0 = unlimited) }, # ── Brake v2 + reserve 7% cash (DD headroom via reduced leverage) ──────────── "qqqm_low_dd_tqqq_active_v2_gld_brake_v2_r07": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.07, # keep 7% in cash "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 2, "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Brake v2 + wider TQQQ vol threshold (0.22→0.26): more TQQQ exposure in moderate-vol regimes ── "qqqm_low_dd_tqqq_active_v2_gld_brake_v2_vol026": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.26, # raised from 0.22 → more TQQQ in moderate vol "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 2, "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Brake v3: wider SMA trigger + longer cooldown for DD headroom ──────────── "qqqm_low_dd_tqqq_active_v2_gld_brake_v3": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 4, # v2: 2 "cash_parking_overlay_shock_brake_sma_buffer": 0.010, # v2: 0.005 "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Active v2 with temp=1.00 (slightly tighter TQQQ temperature gate) ──────── "qqqm_low_dd_tqqq_active_v2_gld_brake_v2_temp100": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.22, "cash_parking_low_vol_overlay_temperature_max": 1.00, # v2: 1.05 "cash_parking_low_vol_overlay_entropy_max": 1.45, "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 2, "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Conservative TQQQ + Brake v2 (tighter vol gate for DD headroom) ────────── "qqqm_low_dd_tqqq_conservative_gld_brake_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.20, # v2: 0.22 (tighter) "cash_parking_low_vol_overlay_temperature_max": 1.02, # v2: 1.05 (tighter) "cash_parking_low_vol_overlay_entropy_max": 1.40, # v2: 1.45 (tighter) "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, # Same brake as v2 "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 2, "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Aggressive TQQQ + Brake v2 (relaxed vol/temp/entropy for more TQQQ days) ── "qqqm_low_dd_tqqq_active_v3_gld_brake_v2": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", "cash_parking_gate_mode": "volatility", "cash_parking_gate_vol_lookback": 20, "cash_parking_gate_vol_threshold": 0.35, "cash_parking_temperature_threshold": 1.2, "cash_parking_entropy_lookback": 20, "cash_parking_entropy_threshold": 1.45, "cash_parking_require_trend": True, "cash_parking_trend_mode": "momentum", "cash_parking_trend_sma_period": 20, "cash_parking_trend_reentry_pct": 0.001, "cash_parking_autocorr_threshold": 0.0, "cash_parking_topup_max_peak_drawdown_pct": 0.02, "cash_parking_reserve_pct": 0.0, "cash_parking_low_vol_overlay_symbol": "tqqq", "cash_parking_low_vol_overlay_vol_threshold": 0.25, # v2: 0.22 "cash_parking_low_vol_overlay_temperature_max": 1.12, # v2: 1.05 "cash_parking_low_vol_overlay_entropy_max": 1.50, # v2: 1.45 "cash_parking_defensive_symbol": "gld", "cash_parking_defensive_relay_enabled": True, "cash_parking_defensive_relay_trigger_mode": "always", "cash_parking_defensive_relay_risk_score_max": 100.0, "cash_parking_defensive_momentum_min": 0.05, # Same brake as v2 "cash_parking_overlay_shock_brake_enabled": True, "cash_parking_overlay_shock_brake_rv_ratio": 99.0, "cash_parking_overlay_shock_brake_dd5_pct": 1.0, "cash_parking_overlay_shock_brake_sma_cross": False, "cash_parking_overlay_shock_brake_cooldown_days": 2, "cash_parking_overlay_shock_brake_sma_buffer": 0.005, "cash_parking_overlay_shock_brake_rv_ratio_upper": 0.45, }, # ── Bearish Parking (SH inverse ETF) ──────────────────────────────────── # Two-stage: normal→QQQ, mild stress→SGOV, deep stress→SH # Uses composite risk score: < exit_score=QQQ, exit_score→SGOV, > bearish_threshold→SH "composite_sh_60": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 20, "cash_parking_bearish_symbol": "sh", "cash_parking_bearish_threshold": 60, }, "composite_sh_65": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 20, "cash_parking_bearish_symbol": "sh", "cash_parking_bearish_threshold": 65, }, "composite_sh_70": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 20, "cash_parking_bearish_symbol": "sh", "cash_parking_bearish_threshold": 70, }, # Option A: strict QQQ re-entry (enter_score=10) — prevents premature QQQ re-entry during bear market rallies "composite_sh_60_strict": { "cash_parking_enabled": True, "cash_parking_symbol": "qqq", "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 10, # much harder to re-enter QQQ "cash_parking_bearish_symbol": "sh", "cash_parking_bearish_threshold": 60, }, # Option B: SGOV+SH only — no QQQ parking; SGOV when safe, SH when deep stress "sgov_sh_60": { "cash_parking_enabled": True, "cash_parking_symbol": "sgov", # base is always SGOV, never QQQ "cash_parking_gate_mode": "composite", "cash_parking_composite_exit_score": 40, "cash_parking_composite_enter_score": 20, "cash_parking_bearish_symbol": "sh", "cash_parking_bearish_threshold": 60, }, # ── Multi-tier Regime Parking ──────────────────────────────────────────── # VIX-driven 3-tier rotation: Risk-On→JEPQ, Neutral→QQQM, Risk-Off→SGOV # Requires JEPQ price data in Oracle. JEPQ dividend income not modeled in backtest. "regime_tiered_jepq": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", # default fallback symbol "cash_parking_gate_mode": "regime_tiered", "cash_parking_regime_risk_on_symbol": "jepq", "cash_parking_regime_neutral_symbol": "qqqm", "cash_parking_regime_vix_low_threshold": 20.0, "cash_parking_regime_vix_high_threshold": 25.0, "cash_parking_regime_hysteresis_buffer": 1.0, "cash_parking_reserve_pct": 0.02, "cash_parking_stop_pct": 0.045, "cash_parking_stop_recovery_days": 5, "cash_parking_stop_recovery_pct": 0.02, }, } # --------------------------------------------------------------------------- # Named idle-alpha sleeve presets — referenced by BacktestConfig # --------------------------------------------------------------------------- _MICRO_EVENT_ALPHA_ENGINES: list[dict[str, Any]] = [ { "engine_id": "next_open_long_material_contract_mixed_micro_postmarket", "selection_priority": -1, "event_types": ["material_contract"], "event_directions": ["mixed"], "guidance_statuses": ["not_provided"], "filing_time_buckets": ["post_market"], "timing_class": "after_close", "direction": "long_only", "entry_timing_policy": "next_open", "max_holding_days": 12, "engine_risk_budget_pct": 0.165, "reaction_day_return_min": -0.01, "reaction_day_return_max": 0.04, "close_location_min": 0.6, "close_location_max": 1.0, "gap_size_min": -0.02, "gap_size_max": 0.02, "volume_ratio_min": 0.5, "volume_ratio_max": 1.9, "max_market_cap_proxy": 15000000000.0, "document_quality_score_min": 0.5, "parse_confidence_overall_min": 0.45, "score_threshold_override": 0.0, "residual_reserve_selected": True, "post_allocation_idle_only": True, "next_open_gap_cap_pct": 0.02, "early_failure_close_below_entry_and_reaction_close_override": False, "early_failure_no_progress_days_override": 12, "early_failure_no_progress_r_override": 0.0, "early_failure_no_progress_fraction_override": 1.0, "target_1_r_override": 3.0, "target_1_fraction_override": 0.1, "trailing_warmup_days_override": 12, "enabled": True, "veto_parse_confidence_min_override": 0.45, "per_trade_risk_pct_override": 0.12375, "stop_atr_multiplier_override": 3.0, "use_reaction_day_low_stop_override": False, "pre_event_entropy_60d_max": 2.05, "pre_event_market_temperature_max": 1.85, "macro_vix_max": 30.0, }, { "engine_id": "next_open_long_guidance_mixed_micro_postmarket", "selection_priority": -1, "event_types": ["guidance_update"], "event_directions": ["mixed"], "guidance_statuses": ["not_provided"], "filing_time_buckets": ["post_market"], "timing_class": "after_close", "direction": "long_only", "entry_timing_policy": "next_open", "max_holding_days": 12, "engine_risk_budget_pct": 0.12375, "per_trade_risk_pct_override": 0.1155, "reaction_day_return_min": -0.02, "reaction_day_return_max": 0.05, "close_location_min": 0.55, "close_location_max": 1.0, "gap_size_min": -0.02, "gap_size_max": 0.025, "volume_ratio_min": 0.5, "volume_ratio_max": 2.3, "max_market_cap_proxy": 15000000000.0, "document_quality_score_min": 0.5, "parse_confidence_overall_min": 0.45, "score_threshold_override": 0.0, "residual_reserve_selected": True, "post_allocation_idle_only": True, "next_open_gap_cap_pct": 0.025, "early_failure_close_below_entry_and_reaction_close_override": False, "early_failure_no_progress_days_override": 12, "early_failure_no_progress_r_override": 0.0, "early_failure_no_progress_fraction_override": 1.0, "target_1_r_override": 5.0, "target_1_fraction_override": 0.1, "trailing_warmup_days_override": 12, "enabled": True, "veto_parse_confidence_min_override": 0.45, "stop_atr_multiplier_override": 3.0, "use_reaction_day_low_stop_override": False, "pre_event_entropy_60d_max": 1.7, "pre_event_market_temperature_max": 0.8, "pre_event_gravitational_pull_min": 1.0, "macro_vix_max": 30.0, }, { "engine_id": "next_open_long_earnings_unknown_inline_postmarket_strict", "selection_priority": -1, "event_types": ["earnings_release"], "event_directions": ["unknown"], "guidance_statuses": ["inline_or_maintained"], "filing_time_buckets": ["post_market"], "timing_class": "after_close", "direction": "long_only", "entry_timing_policy": "next_open", "max_holding_days": 12, "engine_risk_budget_pct": 0.12375, "per_trade_risk_pct_override": 0.066, "reaction_day_return_min": -0.02, "reaction_day_return_max": 0.04, "close_location_min": 0.1, "close_location_max": 1.0, "gap_size_min": -0.05, "gap_size_max": 0.05, "volume_ratio_min": 1.2, "volume_ratio_max": 3.0, "min_market_cap_proxy": 4000000000.0, "max_market_cap_proxy": 15000000000.0, "document_quality_score_min": 0.5, "parse_confidence_overall_min": 0.45, "score_threshold_override": 0.0, "residual_reserve_selected": True, "post_allocation_idle_only": True, "veto_parse_confidence_min_override": 0.45, "next_open_gap_cap_pct": 0.05, "early_failure_close_below_entry_and_reaction_close_override": False, "early_failure_no_progress_days_override": 10, "early_failure_no_progress_r_override": 0.0, "early_failure_no_progress_fraction_override": 1.0, "target_1_r_override": 5.0, "target_1_fraction_override": 0.1, "trailing_warmup_days_override": 10, "enabled": True, "stop_atr_multiplier_override": 3.0, "use_reaction_day_low_stop_override": False, "pre_event_entropy_60d_max": 1.9, "pre_event_market_temperature_max": 1.1, "macro_vix_max": 30.0, }, { "engine_id": "next_open_long_bullish_raised_strong", "selection_priority": -1, "event_types": ["earnings_release"], "event_directions": ["bullish"], "guidance_statuses": ["raised"], "filing_time_buckets": ["post_market"], "timing_class": "after_close", "direction": "long_only", "entry_timing_policy": "next_open", "max_holding_days": 20, "engine_risk_budget_pct": 0.165, "reaction_day_return_min": 0.03, "reaction_day_return_max": 0.2, "close_location_min": 0.96, "volume_ratio_min": 1.2, "volume_ratio_max": 6.0, "min_market_cap_proxy": 5000000000.0, "document_quality_score_min": 0.6, "parse_confidence_overall_min": 0.6, "score_threshold_override": 0.0, "residual_reserve_selected": True, "post_allocation_idle_only": True, "next_open_gap_cap_pct": 0.1, "early_failure_close_below_entry_and_reaction_close_override": False, "early_failure_no_progress_days_override": 12, "early_failure_no_progress_r_override": 0.0, "early_failure_no_progress_fraction_override": 1.0, "target_1_r_override": 5.0, "target_1_fraction_override": 0.1, "trailing_warmup_days_override": 10, "enabled": True, "per_trade_risk_pct_override": 0.12375, "stop_atr_multiplier_override": 3.9, "use_reaction_day_low_stop_override": False, "pre_event_entropy_60d_min": 1.97, }, ] _MICRO_EVENT_ALPHA_ENGINES_MICROCAP8_GUARDED: list[dict[str, Any]] = [ { **engine, "max_market_cap_proxy": 8_000_000_000.0, } if engine["engine_id"] == "next_open_long_guidance_mixed_micro_postmarket" else {**engine} for engine in _MICRO_EVENT_ALPHA_ENGINES ] _MICRO_EVENT_ALPHA_BREADTH_ENGINE: dict[str, Any] = { "engine_id": "idle_macro_breadth_smh_postalloc", "selection_priority": -2, "timing_class": "after_close", "direction": "long_only", "entry_timing_policy": "next_open", "max_holding_days": 2, "engine_risk_budget_pct": 0.022, "per_trade_risk_pct_override": 0.0033, "stop_atr_multiplier_override": 1.9, "target_1_r_override": 99.0, "target_1_fraction_override": 0.0, "trailing_warmup_days_override": 1, "early_failure_close_below_entry_and_reaction_close_override": False, "next_open_gap_cap_pct": 0.018, "macro_long_symbol": "SMH", "macro_long_trade_symbol_mode": "fixed", "macro_long_reaction_day_return_min": 0.018, "macro_long_volume_ratio_min": 1.1, "macro_long_gap_size_min": 0.0, "macro_long_close_location_min": 0.64, "macro_long_breadth_symbols": ["QQQ", "XLK", "SMH"], "macro_long_min_breadth_count": 2, "macro_long_breadth_reaction_day_return_min": 0.01, "macro_long_breadth_close_location_min": 0.6, "macro_long_leadership_vs_spy_min": 0.004, "macro_vix_max": 27.0, "enabled": True, "synthetic_only": True, "post_allocation_idle_only": True, } _IDLE_ALPHA_PLUS_EVENT_PLUS_ALLOCATOR: dict[str, Any] = { "dynamic_allocator_enabled": True, "dynamic_allocator_cash_ratio_low": 0.04, "dynamic_allocator_cash_ratio_high": 0.16, "dynamic_allocator_cash_scale_low": 0.8, "dynamic_allocator_cash_scale_high": 1.025, "dynamic_allocator_crowded_primary_candidate_count": 6, "dynamic_allocator_crowded_primary_unique_sector_count": 4, "dynamic_allocator_crowded_scale": 0.79, "dynamic_allocator_synthetic_scale_multiplier": 1.03, "dynamic_allocator_snapshot_scale_multiplier": 1.02, "dynamic_allocator_synthetic_reentry_cooldown_days": 2, "dynamic_allocator_min_scale": 0.6, "dynamic_allocator_max_scale": 1.045, } _IDLE_ALPHA_PLUS_EVENT_PLUS_CASH_CONVEX_ALLOCATOR: dict[str, Any] = { **_IDLE_ALPHA_PLUS_EVENT_PLUS_ALLOCATOR, "dynamic_allocator_cash_scale_low": 0.78, "dynamic_allocator_cash_scale_high": 1.07, "dynamic_allocator_synthetic_scale_multiplier": 1.05, "dynamic_allocator_max_scale": 1.07, } _IDLE_ALPHA_PLUS_EVENT_PLUS_SNAPSHOT_CONVEX_ALLOCATOR: dict[str, Any] = { **_IDLE_ALPHA_PLUS_EVENT_PLUS_ALLOCATOR, "dynamic_allocator_cash_scale_low": 0.78, "dynamic_allocator_cash_scale_high": 1.08, "dynamic_allocator_synthetic_scale_multiplier": 1.0, "dynamic_allocator_snapshot_scale_multiplier": 1.04, "dynamic_allocator_max_scale": 1.10, } _MICRO_EVENT_ALPHA_BREADTH_ENGINE_STRICT_SOFT67: dict[str, Any] = { **_MICRO_EVENT_ALPHA_BREADTH_ENGINE, "max_holding_days": 1, "engine_risk_budget_pct": 0.018, "per_trade_risk_pct_override": 0.0028, "macro_long_reaction_day_return_min": 0.021, "macro_long_breadth_reaction_day_return_min": 0.0115, "macro_long_close_location_min": 0.67, "macro_long_breadth_close_location_min": 0.615, } IDLE_ALPHA_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "micro_event_alpha": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES), ], }, "micro_event_alpha_plus_event_plus": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES), {**_MICRO_EVENT_ALPHA_BREADTH_ENGINE}, ], "idle_alpha": {**_IDLE_ALPHA_PLUS_EVENT_PLUS_ALLOCATOR}, }, "micro_event_alpha_plus_event_plus_cash_convex": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES), {**_MICRO_EVENT_ALPHA_BREADTH_ENGINE}, ], "idle_alpha": {**_IDLE_ALPHA_PLUS_EVENT_PLUS_CASH_CONVEX_ALLOCATOR}, }, "micro_event_alpha_plus_event_plus_cash_convex_microcap8_guarded": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES_MICROCAP8_GUARDED), {**_MICRO_EVENT_ALPHA_BREADTH_ENGINE}, ], "idle_alpha": {**_IDLE_ALPHA_PLUS_EVENT_PLUS_CASH_CONVEX_ALLOCATOR}, }, "micro_event_alpha_plus_event_plus_strict_breadth_cash_experimental": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES), {**_MICRO_EVENT_ALPHA_BREADTH_ENGINE_STRICT_SOFT67}, ], "idle_alpha": {**_IDLE_ALPHA_PLUS_EVENT_PLUS_CASH_CONVEX_ALLOCATOR}, }, "micro_event_alpha_plus_event_plus_strict_breadth_snapshot_convex": { "strategy_engines": [ *(_MICRO_EVENT_ALPHA_ENGINES), {**_MICRO_EVENT_ALPHA_BREADTH_ENGINE_STRICT_SOFT67}, ], "idle_alpha": {**_IDLE_ALPHA_PLUS_EVENT_PLUS_SNAPSHOT_CONVEX_ALLOCATOR}, }, } # --------------------------------------------------------------------------- # Named dividend-capture sleeve presets — referenced by BacktestConfig # --------------------------------------------------------------------------- DIVIDEND_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = {} FORM4_PIT_EVENTS_V1_PATH = "data/reference/form4_daily_events_pit.parquet" FORM4_PIT_EVENTS_V3_PATH = "data/reference/form4_daily_events_pit_v3.parquet" FORM4_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "reserve_form4_cluster": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V1_PATH, "reserve_pct": 0.04, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.0, "max_lag_days": None, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V1_PATH, "reserve_pct": 0.06, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.0, "max_lag_days": None, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V1_PATH, "reserve_pct": 0.06, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.0, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V1_PATH, "reserve_pct": 0.06, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.0, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, # Experimental lane. Keep v1 presets frozen so existing stacks remain reproducible. "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_high": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.46, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_high_maxval500m": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.46, "min_owner_count": 2, "min_total_value": 5_000_000.0, "max_total_value": 500_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_balanced36": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.36, "min_owner_count": 2, "min_transaction_count": 2, "min_c_suite_count": 0, "min_cfo_count": 0, "min_role_weight_score": 0.0, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_event_day_count": 1, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "require_officer_or_director": False, "symbol_cooldown_days_after_loss": 180, "symbol_max_entries_in_lookback": None, "symbol_entry_lookback_days": 365, "disable_day1_early_failure": False, "no_progress_days_override": None, "no_progress_r_override": None, "no_progress_fraction_override": None, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_ultra": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.50, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_high_plus": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.48, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_high_47": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.47, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_ultra_mp5": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.50, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 20, "max_positions": 5, "max_new_per_day": 2, }, "reserve_form4_cluster_plus_fresh_same_day_v3_aggressive_plus_cooldown180_ultra_hd15": { "enabled": True, "pit_events_path": FORM4_PIT_EVENTS_V3_PATH, "reserve_pct": 0.50, "min_owner_count": 2, "min_total_value": 5_000_000.0, "min_purchase_pct": 0.005, "min_transaction_count": 2, "max_lag_days": 2, "max_min_lag_days": 1, "max_transaction_span_days": 0, "symbol_cooldown_days_after_loss": 180, "hold_days": 15, "max_positions": 6, "max_new_per_day": 2, }, } OWNERSHIP_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "ownership_13d_raise_reserve_plus_strict": { "enabled": True, "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", "reserve_pct": 0.15, "form_groups": ["13D"], "min_percent_owned": 5.0, "min_percent_delta_points": 2.0, "require_amendment": True, "require_initial": False, "require_activist": False, "require_13g_to_13d_transition": False, "hold_days": 30, "min_avg_dollar_volume": 10_000_000.0, "max_positions": 6, "max_new_per_day": 2, "min_cash_ratio_for_overlay": 0.25, "max_idle_deploy_pct": 0.0, }, "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95": { "enabled": True, "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", "reserve_pct": 0.95, "form_groups": ["13D"], "min_percent_owned": 5.0, "min_percent_delta_points": 2.0, "exclude_housekeeping_purpose": True, "exclude_structural_exchange_purpose": True, "require_amendment": True, "require_initial": False, "require_activist": False, "require_13g_to_13d_transition": False, "symbol_cooldown_days_after_loss": 90, "hold_days": 30, "min_avg_dollar_volume": 10_000_000.0, "max_positions": 6, "max_new_per_day": 2, "min_cash_ratio_for_overlay": 0.25, "max_idle_deploy_pct": 0.0, }, } RISK_OFF_ALPHA_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "risk_off_alpha_gld_crisis65_balanced_refined": { "enabled": True, "reserve_pct": 0.37, "symbols": ["gld"], "momentum_lookback_days": 20, "min_symbol_momentum": 0.05, "min_consecutive_sgov_days": 3, "min_parking_risk_score": 65.0, "rotation_momentum_gap": 0.02, "max_holding_days": 0, "min_cash_ratio_for_overlay": 0.0, "max_idle_deploy_pct": 0.0, }, "risk_off_alpha_gld_crisis60": { "enabled": True, "reserve_pct": 0.60, "symbols": ["gld"], "momentum_lookback_days": 20, "min_symbol_momentum": 0.05, "min_consecutive_sgov_days": 3, "min_parking_risk_score": 60.0, "rotation_momentum_gap": 0.02, "max_holding_days": 0, "min_cash_ratio_for_overlay": 0.0, "max_idle_deploy_pct": 0.0, }, "risk_off_alpha_gld_crisis65_heavy": { "enabled": True, "reserve_pct": 0.70, "symbols": ["gld"], "momentum_lookback_days": 20, "min_symbol_momentum": 0.05, "min_consecutive_sgov_days": 3, "min_parking_risk_score": 65.0, "rotation_momentum_gap": 0.02, "max_holding_days": 0, "min_cash_ratio_for_overlay": 0.0, "max_idle_deploy_pct": 0.0, }, } class RiskConfig(BaseModel): per_trade_risk_pct: float = 0.01 # 1% of equity per trade per_trade_risk_pct_a_tier: float | None = None max_daily_new_risk_pct: float = 0.03 # 3% of equity per day allow_budget_downsizing: bool = False # when True, clip order size to remaining daily/engine risk budget allow_oneoff_downsizing: bool = False # when True, clip risk for high one-off candidates instead of hard veto oneoff_downsize_floor: float = 0.25 # minimum risk scaler when oneoff_downsizing is enabled max_positions: int = 10 max_positions_per_sector: int = 3 buying_power_multiplier: float = 1.0 # max gross notional / equity for new long exposure max_position_value_pct: float | None = None # max fraction of equity in one position max_adv_fraction: float | None = None # max fraction of avg daily volume cooldown_after_loss_streak: int = 0 # consecutive losses to trigger cooldown cooldown_days: int = 0 # days to sit out after streak macro_regime_enabled: bool = False # block entries when SPY < SMA macro_regime_size_scaler: float = 1.0 # size scaler when SPY < SMA (< 1.0 = scale down instead of block) macro_regime_mode: str = "legacy_spy" # "legacy_spy" or "spy_qqq_scaler" macro_regime_neutral_size_scaler: float | None = None macro_regime_risk_off_size_scaler: float | None = None macro_regime_risk_off_a_tier_only: bool = False macro_sma_period: int = 20 # SMA lookback for macro regime stop_atr_multiplier: float = 1.5 # ATR multiplier for stop distance dynamic_stop_enabled: bool = False # scale ATR multiplier by reaction size & entropy dynamic_stop_reaction_low: float = 0.03 # reaction_day_return below this -> tighter stop dynamic_stop_reaction_high: float = 0.12 # reaction_day_return above this -> wider stop dynamic_stop_reaction_scaler_low: float = 0.8 # ATR scaler for calm reactions dynamic_stop_reaction_scaler_high: float = 1.3 # ATR scaler for extreme reactions dynamic_stop_entropy_low: float = 1.4 # pre_event_entropy_60d below this -> tighter stop dynamic_stop_entropy_high: float = 2.0 # pre_event_entropy_60d above this -> wider stop dynamic_stop_entropy_scaler_low: float = 0.85 # ATR scaler for low-entropy stocks dynamic_stop_entropy_scaler_high: float = 1.25 # ATR scaler for high-entropy stocks dynamic_stop_combined_floor: float = 0.7 # minimum combined scaler dynamic_stop_combined_ceiling: float = 1.5 # maximum combined scaler backtest_mode: str = "research" # "research" or "live" kill_switch_cooldown_days: int = 20 # trading days before reset (research only) kill_switch_log_only: bool = False # log-only mode (don't trigger, just observe) veto_oneoff_penalty: float = 0.5 # block if oneoff_penalty >= this veto_parse_confidence_min: float = 0.4 # block if parse_confidence < this veto_unknown_direction: bool = True # block if event_direction == "unknown" veto_bearish_direction: bool = True # block if event_direction == "bearish" fixed_capital_sizing: bool = False # when True, position sizing uses initial_capital instead of current equity reaction_size_cap_threshold: float | None = None # abs(reaction)% above which size scales down (e.g. 0.08) momentum_size_scaler_threshold: float | None = None # pre-event mom_20d above which size scales down (e.g. 0.10) momentum_size_scaler_floor: float = 0.3 # minimum scaler for high-momentum entries momentum_size_scaler_feature: str = "pre_event_momentum_20d" # or "price_vs_sma20" contrarian_boost_threshold: float | None = None # pre-event mom_20d below which size scales UP (e.g. -0.03) contrarian_boost_max: float = 1.5 # max scaler for low-momentum entries high_momentum_max_holding_days: int | None = None # override max_holding_days when mom > threshold high_momentum_holding_threshold: float | None = None # momentum threshold for holding day reduction vix_size_scaler_low: float = 15.0 # VIX level below which sizing is 1.0 (full) vix_size_scaler_high: float = 30.0 # VIX level above which sizing is at minimum vix_size_scaler_min: float = 0.35 # minimum size scaler at high VIX volatility_size_scaler_enabled: bool = False # inverse-vol position sizing volatility_size_scaler_low: float = 0.01 # daily vol below this = full size (1.0) volatility_size_scaler_high: float = 0.04 # daily vol above this = min size volatility_size_scaler_min: float = 0.5 # floor scaler at high vol breadth_throttle_enabled: bool = False # scale down when the daily selected slate is crowded breadth_throttle_candidate_count_threshold: int = 8 breadth_throttle_min: float = 0.7 sector_crowding_penalty_enabled: bool = False # scale down when too many same-sector candidates compete sector_crowding_candidate_count_threshold: int = 3 sector_crowding_penalty_min: float = 0.65 tail_risk_adjuster_enabled: bool = False # combined left-tail penalty using reaction/oneoff/orderliness features tail_risk_penalty_threshold: float = 0.62 tail_risk_penalty_min: float = 0.65 tail_risk_min_signals: int = 2 tail_exit_adjuster_enabled: bool = False # shorten holds / tighten no-progress exits for high tail candidates tail_exit_threshold: float = 0.62 tail_exit_min_signals: int = 2 tail_exit_max_holding_days: int | None = None tail_exit_no_progress_days: int | None = None tail_exit_no_progress_r: float | None = None tail_exit_no_progress_fraction: float | None = None technical_conviction_boost_enabled: bool = False # boost sizing for favorable technicals technical_conviction_boost_max: float = 1.3 # max size multiplier for conviction trades seasonal_reset_enabled: bool = False # close all positions before peak event season seasonal_reset_month: int = 1 # month to trigger reset (1=Jan → free capital for Feb earnings) seasonal_reset_day: int = 20 # day of month to trigger reset cash_parking_enabled: bool = False # park idle cash in index when no event positions cash_parking_preset: str | None = None # named preset (overrides all other parking params) cash_parking_account_type: str = "cash" # "cash" (no PDT, GFV only) or "margin" (PDT day trade rules) cash_parking_symbol: str = "spy" # "spy", "spym", "qual", "qqq", "qqqm", "dynamic", "sgov" cash_parking_defensive_symbol: str = "spy" # fallback defensive ETF for pair/regime parking ("spy", "spym", "qual") cash_parking_defensive_alt_symbol: str | None = None # optional alternate defensive ETF; pick the stronger valid candidate when set cash_parking_defensive_relay_enabled: bool = False # when primary gate says SGOV, allow defensive ETF instead if it is still healthy cash_parking_defensive_relay_trigger_mode: str = "always" # "always", "turn_of_month", "recovery", "turn_or_recovery" cash_parking_defensive_relay_turn_strength_min: float = 0.0 # minimum month-turn strength to allow relay cash_parking_defensive_relay_recovery_momentum_days: int = 5 # lookback for rebound confirmation cash_parking_defensive_relay_recovery_momentum_min: float = 0.0 # minimum rebound return to allow relay cash_parking_defensive_relay_drawdown_accel_max: float = 999.0 # require drawdown damage to be stabilizing/improving cash_parking_defensive_relay_risk_score_max: float = 100.0 # hard cap for relay even if exit threshold is wider cash_parking_defensive_momentum_min: float = -0.01 # require defensive ETF momentum above this to replace SGOV cash_parking_defensive_vol_max: float = 0.0 # optional tighter vol cap for defensive ETF (0 = use parking vol threshold) cash_parking_defensive_alloc_pct: float = 1.0 # if <1, allocate this fraction to defensive_symbol and keep the rest in SGOV cash_parking_reserve_pct: float = 0.02 # keep this % of equity as cash reserve cash_parking_trend_gate: bool = False # only park when price > SMA (skip downtrends) cash_parking_gate_sma_period: int = 20 # SMA period for trend gate (10, 20, 50, etc.) cash_parking_gate_mode: str = "price_above" # "price_above", "dual_sma", "drawdown", "momentum", "pct_threshold", "combo", "hysteresis", "slope", "breakout", "volatility", "recovery", "vol_trend", "vol_dd", "vol_regime", "guarded_regime", "composite", "science_regime", "science_blend", "relative_strength", "vt_blend", "vt_pair_blend", "regime_tiered" cash_parking_composite_exit_score: int = 40 # risk score >= this → SGOV cash_parking_composite_enter_score: int = 20 # risk score <= this → QQQ (hysteresis) cash_parking_composite_spy_score: int = 28 # science_regime: mid-risk parking goes to defensive ETF below this score cash_parking_gate_sma_long: int = 50 # long SMA period for dual_sma mode cash_parking_gate_drawdown_lookback: int = 50 # rolling high lookback days for drawdown gate cash_parking_gate_drawdown_pct: float = 0.07 # max drawdown from rolling high before SGOV cash_parking_gate_momentum_days: int = 20 # N-day return for momentum gate cash_parking_gate_pct_threshold: float = 0.02 # must be X% above SMA for pct_threshold gate cash_parking_gate_combo_require: int = 2 # how many sub-gates must agree for combo # Hysteresis: different entry/exit thresholds cash_parking_gate_hyst_enter_pct: float = 0.05 # enter QQQ when dd < this from high cash_parking_gate_hyst_exit_pct: float = 0.12 # exit to SGOV when dd > this from high # Slope: SMA slope direction cash_parking_gate_slope_period: int = 5 # days to measure SMA slope # Breakout: new N-day high cash_parking_gate_breakout_lookback: int = 50 # new high within N days → QQQ # Volatility: realized vol threshold cash_parking_gate_vol_lookback: int = 20 # days for vol calculation cash_parking_gate_vol_threshold: float = 0.25 # annualized vol threshold (above → SGOV) # Recovery: after SGOV, require N-day return > X% to re-enter QQQ cash_parking_gate_recovery_days: int = 10 # confirm recovery over N days cash_parking_gate_recovery_pct: float = 0.05 # require X% gain to re-enter # Proportional vol parking: scale QQQ allocation by vol level cash_parking_gate_vol_full_pct: float = 0.15 # vol below this → 100% QQQ cash_parking_gate_vol_zero_pct: float = 0.24 # vol above this → 0% QQQ (100% SGOV) # Vol TQQQ: ultra-low vol → TQQQ, normal → QQQ, high → SGOV cash_parking_gate_vol_tqqq_pct: float = 0.15 # vol below this → TQQQ instead of QQQ cash_parking_gate_vol_spy_threshold: float = 0.28 # guarded_regime: vol below this → defensive ETF fallback cash_parking_sgov_annual_rate: float = 0.05 # SGOV annualized yield (for "sgov" mode) cash_parking_stop_pct: float = 0.0 # parking trailing stop: exit if down X% from peak since entry (0=disabled) cash_parking_stop_recovery_days: int = 5 # after stop, require N-day positive return to re-enter cash_parking_stop_recovery_pct: float = 0.02 # require X% gain over recovery_days to confirm bounce cash_parking_require_trend: bool = False # also require trend confirmation to stay in QQQ cash_parking_trend_sma_period: int = 50 # SMA period for trend (sma mode) or lookback days (momentum mode) cash_parking_trend_mode: str = "sma" # "sma" (close > SMA) or "momentum" (N-day return > 0) cash_parking_trend_reentry_pct: float = 0.0 # momentum must exceed this to re-enter QQQ (0 = same as exit) cash_parking_exit_confirm_days: int = 1 # require N consecutive days before risk asset -> SGOV switch cash_parking_entry_confirm_days: int = 1 # require N consecutive days before SGOV -> risk asset switch cash_parking_stress_reentry_vol_mult: float = 1.0 # after a gate-driven SGOV exit, require vol < threshold * this cash_parking_stress_reentry_temperature_mult: float = 1.0 # stricter temperature threshold on re-entry (<1 = tighter) cash_parking_stress_reentry_entropy_mult: float = 1.0 # stricter entropy threshold on re-entry (<1 = tighter) cash_parking_stress_reentry_autocorr_buffer: float = 0.0 # require autocorr >= base threshold + buffer after gate exit cash_parking_topup_min_gain_pct: float = -999.0 # only add new idle cash to an existing risk parking sleeve when current price is above avg by this pct cash_parking_topup_min_days_held: int = 0 # require an existing risk parking sleeve to age N days before adding more cash cash_parking_topup_risk_score_max: float = 0.0 # maximum composite parking risk score allowed for pullback top-up (0 = disabled) cash_parking_topup_max_peak_drawdown_pct: float = 0.02 # block adding new cash when current price is more than this pct below the sleeve's post-entry peak cash_parking_vix_reentry_max: float = 0.0 # VIX must be below this to re-enter QQQ (0 = disabled) cash_parking_entropy_lookback: int = 20 # days for entropy calculation cash_parking_entropy_threshold: float = 0.0 # entropy above this → SGOV (0 = disabled) # Novel signals (physics/information theory/financial economics) cash_parking_vrp_threshold: float = 0.0 # VRP (VIX - realized_vol*100) above this → SGOV (0=disabled, academic: 8) cash_parking_temperature_threshold: float = 0.0 # vol_15/vol_50 ratio above this → SGOV (0=disabled, academic: 1.3) cash_parking_hurst_threshold: float = 0.0 # Hurst exponent below this → SGOV (0=disabled, academic: 0.45) cash_parking_efficiency_threshold: float = 0.0 # efficiency below this → SGOV (0=disabled) cash_parking_downside_vol_threshold: float = 0.0 # downside semivol above this → SGOV (0=disabled) cash_parking_ulcer_threshold: float = 0.0 # ulcer index above this → SGOV (0=disabled) cash_parking_drawdown_accel_threshold: float = 0.0 # drawdown acceleration above this → SGOV (0=disabled) cash_parking_kurtosis_threshold: float = 0.0 # excess kurtosis above this → SGOV (0=disabled, academic: 3.0) cash_parking_autocorr_threshold: float = -99.0 # autocorrelation below this → SGOV (-99=disabled, academic: -0.1) cash_parking_corr_threshold: float = 0.0 # SPY-QQQ corr below this → SGOV (0=disabled, academic: 0.80) cash_parking_low_vol_overlay_symbol: str | None = None # optional overlay symbol (e.g. TQQQ) when regime is ultra-calm cash_parking_low_vol_overlay_vol_threshold: float = 0.0 # require QQQ realized vol below this to use overlay cash_parking_low_vol_overlay_temperature_max: float = 0.0 # require vol_15/vol_50 <= this to use overlay cash_parking_low_vol_overlay_entropy_max: float = 0.0 # require entropy <= this to use overlay cash_parking_low_vol_overlay_hurst_min: float = 0.0 # require Hurst >= this to use overlay # TQQQ overlay shock brake: demote overlay → base symbol on acceleration signals cash_parking_overlay_shock_brake_enabled: bool = False cash_parking_overlay_shock_brake_rv_ratio: float = 1.35 # rv5/rv20 > this → brake cash_parking_overlay_shock_brake_dd5_pct: float = 0.0225 # QQQ 5-day drawdown > this → brake cash_parking_overlay_shock_brake_sma_cross: bool = True # QQQ < SMA10 AND smh_mom_5 < 0 → brake cash_parking_overlay_shock_brake_cooldown_days: int = 2 # days before TQQQ re-entry cash_parking_overlay_shock_brake_sma_buffer: float = 0.0 # 0=disabled; 0.005=exit when QQQ within 0.5% ABOVE SMA10 (pre-emptive) cash_parking_overlay_shock_brake_rv_ratio_upper: float = 0.0 # Signal 4 upper vol bound; 0=disabled; 0.45=only fire when vol5/vol20 in (0.25,0.45) # TQQQ overlay dwell cap: limit consecutive hold days and periodic revalidation cash_parking_overlay_max_hold_days: int = 0 # max overlay days (0=unlimited) cash_parking_overlay_revalidation_days: int = 0 # re-check every N days (0=disabled) # TQQQ overlay continuous leverage blend: QQQM+TQQQ dual-position cash_parking_overlay_blend_enabled: bool = False # QQQM+TQQQ continuous blend cash_parking_overlay_blend_target_vol: float = 0.30 # target parking vol cash_parking_overlay_blend_max_leverage: float = 2.3 # max effective leverage cash_parking_rotation_fast_momentum_days: int = 10 # fast leadership lookback for relative-strength parking cash_parking_rotation_slow_momentum_days: int = 20 # slow leadership lookback for relative-strength parking cash_parking_rotation_lead_threshold: float = 0.03 # minimum QQQ score edge vs SPY to prefer QQQM/QQQ cash_parking_rotation_strong_threshold: float = 0.08 # strong QQQ score edge threshold cash_parking_turn_of_month_lead_days: int = 2 # include the last N trading days of the month cash_parking_turn_of_month_lag_days: int = 3 # include the first N trading days of the month cash_parking_turn_of_month_boost: float = 0.08 # extra QQQ score boost during turn-of-month # Bearish parking override: when gate would return SGOV and composite risk is very high, # use this inverse ETF instead (e.g. "sh") for active bear-market alpha. # None = disabled (default, existing behavior unchanged). cash_parking_bearish_symbol: str | None = None # "sh", "sds", etc. — inverse ETF for deep stress cash_parking_bearish_threshold: int = 60 # composite risk score >= this → use bearish_symbol cash_parking_bearish_alloc_pct: float = 1.0 # if <1, allocate this fraction to bearish_symbol and keep the rest in SGOV cash_parking_crisis_symbol: str | None = None # optional crisis safe-haven ETF for deep stress (e.g. "ief", "iei") cash_parking_crisis_threshold: int = 75 # composite risk score >= this enables crisis_symbol evaluation cash_parking_crisis_momentum_days: int = 20 # lookback for crisis symbol momentum confirmation cash_parking_crisis_momentum_min: float = 0.0 # require crisis symbol momentum above this to replace SGOV cash_parking_crisis_vol_max: float = 0.0 # optional max realized vol allowed for crisis symbol (0 = disabled) # Multi-tier regime parking: VIX-driven 3-symbol rotation (used with gate_mode="regime_tiered") cash_parking_regime_risk_on_symbol: str = "jepq" # symbol when VIX < vix_low_threshold cash_parking_regime_neutral_symbol: str = "qqqm" # symbol when VIX between thresholds cash_parking_regime_vix_low_threshold: float = 20.0 # VIX below this → risk-on symbol cash_parking_regime_vix_high_threshold: float = 25.0 # VIX above this → SGOV cash_parking_regime_hysteresis_buffer: float = 1.0 # extra VIX margin to prevent whipsaw on tier transitions # HY Credit spread size scaler (uses macro_hy_spread from candidate features) credit_spread_size_scaler_enabled: bool = False credit_spread_tight_threshold: float = 4.0 # spread below this = full size credit_spread_wide_threshold: float = 5.5 # spread above this = stress scaler credit_spread_wide_scaler: float = 0.75 # scaler for spread in [tight, wide) range credit_spread_stress_scaler: float = 0.50 # scaler for spread >= wide_threshold # Yield curve size scaler (uses macro_t10y2y from candidate features) yield_curve_size_scaler_enabled: bool = False yield_curve_normal_threshold: float = 0.5 # T10Y2Y above this = normal (full size) yield_curve_flat_scaler: float = 0.75 # scaler when T10Y2Y in [0, normal_threshold) yield_curve_inverted_scaler: float = 0.50 # scaler when T10Y2Y < 0 (inverted) def apply_parking_preset(self) -> None: """Apply named parking preset, overriding individual params.""" if not self.cash_parking_preset: return preset = PARKING_PRESETS.get(self.cash_parking_preset) if preset is None: raise ValueError(f"Unknown parking preset: {self.cash_parking_preset}. Available: {list(PARKING_PRESETS.keys())}") for k, v in preset.items(): setattr(self, k, v) class ExecutionConfig(BaseModel): entry_fill_model: str = "next_open" exit_fill_model: str = "daily_bar_approximation" slippage_bps_base: float = 10.0 commission_per_share: float = 0.005 same_bar_priority: str = "stop_first_conservative" stop_model: str | None = None target_model: str = "fixed_r" # "fixed_r" or "atr_multiple" target_1_r: float | None = None # R-multiple for first target (fixed_r model) target_atr_multiplier: float = 1.5 # ATR multiplier for target (atr_multiple model) target_1_fraction: float | None = None # fraction to exit at target_1 (partial exit) use_tiered_targets: bool = False a_tier_target_1_r: float | None = None a_tier_target_1_fraction: float | None = None non_a_tier_target_1_r: float | None = None non_a_tier_target_1_fraction: float | None = None trailing_model: str | None = None trailing_warmup_days: int = 0 # days after entry before trailing activates max_holding_days: int = 10 lookback_entry_enabled: bool = False # enter positions for pre-start events still within holding window lookback_min_remaining_days: int | None = 3 # min holding days remaining for a lookback entry to be allowed no_follow_through_exit: bool = False # exit at D+1 close if close < entry price early_failure_close_below_entry_and_reaction_close: bool = False early_failure_no_progress_days: int | None = None early_failure_no_progress_r: float | None = None early_failure_no_progress_fraction: float | None = None early_pop_giveback_days_min: int | None = None early_pop_giveback_days_max: int | None = None early_pop_giveback_trigger_r: float | None = None early_pop_giveback_min_r: float | None = None early_pop_giveback_from_peak_pct: float | None = None early_pop_giveback_fraction: float | None = None expected_decay_exit_enabled: bool = False expected_decay_lambda: float | None = None expected_decay_score_floor: float | None = None expected_decay_min_days_held: int = 1 dynamic_hold_enabled: bool = False # adaptive mhd: extend for winners, cut losers early dynamic_hold_checkpoints: list[tuple[int, float]] | None = None # [(day, min_r), ...] cut if R below threshold dynamic_hold_extend_day: int = 8 # check day for extending mhd dynamic_hold_extend_r: float = 0.3 # min R to qualify for extension dynamic_hold_extend_to: int = 20 # extended mhd for qualifying positions adaptive_exit_enabled: bool = False adaptive_exit_exhaustion_close_min: float = 0.90 adaptive_exit_exhaustion_max_hold: int = 7 adaptive_exit_exhaustion_trailing_warmup: int = 1 adaptive_exit_orderly_close_min: float = 0.70 adaptive_exit_orderly_close_max: float = 0.88 adaptive_exit_orderly_max_hold: int = 25 adaptive_exit_orderly_trailing_warmup: int = 12 class StrategyEngineConfig(BaseModel): """Specialist engine routing and execution policy.""" engine_id: str inherits_from_engine_id: str | None = None exclude_if_matches_engine_id: str | None = None selection_priority: int = 0 ranking_fields_override: list[str] | None = None event_types: list[str] = Field(default_factory=list) entry_conventions: list[str] | None = None allowed_macro_regimes: list[str] | None = None event_directions: list[str] | None = None guidance_statuses: list[str] | None = None filing_time_buckets: list[str] | None = None allowed_exchanges: list[str] | None = None allowed_sectors: list[str] | None = None excluded_symbols: list[str] | None = None timing_class: str = "any" # "same_day", "after_close", "any" direction: str = "any" # "long_only", "short_only", "any" forced_trade_direction_override: str | None = None # "long" or "short" trade_symbol_mode: str = "event" # "event", "sector_etf", "peer_proxy" entry_timing_policy: str = "next_open" # "next_open", "reaction_close" max_holding_days: int | None = None max_positions_per_sector_override: int | None = None max_position_value_pct_override: float | None = None max_adv_fraction_override: float | None = None engine_risk_budget_pct: float = 1.0 capital_bucket_id: str | None = None capital_bucket_allocation_pct: float | None = None per_trade_risk_pct_override: float | None = None macro_vix_size_scaler_low: float | None = None macro_vix_size_scaler_high: float | None = None macro_vix_size_scaler_min: float | None = None macro_hy_spread_size_scaler_low: float | None = None macro_hy_spread_size_scaler_high: float | None = None macro_hy_spread_size_scaler_min: float | None = None score_size_scaler_low: float | None = None score_size_scaler_high: float | None = None score_size_scaler_min: float | None = None entropy_size_scaler_low: float | None = None entropy_size_scaler_high: float | None = None entropy_size_scaler_min: float | None = None stop_atr_multiplier_override: float | None = None target_atr_multiplier_override: float | None = None target_1_r_override: float | None = None target_1_fraction_override: float | None = None recycle_on_cash_block: bool = False recycle_min_days_held: int | None = None recycle_min_score_delta: float | None = None recycle_allowed_victim_engine_ids: list[str] | None = None recycle_allow_any_victim_engine: bool = False recycle_allow_cross_timing: bool = False recycle_positive_pnl_only: bool = True recycle_max_victim_fitness: float | None = None recycle_max_victim_unrealized_r: float | None = None rotation_enabled: bool = False rotation_min_days_held: int = 5 rotation_fitness_threshold: float = 0.20 rotation_min_candidate_score: float = 0.40 rotation_max_unrealized_r: float | None = None rotation_min_unrealized_r: float | None = None trailing_model_override: str | None = None trailing_warmup_days_override: int | None = None use_reaction_day_low_stop_override: bool | None = None early_failure_close_below_entry_and_reaction_close_override: bool | None = None early_failure_no_progress_days_override: int | None = None early_failure_no_progress_r_override: float | None = None early_failure_no_progress_fraction_override: float | None = None dynamic_hold_checkpoints_override: list[list[float]] | None = None # [[day, min_r], ...] dynamic_hold_extend_day_override: int | None = None dynamic_hold_extend_r_override: float | None = None dynamic_hold_extend_to_override: int | None = None veto_oneoff_penalty_override: float | None = None allow_oneoff_downsizing_override: bool | None = None oneoff_downsize_floor_override: float | None = None veto_parse_confidence_min_override: float | None = None score_threshold_override: float | None = None residual_reserve_selected: bool = False pead_reaction_threshold_override: float | None = None pead_volume_threshold_override: float | None = None reaction_day_return_min: float | None = None reaction_day_return_max: float | None = None close_location_min: float | None = None close_location_max: float | None = None volume_ratio_min: float | None = None volume_ratio_max: float | None = None min_entry_price_override: float | None = None max_entry_price_override: float | None = None avg_dollar_volume_min: float | None = None avg_dollar_volume_max: float | None = None gap_size_min: float | None = None gap_size_max: float | None = None proxy_reaction_day_return_min: float | None = None proxy_reaction_day_return_max: float | None = None proxy_close_location_min: float | None = None proxy_close_location_max: float | None = None proxy_volume_ratio_min: float | None = None proxy_volume_ratio_max: float | None = None proxy_gap_size_min: float | None = None proxy_gap_size_max: float | None = None proxy_avg_dollar_volume_min: float | None = None proxy_avg_dollar_volume_max: float | None = None reaction_day_range_pct_min: float | None = None reaction_day_range_pct_max: float | None = None upper_wick_pct_min: float | None = None upper_wick_pct_max: float | None = None min_market_cap_proxy: float | None = None max_market_cap_proxy: float | None = None document_quality_score_min: float | None = None document_quality_score_max: float | None = None signal_strength_score_min: float | None = None signal_strength_score_max: float | None = None oneoff_penalty_min: float | None = None oneoff_penalty_max: float | None = None parse_confidence_overall_min: float | None = None parse_confidence_overall_max: float | None = None prior_event_fwd5d_min: float | None = None prior_event_fwd5d_max: float | None = None lm_net_sentiment_min: float | None = None lm_net_sentiment_max: float | None = None earnings_surprise_pct_min: float | None = None earnings_surprise_pct_max: float | None = None peer_sector_event_count_365d_min: float | None = None peer_sector_event_count_365d_max: float | None = None sector_recent_event_count_3d_min: float | None = None sector_recent_event_count_3d_max: float | None = None sector_recent_leader_count_3d_min: float | None = None sector_recent_leader_count_3d_max: float | None = None sector_recent_leader_reaction_max_3d_min: float | None = None sector_recent_leader_reaction_max_3d_max: float | None = None peer_relative_surprise_pct_365d_min: float | None = None peer_relative_surprise_pct_365d_max: float | None = None peer_relative_sue_hist_mean_4q_365d_min: float | None = None peer_relative_sue_hist_mean_4q_365d_max: float | None = None prior_catalyst_count_20d_min: float | None = None prior_catalyst_count_20d_max: float | None = None prior_catalyst_count_60d_min: float | None = None prior_catalyst_count_60d_max: float | None = None prior_catalyst_type_diversity_20d_min: float | None = None prior_catalyst_type_diversity_20d_max: float | None = None prior_catalyst_type_diversity_60d_min: float | None = None prior_catalyst_type_diversity_60d_max: float | None = None sentiment_surprise_min: float | None = None sentiment_surprise_max: float | None = None price_text_dislocation_min: float | None = None price_text_dislocation_max: float | None = None positive_price_text_dislocation_min: float | None = None positive_price_text_dislocation_max: float | None = None positive_price_text_dislocation_rank_min: float | None = None positive_price_text_dislocation_rank_max: float | None = None macro_vix_min: float | None = None macro_vix_max: float | None = None volatility_crush_only: bool = False volatility_crush_vix_drop_pct_min: float | None = None volatility_crush_spy_return_min: float | None = None volatility_crush_score_threshold_override: float | None = None volatility_crush_per_trade_risk_pct_override: float | None = None volatility_crush_engine_risk_budget_pct_override: float | None = None volatility_crush_macro_vix_max_override: float | None = None macro_hy_spread_min: float | None = None macro_hy_spread_max: float | None = None pre_event_hurst_60d_min: float | None = None pre_event_hurst_60d_max: float | None = None pre_event_entropy_60d_min: float | None = None pre_event_entropy_60d_max: float | None = None pre_event_short_ratio_min: float | None = None pre_event_short_ratio_max: float | None = None pre_event_sector_momentum_20d_min: float | None = None pre_event_sector_momentum_20d_max: float | None = None pre_event_bb_position_min: float | None = None pre_event_bb_position_max: float | None = None pre_event_gravitational_pull_min: float | None = None pre_event_gravitational_pull_max: float | None = None pre_event_market_temperature_min: float | None = None pre_event_market_temperature_max: float | None = None weak_reaction_threshold: float | None = None weak_reaction_gap_max: float | None = None unknown_direction_reaction_min: float | None = None unknown_direction_close_location_min: float | None = None unknown_direction_close_location_max: float | None = None unknown_direction_gap_size_min: float | None = None unknown_inline_exit_close_location_min: float | None = None unknown_inline_exit_gap_size_max: float | None = None unknown_inline_early_failure_close_below_entry_and_reaction_close_override: bool | None = None unknown_inline_early_failure_no_progress_days_override: int | None = None unknown_inline_early_failure_no_progress_r_override: float | None = None unknown_inline_early_failure_no_progress_fraction_override: float | None = None mixed_inline_close_location_min: float | None = None mixed_inline_close_location_max: float | None = None mixed_inline_gap_size_max: float | None = None mixed_inline_early_failure_close_below_entry_and_reaction_close_override: bool | None = None mixed_inline_early_failure_no_progress_days_override: int | None = None mixed_inline_early_failure_no_progress_r_override: float | None = None mixed_inline_early_failure_no_progress_fraction_override: float | None = None next_open_gap_cap_pct: float | None = None add_on_min_parent_days_held: int | None = None add_on_max_parent_days_held: int | None = None add_on_schedule_days: list[int] | None = None add_on_close_location_min: float | None = None add_on_progress_r_min: float | None = None add_on_progress_r_levels: list[float] | None = None add_on_parent_score_min: float | None = None add_on_parent_engine_ids: list[str] | None = None add_on_max_count: int = 1 add_on_size_fraction: float = 0.5 add_on_require_above_reaction_high: bool = False delayed_entry_lookback_days: int | None = None # e.g. 3 = look at events from 3 trading days ago delayed_entry_source_engine_ids: list[str] | None = None # which engines' candidates to consider delayed_entry_min_drift_pct: float | None = None # min price change since reaction close delayed_entry_close_location_min: float | None = None # today's close location requirement leader_follower_lookahead_days: int | None = None # trading-day window to upcoming follower earnings leader_follower_min_days_to_event: int | None = None # minimum trading days until follower event leader_follower_hold_buffer_days: int = 1 # exit before follower event by this many trading days leader_follower_calendar_mode: str = "future_row" # "future_row", "pit_calendar", "pit_then_fallback" leader_follower_extra_peer_symbols_by_leader: dict[str, list[str]] | None = None leader_follower_extra_peer_symbols_by_sector: dict[str, list[str]] | None = None leader_follower_allowed_peer_symbols: list[str] | None = None attention_min_wiki_spike_10d: float | None = None attention_min_wiki_zscore_20d: float | None = None attention_max_wiki_spike_10d: float | None = None attention_max_wiki_zscore_20d: float | None = None attention_min_article_count_3d: int | None = None attention_min_us_article_count_3d: int | None = None attention_min_resolver_confidence: float | None = None shadow_only: bool = False synthetic_only: bool = False post_allocation_idle_only: bool = False enabled: bool = True # Macro short engine: generate SH candidate when composite risk score >= threshold macro_short_risk_threshold: int | None = None # Macro long engine: generate ETF long candidate when leadership/breadth trigger fires macro_long_symbol: str | None = None macro_long_reaction_day_return_min: float | None = None macro_long_reaction_day_return_max: float | None = None macro_long_volume_ratio_min: float | None = None macro_long_volume_ratio_max: float | None = None macro_long_gap_size_min: float | None = None macro_long_gap_size_max: float | None = None macro_long_close_location_min: float | None = None macro_long_close_location_max: float | None = None macro_long_trade_symbol_mode: str | None = None macro_long_breadth_symbols: list[str] | None = None macro_long_min_breadth_count: int | None = None macro_long_breadth_reaction_day_return_min: float | None = None macro_long_breadth_reaction_day_return_max: float | None = None macro_long_breadth_volume_ratio_min: float | None = None macro_long_breadth_volume_ratio_max: float | None = None macro_long_breadth_gap_size_min: float | None = None macro_long_breadth_gap_size_max: float | None = None macro_long_breadth_close_location_min: float | None = None macro_long_breadth_close_location_max: float | None = None macro_long_leadership_vs_spy_min: float | None = None macro_long_min_daily_candidate_count: int | None = None macro_long_min_unique_sector_count: int | None = None # --- EarningsRunup pre-event drift engine --- # Trigger: long entry T-1 close when scheduled earnings is `days_to_earnings` trading days # ahead AND attention z-score and dollar-volume z-score both clear minimums. # Exit: stop -X%, target +Y%, trailing activated after +Z%, hard exit one day before print. earnings_runup_enabled: bool = False earnings_runup_days_to_earnings_min: int | None = None earnings_runup_days_to_earnings_max: int | None = None earnings_runup_attention_zscore_20d_min: float | None = None earnings_runup_dollar_volume_zscore_20d_min: float | None = None earnings_runup_min_avg_dollar_volume: float | None = None earnings_runup_stop_pct: float = 0.04 # -4% from entry earnings_runup_target_pct: float = 0.08 # +8% from entry earnings_runup_trailing_activate_pct: float = 0.05 # +5% triggers trailing earnings_runup_trailing_giveback_pct: float = 0.03 # 3% giveback after activation earnings_runup_calendar_buffer_days: int = 1 # exit by close of T-1 before print # --- PeerSympathy engine --- # Trigger: when a sector leader fires a qualifying PEAD event with reaction_close # >= +5%, buy the top-correlated peers at next_open. Catches sympathy rallies that # PEAD architecturally misses. Correlation is computed on `[T-65, T-5]` log-returns # (skip last 5 days to avoid co-movement leakage from leader's own pre-event drift). peer_sympathy_enabled: bool = False peer_sympathy_leader_event_types: list[str] | None = None # which event_types qualify the leader peer_sympathy_leader_reaction_min: float = 0.05 # leader reaction_day_return >= +5% peer_sympathy_correlation_min: float = 0.55 # peer 60d return-correlation threshold peer_sympathy_correlation_window_start: int = 65 # T-65 (inclusive of skip tail) peer_sympathy_correlation_window_end_skip: int = 5 # skip last 5 trading days peer_sympathy_top_n_peers: int = 2 peer_sympathy_blackout_days_to_peer_event: int = 3 # hard exit if peer's own earnings within N trading days peer_sympathy_stop_pct: float = 0.035 # -3.5% from entry peer_sympathy_target_pct: float = 0.06 # +6% from entry peer_sympathy_max_holding_days: int = 3 # --- VolBreakout52w engine --- # Honest, look-ahead-safe descendant of the retired topgainer family. # Trigger (ALL on T-1 close): # 1. close_T-1 > max(high[T-252..T-2]) # 2. volume_T-1 >= 2 * median_volume_20d_T-2 # 3. ATR_14_T-1 / close_T-1 in [0.015, 0.06] # Entry T next_open. Skip if pre-open implied gap > +4%. # Exits: -3% stop / +5% target / max_holding_days=2 (mandatory MOC day 2). vol_breakout_52w_enabled: bool = False vol_breakout_52w_lookback_days: int = 252 vol_breakout_52w_volume_ratio_min: float = 2.0 vol_breakout_52w_volume_median_window: int = 20 vol_breakout_52w_atr_normalized_min: float = 0.015 vol_breakout_52w_atr_normalized_max: float = 0.06 vol_breakout_52w_pre_open_gap_max: float = 0.04 vol_breakout_52w_skip_if_no_gap_data: bool = False # log loudly when missing vol_breakout_52w_min_avg_dollar_volume: float = 10_000_000.0 vol_breakout_52w_min_price: float = 5.0 vol_breakout_52w_stop_pct: float = 0.03 # -3% intraday vol_breakout_52w_target_pct: float = 0.05 # +5% vol_breakout_52w_max_holding_days: int = 2 # mandatory MOC exit on day 2 class EventTypeProfile(BaseModel): """Per-event-type overrides for scoring, risk, and exit parameters.""" enabled: bool = True score_threshold_override: float | None = None max_holding_days_override: int | None = None stop_atr_multiplier_override: float | None = None target_atr_multiplier_override: float | None = None direction_filter: str = "any" # "bullish_only", "bearish_only", "any" class ReportingConfig(BaseModel): write_trade_blotter: bool = True write_equity_curve: bool = True write_metrics_summary: bool = True generate_plots: bool = False attribution_buckets: list[str] = Field(default_factory=list) class DividendCaptureConfig(BaseModel): enabled: bool = False pit_calendar_path: str | None = None reserve_pct: float = 0.0 min_dividend_yield_pct: float = 0.0025 max_dividend_yield_pct: float | None = 0.02 min_avg_dollar_volume: float = 20_000_000.0 max_positions: int = 5 class Form4CaptureConfig(BaseModel): enabled: bool = False pit_events_path: str | None = None reserve_pct: float = 0.0 min_owner_count: int = 2 min_transaction_count: int = 1 min_c_suite_count: int = 0 min_cfo_count: int = 0 min_role_weight_score: float = 0.0 min_total_value: float = 5_000_000.0 max_total_value: float | None = None min_purchase_pct: float = 0.0 min_event_day_count: int = 1 max_lag_days: int | None = None max_min_lag_days: int | None = None max_transaction_span_days: int | None = None require_officer_or_director: bool = False symbol_cooldown_days_after_loss: int = 0 symbol_max_entries_in_lookback: int | None = None symbol_entry_lookback_days: int = 365 disable_day1_early_failure: bool = False no_progress_days_override: int | None = None no_progress_r_override: float | None = None no_progress_fraction_override: float | None = None hold_days: int = 20 max_positions: int = 6 max_new_per_day: int = 2 class OwnershipCaptureConfig(BaseModel): enabled: bool = False pit_events_path: str | None = None reserve_pct: float = 0.0 form_groups: list[str] = Field(default_factory=lambda: ["13D"]) min_percent_owned: float = 5.0 min_percent_delta_points: float = 0.0 exclude_housekeeping_purpose: bool = False exclude_structural_exchange_purpose: bool = False min_strength_score: int | None = None require_amendment: bool = False require_initial: bool = False require_activist: bool = False require_13g_to_13d_transition: bool = False symbol_cooldown_days_after_loss: int = 0 symbol_max_entries_in_lookback: int | None = None symbol_entry_lookback_days: int = 365 extra_idle_deploy_pct_above_reserve: float = 0.0 disable_day1_early_failure: bool = False no_progress_days_override: int | None = None no_progress_r_override: float | None = None no_progress_fraction_override: float | None = None hold_days: int = 30 min_avg_dollar_volume: float = 10_000_000.0 max_positions: int = 6 max_new_per_day: int = 2 min_cash_ratio_for_overlay: float = 0.25 max_idle_deploy_pct: float = 1.0 class RiskOffAlphaConfig(BaseModel): enabled: bool = False reserve_pct: float = 0.0 symbols: list[str] = Field(default_factory=lambda: ["gld", "dbc"]) momentum_lookback_days: int = 20 min_symbol_momentum: float = 0.05 min_consecutive_sgov_days: int = 3 min_parking_risk_score: float = 0.0 rotation_momentum_gap: float = 0.02 max_holding_days: int = 0 min_cash_ratio_for_overlay: float = 0.0 max_idle_deploy_pct: float = 0.0 adaptive_reserve_enabled: bool = False adaptive_reserve_score_mid: float = 0.0 adaptive_reserve_score_high: float = 0.0 adaptive_reserve_pct_low: float = 0.0 adaptive_reserve_pct_mid: float = 0.0 adaptive_reserve_pct_high: float = 0.0 class IdleAlphaConfig(BaseModel): dynamic_allocator_enabled: bool = False dynamic_allocator_cash_ratio_low: float = 0.04 dynamic_allocator_cash_ratio_high: float = 0.16 dynamic_allocator_cash_scale_low: float = 0.7 dynamic_allocator_cash_scale_high: float = 1.1 dynamic_allocator_crowded_primary_candidate_count: int | None = None dynamic_allocator_crowded_primary_unique_sector_count: int | None = None dynamic_allocator_crowded_scale: float = 0.85 dynamic_allocator_synthetic_scale_multiplier: float = 1.0 dynamic_allocator_snapshot_scale_multiplier: float = 1.0 dynamic_allocator_synthetic_reentry_cooldown_days: int = 0 dynamic_allocator_min_scale: float = 0.55 dynamic_allocator_max_scale: float = 1.2 class NonCoreAllocatorWeightsConfig(BaseModel): native_strength: float = 1.00 hold_penalty: float = 0.20 liquidity_penalty: float = 0.25 overlap_penalty: float = 0.20 parking_opportunity_penalty: float = 0.35 class NonCoreAllocatorConfig(BaseModel): enabled: bool = False mode: str = "shadow" # "shadow" or "live" scope: str = "non_core" benchmark_mode: str = "current_effective_parking" weights: NonCoreAllocatorWeightsConfig = Field(default_factory=NonCoreAllocatorWeightsConfig) class BacktestConfig(BaseModel): strategy_name: str dataset_snapshot_id: str requested_snapshot_id: str | None = None canonical_snapshot_id: str | None = None earnings_calendar_pit_path: str | None = None dividend_capture_sleeve_preset: str | None = None form4_capture_sleeve_preset: str | None = None ownership_capture_sleeve_preset: str | None = None risk_off_alpha_sleeve_preset: str | None = None universe: UniverseConfig = Field(default_factory=UniverseConfig) signal: SignalConfig = Field(default_factory=SignalConfig) risk: RiskConfig = Field(default_factory=RiskConfig) execution: ExecutionConfig = Field(default_factory=ExecutionConfig) reporting: ReportingConfig = Field(default_factory=ReportingConfig) dividend_capture: DividendCaptureConfig = Field(default_factory=DividendCaptureConfig) form4_capture: Form4CaptureConfig = Field(default_factory=Form4CaptureConfig) ownership_capture: OwnershipCaptureConfig = Field(default_factory=OwnershipCaptureConfig) risk_off_alpha: RiskOffAlphaConfig = Field(default_factory=RiskOffAlphaConfig) idle_alpha: IdleAlphaConfig = Field(default_factory=IdleAlphaConfig) non_core_allocator_v2: NonCoreAllocatorConfig = Field(default_factory=NonCoreAllocatorConfig) event_type_profiles: dict[str, EventTypeProfile] = Field(default_factory=dict) idle_alpha_sleeve_preset: str | None = None idle_alpha_dedup_mode: str = "skip" # "skip" (default, backward-compat) or "rename" (suffix __ia_sleeve on conflict) strategy_engines: list[StrategyEngineConfig] = Field(default_factory=list) strategy_engine_selection_mode: str = "interleave" # "interleave", "interleave_head_score", "global_score", "interleave_cap_efficiency_soft", "interleave_cap_efficiency_strict", or "interleave_cash_tiebreak" def model_post_init(self, __context: Any) -> None: self.risk.apply_parking_preset() self.apply_dividend_capture_sleeve_preset() self.apply_form4_capture_sleeve_preset() self.apply_ownership_capture_sleeve_preset() self.apply_risk_off_alpha_sleeve_preset() self.apply_idle_alpha_sleeve_preset() def apply_dividend_capture_sleeve_preset(self) -> None: if not self.dividend_capture_sleeve_preset: return preset = DIVIDEND_CAPTURE_SLEEVE_PRESETS.get(self.dividend_capture_sleeve_preset) if preset is None: raise ValueError( "Unknown dividend capture sleeve preset: " f"{self.dividend_capture_sleeve_preset}. Available: {list(DIVIDEND_CAPTURE_SLEEVE_PRESETS.keys())}" ) current = self.dividend_capture.model_dump() current.update(preset) self.dividend_capture = DividendCaptureConfig.model_validate(current) def apply_form4_capture_sleeve_preset(self) -> None: if not self.form4_capture_sleeve_preset: return preset = FORM4_CAPTURE_SLEEVE_PRESETS.get(self.form4_capture_sleeve_preset) if preset is None: raise ValueError( "Unknown Form 4 capture sleeve preset: " f"{self.form4_capture_sleeve_preset}. Available: {list(FORM4_CAPTURE_SLEEVE_PRESETS.keys())}" ) current = self.form4_capture.model_dump() current.update(preset) self.form4_capture = Form4CaptureConfig.model_validate(current) def apply_ownership_capture_sleeve_preset(self) -> None: if not self.ownership_capture_sleeve_preset: return preset = OWNERSHIP_CAPTURE_SLEEVE_PRESETS.get(self.ownership_capture_sleeve_preset) if preset is None: raise ValueError( "Unknown ownership capture sleeve preset: " f"{self.ownership_capture_sleeve_preset}. Available: {list(OWNERSHIP_CAPTURE_SLEEVE_PRESETS.keys())}" ) current = self.ownership_capture.model_dump() current.update(preset) self.ownership_capture = OwnershipCaptureConfig.model_validate(current) def apply_risk_off_alpha_sleeve_preset(self) -> None: if not self.risk_off_alpha_sleeve_preset: return preset = RISK_OFF_ALPHA_SLEEVE_PRESETS.get(self.risk_off_alpha_sleeve_preset) if preset is None: raise ValueError( "Unknown risk-off alpha sleeve preset: " f"{self.risk_off_alpha_sleeve_preset}. Available: {list(RISK_OFF_ALPHA_SLEEVE_PRESETS.keys())}" ) current = self.risk_off_alpha.model_dump() current.update(preset) self.risk_off_alpha = RiskOffAlphaConfig.model_validate(current) def apply_idle_alpha_sleeve_preset(self) -> None: """Append named idle-alpha sleeve engines without touching risk config.""" if not self.idle_alpha_sleeve_preset: return preset = IDLE_ALPHA_SLEEVE_PRESETS.get(self.idle_alpha_sleeve_preset) if preset is None: raise ValueError( "Unknown idle alpha sleeve preset: " f"{self.idle_alpha_sleeve_preset}. Available: {list(IDLE_ALPHA_SLEEVE_PRESETS.keys())}" ) existing_engine_ids = {engine.engine_id for engine in self.strategy_engines} appended_engines: list[StrategyEngineConfig] = [] for engine_payload in preset["strategy_engines"]: engine = StrategyEngineConfig.model_validate(engine_payload) if engine.engine_id in existing_engine_ids: if self.idle_alpha_dedup_mode == "rename": engine = engine.model_copy(update={"engine_id": f"{engine.engine_id}__ia_sleeve"}) else: continue appended_engines.append(engine) existing_engine_ids.add(engine.engine_id) if appended_engines: self.strategy_engines.extend(appended_engines) idle_alpha_payload = preset.get("idle_alpha") if idle_alpha_payload: current_idle_alpha = self.idle_alpha.model_dump() current_idle_alpha.update(idle_alpha_payload) self.idle_alpha = IdleAlphaConfig.model_validate(current_idle_alpha) def get_event_profile(self, event_type: str) -> EventTypeProfile | None: """Look up event-type-specific profile. Returns None if no override.""" return self.event_type_profiles.get(event_type) def _build_strategy_engine_lookup(self) -> dict[str, StrategyEngineConfig]: return {engine.engine_id: engine for engine in self.strategy_engines} def resolve_strategy_engine( self, engine: StrategyEngineConfig, *, _seen: set[str] | None = None, ) -> StrategyEngineConfig: parent_id = engine.inherits_from_engine_id if not parent_id: return engine parent = self._build_strategy_engine_lookup().get(parent_id) if parent is None: return engine seen = set(_seen or set()) if engine.engine_id in seen or parent_id in seen: raise ValueError(f"Cyclic strategy engine inheritance detected for {engine.engine_id}") seen.add(engine.engine_id) resolved_parent = self.resolve_strategy_engine(parent, _seen=seen) merged = resolved_parent.model_dump() for field_name in engine.model_fields_set: merged[field_name] = getattr(engine, field_name) return StrategyEngineConfig.model_validate(merged) def get_strategy_engines(self) -> list[StrategyEngineConfig]: """Enabled strategy engines ordered by priority, then manifest order.""" resolved_engines: list[tuple[int, StrategyEngineConfig]] = [] for index, engine in enumerate(self.strategy_engines): resolved = self.resolve_strategy_engine(engine) if resolved.enabled: resolved_engines.append((index, resolved)) resolved_engines.sort( key=lambda item: (-int(item[1].selection_priority), item[0]), ) return [engine for _, engine in resolved_engines] def get_active_strategy_engines(self) -> list[StrategyEngineConfig]: """Enabled engines that participate in the live portfolio.""" return [engine for engine in self.get_strategy_engines() if not engine.shadow_only] def get_shadow_strategy_engines(self) -> list[StrategyEngineConfig]: """Enabled engines that run in paper/shadow mode only.""" return [engine for engine in self.get_strategy_engines() if engine.shadow_only] # --------------------------------------------------------------------------- # Experiment models # --------------------------------------------------------------------------- class SplitSpec(BaseModel): kind: str params: dict[str, Any] = Field(default_factory=dict) class ExperimentManifest(BaseModel): experiment_name: str dataset_snapshot_id: str description: str | None = None base_config: str # path to base config JSON file overrides: dict[str, Any] = Field(default_factory=dict) strategy_engines: list[StrategyEngineConfig] = Field(default_factory=list) splits: list[SplitSpec] = Field(default_factory=list) tags: list[str] = Field(default_factory=list) notes: str | None = None # --- metadata --- id: int | None = None # unique sequential experiment ID aliases: list[str] = Field(default_factory=list) parent: str | None = None created_at: str | None = None created_by: str | None = None status: str = "active" # draft | active | promoted | retired generation: int | None = None version_family: str | None = None changelog: str | None = None performance_summary: dict[str, Any] | None = None class ExperimentResult(BaseModel): model_config = ConfigDict(frozen=True) run_id: str manifest: ExperimentManifest resolved_config: BacktestConfig metrics: MetricsBundle artifact_paths: dict[str, str] = Field(default_factory=dict) started_at: dt.datetime finished_at: dt.datetime total_trading_days: int total_candidates_seen: int total_orders_rejected: int # --------------------------------------------------------------------------- # Improvement Tracking models # --------------------------------------------------------------------------- class SQSWeights(BaseModel): """Weights for Strategy Quality Score computation.""" profitability: float = 0.40 risk: float = 0.25 consistency: float = 0.20 robustness: float = 0.15 low_trade_penalty_threshold: int = 20 low_trade_penalty_factor: float = 0.5 class SQSv2Weights(BaseModel): """Weights for Strategy Quality Score v2 with capital efficiency.""" profitability: float = 0.35 risk: float = 0.25 consistency: float = 0.20 robustness: float = 0.10 capital_efficiency: float = 0.10 low_trade_penalty_threshold: int = 20 low_trade_penalty_factor: float = 0.5 class PromotionScoreWeights(BaseModel): """Weights for promotion scoring across valid/test splits.""" valid_quality: float = 0.55 test_quality: float = 0.15 floor_quality: float = 0.30 class UnifiedScoreWeights(BaseModel): """Weights for a stricter single ranking score across valid/test splits.""" split_profitability: float = 0.25 split_risk: float = 0.20 split_consistency: float = 0.15 split_robustness: float = 0.20 split_capital_efficiency: float = 0.20 valid_quality: float = 0.45 test_quality: float = 0.20 floor_quality: float = 0.20 gap_quality: float = 0.15 class ReturnScoreWeights(BaseModel): """Weights for return-max ranking across train/valid/test splits.""" split_total_return: float = 0.28 split_annualized_return: float = 0.12 split_profitability: float = 0.12 split_sharpe: float = 0.08 split_drawdown: float = 0.12 split_return_on_gross: float = 0.18 split_gross_exposure: float = 0.05 split_days_in_market: float = 0.05 train_quality: float = 0.35 valid_quality: float = 0.30 test_quality: float = 0.35 floor_quality: float = 0.15 gap_quality: float = 0.10 missing_train_penalty: float = 0.85 low_trade_penalty_threshold: int = 10 low_trade_penalty_factor: float = 0.85 class WalkForwardScoreWeights(BaseModel): """Weights for walk-forward robustness scoring.""" median_return: float = 0.25 mean_return: float = 0.15 worst_return: float = 0.15 positive_fold_rate: float = 0.15 profit_factor: float = 0.10 drawdown: float = 0.10 train_test_gap: float = 0.05 fold_count: float = 0.05 low_fold_penalty_threshold: int = 6 low_fold_penalty_factor: float = 0.85 class WFQSv2Weights(BaseModel): """Weights for walk-forward quality score v2 with multiplicative penalties.""" median_return: float = 0.25 mean_return: float = 0.15 worst_return: float = 0.20 positive_fold_rate: float = 0.15 profit_factor: float = 0.10 drawdown: float = 0.10 fold_count: float = 0.05 low_fold_penalty_threshold: int = 6 low_fold_penalty_factor: float = 0.85 recent_fold_quality: float = 0.20 recent_lookback_days: int = 365 recent_min_folds: int = 2 class DeploymentScoreWeights(BaseModel): """Weights for deployment-oriented scoring.""" rqs_quality: float = 0.45 wfqs_quality: float = 0.55 class SplitResult(BaseModel): """Metrics for a single backtest split (train/valid/test).""" run_id: str trade_count: int = 0 profit_factor: float | None = None total_return_pct: float | None = None annualized_return_pct: float | None = None win_rate: float | None = None max_drawdown_pct: float | None = None sharpe_ratio: float | None = None monthly_win_rate: float | None = None equity_curve_r_squared: float | None = None avg_gross_exposure_pct: float | None = None avg_net_exposure_pct: float | None = None days_in_market_pct: float | None = None class WalkForwardFoldResult(BaseModel): """Metrics and run metadata for one walk-forward fold.""" fold_index: int train_start: dt.date train_end: dt.date test_start: dt.date test_end: dt.date train_run_id: str test_run_id: str train_metrics: SplitResult test_metrics: SplitResult class WalkForwardAggregate(BaseModel): """Aggregate statistics over walk-forward folds.""" mean_return_pct: float | None = None median_return_pct: float | None = None worst_return_pct: float | None = None positive_fold_rate_pct: float | None = None mean_profit_factor: float | None = None mean_max_drawdown_pct: float | None = None mean_trade_count: float | None = None mean_win_rate: float | None = None class WalkForwardGapStats(BaseModel): """Train vs test drift statistics over walk-forward folds.""" mean_train_test_return_gap_pct: float | None = None worst_train_test_return_gap_pct: float | None = None fold_return_cv: float | None = None class WalkForwardSummary(BaseModel): """Full walk-forward validation summary.""" window_mode: str = "rolling_fixed" train_days: int test_days: int step_days: int fold_count: int folds: list[WalkForwardFoldResult] = Field(default_factory=list) train_aggregate: WalkForwardAggregate = Field(default_factory=WalkForwardAggregate) test_aggregate: WalkForwardAggregate = Field(default_factory=WalkForwardAggregate) gap_stats: WalkForwardGapStats = Field(default_factory=WalkForwardGapStats) engine_reliability_ratio: float | None = None class RobustnessHorizonSummary(BaseModel): """Aggregate statistics for one rolling horizon in the robustness matrix.""" horizon_days: int window_count: int mean_return_pct: float | None = None median_return_pct: float | None = None worst_return_pct: float | None = None positive_window_rate_pct: float | None = None mean_max_drawdown_pct: float | None = None class RobustnessMatrixSummary(BaseModel): """Compact summary of horizon/start-date robustness validation.""" window_mode: str = "rolling_horizon" horizons_days: list[int] = Field(default_factory=list) step_days: int overall_window_count: int = 0 overall_positive_window_rate_pct: float | None = None overall_worst_return_pct: float | None = None horizon_summaries: list[RobustnessHorizonSummary] = Field(default_factory=list) class CommonWindowSummary(BaseModel): """Continuous full-cycle run summary used for capital-growth comparisons.""" window_name: str = "common_window" snapshot_id: str = "" start_date: dt.date end_date: dt.date initial_equity: float = 10_000.0 run_id: str | None = None metrics: MetricsBundle class MultiCapitalCommonWindowSummary(BaseModel): """Comparable common-window summaries across several initial capital levels.""" capital_summaries: list[CommonWindowSummary] = Field(default_factory=list) class ResetCommonWindowSummary(BaseModel): """Path-neutral common-window summary built from reset-capital segments.""" window_name: str = "reset_common_window" snapshot_id: str = "" start_date: dt.date end_date: dt.date reset_initial_equity: float = 10_000.0 segment_days: int | None = None segment_summaries: list[CommonWindowSummary] = Field(default_factory=list) class ConfigDelta(BaseModel): """Records what changed from a baseline experiment.""" base_experiment: str changes: dict[str, str] = Field(default_factory=dict) class JournalEntry(BaseModel): """One improvement cycle entry in the journal.""" entry_id: str timestamp: str experiment_name: str hypothesis: str config_delta: ConfigDelta | None = None results: dict[str, SplitResult] = Field(default_factory=dict) # split_name → SplitResult walk_forward_summary: WalkForwardSummary | None = None robustness_matrix_summary: RobustnessMatrixSummary | None = None out_of_time_robustness_summary: RobustnessMatrixSummary | None = None common_window_summary: CommonWindowSummary | None = None reset_common_window_summary: ResetCommonWindowSummary | None = None multi_capital_common_window_summary: MultiCapitalCommonWindowSummary | None = None sqs_score: float | None = None sqs_breakdown: dict[str, float] = Field(default_factory=dict) sqs_v3_score: float | None = None sqs_v3_breakdown: dict[str, float] = Field(default_factory=dict) stress_sqs_score: float | None = None stress_sqs_breakdown: dict[str, float] = Field(default_factory=dict) sqs_v2_score: float | None = None sqs_v2_breakdown: dict[str, float] = Field(default_factory=dict) promotion_score: float | None = None promotion_breakdown: dict[str, float] = Field(default_factory=dict) unified_score: float | None = None unified_breakdown: dict[str, float] = Field(default_factory=dict) rqs_score: float | None = None rqs_breakdown: dict[str, float] = Field(default_factory=dict) wfqs_score: float | None = None wfqs_breakdown: dict[str, float] = Field(default_factory=dict) wfqs_v2_score: float | None = None wfqs_v2_breakdown: dict[str, float] = Field(default_factory=dict) deployment_score: float | None = None deployment_breakdown: dict[str, float] = Field(default_factory=dict) common_window_score: float | None = None common_window_breakdown: dict[str, float] = Field(default_factory=dict) reset_common_window_score: float | None = None reset_common_window_breakdown: dict[str, float] = Field(default_factory=dict) multi_capital_common_window_score: float | None = None multi_capital_common_window_breakdown: dict[str, float] = Field(default_factory=dict) scenario_robustness_score: float | None = None scenario_robustness_breakdown: dict[str, float] = Field(default_factory=dict) overfit_check_score: float | None = None overfit_check_breakdown: dict[str, float] = Field(default_factory=dict) verdict: str = "unknown" # better / worse / neutral / unknown verdict_reasoning: str = "" next_direction: str = "" tags: list[str] = Field(default_factory=list) class RegistryEntry(BaseModel): """A leaderboard row derived from a JournalEntry.""" entry_id: str experiment_name: str strategy_family: str = "other" is_retired: bool = False sqs_score: float | None = None sqs_v3_score: float | None = None stress_sqs_score: float | None = None sqs_v2_score: float | None = None promotion_score: float | None = None unified_score: float | None = None rqs_score: float | None = None wfqs_score: float | None = None wfqs_v2_score: float | None = None deployment_score: float | None = None common_window_score: float | None = None common_window_summary: CommonWindowSummary | None = None reset_common_window_score: float | None = None reset_common_window_summary: ResetCommonWindowSummary | None = None multi_capital_common_window_score: float | None = None multi_capital_common_window_summary: MultiCapitalCommonWindowSummary | None = None walk_forward_summary: WalkForwardSummary | None = None robustness_matrix_summary: RobustnessMatrixSummary | None = None out_of_time_robustness_summary: RobustnessMatrixSummary | None = None scenario_robustness_score: float | None = None overfit_check_score: float | None = None # train split metrics train_total_return_pct: float | None = None train_annualized_return_pct: float | None = None # test split metrics profit_factor: float | None = None total_return_pct: float | None = None annualized_return_pct: float | None = None win_rate: float | None = None sharpe_ratio: float | None = None max_drawdown_pct: float | None = None trade_count: int = 0 avg_gross_exposure_pct: float | None = None avg_net_exposure_pct: float | None = None days_in_market_pct: float | None = None # valid split metrics valid_profit_factor: float | None = None valid_total_return_pct: float | None = None valid_annualized_return_pct: float | None = None valid_win_rate: float | None = None valid_sharpe_ratio: float | None = None valid_max_drawdown_pct: float | None = None valid_trade_count: int = 0 valid_avg_gross_exposure_pct: float | None = None valid_avg_net_exposure_pct: float | None = None valid_days_in_market_pct: float | None = None timestamp: str = "" class ExperimentRegistry(BaseModel): """Full leaderboard data (regenerated from journal).""" entries: list[RegistryEntry] = Field(default_factory=list) updated_at: str = ""