feat(weights): shadow chronos/lgbm 예측 + ensemble_weights 자동 보정

- ensemble.predict() 가 chronos_raw / lgbm_raw 를 함께 반환
- predict_and_store() 가 매 호출마다 3종 행 적재:
    model='ensemble' (user_triggered=인자)
    model='chronos'  (user_triggered=FALSE, shadow)
    model='lgbm'     (user_triggered=FALSE, shadow)
- retrain_weekly.adjust_weights(): 최근 30일 prediction_outcomes 의
  chronos vs lgbm hit_rate 로 ensemble_weights upsert
    w_chronos = clamp(0.1, hr_c/(hr_c+hr_l), 0.9), w_lgbm = 1 - w_chronos
  모델별 표본 < 10 이면 기본값(0.6/0.4) 유지
- API 응답에 saved_shadow_ids 추가 (TS 타입도 동기화)
- README: 동작 모델 메모 섹션을 실제 구현과 일치하도록 갱신

리뷰어 지적 3번 (ensemble_weights 가 영원히 갱신 안됨, upsert_weights 미호출) 해결.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
tkrmagid
2026-05-20 16:27:21 +09:00
parent 0af556396e
commit 5e6ce11491
5 changed files with 296 additions and 69 deletions

View File

@@ -19,7 +19,7 @@ Chronos 도 실패하면 LGBM 단독으로 진행.
from __future__ import annotations
import logging
from dataclasses import dataclass
from dataclasses import dataclass, field
import numpy as np
@@ -53,6 +53,10 @@ class EnsemblePrediction:
horizons: list[int]
steps: list[EnsembleStep]
sources_used: list[str]
# shadow 저장용 원본 출력 (predict_one.py 가 ensemble + chronos 단독 + lgbm 단독
# 3 종을 predictions 에 적재해서 retrain_weekly 가 모델별 hit_rate 비교 가능하게 함).
chronos_raw: ChronosForecast | None = None
lgbm_raw: dict[int, LgbmForecast] = field(default_factory=dict)
def _chronos_direction(samples: list[list[float]], base_close: float, horizon: int) -> tuple[float, float, float]:
@@ -90,12 +94,14 @@ def predict(code: str, *, horizons: tuple[int, ...] = (1, 3, 5)) -> EnsemblePred
logger.warning("chronos forecast failed for %s: %s", code, exc)
steps: list[EnsembleStep] = []
lgbm_raw: dict[int, LgbmForecast] = {}
for h in horizons:
lf: LgbmForecast | None = None
try:
lf = lgbm_predict(code, h)
if lf is not None:
sources_used.append(f"lgbm_h{h}")
lgbm_raw[h] = lf
except Exception as exc: # noqa: BLE001
logger.warning("lgbm predict failed for %s h=%d: %s", code, h, exc)
@@ -171,4 +177,6 @@ def predict(code: str, *, horizons: tuple[int, ...] = (1, 3, 5)) -> EnsemblePred
horizons=list(horizons),
steps=steps,
sources_used=sources_used,
chronos_raw=cf,
lgbm_raw=lgbm_raw,
)