Passive 에서의 시장예측 모델링

2026. 10. 23. 17:29ㆍ카테고리 없음

화면 뒤의 데이터 판단 · 방향예측 모델

pattern_demo/kr_dir5_v3.py — "5일 뒤 오를까"를 0~100점으로 만드는 과정

정답 정하기 → 재료 손질 → 학습 → 섞기·보정 → 점수, 그리고 20일 모델은 무엇이 다른가

Passive 홈의 기간별 예측점수는 "앞으로 5일·20일·60일 동안 시장이 오를 가능성"을 0~100점으로 보여 줍니다. 이 글은 그중 국내 5일 점수를 만드는 모델이 어떤 순서로 만들어졌는지를 실제 코드와 모델 파일(models/*.pkl, *_meta.json) 기준으로 따라갑니다.

1. 지금 운영 중인 모델

적용 화면 — 홈 › 기간별 예측점수 세 칸(5일·20일·60일). 5일 칸은 서버가 바로 채우고, 20·60일 칸은 /api/horizon-scores 가 채웁니다.

기간 모델 파일 한 줄 요약
국내 5일 방향신호_한국_5_3.pkl 추세모형과 v2 모델을 반반 섞은 점수. 실패하면 5_2 → 5_1 순서로 대신 씀.
국내 20일 방향신호_한국_20_3.pkl 상승장·하락장을 나눠 따로 배운 모델.
국내 60일 방향신호_한국_60_5.pkl 국가지수 패널 로지스틱. 실패하면 다른 모델로 바꾸지 않고 직전 값을 유지.
미국 5일 방향신호_미국_5_3.pkl S&P500(SPY) 대상, 추세 피처 195개.

표 1. 파일 연결은 processor/horizon_scores.py·pattern_demo/front_signals.py·pattern_demo/us_spy_combo.py 에서 확인.

2. 정답 정하기 — 오르냐 내리냐, 둘 중 하나

적용 화면 — 홈 › 기간별 예측점수 › 5일 칸(점수가 맞히려는 대상).

모델이 맞히는 정답(종속변수)은 "5일 뒤 종가가 오늘보다 높은가"입니다. 상승이면 1, 하락이면 0, 횡보라는 세 번째 답은 없습니다.

대신 학습할 때만 한 가지 거름망을 겁니다. 5일 동안 2%도 안 움직인 날은 오른 건지 내린 건지 잡음에 가깝기 때문에 공부 자료에서 뺍니다. 평가는 전체 날짜로 합니다(scripts/build_kr_dir5_v2.py 58줄, meta 의 move_th: 0.02).

3. 재료와 전처리

적용 화면 — 화면에 직접 노출되지 않음(배치가 점수를 계산할 때 내부에서 사용).

재료는 세 가지입니다. KODEX200·삼성전자·SK하이닉스 등 국내 8종목의 일봉, 외국인·기관·개인 수급, 그리고 S&P·나스닥·환율·반도체 등 해외 자산 9개입니다. 8종목을 한 모델에 같이 넣어(풀링) 학습 표본을 8배로 늘립니다.

  • 미래 누수 차단 — 해외 자료는 예측 시점 t 보다 앞선 날짜만 붙이고, 비는 날은 직전 값으로 채웁니다. 20주선 같은 주봉 지표는 다 끝난 봉만 씁니다.
  • 결측 — 학습할 땐 빈칸이 있는 행을 버리고, 실제 예측 땐 빈칸 없는 가장 최근 행을 씁니다.
  • 신선도 — 미국 5일 피처 빌더는 외부 시세가 종가보다 너무 늦게 들어오면 StaleData 로 그날 계산을 막습니다. 국내는 수집이 실패하면 예비 모델로 내려갑니다.

4. 피처 엔지니어링 — 재료를 모델이 먹기 좋게

적용 화면 — 화면에 직접 노출되지 않음(점수 계산 내부).

원재료를 그대로 넣지 않고 "모델이 비교할 수 있는 숫자"로 바꿉니다. 국내 5일 v3 는 111개 피처를 20개 군으로 묶어 씁니다.

  • 수급·가격 군(84개) — 기존 v2 모델의 국내 피처 군(flows_short·instdetail·retail 등)을 그대로 가져오고, 효과 없던 vrp 군만 뺐습니다.
  • 교차자산 추세(27개) — 해외 자산 9개마다 "20개월선·20주선·20일선 위에 몇 개나 있나" 합계와 20주·20일 이격도.
  • 추세 × 국면(6개) — 추세가 어떤 시장 분위기에서 나왔는지 곱한 값.

크기를 맞추는 방법은 쉽게 말해 "오늘 오른 폭을 최근 석 달 평소 흔들림과 비교해 몇 배인지로 바꾸고, 너무 튀는 값은 잘라내고, 어떤 값은 지난 1년 중 몇 등인지로 바꾸는" 것입니다. 미국 5일 피처 빌더(pattern_demo/us_h5_trend_features.py)는 수익률을 60일 변동성으로 나눠 ±4~5 에서 자르고, 변동성 비율을 250일 백분위로 바꿉니다.

5. 학습 — 얕은 LightGBM + 에라 부스팅 + 시드 9개

적용 화면 — 화면에 직접 노출되지 않음(models/방향신호_한국_5_3.pkl 로 저장).

모델은 깊이를 얕게 제한한 LightGBM 분류기입니다. 여기에 에라 부스팅을 얹습니다. 학습 기간을 반기(6개월) 단위로 나눠, 모델이 유독 못 맞힌 하위 절반 시기에만 나무를 더 붙이는 일을 6번 반복하는 방법입니다(Numerai 에서 쓰는 기법). 특정 장세에만 잘 맞는 모델이 아니라 어느 시기에도 고르게 맞는 모델을 노립니다.

research/kr_dir5/core.py · walkforward() · 3104~3115줄

# 설정에 에라 부스팅 횟수(운영 6회)가 있을 때만 이 방식으로 학습한다
if cfg.get('eraboost'):
    # 시기별 맞힘 정도(AUC)를 재는 함수를 불러온다
    from sklearn.metrics import roc_auc_score
    # 학습 날짜를 6개월 단위 구간 번호로 바꾼다
    era = _era(dates[tr], cfg.get('era_m', 6))
    # 먼저 나무 100그루짜리 기본 모델을 전체 학습 데이터로 학습한다
    m = make_model(kind, dict(mp, n_estimators=100), seed=42 + s).fit(Xtr, ytr)
    # 아래 보강을 6번 반복한다
    for _ in range(cfg['eraboost']):
        # 현재 모델로 학습 데이터의 상승확률을 예측하고, 구간별 성적표를 준비한다
        pe = _score(m, Xtr); ea = {}
        # 6개월 구간을 하나씩 돈다
        for e_ in np.unique(era):
            # 이번 구간에 속한 행만 고른다
            k_ = era == e_
            # 그 구간의 AUC 를 잰다(정답이 한쪽뿐이라 잴 수 없으면 1.0)
            ea[e_] = roc_auc_score(ytr[k_], pe[k_]) if len(set(ytr[k_])) > 1 else 1.0
        # AUC 가 낮은 하위 절반 구간의 행만 표시한다
        worst = np.isin(era, sorted(ea, key=ea.get)[:max(1, len(ea) // 2)])
        # 나무 50그루를 추가할 새 모델 틀을 만든다
        m2 = make_model(kind, dict(mp, n_estimators=50), seed=42 + s)
        # 기존 모델에 이어서, 못 맞힌 구간 데이터로만 50그루를 더 학습한다
        m2.fit(Xtr[worst], ytr[worst], init_model=m.booster_); m = m2

make_model 은 학습 전의 빈 LightGBM 을 만드는 함수이고, 실제 학습은 .fit() 이 합니다. _score 는 학습된 모델에서 상승확률만 꺼내는 도우미입니다. 시드 하나당 나무는 100 + 50×6 = 400그루가 됩니다.

시드 9개는 설정은 똑같고 무작위 출발점만 다른 모델 9개를 따로 학습해 확률을 평균한 것입니다. 같은 문제를 9명에게 풀게 해 한 명의 운에 휘둘리지 않게 하는 셈입니다. 처음엔 3개였는데, 돌릴 때마다 검증 성적(AUC)이 0.5405~0.546 사이로 흔들려서 9개로 늘렸더니 안정됐습니다(research/kr_h5_trend/plan.md 75~77줄). 5개·7개 같은 다른 개수와 체계적으로 비교한 기록은 없습니다.

6. 섞기·보정·점수화

적용 화면 — 홈 › 기간별 예측점수 › 5일 칸의 큰 숫자와 판정 이름(하락 우세 ~ 상승 우세).

추세모형 9개 평균 확률을 기존 v2 모델 확률과 0.5 : 0.5 로 섞은 뒤, 두 단계를 거쳐 점수로 바꿉니다.

  • 보정 — 과거 몇 년 치를 모델에게 미리 풀게 해서 "모델이 60% 라고 했던 날 실제로는 몇 % 올랐나"를 맞춰 보고, 그 차이만큼 확률을 늘리거나 줄이는 환산표(sigmoid(a·logit(p)+b), 플랫 스케일링)를 만듭니다. 이 과거 예측은 워크포워드로 만들었습니다. 2018년은 2017년까지만 배운 모델이 맞히는 식이라, 모델이 처음 보는 문제의 답안만 씁니다.
  • 점수화 — 평소 오르던 비율(기저 약 53.7%)이면 50점, 그보다 높으면 50점 위로 늘려 0~100점 안에 넣습니다. clip(50 + 50·(보정확률 − 기저)/span, 0, 100)

예를 들어 모델이 "60% 오른다"고 하면(예시 입력), 운영 모델에 저장된 보정값으로 계산해 약 56.4% 가 됩니다. 기저 53.7% 보다 2.7%p 높고, 이 모델은 약 5.8%p 를 50점 폭으로 치니 50 + 50 × (2.7 ÷ 5.8) ≈ 73점이 됩니다.

7. 전체 흐름과 운영

적용 화면 — 홈 › 기간별 예측점수(app_cache 에 저장된 값을 읽어 그림).

그림 1. 재료 → 정답·피처 → 학습 → 섞기·점수화 → 화면.

점수는 매일 정해진 시간에 서버 배치가 새 데이터로 다시 계산해 app_cache 에 저장하고, 앱은 그 저장값만 꺼내 보여 줍니다. v3 계산이 실패한 날(수집 실패·피처 결측·pkl 없음)은 v2 → v1 순서로 내려가고, 어떤 모델이 쓰였는지와 실패 사유를 캐시에 같이 남깁니다. 폴백이 조용히 일어나 며칠간 옛 값을 쓴 사고(2026-08-07)가 있었기 때문입니다.

8. 20일 모델은 무엇이 다른가

적용 화면 — 홈 › 기간별 예측점수 › 20일 칸(processor/horizon_scores.py → pattern_demo/kr_dir20_v3.py).

  • 정답 기준 — 5일 뒤 2% 대신 20일 뒤, 평가는 3% 이상 움직인 날 기준.
  • 재료 — 수급·해외추세 없이 자기 가격·아시아 지수(닛케이·항셍·상하이)·과열/조정 지표 51개. 아직 결과가 안 나온 최근 20일은 학습에서 뺍니다.
  • 장세 분리 — 주가가 200일선 위인 날(상승장)과 아래인 날(하락장)을 갈라 각각 전용 모델을 만들고, 전체 날짜로 배운 예비 모델도 하나 둡니다. 오늘 장에 맞는 전용 모델로 예측하고, 한쪽 장 표본이 400일 이하면 예비 모델을 씁니다. 세 모델 모두 시드 5개 평균입니다.

정리

  • 정답은 5일 뒤 오르냐 내리냐 둘 중 하나이고, 2% 미만 움직인 날은 학습에서만 뺀다.
  • 재료는 가격·수급·해외 자산이며, 해외 자료는 예측 시점 이전 날짜만 쓴다.
  • 학습은 얕은 LightGBM 에 에라 부스팅 6회, 시드 9개 평균.
  • 점수는 v2 와 반반 섞은 확률을 워크포워드 기준으로 보정해 기저 50점으로 바꾼 값이다.
  • 20일 모델은 재료를 줄이고 200일선 기준으로 장세를 나눠 따로 배운다.

이 점수는 투자 참고용 정보이며 수익을 보장하지 않습니다.

더 많은 글 → dinsightlab.com

 

Passive

미국 지수 투자자를 위한 금융 데이터 분석 사이트

dinsightlab.com