- 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>
76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
"""APScheduler 기반 백그라운드 잡.
|
|
|
|
- 16:00 KST 평일: daily_batch (시드 10종목 EOD/뉴스/공시/거시 갱신)
|
|
- (Phase 4) 16:30 KST 평일: prediction_outcomes 매칭 배치.
|
|
user_triggered=TRUE 예측 중 target_date == 당일 거래일 인 행을,
|
|
KRX 정규장 마감(15:30) 후 확정된 종가/방향과 매칭해 적재.
|
|
주말/공휴일이 끼면 다음 거래일로 자연 이월(거래일이 아니면 매칭할 종가가 없으니 스킵).
|
|
- (Phase 4) 02:00 KST 일요일: 최근 30일 hit rate 기반 앙상블 가중치 보정,
|
|
임계 미만 모델은 LGBM 재학습.
|
|
|
|
예측 추론(inference) 자체는 사용자가 "예상차트 보기" 누른 시점에 on-demand 로만 돌고,
|
|
스케줄러에서는 돌리지 않음.
|
|
|
|
FastAPI 기동 시점에 lifespan 으로 start, 종료 시 shutdown.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from pytz import timezone
|
|
|
|
from app.pipelines.daily_batch import run_daily_batch
|
|
from app.pipelines.match_outcomes import match_today
|
|
from app.pipelines.retrain_weekly import run_weekly
|
|
|
|
logger = logging.getLogger(__name__)
|
|
KST = timezone("Asia/Seoul")
|
|
|
|
_scheduler: BackgroundScheduler | None = None
|
|
|
|
|
|
def start_scheduler() -> BackgroundScheduler:
|
|
global _scheduler
|
|
if _scheduler:
|
|
return _scheduler
|
|
_scheduler = BackgroundScheduler(timezone=KST)
|
|
# 16:00 평일: 시드 10종목 EOD/뉴스/공시/거시 갱신
|
|
_scheduler.add_job(
|
|
run_daily_batch,
|
|
CronTrigger(day_of_week="mon-fri", hour=16, minute=0, timezone=KST),
|
|
id="daily_batch_16",
|
|
replace_existing=True,
|
|
max_instances=1,
|
|
)
|
|
# 16:30 평일: prediction_outcomes 매칭 배치
|
|
_scheduler.add_job(
|
|
match_today,
|
|
CronTrigger(day_of_week="mon-fri", hour=16, minute=30, timezone=KST),
|
|
id="match_outcomes_1630",
|
|
replace_existing=True,
|
|
max_instances=1,
|
|
)
|
|
# 일요일 02:00: LGBM 재학습 + 성능 기록
|
|
_scheduler.add_job(
|
|
run_weekly,
|
|
CronTrigger(day_of_week="sun", hour=2, minute=0, timezone=KST),
|
|
id="retrain_weekly_sun_0200",
|
|
replace_existing=True,
|
|
max_instances=1,
|
|
)
|
|
_scheduler.start()
|
|
logger.info(
|
|
"scheduler started: daily_batch(16:00 mon-fri), match_outcomes(16:30 mon-fri), retrain_weekly(sun 02:00) KST"
|
|
)
|
|
return _scheduler
|
|
|
|
|
|
def shutdown_scheduler() -> None:
|
|
global _scheduler
|
|
if _scheduler:
|
|
_scheduler.shutdown(wait=False)
|
|
_scheduler = None
|
|
logger.info("scheduler stopped")
|