feat: option lab
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user