"""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")