2026. 8. 22. 02:12ㆍ카테고리 없음

0. 무엇을 만들었나
미국 지수의 5영업일 뒤 방향을 맞히는 모델입니다.
구성은 spy, 지평 5영업일, 피처157개, 학습창 750일, 재학습 21엽업일, 로직스티 입니다.
결과는 이렇습니다.
홀드아웃(2025년) spy기준 auc 0.5634, 네 지수 전부 엣지 양수
qqq dia iwm 까지 네 지수 전부 엣지 양수(테스트 통과)
다음은 반영 상태입니다.

미국 단기점수는 검증된 요인만 사용합니다. 이전 20봉의 예측모델에서 피처를 넘겨받았고 이전 모델은 상승에만 유리한 모델이었습니다.
h=5 (5봉 모델) 스물다섯번 시도하고 그중 가장 좋은 평가를 고른 과정을 기록한 글입니다.
1. 전체 아키텍처 - 피처 표 하나, 소비자 셋

이 모델은 5영업일 뒤 미국 지수의 방향을 예측하는걸 목표로합니다.
먼저 피처 표를 만듭니다. 행은 거래일, 열은 그날까지의 시장 상태 157개입니다. 정답인 실제 등락은 들어있지 않고 학습을 진행합니다.
이표를 쓰는쪽은 셋입니다.
- 성능제기: 과거 시점에 서서 그때까지 데이터로만학습하고 다음 21영업일을 예측한뒤, 앞으로 이동해 재학습합니다. 성능 숫자는 여기서만 나옵니다.
- 파일 굳히기 - 최근 750거래일로 한번 학습하여 저장합니다.
- 운영 추론: 그 파일로 오늘 확률을 냅니다. 미국 단기 점수의 방향 요인이 2026-08-16부터 이 모델입니다.
나눈 이유는 하나입니다. 전체 데이터로 학습한 모델이 그 데이터를 맞히는건 성능이 아닌데, 한 코드에 두면 그걸 성능으로 착각합니다. 그래서 파일 굳히는 쪽에는 성능을 계산하는 코드가 없습니다.
대신 셋이 어긋나면 앞서 잰 숫자가 거짓말이 됩니다. 판단 기준선은 학습한쪽이 계산하여 파일에 담고, 피처 코드는 세곳이 같은 모듈을 부릅니다.
그 대가로 실험용 research/ 폴더가 운영 의존성이 됩니다.
-> 학습 저장 운영 세곳이 피처를 똑같이 계산해야 해서 운영이 실험 폴더의 코드를 그대로 가져다 쓰는데, 그 바람에 실험용이라 여긴 그 폴더를 손대면 운영 예측이 바뀌어 버립니다.

반대방향: 실험 실험 스크립트가 운영 모델 파일을 덮어쓰는것은 실행 전후 해시 비교로 막습니다.
파일이 없으면 넘어가지 않고 예외를 냅니다. 해시가 비면 무엇을 덮어써도 변경없음 으로 통과하기 때문입니다.
2. 네개 층 - 각 층이 받는것과 내놓는것

코드는 네층으로 나뉘고, 각층은 앞층이 만든것 하나만 받습니다.
1. 피처 만들기는 일봉과 거시 시계열을 받아 157열짜리 피처 표를 내놓습니다. 버전마다 블록을 하나씩만 얹었습니다. (블록: 피처표를 한번에 다 만든게 아니라 성격별 묶음을 쌓았고, 그 한묶음이 블록)
2. 성능 재기는 피처 표를 받아 날짜별 예측 기록을 내놓습니다. 재학습 때마다 만든 모델은 예측만 남기고 버립니다. 나중에 다시 쓸일이 없기 때문입니다.
3. 판정은 그 예측 기록만 받습니다. 모델도 피처도 보지않습니다. 구간을 잘라 성적과 P값을 냅니다.
4. 파일 굳히기는 검증된 설정으로 최근 750일을 한번 학습해 모델 파일을 저장하고, 성능측정에서 낸 성적을 기록 파일에 옮겨 적습니다.
왜 판정을 따로 뒀나
성능측정이 내놓는 예측 기록은 날짜, 실제 등락, 예측확률 네칸짜리 표입니다. 모델도 피처도 들어있지 않습니다.
이렇게 해두면 판정 기준을 새로 추가할때마다 다시 돌릴 필요가 없습니다. 실제로 겹침을 보정한 P값 계산을 나중에 도입했는데 , 그때 한일은 이 표를 읽는 함수 하나를 더한 것뿐이었습니다.

먼저 이득을 잽니다. 상승이라 찍은날의 적중 66.47%에서 그냥 오른 비율 61.67%를 빼면 4.8%, 하락쪽은 +4.3% 입니다.전체 정확도를 안쓰는 이유는 무조건 상승이라 답해도 61.67%가 나오기때문입니다. 다음은 우연 여부입니다. 5일 정답 구간이 이웃끼리 나흘씩 겹쳐 일반 검정은 p를 작게 잡습니다. 그래서 5일째 2000번 다시 뽑아 이득이 사라진 재표본 비율을 p로 썼습니다.
상승 하락 이득이 둘다 양수이고 p도 둘다 0.05 미만이어야 통과 입니다. 결과는 이득 표본 모두 양수, p값을 통해 통과여부를 갈랐습니다.
03. 피처 선정

모델이 보는것은 매 거래일마다 157개의 숫자입니다. 성격이 같은것끼리 묵어 여섯 덩이리로 나뉩니다.
기존 93열이 출발점입니다. 지수 자신의 등락(1-60일), 이동평균에서 얼마나 떨어져 있는지, RSI, 실현 변동성, 변동성지수의 구조, 변동성지수의 구조, 하이일드와 투자등급 회사채의 신용 스프레드, 장단기 금리차와 달러, 그리고 닛케이 항셍 닥스 ttse등 10개국 지수, fomc일정과 월말 옵션만기 같은 달력 정보입니다.
여기서 한열은 일부러 뺐습니다. 특정 etf를 쓰는 열이었는데 그 종목 상장이 2011년이라, 넣으면 결손 때문에 학습 표본이 3년 잘렸습니다. 피처 하나와 3년치 데이터를 맞바꾼 셈입니다.
내재시간은 20열은 달력 대신 가격이 움직일때만 시계를 돌리는 방식입니다. 조용한날과 폭락한 날이 달력에서는 똑같이 한칸이라는 문제를 다룹니다.
분수차분 12열은 종가 거래량 변동성지수의 추세를 걷어내되 기억은 남기는 값입니다. 어제와의 차이만 쓰면 지금이 고점 근처인지 저점근처인지도 함께 사라지기 때문입니다.
나머지 세 덩어리는 전부 한가지 축입니다. 미국장이 닫혀 있는 동안 무슨일이 있었나.
국제·야간 12열은 아시아·유럽이 미국보다 먼저 마감한다는 사실을 씁니다. 지역별 등락과 두 지역의 격차, 그리고 밤사이 등락과 장중 등락을 나눠서 넣었습니다. 성적이 0.5088에서 0.5489로 가장 크게 오른 블록입니다.
선물·통화 10열은 같은 축을 다른 창으로 봅니다. 장 밖 정보는 지수보다 선물·달러·엔에 먼저 찍히기 때문입니다.
국면 교차 10열은 조금 다릅니다. 새 정보를 넣는 게 아니라, 시장이 불안한 정도를 0~1로 만든 뒤 대표 열 5개에 곱해 "불안할 때 쓰는 몫"과 "평온할 때 쓰는 몫"으로 쪼갠 것입니다.

같은 신호라도 장세에 따라 다르게 읽으라고 알려주는 셈이고, 이 블록이 마지막 0.5516 → 0.5634를 만들었습니다.
03. 표본구성 - 4567행에서 750행까지

피처 표에는 2008년 6우러부터 2026년 8월까지 4567일이 들어있습니다. 그런데 실제 학습에 쓰는건 750일입니다. 어떻게 줄어드는지가 이 절의 내용입니다.
1. 정답 만들기: 5영업일 뒤 종가가 오늘보다 높으면 1, 아니면 0입니다. 거의 안 움직인 날도 빼지 않습니다.
예전에는 +-2% 안쪽 횡보를 학습에서 뺐습니다. 애매한 날을 지우면 깨끗해질 거라 봤는데, 표본이 2625행에서 742행으로 줄었고 남은 표본이 위기 국면으로만 채워졌습니다. 그래서 되돌렸습니다.
2. 빈칸있는날 빼기: 오늘 예측의 정답은 5영업일 뒤에야 확정됩니다. 달력으로 8일입니다. 그 8일을 기다리고, 확정 직후 8일을 더 띄웁니다. 결과적으로 예측 시작일 보다 9일이상 앞선 날만 학습에 씁니다.
굳이 더 띄우는 이유는 구간이 겹치기 때문입니다. 확정되자마자 쓰면 예측하려는 구간과 살짝 맞물린 날을 배우게 됩니다.
3. 최근 750일만 남기기: 남은것중 가장 최근 750 거래일, 약 3년치만 씁니다. 더 옛날 데이터는 있어도 쓰지 않습니다. 1500일로 늘려봤더니 0.48로 오히려 더 나빠졌습니다. 표본이 두 배가 돼도 옛 장세는 지금과 성격이 달라 방해가 됐습니다.

크게 움직인 주는 더 배우고, 조용한 주도 최소 1의 무게로 남깁니다. 예전에 작은 변동을 아예 잘라냈을 때는 표본의 71.7%가 사라졌습니다. 지우는 대신 무게로 다루는 쪽으로 바꾼 것입니다.
②③④는 전부 같은 종류의 사고를 막습니다. 빈칸을 채우다 미래가 새는 것, 아직 답이 없는 날을 답으로 배우는 것, 지금과 다른 장세를 배우는 것 — 셋 다 성적을 좋아 보이게 만드는 방향으로 틀어지고, 그래서 실제로 운용하면 그 성적이 재현되지 않습니다.
4. 모델 선택 — 왜 단순한 쪽을 골랐나

학습에 쓰는 표는 750행 × 157열입니다. 한 열당 다섯 행꼴이고, 맞히려는 것은 동전에 가깝습니다. 그냥 상승이 61.67%인 판에서 몇 %p를 더 얻는 게 목표입니다.
이런 표에서는 복잡한 모델이 규칙을 찾기 전에 외워버립니다. 실제로 그랬습니다.
같은 조건에서 네 가지를 재봤습니다.

앙상블은 세 번 시도해서 세 번 다 졌습니다.
부스팅이 진 것도 기록해 둘 만합니다.
나무모델의 장점은 열 사이의 조합을 스스로 찾는 것인데, 750행에서는 그 조합을 찾기 전에 과적합했습니다.
반대로 "불안한 장세인지 평온한 장세인지"라는 조합 하나를 사람이 직접 열로 만들어 준 쪽이 자동 탐색보다 나았습니다.
채택한 구성

표준화(StandardScaler) — 각 피처를 평균 0, 표준편차 1로 변환합니다. L2 정규화(큰 계수 하나보다 작은 계수 여러 개를 선호하게 만들어서, 상관 높은 열끼리 가중치를 나눠 갖게 하고 특정 피처가 혼자 결과를 좌우하는 걸 막는 기능)는 계수 크기에 페널티를 주므로, 스케일이 큰 피처가 불리하게 눌립니다. 스케일을 맞추지 않으면 정규화 강도가 피처마다 달라집니다.
L2 정규화 C=0.05 — 상당히 강한 정규화입니다. 국면 교차항이 원본 피처와 높은 상관을 갖기 때문에(다중공선성), 계수를 눌러 두지 않으면 서로 상쇄되며 불안정해집니다.
샘플 가중치 — 학습에만 적용하고 평가 구간에는 적용하지 않습니다. 평가에 가중을 주면 성능 지표가 왜곡됩니다.
결정 임계값(decision threshold)
기본값 0.5를 쓰지 않습니다. 클래스 불균형 때문에 0.5로 자르면 1,101일 중 884일(80%)이 상승으로 분류되어, 다수 클래스 예측(majority-class baseline)과 사실상 같아집니다.
대신 학습 샘플의 예측 확률 분포에서 중앙값을 임계값으로 씁니다. h=5는 0.6322, h=20은 0.8024입니다.
이 값은 재학습마다 달라지므로 하드코딩하지 않고 모델 아티팩트(pkl) 안에 함께 직렬화합니다. (랜덤화를 막기위해)
추론 시에는 단조 변환으로 임계값을 0.5로 재정렬해 반환하므로, 소비하는 쪽 코드는 0.5 기준만 보면 됩니다. 단조 변환이라 AUC는 불변입니다.
5. 검증 설계 — 워크포워드

시계열에서는 데이터를 무작위로 섞어 나누는 교차검증(k-fold)을 쓸 수 없습니다.
미래 데이터로 학습해 과거를 예측하는 셈이 되어 룩어헤드 바이어스(look-ahead bias)가 들어갑니다.
대신 워크포워드(walk-forward) 를 씁니다. 과거 한 시점에 서서 그때까지의 데이터로만 학습하고, 다음 21영업일을 예측하고, 21영업일 앞으로 이동해 재학습하는 것을 끝까지 반복합니다. 재학습 주기 21영업일은 실제 운영에서 모델을 갱신하는 주기와 같게 맞췄습니다.
한 블록의 구조
학습 구간은 최근 750거래일이고, 그 뒤에 두 개의 공백이 붙습니다.
퍼지(purge) 8일 — 5영업일 뒤 수익률이 정답이므로, 최근 며칠은 아직 정답이 확정되지 않았습니다. 달력으로 8일치를 학습에서 제외합니다.
엠바고(embargo) 8일 — 정답이 확정된 직후 구간도 예측 구간과 라벨이 맞물립니다. 같은 길이만큼 더 띄웁니다.
# 각 행의 정답이 확정되는 날짜 (영업일 5일 ≈ 달력 8일)
lab_done = dts + np.timedelta64(int(HZN * 1.45) + 1, 'D')
embargo = np.timedelta64(int(HZN * 1.45) + 1, 'D')
# 확정일이 예측 시작보다 엠바고만큼 앞선 행만 학습에 쓴다
tr = lab_done < (dts[blk[0]] - np.timedelta64(1, 'D') - embargo)
이 두 장치가 없으면 데이터 누수(data leakage)로 성능이 부풀려집니다.
평가 구간을 나눈 방식
전 구간 성적을 하나의 숫자로 내면 어디서 통했는지 알 수 없습니다. 그래서 겹치지 않는 2년 창 8개로 나눠 따로 채점합니다.
여기서 창마다 깨끗 / 오염 태그를 붙였습니다. 피처를 설계할 때 2016년 이후 구간을 들여다보며 만들었기 때문에, 2017~2024 성적에는 선택 편향(selection bias)이 들어 있습니다. 그 구간을 오염으로 표시하고, 설계에 쓰지 않은 2010-11·2012-13·2014-15·2025~ 를 깨끗으로 둡니다. 2016년은 경계라 아예 제외했습니다.
구간 AUC 구분
| 2010-11 | 0.4783 | 깨끗 |
| 2012-13 | 0.5047 | 깨끗 |
| 2014-15 | 0.5192 | 깨끗 |
| 2016 | — | 제외 (경계) |
| 2017-18 | 0.5170 | 오염 |
| 2019-20 | 0.5886 | 오염 |
| 2021-22 | 0.5399 | 오염 |
| 2023-24 | 0.4637 | 오염 |
| 2025~ | 0.5634 | 홀드아웃 |
판정 두 층
1차 판정은 깨끗 구간을 합쳐서 봅니다. 결과는 1,843건에 AUC 0.5076, 이득 −0.16/−0.20으로 미통과입니다.
이 기준은 실험을 돌리기 전에 먼저 적어 두었습니다(사전등록). 결과를 본 뒤에 기준을 고치면 어떤 결과든 통과시킬 수 있기 때문입니다.
2차 판정은 홀드아웃(2025-01-01 이후)과 교차 지수입니다. SPY로 만든 모델을 QQQ·DIA·IWM에 그대로 적용해, SPY에만 과적합된 것인지 확인합니다. 네 지수 모두 이득은 양수였지만, 우연 검정은 IWM 하나만 통과했습니다. (연속 5일 덩어리째 무작위로 다시 뽑아 성적을 2,000번 재계산하고, 그중 상승·하락 이득이 0 이하로 사라진 재표본 비율을 p값으로 써서 0.05 미만)
한 가지는 그대로 적어 둡니다. v57·v59·v62를 만들 때 매번 홀드아웃 성적을 보고 다음 실험을 정했습니다. 엄밀히 말하면 홀드아웃이 여러 번 소비된 상태이고, 낙관 편향이 남아 있습니다.
평가와 유의성 판정
이득이 남는지 재고, 그 이득이 우연히도 나오는지 다시 잰다
판정에 쓰는 입력은 워크포워드가 남긴 예측 기록 하나입니다. 날짜, 모델이 낸 확률, 나중에 확정된 실제 방향이 짝지어진 표이고, 피처도 모델 객체도 들어 있지 않습니다.
이 표만 있으면 판정 기준을 나중에 추가해도 모델을 다시 돌릴 필요가 없습니다. 실제로 겹침을 보정한 p값 계산은 나중에 도입했는데, 그때 한 일은 이 표를 읽는 함수 하나를 더한 것뿐이었습니다.

① 이득 — 베이스라인을 얼마나 넘었나
모델이 상승이라고 분류한 날들의 실제 상승 비율(정밀도)에서, 그 구간의 기저 상승률을 뺍니다. 하락 쪽도 같은 방식입니다.
# 베이스라인 = 그 구간에서 그냥 오른 날의 비율
base_up = float(y.mean() * 100)
base_dn = 100.0 - base_up
# 상승/하락이라 분류한 날들의 실제 적중률
prec_up = float(y[up].mean() * 100)
prec_dn = float((1 - y[dn]).mean() * 100)
# 판정에 쓰는 유일한 숫자 — 계층별 초과분
e_up, e_dn = prec_up - base_up, prec_dn - base_dn
홀드아웃 구간 SPY 기준으로 상승은 66.47% − 61.67% = +4.80%p, 하락은 42.63% − 38.33% = +4.30%p 입니다.
상승과 하락에 조건을 각각 건 것도 의도입니다. 한쪽만 좋으면 방향을 맞히는 게 아니라 한쪽으로 치우친 것뿐입니다. 실제로 이전 20봉 모델이 그랬습니다. 상승 +6.5%p, 하락 +1.9%p로 사실상 상승만 맞히는 모델이었습니다.
이 상태에서 이항검정을 쓰면 겹친 표본을 전부 독립으로 세어, 실제보다 표본이 많은 것처럼 계산합니다. p값이 작게 나와 유의한 것처럼 보입니다.
그래서 블록 부트스트랩(block bootstrap) 을 씁니다. 하루 단위가 아니라 연속 5일 블록 단위로 복원추출해 원래 길이만큼 재표본을 만들고, 그 재표본에서 이득을 다시 계산합니다. 이걸 2,000번 반복해 이득이 0 이하로 무너진 비율을 p값으로 씁니다.
# 블록 시작점을 복원추출해 연속 5일씩 이어 붙인다
pick = rng.choice(starts, size=max(1, n // L), replace=True)
ix = np.concatenate([np.arange(s, min(s + L, n)) for s in pick])
# 베이스라인도 재표본마다 다시 계산한다
b_up = yy.mean() * 100
if (yy[u].mean() * 100 - b_up) <= 0:
up_bad += 1 # 이득이 사라진 재표본
p = 0.05는 2,000번 중 이득이 사라진 경우가 100번 미만이어야 한다는 뜻입니다.
통과 조건
- 상승 이득 > 0
- 하락 이득 > 0
- 상승 p < 0.05
- 하락 p < 0.05
넷 중 하나라도 어긋나면 탈락입니다. 이 기준은 실험 전에 고정했습니다.
결과
| SPY | 0.5634 | +4.80%p | +4.30%p | 0.0995 |
| QQQ | 0.5440 | +2.78%p | +2.78%p | 0.2125 |
| DIA | 0.5454 | +3.12%p | +3.57%p | 0.1205 |
| IWM | 0.5892 | +6.36%p | +5.44%p | 0.0495 |
이득은 네 지수 모두 양수였지만, 유의성은 IWM 하나만 통과했습니다. SPY는 0.0995로 미달입니다.
방향은 맞았지만 우연이 아니라고 단언할 만큼은 아니었다는 것이 이 절의 결론이고, 운영에서 이 모델을 참고 지표로만 취급하는 근거입니다.
판정에 쓰지 않은 지표
- 전체 정확도 — 기저율과 구분되지 않아 제외
- AUC 단독 — 목표 미달 여부를 볼 때만 참고. 통과 판정에는 쓰지 않음
- 이항검정 p — 겹침을 무시해 p를 과소평가. 기록만 남기고 판정에서 제외
07. 반복 실험 — 무엇을 넣고 무엇을 버렸나
앞 절까지가 "무엇으로 채점하는가"였다면 이 절은 "그 채점표를 들고 무엇을 바꿔봤나"입니다. h=5 모델은 홀드아웃 AUC 0.5088에서 0.5634까지 왔고, 그 사이에 스물다섯 번의 시도가 있었습니다.
규칙 — 한 번에 한 곳만 바꾼다
모든 시도는 직전 최고 성적 모델에 딱 한 곳만 바꿔서 만들었습니다. 피처 묶음 하나를 더하거나, 모델을 바꾸거나, 학습창 길이를 바꾸거나 — 둘을 동시에 바꾼 적은 없습니다.
이유는 단순합니다. 두 곳을 바꿔서 성적이 오르면 어느 쪽이 올린 건지 알 수 없고, 내려가도 어느 쪽이 깎았는지 알 수 없습니다. 그러면 다음 시도를 정할 근거가 사라집니다. 한 곳씩 바꾸면 시도 횟수는 늘지만 매번 배우는 게 남습니다.
남긴 네 단계

네 단계가 따로 논 게 아닙니다. 하나의 축입니다 — v57과 v59는 "미국 장이 닫혀 있는 동안 밖에서 무슨 일이 있었나"를 모으는 일이고, v62는 "그 정보를 어떤 국면에서 얼마나 믿을까"를 정하는 일입니다.
가장 큰 도약은 v42 → v57의 +0.0401입니다. 가격에서 짜낸 피처(분수차분)만으로는 0.51에서 멈췄고, 미국 밖 정보를 넣자 비로소 움직였습니다.
v62의 국면 교차항이 왜 작동했나
같은 직관을 앞서 v26에서도 시도했습니다. 그때는 "예측 크기 상위 1/3만 판정한다"로 구현했고 실패했습니다. 자신 있는 날만 골라 채점하면 성적이 오를 것 같지만, 실제로 한 일은 표본을 자른 것이었습니다.
v62는 자르지 않습니다. 대신 모델이 국면별 가중을 직접 배우게 했습니다 — 국면 지표(vt_ratio3m의 250일 백분위)를 주요 축 5개에 곱해서, 스트레스 성분과 평온 성분을 따로 줍니다. 표본은 그대로 두고 정보만 늘린 셈입니다.
기각한 대안들
모두 v62와 같은 기준선·같은 홀드아웃에서 비교했습니다.

버린 것에서 배운 세 가지
1. 동률이면 단순한 쪽을 남긴다. v67은 v62보다 0.0009 높았습니다. 숫자만 보면 v67이 이겼지만 채택하지 않았습니다. 0.0009는 홀드아웃 360건에서 측정 잡음보다 작은 차이이고, 그 대가로 시차 정렬 코드가 복잡해집니다. 재현·유지 비용을 이길 만한 이득이 아닙니다.
2. 앙상블이 세 번 연속 실패했다. v64(스태킹)·v66(2모델 평균)·그 이전 시도까지 앙상블 계열은 한 번도 개선을 못 냈습니다. 보통 앙상블은 공짜 점심으로 취급되지만, 여기서는 각 모델이 같은 750행을 보고 같은 실수를 하는 상태라 평균을 내도 실수가 안 지워집니다. 다양성이 없는 앙상블은 비용만 늘립니다.
3. 피처를 줄이면 무너진다 — 그런데 이유를 모른다. v61에서 검증된 축(국제 선행·국면 교차항)과 앵커만 32개로 추리자 0.4955, 동전 아래로 떨어졌습니다. 즉 기존 125개 피처가 배경 역할을 하고 있는데 그 역할이 무엇인지 설명하지 못합니다. 이건 이 모델의 약점으로 남겨 둡니다.
스스로 인정하는 문제 — 홀드아웃을 여러 번 봤다
v57·v59·v62는 모두 홀드아웃 성적을 보고 다음에 무엇을 시도할지 정했습니다. 엄밀히 하려면 홀드아웃은 마지막에 한 번만 열어야 합니다. 여러 번 열어 보고 그중 제일 좋은 걸 고르면, 홀드아웃이 사실상 개발 데이터가 되어 성적에 낙관 편향이 남습니다.
완화 요인이 하나 있습니다. v58에서 같은 국제 선행 축을 h=20 모델에 그대로 얹어 독립적으로 재현됐습니다.
# h=20 에 L축을 얹었을 때 (v58) — h=5 와 무관하게 오른다
SPY 0.6186 → 0.6347
QQQ 0.6024 → 0.6183
IWM 0.6921 → 0.6941
서로 다른 지평에서 같은 축이 같은 방향으로 움직였다는 건, 적어도 그 축만큼은 홀드아웃을 반복해서 본 결과의 부산물이 아니라는 뜻입니다.
이 절의 결론
- 작동한 축은 하나다 — 미국 장 밖 정보(v57·v59)와 그것을 국면으로 조건화하는 것(v62). 나머지는 전부 이 축의 변주이거나 실패다.
- 모델을 바꾸는 시도는 전부 졌다 — LightGBM·스태킹·앙상블. 이긴 것은 전부 피처 쪽이었다.
- 표본이 적을 때는 자동 탐색보다 도메인 지식이 낫다 — 손으로 만든 교차항 2개가 자동 탐색(v68)을 0.0446 앞섰다.
- 동률은 이긴 게 아니다 — 측정 잡음보다 작은 차이로 더 복잡한 구조를 들이지 않는다.
다음 절에서는 이 결과를 실제 운영에 어떻게 연결했는지 — 그리고 왜 운영 배선을 교체하지 않았는지를 다룹니다.
방향신호모델 h=5 (v62) 모델링 기록 · 8절
운영 반영 — 왜 배선을 바꾸지 않았나
목표를 넘겼는데도 실전에 넣지 않았다. 그 판단의 근거를 전부 적는다
v62는 목표였던 AUC 0.55를 넘겼고(0.5634), 네 지수 모두 이득이 양수였습니다. 상식적으로는 교체할 차례입니다.
교체하지 않았습니다. 운영은 지금도 이전 모델(방향신호_미국_5_1.pkl)을 씁니다. 새 모델은 아티팩트로만 저장했습니다.
# pattern_demo/us_spy_combo.py — 운영 배선. 바뀐 게 없다
PATHS = {5: "models/방향신호_미국_5_1.pkl", ...}
# models/방향신호_미국_5_2_meta.json — 새 모델 메타에 그대로 적어 뒀다
"wiring": "운영 배선은 아직 방향신호_미국_5_1.pkl 을 쓴다. 교체하지 않았다."
이유 ① 홀드아웃 밖에서는 신호가 0이었다
가장 무거운 근거입니다. 사전에 정해 둔 1차 판정은 홀드아웃이 아니라 깨끗 구간 통합입니다 — 피처 집합을 설계할 때 한 번도 들여다본 적 없는 구간(2010-11 · 2012-13 · 2014-15 · 2025~)을 다 합쳐서 재는 것입니다.
구간표본AUC상승 이득하락 이득판정
| 홀드아웃 (2025~) | 360 | 0.5634 | +4.80%p | +4.30%p | 목표 초과 |
| 깨끗 구간 통합 | 1,843 | 0.5076 | −0.16%p | −0.20%p | 미통과 |
표본이 다섯 배 많은 쪽에서 이득이 음수입니다. 0.5076은 사실상 동전입니다. 즉 홀드아웃 성적은 "이 모델이 늘 잘한다"가 아니라 "2025년 이후 구간에서 잘했다"는 뜻이고, 그 둘은 전혀 다른 주장입니다.
창별로 보면 갈린다
강함 2019-20 0.5886 · 2025~ 0.5634 · 2021-22 0.5399 · 2014-15 0.5192
약함 2010-11 0.4783 · 2023-24 0.4637
강한 구간은 코로나·약세장·관세충격처럼 변동성이 컸던 시기에 몰려 있습니다. 국면 교차항(v62)이 그 구간을 더 살렸을 뿐, 약한 구간을 고치지는 못했습니다.
이유 ② 추론 경로가 이 모델을 못 돌린다
기술적인 문제이지만 실질적입니다. 158개 피처를 만드는 코드가 연구 폴더 안에만 있습니다.
research/us_dir_v62/wf_inter.build
→ v59.build → v57.build → v42.build → v40 / v35 / v32
여섯 단계를 거슬러 올라가는 사슬이고, 운영 서버가 배포될 때 이 경로는 따라오지 않습니다. 교체하려면 이 빌더 전체를 research/ 밖으로 꺼내 안정된 모듈로 옮겨야 합니다.
이유 ③ 문턱이 0.5가 아니다
이 모델의 판정 문턱은 0.6376입니다. 학습 표본이 낸 예측확률의 중앙값을 쓴 값이고, 0.5가 아닙니다.
"threshold": 0.637638
"threshold_rule": "학습 표본 예측확률의 중앙값. 0.5 가 아니다."
"decision": "p > threshold 이면 상승, 아니면 하락"
추론 경로가 이걸 모르고 관행대로 0.5를 쓰면, 확률 0.5~0.6376 구간의 날들이 전부 상승으로 넘어갑니다. 거의 모든 날을 상승이라 찍는 모델이 되고, 그건 6절에서 배제한 다수 클래스 예측과 같아집니다.
더 나쁜 점은 이 실수가 에러 없이 조용히 일어난다는 것입니다. 화면에는 신호가 정상적으로 뜨고, 성적만 기저율로 주저앉습니다.
그럼에도 아티팩트는 남겼다
배선을 안 바꿨다고 결과를 버리지는 않습니다. 방향신호_미국_5_2.pkl과 짝이 되는 _meta.json에 평가방법 · 학습데이터 · 성능 세 가지를 함께 적어 뒀습니다.
기록내용
| 평가방법 | 워크포워드 창 750 · 재학습 21영업일 · 컷오프 origin−1일 · 퍼지 8일 + 엠바고 8일 · 블록 부트스트랩(길이 5, 2,000회) |
| 학습데이터 | yfinance 일봉 + FRED, 2004-01-01부터 · 158피처(기존 93 + DC 20 + 분수차분 12 + L 12 + E 10 + I 10) · 횡보 제외 없음 |
| 성능 | 홀드아웃 4지수 + 깨끗 통합 0.5076(미통과)까지 나쁜 숫자를 포함해 기록 |
몇 주 뒤에 이 파일을 다시 열었을 때 "이게 왜 운영에 안 들어갔더라"를 코드를 뒤지지 않고 알 수 있어야 합니다. 그래서 미통과 판정과 배선 미교체 사실을 메타 안에 같이 적었습니다.
교체하려면 무엇이 충족돼야 하나
- 깨끗 구간 통합에서 이득이 양수로 돌아설 것. 홀드아웃 성적이 아무리 좋아도 이게 음수면 안 넣습니다. 지금은 −0.16 / −0.20입니다.
- 158피처 빌더가 research/ 밖으로 나올 것. 여섯 단계 사슬을 하나의 안정된 모듈로 정리하고, 피처 이름·순서가 학습 때와 같은지 검사하는 테스트를 함께 남깁니다.
- 문턱 0.6376이 추론 경로에 실릴 것. 모델과 문턱이 같은 아티팩트에서 나와야 하고, 0.5로 떨어지면 테스트가 실패해야 합니다.
남은 질문
- 2010-11과 2023-24는 왜 뒤집히나. h=5와 h=20이 같은 구간에서 함께 무너집니다. 공통 원인을 찾으면 두 모델이 같이 좋아질 수 있습니다.
- 왜 검증된 축만으로는 안 서나. 국제 선행·국면 교차항만 32개로 추리면 0.4955로 무너집니다. 나머지 125개가 하는 배경 역할을 설명하지 못합니다.
- 경로 모델의 h=5 자리를 무엇으로 채울까. 이 신호를 넣을지, 아니면 중립(부호 0)으로 둘지 — 깨끗 통합이 0.5076이라 후자가 안전할 수 있습니다.
이 시리즈의 결론
스물다섯 번의 시도로 AUC를 0.5088에서 0.5634까지 올렸고, 홀드아웃 네 지수에서 이득이 전부 양수였습니다. 그리고 그것으로는 부족하다는 것이 결론입니다.
모델을 만드는 일보다 그 모델을 믿어도 되는지 판정하는 장치를 먼저 세워 둔 것이 이 기록에서 실제로 값을 한 부분입니다. 사전등록된 1차 판정이 없었다면, 홀드아웃 0.5634만 보고 교체했을 것이고 깨끗 구간의 −0.16은 영영 몰랐을 것입니다.