""" Gainer snapshot collector — fetches top 200 day_gainers every 5-min slot. Uses direct Yahoo Finance API via yahoo_client (no yfinance_plus). """ import asyncio import logging from datetime import datetime, timezone logger = logging.getLogger(__name__) def _floor_to_5min(dt: datetime) -> datetime: return dt.replace(minute=dt.minute - (dt.minute % 5), second=0, microsecond=0) async def collect_gainer_snapshot() -> int: """Fetch 200 day_gainers and bulk-insert into gainer_snapshots. Returns inserted count.""" from app.services.gainers.yahoo_client import fetch_day_gainers_sync, _parse_quote from app.core.database import AsyncSessionLocal from app.models.gainer_snapshot import GainerSnapshot from sqlalchemy.dialects.postgresql import insert as pg_insert snapshot_at = _floor_to_5min(datetime.now(timezone.utc)) loop = asyncio.get_event_loop() raw_quotes, total, error = await loop.run_in_executor( None, fetch_day_gainers_sync, 200, 0 ) if error: logger.error("[Gainers] fetch failed: %s", error) return 0 if not raw_quotes: logger.warning("[Gainers] No stocks returned (total=%d)", total) return 0 rows = [ { "snapshot_at": snapshot_at, "rank": rank, "symbol": s["symbol"], "name": s.get("name"), "exchange": s.get("exchange"), "price": s.get("price"), "change_percent": s.get("change_percent"), "volume": s.get("volume"), "avg_volume_3m": s.get("avg_volume_3m"), "market_cap": s.get("market_cap"), "pe_ratio": s.get("pe_ratio"), "forward_pe": s.get("forward_pe"), "eps_ttm": s.get("eps_ttm"), "dividend_yield": s.get("dividend_yield"), "fifty_two_week_high": s.get("fifty_two_week_high"), "fifty_two_week_low": s.get("fifty_two_week_low"), } for rank, s in enumerate((_parse_quote(q) for q in raw_quotes), start=1) ] async with AsyncSessionLocal() as db: stmt = pg_insert(GainerSnapshot).values(rows) stmt = stmt.on_conflict_do_nothing(constraint="uq_gainer_snapshot_symbol") await db.execute(stmt) await db.commit() logger.info("[Gainers] snapshot %s — %d/%d rows", snapshot_at.isoformat(), len(rows), total) return len(rows)