|
|
# Public SQS Calculation
|
|
|
|
|
|
이 문서는 tracker가 최종 public `SQS`를 어떻게 계산하는지, 그리고 왜
|
|
|
split-only 점수와 final leaderboard 점수가 다를 수 있는지를 정리한다.
|
|
|
|
|
|
2026-04-07부터 기본 leaderboard는 **public `SQS v9`**을 쓴다.
|
|
|
|
|
|
- `SQS v9`: **3-pillar additive core** + regime adaptability (scenario-test RRS) (기본 rank)
|
|
|
- `SQS v8`: `SQS v7` backbone + reset common-window blend (이전 기본; 하위호환 유지)
|
|
|
- `SQS v7`: continuous OOT factor + 3-criterion deployment gate (v8 backbone)
|
|
|
- `SQS v6`: `SQS v3` + reset common-window
|
|
|
- `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 v9`의 backbone은 아래다.
|
|
|
- `train` / `valid` / `test` split (RQS 35%)
|
|
|
- walk-forward summary (WFQS_v2 40%)
|
|
|
- regime adaptability: scenario-test RRS → OOT quality → 50.0 neutral (25%)
|
|
|
- main robustness summary (multiplied gate)
|
|
|
- 여기에 comparable reset common-window summary가 있으면 CW blend 적용.
|
|
|
- reset summary 없이 comparable `10k common-window summary`만 있으면 common-window blend.
|
|
|
- `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` 이후다.
|
|
|
그 전 실험은 retired research로 남고, `--include-retired`에서만 본다.
|
|
|
- 다만 `stress_sqs_score`도 같이 계산해 registry에 남긴다.
|
|
|
|
|
|
## v9 핵심 변경사항 (2026-04-07)
|
|
|
|
|
|
### 배경: OOT 편향 문제
|
|
|
|
|
|
v8의 OOT factor는 2020-2021 COVID 특수 구간 성과에 의존한다:
|
|
|
- `compute_oot_factor_v2` = 0.75 + 0.25 × (oot_quality / 100)
|
|
|
- COVID-era에서 잘 된 전략은 최대 +25% 보너스, 저조한 전략은 최대 -25% 패널티
|
|
|
|
|
|
실증 문제 (v8 기준):
|
|
|
- v7.70: OOT quality 100 → factor 1.000 (+0pp 패널티 없음)
|
|
|
- v7.119: OOT quality 50.2 → factor 0.876 (-8.7점 감점)
|
|
|
- v7.119의 수익률/WFQS가 우수함에도 COVID 특이성으로 인한 의미 없는 감점
|
|
|
|
|
|
→ OOT를 multiplier에서 제거하고, 12개 합성 시장 환경(scenario-test) 점수로 대체.
|
|
|
|
|
|
### v9 공식
|
|
|
|
|
|
```
|
|
|
core = RQS × 0.35 + WFQS_v2 × 0.40 + regime_score × 0.25
|
|
|
|
|
|
v9_base = core × deployment_gate × rb_gate × activity_factor
|
|
|
```
|
|
|
|
|
|
#### 3 Pillars
|
|
|
|
|
|
| Pillar | 가중치 | 출처 | 측정 대상 |
|
|
|
|--------|--------|------|----------|
|
|
|
| **RQS** | 35% | `compute_rqs` | test split 성과: 수익률, Sharpe, DD, PF |
|
|
|
| **WFQS_v2** | 40% | `compute_wfqs_v2` | WFV 교차검증 일관성 + overfitting_penalty |
|
|
|
| **Regime Adaptability** | 25% | scenario-test RRS | 12개 가상 시장 환경 적응력 |
|
|
|
|
|
|
#### regime_score 결정 (fallback chain)
|
|
|
|
|
|
```python
|
|
|
if scenario_robustness_score is not None:
|
|
|
regime_score = scenario_robustness_score # scenario-test RRS (0-100)
|
|
|
elif oot_quality is not None:
|
|
|
regime_score = oot_quality # 기존 OOT quality (하위호환)
|
|
|
else:
|
|
|
regime_score = 50.0 # 중립 fallback
|
|
|
```
|
|
|
|
|
|
#### CW blend (v8과 동일)
|
|
|
|
|
|
reset common-window 또는 common-window가 comparable하면 15% weight blend.
|
|
|
|
|
|
### v9 시뮬레이션 (regime=50 중립 기준)
|
|
|
|
|
|
| 전략 | v8 SQS | v9 SQS (est.) | 변동 | 근거 |
|
|
|
|------|--------|---------------|------|------|
|
|
|
| v7.119 | 57.3 | ~61.0 | **+3.7** | OOT 패널티(-8.7점) 해소 |
|
|
|
| v17.72 | 53.6 | ~57.5 | +3.9 | OOT 패널티 해소 |
|
|
|
| v7.70 | 67.9 | ~68.4 | +0.5 | OOT 보너스(100) 제거되지만 높은 RQS 보상 |
|
|
|
| v19.1 | 69.9 | ~71.3 | +1.4 | RQS+WFQS 모두 우수 |
|
|
|
| v7.123 | 70.1 | ~68.6 | -1.5 | OOT 보너스 제거 |
|
|
|
|
|
|
### scenario-test 자동 실행
|
|
|
|
|
|
`fithia2 rescore-public`이 실행될 때, `scenario_robustness_score`가 없는 전략은
|
|
|
자동으로 12개 시나리오를 실행하여 RRS를 계산한다 (~10초/전략).
|
|
|
|
|
|
```bash
|
|
|
fithia2 rescore-public # scenario-test 자동 실행 포함
|
|
|
fithia2 scenario-test --config v7.70 # 수동으로 단일 전략 테스트
|
|
|
```
|
|
|
|
|
|
### overfit-check 분리
|
|
|
|
|
|
`fithia2 overfit-check` (7-30분 소요)는 SQS에 포함하지 않고 별도 필드로 관리:
|
|
|
|
|
|
```bash
|
|
|
fithia2 aoc <experiment> --report overfit_report.json # 기존 결과 attach
|
|
|
fithia2 aoc <experiment> --run [--quick] # 직접 실행
|
|
|
```
|
|
|
|
|
|
`JournalEntry.overfit_check_score` (0-100) + `overfit_check_breakdown`으로 저장.
|
|
|
leaderboard에서 SQS 옆 참고 정보로 표시.
|
|
|
|
|
|
---
|
|
|
|
|
|
## v7/v8 핵심 변경사항 (2026-04-07)
|
|
|
|
|
|
### 변경 1: OOT binary gate → continuous factor
|
|
|
|
|
|
기존 v3 backbone은 4개 기준의 pass/fail step function:
|
|
|
```
|
|
|
4/4 → 1.00, 3/4 → 0.85, 2/4 → 0.65, 1/4 → 0.40, 0/4 → 0.20
|
|
|
```
|
|
|
|
|
|
v7부터 `compute_oot_factor_v2` (continuous, [0.75, 1.00]):
|
|
|
```
|
|
|
factor = 0.75 + 0.25 * (oot_quality / 100.0)
|
|
|
```
|
|
|
|
|
|
- OOT quality score는 기존 `compute_oot_robustness_quality` 재활용 (COVID 조정 정규화 범위)
|
|
|
- 252d median 범위: [-15%, +10%] → 비례 점수 (기존 pass/fail 임계값 3.0% 폐기)
|
|
|
- 최대 페널티: 25% (기존 80%)
|
|
|
|
|
|
**이유**: COVID(2020-2021)는 단일 극단 이벤트. "orderly" 이벤트 전략이 COVID에서 저조한 건
|
|
|
설계 의도대로. binary gate는 역 인센티브 유발 (v7.123 사례: COVID 엔진 추가 → 수익률 하락).
|
|
|
|
|
|
### 변경 2: deployment gate 이중 페널티 제거
|
|
|
|
|
|
기존 deployment gate는 4개 기준 (positive_fold_rate, median, worst, **WFV gap**).
|
|
|
WFV gap은 이미 `_gap_penalty`에서 연속적으로 WFQS_v2에 반영됨.
|
|
|
|
|
|
v7부터 3개 기준:
|
|
|
```
|
|
|
pass_positive = positive_fold_rate_pct >= 70%
|
|
|
pass_median = median_return_pct >= 5%
|
|
|
pass_worst = worst_return_pct >= -5%
|
|
|
factor: {3: 1.00, 2: 0.85, 1: 0.65, 0: 0.40}
|
|
|
```
|
|
|
|
|
|
## 1. 어디서 계산되나
|
|
|
|
|
|
최종 public `SQS v9` 계산 진입점은
|
|
|
[`compute_public_sqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L568)
|
|
|
이고, 실제 계산은 [`compute_public_sqs_v9`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1762)가 한다.
|
|
|
|
|
|
`v8` backbone인 `v7` 계산은
|
|
|
[`compute_public_sqs_v7`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1312)
|
|
|
에 있다.
|
|
|
|
|
|
`v7` backbone인 `v3` 계산은
|
|
|
[`compute_public_sqs_v3`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1258)
|
|
|
에 남아 있다.
|
|
|
|
|
|
이전 reset CW 기본 표준 `v6` 계산은
|
|
|
[`compute_public_sqs_v6`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1552)
|
|
|
에 남아 있다 (하위호환).
|
|
|
|
|
|
multi-capital experimental score는
|
|
|
[`compute_public_sqs_v5`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1467)
|
|
|
에 남아 있다.
|
|
|
|
|
|
legacy stress-adjusted score는
|
|
|
[`compute_public_sqs_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1199)
|
|
|
가 그대로 유지한다.
|
|
|
|
|
|
public leaderboard / registry rebuild도 같은 함수를 쓴다:
|
|
|
|
|
|
- [`rebuild_registry`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L2929)
|
|
|
|
|
|
즉, 수동 계산과 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 v8`의 backbone인 `SQS v7/v3`는:
|
|
|
|
|
|
- `RQS`
|
|
|
- `WFQS v2`
|
|
|
- deployment gate
|
|
|
- main robustness gate
|
|
|
- OOT robustness factor (v7부터 continuous [0.75, 1.00])
|
|
|
- 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 왜 `v8`이 default인가
|
|
|
|
|
|
### v3 → v4/v5/v6 변천사
|
|
|
|
|
|
한동안 `v5`로 `10k/25k/100k` multi-capital common-window를 기본 점수에 섞었다.
|
|
|
하지만 이 방식은 연구 속도를 떨어뜨렸고, historical baseline까지 전부 같은 three-bucket으로
|
|
|
다시 채워 넣어야 공정 비교가 가능했다.
|
|
|
|
|
|
그래서 기본 rank는 `v4`(10k reset CW)로 돌렸다가, 이후 `v6`(reset CW + v3 backbone)로 올랐다.
|
|
|
|
|
|
### v6 → v7/v8 (2026-04-07)
|
|
|
|
|
|
`v6`는 OOT binary gate(5-step)와 deployment gate의 WFV gap 이중 페널티 문제가 있었다.
|
|
|
`v7`는 두 가지를 동시에 고쳤다:
|
|
|
|
|
|
1. OOT binary gate → continuous factor `[0.75, 1.00]`
|
|
|
2. deployment gate에서 WFV gap 기준 제거 (이미 `_gap_penalty`에서 반영)
|
|
|
|
|
|
`v8`은 `v7` backbone + reset common-window blend:
|
|
|
|
|
|
```text
|
|
|
final_v8 = v7_score * 0.80 + reset_common_window_score * 0.20 # reset CW 있을 때
|
|
|
final_v8 = v7_score * 0.80 + common_window_score * 0.20 # fallback: 10k CW
|
|
|
final_v8 = v7_score # fallback: CW 없음
|
|
|
```
|
|
|
|
|
|
multi-capital common-window summary는 계속 저장한다.
|
|
|
다만 이건 `default rank`가 아니라 `capital scalability` 진단용으로 본다.
|
|
|
|
|
|
### 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 v8`은 아래 순서로 계산된다.
|
|
|
|
|
|
### 4.1 RQS 계산
|
|
|
|
|
|
split 3개에서
|
|
|
[`compute_rqs`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L691)
|
|
|
를 계산한다.
|
|
|
|
|
|
### 4.2 WFV 기반 `WFQS v2`
|
|
|
|
|
|
walk-forward summary에서
|
|
|
[`compute_wfqs_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L979)
|
|
|
를 계산한다.
|
|
|
|
|
|
WFQS v2는 test fold 성과를 기반으로 한 base score에 4개의 multiplicative penalty를 곱한다:
|
|
|
|
|
|
- **overfitting_penalty** (`_overfitting_penalty`): win rate gap 기반. train/test 간 per-trade 정확도 차이 탐지.
|
|
|
- 0~3pp: 1.0 (정상 노이즈), 3~10pp: 1.0→0.85, 10~20pp: 0.85→0.60, >20pp: 0.5
|
|
|
- raw return gap 비교는 train/test 기간 차이(e.g. 504d vs 63d) 때문에 과적합이 없어도 큰 gap이 생기므로 사용하지 않는다
|
|
|
- 구 journal 데이터에 win rate 없으면 기존 `_gap_penalty`로 fallback
|
|
|
- **fold_variance_penalty**: test fold 수익률 CV (변동계수) 기반. 특정 시기에만 작동하는 전략 탐지.
|
|
|
- **trade_credibility**: fold당 거래 수 + win rate 이상값 탐지.
|
|
|
- **engine_reliability**: engine 신뢰도 비율.
|
|
|
|
|
|
### 4.3 Deployment gate를 반영한 base score
|
|
|
|
|
|
코드 그대로 쓰면 (v7/v8 기준):
|
|
|
|
|
|
```text
|
|
|
base_score = (RQS * 0.45 + WFQS_v2 * 0.55) * deployment_gate
|
|
|
```
|
|
|
|
|
|
deployment gate는 아래 **3개** 통과 개수로 정해진다 (v7부터 WFV gap 기준 제거).
|
|
|
|
|
|
- WFV positive fold rate >= 70%
|
|
|
- WFV median return >= 5%
|
|
|
- WFV worst return >= -5%
|
|
|
|
|
|
통과 개수별 factor:
|
|
|
|
|
|
- 3개: `1.00`
|
|
|
- 2개: `0.85`
|
|
|
- 1개: `0.65`
|
|
|
- 0개: `0.40`
|
|
|
|
|
|
> **v6 이하**에서는 4번째 기준(`WFV mean train-test gap <= 35%`)이 있어 5-step
|
|
|
> `{4:1.00, 3:0.85, 2:0.65, 1:0.40, 0:0.20}`이었다. v7부터 gap은 `_gap_penalty`에서
|
|
|
> WFQS_v2에 이미 연속 반영되므로 deployment gate에서 제거.
|
|
|
|
|
|
구현:
|
|
|
[`_compute_public_sqs_components_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1128)
|
|
|
|
|
|
### 4.4 Main robustness gate
|
|
|
|
|
|
[`compute_robustness_gate`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1860)
|
|
|
를 곱한다.
|
|
|
|
|
|
기준:
|
|
|
|
|
|
- overall positive window rate >= 65%
|
|
|
- 63d median >= 3%
|
|
|
- 252d median >= 8%
|
|
|
- overall worst return >= -12%
|
|
|
|
|
|
factor는 `1.00 / 0.85 / 0.65 / 0.40 / 0.20` 구조 (4-criterion 그대로).
|
|
|
|
|
|
### 4.5 OOT robustness factor (v7부터 continuous)
|
|
|
|
|
|
[`compute_oot_factor_v2`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L2084)
|
|
|
를 곱한다.
|
|
|
|
|
|
v7/v8에서는 binary gate(5-step) 대신 continuous factor `[0.75, 1.00]`:
|
|
|
|
|
|
```text
|
|
|
oot_factor = 0.75 + 0.25 * (oot_quality / 100.0)
|
|
|
```
|
|
|
|
|
|
- `oot_quality`는 기존 `compute_oot_robustness_quality` 재활용 (COVID 조정 정규화 범위)
|
|
|
- 최소 0.75 (최악 OOT에서도 25% 감점) ← 기존 최대 80% 감점 대비
|
|
|
|
|
|
OOT quality 정규화 범위:
|
|
|
|
|
|
| 지표 | 낮음 (0점) | 높음 (100점) |
|
|
|
|------|-----------|-------------|
|
|
|
| 252d median | -15% | +10% |
|
|
|
| 63d median | -8% | +3% |
|
|
|
| positive_rate 63d+ | 20% | 70% |
|
|
|
| worst_return | -30% | -5% |
|
|
|
|
|
|
예외: OOT summary 전 horizon에서 `all-zero sparse no-trade` 패턴이면
|
|
|
`factor = 1.0` 중립 처리 (stress에서 거래하지 않은 것 = 전략 설계 의도).
|
|
|
|
|
|
> **v6 이하**에서는 `compute_oot_robustness_gate`
|
|
|
> (L[1945](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L1945))의
|
|
|
> binary 4-criterion gate를 사용했다. v7부터 이 함수는 하위호환 유지용으로만 남음.
|
|
|
|
|
|
### 4.6 OOT quality — v7/v8에서 factor로 사용
|
|
|
|
|
|
[`compute_oot_robustness_quality`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L2033)
|
|
|
로 `0~100` quality를 계산한다.
|
|
|
|
|
|
v7/v8에서는 이 값이 **OOT factor**에 비례 반영된다 (위 4.5 참조).
|
|
|
`SQS v3`/`v4`/`v5`/`v6`에서는 랭킹 점수에 곱하지 않고 registry breakdown과
|
|
|
`stress_sqs_score`에서만 사용했었다.
|
|
|
|
|
|
### 4.7 Activity factor
|
|
|
|
|
|
마지막으로
|
|
|
[`_public_activity_factor`](/Users/yirugi/mycloud/personal/workspace/fithia2/libs/backtest/tracker.py#L940)
|
|
|
를 곱한다.
|
|
|
|
|
|
이 단계 때문에:
|
|
|
|
|
|
- replacement 전략
|
|
|
- 지나치게 trade 수가 적은 rotation 전략
|
|
|
|
|
|
은 split/WFV가 좋아도 public 점수가 낮아질 수 있다.
|
|
|
|
|
|
### 4.8 Common-window blend (v8 우선순위)
|
|
|
|
|
|
v8는 아래 우선순위로 CW blend를 적용한다:
|
|
|
|
|
|
**1순위: reset common-window (`v8_continuous_oot+reset_common_window`)**
|
|
|
|
|
|
comparable reset CW summary가 있으면:
|
|
|
|
|
|
```text
|
|
|
final_v8 = v7_score * 0.80 + reset_common_window_score * 0.20
|
|
|
```
|
|
|
|
|
|
**2순위: 10k common-window (`v8_continuous_oot+common_window`)**
|
|
|
|
|
|
reset CW 없고 comparable 10k CW summary가 있으면:
|
|
|
|
|
|
```text
|
|
|
final_v8 = v7_score * 0.80 + common_window_score * 0.20
|
|
|
```
|
|
|
|
|
|
**fallback: bare v7 (`v8_fallback_v7`)**
|
|
|
|
|
|
CW summary 없으면 `v7_score` 그대로.
|
|
|
|
|
|
개별 `common_window_score`는 같은 연속 기간, 같은 초기 자본으로 돌린 run의:
|
|
|
|
|
|
- total return
|
|
|
- profit factor
|
|
|
- Sharpe
|
|
|
- max drawdown
|
|
|
- return on gross exposure
|
|
|
- capital velocity
|
|
|
|
|
|
를 합성한 값이다.
|
|
|
|
|
|
### 4.9 Multi-capital common-window diagnostics
|
|
|
|
|
|
`10k/25k/100k` common-window summary가 모두 comparable이면
|
|
|
multi-capital score도 계산해 registry에 저장한다.
|
|
|
|
|
|
multi-capital score는:
|
|
|
|
|
|
- `10k` 점수 `60%`
|
|
|
- `25k` 점수 `25%`
|
|
|
- `100k` 점수 `15%`
|
|
|
- 세 bucket 중 최저 점수를 floor로 `10%` blend
|
|
|
|
|
|
구조다.
|
|
|
|
|
|
이 값은 현재 **기본 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`이었다.
|
|
|
|
|
|
실제로:
|
|
|
|
|
|
```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 252 \
|
|
|
--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`](/Users/yirugi/mycloud/personal/workspace/fithia2/data/datasets/snapshots/midlarge-liquid-long-v1-oot-2020-2021/manifest.json)이다.
|
|
|
다만 `pre_event_hurst_60d`, `pre_event_entropy_60d`, `pre_event_market_temperature` 같은
|
|
|
`tier2/tier3` feature를 실제 선택 규칙에 쓰는 전략은
|
|
|
[`midlarge-liquid-long-v1-oot-2020-2021_tier3`](/Users/yirugi/mycloud/personal/workspace/fithia2/data/parquet/midlarge-liquid-long-v1-oot-2020-2021_tier3/manifest.json)
|
|
|
처럼 feature-matched OOT snapshot을 써야 한다.
|
|
|
|
|
|
### 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도 같게 맞춘다
|
|
|
- 현재 official `v8` 기준값은 `10,000`이다
|
|
|
- partial backfill 상태에선 `v8`와 `v7 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` (WFV gap이 크면 `_gap_penalty`로 크게 감점)
|
|
|
- deployment gate (3-criterion: positive_rate, median, worst)
|
|
|
- robustness gate
|
|
|
- OOT factor (breakdown의 `oot_quality` 값 확인; v7/v8에서 0.75~1.00)
|
|
|
- activity factor
|
|
|
- common-window summary 존재 여부와 comparable window 설정
|
|
|
|
|
|
특히 rotation / low-activity 전략은 activity factor 때문에 public 점수가 크게 깎일 수 있다.
|
|
|
|
|
|
반대로 연속 구간 복리 수익이 강한 전략은 reset common-window가 붙으면 더 올라갈 수 있다.
|
|
|
|
|
|
### registry와 수동 계산이 다르면
|
|
|
|
|
|
아래를 본다.
|
|
|
|
|
|
- journal entry에 attach가 실제로 되었는가
|
|
|
- registry가 rebuild 되었는가 (`rsp` 또는 `python -m apps.tracker.cli leaderboard`)
|
|
|
- 같은 experiment 이름의 중복 journal entry 중 최신 exact match가 선택되었는가
|
|
|
- common-window가 일부 전략에만 backfill된 partial 상태가 아닌가
|
|
|
- `sqs_score` 필드가 최신 v8 기준인지 확인 (이전 v6 기준 registry 캐시가 남아 있을 수 있음)
|
|
|
|
|
|
### 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()` 경로로만 확인한다.
|
|
|
- WFV/robustness/OOT 또는 scenario-test가 없는 후보는 “점수 미계산”이지 “점수 0”이 아니다.
|
|
|
- journal과 registry가 다르면 journal + tracker 함수가 source of truth다.
|
|
|
- `v9` 비교는 comparable common-window가 붙은 후보끼리 우선 본다.
|
|
|
- scenario-test RRS가 없으면 OOT quality로 fallback, 없으면 50.0 중립.
|
|
|
- deployment gate는 WFV gap을 포함하지 않는다 (이미 WFQS_v2 `_gap_penalty`에서 반영).
|
|
|
- **SQS 점수는 날짜 간 비교 불가** — Yahoo Finance adjusted close 비결정성으로 ±15pp 변동. 같은 날 계산된 점수끼리만 상대 비교 신뢰.
|
|
|
- `overfit-check` 점수는 SQS에 포함되지 않으며, 별도 필드(`overfit_check_score`)로 관리한다.
|