Files
OpenFin/backend/services/vx_fetcher.py
OpenSquared acc8bef29d feat: 4 remaining institutional reports — Earnings, VX curve, Central Bank RSS, Sentiment
New fetchers (no API keys required):
- earnings_fetcher.py: yfinance EPS calendar + surprise tracking for 23 geo-relevant tickers
- vx_fetcher.py: VIX term structure (^VIX/^VXV/^VXMT) + CBOE delayed futures, regime detection
- central_bank_fetcher.py: Fed + ECB RSS feeds, keyword-based hawkish/dovish classification
- sentiment_fetcher.py: CNN Fear & Greed (primary) + NAAIM + AAII (optional fallbacks)

Wiring:
- institutional_scheduler.py: all 4 now scheduled daily (≥08:00 UTC), deduplicated per day
- institutional.py /refresh: all 6 types handled with _run() helper
- ai_analyzer.py build_institutional_block(): limit 6→12, generic header text
- InstitutionalReports.tsx: 6-type color map, individual refresh buttons, expanded filters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 14:26:19 +02:00

189 lines
6.5 KiB
Python

"""
VIX term structure fetcher — VX futures curve, contango/backwardation regime.
Uses yfinance (^VIX, ^VXMT, ^VXV) + CBOE delayed quotes API as secondary.
No API key required.
"""
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import requests
import yfinance as yf
logger = logging.getLogger(__name__)
CBOE_VX_URL = "https://cdn.cboe.com/api/global/delayed_quotes/futures/VX.json"
VIX_TICKERS = ["^VIX", "^VXV", "^VXMT"]
def _detect_regime(vix_spot: float, slope_1m_3m: float) -> str:
if vix_spot > 30:
return "crisis"
elif slope_1m_3m < -2:
return "backwardation_strong"
elif slope_1m_3m < 0:
return "backwardation_mild"
elif slope_1m_3m > 5:
return "contango_steep"
return "contango_mild"
def _fetch_cboe_vx_futures() -> Optional[List[Dict]]:
try:
resp = requests.get(CBOE_VX_URL, timeout=15)
resp.raise_for_status()
payload = resp.json()
data = payload.get("data", [])
contracts = []
for item in data[:5]:
contracts.append({
"symbol": item.get("symbol"),
"expiration": item.get("expiration"),
"last": item.get("last"),
"bid": item.get("bid"),
"ask": item.get("ask"),
})
return contracts if contracts else None
except Exception as e:
logger.warning(f"[VX] CBOE API unavailable: {e}")
return None
def fetch_vx_report() -> Optional[Dict]:
"""Fetch VIX term structure and detect contango/backwardation regime. Returns None if yfinance fails."""
report_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
try:
data = yf.download(VIX_TICKERS, period="5d", interval="1d", progress=False, auto_adjust=True)
if data is None or data.empty:
logger.warning("[VX] yfinance returned empty data")
return None
close = data["Close"] if "Close" in data.columns else data
def latest_close(ticker: str) -> Optional[float]:
try:
col = close[ticker].dropna()
if col.empty:
return None
return float(col.iloc[-1])
except Exception:
return None
vix_spot = latest_close("^VIX")
vix_3m = latest_close("^VXV")
vix_6m = latest_close("^VXMT")
if vix_spot is None:
logger.warning("[VX] ^VIX data unavailable")
return None
except Exception as e:
logger.warning(f"[VX] yfinance download failed: {e}")
return None
slope_1m_3m = (vix_3m - vix_spot) if vix_3m is not None else None
slope_3m_6m = (vix_6m - vix_3m) if (vix_6m is not None and vix_3m is not None) else None
contango_pct = ((vix_3m - vix_spot) / vix_spot * 100) if vix_3m is not None else None
regime = _detect_regime(vix_spot, slope_1m_3m if slope_1m_3m is not None else 0.0)
cboe_contracts = _fetch_cboe_vx_futures()
raw_data: Dict[str, Any] = {
"vix_spot": round(vix_spot, 2),
"vix_3m": round(vix_3m, 2) if vix_3m is not None else None,
"vix_6m": round(vix_6m, 2) if vix_6m is not None else None,
"slope_1m_3m": round(slope_1m_3m, 2) if slope_1m_3m is not None else None,
"slope_3m_6m": round(slope_3m_6m, 2) if slope_3m_6m is not None else None,
"contango_pct": round(contango_pct, 2) if contango_pct is not None else None,
"regime": regime,
"cboe_futures": cboe_contracts,
}
key_points: List[str] = []
vix_3m_str = f"{vix_3m:.1f}" if vix_3m is not None else "N/A"
vix_6m_str = f"{vix_6m:.1f}" if vix_6m is not None else "N/A"
key_points.append(f"VIX spot: {vix_spot:.1f} | 3M: {vix_3m_str} | 6M: {vix_6m_str}")
if slope_1m_3m is not None:
key_points.append(
f"Term structure slope (1M→3M): {slope_1m_3m:+.1f}pt → {regime}"
)
if regime in ("backwardation_strong", "backwardation_mild"):
key_points.append("BACKWARDATION: front > back → elevated fear, options expensive")
if contango_pct is not None and contango_pct > 10:
monthly_decay = round(contango_pct / 12, 1)
key_points.append(
f"Steep contango: VXX roll decay ~{monthly_decay}%/month → premium selling environment"
)
if contango_pct is not None:
key_points.append(f"Contango: {contango_pct:+.1f}%")
if regime in ("crisis", "backwardation_strong"):
signal_indices = "bearish"
elif regime == "contango_steep":
signal_indices = "bullish"
else:
signal_indices = "neutral"
implications: List[str] = []
if regime in ("backwardation_strong", "backwardation_mild"):
implications.append("VXX/UVXY positive carry — consider long vol as hedge")
elif regime == "contango_steep":
implications.append(
"High roll decay in VXX — premium selling favored, spreads over naked buys"
)
elif regime == "crisis":
implications.append(
"Extreme fear — tail protection is expensive, wait for calmer entry"
)
if vix_spot > 20:
implications.append(
f"Elevated VIX ({vix_spot:.1f}) — implied vol rich, consider selling premium with defined risk"
)
elif vix_spot < 14:
implications.append(
f"Low VIX ({vix_spot:.1f}) — vol cheap, consider buying tail protection"
)
if not implications:
implications = ["VIX term structure neutral — no extreme regime detected"]
abs_slope = abs(slope_1m_3m) if slope_1m_3m is not None else 0.0
importance = 3 if (regime == "crisis" or abs_slope > 5) else 2
slope_str = f"{slope_1m_3m:+.1f}pt" if slope_1m_3m is not None else "N/A"
ai_summary = (
f"VIX term structure ({report_date}). "
f"Spot {vix_spot:.1f} | 3M {vix_3m_str} | 6M {vix_6m_str}. "
f"Slope 1M→3M: {slope_str}. "
f"Regime: {regime.upper()}. "
f"Signal: {signal_indices.upper()}. "
+ implications[0]
)
return {
"report_type": "vx_curve",
"report_date": report_date,
"title": f"VIX Term Structure — {report_date}",
"source": "yfinance + CBOE delayed API",
"importance": importance,
"category": "volatility",
"raw_data": raw_data,
"key_points": key_points,
"trading_implications": " | ".join(implications),
"signal_energy": "neutral",
"signal_metals": "neutral",
"signal_indices": signal_indices,
"signal_forex": "neutral",
"ai_summary": ai_summary,
}