Recalibrate public SQS and exposure-aware tracking

main
I Luk Kim 5 months ago
parent 9ec0b26e10
commit c646303423

@ -491,41 +491,50 @@ journal/
``` ```
1. 실험 config 생성 configs/experiments/my_experiment.json 1. 실험 config 생성 configs/experiments/my_experiment.json
2. 3-split 백테스트 실행 for split in train valid test; do ... done 2. 3-split 백테스트 실행 for split in train valid test; do ... done
3. Journal에 기록 python -m apps.tracker.cli record ... 3. Journal에 기록 fithia2 rec -e my_experiment ...
4. Leaderboard 확인 python -m apps.tracker.cli leaderboard ... 4. Leaderboard 확인 fithia2 lb
5. 다음 실험 계획 verdict + SQS breakdown 기반 5. 다음 실험 계획 verdict + SQS breakdown 기반
6. 중복 실험 확인 python -m apps.tracker.cli check-duplicate ... 6. 중복 실험 확인 fithia2 dup -e my_experiment
7. 1번부터 반복 7. 1번부터 반복
``` ```
### 8.5 Tracker CLI ### 8.5 Tracker CLI
`pip install -e .``fithia2` 명령어로 실행. `--journal-dir` / `--runs-dir` 생략 시 기본값(`journal/`, `runs/`) 사용.
```bash ```bash
# 도움말
fithia2
# 리더보드 출력 (top 10)
fithia2 lb
# 리더보드 — 상위 N개
fithia2 lb -n 20
# 실험 결과 기록 # 실험 결과 기록
python -m apps.tracker.cli record \ fithia2 rec \
--journal-dir journal/ \ -e pead_midcap_step14_score65 \
--runs-dir runs/midcap_steps/ \ -H "Score threshold 0.60→0.65" \
--experiment pead_midcap_step14_score65 \ -b pead_midcap_step13_best \
--hypothesis "Score threshold 0.60→0.65" \ -v better \
--baseline pead_midcap_step13_best \ -r "Test SQS 64.2 > 57.7, Return +0.73% > +0.43%" \
--verdict better \ -n "Test exit tuning on top of score 0.65"
--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/
# 엔트리 상세 조회 # 엔트리 상세 조회
python -m apps.tracker.cli show --journal-dir journal/ IMP-0015 fithia2 s IMP-0015
# 중복 실험 확인 # 중복 실험 확인
python -m apps.tracker.cli check-duplicate \ fithia2 dup -e my_experiment_name
--journal-dir journal/ --experiment my_experiment_name
# 레지스트리 재생성
python -m apps.tracker.cli rebuild-registry --journal-dir journal/
``` ```
| 명령 | alias | 설명 |
|------|-------|------|
| `leaderboard` | `lb` | SQS 순위표 출력 및 LEADERBOARD.md 재생성 |
| `record` | `rec` | 실험 결과를 저널에 기록 |
| `show` | `s` | 특정 저널 항목 상세 조회 |
| `check-duplicate` | `dup` | 동일 실험명 중복 여부 확인 |
### 8.6 핵심 발견사항 ### 8.6 핵심 발견사항
21개 실험을 통해 얻은 인사이트: 21개 실험을 통해 얻은 인사이트:
@ -695,18 +704,16 @@ done
```bash ```bash
# Journal에 기록 # Journal에 기록
python -m apps.tracker.cli record \ fithia2 rec \
--journal-dir journal/ \ -e pead_midcap_step14_score65 \
--runs-dir runs/midcap_steps/ \ -H "Score threshold 0.60→0.65" \
--experiment pead_midcap_step14_score65 \ -b pead_midcap_step13_best \
--hypothesis "Score threshold 0.60→0.65" \ -v better \
--baseline pead_midcap_step13_best \ -r "Test SQS 64.2 > 57.7" \
--verdict better \ -n "Exit tuning on top of score 0.65"
--reasoning "Test SQS 64.2 > 57.7" \
--next "Exit tuning on top of score 0.65"
# 리더보드 확인 # 리더보드 확인
python -m apps.tracker.cli leaderboard --journal-dir journal/ fithia2 lb
``` ```
### 테스트 ### 테스트

@ -93,6 +93,22 @@ class BacktestRunner:
self._kill_switch_cooldown_remaining = 0 self._kill_switch_cooldown_remaining = 0
self._engine_daily_new_risk_used: dict[str, float] = defaultdict(float) 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: def run(self, output_root: str | Path | None = None) -> ExperimentResult:
"""Execute the full simulation. Returns ExperimentResult.""" """Execute the full simulation. Returns ExperimentResult."""
started_at = utc_now() started_at = utc_now()
@ -369,17 +385,14 @@ class BacktestRunner:
if self._peak_equity > 0 if self._peak_equity > 0
else 0.0 else 0.0
) )
gross_exposure, net_exposure = self._compute_portfolio_exposure(date)
self._equity_curve.append( self._equity_curve.append(
DailyPortfolioState( DailyPortfolioState(
date=date, date=date,
equity=self._equity, equity=self._equity,
cash_available=self._cash, cash_available=self._cash,
gross_exposure=sum( gross_exposure=gross_exposure,
p.entry_price * p.shares_open for p in self._open_positions net_exposure=net_exposure,
),
net_exposure=sum(
p.entry_price * p.shares_open for p in self._open_positions
),
reserved_risk_budget=self._daily_new_risk_used, reserved_risk_budget=self._daily_new_risk_used,
unrealized_pnl=unrealized_final, unrealized_pnl=unrealized_final,
realized_pnl=self._realized_pnl, realized_pnl=self._realized_pnl,
@ -563,16 +576,13 @@ class BacktestRunner:
drawdown_pct: float, drawdown_pct: float,
unrealized: float, unrealized: float,
) -> DailyPortfolioState: ) -> DailyPortfolioState:
gross_exposure, net_exposure = self._compute_portfolio_exposure(date)
return DailyPortfolioState( return DailyPortfolioState(
date=date, date=date,
equity=self._equity, equity=self._equity,
cash_available=self._cash, cash_available=self._cash,
gross_exposure=sum( gross_exposure=gross_exposure,
p.entry_price * p.shares_open for p in self._open_positions net_exposure=net_exposure,
),
net_exposure=sum(
p.entry_price * p.shares_open for p in self._open_positions
),
reserved_risk_budget=self._daily_new_risk_used, reserved_risk_budget=self._daily_new_risk_used,
unrealized_pnl=unrealized, unrealized_pnl=unrealized,
realized_pnl=self._realized_pnl, realized_pnl=self._realized_pnl,

@ -5,6 +5,14 @@ import argparse
import sys import sys
from pathlib import Path 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 ( from libs.backtest.domain import (
ConfigDelta, ConfigDelta,
JournalEntry, JournalEntry,
@ -15,7 +23,11 @@ from libs.backtest.tracker import (
append_journal_entry, append_journal_entry,
build_split_result, build_split_result,
check_duplicate, check_duplicate,
compute_public_sqs,
compute_promotion_score,
compute_sqs, compute_sqs,
compute_sqs_v2,
compute_unified_score,
get_next_entry_id, get_next_entry_id,
load_journal, load_journal,
rebuild_registry, rebuild_registry,
@ -54,17 +66,41 @@ def cmd_record(args: argparse.Namespace) -> None:
for split_name, (run_id, metrics) in split_runs.items(): for split_name, (run_id, metrics) in split_runs.items():
results[split_name] = build_split_result(split_name, run_id, metrics) 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 test_metrics = None
for preferred in ["test", "valid", "train"]: for preferred in ["test", "valid", "train"]:
if preferred in split_runs: if preferred in split_runs:
_, test_metrics = split_runs[preferred] _, test_metrics = split_runs[preferred]
break break
sqs_score = 0.0 sqs_score = None
sqs_breakdown: dict[str, float] = {} 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: 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 # Build config delta
config_delta = None config_delta = None
@ -84,6 +120,12 @@ def cmd_record(args: argparse.Namespace) -> None:
results=results, results=results,
sqs_score=sqs_score, sqs_score=sqs_score,
sqs_breakdown=sqs_breakdown, 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=args.verdict or "unknown",
verdict_reasoning=args.reasoning or "", verdict_reasoning=args.reasoning or "",
next_direction=args.next or "", next_direction=args.next or "",
@ -91,7 +133,8 @@ def cmd_record(args: argparse.Namespace) -> None:
) )
append_journal_entry(journal_path, entry) 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 # Show splits found
for split_name, sr in results.items(): for split_name, sr in results.items():
@ -104,6 +147,15 @@ def cmd_record(args: argparse.Namespace) -> None:
print(f"Leaderboard updated: {leaderboard_path}") 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: def cmd_leaderboard(args: argparse.Namespace) -> None:
"""Show or regenerate the leaderboard.""" """Show or regenerate the leaderboard."""
journal_dir = Path(args.journal_dir) 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) registry = rebuild_registry(journal_path, registry_path, leaderboard_path)
# Print to console top_n = getattr(args, "top", 10)
print(f"\n{'#':>3} {'Experiment':<40} {'SQS':>5} {'PF':>5} {'Ret%':>6} {'Trades':>6}") sort_by = getattr(args, "sort", "sqs")
print("-" * 70) if sort_by == "promotion":
for rank, e in enumerate(registry.entries, 1): ranked_entries = sorted(
pf = f"{e.profit_factor:.2f}" if e.profit_factor is not None else "-" registry.entries,
ret = f"{e.total_return_pct:+.1f}" if e.total_return_pct is not None else "-" key=lambda e: (e.promotion_score is None, -(e.promotion_score or 0.0), -e.sqs_score),
print(f"{rank:>3} {e.experiment_name:<40} {e.sqs_score:>5.1f} {pf:>5} {ret:>6} {e.trade_count:>6}") )
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: def cmd_show(args: argparse.Namespace) -> None:
@ -144,11 +249,28 @@ def cmd_show(args: argparse.Namespace) -> None:
sys.exit(1) sys.exit(1)
for e in found: 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"\n{e.entry_id}{e.experiment_name}")
print(f" Timestamp: {e.timestamp}") print(f" Timestamp: {e.timestamp}")
print(f" Hypothesis: {e.hypothesis}") print(f" Hypothesis: {e.hypothesis}")
print(f" Verdict: {e.verdict}") 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: if e.config_delta:
print(f" Baseline: {e.config_delta.base_experiment}") print(f" Baseline: {e.config_delta.base_experiment}")
for k, v in e.config_delta.changes.items(): 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}'.") 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: 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) sub = parser.add_subparsers(dest="command", required=True)
# record _jdir_kwargs = {"default": _DEFAULT_JOURNAL_DIR, "help": f"Path to journal/ directory (default: {_DEFAULT_JOURNAL_DIR})"}
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") # record (alias: rec)
p_record.add_argument("--runs-dir", required=True, help="Path to runs output directory") for name in ("record", "rec"):
p_record.add_argument("--experiment", required=True, help="Experiment name (matches manifest)") p = sub.add_parser(name, help="Record an experiment to the journal")
p_record.add_argument("--hypothesis", help="What you expected this change to do") p.add_argument("--journal-dir", **_jdir_kwargs)
p_record.add_argument("--baseline", help="Baseline experiment name for comparison") p.add_argument("--runs-dir", default="runs", help="Path to runs output directory (default: runs)")
p_record.add_argument("--verdict", choices=["better", "worse", "neutral", "unknown"], default="unknown") p.add_argument("--experiment", "-e", required=True, help="Experiment name (matches manifest)")
p_record.add_argument("--reasoning", help="Why this verdict") p.add_argument("--hypothesis", "-H", help="What you expected this change to do")
p_record.add_argument("--next", help="Next experiment direction") p.add_argument("--baseline", "-b", help="Baseline experiment name for comparison")
p_record.add_argument("--force", action="store_true", help="Allow duplicate experiment names") p.add_argument("--verdict", "-v", choices=["better", "worse", "neutral", "unknown"], default="unknown")
p.add_argument("--reasoning", "-r", help="Why this verdict")
# leaderboard p.add_argument("--next", "-n", help="Next experiment direction")
p_lb = sub.add_parser("leaderboard", help="Show/regenerate the leaderboard") p.add_argument("--force", "-f", action="store_true", help="Allow duplicate experiment names")
p_lb.add_argument("--journal-dir", required=True, help="Path to journal/ directory")
# leaderboard (alias: lb)
# show for name in ("leaderboard", "lb"):
p_show = sub.add_parser("show", help="Show details of a journal entry") p = sub.add_parser(name, help="Show/regenerate the leaderboard")
p_show.add_argument("--journal-dir", required=True, help="Path to journal/ directory") p.add_argument("--journal-dir", **_jdir_kwargs)
p_show.add_argument("entry_id", help="Entry ID (IMP-0001) or experiment name") 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")
# check-duplicate
p_dup = sub.add_parser("check-duplicate", help="Check if experiment already recorded") # show (alias: s)
p_dup.add_argument("--journal-dir", required=True, help="Path to journal/ directory") for name in ("show", "s"):
p_dup.add_argument("--experiment", required=True, help="Experiment name to check") 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() args = parser.parse_args()
dispatch = { dispatch = {
"record": cmd_record, "record": cmd_record, "rec": cmd_record,
"leaderboard": cmd_leaderboard, "leaderboard": cmd_leaderboard, "lb": cmd_leaderboard,
"show": cmd_show, "show": cmd_show, "s": cmd_show,
"check-duplicate": cmd_check_duplicate, "check-duplicate": cmd_check_duplicate, "dup": cmd_check_duplicate,
} }
dispatch[args.command](args) dispatch[args.command](args)

@ -1,84 +1,84 @@
# Strategy Improvement Leaderboard # 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 | | # | 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_step35_balanced_sleeves_nofrac_aclong12 | 90.4 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 2026-03-17 | | 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_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 90.3 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 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_step44_short_core_macro50 | 87.9 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 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_step47_short_core_macro_block_sdlong25 | 87.0 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 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_step45_short_core_macro_block | 86.7 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 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_step51_short_core_macro_block_crashcap | 86.7 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 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_step46_short_core_macro_block_acshort12 | 86.6 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 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_step52_short_core_macro_block_crashcap_gap10 | 86.3 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 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_step48_short_core_macro_block_nolong | 85.7 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 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_step40_short_core_sdlong12 | 82.1 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 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_step34_balanced_sleeves_nofrac_aclong25 | 81.3 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 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_step43_short_core_sdlong25 | 78.9 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 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_step31_balanced_sleeves_nofrac_acshort12 | 77.9 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 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_step39_balanced_sleeves_sdlong12 | 77.9 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 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_step30_balanced_sleeves_nofrac | 75.2 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 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_step33_balanced_sleeves_nofrac_acshort6 | 73.9 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 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_step37_balanced_sleeves_aclong_vol3 | 73.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 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_step41_short_core_sdlong12_acshort50 | 72.6 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 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_step42_short_core_only | 66.3 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 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_step14_score65 | 64.2 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | 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_step18_nofrac | 63.2 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | 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_step19_hold5 | 62.9 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | 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_step20_best3 | 62.0 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | 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 | 59.4 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | 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_step27_sdlong_close7_budget25 | 57.9 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 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_step13_best | 57.7 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | 2026-03-16 | | 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_step23_sdlong_close7 | 55.1 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 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_step38_balanced_sleeves_aclong_vol4 | 54.2 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 2026-03-17 | | 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_step16_react7_score65 | 53.2 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | 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_step5_maxcand3 | 52.5 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | 2026-03-16 | | 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_portfolio_v2 | 52.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 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_step15_react7 | 51.7 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | 2026-03-17 | | 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_step11_score60 | 50.2 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | 2026-03-16 | | 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_step12_vol2x | 50.2 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | 2026-03-16 | | 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_step3_10pct | 49.8 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | 2026-03-16 | | 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_step10_short | 49.2 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | 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_step53_short_core_macro_block_crashcap_gap14 | 43.1 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2026-03-17 | | 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_step50_same_day_short_long_macro_block | 41.9 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 2026-03-17 | | 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_step2_notrail | 41.4 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | 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_step1_fixedr | 39.7 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | 2026-03-16 | | 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_combo_10pct_maxcand3 | 39.2 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | 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_step49_same_day_short_macro_block | 38.9 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 2026-03-17 | | 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 | 37.0 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | 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 | 35.4 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | 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 | 32.6 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | 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 | 31.3 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | 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 | 31.1 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | 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 ## Recent Entries
### IMP-0047 (2026-03-17) — pead_midcap_step53_short_core_macro_block_crashcap_gap14 ### 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. 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. 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. 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 ### 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. 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. 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. 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 ### 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. 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. 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. 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 ### 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. 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. 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. Next: Refine sleeve quality rather than deleting sleeves wholesale.
### IMP-0043 (2026-03-17) — pead_midcap_step49_same_day_short_macro_block ### 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. 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. 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. Next: Keep the supporting sleeves and test smaller structural adjustments instead.

File diff suppressed because it is too large Load Diff

@ -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-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-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-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-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-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"]} {"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-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-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-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-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-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-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-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-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-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-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_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-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"]} {"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"]}

@ -165,6 +165,9 @@ class MetricsBundle(BaseModel):
sortino_ratio: float | None = None sortino_ratio: float | None = None
avg_daily_pnl: float | None = None avg_daily_pnl: float | None = None
avg_positions_held: 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) # Stability metrics (4)
trade_skewness: float | None = None trade_skewness: float | None = None
@ -364,6 +367,40 @@ class SQSWeights(BaseModel):
low_trade_penalty_factor: float = 0.5 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): class SplitResult(BaseModel):
"""Metrics for a single backtest split (train/valid/test).""" """Metrics for a single backtest split (train/valid/test)."""
@ -376,6 +413,9 @@ class SplitResult(BaseModel):
sharpe_ratio: float | None = None sharpe_ratio: float | None = None
monthly_win_rate: float | None = None monthly_win_rate: float | None = None
equity_curve_r_squared: 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): class ConfigDelta(BaseModel):
@ -396,6 +436,12 @@ class JournalEntry(BaseModel):
results: dict[str, SplitResult] = Field(default_factory=dict) # split_name → SplitResult results: dict[str, SplitResult] = Field(default_factory=dict) # split_name → SplitResult
sqs_score: float | None = None sqs_score: float | None = None
sqs_breakdown: dict[str, float] = Field(default_factory=dict) 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: str = "unknown" # better / worse / neutral / unknown
verdict_reasoning: str = "" verdict_reasoning: str = ""
next_direction: str = "" next_direction: str = ""
@ -408,12 +454,29 @@ class RegistryEntry(BaseModel):
entry_id: str entry_id: str
experiment_name: str experiment_name: str
sqs_score: float 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 profit_factor: float | None = None
total_return_pct: float | None = None total_return_pct: float | None = None
win_rate: float | None = None win_rate: float | None = None
sharpe_ratio: float | None = None sharpe_ratio: float | None = None
max_drawdown_pct: float | None = None max_drawdown_pct: float | None = None
trade_count: int = 0 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 = "" timestamp: str = ""

@ -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) 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) # Stability metrics (4)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -409,6 +448,9 @@ def build_metrics_bundle(
sortino_ratio=compute_sortino_ratio(equity_curve), sortino_ratio=compute_sortino_ratio(equity_curve),
avg_daily_pnl=compute_avg_daily_pnl(equity_curve), avg_daily_pnl=compute_avg_daily_pnl(equity_curve),
avg_positions_held=compute_avg_positions_held(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 # Stability
trade_skewness=compute_trade_skewness(trades), trade_skewness=compute_trade_skewness(trades),
trade_kurtosis=compute_trade_kurtosis(trades), trade_kurtosis=compute_trade_kurtosis(trades),

@ -1,6 +1,7 @@
"""Strategy improvement tracker: SQS computation, journal I/O, leaderboard.""" """Strategy improvement tracker: SQS computation, journal I/O, leaderboard."""
from __future__ import annotations from __future__ import annotations
import functools
import json import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -10,9 +11,12 @@ from libs.backtest.domain import (
ExperimentRegistry, ExperimentRegistry,
JournalEntry, JournalEntry,
MetricsBundle, MetricsBundle,
PromotionScoreWeights,
RegistryEntry, RegistryEntry,
SplitResult, SplitResult,
SQSWeights, SQSWeights,
SQSv2Weights,
UnifiedScoreWeights,
) )
from libs.common.logging import get_logger from libs.common.logging import get_logger
from libs.common.time_utils import utc_now from libs.common.time_utils import utc_now
@ -20,6 +24,9 @@ from libs.common.time_utils import utc_now
logger = get_logger(__name__) logger = get_logger(__name__)
_DEFAULT_WEIGHTS = SQSWeights() _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)) 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( def compute_sqs(
metrics: MetricsBundle, metrics: MetricsBundle,
weights: SQSWeights | None = None, weights: SQSWeights | None = None,
@ -101,6 +141,67 @@ def compute_sqs(
return sqs, breakdown 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 # Helper builders
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -118,9 +219,248 @@ def build_split_result(split_name: str, run_id: str, metrics: MetricsBundle) ->
sharpe_ratio=metrics.sharpe_ratio, sharpe_ratio=metrics.sharpe_ratio,
monthly_win_rate=metrics.monthly_win_rate, monthly_win_rate=metrics.monthly_win_rate,
equity_curve_r_squared=metrics.equity_curve_r_squared, 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( def compute_config_delta(
current: dict[str, Any], current: dict[str, Any],
baseline: dict[str, Any], baseline: dict[str, Any],
@ -195,24 +535,58 @@ def rebuild_registry(
registry_entries: list[RegistryEntry] = [] registry_entries: list[RegistryEntry] = []
for je in entries: 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( registry_entries.append(
RegistryEntry( RegistryEntry(
entry_id=je.entry_id, entry_id=je.entry_id,
experiment_name=je.experiment_name, 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, profit_factor=test_result.profit_factor if test_result else None,
total_return_pct=test_result.total_return_pct 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, win_rate=test_result.win_rate if test_result else None,
sharpe_ratio=test_result.sharpe_ratio 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, max_drawdown_pct=test_result.max_drawdown_pct if test_result else None,
trade_count=test_result.trade_count if test_result else 0, 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, timestamp=je.timestamp,
) )
) )
# Sort by SQS descending # Sort by canonical SQS descending, then promotion.
registry_entries.sort(key=lambda e: e.sqs_score, reverse=True) 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( registry = ExperimentRegistry(
entries=registry_entries, entries=registry_entries,
@ -238,8 +612,8 @@ def _write_leaderboard_md(
lines: list[str] = [] lines: list[str] = []
lines.append("# Strategy Improvement Leaderboard") lines.append("# Strategy Improvement Leaderboard")
lines.append(f"_Updated: {registry.updated_at}_\n") lines.append(f"_Updated: {registry.updated_at}_\n")
lines.append("| # | Experiment | SQS | PF | Ret% | WR | Sharpe | DD% | Trades | Date |") 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("|---|-----------|-----|-----|------|-----|--------|-----|--------|------|") lines.append("|---|-----------|-----|-------|---------|-------|-----------|--------|------|-----------|---------|---------|-------|---------|-------|-----------|--------|------|-----------|---------|---------|------|")
for rank, e in enumerate(registry.entries, 1): for rank, e in enumerate(registry.entries, 1):
pf = f"{e.profit_factor:.2f}" if e.profit_factor is not None else "-" 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 "-" 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 "-" 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 "-" 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 "-" ts = e.timestamp[:10] if e.timestamp else "-"
lines.append( 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 entries (last 5)
recent = list(reversed(journal_entries))[:5] recent = list(reversed(journal_entries))[:5]
if recent: if recent:
registry_by_id = {entry.entry_id: entry for entry in registry.entries}
lines.append("\n## Recent Entries") lines.append("\n## Recent Entries")
for je in recent: 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"### {je.entry_id} ({je.timestamp[:10]}) \u2014 {je.experiment_name}")
lines.append(f"Hypothesis: {je.hypothesis}") 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: if je.verdict_reasoning:
lines.append(f"Reasoning: {je.verdict_reasoning}") lines.append(f"Reasoning: {je.verdict_reasoning}")
if je.next_direction: if je.next_direction:

@ -38,6 +38,9 @@ dev = [
"types-beautifulsoup4", "types-beautifulsoup4",
] ]
[project.scripts]
fithia2 = "apps.tracker.cli:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["apps", "libs"] packages = ["apps", "libs"]

@ -323,6 +323,7 @@ class TestBacktestRunIntegration:
def test_multi_engine_run_writes_per_engine_metrics(self, tmp_path): def test_multi_engine_run_writes_per_engine_metrics(self, tmp_path):
from apps.backtester.run import BacktestRunner from apps.backtester.run import BacktestRunner
from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
import json
import pyarrow.parquet as pq import pyarrow.parquet as pq
store = _build_multi_engine_store() store = _build_multi_engine_store()
@ -382,3 +383,11 @@ class TestBacktestRunIntegration:
trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist() trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
engine_ids = {row["engine_id"] for row in trade_blotter} engine_ids = {row["engine_id"] for row in trade_blotter}
assert "earnings_after_close_short_v1" not in engine_ids 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

@ -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( return DailyPortfolioState(
date=date, date=date,
equity=equity, equity=equity,
cash_available=equity, cash_available=equity,
gross_exposure=0.0, gross_exposure=gross_exposure,
net_exposure=0.0, net_exposure=net_exposure,
reserved_risk_budget=0.0, reserved_risk_budget=0.0,
unrealized_pnl=0.0, unrealized_pnl=0.0,
realized_pnl=0.0, realized_pnl=0.0,
@ -168,6 +174,25 @@ class TestSharpeRatio:
assert compute_sharpe_ratio(curve) is None 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: class TestStopExitRate:
def test_basic(self): def test_basic(self):
from libs.backtest.metrics import compute_stop_exit_rate from libs.backtest.metrics import compute_stop_exit_rate
@ -218,6 +243,23 @@ class TestBuildMetricsBundle:
assert m.trade_count == 2 assert m.trade_count == 2
assert m.win_rate == pytest.approx(0.5) assert m.win_rate == pytest.approx(0.5)
assert m.total_return_pct == pytest.approx(5.0) 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): def test_empty_trades(self):
from libs.backtest.metrics import build_metrics_bundle from libs.backtest.metrics import build_metrics_bundle

@ -14,11 +14,17 @@ from libs.backtest.domain import (
) )
from libs.backtest.tracker import ( from libs.backtest.tracker import (
_normalize, _normalize,
_normalize_band,
_normalize_inverse, _normalize_inverse,
append_journal_entry, append_journal_entry,
build_split_result, build_split_result,
check_duplicate, check_duplicate,
compute_public_sqs,
compute_promotion_score,
compute_sqs, compute_sqs,
compute_sqs_v2,
compute_unified_score,
compute_unified_split_quality,
get_next_entry_id, get_next_entry_id,
load_journal, load_journal,
rebuild_registry, rebuild_registry,
@ -66,6 +72,18 @@ class TestNormalizeInverse:
assert _normalize_inverse(None, low=10.0, high=1.0) == 0.0 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 # compute_sqs
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -162,6 +180,257 @@ class TestComputeSQS:
assert sqs_profit > sqs_risk 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 # build_split_result
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -177,12 +446,18 @@ def test_build_split_result():
sharpe_ratio=0.8, sharpe_ratio=0.8,
monthly_win_rate=0.60, monthly_win_rate=0.60,
equity_curve_r_squared=0.40, 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) sr = build_split_result("test", "bt_run123", m)
assert sr.run_id == "bt_run123" assert sr.run_id == "bt_run123"
assert sr.trade_count == 50 assert sr.trade_count == 50
assert sr.profit_factor == 1.2 assert sr.profit_factor == 1.2
assert sr.total_return_pct == 2.5 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, profit_factor=1.0 + i * 0.2,
total_return_pct=float(i), total_return_pct=float(i),
win_rate=0.5, 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 = JournalEntry(
entry_id=f"IMP-{i+1:04d}", entry_id=f"IMP-{i+1:04d}",
@ -305,6 +585,8 @@ class TestRebuildRegistry:
experiment_name=name, experiment_name=name,
hypothesis=f"h{i}", hypothesis=f"h{i}",
sqs_score=sqs, sqs_score=sqs,
promotion_score=sqs - 5,
unified_score=sqs - 10,
results={"test": test_result}, results={"test": test_result},
verdict="better" if sqs > 50 else "worse", verdict="better" if sqs > 50 else "worse",
) )
@ -312,11 +594,13 @@ class TestRebuildRegistry:
registry = rebuild_registry(journal_path, registry_path, leaderboard_path) registry = rebuild_registry(journal_path, registry_path, leaderboard_path)
# Sorted by SQS descending # Sorted by public SQS descending
assert len(registry.entries) == 3 assert len(registry.entries) == 3
assert registry.entries[0].experiment_name == "exp_high" assert registry.entries[0].experiment_name == "exp_mid"
assert registry.entries[0].sqs_score == 70.0
assert registry.entries[2].experiment_name == "exp_low" 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 # Files exist
assert registry_path.exists() assert registry_path.exists()
@ -327,3 +611,5 @@ class TestRebuildRegistry:
assert "exp_high" in lb_text assert "exp_high" in lb_text
assert "exp_low" in lb_text assert "exp_low" in lb_text
assert "| # |" in lb_text assert "| # |" in lb_text
assert "| # | Experiment | SQS |" in lb_text
assert "[T]Gross%" in lb_text

Loading…
Cancel
Save