이 저장소는 채택만이 아니라 기각도 함께 싣는다. README 는 결론만 적고, 그 결론이 나오기까지의 자취는 이 파일에 남긴다 — 틀린 채로 발표했던 두 숫자, 폐기하기 전까지 표제였던 전략, 사전등록하고 전부 기각한 자산배분 9개 구성.
PBO 는 집계 주기와 블록 수에 따라 판정이 뒤집힌다. 이 저장소에서 실측한 값이다:
일별(4,176행) S=8/10/12/16 → 0.657 / 0.524 / 0.599 / 0.544 전부 탈락
월별(201개월) S=8/10/12/16 → 0.314 / 0.155 / 0.294 / 0.278 전부 통과
seed 를 0~5 로 바꿔도 소수점 셋째 자리까지 같다 — 난수 잡음이 아니라 방법 선택이 결론을 정하는 자리다. 그래서 근거를 대고 골라야 한다.
CSCV 원논문(Bailey·Borwein·López de Prado·Zhu 2016)이 기준을 준다 — Sharpe 를 쓴다면 “IID Normal 가정이 성과의 여러 조각에서 유지될 수 있어야” 하고, S=16 이 대부분의 경우 적당하다. 네 주기를 그 기준으로 재면:
| 주기 | 관측 | 독립성 (VR−1 절댓값) | 정규성 (초과첨도) | PBO (S=16) |
|---|---|---|---|---|
| 일별 | 4,176 | 0.068 최상 | 16.29 최악 | 0.544 ✗ |
| 주별 | 835 | 0.092 | 13.19 | 0.538 ✗ |
| 월별 | 198 | 0.109 양호 | 8.90 양호 | 0.303 ✓ |
| 분기별 | 66 | 0.345 최악 | 2.79 최상 | 0.675 ✗ |
두 조건을 동시에 만족하는 것은 월별뿐이다. 일별은 초과첨도 16 이라 Sharpe 추정 자체가 성립하지 않고, 분기별은 lag-1 자기상관 0.218 에 관측이 66개다.
일별이 왜 틀리는지도 기전이 측정됐다. 분산비 VR(21)이 구성마다 0.78~1.45 로
다르다(35개 중 20개가 1.2 초과). VR>1 은 일별이 위험을 과소평가한다는 뜻이라,
일별 Sharpe 로 35개를 줄세우면 각자 다른 크기로 부풀린 값을 비교하게 된다.
증거는 VR 과 순위변동의 상관 +0.659 다 — quantus_ens3(VR 1.44)는 일별
6위에서 월별 15위로 내려가고, 운용 후보 E안(VR 0.99)은 일별 13위에서 월별
4위로 올라온다.
첫 번째는 유리한 쪽으로 틀렸고 두 번째는 불리한 쪽으로 틀렸다. 방향은 달라도 원인은 같다 — 근거 없이 하나를 골랐다.
남은 한계. 월 경계를 하루씩 미는 21개 오프셋에서 PBO 는 0.143~0.468 이다. 전부 관문을 넘지만 최악값이 0.5 에 가깝다. 그리고 35개는 서로 다른 유니버스를 쓰는 이질적 구성인데, CSCV 는 본래 한 전략의 파라미터 변형을 전제한다. 이 한계를 없애는 길은 하나뿐이다 — 하나를 미리 골라 단일 가설로 사전등록하는 것.
uv run python scripts/strategy_search_cost.py # 위 표를 그대로 재현한다
같은 전략, 같은 구간. 다른 것은 슬리피지·최소 주가·최소 거래대금 셋뿐이다. 파란 선이 2026-08-16 까지 이 README 의 표제였다.
2026-08-16 까지 이 자리에는 초소형주 8팩터 전략이 CAGR 23.78% · Sharpe 1.047 로 걸려 있었다. 설계 문서가 필수라고 적은 방어 장치 셋(슬리피지 · 최소 주가 $5 · 최소 거래대금 $1M)을 켜고 다시 재자 Sharpe 1.047 → −0.224 · MDD −23.7% → −99.2% 로 무너졌다.
원인은 실측으로 갈렸다 — 방어를 켜면 유니버스의 98% 가 사라진다. 분기말 기준 후보가 15~43종목만 남아, “1,000개 중 상위 20개”가 아니라 “존재하는 전부”를 담게 된다. 팩터 전략이 아니게 되는 것이다. 실제 보유 종목의 일 거래대금 중앙값은 약 $45k 이고 그중 둘은 0 이었다. 원금 상한이 1억 남짓이라는 뜻이다.
같은 검증에서 슬리피지는 문제가 아니었다 — 150bps 라는 가혹한 가정에서도 DSR 0.995 로 관문을 넘는다. 무너뜨린 것은 유동성 필터다.
그래서 운용 후보를 대형주 E안으로 바꿨다. E안은 원래 “수익률이 낮다” 는 이유로 밀려 있었는데, E안은 처음부터 방어가 켜져 있었고 초소형주 전략은 꺼져 있었다 — 두 전략을 같은 조건으로 비교한 적이 없었다. 같은 조건에서는 DSR 0.996 vs 0.002 다.
전 과정은 docs/factor-system/07-experiment-log.md
§5.5·§5.8 에 있다.
왼쪽 위가 좋다. 파란 점이 채택한 것, 빨간 점이 폐기한 것이다. 아래쪽의
VAA-G4 · BAA · 60/40 은 이 저장소가 만들어 놓고 채택하지 않은 전술적
자산배분 구성들이다 — PBO 0.770 으로 관문에서 탈락했다
(docs/taa/01-results.md).
구간이 서로 다르다 — 팩터 계열은 2002-12~2026-08, TAA 계열은 2008-07~2026-08(218개월)에서 쟀다. 그래서 여기 SPY(−41.8%)는 위 성과표의 SPY(−55.2%)와 다른 숫자다 — 2008년 하락의 앞부분이 TAA 창 밖에 있다. 같은 눈금 위에 있다고 같은 시험을 친 것은 아니다.
엔진도, 158팩터 정의도, 채택 파라미터도 configs/ 에 그대로 있다. 한때
감췄다가 열었다 — 감춘 레시피(초소형주)는 방어 장치를 켜면 무너져 실제로 굴릴
물건이 아니었고, 실제로 쓸 대형주 전략은 용량 제약이 없어 감출 이유가 없었다.
경위는 configs/README.md.
구조적으로 튼튼한 것: 룩어헤드 없음(파라미터는 학습 구간 안에서만 고르고 검증 구간은 한 번만 실행), 생존편향 없음(폐지 종목이 유니버스에 있다), 재공시 미사용 (최초 공시 우선), 수수료·슬리피지·유동성 필터 모두 켠 상태.
남은 한계:
| 항목 | 내용 |
|---|---|
| 구간 | 2002-12 이후 23.6년, walk-forward 24폴드. 그 안의 대형 하락은 2008·2020·2022 세 번뿐이다 |
| 데이터 정지 | 2026-08-14 에서 멈춰 있다 (구독 종료). 이후 갱신 없음 |
| 세금 | 모델링하지 않았다 |
| 거래비용 | 15bps 와 50bps 두 가정을 모두 보고한다. 실제 스프레드를 보유 종목으로 재보지는 않았다 |
Deflated Sharpe 가 이 표에서 가장 중요한 숫자다. 같은 데이터로 여러 번 시도해 고른 결과에서, 순수한 노이즈만으로 기대되는 최대 Sharpe 를 빼고 남는 값이다. 0.95 를 넘지 못하면 채택하지 않는다 — 실제로 이 저장소에서 20개 넘는 전략이 이 관문에서 탈락했다.
전 과정 기록은 docs/factor-system/07-experiment-log.md 에 있다.
전략은 JSON 한 파일로 완전히 선언된다. 아래는 폐기된 초소형주 전략
(configs/strategy_quantus_timed.json)이다 — 방어 장치를 끈 상태가 어떤 모습인지
보이려고 남긴다. 운용 후보는 configs/strategy_lean_timed.json 이다.
{
"factors": ["PER", "PSR", "POR", "PGPR",
"NETINC_GROWTH_YOY", "OPINC_GROWTH_YOY",
"GP_GROWTH_YOY", "REVENUE_GROWTH_YOY"],
"universe": {
"min_mcap_usd": 5000000, "max_mcap_usd": 80000000,
"min_price_usd": 0.0, // ⚠ 설계 문서는 $5 를 필수라 적었다
"min_adv_usd": 0.0, // ⚠ 설계 문서는 $1M 을 필수라 적었다
"exclude_financials": true, "exclude_distressed": true
},
"backtest": {
"n_stocks": 20, "rebalance": "QE", "weighting": "equal",
"max_weight": 0.06,
"cost": {"commission_bps": 50, "slippage_bps": 0} // ⚠ 기본값은 10
},
"timing_ma_days": 200, // 마켓타이밍 오버레이
"timing_reentry_days": 5
}
⚠ 표시한 셋은 설계 문서가 필수라고 적은 방어 장치를 끈 것이다. 켜면 이 전략은 무너진다 (Sharpe 1.047 → −0.22). 그 검증이
07-experiment-log.md§5.5 다. 이 설정을 그대로 쓰지 마시라 — 무엇이 왜 문제인지 보이려고 공개한다.
구현과 채택은 다르다. 아래는 전부 테스트와 함께 들어와 있고, 판정 칸은 walk-forward 가 이 유니버스에서 내린 답이다.
| 기법 | 판정 |
|---|---|
| 마켓타이밍 오버레이 (Faber 200일 이평) | 채택 — 낙폭 −63.8% → −23.7% (초소형주 기준. 운용 후보 E안도 이 오버레이를 쓴다) |
| 균등가중 | 채택 — 최적화 6종을 전부 이겼다 (DeMiguel 1/N) |
매매 유예구간 (hold_multiple) |
기각 — 회전율 −23% 대신 수익 −0.86%p |
| 레짐 조건부 팩터 가중 | 기각 — 16.90% → 15.45%, 레짐당 표본 부족 |
| 변동성 타게팅 (Moreira & Muir 2017) | 기각 — 단독으로 쓰면 타이밍을 아예 안 한 것보다 나쁘다 (Sharpe 0.513 → 0.396) |
파라미터 앙상블 (--ensemble k) |
기각 — 표에서 CAGR 은 제일 높지만 낙폭 −23.7% → −30.6%, Calmar 0.71 → 0.60 |
섹터 비중 상한 (max_sector_weight) |
성과 중립 — 차이가 0 과 구분되지 않는다 (t = 0.77). 수익 장치가 아니라 위험 장치로 남긴다 |
| 학습 구간 내 팩터 선별 (IC / 잔차 기여도) | 미채택 — 둘 다 고정 8팩터와 차이를 잴 수 없다 (t ≈ 0.5). 고정 쪽이 DSR 이 높고 움직이는 부품이 적다 |
뒤 세 개는 정교해 보여서가 아니라 측정이 가리켜서 만들었다 — 섹터 상한은 실제 보유 포트폴리오가 Technology 32% 로 드러난 뒤에 썼다. 그건 아무도 선택하지 않은 매크로 베팅이다.
Wouter Keller 의 Vigilant Asset Allocation(2017)이 출발점이었다.
모멘텀 = 12·R(1M) + 4·R(3M) + 2·R(6M) + 1·R(12M) ← Keller 13612W
SPY·EFA·EEM·AGG) 중 모멘텀 1위를 산다.LQD·IEF·SHY) 중 1위로 전환한다 (breadth 규칙).논문은 연 16~17% 를 적었는데 실측은 연 6.07% 였다 (2008-07~2026-08, 218개월). 구현이 틀린 게 아니다 — 원인을 재보면 이렇다.
| 무엇 | 실측 |
|---|---|
| 전체 기간 중 방어 자산으로 보낸 비중 | 55.7% |
그중 SHY 한 종목에 묶인 기간 |
44개월 — 당시 연 0.05%, 사실상 현금 |
| Keller 의 검증 구간 | 1970~2015 — 그 시절 방어 자산이 연 8~15% 를 줬다 |
즉 VAA 는 절반 이상을 “안전한 곳”에 두는 전략인데, 2008년 이후의 안전한 곳은 수익이 0 이다. 전제가 시대와 안 맞는다. 상세: 설계 문서 §0.
별개로, 이전 버전 README 는 Sharpe 를 20.9배 부풀려 적고 있었다 — 월별 수익률을 √252 로 연율화한 버그다(정상은 √12,
b9043d7수정). 성과는 맞는데 위험지표만 틀린 상태라 오래 살아남았다. 회귀 테스트:tests/test_risk_annualization.py.
VAA 의 진짜 문제는 공격 4자산이 투자 대상이면서 동시에 경보기라는 점이다.
2011~2026 에 EEM·EFA 는 만성적으로 약했고, 그래서 살 생각도 없던 자산의
약세가 포트폴리오 전체를 방어로 밀어냈다. 그동안 SPY 는 올랐다.
이 진단은 우리가 만든 게 아니다 — Keller 본인이 같은 문제를 인정하고 후속 논문을 냈다. 그 계보가 존재한다는 사실 자체가 진단의 독립 확인이다.
| 연도 | 전략 | 무엇을 바꿨나 | 여기서 |
|---|---|---|---|
| 2017 | VAA | (문제의 원형) | 기준선으로 구현 |
| 2018 | DAA | 카나리아 유니버스 — 경보기를 투자 대상에서 분리 | 미구현 — 규칙 출처가 404 였다. 확인 못 한 명세는 구현하지 않는다 (CLAUDE.md §4) |
| 2022 | BAA | 카나리아 + 별도 공격군 + 확장 방어군 + 선택 지표 교체 | 여기를 중심으로 6개 변형 |
| 2023 | HAA | 인플레이션·금리 상승기 대응 | 미구현 — 제외 이유가 기록되지 않았다. 설계 문서가 계보 표에 적어놓고 그대로 지나갔다 |
마지막 줄은 이 라운드의 미완이다. 빠뜨린 것을 빠뜨렸다고 적어둔다.
BAA 는 카나리아(경보 자산)를 투자 대상에서 분리한다. 그것을 중심으로 9개 구성을 결과를 보기 전에 등록하고 한 번에 쟀다.
| 구성 | CAGR | MDD | Calmar | DSR |
|---|---|---|---|---|
spy (기준선) |
12.46% | −41.8% | 0.298 | 0.968 |
static_60_40 (기준선) |
8.87% | −25.1% | 0.354 | 0.986 |
vaa_g4 |
6.07% | −20.9% | 0.290 | 0.871 |
baa_agg |
8.82% | −16.5% | 0.535 | 0.976 |
baa_bal |
7.28% | −11.1% | 0.654 | 0.994 |
baa_agg_ma |
8.81% | −13.4% | 0.656 | 0.985 |
baa_bal_tranche |
8.11% | −11.3% | 0.717 | 0.996 |
baa_bal_ma_tranche |
7.32% | −10.0% | 0.731 | 0.997 |
baa_bal_ma |
6.72% | −8.3% | 0.812 | 0.997 |
PBO = 0.770 → 채택 0건. 완화하지 않았다.
PBO 0.77 은 “이 9개 중 인샘플 1등을 뽑아도 아웃샘플에서 중간 이하로 떨어질 확률이 77%” 라는 뜻이다. “아무것도 안 통한다”는 뜻이 아니다 — 그리고 이 구분이 이 섹션에서 가장 중요하다. 실제로 BAA 계열 6개는 예외 없이 전부 60/40 을 Calmar 로 이긴다(0.535~0.812 vs 0.354). 동시에 그 6개끼리만 PBO 를 재면 0.861 로, 어느 것이 최선인지는 이 데이터로 고를 수 없다.
두 문장은 같이 있어야 한다. “BAA 가 60/40 을 이긴다”와 “BAA 중 무엇이 최선인지는 모른다”는 둘 다 참이고, 어느 한쪽으로 뭉개면 틀린 결론이 된다.
정직하게 시험하려면 BAA 하나를 미리 골라 60/40 과 단일 가설로 사전등록해야 한다. 성과가 가장 좋은 것을 사후에 골라 “이게 이긴다”고 하는 것은 이 저장소가 팩터 쪽에서 이미 한 번 저지른 실수다.
채택이 0건이라 결과보다 이쪽이 값어치 있다. 거의 전부가 성과를 좋아 보이게 만드는 방향이었다 — 그런 실수는 의심할 이유를 만들지 않아 오래 산다.
근거 문서의 §3(방법론 결함 1건)과 §4(구현 결함 6건) 전부에, 코드에만 기록된 1건을 더한 목록이다.
| # | 결함 | 영향 |
|---|---|---|
| §3 | 트랜치가 가격 패널 전체를 시프트해 슬리브마다 다른 기간을 쟀다 | 분산이 아니라 평활화. 슬리브 상관 0.381(정상 0.819). 고치자 PBO 0.139 → 0.770 |
| §4-1 | 수익률 라벨이 한 달 밀려 있었다 (계획서 결함) | 결정 월이 아니라 실현 월로 라벨링해야 룩어헤드가 없다 |
| §4-2 | 공통 구간이 스펙에 2007-06 이라 적혔으나 실제는 2008-07 (BIL 상장 + 워밍업) |
230개월 → 218개월 |
| §4-3 | 구성 9가 구성 8의 조용한 복제본이었다 (if/elif 디스패치) |
사전등록 9개 중 실질 8개만 달랐다 |
| §4-4 | §5 는 PBO 를 주 관문으로 적었는데 §6 채택 공식에는 PBO 가 없었다 | 패시브 기준선(spy·60/40)까지 “PBO 초과”로 기각된다 — 탐색의 산물이 아닌데도 |
| §4-5 | run_with_ma_overlay·run_with_tranches 가 “equity 에 원금을 붙인다”는 규약을 안 따랐다 |
트랜치 MDD −9.35% → 실제 −10.99%. 하필 개선안 4개에만 유리한 오차였다 |
| §4-6 | 같은 원금 누락이 summarize() 에 따로 또 있었다 |
baa_bal_tranche MDD −9.72% → −11.31%. 하나를 고쳐도 다른 하나는 안 고쳐진다 |
| (코드) | pandas.pct_change() 기본값이 결측을 앞값으로 메워 0% 수익을 조작한다 |
전 신호 계산에 fill_method=None 강제 (taa/signals.py 주석) |
전 과정과 사전 예상 대조는 docs/taa/01-results.md 에 있다.
| 도구 | 무엇을 답하는가 |
|---|---|
scripts/factor_lab.py |
이 팩터에 예측력이 있는가 (10분할 스프레드 · 단조성 · 회전율) |
research/ic.py |
Rank IC · IC-IR · 감쇠 프로파일 |
research/overfitting.py |
이 성과가 우연인가 — DSR · PBO(CSCV) |
research/regime.py |
어느 국면에서 작동하는가 (추세 × 변동성 2×2) |
research/selection.py |
팩터 선택을 학습 구간 안에서 — 조합 탐색의 정직한 형태 |
optimize/walkforward.py |
확장·롤링 윈도, 엠바고, 폴드별 파라미터 안정성 |
비중 스킴은 7종이다 — 균등 · 시총 · 역변동성 · 리스크패리티 · HRP · MVO · Black-Litterman. 다만 이 저장소의 실증 결과는 균등가중이 이긴다 (DeMiguel et al. 2009 의 1/N 결과가 두 번 재현됐다).
src/opt_portfolio/
├── factor/ # 미국 주식 팩터 엔진
│ ├── data/ # 벤더 어댑터 · PIT 스토어 (DuckDB)
│ ├── dsl/ # 표현식 트리 · PIT 평가 컨텍스트 · 레지스트리
│ ├── library/ # 팩터 158개 선언
│ ├── universe/ # 유동성·시총·섹터 필터
│ ├── portfolio/ # 스코어 합성 · 비중 7종 · 수축 공분산
│ ├── backtest/ # 횡단면 백테스트 · 비용 · 마켓타이밍
│ ├── optimize/ # walk-forward · grid/random/GP-EI 탐색
│ ├── research/ # IC · 분위수 · DSR/PBO · 레짐 · 팩터 선택
│ ├── holdings.py # 오늘 살 종목 · 매매 계획
│ └── tui.py # 운용 화면
├── taa/ # 전술적 자산배분 — 9개 구성 사전등록·PBO 관문
│ ├── data.py # Sharadar 펀드 벌크 → 배당조정 가격 패널
│ ├── signals.py # 13612W 모멘텀 · 200일 이평
│ ├── strategy.py # StrategySpec (카나리아/공격/방어 유니버스)
│ ├── backtest.py # 월별 리밸런싱 · 이평 오버레이 · 트랜치
│ ├── registry.py # 사전등록 9개 구성 · N_TRIALS
│ └── evaluate.py # DSR · PBO · 채택 판정
├── strategies/ # VAA 원본 — 모멘텀 · 자산 선택 · OU 예측(실험)
├── analysis/ # 백테스트 · 최적화 · 리스크 · 성과
├── core/ # DuckDB 증분 캐시 · 포지션
└── config.py # frozen dataclass 설정