mirror of
https://github.com/Mihirkansara/nexus-quant-terminal.git
synced 2026-07-27 18:47:49 +00:00
61e145a442
Full-stack forex options analytics terminal with Bloomberg-inspired UI. Backend (FastAPI + Python): - Garman-Kohlhagen options pricing engine with full Greeks - Goldman Sachs gs-quant AI signals (RSI, MACD, Bollinger, Hurst, OU) - Monte Carlo GBM simulation and volatility surface generation - CFTC COT institutional positioning + Forex Factory economic calendar - Live data proxy: OpenSky aircraft + USGS earthquakes (CORS-safe) - Multi-leg strategy library (straddle, iron condor, butterfly, spreads) Frontend (React 18 + Vite): - NEXUS animated orbital logo (3-ring SVG) + canvas favicon animation - Bloomberg terminal design: JetBrains Mono, color-mix() tokens - 11 dashboard tabs: Greeks, Chart, AI Signals, 3D Surfaces, Breakeven, Scenarios, Monte Carlo, Institutional, Calendar, Live Map, Live Feeds - Live World Map (react-leaflet): aircraft, earthquakes, weather radar - Live Feeds: CoinGecko crypto top-12 + Windy.com global webcams - Economic calendar with filters + institutional flow (CFTC COT) - Animated landing page + session-based routing - Fully responsive dark-only terminal design system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
271 lines
10 KiB
Python
271 lines
10 KiB
Python
"""
|
|
institutional.py — Free institutional flow data.
|
|
|
|
Sources:
|
|
1. CFTC TFF (Traders in Financial Futures) — Socrata API on publicreporting.cftc.gov
|
|
Dataset: gpe5-46if (no API key needed, weekly, official CFTC data)
|
|
Categories: Dealers, Asset Managers, Leveraged Money (hedge funds), Other, Non-reportable
|
|
2. Volume Profile — approximated from yfinance OHLCV daily bars
|
|
(distributes bar volume proportionally across the High-Low range)
|
|
"""
|
|
|
|
import numpy as np
|
|
import httpx
|
|
import urllib.parse
|
|
import yfinance as yf
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
router = APIRouter(prefix="/institutional", tags=["institutional"])
|
|
|
|
CFTC_BASE = "https://publicreporting.cftc.gov"
|
|
CFTC_DATASET = "gpe5-46if"
|
|
|
|
# Map forex pair → CFTC market name + yfinance symbol
|
|
COT_MAP = {
|
|
"EURUSD": {"cot": "EURO FX - CHICAGO MERCANTILE EXCHANGE", "sym": "EURUSD=X"},
|
|
"GBPUSD": {"cot": "BRITISH POUND STERLING - CHICAGO MERCANTILE EXCHANGE", "sym": "GBPUSD=X"},
|
|
"USDJPY": {"cot": "JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE", "sym": "USDJPY=X"},
|
|
"AUDUSD": {"cot": "AUSTRALIAN DOLLAR - CHICAGO MERCANTILE EXCHANGE", "sym": "AUDUSD=X"},
|
|
"USDCAD": {"cot": "CANADIAN DOLLAR - CHICAGO MERCANTILE EXCHANGE", "sym": "USDCAD=X"},
|
|
"NZDUSD": {"cot": "NEW ZEALAND DOLLAR - CHICAGO MERCANTILE EXCHANGE", "sym": "NZDUSD=X"},
|
|
"USDCHF": {"cot": "SWISS FRANC - CHICAGO MERCANTILE EXCHANGE", "sym": "USDCHF=X"},
|
|
"EURJPY": {"cot": "EURO FX - CHICAGO MERCANTILE EXCHANGE", "sym": "EURJPY=X"},
|
|
"GBPJPY": {"cot": "BRITISH POUND STERLING - CHICAGO MERCANTILE EXCHANGE", "sym": "GBPJPY=X"},
|
|
"EURGBP": {"cot": "EURO FX - CHICAGO MERCANTILE EXCHANGE", "sym": "EURGBP=X"},
|
|
"XAUUSD": {"cot": "GOLD - COMMODITY EXCHANGE INC.", "sym": "GC=F"},
|
|
}
|
|
|
|
|
|
def _safe_int(v) -> int:
|
|
try: return int(v or 0)
|
|
except: return 0
|
|
|
|
def _safe_float(v) -> float:
|
|
try: return float(v or 0)
|
|
except: return 0.0
|
|
|
|
|
|
def _fetch_cot(market_name: str, weeks: int = 52) -> list[dict]:
|
|
"""Fetch TFF COT data from CFTC Socrata API (publicreporting.cftc.gov)."""
|
|
where = urllib.parse.quote(f"market_and_exchange_names='{market_name}'")
|
|
url = (
|
|
f"{CFTC_BASE}/resource/{CFTC_DATASET}.json"
|
|
f"?$where={where}"
|
|
f"&$order=report_date_as_yyyy_mm_dd+DESC"
|
|
f"&$limit={weeks}"
|
|
)
|
|
try:
|
|
resp = httpx.get(url, timeout=20, follow_redirects=True)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _parse_tff(records: list[dict]) -> dict:
|
|
"""Parse TFF records into structured signal data (newest-first records → oldest-first output)."""
|
|
if not records:
|
|
return {}
|
|
|
|
rows = list(reversed(records)) # oldest first for charting
|
|
|
|
dates = []
|
|
oi, dealer_net, assetmgr_net, levmoney_net, other_net = [], [], [], [], []
|
|
chg_oi, chg_am_long, chg_am_short, chg_lm_long, chg_lm_short = [], [], [], [], []
|
|
pct_am_long, pct_am_short, pct_lm_long, pct_lm_short = [], [], [], []
|
|
am_long_list, am_short_list, lm_long_list, lm_short_list = [], [], [], []
|
|
|
|
for r in rows:
|
|
d = (r.get("report_date_as_yyyy_mm_dd") or "")[:10]
|
|
if not d:
|
|
continue
|
|
dates.append(d)
|
|
|
|
aml = _safe_int(r.get("asset_mgr_positions_long"))
|
|
ams = _safe_int(r.get("asset_mgr_positions_short"))
|
|
lml = _safe_int(r.get("lev_money_positions_long"))
|
|
lms = _safe_int(r.get("lev_money_positions_short"))
|
|
dl = _safe_int(r.get("dealer_positions_long_all"))
|
|
ds = _safe_int(r.get("dealer_positions_short_all"))
|
|
ol = _safe_int(r.get("other_rept_positions_long"))
|
|
os_ = _safe_int(r.get("other_rept_positions_short"))
|
|
total_oi = _safe_int(r.get("open_interest_all"))
|
|
|
|
oi.append(total_oi)
|
|
dealer_net.append(dl - ds)
|
|
assetmgr_net.append(aml - ams)
|
|
levmoney_net.append(lml - lms)
|
|
other_net.append(ol - os_)
|
|
am_long_list.append(aml)
|
|
am_short_list.append(ams)
|
|
lm_long_list.append(lml)
|
|
lm_short_list.append(lms)
|
|
|
|
chg_oi.append(_safe_int(r.get("change_in_open_interest_all")))
|
|
chg_am_long.append(_safe_int(r.get("change_in_asset_mgr_long")))
|
|
chg_am_short.append(_safe_int(r.get("change_in_asset_mgr_short")))
|
|
chg_lm_long.append(_safe_int(r.get("change_in_lev_money_long")))
|
|
chg_lm_short.append(_safe_int(r.get("change_in_lev_money_short")))
|
|
|
|
pct_am_long.append(_safe_float(r.get("pct_of_oi_asset_mgr_long")))
|
|
pct_am_short.append(_safe_float(r.get("pct_of_oi_asset_mgr_short")))
|
|
pct_lm_long.append(_safe_float(r.get("pct_of_oi_lev_money_long")))
|
|
pct_lm_short.append(_safe_float(r.get("pct_of_oi_lev_money_short")))
|
|
|
|
if not dates:
|
|
return {}
|
|
|
|
def _cot_index(series):
|
|
lo, hi = min(series), max(series)
|
|
return round((series[-1] - lo) / max(hi - lo, 1) * 100, 1) if hi > lo else 50.0
|
|
|
|
am_idx = _cot_index(assetmgr_net)
|
|
lm_idx = _cot_index(levmoney_net)
|
|
|
|
def _bias(idx):
|
|
return "BULLISH" if idx >= 65 else ("BEARISH" if idx <= 35 else "NEUTRAL")
|
|
|
|
am_bias = _bias(am_idx)
|
|
lm_bias = _bias(lm_idx)
|
|
|
|
# Composite: weight asset managers 60%, leveraged money 40%
|
|
composite_idx = round(am_idx * 0.6 + lm_idx * 0.4, 1)
|
|
composite_bias = _bias(composite_idx)
|
|
|
|
wk_chg_am = (assetmgr_net[-1] - assetmgr_net[-2]) if len(assetmgr_net) >= 2 else 0
|
|
wk_chg_lm = (levmoney_net[-1] - levmoney_net[-2]) if len(levmoney_net) >= 2 else 0
|
|
|
|
return {
|
|
"dates": dates,
|
|
"open_interest": oi,
|
|
"change_oi": chg_oi,
|
|
# Asset Managers (institutional — real money)
|
|
"am_net": assetmgr_net,
|
|
"am_long": am_long_list,
|
|
"am_short": am_short_list,
|
|
"am_pct_long": pct_am_long,
|
|
"am_pct_short": pct_am_short,
|
|
"am_index": am_idx,
|
|
"am_bias": am_bias,
|
|
"am_current_net": assetmgr_net[-1],
|
|
"am_wk_change": wk_chg_am,
|
|
# Leveraged Money (hedge funds, CTAs)
|
|
"lm_net": levmoney_net,
|
|
"lm_long": lm_long_list,
|
|
"lm_short": lm_short_list,
|
|
"lm_pct_long": pct_lm_long,
|
|
"lm_pct_short": pct_lm_short,
|
|
"lm_index": lm_idx,
|
|
"lm_bias": lm_bias,
|
|
"lm_current_net": levmoney_net[-1],
|
|
"lm_wk_change": wk_chg_lm,
|
|
# Dealers
|
|
"dealer_net": dealer_net,
|
|
# Other
|
|
"other_net": other_net,
|
|
# Composite
|
|
"composite_index": composite_idx,
|
|
"composite_bias": composite_bias,
|
|
"weeks": len(dates),
|
|
"latest_date": dates[-1],
|
|
"source": "CFTC TFF — Traders in Financial Futures (Socrata API)",
|
|
}
|
|
|
|
|
|
def _volume_profile(sym: str, period: str = "3mo", buckets: int = 40) -> dict:
|
|
"""
|
|
Approximate volume profile from daily OHLCV.
|
|
Distributes each bar's volume uniformly across its High-Low range.
|
|
"""
|
|
try:
|
|
hist = yf.download(sym, period=period, interval="1d",
|
|
progress=False, auto_adjust=True)
|
|
if hist.empty:
|
|
return {}
|
|
if hasattr(hist.columns, "droplevel"):
|
|
try: hist.columns = hist.columns.droplevel(1)
|
|
except Exception: pass
|
|
|
|
highs = hist["High"].dropna().values.astype(float)
|
|
lows = hist["Low"].dropna().values.astype(float)
|
|
volumes = hist["Volume"].dropna().values.astype(float)
|
|
closes = hist["Close"].dropna().values.astype(float)
|
|
|
|
global_lo = float(np.min(lows))
|
|
global_hi = float(np.max(highs))
|
|
if global_hi <= global_lo:
|
|
return {}
|
|
|
|
bucket_size = (global_hi - global_lo) / buckets
|
|
vol_profile = np.zeros(buckets)
|
|
|
|
for i in range(len(highs)):
|
|
lo_b = int((lows[i] - global_lo) / bucket_size)
|
|
hi_b = int((highs[i] - global_lo) / bucket_size)
|
|
lo_b = max(0, min(lo_b, buckets - 1))
|
|
hi_b = max(0, min(hi_b, buckets - 1))
|
|
span = max(hi_b - lo_b + 1, 1)
|
|
vol_per_bucket = volumes[i] / span if volumes[i] > 0 else 0
|
|
vol_profile[lo_b:hi_b + 1] += vol_per_bucket
|
|
|
|
prices = [round(global_lo + (j + 0.5) * bucket_size, 5) for j in range(buckets)]
|
|
poc_idx = int(np.argmax(vol_profile))
|
|
poc = prices[poc_idx]
|
|
|
|
total_vol = float(np.sum(vol_profile))
|
|
va_target = total_vol * 0.70
|
|
lo_idx, hi_idx = poc_idx, poc_idx
|
|
va_vol = float(vol_profile[poc_idx])
|
|
while va_vol < va_target and (lo_idx > 0 or hi_idx < buckets - 1):
|
|
expand_lo = vol_profile[lo_idx - 1] if lo_idx > 0 else 0
|
|
expand_hi = vol_profile[hi_idx + 1] if hi_idx < buckets - 1 else 0
|
|
if expand_hi >= expand_lo:
|
|
hi_idx = min(hi_idx + 1, buckets - 1); va_vol += expand_hi
|
|
else:
|
|
lo_idx = max(lo_idx - 1, 0); va_vol += expand_lo
|
|
|
|
return {
|
|
"prices": prices,
|
|
"volumes": [round(float(v), 0) for v in vol_profile],
|
|
"poc": poc,
|
|
"vah": prices[hi_idx],
|
|
"val": prices[lo_idx],
|
|
"current_price": round(float(closes[-1]), 5),
|
|
"global_hi": round(global_hi, 5),
|
|
"global_lo": round(global_lo, 5),
|
|
"total_volume": round(total_vol, 0),
|
|
}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
@router.get("/{pair}")
|
|
def get_institutional(pair: str, weeks: int = 26):
|
|
"""
|
|
Institutional flow data: CFTC TFF positioning + Volume Profile.
|
|
Data sources are 100% free — no API keys required.
|
|
"""
|
|
pair = pair.upper()
|
|
if pair not in COT_MAP:
|
|
raise HTTPException(404, f"No institutional data for '{pair}'.")
|
|
|
|
meta = COT_MAP[pair]
|
|
weeks = min(max(weeks, 4), 52)
|
|
|
|
raw = _fetch_cot(meta["cot"], weeks)
|
|
cot = _parse_tff(raw)
|
|
vp = _volume_profile(meta["sym"])
|
|
|
|
if not cot and not vp:
|
|
raise HTTPException(503, "CFTC API and volume profile both unavailable.")
|
|
|
|
return {
|
|
"pair": pair,
|
|
"cot": cot,
|
|
"volume_profile": vp,
|
|
"data_sources": [
|
|
"CFTC TFF (Traders in Financial Futures) — publicreporting.cftc.gov, weekly, free",
|
|
"Volume Profile — yfinance OHLCV daily, 3 months",
|
|
],
|
|
}
|