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