feat: option lab

This commit is contained in:
OpenSquared
2026-07-28 11:14:31 +02:00
parent 568414ca0c
commit d2c393b8e5
11 changed files with 757 additions and 29 deletions

View File

@@ -2,7 +2,7 @@ import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
usePortfolioPositions, usePortfolioSummary, usePnlHistory,
useAddPosition, useClosePosition, usePositionPayoff
useAddPosition, useClosePosition, usePositionPayoff, useRetrospectiveOptimal
} from '../hooks/useApi'
import { useQueryClient, useMutation } from '@tanstack/react-query'
import axios from 'axios'
@@ -263,6 +263,91 @@ function PositionPayoffChart({ posId, enabled, legs }: { posId: string; enabled:
)
}
function RetrospectiveComparisonCard({ posId, enabled }: { posId: string; enabled: boolean }) {
const { data, isFetching, refetch, isFetched } = useRetrospectiveOptimal(posId)
if (!enabled) return null
return (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="flex items-center justify-between mb-1.5">
<span className="text-slate-500 uppercase tracking-wide text-[10px]">
Comparaison rétrospective — qu'aurait-il fallu faire ?
</span>
<button
onClick={() => refetch()}
disabled={isFetching}
className="text-[10px] bg-blue-600/80 hover:bg-blue-500 disabled:opacity-50 text-white px-2 py-1 rounded font-semibold"
>
{isFetching ? 'Calcul… (~1 min)' : isFetched ? 'Recalculer' : 'Calculer'}
</button>
</div>
{!isFetched && !isFetching && (
<p className="text-[11px] text-slate-600">
Reconstruit le chain Saxo réel à la date d'entrée et le compare au mouvement réellement survenu depuis —
pas un scénario deviné. Fait tourner l'optimiseur complet (~1 min), calculé à la demande.
</p>
)}
{isFetching && (
<p className="text-[11px] text-slate-600">Recherche parmi les stratégies possibles à la date d'entrée…</p>
)}
{data && !data.available && (
<p className="text-[11px] text-amber-400">{data.reason}</p>
)}
{data?.available && (
<div className="space-y-2">
<div className="text-[11px] text-slate-400">
Du {data.entry_date} au {data.as_of} ({data.horizon_days}j) — mouvement réel :{' '}
<span className={clsx('font-mono font-semibold', (data.realized_spot_shock_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
spot {(data.realized_spot_shock_pct ?? 0) >= 0 ? '+' : ''}{data.realized_spot_shock_pct}%
</span>
{', '}
<span className="font-mono font-semibold text-slate-300">
IV {(data.realized_iv_shift ?? 0) >= 0 ? '+' : ''}{((data.realized_iv_shift ?? 0) * 100).toFixed(1)}pts
</span>
</div>
<div className="flex items-center gap-4 text-xs">
<div>
<span className="text-slate-500">Votre position : </span>
<span className={clsx('font-mono font-bold', (data.actual_return_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{data.actual_return_pct != null ? `${data.actual_return_pct >= 0 ? '+' : ''}${data.actual_return_pct}%` : ''}
</span>
</div>
</div>
<table className="w-full text-[11px]">
<thead>
<tr className="text-slate-500 text-left">
<th className="py-1 pr-2">Structure optimale (rétrospective)</th>
<th className="py-1 pr-2 text-right" title="Même capital que votre position réelle">Retour sur même capital</th>
<th className="py-1 text-right">Δ net</th>
</tr>
</thead>
<tbody>
{(data.optimal_candidates ?? []).map((c, i) => (
<tr key={i} className="border-t border-slate-700/20">
<td className="py-1 pr-2 text-slate-300">{c.template_name}</td>
<td className="py-1 pr-2 text-right font-mono text-slate-200">
{c.return_on_capital_pct != null ? `${c.return_on_capital_pct >= 0 ? '+' : ''}${c.return_on_capital_pct}%` : ''}
</td>
<td className="py-1 text-right font-mono text-slate-500">{c.net_delta_now.toFixed(3)}</td>
</tr>
))}
</tbody>
</table>
<p className="text-[10px] text-slate-600 italic">
Comparaison en % du même capital investi que votre position réelle — pas en dollars bruts, les deux
moteurs de pricing (Portfolio et Strategy Builder) n'utilisent pas la même convention de taille de contrat.
</p>
</div>
)}
</div>
)
}
function PositionCard({ pos }: { pos: Record<string, any> }) {
const navigate = useNavigate()
const [showClose, setShowClose] = useState(false)
@@ -447,6 +532,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
</div>
<PositionPayoffChart posId={pos.id} enabled={showDetails} legs={pos.legs} />
<RetrospectiveComparisonCard posId={pos.id} enabled={showDetails} />
</div>
)}