""" APScheduler entry point for gainer snapshot collection. Job: every 5 minutes on Mon-Fri ET, guarded to NYSE market hours (9:30–16:00 ET). Note: US market holidays are treated as normal weekdays — Yahoo simply returns fewer or no results on those days, which is harmless. """ import logging from datetime import datetime, time logger = logging.getLogger(__name__) _scheduler = None def _is_market_open() -> bool: """Return True if current ET time is within NYSE regular session (9:30–16:00).""" try: from zoneinfo import ZoneInfo now_et = datetime.now(ZoneInfo("America/New_York")) except ImportError: import pytz now_et = datetime.now(pytz.timezone("America/New_York")) return time(9, 30) <= now_et.time() <= time(16, 0) def _get_scheduler(): global _scheduler if _scheduler is None: try: from apscheduler.schedulers.asyncio import AsyncIOScheduler _scheduler = AsyncIOScheduler(timezone="UTC") except ImportError: logger.warning("apscheduler not installed; gainer collection disabled") return None return _scheduler async def _run_collect_job() -> None: if not _is_market_open(): return try: from app.services.gainers.collector import collect_gainer_snapshot await collect_gainer_snapshot() except Exception as e: logger.error("[Gainers] collection failed: %s", e) def start_gainer_scheduler() -> None: sched = _get_scheduler() if sched is None: return try: from apscheduler.triggers.cron import CronTrigger sched.add_job( _run_collect_job, trigger=CronTrigger( minute="*/5", day_of_week="mon-fri", timezone="America/New_York", ), id="gainer_snapshot_collect", replace_existing=True, max_instances=1, misfire_grace_time=120, coalesce=True, ) if not sched.running: sched.start() logger.info("[Gainers] scheduler started — every 5 min Mon-Fri ET (market hours guard)") except Exception as e: logger.error("[Gainers] scheduler start failed: %s", e) def stop_gainer_scheduler() -> None: sched = _get_scheduler() if sched and sched.running: sched.shutdown(wait=False) logger.info("[Gainers] scheduler stopped")