발단: 단순한 설정 누락
docker-compose.yml의 environment 항목은 어떤 환경변수를 컨테이너에 넘길지 명시적으로 나열하는 방식이다. 그런데 이 목록에 HYBRID_ENABLE_ML이 빠져 있었다. .env 파일에 값을 뭐라고 적어두든 컨테이너 안까지 전달되지 않았고, 그 결과 enable_ml은 항상 기본값인 false로 떨어졌다. 앙상블 전략에서 ML에 배정된 가중치 0.15가 항상 0을 출력하고 있었다는 뜻이다. 2026년 7월 28일 개장 점검 과정에서 발견됐다.
고치는 건 간단했다, 그런데
HYBRID_ENABLE_ML=${HYBRID_ENABLE_ML:-false}를 목록에 추가해서 전달 경로 자체는 복구했다. 하지만 여기서 그냥 “이제 .env 값대로 켜지겠지”라고 끝내지 않았다. 기본값을 false로 확정했다 — 즉 지금 당장 동작을 바꾸지 않기로 한 것이다.
이유는 “그럼 ML을 켜는 게 실제로 더 나은가”라는 질문이 따로 남아 있었기 때문이다. 버그를 고치는 것과, 고친 다음 그 기능을 켤지 말지는 별개의 판단이다.
세 방향에서 검증하고, 셋 다 기각했다
36개 종목을 대상으로 세 가지 방식으로 확인했다.
1) ML을 켰을 때와 껐을 때의 A/B 백테스트
| 기간 | 설정 | Sharpe | 평균수익률 | 승률 |
|---|---|---|---|---|
| OOS | OFF | -1.69 | -0.19% | 31.2% |
| OOS | ON | -1.25 | -0.31% | 27.9% |
| 2년 | OFF | -0.84 | +3.07% | 45.8% |
| 2년 | ON | -1.31 | +1.61% | 39.4% |
두 기간 모두 승률과 수익률이 ML을 켰을 때 오히려 나빠졌다. 거래량만 약 40% 늘었다.
2) 분류 타깃을 바꿔봐도 기저율을 못 넘었다
| 방식 | cv_acc | 기저율 |
|---|---|---|
| 3분류(현행, ±1%) | 0.442 | 0.485 |
| 클래스 가중 3분류 | 0.431 | 0.485 |
| 2분류(flat 제거) | 0.516 | 0.555 |
분류 방식을 세 가지로 바꿔봤지만 어떤 형태로도 “아무것도 안 하고 다수 클래스만 찍는 것”보다 나은 성능이 나오지 않았다.
3) 회귀 타깃으로 바꿔서 더 엄격하게 재검증
회귀 방식으로 전환해서 정보계수(IC)를 측정했는데, walk-forward 방식으로 엄격하게 측정하니 IC가 +0.0051이었다. 95% 신뢰구간은 [-0.033, +0.044]로 0을 포함한다 — 통계적으로 예측력이 있다고 말할 수 없는 수준이다. 백테스트에서도 baseline이 수익(+2.91%)과 승률(45.4%) 모두에서 회귀 ML(+2.33%, 43.1%)을 앞섰다.
스스로 잡아낸 함정
여기서 중요한 대목이 하나 있다. 진단 초기 단계에서 TimeSeriesSplit 방식의 교차검증(CV)으로는 IC가 +0.050으로 나왔었다. 언뜻 보면 쓸만한 수치다. 하지만 이걸 그대로 믿지 않고 더 엄격한 walk-forward 방식으로 재측정했더니, 그 +0.050이라는 값이 0과 통계적으로 구분되지 않는 수준으로 무너졌다.
진단 단계의 TimeSeriesSplit CV에서는 IC +0.050이 나왔으나, 엄격 walk-forward로 재측정하니 0과 구분되지 않았다. CV 방식의 낙관 편향이었다.
이건 “처음 방법이 틀렸다”는 걸 스스로 인정하고 더 보수적인 검증으로 넘어간 사례다. CV가 실제보다 낙관적인 결과를 주는 경향이 있다는 걸 알고 있었기 때문에, 그 결과를 최종 판단의 근거로 쓰지 않고 재검증을 한 단계 더 거친 것이다.
근본 원인: 라벨이 아니라 피처
| 세 검증 모두 ML을 켜는 걸 기각했는데, 그 이유를 타깃(라벨)을 어떻게 정의하느냐의 문제로 보지 않았다. 실제 원인은 피처 쪽이었다. 사용 중인 피처 15개 중 | IC | 가 0.03 이상인 것은 단 2개뿐이었다(feat_atr -0.055, feat_volatility -0.047). 타깃을 3분류로 하든 2분류로 하든 회귀로 하든,애초에 학습할 만한 정보가 피처에 담겨 있지 않았다는 뜻이다. |
이 피처들을 살리려면 재무 데이터, 수급 데이터, 뉴스 같은 완전히 새로운 정보원이 필요하다는 결론을 남기고, 그런 정보원이 갖춰지기 전까지는 HYBRID_ENABLE_ML을 true로 바꾸지 않기로 했다.
검증
docker compose config | grep HYBRID_ENABLE_ML
→ HYBRID_ENABLE_ML: "false" # .env 값이 정상 전달됨(주석 혼입 없음)
전달 경로 자체가 고쳐졌는지를 확인했고, 적용은 다음 컨테이너 재시작(장 마감 후) 시점으로 미뤘다 — 실거래 중인 시스템이라 장중에 컨테이너를 건드리지 않기 위해서다.
남는 교훈
설정 버그 하나(환경변수 누락)를 고치는 일이, “이 기능을 켜는 게 맞는가”라는 훨씬 큰 질문으로 자연스럽게 이어졌다. 그리고 그 질문에 답하는 과정에서, 처음 썼던 검증 방법(TimeSeriesSplit CV)이 실제보다 낙관적인 결과를 준다는 것까지 발견하고 더 엄격한 방법(walk-forward)으로 다시 확인했다.
머신러닝을 다루는 실무에서 이런 낙관 편향은 꽤 흔하게 발생한다. 좋아 보이는 첫 결과를 그대로 믿지 않고, “이 검증 방법 자체가 나를 속이고 있는 건 아닌가”를 한 번 더 의심하는 태도가 이 작업 전체를 관통하고 있었다.