You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fithia2/docs/public_sqs_calculation.md

541 lines
16 KiB
Markdown

# 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가 붙은 후보끼리 우선 본다.