portfolio-research

한국어 · English

탐색 비용을 스스로 정산하는 미국 주식 팩터 연구 — 성과는 walk-forward 만, 관문은 Deflated Sharpe 와 PBO, 기각한 것까지 함께 공개한다.

Python 3.10+ License: MIT LinkedIn

대형주 5팩터 + 200일 타이밍의 walk-forward 검증 구간 누적 성장 — 2002-12부터 2026-08까지 15bps 36배, 50bps 27배, SPY 14배. 세로축 로그

세로축은 로그다 — 기울기가 같으면 수익률이 같다. 2008·2012·2022 의 평평한 구간은 200일 이평이 현금으로 뺀 자리다. 그 세 번이 SPY 와 낙폭이 갈리는 지점이다.

이 저장소가 흔치 않은 이유 셋

빠른 시작

make install  # uv sync --extra dev — uv 필요 (https://docs.astral.sh/uv/)
make test     # pytest + coverage — 정상 설치 검증

저장소에 이미 커밋된 결과를 확인하는 데에는 API 키가 필요 없다:

# 전략 탐색 비용(DSR/PBO) 재현 — 벤더 데이터 없이 results/oos/ 만 읽음
uv run python scripts/strategy_search_cost.py

# VAA 원본 전략 — 무료 데이터(yfinance), 대화형 메뉴
make run

팩터 엔진이나 TAA 를 처음부터 다시 돌리려면(최신 데이터 적재, 신규 설정 최적화) Sharadar 구독이 필요하다 — 아래 사용법. 이 README 의 숫자는 전부 2026-08-14 까지의 데이터로 쟀고, 산출물이 results/ 에 함께 커밋되어 있어 벤더 데이터 없이도 대조할 수 있다.

무엇이 들어 있나

세 개의 서브시스템이 서로 다른 질문에 답하고, 같은 과최적화 관문(Deflated Sharpe + PBO)으로 잰다.

  팩터 엔진 (factor/) TAA 자산배분 (taa/) VAA 원본 (strategies/)
질문 어떤 종목을 살까 어떤 자산군으로 갈아탈까 (같음 — 첫 시도)
대상 미국 개별주 (20,931종목, 1997~2026) ETF 18종 ETF 7~11종
데이터 Sharadar 직판 (PIT · 상장폐지 포함) Sharadar 펀드 벌크 (closeadj) yfinance 일간 종가
진입점 opt-factor · opt-factor-tui scripts/run_taa.py make run · run.py
결과 채택 1건 (대형주 E안) 채택 0건 — 9개 전부 PBO 관문 탈락 보존 — 왜 실패했는지의 기록

세 시스템은 서로를 import 하지 않는다. 예외는 taa/ → factor.research.overfitting(DSR·PBO) 하나뿐이다.

성과

운용 후보 · walk-forward 검증 구간 · 2002-12 – 2026-08 (23.6년)

대형주 5팩터 + 200일 이평 타이밍 (configs/strategy_lean_timed.json)

지표 슬리피지 15bps 슬리피지 50bps SPY (같은 구간)
연평균 수익률 16.34% 14.91% 11.66%
최대낙폭 −24.3% −24.3% −55.2%
변동성 15.7% 15.7% 18.6%
Sharpe 0.727 0.648 0.418
Calmar 0.67 0.61 0.21
Deflated Sharpe (파라미터 시도 72회) 0.996 ✓ 0.988 ✓ —
Deflated Sharpe (전략 탐색 35회) 0.982 ✓ 0.957 ✓ —
PBO (35개 구성 CSCV · 월별 · S=16) 0.303 ✓ — —

이 전략은 방어 장치를 켠 채로 측정됐다 — 최소 주가 $5 · 최소 거래대금 $1M · 슬리피지. 유니버스가 역대 S&P500 이라 용량 제약이 없다. 슬리피지를 50bps 로 올려도 낙폭과 변동성이 그대로고 수익만 1.4%p 깎인다.

마지막 두 줄은 walk-forward 바깥을 정산한 값이다 — “35개 전략을 훑어 하나를 골랐다”는 상위 탐색까지 시도 횟수에 넣었다. 관문 설정(월별 · S=16)을 고른 근거는 아래에 있다. 재현: uv run python scripts/strategy_search_cost.py — 벤더 데이터 없이 results/oos/ 만으로 돈다.

왜 이렇게 만들었나

퀀트 백테스트가 실패하는 방식은 대개 정해져 있고, 이 엔진은 그 각각을 구조로 막는다. 원칙은 하나다 — 조용히 틀리지 않는다.

흔한 실패 이 엔진의 대응
생존편향 — 지금 살아남은 종목만 본다 상장폐지 종목 포함 (엔론·구 아메리칸항공 등 실제 확인)
Look-ahead — 발표 전 숫자를 쓴다 표현식이 원시 테이블에 접근 못 하고 PanelContext 만 통과, datekey 정렬 강제
재공시 오염 — 정정된 숫자를 쓴다 최초 공시 우선 — 시장이 처음 본 값만 저장
조용한 절단 — 데이터가 덜 왔는데 성공 처리 페이지네이션이 기대 범위 미달 시 TruncatedDataError
과최적화 — 수백 번 돌려 최고를 고른다 DSR(Deflated Sharpe) + PBO 로 시도 횟수를 정산
인샘플 성과 보고 공식 성과는 walk-forward 뿐. 단일 백테스트는 참고용 명시

폐기한 것과 그 대가

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

같은 전략, 같은 구간. 차이는 슬리피지·최소 주가·최소 거래대금뿐이다. 파란 선이 2026-08-16 까지 이 README 의 표제였다.

CAGR 23.78% 의 초소형주 8팩터 전략이 이 자리에 있었다. 설계 문서가 필수라고 적은 방어 장치 셋을 켜자 무너졌다 — Sharpe 1.047 → −0.224. 방어를 켜면 유니버스의 98% 가 사라지기 때문이다. 표제는 대형주 전략으로 옮겼다. 그쪽은 처음부터 방어를 켠 채로 쟀고, 그래서 두 전략을 같은 조건에서 비교한 적이 없었다.

DSR 관문에서 기각한 후보가 스무 개를 넘고, PBO 는 제대로 재기 전까지 두 번 틀린 채로 발표됐다. 그 자취를 전부 남겼다 — docs/journal/.

전술적 자산배분 — 9개 사전등록, 9개 기각

개별 종목 대신 ETF 18종을 매월 교체한다. 9개 구성을 사전등록했고 9개 전부 기각했다 — PBO 0.770. 관문을 완화하지 않았다.

아무것도 안 통한다는 뜻은 아니다. BAA(Keller 2022) 변형 6개는 예외 없이 Calmar 에서 60/40 을 이긴다(0.535~0.812 대 0.354). 다만 그 6개만으로 잰 PBO 가 0.861 이라 그중 어느 것이 최선인지는 이 데이터로 정할 수 없다. 두 문장은 함께 서야 한다.

9개 구성과 이 과정에서 잡은 결함 8건, VAA 가 논문의 16~17% 대신 여기서 6.07% 로 측정된 이유는 docs/taa/01-results.md 와 docs/journal/ 에 있다.

아키텍처

flowchart LR
    subgraph Factor engine["팩터 엔진 (factor/)"]
        SH[(Sharadar<br/>sf1 · sep · daily · actions)] -->|"opt-factor ingest"| STORE[(PITStore<br/>us.duckdb)]
        STORE --> CTX["PanelContext<br/>(datekey 정렬, look-ahead 차단)"]
        CTX --> DSL["Factor DSL<br/>158개 팩터, factor/library"]
        DSL --> UNIV["유니버스 필터<br/>가격 · 거래대금 · 섹터"]
        UNIV --> PIPE["FactorPipeline.run<br/>factor/pipeline.py"]
        PIPE --> BT["백테스트 엔진<br/>factor/backtest"]
        BT --> WF["walk-forward 최적화<br/>opt-factor optimize"]
        WF --> DSRF["DSR + PBO 게이트<br/>factor/research/overfitting.py"]
        DSRF -->|"통과 시"| HOLD["opt-factor holdings<br/>매매 계획"]
    end

    subgraph TAA allocation["TAA 자산배분 (taa/)"]
        FUNDS[(Sharadar funds bulk<br/>closeadj, 18개 ETF)] -->|"taa/data.py"| SIG["시그널<br/>13612w · sma13"]
        SIG --> STRAT["StrategySpec 레지스트리<br/>9개 사전등록 설정"]
        STRAT --> TBT["월간 백테스트<br/>taa/backtest.py"]
        TBT --> EVAL["evaluate_all + 판정<br/>taa/evaluate.py"]
        EVAL --> DSRF
    end

    subgraph Original VAA["VAA 원본 (strategies/)"]
        YF[(yfinance<br/>일간 종가)] --> VAA["VAAStrategy<br/>strategies/vaa.py"]
        VAA --> RUNPY["run.py / make run<br/>백테스트 + 리포트"]
    end

사용법

아래 적재·최적화 명령을 다시 돌리려면 Sharadar 구독이 필요하다. PIT 재무제표와 폐지 종목 커버리지를 함께 주는 유일한 소매 가격 소스다. 어댑터는 중립적인 Provider 프로토콜 뒤에 격리돼 있어 소스 교체는 파일 하나를 다시 쓰는 일이다.

# 데이터 적재 (Sharadar 구독 필요)
export SHARADAR_API_KEY=...
opt-factor ingest --store us.duckdb --provider sharadar \
  --tables sf1,sep,daily,actions,sp500,tickers

# 팩터 예측력 검증 — 10분할 · IC · 회전율
uv run python scripts/factor_lab.py --store us.duckdb --factors GP_A,PER,SIZE

# 공식 성과 (walk-forward + DSR)
opt-factor optimize --store us.duckdb --config configs/strategy_lean_timed.json \
  --space configs/space.json --objective calmar

# 오늘 살 종목 (현재 보유를 주면 매매 계획까지)
opt-factor holdings --store us.duckdb \
  --config configs/strategy_lean_timed.json --current my_holdings.csv

# TAA: 9개 구성 · PBO · 판정표
uv run python scripts/run_taa.py

개발

make lint           # ruff check + format --check
make typecheck      # mypy src/

패키지 관리는 uv(uv.lock)다. pip install 하지 않는다. 코드는 src/opt_portfolio/ 아래 factor/·taa/·strategies/ 로 나뉘고 analysis/·core/ 를 공유한다.

문서

설계 문서는 docs/factor-system/ 에 있다 — 명세, 데이터 계약(스토어 스키마 · PIT 규약 · 벤더 실측), walk-forward 수식, 그리고 기각 목록이 담긴 실험 로그. 연구 저널은 docs/journal/, 팩터 카탈로그는 docs/factor-library.ko.md.

한계와 가정

⚠️ 모든 백테스트는 과거 데이터 기반이며 미래 수익을 보장하지 않는다.

라이선스

MIT

기여 및 이슈 제보

관련 프로젝트 — 오픈소스 퀀트 스택

한국·미국 주식과 암호화폐를 아우르는 오픈소스 스택입니다. 각 저장소는 독립적으로 쓸 수 있습니다.

축 프로젝트 설명
🇰🇷 한국 주식 kiwoom-client 키움증권 REST API Python 라이브러리 — 국내주식 엔드포인트 전수·실시간 WebSocket, sync + async (pip install kiwoom-client)
🇰🇷 한국 주식 krx-fundamentals-client 국내 기업 펀더멘탈 Python 클라이언트 라이브러리 — 재무제표·투자지표·배당·종목 스크리닝 (DART + KRX + 네이버)
🇰🇷 한국 주식 krx-news-client 한국 주식 뉴스·공시를 한 스키마로 내주는 Python 클라이언트 라이브러리 (DART + 토스증권)
🇰🇷 한국 주식 krx-quant-core 한국 주식 퀀트 시스템 공통 Python 코어 — KRX 호가·가격제한폭·세션 규칙, 시행일별 거래세 비용모델, 키움 주문 가드, DART 중대공시 위험 분류, 체결 시뮬레이션, Deflated Sharpe·purged CV 통계
🇰🇷 한국 주식 fin-checkup 관심종목 위험 공시 텔레그램 알림 + DART·SEC 재무 건강검진 — 측정값과 사실만 전달한다
🇰🇷 한국 주식 quant-airflow 시세·수급·실적을 TimescaleDB 로 수집하는 Airflow 파이프라인 — 상장폐지 종목까지 담아 생존편향을 막는다
🇰🇷 한국 주식 swing-it 코스피·코스닥 알파 심사 프레임워크 — 개별 트레이드 분포로 판정하고 랜덤 음성대조·purged CV·Deflated Sharpe 를 CI 가드레일로 강제
🇺🇸 미국 주식 macro-sector-agent 하향식 미국 산업 사이클 리서치 — "어느 산업이 잊혔나"를 묻고 기계는 제외만 한다
🇺🇸 미국 주식 automated-stock-trading-systems Bensdorp 의 7개 비상관 트레이딩 시스템 백테스터 (교육용 재구현)
₿ 암호화폐 binance-quant-engine 암호화폐 선물 백테스트·실행 엔진 — 룩어헤드 0, 백테스트↔실거래 일체화
₿ 암호화폐 crypto-quant-insights 실거래 선물 시스템을 검증하며 나온 차트 연구 노트 — 폭락 스트레스 테스트·Kelly 레버리지·체결 간극

만든 사람

채영환 (Younghwan Chae) · GitHub @younghwan91 · LinkedIn

전체 오픈소스 퀀트 스택은 프로필에서 한눈에 볼 수 있습니다.