From c6463034230e8aa5a1f2af1561b9a50ca1e1a3b4 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 17 Mar 2026 02:18:41 -0700 Subject: [PATCH] Recalibrate public SQS and exposure-aware tracking --- README.md | 69 +- apps/backtester/run.py | 34 +- apps/tracker/cli.py | 270 +++- journal/LEADERBOARD.md | 110 +- journal/experiment_registry.json | 1161 +++++++++++++---- journal/improvement_journal.jsonl | 8 +- libs/backtest/domain.py | 63 + libs/backtest/metrics.py | 42 + libs/backtest/tracker.py | 406 +++++- pyproject.toml | 3 + .../integration/backtest/test_backtest_run.py | 9 + tests/unit/backtest/test_metrics.py | 48 +- tests/unit/backtest/test_tracker.py | 292 ++++- 13 files changed, 2129 insertions(+), 386 deletions(-) diff --git a/README.md b/README.md index 0c5b6c2..917c08a 100644 --- a/README.md +++ b/README.md @@ -491,41 +491,50 @@ journal/ ``` 1. 실험 config 생성 configs/experiments/my_experiment.json 2. 3-split 백테스트 실행 for split in train valid test; do ... done -3. Journal에 기록 python -m apps.tracker.cli record ... -4. Leaderboard 확인 python -m apps.tracker.cli leaderboard ... +3. Journal에 기록 fithia2 rec -e my_experiment ... +4. Leaderboard 확인 fithia2 lb 5. 다음 실험 계획 verdict + SQS breakdown 기반 -6. 중복 실험 확인 python -m apps.tracker.cli check-duplicate ... +6. 중복 실험 확인 fithia2 dup -e my_experiment 7. 1번부터 반복 ``` ### 8.5 Tracker CLI +`pip install -e .` 후 `fithia2` 명령어로 실행. `--journal-dir` / `--runs-dir` 생략 시 기본값(`journal/`, `runs/`) 사용. + ```bash +# 도움말 +fithia2 + +# 리더보드 출력 (top 10) +fithia2 lb + +# 리더보드 — 상위 N개 +fithia2 lb -n 20 + # 실험 결과 기록 -python -m apps.tracker.cli record \ - --journal-dir journal/ \ - --runs-dir runs/midcap_steps/ \ - --experiment pead_midcap_step14_score65 \ - --hypothesis "Score threshold 0.60→0.65" \ - --baseline pead_midcap_step13_best \ - --verdict better \ - --reasoning "Test SQS 64.2 > 57.7, Return +0.73% > +0.43%" \ - --next "Test exit tuning on top of score 0.65" - -# 리더보드 출력 -python -m apps.tracker.cli leaderboard --journal-dir journal/ +fithia2 rec \ + -e pead_midcap_step14_score65 \ + -H "Score threshold 0.60→0.65" \ + -b pead_midcap_step13_best \ + -v better \ + -r "Test SQS 64.2 > 57.7, Return +0.73% > +0.43%" \ + -n "Test exit tuning on top of score 0.65" # 엔트리 상세 조회 -python -m apps.tracker.cli show --journal-dir journal/ IMP-0015 +fithia2 s IMP-0015 # 중복 실험 확인 -python -m apps.tracker.cli check-duplicate \ - --journal-dir journal/ --experiment my_experiment_name - -# 레지스트리 재생성 -python -m apps.tracker.cli rebuild-registry --journal-dir journal/ +fithia2 dup -e my_experiment_name ``` +| 명령 | alias | 설명 | +|------|-------|------| +| `leaderboard` | `lb` | SQS 순위표 출력 및 LEADERBOARD.md 재생성 | +| `record` | `rec` | 실험 결과를 저널에 기록 | +| `show` | `s` | 특정 저널 항목 상세 조회 | +| `check-duplicate` | `dup` | 동일 실험명 중복 여부 확인 | + ### 8.6 핵심 발견사항 21개 실험을 통해 얻은 인사이트: @@ -695,18 +704,16 @@ done ```bash # Journal에 기록 -python -m apps.tracker.cli record \ - --journal-dir journal/ \ - --runs-dir runs/midcap_steps/ \ - --experiment pead_midcap_step14_score65 \ - --hypothesis "Score threshold 0.60→0.65" \ - --baseline pead_midcap_step13_best \ - --verdict better \ - --reasoning "Test SQS 64.2 > 57.7" \ - --next "Exit tuning on top of score 0.65" +fithia2 rec \ + -e pead_midcap_step14_score65 \ + -H "Score threshold 0.60→0.65" \ + -b pead_midcap_step13_best \ + -v better \ + -r "Test SQS 64.2 > 57.7" \ + -n "Exit tuning on top of score 0.65" # 리더보드 확인 -python -m apps.tracker.cli leaderboard --journal-dir journal/ +fithia2 lb ``` ### 테스트 diff --git a/apps/backtester/run.py b/apps/backtester/run.py index 88252d6..d0f8555 100644 --- a/apps/backtester/run.py +++ b/apps/backtester/run.py @@ -93,6 +93,22 @@ class BacktestRunner: self._kill_switch_cooldown_remaining = 0 self._engine_daily_new_risk_used: dict[str, float] = defaultdict(float) + def _compute_portfolio_exposure(self, date: dt.date) -> tuple[float, float]: + """Return (gross, net) exposure using current close notional when available.""" + gross = 0.0 + net = 0.0 + for pos in self._open_positions: + bar = self.store.get_bar(pos.plan.candidate.symbol, date) + close = ( + float(bar["close"]) + if bar and bar.get("close") is not None and float(bar["close"]) > 0 + else pos.entry_price + ) + notional = close * pos.shares_open + gross += abs(notional) + net += -notional if pos.plan.candidate.trade_direction == "short" else notional + return gross, net + def run(self, output_root: str | Path | None = None) -> ExperimentResult: """Execute the full simulation. Returns ExperimentResult.""" started_at = utc_now() @@ -369,17 +385,14 @@ class BacktestRunner: if self._peak_equity > 0 else 0.0 ) + gross_exposure, net_exposure = self._compute_portfolio_exposure(date) self._equity_curve.append( DailyPortfolioState( date=date, equity=self._equity, cash_available=self._cash, - gross_exposure=sum( - p.entry_price * p.shares_open for p in self._open_positions - ), - net_exposure=sum( - p.entry_price * p.shares_open for p in self._open_positions - ), + gross_exposure=gross_exposure, + net_exposure=net_exposure, reserved_risk_budget=self._daily_new_risk_used, unrealized_pnl=unrealized_final, realized_pnl=self._realized_pnl, @@ -563,16 +576,13 @@ class BacktestRunner: drawdown_pct: float, unrealized: float, ) -> DailyPortfolioState: + gross_exposure, net_exposure = self._compute_portfolio_exposure(date) return DailyPortfolioState( date=date, equity=self._equity, cash_available=self._cash, - gross_exposure=sum( - p.entry_price * p.shares_open for p in self._open_positions - ), - net_exposure=sum( - p.entry_price * p.shares_open for p in self._open_positions - ), + gross_exposure=gross_exposure, + net_exposure=net_exposure, reserved_risk_budget=self._daily_new_risk_used, unrealized_pnl=unrealized, realized_pnl=self._realized_pnl, diff --git a/apps/tracker/cli.py b/apps/tracker/cli.py index 07a35d9..3b404c3 100644 --- a/apps/tracker/cli.py +++ b/apps/tracker/cli.py @@ -5,6 +5,14 @@ import argparse import sys from pathlib import Path +from rich import box +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +_console = Console(width=140) + from libs.backtest.domain import ( ConfigDelta, JournalEntry, @@ -15,7 +23,11 @@ from libs.backtest.tracker import ( append_journal_entry, build_split_result, check_duplicate, + compute_public_sqs, + compute_promotion_score, compute_sqs, + compute_sqs_v2, + compute_unified_score, get_next_entry_id, load_journal, rebuild_registry, @@ -54,17 +66,41 @@ def cmd_record(args: argparse.Namespace) -> None: for split_name, (run_id, metrics) in split_runs.items(): results[split_name] = build_split_result(split_name, run_id, metrics) - # Compute SQS from test split (or best available) + # Compute public SQS from valid+test when available, or test split fallback. test_metrics = None for preferred in ["test", "valid", "train"]: if preferred in split_runs: _, test_metrics = split_runs[preferred] break - sqs_score = 0.0 + sqs_score = None sqs_breakdown: dict[str, float] = {} + sqs_v2_score = None + sqs_v2_breakdown: dict[str, float] = {} + promotion_score = None + promotion_breakdown: dict[str, float] = {} + unified_score = None + unified_breakdown: dict[str, float] = {} if test_metrics: - sqs_score, sqs_breakdown = compute_sqs(test_metrics) + legacy_sqs_score, legacy_sqs_breakdown = compute_sqs(test_metrics) + sqs_v2_score, sqs_v2_breakdown = compute_sqs_v2(test_metrics) + sqs_score = legacy_sqs_score + sqs_breakdown = legacy_sqs_breakdown + promotion_score, promotion_breakdown = compute_promotion_score( + results.get("test"), + results.get("valid"), + ) + public_sqs, public_breakdown, public_source = compute_public_sqs( + results.get("test"), + results.get("valid"), + ) + if public_sqs is not None: + sqs_score = public_sqs + sqs_breakdown = public_breakdown + unified_score, unified_breakdown = compute_unified_score( + results.get("test"), + results.get("valid"), + ) # Build config delta config_delta = None @@ -84,6 +120,12 @@ def cmd_record(args: argparse.Namespace) -> None: results=results, sqs_score=sqs_score, sqs_breakdown=sqs_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, verdict=args.verdict or "unknown", verdict_reasoning=args.reasoning or "", next_direction=args.next or "", @@ -91,7 +133,8 @@ def cmd_record(args: argparse.Namespace) -> None: ) append_journal_entry(journal_path, entry) - print(f"Recorded {entry_id}: {args.experiment} (SQS={sqs_score})") + source_label = f", source={public_source}" if public_source else "" + print(f"Recorded {entry_id}: {args.experiment} (SQS={sqs_score}{source_label})") # Show splits found for split_name, sr in results.items(): @@ -104,6 +147,15 @@ def cmd_record(args: argparse.Namespace) -> None: print(f"Leaderboard updated: {leaderboard_path}") +_DEFAULT_JOURNAL_DIR = "journal" + +_COL_NAME = 38 # max experiment name width before truncation + + +def _fmt(val: float | None, fmt: str) -> str: + return format(val, fmt) if val is not None else "-" + + def cmd_leaderboard(args: argparse.Namespace) -> None: """Show or regenerate the leaderboard.""" journal_dir = Path(args.journal_dir) @@ -117,13 +169,66 @@ def cmd_leaderboard(args: argparse.Namespace) -> None: registry = rebuild_registry(journal_path, registry_path, leaderboard_path) - # Print to console - print(f"\n{'#':>3} {'Experiment':<40} {'SQS':>5} {'PF':>5} {'Ret%':>6} {'Trades':>6}") - print("-" * 70) - for rank, e in enumerate(registry.entries, 1): - pf = f"{e.profit_factor:.2f}" if e.profit_factor is not None else "-" - ret = f"{e.total_return_pct:+.1f}" if e.total_return_pct is not None else "-" - print(f"{rank:>3} {e.experiment_name:<40} {e.sqs_score:>5.1f} {pf:>5} {ret:>6} {e.trade_count:>6}") + top_n = getattr(args, "top", 10) + sort_by = getattr(args, "sort", "sqs") + if sort_by == "promotion": + ranked_entries = sorted( + registry.entries, + key=lambda e: (e.promotion_score is None, -(e.promotion_score or 0.0), -e.sqs_score), + ) + title_mode = "Promotion 기준 내림차순" + elif sort_by in {"sqs", "unified"}: + ranked_entries = sorted(registry.entries, key=lambda e: -e.sqs_score) + title_mode = "SQS 기준 내림차순" + else: + ranked_entries = sorted(registry.entries, key=lambda e: -e.sqs_score) + title_mode = "SQS 기준 내림차순" + total = len(ranked_entries) + + tbl = Table( + box=box.SIMPLE_HEAD, + show_header=True, + header_style="bold yellow", + row_styles=["", "dim"], + padding=(0, 1), + title=f"[bold cyan]Top {top_n} / {total}[/] [dim]· {title_mode} · T=test V=valid[/]", + title_justify="left", + expand=False, + ) + # Columns: #, Experiment, SQS | Test: PF / Ret% / WR / DD% / N | Valid: PF / Ret% / WR / N + tbl.add_column("#", justify="right", style="bold", no_wrap=True, min_width=3) + tbl.add_column("Experiment", no_wrap=True, min_width=30) + tbl.add_column("SQS", justify="right", style="bold cyan", no_wrap=True, min_width=5) + tbl.add_column("T.PF", justify="right", no_wrap=True, min_width=5) + tbl.add_column("T.Ret%", justify="right", no_wrap=True, min_width=6) + tbl.add_column("T.WR%", justify="right", no_wrap=True, min_width=5) + tbl.add_column("T.DD%", justify="right", no_wrap=True, min_width=5) + tbl.add_column("T.N", justify="right", no_wrap=True, min_width=4) + tbl.add_column("V.PF", justify="right", style="green", no_wrap=True, min_width=5) + tbl.add_column("V.Ret%", justify="right", style="green", no_wrap=True, min_width=6) + tbl.add_column("V.WR%", justify="right", style="green", no_wrap=True, min_width=5) + tbl.add_column("V.N", justify="right", style="green", no_wrap=True, min_width=4) + + for rank, e in enumerate(ranked_entries[:top_n], 1): + name = e.experiment_name + if len(name) > _COL_NAME: + name = name[: _COL_NAME - 1] + "…" + pf = _fmt(e.profit_factor, ".2f") + ret = _fmt(e.total_return_pct, "+.1f") + wr = _fmt(e.win_rate * 100 if e.win_rate is not None else None, ".0f") + dd = _fmt(e.max_drawdown_pct, ".1f") + vpf = _fmt(e.valid_profit_factor, ".2f") + vret = _fmt(e.valid_total_return_pct, "+.1f") + vwr = _fmt(e.valid_win_rate * 100 if e.valid_win_rate is not None else None, ".0f") + tbl.add_row( + str(rank), name, f"{e.sqs_score:.1f}", + pf, ret, wr, dd, str(e.trade_count), + vpf, vret, vwr, str(e.valid_trade_count) if e.valid_trade_count else "-", + ) + + _console.print() + _console.print(tbl) + _console.print(f" [dim]LEADERBOARD.md → {leaderboard_path}[/]\n") def cmd_show(args: argparse.Namespace) -> None: @@ -144,11 +249,28 @@ def cmd_show(args: argparse.Namespace) -> None: sys.exit(1) for e in found: + promotion_score = e.promotion_score + promotion_breakdown = e.promotion_breakdown + if promotion_score is None: + promotion_score, promotion_breakdown = compute_promotion_score( + e.results.get("test"), + e.results.get("valid"), + ) + public_sqs, public_breakdown, public_source = compute_public_sqs( + e.results.get("test"), + e.results.get("valid"), + ) + if public_sqs is None: + public_sqs = e.sqs_score + public_breakdown = e.sqs_breakdown + public_source = "stored" print(f"\n{e.entry_id} — {e.experiment_name}") print(f" Timestamp: {e.timestamp}") print(f" Hypothesis: {e.hypothesis}") print(f" Verdict: {e.verdict}") - print(f" SQS: {e.sqs_score} {e.sqs_breakdown}") + print(f" SQS: {public_sqs} {public_breakdown} [{public_source}]") + if promotion_score is not None: + print(f" Promotion: {promotion_score} {promotion_breakdown} [internal]") if e.config_delta: print(f" Baseline: {e.config_delta.base_experiment}") for k, v in e.config_delta.changes.items(): @@ -177,43 +299,107 @@ def cmd_check_duplicate(args: argparse.Namespace) -> None: print(f"No existing entries for '{args.experiment}'.") +def _print_help() -> None: + _console.print() + _console.print(Panel( + "[bold cyan]fithia2[/] — ACE-F Strategy Improvement Tracker\n" + "[dim]백테스트 실험을 기록하고 전략 품질 점수(SQS)로 순위를 매깁니다.[/]", + border_style="cyan", + padding=(0, 2), + )) + + t = Table(box=box.SIMPLE, show_header=True, header_style="bold yellow", padding=(0, 2)) + t.add_column("Command", style="bold green", no_wrap=True) + t.add_column("Description") + t.add_column("Key Options", style="dim") + + t.add_row( + "leaderboard [dim]lb[/]", + "SQS 순위표 출력 및 LEADERBOARD.md 재생성", + "-n N --sort sqs|promotion", + ) + t.add_row( + "record [dim]rec[/]", + "실험 결과를 저널에 기록", + "-e NAME -H TEXT -v better|worse|neutral -b BASELINE", + ) + t.add_row( + "show [dim]s[/]", + "특정 저널 항목 상세 조회", + "ENTRY_ID (예: IMP-0001 또는 실험명)", + ) + t.add_row( + "check-duplicate [dim]dup[/]", + "동일 실험명이 이미 기록됐는지 확인", + "-e NAME", + ) + + _console.print(t) + + _console.print( + " [dim]공통 옵션:[/] [bold]--journal-dir[/] [dim](기본: journal/)[/]" + " [bold]--runs-dir[/] [dim](기본: runs/)[/]\n" + ) + + _console.print(" [bold]예시[/]") + _console.print(" [green]fithia2 leaderboard[/]") + _console.print(" [green]fithia2 leaderboard --top 20[/]") + _console.print(" [green]fithia2 leaderboard --sort sqs[/]") + _console.print(" [green]fithia2 leaderboard --sort promotion[/]") + _console.print(" [green]fithia2 record --experiment pead_v2 --hypothesis '...' --verdict better[/]") + _console.print(" [green]fithia2 show IMP-0007[/]") + _console.print() + + def main() -> None: - parser = argparse.ArgumentParser(description="Strategy Improvement Tracker") + if len(sys.argv) == 1: + _print_help() + sys.exit(0) + + parser = argparse.ArgumentParser(description="Strategy Improvement Tracker", add_help=True) sub = parser.add_subparsers(dest="command", required=True) - # record - p_record = sub.add_parser("record", help="Record an experiment to the journal") - p_record.add_argument("--journal-dir", required=True, help="Path to journal/ directory") - p_record.add_argument("--runs-dir", required=True, help="Path to runs output directory") - p_record.add_argument("--experiment", required=True, help="Experiment name (matches manifest)") - p_record.add_argument("--hypothesis", help="What you expected this change to do") - p_record.add_argument("--baseline", help="Baseline experiment name for comparison") - p_record.add_argument("--verdict", choices=["better", "worse", "neutral", "unknown"], default="unknown") - p_record.add_argument("--reasoning", help="Why this verdict") - p_record.add_argument("--next", help="Next experiment direction") - p_record.add_argument("--force", action="store_true", help="Allow duplicate experiment names") - - # leaderboard - p_lb = sub.add_parser("leaderboard", help="Show/regenerate the leaderboard") - p_lb.add_argument("--journal-dir", required=True, help="Path to journal/ directory") - - # show - p_show = sub.add_parser("show", help="Show details of a journal entry") - p_show.add_argument("--journal-dir", required=True, help="Path to journal/ directory") - p_show.add_argument("entry_id", help="Entry ID (IMP-0001) or experiment name") - - # check-duplicate - p_dup = sub.add_parser("check-duplicate", help="Check if experiment already recorded") - p_dup.add_argument("--journal-dir", required=True, help="Path to journal/ directory") - p_dup.add_argument("--experiment", required=True, help="Experiment name to check") + _jdir_kwargs = {"default": _DEFAULT_JOURNAL_DIR, "help": f"Path to journal/ directory (default: {_DEFAULT_JOURNAL_DIR})"} + + # record (alias: rec) + for name in ("record", "rec"): + p = sub.add_parser(name, help="Record an experiment to the journal") + p.add_argument("--journal-dir", **_jdir_kwargs) + p.add_argument("--runs-dir", default="runs", help="Path to runs output directory (default: runs)") + p.add_argument("--experiment", "-e", required=True, help="Experiment name (matches manifest)") + p.add_argument("--hypothesis", "-H", help="What you expected this change to do") + p.add_argument("--baseline", "-b", help="Baseline experiment name for comparison") + p.add_argument("--verdict", "-v", choices=["better", "worse", "neutral", "unknown"], default="unknown") + p.add_argument("--reasoning", "-r", help="Why this verdict") + p.add_argument("--next", "-n", help="Next experiment direction") + p.add_argument("--force", "-f", action="store_true", help="Allow duplicate experiment names") + + # leaderboard (alias: lb) + for name in ("leaderboard", "lb"): + p = sub.add_parser(name, help="Show/regenerate the leaderboard") + p.add_argument("--journal-dir", **_jdir_kwargs) + p.add_argument("--top", "-n", type=int, default=10, help="Show top N entries (default: 10)") + p.add_argument("--sort", choices=["sqs", "promotion", "unified"], default="sqs", help="Sort leaderboard by public SQS (default) or internal promotion score") + + # show (alias: s) + for name in ("show", "s"): + p = sub.add_parser(name, help="Show details of a journal entry") + p.add_argument("--journal-dir", **_jdir_kwargs) + p.add_argument("entry_id", help="Entry ID (IMP-0001) or experiment name") + + # check-duplicate (alias: dup) + for name in ("check-duplicate", "dup"): + p = sub.add_parser(name, help="Check if experiment already recorded") + p.add_argument("--journal-dir", **_jdir_kwargs) + p.add_argument("--experiment", "-e", required=True, help="Experiment name to check") args = parser.parse_args() dispatch = { - "record": cmd_record, - "leaderboard": cmd_leaderboard, - "show": cmd_show, - "check-duplicate": cmd_check_duplicate, + "record": cmd_record, "rec": cmd_record, + "leaderboard": cmd_leaderboard, "lb": cmd_leaderboard, + "show": cmd_show, "s": cmd_show, + "check-duplicate": cmd_check_duplicate, "dup": cmd_check_duplicate, } dispatch[args.command](args) diff --git a/journal/LEADERBOARD.md b/journal/LEADERBOARD.md index ae3937e..f98d375 100644 --- a/journal/LEADERBOARD.md +++ b/journal/LEADERBOARD.md @@ -1,84 +1,84 @@ # Strategy Improvement Leaderboard -_Updated: 2026-03-17T07:57:00.128792+00:00_ +_Updated: 2026-03-17T09:17:27.726326+00:00_ -| # | Experiment | SQS | PF | Ret% | WR | Sharpe | DD% | Trades | Date | -|---|-----------|-----|-----|------|-----|--------|-----|--------|------| -| 1 | pead_midcap_step35_balanced_sleeves_nofrac_aclong12 | 90.4 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 2026-03-17 | -| 2 | pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 90.3 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 2026-03-17 | -| 3 | pead_midcap_step44_short_core_macro50 | 87.9 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 2026-03-17 | -| 4 | pead_midcap_step47_short_core_macro_block_sdlong25 | 87.0 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 2026-03-17 | -| 5 | pead_midcap_step45_short_core_macro_block | 86.7 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 2026-03-17 | -| 6 | pead_midcap_step51_short_core_macro_block_crashcap | 86.7 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 2026-03-17 | -| 7 | pead_midcap_step46_short_core_macro_block_acshort12 | 86.6 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 2026-03-17 | -| 8 | pead_midcap_step52_short_core_macro_block_crashcap_gap10 | 86.3 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 2026-03-17 | -| 9 | pead_midcap_step48_short_core_macro_block_nolong | 85.7 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 2026-03-17 | -| 10 | pead_midcap_step40_short_core_sdlong12 | 82.1 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 2026-03-17 | -| 11 | pead_midcap_step34_balanced_sleeves_nofrac_aclong25 | 81.3 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 2026-03-17 | -| 12 | pead_midcap_step43_short_core_sdlong25 | 78.9 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 2026-03-17 | -| 13 | pead_midcap_step31_balanced_sleeves_nofrac_acshort12 | 77.9 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 2026-03-17 | -| 14 | pead_midcap_step39_balanced_sleeves_sdlong12 | 77.9 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 2026-03-17 | -| 15 | pead_midcap_step30_balanced_sleeves_nofrac | 75.2 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 2026-03-17 | -| 16 | pead_midcap_step33_balanced_sleeves_nofrac_acshort6 | 73.9 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 2026-03-17 | -| 17 | pead_midcap_step37_balanced_sleeves_aclong_vol3 | 73.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 2026-03-17 | -| 18 | pead_midcap_step41_short_core_sdlong12_acshort50 | 72.6 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 2026-03-17 | -| 19 | pead_midcap_step42_short_core_only | 66.3 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 2026-03-17 | -| 20 | pead_midcap_step14_score65 | 64.2 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | 2026-03-17 | -| 21 | pead_midcap_step18_nofrac | 63.2 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | 2026-03-17 | -| 22 | pead_midcap_step19_hold5 | 62.9 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | 2026-03-17 | -| 23 | pead_midcap_step20_best3 | 62.0 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | 2026-03-17 | -| 24 | pead_midcap_step17_target2 | 59.4 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | 2026-03-17 | -| 25 | pead_midcap_step27_sdlong_close7_budget25 | 57.9 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 2026-03-17 | -| 26 | pead_midcap_step13_best | 57.7 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | 2026-03-16 | -| 27 | pead_midcap_step23_sdlong_close7 | 55.1 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 2026-03-17 | -| 28 | pead_midcap_step38_balanced_sleeves_aclong_vol4 | 54.2 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 2026-03-17 | -| 29 | pead_midcap_step16_react7_score65 | 53.2 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | 2026-03-17 | -| 30 | pead_midcap_step5_maxcand3 | 52.5 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | 2026-03-16 | -| 31 | pead_midcap_portfolio_v2 | 52.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 2026-03-17 | -| 32 | pead_midcap_step15_react7 | 51.7 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | 2026-03-17 | -| 33 | pead_midcap_step11_score60 | 50.2 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | 2026-03-16 | -| 34 | pead_midcap_step12_vol2x | 50.2 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | 2026-03-16 | -| 35 | pead_midcap_step3_10pct | 49.8 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | 2026-03-16 | -| 36 | pead_midcap_step10_short | 49.2 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | 2026-03-16 | -| 37 | pead_midcap_step53_short_core_macro_block_crashcap_gap14 | 43.1 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2026-03-17 | -| 38 | pead_midcap_step50_same_day_short_long_macro_block | 41.9 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 2026-03-17 | -| 39 | pead_midcap_step2_notrail | 41.4 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | 2026-03-16 | -| 40 | pead_midcap_step1_fixedr | 39.7 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | 2026-03-16 | -| 41 | pead_midcap_combo_10pct_maxcand3 | 39.2 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | 2026-03-16 | -| 42 | pead_midcap_step49_same_day_short_macro_block | 38.9 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 2026-03-17 | -| 43 | pead_midcap_step6_drift | 37.0 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | 2026-03-16 | -| 44 | pead_midcap_step7_fixedr | 35.4 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | 2026-03-16 | -| 45 | pead_midcap_step4_longonly | 32.6 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | 2026-03-16 | -| 46 | pead_midcap_step8_nft | 31.3 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | 2026-03-16 | -| 47 | pead_midcap_step9_stop2 | 31.1 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | 2026-03-16 | +| # | Experiment | SQS | [T]PF | [T]Ret% | [T]WR | [T]Sharpe | [T]DD% | [T]N | [T]Gross% | [T]Net% | [T]DIM% | [V]PF | [V]Ret% | [V]WR | [V]Sharpe | [V]DD% | [V]N | [V]Gross% | [V]Net% | [V]DIM% | Date | +|---|-----------|-----|-------|---------|-------|-----------|--------|------|-----------|---------|---------|-------|---------|-------|-----------|--------|------|-----------|---------|---------|------| +| 1 | pead_midcap_step52_short_core_macro_block_crashcap_gap10 | 49.8 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 2.9 | -2.2 | 53.2 | 5.66 | +1.8 | 72% | 4.8 | 0.2 | 25 | 3.6 | -2.6 | 56.1 | 2026-03-17 | +| 2 | pead_midcap_step48_short_core_macro_block_nolong | 46.9 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 2.6 | +2.6 | 52.2 | 5.73 | +1.3 | 70% | 3.4 | 0.3 | 20 | 3.3 | +3.3 | 51.8 | 2026-03-17 | +| 3 | pead_midcap_step46_short_core_macro_block_acshort12 | 45.8 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 2.1 | +2.1 | 40.4 | 2.44 | +1.3 | 69% | 3.2 | 0.4 | 26 | 4.0 | +4.0 | 57.9 | 2026-03-17 | +| 4 | pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 44.5 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 6.0 | +6.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 5 | pead_midcap_step51_short_core_macro_block_crashcap | 44.4 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 2.3 | -0.8 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.1 | -1.9 | 57.9 | 2026-03-17 | +| 6 | pead_midcap_step45_short_core_macro_block | 44.3 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 2.4 | +2.4 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.2 | +4.2 | 57.9 | 2026-03-17 | +| 7 | pead_midcap_step53_short_core_macro_block_crashcap_gap14 | 39.5 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2.3 | +2.3 | 53.2 | 8.21 | +2.0 | 76% | 5.3 | 0.2 | 25 | 3.6 | +3.6 | 56.1 | 2026-03-17 | +| 8 | pead_midcap_step50_same_day_short_long_macro_block | 36.2 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 1.6 | +1.6 | 38.3 | 2.21 | +1.0 | 65% | 2.7 | 0.3 | 20 | 3.4 | +3.4 | 43.9 | 2026-03-17 | +| 9 | pead_midcap_step30_balanced_sleeves_nofrac | 35.4 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 8.0 | +8.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 10 | pead_midcap_step47_short_core_macro_block_sdlong25 | 35.2 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 2.9 | +2.9 | 48.9 | 1.77 | +1.0 | 65% | 2.2 | 0.4 | 31 | 4.6 | +4.6 | 57.9 | 2026-03-17 | +| 11 | pead_midcap_step44_short_core_macro50 | 35.0 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 4.5 | -1.6 | 72.3 | 1.78 | +1.1 | 55% | 2.5 | 0.4 | 40 | 4.8 | -2.0 | 73.7 | 2026-03-17 | +| 12 | pead_midcap_step33_balanced_sleeves_nofrac_acshort6 | 35.0 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 6.2 | +6.2 | 70.2 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 13 | pead_midcap_step43_short_core_sdlong25 | 31.7 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 7.1 | +7.1 | 72.3 | 1.46 | +1.0 | 56% | 1.9 | 0.6 | 45 | 6.2 | +6.2 | 73.7 | 2026-03-17 | +| 14 | pead_midcap_step41_short_core_sdlong12_acshort50 | 30.7 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 6.8 | +6.8 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | +| 15 | pead_midcap_step37_balanced_sleeves_aclong_vol3 | 30.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 7.5 | +7.5 | 76.6 | 1.38 | +1.1 | 51% | 1.8 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | +| 16 | pead_midcap_step42_short_core_only | 30.5 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 5.5 | +5.5 | 71.7 | 2.55 | +1.2 | 58% | 2.8 | 0.5 | 31 | 4.5 | +4.5 | 71.4 | 2026-03-17 | +| 17 | pead_midcap_step14_score65 | 29.9 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | - | - | - | 1.40 | +1.0 | 56% | 1.6 | 0.7 | 66 | - | - | - | 2026-03-17 | +| 18 | pead_midcap_step18_nofrac | 29.4 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | - | - | - | 1.46 | +1.2 | 47% | 1.9 | 0.6 | 55 | - | - | - | 2026-03-17 | +| 19 | pead_midcap_step31_balanced_sleeves_nofrac_acshort12 | 29.3 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 7.6 | +7.6 | 76.6 | 1.38 | +1.1 | 52% | 1.8 | 0.9 | 58 | 8.0 | +8.0 | 77.2 | 2026-03-17 | +| 20 | pead_midcap_step19_hold5 | 29.2 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | - | - | - | 1.55 | +1.4 | 57% | 2.2 | 0.7 | 68 | - | - | - | 2026-03-17 | +| 21 | pead_midcap_step49_same_day_short_macro_block | 29.1 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 1.0 | +1.0 | 26.1 | 25.09 | +1.2 | 75% | 3.2 | 0.3 | 12 | 2.6 | +2.6 | 32.1 | 2026-03-17 | +| 22 | pead_midcap_step20_best3 | 28.7 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | - | - | - | 1.61 | +1.6 | 48% | 2.5 | 0.6 | 56 | - | - | - | 2026-03-17 | +| 23 | pead_midcap_step40_short_core_sdlong12 | 28.6 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 6.4 | +6.4 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | +| 24 | pead_midcap_step17_target2 | 27.3 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | - | - | - | 1.38 | +1.0 | 51% | 1.5 | 0.7 | 59 | - | - | - | 2026-03-17 | +| 25 | pead_midcap_step39_balanced_sleeves_sdlong12 | 27.0 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 7.4 | +7.4 | 76.6 | 1.38 | +1.0 | 51% | 1.7 | 0.9 | 53 | 7.5 | +7.5 | 77.2 | 2026-03-17 | +| 26 | pead_midcap_step27_sdlong_close7_budget25 | 26.4 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 8.5 | +8.5 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 27 | pead_midcap_step13_best | 26.3 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | +| 28 | pead_midcap_step23_sdlong_close7 | 24.9 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 8.6 | +8.6 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 29 | pead_midcap_step34_balanced_sleeves_nofrac_aclong25 | 23.9 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 7.4 | +7.4 | 76.6 | 1.30 | +0.9 | 51% | 1.4 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | +| 30 | pead_midcap_step16_react7_score65 | 23.8 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | - | - | - | 2.01 | +2.8 | 63% | 3.7 | 0.8 | 83 | - | - | - | 2026-03-17 | +| 31 | pead_midcap_step5_maxcand3 | 23.4 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | - | - | - | 2.08 | +3.3 | 65% | 4.0 | 1.1 | 89 | - | - | - | 2026-03-16 | +| 32 | pead_midcap_step38_balanced_sleeves_aclong_vol4 | 23.3 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 7.4 | +7.4 | 76.6 | 1.29 | +0.8 | 53% | 1.4 | 0.9 | 53 | 7.6 | +7.6 | 77.2 | 2026-03-17 | +| 33 | pead_midcap_step15_react7 | 23.0 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | - | - | - | 1.89 | +2.6 | 62% | 3.5 | 0.9 | 84 | - | - | - | 2026-03-17 | +| 34 | pead_midcap_portfolio_v2 | 23.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 7.9 | +7.9 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 35 | pead_midcap_step11_score60 | 22.1 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 36 | pead_midcap_step12_vol2x | 22.1 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 37 | pead_midcap_step3_10pct | 21.9 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | - | - | - | 1.66 | +2.0 | 60% | 2.9 | 0.7 | 78 | - | - | - | 2026-03-16 | +| 38 | pead_midcap_step10_short | 21.5 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | +| 39 | pead_midcap_step35_balanced_sleeves_nofrac_aclong12 | 21.2 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 6.2 | +1.1 | 76.6 | 1.24 | +0.7 | 51% | 1.2 | 0.9 | 53 | 7.5 | +1.6 | 77.2 | 2026-03-17 | +| 40 | pead_midcap_step2_notrail | 17.2 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | - | - | - | 1.07 | +0.4 | 73% | 0.4 | 1.7 | 62 | - | - | - | 2026-03-16 | +| 41 | pead_midcap_step1_fixedr | 16.2 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | - | - | - | 1.77 | +2.8 | 49% | 3.4 | 1.6 | 69 | - | - | - | 2026-03-16 | +| 42 | pead_midcap_combo_10pct_maxcand3 | 15.9 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 43 | pead_midcap_step6_drift | 14.7 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | - | - | - | 1.70 | +2.7 | 51% | 3.7 | 1.0 | 75 | - | - | - | 2026-03-16 | +| 44 | pead_midcap_step7_fixedr | 13.8 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | - | - | - | 2.00 | +2.4 | 53% | 3.1 | 0.7 | 53 | - | - | - | 2026-03-16 | +| 45 | pead_midcap_step4_longonly | 12.2 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | - | - | - | 1.43 | +1.5 | 61% | 1.7 | 1.6 | 76 | - | - | - | 2026-03-16 | +| 46 | pead_midcap_step8_nft | 11.5 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | - | - | - | 1.55 | +1.8 | 46% | 2.4 | 1.0 | 57 | - | - | - | 2026-03-16 | +| 47 | pead_midcap_step9_stop2 | 11.4 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | - | - | - | 1.81 | +3.2 | 53% | 2.8 | 1.2 | 53 | - | - | - | 2026-03-16 | ## Recent Entries ### IMP-0047 (2026-03-17) — pead_midcap_step53_short_core_macro_block_crashcap_gap14 Hypothesis: A stricter same-day long gap filter might further concentrate the overlay into only the strongest continuation setups. -Verdict: **WORSE** (SQS 43.1) +Verdict: **WORSE** (SQS 39.5) Reasoning: The stricter gap filter over-concentrated the overlay, dropped total trade count below a healthy level, and cratered test SQS. Next: Use moderate overlay filters only; the strict version is too sparse. ### IMP-0046 (2026-03-17) — pead_midcap_step52_short_core_macro_block_crashcap_gap10 Hypothesis: The same-day long overlay may work better when restricted to larger reaction-day gap moves. -Verdict: **NEUTRAL** (SQS 86.3) +Verdict: **NEUTRAL** (SQS 49.8) Reasoning: A 10% gap filter made train and valid much stronger but gave back some test performance, so this is a balanced alternative rather than a clear new leader. Next: If optimizing for robustness across splits, keep exploring overlay quality gates around this variant. ### IMP-0045 (2026-03-17) — pead_midcap_step51_short_core_macro_block_crashcap Hypothesis: Extreme one-day crash continuations are too stretched for the same-day short sleeve and should be excluded. -Verdict: **BETTER** (SQS 86.7) +Verdict: **BETTER** (SQS 44.4) Reasoning: Capping same-day shorts at -45% reaction preserved train and valid while modestly improving test return, PF, drawdown, and Sharpe versus step45. Next: Combine the crash cap with a quality filter on the same-day long overlay. ### IMP-0044 (2026-03-17) — pead_midcap_step50_same_day_short_long_macro_block Hypothesis: The same-day long overlay may matter, but the after-close short sleeve may be removable. -Verdict: **WORSE** (SQS 41.9) +Verdict: **WORSE** (SQS 36.2) Reasoning: Dropping the after-close short sleeve reduced both valid and test performance, so step45 still benefits from carrying all three active sleeves. Next: Refine sleeve quality rather than deleting sleeves wholesale. ### IMP-0043 (2026-03-17) — pead_midcap_step49_same_day_short_macro_block Hypothesis: The pure same-day short engine might dominate the portfolio and make other sleeves unnecessary. -Verdict: **WORSE** (SQS 38.9) +Verdict: **WORSE** (SQS 29.1) Reasoning: The single-sleeve version collapsed SQS because trade count and robustness fell too far, even though the kept trades were profitable. Next: Keep the supporting sleeves and test smaller structural adjustments instead. diff --git a/journal/experiment_registry.json b/journal/experiment_registry.json index d6ce77d..422a617 100644 --- a/journal/experiment_registry.json +++ b/journal/experiment_registry.json @@ -1,569 +1,1274 @@ { "entries": [ - { - "entry_id": "IMP-0029", - "experiment_name": "pead_midcap_step35_balanced_sleeves_nofrac_aclong12", - "sqs_score": 90.4, - "profit_factor": 2.009368125746884, - "total_return_pct": 2.181331690538893, - "win_rate": 0.6071428571428571, - "sharpe_ratio": 4.216651506745797, - "max_drawdown_pct": 0.4424438123900949, - "trade_count": 56, - "timestamp": "2026-03-17T07:05:47.363036+00:00" - }, - { - "entry_id": "IMP-0030", - "experiment_name": "pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12", - "sqs_score": 90.3, - "profit_factor": 2.061648751658133, - "total_return_pct": 2.1478646359501146, - "win_rate": 0.6111111111111112, - "sharpe_ratio": 4.4957307480895325, - "max_drawdown_pct": 0.3579823467213594, - "trade_count": 54, - "timestamp": "2026-03-17T07:05:52.980782+00:00" - }, - { - "entry_id": "IMP-0038", - "experiment_name": "pead_midcap_step44_short_core_macro50", - "sqs_score": 87.9, - "profit_factor": 2.0052763103391356, - "total_return_pct": 1.2986802302195721, - "win_rate": 0.5689655172413793, - "sharpe_ratio": 2.6217356101467053, - "max_drawdown_pct": 0.7045335236824514, - "trade_count": 58, - "timestamp": "2026-03-17T07:54:25.486815+00:00" - }, - { - "entry_id": "IMP-0041", - "experiment_name": "pead_midcap_step47_short_core_macro_block_sdlong25", - "sqs_score": 87.0, - "profit_factor": 3.429404855633068, - "total_return_pct": 1.2833562667310616, - "win_rate": 0.68, - "sharpe_ratio": 3.32348066596022, - "max_drawdown_pct": 0.2608177180219861, - "trade_count": 25, - "timestamp": "2026-03-17T07:54:26.612610+00:00" - }, - { - "entry_id": "IMP-0039", - "experiment_name": "pead_midcap_step45_short_core_macro_block", - "sqs_score": 86.7, - "profit_factor": 3.7827916861128097, - "total_return_pct": 1.2021184808416436, - "win_rate": 0.6956521739130435, - "sharpe_ratio": 3.3795660622862123, - "max_drawdown_pct": 0.2608177180219861, - "trade_count": 23, - "timestamp": "2026-03-17T07:54:25.866482+00:00" - }, - { - "entry_id": "IMP-0045", - "experiment_name": "pead_midcap_step51_short_core_macro_block_crashcap", - "sqs_score": 86.7, - "profit_factor": 4.190184861108528, - "total_return_pct": 1.2441182731003355, - "win_rate": 0.7272727272727273, - "sharpe_ratio": 3.5956143566120704, - "max_drawdown_pct": 0.22380328257556925, - "trade_count": 22, - "timestamp": "2026-03-17T07:54:28.043618+00:00" - }, - { - "entry_id": "IMP-0040", - "experiment_name": "pead_midcap_step46_short_core_macro_block_acshort12", - "sqs_score": 86.6, - "profit_factor": 4.522043594902001, - "total_return_pct": 1.2518263219734362, - "win_rate": 0.7142857142857143, - "sharpe_ratio": 3.6389410132883055, - "max_drawdown_pct": 0.2783619920052574, - "trade_count": 21, - "timestamp": "2026-03-17T07:54:26.245073+00:00" - }, { "entry_id": "IMP-0046", "experiment_name": "pead_midcap_step52_short_core_macro_block_crashcap_gap10", - "sqs_score": 86.3, + "sqs_score": 49.8, + "sqs_v2_score": 87.2, + "promotion_score": 89.0, + "unified_score": 49.8, "profit_factor": 3.351800408128271, "total_return_pct": 1.0111883417758072, "win_rate": 0.7727272727272727, "sharpe_ratio": 3.031867507475822, "max_drawdown_pct": 0.24257912061402945, "trade_count": 22, + "avg_gross_exposure_pct": 2.858350680305373, + "avg_net_exposure_pct": -2.222595187645864, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 5.660464878695176, + "valid_total_return_pct": 1.818984312375629, + "valid_win_rate": 0.72, + "valid_sharpe_ratio": 4.755652871139783, + "valid_max_drawdown_pct": 0.21695852738272095, + "valid_trade_count": 25, + "valid_avg_gross_exposure_pct": 3.5838805580978437, + "valid_avg_net_exposure_pct": -2.593156236925373, + "valid_days_in_market_pct": 56.14035087719298, "timestamp": "2026-03-17T07:54:28.401055+00:00" }, { "entry_id": "IMP-0042", "experiment_name": "pead_midcap_step48_short_core_macro_block_nolong", - "sqs_score": 85.7, + "sqs_score": 46.9, + "sqs_v2_score": 86.3, + "promotion_score": 86.4, + "unified_score": 46.9, "profit_factor": 3.402559985815088, "total_return_pct": 0.8451446297187069, "win_rate": 0.8, "sharpe_ratio": 2.4413021645869604, "max_drawdown_pct": 0.4239007555767844, "trade_count": 20, + "avg_gross_exposure_pct": 2.6417378272162013, + "avg_net_exposure_pct": 2.6417378272162013, + "days_in_market_pct": 52.17391304347826, + "valid_profit_factor": 5.726207009868067, + "valid_total_return_pct": 1.2584927752282966, + "valid_win_rate": 0.7, + "valid_sharpe_ratio": 3.3529664036078217, + "valid_max_drawdown_pct": 0.29982153086364316, + "valid_trade_count": 20, + "valid_avg_gross_exposure_pct": 3.337962191482299, + "valid_avg_net_exposure_pct": 3.337962191482299, + "valid_days_in_market_pct": 51.78571428571429, "timestamp": "2026-03-17T07:54:26.967747+00:00" }, { - "entry_id": "IMP-0034", - "experiment_name": "pead_midcap_step40_short_core_sdlong12", - "sqs_score": 82.1, - "profit_factor": 1.7695290624299977, - "total_return_pct": 1.52020169384827, - "win_rate": 0.5818181818181818, - "sharpe_ratio": 2.2087400904317267, - "max_drawdown_pct": 1.128135882565315, - "trade_count": 55, - "timestamp": "2026-03-17T07:20:29.978842+00:00" + "entry_id": "IMP-0040", + "experiment_name": "pead_midcap_step46_short_core_macro_block_acshort12", + "sqs_score": 45.8, + "sqs_v2_score": 89.5, + "promotion_score": 87.0, + "unified_score": 45.8, + "profit_factor": 4.522043594902001, + "total_return_pct": 1.2518263219734362, + "win_rate": 0.7142857142857143, + "sharpe_ratio": 3.6389410132883055, + "max_drawdown_pct": 0.2783619920052574, + "trade_count": 21, + "avg_gross_exposure_pct": 2.118517848385521, + "avg_net_exposure_pct": 2.118517848385521, + "days_in_market_pct": 40.42553191489361, + "valid_profit_factor": 2.4445635979938296, + "valid_total_return_pct": 1.339314216731771, + "valid_win_rate": 0.6923076923076923, + "valid_sharpe_ratio": 3.1583041871985076, + "valid_max_drawdown_pct": 0.37458871743417604, + "valid_trade_count": 26, + "valid_avg_gross_exposure_pct": 4.009149815441572, + "valid_avg_net_exposure_pct": 4.009149815441572, + "valid_days_in_market_pct": 57.89473684210527, + "timestamp": "2026-03-17T07:54:26.245073+00:00" }, { - "entry_id": "IMP-0028", - "experiment_name": "pead_midcap_step34_balanced_sleeves_nofrac_aclong25", - "sqs_score": 81.3, - "profit_factor": 1.6229787581193362, - "total_return_pct": 1.809973740790665, - "win_rate": 0.5645161290322581, - "sharpe_ratio": 3.784062042365747, - "max_drawdown_pct": 0.5514994557958487, - "trade_count": 62, - "timestamp": "2026-03-17T07:05:41.969503+00:00" + "entry_id": "IMP-0030", + "experiment_name": "pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12", + "sqs_score": 44.5, + "sqs_v2_score": 90.2, + "promotion_score": null, + "unified_score": null, + "profit_factor": 2.061648751658133, + "total_return_pct": 2.1478646359501146, + "win_rate": 0.6111111111111112, + "sharpe_ratio": 4.4957307480895325, + "max_drawdown_pct": 0.3579823467213594, + "trade_count": 54, + "avg_gross_exposure_pct": 6.016398268720172, + "avg_net_exposure_pct": 6.016398268720172, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, + "timestamp": "2026-03-17T07:05:52.980782+00:00" }, { - "entry_id": "IMP-0037", - "experiment_name": "pead_midcap_step43_short_core_sdlong25", - "sqs_score": 78.9, - "profit_factor": 1.6628306811204845, - "total_return_pct": 1.4636686220428092, - "win_rate": 0.5689655172413793, - "sharpe_ratio": 2.0112049109753753, - "max_drawdown_pct": 1.2630043081731899, - "trade_count": 58, - "timestamp": "2026-03-17T07:20:29.978670+00:00" + "entry_id": "IMP-0045", + "experiment_name": "pead_midcap_step51_short_core_macro_block_crashcap", + "sqs_score": 44.4, + "sqs_v2_score": 89.6, + "promotion_score": 86.8, + "unified_score": 44.4, + "profit_factor": 4.190184861108528, + "total_return_pct": 1.2441182731003355, + "win_rate": 0.7272727272727273, + "sharpe_ratio": 3.5956143566120704, + "max_drawdown_pct": 0.22380328257556925, + "trade_count": 22, + "avg_gross_exposure_pct": 2.2928950819181733, + "avg_net_exposure_pct": -0.8189811228558067, + "days_in_market_pct": 42.5531914893617, + "valid_profit_factor": 2.3084684930008983, + "valid_total_return_pct": 1.30245295115927, + "valid_win_rate": 0.6785714285714286, + "valid_sharpe_ratio": 3.0487382658615467, + "valid_max_drawdown_pct": 0.37472483014430374, + "valid_trade_count": 28, + "valid_avg_gross_exposure_pct": 4.09489843643577, + "valid_avg_net_exposure_pct": -1.9273432556417505, + "valid_days_in_market_pct": 57.89473684210527, + "timestamp": "2026-03-17T07:54:28.043618+00:00" }, { - "entry_id": "IMP-0026", - "experiment_name": "pead_midcap_step31_balanced_sleeves_nofrac_acshort12", - "sqs_score": 77.9, - "profit_factor": 1.4929664814770336, - "total_return_pct": 1.5563811867822805, - "win_rate": 0.546875, - "sharpe_ratio": 3.235108878683093, - "max_drawdown_pct": 0.6813257982524192, - "trade_count": 64, - "timestamp": "2026-03-17T06:59:09.255218+00:00" + "entry_id": "IMP-0039", + "experiment_name": "pead_midcap_step45_short_core_macro_block", + "sqs_score": 44.3, + "sqs_v2_score": 89.6, + "promotion_score": 86.7, + "unified_score": 44.3, + "profit_factor": 3.7827916861128097, + "total_return_pct": 1.2021184808416436, + "win_rate": 0.6956521739130435, + "sharpe_ratio": 3.3795660622862123, + "max_drawdown_pct": 0.2608177180219861, + "trade_count": 23, + "avg_gross_exposure_pct": 2.3527342845925694, + "avg_net_exposure_pct": 2.3527342845925694, + "days_in_market_pct": 42.5531914893617, + "valid_profit_factor": 2.3084684930008983, + "valid_total_return_pct": 1.30245295115927, + "valid_win_rate": 0.6785714285714286, + "valid_sharpe_ratio": 3.0487382658615467, + "valid_max_drawdown_pct": 0.37472483014430374, + "valid_trade_count": 28, + "valid_avg_gross_exposure_pct": 4.177369662687912, + "valid_avg_net_exposure_pct": 4.177369662687912, + "valid_days_in_market_pct": 57.89473684210527, + "timestamp": "2026-03-17T07:54:25.866482+00:00" }, { - "entry_id": "IMP-0033", - "experiment_name": "pead_midcap_step39_balanced_sleeves_sdlong12", - "sqs_score": 77.9, - "profit_factor": 1.5016295541562696, - "total_return_pct": 1.5150882212722936, - "win_rate": 0.5483870967741935, - "sharpe_ratio": 3.3681033592846275, - "max_drawdown_pct": 0.5454456407735768, - "trade_count": 62, - "timestamp": "2026-03-17T07:11:34.247400+00:00" + "entry_id": "IMP-0047", + "experiment_name": "pead_midcap_step53_short_core_macro_block_crashcap_gap14", + "sqs_score": 39.5, + "sqs_v2_score": 44.5, + "promotion_score": 69.9, + "unified_score": 39.5, + "profit_factor": 4.599773676047558, + "total_return_pct": 1.1191525844285641, + "win_rate": 0.8421052631578947, + "sharpe_ratio": 3.463700507402551, + "max_drawdown_pct": 0.21947031826165894, + "trade_count": 19, + "avg_gross_exposure_pct": 2.291260874466458, + "avg_net_exposure_pct": 2.291260874466458, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 8.207760543782888, + "valid_total_return_pct": 2.0180349316014032, + "valid_win_rate": 0.76, + "valid_sharpe_ratio": 5.288977955005757, + "valid_max_drawdown_pct": 0.2179161262122437, + "valid_trade_count": 25, + "valid_avg_gross_exposure_pct": 3.639413476436239, + "valid_avg_net_exposure_pct": 3.639413476436239, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T07:54:28.762757+00:00" + }, + { + "entry_id": "IMP-0044", + "experiment_name": "pead_midcap_step50_same_day_short_long_macro_block", + "sqs_score": 36.2, + "sqs_v2_score": 43.3, + "promotion_score": 65.9, + "unified_score": 36.2, + "profit_factor": 3.2060142950020296, + "total_return_pct": 0.805535692316771, + "win_rate": 0.6, + "sharpe_ratio": 2.6871013818111624, + "max_drawdown_pct": 0.42315703199236054, + "trade_count": 15, + "avg_gross_exposure_pct": 1.635855413361969, + "avg_net_exposure_pct": 1.635855413361969, + "days_in_market_pct": 38.297872340425535, + "valid_profit_factor": 2.2051399980357798, + "valid_total_return_pct": 0.9530265960178512, + "valid_win_rate": 0.65, + "valid_sharpe_ratio": 2.687864694846263, + "valid_max_drawdown_pct": 0.3444323377542605, + "valid_trade_count": 20, + "valid_avg_gross_exposure_pct": 3.390326846309195, + "valid_avg_net_exposure_pct": 3.390326846309195, + "valid_days_in_market_pct": 43.859649122807014, + "timestamp": "2026-03-17T07:54:27.691772+00:00" }, { "entry_id": "IMP-0025", "experiment_name": "pead_midcap_step30_balanced_sleeves_nofrac", - "sqs_score": 75.2, + "sqs_score": 35.4, + "sqs_v2_score": 74.0, + "promotion_score": null, + "unified_score": null, "profit_factor": 1.4091528424597795, "total_return_pct": 1.3757295850866649, "win_rate": 0.5373134328358209, "sharpe_ratio": 2.8484960542107354, "max_drawdown_pct": 0.8128025480297582, "trade_count": 67, + "avg_gross_exposure_pct": 7.952952009182568, + "avg_net_exposure_pct": 7.952952009182568, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T06:59:08.875770+00:00" }, + { + "entry_id": "IMP-0041", + "experiment_name": "pead_midcap_step47_short_core_macro_block_sdlong25", + "sqs_score": 35.2, + "sqs_v2_score": 88.9, + "promotion_score": 81.4, + "unified_score": 35.2, + "profit_factor": 3.429404855633068, + "total_return_pct": 1.2833562667310616, + "win_rate": 0.68, + "sharpe_ratio": 3.32348066596022, + "max_drawdown_pct": 0.2608177180219861, + "trade_count": 25, + "avg_gross_exposure_pct": 2.9214777467972883, + "avg_net_exposure_pct": 2.9214777467972883, + "days_in_market_pct": 48.93617021276596, + "valid_profit_factor": 1.7661839600751525, + "valid_total_return_pct": 1.03177071332802, + "valid_win_rate": 0.6451612903225806, + "valid_sharpe_ratio": 2.218739891960781, + "valid_max_drawdown_pct": 0.42599257212123065, + "valid_trade_count": 31, + "valid_avg_gross_exposure_pct": 4.584523096706853, + "valid_avg_net_exposure_pct": 4.584523096706853, + "valid_days_in_market_pct": 57.89473684210527, + "timestamp": "2026-03-17T07:54:26.612610+00:00" + }, + { + "entry_id": "IMP-0038", + "experiment_name": "pead_midcap_step44_short_core_macro50", + "sqs_score": 35.0, + "sqs_v2_score": 86.9, + "promotion_score": 78.7, + "unified_score": 35.0, + "profit_factor": 2.0052763103391356, + "total_return_pct": 1.2986802302195721, + "win_rate": 0.5689655172413793, + "sharpe_ratio": 2.6217356101467053, + "max_drawdown_pct": 0.7045335236824514, + "trade_count": 58, + "avg_gross_exposure_pct": 4.453478837236966, + "avg_net_exposure_pct": -1.5519032472252936, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": 1.7786229517773076, + "valid_total_return_pct": 1.0842382260887244, + "valid_win_rate": 0.55, + "valid_sharpe_ratio": 2.4891782517624543, + "valid_max_drawdown_pct": 0.4065245518278289, + "valid_trade_count": 40, + "valid_avg_gross_exposure_pct": 4.823763728554508, + "valid_avg_net_exposure_pct": -1.952624774981557, + "valid_days_in_market_pct": 73.68421052631578, + "timestamp": "2026-03-17T07:54:25.486815+00:00" + }, { "entry_id": "IMP-0027", "experiment_name": "pead_midcap_step33_balanced_sleeves_nofrac_acshort6", - "sqs_score": 73.9, + "sqs_score": 35.0, + "sqs_v2_score": 73.2, + "promotion_score": null, + "unified_score": null, "profit_factor": 1.4726487821009215, "total_return_pct": 1.2304910166146, "win_rate": 0.5208333333333334, "sharpe_ratio": 2.3026797742321596, "max_drawdown_pct": 0.7081594843000599, "trade_count": 48, + "avg_gross_exposure_pct": 6.231124320485157, + "avg_net_exposure_pct": 6.231124320485157, + "days_in_market_pct": 70.2127659574468, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T06:59:09.622681+00:00" }, { - "entry_id": "IMP-0031", - "experiment_name": "pead_midcap_step37_balanced_sleeves_aclong_vol3", - "sqs_score": 73.6, - "profit_factor": 1.377310183342628, - "total_return_pct": 1.1862161229211343, - "win_rate": 0.5396825396825397, - "sharpe_ratio": 2.4626680484273273, - "max_drawdown_pct": 0.8243191281564817, - "trade_count": 63, - "timestamp": "2026-03-17T07:11:20.119143+00:00" + "entry_id": "IMP-0037", + "experiment_name": "pead_midcap_step43_short_core_sdlong25", + "sqs_score": 31.7, + "sqs_v2_score": 78.2, + "promotion_score": 71.5, + "unified_score": 31.7, + "profit_factor": 1.6628306811204845, + "total_return_pct": 1.4636686220428092, + "win_rate": 0.5689655172413793, + "sharpe_ratio": 2.0112049109753753, + "max_drawdown_pct": 1.2630043081731899, + "trade_count": 58, + "avg_gross_exposure_pct": 7.104116345649285, + "avg_net_exposure_pct": 7.104116345649285, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": 1.4604102163883825, + "valid_total_return_pct": 0.9859009158709378, + "valid_win_rate": 0.5555555555555556, + "valid_sharpe_ratio": 1.9269062309154923, + "valid_max_drawdown_pct": 0.6469365320312458, + "valid_trade_count": 45, + "valid_avg_gross_exposure_pct": 6.177053219765358, + "valid_avg_net_exposure_pct": 6.177053219765358, + "valid_days_in_market_pct": 73.68421052631578, + "timestamp": "2026-03-17T07:20:29.978670+00:00" }, { "entry_id": "IMP-0035", "experiment_name": "pead_midcap_step41_short_core_sdlong12_acshort50", - "sqs_score": 72.6, + "sqs_score": 30.7, + "sqs_v2_score": 72.4, + "promotion_score": 69.7, + "unified_score": 30.7, "profit_factor": 1.5287187084320328, "total_return_pct": 1.231827071365813, "win_rate": 0.559322033898305, "sharpe_ratio": 1.6310365384096348, "max_drawdown_pct": 1.2759660172387226, "trade_count": 59, + "avg_gross_exposure_pct": 6.758906690551582, + "avg_net_exposure_pct": 6.758906690551582, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": 1.483920817012022, + "valid_total_return_pct": 0.8662763872782817, + "valid_win_rate": 0.55, + "valid_sharpe_ratio": 1.8482432920991685, + "valid_max_drawdown_pct": 0.6476993822793542, + "valid_trade_count": 40, + "valid_avg_gross_exposure_pct": 5.63530625780908, + "valid_avg_net_exposure_pct": 5.63530625780908, + "valid_days_in_market_pct": 73.68421052631578, "timestamp": "2026-03-17T07:20:29.978594+00:00" }, + { + "entry_id": "IMP-0031", + "experiment_name": "pead_midcap_step37_balanced_sleeves_aclong_vol3", + "sqs_score": 30.6, + "sqs_v2_score": 72.5, + "promotion_score": 68.3, + "unified_score": 30.6, + "profit_factor": 1.377310183342628, + "total_return_pct": 1.1862161229211343, + "win_rate": 0.5396825396825397, + "sharpe_ratio": 2.4626680484273273, + "max_drawdown_pct": 0.8243191281564817, + "trade_count": 63, + "avg_gross_exposure_pct": 7.516868431732929, + "avg_net_exposure_pct": 7.516868431732929, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.375336580561267, + "valid_total_return_pct": 1.1216330418461293, + "valid_win_rate": 0.5087719298245614, + "valid_sharpe_ratio": 1.7701058914354275, + "valid_max_drawdown_pct": 0.8896384382974281, + "valid_trade_count": 57, + "valid_avg_gross_exposure_pct": 7.940042139043084, + "valid_avg_net_exposure_pct": 7.940042139043084, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T07:11:20.119143+00:00" + }, { "entry_id": "IMP-0036", "experiment_name": "pead_midcap_step42_short_core_only", - "sqs_score": 66.3, + "sqs_score": 30.5, + "sqs_v2_score": 67.1, + "promotion_score": 75.7, + "unified_score": 30.5, "profit_factor": 1.4446704886262167, "total_return_pct": 0.7775531748585345, "win_rate": 0.6304347826086957, "sharpe_ratio": 1.1740322533878838, "max_drawdown_pct": 1.2423515817014616, "trade_count": 46, + "avg_gross_exposure_pct": 5.456222663276362, + "avg_net_exposure_pct": 5.456222663276362, + "days_in_market_pct": 71.73913043478261, + "valid_profit_factor": 2.549217811707667, + "valid_total_return_pct": 1.193718767675222, + "valid_win_rate": 0.5806451612903226, + "valid_sharpe_ratio": 2.752802585098115, + "valid_max_drawdown_pct": 0.5448919617489582, + "valid_trade_count": 31, + "valid_avg_gross_exposure_pct": 4.5025068223925215, + "valid_avg_net_exposure_pct": 4.5025068223925215, + "valid_days_in_market_pct": 71.42857142857143, "timestamp": "2026-03-17T07:20:29.978668+00:00" }, { "entry_id": "IMP-0015", "experiment_name": "pead_midcap_step14_score65", - "sqs_score": 64.2, + "sqs_score": 29.9, + "sqs_v2_score": null, + "promotion_score": 68.3, + "unified_score": null, "profit_factor": 1.2192504658724292, "total_return_pct": 0.7284410148207681, "win_rate": 0.5694444444444444, "sharpe_ratio": 1.3410575857129192, "max_drawdown_pct": 0.8797326864948225, "trade_count": 72, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.3965047202887975, + "valid_total_return_pct": 1.0156494476850202, + "valid_win_rate": 0.5606060606060606, + "valid_sharpe_ratio": 1.5832382440205535, + "valid_max_drawdown_pct": 0.6560660596828557, + "valid_trade_count": 66, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:16:29.511903+00:00" }, { "entry_id": "IMP-0019", "experiment_name": "pead_midcap_step18_nofrac", - "sqs_score": 63.2, + "sqs_score": 29.4, + "sqs_v2_score": null, + "promotion_score": 67.5, + "unified_score": null, "profit_factor": 1.233557927923508, "total_return_pct": 0.7760032481101371, "win_rate": 0.515625, "sharpe_ratio": 1.4353513823455328, "max_drawdown_pct": 0.8900540815144506, "trade_count": 64, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.4603949049610698, + "valid_total_return_pct": 1.180333505141476, + "valid_win_rate": 0.4727272727272727, + "valid_sharpe_ratio": 1.8572252886649268, + "valid_max_drawdown_pct": 0.624348249096465, + "valid_trade_count": 55, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:17:01.474375+00:00" }, + { + "entry_id": "IMP-0026", + "experiment_name": "pead_midcap_step31_balanced_sleeves_nofrac_acshort12", + "sqs_score": 29.3, + "sqs_v2_score": 76.7, + "promotion_score": 69.4, + "unified_score": 29.3, + "profit_factor": 1.4929664814770336, + "total_return_pct": 1.5563811867822805, + "win_rate": 0.546875, + "sharpe_ratio": 3.235108878683093, + "max_drawdown_pct": 0.6813257982524192, + "trade_count": 64, + "avg_gross_exposure_pct": 7.608670757364645, + "avg_net_exposure_pct": 7.608670757364645, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.37572962559925, + "valid_total_return_pct": 1.122807593766629, + "valid_win_rate": 0.5172413793103449, + "valid_sharpe_ratio": 1.8162493466149443, + "valid_max_drawdown_pct": 0.8884863869932943, + "valid_trade_count": 58, + "valid_avg_gross_exposure_pct": 8.000843881269224, + "valid_avg_net_exposure_pct": 8.000843881269224, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T06:59:09.255218+00:00" + }, { "entry_id": "IMP-0020", "experiment_name": "pead_midcap_step19_hold5", - "sqs_score": 62.9, + "sqs_score": 29.2, + "sqs_v2_score": null, + "promotion_score": 70.9, + "unified_score": null, "profit_factor": 1.2003700113675047, "total_return_pct": 0.6659802746770583, "win_rate": 0.5694444444444444, "sharpe_ratio": 1.2282104833801621, "max_drawdown_pct": 0.8797326864948225, "trade_count": 72, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.5486357888297084, + "valid_total_return_pct": 1.4116877933366923, + "valid_win_rate": 0.5735294117647058, + "valid_sharpe_ratio": 2.211147627502439, + "valid_max_drawdown_pct": 0.6559648601902311, + "valid_trade_count": 68, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:17:01.827556+00:00" }, + { + "entry_id": "IMP-0043", + "experiment_name": "pead_midcap_step49_same_day_short_macro_block", + "sqs_score": 29.1, + "sqs_v2_score": 39.9, + "promotion_score": 41.4, + "unified_score": 29.1, + "profit_factor": 2.4025420210691006, + "total_return_pct": 0.40093684044296973, + "win_rate": 0.7, + "sharpe_ratio": 1.5580313035787354, + "max_drawdown_pct": 0.3693400619520335, + "trade_count": 10, + "avg_gross_exposure_pct": 1.0192753920189228, + "avg_net_exposure_pct": 1.0192753920189228, + "days_in_market_pct": 26.08695652173913, + "valid_profit_factor": 25.094599474518425, + "valid_total_return_pct": 1.1648266582814248, + "valid_win_rate": 0.75, + "valid_sharpe_ratio": 3.227317987298049, + "valid_max_drawdown_pct": 0.33436592553948014, + "valid_trade_count": 12, + "valid_avg_gross_exposure_pct": 2.624158067931487, + "valid_avg_net_exposure_pct": 2.624158067931487, + "valid_days_in_market_pct": 32.142857142857146, + "timestamp": "2026-03-17T07:54:27.322093+00:00" + }, { "entry_id": "IMP-0021", "experiment_name": "pead_midcap_step20_best3", - "sqs_score": 62.0, + "sqs_score": 28.7, + "sqs_v2_score": null, + "promotion_score": 69.2, + "unified_score": null, "profit_factor": 1.2154843477872561, "total_return_pct": 0.7161940318976412, "win_rate": 0.515625, "sharpe_ratio": 1.3247288791439433, "max_drawdown_pct": 0.8900540815144506, "trade_count": 64, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.6107426395301327, + "valid_total_return_pct": 1.5733831641505238, + "valid_win_rate": 0.48214285714285715, + "valid_sharpe_ratio": 2.475989818011853, + "valid_max_drawdown_pct": 0.628137470729625, + "valid_trade_count": 56, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:17:02.185037+00:00" }, + { + "entry_id": "IMP-0034", + "experiment_name": "pead_midcap_step40_short_core_sdlong12", + "sqs_score": 28.6, + "sqs_v2_score": 81.4, + "promotion_score": 71.0, + "unified_score": 28.6, + "profit_factor": 1.7695290624299977, + "total_return_pct": 1.52020169384827, + "win_rate": 0.5818181818181818, + "sharpe_ratio": 2.2087400904317267, + "max_drawdown_pct": 1.128135882565315, + "trade_count": 55, + "avg_gross_exposure_pct": 6.385826388232029, + "avg_net_exposure_pct": 6.385826388232029, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": 1.483920817012022, + "valid_total_return_pct": 0.8662763872782817, + "valid_win_rate": 0.55, + "valid_sharpe_ratio": 1.8482432920991685, + "valid_max_drawdown_pct": 0.6476993822793542, + "valid_trade_count": 40, + "valid_avg_gross_exposure_pct": 5.63530625780908, + "valid_avg_net_exposure_pct": 5.63530625780908, + "valid_days_in_market_pct": 73.68421052631578, + "timestamp": "2026-03-17T07:20:29.978842+00:00" + }, { "entry_id": "IMP-0018", "experiment_name": "pead_midcap_step17_target2", - "sqs_score": 59.4, + "sqs_score": 27.3, + "sqs_v2_score": null, + "promotion_score": 64.1, + "unified_score": null, "profit_factor": 1.1771413380479396, "total_return_pct": 0.5891346601967962, "win_rate": 0.5303030303030303, "sharpe_ratio": 1.0752287681773998, "max_drawdown_pct": 0.8393581435568818, "trade_count": 66, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.3782504917267295, + "valid_total_return_pct": 0.9689947279659247, + "valid_win_rate": 0.5084745762711864, + "valid_sharpe_ratio": 1.4547676904551556, + "valid_max_drawdown_pct": 0.7074931393576637, + "valid_trade_count": 59, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:16:52.415056+00:00" }, + { + "entry_id": "IMP-0033", + "experiment_name": "pead_midcap_step39_balanced_sleeves_sdlong12", + "sqs_score": 27.0, + "sqs_v2_score": 76.8, + "promotion_score": 68.5, + "unified_score": 27.0, + "profit_factor": 1.5016295541562696, + "total_return_pct": 1.5150882212722936, + "win_rate": 0.5483870967741935, + "sharpe_ratio": 3.3681033592846275, + "max_drawdown_pct": 0.5454456407735768, + "trade_count": 62, + "avg_gross_exposure_pct": 7.352326510043215, + "avg_net_exposure_pct": 7.352326510043215, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.3838842470402677, + "valid_total_return_pct": 1.0111821812581183, + "valid_win_rate": 0.5094339622641509, + "valid_sharpe_ratio": 1.7379492731088386, + "valid_max_drawdown_pct": 0.8840772162266693, + "valid_trade_count": 53, + "valid_avg_gross_exposure_pct": 7.458236697573659, + "valid_avg_net_exposure_pct": 7.458236697573659, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T07:11:34.247400+00:00" + }, { "entry_id": "IMP-0024", "experiment_name": "pead_midcap_step27_sdlong_close7_budget25", - "sqs_score": 57.9, + "sqs_score": 26.4, + "sqs_v2_score": 57.9, + "promotion_score": null, + "unified_score": null, "profit_factor": 1.1707262231202136, "total_return_pct": 0.5844127796271495, "win_rate": 0.5441176470588235, "sharpe_ratio": 0.9376525375900492, "max_drawdown_pct": 1.306567610162907, "trade_count": 68, + "avg_gross_exposure_pct": 8.488482479985477, + "avg_net_exposure_pct": 8.488482479985477, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T06:59:08.507830+00:00" }, { "entry_id": "IMP-0014", "experiment_name": "pead_midcap_step13_best", - "sqs_score": 57.7, + "sqs_score": 26.3, + "sqs_v2_score": null, + "promotion_score": 70.1, + "unified_score": null, "profit_factor": 1.1182776518630022, "total_return_pct": 0.4293895929202554, "win_rate": 0.5466666666666666, "sharpe_ratio": 0.7812081874914202, "max_drawdown_pct": 0.8801478276052886, "trade_count": 75, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.6081573944406065, + "valid_total_return_pct": 1.5626791854819602, + "valid_win_rate": 0.5857142857142857, + "valid_sharpe_ratio": 2.2442047142281463, + "valid_max_drawdown_pct": 0.6539417061012387, + "valid_trade_count": 70, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:05:05.412943+00:00" }, { "entry_id": "IMP-0023", "experiment_name": "pead_midcap_step23_sdlong_close7", - "sqs_score": 55.1, + "sqs_score": 24.9, + "sqs_v2_score": 55.2, + "promotion_score": null, + "unified_score": null, "profit_factor": 1.1334708809037874, "total_return_pct": 0.4719010862756259, "win_rate": 0.5362318840579711, "sharpe_ratio": 0.7440066482222032, "max_drawdown_pct": 1.419114371849651, "trade_count": 69, + "avg_gross_exposure_pct": 8.585446124185925, + "avg_net_exposure_pct": 8.585446124185925, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T06:59:08.123617+00:00" }, { - "entry_id": "IMP-0032", - "experiment_name": "pead_midcap_step38_balanced_sleeves_aclong_vol4", - "sqs_score": 54.2, - "profit_factor": 1.1204890397898135, - "total_return_pct": 0.388000418802214, - "win_rate": 0.5081967213114754, - "sharpe_ratio": 0.8136845994700913, - "max_drawdown_pct": 0.9399793760032171, - "trade_count": 61, - "timestamp": "2026-03-17T07:11:26.965107+00:00" + "entry_id": "IMP-0028", + "experiment_name": "pead_midcap_step34_balanced_sleeves_nofrac_aclong25", + "sqs_score": 23.9, + "sqs_v2_score": 80.4, + "promotion_score": 66.2, + "unified_score": 23.9, + "profit_factor": 1.6229787581193362, + "total_return_pct": 1.809973740790665, + "win_rate": 0.5645161290322581, + "sharpe_ratio": 3.784062042365747, + "max_drawdown_pct": 0.5514994557958487, + "trade_count": 62, + "avg_gross_exposure_pct": 7.399104242788882, + "avg_net_exposure_pct": 7.399104242788882, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.2977962107392036, + "valid_total_return_pct": 0.888003662238436, + "valid_win_rate": 0.5087719298245614, + "valid_sharpe_ratio": 1.43172550959332, + "valid_max_drawdown_pct": 0.884279658549619, + "valid_trade_count": 57, + "valid_avg_gross_exposure_pct": 7.9217039449630455, + "valid_avg_net_exposure_pct": 7.9217039449630455, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T07:05:41.969503+00:00" }, { "entry_id": "IMP-0017", "experiment_name": "pead_midcap_step16_react7_score65", - "sqs_score": 53.2, + "sqs_score": 23.8, + "sqs_v2_score": null, + "promotion_score": 74.9, + "unified_score": null, "profit_factor": 0.9701741019713591, "total_return_pct": -0.12615119218212204, "win_rate": 0.5617977528089888, "sharpe_ratio": -0.2273584908824299, "max_drawdown_pct": 1.5543376309462142, "trade_count": 89, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 2.0144135190573675, + "valid_total_return_pct": 2.7839796082119577, + "valid_win_rate": 0.6265060240963856, + "valid_sharpe_ratio": 3.7031973701157277, + "valid_max_drawdown_pct": 0.7815696983079155, + "valid_trade_count": 83, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:16:52.056303+00:00" }, { "entry_id": "IMP-0005", "experiment_name": "pead_midcap_step5_maxcand3", - "sqs_score": 52.5, + "sqs_score": 23.4, + "sqs_v2_score": null, + "promotion_score": 75.4, + "unified_score": null, "profit_factor": 0.9504596545547419, "total_return_pct": -0.2388243054896011, "win_rate": 0.5416666666666666, "sharpe_ratio": -0.4009079471879985, "max_drawdown_pct": 1.4011627353834164, "trade_count": 96, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 2.081614051785466, + "valid_total_return_pct": 3.3263332066270377, + "valid_win_rate": 0.651685393258427, + "valid_sharpe_ratio": 3.9774346553681674, + "valid_max_drawdown_pct": 1.1046989753464673, + "valid_trade_count": 89, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:30.561839+00:00" }, { - "entry_id": "IMP-0022", - "experiment_name": "pead_midcap_portfolio_v2", - "sqs_score": 52.0, - "profit_factor": 1.0823586029850167, - "total_return_pct": 0.28994018403757943, - "win_rate": 0.5142857142857142, - "sharpe_ratio": 0.4840289893207606, - "max_drawdown_pct": 1.7544248374390794, - "trade_count": 70, - "timestamp": "2026-03-17T06:59:07.766076+00:00" + "entry_id": "IMP-0032", + "experiment_name": "pead_midcap_step38_balanced_sleeves_aclong_vol4", + "sqs_score": 23.3, + "sqs_v2_score": 54.5, + "promotion_score": 59.5, + "unified_score": 23.3, + "profit_factor": 1.1204890397898135, + "total_return_pct": 0.388000418802214, + "win_rate": 0.5081967213114754, + "sharpe_ratio": 0.8136845994700913, + "max_drawdown_pct": 0.9399793760032171, + "trade_count": 61, + "avg_gross_exposure_pct": 7.401658621256645, + "avg_net_exposure_pct": 7.401658621256645, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.2930052787721988, + "valid_total_return_pct": 0.8333477612284769, + "valid_win_rate": 0.5283018867924528, + "valid_sharpe_ratio": 1.4376847128011023, + "valid_max_drawdown_pct": 0.8783421675410786, + "valid_trade_count": 53, + "valid_avg_gross_exposure_pct": 7.580838934501948, + "valid_avg_net_exposure_pct": 7.580838934501948, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T07:11:26.965107+00:00" }, { "entry_id": "IMP-0016", "experiment_name": "pead_midcap_step15_react7", - "sqs_score": 51.7, + "sqs_score": 23.0, + "sqs_v2_score": null, + "promotion_score": 72.4, + "unified_score": null, "profit_factor": 0.9429428813222424, "total_return_pct": -0.2583590451609634, "win_rate": 0.5494505494505495, "sharpe_ratio": -0.4757579191663637, "max_drawdown_pct": 1.5948632659956048, "trade_count": 91, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.8949702222034897, + "valid_total_return_pct": 2.6109835614154147, + "valid_win_rate": 0.6190476190476191, + "valid_sharpe_ratio": 3.486940708619626, + "valid_max_drawdown_pct": 0.9498747174984115, + "valid_trade_count": 84, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-17T02:16:34.131542+00:00" }, + { + "entry_id": "IMP-0022", + "experiment_name": "pead_midcap_portfolio_v2", + "sqs_score": 23.0, + "sqs_v2_score": 51.9, + "promotion_score": null, + "unified_score": null, + "profit_factor": 1.0823586029850167, + "total_return_pct": 0.28994018403757943, + "win_rate": 0.5142857142857142, + "sharpe_ratio": 0.4840289893207606, + "max_drawdown_pct": 1.7544248374390794, + "trade_count": 70, + "avg_gross_exposure_pct": 7.911601962289335, + "avg_net_exposure_pct": 7.911601962289335, + "days_in_market_pct": 72.3404255319149, + "valid_profit_factor": null, + "valid_total_return_pct": null, + "valid_win_rate": null, + "valid_sharpe_ratio": null, + "valid_max_drawdown_pct": null, + "valid_trade_count": 0, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, + "timestamp": "2026-03-17T06:59:07.766076+00:00" + }, { "entry_id": "IMP-0012", "experiment_name": "pead_midcap_step11_score60", - "sqs_score": 50.2, + "sqs_score": 22.1, + "sqs_v2_score": null, + "promotion_score": 71.9, + "unified_score": null, "profit_factor": 1.0190825170597053, "total_return_pct": 0.07685407145853969, "win_rate": 0.5194805194805194, "sharpe_ratio": 0.15103876740694316, "max_drawdown_pct": 0.8954589900133336, "trade_count": 77, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.9089335238197231, + "valid_total_return_pct": 2.2525004674136144, + "valid_win_rate": 0.625, + "valid_sharpe_ratio": 2.9527896546854935, + "valid_max_drawdown_pct": 0.6530874475348815, + "valid_trade_count": 72, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:04:14.974407+00:00" }, { "entry_id": "IMP-0013", "experiment_name": "pead_midcap_step12_vol2x", - "sqs_score": 50.2, + "sqs_score": 22.1, + "sqs_v2_score": null, + "promotion_score": 71.9, + "unified_score": null, "profit_factor": 1.018057074188992, "total_return_pct": 0.07292559720991995, "win_rate": 0.5194805194805194, "sharpe_ratio": 0.1450757451336352, "max_drawdown_pct": 0.8904960176547445, "trade_count": 77, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.9089335238197231, + "valid_total_return_pct": 2.2525004674136144, + "valid_win_rate": 0.625, + "valid_sharpe_ratio": 2.9527896546854935, + "valid_max_drawdown_pct": 0.6530874475348815, + "valid_trade_count": 72, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:04:15.346697+00:00" }, { "entry_id": "IMP-0003", "experiment_name": "pead_midcap_step3_10pct", - "sqs_score": 49.8, + "sqs_score": 21.9, + "sqs_v2_score": null, + "promotion_score": 68.6, + "unified_score": null, "profit_factor": 1.0020701077990406, "total_return_pct": 0.013499456389559782, "win_rate": 0.5, "sharpe_ratio": 0.03939447191001189, "max_drawdown_pct": 1.4225278540108528, "trade_count": 98, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.664496799271172, + "valid_total_return_pct": 1.9591117241190805, + "valid_win_rate": 0.6025641025641025, + "valid_sharpe_ratio": 2.907578829549799, + "valid_max_drawdown_pct": 0.6538050285655718, + "valid_trade_count": 78, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:29.857239+00:00" }, { "entry_id": "IMP-0011", "experiment_name": "pead_midcap_step10_short", - "sqs_score": 49.2, + "sqs_score": 21.5, + "sqs_v2_score": null, + "promotion_score": 66.3, + "unified_score": null, "profit_factor": 0.9958404155353467, "total_return_pct": -0.013683964940166334, "win_rate": 0.5189873417721519, "sharpe_ratio": -0.007699537912677983, "max_drawdown_pct": 0.9293000795486674, "trade_count": 79, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.6081573944406065, + "valid_total_return_pct": 1.5626791854819602, + "valid_win_rate": 0.5857142857142857, + "valid_sharpe_ratio": 2.2442047142281463, + "valid_max_drawdown_pct": 0.6539417061012387, + "valid_trade_count": 70, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:04:14.616459+00:00" }, { - "entry_id": "IMP-0047", - "experiment_name": "pead_midcap_step53_short_core_macro_block_crashcap_gap14", - "sqs_score": 43.1, - "profit_factor": 4.599773676047558, - "total_return_pct": 1.1191525844285641, - "win_rate": 0.8421052631578947, - "sharpe_ratio": 3.463700507402551, - "max_drawdown_pct": 0.21947031826165894, - "trade_count": 19, - "timestamp": "2026-03-17T07:54:28.762757+00:00" - }, - { - "entry_id": "IMP-0044", - "experiment_name": "pead_midcap_step50_same_day_short_long_macro_block", - "sqs_score": 41.9, - "profit_factor": 3.2060142950020296, - "total_return_pct": 0.805535692316771, - "win_rate": 0.6, - "sharpe_ratio": 2.6871013818111624, - "max_drawdown_pct": 0.42315703199236054, - "trade_count": 15, - "timestamp": "2026-03-17T07:54:27.691772+00:00" + "entry_id": "IMP-0029", + "experiment_name": "pead_midcap_step35_balanced_sleeves_nofrac_aclong12", + "sqs_score": 21.2, + "sqs_v2_score": 90.1, + "promotion_score": 64.8, + "unified_score": 21.2, + "profit_factor": 2.009368125746884, + "total_return_pct": 2.181331690538893, + "win_rate": 0.6071428571428571, + "sharpe_ratio": 4.216651506745797, + "max_drawdown_pct": 0.4424438123900949, + "trade_count": 56, + "avg_gross_exposure_pct": 6.227596335660352, + "avg_net_exposure_pct": 1.1262657538133194, + "days_in_market_pct": 76.59574468085107, + "valid_profit_factor": 1.2430238868359724, + "valid_total_return_pct": 0.7082451550234983, + "valid_win_rate": 0.5094339622641509, + "valid_sharpe_ratio": 1.1796633240683954, + "valid_max_drawdown_pct": 0.8845604127450726, + "valid_trade_count": 53, + "valid_avg_gross_exposure_pct": 7.5108896612986165, + "valid_avg_net_exposure_pct": 1.5673421201773916, + "valid_days_in_market_pct": 77.19298245614034, + "timestamp": "2026-03-17T07:05:47.363036+00:00" }, { "entry_id": "IMP-0002", "experiment_name": "pead_midcap_step2_notrail", - "sqs_score": 41.4, + "sqs_score": 17.2, + "sqs_v2_score": null, + "promotion_score": 45.5, + "unified_score": null, "profit_factor": 0.9313134618740863, "total_return_pct": -0.26847486723051406, "win_rate": 0.6666666666666666, "sharpe_ratio": -0.366331653948068, "max_drawdown_pct": 1.7431414336132605, "trade_count": 54, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.073493344735863, + "valid_total_return_pct": 0.3745287237202429, + "valid_win_rate": 0.7258064516129032, + "valid_sharpe_ratio": 0.39419762465723635, + "valid_max_drawdown_pct": 1.7163906170323242, + "valid_trade_count": 62, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:29.505541+00:00" }, { "entry_id": "IMP-0001", "experiment_name": "pead_midcap_step1_fixedr", - "sqs_score": 39.7, + "sqs_score": 16.2, + "sqs_v2_score": null, + "promotion_score": 59.2, + "unified_score": null, "profit_factor": 0.9135744830352539, "total_return_pct": -0.4557621829436975, "win_rate": 0.47368421052631576, "sharpe_ratio": -0.690798512095932, "max_drawdown_pct": 1.4922738831283402, "trade_count": 95, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.7731598619764342, + "valid_total_return_pct": 2.779776127733232, + "valid_win_rate": 0.4927536231884058, + "valid_sharpe_ratio": 3.3855945499328657, + "valid_max_drawdown_pct": 1.5546437384924012, + "valid_trade_count": 69, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:29.147972+00:00" }, { "entry_id": "IMP-0007", "experiment_name": "pead_midcap_combo_10pct_maxcand3", - "sqs_score": 39.2, + "sqs_score": 15.9, + "sqs_v2_score": null, + "promotion_score": 66.9, + "unified_score": null, "profit_factor": 0.9701095917976659, "total_return_pct": -0.11821922524747788, "win_rate": 0.5063291139240507, "sharpe_ratio": -0.18779926674176511, "max_drawdown_pct": 0.9293000795486674, "trade_count": 79, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.9089335238197231, + "valid_total_return_pct": 2.2525004674136144, + "valid_win_rate": 0.625, + "valid_sharpe_ratio": 2.9527896546854935, + "valid_max_drawdown_pct": 0.6530874475348815, + "valid_trade_count": 72, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:31.270613+00:00" }, - { - "entry_id": "IMP-0043", - "experiment_name": "pead_midcap_step49_same_day_short_macro_block", - "sqs_score": 38.9, - "profit_factor": 2.4025420210691006, - "total_return_pct": 0.40093684044296973, - "win_rate": 0.7, - "sharpe_ratio": 1.5580313035787354, - "max_drawdown_pct": 0.3693400619520335, - "trade_count": 10, - "timestamp": "2026-03-17T07:54:27.322093+00:00" - }, { "entry_id": "IMP-0006", "experiment_name": "pead_midcap_step6_drift", - "sqs_score": 37.0, + "sqs_score": 14.7, + "sqs_v2_score": null, + "promotion_score": 58.6, + "unified_score": null, "profit_factor": 0.8557264506988863, "total_return_pct": -0.8788682238009642, "win_rate": 0.43, "sharpe_ratio": -1.5533516509762217, "max_drawdown_pct": 1.7395188967538284, "trade_count": 100, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.704720436988778, + "valid_total_return_pct": 2.6652714937057898, + "valid_win_rate": 0.5066666666666667, + "valid_sharpe_ratio": 3.7096311156929622, + "valid_max_drawdown_pct": 1.0499246227859185, + "valid_trade_count": 75, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:30.916045+00:00" }, { "entry_id": "IMP-0008", "experiment_name": "pead_midcap_step7_fixedr", - "sqs_score": 35.4, + "sqs_score": 13.8, + "sqs_v2_score": null, + "promotion_score": 63.6, + "unified_score": null, "profit_factor": 0.9376665114180485, "total_return_pct": -0.24986939032697408, "win_rate": 0.4507042253521127, "sharpe_ratio": -0.39554365980509315, "max_drawdown_pct": 0.9924460035465441, "trade_count": 71, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 2.0006534393926754, + "valid_total_return_pct": 2.3868649231372836, + "valid_win_rate": 0.5283018867924528, + "valid_sharpe_ratio": 3.087810484110416, + "valid_max_drawdown_pct": 0.6860864501039302, + "valid_trade_count": 53, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:01:54.422047+00:00" }, { "entry_id": "IMP-0004", "experiment_name": "pead_midcap_step4_longonly", - "sqs_score": 32.6, + "sqs_score": 12.2, + "sqs_v2_score": null, + "promotion_score": 53.4, + "unified_score": null, "profit_factor": 0.8355717037294544, "total_return_pct": -0.8088267292581296, "win_rate": 0.44871794871794873, "sharpe_ratio": -1.1731301213701413, "max_drawdown_pct": 1.4401741814514755, "trade_count": 78, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.430929775113959, + "valid_total_return_pct": 1.4935870708133152, + "valid_win_rate": 0.6052631578947368, + "valid_sharpe_ratio": 1.6551004874178592, + "valid_max_drawdown_pct": 1.597646524797458, + "valid_trade_count": 76, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T22:52:30.203924+00:00" }, { "entry_id": "IMP-0009", "experiment_name": "pead_midcap_step8_nft", - "sqs_score": 31.3, + "sqs_score": 11.5, + "sqs_v2_score": null, + "promotion_score": 51.4, + "unified_score": null, "profit_factor": 0.6522816324098041, "total_return_pct": -1.8153531028857979, "win_rate": 0.4084507042253521, "sharpe_ratio": -2.981974625656072, "max_drawdown_pct": 2.1930148870201824, "trade_count": 71, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.5546541014245128, + "valid_total_return_pct": 1.8349072453313857, + "valid_win_rate": 0.45614035087719296, + "valid_sharpe_ratio": 2.3501871656264828, + "valid_max_drawdown_pct": 0.9575880067210154, + "valid_trade_count": 57, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:01:54.804591+00:00" }, { "entry_id": "IMP-0010", "experiment_name": "pead_midcap_step9_stop2", - "sqs_score": 31.1, + "sqs_score": 11.4, + "sqs_v2_score": null, + "promotion_score": 59.8, + "unified_score": null, "profit_factor": 0.7658210615369154, "total_return_pct": -1.7396298311107383, "win_rate": 0.4507042253521127, "sharpe_ratio": -1.8398745071636018, "max_drawdown_pct": 2.4893517163865826, "trade_count": 71, + "avg_gross_exposure_pct": null, + "avg_net_exposure_pct": null, + "days_in_market_pct": null, + "valid_profit_factor": 1.8143851030277298, + "valid_total_return_pct": 3.223451666916022, + "valid_win_rate": 0.5283018867924528, + "valid_sharpe_ratio": 2.813496185483277, + "valid_max_drawdown_pct": 1.2331625402933826, + "valid_trade_count": 53, + "valid_avg_gross_exposure_pct": null, + "valid_avg_net_exposure_pct": null, + "valid_days_in_market_pct": null, "timestamp": "2026-03-16T23:01:55.163461+00:00" } ], - "updated_at": "2026-03-17T07:57:00.128792+00:00" + "updated_at": "2026-03-17T09:17:27.726326+00:00" } \ No newline at end of file diff --git a/journal/improvement_journal.jsonl b/journal/improvement_journal.jsonl index fbb3448..8c0efc2 100644 --- a/journal/improvement_journal.jsonl +++ b/journal/improvement_journal.jsonl @@ -26,7 +26,7 @@ {"entry_id":"IMP-0026","timestamp":"2026-03-17T06:59:09.255218+00:00","experiment_name":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","hypothesis":"Shrink the after-close short sleeve again while keeping the rest of the balanced step30 structure intact.","config_delta":{"base_experiment":"pead_midcap_step30_balanced_sleeves_nofrac","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065814_ca861015","trade_count":64,"profit_factor":1.4929664814770336,"total_return_pct":1.5563811867822805,"win_rate":0.546875,"max_drawdown_pct":0.6813257982524192,"sharpe_ratio":3.235108878683093,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8144638886979153},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065750_ca861015","trade_count":426,"profit_factor":1.0908713237524914,"total_return_pct":2.45066812206927,"win_rate":0.45539906103286387,"max_drawdown_pct":2.6845497224397294,"sharpe_ratio":0.3357364835430949,"monthly_win_rate":0.5142857142857142,"equity_curve_r_squared":0.6011932815228401},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065810_ca861015","trade_count":58,"profit_factor":1.37572962559925,"total_return_pct":1.122807593766629,"win_rate":0.5172413793103449,"max_drawdown_pct":0.8884863869932943,"sharpe_ratio":1.8162493466149443,"monthly_win_rate":0.75,"equity_curve_r_squared":0.3292893146712864}},"sqs_score":77.9,"sqs_breakdown":{"profitability":60.9,"risk":100.0,"consistency":82.8,"robustness":80.0},"verdict":"better","verdict_reasoning":"New best. Train/valid/test all held up, with test SQS 77.9, return +1.56%, PF 1.66, Sharpe 2.39, and max drawdown 0.32%. This beat both step30 and step14 by a wide margin.","next_direction":"Use step31 as the new base and only explore very local refinements around sleeve weights or execution if further gains are needed.","tags":["pead","midcap","step31","balanced","sleeves","nofrac","acshort12"]} {"entry_id":"IMP-0027","timestamp":"2026-03-17T06:59:09.622681+00:00","experiment_name":"pead_midcap_step33_balanced_sleeves_nofrac_acshort6","hypothesis":"Cut the after-close short sleeve even further to see if the balanced portfolio still benefits from the bucket at very low size.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065700_765096af","trade_count":48,"profit_factor":1.4726487821009215,"total_return_pct":1.2304910166146,"win_rate":0.5208333333333334,"max_drawdown_pct":0.7081594843000599,"sharpe_ratio":2.3026797742321596,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7956775968800425}},"sqs_score":73.9,"sqs_breakdown":{"profitability":58.6,"risk":100.0,"consistency":74.3,"robustness":70.8},"verdict":"worse","verdict_reasoning":"Test-only run stayed strong but slipped versus step31: SQS 73.9 vs 77.9 and return +1.23% vs +1.56%. The smaller sleeve gave up too much trade-count support.","next_direction":"Keep the 12.5% after-close short sleeve from step31.","tags":["pead","midcap","step33","balanced","sleeves","nofrac","acshort6"]} {"entry_id":"IMP-0028","timestamp":"2026-03-17T07:05:41.969503+00:00","experiment_name":"pead_midcap_step34_balanced_sleeves_nofrac_aclong25","hypothesis":"Cap the after-close long sleeve to two trades per day so only the best-ranked overnight continuation names survive.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_6bef3bf5","trade_count":62,"profit_factor":1.6229787581193362,"total_return_pct":1.809973740790665,"win_rate":0.5645161290322581,"max_drawdown_pct":0.5514994557958487,"sharpe_ratio":3.784062042365747,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8527995261422007},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070447_6bef3bf5","trade_count":57,"profit_factor":1.2977962107392036,"total_return_pct":0.888003662238436,"win_rate":0.5087719298245614,"max_drawdown_pct":0.884279658549619,"sharpe_ratio":1.43172550959332,"monthly_win_rate":0.75,"equity_curve_r_squared":0.26161939040716076}},"sqs_score":81.3,"sqs_breakdown":{"profitability":68.4,"risk":100.0,"consistency":85.8,"robustness":78.9},"verdict":"worse","verdict_reasoning":"Test improved to SQS 81.3 and +1.81% return, but valid slipped to SQS 63.7 and +0.89% versus step31 valid SQS 68.2 and +1.12%. The top-two cap on after-close longs was not robust across splits.","next_direction":"Keep step31 as the robust base. If after-close long needs filtering, prefer a quality gate rather than a blunt daily-cap reduction.","tags":["pead","midcap","step34","balanced","sleeves","nofrac","aclong25"]} -{"entry_id":"IMP-0029","timestamp":"2026-03-17T07:05:47.363036+00:00","experiment_name":"pead_midcap_step35_balanced_sleeves_nofrac_aclong12","hypothesis":"Cap the after-close long sleeve to one trade per day so the portfolio fully leans into the strongest overnight continuation name only.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_01aa6342","trade_count":56,"profit_factor":2.009368125746884,"total_return_pct":2.181331690538893,"win_rate":0.6071428571428571,"max_drawdown_pct":0.4424438123900949,"sharpe_ratio":4.216651506745797,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8888944531705611},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070357_01aa6342","trade_count":53,"profit_factor":1.2430238868359724,"total_return_pct":0.7082451550234983,"win_rate":0.5094339622641509,"max_drawdown_pct":0.8845604127450726,"sharpe_ratio":1.1796633240683954,"monthly_win_rate":0.75,"equity_curve_r_squared":0.14646025414461133},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070358_01aa6342","trade_count":393,"profit_factor":1.1135008933147288,"total_return_pct":2.755962139586409,"win_rate":0.46055979643765904,"max_drawdown_pct":2.418955602136075,"sharpe_ratio":0.38778263039611355,"monthly_win_rate":0.4857142857142857,"equity_curve_r_squared":0.6345243468091162}},"sqs_score":90.4,"sqs_breakdown":{"profitability":88.7,"risk":100.0,"consistency":92.9,"robustness":75.6},"verdict":"worse","verdict_reasoning":"This became a new test-only high water mark at SQS 90.4 and +2.18%, but valid deteriorated to SQS 59.8 and +0.71%, materially below step31. The tighter top-one cap overfit to the test window.","next_direction":"Avoid promoting step35. Explore engine-specific quality filters for after-close longs instead of hard caps that reshuffle trade selection too aggressively.","tags":["pead","midcap","step35","balanced","sleeves","nofrac","aclong12"]} +{"entry_id":"IMP-0029","timestamp":"2026-03-17T07:05:47.363036+00:00","experiment_name":"pead_midcap_step35_balanced_sleeves_nofrac_aclong12","hypothesis":"Cap the after-close long sleeve to one trade per day so the portfolio fully leans into the strongest overnight continuation name only.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081806944455_48768969","trade_count":56,"profit_factor":2.009368125746884,"total_return_pct":2.181331690538893,"win_rate":0.6071428571428571,"max_drawdown_pct":0.4424438123900949,"sharpe_ratio":4.216651506745797,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8888944531705611,"avg_gross_exposure_pct":6.227596335660352,"avg_net_exposure_pct":1.1262657538133194,"days_in_market_pct":76.59574468085107},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081817695254_48768969","trade_count":53,"profit_factor":1.2430238868359724,"total_return_pct":0.7082451550234983,"win_rate":0.5094339622641509,"max_drawdown_pct":0.8845604127450726,"sharpe_ratio":1.1796633240683954,"monthly_win_rate":0.75,"equity_curve_r_squared":0.14646025414461133,"avg_gross_exposure_pct":7.5108896612986165,"avg_net_exposure_pct":1.5673421201773916,"days_in_market_pct":77.19298245614034},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070358_01aa6342","trade_count":393,"profit_factor":1.1135008933147288,"total_return_pct":2.755962139586409,"win_rate":0.46055979643765904,"max_drawdown_pct":2.418955602136075,"sharpe_ratio":0.38778263039611355,"monthly_win_rate":0.4857142857142857,"equity_curve_r_squared":0.6345243468091162}},"sqs_score":90.4,"sqs_breakdown":{"profitability":88.7,"risk":100.0,"consistency":92.9,"robustness":75.6},"verdict":"worse","verdict_reasoning":"This became a new test-only high water mark at SQS 90.4 and +2.18%, but valid deteriorated to SQS 59.8 and +0.71%, materially below step31. The tighter top-one cap overfit to the test window.","next_direction":"Avoid promoting step35. Explore engine-specific quality filters for after-close longs instead of hard caps that reshuffle trade selection too aggressively.","tags":["pead","midcap","step35","balanced","sleeves","nofrac","aclong12"],"sqs_v2_score":90.1,"sqs_v2_breakdown":{"profitability":88.7,"risk":100.0,"consistency":92.9,"robustness":75.6,"capital_efficiency":79.0}} {"entry_id":"IMP-0030","timestamp":"2026-03-17T07:05:52.980782+00:00","experiment_name":"pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12","hypothesis":"Keep only one same-day long close trade per day alongside the top-one after-close long sleeve, concentrating the portfolio into the single best long continuation setups.","config_delta":{"base_experiment":"pead_midcap_step35_balanced_sleeves_nofrac_aclong12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_18804e7c","trade_count":54,"profit_factor":2.061648751658133,"total_return_pct":2.1478646359501146,"win_rate":0.6111111111111112,"max_drawdown_pct":0.3579823467213594,"sharpe_ratio":4.4957307480895325,"monthly_win_rate":1.0,"equity_curve_r_squared":0.9122676472502861}},"sqs_score":90.3,"sqs_breakdown":{"profitability":88.6,"risk":100.0,"consistency":93.5,"robustness":74.4},"verdict":"worse","verdict_reasoning":"Test stayed extremely strong at SQS 90.3 and +2.15%, but it did not exceed step35 on test and reduced trade support further. Without valid/train confirmation, it is not a better promotion candidate than step31.","next_direction":"Keep the same-day long sleeve at two trades per day if using this family, and focus next on smarter after-close long quality filtering.","tags":["pead","midcap","step36","balanced","sleeves","nofrac","aclong12","sdlong12"]} {"entry_id":"IMP-0031","timestamp":"2026-03-17T07:11:20.119143+00:00","experiment_name":"pead_midcap_step37_balanced_sleeves_aclong_vol3","hypothesis":"Require stronger volume confirmation for after-close long signals only, while leaving the rest of the step31 sleeve mix unchanged.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110746395_1ae4b33a","trade_count":63,"profit_factor":1.377310183342628,"total_return_pct":1.1862161229211343,"win_rate":0.5396825396825397,"max_drawdown_pct":0.8243191281564817,"sharpe_ratio":2.4626680484273273,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6885324256339993},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110780542_1ae4b33a","trade_count":57,"profit_factor":1.375336580561267,"total_return_pct":1.1216330418461293,"win_rate":0.5087719298245614,"max_drawdown_pct":0.8896384382974281,"sharpe_ratio":1.7701058914354275,"monthly_win_rate":0.75,"equity_curve_r_squared":0.3302488231788083}},"sqs_score":73.6,"sqs_breakdown":{"profitability":53.6,"risk":100.0,"consistency":81.6,"robustness":72.5},"verdict":"worse","verdict_reasoning":"Engine-specific volume gating on after-close longs did not help. Test fell to SQS 73.6 and valid to 67.6, both below the step31 base. The extra volume filter removed too much breadth without improving robustness.","next_direction":"Do not tighten after-close long volume gates further. Keep after-close long breadth and search elsewhere if more robustness is needed.","tags":["pead","midcap","step37","balanced","sleeves","aclong","vol3"]} {"entry_id":"IMP-0032","timestamp":"2026-03-17T07:11:26.965107+00:00","experiment_name":"pead_midcap_step38_balanced_sleeves_aclong_vol4","hypothesis":"Push the after-close long sleeve to an even stricter volume gate so only the highest-conviction overnight reactions remain.","config_delta":{"base_experiment":"pead_midcap_step37_balanced_sleeves_aclong_vol3","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110780988_d9020d34","trade_count":61,"profit_factor":1.1204890397898135,"total_return_pct":0.388000418802214,"win_rate":0.5081967213114754,"max_drawdown_pct":0.9399793760032171,"sharpe_ratio":0.8136845994700913,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.043822307069783864},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110695904_d9020d34","trade_count":53,"profit_factor":1.2930052787721988,"total_return_pct":0.8333477612284769,"win_rate":0.5283018867924528,"max_drawdown_pct":0.8783421675410786,"sharpe_ratio":1.4376847128011023,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19294412573010883}},"sqs_score":54.2,"sqs_breakdown":{"profitability":37.6,"risk":80.2,"consistency":72.2,"robustness":31.1},"verdict":"worse","verdict_reasoning":"The stricter after-close long filter clearly broke the portfolio. Test dropped to SQS 54.2 and valid to 63.2, confirming that this sleeve cannot be improved by simply tightening volume thresholds.","next_direction":"Abandon the after-close long volume-threshold path. If that sleeve is revisited, it needs a different filter than raw PEAD volume.","tags":["pead","midcap","step38","balanced","sleeves","aclong","vol4"]} @@ -35,13 +35,13 @@ {"entry_id":"IMP-0035","timestamp":"2026-03-17T07:20:29.978594+00:00","experiment_name":"pead_midcap_step41_short_core_sdlong12_acshort50","hypothesis":"Lean harder into the after-close short sleeve inside the new short-core portfolio.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756232509_4c9fd4ce","trade_count":40,"profit_factor":1.483920817012022,"total_return_pct":0.8662763872782817,"win_rate":0.55,"max_drawdown_pct":0.6476993822793542,"sharpe_ratio":1.8482432920991685,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19435203451054603},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756491852_4c9fd4ce","trade_count":59,"profit_factor":1.5287187084320328,"total_return_pct":1.231827071365813,"win_rate":0.559322033898305,"max_drawdown_pct":1.2759660172387226,"sharpe_ratio":1.6310365384096348,"monthly_win_rate":1.0,"equity_curve_r_squared":0.4227182249282274}},"sqs_score":72.6,"sqs_breakdown":{"profitability":61.4,"risk":92.3,"consistency":84.9,"robustness":53.6},"verdict":"worse","verdict_reasoning":"Increasing after-close short capacity weakened the portfolio: test dropped from SQS 82.1 to 72.6 and valid stayed flat at 68.4. The short core benefits from the bucket, but not at this larger size.","next_direction":"Keep the after-close short sleeve at 25% inside the short-core family.","tags":["pead","midcap","step41","short","core","sdlong12","acshort50"]} {"entry_id":"IMP-0037","timestamp":"2026-03-17T07:20:29.978670+00:00","experiment_name":"pead_midcap_step43_short_core_sdlong25","hypothesis":"Restore a larger same-day long close sleeve after removing after-close longs, to see if breadth improves the short-core portfolio.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071957079395_9046f612","trade_count":45,"profit_factor":1.4604102163883825,"total_return_pct":0.9859009158709378,"win_rate":0.5555555555555556,"max_drawdown_pct":0.6469365320312458,"sharpe_ratio":1.9269062309154923,"monthly_win_rate":0.75,"equity_curve_r_squared":0.2568675902061873},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071957079892_9046f612","trade_count":58,"profit_factor":1.6628306811204845,"total_return_pct":1.4636686220428092,"win_rate":0.5689655172413793,"max_drawdown_pct":1.2630043081731899,"sharpe_ratio":2.0112049109753753,"monthly_win_rate":1.0,"equity_curve_r_squared":0.57291038217223}},"sqs_score":78.9,"sqs_breakdown":{"profitability":69.0,"risk":98.5,"consistency":86.5,"robustness":62.5},"verdict":"worse","verdict_reasoning":"Restoring more same-day long breadth weakened both splits versus step40: valid moved from SQS 68.4 to 69.7 but test fell from 82.1 to 78.9 and profitability dropped. The smaller 12.5% sleeve remains the better balance.","next_direction":"Keep the same-day long overlay small inside step40.","tags":["pead","midcap","step43","short","core","sdlong25"]} {"entry_id":"IMP-0034","timestamp":"2026-03-17T07:20:29.978842+00:00","experiment_name":"pead_midcap_step40_short_core_sdlong12","hypothesis":"Drop the unstable after-close long sleeve and reallocate the portfolio to same-day shorts, after-close shorts, and a small same-day close-entry long overlay.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756453145_9eb08c8d","trade_count":40,"profit_factor":1.483920817012022,"total_return_pct":0.8662763872782817,"win_rate":0.55,"max_drawdown_pct":0.6476993822793542,"sharpe_ratio":1.8482432920991685,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19435203451054603},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756505329_9eb08c8d","trade_count":55,"profit_factor":1.7695290624299977,"total_return_pct":1.52020169384827,"win_rate":0.5818181818181818,"max_drawdown_pct":1.128135882565315,"sharpe_ratio":2.2087400904317267,"monthly_win_rate":1.0,"equity_curve_r_squared":0.632820774620957},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071814970965_9eb08c8d","trade_count":306,"profit_factor":1.2169887676968005,"total_return_pct":3.708684730821959,"win_rate":0.4869281045751634,"max_drawdown_pct":2.5507043144769854,"sharpe_ratio":0.5720232080317771,"monthly_win_rate":0.625,"equity_curve_r_squared":0.39497202444939644}},"sqs_score":82.1,"sqs_breakdown":{"profitability":74.6,"risk":99.3,"consistency":88.6,"robustness":64.6},"verdict":"better","verdict_reasoning":"New robust leader. Train improved from SQS 55.5 to 63.1 and return +2.45% to +3.71%. Valid edged up from SQS 68.2 to 68.4 with lower drawdown, and test improved from SQS 77.9 to 82.1 with PF 1.77. Removing after-close longs fixed the biggest unstable sleeve without giving up the same-day long upside.","next_direction":"Use step40 as the new base. Only explore local refinements around the short-core structure if needed.","tags":["pead","midcap","step40","short","core","sdlong12"]} -{"entry_id":"IMP-0038","timestamp":"2026-03-17T07:54:25.486815+00:00","experiment_name":"pead_midcap_step44_short_core_macro50","hypothesis":"Scaling entries down in weak macro regimes will keep the short-core structure while cutting drawdowns.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073536976331_9c3139ba","trade_count":40,"profit_factor":1.7786229517773076,"total_return_pct":1.0842382260887244,"win_rate":0.55,"max_drawdown_pct":0.4065245518278289,"sharpe_ratio":2.4891782517624543,"monthly_win_rate":0.75,"equity_curve_r_squared":0.34564834918762316},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073537015426_9c3139ba","trade_count":58,"profit_factor":2.0052763103391356,"total_return_pct":1.2986802302195721,"win_rate":0.5689655172413793,"max_drawdown_pct":0.7045335236824514,"sharpe_ratio":2.6217356101467053,"monthly_win_rate":1.0,"equity_curve_r_squared":0.7991432493330419},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073555963588_9c3139ba","trade_count":307,"profit_factor":1.2319887960741855,"total_return_pct":3.4232792009141852,"win_rate":0.4820846905537459,"max_drawdown_pct":1.97377752972113,"sharpe_ratio":0.6203457661608955,"monthly_win_rate":0.625,"equity_curve_r_squared":0.4148680874417292}},"sqs_score":87.9,"sqs_breakdown":{"profitability":85.2,"risk":100.0,"consistency":86.5,"robustness":76.6},"verdict":"better","verdict_reasoning":"Half-size macro scaling materially improved valid and test risk-adjusted performance versus step40, confirming that SPY-below-SMA exposure was a real drag.","next_direction":"Try a full macro block to see whether removing weak-regime entries entirely is even cleaner.","tags":["pead","midcap","step44","short","core","macro50"]} +{"entry_id":"IMP-0038","timestamp":"2026-03-17T07:54:25.486815+00:00","experiment_name":"pead_midcap_step44_short_core_macro50","hypothesis":"Scaling entries down in weak macro regimes will keep the short-core structure while cutting drawdowns.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081817694940_42f8ea0e","trade_count":40,"profit_factor":1.7786229517773076,"total_return_pct":1.0842382260887244,"win_rate":0.55,"max_drawdown_pct":0.4065245518278289,"sharpe_ratio":2.4891782517624543,"monthly_win_rate":0.75,"equity_curve_r_squared":0.34564834918762316,"avg_gross_exposure_pct":4.823763728554508,"avg_net_exposure_pct":-1.952624774981557,"days_in_market_pct":73.68421052631578},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081807057141_42f8ea0e","trade_count":58,"profit_factor":2.0052763103391356,"total_return_pct":1.2986802302195721,"win_rate":0.5689655172413793,"max_drawdown_pct":0.7045335236824514,"sharpe_ratio":2.6217356101467053,"monthly_win_rate":1.0,"equity_curve_r_squared":0.7991432493330419,"avg_gross_exposure_pct":4.453478837236966,"avg_net_exposure_pct":-1.5519032472252936,"days_in_market_pct":72.3404255319149},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073555963588_9c3139ba","trade_count":307,"profit_factor":1.2319887960741855,"total_return_pct":3.4232792009141852,"win_rate":0.4820846905537459,"max_drawdown_pct":1.97377752972113,"sharpe_ratio":0.6203457661608955,"monthly_win_rate":0.625,"equity_curve_r_squared":0.4148680874417292}},"sqs_score":87.9,"sqs_breakdown":{"profitability":85.2,"risk":100.0,"consistency":86.5,"robustness":76.6},"verdict":"better","verdict_reasoning":"Half-size macro scaling materially improved valid and test risk-adjusted performance versus step40, confirming that SPY-below-SMA exposure was a real drag.","next_direction":"Try a full macro block to see whether removing weak-regime entries entirely is even cleaner.","tags":["pead","midcap","step44","short","core","macro50"],"sqs_v2_score":86.9,"sqs_v2_breakdown":{"profitability":85.2,"risk":100.0,"consistency":86.5,"robustness":76.6,"capital_efficiency":70.8}} {"entry_id":"IMP-0039","timestamp":"2026-03-17T07:54:25.866482+00:00","experiment_name":"pead_midcap_step45_short_core_macro_block","hypothesis":"If weak-regime entries are mostly noise, hard-blocking them should outperform simple size scaling.","config_delta":{"base_experiment":"pead_midcap_step44_short_core_macro50","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073536967478_c3a3615c","trade_count":28,"profit_factor":2.3084684930008983,"total_return_pct":1.30245295115927,"win_rate":0.6785714285714286,"max_drawdown_pct":0.37472483014430374,"sharpe_ratio":3.0487382658615467,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48762905493056535},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073537015511_c3a3615c","trade_count":23,"profit_factor":3.7827916861128097,"total_return_pct":1.2021184808416436,"win_rate":0.6956521739130435,"max_drawdown_pct":0.2608177180219861,"sharpe_ratio":3.3795660622862123,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9110928059216074},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073556022527_c3a3615c","trade_count":220,"profit_factor":1.2595187982631553,"total_return_pct":3.200633818982489,"win_rate":0.5045454545454545,"max_drawdown_pct":1.389607178327899,"sharpe_ratio":0.6336391727317187,"monthly_win_rate":0.6551724137931034,"equity_curve_r_squared":0.39958317262208615}},"sqs_score":86.7,"sqs_breakdown":{"profitability":84.8,"risk":100.0,"consistency":95.8,"robustness":57.2},"verdict":"better","verdict_reasoning":"The hard macro block improved valid and test again, with sharper PF and much lower drawdown than the 50% scaler version.","next_direction":"Stress the sleeve mix around the new macro-blocked core.","tags":["pead","midcap","step45","short","core","macro","block"]} {"entry_id":"IMP-0040","timestamp":"2026-03-17T07:54:26.245073+00:00","experiment_name":"pead_midcap_step46_short_core_macro_block_acshort12","hypothesis":"The after-close short sleeve may be oversized after the macro block and could improve if reduced.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073936915792_e3adb886","trade_count":26,"profit_factor":2.4445635979938296,"total_return_pct":1.339314216731771,"win_rate":0.6923076923076923,"max_drawdown_pct":0.37458871743417604,"sharpe_ratio":3.1583041871985076,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48286315791941375},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073939814431_e3adb886","trade_count":21,"profit_factor":4.522043594902001,"total_return_pct":1.2518263219734362,"win_rate":0.7142857142857143,"max_drawdown_pct":0.2783619920052574,"sharpe_ratio":3.6389410132883055,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9244229299732519}},"sqs_score":86.6,"sqs_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.1},"verdict":"worse","verdict_reasoning":"Shrinking the after-close short sleeve slightly degraded both valid and test, so the step45 25% sleeve was not the problem.","next_direction":"Test whether the same-day long sleeve or the short-only core is the real source of edge.","tags":["pead","midcap","step46","short","core","macro","block","acshort12"]} {"entry_id":"IMP-0041","timestamp":"2026-03-17T07:54:26.612610+00:00","experiment_name":"pead_midcap_step47_short_core_macro_block_sdlong25","hypothesis":"A larger same-day reaction-close long overlay might scale once the macro block removes bad regimes.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073936910739_a38d69e4","trade_count":31,"profit_factor":1.7661839600751525,"total_return_pct":1.03177071332802,"win_rate":0.6451612903225806,"max_drawdown_pct":0.42599257212123065,"sharpe_ratio":2.218739891960781,"monthly_win_rate":0.75,"equity_curve_r_squared":0.4214549671036421},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073939814560_a38d69e4","trade_count":25,"profit_factor":3.429404855633068,"total_return_pct":1.2833562667310616,"win_rate":0.68,"max_drawdown_pct":0.2608177180219861,"sharpe_ratio":3.32348066596022,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.889835319415119}},"sqs_score":87.0,"sqs_breakdown":{"profitability":85.1,"risk":100.0,"consistency":95.8,"robustness":58.3},"verdict":"worse","verdict_reasoning":"Increasing the same-day long sleeve hurt valid materially and did not produce a cleaner overall profile than step45.","next_direction":"Try sleeve removal experiments instead of scaling overlays up.","tags":["pead","midcap","step47","short","core","macro","block","sdlong25"]} {"entry_id":"IMP-0042","timestamp":"2026-03-17T07:54:26.967747+00:00","experiment_name":"pead_midcap_step48_short_core_macro_block_nolong","hypothesis":"The macro-blocked short core may be strong enough without the same-day long overlay.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234188388_d9b28f40","trade_count":20,"profit_factor":5.726207009868067,"total_return_pct":1.2584927752282966,"win_rate":0.7,"max_drawdown_pct":0.29982153086364316,"sharpe_ratio":3.3529664036078217,"monthly_win_rate":1.0,"equity_curve_r_squared":0.4654938970631658},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234267904_d9b28f40","trade_count":20,"profit_factor":3.402559985815088,"total_return_pct":0.8451446297187069,"win_rate":0.8,"max_drawdown_pct":0.4239007555767844,"sharpe_ratio":2.4413021645869604,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7861833255279326}},"sqs_score":85.7,"sqs_breakdown":{"profitability":83.4,"risk":100.0,"consistency":95.8,"robustness":54.7},"verdict":"worse","verdict_reasoning":"Removing the long sleeve weakened both valid and test relative to step45, so the small same-day long overlay still adds useful diversification.","next_direction":"Test whether the same-day short sleeve can stand alone or whether the after-close short sleeve is also required.","tags":["pead","midcap","step48","short","core","macro","block","nolong"]} {"entry_id":"IMP-0043","timestamp":"2026-03-17T07:54:27.322093+00:00","experiment_name":"pead_midcap_step49_same_day_short_macro_block","hypothesis":"The pure same-day short engine might dominate the portfolio and make other sleeves unnecessary.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074233919071_b5cae2ca","trade_count":12,"profit_factor":25.094599474518425,"total_return_pct":1.1648266582814248,"win_rate":0.75,"max_drawdown_pct":0.33436592553948014,"sharpe_ratio":3.227317987298049,"monthly_win_rate":1.0,"equity_curve_r_squared":0.3679821567954865},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234357252_b5cae2ca","trade_count":10,"profit_factor":2.4025420210691006,"total_return_pct":0.40093684044296973,"win_rate":0.7,"max_drawdown_pct":0.3693400619520335,"sharpe_ratio":1.5580313035787354,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.3032092765977471}},"sqs_score":38.9,"sqs_breakdown":{"profitability":81.6,"risk":92.6,"consistency":95.8,"robustness":19.0},"verdict":"worse","verdict_reasoning":"The single-sleeve version collapsed SQS because trade count and robustness fell too far, even though the kept trades were profitable.","next_direction":"Keep the supporting sleeves and test smaller structural adjustments instead.","tags":["pead","midcap","step49","same","day","short","macro","block"]} {"entry_id":"IMP-0044","timestamp":"2026-03-17T07:54:27.691772+00:00","experiment_name":"pead_midcap_step50_same_day_short_long_macro_block","hypothesis":"The same-day long overlay may matter, but the after-close short sleeve may be removable.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234172662_a01fa9b8","trade_count":20,"profit_factor":2.2051399980357798,"total_return_pct":0.9530265960178512,"win_rate":0.65,"max_drawdown_pct":0.3444323377542605,"sharpe_ratio":2.687864694846263,"monthly_win_rate":0.75,"equity_curve_r_squared":0.41775889730721794},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234263930_a01fa9b8","trade_count":15,"profit_factor":3.2060142950020296,"total_return_pct":0.805535692316771,"win_rate":0.6,"max_drawdown_pct":0.42315703199236054,"sharpe_ratio":2.6871013818111624,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8242465440616173}},"sqs_score":41.9,"sqs_breakdown":{"profitability":83.2,"risk":100.0,"consistency":87.5,"robustness":52.8},"verdict":"worse","verdict_reasoning":"Dropping the after-close short sleeve reduced both valid and test performance, so step45 still benefits from carrying all three active sleeves.","next_direction":"Refine sleeve quality rather than deleting sleeves wholesale.","tags":["pead","midcap","step50","same","day","short","long","macro","block"]} -{"entry_id":"IMP-0045","timestamp":"2026-03-17T07:54:28.043618+00:00","experiment_name":"pead_midcap_step51_short_core_macro_block_crashcap","hypothesis":"Extreme one-day crash continuations are too stretched for the same-day short sleeve and should be excluded.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074801780500_1585063f","trade_count":28,"profit_factor":2.3084684930008983,"total_return_pct":1.30245295115927,"win_rate":0.6785714285714286,"max_drawdown_pct":0.37472483014430374,"sharpe_ratio":3.0487382658615467,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48762905493056535},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074801781828_1585063f","trade_count":22,"profit_factor":4.190184861108528,"total_return_pct":1.2441182731003355,"win_rate":0.7272727272727273,"max_drawdown_pct":0.22380328257556925,"sharpe_ratio":3.5956143566120704,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.918016321908289},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074932001383_1585063f","trade_count":220,"profit_factor":1.2595187982631553,"total_return_pct":3.200633818982489,"win_rate":0.5045454545454545,"max_drawdown_pct":1.389607178327899,"sharpe_ratio":0.6336391727317187,"monthly_win_rate":0.6551724137931034,"equity_curve_r_squared":0.39958317262208615}},"sqs_score":86.7,"sqs_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"better","verdict_reasoning":"Capping same-day shorts at -45% reaction preserved train and valid while modestly improving test return, PF, drawdown, and Sharpe versus step45.","next_direction":"Combine the crash cap with a quality filter on the same-day long overlay.","tags":["pead","midcap","step51","short","core","macro","block","crashcap"]} -{"entry_id":"IMP-0046","timestamp":"2026-03-17T07:54:28.401055+00:00","experiment_name":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","hypothesis":"The same-day long overlay may work better when restricted to larger reaction-day gap moves.","config_delta":{"base_experiment":"pead_midcap_step51_short_core_macro_block_crashcap","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123185892_7592b1dc","trade_count":25,"profit_factor":5.660464878695176,"total_return_pct":1.818984312375629,"win_rate":0.72,"max_drawdown_pct":0.21695852738272095,"sharpe_ratio":4.755652871139783,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6608034205824956},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123226004_7592b1dc","trade_count":22,"profit_factor":3.351800408128271,"total_return_pct":1.0111883417758072,"win_rate":0.7727272727272727,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":3.031867507475822,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8468990955221466},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075225071209_7592b1dc","trade_count":201,"profit_factor":1.3153549627707726,"total_return_pct":3.2988485556152156,"win_rate":0.4975124378109453,"max_drawdown_pct":1.5599163185209133,"sharpe_ratio":0.683194016870928,"monthly_win_rate":0.6923076923076923,"equity_curve_r_squared":0.7186047701175057}},"sqs_score":86.3,"sqs_breakdown":{"profitability":84.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"neutral","verdict_reasoning":"A 10% gap filter made train and valid much stronger but gave back some test performance, so this is a balanced alternative rather than a clear new leader.","next_direction":"If optimizing for robustness across splits, keep exploring overlay quality gates around this variant.","tags":["pead","midcap","step52","short","core","macro","block","crashcap","gap10"]} +{"entry_id":"IMP-0045","timestamp":"2026-03-17T07:54:28.043618+00:00","experiment_name":"pead_midcap_step51_short_core_macro_block_crashcap","hypothesis":"Extreme one-day crash continuations are too stretched for the same-day short sleeve and should be excluded.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081833666987_5039805d","trade_count":28,"profit_factor":2.3084684930008983,"total_return_pct":1.30245295115927,"win_rate":0.6785714285714286,"max_drawdown_pct":0.37472483014430374,"sharpe_ratio":3.0487382658615467,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48762905493056535,"avg_gross_exposure_pct":4.09489843643577,"avg_net_exposure_pct":-1.9273432556417505,"days_in_market_pct":57.89473684210527},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081834927778_5039805d","trade_count":22,"profit_factor":4.190184861108528,"total_return_pct":1.2441182731003355,"win_rate":0.7272727272727273,"max_drawdown_pct":0.22380328257556925,"sharpe_ratio":3.5956143566120704,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.918016321908289,"avg_gross_exposure_pct":2.2928950819181733,"avg_net_exposure_pct":-0.8189811228558067,"days_in_market_pct":42.5531914893617},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074932001383_1585063f","trade_count":220,"profit_factor":1.2595187982631553,"total_return_pct":3.200633818982489,"win_rate":0.5045454545454545,"max_drawdown_pct":1.389607178327899,"sharpe_ratio":0.6336391727317187,"monthly_win_rate":0.6551724137931034,"equity_curve_r_squared":0.39958317262208615}},"sqs_score":86.7,"sqs_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"better","verdict_reasoning":"Capping same-day shorts at -45% reaction preserved train and valid while modestly improving test return, PF, drawdown, and Sharpe versus step45.","next_direction":"Combine the crash cap with a quality filter on the same-day long overlay.","tags":["pead","midcap","step51","short","core","macro","block","crashcap"],"sqs_v2_score":89.6,"sqs_v2_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.7,"capital_efficiency":100.0}} +{"entry_id":"IMP-0046","timestamp":"2026-03-17T07:54:28.401055+00:00","experiment_name":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","hypothesis":"The same-day long overlay may work better when restricted to larger reaction-day gap moves.","config_delta":{"base_experiment":"pead_midcap_step51_short_core_macro_block_crashcap","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081834927839_7592b1dc","trade_count":25,"profit_factor":5.660464878695176,"total_return_pct":1.818984312375629,"win_rate":0.72,"max_drawdown_pct":0.21695852738272095,"sharpe_ratio":4.755652871139783,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6608034205824956,"avg_gross_exposure_pct":3.5838805580978437,"avg_net_exposure_pct":-2.593156236925373,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081833658080_7592b1dc","trade_count":22,"profit_factor":3.351800408128271,"total_return_pct":1.0111883417758072,"win_rate":0.7727272727272727,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":3.031867507475822,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8468990955221466,"avg_gross_exposure_pct":2.858350680305373,"avg_net_exposure_pct":-2.222595187645864,"days_in_market_pct":53.191489361702125},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075225071209_7592b1dc","trade_count":201,"profit_factor":1.3153549627707726,"total_return_pct":3.2988485556152156,"win_rate":0.4975124378109453,"max_drawdown_pct":1.5599163185209133,"sharpe_ratio":0.683194016870928,"monthly_win_rate":0.6923076923076923,"equity_curve_r_squared":0.7186047701175057}},"sqs_score":86.3,"sqs_breakdown":{"profitability":84.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"neutral","verdict_reasoning":"A 10% gap filter made train and valid much stronger but gave back some test performance, so this is a balanced alternative rather than a clear new leader.","next_direction":"If optimizing for robustness across splits, keep exploring overlay quality gates around this variant.","tags":["pead","midcap","step52","short","core","macro","block","crashcap","gap10"],"sqs_v2_score":87.2,"sqs_v2_breakdown":{"profitability":84.0,"risk":100.0,"consistency":95.8,"robustness":56.7,"capital_efficiency":79.5}} {"entry_id":"IMP-0047","timestamp":"2026-03-17T07:54:28.762757+00:00","experiment_name":"pead_midcap_step53_short_core_macro_block_crashcap_gap14","hypothesis":"A stricter same-day long gap filter might further concentrate the overlay into only the strongest continuation setups.","config_delta":{"base_experiment":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123208169_66d09993","trade_count":25,"profit_factor":8.207760543782888,"total_return_pct":2.0180349316014032,"win_rate":0.76,"max_drawdown_pct":0.2179161262122437,"sharpe_ratio":5.288977955005757,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6385560108285373},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123225576_66d09993","trade_count":19,"profit_factor":4.599773676047558,"total_return_pct":1.1191525844285641,"win_rate":0.8421052631578947,"max_drawdown_pct":0.21947031826165894,"sharpe_ratio":3.463700507402551,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8852420626052896}},"sqs_score":43.1,"sqs_breakdown":{"profitability":84.5,"risk":100.0,"consistency":95.8,"robustness":55.0},"verdict":"worse","verdict_reasoning":"The stricter gap filter over-concentrated the overlay, dropped total trade count below a healthy level, and cratered test SQS.","next_direction":"Use moderate overlay filters only; the strict version is too sparse.","tags":["pead","midcap","step53","short","core","macro","block","crashcap","gap14"]} diff --git a/libs/backtest/domain.py b/libs/backtest/domain.py index 707660d..673aecd 100644 --- a/libs/backtest/domain.py +++ b/libs/backtest/domain.py @@ -165,6 +165,9 @@ class MetricsBundle(BaseModel): sortino_ratio: float | None = None avg_daily_pnl: float | None = None avg_positions_held: float | None = None + avg_gross_exposure_pct: float | None = None + avg_net_exposure_pct: float | None = None + days_in_market_pct: float | None = None # Stability metrics (4) trade_skewness: float | None = None @@ -364,6 +367,40 @@ class SQSWeights(BaseModel): low_trade_penalty_factor: float = 0.5 +class SQSv2Weights(BaseModel): + """Weights for Strategy Quality Score v2 with capital efficiency.""" + + profitability: float = 0.35 + risk: float = 0.25 + consistency: float = 0.20 + robustness: float = 0.10 + capital_efficiency: float = 0.10 + low_trade_penalty_threshold: int = 20 + low_trade_penalty_factor: float = 0.5 + + +class PromotionScoreWeights(BaseModel): + """Weights for promotion scoring across valid/test splits.""" + + valid_quality: float = 0.55 + test_quality: float = 0.15 + floor_quality: float = 0.30 + + +class UnifiedScoreWeights(BaseModel): + """Weights for a stricter single ranking score across valid/test splits.""" + + split_profitability: float = 0.25 + split_risk: float = 0.20 + split_consistency: float = 0.15 + split_robustness: float = 0.20 + split_capital_efficiency: float = 0.20 + valid_quality: float = 0.45 + test_quality: float = 0.20 + floor_quality: float = 0.20 + gap_quality: float = 0.15 + + class SplitResult(BaseModel): """Metrics for a single backtest split (train/valid/test).""" @@ -376,6 +413,9 @@ class SplitResult(BaseModel): sharpe_ratio: float | None = None monthly_win_rate: float | None = None equity_curve_r_squared: float | None = None + avg_gross_exposure_pct: float | None = None + avg_net_exposure_pct: float | None = None + days_in_market_pct: float | None = None class ConfigDelta(BaseModel): @@ -396,6 +436,12 @@ class JournalEntry(BaseModel): results: dict[str, SplitResult] = Field(default_factory=dict) # split_name → SplitResult sqs_score: float | None = None sqs_breakdown: dict[str, float] = Field(default_factory=dict) + sqs_v2_score: float | None = None + sqs_v2_breakdown: dict[str, float] = Field(default_factory=dict) + promotion_score: float | None = None + promotion_breakdown: dict[str, float] = Field(default_factory=dict) + unified_score: float | None = None + unified_breakdown: dict[str, float] = Field(default_factory=dict) verdict: str = "unknown" # better / worse / neutral / unknown verdict_reasoning: str = "" next_direction: str = "" @@ -408,12 +454,29 @@ class RegistryEntry(BaseModel): entry_id: str experiment_name: str sqs_score: float + sqs_v2_score: float | None = None + promotion_score: float | None = None + unified_score: float | None = None + # test split metrics profit_factor: float | None = None total_return_pct: float | None = None win_rate: float | None = None sharpe_ratio: float | None = None max_drawdown_pct: float | None = None trade_count: int = 0 + avg_gross_exposure_pct: float | None = None + avg_net_exposure_pct: float | None = None + days_in_market_pct: float | None = None + # valid split metrics + valid_profit_factor: float | None = None + valid_total_return_pct: float | None = None + valid_win_rate: float | None = None + valid_sharpe_ratio: float | None = None + valid_max_drawdown_pct: float | None = None + valid_trade_count: int = 0 + valid_avg_gross_exposure_pct: float | None = None + valid_avg_net_exposure_pct: float | None = None + valid_days_in_market_pct: float | None = None timestamp: str = "" diff --git a/libs/backtest/metrics.py b/libs/backtest/metrics.py index e012a99..5597f8a 100644 --- a/libs/backtest/metrics.py +++ b/libs/backtest/metrics.py @@ -180,6 +180,45 @@ def compute_avg_positions_held(equity_curve: list[DailyPortfolioState]) -> float return statistics.mean(len(s.open_positions) for s in equity_curve) +def compute_avg_gross_exposure_pct( + equity_curve: list[DailyPortfolioState], +) -> float | None: + if not equity_curve: + return None + exposure_pcts = [ + (state.gross_exposure / state.equity) * 100.0 + for state in equity_curve + if state.equity > 0 + ] + if not exposure_pcts: + return None + return statistics.mean(exposure_pcts) + + +def compute_avg_net_exposure_pct( + equity_curve: list[DailyPortfolioState], +) -> float | None: + if not equity_curve: + return None + exposure_pcts = [ + (state.net_exposure / state.equity) * 100.0 + for state in equity_curve + if state.equity > 0 + ] + if not exposure_pcts: + return None + return statistics.mean(exposure_pcts) + + +def compute_days_in_market_pct( + equity_curve: list[DailyPortfolioState], +) -> float | None: + if not equity_curve: + return None + days_in_market = sum(1 for state in equity_curve if state.gross_exposure > 0) + return days_in_market / len(equity_curve) * 100.0 + + # --------------------------------------------------------------------------- # Stability metrics (4) # --------------------------------------------------------------------------- @@ -409,6 +448,9 @@ def build_metrics_bundle( sortino_ratio=compute_sortino_ratio(equity_curve), avg_daily_pnl=compute_avg_daily_pnl(equity_curve), avg_positions_held=compute_avg_positions_held(equity_curve), + avg_gross_exposure_pct=compute_avg_gross_exposure_pct(equity_curve), + avg_net_exposure_pct=compute_avg_net_exposure_pct(equity_curve), + days_in_market_pct=compute_days_in_market_pct(equity_curve), # Stability trade_skewness=compute_trade_skewness(trades), trade_kurtosis=compute_trade_kurtosis(trades), diff --git a/libs/backtest/tracker.py b/libs/backtest/tracker.py index 7e1b574..767d8af 100644 --- a/libs/backtest/tracker.py +++ b/libs/backtest/tracker.py @@ -1,6 +1,7 @@ """Strategy improvement tracker: SQS computation, journal I/O, leaderboard.""" from __future__ import annotations +import functools import json from pathlib import Path from typing import Any @@ -10,9 +11,12 @@ from libs.backtest.domain import ( ExperimentRegistry, JournalEntry, MetricsBundle, + PromotionScoreWeights, RegistryEntry, SplitResult, SQSWeights, + SQSv2Weights, + UnifiedScoreWeights, ) from libs.common.logging import get_logger from libs.common.time_utils import utc_now @@ -20,6 +24,9 @@ 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() # --------------------------------------------------------------------------- @@ -49,6 +56,39 @@ def _normalize_inverse(value: float | None, low: float, high: float) -> float: 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 _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, @@ -101,6 +141,67 @@ def compute_sqs( 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 # --------------------------------------------------------------------------- @@ -118,9 +219,248 @@ def build_split_result(split_name: str, run_id: str, metrics: MetricsBundle) -> 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) + + +@functools.lru_cache(maxsize=512) +def _load_run_metrics_summary(run_id: str) -> dict[str, Any] | None: + metrics_path = Path("runs") / run_id / "metrics" / "metrics_summary.json" + if not metrics_path.exists(): + return None + summary = json.loads(metrics_path.read_text()) + needed_fields = ( + "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 = Path("runs") / run_id / "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 = ( + "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( + test_result: SplitResult | None, + valid_result: SplitResult | None, +) -> tuple[float | None, dict[str, float], str | None]: + """Return the public-facing SQS. + + Prefer the stricter integrated score when both valid/test are available. + Fall back to the best available single-split quality score otherwise, + using the same harsher calibration band. + """ + integrated_score, integrated_breakdown = compute_unified_score(test_result, valid_result) + if integrated_score is not None: + return integrated_score, integrated_breakdown, "integrated" + + split_score, split_breakdown, split_source = _compute_split_quality_score(test_result) + if split_score is not None: + return _apply_single_split_penalty(_calibrate_sqs(split_score)), split_breakdown, split_source + + return None, {}, None + + def compute_config_delta( current: dict[str, Any], baseline: dict[str, Any], @@ -195,24 +535,58 @@ def rebuild_registry( registry_entries: list[RegistryEntry] = [] for je in entries: - test_result = je.results.get("test") + test_result = _hydrate_split_result(je.results.get("test")) + valid_result = _hydrate_split_result(je.results.get("valid")) + computed_sqs_v2 = None + 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_public_sqs, _, _ = compute_public_sqs(test_result, valid_result) + canonical_sqs = computed_public_sqs if computed_public_sqs is not None else je.sqs_score or 0.0 registry_entries.append( RegistryEntry( entry_id=je.entry_id, experiment_name=je.experiment_name, - sqs_score=je.sqs_score or 0.0, + sqs_score=canonical_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 + ), profit_factor=test_result.profit_factor if test_result else None, total_return_pct=test_result.total_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_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, ) ) - # Sort by SQS descending - registry_entries.sort(key=lambda e: e.sqs_score, reverse=True) + # Sort by canonical SQS descending, then promotion. + registry_entries.sort( + key=lambda e: ( + -(e.sqs_score or 0.0), + e.promotion_score is None, + -(e.promotion_score or 0.0), + ) + ) registry = ExperimentRegistry( entries=registry_entries, @@ -238,8 +612,8 @@ def _write_leaderboard_md( lines: list[str] = [] lines.append("# Strategy Improvement Leaderboard") lines.append(f"_Updated: {registry.updated_at}_\n") - lines.append("| # | Experiment | SQS | PF | Ret% | WR | Sharpe | DD% | Trades | Date |") - lines.append("|---|-----------|-----|-----|------|-----|--------|-----|--------|------|") + lines.append("| # | Experiment | SQS | [T]PF | [T]Ret% | [T]WR | [T]Sharpe | [T]DD% | [T]N | [T]Gross% | [T]Net% | [T]DIM% | [V]PF | [V]Ret% | [V]WR | [V]Sharpe | [V]DD% | [V]N | [V]Gross% | [V]Net% | [V]DIM% | Date |") + lines.append("|---|-----------|-----|-------|---------|-------|-----------|--------|------|-----------|---------|---------|-------|---------|-------|-----------|--------|------|-----------|---------|---------|------|") for rank, e in enumerate(registry.entries, 1): pf = f"{e.profit_factor:.2f}" if e.profit_factor is not None else "-" @@ -247,19 +621,35 @@ def _write_leaderboard_md( wr = f"{e.win_rate:.0%}" if e.win_rate is not None else "-" sharpe = f"{e.sharpe_ratio:.1f}" if e.sharpe_ratio is not None else "-" dd = f"{e.max_drawdown_pct:.1f}" if e.max_drawdown_pct is not None else "-" + tgross = f"{e.avg_gross_exposure_pct:.1f}" if e.avg_gross_exposure_pct is not None else "-" + tnet = f"{e.avg_net_exposure_pct:+.1f}" if e.avg_net_exposure_pct is not None else "-" + tdim = f"{e.days_in_market_pct:.1f}" if e.days_in_market_pct is not None else "-" + vpf = f"{e.valid_profit_factor:.2f}" if e.valid_profit_factor is not None else "-" + vret = f"{e.valid_total_return_pct:+.1f}" if e.valid_total_return_pct is not None else "-" + vwr = f"{e.valid_win_rate:.0%}" if e.valid_win_rate is not None else "-" + vsharpe = f"{e.valid_sharpe_ratio:.1f}" if e.valid_sharpe_ratio is not None else "-" + vdd = f"{e.valid_max_drawdown_pct:.1f}" if e.valid_max_drawdown_pct is not None else "-" + vgross = f"{e.valid_avg_gross_exposure_pct:.1f}" if e.valid_avg_gross_exposure_pct is not None else "-" + vnet = f"{e.valid_avg_net_exposure_pct:+.1f}" if e.valid_avg_net_exposure_pct is not None else "-" + vdim = f"{e.valid_days_in_market_pct:.1f}" if e.valid_days_in_market_pct is not None else "-" ts = e.timestamp[:10] if e.timestamp else "-" lines.append( - f"| {rank} | {e.experiment_name} | {e.sqs_score:.1f} | {pf} | {ret} | {wr} | {sharpe} | {dd} | {e.trade_count} | {ts} |" + f"| {rank} | {e.experiment_name} | {e.sqs_score:.1f}" + f" | {pf} | {ret} | {wr} | {sharpe} | {dd} | {e.trade_count} | {tgross} | {tnet} | {tdim}" + f" | {vpf} | {vret} | {vwr} | {vsharpe} | {vdd} | {e.valid_trade_count} | {vgross} | {vnet} | {vdim}" + f" | {ts} |" ) # Recent entries (last 5) recent = list(reversed(journal_entries))[:5] if recent: + registry_by_id = {entry.entry_id: entry for entry in registry.entries} lines.append("\n## Recent Entries") for je in recent: + canonical_sqs = registry_by_id.get(je.entry_id).sqs_score if je.entry_id in registry_by_id else je.sqs_score lines.append(f"### {je.entry_id} ({je.timestamp[:10]}) \u2014 {je.experiment_name}") lines.append(f"Hypothesis: {je.hypothesis}") - lines.append(f"Verdict: **{je.verdict.upper()}** (SQS {je.sqs_score})") + lines.append(f"Verdict: **{je.verdict.upper()}** (SQS {canonical_sqs})") if je.verdict_reasoning: lines.append(f"Reasoning: {je.verdict_reasoning}") if je.next_direction: diff --git a/pyproject.toml b/pyproject.toml index 2a70838..6294684 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,9 @@ dev = [ "types-beautifulsoup4", ] +[project.scripts] +fithia2 = "apps.tracker.cli:main" + [tool.hatch.build.targets.wheel] packages = ["apps", "libs"] diff --git a/tests/integration/backtest/test_backtest_run.py b/tests/integration/backtest/test_backtest_run.py index 48e5bb2..fcd0b7c 100644 --- a/tests/integration/backtest/test_backtest_run.py +++ b/tests/integration/backtest/test_backtest_run.py @@ -323,6 +323,7 @@ class TestBacktestRunIntegration: def test_multi_engine_run_writes_per_engine_metrics(self, tmp_path): from apps.backtester.run import BacktestRunner from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig + import json import pyarrow.parquet as pq store = _build_multi_engine_store() @@ -382,3 +383,11 @@ class TestBacktestRunIntegration: trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist() engine_ids = {row["engine_id"] for row in trade_blotter} assert "earnings_after_close_short_v1" not in engine_ids + + equity_curve = pq.read_table(run_dir / "artifacts" / "daily_equity_curve.parquet").to_pylist() + assert any(float(row["net_exposure"]) < 0 for row in equity_curve if float(row["gross_exposure"]) > 0) + + metrics_summary = json.loads((run_dir / "metrics" / "metrics_summary.json").read_text()) + assert "avg_gross_exposure_pct" in metrics_summary + assert "avg_net_exposure_pct" in metrics_summary + assert "days_in_market_pct" in metrics_summary diff --git a/tests/unit/backtest/test_metrics.py b/tests/unit/backtest/test_metrics.py index 056b52e..57294fc 100644 --- a/tests/unit/backtest/test_metrics.py +++ b/tests/unit/backtest/test_metrics.py @@ -38,13 +38,19 @@ def _make_trade( ) -def _make_equity_state(date: dt.date, equity: float, n_positions: int = 0) -> DailyPortfolioState: +def _make_equity_state( + date: dt.date, + equity: float, + n_positions: int = 0, + gross_exposure: float = 0.0, + net_exposure: float = 0.0, +) -> DailyPortfolioState: return DailyPortfolioState( date=date, equity=equity, cash_available=equity, - gross_exposure=0.0, - net_exposure=0.0, + gross_exposure=gross_exposure, + net_exposure=net_exposure, reserved_risk_budget=0.0, unrealized_pnl=0.0, realized_pnl=0.0, @@ -168,6 +174,25 @@ class TestSharpeRatio: assert compute_sharpe_ratio(curve) is None +class TestExposureMetrics: + def test_avg_gross_and_net_exposure_pct(self): + from libs.backtest.metrics import ( + compute_avg_gross_exposure_pct, + compute_avg_net_exposure_pct, + compute_days_in_market_pct, + ) + + curve = [ + _make_equity_state(dt.date(2026, 1, 5), 100_000, gross_exposure=0.0, net_exposure=0.0), + _make_equity_state(dt.date(2026, 1, 6), 100_000, gross_exposure=20_000.0, net_exposure=-20_000.0), + _make_equity_state(dt.date(2026, 1, 7), 100_000, gross_exposure=10_000.0, net_exposure=5_000.0), + ] + + assert compute_avg_gross_exposure_pct(curve) == pytest.approx(10.0) + assert compute_avg_net_exposure_pct(curve) == pytest.approx(-5.0) + assert compute_days_in_market_pct(curve) == pytest.approx(66.6666666667) + + class TestStopExitRate: def test_basic(self): from libs.backtest.metrics import compute_stop_exit_rate @@ -218,6 +243,23 @@ class TestBuildMetricsBundle: assert m.trade_count == 2 assert m.win_rate == pytest.approx(0.5) assert m.total_return_pct == pytest.approx(5.0) + assert m.avg_gross_exposure_pct == pytest.approx(0.0) + assert m.avg_net_exposure_pct == pytest.approx(0.0) + assert m.days_in_market_pct == pytest.approx(0.0) + + def test_builds_exposure_metrics(self): + from libs.backtest.metrics import build_metrics_bundle + + curve = [ + _make_equity_state(dt.date(2026, 1, 5), 100_000, gross_exposure=0.0, net_exposure=0.0), + _make_equity_state(dt.date(2026, 1, 6), 100_000, gross_exposure=30_000.0, net_exposure=-10_000.0), + _make_equity_state(dt.date(2026, 1, 7), 100_000, gross_exposure=10_000.0, net_exposure=10_000.0), + ] + + m = build_metrics_bundle([], curve) + assert m.avg_gross_exposure_pct == pytest.approx(13.3333333333) + assert m.avg_net_exposure_pct == pytest.approx(0.0) + assert m.days_in_market_pct == pytest.approx(66.6666666667) def test_empty_trades(self): from libs.backtest.metrics import build_metrics_bundle diff --git a/tests/unit/backtest/test_tracker.py b/tests/unit/backtest/test_tracker.py index b49dda8..3e743ab 100644 --- a/tests/unit/backtest/test_tracker.py +++ b/tests/unit/backtest/test_tracker.py @@ -14,11 +14,17 @@ from libs.backtest.domain import ( ) from libs.backtest.tracker import ( _normalize, + _normalize_band, _normalize_inverse, append_journal_entry, build_split_result, check_duplicate, + compute_public_sqs, + compute_promotion_score, compute_sqs, + compute_sqs_v2, + compute_unified_score, + compute_unified_split_quality, get_next_entry_id, load_journal, rebuild_registry, @@ -66,6 +72,18 @@ class TestNormalizeInverse: assert _normalize_inverse(None, low=10.0, high=1.0) == 0.0 +class TestNormalizeBand: + def test_band_plateau_scores_max(self): + assert _normalize_band(60.0, low_bad=10.0, low_good=40.0, high_good=80.0, high_bad=100.0) == 100.0 + + def test_below_band_ramps_up(self): + assert _normalize_band(25.0, low_bad=10.0, low_good=40.0, high_good=80.0, high_bad=100.0) == pytest.approx(50.0) + + def test_outside_band_scores_zero(self): + assert _normalize_band(5.0, low_bad=10.0, low_good=40.0, high_good=80.0, high_bad=100.0) == 0.0 + assert _normalize_band(100.0, low_bad=10.0, low_good=40.0, high_good=80.0, high_bad=100.0) == 0.0 + + # --------------------------------------------------------------------------- # compute_sqs # --------------------------------------------------------------------------- @@ -162,6 +180,257 @@ class TestComputeSQS: assert sqs_profit > sqs_risk +class TestComputeSQSv2: + def test_missing_exposure_returns_none(self): + m = MetricsBundle( + trade_count=50, + profit_factor=1.5, + total_return_pct=2.0, + max_drawdown_pct=2.0, + sharpe_ratio=1.2, + win_rate=0.55, + monthly_win_rate=0.60, + equity_curve_r_squared=0.50, + ) + sqs_v2, breakdown = compute_sqs_v2(m) + assert sqs_v2 is None + assert breakdown == {} + + def test_capital_efficiency_can_break_tie(self): + base = dict( + trade_count=50, + profit_factor=1.5, + total_return_pct=2.0, + max_drawdown_pct=2.0, + sharpe_ratio=1.2, + win_rate=0.55, + monthly_win_rate=0.60, + equity_curve_r_squared=0.50, + days_in_market_pct=60.0, + ) + efficient = MetricsBundle( + **base, + avg_gross_exposure_pct=4.0, + avg_net_exposure_pct=-1.0, + ) + inefficient = MetricsBundle( + **base, + avg_gross_exposure_pct=10.0, + avg_net_exposure_pct=-1.0, + ) + efficient_score, _ = compute_sqs_v2(efficient) + inefficient_score, _ = compute_sqs_v2(inefficient) + assert efficient_score is not None + assert inefficient_score is not None + assert efficient_score > inefficient_score + + +class TestComputePromotionScore: + def test_requires_valid_and_test(self): + score, breakdown = compute_promotion_score(None, None) + assert score is None + assert breakdown == {} + + def test_penalizes_test_only_outperformance(self): + overfit_test = SplitResult( + run_id="bt_test", + trade_count=56, + profit_factor=2.0, + total_return_pct=2.2, + win_rate=0.61, + max_drawdown_pct=0.4, + sharpe_ratio=4.2, + monthly_win_rate=1.0, + equity_curve_r_squared=0.88, + avg_gross_exposure_pct=6.2, + avg_net_exposure_pct=1.1, + days_in_market_pct=76.6, + ) + overfit_valid = SplitResult( + run_id="bt_valid", + trade_count=53, + profit_factor=1.24, + total_return_pct=0.7, + win_rate=0.51, + max_drawdown_pct=0.9, + sharpe_ratio=1.2, + monthly_win_rate=0.75, + equity_curve_r_squared=0.15, + avg_gross_exposure_pct=7.5, + avg_net_exposure_pct=1.6, + days_in_market_pct=77.2, + ) + robust_test = SplitResult( + run_id="bt_test_robust", + trade_count=22, + profit_factor=4.19, + total_return_pct=1.24, + win_rate=0.73, + max_drawdown_pct=0.22, + sharpe_ratio=3.6, + monthly_win_rate=0.67, + equity_curve_r_squared=0.92, + avg_gross_exposure_pct=2.3, + avg_net_exposure_pct=-0.8, + days_in_market_pct=42.6, + ) + robust_valid = SplitResult( + run_id="bt_valid_robust", + trade_count=28, + profit_factor=2.31, + total_return_pct=1.3, + win_rate=0.68, + max_drawdown_pct=0.37, + sharpe_ratio=3.05, + monthly_win_rate=0.75, + equity_curve_r_squared=0.49, + avg_gross_exposure_pct=4.1, + avg_net_exposure_pct=-1.9, + days_in_market_pct=57.9, + ) + + overfit_score, overfit_breakdown = compute_promotion_score(overfit_test, overfit_valid) + robust_score, robust_breakdown = compute_promotion_score(robust_test, robust_valid) + assert overfit_score is not None + assert robust_score is not None + assert robust_score > overfit_score + assert overfit_breakdown["floor_quality"] < overfit_breakdown["test_quality"] + + +class TestComputeUnifiedScore: + def test_requires_valid_and_test(self): + score, breakdown = compute_unified_score(None, None) + assert score is None + assert breakdown == {} + + def test_split_quality_rewards_efficiency(self): + efficient = MetricsBundle( + trade_count=30, + profit_factor=2.0, + total_return_pct=1.2, + max_drawdown_pct=0.4, + sharpe_ratio=2.5, + win_rate=0.60, + monthly_win_rate=0.75, + equity_curve_r_squared=0.70, + avg_gross_exposure_pct=3.0, + avg_net_exposure_pct=-1.0, + days_in_market_pct=55.0, + ) + inefficient = efficient.model_copy( + update={"avg_gross_exposure_pct": 8.0, "days_in_market_pct": 85.0} + ) + efficient_score, _ = compute_unified_split_quality(efficient) + inefficient_score, _ = compute_unified_split_quality(inefficient) + assert efficient_score is not None + assert inefficient_score is not None + assert efficient_score > inefficient_score + + def test_overfit_strategy_scores_below_robust_strategy(self): + overfit_test = SplitResult( + run_id="bt_step35_test", + trade_count=56, + profit_factor=2.009, + total_return_pct=2.181, + win_rate=0.607, + max_drawdown_pct=0.442, + sharpe_ratio=4.217, + monthly_win_rate=1.0, + equity_curve_r_squared=0.889, + avg_gross_exposure_pct=6.228, + avg_net_exposure_pct=1.126, + days_in_market_pct=76.6, + ) + overfit_valid = SplitResult( + run_id="bt_step35_valid", + trade_count=53, + profit_factor=1.243, + total_return_pct=0.708, + win_rate=0.509, + max_drawdown_pct=0.885, + sharpe_ratio=1.180, + monthly_win_rate=0.75, + equity_curve_r_squared=0.146, + avg_gross_exposure_pct=7.511, + avg_net_exposure_pct=1.567, + days_in_market_pct=77.2, + ) + robust_test = SplitResult( + run_id="bt_step52_test", + trade_count=22, + profit_factor=3.352, + total_return_pct=1.011, + win_rate=0.773, + max_drawdown_pct=0.243, + sharpe_ratio=3.032, + monthly_win_rate=0.667, + equity_curve_r_squared=0.847, + avg_gross_exposure_pct=2.858, + avg_net_exposure_pct=-2.223, + days_in_market_pct=53.2, + ) + robust_valid = SplitResult( + run_id="bt_step52_valid", + trade_count=25, + profit_factor=5.660, + total_return_pct=1.819, + win_rate=0.72, + max_drawdown_pct=0.217, + sharpe_ratio=4.756, + monthly_win_rate=1.0, + equity_curve_r_squared=0.661, + avg_gross_exposure_pct=3.584, + avg_net_exposure_pct=-2.593, + days_in_market_pct=56.1, + ) + + overfit_score, overfit_breakdown = compute_unified_score(overfit_test, overfit_valid) + robust_score, robust_breakdown = compute_unified_score(robust_test, robust_valid) + assert overfit_score is not None + assert robust_score is not None + assert robust_score > overfit_score + assert overfit_breakdown["gap_quality"] < robust_breakdown["gap_quality"] + + +class TestComputePublicSQS: + def test_public_score_prefers_integrated_and_is_harsher(self): + test_result = SplitResult( + run_id="bt_test", + trade_count=22, + profit_factor=3.352, + total_return_pct=1.011, + win_rate=0.773, + max_drawdown_pct=0.243, + sharpe_ratio=3.032, + monthly_win_rate=0.667, + equity_curve_r_squared=0.847, + avg_gross_exposure_pct=2.858, + avg_net_exposure_pct=-2.223, + days_in_market_pct=53.2, + ) + valid_result = SplitResult( + run_id="bt_valid", + trade_count=25, + profit_factor=5.660, + total_return_pct=1.819, + win_rate=0.72, + max_drawdown_pct=0.217, + sharpe_ratio=4.756, + monthly_win_rate=1.0, + equity_curve_r_squared=0.661, + avg_gross_exposure_pct=3.584, + avg_net_exposure_pct=-2.593, + days_in_market_pct=56.1, + ) + + public_score, _, source = compute_public_sqs(test_result, valid_result) + raw_integrated_score, _ = compute_unified_score(test_result, valid_result) + assert source == "integrated" + assert public_score == raw_integrated_score + assert public_score is not None + assert public_score < 60.0 + + # --------------------------------------------------------------------------- # build_split_result # --------------------------------------------------------------------------- @@ -177,12 +446,18 @@ def test_build_split_result(): sharpe_ratio=0.8, monthly_win_rate=0.60, equity_curve_r_squared=0.40, + avg_gross_exposure_pct=18.5, + avg_net_exposure_pct=-6.5, + days_in_market_pct=27.0, ) sr = build_split_result("test", "bt_run123", m) assert sr.run_id == "bt_run123" assert sr.trade_count == 50 assert sr.profit_factor == 1.2 assert sr.total_return_pct == 2.5 + assert sr.avg_gross_exposure_pct == 18.5 + assert sr.avg_net_exposure_pct == -6.5 + assert sr.days_in_market_pct == 27.0 # --------------------------------------------------------------------------- @@ -298,6 +573,11 @@ class TestRebuildRegistry: profit_factor=1.0 + i * 0.2, total_return_pct=float(i), win_rate=0.5, + monthly_win_rate=0.6, + equity_curve_r_squared=0.5, + avg_gross_exposure_pct=5.0, + avg_net_exposure_pct=-1.0, + days_in_market_pct=60.0, ) entry = JournalEntry( entry_id=f"IMP-{i+1:04d}", @@ -305,6 +585,8 @@ class TestRebuildRegistry: experiment_name=name, hypothesis=f"h{i}", sqs_score=sqs, + promotion_score=sqs - 5, + unified_score=sqs - 10, results={"test": test_result}, verdict="better" if sqs > 50 else "worse", ) @@ -312,11 +594,13 @@ class TestRebuildRegistry: registry = rebuild_registry(journal_path, registry_path, leaderboard_path) - # Sorted by SQS descending + # Sorted by public SQS descending assert len(registry.entries) == 3 - assert registry.entries[0].experiment_name == "exp_high" - assert registry.entries[0].sqs_score == 70.0 + assert registry.entries[0].experiment_name == "exp_mid" assert registry.entries[2].experiment_name == "exp_low" + assert registry.entries[0].sqs_v2_score is not None + assert registry.entries[0].promotion_score is not None + assert registry.entries[0].sqs_score < 70.0 # Files exist assert registry_path.exists() @@ -327,3 +611,5 @@ class TestRebuildRegistry: assert "exp_high" in lb_text assert "exp_low" in lb_text assert "| # |" in lb_text + assert "| # | Experiment | SQS |" in lb_text + assert "[T]Gross%" in lb_text