하룻밤에 165MB — 검증 엔진의 바닥을 깔았다
전종목 10년 일봉 648만 행, DART 재무 63만 행, 폐지종목 417개. 데이터보다 중요한 건 같이 만든 검증 방법론 9원칙이다.
검증을 하려면 데이터가 먼저다. 하룻밤에 165MB를 모았고, 세션이 끝나도 남도록 프로젝트 안에 뒀다.
모은 것
research/
scripts/ 20개 — 수집·백테스트 스크립트
data/ 16개, 165MB
reports/ 19개 — 검증 리포트
| 파일 | 내용 |
|---|---|
daily10y.csv.gz | 전종목 10년 일봉 2015-01~2026-09, 2,869종목, 648만 행 |
dart_fin.csv.gz | DART 연간재무 2015~2025 — 63만 행 / 3,088사 |
delisted_prices.csv.gz | 폐지종목 일봉 417종목 / 54.7만 행 |
etf_prices.csv.gz | 국내 ETF 60종 10년 일봉 |
minutes.csv / minutes2.csv | 1분봉 (09:00~09:35 / 09:00~15:30) |
index_long.csv.gz | 지수 36년 |
macro.csv.gz | 매크로 10종 |
임시 폴더에 두지 않은 이유가 있다. 앞선 세션에서 수집 스크립트를 scratchpad에 만들었다가 세션이 끝나면서 통째로 날렸다. 다시 만드는 데 한나절이 걸린다.
헛디디기 쉬운 것들 (실측으로 확인)
pykrx는 죽었다. 전종목·티커목록·fundamental API가 전부 실패한다. KRX가 로그인 정보를 요구하도록 바꿨다. 개별 종목 일봉만 된다.
StockListing('KRX-DELISTING')은 0건을 준다. 그런데 DataReader(폐지코드)로 개별 조회하면 작동한다. 450개 시도해서 417개를 받았다. "폐지종목은 못 받는다"고 적어뒀던 기존 기록이 틀렸던 것이다.
DART는 계정명이 함정이다. 당기순이익이 아니라 당기순이익(손실)이다. 영업이익과 영업이익(손실)도 둘 다 존재한다. 하나만 매핑하면 순이익이 통째로 빈다.
그리고 rcept_no 앞 8자리가 실제 접수일이다. 이걸 쓰면 "재무제표는 3월에 공시된다고 가정" 같은 어림짐작이 필요 없다. 다중회사 조회는 최대 100개/호출이고 200개를 넣으면 status=021로 거절한다. 3,990사 × 11년이 440호출, 7분이었다.
검증 방법론 9원칙
데이터보다 이게 더 중요하다. 성급한 보고로 몇 번 무너진 뒤에 고정한 규칙이다.
- 벤치마크 대비로만 판정한다. 절대수익 +10%도 같은 기간 지수가 +15%면 진 것이다
- 기간을 나눈다. 구축(~2020) / 검증(2021~). 한쪽만 좋으면 과적합
- 가중 방식을 맞춘다. 지수가 시총가중이면 전략도 시총가중으로 비교한다
- 체결 가능성을 먼저 본다. 상한가 종가 매수, 틱 단위 수익 같은 것
- look-ahead를 의심한다. 진입가가 종가면 청산 판정은 다음 봉부터
- 좋은 결과일수록 교차검증 후 보고한다
- 생존편향·비용·슬리피지를 매번 명시한다
- 시총가중 포트폴리오는 유효종목수(1/Σw²)를 확인한다
- 결과가 상식에 반하면 코드부터 의심한다
8번과 9번은 같은 날 각각 실제 버그를 잡아낸 조항이다. 그래서 목록에 올렸다.
이게 자산인 이유
전략은 기각되면 사라지지만, 데이터와 방법론은 다음 가설에 그대로 재사용된다. 팩터를 새로 시험할 때 value_factor.py에 지표 컬럼 하나만 추가하면 되고, 집중도 확인은 value_diagnose.py가 대신해준다.
같은 길을 두 번 파지 않기 위한 장치이기도 하다.