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.

3559 lines
140 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""Strategy improvement tracker: SQS computation, journal I/O, leaderboard."""
from __future__ import annotations
import contextlib
import datetime as dt
import functools
import fcntl
import json
import statistics
from pathlib import Path
from typing import Any, Callable, Iterator
from libs.backtest.experiments import normalize_experiment_status
from libs.backtest.domain import (
CommonWindowSummary,
ConfigDelta,
DeploymentScoreWeights,
ExperimentRegistry,
JournalEntry,
MetricsBundle,
MultiCapitalCommonWindowSummary,
ResetCommonWindowSummary,
RobustnessMatrixSummary,
WalkForwardScoreWeights,
WFQSv2Weights,
ReturnScoreWeights,
PromotionScoreWeights,
RegistryEntry,
SplitResult,
SQSWeights,
SQSv2Weights,
UnifiedScoreWeights,
WalkForwardAggregate,
WalkForwardSummary,
)
from libs.backtest.snapshots import resolve_snapshot
from libs.common.logging import get_logger
from libs.common.time_utils import utc_now
logger = get_logger(__name__)
_DEFAULT_WEIGHTS = SQSWeights()
_DEFAULT_V2_WEIGHTS = SQSv2Weights()
_DEFAULT_PROMOTION_WEIGHTS = PromotionScoreWeights()
_DEFAULT_UNIFIED_WEIGHTS = UnifiedScoreWeights()
_DEFAULT_RQS_WEIGHTS = ReturnScoreWeights()
_DEFAULT_WFQS_WEIGHTS = WalkForwardScoreWeights()
_DEFAULT_WFQS_V2_WEIGHTS = WFQSv2Weights()
_DEFAULT_DEPLOYMENT_WEIGHTS = DeploymentScoreWeights()
_DEFAULT_PUBLIC_COMMON_WINDOW_WEIGHT = 0.20
_DEFAULT_PUBLIC_COMMON_WINDOW_INITIAL_EQUITY = 10_000.0
_DEFAULT_PUBLIC_RESET_COMMON_WINDOW_WEIGHT = 0.20
_DEFAULT_PUBLIC_MULTI_CAPITAL_COMMON_WINDOW_WEIGHT = 0.20
_DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS = {
10_000.0: 0.60,
25_000.0: 0.25,
100_000.0: 0.15,
}
_DEFAULT_PUBLIC_MULTI_CAPITAL_FLOOR_BLEND = 0.10
_DEFAULT_ACTIVE_RESEARCH_ENTRY_ID_FLOOR = 606 # IMP-0606 == return_max_long_v6new.29
_RETIRED_STRATEGY_FAMILIES = {
"short_core",
"legacy_pead",
"leveraged_return_max_long",
"exact_pocket_return_max_long",
"named_micro_return_max_long",
}
_EXPERIMENTS_DIR = Path("configs/experiments")
_NAMED_MICRO_ENGINE_TOKENS = (
"epam_micro",
"nrg_np12",
"glw_np11",
"pl_micro",
"exas_micro",
"aap_micro",
"apld_micro",
"czr_hot_micro",
)
# ---------------------------------------------------------------------------
# SQS computation (pure functions, no I/O)
# ---------------------------------------------------------------------------
def _normalize(value: float | None, low: float, high: float) -> float:
"""Linear normalise *value* to 0-100 between *low* (0 pts) and *high* (100 pts)."""
if value is None:
return 0.0
if high == low:
return 50.0
score = (value - low) / (high - low) * 100.0
return max(0.0, min(100.0, score))
def _normalize_inverse(value: float | None, low: float, high: float) -> float:
"""Like _normalize but lower values are better (e.g. drawdown)."""
if value is None:
return 0.0
if high == low:
return 50.0
# low = worst (0 pts), high = best (100 pts) — but for inverse metrics
# low value = good, high value = bad. Swap the interpretation.
score = (low - value) / (low - high) * 100.0
return max(0.0, min(100.0, score))
def _normalize_band(
value: float | None,
low_bad: float,
low_good: float,
high_good: float,
high_bad: float,
) -> float:
"""Score 0-100 with an optimal middle band."""
if value is None:
return 0.0
if value <= low_bad or value >= high_bad:
return 0.0
if low_good <= value <= high_good:
return 100.0
if value < low_good:
return (value - low_bad) / (low_good - low_bad) * 100.0
return (high_bad - value) / (high_bad - high_good) * 100.0
def _calibrate_sqs(value: float | None) -> float | None:
"""Compress raw integrated scores into a harsher absolute-looking range."""
if value is None:
return None
return round(max(0.0, value * 0.70 - 7.5), 1)
def _safe_ratio(numerator: float | None, denominator: float | None) -> float | None:
if numerator is None or denominator is None or denominator <= 0:
return None
return numerator / denominator
def _is_public_common_window_comparable(
common_window_summary: CommonWindowSummary | None,
) -> bool:
if common_window_summary is None:
return False
return abs(common_window_summary.initial_equity - _DEFAULT_PUBLIC_COMMON_WINDOW_INITIAL_EQUITY) < 1e-9
def _is_public_multi_capital_common_window_comparable(
multi_capital_common_window_summary: MultiCapitalCommonWindowSummary | None,
) -> bool:
if multi_capital_common_window_summary is None:
return False
seen_capitals = {
round(summary.initial_equity, 2)
for summary in multi_capital_common_window_summary.capital_summaries
}
required = {round(capital, 2) for capital in _DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS}
return required.issubset(seen_capitals)
def _is_public_reset_common_window_comparable(
reset_common_window_summary: ResetCommonWindowSummary | None,
) -> bool:
if reset_common_window_summary is None:
return False
if (
abs(
reset_common_window_summary.reset_initial_equity
- _DEFAULT_PUBLIC_COMMON_WINDOW_INITIAL_EQUITY
)
>= 1e-9
):
return False
if not reset_common_window_summary.segment_summaries:
return False
return all(
abs(segment.initial_equity - _DEFAULT_PUBLIC_COMMON_WINDOW_INITIAL_EQUITY) < 1e-9
for segment in reset_common_window_summary.segment_summaries
)
def _apply_single_split_penalty(value: float | None) -> float | None:
"""Discount scores that have no valid/test confirmation pair."""
if value is None:
return None
return round(value * 0.80, 1)
def compute_sqs(
metrics: MetricsBundle,
weights: SQSWeights | None = None,
) -> tuple[float, dict[str, float]]:
"""Compute Strategy Quality Score from test-split metrics.
Returns (sqs_score, breakdown_dict).
"""
w = weights or _DEFAULT_WEIGHTS
# --- Profitability (40%) ---
pf_score = _normalize(metrics.profit_factor, low=0.8, high=2.0)
ret_score = _normalize(metrics.total_return_pct, low=-5.0, high=5.0)
profitability = pf_score * 0.6 + ret_score * 0.4
# --- Risk (25%) ---
dd_score = _normalize_inverse(metrics.max_drawdown_pct, low=10.0, high=1.0)
sharpe_score = _normalize(metrics.sharpe_ratio, low=-1.0, high=2.0)
risk = dd_score * 0.5 + sharpe_score * 0.5
# --- Consistency (20%) ---
wr_score = _normalize(metrics.win_rate, low=0.35, high=0.65)
mwr_score = _normalize(metrics.monthly_win_rate, low=0.30, high=0.70)
consistency = wr_score * 0.5 + mwr_score * 0.5
# --- Robustness (15%) ---
r2_score = _normalize(metrics.equity_curve_r_squared, low=0.0, high=0.80)
tc_score = _normalize(float(metrics.trade_count), low=10.0, high=100.0)
robustness = r2_score * 0.5 + tc_score * 0.5
# Weighted total
sqs = (
profitability * w.profitability
+ risk * w.risk
+ consistency * w.consistency
+ robustness * w.robustness
)
# Low-trade penalty
if metrics.trade_count < w.low_trade_penalty_threshold:
sqs *= w.low_trade_penalty_factor
sqs = round(sqs, 1)
breakdown = {
"profitability": round(profitability, 1),
"risk": round(risk, 1),
"consistency": round(consistency, 1),
"robustness": round(robustness, 1),
}
return sqs, breakdown
def compute_sqs_v2(
metrics: MetricsBundle,
weights: SQSv2Weights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute SQS v2, adding a capital-efficiency sleeve to the score."""
if metrics.avg_gross_exposure_pct is None or metrics.days_in_market_pct is None:
return None, {}
w = weights or _DEFAULT_V2_WEIGHTS
pf_score = _normalize(metrics.profit_factor, low=0.8, high=2.0)
ret_score = _normalize(metrics.total_return_pct, low=-5.0, high=5.0)
profitability = pf_score * 0.6 + ret_score * 0.4
dd_score = _normalize_inverse(metrics.max_drawdown_pct, low=10.0, high=1.0)
sharpe_score = _normalize(metrics.sharpe_ratio, low=-1.0, high=2.0)
risk = dd_score * 0.5 + sharpe_score * 0.5
wr_score = _normalize(metrics.win_rate, low=0.35, high=0.65)
mwr_score = _normalize(metrics.monthly_win_rate, low=0.30, high=0.70)
consistency = wr_score * 0.5 + mwr_score * 0.5
r2_score = _normalize(metrics.equity_curve_r_squared, low=0.0, high=0.80)
tc_score = _normalize(float(metrics.trade_count), low=10.0, high=100.0)
robustness = r2_score * 0.5 + tc_score * 0.5
return_on_gross_exposure = None
if metrics.total_return_pct is not None and metrics.avg_gross_exposure_pct > 0:
return_on_gross_exposure = metrics.total_return_pct / metrics.avg_gross_exposure_pct
roe_score = _normalize(return_on_gross_exposure, low=0.0, high=0.50)
dim_score = _normalize_band(
metrics.days_in_market_pct,
low_bad=10.0,
low_good=40.0,
high_good=80.0,
high_bad=100.0,
)
capital_efficiency = roe_score * 0.7 + dim_score * 0.3
sqs_v2 = (
profitability * w.profitability
+ risk * w.risk
+ consistency * w.consistency
+ robustness * w.robustness
+ capital_efficiency * w.capital_efficiency
)
if metrics.trade_count < w.low_trade_penalty_threshold:
sqs_v2 *= w.low_trade_penalty_factor
sqs_v2 = round(sqs_v2, 1)
breakdown = {
"profitability": round(profitability, 1),
"risk": round(risk, 1),
"consistency": round(consistency, 1),
"robustness": round(robustness, 1),
"capital_efficiency": round(capital_efficiency, 1),
}
return sqs_v2, breakdown
# ---------------------------------------------------------------------------
# Helper builders
# ---------------------------------------------------------------------------
def build_split_result(split_name: str, run_id: str, metrics: MetricsBundle) -> SplitResult:
"""Create a SplitResult from MetricsBundle."""
return SplitResult(
run_id=run_id,
trade_count=metrics.trade_count,
profit_factor=metrics.profit_factor,
total_return_pct=metrics.total_return_pct,
annualized_return_pct=metrics.annualized_return_pct,
win_rate=metrics.win_rate,
max_drawdown_pct=metrics.max_drawdown_pct,
sharpe_ratio=metrics.sharpe_ratio,
monthly_win_rate=metrics.monthly_win_rate,
equity_curve_r_squared=metrics.equity_curve_r_squared,
avg_gross_exposure_pct=metrics.avg_gross_exposure_pct,
avg_net_exposure_pct=metrics.avg_net_exposure_pct,
days_in_market_pct=metrics.days_in_market_pct,
)
def _metrics_from_split_result(result: SplitResult | None) -> MetricsBundle | None:
if result is None:
return None
payload = result.model_dump(exclude={"run_id"})
return MetricsBundle.model_validate(payload)
_metrics_path_index: dict[str, Path] | None = None
def _get_metrics_path_index() -> dict[str, Path]:
"""Build run_id -> metrics_summary.json path index (single rglob)."""
global _metrics_path_index
if _metrics_path_index is not None:
return _metrics_path_index
_metrics_path_index = {}
runs_root = Path("runs")
for metrics_path in runs_root.rglob("metrics/metrics_summary.json"):
# run_dir is metrics_path.parent.parent (e.g. runs/.../run_id/)
run_dir = metrics_path.parent.parent
run_dir_name = run_dir.name
# Index by directory name as run_id
if run_dir_name not in _metrics_path_index or metrics_path.stat().st_mtime > _metrics_path_index[run_dir_name].stat().st_mtime:
_metrics_path_index[run_dir_name] = metrics_path
return _metrics_path_index
@functools.lru_cache(maxsize=2048)
def _load_run_metrics_summary(run_id: str) -> dict[str, Any] | None:
runs_root = Path("runs")
metrics_path = runs_root / run_id / "metrics" / "metrics_summary.json"
if not metrics_path.exists():
idx = _get_metrics_path_index()
found = idx.get(run_id)
if not found:
return None
metrics_path = found
summary = json.loads(metrics_path.read_text())
needed_fields = (
"annualized_return_pct",
"avg_gross_exposure_pct",
"avg_net_exposure_pct",
"days_in_market_pct",
)
if all(summary.get(field) is not None for field in needed_fields):
return summary
equity_curve_path = metrics_path.parent.parent / "artifacts" / "daily_equity_curve.parquet"
if not equity_curve_path.exists():
return summary
try:
import pyarrow.parquet as pq
rows = pq.read_table(
equity_curve_path,
columns=["equity", "gross_exposure", "net_exposure"],
).to_pylist()
except Exception:
return summary
exposure_rows = [row for row in rows if float(row["equity"]) > 0]
if not exposure_rows:
return summary
if summary.get("avg_gross_exposure_pct") is None:
summary["avg_gross_exposure_pct"] = sum(
float(row["gross_exposure"]) / float(row["equity"]) * 100.0
for row in exposure_rows
) / len(exposure_rows)
if summary.get("avg_net_exposure_pct") is None:
summary["avg_net_exposure_pct"] = sum(
float(row["net_exposure"]) / float(row["equity"]) * 100.0
for row in exposure_rows
) / len(exposure_rows)
if summary.get("days_in_market_pct") is None:
summary["days_in_market_pct"] = (
sum(1 for row in rows if float(row["gross_exposure"]) > 0) / len(rows) * 100.0
)
return summary
def _hydrate_split_result(result: SplitResult | None) -> SplitResult | None:
if result is None:
return None
needed_fields = (
"annualized_return_pct",
"avg_gross_exposure_pct",
"avg_net_exposure_pct",
"days_in_market_pct",
)
if all(getattr(result, field) is not None for field in needed_fields):
return result
summary = _load_run_metrics_summary(result.run_id)
if summary is None:
return result
updates = {
field: summary.get(field)
for field in needed_fields
if getattr(result, field) is None and summary.get(field) is not None
}
if not updates:
return result
return result.model_copy(update=updates)
def _compute_split_quality_score(
result: SplitResult | None,
) -> tuple[float | None, dict[str, float], str | None]:
metrics = _metrics_from_split_result(_hydrate_split_result(result))
if metrics is None:
return None, {}, None
sqs_v2_score, sqs_v2_breakdown = compute_sqs_v2(metrics)
if sqs_v2_score is not None:
return sqs_v2_score, sqs_v2_breakdown, "sqs_v2"
sqs_score, sqs_breakdown = compute_sqs(metrics)
return sqs_score, sqs_breakdown, "sqs"
def compute_unified_split_quality(
metrics: MetricsBundle,
weights: UnifiedScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute a stricter split-level quality score."""
if metrics.avg_gross_exposure_pct is None or metrics.days_in_market_pct is None:
return None, {}
w = weights or _DEFAULT_UNIFIED_WEIGHTS
pf_score = _normalize(metrics.profit_factor, low=1.0, high=3.0)
ret_score = _normalize(metrics.total_return_pct, low=0.0, high=3.0)
profitability = pf_score * 0.5 + ret_score * 0.5
dd_score = _normalize_inverse(metrics.max_drawdown_pct, low=5.0, high=0.5)
sharpe_score = _normalize(metrics.sharpe_ratio, low=0.0, high=3.5)
risk = dd_score * 0.5 + sharpe_score * 0.5
wr_score = _normalize(metrics.win_rate, low=0.50, high=0.65)
mwr_score = _normalize(metrics.monthly_win_rate, low=0.45, high=0.75)
consistency = wr_score * 0.5 + mwr_score * 0.5
r2_score = _normalize(metrics.equity_curve_r_squared, low=0.10, high=0.90)
tc_score = _normalize(float(metrics.trade_count), low=20.0, high=80.0)
robustness = r2_score * 0.5 + tc_score * 0.5
return_on_gross_exposure = None
if metrics.total_return_pct is not None and metrics.avg_gross_exposure_pct > 0:
return_on_gross_exposure = metrics.total_return_pct / metrics.avg_gross_exposure_pct
roe_score = _normalize(return_on_gross_exposure, low=0.05, high=0.40)
dim_score = _normalize_band(
metrics.days_in_market_pct,
low_bad=20.0,
low_good=45.0,
high_good=75.0,
high_bad=90.0,
)
capital_efficiency = roe_score * 0.7 + dim_score * 0.3
split_quality = (
profitability * w.split_profitability
+ risk * w.split_risk
+ consistency * w.split_consistency
+ robustness * w.split_robustness
+ capital_efficiency * w.split_capital_efficiency
)
if metrics.trade_count < 20:
split_quality *= 0.75
split_quality = round(split_quality, 1)
breakdown = {
"profitability": round(profitability, 1),
"risk": round(risk, 1),
"consistency": round(consistency, 1),
"robustness": round(robustness, 1),
"capital_efficiency": round(capital_efficiency, 1),
}
return split_quality, breakdown
def compute_promotion_score(
test_result: SplitResult | None,
valid_result: SplitResult | None,
weights: PromotionScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute a promotion score using valid/test quality and a floor term."""
test_score, _, _ = _compute_split_quality_score(test_result)
valid_score, _, _ = _compute_split_quality_score(valid_result)
if test_score is None or valid_score is None:
return None, {}
w = weights or _DEFAULT_PROMOTION_WEIGHTS
floor_score = min(test_score, valid_score)
promotion_score = (
valid_score * w.valid_quality
+ test_score * w.test_quality
+ floor_score * w.floor_quality
)
breakdown = {
"valid_quality": round(valid_score, 1),
"test_quality": round(test_score, 1),
"floor_quality": round(floor_score, 1),
}
return round(promotion_score, 1), breakdown
def compute_unified_score(
test_result: SplitResult | None,
valid_result: SplitResult | None,
weights: UnifiedScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute one integrated score for real promotion decisions."""
test_result = _hydrate_split_result(test_result)
valid_result = _hydrate_split_result(valid_result)
test_metrics = _metrics_from_split_result(test_result)
valid_metrics = _metrics_from_split_result(valid_result)
if test_metrics is None or valid_metrics is None:
return None, {}
w = weights or _DEFAULT_UNIFIED_WEIGHTS
test_quality, _ = compute_unified_split_quality(test_metrics, w)
valid_quality, _ = compute_unified_split_quality(valid_metrics, w)
if test_quality is None or valid_quality is None:
return None, {}
floor_quality = min(test_quality, valid_quality)
gap_quality = _normalize_inverse(abs(valid_quality - test_quality), low=35.0, high=5.0)
unified_score = (
valid_quality * w.valid_quality
+ test_quality * w.test_quality
+ floor_quality * w.floor_quality
+ gap_quality * w.gap_quality
)
breakdown = {
"valid_quality": round(valid_quality, 1),
"test_quality": round(test_quality, 1),
"floor_quality": round(floor_quality, 1),
"gap_quality": round(gap_quality, 1),
}
return _calibrate_sqs(unified_score), breakdown
def compute_public_sqs(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
common_window_summary: CommonWindowSummary | None = None,
reset_common_window_summary: ResetCommonWindowSummary | None = None,
multi_capital_common_window_summary: MultiCapitalCommonWindowSummary | None = None,
rqs_score: float | None = None,
wfqs_score: float | None = None,
deployment_score: float | None = None,
scenario_robustness_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return the current public-facing SQS.
Default public ranking uses the v9 3-pillar additive core:
core = RQS × 0.35 + WFQS_v2 × 0.40 + regime_score × 0.25
regime_score comes from scenario-test RRS, falling back to OOT quality
or 50.0 (neutral). Reset common-window blend applied when available.
"""
return compute_public_sqs_v9(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
reset_common_window_summary=reset_common_window_summary,
scenario_robustness_score=scenario_robustness_score,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_score,
)
def _compute_return_split_score(
metrics: MetricsBundle,
split_name: str,
weights: ReturnScoreWeights | None = None,
) -> tuple[float, dict[str, float]]:
"""Compute a return-max score for a single split."""
w = weights or _DEFAULT_RQS_WEIGHTS
return_bands = {
"train": (0.0, 50.0),
"valid": (0.0, 30.0),
"test": (0.0, 35.0),
}
annualized_return_bands = {
"train": (0.0, 25.0),
"valid": (0.0, 120.0),
"test": (0.0, 120.0),
}
dd_bands = {
"train": (20.0, 2.0),
"valid": (12.0, 2.0),
"test": (12.0, 2.0),
}
ret_low, ret_high = return_bands.get(split_name, (0.0, 35.0))
ann_low, ann_high = annualized_return_bands.get(split_name, (0.0, 100.0))
dd_low, dd_high = dd_bands.get(split_name, (12.0, 2.0))
total_return_score = _normalize(metrics.total_return_pct, low=ret_low, high=ret_high)
annualized_return_score = _normalize(metrics.annualized_return_pct, low=ann_low, high=ann_high)
effective_profit_factor = metrics.profit_factor
if (
effective_profit_factor is None
and metrics.trade_count > 0
and metrics.win_rate is not None
and metrics.win_rate >= 0.999
):
# Backtest summaries emit ``None`` when there are no losing trades.
# For return-max ranking that should be treated as capped-best, not zero.
effective_profit_factor = 3.0
pf_score = _normalize(effective_profit_factor, low=1.0, high=3.0)
sharpe_score = _normalize(metrics.sharpe_ratio, low=0.0, high=3.5)
drawdown_score = _normalize_inverse(metrics.max_drawdown_pct, low=dd_low, high=dd_high)
gross_score = _normalize_band(
metrics.avg_gross_exposure_pct,
low_bad=2.0,
low_good=8.0,
high_good=40.0,
high_bad=75.0,
)
dim_score = _normalize_band(
metrics.days_in_market_pct,
low_bad=5.0,
low_good=15.0,
high_good=60.0,
high_bad=90.0,
)
return_on_gross_exposure = None
if metrics.total_return_pct is not None and metrics.avg_gross_exposure_pct and metrics.avg_gross_exposure_pct > 0:
return_on_gross_exposure = metrics.total_return_pct / metrics.avg_gross_exposure_pct
return_on_gross_score = _normalize(return_on_gross_exposure, low=0.20, high=2.50)
split_score = (
total_return_score * w.split_total_return
+ annualized_return_score * w.split_annualized_return
+ pf_score * w.split_profitability
+ sharpe_score * w.split_sharpe
+ drawdown_score * w.split_drawdown
+ return_on_gross_score * w.split_return_on_gross
+ gross_score * w.split_gross_exposure
+ dim_score * w.split_days_in_market
)
if metrics.trade_count < w.low_trade_penalty_threshold:
split_score *= w.low_trade_penalty_factor
split_score = round(split_score, 1)
breakdown = {
"total_return": round(total_return_score, 1),
"annualized_return": round(annualized_return_score, 1),
"profitability": round(pf_score, 1),
"sharpe": round(sharpe_score, 1),
"drawdown": round(drawdown_score, 1),
"return_on_gross": round(return_on_gross_score, 1),
"gross_exposure": round(gross_score, 1),
"days_in_market": round(dim_score, 1),
}
return split_score, breakdown
def compute_rqs(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
weights: ReturnScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute Return Quality Score for return-max strategy ranking."""
w = weights or _DEFAULT_RQS_WEIGHTS
hydrated = {
"train": _hydrate_split_result(train_result),
"valid": _hydrate_split_result(valid_result),
"test": _hydrate_split_result(test_result),
}
split_scores: dict[str, float] = {}
for split_name, result in hydrated.items():
metrics = _metrics_from_split_result(result)
if metrics is None:
continue
split_score, _ = _compute_return_split_score(metrics, split_name, w)
split_scores[split_name] = split_score
if "valid" not in split_scores or "test" not in split_scores:
return None, {}
weighted_sum = 0.0
weighted_den = 0.0
if "train" in split_scores:
weighted_sum += split_scores["train"] * w.train_quality
weighted_den += w.train_quality
weighted_sum += split_scores["valid"] * w.valid_quality
weighted_den += w.valid_quality
weighted_sum += split_scores["test"] * w.test_quality
weighted_den += w.test_quality
split_quality = weighted_sum / weighted_den if weighted_den > 0 else 0.0
floor_quality = min(split_scores.values())
gap_quality = _normalize_inverse(
max(split_scores.values()) - min(split_scores.values()),
low=60.0,
high=10.0,
)
rqs = split_quality * (1.0 - w.floor_quality - w.gap_quality) + floor_quality * w.floor_quality + gap_quality * w.gap_quality
if "train" not in split_scores:
rqs *= w.missing_train_penalty
breakdown = {
"train_quality": round(split_scores.get("train", 0.0), 1),
"valid_quality": round(split_scores["valid"], 1),
"test_quality": round(split_scores["test"], 1),
"floor_quality": round(floor_quality, 1),
"gap_quality": round(gap_quality, 1),
}
return round(rqs, 1), breakdown
def compute_wfqs(
walk_forward_summary: WalkForwardSummary | None,
weights: WalkForwardScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute a walk-forward robustness score from fold-level aggregates."""
if walk_forward_summary is None or walk_forward_summary.fold_count <= 0:
return None, {}
w = weights or _DEFAULT_WFQS_WEIGHTS
test = walk_forward_summary.test_aggregate
gap = walk_forward_summary.gap_stats
median_return_score = _normalize(test.median_return_pct, low=0.0, high=25.0)
mean_return_score = _normalize(test.mean_return_pct, low=0.0, high=25.0)
worst_return_score = _normalize(test.worst_return_pct, low=-5.0, high=10.0)
positive_fold_rate_score = _normalize(test.positive_fold_rate_pct, low=50.0, high=100.0)
profit_factor_score = _normalize(test.mean_profit_factor, low=1.0, high=4.0)
drawdown_score = _normalize_inverse(test.mean_max_drawdown_pct, low=15.0, high=2.0)
gap_score = _normalize_inverse(gap.mean_train_test_return_gap_pct, low=70.0, high=15.0)
fold_count_score = _normalize(float(walk_forward_summary.fold_count), low=4.0, high=10.0)
wfqs = (
median_return_score * w.median_return
+ mean_return_score * w.mean_return
+ worst_return_score * w.worst_return
+ positive_fold_rate_score * w.positive_fold_rate
+ profit_factor_score * w.profit_factor
+ drawdown_score * w.drawdown
+ gap_score * w.train_test_gap
+ fold_count_score * w.fold_count
)
if walk_forward_summary.fold_count < w.low_fold_penalty_threshold:
wfqs *= w.low_fold_penalty_factor
breakdown = {
"median_return": round(median_return_score, 1),
"mean_return": round(mean_return_score, 1),
"worst_return": round(worst_return_score, 1),
"positive_fold_rate": round(positive_fold_rate_score, 1),
"profit_factor": round(profit_factor_score, 1),
"drawdown": round(drawdown_score, 1),
"train_test_gap": round(gap_score, 1),
"fold_count": round(fold_count_score, 1),
}
return round(wfqs, 1), breakdown
# ---------------------------------------------------------------------------
# WFQS v2: multiplicative penalty model
# ---------------------------------------------------------------------------
def _gap_penalty(gap_pct: float | None) -> float:
"""Multiplicative penalty based on mean train-test gap percentage."""
if gap_pct is None:
return 1.0
if gap_pct <= 30.0:
return 1.0
if gap_pct <= 100.0:
return 1.0 - (gap_pct - 30.0) / (100.0 - 30.0) * 0.4 # 1.0 → 0.6
if gap_pct <= 300.0:
return 0.6 - (gap_pct - 100.0) / (300.0 - 100.0) * 0.3 # 0.6 → 0.3
return 0.2
def _overfitting_penalty(walk_forward_summary: "WalkForwardSummary") -> float:
"""Win-rate-based overfitting penalty replacing the return-gap penalty.
Raw return gap (train - test) is a poor overfitting signal because train
windows are longer than test windows (e.g. 252d vs 63d), so train returns
naturally accumulate more even without any overfitting. Win rate is
time-independent: it measures per-trade accuracy, which genuinely degrades
when a strategy has memorised training data.
Thresholds (percentage-point difference, train_wr - test_wr):
<= 3pp : no penalty — within statistical noise for 5-15 trades/fold
3-10pp : 1.00 → 0.85 — mild concern
10-20pp : 0.85 → 0.60 — meaningful quality degradation
> 20pp : 0.50 — severe overfitting signal
Falls back to _gap_penalty when aggregate win rates are unavailable
(old journal entries predating train_aggregate.mean_win_rate).
"""
train_wr = walk_forward_summary.train_aggregate.mean_win_rate
test_wr = walk_forward_summary.test_aggregate.mean_win_rate
if train_wr is None or test_wr is None:
return _gap_penalty(walk_forward_summary.gap_stats.mean_train_test_return_gap_pct)
wr_gap_pp = (train_wr - test_wr) * 100.0
if wr_gap_pp <= 3.0:
return 1.0
if wr_gap_pp <= 10.0:
return 1.0 - (wr_gap_pp - 3.0) / 7.0 * 0.15 # 1.00 → 0.85
if wr_gap_pp <= 20.0:
return 0.85 - (wr_gap_pp - 10.0) / 10.0 * 0.25 # 0.85 → 0.60
return 0.5
def _fold_variance_penalty(fold_return_cv: float | None) -> float:
"""Multiplicative penalty based on CV of fold test returns."""
if fold_return_cv is None:
return 1.0
if fold_return_cv <= 0.5:
return 1.0
if fold_return_cv <= 1.5:
return 1.0 - (fold_return_cv - 0.5) / (1.5 - 0.5) * 0.3 # 1.0 → 0.7
return 0.6
def _trade_credibility(mean_trades: float | None, mean_win_rate: float | None) -> float:
"""Multiplicative penalty for low trade counts or suspiciously high win rates."""
if mean_trades is None:
return 0.5
if mean_trades < 5:
factor = 0.5
elif mean_trades < 10:
factor = 0.7
elif mean_trades < 20:
factor = 0.85
else:
factor = 1.0
if (mean_win_rate or 0.0) > 0.95 and mean_trades < 15:
factor *= 0.8
return factor
def _engine_reliability_penalty(ratio: float | None) -> float:
"""Multiplicative penalty based on engine reliability ratio."""
if ratio is None:
return 1.0
if ratio >= 0.7:
return 1.0
if ratio >= 0.3:
return 0.5 + (ratio - 0.3) / (0.7 - 0.3) * 0.5 # 0.5 → 1.0
return 0.4
def _derive_fold_stats(
walk_forward_summary: WalkForwardSummary,
) -> tuple[float | None, float | None, float | None]:
"""Derive mean_trade_count, mean_win_rate, fold_return_cv from folds when aggregate fields are missing."""
import statistics as _stats
folds = walk_forward_summary.folds
if not folds:
return None, None, None
trade_counts = [float(f.test_metrics.trade_count) for f in folds]
win_rates = [f.test_metrics.win_rate for f in folds if f.test_metrics.win_rate is not None]
returns = [f.test_metrics.total_return_pct for f in folds if f.test_metrics.total_return_pct is not None]
mean_tc = round(_stats.mean(trade_counts), 1) if trade_counts else None
mean_wr = round(_stats.mean(win_rates), 4) if win_rates else None
cv = None
if len(returns) >= 2:
mean_ret = _stats.mean(returns)
if abs(mean_ret) > 1e-9:
cv = round(_stats.stdev(returns) / abs(mean_ret), 3)
return mean_tc, mean_wr, cv
def _wfqs_v2_quality_from_aggregate(
aggregate: WalkForwardAggregate,
fold_count: int,
weights: WFQSv2Weights,
) -> float:
"""Compute additive WFQS v2 quality score before multiplicative penalties."""
median_return_score = _normalize(aggregate.median_return_pct, low=0.0, high=25.0)
mean_return_score = _normalize(aggregate.mean_return_pct, low=0.0, high=25.0)
worst_return_score = _normalize(aggregate.worst_return_pct, low=-5.0, high=10.0)
positive_fold_rate_score = _normalize(aggregate.positive_fold_rate_pct, low=50.0, high=100.0)
profit_factor_score = _normalize(aggregate.mean_profit_factor, low=1.0, high=4.0)
drawdown_score = _normalize_inverse(aggregate.mean_max_drawdown_pct, low=15.0, high=2.0)
fold_count_score = _normalize(float(fold_count), low=4.0, high=10.0)
quality = (
median_return_score * weights.median_return
+ mean_return_score * weights.mean_return
+ worst_return_score * weights.worst_return
+ positive_fold_rate_score * weights.positive_fold_rate
+ profit_factor_score * weights.profit_factor
+ drawdown_score * weights.drawdown
+ fold_count_score * weights.fold_count
)
if fold_count < weights.low_fold_penalty_threshold:
quality *= weights.low_fold_penalty_factor
return quality
def _build_recent_fold_aggregate(
walk_forward_summary: WalkForwardSummary,
lookback_days: int,
min_folds: int,
) -> tuple[WalkForwardAggregate | None, int]:
"""Aggregate test-fold quality for folds ending within the recent lookback window."""
import statistics as _stats
folds = walk_forward_summary.folds
if not folds:
return None, 0
anchor = max(f.test_end for f in folds)
cutoff = anchor - dt.timedelta(days=max(lookback_days - 1, 0))
recent_folds = [f for f in folds if f.test_end >= cutoff]
if len(recent_folds) < min_folds:
return None, len(recent_folds)
returns = [f.test_metrics.total_return_pct for f in recent_folds if f.test_metrics.total_return_pct is not None]
pfs = [f.test_metrics.profit_factor for f in recent_folds if f.test_metrics.profit_factor is not None]
dds = [f.test_metrics.max_drawdown_pct for f in recent_folds if f.test_metrics.max_drawdown_pct is not None]
trade_counts = [float(f.test_metrics.trade_count) for f in recent_folds]
win_rates = [f.test_metrics.win_rate for f in recent_folds if f.test_metrics.win_rate is not None]
positive = [r for r in returns if r > 0]
aggregate = WalkForwardAggregate(
mean_return_pct=round(_stats.mean(returns), 2) if returns else None,
median_return_pct=round(_stats.median(returns), 2) if returns else None,
worst_return_pct=round(min(returns), 2) if returns else None,
positive_fold_rate_pct=round(len(positive) / len(recent_folds) * 100.0, 1) if recent_folds else None,
mean_profit_factor=round(_stats.mean(pfs), 2) if pfs else None,
mean_max_drawdown_pct=round(_stats.mean(dds), 2) if dds else None,
mean_trade_count=round(_stats.mean(trade_counts), 1) if trade_counts else None,
mean_win_rate=round(_stats.mean(win_rates), 4) if win_rates else None,
)
return aggregate, len(recent_folds)
def _public_activity_factor(
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None,
) -> tuple[float, dict[str, float]]:
"""Return a light multiplicative penalty for strategies that barely trade."""
if test_result is None or walk_forward_summary is None:
return 1.0, {}
mean_tc = walk_forward_summary.test_aggregate.mean_trade_count
if mean_tc is None:
derived_tc, _, _ = _derive_fold_stats(walk_forward_summary)
mean_tc = derived_tc
test_trade_score = _normalize(float(test_result.trade_count), low=8.0, high=24.0)
wf_trade_score = _normalize(mean_tc, low=3.0, high=10.0)
dim_score = _normalize(test_result.days_in_market_pct, low=20.0, high=60.0)
activity_quality = (
test_trade_score * 0.45
+ wf_trade_score * 0.35
+ dim_score * 0.20
)
if test_result.trade_count < 10:
activity_quality *= 0.85
if (mean_tc or 0.0) < 4.0:
activity_quality *= 0.85
factor = 0.50 + 0.50 * (activity_quality / 100.0)
breakdown = {
"activity_factor": round(factor, 2),
"activity_quality": round(activity_quality, 1),
"activity_test_trades": round(test_trade_score, 1),
"activity_wf_mean_trades": round(wf_trade_score, 1),
"activity_days_in_market": round(dim_score, 1),
}
return factor, breakdown
def compute_wfqs_v2(
walk_forward_summary: WalkForwardSummary | None,
weights: WFQSv2Weights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute WFQS v2 with multiplicative overfitting penalties."""
if walk_forward_summary is None or walk_forward_summary.fold_count <= 0:
return None, {}
w = weights or _DEFAULT_WFQS_V2_WEIGHTS
test = walk_forward_summary.test_aggregate
gap = walk_forward_summary.gap_stats
# Derive missing stats from folds when aggregate fields are absent
mean_tc = test.mean_trade_count
mean_wr = test.mean_win_rate
fold_cv = gap.fold_return_cv
if mean_tc is None or mean_wr is None or fold_cv is None:
derived_tc, derived_wr, derived_cv = _derive_fold_stats(walk_forward_summary)
if mean_tc is None:
mean_tc = derived_tc
if mean_wr is None:
mean_wr = derived_wr
if fold_cv is None:
fold_cv = derived_cv
overall_quality = _wfqs_v2_quality_from_aggregate(
test,
walk_forward_summary.fold_count,
w,
)
median_return_score = _normalize(test.median_return_pct, low=0.0, high=25.0)
mean_return_score = _normalize(test.mean_return_pct, low=0.0, high=25.0)
worst_return_score = _normalize(test.worst_return_pct, low=-5.0, high=10.0)
positive_fold_rate_score = _normalize(test.positive_fold_rate_pct, low=50.0, high=100.0)
profit_factor_score = _normalize(test.mean_profit_factor, low=1.0, high=4.0)
drawdown_score = _normalize_inverse(test.mean_max_drawdown_pct, low=15.0, high=2.0)
fold_count_score = _normalize(float(walk_forward_summary.fold_count), low=4.0, high=10.0)
recent_aggregate, recent_fold_count = _build_recent_fold_aggregate(
walk_forward_summary,
lookback_days=w.recent_lookback_days,
min_folds=w.recent_min_folds,
)
recent_quality = None
if recent_aggregate is not None:
recent_quality = _wfqs_v2_quality_from_aggregate(
recent_aggregate,
recent_fold_count,
w,
)
if recent_quality is not None:
base_score = overall_quality * (1.0 - w.recent_fold_quality) + recent_quality * w.recent_fold_quality
else:
base_score = overall_quality
# Multiplicative penalties
gp = _overfitting_penalty(walk_forward_summary)
fvp = _fold_variance_penalty(fold_cv)
tc = _trade_credibility(mean_tc, mean_wr)
er = _engine_reliability_penalty(walk_forward_summary.engine_reliability_ratio)
wfqs_v2 = base_score * gp * fvp * tc * er
breakdown = {
"base_score": round(base_score, 1),
"median_return": round(median_return_score, 1),
"mean_return": round(mean_return_score, 1),
"worst_return": round(worst_return_score, 1),
"positive_fold_rate": round(positive_fold_rate_score, 1),
"profit_factor": round(profit_factor_score, 1),
"drawdown": round(drawdown_score, 1),
"fold_count": round(fold_count_score, 1),
"overall_quality": round(overall_quality, 1),
"recent_quality": round(recent_quality, 1) if recent_quality is not None else 0.0,
"recent_fold_count": float(recent_fold_count),
"recent_fold_weight": round(w.recent_fold_quality if recent_quality is not None else 0.0, 2),
"gap_penalty": round(gp, 3),
"fold_variance_penalty": round(fvp, 3),
"trade_credibility": round(tc, 3),
"engine_reliability": round(er, 3),
}
return round(wfqs_v2, 1), breakdown
def _compute_public_sqs_components(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[dict[str, Any] | None, dict[str, float], str | None]:
"""Resolve shared components for public SQS variants."""
resolved_rqs = rqs_score
if resolved_rqs is None:
resolved_rqs, _ = compute_rqs(train_result, valid_result, test_result)
if resolved_rqs is None:
return None, {}, None
missing_requirements: dict[str, float] = {}
if walk_forward_summary is None:
missing_requirements["requires_walk_forward"] = 1.0
if robustness_matrix_summary is None:
missing_requirements["requires_robustness"] = 1.0
if out_of_time_robustness_summary is None:
missing_requirements["requires_out_of_time_robustness"] = 1.0
if missing_requirements:
return None, missing_requirements, "pending_validation"
resolved_wfqs_v2 = wfqs_v2_score
if resolved_wfqs_v2 is None:
resolved_wfqs_v2, _ = compute_wfqs_v2(walk_forward_summary)
if resolved_wfqs_v2 is None:
return None, {}, None
test = walk_forward_summary.test_aggregate
gap = walk_forward_summary.gap_stats
pass_positive = (test.positive_fold_rate_pct or 0.0) >= 70.0
pass_median = (test.median_return_pct or 0.0) >= 5.0
pass_worst = (test.worst_return_pct or 0.0) >= -5.0
pass_gap = (gap.mean_train_test_return_gap_pct or 999.0) <= 35.0
pass_count = sum([pass_positive, pass_median, pass_worst, pass_gap])
deployment_gate_factor = {4: 1.00, 3: 0.85, 2: 0.65, 1: 0.40, 0: 0.20}[pass_count]
base_score = (resolved_rqs * 0.45 + resolved_wfqs_v2 * 0.55) * deployment_gate_factor
gate_factor_rb, gate_breakdown = compute_robustness_gate(robustness_matrix_summary)
oot_gate_factor, oot_gate_breakdown = compute_oot_robustness_gate(out_of_time_robustness_summary)
oot_quality, oot_quality_breakdown = compute_oot_robustness_quality(out_of_time_robustness_summary)
activity_factor, activity_breakdown = _public_activity_factor(test_result, walk_forward_summary)
return (
{
"base_score": base_score,
"deployment_gate_factor": deployment_gate_factor,
"rb_gate_factor": gate_factor_rb,
"rb_gate_breakdown": gate_breakdown,
"oot_gate_factor": oot_gate_factor,
"oot_gate_breakdown": oot_gate_breakdown,
"oot_quality": oot_quality,
"oot_quality_breakdown": oot_quality_breakdown,
"activity_factor": activity_factor,
"activity_breakdown": activity_breakdown,
},
{},
"v3_deployment",
)
def _compute_public_sqs_components_v2(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[dict[str, Any] | None, dict[str, float], str | None]:
"""Resolve shared components for public SQS v7+.
Changes from v1:
- Deployment gate uses 3 criteria instead of 4: WFV gap criterion removed
because _gap_penalty inside WFQS v2 already penalises it continuously.
- OOT uses continuous compute_oot_factor_v2 instead of binary gate.
"""
resolved_rqs = rqs_score
if resolved_rqs is None:
resolved_rqs, _ = compute_rqs(train_result, valid_result, test_result)
if resolved_rqs is None:
return None, {}, None
missing_requirements: dict[str, float] = {}
if walk_forward_summary is None:
missing_requirements["requires_walk_forward"] = 1.0
if robustness_matrix_summary is None:
missing_requirements["requires_robustness"] = 1.0
if out_of_time_robustness_summary is None:
missing_requirements["requires_out_of_time_robustness"] = 1.0
if missing_requirements:
return None, missing_requirements, "pending_validation"
resolved_wfqs_v2 = wfqs_v2_score
if resolved_wfqs_v2 is None:
resolved_wfqs_v2, _ = compute_wfqs_v2(walk_forward_summary)
if resolved_wfqs_v2 is None:
return None, {}, None
test = walk_forward_summary.test_aggregate
# 3 criteria: gap dropped — already penalised continuously via _gap_penalty in WFQS v2
pass_positive = (test.positive_fold_rate_pct or 0.0) >= 70.0
pass_median = (test.median_return_pct or 0.0) >= 5.0
pass_worst = (test.worst_return_pct or 0.0) >= -5.0
pass_count = sum([pass_positive, pass_median, pass_worst])
deployment_gate_factor = {3: 1.00, 2: 0.85, 1: 0.65, 0: 0.40}[pass_count]
base_score = (resolved_rqs * 0.45 + resolved_wfqs_v2 * 0.55) * deployment_gate_factor
gate_factor_rb, gate_breakdown = compute_robustness_gate(robustness_matrix_summary)
oot_factor, oot_factor_breakdown = compute_oot_factor_v2(out_of_time_robustness_summary)
oot_quality, oot_quality_breakdown = compute_oot_robustness_quality(out_of_time_robustness_summary)
activity_factor, activity_breakdown = _public_activity_factor(test_result, walk_forward_summary)
return (
{
"base_score": base_score,
"deployment_gate_factor": deployment_gate_factor,
"rb_gate_factor": gate_factor_rb,
"rb_gate_breakdown": gate_breakdown,
"oot_gate_factor": oot_factor,
"oot_gate_breakdown": oot_factor_breakdown,
"oot_quality": oot_quality,
"oot_quality_breakdown": oot_quality_breakdown,
"activity_factor": activity_factor,
"activity_breakdown": activity_breakdown,
},
{},
"v7_deployment",
)
def compute_public_sqs_v2(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return legacy stress-adjusted public SQS.
This keeps the historical multiplicative OOT quality factor that
compresses strong recent strategies when stress-period quality is weaker.
"""
components, breakdown, source = _compute_public_sqs_components(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if components is None:
return None, breakdown, source
oot_quality_factor = 1.0
if components["oot_quality"] is not None:
oot_quality_factor = 0.30 + 0.70 * (components["oot_quality"] / 100.0)
final_score = (
components["base_score"]
* components["rb_gate_factor"]
* components["oot_gate_factor"]
* oot_quality_factor
* components["activity_factor"]
)
resolved_breakdown = {
"base_score": round(components["base_score"], 1),
"gate_factor": round(components["rb_gate_factor"], 2),
**components["rb_gate_breakdown"],
"oot_gate_factor": round(components["oot_gate_factor"], 2),
"oot_quality_factor": round(oot_quality_factor, 2),
**{
f"oot_{key}": value
for key, value in components["oot_gate_breakdown"].items()
},
**{
f"oot_{key}": value
for key, value in components["oot_quality_breakdown"].items()
},
**components["activity_breakdown"],
}
if components["oot_quality"] is not None:
resolved_breakdown["oot_quality"] = round(components["oot_quality"], 1)
return round(final_score, 1), resolved_breakdown, "v2_deployment+robustness+oot"
def compute_public_sqs_v3(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v3.
v3 treats stress OOT as a hard gate for eligibility, but keeps OOT quality
as a diagnostic instead of a multiplicative ranking penalty.
"""
components, breakdown, source = _compute_public_sqs_components(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if components is None:
return None, breakdown, source
final_score = (
components["base_score"]
* components["rb_gate_factor"]
* components["oot_gate_factor"]
* components["activity_factor"]
)
resolved_breakdown = {
"base_score": round(components["base_score"], 1),
"gate_factor": round(components["rb_gate_factor"], 2),
**components["rb_gate_breakdown"],
"oot_gate_factor": round(components["oot_gate_factor"], 2),
**{
f"oot_{key}": value
for key, value in components["oot_gate_breakdown"].items()
},
**{
f"oot_{key}": value
for key, value in components["oot_quality_breakdown"].items()
},
**components["activity_breakdown"],
}
if components["oot_quality"] is not None:
resolved_breakdown["oot_quality"] = round(components["oot_quality"], 1)
return round(final_score, 1), resolved_breakdown, "v3_deployment+robustness+oot_gate"
def compute_public_sqs_v7(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v7.
v7 addresses two structural flaws in the v3 backbone:
1. Binary OOT gate → continuous factor: replaces the 5-step {0.20..1.00}
gate with a smooth quality-based factor in [0.75, 1.00]. This eliminates
cliff effects and perverse incentives to add COVID-specific engines.
2. Deployment gate de-duplication: removes the WFV gap criterion that
was already penalised continuously by _gap_penalty inside WFQS v2.
"""
components, breakdown, source = _compute_public_sqs_components_v2(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if components is None:
return None, breakdown, source
final_score = (
components["base_score"]
* components["rb_gate_factor"]
* components["oot_gate_factor"]
* components["activity_factor"]
)
resolved_breakdown = {
"base_score": round(components["base_score"], 1),
"gate_factor": round(components["rb_gate_factor"], 2),
**components["rb_gate_breakdown"],
"oot_gate_factor": round(components["oot_gate_factor"], 3),
**{
f"oot_{key}": value
for key, value in components["oot_gate_breakdown"].items()
},
**{
f"oot_{key}": value
for key, value in components["oot_quality_breakdown"].items()
},
**components["activity_breakdown"],
}
if components["oot_quality"] is not None:
resolved_breakdown["oot_quality"] = round(components["oot_quality"], 1)
return round(final_score, 1), resolved_breakdown, "v7_continuous_oot+3crit_deployment"
def compute_public_sqs_v4(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
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,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v4.
v4 keeps v3 intact as the deployment/stress backbone, then blends in a
modest full-cycle capital-growth score when a comparable common-window
run is available. Official comparability requires the standard 10k
initial equity. Missing or mismatched common-window summaries fall back to v3.
"""
v3_score, v3_breakdown, source = compute_public_sqs_v3(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if v3_score is None:
return None, v3_breakdown, source
if common_window_summary is None:
return v3_score, v3_breakdown, "v4_fallback_v3_missing_common_window"
if not _is_public_common_window_comparable(common_window_summary):
return v3_score, v3_breakdown, "v4_fallback_v3_noncomparable_common_window"
common_score, common_breakdown = compute_common_window_score(common_window_summary)
if common_score is None:
return v3_score, v3_breakdown, "v4_fallback_v3_missing_common_window"
weight = _DEFAULT_PUBLIC_COMMON_WINDOW_WEIGHT
final_score = v3_score * (1.0 - weight) + common_score * weight
breakdown = {
"v3_score": round(v3_score, 1),
"common_window_score": round(common_score, 1),
"common_window_weight": round(weight, 2),
**v3_breakdown,
**common_breakdown,
}
return round(final_score, 1), breakdown, "v4_deployment+common_window"
def compute_multi_capital_common_window_score(
multi_capital_common_window_summary: MultiCapitalCommonWindowSummary | None,
) -> tuple[float | None, dict[str, float]]:
"""Blend capital-growth quality across the official 10k/25k/100k buckets."""
if multi_capital_common_window_summary is None:
return None, {}
scores_by_capital: dict[float, float] = {}
breakdown: dict[str, float] = {}
for summary in multi_capital_common_window_summary.capital_summaries:
capital = float(summary.initial_equity)
if capital not in _DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS:
continue
score, _ = compute_common_window_score(summary)
if score is None:
continue
scores_by_capital[capital] = score
breakdown[f"mcw_{int(capital/1000)}k_score"] = round(score, 1)
required = set(_DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS)
if not required.issubset(scores_by_capital):
return None, {}
weighted_average = sum(
scores_by_capital[capital] * weight
for capital, weight in _DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS.items()
)
floor_score = min(scores_by_capital.values())
blended_score = (
weighted_average * (1.0 - _DEFAULT_PUBLIC_MULTI_CAPITAL_FLOOR_BLEND)
+ floor_score * _DEFAULT_PUBLIC_MULTI_CAPITAL_FLOOR_BLEND
)
breakdown.update({
"mcw_weight_10k": round(_DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS[10_000.0], 2),
"mcw_weight_25k": round(_DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS[25_000.0], 2),
"mcw_weight_100k": round(_DEFAULT_PUBLIC_MULTI_CAPITAL_CAPITAL_WEIGHTS[100_000.0], 2),
"mcw_floor_score": round(floor_score, 1),
"mcw_floor_blend": round(_DEFAULT_PUBLIC_MULTI_CAPITAL_FLOOR_BLEND, 2),
})
return round(blended_score, 1), breakdown
def compute_public_sqs_v5(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
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,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v5.
v5 preserves the v3 deployment/stress backbone, then blends in a
modest multi-capital common-window term using comparable 10k/25k/100k
runs. Missing multi-capital summaries fall back to v4.
"""
v3_score, v3_breakdown, source = compute_public_sqs_v3(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if v3_score is None:
return None, v3_breakdown, source
if multi_capital_common_window_summary is None:
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if not _is_public_multi_capital_common_window_comparable(multi_capital_common_window_summary):
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
multi_score, multi_breakdown = compute_multi_capital_common_window_score(
multi_capital_common_window_summary
)
if multi_score is None:
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
weight = _DEFAULT_PUBLIC_MULTI_CAPITAL_COMMON_WINDOW_WEIGHT
final_score = v3_score * (1.0 - weight) + multi_score * weight
breakdown = {
"v3_score": round(v3_score, 1),
"multi_capital_common_window_score": round(multi_score, 1),
"multi_capital_common_window_weight": round(weight, 2),
**v3_breakdown,
**multi_breakdown,
}
return round(final_score, 1), breakdown, "v5_deployment+multi_capital_common_window"
def compute_public_sqs_v6(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
common_window_summary: CommonWindowSummary | None = None,
reset_common_window_summary: ResetCommonWindowSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v6.
v6 keeps the v3 deployment/stress backbone, then blends in a
path-neutral reset common-window score when comparable segment-reset runs
are available. Missing or mismatched reset summaries fall back to v4.
"""
v3_score, v3_breakdown, source = compute_public_sqs_v3(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if v3_score is None:
return None, v3_breakdown, source
if reset_common_window_summary is None:
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if not _is_public_reset_common_window_comparable(reset_common_window_summary):
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
reset_score, reset_breakdown = compute_reset_common_window_score(reset_common_window_summary)
if reset_score is None:
return compute_public_sqs_v4(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
common_window_summary=common_window_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
weight = _DEFAULT_PUBLIC_RESET_COMMON_WINDOW_WEIGHT
final_score = v3_score * (1.0 - weight) + reset_score * weight
breakdown = {
"v3_score": round(v3_score, 1),
"reset_common_window_score": round(reset_score, 1),
"reset_common_window_weight": round(weight, 2),
**v3_breakdown,
**reset_breakdown,
}
return round(final_score, 1), breakdown, "v6_deployment+reset_common_window"
def compute_public_sqs_v8(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None = None,
robustness_matrix_summary: RobustnessMatrixSummary | None = None,
out_of_time_robustness_summary: RobustnessMatrixSummary | None = None,
common_window_summary: CommonWindowSummary | None = None,
reset_common_window_summary: ResetCommonWindowSummary | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v8.
v8 keeps the v7 backbone (continuous OOT factor + 3-criterion deployment
gate), then blends in a path-neutral reset common-window score when
available. Missing or mismatched reset summaries fall back to v7 backbone.
When no reset data is present but a comparable 10k common-window exists,
falls back to a v7-backbone + common-window blend.
"""
v7_score, v7_breakdown, source = compute_public_sqs_v7(
train_result,
valid_result,
test_result,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
if v7_score is None:
return None, v7_breakdown, source
# Prefer reset common-window
if reset_common_window_summary is not None and _is_public_reset_common_window_comparable(
reset_common_window_summary
):
reset_score, reset_breakdown = compute_reset_common_window_score(reset_common_window_summary)
if reset_score is not None:
weight = _DEFAULT_PUBLIC_RESET_COMMON_WINDOW_WEIGHT
final_score = v7_score * (1.0 - weight) + reset_score * weight
breakdown = {
"v7_score": round(v7_score, 1),
"reset_common_window_score": round(reset_score, 1),
"reset_common_window_weight": round(weight, 2),
**v7_breakdown,
**reset_breakdown,
}
return round(final_score, 1), breakdown, "v8_continuous_oot+reset_common_window"
# Fallback: 10k compounded common-window
if common_window_summary is not None and _is_public_common_window_comparable(common_window_summary):
common_score, common_breakdown = compute_common_window_score(common_window_summary)
if common_score is not None:
weight = _DEFAULT_PUBLIC_COMMON_WINDOW_WEIGHT
final_score = v7_score * (1.0 - weight) + common_score * weight
breakdown = {
"v7_score": round(v7_score, 1),
"common_window_score": round(common_score, 1),
"common_window_weight": round(weight, 2),
**v7_breakdown,
**common_breakdown,
}
return round(final_score, 1), breakdown, "v8_continuous_oot+common_window"
return v7_score, v7_breakdown, "v8_fallback_v7"
def _resolve_regime_score(
scenario_robustness_score: float | None,
out_of_time_robustness_summary: "RobustnessMatrixSummary | None",
) -> tuple[float, str]:
"""Resolve regime adaptability score (0-100) with fallback chain.
Priority:
1. Scenario-test RRS (synthetic market environments, unbiased)
2. OOT quality (2020-2021 COVID era, historical fallback)
3. 50.0 neutral (no data)
Returns (score, source_label).
"""
if scenario_robustness_score is not None:
return scenario_robustness_score, "scenario_rrs"
oot_quality, _ = compute_oot_robustness_quality(out_of_time_robustness_summary)
if oot_quality is not None:
return oot_quality, "oot_quality_fallback"
return 50.0, "neutral_fallback"
def compute_public_sqs_v9(
train_result: "SplitResult | None",
valid_result: "SplitResult | None",
test_result: "SplitResult | None",
walk_forward_summary: "WalkForwardSummary | None" = None,
robustness_matrix_summary: "RobustnessMatrixSummary | None" = None,
out_of_time_robustness_summary: "RobustnessMatrixSummary | None" = None,
common_window_summary: "CommonWindowSummary | None" = None,
reset_common_window_summary: "ResetCommonWindowSummary | None" = None,
scenario_robustness_score: float | None = None,
rqs_score: float | None = None,
wfqs_v2_score: float | None = None,
) -> tuple[float | None, dict[str, float], str | None]:
"""Return public SQS v9.
v9 replaces the multiplicative OOT factor with a 3-pillar additive core:
core = RQS × 0.35 + WFQS_v2 × 0.40 + regime_score × 0.25
where regime_score (0-100) comes from scenario-test RRS (12 synthetic
market environments), falling back to OOT quality (COVID era) or 50.0
(neutral) if neither is available.
Multiplicative gates (deployment, robustness, activity) are preserved.
CW blend layer is unchanged from v8.
"""
resolved_rqs = rqs_score
if resolved_rqs is None:
resolved_rqs, _ = compute_rqs(train_result, valid_result, test_result)
if resolved_rqs is None:
return None, {}, None
if walk_forward_summary is None:
return None, {"requires_walk_forward": 1.0}, "pending_validation"
if robustness_matrix_summary is None:
return None, {"requires_robustness": 1.0}, "pending_validation"
if out_of_time_robustness_summary is None and scenario_robustness_score is None:
return None, {"requires_out_of_time_robustness": 1.0}, "pending_validation"
resolved_wfqs_v2 = wfqs_v2_score
if resolved_wfqs_v2 is None:
resolved_wfqs_v2, _ = compute_wfqs_v2(walk_forward_summary)
if resolved_wfqs_v2 is None:
return None, {}, None
# --- Regime score (25% pillar) ---
regime_score, regime_source = _resolve_regime_score(
scenario_robustness_score, out_of_time_robustness_summary
)
# --- 3-pillar additive core ---
core_score = resolved_rqs * 0.35 + resolved_wfqs_v2 * 0.40 + regime_score * 0.25
# --- Deployment gate (3 criteria, unchanged) ---
test = walk_forward_summary.test_aggregate
pass_positive = (test.positive_fold_rate_pct or 0.0) >= 70.0
pass_median = (test.median_return_pct or 0.0) >= 5.0
pass_worst = (test.worst_return_pct or 0.0) >= -5.0
pass_count = sum([pass_positive, pass_median, pass_worst])
deployment_gate_factor = {3: 1.00, 2: 0.85, 1: 0.65, 0: 0.40}[pass_count]
# --- Robustness gate + activity factor ---
gate_factor_rb, gate_breakdown = compute_robustness_gate(robustness_matrix_summary)
activity_factor, activity_breakdown = _public_activity_factor(test_result, walk_forward_summary)
v9_base = core_score * deployment_gate_factor * gate_factor_rb * activity_factor
breakdown: dict[str, float] = {
"rqs_score": round(resolved_rqs, 1),
"wfqs_v2_score": round(resolved_wfqs_v2, 1),
"regime_score": round(regime_score, 1),
"core_score": round(core_score, 1),
"deployment_gate_factor": deployment_gate_factor,
"gate_factor": round(gate_factor_rb, 2),
**gate_breakdown,
**activity_breakdown,
"regime_source": 0.0, # informational placeholder (string stored separately)
}
# --- CW blend layer (unchanged from v8) ---
if reset_common_window_summary is not None and _is_public_reset_common_window_comparable(
reset_common_window_summary
):
reset_score, reset_breakdown = compute_reset_common_window_score(reset_common_window_summary)
if reset_score is not None:
weight = _DEFAULT_PUBLIC_RESET_COMMON_WINDOW_WEIGHT
final_score = v9_base * (1.0 - weight) + reset_score * weight
breakdown.update({
"v9_base": round(v9_base, 1),
"reset_common_window_score": round(reset_score, 1),
"reset_common_window_weight": round(weight, 2),
**reset_breakdown,
})
source = f"v9_3pillar+reset_cw[{regime_source}]"
return round(final_score, 1), breakdown, source
if common_window_summary is not None and _is_public_common_window_comparable(common_window_summary):
common_score, common_breakdown = compute_common_window_score(common_window_summary)
if common_score is not None:
weight = _DEFAULT_PUBLIC_COMMON_WINDOW_WEIGHT
final_score = v9_base * (1.0 - weight) + common_score * weight
breakdown.update({
"v9_base": round(v9_base, 1),
"common_window_score": round(common_score, 1),
"common_window_weight": round(weight, 2),
**common_breakdown,
})
source = f"v9_3pillar+cw[{regime_source}]"
return round(final_score, 1), breakdown, source
breakdown["v9_base"] = round(v9_base, 1)
return round(v9_base, 1), breakdown, f"v9_3pillar[{regime_source}]"
def compute_deployment_score(
train_result: SplitResult | None,
valid_result: SplitResult | None,
test_result: SplitResult | None,
walk_forward_summary: WalkForwardSummary | None,
rqs_score: float | None = None,
wfqs_score: float | None = None,
weights: DeploymentScoreWeights | None = None,
) -> tuple[float | None, dict[str, float]]:
"""Compute a deployment-oriented score that requires walk-forward confirmation."""
if walk_forward_summary is None:
return None, {}
rqs = rqs_score
if rqs is None:
rqs, _ = compute_rqs(train_result, valid_result, test_result)
wfqs = wfqs_score
if wfqs is None:
wfqs, _ = compute_wfqs(walk_forward_summary)
if rqs is None or wfqs is None:
return None, {}
w = weights or _DEFAULT_DEPLOYMENT_WEIGHTS
test = walk_forward_summary.test_aggregate
gap = walk_forward_summary.gap_stats
pass_positive = (test.positive_fold_rate_pct or 0.0) >= 70.0
pass_median = (test.median_return_pct or 0.0) >= 5.0
pass_worst = (test.worst_return_pct or 0.0) >= -5.0
pass_gap = (gap.mean_train_test_return_gap_pct or 999.0) <= 35.0
pass_count = sum([pass_positive, pass_median, pass_worst, pass_gap])
gate_factor = {
4: 1.00,
3: 0.85,
2: 0.65,
1: 0.40,
0: 0.20,
}[pass_count]
deployment = (rqs * w.rqs_quality + wfqs * w.wfqs_quality) * gate_factor
breakdown = {
"rqs_quality": round(rqs, 1),
"wfqs_quality": round(wfqs, 1),
"gate_factor": round(gate_factor, 2),
"gate_positive_fold_rate": 1.0 if pass_positive else 0.0,
"gate_median_return": 1.0 if pass_median else 0.0,
"gate_worst_return": 1.0 if pass_worst else 0.0,
"gate_train_test_gap": 1.0 if pass_gap else 0.0,
}
return round(deployment, 1), breakdown
def compute_common_window_score(
common_window_summary: CommonWindowSummary | None,
) -> tuple[float | None, dict[str, float]]:
"""Score a continuous full-cycle run for capital-growth / recycling quality."""
if common_window_summary is None:
return None, {}
metrics = common_window_summary.metrics
total_return_score = _normalize(metrics.total_return_pct, low=0.0, high=250.0)
profit_factor_score = _normalize(metrics.profit_factor, low=1.0, high=6.0)
sharpe_score = _normalize(metrics.sharpe_ratio, low=0.0, high=3.0)
drawdown_score = _normalize_inverse(metrics.max_drawdown_pct, low=12.0, high=2.5)
return_on_gross = _safe_ratio(metrics.total_return_pct, metrics.avg_gross_exposure_pct)
return_on_gross_score = _normalize(return_on_gross, low=1.0, high=8.0)
capital_velocity = _safe_ratio(metrics.total_return_pct, metrics.days_in_market_pct)
capital_velocity_score = _normalize(capital_velocity, low=0.4, high=3.0)
score = (
total_return_score * 0.35
+ profit_factor_score * 0.10
+ sharpe_score * 0.15
+ drawdown_score * 0.15
+ return_on_gross_score * 0.20
+ capital_velocity_score * 0.05
)
if metrics.trade_count < 25:
score *= 0.85
breakdown = {
"cw_total_return": round(total_return_score, 1),
"cw_profit_factor": round(profit_factor_score, 1),
"cw_sharpe": round(sharpe_score, 1),
"cw_drawdown": round(drawdown_score, 1),
"cw_return_on_gross": round(return_on_gross_score, 1),
"cw_capital_velocity": round(capital_velocity_score, 1),
"cw_trade_count": float(metrics.trade_count),
}
return round(score, 1), breakdown
def compute_reset_common_window_score(
reset_common_window_summary: ResetCommonWindowSummary | None,
) -> tuple[float | None, dict[str, float]]:
"""Score reset-capital full-cycle segments for path-neutral research ranking."""
if reset_common_window_summary is None:
return None, {}
segment_scores: list[float] = []
segment_returns: list[float] = []
for segment in reset_common_window_summary.segment_summaries:
score, _ = compute_common_window_score(segment)
if score is None:
continue
segment_scores.append(score)
segment_returns.append(segment.metrics.total_return_pct or 0.0)
if len(segment_scores) < 2:
return None, {}
mean_segment_score = sum(segment_scores) / len(segment_scores)
median_segment_score = statistics.median(segment_scores)
floor_segment_score = min(segment_scores)
positive_segment_rate = (
100.0 * sum(1 for value in segment_returns if value > 0.0) / len(segment_returns)
)
spread_score = _normalize_inverse(
max(segment_returns) - min(segment_returns),
low=80.0,
high=15.0,
)
positive_rate_score = _normalize(positive_segment_rate, low=50.0, high=100.0)
score = (
mean_segment_score * 0.40
+ median_segment_score * 0.20
+ floor_segment_score * 0.20
+ positive_rate_score * 0.15
+ spread_score * 0.05
)
if len(segment_scores) < 4:
score *= 0.90
breakdown = {
"rcw_mean_segment_score": round(mean_segment_score, 1),
"rcw_median_segment_score": round(median_segment_score, 1),
"rcw_floor_segment_score": round(floor_segment_score, 1),
"rcw_positive_segment_rate": round(positive_segment_rate, 1),
"rcw_return_spread_score": round(spread_score, 1),
"rcw_segment_count": float(len(segment_scores)),
}
return round(score, 1), breakdown
def _get_robustness_horizon_summary(
summary: RobustnessMatrixSummary | None,
horizon_days: int,
) -> dict[str, float | int | None] | None:
if summary is None:
return None
for item in summary.horizon_summaries:
if item.horizon_days == horizon_days:
return item.model_dump()
return None
def compute_robustness_gate(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> tuple[float, dict[str, float]]:
"""Return multiplicative gate from the robustness matrix summary."""
if robustness_matrix_summary is None:
return 1.0, {}
h63 = _get_robustness_horizon_summary(robustness_matrix_summary, 63)
h252 = _get_robustness_horizon_summary(robustness_matrix_summary, 252)
pass_positive = (robustness_matrix_summary.overall_positive_window_rate_pct or 0.0) >= 65.0
pass_63 = ((h63 or {}).get("median_return_pct") or 0.0) >= 3.0
pass_252 = ((h252 or {}).get("median_return_pct") or 0.0) >= 8.0
pass_worst = (robustness_matrix_summary.overall_worst_return_pct or -999.0) >= -12.0
pass_count = sum([pass_positive, pass_63, pass_252, pass_worst])
gate_factor = {
4: 1.00,
3: 0.85,
2: 0.65,
1: 0.40,
0: 0.20,
}[pass_count]
breakdown = {
"rb_gate_positive_rate": 1.0 if pass_positive else 0.0,
"rb_gate_63d_median": 1.0 if pass_63 else 0.0,
"rb_gate_252d_median": 1.0 if pass_252 else 0.0,
"rb_gate_worst_return": 1.0 if pass_worst else 0.0,
}
return gate_factor, breakdown
def _compute_oot_positive_rate_63plus(
summary: RobustnessMatrixSummary,
) -> float | None:
"""63d+ horizon weighted-average positive window rate.
21d windows penalise catalyst strategies unfairly (0-trade windows
count as non-positive). 63d+ windows give a fairer survival signal.
"""
qualifying = [h for h in summary.horizon_summaries if h.horizon_days >= 63]
if not qualifying:
return None
total_windows = sum(h.window_count for h in qualifying)
if total_windows == 0:
return None
weighted_positive = sum(
(h.positive_window_rate_pct or 0.0) * h.window_count
for h in qualifying
)
return round(weighted_positive / total_windows, 1)
def _is_sparse_zero_trade_robustness_summary(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> bool:
"""Detect OOT summaries that are all-zero because the strategy never fired.
Some sparse-stress evaluations produce a formally valid robustness summary
with non-zero window counts but every return / drawdown / positive-rate
field equal to zero. That pattern is not evidence of failure; it means the
strategy had no eligible trades in the OOT regime and should be treated as
non-comparable rather than heavily penalised.
"""
if robustness_matrix_summary is None:
return False
if not robustness_matrix_summary.horizon_summaries:
return False
if (robustness_matrix_summary.overall_positive_window_rate_pct or 0.0) != 0.0:
return False
if (robustness_matrix_summary.overall_worst_return_pct or 0.0) != 0.0:
return False
for horizon in robustness_matrix_summary.horizon_summaries:
if any(
(value or 0.0) != 0.0
for value in (
horizon.mean_return_pct,
horizon.median_return_pct,
horizon.worst_return_pct,
horizon.positive_window_rate_pct,
horizon.mean_max_drawdown_pct,
)
):
return False
return True
def compute_oot_robustness_gate(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> tuple[float, dict[str, float]]:
"""OOT stress-test robustness gate with relaxed thresholds.
Stress-test thresholds (vs main-period):
- 63d+ positive rate >= 50% (main: overall >= 65%)
- 63d median >= 0.5% (main: >= 3.0%)
- 252d median >= 3.0% (main: >= 8.0%)
- worst return >= -15.0% (main: >= -12.0%)
"""
if robustness_matrix_summary is None:
return 1.0, {}
if _is_sparse_zero_trade_robustness_summary(robustness_matrix_summary):
return 1.0, {"rb_gate_sparse_no_trade": 1.0}
h63 = _get_robustness_horizon_summary(robustness_matrix_summary, 63)
h252 = _get_robustness_horizon_summary(robustness_matrix_summary, 252)
positive_rate_63plus = _compute_oot_positive_rate_63plus(robustness_matrix_summary)
pass_positive = (positive_rate_63plus or 0.0) >= 50.0
pass_63 = ((h63 or {}).get("median_return_pct") or 0.0) >= 0.5
pass_252 = ((h252 or {}).get("median_return_pct") or 0.0) >= 3.0
pass_worst = (robustness_matrix_summary.overall_worst_return_pct or -999.0) >= -15.0
pass_count = sum([pass_positive, pass_63, pass_252, pass_worst])
gate_factor = {
4: 1.00,
3: 0.85,
2: 0.65,
1: 0.40,
0: 0.20,
}[pass_count]
breakdown = {
"rb_gate_positive_rate": 1.0 if pass_positive else 0.0,
"rb_gate_63d_median": 1.0 if pass_63 else 0.0,
"rb_gate_252d_median": 1.0 if pass_252 else 0.0,
"rb_gate_worst_return": 1.0 if pass_worst else 0.0,
}
return gate_factor, breakdown
def compute_robustness_quality(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> tuple[float | None, dict[str, float]]:
"""Return a continuous robustness quality score in [0, 100].
This complements the coarse pass/fail gate so repaired OOT runs can
meaningfully reorder strategies instead of collapsing into the same gate.
"""
if robustness_matrix_summary is None:
return None, {}
h63 = _get_robustness_horizon_summary(robustness_matrix_summary, 63)
h252 = _get_robustness_horizon_summary(robustness_matrix_summary, 252)
positive_rate_score = _normalize(
robustness_matrix_summary.overall_positive_window_rate_pct,
low=30.0,
high=80.0,
)
worst_return_score = _normalize(
robustness_matrix_summary.overall_worst_return_pct,
low=-25.0,
high=0.0,
)
h63_median_score = _normalize(
(h63 or {}).get("median_return_pct"),
low=-5.0,
high=5.0,
)
h252_median_score = _normalize(
(h252 or {}).get("median_return_pct"),
low=-10.0,
high=15.0,
)
quality = (
positive_rate_score * 0.30
+ h63_median_score * 0.25
+ h252_median_score * 0.30
+ worst_return_score * 0.15
)
breakdown = {
"rb_quality_positive_rate": round(positive_rate_score, 1),
"rb_quality_63d_median": round(h63_median_score, 1),
"rb_quality_252d_median": round(h252_median_score, 1),
"rb_quality_worst_return": round(worst_return_score, 1),
}
return round(quality, 1), breakdown
def compute_oot_robustness_quality(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> tuple[float | None, dict[str, float]]:
"""OOT stress-test continuous quality score [0, 100].
Normalisation ranges adjusted for stress-test periods (e.g. COVID).
Uses 63d+ positive rate instead of overall (excludes 21d noise).
"""
if robustness_matrix_summary is None:
return None, {}
if _is_sparse_zero_trade_robustness_summary(robustness_matrix_summary):
return None, {"rb_quality_sparse_no_trade": 1.0}
h63 = _get_robustness_horizon_summary(robustness_matrix_summary, 63)
h252 = _get_robustness_horizon_summary(robustness_matrix_summary, 252)
positive_rate_63plus = _compute_oot_positive_rate_63plus(robustness_matrix_summary)
positive_rate_score = _normalize(
positive_rate_63plus,
low=20.0,
high=70.0,
)
worst_return_score = _normalize(
robustness_matrix_summary.overall_worst_return_pct,
low=-30.0,
high=-5.0,
)
h63_median_score = _normalize(
(h63 or {}).get("median_return_pct"),
low=-8.0,
high=3.0,
)
h252_median_score = _normalize(
(h252 or {}).get("median_return_pct"),
low=-15.0,
high=10.0,
)
quality = (
positive_rate_score * 0.30
+ h63_median_score * 0.25
+ h252_median_score * 0.30
+ worst_return_score * 0.15
)
breakdown = {
"rb_quality_positive_rate": round(positive_rate_score, 1),
"rb_quality_63d_median": round(h63_median_score, 1),
"rb_quality_252d_median": round(h252_median_score, 1),
"rb_quality_worst_return": round(worst_return_score, 1),
}
return round(quality, 1), breakdown
def compute_oot_factor_v2(
robustness_matrix_summary: RobustnessMatrixSummary | None,
) -> tuple[float, dict[str, float]]:
"""Continuous OOT stress-test factor in [0.75, 1.00].
Replaces the coarse 5-step binary gate with a smooth quality-based factor.
Maximum penalty is 25% (vs the legacy gate's 80%), since COVID is a single
extreme regime and overfitting is already guarded by WFV gap_penalty and
the main-period robustness gate.
Uses the stress-adjusted normalisation ranges from compute_oot_robustness_quality.
"""
if robustness_matrix_summary is None:
return 1.0, {}
if _is_sparse_zero_trade_robustness_summary(robustness_matrix_summary):
return 1.0, {"oot_factor_sparse_no_trade": 1.0}
quality, breakdown = compute_oot_robustness_quality(robustness_matrix_summary)
if quality is None:
return 1.0, breakdown
# quality=0 → 0.75, quality=100 → 1.00
factor = 0.75 + 0.25 * (quality / 100.0)
return round(factor, 3), {**breakdown, "oot_quality": round(quality, 1)}
def compute_config_delta(
current: dict[str, Any],
baseline: dict[str, Any],
baseline_name: str,
) -> ConfigDelta:
"""Compute a flat diff between two config dicts (one level deep)."""
changes: dict[str, str] = {}
_diff_recursive(baseline, current, prefix="", changes=changes)
return ConfigDelta(base_experiment=baseline_name, changes=changes)
def _diff_recursive(
old: dict[str, Any],
new: dict[str, Any],
prefix: str,
changes: dict[str, str],
) -> None:
all_keys = set(old.keys()) | set(new.keys())
for key in sorted(all_keys):
full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
old_val = old.get(key)
new_val = new.get(key)
if isinstance(old_val, dict) and isinstance(new_val, dict):
_diff_recursive(old_val, new_val, full_key, changes)
elif old_val != new_val:
changes[full_key] = f"{old_val} \u2192 {new_val}"
# ---------------------------------------------------------------------------
# Journal I/O
# ---------------------------------------------------------------------------
def get_next_entry_id(journal_path: Path) -> str:
"""Return next entry ID like 'IMP-0001'."""
entries = load_journal(journal_path)
return f"IMP-{len(entries) + 1:04d}"
@contextlib.contextmanager
def journal_lock(journal_path: Path) -> Iterator[None]:
"""Serialize journal mutations across concurrent record commands."""
journal_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = journal_path.with_suffix(f"{journal_path.suffix}.lock")
with lock_path.open("a+") as lock_file:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def append_journal_entry(journal_path: Path, entry: JournalEntry) -> None:
"""Append a single JournalEntry as one JSON line."""
journal_path.parent.mkdir(parents=True, exist_ok=True)
with open(journal_path, "a") as f:
f.write(entry.model_dump_json() + "\n")
logger.info("journal_entry_appended", entry_id=entry.entry_id, experiment=entry.experiment_name)
def replace_journal_entry(journal_path: Path, entry: JournalEntry) -> None:
"""Replace an existing journal entry by entry_id."""
entries = load_journal(journal_path)
replaced = False
updated_entries: list[JournalEntry] = []
for current in entries:
if current.entry_id == entry.entry_id:
updated_entries.append(entry)
replaced = True
else:
updated_entries.append(current)
if not replaced:
raise ValueError(f"Journal entry not found: {entry.entry_id}")
journal_path.write_text("".join(e.model_dump_json() + "\n" for e in updated_entries))
logger.info("journal_entry_replaced", entry_id=entry.entry_id, experiment=entry.experiment_name)
def load_journal(journal_path: Path) -> list[JournalEntry]:
"""Load all journal entries from JSONL file."""
if not journal_path.exists():
return []
entries: list[JournalEntry] = []
for line in journal_path.read_text().strip().splitlines():
line = line.strip()
if line:
entries.append(JournalEntry.model_validate_json(line))
return entries
def refresh_public_scores(
journal_path: Path,
selector: Callable[[JournalEntry], bool] | None = None,
) -> int:
"""Recompute stored public scores and score diagnostics for journal entries."""
entries = load_journal(journal_path)
updated_entries: list[JournalEntry] = []
updated_count = 0
for entry in entries:
if selector is not None and not selector(entry):
updated_entries.append(entry)
continue
test_result = _hydrate_split_result(entry.results.get("test"))
valid_result = _hydrate_split_result(entry.results.get("valid"))
train_result = _hydrate_split_result(entry.results.get("train"))
test_metrics = _metrics_from_split_result(test_result)
sqs_v2_score = None
sqs_v2_breakdown: dict[str, float] = {}
if test_metrics is not None:
sqs_v2_score, sqs_v2_breakdown = compute_sqs_v2(test_metrics)
rqs_score, rqs_breakdown = compute_rqs(train_result, valid_result, test_result)
wfqs_score, wfqs_breakdown = compute_wfqs(entry.walk_forward_summary)
wfqs_v2_score, wfqs_v2_breakdown = compute_wfqs_v2(entry.walk_forward_summary)
deployment_score, deployment_breakdown = compute_deployment_score(
train_result,
valid_result,
test_result,
entry.walk_forward_summary,
rqs_score=rqs_score,
wfqs_score=wfqs_score,
)
common_window_score, common_window_breakdown = compute_common_window_score(
entry.common_window_summary
)
reset_common_window_score, reset_common_window_breakdown = compute_reset_common_window_score(
entry.reset_common_window_summary
)
multi_capital_common_window_score, multi_capital_common_window_breakdown = (
compute_multi_capital_common_window_score(entry.multi_capital_common_window_summary)
)
sqs_v3_score, sqs_v3_breakdown, _ = compute_public_sqs_v3(
train_result,
valid_result,
test_result,
walk_forward_summary=entry.walk_forward_summary,
robustness_matrix_summary=entry.robustness_matrix_summary,
out_of_time_robustness_summary=entry.out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
sqs_score, sqs_breakdown, _ = compute_public_sqs(
train_result,
valid_result,
test_result,
walk_forward_summary=entry.walk_forward_summary,
robustness_matrix_summary=entry.robustness_matrix_summary,
out_of_time_robustness_summary=entry.out_of_time_robustness_summary,
common_window_summary=entry.common_window_summary,
reset_common_window_summary=entry.reset_common_window_summary,
multi_capital_common_window_summary=entry.multi_capital_common_window_summary,
rqs_score=rqs_score,
wfqs_score=wfqs_v2_score,
scenario_robustness_score=entry.scenario_robustness_score,
)
stress_sqs_score, stress_sqs_breakdown, _ = compute_public_sqs_v2(
train_result,
valid_result,
test_result,
walk_forward_summary=entry.walk_forward_summary,
robustness_matrix_summary=entry.robustness_matrix_summary,
out_of_time_robustness_summary=entry.out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
refreshed = entry.model_copy(
update={
"sqs_score": sqs_score,
"sqs_breakdown": sqs_breakdown,
"sqs_v3_score": sqs_v3_score,
"sqs_v3_breakdown": sqs_v3_breakdown,
"stress_sqs_score": stress_sqs_score,
"stress_sqs_breakdown": stress_sqs_breakdown,
"sqs_v2_score": sqs_v2_score,
"sqs_v2_breakdown": sqs_v2_breakdown,
"rqs_score": rqs_score,
"rqs_breakdown": rqs_breakdown,
"wfqs_score": wfqs_score,
"wfqs_breakdown": wfqs_breakdown,
"wfqs_v2_score": wfqs_v2_score,
"wfqs_v2_breakdown": wfqs_v2_breakdown,
"deployment_score": deployment_score,
"deployment_breakdown": deployment_breakdown,
"common_window_score": common_window_score,
"common_window_breakdown": common_window_breakdown,
"reset_common_window_score": reset_common_window_score,
"reset_common_window_breakdown": reset_common_window_breakdown,
"multi_capital_common_window_score": multi_capital_common_window_score,
"multi_capital_common_window_breakdown": multi_capital_common_window_breakdown,
}
)
if refreshed.model_dump() != entry.model_dump():
updated_count += 1
updated_entries.append(refreshed)
if updated_count:
journal_path.write_text("".join(item.model_dump_json() + "\n" for item in updated_entries))
logger.info("journal_public_scores_refreshed", updated_count=updated_count)
return updated_count
def _resolve_journal_target(entries: list[JournalEntry], target: str) -> JournalEntry:
target_upper = target.upper()
by_id = [entry for entry in entries if entry.entry_id.upper() == target_upper]
if by_id:
return by_id[0]
exact_name = [entry for entry in entries if entry.experiment_name == target]
if len(exact_name) == 1:
return exact_name[0]
if len(exact_name) > 1:
return sorted(exact_name, key=lambda e: e.timestamp)[-1]
partial = [entry for entry in entries if target.lower() in entry.experiment_name.lower()]
if len(partial) != 1:
raise ValueError(f"Unable to uniquely match journal entry: {target}")
return partial[0]
def attach_walk_forward_summary(
journal_path: Path,
target: str,
summary: WalkForwardSummary,
) -> JournalEntry:
"""Attach walk-forward validation summary to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
wfqs_score, wfqs_breakdown = compute_wfqs(summary)
wfqs_v2_score, wfqs_v2_breakdown = compute_wfqs_v2(summary)
deployment_score, deployment_breakdown = compute_deployment_score(
selected.results.get("train"),
selected.results.get("valid"),
selected.results.get("test"),
summary,
)
updated = selected.model_copy(
update={
"walk_forward_summary": summary,
"wfqs_score": wfqs_score,
"wfqs_breakdown": wfqs_breakdown,
"wfqs_v2_score": wfqs_v2_score,
"wfqs_v2_breakdown": wfqs_v2_breakdown,
"deployment_score": deployment_score,
"deployment_breakdown": deployment_breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_robustness_summary(
journal_path: Path,
target: str,
summary: RobustnessMatrixSummary,
) -> JournalEntry:
"""Attach robustness matrix summary to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
updated = selected.model_copy(
update={
"robustness_matrix_summary": summary,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_out_of_time_robustness_summary(
journal_path: Path,
target: str,
summary: RobustnessMatrixSummary,
) -> JournalEntry:
"""Attach out-of-time robustness summary to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
updated = selected.model_copy(
update={
"out_of_time_robustness_summary": summary,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_common_window_summary(
journal_path: Path,
target: str,
summary: CommonWindowSummary,
) -> JournalEntry:
"""Attach common-window continuous-run summary to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
common_window_score, common_window_breakdown = compute_common_window_score(summary)
updated = selected.model_copy(
update={
"common_window_summary": summary,
"common_window_score": common_window_score,
"common_window_breakdown": common_window_breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_reset_common_window_summary(
journal_path: Path,
target: str,
summary: ResetCommonWindowSummary,
) -> JournalEntry:
"""Attach reset common-window segment summary to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
score, breakdown = compute_reset_common_window_score(summary)
updated = selected.model_copy(
update={
"reset_common_window_summary": summary,
"reset_common_window_score": score,
"reset_common_window_breakdown": breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_multi_capital_common_window_summary(
journal_path: Path,
target: str,
summary: MultiCapitalCommonWindowSummary,
) -> JournalEntry:
"""Attach comparable 10k/25k/100k common-window runs to an existing entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
score, breakdown = compute_multi_capital_common_window_score(summary)
updated = selected.model_copy(
update={
"multi_capital_common_window_summary": summary,
"multi_capital_common_window_score": score,
"multi_capital_common_window_breakdown": breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_scenario_robustness(
journal_path: Path,
target: str,
rrs: float,
breakdown: dict[str, float],
) -> JournalEntry:
"""Attach scenario-test RRS to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
updated = selected.model_copy(
update={
"scenario_robustness_score": rrs,
"scenario_robustness_breakdown": breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
def attach_overfit_check(
journal_path: Path,
target: str,
score: float,
breakdown: dict[str, float],
) -> JournalEntry:
"""Attach overfit-check score to an existing journal entry."""
entries = load_journal(journal_path)
selected = _resolve_journal_target(entries, target)
updated = selected.model_copy(
update={
"overfit_check_score": score,
"overfit_check_breakdown": breakdown,
}
)
replace_journal_entry(journal_path, updated)
return updated
@functools.lru_cache(maxsize=1024)
def _load_manifest_json(experiment_name: str) -> dict[str, Any] | None:
path = _EXPERIMENTS_DIR / f"{experiment_name}.json"
if not path.exists():
return None
with path.open("r", encoding="utf-8") as fh:
return json.load(fh)
def _manifest_has_exact_pocket_structure(experiment_name: str) -> bool:
manifest = _load_manifest_json(experiment_name)
if manifest is None:
return False
for engine in manifest.get("strategy_engines", []):
engine_id = str(engine.get("engine_id", "")).lower()
if engine.get("enabled", True) and "exact" in engine_id:
return True
return False
def _manifest_has_named_micro_structure(experiment_name: str) -> bool:
manifest = _load_manifest_json(experiment_name)
if manifest is None:
return False
for engine in manifest.get("strategy_engines", []):
engine_id = str(engine.get("engine_id", "")).lower()
if any(token in engine_id for token in _NAMED_MICRO_ENGINE_TOKENS):
return True
return False
def classify_strategy_family(experiment_name: str, tags: list[str] | None = None) -> str:
"""Classify an experiment into a coarse strategy family for leaderboard filtering."""
lowered_name = experiment_name.lower()
lowered_tags = {tag.lower() for tag in tags or []}
if lowered_name.startswith("return_book_overlay") or "overlay" in lowered_tags:
return "overlay"
if _manifest_has_exact_pocket_structure(experiment_name):
return "exact_pocket_return_max_long"
if _manifest_has_named_micro_structure(experiment_name):
return "named_micro_return_max_long"
if (
"exact" in lowered_name
or "combo" in lowered_name
or "exact" in lowered_tags
or "combo" in lowered_tags
):
return "exact_pocket_return_max_long"
if lowered_name.startswith("return_max_long") and "_bp" in lowered_name:
return "leveraged_return_max_long"
if lowered_name.startswith("return_max_long"):
return "return_max_long"
if "short_core" in lowered_name or "short_core" in lowered_tags:
return "short_core"
if lowered_name.startswith("pead_") or "pead" in lowered_tags:
return "legacy_pead"
return "other"
def is_retired_strategy_family(strategy_family: str) -> bool:
"""Return True when the strategy family is retired from the default workflow."""
return strategy_family in _RETIRED_STRATEGY_FAMILIES
def _entry_id_number(entry_id: str | None) -> int | None:
if not entry_id:
return None
lowered = entry_id.lower()
if not lowered.startswith("imp-"):
return None
raw = entry_id.split("-", 1)[1]
return int(raw) if raw.isdigit() else None
def is_active_research_entry(entry_id: str | None) -> bool:
"""Return True for experiments inside the current active research window."""
number = _entry_id_number(entry_id)
if number is None:
return True
return number >= _DEFAULT_ACTIVE_RESEARCH_ENTRY_ID_FLOOR
def _should_apply_active_research_cutoff(entry_ids: list[str]) -> bool:
numbers = [_entry_id_number(entry_id) for entry_id in entry_ids]
return any(number is not None and number >= _DEFAULT_ACTIVE_RESEARCH_ENTRY_ID_FLOOR for number in numbers)
def filter_registry_entries(
entries: list[RegistryEntry],
*,
include_retired: bool = False,
) -> list[RegistryEntry]:
"""Filter registry entries for the default surfaced leaderboard."""
status_map = _load_exp_status_map()
enforce_status_membership = bool(status_map) and any(
entry.experiment_name in status_map for entry in entries
)
filtered = list(entries)
if not include_retired:
filtered = [
entry
for entry in filtered
if (
not entry.is_retired
and (
not enforce_status_membership
or (
entry.experiment_name in status_map
and status_map.get(entry.experiment_name) != "retired"
)
)
)
]
filtered = [
entry
for entry in filtered
if (
entry.sqs_score is not None
and entry.strategy_family != "overlay"
and entry.trade_count > 0
and entry.valid_trade_count > 0
)
]
deduped: list[RegistryEntry] = []
seen_names: set[str] = set()
for entry in filtered:
if entry.experiment_name in seen_names:
continue
seen_names.add(entry.experiment_name)
deduped.append(entry)
return deduped
def _is_retired_journal_entry(entry: JournalEntry, *, apply_active_cutoff: bool = False) -> bool:
return (
is_retired_strategy_family(classify_strategy_family(entry.experiment_name, entry.tags))
or (apply_active_cutoff and not is_active_research_entry(entry.entry_id))
)
def _is_complete_journal_entry(entry: JournalEntry) -> bool:
test_result = entry.results.get("test")
valid_result = entry.results.get("valid")
return (
test_result is not None
and valid_result is not None
and test_result.trade_count > 0
and valid_result.trade_count > 0
)
def _load_optional_walk_forward_summary(
experiment_name: str,
runs_dir: Path | None = None,
) -> WalkForwardSummary | None:
runs_root = runs_dir or Path("runs")
summary_path = runs_root / f"{experiment_name}_wfv" / "walk_forward" / "walk_forward_summary.json"
if not summary_path.exists():
return None
return WalkForwardSummary.model_validate_json(summary_path.read_text())
def _load_optional_robustness_summary(
experiment_name: str,
runs_dir: Path | None = None,
) -> RobustnessMatrixSummary | None:
runs_root = runs_dir or Path("runs")
summary_path = runs_root / f"{experiment_name}_rm" / "robustness_matrix" / "robustness_matrix_summary.json"
if not summary_path.exists():
return None
return RobustnessMatrixSummary.model_validate_json(summary_path.read_text())
def _load_optional_out_of_time_robustness_summary(
experiment_name: str,
runs_dir: Path | None = None,
) -> RobustnessMatrixSummary | None:
runs_root = runs_dir or Path("runs")
summary_path = runs_root / f"{experiment_name}_oot_rm" / "robustness_matrix" / "robustness_matrix_summary.json"
if not summary_path.exists():
return None
return RobustnessMatrixSummary.model_validate_json(summary_path.read_text())
def _build_manifest_index(
runs_dir: Path,
) -> dict[str, list[tuple[Path, dict, dict]]]:
"""Build experiment_name -> [(run_dir, manifest_data, metadata)] index in one rglob pass."""
index: dict[str, list[tuple[Path, dict, dict]]] = {}
for manifest_file in sorted(runs_dir.rglob("manifest.json")):
run_path = manifest_file.parent
try:
manifest_data = json.loads(manifest_file.read_text())
except Exception:
continue
exp_name = manifest_data.get("experiment_name")
if not exp_name:
continue
metadata_file = run_path / "metadata.json"
try:
metadata = json.loads(metadata_file.read_text()) if metadata_file.exists() else {}
except Exception:
metadata = {}
index.setdefault(exp_name, []).append((run_path, manifest_data, metadata))
return index
def _run_matches_manifest_snapshot(run_path: Path, manifest_data: dict) -> bool:
"""Return True when the run used the manifest's canonical snapshot id."""
expected_snapshot_id = manifest_data.get("dataset_snapshot_id")
if not expected_snapshot_id:
return True
try:
expected_canonical_snapshot_id = resolve_snapshot(expected_snapshot_id).canonical_snapshot_id
except Exception:
expected_canonical_snapshot_id = expected_snapshot_id
resolved_config_path = run_path / "resolved_config.json"
if not resolved_config_path.exists():
return True
try:
resolved_config = json.loads(resolved_config_path.read_text())
except Exception:
return True
actual_snapshot_id = (
resolved_config.get("canonical_snapshot_id")
or resolved_config.get("dataset_snapshot_id")
)
if not actual_snapshot_id:
return True
try:
actual_canonical_snapshot_id = resolve_snapshot(actual_snapshot_id).canonical_snapshot_id
except Exception:
actual_canonical_snapshot_id = actual_snapshot_id
return actual_canonical_snapshot_id == expected_canonical_snapshot_id
def _scan_runs_from_index(
run_entries: list[tuple[Path, dict, dict]],
) -> dict[str, tuple[str, "MetricsBundle"]]:
"""Resolve split runs from pre-indexed manifest entries (no rglob)."""
from libs.backtest.domain import MetricsBundle
matches: list[tuple[Path, str, str, str, str | None]] = []
for run_path, manifest_data, metadata in run_entries:
if not _run_matches_manifest_snapshot(run_path, manifest_data):
continue
run_id = metadata.get("run_id", run_path.name)
started_at = str(metadata.get("started_at") or metadata.get("finished_at") or "")
split_name = metadata.get("split_name")
parts = run_path.name.split("_")
config_hash = parts[-1] if len(parts) >= 2 else ""
matches.append((run_path, run_id, config_hash, started_at, split_name))
if not matches:
return {}
results: dict[str, tuple[str, MetricsBundle]] = {}
has_split_names = any(sn is not None for _, _, _, _, sn in matches)
canonical_splits = {"train", "valid", "test"}
if has_split_names:
canonical_matches = [m for m in matches if m[4] in canonical_splits]
if not canonical_matches:
return {}
latest_by_split: dict[str, tuple[str, Path, str]] = {}
for run_path, run_id, _, started_at, split_name in canonical_matches:
split = split_name or "unknown"
current = latest_by_split.get(split)
if current is None or started_at >= current[0]:
latest_by_split[split] = (started_at, run_path, run_id)
for split, (_, run_path, run_id) in latest_by_split.items():
metrics_file = run_path / "metrics" / "metrics_summary.json"
if metrics_file.exists():
metrics = MetricsBundle.model_validate_json(metrics_file.read_text())
results[split] = (run_id, metrics)
else:
from collections import defaultdict
groups: dict[str, list[tuple[str, Path, str]]] = defaultdict(list)
for run_path, run_id, config_hash, started_at, _ in matches:
groups[config_hash].append((started_at, run_path, run_id))
best_hash = max(groups, key=lambda h: max(t[0] for t in groups[h]))
sorted_runs = sorted(groups[best_hash], key=lambda t: t[0])
split_order = ["train", "valid", "test"]
for i, (_, run_path, run_id) in enumerate(sorted_runs[-3:]):
split = split_order[i] if i < 3 else f"extra_{i}"
metrics_file = run_path / "metrics" / "metrics_summary.json"
if metrics_file.exists():
metrics = MetricsBundle.model_validate_json(metrics_file.read_text())
results[split] = (run_id, metrics)
return results
def sync_official_manifests(
journal_path: Path,
runs_dir: Path,
configs_dir: Path | None = None,
) -> list[JournalEntry]:
"""Append official manifests with full public validation that are missing from the journal."""
configs_root = configs_dir or _EXPERIMENTS_DIR
existing_entries = load_journal(journal_path)
existing_names = {entry.experiment_name for entry in existing_entries}
next_index = len(existing_entries) + 1
synced_entries: list[JournalEntry] = []
# Build manifest index once (single rglob) instead of per-experiment
manifest_index = _build_manifest_index(runs_dir)
for manifest_path in sorted(configs_root.glob("*.json")):
if manifest_path.name.startswith("."):
continue
manifest_payload = json.loads(manifest_path.read_text())
experiment_name = str(manifest_payload.get("experiment_name") or manifest_path.stem)
if experiment_name in existing_names:
continue
run_entries = manifest_index.get(experiment_name)
if not run_entries:
continue
split_runs = _scan_runs_from_index(run_entries)
if not {"train", "valid", "test"}.issubset(split_runs):
continue
results: dict[str, SplitResult] = {}
for split_name in ("train", "valid", "test"):
run_id, metrics = split_runs[split_name]
results[split_name] = build_split_result(split_name, run_id, metrics)
test_metrics = _metrics_from_split_result(results.get("test"))
sqs_score = None
sqs_breakdown: dict[str, float] = {}
sqs_v2_score = None
sqs_v2_breakdown: dict[str, float] = {}
if test_metrics is not None:
sqs_score, sqs_breakdown = compute_sqs(test_metrics)
sqs_v2_score, sqs_v2_breakdown = compute_sqs_v2(test_metrics)
promotion_score, promotion_breakdown = compute_promotion_score(
results.get("test"),
results.get("valid"),
)
unified_score, unified_breakdown = compute_unified_score(
results.get("test"),
results.get("valid"),
)
rqs_score, rqs_breakdown = compute_rqs(
results.get("train"),
results.get("valid"),
results.get("test"),
)
walk_forward_summary = _load_optional_walk_forward_summary(experiment_name, runs_dir)
wfqs_score, wfqs_breakdown = compute_wfqs(walk_forward_summary)
wfqs_v2_score, wfqs_v2_breakdown = compute_wfqs_v2(walk_forward_summary)
deployment_score, deployment_breakdown = compute_deployment_score(
results.get("train"),
results.get("valid"),
results.get("test"),
walk_forward_summary,
rqs_score=rqs_score,
wfqs_score=wfqs_score,
)
robustness_matrix_summary = _load_optional_robustness_summary(experiment_name, runs_dir)
out_of_time_robustness_summary = _load_optional_out_of_time_robustness_summary(experiment_name, runs_dir)
public_sqs, public_breakdown, _ = compute_public_sqs(
results.get("train"),
results.get("valid"),
results.get("test"),
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_score=wfqs_v2_score,
)
if public_sqs is None:
continue
public_sqs_v3, public_breakdown_v3, _ = compute_public_sqs_v3(
results.get("train"),
results.get("valid"),
results.get("test"),
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
stress_sqs, stress_breakdown, _ = compute_public_sqs_v2(
results.get("train"),
results.get("valid"),
results.get("test"),
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
rqs_score=rqs_score,
wfqs_v2_score=wfqs_v2_score,
)
sqs_score = public_sqs
sqs_breakdown = public_breakdown
timestamp = utc_now().isoformat()
entry = JournalEntry(
entry_id=f"IMP-{next_index:04d}",
timestamp=timestamp,
experiment_name=experiment_name,
hypothesis=str(manifest_payload.get("description") or manifest_payload.get("notes") or ""),
results=results,
walk_forward_summary=walk_forward_summary,
robustness_matrix_summary=robustness_matrix_summary,
out_of_time_robustness_summary=out_of_time_robustness_summary,
sqs_score=sqs_score,
sqs_breakdown=sqs_breakdown,
sqs_v3_score=public_sqs_v3,
sqs_v3_breakdown=public_breakdown_v3,
stress_sqs_score=stress_sqs,
stress_sqs_breakdown=stress_breakdown,
sqs_v2_score=sqs_v2_score,
sqs_v2_breakdown=sqs_v2_breakdown,
promotion_score=promotion_score,
promotion_breakdown=promotion_breakdown,
unified_score=unified_score,
unified_breakdown=unified_breakdown,
rqs_score=rqs_score,
rqs_breakdown=rqs_breakdown,
wfqs_score=wfqs_score,
wfqs_breakdown=wfqs_breakdown,
wfqs_v2_score=wfqs_v2_score,
wfqs_v2_breakdown=wfqs_v2_breakdown,
deployment_score=deployment_score,
deployment_breakdown=deployment_breakdown,
verdict="unknown",
verdict_reasoning="Auto-synced from official manifest.",
next_direction="",
tags=list(manifest_payload.get("tags") or []),
)
append_journal_entry(journal_path, entry)
synced_entries.append(entry)
existing_names.add(experiment_name)
next_index += 1
# Update experiment JSON with cached performance summary and activate draft
try:
from libs.backtest.experiments import update_performance_summary
test_result = results.get("test")
perf = {
"sqs_score": sqs_score,
"public_sqs": public_sqs,
"trade_count_test": test_result.trade_count if test_result is not None else None,
}
update_performance_summary(
experiment_name, perf,
configs_dir=configs_root,
activate_if_draft=True,
)
except Exception:
pass
return synced_entries
# ---------------------------------------------------------------------------
# Registry / Leaderboard
# ---------------------------------------------------------------------------
def rebuild_registry(
journal_path: Path,
registry_path: Path,
leaderboard_path: Path,
) -> ExperimentRegistry:
"""Rebuild experiment_registry.json and LEADERBOARD.md from journal."""
entries = load_journal(journal_path)
apply_active_cutoff = _should_apply_active_research_cutoff([entry.entry_id for entry in entries])
registry_entries: list[RegistryEntry] = []
for je in entries:
strategy_family = classify_strategy_family(je.experiment_name, je.tags)
is_retired = is_retired_strategy_family(strategy_family) or (
apply_active_cutoff and not is_active_research_entry(je.entry_id)
)
if strategy_family == "overlay":
continue
test_result = _hydrate_split_result(je.results.get("test"))
valid_result = _hydrate_split_result(je.results.get("valid"))
train_result = _hydrate_split_result(je.results.get("train"))
computed_sqs_v2 = None
computed_rqs = None
computed_rqs_breakdown: dict[str, float] = {}
computed_wfqs = None
computed_wfqs_breakdown: dict[str, float] = {}
computed_deployment = None
computed_deployment_breakdown: dict[str, float] = {}
test_metrics = _metrics_from_split_result(test_result)
if test_metrics is not None:
computed_sqs_v2, _ = compute_sqs_v2(test_metrics)
computed_promotion_score, _ = compute_promotion_score(test_result, valid_result)
computed_unified_score, _ = compute_unified_score(test_result, valid_result)
computed_rqs, computed_rqs_breakdown = compute_rqs(
train_result,
valid_result,
test_result,
)
computed_wfqs, computed_wfqs_breakdown = compute_wfqs(je.walk_forward_summary)
computed_wfqs_v2, _ = compute_wfqs_v2(je.walk_forward_summary)
computed_deployment, computed_deployment_breakdown = compute_deployment_score(
train_result,
valid_result,
test_result,
je.walk_forward_summary,
rqs_score=computed_rqs,
wfqs_score=computed_wfqs,
)
computed_common_window, computed_common_window_breakdown = compute_common_window_score(
je.common_window_summary
)
computed_reset_common_window, computed_reset_common_window_breakdown = (
compute_reset_common_window_score(je.reset_common_window_summary)
)
computed_multi_capital_common_window, computed_multi_capital_common_window_breakdown = (
compute_multi_capital_common_window_score(je.multi_capital_common_window_summary)
)
computed_public_sqs_v3, _, _ = compute_public_sqs_v3(
train_result,
valid_result,
test_result,
walk_forward_summary=je.walk_forward_summary,
robustness_matrix_summary=je.robustness_matrix_summary,
out_of_time_robustness_summary=je.out_of_time_robustness_summary,
rqs_score=computed_rqs,
wfqs_v2_score=computed_wfqs_v2,
)
computed_public_sqs, computed_public_breakdown, _ = compute_public_sqs(
train_result,
valid_result,
test_result,
walk_forward_summary=je.walk_forward_summary,
robustness_matrix_summary=je.robustness_matrix_summary,
out_of_time_robustness_summary=je.out_of_time_robustness_summary,
common_window_summary=je.common_window_summary,
reset_common_window_summary=je.reset_common_window_summary,
multi_capital_common_window_summary=je.multi_capital_common_window_summary,
rqs_score=computed_rqs,
wfqs_score=computed_wfqs_v2,
scenario_robustness_score=je.scenario_robustness_score,
)
computed_stress_sqs, _, _ = compute_public_sqs_v2(
train_result,
valid_result,
test_result,
walk_forward_summary=je.walk_forward_summary,
robustness_matrix_summary=je.robustness_matrix_summary,
out_of_time_robustness_summary=je.out_of_time_robustness_summary,
rqs_score=computed_rqs,
wfqs_v2_score=computed_wfqs_v2,
)
canonical_sqs = computed_public_sqs
registry_entries.append(
RegistryEntry(
entry_id=je.entry_id,
experiment_name=je.experiment_name,
strategy_family=strategy_family,
is_retired=is_retired,
sqs_score=canonical_sqs,
sqs_v3_score=computed_public_sqs_v3,
stress_sqs_score=computed_stress_sqs,
sqs_v2_score=je.sqs_v2_score if je.sqs_v2_score is not None else computed_sqs_v2,
promotion_score=(
je.promotion_score if je.promotion_score is not None else computed_promotion_score
),
unified_score=(
je.unified_score if je.unified_score is not None else computed_unified_score
),
rqs_score=computed_rqs if computed_rqs is not None else je.rqs_score,
wfqs_score=computed_wfqs if computed_wfqs is not None else je.wfqs_score,
wfqs_v2_score=computed_wfqs_v2 if computed_wfqs_v2 is not None else je.wfqs_v2_score,
deployment_score=computed_deployment if computed_deployment is not None else je.deployment_score,
common_window_score=(
computed_common_window if computed_common_window is not None else je.common_window_score
),
common_window_summary=je.common_window_summary,
reset_common_window_score=(
computed_reset_common_window
if computed_reset_common_window is not None
else je.reset_common_window_score
),
reset_common_window_summary=je.reset_common_window_summary,
multi_capital_common_window_score=(
computed_multi_capital_common_window
if computed_multi_capital_common_window is not None
else je.multi_capital_common_window_score
),
multi_capital_common_window_summary=je.multi_capital_common_window_summary,
walk_forward_summary=je.walk_forward_summary,
robustness_matrix_summary=je.robustness_matrix_summary,
out_of_time_robustness_summary=je.out_of_time_robustness_summary,
scenario_robustness_score=je.scenario_robustness_score,
overfit_check_score=je.overfit_check_score,
train_total_return_pct=train_result.total_return_pct if train_result else None,
train_annualized_return_pct=train_result.annualized_return_pct if train_result else None,
profit_factor=test_result.profit_factor if test_result else None,
total_return_pct=test_result.total_return_pct if test_result else None,
annualized_return_pct=test_result.annualized_return_pct if test_result else None,
win_rate=test_result.win_rate if test_result else None,
sharpe_ratio=test_result.sharpe_ratio if test_result else None,
max_drawdown_pct=test_result.max_drawdown_pct if test_result else None,
trade_count=test_result.trade_count if test_result else 0,
avg_gross_exposure_pct=test_result.avg_gross_exposure_pct if test_result else None,
avg_net_exposure_pct=test_result.avg_net_exposure_pct if test_result else None,
days_in_market_pct=test_result.days_in_market_pct if test_result else None,
valid_profit_factor=valid_result.profit_factor if valid_result else None,
valid_total_return_pct=valid_result.total_return_pct if valid_result else None,
valid_annualized_return_pct=valid_result.annualized_return_pct if valid_result else None,
valid_win_rate=valid_result.win_rate if valid_result else None,
valid_sharpe_ratio=valid_result.sharpe_ratio if valid_result else None,
valid_max_drawdown_pct=valid_result.max_drawdown_pct if valid_result else None,
valid_trade_count=valid_result.trade_count if valid_result else 0,
valid_avg_gross_exposure_pct=valid_result.avg_gross_exposure_pct if valid_result else None,
valid_avg_net_exposure_pct=valid_result.avg_net_exposure_pct if valid_result else None,
valid_days_in_market_pct=valid_result.days_in_market_pct if valid_result else None,
timestamp=je.timestamp,
)
)
# Default registry / markdown ordering: SQS descending, then visible return
# metrics as tiebreakers (higher return / lower DD wins), then internal scores.
registry_entries.sort(
key=lambda e: (
-(e.sqs_score or 0.0),
# Visible table tiebreakers
e.valid_total_return_pct is None,
-(e.valid_total_return_pct or 0.0),
e.total_return_pct is None,
-(e.total_return_pct or 0.0),
e.annualized_return_pct is None,
-(e.annualized_return_pct or 0.0),
e.max_drawdown_pct is None,
+(e.max_drawdown_pct or 100.0), # lower DD is better
# Internal score tiebreakers
e.stress_sqs_score is None,
-(e.stress_sqs_score or 0.0),
e.deployment_score is None,
-(e.deployment_score or 0.0),
e.rqs_score is None,
-(e.rqs_score or 0.0),
e.promotion_score is None,
-(e.promotion_score or 0.0),
)
)
registry = ExperimentRegistry(
entries=registry_entries,
updated_at=utc_now().isoformat(),
)
# Write registry JSON
registry_path.parent.mkdir(parents=True, exist_ok=True)
registry_path.write_text(registry.model_dump_json(indent=2))
# Write LEADERBOARD.md
_write_leaderboard_md(leaderboard_path, registry, entries)
logger.info("registry_rebuilt", count=len(registry_entries))
return registry
def _load_exp_index_map(configs_dir: Path | None = None) -> dict[str, dict]:
"""Load experiment metadata from .index.json keyed by experiment name."""
idx = (configs_dir or Path("configs/experiments")) / ".index.json"
if not idx.exists():
return {}
try:
data = json.loads(idx.read_text())
experiments = data.get("experiments", {})
if isinstance(experiments, dict):
return experiments
return {}
except Exception:
return {}
def _load_exp_id_map(configs_dir: Path | None = None) -> dict[str, int]:
"""Load experiment name -> id mapping from .index.json."""
return {
name: meta["id"]
for name, meta in _load_exp_index_map(configs_dir).items()
if isinstance(meta.get("id"), int)
}
def _load_exp_status_map(configs_dir: Path | None = None) -> dict[str, str]:
"""Load experiment name -> status mapping from .index.json."""
return {
name: normalize_experiment_status(str(meta.get("status", "")))
for name, meta in _load_exp_index_map(configs_dir).items()
if isinstance(meta, dict)
}
def _write_leaderboard_md(
path: Path,
registry: ExperimentRegistry,
journal_entries: list[JournalEntry],
) -> None:
apply_active_cutoff = _should_apply_active_research_cutoff([entry.entry_id for entry in journal_entries])
status_map = _load_exp_status_map()
enforce_status_membership = bool(status_map) and any(
entry.experiment_name in status_map for entry in journal_entries
)
visible_entries = filter_registry_entries(
registry.entries,
include_retired=False,
)
exp_id_map = _load_exp_id_map()
visible_recent = [
entry
for entry in reversed(journal_entries)
if (
not _is_retired_journal_entry(entry, apply_active_cutoff=apply_active_cutoff)
and (
not enforce_status_membership
or (
entry.experiment_name in status_map
and status_map.get(entry.experiment_name) != "retired"
)
)
and _is_complete_journal_entry(entry)
and classify_strategy_family(entry.experiment_name, entry.tags) != "overlay"
)
][:5]
lines: list[str] = []
lines.append("# Strategy Improvement Leaderboard")
lines.append(f"_Updated: {registry.updated_at}_\n")
lines.append("_Default view excludes retired legacy PEAD / short-core / exact-pocket families, retired pre-IMP-0606 research, and incomplete train-only scans. Use `fithia2 lb --include-retired` to inspect retired research._\n")
lines.append("_`SQS` below is public SQS v9: 3-pillar additive core (RQS 35% + WFQS 40% + Regime Adaptability 25%). Regime score uses scenario-test RRS when available, falls back to OOT quality, then 50.0 neutral. OOT bias toward COVID-era performance removed. CW blend and deployment gates unchanged from v8._\n")
lines.append("| # | ID | Experiment | SQS | RCW | [Tr]Ret% | [V]Ret% | [T]Ret% | [T]Ann% | [T]DD% | [T]Gross% | [T]DIM% | [T]R/G | Date |")
lines.append("|---|-----|-----------|-----|-----|----------|----------|----------|----------|--------|------------|----------|---------|------|")
for rank, e in enumerate(visible_entries, 1):
rcw = f"{e.reset_common_window_score:.1f}" if e.reset_common_window_score is not None else "-"
trret = f"{e.train_total_return_pct:+.1f}" if e.train_total_return_pct is not None else "-"
vret = f"{e.valid_total_return_pct:+.1f}" if e.valid_total_return_pct is not None else "-"
ret = f"{e.total_return_pct:+.1f}" if e.total_return_pct is not None else "-"
ann = f"{e.annualized_return_pct:+.1f}" if e.annualized_return_pct is not None else "-"
dd = f"{e.max_drawdown_pct:.1f}" if e.max_drawdown_pct is not None else "-"
gross = f"{e.avg_gross_exposure_pct:.1f}" if e.avg_gross_exposure_pct is not None else "-"
dim = f"{e.days_in_market_pct:.1f}" if e.days_in_market_pct is not None else "-"
ret_on_gross = "-"
if (
e.total_return_pct is not None
and e.avg_gross_exposure_pct is not None
and e.avg_gross_exposure_pct > 0
):
ret_on_gross = f"{e.total_return_pct / e.avg_gross_exposure_pct:.2f}"
ts = e.timestamp[:10] if e.timestamp else "-"
eid = exp_id_map.get(e.experiment_name, "-")
lines.append(
f"| {rank} | {eid} | {e.experiment_name} | {e.sqs_score:.1f}"
f" | {rcw}"
f" | {trret} | {vret} | {ret} | {ann} | {dd} | {gross} | {dim} | {ret_on_gross} | {ts} |"
)
# Recent entries (last 5)
if visible_recent:
registry_by_id = {entry.entry_id: entry for entry in registry.entries}
lines.append("\n## Recent Entries")
for je in visible_recent:
canonical_sqs = registry_by_id.get(je.entry_id).sqs_score if je.entry_id in registry_by_id else je.sqs_score
stress_sqs = (
registry_by_id.get(je.entry_id).stress_sqs_score
if je.entry_id in registry_by_id
else je.stress_sqs_score
)
lines.append(f"### {je.entry_id} ({je.timestamp[:10]}) \u2014 {je.experiment_name}")
lines.append(f"Hypothesis: {je.hypothesis}")
stress_suffix = f", Stress SQS {stress_sqs:.1f}" if stress_sqs is not None else ""
lines.append(f"Verdict: **{je.verdict.upper()}** (SQS {canonical_sqs}{stress_suffix})")
if je.verdict_reasoning:
lines.append(f"Reasoning: {je.verdict_reasoning}")
if je.next_direction:
lines.append(f"Next: {je.next_direction}")
lines.append("")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines) + "\n")
# ---------------------------------------------------------------------------
# Run scanning helpers (for CLI record command)
# ---------------------------------------------------------------------------
def scan_runs_for_experiment(
runs_dir: Path,
experiment_name: str,
) -> dict[str, tuple[str, MetricsBundle]]:
"""Scan runs_dir for runs matching experiment_name.
Returns dict of split_name -> (run_id, MetricsBundle).
Uses manifest.json for experiment matching, metadata.json for split_name,
falls back to time-ordered grouping by config hash.
"""
from libs.backtest.domain import MetricsBundle
matches: list[tuple[Path, str, str, str, str | None]] = [] # (run_dir, run_id, config_hash, started_at, split)
for manifest_file in sorted(runs_dir.rglob("manifest.json")):
run_path = manifest_file.parent
manifest_data = json.loads(manifest_file.read_text())
if manifest_data.get("experiment_name") != experiment_name:
continue
if not _run_matches_manifest_snapshot(run_path, manifest_data):
continue
metadata_file = run_path / "metadata.json"
metadata = json.loads(metadata_file.read_text()) if metadata_file.exists() else {}
run_id = metadata.get("run_id", run_path.name)
started_at = str(metadata.get("started_at") or metadata.get("finished_at") or "")
split_name = metadata.get("split_name")
# Extract config hash from run_id (last segment after underscore)
parts = run_path.name.split("_")
config_hash = parts[-1] if len(parts) >= 2 else ""
matches.append((run_path, run_id, config_hash, started_at, split_name))
if not matches:
return {}
results: dict[str, tuple[str, MetricsBundle]] = {}
# Check for split_name in metadata first
has_split_names = False
for _, _, _, _, split_name in matches:
if split_name is not None:
has_split_names = True
break
canonical_splits = {"train", "valid", "test"}
if has_split_names:
canonical_matches = [
(run_path, run_id, config_hash, started_at, split_name)
for run_path, run_id, config_hash, started_at, split_name in matches
if split_name in canonical_splits
]
if not canonical_matches:
return {}
latest_by_split: dict[str, tuple[str, Path, str]] = {}
for run_path, run_id, _, started_at, split_name in canonical_matches:
split = split_name or "unknown"
current = latest_by_split.get(split)
if current is None or started_at >= current[0]:
latest_by_split[split] = (started_at, run_path, run_id)
for split, (_, run_path, run_id) in latest_by_split.items():
metrics_file = run_path / "metrics" / "metrics_summary.json"
if metrics_file.exists():
metrics = MetricsBundle.model_validate_json(metrics_file.read_text())
results[split] = (run_id, metrics)
else:
# Fallback: group by config_hash, then assign train/valid/test by time order
from collections import defaultdict
groups: dict[str, list[tuple[str, Path, str]]] = defaultdict(list)
for run_path, run_id, config_hash, started_at, _ in matches:
groups[config_hash].append((started_at, run_path, run_id))
# Use the largest group (most likely the 3-split set)
biggest_group = max(groups.values(), key=len) if groups else []
split_names = ["train", "valid", "test"]
for i, (_, run_path, run_id) in enumerate(sorted(biggest_group)):
split = split_names[i] if i < len(split_names) else f"extra_{i}"
metrics_file = run_path / "metrics" / "metrics_summary.json"
if metrics_file.exists():
metrics = MetricsBundle.model_validate_json(metrics_file.read_text())
results[split] = (run_id, metrics)
return results
def check_duplicate(journal_path: Path, experiment_name: str) -> list[JournalEntry]:
"""Check if experiment_name already exists in journal."""
entries = load_journal(journal_path)
return [e for e in entries if e.experiment_name == experiment_name]