""" EIA Petroleum Weekly Status Report fetcher. Uses EIA API v2 — free key stored in DB config as 'eia_api_key'. """ import logging from datetime import datetime from typing import Any, Dict, List, Optional import requests logger = logging.getLogger(__name__) EIA_BASE_URL = "https://api.eia.gov/v2/petroleum/stoc/wstk/data/" EIA_SERIES: Dict[str, Dict[str, str]] = { "WCRSTUS1": {"name": "US Crude Oil Stocks (ex-SPR)", "unit": "Mb"}, "WCUOK1A": {"name": "Cushing, OK Crude Stocks", "unit": "Mb"}, "WGTSTUS1": {"name": "US Total Gasoline Stocks", "unit": "Mb"}, "WDISTUS1": {"name": "US Distillate Fuel Oil Stocks", "unit": "Mb"}, } SURPRISE_THRESHOLD_MB = 2.0 def _fetch_series(series_id: str, api_key: str, num_weeks: int = 8) -> List[Dict]: params = { "api_key": api_key, "frequency": "weekly", "data[]": "value", "facets[series][]": series_id, "sort[0][column]": "period", "sort[0][direction]": "desc", "length": num_weeks, "offset": 0, } try: resp = requests.get(EIA_BASE_URL, params=params, timeout=20) resp.raise_for_status() return resp.json().get("response", {}).get("data", []) except Exception as e: logger.warning(f"[EIA] Failed to fetch {series_id}: {e}") return [] def fetch_eia_report(api_key: str) -> Optional[Dict]: """Fetch latest EIA petroleum weekly stocks. Returns None if key missing or all fetches fail.""" if not api_key: logger.warning("[EIA] No API key — skipping") return None results: Dict[str, Any] = {} report_dates: List[str] = [] for sid, meta in EIA_SERIES.items(): rows = _fetch_series(sid, api_key) if not rows: continue current = rows[0] prior = rows[1] if len(rows) > 1 else None cur_val = float(current.get("value") or 0) pri_val = float(prior.get("value") or 0) if prior else None wow = round(cur_val - pri_val, 1) if pri_val is not None else None results[sid] = { "name": meta["name"], "period": (current.get("period") or "")[:10], "value_mb": round(cur_val, 1), "prior_mb": round(pri_val, 1) if pri_val is not None else None, "wow_change_mb": wow, } if current.get("period"): report_dates.append(current["period"][:10]) if not results: logger.warning("[EIA] No data returned") return None report_date = max(report_dates) if report_dates else datetime.utcnow().strftime("%Y-%m-%d") key_points: List[str] = [] implications: List[str] = [] surprises: List[str] = [] signal_energy = "neutral" crude = results.get("WCRSTUS1") if crude: wow_str = f"{crude['wow_change_mb']:+.1f} Mb" if crude["wow_change_mb"] is not None else "N/A" kp = f"US crude oil stocks: {crude['value_mb']:.1f} Mb (WoW {wow_str})" if crude["wow_change_mb"] is not None and abs(crude["wow_change_mb"]) >= SURPRISE_THRESHOLD_MB: direction = "draw" if crude["wow_change_mb"] < 0 else "build" kp += f" — significant {direction}" surprises.append("crude") signal_energy = "bullish" if crude["wow_change_mb"] < 0 else "bearish" key_points.append(kp) cushing = results.get("WCUOK1A") if cushing: wow_str = f"{cushing['wow_change_mb']:+.1f} Mb" if cushing["wow_change_mb"] is not None else "N/A" key_points.append(f"Cushing, OK: {cushing['value_mb']:.1f} Mb (WoW {wow_str})") gasoline = results.get("WGTSTUS1") if gasoline: wow_str = f"{gasoline['wow_change_mb']:+.1f} Mb" if gasoline["wow_change_mb"] is not None else "N/A" kp = f"US gasoline stocks: {gasoline['value_mb']:.1f} Mb (WoW {wow_str})" if gasoline["wow_change_mb"] is not None and abs(gasoline["wow_change_mb"]) >= SURPRISE_THRESHOLD_MB: surprises.append("gasoline") key_points.append(kp) distillates = results.get("WDISTUS1") if distillates: wow_str = f"{distillates['wow_change_mb']:+.1f} Mb" if distillates["wow_change_mb"] is not None else "N/A" kp = f"US distillate stocks: {distillates['value_mb']:.1f} Mb (WoW {wow_str})" if distillates["wow_change_mb"] is not None and abs(distillates["wow_change_mb"]) >= SURPRISE_THRESHOLD_MB: surprises.append("distillates") key_points.append(kp) crude_wow = (crude or {}).get("wow_change_mb") if crude_wow is not None: if crude_wow < -SURPRISE_THRESHOLD_MB: implications.append( f"Crude draw of {abs(crude_wow):.1f} Mb — bullish for WTI/Brent spot" ) elif crude_wow > SURPRISE_THRESHOLD_MB: implications.append( f"Crude build of {crude_wow:.1f} Mb — bearish for WTI/Brent spot" ) gasoline_wow = (gasoline or {}).get("wow_change_mb") if gasoline_wow is not None and abs(gasoline_wow) >= SURPRISE_THRESHOLD_MB: demand_str = "strong" if gasoline_wow < 0 else "weak" implications.append( f"Gasoline {'draw' if gasoline_wow < 0 else 'build'} ({gasoline_wow:+.1f} Mb) — {demand_str} driving demand" ) if not implications: implications = ["EIA petroleum stocks in line with seasonal norms — no major surprise"] importance = 3 if len(surprises) >= 2 else 2 if len(surprises) == 1 else 1 crude_val_str = f"{crude['value_mb']:.1f}" if crude else "N/A" crude_wow_str = f" (WoW {crude['wow_change_mb']:+.1f} Mb)" if crude and crude["wow_change_mb"] is not None else "" ai_summary = ( f"EIA Petroleum Weekly ({report_date}). " f"Crude: {crude_val_str} Mb{crude_wow_str}. " f"Signal: {signal_energy.upper()}. " + (f"Surprises: {', '.join(surprises)}. " if surprises else "") + implications[0] ) return { "report_type": "eia", "report_date": report_date, "title": f"EIA Petroleum Weekly — {report_date}", "source": "EIA API v2", "importance": importance, "category": "energy", "raw_data": results, "key_points": key_points, "trading_implications": " | ".join(implications), "signal_energy": signal_energy, "signal_metals": "neutral", "signal_indices": "neutral", "signal_forex": "neutral", "ai_summary": ai_summary, }