feat(phase-4): LGBM 모델 + 앙상블 + 매칭/재학습 잡
- backend/app/models/lgbm.py: 종목 × horizon 별 LightGBM 회귀(y_ret_h)
+ 다중분류(y_dir_h, 3-class). joblib 으로 backend/data/models/{code}_h{H}_*.pkl
저장. early_stopping(30). predict_one() 으로 최신 영업일 피처에 추론.
- backend/app/models/weights.py: ensemble_weights 테이블 IO,
default w_chronos=0.6 / w_lgbm=0.4 (DB 행 없으면 fallback).
- backend/app/models/ensemble.py: Chronos sample 분포 + LGBM regression+cls
결합. point/q10/q90 + prob_up/flat/down + direction 라벨. 한쪽 모델
실패 시 다른 쪽 단독 fallback (cold start: chronos 단독).
- backend/app/pipelines/predict_one.py: predict_and_store(). 결과를
predictions 테이블에 UPSERT, user_triggered 누적 OR. base_date = 마지막
ohlcv 거래일, target_date = base_date + H 영업일(주말 스킵, 공휴일은
매칭잡에서 자연 보정).
- backend/app/pipelines/match_outcomes.py: target_date == d 인
user_triggered=TRUE 예측을 d 의 실제 종가와 매칭 → prediction_outcomes
적재. direction_hit(±0.3% flat band) + abs_error. 실제 종가 없으면
자연 skip.
- backend/app/pipelines/retrain_weekly.py: 시드 10종목 × H 재학습 +
최근 30일 model_performance 적재.
- backend/app/db/migrations/003_ensemble_weights.sql: (code, horizon) →
(w_chronos, w_lgbm, hit_rate_*, sample_count).
- backend/app/pipelines/scheduler.py:
daily_batch : 평일 16:00 KST
match_outcomes : 평일 16:30 KST ← 사용자가 확정한 매칭 시점
retrain_weekly : 일요일 02:00 KST
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
75
backend/app/models/weights.py
Normal file
75
backend/app/models/weights.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""ensemble_weights 테이블 IO. 기본 가중치 (chronos 0.6, lgbm 0.4)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db.connection import get_engine
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnsembleWeights:
|
||||
code: str
|
||||
horizon: int
|
||||
w_chronos: float
|
||||
w_lgbm: float
|
||||
|
||||
|
||||
DEFAULT_W_CHRONOS = 0.6
|
||||
DEFAULT_W_LGBM = 0.4
|
||||
|
||||
|
||||
def load_weights(code: str, horizon: int) -> EnsembleWeights:
|
||||
eng = get_engine()
|
||||
with eng.connect() as conn:
|
||||
row = conn.execute(
|
||||
text(
|
||||
"SELECT w_chronos, w_lgbm FROM ensemble_weights "
|
||||
"WHERE code = :code AND horizon = :h"
|
||||
),
|
||||
{"code": code, "h": horizon},
|
||||
).first()
|
||||
if not row:
|
||||
return EnsembleWeights(code, horizon, DEFAULT_W_CHRONOS, DEFAULT_W_LGBM)
|
||||
return EnsembleWeights(code, horizon, float(row[0]), float(row[1]))
|
||||
|
||||
|
||||
def upsert_weights(
|
||||
code: str,
|
||||
horizon: int,
|
||||
w_chronos: float,
|
||||
w_lgbm: float,
|
||||
*,
|
||||
hit_rate_chronos: float | None = None,
|
||||
hit_rate_lgbm: float | None = None,
|
||||
sample_count: int | None = None,
|
||||
) -> None:
|
||||
eng = get_engine()
|
||||
with eng.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO ensemble_weights
|
||||
(code, horizon, w_chronos, w_lgbm, hit_rate_chronos, hit_rate_lgbm, sample_count, updated_at)
|
||||
VALUES
|
||||
(:code, :h, :wc, :wl, :hc, :hl, :n, NOW())
|
||||
ON CONFLICT (code, horizon) DO UPDATE SET
|
||||
w_chronos = EXCLUDED.w_chronos,
|
||||
w_lgbm = EXCLUDED.w_lgbm,
|
||||
hit_rate_chronos = EXCLUDED.hit_rate_chronos,
|
||||
hit_rate_lgbm = EXCLUDED.hit_rate_lgbm,
|
||||
sample_count = EXCLUDED.sample_count,
|
||||
updated_at = NOW()
|
||||
"""
|
||||
),
|
||||
{
|
||||
"code": code,
|
||||
"h": horizon,
|
||||
"wc": float(w_chronos),
|
||||
"wl": float(w_lgbm),
|
||||
"hc": hit_rate_chronos,
|
||||
"hl": hit_rate_lgbm,
|
||||
"n": sample_count,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user