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.
541 lines
16 KiB
Markdown
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가 붙은 후보끼리 우선 본다.
|