Files
OpenFin/backend/services/eia_fetcher.py
OpenSquared 3edbd6b0b7 feat: institutional reports — CFTC COT + EIA petroleum weekly
- New institutional_reports table (DB) with importance, signals per asset class, key points, absorption tracking
- cot_fetcher.py: CFTC Socrata API (6dca-aqww), 7 instruments (Gold/Silver/Copper/WTI/NatGas/SP500/EURUSD), net positioning + 52-week z-score
- eia_fetcher.py: EIA API v2, 4 series (crude/Cushing/gasoline/distillates), WoW surprise detection
- institutional.py router: GET /reports, GET /reports/{id}, POST /refresh, GET /stats
- institutional_scheduler.py: weekly auto-fetch (COT Saturdays, EIA Wednesday afternoons)
- ai_analyzer.py: build_institutional_block() + institutional_block param injected into AI scoring prompt
- auto_cycle.py: inject institutional block into suggestion + scoring, absorption tracking via keyword overlap after each cycle commentary
- InstitutionalReports.tsx: full page with filter bar (type/category/importance/period), cards with key point bullets, EXTREME alerts highlighted, signal badges, absorption badge, trading implications, expandable detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 13:45:07 +02:00

166 lines
6.3 KiB
Python

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