Files
nexus-quant-terminal/backend/app/routers/forex.py
T
KansaramandClaude Sonnet 4.6 61e145a442 feat: launch NEXUS TERMINAL — Bloomberg-style FX options analytics platform
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>
2026-06-07 18:19:23 +05:30

148 lines
5.5 KiB
Python

"""forex.py — Live forex data and quant signal endpoints via yfinance."""
import numpy as np
import yfinance as yf
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from ..core.quant_analysis import compute_signals
router = APIRouter(prefix="/forex", tags=["forex"])
# Supported pairs: yfinance symbol → display label + default rates
PAIRS = {
"EURUSD": {"sym": "EURUSD=X", "r_d": 0.0525, "r_f": 0.0400, "pip": 0.0001},
"GBPUSD": {"sym": "GBPUSD=X", "r_d": 0.0525, "r_f": 0.0525, "pip": 0.0001},
"USDJPY": {"sym": "USDJPY=X", "r_d": 0.0010, "r_f": 0.0525, "pip": 0.01},
"USDCHF": {"sym": "USDCHF=X", "r_d": 0.0175, "r_f": 0.0525, "pip": 0.0001},
"AUDUSD": {"sym": "AUDUSD=X", "r_d": 0.0525, "r_f": 0.0435, "pip": 0.0001},
"USDCAD": {"sym": "USDCAD=X", "r_d": 0.0500, "r_f": 0.0525, "pip": 0.0001},
"NZDUSD": {"sym": "NZDUSD=X", "r_d": 0.0525, "r_f": 0.0550, "pip": 0.0001},
"EURJPY": {"sym": "EURJPY=X", "r_d": 0.0010, "r_f": 0.0400, "pip": 0.01},
"GBPJPY": {"sym": "GBPJPY=X", "r_d": 0.0010, "r_f": 0.0525, "pip": 0.01},
"EURGBP": {"sym": "EURGBP=X", "r_d": 0.0525, "r_f": 0.0400, "pip": 0.0001},
"XAUUSD": {"sym": "GC=F", "r_d": 0.0525, "r_f": 0.0000, "pip": 0.01},
}
def _fetch_rate(sym: str) -> dict:
t = yf.Ticker(sym)
fi = t.fast_info
spot = fi.last_price
prev = fi.previous_close
if not spot:
return None
change = round((spot - prev) / prev * 100, 3) if prev else 0.0
return {"spot": round(float(spot), 5), "prev": round(float(prev), 5) if prev else None,
"change_pct": change}
@router.get("/pairs")
def list_pairs():
"""Return metadata for all supported forex pairs."""
return [{"pair": k, **{f: v for f, v in meta.items() if f != "sym"}}
for k, meta in PAIRS.items()]
@router.get("/rates")
def all_rates():
"""Fetch current rates for all major pairs (bulk call)."""
results = []
for pair, meta in PAIRS.items():
try:
data = _fetch_rate(meta["sym"])
if data:
results.append({"pair": pair, **data,
"r_d": meta["r_d"], "r_f": meta["r_f"]})
except Exception:
pass
return results
@router.get("/rate/{pair}")
def get_rate(pair: str):
"""Current spot rate + 24h change for a single pair."""
pair = pair.upper()
if pair not in PAIRS:
raise HTTPException(404, f"Unknown pair '{pair}'. Supported: {list(PAIRS)}")
meta = PAIRS[pair]
data = _fetch_rate(meta["sym"])
if not data:
raise HTTPException(503, "Rate unavailable from data provider.")
return {"pair": pair, **data, "r_d": meta["r_d"], "r_f": meta["r_f"],
"pip": meta["pip"]}
@router.get("/ohlc/{pair}")
def get_ohlc(pair: str, interval: str = "5m", period: str = "2d"):
"""
OHLC candlestick data for a pair.
interval: 1m 5m 15m 30m 1h 4h 1d
period: 1d 2d 5d 1mo
"""
pair = pair.upper()
if pair not in PAIRS:
raise HTTPException(404, f"Unknown pair '{pair}'.")
sym = PAIRS[pair]["sym"]
valid_intervals = {"1m", "5m", "15m", "30m", "1h", "4h", "1d"}
if interval not in valid_intervals:
interval = "5m"
try:
hist = yf.download(sym, period=period, interval=interval,
progress=False, auto_adjust=True)
if hist.empty:
raise HTTPException(503, "No OHLC data returned.")
hist = hist.dropna()
# Flatten MultiIndex columns if present
if isinstance(hist.columns, type(hist.columns)) and hasattr(hist.columns, 'droplevel'):
try:
hist.columns = hist.columns.droplevel(1)
except Exception:
pass
return {
"pair": pair,
"interval": interval,
"dates": [str(d) for d in hist.index],
"open": [round(float(v), 5) for v in hist["Open"]],
"high": [round(float(v), 5) for v in hist["High"]],
"low": [round(float(v), 5) for v in hist["Low"]],
"close": [round(float(v), 5) for v in hist["Close"]],
"volume": [int(v) for v in hist.get("Volume", [0]*len(hist))],
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(503, f"Data fetch failed: {e}")
@router.get("/signals/{pair}")
def get_signals(pair: str):
"""
Compute full quant signal suite using 60 days of daily closes.
Signals: EWMA vol, VaR/CVaR, Hurst exponent, OU mean-reversion,
momentum, carry — all with academic citations.
"""
pair = pair.upper()
if pair not in PAIRS:
raise HTTPException(404, f"Unknown pair '{pair}'.")
meta = PAIRS[pair]
try:
hist = yf.download(meta["sym"], period="90d", interval="1d",
progress=False, auto_adjust=True)
if hist.empty or len(hist) < 10:
raise HTTPException(503, "Insufficient history for signal computation.")
if isinstance(hist.columns, type(hist.columns)) and hasattr(hist.columns, 'droplevel'):
try:
hist.columns = hist.columns.droplevel(1)
except Exception:
pass
closes = [float(v) for v in hist["Close"].dropna()]
signals = compute_signals(closes, r_d=meta["r_d"], r_f=meta["r_f"])
signals["pair"] = pair
signals["current_price"] = round(closes[-1], 5)
return signals
except HTTPException:
raise
except Exception as e:
raise HTTPException(503, f"Signal computation failed: {e}")