portfolio-research

연구 저널 — 고친 것, 폐기한 것, 기각한 것

← README · English

이 저장소는 채택만이 아니라 기각도 함께 싣는다. README 는 결론만 적고, 그 결론이 나오기까지의 자취는 이 파일에 남긴다 — 틀린 채로 발표했던 두 숫자, 폐기하기 전까지 표제였던 전략, 사전등록하고 전부 기각한 자산배분 9개 구성.


1. 팩터 엔진

관문을 어느 눈금으로 재는가 — 두 번 고친 자리

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위로 올라온다.

이 자리에서 두 번 틀렸다

  1. 2026-08-17 오전 — “DSR 0.988 · PBO 0.139” 이라고 적었다. 그건 열여섯 조합 중 최소값이었고, 방법 명시도 재현 스크립트도 없었다.
  2. 같은 날 저녁 — 재현되지 않는 것을 보고 “관문을 못 넘는다” 며 통째로 철회했다. 이것도 과했다 — 틀린 것은 숫자였지 결론이 아니었는데, 어느 주기가 옳은지 재보지도 않고 철회했다.

첫 번째는 유리한 쪽으로 틀렸고 두 번째는 불리한 쪽으로 틀렸다. 방향은 달라도 원인은 같다 — 근거 없이 하나를 골랐다.

남은 한계. 월 경계를 하루씩 미는 21개 오프셋에서 PBO 는 0.143~0.468 이다. 전부 관문을 넘지만 최악값이 0.5 에 가깝다. 그리고 35개는 서로 다른 유니버스를 쓰는 이질적 구성인데, CSCV 는 본래 한 전략의 파라미터 변형을 전제한다. 이 한계를 없애는 길은 하나뿐이다 — 하나를 미리 골라 단일 가설로 사전등록하는 것.

uv run python scripts/strategy_search_cost.py   # 위 표를 그대로 재현한다

표제가 바뀐 경위 — 초소형주 전략은 폐기했다

초소형주 전략의 누적 성장 — 방어 장치를 끄면 153배, 켜면 0.040배로 원금의 96%를 잃는다. 세로축 로그

같은 전략, 같은 구간. 다른 것은 슬리피지·최소 주가·최소 거래대금 셋뿐이다. 파란 선이 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 에 있다.

만든 것 전부를 한 장에

위험 대비 수익 산점도 — 가로축 최대낙폭, 세로축 연평균 수익률. E안 대형주가 낙폭 24%에 수익 16%로 왼쪽 위, SPY는 낙폭 42%에 12%, 방어를 켠 초소형주는 낙폭 96%에 수익 −16%

왼쪽 위가 좋다. 파란 점이 채택한 것, 빨간 점이 폐기한 것이다. 아래쪽의 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% 로 드러난 뒤에 썼다. 그건 아무도 선택하지 않은 매크로 베팅이다.


2. 전술적 자산배분

VAA 로 시작했고, VAA 는 실패했다

Wouter Keller 의 Vigilant Asset Allocation(2017)이 출발점이었다.

모멘텀 = 12·R(1M) + 4·R(3M) + 2·R(6M) + 1·R(12M)      ← Keller 13612W

논문은 연 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.

Keller 본인이 두 번 고쳤다

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 설정