""" APScheduler-based batch scheduler for the Overlay pipeline. Integrated into FastAPI's lifespan via start_scheduler() / stop_scheduler(). """ import logging from datetime import datetime, timezone logger = logging.getLogger(__name__) _scheduler = None def _get_scheduler(): """Lazily create the APScheduler instance.""" global _scheduler if _scheduler is None: try: from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger _scheduler = AsyncIOScheduler(timezone="UTC") except ImportError: logger.warning("apscheduler not installed; overlay batch scheduling disabled") return None return _scheduler async def _run_collect_job() -> None: """Scheduled job: collect RSS + wiki + youtube + trends.""" from app.core.database import AsyncSessionLocal from app.services.overlay.overlay_pipeline import OverlayPipeline pipeline = OverlayPipeline() async with AsyncSessionLocal() as db: try: counts = await pipeline.collect_all(db) logger.info(f"[Scheduler] collect_all completed: {counts}") except Exception as e: logger.error(f"[Scheduler] collect_all failed: {e}") async def _run_build_job() -> None: """Scheduled job: build features for all TOP_50 symbols.""" from app.core.database import AsyncSessionLocal from app.services.overlay.overlay_pipeline import OverlayPipeline pipeline = OverlayPipeline() async with AsyncSessionLocal() as db: try: built = await pipeline.build_features_batch(db) logger.info(f"[Scheduler] feature_build completed: {built} symbols") except Exception as e: logger.error(f"[Scheduler] feature_build failed: {e}") def start_scheduler() -> None: """Start the APScheduler with overlay jobs. Call from FastAPI lifespan startup.""" from app.core.config import settings if not getattr(settings, "OVERLAY_ENABLED", True): logger.info("Overlay disabled; scheduler not started") return sched = _get_scheduler() if sched is None: return try: from apscheduler.triggers.cron import CronTrigger # Collect: 23:30 UTC daily on weekdays (≈ 18:30 ET) sched.add_job( _run_collect_job, trigger=CronTrigger(day_of_week="mon-fri", hour=23, minute=30, timezone="UTC"), id="overlay_collect", replace_existing=True, misfire_grace_time=600, ) # Feature build: 01:30 UTC (≈ 20:30 ET) sched.add_job( _run_build_job, trigger=CronTrigger(day_of_week="mon-fri", hour=1, minute=30, timezone="UTC"), id="overlay_feature_build", replace_existing=True, misfire_grace_time=600, ) sched.start() logger.info("Overlay scheduler started (collect @ 23:30 UTC, build @ 01:30 UTC, weekdays)") except Exception as e: logger.error(f"Overlay scheduler start failed: {e}") def stop_scheduler() -> None: """Stop the scheduler. Call from FastAPI lifespan shutdown.""" sched = _get_scheduler() if sched and sched.running: sched.shutdown(wait=False) logger.info("Overlay scheduler stopped")