From a81b3a6ac4945f724281594024f9df9188f6a7fe Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Thu, 26 Mar 2026 20:17:21 -0700 Subject: [PATCH] Update tracker, leaderboard, docs, and overlay leaderboard Additional tracker/leaderboard updates, overlay leaderboard, and documentation improvements. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/tracker/cli.py | 36 +- .../configuration_and_schemas.md | 2 +- docs/public_sqs_calculation.md | 540 ++++++++++++++++++ docs/research_workflow_and_handoff.md | 422 ++++++++++++++ journal/LEADERBOARD.md | 2 +- journal/OVERLAY_LEADERBOARD.md | 21 + journal/experiment_registry.json | 2 +- libs/backtest/tracker.py | 49 +- 8 files changed, 1042 insertions(+), 32 deletions(-) create mode 100644 docs/public_sqs_calculation.md create mode 100644 docs/research_workflow_and_handoff.md create mode 100644 journal/OVERLAY_LEADERBOARD.md diff --git a/apps/tracker/cli.py b/apps/tracker/cli.py index d051a30..487c972 100644 --- a/apps/tracker/cli.py +++ b/apps/tracker/cli.py @@ -42,6 +42,7 @@ from libs.backtest.tracker import ( compute_unified_score, compute_wfqs, compute_wfqs_v2, + filter_overlay_registry_entries, filter_registry_entries, get_next_entry_id, journal_lock, @@ -469,6 +470,7 @@ def cmd_leaderboard(args: argparse.Namespace) -> None: journal_path = journal_dir / "improvement_journal.jsonl" registry_path = journal_dir / "experiment_registry.json" leaderboard_path = journal_dir / "LEADERBOARD.md" + overlay_leaderboard_path = journal_dir / "OVERLAY_LEADERBOARD.md" if not journal_path.exists(): journal_path.parent.mkdir(parents=True, exist_ok=True) @@ -476,16 +478,31 @@ def cmd_leaderboard(args: argparse.Namespace) -> None: with journal_lock(journal_path): registry = _sync_and_rebuild(journal_path, registry_path, leaderboard_path) - ranked_source = filter_registry_entries( - registry.entries, - include_retired=getattr(args, "include_retired", False), - ) + overlay_only = getattr(args, "overlay_only", False) + if overlay_only: + ranked_source = filter_overlay_registry_entries( + registry.entries, + include_retired=getattr(args, "include_retired", False), + ) + displayed_leaderboard_path = overlay_leaderboard_path + else: + ranked_source = filter_registry_entries( + registry.entries, + include_retired=getattr(args, "include_retired", False), + include_overlays=False, + ) + displayed_leaderboard_path = leaderboard_path sort_by = getattr(args, "sort", "sqs") ranked_entries = sorted(ranked_source, key=lambda entry: _score_sort_key(sort_by, entry)) total = len(ranked_entries) top_n = getattr(args, "top", 10) - diagnostics = sort_by in {"deployment", "dep", "wfqs", "rqs", "promotion", "unified", "sqs2"} + diagnostics = (not overlay_only) and sort_by in {"deployment", "dep", "wfqs", "rqs", "promotion", "unified", "sqs2"} + title = ( + f"[bold cyan]Top {top_n} / {total}[/] [dim]· overlay official SQS · Common=full-window Stress=OOT[/]" + if overlay_only + else f"[bold cyan]Top {top_n} / {total}[/] [dim]· {_title_for_sort(sort_by)} · Tr=train V=valid T=test[/]" + ) tbl = Table( box=box.SIMPLE_HEAD, @@ -493,7 +510,7 @@ def cmd_leaderboard(args: argparse.Namespace) -> None: header_style="bold yellow", row_styles=["", "dim"], padding=(0, 1), - title=f"[bold cyan]Top {top_n} / {total}[/] [dim]· {_title_for_sort(sort_by)} · Tr=train V=valid T=test[/]", + title=title, title_justify="left", expand=False, ) @@ -549,7 +566,7 @@ def cmd_leaderboard(args: argparse.Namespace) -> None: _console.print() _console.print(tbl) - _console.print(f" [dim]LEADERBOARD.md → {leaderboard_path}[/]\n") + _console.print(f" [dim]LEADERBOARD.md → {displayed_leaderboard_path}[/]\n") def cmd_show(args: argparse.Namespace) -> None: @@ -1017,6 +1034,11 @@ def main() -> None: action="store_true", help="Include retired legacy PEAD / short-core / exact-pocket families", ) + subparser.add_argument( + "--overlay-only", + action="store_true", + help="Show the separate overlay/book-of-books leaderboard instead of the default single-book leaderboard", + ) for name in ("show", "s"): subparser = sub.add_parser(name, help="Show details of a journal entry") diff --git a/dev/finished/phase4_deliverables/configuration_and_schemas.md b/dev/finished/phase4_deliverables/configuration_and_schemas.md index 0ed98d8..cad7e26 100644 --- a/dev/finished/phase4_deliverables/configuration_and_schemas.md +++ b/dev/finished/phase4_deliverables/configuration_and_schemas.md @@ -125,7 +125,7 @@ runs/ ```bash python -m apps.backtester.run \ - --manifest configs/experiments/baseline_v1.json \ + --manifest configs/experiments/return_max_long_v1.1.json \ --snapshot-id snapshot_2026_03_20 \ --output-root ./runs ``` diff --git a/docs/public_sqs_calculation.md b/docs/public_sqs_calculation.md new file mode 100644 index 0000000..b903340 --- /dev/null +++ b/docs/public_sqs_calculation.md @@ -0,0 +1,540 @@ +# Public SQS Calculation + +이 문서는 tracker가 최종 public `SQS`를 어떻게 계산하는지, 그리고 왜 +split-only 점수와 final leaderboard 점수가 다를 수 있는지를 정리한다. + +2026-03-26부터 기본 leaderboard는 **public `SQS v4`**를 쓴다. + +- `SQS v4`: `SQS v3` + common-window capital-growth blend +- `sqs_v3_score`: deployment/WFV-first primary rank +- `stress_sqs_score`: 예전 public score. stress OOT quality를 추가 패널티로 곱한 legacy score +- overlay entry는 split/WFV 기반 `SQS v4` 대신 **overlay 전용 official score**를 쓴다. + +핵심 결론부터 말하면: + +- `test split`만으로 나오는 `sqs_v2_score`는 final public `SQS`가 아니다. +- final public `SQS v4`의 backbone은 아래 6개다. + - `train` split + - `valid` split + - `test` split + - walk-forward summary + - main robustness summary + - repaired out-of-time robustness summary +- 여기에 comparable `common-window summary`가 있으면 `v4`가 계산되고, + 없으면 `v3`로 fallback된다. +- 다만 `stress_sqs_score`도 같이 계산해 registry에 남긴다. + +## 1. 어디서 계산되나 + +최종 public `SQS v4` 계산 진입점은 +[`compute_public_sqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L482) +이고, 실제 계산은 [`compute_public_sqs_v4`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1180)가 한다. + +legacy stress-adjusted score는 +[`compute_public_sqs_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1067) +가 그대로 유지한다. + +`v4` backbone인 `v3` 계산은 +[`compute_public_sqs_v3`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1126) +에 남아 있다. + +public leaderboard / registry rebuild도 같은 함수를 쓴다: + +- [`rebuild_registry`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1808) + +즉, 수동 계산과 leaderboard 계산은 같은 코드 경로를 따라야 한다. + +## 1.5 Overlay는 어떻게 공식 점수화되나 + +overlay는 train/valid/test/WFV 구조가 없으므로 single-book `SQS v4`를 그대로 쓸 수 없다. + +대신 아래 두 summary를 붙여서 공식 점수를 계산한다. + +- `overlay_common_window_summary` +- `overlay_stress_window_summary` + +계산 함수는: + +- [`compute_overlay_public_sqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1356) +- [`compute_overlay_stress_sqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1381) + +공식 overlay 점수는: + +```text +overlay_window_score * overlay_stress_gate +``` + +legacy overlay stress 점수는: + +```text +overlay_window_score * overlay_stress_gate * overlay_quality_factor +``` + +즉 overlay도 stress window는 ranking 보조가 아니라 **공식 통과 게이트**에 가깝게 다룬다. + +추가 원칙: + +- overlay official score는 spec의 `books[].equity_csv`를 **frozen input**으로 본다. +- [`fithia2 paper backtest --overlay`](/Users/yirugi/mycloud/personal/workspace/fithia2/apps/paper_trader/cli.py)도 같은 frozen `equity_csv`를 우선 replay해야 한다. +- 그래서 overlay leaderboard 숫자와 paper overlay 숫자가 다르면 먼저 + `equity_csv`, `regime_source`, `start_date`, `end_date`가 같은지 확인한다. +- stress OOT overlay는 full-window csv를 재사용하면 안 된다. + OOT 전용 `equity_csv`와 OOT snapshot `regime_source.snapshot_dir`를 같이 고정해야 한다. + +## 2. 흔한 오해 + +### `sqs_v2_score`는 final public `SQS`가 아니다 + +`journal/improvement_journal.jsonl`에 들어가는 `sqs_v2_score`는 +`test split` 하나만 보고 계산한 split-level quality 점수다. + +이 값은 아래 함수에서 나온다: + +- [`compute_sqs_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L178) + +반면 final public `SQS v4`의 backbone인 `SQS v3`는: + +- `RQS` +- `WFQS v2` +- deployment gate +- main robustness gate +- OOT robustness gate +- OOT quality factor +- activity factor + +를 모두 반영한 값이다. + +그래서 `sqs_v2_score=90.7`인데 public `SQS=54.7` 같은 일이 정상적으로 발생할 수 있다. + +## 2.5 왜 `v3`로 바뀌었나 + +기존 public score는: + +```text +base_score * main_rb_gate * oot_gate * oot_quality_factor * activity_factor +``` + +였다. 이 구조는 stress OOT를 + +- eligibility gate로 한 번 쓰고 +- quality penalty로 한 번 더 써서 + +최근/실전 적합도가 강한 전략을 과하게 깎을 수 있었다. + +`SQS v3`는 다음처럼 바꿨다. + +```text +base_score * main_rb_gate * oot_gate * activity_factor +``` + +즉 stress OOT는 **통과 여부를 확인하는 safety layer**로 남기고, +연속 quality 패널티는 primary ranking에서 제거했다. + +대신 예전 점수는 `stress_sqs_score`로 registry에 그대로 남긴다. + +## 2.6 왜 `v4`로 바뀌었나 + +`v3`는 deployment/WFV/stress gate는 잘 반영했지만, +연속 운용 구간에서의 복리 수익과 자본 효율을 직접 보지 못했다. + +그래서 `v4`는 comparable common-window run이 있는 경우에 한해 +아래처럼 `v3`와 `common_window_score`를 섞는다. + +```text +final_v4 = v3_score * 0.80 + common_window_score * 0.20 +``` + +여기서 `common_window_score`는 같은 연속 기간, 같은 초기 자본으로 돌린 run의: + +- total return +- profit factor +- Sharpe +- max drawdown +- return on gross exposure +- capital velocity + +를 합성한 값이다. + +중요한 점: + +- common-window summary가 없으면 `v4`는 억지 추정을 하지 않고 `v3`로 fallback한다. +- 즉 현재 leaderboard는 `v4 + partial backfill`일 수 있다. +- 상위권끼리 공정 비교하려면 같은 common-window가 붙어 있어야 한다. + +### validation stack 중 하나라도 없으면 public `SQS`는 `None` + +아래 세 개 중 하나라도 빠지면 public `SQS`는 계산되지 않는다. + +- walk-forward +- main robustness +- out-of-time robustness + +이 경우 함수는 숫자 대신 `None`을 주고, breakdown에 missing reason을 넣는다. + +예: + +```python +score, breakdown, source = compute_public_sqs(...) +# score == None +# source == "pending_validation" +# breakdown == {"requires_out_of_time_robustness": 1.0} +``` + +## 3. 입력 산출물 경로 + +표준 경로는 아래다. + +### Walk-Forward + +[`runs/_wfv/walk_forward/walk_forward_summary.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) + +### Main Robustness + +[`runs/_rm/robustness_matrix/robustness_matrix_summary.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) + +### OOT Robustness + +[`runs/_oot_rm/robustness_matrix/robustness_matrix_summary.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) + +journal에 attach된 경우엔 file path가 없어도 journal entry만으로 rebuild가 가능하다. +다만 디버그와 재현을 위해서는 run file도 남아 있는 쪽이 낫다. + +## 4. 실제 계산 순서 + +public `SQS v4`는 아래 순서로 계산된다. + +### 4.1 RQS 계산 + +split 3개에서 +[`compute_rqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L852) +를 계산한다. + +### 4.2 WFV 기반 `WFQS v2` + +walk-forward summary에서 +[`compute_wfqs_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L890) +를 계산한다. + +### 4.3 Deployment gate를 반영한 base score + +코드 그대로 쓰면: + +```text +base_score = (RQS * 0.45 + WFQS_v2 * 0.55) * deployment_gate +``` + +deployment gate는 아래 4개 통과 개수로 정해진다. + +- WFV positive fold rate >= 70% +- WFV median return >= 5% +- WFV worst return >= -5% +- WFV mean train-test gap <= 35% + +통과 개수별 factor: + +- 4개: `1.00` +- 3개: `0.85` +- 2개: `0.65` +- 1개: `0.40` +- 0개: `0.20` + +### 4.4 Main robustness gate + +[`compute_robustness_gate`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1124) +를 곱한다. + +기준: + +- overall positive window rate >= 65% +- 63d median >= 3% +- 252d median >= 8% +- overall worst return >= -12% + +factor는 deployment gate와 같은 `1.00 / 0.85 / 0.65 / 0.40 / 0.20` 구조다. + +### 4.5 OOT robustness gate + +[`compute_oot_robustness_gate`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1175) +를 곱한다. + +OOT는 스트레스 테스트라 threshold가 조금 느슨하다. + +- 63d+ positive rate >= 50% +- 63d median >= 0.5% +- 252d median >= 3% +- worst return >= -15% + +### 4.6 OOT quality는 보조지표 + +[`compute_oot_robustness_quality`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1261) +로 `0~100` quality를 계산한다. + +`SQS v3`/`v4`에서는 이 값을 **랭킹 점수에 곱하지 않는다.** +대신 registry breakdown과 `stress_sqs_score`에서만 사용한다. + +### 4.7 Activity factor + +마지막으로 +[`_public_activity_factor`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L658) +를 곱한다. + +이 단계 때문에: + +- replacement 전략 +- 지나치게 trade 수가 적은 rotation 전략 + +은 split/WFV가 좋아도 public 점수가 낮아질 수 있다. + +### 4.8 Common-window blend + +common-window summary가 있으면 +[`compute_common_window_score`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1278) +로 capital-growth score를 계산한 뒤: + +```text +final_v4 = v3_score * 0.80 + common_window_score * 0.20 +``` + +를 적용한다. + +common-window summary가 없으면: + +- final score는 `v3_score` +- source는 `v4_fallback_v3_missing_common_window` + +으로 남는다. + +## 5. `return_max_long_v6new.29` / `v6new.28` 같은 사례를 어떻게 읽어야 하나 + +`v6new.29`는 split/WFV/deployment도 강하고, +common-window capital-growth가 특히 강한 전형적인 케이스다. + +- `SQS v4`에서는 이 전략이 더 높은 순위를 받는다. +- `stress_sqs_score`는 여전히 낮아서 stress 취약성은 숨겨지지 않는다. + +`v6new.28`은 다른 방향의 예시다. + +### 5.1 왜 헷갈렸나 + +journal에는 아래 값이 이미 있었다. + +- `sqs_v2_score = 90.7` +- `rqs_score = 83.5` +- `wfqs_v2_score = 48.6` + +그런데 final public `SQS`는 비어 있었다. + +이유는 간단했다. + +- walk-forward 있음 +- main robustness 있음 +- OOT robustness 없음 + +즉 tracker 기준으로는 아직 `pending_validation`이었다. + +실제로: + +```python +score, breakdown, source = compute_public_sqs(...) +``` + +결과는: + +- `score = None` +- `source = "pending_validation"` +- `breakdown = {"requires_out_of_time_robustness": 1.0}` + +였다. + +### 5.2 OOT를 붙인 뒤 최종 계산 + +OOT run: + +- [robustness_matrix_summary.json](/Users/yirugi/mycloud/personal/workspace/fithia2/runs/return_max_long_v6new.28_oot_rm/robustness_matrix/robustness_matrix_summary.json) + +OOT 주요 수치: + +- overall positive `90.0%` +- overall worst `-4.59%` +- `63d median 11.8%` +- `252d median 72.07%` + +최종 계산 결과: + +- `RQS = 83.5` +- `WFQS v2 = 48.6` +- public `SQS = 54.7` +- source = `v2_deployment+robustness+oot` + +이 케이스에서는: + +- deployment gate = `0.85` +- main robustness gate = `1.0` +- OOT gate = `1.0` +- OOT quality factor = `1.0` +- activity factor = `1.0` + +라서 실질적으로: + +```text +base_score = (83.5 * 0.45 + 48.6 * 0.55) * 0.85 = 54.7 +final_public_sqs = 54.7 +``` + +즉 `90.7`이 틀린 값이 아니라, +**그 값은 public `SQS`가 아니라 split-only `sqs_v2_score`였다**가 정확한 설명이다. + +## 6. 권장 계산 절차 + +### 6.1 validation run 만들기 + +```bash +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --walk-forward \ + --wf-train-days 504 \ + --wf-test-days 63 \ + --wf-step-days 63 \ + --output-root runs/_wfv + +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --robustness-matrix \ + --rm-horizons 21,63,126,252,504 \ + --rm-step-days 21 \ + --output-root runs/_rm + +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --snapshot-id \ + --robustness-matrix \ + --rm-horizons 21,63,126,252 \ + --rm-step-days 21 \ + --output-root runs/_oot_rm +``` + +### 6.2 journal에 attach + +```bash +python apps/tracker/cli.py attach-wfv \ + --journal-dir journal \ + \ + --summary runs/_wfv/walk_forward/walk_forward_summary.json + +python apps/tracker/cli.py attach-robustness \ + --journal-dir journal \ + \ + --summary runs/_rm/robustness_matrix/robustness_matrix_summary.json + +python apps/tracker/cli.py attach-oot-robustness \ + --journal-dir journal \ + \ + --summary runs/_oot_rm/robustness_matrix/robustness_matrix_summary.json +``` + +### 6.3 Python으로 직접 계산 + +```python +import json +from pathlib import Path +from libs.backtest.domain import SplitResult, WalkForwardSummary, RobustnessMatrixSummary +from libs.backtest.tracker import compute_public_sqs + +entry = ... # journal entry dict + +train = SplitResult(**entry["results"]["train"]) +valid = SplitResult(**entry["results"]["valid"]) +test = SplitResult(**entry["results"]["test"]) +wfv = WalkForwardSummary(**entry["walk_forward_summary"]) +rm = RobustnessMatrixSummary(**entry["robustness_matrix_summary"]) +oot = RobustnessMatrixSummary(**entry["out_of_time_robustness_summary"]) + +score, breakdown, source = compute_public_sqs( + train, + valid, + test, + walk_forward_summary=wfv, + robustness_matrix_summary=rm, + out_of_time_robustness_summary=oot, +) +print(score, source, breakdown) +``` + +### 6.4 Common-window attach + +common-window는 같은 기간과 같은 초기 자본으로 맞춰서 별도로 붙인다. + +권장 원칙: + +- contenders끼리는 같은 `start_date / end_date`를 쓴다 +- initial equity도 같게 맞춘다 +- partial backfill 상태에선 `v4`와 `v3 fallback`이 섞일 수 있음을 인지한다 + +attach는 tracker CLI 또는 Python helper로 한다. +요약만 붙어 있으면 registry rebuild가 가능하다. + +## 7. 디버그 체크리스트 + +### score가 `None`이면 + +아래를 먼저 본다. + +- `walk_forward_summary` 존재 여부 +- `robustness_matrix_summary` 존재 여부 +- `out_of_time_robustness_summary` 존재 여부 + +### `sqs_v2_score`는 큰데 public `SQS`가 낮으면 + +아래를 본다. + +- `wfqs_v2_score` +- deployment gate +- robustness gate +- OOT gate / OOT quality factor +- activity factor +- common-window summary 존재 여부와 comparable window 설정 + +특히 rotation / low-activity 전략은 activity factor 때문에 public 점수가 크게 깎일 수 있다. + +반대로 연속 구간 복리 수익이 강한 전략은 `v4` common-window가 붙으면 더 올라갈 수 있다. + +### registry와 수동 계산이 다르면 + +아래를 본다. + +- journal entry에 attach가 실제로 되었는가 +- registry가 rebuild 되었는가 +- 같은 experiment 이름의 중복 journal entry 중 최신 exact match가 선택되었는가 +- common-window가 일부 전략에만 backfill된 partial 상태가 아닌가 + +### run 숫자가 leaderboard/journal과 다르면 + +점수 계산식보다 먼저 snapshot provenance를 본다. + +- run dir의 [`snapshot_manifest.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) +- run dir의 [`snapshot_fingerprint.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) +- `snapshot_manifest.json`의 `export_enrichments`가 다르면 같은 snapshot id 계열이라도 직접 비교 대상으로 섞지 않는다. + +여기서 특히 아래를 본다. + +- `dataset_snapshot_id` +- `source_manifest_path` +- `snapshot_dir_name` +- `manifest_snapshot_id` +- `snapshot_id_matches_manifest` +- `output_dir_matches_manifest` + +둘 중 하나라도 `false`면, +같은 snapshot id 아래 다른 내용이 덮어써졌거나 잘못 복사된 manifest일 수 있다. +이 경우 점수 비교 전에 snapshot provenance부터 정리해야 한다. + +## 8. 운영 원칙 + +- split-only `sqs_v2_score`를 final public `SQS`로 말하지 않는다. +- leaderboard 숫자는 반드시 `compute_public_sqs()` 경로로만 확인한다. +- OOT가 빠진 후보는 “점수 미계산”이지 “점수 0”이 아니다. +- journal과 registry가 다르면 journal + tracker 함수가 source of truth다. +- `v4` 비교는 comparable common-window가 붙은 후보끼리 우선 본다. diff --git a/docs/research_workflow_and_handoff.md b/docs/research_workflow_and_handoff.md new file mode 100644 index 0000000..2dbdaba --- /dev/null +++ b/docs/research_workflow_and_handoff.md @@ -0,0 +1,422 @@ +# Strategy Research Workflow And Handoff + +이 문서는 `return-max long` 전략 연구를 여러 AI 에이전트가 이어서 하더라도 +같은 실수를 반복하지 않도록 하기 위한 운영 기준이다. + +가장 중요한 배경은 과거 `v797_v793_epam_rklb_alk_combo` 누락 사례와, +그 뒤에 드러난 `disabled named micro contamination`, `repaired OOT snapshot` 문제다. +이 문서는 그런 실수와 재오염을 다시 만들지 않기 위한 운영 규칙을 정의한다. + +## 1. Source Of Truth + +전략 연구의 source of truth는 아래 순서로 본다. + +1. [`configs/experiments`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments) +2. [`journal/improvement_journal.jsonl`](/Users/yirugi/mycloud/personal/workspace/fithia2/journal/improvement_journal.jsonl) +3. [`journal/experiment_registry.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/journal/experiment_registry.json) +4. [`runs/`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs) + +원칙: + +- `runs/`는 실험 산출물 저장소다. 단독으로는 정식 전략 정의가 아니다. +- `journal`은 평가 기록이다. 정식 전략 정의를 대신하지 않는다. +- 실제로 다시 돌릴 수 있는 전략 정의는 반드시 `configs/experiments/*.json`에 있어야 한다. +- run 비교가 안 맞으면 `runs//snapshot_manifest.json`, `snapshot_fingerprint.json`을 먼저 본다. +- `snapshot_manifest.json`의 `export_enrichments`가 다르면 같은 base snapshot name이라도 다른 dataset으로 본다. + +## 2. 용어 정의 + +### Scratch Candidate + +임시 탐색용 manifest. + +- `/tmp/*.json` 또는 배치용 임시 경로에 둘 수 있다. +- 빠른 조합 탐색과 train scan에는 허용한다. +- 이 상태로는 leaderboard/journal의 최종 후보가 될 수 없다. + +### Official Experiment + +정식 manifest가 있고 full-split 결과까지 있는 실험. + +필수 조건: + +1. [`configs/experiments`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments)에 `.json` 파일이 있다. +2. 파일명 stem과 `experiment_name`이 같다. +3. `train / valid / test` full split 결과가 있다. +4. journal entry가 있다. + +### Deploy Candidate + +실전 승격을 검토할 수 있는 실험. + +Official Experiment 조건에 더해: + +1. walk-forward summary가 있다. +2. robustness matrix summary가 있다. +3. repaired out-of-time robustness summary가 있다. +4. public `SQS`가 계산돼 있다. + +### Archived Overfit Branch + +다음 조건 중 하나라도 만족하면 기본 workflow에서 archive 대상으로 본다. + +- manifest의 `strategy_engines[].engine_id` 중 하나라도 `exact`를 포함 +- manifest에 named micro engine이 존재함 + - `enabled: false`여도 contamination으로 본다. +- symbol/date-specific pocket을 누적해 funded trade set을 직접 외우는 구조 + +이 계열은 연구 참고용으로만 남기고, 기본 leaderboard와 실전 후보군에서는 제외한다. + +## 2.1 버전 규칙 + +- 기존 `return_max_long_v326` 같은 historical 전략은 **의미상 `v0.326`** 으로 취급한다. +- 과거 journal/manifest 파일명을 전부 물리적으로 바꾸지는 않는다. 재현성과 참조 무결성 때문이다. +- 새 clean lineage는 **`v1.1`부터 시작**한다. +- lineage 시작점은 `v1.1`이지만, 문서 작성 시점의 active clean baseline은 + [`return_max_long_v1.51.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.51.json) 이다. +- 이후 새 실험은 `v1.52`, `v1.53` 식으로 올린다. + +## 3. 정식 승격 기준 + +다음 중 하나에 해당하면 scratch를 official로 승격한다. + +- full-split에서 현재 baseline을 이겼다. +- leaderboard에 올릴 가치가 있다. +- 다음 세션에서도 다시 이어서 연구할 가능성이 높다. +- WFV/robustness까지 붙일 대상이다. + +즉, "의미 있는 winner"는 반드시 repo manifest로 옮긴다. + +## 4. 금지 규칙 + +아래는 금지한다. + +- scratch manifest 상태로 journal만 기록하는 것 +- `experiment_name`과 다른 파일명으로 공식 manifest를 저장하는 것 +- leaderboard 상위 전략이 `configs/experiments`에 없는 상태로 남는 것 +- WFV/robustness가 붙은 전략의 summary 경로를 journal에 연결하지 않는 것 + +## 5. 표준 연구 흐름 + +현재 active clean baseline은 [`return_max_long_v1.51.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.51.json) 이다. +이 전략은 old `v1.12` 계열에서 disabled named micro를 물리적으로 제거한 clean replacement다. +즉, 지금부터의 baseline 해석은 "계보의 시작"과 "현재 active baseline"을 구분해야 한다. + +### Step 1. Baseline 선택 + +- 기준 전략은 반드시 [`configs/experiments`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments) 안의 manifest여야 한다. +- baseline 이름은 journal entry와 동일하게 사용한다. +- paper backtest와 research backtester는 같은 snapshot을 써야 비교가 된다. + 경로 해석 우선순위는 `settings.parquet_dir` 다음 `data/datasets/snapshots`다. + +### Step 2. Scratch 탐색 + +- `/tmp`에 scratch manifest를 만들고 배치 탐색을 돌릴 수 있다. +- 이 단계에서는 train-only scan이나 fast full-split batch를 허용한다. +- 아직 journal 최종 기록 대상은 아니다. + +### Step 3. Winner 확정 + +scratch 후보가 baseline을 이기면 먼저 repo에 정식 manifest를 만든다. + +권장 순서: + +1. `configs/experiments/.json` 생성 +2. 해당 파일로 다시 full split 실행 +3. 그 뒤에 journal 기록 + +### Step 4. Journal 기록 + +정식 manifest 기준으로만 기록한다. + +필수 확인: + +- `experiment_name == manifest filename stem` +- `train / valid / test` 결과가 모두 존재 +- hypothesis / config_delta / verdict가 비어 있지 않음 + +### Step 5. WFV / Robustness + +deploy 후보는 아래를 붙인다. + +- walk-forward validation +- robustness matrix +- repaired out-of-time robustness + - 현재 기준 snapshot은 + [`midlarge-liquid-long-v1-oot-2020-2021`](/Users/yirugi/mycloud/personal/workspace/fithia2/data/datasets/snapshots/midlarge-liquid-long-v1-oot-2020-2021/manifest.json) + +그리고 tracker에 attach한다. + +### Step 6. Leaderboard 갱신 + +정식 manifest + journal + WFV/robustness attach까지 끝난 뒤에 leaderboard를 본다. + +public `SQS` 계산 상세와 디버그 절차는 +[`docs/public_sqs_calculation.md`](/Users/yirugi/mycloud/personal/workspace/fithia2/docs/public_sqs_calculation.md) +를 본다. + +## 6. 세션 종료 전 체크리스트 + +세션을 끝내기 전에 아래를 확인한다. + +1. 오늘 새 winner가 scratch만 있고 repo manifest가 없는가 +2. journal에 기록한 실험명이 실제 `configs/experiments/.json`과 대응하는가 +3. deploy 후보인데 WFV/robustness attach가 빠진 것이 없는가 +4. deploy 후보인데 repaired OOT attach가 빠진 것이 없는가 +5. leaderboard 상단 전략 중 repo manifest가 없는 것이 없는가 +6. default leaderboard에 named micro manifest가 보이지 않는가 +7. 새로 만든 run의 `snapshot_fingerprint.json`에서 manifest mismatch가 없는가 + +하나라도 `yes`면 세션 종료 전에 정리한다. + +## 7. 권장 명령 흐름 + +### Full Split + +```bash +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --output-root runs/_fullsplit +``` + +### Walk-Forward + +```bash +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --walk-forward \ + --wf-train-days 504 \ + --wf-test-days 63 \ + --wf-step-days 63 \ + --output-root runs/_wfv +``` + +### Robustness Matrix + +```bash +python apps/backtester/run.py \ + --manifest configs/experiments/.json \ + --snapshot-dir data/datasets/snapshots \ + --robustness-matrix \ + --rm-horizons 21,63,126,252,504 \ + --rm-step-days 21 \ + --output-root runs/_rm +``` + +### Attach Summaries + +```bash +python apps/tracker/cli.py attach-wfv \ + --journal-dir journal \ + \ + --summary runs/_wfv/walk_forward/walk_forward_summary.json + +python apps/tracker/cli.py attach-robustness \ + --journal-dir journal \ + \ + --summary runs/_rm/robustness_matrix/robustness_matrix_summary.json + +python apps/tracker/cli.py attach-oot-robustness \ + --journal-dir journal \ + \ + --summary runs/_oot_rm/robustness_matrix/robustness_matrix_summary.json +``` + +### Book Overlay Evaluation + +single-book manifest를 억지로 섞지 말고, 별도 book을 각각 먼저 고정 기간으로 돌린 뒤 +overlay를 따로 평가한다. + +1. 같은 기간, 같은 초기 자본으로 각 book의 equity curve를 만든다. +2. [`configs/overlays`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/overlays) 아래 spec에 + regime별 자본 배분을 적는다. +3. [`apps/tools/evaluate_book_overlay.py`](/Users/yirugi/mycloud/personal/workspace/fithia2/apps/tools/evaluate_book_overlay.py)로 + overlay equity와 summary를 만든다. +4. full-window spec/summary와 stress-window spec/summary를 모두 만든 뒤 + [`apps/tracker/cli.py`](/Users/yirugi/mycloud/personal/workspace/fithia2/apps/tracker/cli.py) + `record-overlay`로 공식 journal/leaderboard에 등록한다. + +주의: + +- overlay spec의 `books[].equity_csv`는 **재현 기준 입력**이다. +- 공식 평가와 [`fithia2 paper backtest --overlay`](/Users/yirugi/mycloud/personal/workspace/fithia2/apps/paper_trader/cli.py)는 + 둘 다 이 frozen curve를 우선 replay해야 한다. +- OOT overlay는 full-window csv를 재사용하면 안 된다. + `runs/book_overlay_oot_inputs/...`처럼 OOT 전용 book curve를 따로 만든다. +- `regime_source.snapshot_dir`가 explicit snapshot directory라면 evaluator는 그 디렉터리를 그대로 merged load해야 한다. + +예시: + +```bash +fithia2 paper backtest \ + --config configs/experiments/return_max_long_v6.221.json \ + --config configs/experiments/return_max_long_v6new.54.json \ + --capital 10000 \ + --start 2022-03-03 \ + --end 2026-03-13 \ + --output runs/book_overlay_v1_inputs \ + --no-trades + +python apps/tools/evaluate_book_overlay.py \ + --spec configs/overlays/return_book_overlay_v1.json \ + --output-dir runs/return_book_overlay_v1_eval + +python apps/tracker/cli.py record-overlay \ + --spec configs/overlays/return_book_overlay_v3.json \ + --summary runs/return_book_overlay_v3_eval/overlay_summary.json \ + --stress-spec configs/overlays/return_book_overlay_v3_oot.json \ + --stress-summary runs/return_book_overlay_v3_oot_eval/overlay_summary.json \ + --hypothesis "Official overlay candidate" +``` + +## 8. 빠른 무결성 점검 + +journal entry와 manifest 대응을 확인하려면: + +```bash +python - <<'PY' +import json +from pathlib import Path + +cfg = Path("configs/experiments") +missing = [] +for line in Path("journal/improvement_journal.jsonl").read_text().splitlines(): + if not line.strip(): + continue + obj = json.loads(line) + name = obj.get("experiment_name") + if name and not (cfg / f"{name}.json").exists(): + missing.append(name) + +print("missing", len(missing)) +for name in missing[-20:]: + print(name) +PY +``` + +이 출력은 항상 `0`이어야 한다. + +### Snapshot Provenance 점검 + +run 결과가 journal/leaderboard 숫자와 다르면 아래 두 파일을 먼저 본다. + +- `runs//snapshot_manifest.json` +- `runs//snapshot_fingerprint.json` +- 특히 `snapshot_manifest.json`의 `feature_version`, `export_enrichments`, `created_at_utc`를 같이 본다. + +핵심 필드: + +- `dataset_snapshot_id` +- `source_manifest_path` +- `snapshot_dir_name` +- `manifest_snapshot_id` +- `snapshot_id_matches_manifest` +- `output_dir_matches_manifest` + +`snapshot_id_matches_manifest=false` 또는 `output_dir_matches_manifest=false`면 +같은 snapshot id 아래 다른 내용이 덮어써졌거나, 잘못 복사된 manifest일 수 있다. +이 상태에서 성능 비교를 계속하면 안 된다. + +## 9. 다른 에이전트에게 넘길 때 남겨야 할 것 + +handoff에는 최소 아래를 포함한다. + +- 현재 active baseline manifest 경로 +- 현재 raw-return winner와 deploy winner +- 마지막으로 유효했던 개선 축 +- 실패한 축 3~5개 +- 아직 scratch 상태인 강한 후보가 있으면 그 manifest 경로 +- 진행 중인 WFV / robustness 실행 경로 + +즉, "무엇이 최고였는가"보다 "무엇이 먹혔고 무엇이 죽었는가"를 남겨야 한다. + +## 10. 운영 교훈 + +### 10.1 Disabled Named Micro도 오염이다 + +manifest에 named micro engine이 남아 있으면 `enabled: false`여도 clean 전략으로 보지 않는다. +실제로 old `v1.10~v1.13`은 disabled named micro가 남아 있었고, +이 때문에 현재 clean replacement로 [`return_max_long_v1.51.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.51.json), +[`return_max_long_v1.52.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.52.json) 을 분리했다. + +### 10.2 OOT는 최적화 목표가 아니라 품질 계층이다 + +`2020~2021` repaired OOT는 점수를 올리기 위한 최적화 목표가 아니라, +현재 전략이 특정 시기 구조에만 맞는지 확인하는 추가 품질 계층이다. +OOT를 보고 규칙을 직접 맞추기 시작하면 그 순간 또 오염된다. + +### 10.3 Snapshot 결함부터 의심한다 + +OOT가 전부 `0 trade`로 보이면 전략 탓만 하지 말고 snapshot 자체를 먼저 점검한다. +실제로 symbol 기반 export에서 `market_cap_proxy`, `exchange_proxy`가 비어 있던 버그가 있었고, +이를 고친 뒤에야 repaired OOT가 의미 있는 비교 지표가 됐다. + +### 10.4 Paper Backtest stale 판정은 manifest 나이가 아니라 coverage로 본다 + +`fithia2 paper backtest`는 snapshot이 오래됐다는 이유만으로 refresh하면 안 된다. +실제로는 기존 snapshot이 충분한 날짜 범위를 이미 덮고 있을 수 있다. + +현재 원칙: + +- stale 여부는 parquet `event_date` coverage로 판단한다 +- snapshot 경로는 `settings.parquet_dir`를 먼저 보고, 없으면 `data/datasets/snapshots`를 본다 +- refresh가 실패해도 기존 snapshot이 requested period를 덮으면 그대로 사용한다 + +즉 "manifest created_at이 오래됐다"는 이유만으로 refresh를 강제하면 안 된다. + +### 10.5 Screener 장애 fallback은 local-only여야 한다 + +snapshot export 중 live screener가 `500`으로 죽을 수 있다. +이때 조용히 다른 remote endpoint로 넘어가면, 시점마다 다른 값이 섞여 재현성이 깨진다. + +현재 원칙: + +- universe screener가 실패하면 우선 기존 local snapshot metadata를 deterministic하게 재사용한다 +- local fallback조차 없으면 export는 그대로 실패시킨다 +- 임의의 remote fallback으로 조용히 성공시키지 않는다 + +즉 장애 대응보다 재현성을 우선한다. + +## 11. 현재 상태 + +문서 작성 시점의 active clean 후보는 아래다. + +1. [`return_max_long_v1.51.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.51.json) +2. [`return_max_long_v1.52.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.52.json) +3. [`return_max_long_v1.53.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.53.json) + +현재 baseline은 `v1.51`이다. + +- full split `+37.9 / +35.3 / +42.7` +- WFV mean `+11.10%`, gap `29.67%` +- repaired OOT positive window rate `48.7%` + +최근 실패한 축: + +- [`return_max_long_v1.53.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.53.json) + - raw return은 좋아졌지만 WFV gap/OOT가 나빠져 탈락 +- [`return_max_long_v1.55.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.55.json) + - test는 좋아졌지만 repaired OOT가 크게 악화 +- [`return_max_long_v1.56.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.56.json), + [`return_max_long_v1.57.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.57.json), + [`return_max_long_v1.58.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v1.58.json) + - general-only 미세조정이었지만 no-op 또는 후퇴 + +## 12. 현재 예외 + +현재 문서 작성 시점의 active known exception은 없다. + +과거 `v797_v793_epam_rklb_alk_combo` 누락은 이미 역사적 교훈으로만 남기고, +현재 workflow에선 `journal ↔ manifest parity`와 auto-sync 규칙으로 막는다. + +## 13. 운영 원칙 요약 + +- scratch는 빠르게, official은 엄격하게 관리한다. +- journal에 적을 정도면 manifest도 repo에 있어야 한다. +- leaderboard 상단 전략은 반드시 재현 가능해야 한다. +- deploy 후보는 full split만으로 끝내지 않고 WFV/robustness/repaired OOT까지 붙인다. +- named micro는 disabled여도 clean 전략에 남겨두지 않는다. +- 세션 종료 전 `journal ↔ manifest` parity를 확인한다. +- snapshot provenance가 틀리면 점수 논쟁보다 먼저 metadata부터 고친다. diff --git a/journal/LEADERBOARD.md b/journal/LEADERBOARD.md index 020d771..e53e9f5 100644 --- a/journal/LEADERBOARD.md +++ b/journal/LEADERBOARD.md @@ -1,5 +1,5 @@ # Strategy Improvement Leaderboard -_Updated: 2026-03-27T03:14:36.477455+00:00_ +_Updated: 2026-03-27T03:16:32.096793+00:00_ _Default view excludes overlay/book-of-books rows, retired legacy PEAD / short-core / exact-pocket families, and incomplete train-only scans. Use `fithia2 lb --overlay-only` for overlays or `fithia2 lb --include-retired` to inspect archived research._ diff --git a/journal/OVERLAY_LEADERBOARD.md b/journal/OVERLAY_LEADERBOARD.md new file mode 100644 index 0000000..0c99eea --- /dev/null +++ b/journal/OVERLAY_LEADERBOARD.md @@ -0,0 +1,21 @@ +# Overlay Strategy Leaderboard +_Updated: 2026-03-27T03:16:32.096793+00:00_ + +_This board is separate from the default single-book leaderboard. Overlay rows are book-of-books evaluations and are not directly comparable to single-book `SQS` rows._ + +_`SQS` here is overlay official SQS: common-window overlay score with a stress OOT gate. `T.*` columns are common-window overlay metrics._ + +| # | Overlay | SQS | [T]Ret% | [T]Ann% | [T]DD% | Stress Ret% | Stress DD% | Stress Sharpe | Date | +|---|---------|-----|----------|----------|--------|-------------|------------|---------------|------| +| 1 | return_book_overlay_v3 | 90.2 | +175.6 | +28.6 | 4.5 | +6.0 | 5.4 | +0.67 | 2026-03-26 | +| 2 | return_book_overlay_v3b | 87.7 | +169.5 | +27.9 | 5.0 | +7.4 | 5.8 | +0.80 | 2026-03-26 | + +## Recent Overlay Entries +### IMP-0738 (2026-03-26) — return_book_overlay_v3b +Hypothesis: Balanced overlay variant with 50/50 neutral allocation between v6.221 and v6new.54. +Verdict: **UNKNOWN** (SQS 87.7) + +### IMP-0737 (2026-03-26) — return_book_overlay_v3 +Hypothesis: Book-of-books overlay using v6.221 as stress fallback and v6new.54 as normal-regime book. +Verdict: **UNKNOWN** (SQS 90.2) + diff --git a/journal/experiment_registry.json b/journal/experiment_registry.json index 1d4b368..11e6675 100644 --- a/journal/experiment_registry.json +++ b/journal/experiment_registry.json @@ -195632,5 +195632,5 @@ "timestamp": "2026-03-26T14:56:24.543029+00:00" } ], - "updated_at": "2026-03-27T03:14:36.477455+00:00" + "updated_at": "2026-03-27T03:16:32.096793+00:00" } \ No newline at end of file diff --git a/libs/backtest/tracker.py b/libs/backtest/tracker.py index 380738e..a3cb83e 100644 --- a/libs/backtest/tracker.py +++ b/libs/backtest/tracker.py @@ -2092,32 +2092,34 @@ def _is_complete_journal_entry(entry: JournalEntry) -> bool: ) -def _load_optional_walk_forward_summary(experiment_name: str) -> WalkForwardSummary | None: - summary_path = Path("runs") / f"{experiment_name}_wfv" / "walk_forward" / "walk_forward_summary.json" +def _load_optional_walk_forward_summary( + experiment_name: str, + runs_dir: Path | None = None, +) -> WalkForwardSummary | None: + runs_root = runs_dir or Path("runs") + summary_path = runs_root / f"{experiment_name}_wfv" / "walk_forward" / "walk_forward_summary.json" if not summary_path.exists(): return None return WalkForwardSummary.model_validate_json(summary_path.read_text()) -def _load_optional_robustness_summary(experiment_name: str) -> RobustnessMatrixSummary | None: - summary_path = ( - Path("runs") - / f"{experiment_name}_rm" - / "robustness_matrix" - / "robustness_matrix_summary.json" - ) +def _load_optional_robustness_summary( + experiment_name: str, + runs_dir: Path | None = None, +) -> RobustnessMatrixSummary | None: + runs_root = runs_dir or Path("runs") + summary_path = runs_root / f"{experiment_name}_rm" / "robustness_matrix" / "robustness_matrix_summary.json" if not summary_path.exists(): return None return RobustnessMatrixSummary.model_validate_json(summary_path.read_text()) -def _load_optional_out_of_time_robustness_summary(experiment_name: str) -> RobustnessMatrixSummary | None: - summary_path = ( - Path("runs") - / f"{experiment_name}_oot_rm" - / "robustness_matrix" - / "robustness_matrix_summary.json" - ) +def _load_optional_out_of_time_robustness_summary( + experiment_name: str, + runs_dir: Path | None = None, +) -> RobustnessMatrixSummary | None: + runs_root = runs_dir or Path("runs") + summary_path = runs_root / f"{experiment_name}_oot_rm" / "robustness_matrix" / "robustness_matrix_summary.json" if not summary_path.exists(): return None return RobustnessMatrixSummary.model_validate_json(summary_path.read_text()) @@ -2256,7 +2258,7 @@ def sync_official_manifests( results.get("valid"), results.get("test"), ) - walk_forward_summary = _load_optional_walk_forward_summary(experiment_name) + walk_forward_summary = _load_optional_walk_forward_summary(experiment_name, runs_dir) wfqs_score, wfqs_breakdown = compute_wfqs(walk_forward_summary) wfqs_v2_score, wfqs_v2_breakdown = compute_wfqs_v2(walk_forward_summary) deployment_score, deployment_breakdown = compute_deployment_score( @@ -2267,8 +2269,8 @@ def sync_official_manifests( rqs_score=rqs_score, wfqs_score=wfqs_score, ) - robustness_matrix_summary = _load_optional_robustness_summary(experiment_name) - out_of_time_robustness_summary = _load_optional_out_of_time_robustness_summary(experiment_name) + robustness_matrix_summary = _load_optional_robustness_summary(experiment_name, runs_dir) + out_of_time_robustness_summary = _load_optional_out_of_time_robustness_summary(experiment_name, runs_dir) public_sqs, public_breakdown, _ = compute_public_sqs( results.get("train"), results.get("valid"), @@ -2645,12 +2647,15 @@ def _write_overlay_leaderboard_md( for rank, e in enumerate(visible_entries, 1): stress = e.overlay_stress_window_summary ts = e.timestamp[:10] if e.timestamp else "-" + stress_ret = f"{stress.return_pct:+.1f}" if stress and stress.return_pct is not None else "-" + stress_dd = f"{stress.max_drawdown_pct:.1f}" if stress and stress.max_drawdown_pct is not None else "-" + stress_sharpe = f"{stress.sharpe_ratio:+.2f}" if stress and stress.sharpe_ratio is not None else "-" lines.append( f"| {rank} | {e.experiment_name} | {e.sqs_score:.1f}" f" | {e.total_return_pct:+.1f} | {e.annualized_return_pct:+.1f} | {e.max_drawdown_pct:.1f}" - f" | {stress.return_pct:+.1f if stress and stress.return_pct is not None else '-'}" - f" | {stress.max_drawdown_pct:.1f if stress and stress.max_drawdown_pct is not None else '-'}" - f" | {stress.sharpe_ratio:+.2f if stress and stress.sharpe_ratio is not None else '-'}" + f" | {stress_ret}" + f" | {stress_dd}" + f" | {stress_sharpe}" f" | {ts} |" )