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

17 KiB

Public SQS Calculation

이 문서는 tracker가 최종 public SQS를 어떻게 계산하는지, 그리고 왜 split-only 점수와 final leaderboard 점수가 다를 수 있는지를 정리한다.

2026-03-27부터 기본 leaderboard는 다시 **public SQS v4**를 쓴다.

  • SQS v4: SQS v3 + comparable 10k common-window
  • SQS v5: optional diagnostic. SQS v3 + multi-capital 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 10k common-window summary가 있으면 v4가 계산된다.
  • multi-capital common-window는 diagnostics로는 저장하지만, 기본 rank에는 쓰지 않는다.
  • optional comparable capital buckets은 현재 10,000 / 25,000 / 100,000이다.
  • official comparable common-window date range는 현재 2022-03-02 ~ 2026-03-24다.
  • 기본 leaderboard active window는 현재 IMP-0606 / return_max_long_v6new.29 이후다. 그 전 실험은 archived research로 남고, --include-retired에서만 본다.
  • 다만 stress_sqs_score도 같이 계산해 registry에 남긴다.

1. 어디서 계산되나

최종 public SQS v4 계산 진입점은 compute_public_sqs 이고, 실제 계산은 compute_public_sqs_v4가 한다.

multi-capital experimental score는 compute_public_sqs_v5 에 남아 있다.

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_scoretest split 하나만 보고 계산한 split-level quality 점수다.

이 값은 아래 함수에서 나온다:

반면 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는:

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 default로 돌렸나

한동안 v510k/25k/100k multi-capital common-window를 기본 점수에 섞었다. 하지만 이 방식은 연구 속도를 떨어뜨렸고, historical baseline까지 전부 같은 three-bucket으로 다시 채워 넣어야 공정 비교가 가능했다.

그래서 기본 rank는 다시 v4로 되돌렸다.

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

를 합성한 값이다.

multi-capital common-window summary는 계속 저장한다. 다만 이건 default rank가 아니라 capital scalability 진단용으로 본다.

validation stack 중 하나라도 없으면 public SQSNone

아래 세 개 중 하나라도 빠지면 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%

예외가 하나 있다. OOT robustness summary가 전 horizon에서 mean/median/worst/positive_rate/dd = 0all-zero sparse no-trade 패턴이면, 이건 stress에서 망했다가 아니라 그 stress 구간에서 전략이 아예 발동하지 않았다는 뜻으로 본다. 이 경우 OOT gate는 1.0으로 중립 처리하고, OOT quality는 비교 불가로 남긴다.

4.6 OOT quality는 보조지표

compute_oot_robustness_quality0~100 quality를 계산한다.

SQS v3/v4/v5에서는 이 값을 랭킹 점수에 곱하지 않는다. 대신 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

으로 남는다.

4.9 Multi-capital common-window diagnostics

10k/25k/100k common-window summary가 모두 comparable이면 compute_multi_capital_common_window_score 로 multi-capital score를 계산한 뒤:

final_v5 = v3_score * 0.80 + multi_capital_common_window_score * 0.20

를 적용한다.

multi-capital score는:

  • 10k 점수 60%
  • 25k 점수 25%
  • 100k 점수 15%
  • 세 bucket 중 최저 점수를 floor로 10% blend

구조다.

최근 v6new.34x ~ 37x lineage는 이 기준으로 10k/25k/100k를 다시 backfill한 상태다. 다만 이 값은 현재 기본 leaderboard rank가 아니라, 자본 규모 민감도와 scalability를 보는 보조 진단이다.

5. return_max_long_v6new.29 / v6new.28 같은 사례를 어떻게 읽어야 하나

v6new.29는 split/WFV/deployment도 강하고, 10k 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이었다.

실제로:

score, breakdown, source = compute_public_sqs(...)

결과는:

  • score = None
  • source = "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.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

라서 실질적으로:

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

기본 <oot_snapshot_id>midlarge-liquid-long-v1-oot-2020-2021이다. 다만 pre_event_hurst_60d, pre_event_entropy_60d, pre_event_market_temperature 같은 tier2/tier3 feature를 실제 선택 규칙에 쓰는 전략은 midlarge-liquid-long-v1-oot-2020-2021_tier3 처럼 feature-matched OOT snapshot을 써야 한다.

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도 같게 맞춘다
    • 현재 official v4 기준값은 10,000이다
  • partial backfill 상태에선 v4v3 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를 본다.

여기서 특히 아래를 본다.

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