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.

827 lines
32 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"
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_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_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
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
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)
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
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
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
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
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_positive_pnl_only: bool = True
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
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
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
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 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 get_strategy_engines(self) -> list[StrategyEngineConfig]:
"""Enabled strategy engines in manifest order."""
return [engine for engine in self.strategy_engines if engine.enabled]
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
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 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
sqs_score: float | None = None
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)
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_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
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 = ""