mirror of
https://github.com/Mihirkansara/nexus-quant-terminal.git
synced 2026-08-18 04:58:08 +00:00
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>
148 lines
5.5 KiB
Python
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}")
|