You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
4.3 KiB
Python

"""Yahoo Finance day_gainers fetcher for TGTC strategy.
Uses yfinance.screen("day_gainers") instead of the raw httpx endpoint
(query1.finance.yahoo.com/v1/finance/screener/predefined/saved was IP-blocked
after the zombie-daemon burst; yfinance routes through a different path).
Usage:
# Async (in engine)
quotes = await fetch_day_gainers()
# Sync one-shot (CLI / testing)
quotes = fetch_day_gainers_sync()
CLI:
python -m apps.tgtc_trader.yahoo_gainers --once
"""
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass
from typing import Any
import yfinance as yf
log = logging.getLogger(__name__)
# Deprecated / blocked endpoint constants kept as comments for reference:
# _YAHOO_URL = "https://query1.finance.yahoo.com/v1/finance/screener/predefined/saved?..."
# _HEADERS = {"User-Agent": "Mozilla/5.0 ..."}
_TIMEOUT_SECS = 12 # kept for documentation; yfinance manages its own timeout
_MAX_RETRIES = 3
_RETRY_DELAY_SECS = 3.0
@dataclass
class GainerQuote:
symbol: str
rank: int
price: float
pct_change: float # e.g. 0.12 = +12%
volume: float
market_cap: float | None
exchange: str | None
def _parse_yf_quotes(result: dict[str, Any]) -> list[GainerQuote]:
"""Extract GainerQuote list from yfinance.screen() result dict."""
try:
quotes_raw = result.get("quotes", [])
except (AttributeError, TypeError):
log.warning("TGTC Yahoo: unexpected yfinance screen result structure")
return []
out: list[GainerQuote] = []
for rank, q in enumerate(quotes_raw, start=1):
try:
sym = q.get("symbol", "").upper()
if not sym:
continue
price = float(q.get("regularMarketPrice") or 0)
# yfinance already returns pct as raw value (e.g. 31.16834 = +31.17%)
pct = float(q.get("regularMarketChangePercent") or 0) / 100.0
vol = float(q.get("regularMarketVolume") or 0)
mktcap_raw = q.get("marketCap")
mktcap = float(mktcap_raw) if mktcap_raw is not None else None
exchange = q.get("exchange") or q.get("fullExchangeName")
out.append(GainerQuote(
symbol=sym,
rank=rank,
price=price,
pct_change=pct,
volume=vol,
market_cap=mktcap,
exchange=exchange,
))
except Exception as exc:
log.debug("TGTC Yahoo: skip quote parse error: %s", exc)
return out
async def fetch_day_gainers(count: int = 100) -> list[GainerQuote]:
"""Async fetch of Yahoo day_gainers screener via yfinance. Retries up to _MAX_RETRIES times."""
last_exc: Exception | None = None
for attempt in range(_MAX_RETRIES):
try:
result = await asyncio.to_thread(yf.screen, "day_gainers", count=count)
return _parse_yf_quotes(result)
except Exception as exc:
last_exc = exc
log.warning("TGTC Yahoo: fetch attempt %d/%d failed: %s",
attempt + 1, _MAX_RETRIES, exc)
if attempt < _MAX_RETRIES - 1:
await asyncio.sleep(_RETRY_DELAY_SECS * (attempt + 1))
log.error("TGTC Yahoo: all %d attempts failed: %s", _MAX_RETRIES, last_exc)
return []
def fetch_day_gainers_sync(count: int = 100) -> list[GainerQuote]:
"""Synchronous wrapper for testing or CLI usage."""
return asyncio.run(fetch_day_gainers(count))
# ── CLI one-shot ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
import argparse
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
parser = argparse.ArgumentParser(description="TGTC Yahoo day_gainers one-shot fetch")
parser.add_argument("--once", action="store_true", help="Fetch once and print")
parser.add_argument("--count", type=int, default=100)
args = parser.parse_args()
if args.once:
quotes = fetch_day_gainers_sync(args.count)
print(f"Fetched {len(quotes)} gainers:")
for q in quotes[:20]:
print(f" {q.rank:3d}. {q.symbol:<8s} {q.pct_change*100:+.1f}% "
f"${q.price:.2f} vol={q.volume:,.0f}")