Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM, Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA. Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
341
frontend/src/pages/OptionsLab.tsx
Normal file
341
frontend/src/pages/OptionsLab.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import { useState } from 'react'
|
||||
import { usePnlCurve } from '../hooks/useApi'
|
||||
import axios from 'axios'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
|
||||
CartesianGrid, ReferenceLine, Legend,
|
||||
} from 'recharts'
|
||||
import { TrendingUp, FlaskConical, Calculator } from 'lucide-react'
|
||||
import type { PnLPoint } from '../types'
|
||||
|
||||
const STRATEGIES = [
|
||||
{ key: 'long_call', label: 'Long Call', desc: 'Pari haussier, gain illimité, perte limitée à la prime' },
|
||||
{ key: 'long_put', label: 'Long Put', desc: 'Pari baissier, gain important, perte limitée à la prime' },
|
||||
{ key: 'bull_call_spread', label: 'Bull Call Spread', desc: 'Haussier modéré, coût réduit, gain plafonné' },
|
||||
{ key: 'bear_put_spread', label: 'Bear Put Spread', desc: 'Baissier modéré, coût réduit, gain plafonné' },
|
||||
{ key: 'straddle', label: 'Long Straddle', desc: 'Pari sur la volatilité, direction neutre' },
|
||||
]
|
||||
|
||||
const WATCHLIST_QUICK = [
|
||||
'GLD', 'USO', 'WEAT', 'UNG', 'SPY', 'QQQ', 'GDX', 'COPX',
|
||||
'XLE', 'FXE', 'UUP', 'XOM', 'LMT',
|
||||
]
|
||||
|
||||
interface Greeks {
|
||||
price: number; delta: number; gamma: number; theta: number; vega: number; rho: number
|
||||
underlying_price: number; sigma: number
|
||||
}
|
||||
|
||||
interface SpreadResult {
|
||||
strategy: string; net_debit: number; max_loss: number; max_gain: number | null
|
||||
breakeven?: number; breakevens?: number[]; legs: Array<{type: string; strike: number; premium: number}>
|
||||
underlying_price: number; sigma: number
|
||||
}
|
||||
|
||||
export default function OptionsLab() {
|
||||
const [strategy, setStrategy] = useState('long_call')
|
||||
const [symbol, setSymbol] = useState('GLD')
|
||||
const [strike, setStrike] = useState(200)
|
||||
const [strikeHigh, setStrikeHigh] = useState(210)
|
||||
const [expiry, setExpiry] = useState(90)
|
||||
const [optionType, setOptionType] = useState('call')
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [result, setResult] = useState<Greeks | SpreadResult | null>(null)
|
||||
const [pnlData, setPnlData] = useState<PnLPoint[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const compute = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
let res: Greeks | SpreadResult
|
||||
if (strategy === 'long_call' || strategy === 'long_put') {
|
||||
const type = strategy === 'long_call' ? 'call' : 'put'
|
||||
const r = await axios.get('/api/options/price', {
|
||||
params: { symbol, strike, expiry_days: expiry, option_type: type }
|
||||
})
|
||||
res = r.data as Greeks
|
||||
const pnl = await axios.get('/api/options/pnl-curve', {
|
||||
params: { symbol, strike, expiry_days: expiry, option_type: type, quantity, premium_paid: res.price }
|
||||
})
|
||||
setPnlData(pnl.data as PnLPoint[])
|
||||
} else if (strategy === 'bull_call_spread') {
|
||||
const r = await axios.get('/api/options/strategy/bull-call-spread', {
|
||||
params: { symbol, strike_low: strike, strike_high: strikeHigh, expiry_days: expiry }
|
||||
})
|
||||
res = r.data as SpreadResult
|
||||
setPnlData([])
|
||||
} else if (strategy === 'bear_put_spread') {
|
||||
const r = await axios.get('/api/options/strategy/bear-put-spread', {
|
||||
params: { symbol, strike_high: strikeHigh, strike_low: strike, expiry_days: expiry }
|
||||
})
|
||||
res = r.data as SpreadResult
|
||||
setPnlData([])
|
||||
} else {
|
||||
const r = await axios.get('/api/options/strategy/straddle', {
|
||||
params: { symbol, strike, expiry_days: expiry }
|
||||
})
|
||||
res = r.data as SpreadResult
|
||||
setPnlData([])
|
||||
}
|
||||
setResult(res)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const isGreeks = result && 'delta' in result
|
||||
const isSpread = result && 'net_debit' in result
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-5">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<FlaskConical className="w-5 h-5 text-blue-400" /> Options Lab
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Pricer Black-Scholes · Greeks · Stratégies · Courbe P&L
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{/* Left: strategy builder */}
|
||||
<div className="col-span-1 space-y-4">
|
||||
{/* Strategy select */}
|
||||
<div className="card">
|
||||
<div className="section-title">Stratégie</div>
|
||||
<div className="space-y-1.5">
|
||||
{STRATEGIES.map(s => (
|
||||
<button
|
||||
key={s.key}
|
||||
onClick={() => setStrategy(s.key)}
|
||||
className={clsx('w-full text-left px-3 py-2 rounded border text-xs transition-all', {
|
||||
'bg-blue-600/20 border-blue-500/60 text-blue-300': strategy === s.key,
|
||||
'border-slate-700/40 text-slate-400 hover:border-slate-600': strategy !== s.key,
|
||||
})}
|
||||
>
|
||||
<div className="font-semibold">{s.label}</div>
|
||||
<div className="text-slate-500 mt-0.5">{s.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parameters */}
|
||||
<div className="card">
|
||||
<div className="section-title flex items-center gap-1"><Calculator className="w-3 h-3" /> Paramètres</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Sous-jacent</label>
|
||||
<div className="flex gap-1 flex-wrap mb-1">
|
||||
{WATCHLIST_QUICK.map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSymbol(s)}
|
||||
className={clsx('px-2 py-0.5 rounded text-xs border', {
|
||||
'bg-blue-600 border-blue-500 text-white': symbol === s,
|
||||
'border-slate-700 text-slate-500 hover:border-slate-500': symbol !== s,
|
||||
})}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={e => setSymbol(e.target.value.toUpperCase())}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="Ex: GLD, USO, SPY"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">
|
||||
Strike {strategy === 'bull_call_spread' || strategy === 'bear_put_spread' ? 'bas' : ''}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={strike}
|
||||
onChange={e => setStrike(Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(strategy === 'bull_call_spread' || strategy === 'bear_put_spread') && (
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Strike haut</label>
|
||||
<input
|
||||
type="number"
|
||||
value={strikeHigh}
|
||||
onChange={e => setStrikeHigh(Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Expiration (jours)</label>
|
||||
<div className="flex gap-1 mb-1">
|
||||
{[30, 60, 90, 180].map(d => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => setExpiry(d)}
|
||||
className={clsx('px-2 py-0.5 rounded text-xs border', {
|
||||
'bg-blue-600 border-blue-500 text-white': expiry === d,
|
||||
'border-slate-700 text-slate-500 hover:border-slate-500': expiry !== d,
|
||||
})}
|
||||
>
|
||||
{d}j
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={expiry}
|
||||
onChange={e => setExpiry(Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(strategy === 'long_call' || strategy === 'long_put') && (
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Nb contrats</label>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={e => setQuantity(Number(e.target.value))}
|
||||
min={1}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={compute}
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-2 text-sm font-semibold transition-colors"
|
||||
>
|
||||
{loading ? 'Calcul...' : '⚡ Calculer'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: results */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
{/* Greeks / Spread summary */}
|
||||
{isGreeks && (
|
||||
<div className="card">
|
||||
<div className="section-title">Prix & Greeks — {symbol} {strike} {strategy === 'long_call' ? 'Call' : 'Put'} {expiry}j</div>
|
||||
<div className="grid grid-cols-4 gap-3 mb-4">
|
||||
{[
|
||||
{ label: 'Prime', value: `$${result.price.toFixed(4)}`, highlight: true },
|
||||
{ label: 'Coût (1 contrat)', value: `$${(result.price * 100).toFixed(2)}` },
|
||||
{ label: 'Spot sous-jacent', value: `$${result.underlying_price?.toFixed(2)}` },
|
||||
{ label: 'Vol. Réalisée', value: `${((result.sigma ?? 0) * 100).toFixed(1)}%` },
|
||||
].map(({ label, value, highlight }) => (
|
||||
<div key={label} className="card-sm text-center">
|
||||
<div className="stat-label">{label}</div>
|
||||
<div className={clsx('stat-value text-lg mt-1', highlight && 'text-blue-400')}>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{[
|
||||
{ label: 'Delta (Δ)', value: result.delta, desc: 'Sensibilité au prix' },
|
||||
{ label: 'Gamma (Γ)', value: result.gamma, desc: 'Variation du delta' },
|
||||
{ label: 'Theta (Θ)', value: result.theta, desc: 'Déclin temporel/j' },
|
||||
{ label: 'Vega (ν)', value: result.vega, desc: 'Sensibilité à IV' },
|
||||
{ label: 'Rho (ρ)', value: result.rho, desc: 'Sensibilité aux taux' },
|
||||
].map(({ label, value, desc }) => (
|
||||
<div key={label} className="card-sm text-center">
|
||||
<div className="text-xs text-slate-500">{label}</div>
|
||||
<div className={clsx('text-base font-bold mt-0.5', {
|
||||
'text-emerald-400': value > 0,
|
||||
'text-red-400': value < 0,
|
||||
'text-slate-400': value === 0,
|
||||
})}>
|
||||
{value?.toFixed(4)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">{desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSpread && (
|
||||
<div className="card">
|
||||
<div className="section-title">Résumé stratégie — {result.strategy}</div>
|
||||
<div className="grid grid-cols-4 gap-3 mb-4">
|
||||
{[
|
||||
{ label: 'Débit net', value: `$${result.net_debit.toFixed(4)}`, color: 'text-red-400' },
|
||||
{ label: 'Perte max', value: `$${result.max_loss.toFixed(2)}`, color: 'text-red-400' },
|
||||
{ label: 'Gain max', value: result.max_gain != null ? `$${result.max_gain.toFixed(2)}` : '∞', color: 'text-emerald-400' },
|
||||
{ label: 'Seuil renta.', value: `$${(result.breakeven ?? (result.breakevens?.[0]) ?? 0).toFixed(2)}`, color: 'text-yellow-400' },
|
||||
].map(({ label, value, color }) => (
|
||||
<div key={label} className="card-sm text-center">
|
||||
<div className="stat-label">{label}</div>
|
||||
<div className={clsx('text-lg font-bold mt-1', color)}>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-2">Jambes de la stratégie</div>
|
||||
{result.legs.map((leg, i) => (
|
||||
<div key={i} className="flex items-center gap-3 text-xs py-1.5 border-b border-slate-700/30 last:border-0">
|
||||
<span className={clsx('badge', leg.type.includes('long') ? 'badge-green' : 'badge-red')}>
|
||||
{leg.type.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-white">Strike: ${leg.strike}</span>
|
||||
<span className="text-slate-400">Prime: ${leg.premium.toFixed(4)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* P&L Chart */}
|
||||
{pnlData.length > 0 && (
|
||||
<div className="card">
|
||||
<div className="section-title">Courbe P&L à l'expiration</div>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<LineChart data={pnlData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
|
||||
<XAxis dataKey="underlying" tick={{ fill: '#475569', fontSize: 9 }}
|
||||
tickFormatter={v => `$${v.toFixed(0)}`} />
|
||||
<YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false}
|
||||
tickFormatter={v => `$${v.toFixed(0)}`} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||
formatter={(v: number) => [`$${v.toFixed(2)}`, 'P&L']}
|
||||
labelFormatter={v => `Prix sous-jacent: $${Number(v).toFixed(2)}`}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
|
||||
<ReferenceLine x={strike} stroke="#f59e0b" strokeDasharray="4 4" label={{ value: 'Strike', fill: '#f59e0b', fontSize: 9 }} />
|
||||
<Line
|
||||
type="monotone" dataKey="pnl"
|
||||
stroke="#3b82f6" strokeWidth={2} dot={false}
|
||||
activeDot={{ r: 4, fill: '#3b82f6' }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!result && !loading && (
|
||||
<div className="card h-80 flex items-center justify-center text-slate-600 text-sm">
|
||||
<div className="text-center">
|
||||
<FlaskConical className="w-10 h-10 mx-auto mb-3 opacity-20" />
|
||||
<div>Configurer et calculer une stratégie</div>
|
||||
<div className="text-xs mt-1">Les résultats apparaîtront ici</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user