feat: option lab
This commit is contained in:
@@ -326,6 +326,31 @@ export const usePositionPayoff = (posId: string, enabled: boolean) =>
|
||||
enabled,
|
||||
})
|
||||
|
||||
// "What would have been optimal, in hindsight?" — reprices the position's real legs and
|
||||
// runs the Strategy Builder optimizer against the REAL historical Saxo chain + the REALIZED
|
||||
// spot/IV move since entry (not a guessed scenario). Expensive (full optimizer run), so
|
||||
// on-demand only — not auto-fetched, call refetch() from a button.
|
||||
export type RetrospectiveCandidate = {
|
||||
template_name: string; legs: StrategyLeg[]; score: number; return_on_capital_pct: number | null
|
||||
net_pnl: number; max_gain: number | null; max_loss: number | null; net_delta_now: number
|
||||
}
|
||||
export type RetrospectiveComparison = {
|
||||
available: boolean; reason?: string
|
||||
underlying?: string; entry_date?: string; as_of?: string; horizon_days?: number
|
||||
spot_entry?: number; spot_realized?: number; realized_spot_shock_pct?: number
|
||||
iv_entry?: number; iv_realized?: number; realized_iv_shift?: number
|
||||
actual_return_pct?: number | null
|
||||
optimal_candidates?: RetrospectiveCandidate[]
|
||||
}
|
||||
|
||||
export const useRetrospectiveOptimal = (posId: string, asOf?: string) =>
|
||||
useQuery<RetrospectiveComparison>({
|
||||
queryKey: ['portfolio-retrospective-optimal', posId, asOf],
|
||||
queryFn: () => api.get(`/portfolio/positions/${posId}/retrospective-optimal`, { params: asOf ? { as_of: asOf } : {} }).then(r => r.data),
|
||||
enabled: false,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
// Reprices every open position under a handful of named macro scenarios (Risk-Off,
|
||||
// Risk-On, inflation persistante, dollar fort, baisse des matières premières) to surface
|
||||
// when several differently-named positions are really the same underlying bet.
|
||||
@@ -1048,6 +1073,36 @@ export const useSaxoIvSnapshot = (symbol: string) =>
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
// Options Lab — "was this option well priced between two dates?" Picks the strike closest
|
||||
// to the underlying's realized outcome at date_b (hindsight), reprices at both dates from
|
||||
// real Saxo history, decomposes the move into Delta/Theta/Vega + a residual.
|
||||
export type PricingCheckLeg = {
|
||||
available: boolean
|
||||
price_a?: number; price_b?: number; actual_change?: number
|
||||
iv_a?: number | null; iv_b?: number | null
|
||||
intrinsic_a?: number; time_value_a?: number; intrinsic_b?: number; time_value_b?: number
|
||||
greeks_a?: { delta: number; gamma: number; theta: number; vega: number }
|
||||
attribution?: { delta_pnl: number; theta_pnl: number; vega_pnl: number; explained: number; residual: number }
|
||||
}
|
||||
export type PricingCheckResult = {
|
||||
available: boolean; reason?: string
|
||||
ticker?: string; date_a?: string; date_b?: string; elapsed_days?: number; target_dte_used?: number
|
||||
expiry_date?: string; expired_by_date_b?: boolean
|
||||
spot_a?: number; spot_b?: number; spot_change_pct?: number; chosen_strike?: number
|
||||
iv_a?: number | null; realized_vol?: number | null; vol_risk_premium?: number | null
|
||||
legs?: { call: PricingCheckLeg; put: PricingCheckLeg }
|
||||
}
|
||||
|
||||
// targetDte omitted/undefined -> backend defaults to the expiry closest to dateB (hindsight,
|
||||
// same principle as the strike selection) — only pass it to force a different expiry.
|
||||
export const usePricingCheck = (ticker: string, dateA: string, dateB: string, targetDte: number | undefined, enabled: boolean) =>
|
||||
useQuery<PricingCheckResult>({
|
||||
queryKey: ['options-pricing-check', ticker, dateA, dateB, targetDte],
|
||||
queryFn: () => api.get('/saxo/pricing-check', { params: { ticker, date_a: dateA, date_b: dateB, target_dte: targetDte } }).then(r => r.data),
|
||||
enabled: enabled && !!ticker && !!dateA && !!dateB,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useSaxoIvHistory = (symbol: string, days = 90) =>
|
||||
useQuery({
|
||||
queryKey: ['saxo-iv-history', symbol, days],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
useIvWatchlist, useIvSnapshot, useIvHistory, useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker,
|
||||
useSaxoIvWatchlist, useSaxoIvSnapshot, useSaxoIvHistory, useInstrumentsWatchlist,
|
||||
useSaxoIvWatchlist, useSaxoIvSnapshot, useSaxoIvHistory, useInstrumentsWatchlist, usePricingCheck,
|
||||
} from '../hooks/useApi'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database, Plus, Trash2, List, Link2 } from 'lucide-react'
|
||||
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database, Plus, Trash2, List, Link2, Search } from 'lucide-react'
|
||||
import { api } from '../hooks/useApi'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import clsx from 'clsx'
|
||||
@@ -528,6 +528,175 @@ function WatchlistManager() {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Pricing check — "était-ce bien pricé entre 2 dates ?" ───────────────────────
|
||||
function fmtSigned(v: number | null | undefined, digits = 2): string {
|
||||
if (v == null) return '—'
|
||||
return `${v >= 0 ? '+' : ''}${v.toFixed(digits)}`
|
||||
}
|
||||
|
||||
function PricingCheckLegCard({ label, leg }: { label: string; leg: any }) {
|
||||
if (!leg?.available) {
|
||||
return (
|
||||
<div className="card-sm">
|
||||
<div className="stat-label mb-1">{label}</div>
|
||||
<div className="text-xs text-slate-600">Pas de cotation exploitable pour cette jambe.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const attr = leg.attribution
|
||||
return (
|
||||
<div className="card-sm space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="stat-label">{label}</div>
|
||||
<div className={clsx('text-sm font-mono font-bold', leg.actual_change >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{fmtSigned(leg.actual_change, 4)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[11px]">
|
||||
<div className="text-slate-500">Prix</div>
|
||||
<div className="text-right font-mono text-slate-300">{leg.price_a?.toFixed(4)} → {leg.price_b?.toFixed(4)}</div>
|
||||
<div className="text-slate-500">IV</div>
|
||||
<div className="text-right font-mono text-slate-300">
|
||||
{leg.iv_a != null ? `${(leg.iv_a * 100).toFixed(1)}%` : '—'} → {leg.iv_b != null ? `${(leg.iv_b * 100).toFixed(1)}%` : 'expiré'}
|
||||
</div>
|
||||
<div className="text-slate-500">Intrinsèque</div>
|
||||
<div className="text-right font-mono text-slate-300">{leg.intrinsic_a?.toFixed(2)} → {leg.intrinsic_b?.toFixed(2)}</div>
|
||||
<div className="text-slate-500">Valeur temps</div>
|
||||
<div className="text-right font-mono text-slate-300">{leg.time_value_a?.toFixed(2)} → {leg.time_value_b?.toFixed(2)}</div>
|
||||
</div>
|
||||
{attr && (
|
||||
<div className="pt-2 border-t border-slate-700/30">
|
||||
<div className="text-[10px] text-slate-600 uppercase tracking-wide mb-1">Décomposition (Greeks à la date A)</div>
|
||||
<div className="space-y-0.5 text-[11px] font-mono">
|
||||
<div className="flex justify-between"><span className="text-slate-500">Δ spot × Delta</span><span className="text-slate-300">{fmtSigned(attr.delta_pnl)}</span></div>
|
||||
<div className="flex justify-between"><span className="text-slate-500">Temps × Theta</span><span className="text-slate-300">{fmtSigned(attr.theta_pnl)}</span></div>
|
||||
<div className="flex justify-between"><span className="text-slate-500">Δ IV × Vega</span><span className="text-slate-300">{fmtSigned(attr.vega_pnl)}</span></div>
|
||||
<div className="flex justify-between border-t border-slate-700/20 pt-0.5 mt-0.5">
|
||||
<span className="text-slate-400">Expliqué par les Greeks</span><span className="text-slate-200 font-semibold">{fmtSigned(attr.explained)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Résidu (gamma, skew, anomalie…)</span>
|
||||
<span className={clsx('font-semibold', Math.abs(attr.residual) > Math.abs(leg.actual_change) * 0.3 ? 'text-amber-400' : 'text-slate-200')}>
|
||||
{fmtSigned(attr.residual)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PricingCheckPanel() {
|
||||
const { data: watchlistInstruments } = useInstrumentsWatchlist()
|
||||
const tickers: any[] = (watchlistInstruments as any[]) ?? []
|
||||
const [ticker, setTicker] = useState('')
|
||||
const [dateA, setDateA] = useState('')
|
||||
const [dateB, setDateB] = useState('')
|
||||
const [targetDte, setTargetDte] = useState<number | undefined>(undefined)
|
||||
const [run, setRun] = useState(false)
|
||||
|
||||
const { data, isFetching, refetch } = usePricingCheck(ticker, dateA, dateB, targetDte, run)
|
||||
|
||||
const handleAnalyze = () => {
|
||||
setRun(true)
|
||||
refetch()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="card">
|
||||
<div className="text-sm font-bold text-white flex items-center gap-2 mb-1">
|
||||
<Search className="w-4 h-4 text-blue-400" /> Vérification de pricing historique
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mb-3">
|
||||
Choisit, avec le recul, le strike le plus proche de là où le sous-jacent a réellement fini — le contrat le
|
||||
plus révélateur pour juger si la volatilité était bien pricée à la date de départ. Repricing réel depuis
|
||||
l'historique Saxo accumulé, décomposé Delta/Theta/Vega.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
|
||||
<div>
|
||||
<label className="text-slate-400 block mb-1">Instrument</label>
|
||||
<select value={ticker} onChange={(e) => setTicker(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-slate-200">
|
||||
<option value="">—</option>
|
||||
{tickers.map(t => <option key={t.ticker} value={t.ticker}>{t.name || t.ticker}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-400 block mb-1">Date A (départ)</label>
|
||||
<input type="date" value={dateA} onChange={(e) => setDateA(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-400 block mb-1">Date B (fin)</label>
|
||||
<input type="date" value={dateB} onChange={(e) => setDateB(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-400 block mb-1" title="Vide = échéance la plus proche de la date B (même logique que le strike)">
|
||||
DTE cible (optionnel)
|
||||
</label>
|
||||
<input type="number" min={1} max={365} placeholder="auto (date B)" value={targetDte ?? ''}
|
||||
onChange={(e) => setTargetDte(e.target.value === '' ? undefined : parseInt(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
disabled={!ticker || !dateA || !dateB || isFetching}
|
||||
className="mt-3 flex items-center gap-1.5 text-xs bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1.5 rounded font-semibold"
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" /> {isFetching ? 'Analyse…' : 'Analyser'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{data && !data.available && (
|
||||
<div className="card border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">{data.reason}</div>
|
||||
)}
|
||||
|
||||
{data?.available && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-[10px] text-slate-500">Spot</div>
|
||||
<div className="text-sm font-mono text-white">{data.spot_a?.toFixed(2)} → {data.spot_b?.toFixed(2)}</div>
|
||||
<div className={clsx('text-[11px] font-mono', (data.spot_change_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{fmtSigned(data.spot_change_pct)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-[10px] text-slate-500">Strike choisi (recul)</div>
|
||||
<div className="text-sm font-mono text-white">{data.chosen_strike}</div>
|
||||
<div className="text-[10px] text-slate-600">
|
||||
échéance {data.expiry_date}{data.expired_by_date_b ? ' (expirée)' : ''} · {data.target_dte_used}j visés
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-[10px] text-slate-500">IV (départ) vs Vol réalisée</div>
|
||||
<div className="text-sm font-mono text-white">
|
||||
{data.iv_a != null ? `${(data.iv_a * 100).toFixed(1)}%` : '—'} / {data.realized_vol != null ? `${(data.realized_vol * 100).toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-[10px] text-slate-500">Prime de risque de vol</div>
|
||||
<div className={clsx('text-sm font-mono font-bold', (data.vol_risk_premium ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{data.vol_risk_premium != null ? `${fmtSigned(data.vol_risk_premium * 100, 1)}pts` : '—'}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-600">{(data.vol_risk_premium ?? 0) >= 0 ? 'IV a surpayé la vol réalisée' : 'IV a sous-payé la vol réalisée'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<PricingCheckLegCard label="Call" leg={data.legs?.call} />
|
||||
<PricingCheckLegCard label="Put" leg={data.legs?.put} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
export default function OptionsLab() {
|
||||
// yfinance-based IV watchlist — disabled per user request (2026-07-21): Options Lab
|
||||
@@ -562,6 +731,7 @@ export default function OptionsLab() {
|
||||
// // Show bootstrap banner if most items have no meaningful IV Rank (stuck at 50 or null)
|
||||
// const needsBootstrap = items.length > 0 && items.filter(i => i.iv_rank == null || i.iv_rank === 50).length > items.length * 0.6
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'iv-rank' | 'pricing-check'>('iv-rank')
|
||||
const { data: saxoData, isLoading: saxoLoading, refetch: refetchSaxo, isFetching: saxoFetching } = useSaxoIvWatchlist()
|
||||
const { data: watchlistInstruments } = useInstrumentsWatchlist()
|
||||
// Cross-reference the Saxo IV watchlist (keyed by Saxo option symbol, e.g. "MCLU6")
|
||||
@@ -608,8 +778,28 @@ export default function OptionsLab() {
|
||||
Saxo broker data · IV Rank · Term Structure · Skew
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||||
<button onClick={() => setActiveTab('iv-rank')}
|
||||
className={clsx('px-3 py-1.5 rounded text-xs font-semibold transition-colors', {
|
||||
'bg-blue-600 text-white': activeTab === 'iv-rank',
|
||||
'text-slate-400 hover:text-slate-200': activeTab !== 'iv-rank',
|
||||
})}>
|
||||
IV Rank
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('pricing-check')}
|
||||
className={clsx('px-3 py-1.5 rounded text-xs font-semibold transition-colors', {
|
||||
'bg-blue-600 text-white': activeTab === 'pricing-check',
|
||||
'text-slate-400 hover:text-slate-200': activeTab !== 'pricing-check',
|
||||
})}>
|
||||
Vérification de pricing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'pricing-check' && <PricingCheckPanel />}
|
||||
|
||||
{activeTab === 'iv-rank' && (
|
||||
<>
|
||||
{/* Légende */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="card bg-red-900/10 border-red-700/20 text-xs">
|
||||
@@ -729,6 +919,8 @@ export default function OptionsLab() {
|
||||
{/* yfinance IV Watchlist Manager — disabled along with the section above.
|
||||
<WatchlistManager />
|
||||
*/}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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