portfolio-research

TAA 결과 — 9개 구성, 채택 0개

2026-08-17. 설계 문서: docs/superpowers/specs/2026-08-17-taa-strategy-design.md.

이 문서의 목적은 07-experiment-log.md(팩터 엔진)와 같다 — 같은 실험을 두 번 하지 않는 것. 결과와 함께 §7 의 사전 예상이 맞았는지 틀렸는지를 적는다.


0. 요약

공통 구간 2008-07-31 ~ 2026-08-31, 218개월. PBO = 0.770. 9개 구성 중 채택 0개.

채택 없음이 결과다. 아래에서 완화하거나 되돌리지 않는다.

이 PBO 행렬은 9개 사전등록 구성 전부(패시브 기준선 spy·static_60_40 포함)로 잰 값이다. 기준선은 9개 탐색의 산물이 아니지만 §5 스펙이 정한 행렬 그대로 실었다 — 이유와 그로 인한 부작용(기준선까지 “PBO 초과”로 기각되는 것)은 §4-4 에 적는다. 탐색 대상만의 값(BAA 6개, 0.861)은 §1 에 있다.

uv run python scripts/run_taa.py 실측 (2026-08-17, summarize() 의 MDD 계산 결함 수정 후 — §2-예상2 참조):

가격 패널: 18종목 1997-12-31 ~ 2026-08-14
⚠ 마지막 데이터 2026-08-14 가 월말이 아니다 — 마지막 달은 반달치 데이터를 한 달로 라벨링한 것일 수 있다

공통 구간: 2008-07-31 ~ 2026-08-31 (218개월)

=== 지표 ===
                     cagr     mdd    vol  calmar  sharpe    dsr   months  defensive
name
spy                0.1246 -0.4181 0.1559  0.2980  0.8353 0.9678 218.0000     0.0000
static_60_40       0.0887 -0.2507 0.0964  0.3537  0.9322 0.9856 218.0000     0.0000
vaa_g4             0.0607 -0.2093 0.1044  0.2899  0.6169 0.8706 218.0000     0.5565
baa_agg            0.0882 -0.1650 0.1099  0.5350  0.8263 0.9764 218.0000     0.5525
baa_bal            0.0728 -0.1113 0.0780  0.6540  0.9423 0.9938 218.0000     0.5525
baa_agg_ma         0.0881 -0.1344 0.1035  0.6556  0.8695 0.9853 218.0000     0.5525
baa_bal_ma         0.0672 -0.0828 0.0683  0.8121  0.9879 0.9972 218.0000     0.5525
baa_bal_tranche    0.0811 -0.1131 0.0816  0.7174  0.9998 0.9957 218.0000     0.5464
baa_bal_ma_tranche 0.0732 -0.1002 0.0711  0.7305  1.0318 0.9972 218.0000     0.5464

PBO = 0.770  (관측 218개월 × 구성 9개)

=== 판정 ===
                    adopted                                                                                 reason
name
spy                   False                     PBO 0.77 > 0.5 · MDD -41.8% 가 한도 초과 · Calmar 0.30 가 60/40(0.35) 이하
static_60_40          False                                                    PBO 0.77 > 0.5 · MDD -25.1% 가 한도 초과
vaa_g4                False  PBO 0.77 > 0.5 · MDD -20.9% 가 한도 초과 · DSR 0.871 < 0.95 · Calmar 0.29 가 60/40(0.35) 이하
baa_agg               False                                                                         PBO 0.77 > 0.5
baa_bal               False                                                                         PBO 0.77 > 0.5
baa_agg_ma            False                                                                         PBO 0.77 > 0.5
baa_bal_ma            False                                                                         PBO 0.77 > 0.5
baa_bal_tranche       False                                                                         PBO 0.77 > 0.5
baa_bal_ma_tranche    False                                                                         PBO 0.77 > 0.5

summarize() 결함 (2026-08-17 후속 리뷰에서 발견, blocking). equity = (1 + returns).cumprod() 로만 만들면 첫 원소가 자기 자신의 cummax 가 되어 첫 달 손실이 낙폭 계산에서 빠진다. BacktestOutput.equity(원금을 맨 앞에 붙이는 규약)가 있는데도 evaluate_allout.returns 만 읽고 .equity 는 어디서도 소비하지 않았다 — Task 6 의 수정이 아무도 읽지 않는 객체 위에 남아 있었던 셈이다. 영향은 baa_bal_tranche 하나뿐이지만 그 하나가 예상 대조 결론을 뒤집는다: MDD -9.72%-11.31%, Calmar 0.8350.717. 수정: summarize 도 원금(1.0)을 equity 맨 앞에 붙인다.


1. PBO 가 말하는 것과 말하지 않는 것

PBO 0.770 은 “이 9개 중 인샘플 1등을 뽑아도 아웃샘플에서 중간 이하로 떨어질 확률이 77%” 라는 뜻이다. 9개가 서로 순위를 구분할 수 없을 만큼 가깝다는 뜻이지, “아무것도 안 통한다”는 뜻이 아니다.

BAA 계열 6개(baa_agg·baa_bal·baa_agg_ma·baa_bal_ma·baa_bal_tranche· baa_bal_ma_tranche)만 따로 PBO 를 재면 0.861 이다 — 사실상 구분 불가다. 서로 간 상관은 0.62~0.79 (Task 7 실측). 그런데 이 6개 전부가 Calmar 로 60/40(0.3537)을 이긴다, 예외 없이:

구성 Calmar
baa_agg 0.535
baa_bal 0.654
baa_agg_ma 0.656
baa_bal_tranche 0.717
baa_bal_ma_tranche 0.731
baa_bal_ma 0.812
60/40 (기준) 0.354

두 사실은 같이 있어야 한다. “BAA 계열이 60/40 을 이긴다”와 “BAA 계열 중 어느 것이 최선인지는 이 데이터로 못 고른다”는 둘 다 참이고, 둘 중 하나로 뭉개면(잘 된다/안 된다) 틀린 결론이 된다.

클래스 수준 우위는 관찰이지 채택된 발견이 아니다. §5 에서 다음 실험으로 남긴다.


2. §7 사전 예상 대조

설계 문서가 결과 전에 못박은 다섯 예상을 실측과 대조한다. 틀린 것도 그대로 남긴다 — 맞은 것만 골라 적으면 다음에 같은 예상을 또 하게 된다.

예상 1 — BAA > VAA. 전반 맞음, 후반 틀림.

전반(카나리아 분리가 성과를 개선한다)은 맞다. baa_agg Calmar 0.535 vs vaa_g4 0.290 — 거의 두 배다.

후반은 “오발 빈도가 줄 것” 이라는 추정이었는데 틀렸다. 방어 비중이 baa_agg 55.25% vs vaa_g4 55.65% — 거의 같다. BAA 의 카나리아(SPY·EFA· EEM·AGG)가 VAA 의 공격 유니버스와 같은 4자산이기 때문이다. 카나리아를 투자 대상에서 분리해도 카나리아 자체는 그대로이므로 방어로 도는 빈도는 안 바뀐다. 바뀐 것은 방어로 돌 때 무엇을 사는가다 — VAA 는 LQD·IEF·SHY 중에서, BAA 는 BIL 대비 dual momentum 을 거쳐 방어군 3종에서 고른다. 성과 개선은 오발을 줄여서가 아니라 오발했을 때 덜 잃어서 나온다.

예상 2 — 200일 이평은 무효하거나 마이너스. 틀림.

Task 7 진행 중 중간 판정(“무효/마이너스 지지”)은 누적수익률로 봤기 때문에 틀렸다 — 채택 기준인 Calmar 로 다시 보면 정반대다. 이평은 세 쌍 모두에서 Calmar 를 올린다:

이평 전 이평 후
baa_aggbaa_agg_ma 0.535 0.656
baa_balbaa_bal_ma 0.654 0.812
baa_bal_tranchebaa_bal_ma_tranche 0.717 0.731

세 쌍 모두 이평이 Calmar 를 올린다 — MDD 를 CAGR 보다 더 많이 깎기 때문이다 (예: baa_bal MDD −11.1% → baa_bal_ma −8.3%, CAGR 은 7.3%→6.7% 로 소폭만 하락). 이중 필터가 VAA 의 병(과잉 방어)을 재발시킬 것이라는 예상은 세 쌍 전부에서 빗나갔다.

정정 이력. 이 문서의 이전 버전은 트랜치 쌍에서 이평이 Calmar 를 0.835→0.731 로 내린다고 적었다. summarize()baa_bal_tranche 의 MDD 를 −9.72%(실제 −11.31%)로 과소평가해 Calmar 를 부풀렸던 결함 때문이었다(위 “summarize() 결함” 참조). 수정 후에는 0.717→0.731 로 오른다 — 세 쌍 모두 개선이므로 예상 2 는 부분이 아니라 완전히 틀렸다. 흥미로운 점: 이 정정 직전 버전 자체가 “Task 7 의 트랜치 착시 재사용을 거부한 정정”이었다 — 같은 종류의 결함(낙관적인 방향으로 치우친 파생 지표)이 한 층 아래에서 다시 나온 것이다.

예상 3 — 트랜치는 CAGR 을 거의 안 바꾸고 Calmar 를 조금 올린다. 맞음.

트랜치 방법론 결함(§3)을 고친 뒤 다시 잰 슬리브 간 분산 감소 효과는 약 4% (Task 7 fix round 2) — 예상이 말한 “조금”에 해당한다. summarize() 의 MDD 결함(§0)까지 고친 결과표도 이제 같은 크기로 말한다: baa_bal(0.654) → baa_bal_tranche(0.717), 약 9.6% 상승. 수정 전 버전이 보인 큰 격차(0.654→0.835) 는 두 결함(트랜치 평활화 + summarize 원금 누락)이 같은 방향(과대평가)으로 겹친 산물이었고, 둘 다 걷어내자 표의 숫자와 4% 분산 감소 측정이 서로 맞아떨어진다.

예상 4 — 어느 것도 CAGR 로 SPY 를 못 이긴다. 맞음.

SPY CAGR 12.46% 가 최고다. 나머지 8개 전부 그 아래다. 승부는 예상대로 MDD 제약 하의 Calmar 에서 났다.

예상 5 — 60/40 을 이기는 것이 진짜 관문. 결론은 맞았지만 이유가 틀렸다.

결론(“전부 기각”)은 맞다. 하지만 판정표의 reason 열을 보면 BAA 6개는 전부 PBO 0.77 > 0.5 하나로만 기각된다 — Calmar 로 60/40 을 못 이겨서가 아니다. 위 §1 의 표대로 6개 전부 60/40 을 이긴다. 예상이 세운 “60/40 을 못 이기면 기각”이라는 인과는 이번 결과에서 한 번도 작동하지 않았다. 기각의 실제 원인은 PBO 다. 이유가 다르면 대응도 달라진다 — “더 나은 구성을 찾자”가 아니라 “구성 선택 자체를 줄이자”가 맞는 다음 수다(§5).


3. 트랜치 방법론 결함 — 가장 교훈적인 사건

baa_bal_tranche 최초 구현은 가격 패널 전체를 시프트해 4개 슬리브를 만들었다. 그 결과 슬리브마다 “같은 달”이라는 라벨이 서로 다른 실제 기간을 가리켰다. 서로 다른 기간의 수익률을 평균 내는 것은 분산 투자가 아니라 평활화(smoothing)다 — 상관이 낮아 보이는 것은 리밸런싱이 분산된 결과가 아니라,애초에 겹치지 않는 구간을 잰 결과였을 뿐이다.

측정으로 잡았다: 슬리브 간 평균 상관 0.381 (슬리브 0 대 3 은 0.076 — 같은 전략·같은 자산을 일주일 시차로 굴리는 슬리브가 이럴 수 없다). 결정적 증거는 평균 후 변동성이 sqrt((1+3ρ)/4) × 평균개별변동성 과 거의 정확히 일치한 것 — 감소분 전부가 순수 통계적 평균 효과였고, 리밸런싱 시점 분산의 효과가 아니었다.

재구현(단일 패널 위에서 리밸런싱 시점만 슬리브마다 다르게 배치, 같은 달력월로 집계)으로 상관은 0.819(최소 0.702)로 올라갔다. 그 결과:

PBO 0.139 → 0.770.

착시가 만든 것은 “트랜치가 좋다”는 결론이 아니라, 한 구성이 인샘플에서 뚜렷한 1등처럼 보이게 만들어 선택 불안정성을 가린 것이었다. 버그가 있을 때 9개 중 승자가 또렷했고(PBO 낮음), 고치자 9개가 다시 뭉쳤다(PBO 높음).

주의: 다중시드 분산 테스트만으로는 이 버그를 못 잡는다. Task 7 재확인에서 버그 버전이 정상 버전보다 더 강한 분산 감소를 보였다(평균비율 0.832 vs 0.962 — 서로 다른 기간을 평균 내는 쪽이 상관 자산을 평균 내는 쪽보다 분산을 더 줄이기 때문). 이 회귀를 실제로 잡는 것은 슬리브 상관 하한 테스트 하나 뿐이다.


4. 실행 중 발견한 스펙 결함

설계 문서(2026-08-17-taa-strategy-design.md)를 구현하며 여섯 가지 결함을 찾았다. 다음번에 같은 결함을 다시 만들지 않기 위해 여기 적는다.

  1. 수익률 라벨이 한 달 밀려 있었다 (계획서 결함). 결정 월이 아니라 실현 월로 라벨링해야 룩어헤드가 없다. 안 고쳤으면 §7 예상 대조에 쓰인 2008·2020·2022 구간별 분해가 한 달씩 어긋나 판정이 틀렸을 것이다.
  2. 공통 구간이 스펙에 2007-06 으로 적혀 있었으나 실제는 2008-06 이다. BAA 유니버스의 BIL(2007-05 상장) + 12개월 모멘텀 워밍업 때문이다. 스펙은 230개월이라 적었고 실제는 218개월이다(§0). 이 문서와 코드는 218개월을 기준으로 삼는다.
  3. 구성 9 가 구성 8 의 조용한 복제본이었다. run_taa.pyif/elif 디스패치가 트랜치와 이평 오버레이를 동시에 요구하는 baa_bal_ma_tranche 에서 이평을 못 받았다 — 사전등록 9개 중 실질 8개만 서로 달랐다. 두 변형을 합성(슬리브별 개별 적용 후 평균)하도록 고쳤다.
  4. §5 는 PBO 를 주 관문으로 적었는데 §6 의 채택 공식에는 PBO 가 없었다. 구현은 §5 를 따라 전 구성에 PBO 를 일괄 적용했고, 그 결과 패시브 기준선(spy·static_60_40)까지 “PBO 초과”로 기각된다 — 위 판정표의 reason 열을 보면 그대로 나온다. 기준선은 9개 탐색의 산물이 아니므로 선택 과최적화 지표를 적용할 대상이 아니다. 고쳐도 채택 결과(0개)는 바뀌지 않는다 — BAA 구성들도 PBO 에 걸리므로.
  5. run_with_ma_overlay·run_with_tranches 두 진입점이 run_backtest 가 확립한 “equity 에 원금 시점을 붙인다”는 규약을 처음엔 따르지 않아 MDD 가 과소평가됐다(트랜치 −9.35% vs 실제 −10.99%). 채택 기준이 MDD ≤ 20% 이므로 경계선 구성을 통과시킬 수 있었고, 영향받는 4개가 하필 개선안이었다 — 개선안에 유리한 방향의 오차였다. 세 진입점 전부에 파라미터화된 불변식 테스트를 추가해 고쳤다.
  6. §4-5 가 고친 문제가 summarize() 에도 따로 있었다. BacktestOutput.equity 는 원금을 붙이지만 evaluate_allsummarize 경로는 matrix(공통 구간으로 자른 returns)만 받아 자체적으로 equity = (1 + returns).cumprod() 를 다시 만들었고, 거기엔 원금이 없었다. 두 결함이 같은 원인(원금 없는 equity → 첫 달 손실이 낙폭에서 빠짐)을 두 곳에서 반복한 것이다. 하나를 고쳤다고 다른 하나가 저절로 고쳐지지 않는다 — 이번엔 결과표 자체(§0)에 반영해 잡았다. 영향: baa_bal_tranche MDD −9.72%→−11.31%, Calmar 0.835→0.717(§2-예상2 정정 이력 참조).

5. 한계

표본이 218개월이고, 그 안의 하락은 세 번뿐이다(2008 · 2020 · 2022). “BAA 계열이 붕괴를 방어한다”는 주장은 사건 3개에 근거하게 된다. 이 한계를 결과 뒤 각주로 붙이지 않고 여기 같은 자리에 적는다 — 07-experiment-log.md §2.2 에서 표제 숫자가 방어 장치를 끈 상태의 산물이었던 일을 겪었기 때문이다.

그 밖에:


6. 다음 실험

클래스 수준 우위(BAA 가 60/40 을 이긴다)는 사후 재해석이 아니라 다음 실험의 사전 가설로 남긴다. 이번 결과에서 그것을 채택으로 전환하지 않은 이유는 §1 — 9개 중에서 골랐다는 사실 자체가 전환을 무효화한다.

정직하게 시험하려면 BAA 구성 하나를 미리 골라 60/40 과 단일 가설로 사전등록해야 한다. 여러 BAA 변형 중 성과가 가장 좋은 것을 골라 “이게 이긴다”고 하는 것은 이번 실험이 이미 겪은 실수(사후 선택)를 반복하는 것이다.

미완으로 남긴 것(§7 스펙, 시도 횟수에 넣지 않음):

채택된 구성이 없으므로 이번 라운드에서는 실행하지 않았다.