# 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 핵심 결론부터 말하면: - `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 경로는 retired 상태다 overlay/book-of-books 평가는 코드베이스에서 제거됐다. - 기본 leaderboard는 single-book 전략만 포함한다. - journal에 남아 있는 과거 overlay entry는 historical record로만 취급한다. - registry rebuild와 `fithia2 lb`는 overlay entry를 건너뛴다. ## 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가 붙은 후보끼리 우선 본다.