feat: Specialist Desks v2 — COT, Forward Curves, Surprise Index, Hawk/Dove scorer
- COT Positioning: CFTC disaggregated + financial futures (19 markets) via Socrata free API net MM position % OI + weekly change stored in cot_data table - Forward Curves: yfinance front-month vs +3M slope (8 commodities) contango/backwardation/flat stored in forward_curve_data table - Surprise Index: consensus_estimate + actual_value on specialist_reports auto-computes surprise_score = actual - consensus on save - Hawk/Dove Text Scorer: GPT-4o-mini endpoint for CB statements score -1..+1, label, summary, key_phrases (forex/bonds: hawk/dove; commodities: bull/bear) - AI context injection: COT net positioning, forward curve structure, surprise scores, upcoming consensus estimates injected into all desk blocks - Frontend: COT panel (net% bars), Forward Curves panel, SurpriseInput on report cards, Hawk/Dove scorer in forex/bonds config tab - auto_cycle.py: non-blocking COT + curve refresh before each cycle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
95
backend/services/forward_curve.py
Normal file
95
backend/services/forward_curve.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Forward Curve Service — contango / backwardation per commodity.
|
||||
Uses yfinance to compare front-month vs near-dated futures contracts.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Month codes for futures tickers (standard CME/NYMEX/CBOT convention)
|
||||
_MONTH_CODES = {1:'F',2:'G',3:'H',4:'J',5:'K',6:'M',7:'N',8:'Q',9:'U',10:'V',11:'X',12:'Z'}
|
||||
|
||||
# (base_ticker, label, asset_class, months_spread)
|
||||
_CURVE_SPECS = [
|
||||
("CL", "WTI Crude", "energy", 3),
|
||||
("NG", "Natural Gas", "energy", 3),
|
||||
("GC", "Gold", "metals", 3),
|
||||
("SI", "Silver", "metals", 3),
|
||||
("HG", "Copper", "metals", 3),
|
||||
("ZC", "Corn", "agri", 3),
|
||||
("ZW", "Wheat", "agri", 3),
|
||||
("ZS", "Soybeans", "agri", 3),
|
||||
]
|
||||
|
||||
|
||||
def _ticker_for_month(base: str, months_ahead: int) -> str:
|
||||
target = datetime.now() + timedelta(days=months_ahead * 30)
|
||||
code = _MONTH_CODES[target.month]
|
||||
year = str(target.year)[-2:]
|
||||
return f"{base}{code}{year}"
|
||||
|
||||
|
||||
def _get_price(ticker: str) -> Optional[float]:
|
||||
try:
|
||||
import yfinance as yf
|
||||
data = yf.download(ticker, period="3d", progress=False, auto_adjust=True)
|
||||
if data.empty:
|
||||
return None
|
||||
close = data["Close"].dropna()
|
||||
if close.empty:
|
||||
return None
|
||||
val = float(close.iloc[-1])
|
||||
# Handle multi-index DataFrames
|
||||
if hasattr(val, '__iter__'):
|
||||
val = list(val)[0]
|
||||
return round(val, 4) if val and val > 0 else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def fetch_forward_curves() -> List[Dict[str, Any]]:
|
||||
"""Fetch front-month and far-month prices to compute curve slope."""
|
||||
results = []
|
||||
for base, label, ac, spread in _CURVE_SPECS:
|
||||
front_ticker = f"{base}=F"
|
||||
front_price = _get_price(front_ticker)
|
||||
if not front_price:
|
||||
logger.warning(f"Forward curve: no front-month price for {label} ({front_ticker})")
|
||||
continue
|
||||
|
||||
# Try M+spread, M+spread-1, M+spread+1 (some contract months are illiquid)
|
||||
far_price = None
|
||||
for offset in [0, -1, 1, -2, 2]:
|
||||
far_ticker = _ticker_for_month(base, spread + offset)
|
||||
far_price = _get_price(far_ticker)
|
||||
if far_price:
|
||||
break
|
||||
|
||||
slope_pct = None
|
||||
structure = "unknown"
|
||||
if far_price:
|
||||
slope_pct = round((far_price - front_price) / front_price * 100, 2)
|
||||
if slope_pct > 0.15:
|
||||
structure = "contango"
|
||||
elif slope_pct < -0.15:
|
||||
structure = "backwardation"
|
||||
else:
|
||||
structure = "flat"
|
||||
|
||||
results.append({
|
||||
"asset": label,
|
||||
"asset_class": ac,
|
||||
"front_price": round(front_price, 2),
|
||||
"far_price": round(far_price, 2) if far_price else None,
|
||||
"slope_pct": slope_pct,
|
||||
"structure": structure,
|
||||
"months_spread": spread,
|
||||
})
|
||||
logger.info(
|
||||
f"Forward curve {label}: front={front_price:.2f} "
|
||||
f"far={far_price} slope={slope_pct}% -> {structure}"
|
||||
)
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user