Files
OpenFin/frontend/src/pages/Backtest.tsx
OpenSquared d256b65d30 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>
2026-06-16 20:29:59 +02:00

311 lines
14 KiB
TypeScript

import { useState } from 'react'
import { useBacktest } from '../hooks/useApi'
import clsx from 'clsx'
import {
AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer,
CartesianGrid, ReferenceLine,
} from 'recharts'
import { History, Play, TrendingUp, TrendingDown, AlertTriangle } from 'lucide-react'
import type { BacktestResult } from '../types'
const STRATEGIES = [
{ key: 'long_call', label: 'Long Call' },
{ key: 'long_put', label: 'Long Put' },
{ key: 'bull_call_spread', label: 'Bull Call Spread' },
{ key: 'bear_put_spread', label: 'Bear Put Spread' },
]
const SYMBOLS = [
'GLD', 'USO', 'WEAT', 'UNG', 'SPY', 'QQQ', 'GDX', 'COPX',
'XLE', 'FXE', 'XOM', 'LMT', 'BA', 'RTX',
]
function StatCard({ label, value, sub, positive }: { label: string; value: string; sub?: string; positive?: boolean }) {
return (
<div className="card-sm text-center">
<div className="stat-label">{label}</div>
<div className={clsx('text-xl font-bold mt-1', {
'text-emerald-400': positive === true,
'text-red-400': positive === false,
'text-white': positive === undefined,
})}>
{value}
</div>
{sub && <div className="text-xs text-slate-600 mt-0.5">{sub}</div>}
</div>
)
}
export default function Backtest() {
const { mutate: runBacktest, data: result, isPending } = useBacktest()
const [form, setForm] = useState({
symbol: 'GLD',
start_date: '2022-01-01',
end_date: '2024-12-31',
strategy: 'long_call',
strike_offset_pct: 0.05,
expiry_days: 90,
capital: 1000,
})
const set = (k: string, v: unknown) => setForm(f => ({ ...f, [k]: v }))
const run = () => runBacktest(form as Record<string, unknown>)
const typed = result as BacktestResult | undefined
const hasResult = typed && !typed.error
return (
<div className="p-6 space-y-5">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<History className="w-5 h-5 text-blue-400" /> Backtest & Simulation
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Testez vos stratégies options sur des données historiques réelles
</p>
</div>
<div className="grid grid-cols-4 gap-5">
{/* Config panel */}
<div className="col-span-1 space-y-4">
<div className="card">
<div className="section-title">Configuration</div>
<div className="space-y-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Sous-jacent</label>
<div className="flex flex-wrap gap-1 mb-1">
{SYMBOLS.slice(0, 7).map(s => (
<button
key={s}
onClick={() => set('symbol', s)}
className={clsx('px-1.5 py-0.5 rounded text-xs border', {
'bg-blue-600 border-blue-500 text-white': form.symbol === s,
'border-slate-700 text-slate-500': form.symbol !== s,
})}
>
{s}
</button>
))}
</div>
<input
type="text"
value={form.symbol}
onChange={e => set('symbol', 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"
/>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Stratégie</label>
{STRATEGIES.map(s => (
<button
key={s.key}
onClick={() => set('strategy', s.key)}
className={clsx('w-full text-left px-2 py-1.5 rounded mb-1 text-xs border transition-all', {
'bg-blue-600/20 border-blue-500/60 text-blue-300': form.strategy === s.key,
'border-slate-700/40 text-slate-400': form.strategy !== s.key,
})}
>
{s.label}
</button>
))}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Période</label>
<input
type="date"
value={form.start_date}
onChange={e => set('start_date', e.target.value)}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white mb-1 focus:outline-none focus:border-blue-500"
/>
<input
type="date"
value={form.end_date}
onChange={e => set('end_date', 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">
Strike OTM: {(form.strike_offset_pct * 100).toFixed(0)}%
</label>
<input
type="range" min={0} max={0.20} step={0.01}
value={form.strike_offset_pct}
onChange={e => set('strike_offset_pct', Number(e.target.value))}
className="w-full accent-blue-500"
/>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Expiration: {form.expiry_days}j</label>
<div className="flex gap-1 mb-1">
{[30, 60, 90, 180].map(d => (
<button
key={d}
onClick={() => set('expiry_days', d)}
className={clsx('flex-1 py-0.5 rounded text-xs border', {
'bg-blue-600 border-blue-500 text-white': form.expiry_days === d,
'border-slate-700 text-slate-500': form.expiry_days !== d,
})}
>
{d}j
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Capital initial ()</label>
<input
type="number"
value={form.capital}
onChange={e => set('capital', 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>
<button
onClick={run}
disabled={isPending}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-2 text-sm font-semibold flex items-center justify-center gap-2 transition-colors"
>
<Play className="w-4 h-4" />
{isPending ? 'Calcul...' : 'Lancer le backtest'}
</button>
</div>
</div>
</div>
{/* Results panel */}
<div className="col-span-3 space-y-4">
{typed?.error && (
<div className="card border-red-700/40 bg-red-900/10">
<div className="flex items-center gap-2 text-red-400 text-sm">
<AlertTriangle className="w-4 h-4" /> {typed.error}
</div>
</div>
)}
{hasResult && (
<>
{/* KPIs */}
<div className="grid grid-cols-4 gap-3">
<StatCard
label="Retour total"
value={`${typed.total_return_pct >= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`}
positive={typed.total_return_pct >= 0}
/>
<StatCard
label="Taux de succès"
value={`${typed.win_rate.toFixed(1)}%`}
sub={`${typed.wins}W / ${typed.losses}L`}
positive={typed.win_rate >= 50}
/>
<StatCard
label="Max Drawdown"
value={`${typed.max_drawdown_pct.toFixed(2)}%`}
positive={false}
/>
<StatCard
label="Profit Factor"
value={typed.profit_factor.toFixed(2)}
sub={`${typed.total_trades} trades`}
positive={typed.profit_factor >= 1}
/>
</div>
{/* Equity curve */}
<div className="card">
<div className="section-title">Courbe d'équité — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}</div>
<div className="text-xs text-slate-500 mb-3">
Capital final: <span className="text-white font-bold">{typed.final_capital.toFixed(2)}€</span>
{' '}(initial: {form.capital}€ · P&L: {typed.total_pnl >= 0 ? '+' : ''}{typed.total_pnl.toFixed(2)}€)
</div>
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={typed.equity_curve}>
<defs>
<linearGradient id="equity-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={typed.total_return_pct >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0.3} />
<stop offset="95%" stopColor={typed.total_return_pct >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
<XAxis dataKey="index" tick={{ fill: '#475569', fontSize: 9 }} />
<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)}€`, 'Capital']}
/>
<ReferenceLine y={form.capital} stroke="#475569" strokeDasharray="4 4" label={{ value: 'Initial', fill: '#475569', fontSize: 9 }} />
<Area
type="monotone" dataKey="capital"
stroke={typed.total_return_pct >= 0 ? '#10b981' : '#ef4444'}
fill="url(#equity-grad)" strokeWidth={2} dot={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Last trades */}
<div className="card">
<div className="section-title">Derniers trades exécutés</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left pb-2">Entrée</th>
<th className="text-left pb-2">Sortie</th>
<th className="text-right pb-2">Prix entrée</th>
<th className="text-right pb-2">Strike</th>
<th className="text-right pb-2">Prime</th>
<th className="text-right pb-2">Prix sortie</th>
<th className="text-right pb-2">P&L</th>
<th className="text-right pb-2">Capital</th>
</tr>
</thead>
<tbody>
{typed.trades.map((t, i) => {
const pnl = t.pnl as number
return (
<tr key={i} className="border-b border-slate-700/20 hover:bg-dark-700/50">
<td className="py-1">{t.entry_date as string}</td>
<td className="py-1">{t.exit_date as string}</td>
<td className="py-1 text-right font-mono">${(t.S_entry as number).toFixed(2)}</td>
<td className="py-1 text-right font-mono">${(t.K as number).toFixed(2)}</td>
<td className="py-1 text-right font-mono">${(t.premium as number).toFixed(4)}</td>
<td className="py-1 text-right font-mono">${(t.S_expiry as number).toFixed(2)}</td>
<td className={clsx('py-1 text-right font-mono font-bold', pnl >= 0 ? 'positive' : 'negative')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(2)}
</td>
<td className="py-1 text-right font-mono text-slate-300">{(t.capital as number).toFixed(2)}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</>
)}
{!hasResult && !isPending && !typed?.error && (
<div className="card h-80 flex items-center justify-center text-slate-600">
<div className="text-center">
<History className="w-10 h-10 mx-auto mb-3 opacity-20" />
<div className="text-sm">Configurer et lancer un backtest</div>
<div className="text-xs mt-1">Données yfinance historique complet disponible</div>
</div>
</div>
)}
</div>
</div>
</div>
)
}