15 KiB
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 blendsqs_v3_score: deployment/WFV-first primary rankstress_sqs_score: 예전 public score. stress OOT quality를 추가 패널티로 곱한 legacy score
핵심 결론부터 말하면:
test split만으로 나오는sqs_v2_score는 final publicSQS가 아니다.- final public
SQS v4의 backbone은 아래 6개다.trainsplitvalidsplittestsplit- 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
이고, 실제 계산은 compute_public_sqs_v4가 한다.
legacy stress-adjusted score는
compute_public_sqs_v2
가 그대로 유지한다.
v4 backbone인 v3 계산은
compute_public_sqs_v3
에 남아 있다.
public leaderboard / registry rebuild도 같은 함수를 쓴다:
즉, 수동 계산과 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 점수다.
이 값은 아래 함수에서 나온다:
반면 final public SQS v4의 backbone인 SQS v3는:
RQSWFQS 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는:
base_score * main_rb_gate * oot_gate * oot_quality_factor * activity_factor
였다. 이 구조는 stress OOT를
- eligibility gate로 한 번 쓰고
- quality penalty로 한 번 더 써서
최근/실전 적합도가 강한 전략을 과하게 깎을 수 있었다.
SQS v3는 다음처럼 바꿨다.
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를 섞는다.
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을 넣는다.
예:
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
Main Robustness
runs/<experiment>_rm/robustness_matrix/robustness_matrix_summary.json
OOT Robustness
runs/<experiment>_oot_rm/robustness_matrix/robustness_matrix_summary.json
journal에 attach된 경우엔 file path가 없어도 journal entry만으로 rebuild가 가능하다. 다만 디버그와 재현을 위해서는 run file도 남아 있는 쪽이 낫다.
4. 실제 계산 순서
public SQS v4는 아래 순서로 계산된다.
4.1 RQS 계산
split 3개에서
compute_rqs
를 계산한다.
4.2 WFV 기반 WFQS v2
walk-forward summary에서
compute_wfqs_v2
를 계산한다.
4.3 Deployment gate를 반영한 base score
코드 그대로 쓰면:
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
를 곱한다.
기준:
- 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
를 곱한다.
OOT는 스트레스 테스트라 threshold가 조금 느슨하다.
- 63d+ positive rate >= 50%
- 63d median >= 0.5%
- 252d median >= 3%
- worst return >= -15%
4.6 OOT quality는 보조지표
compute_oot_robustness_quality
로 0~100 quality를 계산한다.
SQS v3/v4에서는 이 값을 랭킹 점수에 곱하지 않는다.
대신 registry breakdown과 stress_sqs_score에서만 사용한다.
4.7 Activity factor
마지막으로
_public_activity_factor
를 곱한다.
이 단계 때문에:
- replacement 전략
- 지나치게 trade 수가 적은 rotation 전략
은 split/WFV가 좋아도 public 점수가 낮아질 수 있다.
4.8 Common-window blend
common-window summary가 있으면
compute_common_window_score
로 capital-growth score를 계산한 뒤:
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.7rqs_score = 83.5wfqs_v2_score = 48.6
그런데 final public SQS는 비어 있었다.
이유는 간단했다.
- walk-forward 있음
- main robustness 있음
- OOT robustness 없음
즉 tracker 기준으로는 아직 pending_validation이었다.
실제로:
score, breakdown, source = compute_public_sqs(...)
결과는:
score = Nonesource = "pending_validation"breakdown = {"requires_out_of_time_robustness": 1.0}
였다.
5.2 OOT를 붙인 뒤 최종 계산
OOT run:
OOT 주요 수치:
- overall positive
90.0% - overall worst
-4.59% 63d median 11.8%252d median 72.07%
최종 계산 결과:
RQS = 83.5WFQS 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
라서 실질적으로:
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 만들기
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
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으로 직접 계산
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 - run dir의
snapshot_fingerprint.json snapshot_manifest.json의export_enrichments가 다르면 같은 snapshot id 계열이라도 직접 비교 대상으로 섞지 않는다.
여기서 특히 아래를 본다.
dataset_snapshot_idsource_manifest_pathsnapshot_dir_namemanifest_snapshot_idsnapshot_id_matches_manifestoutput_dir_matches_manifest
둘 중 하나라도 false면,
같은 snapshot id 아래 다른 내용이 덮어써졌거나 잘못 복사된 manifest일 수 있다.
이 경우 점수 비교 전에 snapshot provenance부터 정리해야 한다.
8. 운영 원칙
- split-only
sqs_v2_score를 final publicSQS로 말하지 않는다. - leaderboard 숫자는 반드시
compute_public_sqs()경로로만 확인한다. - OOT가 빠진 후보는 “점수 미계산”이지 “점수 0”이 아니다.
- journal과 registry가 다르면 journal + tracker 함수가 source of truth다.
v4비교는 comparable common-window가 붙은 후보끼리 우선 본다.