mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-20 09:27:20 +02:00
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
"""Financial data fetchers — defense stocks and oil prices.
|
|
|
|
Uses yfinance batch download to minimise Yahoo Finance requests and avoid rate limiting.
|
|
"""
|
|
import logging
|
|
import yfinance as yf
|
|
from services.fetchers._store import latest_data, _data_lock, _mark_fresh
|
|
from services.fetchers.retry import with_retry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _batch_fetch(symbols: list[str], period: str = "5d") -> dict:
|
|
"""Fetch multiple tickers in a single yfinance request. Returns {symbol: {price, change_percent, up}}."""
|
|
try:
|
|
hist = yf.download(symbols, period=period, auto_adjust=True, progress=False)
|
|
if hist.empty:
|
|
return {}
|
|
close = hist["Close"]
|
|
result = {}
|
|
for sym in symbols:
|
|
try:
|
|
col = close[sym] if len(symbols) > 1 else close
|
|
col = col.dropna()
|
|
if len(col) < 1:
|
|
continue
|
|
current = float(col.iloc[-1])
|
|
prev = float(col.iloc[0]) if len(col) > 1 else current
|
|
change = ((current - prev) / prev * 100) if prev else 0
|
|
result[sym] = {
|
|
"price": round(current, 2),
|
|
"change_percent": round(change, 2),
|
|
"up": bool(change >= 0),
|
|
}
|
|
except Exception as e:
|
|
logger.warning(f"Could not parse {sym}: {e}")
|
|
return result
|
|
except Exception as e:
|
|
logger.warning(f"Batch fetch failed: {e}")
|
|
return {}
|
|
|
|
|
|
_STOCK_TICKERS = ["RTX", "LMT", "NOC", "GD", "BA", "PLTR"]
|
|
_OIL_MAP = {"WTI Crude": "CL=F", "Brent Crude": "BZ=F"}
|
|
_ALL_TICKERS = _STOCK_TICKERS + list(_OIL_MAP.values())
|
|
|
|
_MARKET_COOLDOWN_SECONDS = 1800 # fetch at most once every 30 minutes
|
|
_last_market_fetch: float = 0.0
|
|
|
|
|
|
def _fetch_all_market_data():
|
|
"""Single yfinance download for all market tickers to avoid rate limiting."""
|
|
raw = _batch_fetch(_ALL_TICKERS, period="5d")
|
|
stocks = {sym: raw[sym] for sym in _STOCK_TICKERS if sym in raw}
|
|
oil = {name: raw[sym] for name, sym in _OIL_MAP.items() if sym in raw}
|
|
return stocks, oil
|
|
|
|
|
|
@with_retry(max_retries=2, base_delay=10)
|
|
def fetch_defense_stocks():
|
|
global _last_market_fetch
|
|
import time
|
|
if time.time() - _last_market_fetch < _MARKET_COOLDOWN_SECONDS:
|
|
return
|
|
try:
|
|
stocks, oil = _fetch_all_market_data()
|
|
if stocks:
|
|
_last_market_fetch = time.time()
|
|
with _data_lock:
|
|
latest_data['stocks'] = stocks
|
|
if oil:
|
|
latest_data['oil'] = oil
|
|
_mark_fresh("stocks")
|
|
if oil:
|
|
_mark_fresh("oil")
|
|
logger.info(f"Markets: {len(stocks)} stocks, {len(oil)} oil tickers")
|
|
else:
|
|
logger.warning("Markets: empty result from yfinance (rate limited?)")
|
|
except Exception as e:
|
|
logger.error(f"Error fetching market data: {e}")
|
|
|
|
|
|
@with_retry(max_retries=1, base_delay=10)
|
|
def fetch_oil_prices():
|
|
# Oil is now fetched together with stocks in fetch_defense_stocks to use a single request.
|
|
# This function is kept for scheduler compatibility but is a no-op if stocks already ran.
|
|
with _data_lock:
|
|
if latest_data.get('oil'):
|
|
return # Already populated by fetch_defense_stocks
|
|
try:
|
|
_, oil = _fetch_all_market_data()
|
|
if oil:
|
|
with _data_lock:
|
|
latest_data['oil'] = oil
|
|
_mark_fresh("oil")
|
|
except Exception as e:
|
|
logger.error(f"Error fetching oil: {e}")
|