You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1435 lines
66 KiB
Python
1435 lines
66 KiB
Python
"""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"
|
|
TRAILING = "TRAILING"
|
|
KILL_SWITCH = "KILL_SWITCH"
|
|
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"
|
|
|
|
|
|
class BacktestMode(str, Enum):
|
|
RESEARCH = "research"
|
|
LIVE = "live"
|
|
|
|
|
|
class Candidate(BaseModel):
|
|
"""An eligible trade candidate derived from a Parquet snapshot row."""
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
event_id: str
|
|
symbol: str
|
|
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_per_trade_risk_pct: 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_direction: str = "long" # "long" or "short"
|
|
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
|
|
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
|
|
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
|
|
|
|
|
|
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
|
|
|
|
# Bootstrap confidence intervals (95%)
|
|
bootstrap_cis: dict[str, tuple[float, float] | None] = Field(default_factory=dict)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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,
|
|
},
|
|
# ── 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,
|
|
},
|
|
}
|
|
|
|
|
|
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", "qqq", "qqqm", "dynamic", "sgov"
|
|
cash_parking_defensive_symbol: str = "spy" # fallback defensive ETF for pair/regime parking ("spy", "spym")
|
|
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_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"
|
|
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
|
|
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
|
|
|
|
# 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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
per_trade_risk_pct_override: 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
|
|
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
|
|
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
|
|
parse_confidence_overall_min: float | None = None
|
|
parse_confidence_overall_max: float | None = None
|
|
macro_vix_min: float | None = None
|
|
macro_vix_max: 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_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
|
|
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
|
|
enabled: bool = True
|
|
# Macro short engine: generate SH candidate when composite risk score >= threshold
|
|
macro_short_risk_threshold: int | None = None
|
|
|
|
|
|
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 BacktestConfig(BaseModel):
|
|
strategy_name: str
|
|
dataset_snapshot_id: str
|
|
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)
|
|
event_type_profiles: dict[str, EventTypeProfile] = Field(default_factory=dict)
|
|
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()
|
|
|
|
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 in manifest order."""
|
|
resolved_engines: list[StrategyEngineConfig] = []
|
|
for engine in self.strategy_engines:
|
|
resolved = self.resolve_strategy_engine(engine)
|
|
if resolved.enabled:
|
|
resolved_engines.append(resolved)
|
|
return 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 | archived
|
|
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 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
|
|
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)
|
|
multi_capital_common_window_score: float | None = None
|
|
multi_capital_common_window_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
|
|
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
|
|
# 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 = ""
|