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) <noreply@anthropic.com>
main
I Luk Kim 5 months ago
parent 2aba6418e6
commit a81b3a6ac4

@ -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")

@ -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
```

@ -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/<experiment>_wfv/walk_forward/walk_forward_summary.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs)
### Main Robustness
[`runs/<experiment>_rm/robustness_matrix/robustness_matrix_summary.json`](/Users/yirugi/mycloud/personal/workspace/fithia2/runs)
### OOT Robustness
[`runs/<experiment>_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/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--walk-forward \
--wf-train-days 504 \
--wf-test-days 63 \
--wf-step-days 63 \
--output-root runs/<experiment>_wfv
python apps/backtester/run.py \
--manifest configs/experiments/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--robustness-matrix \
--rm-horizons 21,63,126,252,504 \
--rm-step-days 21 \
--output-root runs/<experiment>_rm
python apps/backtester/run.py \
--manifest configs/experiments/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--snapshot-id <oot_snapshot_id> \
--robustness-matrix \
--rm-horizons 21,63,126,252 \
--rm-step-days 21 \
--output-root runs/<experiment>_oot_rm
```
### 6.2 journal에 attach
```bash
python apps/tracker/cli.py attach-wfv \
--journal-dir journal \
<experiment> \
--summary runs/<experiment>_wfv/walk_forward/walk_forward_summary.json
python apps/tracker/cli.py attach-robustness \
--journal-dir journal \
<experiment> \
--summary runs/<experiment>_rm/robustness_matrix/robustness_matrix_summary.json
python apps/tracker/cli.py attach-oot-robustness \
--journal-dir journal \
<experiment> \
--summary runs/<experiment>_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가 붙은 후보끼리 우선 본다.

@ -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/<run_id>/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/<experiment_name>.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/<name>.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/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--output-root runs/<experiment>_fullsplit
```
### Walk-Forward
```bash
python apps/backtester/run.py \
--manifest configs/experiments/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--walk-forward \
--wf-train-days 504 \
--wf-test-days 63 \
--wf-step-days 63 \
--output-root runs/<experiment>_wfv
```
### Robustness Matrix
```bash
python apps/backtester/run.py \
--manifest configs/experiments/<experiment>.json \
--snapshot-dir data/datasets/snapshots \
--robustness-matrix \
--rm-horizons 21,63,126,252,504 \
--rm-step-days 21 \
--output-root runs/<experiment>_rm
```
### Attach Summaries
```bash
python apps/tracker/cli.py attach-wfv \
--journal-dir journal \
<experiment_name> \
--summary runs/<experiment>_wfv/walk_forward/walk_forward_summary.json
python apps/tracker/cli.py attach-robustness \
--journal-dir journal \
<experiment_name> \
--summary runs/<experiment>_rm/robustness_matrix/robustness_matrix_summary.json
python apps/tracker/cli.py attach-oot-robustness \
--journal-dir journal \
<experiment_name> \
--summary runs/<experiment>_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/<run_id>/snapshot_manifest.json`
- `runs/<run_id>/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부터 고친다.

@ -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._

@ -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)

@ -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"
}

@ -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} |"
)

Loading…
Cancel
Save