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