feat: strategy builder
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
||||
import { Layers, Plus, Trash2, RefreshCw, AlertTriangle, Search, Save, FolderOpen, X } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
useOptionChainSlice, usePriceStrategy, useOptimizeStrategy,
|
||||
useOptionChainSlice, usePriceStrategy, useOptimizeStrategy, useSuggestedProfile,
|
||||
useScenarios, useSaveScenario, useDeleteScenario,
|
||||
useSavedStrategies, useSaveStrategyRecord, useDeleteSavedStrategy,
|
||||
useWatchlistTickers, useSaxoCatalog, useIvForTrade,
|
||||
type StrategyLeg, type StrategyScenario, type PriceCombo, type StrategyCandidate,
|
||||
type OptimizeConstraints, type SavedScenario,
|
||||
type GreekProfile, type GreekTarget, type GreekState, type GreekTolerance, DEFAULT_GREEK_PROFILE,
|
||||
} from '../hooks/useApi'
|
||||
import { fmtPrice, fmtAsOf } from '../lib/format'
|
||||
|
||||
@@ -87,14 +88,14 @@ function PayoffChart({ priced, spot, scenarioSpot }: { priced: PriceCombo; spot:
|
||||
)
|
||||
}
|
||||
|
||||
function GreeksTile({ label, now, scenario }: { label: string; now: number; scenario: number }) {
|
||||
function GreeksTile({ label, now, scenario, precision = 4, hint }: { label: string; now: number; scenario: number; precision?: number; hint?: string }) {
|
||||
return (
|
||||
<div className="card-sm">
|
||||
<div className="card-sm" title={hint}>
|
||||
<div className="stat-label">{label}</div>
|
||||
<div className="flex items-baseline gap-2 mt-1">
|
||||
<span className="text-lg font-bold text-white">{now.toFixed(4)}</span>
|
||||
<span className="text-lg font-bold text-white">{now.toFixed(precision)}</span>
|
||||
<span className="text-xs text-slate-500">→</span>
|
||||
<span className={clsx('text-sm font-semibold', scenario >= now ? 'text-emerald-400' : 'text-red-400')}>{scenario.toFixed(4)}</span>
|
||||
<span className={clsx('text-sm font-semibold', scenario >= now ? 'text-emerald-400' : 'text-red-400')}>{scenario.toFixed(precision)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -111,16 +112,16 @@ function ScenarioPanel({
|
||||
watchlistTickers: string[]
|
||||
}) {
|
||||
const slider = (
|
||||
key: 'spot_shock_pct' | 'iv_level_shift' | 'skew_tilt' | 'term_shift',
|
||||
key: 'spot_shock_pct' | 'iv_level_shift' | 'skew_tilt' | 'term_slope_shift' | 'rate_shock_bps',
|
||||
label: string, min: number, max: number, step: number, fmt: (v: number) => string,
|
||||
) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs text-slate-400 mb-1">
|
||||
<span>{label}</span>
|
||||
<span className="text-white font-semibold">{fmt(scenario[key])}</span>
|
||||
<span className="text-white font-semibold">{fmt(scenario[key] ?? 0)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range" min={min} max={max} step={step} value={scenario[key]}
|
||||
type="range" min={min} max={max} step={step} value={scenario[key] ?? 0}
|
||||
onChange={(e) => setScenario({ ...scenario, [key]: parseFloat(e.target.value) })}
|
||||
className="w-full accent-blue-500"
|
||||
/>
|
||||
@@ -152,20 +153,41 @@ function ScenarioPanel({
|
||||
</datalist>
|
||||
</div>
|
||||
<div className="w-28">
|
||||
<label className="stat-label block mb-1">Horizon (j)</label>
|
||||
<label className="stat-label block mb-1" title="Date d'évaluation du P&L du scénario — indépendante des échéances utilisées (voir DTE min/max)">
|
||||
Horizon (j)
|
||||
</label>
|
||||
<input
|
||||
type="number" min={1} max={90} value={horizonDays}
|
||||
onChange={(e) => setHorizonDays(parseInt(e.target.value) || 8)}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="stat-label block mb-1" title="Échéances autorisées pour les jambes — laisser vide pour revenir au comportement par défaut (proche de l'horizon)">
|
||||
DTE min
|
||||
</label>
|
||||
<input
|
||||
type="number" min={0} placeholder="—" value={scenario.dte_min ?? ''}
|
||||
onChange={(e) => setScenario({ ...scenario, dte_min: e.target.value === '' ? null : parseInt(e.target.value) })}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="stat-label block mb-1">DTE max</label>
|
||||
<input
|
||||
type="number" min={0} placeholder="—" value={scenario.dte_max ?? ''}
|
||||
onChange={(e) => setScenario({ ...scenario, dte_max: e.target.value === '' ? null : parseInt(e.target.value) })}
|
||||
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{slider('spot_shock_pct', 'Choc spot', -20, 20, 0.5, (v) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`)}
|
||||
{slider('iv_level_shift', 'Choc niveau IV', -0.15, 0.15, 0.005, (v) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}pts`)}
|
||||
{slider('skew_tilt', 'Tilt skew', -0.1, 0.1, 0.005, (v) => v.toFixed(3))}
|
||||
{slider('term_shift', 'Choc terme (/30j)', -0.1, 0.1, 0.005, (v) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}pts`)}
|
||||
{slider('term_slope_shift', 'Pente du terme (/30j)', -0.1, 0.1, 0.005, (v) => `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}pts`)}
|
||||
{slider('rate_shock_bps', 'Choc de taux', -200, 200, 5, (v) => `${v >= 0 ? '+' : ''}${v.toFixed(0)}bps`)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -187,7 +209,7 @@ function ScenarioGrid({
|
||||
const base = estimateBaseIv(chain, daysToExpiry, strikePct, spot)
|
||||
if (base == null) return null
|
||||
const moneyness = Math.log(strikePct / 100)
|
||||
return Math.max(0.01, base + scenario.iv_level_shift + scenario.skew_tilt * moneyness + scenario.term_shift * (daysToExpiry / 30))
|
||||
return Math.max(0.01, base + scenario.iv_level_shift + scenario.skew_tilt * moneyness + scenario.term_slope_shift * (daysToExpiry / 30))
|
||||
}
|
||||
|
||||
const setOverride = (daysToExpiry: number, strikePct: number, iv: number | null) => {
|
||||
@@ -468,6 +490,134 @@ function OptimizerPanel({
|
||||
)
|
||||
}
|
||||
|
||||
const GREEK_STATES: { value: GreekState; label: string }[] = [
|
||||
{ value: 'strong_negative', label: 'Fortement négatif' },
|
||||
{ value: 'negative', label: 'Négatif' },
|
||||
{ value: 'neutral', label: 'Neutre' },
|
||||
{ value: 'positive', label: 'Positif' },
|
||||
{ value: 'strong_positive', label: 'Fortement positif' },
|
||||
{ value: 'free', label: 'Libre / non contraint' },
|
||||
]
|
||||
const GREEK_TOLERANCES: { value: GreekTolerance; label: string }[] = [
|
||||
{ value: 'etroite', label: 'Étroite' },
|
||||
{ value: 'normale', label: 'Normale' },
|
||||
{ value: 'large', label: 'Large' },
|
||||
]
|
||||
const GREEK_ROWS: { key: keyof GreekProfile; label: string; hint: string }[] = [
|
||||
{ key: 'delta', label: 'Delta', hint: 'Exposition directionnelle immédiate' },
|
||||
{ key: 'gamma', label: 'Gamma', hint: 'Le Delta s’améliore-t-il avec le mouvement ?' },
|
||||
{ key: 'theta', label: 'Theta', hint: 'Collecte de prime (+) ou achat de temps (-)' },
|
||||
{ key: 'vega', label: 'Vega', hint: 'Acheteur (+) ou vendeur (-) de volatilité' },
|
||||
{ key: 'rho', label: 'Rho', hint: 'Sensibilité au taux — surtout utile en LEAPS/futures' },
|
||||
]
|
||||
|
||||
const GREEK_STATE_LABEL: Record<GreekState, string> = {
|
||||
strong_negative: 'Fortement négatif', negative: 'Négatif', neutral: 'Neutre',
|
||||
positive: 'Positif', strong_positive: 'Fortement positif', free: 'Libre',
|
||||
}
|
||||
|
||||
function SuggestedProfileCard({
|
||||
scenario, enabled, onAdopt,
|
||||
}: { scenario: StrategyScenario; enabled: boolean; onAdopt: (states: Record<'delta' | 'gamma' | 'theta' | 'vega' | 'rho', GreekState>) => void }) {
|
||||
const { data } = useSuggestedProfile(scenario, enabled)
|
||||
if (!data) return null
|
||||
const rows: { key: 'delta' | 'gamma' | 'theta' | 'vega' | 'rho'; label: string }[] = [
|
||||
{ key: 'delta', label: 'Delta' }, { key: 'gamma', label: 'Gamma' }, { key: 'theta', label: 'Theta' }, { key: 'vega', label: 'Vega' },
|
||||
]
|
||||
const active = rows.filter(r => data[r.key] !== 'free')
|
||||
if (active.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="card border-slate-700/50 bg-dark-700/20 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="stat-label">Profil suggéré par le scénario seul (avant vos propres cibles)</div>
|
||||
<button
|
||||
onClick={() => onAdopt({
|
||||
delta: data.delta, gamma: data.gamma, theta: data.theta, vega: data.vega, rho: data.rho,
|
||||
})}
|
||||
className="text-[11px] bg-blue-600/80 hover:bg-blue-500 text-white px-2 py-1 rounded font-semibold"
|
||||
>
|
||||
Adopter ce profil
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{active.map(r => (
|
||||
<span key={r.key} className="text-[11px] bg-dark-700/60 border border-slate-700/40 rounded px-2 py-0.5 text-slate-200">
|
||||
{r.label} : <span className="font-semibold">{GREEK_STATE_LABEL[data[r.key]]}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ul className="text-[11px] text-slate-500 space-y-0.5 list-disc list-inside">
|
||||
{data.rationale.map((r, i) => <li key={i}>{r}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GreekProfilePanel({
|
||||
profile, setProfile,
|
||||
}: { profile: GreekProfile; setProfile: (v: GreekProfile) => void }) {
|
||||
const setTarget = (key: keyof GreekProfile, patch: Partial<GreekTarget>) =>
|
||||
setProfile({ ...profile, [key]: { ...profile[key], ...patch } })
|
||||
|
||||
const anyActive = Object.values(profile).some(t => t.state !== 'free')
|
||||
|
||||
return (
|
||||
<div className="card space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="stat-label">Profil recherché — comportement Greeks, pas un nouveau scénario</div>
|
||||
{anyActive && (
|
||||
<button onClick={() => setProfile(DEFAULT_GREEK_PROFILE)} className="text-[10px] text-slate-500 hover:text-slate-300">
|
||||
Réinitialiser
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Le scénario ci-dessus produit déjà naturellement certains Greeks — ces contrôles servent à favoriser,
|
||||
tolérer ou interdire certaines expositions parmi les candidats trouvés, pas à décrire un second scénario.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{GREEK_ROWS.map(({ key, label, hint }) => {
|
||||
const t = profile[key]
|
||||
const isFree = t.state === 'free'
|
||||
return (
|
||||
<div key={key} className={clsx('grid grid-cols-12 gap-2 items-center text-xs rounded px-2 py-1.5',
|
||||
isFree ? 'bg-transparent' : 'bg-dark-700/40')}>
|
||||
<div className="col-span-2">
|
||||
<div className="text-slate-200 font-medium">{label}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight" title={hint}>{hint}</div>
|
||||
</div>
|
||||
<select
|
||||
value={t.state}
|
||||
onChange={(e) => setTarget(key, { state: e.target.value as GreekState })}
|
||||
className="col-span-4 bg-dark-700 border border-slate-700/50 rounded px-2 py-1 text-slate-200"
|
||||
>
|
||||
{GREEK_STATES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={t.tolerance}
|
||||
disabled={isFree}
|
||||
onChange={(e) => setTarget(key, { tolerance: e.target.value as GreekTolerance })}
|
||||
className="col-span-2 bg-dark-700 border border-slate-700/50 rounded px-2 py-1 text-slate-200 disabled:opacity-30"
|
||||
>
|
||||
{GREEK_TOLERANCES.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
<input
|
||||
type="range" min={0} max={100} step={5} value={t.weight} disabled={isFree}
|
||||
onChange={(e) => setTarget(key, { weight: parseFloat(e.target.value) })}
|
||||
className="col-span-3 accent-blue-500 disabled:opacity-30"
|
||||
/>
|
||||
<span className={clsx('col-span-1 text-right font-mono', isFree ? 'text-slate-700' : 'text-slate-300')}>
|
||||
{isFree ? '—' : `${t.weight.toFixed(0)}%`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResultsTable({ results, onSelect }: { results: StrategyCandidate[]; onSelect: (c: StrategyCandidate) => void }) {
|
||||
if (!results.length) return <div className="card-sm text-xs text-slate-500">Aucun candidat ne satisfait les contraintes — élargissez le seuil de delta ou le plafond de perte.</div>
|
||||
return (
|
||||
@@ -483,6 +633,7 @@ function ResultsTable({ results, onSelect }: { results: StrategyCandidate[]; onS
|
||||
<th className="py-1 pr-3 text-right" title="À l'échéance de la jambe la plus proche, sous la même vue de vol que le scénario. Approximatif (balayage rapide pour classer des centaines de candidats) — se précise après «Charger».">Max gain</th>
|
||||
<th className="py-1 pr-3 text-right" title="À l'échéance de la jambe la plus proche, sous la même vue de vol que le scénario. Approximatif (balayage rapide pour classer des centaines de candidats) — se précise après «Charger».">Max perte</th>
|
||||
<th className="py-1 pr-3 text-right">Δ net</th>
|
||||
<th className="py-1 pr-3 text-right" title="Correspondance avec le profil de Greeks demandé (0-100%) — n'apparaît que si un profil est actif.">Profil</th>
|
||||
<th className="py-1 pr-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -496,6 +647,9 @@ function ResultsTable({ results, onSelect }: { results: StrategyCandidate[]; onS
|
||||
<td className="py-1.5 pr-3 text-right text-emerald-400">{r.max_gain != null ? fmtMoney(r.max_gain) : '∞'}</td>
|
||||
<td className="py-1.5 pr-3 text-right text-red-400">{r.max_loss != null ? fmtMoney(r.max_loss) : '−∞'}</td>
|
||||
<td className="py-1.5 pr-3 text-right text-slate-400">{r.net_delta_now.toFixed(3)}</td>
|
||||
<td className="py-1.5 pr-3 text-right text-slate-400">
|
||||
{r.greek_match_score != null ? `${(r.greek_match_score * 100).toFixed(0)}%` : '—'}
|
||||
</td>
|
||||
<td className="py-1.5 pr-1 text-blue-400 text-right">Charger →</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -585,7 +739,8 @@ export default function StrategyBuilder() {
|
||||
const [debouncedSymbol, setDebouncedSymbol] = useState('')
|
||||
const [horizonDays, setHorizonDays] = useState(8)
|
||||
const [scenario, setScenario] = useState<StrategyScenario>({
|
||||
symbol: '', horizon_days: 8, spot_shock_pct: 0, iv_level_shift: 0, skew_tilt: 0, term_shift: 0, manual_grid: [],
|
||||
symbol: '', horizon_days: 8, spot_shock_pct: 0, iv_level_shift: 0, skew_tilt: 0, term_slope_shift: 0,
|
||||
rate_shock_bps: 0, dte_min: null, dte_max: null, manual_grid: [],
|
||||
contract_size: 100_000,
|
||||
})
|
||||
|
||||
@@ -599,6 +754,8 @@ export default function StrategyBuilder() {
|
||||
const [constraints, setConstraints] = useState<OptimizeConstraints>({
|
||||
max_legs: 4, delta_threshold: 0.15, max_loss_cap: null, objective: 'net_pnl', top_n: 20,
|
||||
})
|
||||
const [greekProfile, setGreekProfile] = useState<GreekProfile>(DEFAULT_GREEK_PROFILE)
|
||||
const [showAdvancedGreeks, setShowAdvancedGreeks] = useState(false)
|
||||
const [activeTemplate, setActiveTemplate] = useState<string | null>(null)
|
||||
|
||||
const { data: watchlistData } = useWatchlistTickers()
|
||||
@@ -609,7 +766,7 @@ export default function StrategyBuilder() {
|
||||
])).sort()
|
||||
|
||||
const { data: chain, isLoading: chainLoading, isError: chainError, error: chainErrorObj, refetch: refetchChain, isFetching } =
|
||||
useOptionChainSlice(debouncedSymbol, horizonDays, 3)
|
||||
useOptionChainSlice(debouncedSymbol, horizonDays, 3, true, scenario.dte_min, scenario.dte_max)
|
||||
const { data: ivForTrade } = useIvForTrade(debouncedSymbol)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -655,7 +812,7 @@ export default function StrategyBuilder() {
|
||||
|
||||
const handleOptimize = () => {
|
||||
setActiveTemplate(null)
|
||||
optimizeMutation.mutate({ scenario, constraints })
|
||||
optimizeMutation.mutate({ scenario, constraints, greek_profile: greekProfile })
|
||||
}
|
||||
|
||||
const handleSelectCandidate = (c: StrategyCandidate) => {
|
||||
@@ -668,7 +825,8 @@ export default function StrategyBuilder() {
|
||||
setHorizonDays(s.horizon_days)
|
||||
setScenario(prev => ({
|
||||
symbol: s.symbol, horizon_days: s.horizon_days, spot_shock_pct: s.spot_shock_pct,
|
||||
iv_level_shift: s.iv_level_shift, skew_tilt: s.skew_tilt, term_shift: s.term_shift,
|
||||
iv_level_shift: s.iv_level_shift, skew_tilt: s.skew_tilt, term_slope_shift: s.term_slope_shift,
|
||||
rate_shock_bps: s.rate_shock_bps, dte_min: s.dte_min, dte_max: s.dte_max,
|
||||
manual_grid: s.manual_grid, contract_size: prev.contract_size,
|
||||
}))
|
||||
}
|
||||
@@ -775,7 +933,20 @@ export default function StrategyBuilder() {
|
||||
)}
|
||||
|
||||
{chain && (
|
||||
<OptimizerPanel constraints={constraints} setConstraints={setConstraints} onRun={handleOptimize} isRunning={optimizeMutation.isPending} />
|
||||
<>
|
||||
<SuggestedProfileCard
|
||||
scenario={scenario} enabled={!!chain}
|
||||
onAdopt={(states) => setGreekProfile({
|
||||
delta: { state: states.delta, tolerance: 'normale', weight: 60 },
|
||||
gamma: { state: states.gamma, tolerance: 'normale', weight: 60 },
|
||||
theta: { state: states.theta, tolerance: 'normale', weight: 60 },
|
||||
vega: { state: states.vega, tolerance: 'normale', weight: 60 },
|
||||
rho: { state: states.rho, tolerance: 'normale', weight: 60 },
|
||||
})}
|
||||
/>
|
||||
<GreekProfilePanel profile={greekProfile} setProfile={setGreekProfile} />
|
||||
<OptimizerPanel constraints={constraints} setConstraints={setConstraints} onRun={handleOptimize} isRunning={optimizeMutation.isPending} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{optimizeMutation.isError && (
|
||||
@@ -783,8 +954,18 @@ export default function StrategyBuilder() {
|
||||
{(optimizeMutation.error as any)?.response?.data?.detail ?? "Erreur lors de l'optimisation."}
|
||||
</div>
|
||||
)}
|
||||
{optimizeMutation.data && optimizeMutation.data.warnings.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
{optimizeMutation.data.warnings.map((w, i) => (
|
||||
<div key={i} className="flex items-start gap-2 px-3 py-2 rounded border border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">
|
||||
<AlertTriangle className="w-3.5 h-3.5 mt-0.5 shrink-0" />
|
||||
<span>{w}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{optimizeMutation.data && (
|
||||
<ResultsTable results={optimizeMutation.data} onSelect={handleSelectCandidate} />
|
||||
<ResultsTable results={optimizeMutation.data.candidates} onSelect={handleSelectCandidate} />
|
||||
)}
|
||||
|
||||
{priceMutation.isPending && <div className="card-sm text-xs text-slate-500">Calcul en cours…</div>}
|
||||
@@ -843,11 +1024,56 @@ export default function StrategyBuilder() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<GreeksTile label="Delta net" now={priced.greeks_now.delta} scenario={priced.greeks_scenario.delta} />
|
||||
<GreeksTile label="Gamma net" now={priced.greeks_now.gamma} scenario={priced.greeks_scenario.gamma} />
|
||||
<GreeksTile label="Theta net" now={priced.greeks_now.theta} scenario={priced.greeks_scenario.theta} />
|
||||
<GreeksTile label="Vega net" now={priced.greeks_now.vega} scenario={priced.greeks_scenario.vega} />
|
||||
<GreeksTile label="Rho net" now={priced.greeks_now.rho} scenario={priced.greeks_scenario.rho} />
|
||||
</div>
|
||||
|
||||
{priced.vanna_simulation && (
|
||||
<div className="card border-blue-700/30 bg-blue-900/10">
|
||||
<div className="stat-label mb-1">Simulation Vanna — pas juste un signe</div>
|
||||
<p className="text-sm text-slate-200">
|
||||
Spot <span className="font-semibold">{priced.vanna_simulation.spot_shock_pct}%</span>{' '}
|
||||
et IV <span className="font-semibold">+{priced.vanna_simulation.iv_shock_pts}pts</span> →
|
||||
{' '}Delta net passe de{' '}
|
||||
<span className="font-mono font-semibold">{priced.vanna_simulation.delta_before.toFixed(4)}</span>
|
||||
{' '}à{' '}
|
||||
<span className="font-mono font-semibold">{priced.vanna_simulation.delta_after.toFixed(4)}</span>
|
||||
<span className={clsx('ml-2 font-mono font-bold', priced.vanna_simulation.delta_change >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
({priced.vanna_simulation.delta_change >= 0 ? '+' : ''}{priced.vanna_simulation.delta_change.toFixed(4)})
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
Repricing Black-Scholes réel sous ce choc conjoint — pas une approximation linéaire, valable même pour un choc large.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<button onClick={() => setShowAdvancedGreeks(v => !v)} className="stat-label flex items-center gap-1.5 w-full">
|
||||
{showAdvancedGreeks ? '▾' : '▸'} Sensibilités avancées (second ordre)
|
||||
</button>
|
||||
{showAdvancedGreeks && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-3">
|
||||
<GreeksTile label="Vanna" precision={6} hint="∂Delta/∂IV — interaction spot/vol, par point d'IV"
|
||||
now={priced.greeks_now.vanna} scenario={priced.greeks_scenario.vanna} />
|
||||
<GreeksTile label="Charm" precision={6} hint="∂Delta/∂temps — dérive du Delta par jour"
|
||||
now={priced.greeks_now.charm} scenario={priced.greeks_scenario.charm} />
|
||||
<GreeksTile label="Vomma" precision={6} hint="∂Vega/∂IV — convexité de volatilité, par point d'IV"
|
||||
now={priced.greeks_now.vomma} scenario={priced.greeks_scenario.vomma} />
|
||||
<GreeksTile label="Veta" precision={6} hint="∂Vega/∂temps — dérive du Vega par jour"
|
||||
now={priced.greeks_now.veta} scenario={priced.greeks_scenario.veta} />
|
||||
<GreeksTile label="Speed" precision={8} hint="∂Gamma/∂spot — où se déplace la convexité"
|
||||
now={priced.greeks_now.speed} scenario={priced.greeks_scenario.speed} />
|
||||
<GreeksTile label="Color" precision={8} hint="∂Gamma/∂temps — dérive du Gamma par jour"
|
||||
now={priced.greeks_now.color} scenario={priced.greeks_scenario.color} />
|
||||
<GreeksTile label="Zomma" precision={6} hint="∂Gamma/∂IV — le Gamma survit-il à un choc de vol ?"
|
||||
now={priced.greeks_now.zomma} scenario={priced.greeks_scenario.zomma} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user