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>
110 lines
4.0 KiB
React
110 lines
4.0 KiB
React
import { useState } from 'react'
|
|
import Plot from 'react-plotly.js'
|
|
import { fetchIVSurface } from '../api/client'
|
|
import { usePortfolioStore } from '../store/portfolio'
|
|
|
|
export default function IVSurface() {
|
|
const { ticker } = usePortfolioStore()
|
|
const [data, setData] = useState(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
const [optType, setOptType] = useState('call')
|
|
|
|
const load = async () => {
|
|
setLoading(true); setError(null)
|
|
try {
|
|
const result = await fetchIVSurface(ticker)
|
|
setData(result)
|
|
} catch (e) {
|
|
setError(e?.response?.data?.detail || 'Failed to fetch option chain.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const filtered = data?.data.filter(d => d.type === optType) ?? []
|
|
const expiries = [...new Set(filtered.map(d => d.expiry))].sort()
|
|
const strikes = [...new Set(filtered.map(d => d.strike))].sort((a, b) => a - b)
|
|
|
|
// Build IV matrix: rows=strikes, cols=expiries
|
|
const Z = strikes.map(k =>
|
|
expiries.map(exp => {
|
|
const pt = filtered.find(d => d.strike === k && d.expiry === exp)
|
|
return pt ? pt.iv * 100 : null
|
|
})
|
|
)
|
|
|
|
const plotData = data ? [{
|
|
type: 'surface',
|
|
x: expiries,
|
|
y: strikes,
|
|
z: Z,
|
|
colorscale: 'RdYlGn_r',
|
|
colorbar: { title: 'IV (%)', tickfont: { color: '#e2e8f0' } },
|
|
contours: { z: { show: true, usecolormap: true, project: { z: true } } },
|
|
}] : []
|
|
|
|
return (
|
|
<div className="p-4 space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<h2 className="text-sm font-bold uppercase tracking-widest" style={{ color: 'var(--accent)' }}>
|
|
Live IV Surface — {ticker}
|
|
</h2>
|
|
<div className="flex gap-1">
|
|
{['call', 'put'].map(t => (
|
|
<button key={t} onClick={() => setOptType(t)}
|
|
className="text-xs px-2 py-1 rounded capitalize"
|
|
style={{ background: optType === t ? 'var(--accent2)' : 'var(--border)', color: optType === t ? '#fff' : 'var(--text)' }}>
|
|
{t}s
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button onClick={load} disabled={loading}
|
|
className="px-4 py-1.5 text-sm rounded font-medium"
|
|
style={{ background: 'var(--accent)', color: '#0a0e1a' }}>
|
|
{loading ? 'Loading...' : '📡 Fetch Live Data'}
|
|
</button>
|
|
{data && (
|
|
<span className="text-xs" style={{ color: 'var(--muted)' }}>
|
|
Spot: <span className="font-mono" style={{ color: 'var(--text)' }}>${data.spot?.toFixed(2)}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-xs p-3 rounded" style={{ background: 'rgba(248,113,113,0.1)', color: 'var(--red)', border: '1px solid var(--red)' }}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{data ? (
|
|
<Plot data={plotData}
|
|
layout={{
|
|
paper_bgcolor: 'transparent', plot_bgcolor: '#111827',
|
|
scene: {
|
|
xaxis: { title: 'Expiry', color: '#64748b', gridcolor: '#1f2937' },
|
|
yaxis: { title: 'Strike', color: '#64748b', gridcolor: '#1f2937' },
|
|
zaxis: { title: 'IV (%)', color: '#64748b', gridcolor: '#1f2937' },
|
|
bgcolor: '#0a0e1a',
|
|
},
|
|
font: { color: '#e2e8f0' },
|
|
margin: { l: 0, r: 0, t: 20, b: 0 },
|
|
}}
|
|
config={{ responsive: true, displayModeBar: true, displaylogo: false }}
|
|
style={{ width: '100%', height: 440 }} />
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center h-48 rounded gap-2"
|
|
style={{ background: 'var(--surface)', border: '1px solid var(--border)' }}>
|
|
<span className="text-2xl">📡</span>
|
|
<span className="text-sm" style={{ color: 'var(--muted)' }}>
|
|
Fetch live option chain data from Yahoo Finance
|
|
</span>
|
|
<span className="text-xs" style={{ color: 'var(--muted)' }}>
|
|
Works with any US equity ticker (SPY, AAPL, TSLA, QQQ...)
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|