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:
OpenSquared
2026-06-16 20:29:59 +02:00
commit d256b65d30
69 changed files with 18301 additions and 0 deletions

View 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>
)
}