feat: strategy builder
This commit is contained in:
@@ -375,29 +375,6 @@ def expected_pnl_scenario(
|
||||
return float(numerator / denominator) if denominator > 1e-12 else float(pnl.mean())
|
||||
|
||||
|
||||
def time_decay_slices(
|
||||
legs: List[Dict[str, Any]], prices: np.ndarray, surface: Any, eval_days_expiry: float, r: float,
|
||||
entry_ref: float, contract_size: float = DEFAULT_CONTRACT_SIZE, n_slices: int = 4,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Payoff curve at n_slices evenly-spaced elapsed-day checkpoints between today (0) and
|
||||
the nearest leg's expiry — the "T+0/T+10/T+20..." view that shows how the curve morphs
|
||||
from today's time-value-laden shape into the kinked expiry payoff, instead of only the
|
||||
two endpoints at_expiry/at_scenario give. Uses the same scenario vol view as those two
|
||||
curves (see payoff_curves' own comment) so the only thing that varies between slices is
|
||||
time decay, not the vol assumption."""
|
||||
day_points = np.linspace(0, eval_days_expiry, n_slices)
|
||||
slices = []
|
||||
for d in day_points:
|
||||
d = float(d)
|
||||
label = "Aujourd'hui" if d < 0.5 else ("Échéance" if d >= eval_days_expiry - 0.5 else f"J+{round(d)}")
|
||||
points = [
|
||||
{"underlying": round(float(p), 4), "pnl": round(float(value_at(legs, float(p), d, surface, r, contract_size) - entry_ref), 2)}
|
||||
for p in prices
|
||||
]
|
||||
slices.append({"days_from_now": round(d, 1), "label": label, "points": points})
|
||||
return slices
|
||||
|
||||
|
||||
def _find_breakevens(
|
||||
legs: List[Dict[str, Any]], surface: Any, eval_days_expiry: float, r: float,
|
||||
entry_ref: float, spot: float, contract_size: float, n: int = 300,
|
||||
@@ -452,13 +429,33 @@ def payoff_heatmap(
|
||||
price_points = np.unique(np.concatenate([np.linspace(lo, hi, n_prices), np.array(near_breakevens)]))
|
||||
price_points.sort()
|
||||
day_points = np.linspace(0, eval_days_expiry, n_days)
|
||||
rows = [
|
||||
{
|
||||
"days_from_now": round(float(d), 1),
|
||||
"pnl": [round(float(value_at(legs, float(p), float(d), surface, r, contract_size) - entry_ref), 2) for p in price_points],
|
||||
}
|
||||
for d in day_points
|
||||
]
|
||||
# Per-cell Greeks (not just P&L) — same net Greeks the tiles above the chart already
|
||||
# show (per-contract-unit, not scaled by contract_size), just swept across the whole
|
||||
# price x time grid instead of only "now" vs "scenario" — lets the metric selector show
|
||||
# how Delta/Gamma/Theta/Vega/Rho actually evolve across the scenario, not just their
|
||||
# two endpoint values.
|
||||
rows = []
|
||||
for d in day_points:
|
||||
d = float(d)
|
||||
pnl_row, delta_row, gamma_row, theta_row, vega_row, rho_row = [], [], [], [], [], []
|
||||
for p in price_points:
|
||||
p = float(p)
|
||||
pnl_row.append(round(float(value_at(legs, p, d, surface, r, contract_size) - entry_ref), 2))
|
||||
g = greeks_at(legs, p, d, surface, r)
|
||||
# greeks_at's own round() leaves numpy float64 as numpy float64 (round() doesn't
|
||||
# coerce to native Python) — black_scholes is scipy-backed, and FastAPI's default
|
||||
# JSON encoder can't serialize a bare numpy scalar (unlike pnl_row above, which
|
||||
# already goes through float() explicitly).
|
||||
delta_row.append(float(g["delta"]))
|
||||
gamma_row.append(float(g["gamma"]))
|
||||
theta_row.append(float(g["theta"]))
|
||||
vega_row.append(float(g["vega"]))
|
||||
rho_row.append(float(g["rho"]))
|
||||
rows.append({
|
||||
"days_from_now": round(d, 1),
|
||||
"pnl": pnl_row, "delta": delta_row, "gamma": gamma_row,
|
||||
"theta": theta_row, "vega": vega_row, "rho": rho_row,
|
||||
})
|
||||
return {
|
||||
"prices": [round(float(p), 4) for p in price_points],
|
||||
"rows": rows,
|
||||
@@ -473,52 +470,13 @@ def payoff_curves(
|
||||
surface_scenario: ScenarioSurface,
|
||||
horizon_days: int,
|
||||
r: float = 0.05,
|
||||
n: int = 100,
|
||||
contract_size: float = DEFAULT_CONTRACT_SIZE,
|
||||
) -> Dict[str, Any]:
|
||||
spot = chain_slice["spot"]
|
||||
priced = price_combo(legs, chain_slice, surface_now, surface_scenario, horizon_days, r, contract_size)
|
||||
entry_ref = priced["entry_cost"]
|
||||
|
||||
lo, hi = spot * 0.6, spot * 1.4
|
||||
# A calendar/ratio spread's payoff can spike sharply right at a strike — a plain
|
||||
# uniform sweep over the full 0.6x-1.4x range (n points) can straddle right over that
|
||||
# peak without ever sampling it (same issue fixed in check_bounded_risk). Blend a
|
||||
# coarse baseline (overall shape) with a dense window around the legs' own strikes.
|
||||
# The exact spot/scenario spot are forced in as sample points too — otherwise hovering
|
||||
# right on the "Spot"/"Scénario" reference line reads the nearest grid point, which can
|
||||
# sit meaningfully off the true value on a peak this steep, and disagree with the
|
||||
# entry_cost/net_pnl tiles (computed at the exact spot, not a grid sample).
|
||||
baseline = np.linspace(lo, hi, n)
|
||||
strikes = [l["strike"] for l in legs]
|
||||
lo_k, hi_k = max(min(strikes) * 0.9, lo), min(max(strikes) * 1.1, hi)
|
||||
near_strikes = np.linspace(lo_k, hi_k, n * 3)
|
||||
exact_points = np.array([spot, surface_scenario.spot] + strikes)
|
||||
prices = np.unique(np.concatenate([baseline, near_strikes, exact_points]))
|
||||
prices.sort()
|
||||
eval_days_expiry = min(l["days_to_expiry"] for l in legs)
|
||||
|
||||
# Both curves share the scenario's volatility view (surface_scenario) — only the
|
||||
# evaluation DATE differs: "à échéance" prices the day the near leg expires (matching
|
||||
# the Max gain/Max perte tile, itself computed with surface_scenario for the same
|
||||
# reason — see check_bounded_risk), "à J+8" prices your chosen scenario horizon. Using
|
||||
# surface_now here instead would silently mix in today's un-shocked vol, producing a
|
||||
# curve whose peak doesn't match the Max gain tile right next to it.
|
||||
at_expiry = [
|
||||
{"underlying": round(float(p), 4), "pnl": round(float(value_at(legs, float(p), eval_days_expiry, surface_scenario, r, contract_size) - entry_ref), 2)}
|
||||
for p in prices
|
||||
]
|
||||
at_scenario = [
|
||||
{"underlying": round(float(p), 4), "pnl": round(float(value_at(legs, float(p), horizon_days, surface_scenario, r, contract_size) - entry_ref), 2)}
|
||||
for p in prices
|
||||
]
|
||||
|
||||
# Coarser price grid than the two headline curves above — this trades some precision
|
||||
# for keeping a single /price request's added cost bounded (n_slices/heatmap cells x
|
||||
# their own price points, on top of the ~1400 value_at calls at_expiry/at_scenario
|
||||
# above already need).
|
||||
slice_prices = np.linspace(lo, hi, 80)
|
||||
time_slices = time_decay_slices(legs, slice_prices, surface_scenario, eval_days_expiry, r, entry_ref, contract_size)
|
||||
heatmap = payoff_heatmap(legs, surface_scenario, eval_days_expiry, r, spot, entry_ref, contract_size)
|
||||
|
||||
return {"at_expiry": at_expiry, "at_scenario": at_scenario, "time_slices": time_slices, "heatmap": heatmap, **priced}
|
||||
return {"heatmap": heatmap, **priced}
|
||||
|
||||
@@ -1782,9 +1782,12 @@ export type VannaSimulation = {
|
||||
spot_shock_pct: number; iv_shock_pts: number
|
||||
delta_before: number; delta_after: number; delta_change: number
|
||||
}
|
||||
export type PayoffPoint = { underlying: number; pnl: number }
|
||||
export type TimeSlice = { days_from_now: number; label: string; points: PayoffPoint[] }
|
||||
export type PayoffHeatmap = { prices: number[]; rows: { days_from_now: number; pnl: number[] }[]; breakeven_prices: number[] }
|
||||
export type PayoffHeatmapMetric = 'pnl' | 'delta' | 'gamma' | 'theta' | 'vega' | 'rho'
|
||||
export type PayoffHeatmap = {
|
||||
prices: number[]
|
||||
rows: ({ days_from_now: number } & Record<PayoffHeatmapMetric, number[]>)[]
|
||||
breakeven_prices: number[]
|
||||
}
|
||||
|
||||
export type PriceCombo = {
|
||||
entry_cost: number
|
||||
@@ -1801,9 +1804,6 @@ export type PriceCombo = {
|
||||
net_delta_now: number
|
||||
net_delta_scenario: number
|
||||
vanna_simulation: VannaSimulation | null
|
||||
at_expiry: PayoffPoint[]
|
||||
at_scenario: PayoffPoint[]
|
||||
time_slices: TimeSlice[]
|
||||
heatmap: PayoffHeatmap
|
||||
spot: number
|
||||
scenario_spot: number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer,
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Layers, Plus, Trash2, RefreshCw, AlertTriangle, Search, Save, FolderOpen, X, History, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useScenarios, useSaveScenario, useDeleteScenario,
|
||||
useSavedStrategies, useSaveStrategyRecord, useDeleteSavedStrategy,
|
||||
useSaxoSymbols, useIvForTrade,
|
||||
type StrategyLeg, type StrategyScenario, type PriceCombo, type TimeSlice, type PayoffHeatmap, type StrategyCandidate,
|
||||
type StrategyLeg, type StrategyScenario, type PayoffHeatmap, type PayoffHeatmapMetric, type StrategyCandidate,
|
||||
type OptimizeConstraints, type SavedScenario,
|
||||
type GreekProfile, type GreekTarget, type GreekState, type GreekTolerance, DEFAULT_GREEK_PROFILE,
|
||||
} from '../hooks/useApi'
|
||||
@@ -49,98 +49,37 @@ function estimateBaseIv(chain: any, daysToExpiry: number, strikePct: number, spo
|
||||
return nearest.iv
|
||||
}
|
||||
|
||||
// ── Payoff chart ──────────────────────────────────────────────────────────────
|
||||
|
||||
function PayoffChart({ priced, spot, scenarioSpot, horizonDays }: { priced: PriceCombo; spot: number; scenarioSpot: number; horizonDays: number }) {
|
||||
const data = priced.at_expiry.map((p, i) => ({
|
||||
underlying: p.underlying,
|
||||
expiry: p.pnl,
|
||||
scenario: priced.at_scenario[i]?.pnl,
|
||||
}))
|
||||
// Decimals scale with the underlying's own magnitude — an equity at ~740 reads fine
|
||||
// rounded to the unit, but an FX rate at ~1.15 needs several decimals or every tick
|
||||
// collapses to the same rounded label.
|
||||
const decimals = spot < 5 ? 4 : spot < 50 ? 2 : 0
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
|
||||
<XAxis dataKey="underlying" type="number" domain={['dataMin', 'dataMax']}
|
||||
tick={{ fill: '#475569', fontSize: 10 }} tickLine={false}
|
||||
tickFormatter={(v) => v.toFixed(decimals)} />
|
||||
<YAxis tick={{ fill: '#475569', fontSize: 10 }} tickLine={false} axisLine={false}
|
||||
tickFormatter={(v) => `${v}`} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||
labelFormatter={(v) => `Sous-jacent: ${Number(v).toFixed(decimals)}`}
|
||||
formatter={(v: number, name: string) => [fmtMoney(v), name]}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
|
||||
<ReferenceLine x={spot} stroke="#3b82f6" strokeDasharray="2 2" label={{ value: 'Spot', fill: '#3b82f6', fontSize: 9, position: 'top' }} />
|
||||
<ReferenceLine x={scenarioSpot} stroke="#f59e0b" strokeDasharray="2 2" label={{ value: `Scénario J+${horizonDays}`, fill: '#f59e0b', fontSize: 9, position: 'insideTopRight' }} />
|
||||
<Line type="monotone" dataKey="expiry" name="À échéance jambe proche" stroke="#3b82f6" strokeWidth={2} dot={false} />
|
||||
<Line type="monotone" dataKey="scenario" name={`À J+${horizonDays} (scénario)`} stroke="#f59e0b" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// "Paliers T+N" view — same payoff curve at several elapsed-day checkpoints between today
|
||||
// and the nearest leg's expiry (services.strategy_engine.time_decay_slices), so the shape
|
||||
// morphing from today's time-value-laden curve into the kinked expiry payoff is visible
|
||||
// directly, instead of only the two PayoffChart endpoints.
|
||||
const TIME_SLICE_COLORS = ['#a78bfa', '#60a5fa', '#38bdf8', '#f59e0b', '#fb923c', '#f87171']
|
||||
|
||||
function TimeDecayChart({ timeSlices, spot, scenarioSpot }: { timeSlices: TimeSlice[]; spot: number; scenarioSpot: number }) {
|
||||
const decimals = spot < 5 ? 4 : spot < 50 ? 2 : 0
|
||||
const data = (timeSlices[0]?.points ?? []).map((pt, i) => {
|
||||
const row: Record<string, number> = { underlying: pt.underlying }
|
||||
timeSlices.forEach((s, si) => { row[`slice_${si}`] = s.points[i]?.pnl })
|
||||
return row
|
||||
})
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
|
||||
<XAxis dataKey="underlying" type="number" domain={['dataMin', 'dataMax']}
|
||||
tick={{ fill: '#475569', fontSize: 10 }} tickLine={false}
|
||||
tickFormatter={(v) => v.toFixed(decimals)} />
|
||||
<YAxis tick={{ fill: '#475569', fontSize: 10 }} tickLine={false} axisLine={false}
|
||||
tickFormatter={(v) => `${v}`} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||
labelFormatter={(v) => `Sous-jacent: ${Number(v).toFixed(decimals)}`}
|
||||
formatter={(v: number, name: string) => [fmtMoney(v), name]}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
|
||||
<ReferenceLine x={spot} stroke="#3b82f6" strokeDasharray="2 2" label={{ value: 'Spot', fill: '#3b82f6', fontSize: 9, position: 'top' }} />
|
||||
<ReferenceLine x={scenarioSpot} stroke="#94a3b8" strokeDasharray="2 2" label={{ value: 'Scénario', fill: '#94a3b8', fontSize: 9, position: 'insideTopRight' }} />
|
||||
{timeSlices.map((s, si) => (
|
||||
<Line key={si} type="monotone" dataKey={`slice_${si}`} name={s.label}
|
||||
stroke={TIME_SLICE_COLORS[si % TIME_SLICE_COLORS.length]} strokeWidth={2} dot={false} />
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// "Heatmap" view — same payoff grid (services.strategy_engine.payoff_heatmap) as a
|
||||
// price x days-to-expiry table instead of curves. Cell shade encodes sign + magnitude
|
||||
// relative to the grid's own max |P&L|, scaled independently each time (not a fixed
|
||||
// P&L->color scale) since strategies span wildly different notional sizes. The backend
|
||||
// already centers/scales the price columns on spot and the legs' own strikes and pins in
|
||||
// ── Payoff heatmap ──────────────────────────────────────────────────────────────
|
||||
// Sole payoff view — price x days-to-expiry grid (services.strategy_engine.payoff_heatmap).
|
||||
// A metric selector switches which grid is shown (P&L or any first-order Greek), so the
|
||||
// same heatmap explains not just where the position wins/loses but how Delta/Gamma/
|
||||
// Theta/Vega/Rho actually evolve across price and time, not just their two endpoint
|
||||
// values (now vs scenario) the tiles below already show. Cell shade encodes sign +
|
||||
// magnitude relative to the grid's own max |value| for whichever metric is selected
|
||||
// (not a fixed scale) since both P&L and each Greek span wildly different ranges. The
|
||||
// backend centers/scales the price columns on spot and the legs' own strikes and pins in
|
||||
// the exact breakeven(s) — this view shows a 9-wide window over that wider grid (arrows
|
||||
// to pan) and highlights the breakeven column(s) instead of leaving them to blend in.
|
||||
const HEATMAP_METRICS: { value: PayoffHeatmapMetric; label: string }[] = [
|
||||
{ value: 'pnl', label: 'P&L' },
|
||||
{ value: 'delta', label: 'Delta' },
|
||||
{ value: 'gamma', label: 'Gamma' },
|
||||
{ value: 'theta', label: 'Theta' },
|
||||
{ value: 'vega', label: 'Vega' },
|
||||
{ value: 'rho', label: 'Rho' },
|
||||
]
|
||||
const HEATMAP_WINDOW = 9
|
||||
|
||||
function PayoffHeatmapView({ heatmap, spot }: { heatmap: PayoffHeatmap; spot: number }) {
|
||||
function fmtHeatmapCell(v: number, metric: PayoffHeatmapMetric) {
|
||||
return metric === 'pnl' ? fmtMoney(v) : `${v >= 0 ? '+' : ''}${v.toFixed(4)}`
|
||||
}
|
||||
|
||||
function PayoffHeatmapView({ heatmap, spot, metric }: { heatmap: PayoffHeatmap; spot: number; metric: PayoffHeatmapMetric }) {
|
||||
const decimals = spot < 5 ? 4 : spot < 50 ? 2 : 0
|
||||
const maxAbs = Math.max(1, ...heatmap.rows.flatMap(r => r.pnl.map(Math.abs)))
|
||||
const cellBg = (pnl: number) => {
|
||||
const alpha = 0.12 + Math.min(1, Math.abs(pnl) / maxAbs) * 0.55
|
||||
return pnl >= 0 ? `rgba(16,185,129,${alpha})` : `rgba(239,68,68,${alpha})`
|
||||
const maxAbs = Math.max(1e-9, ...heatmap.rows.flatMap(r => r[metric].map(Math.abs)))
|
||||
const cellBg = (v: number) => {
|
||||
const alpha = 0.12 + Math.min(1, Math.abs(v) / maxAbs) * 0.55
|
||||
return v >= 0 ? `rgba(16,185,129,${alpha})` : `rgba(239,68,68,${alpha})`
|
||||
}
|
||||
const rowLabel = (daysFromNow: number, i: number) =>
|
||||
daysFromNow < 0.5 ? "Aujourd'hui" : i === heatmap.rows.length - 1 ? 'Échéance' : `J+${Math.round(daysFromNow)}`
|
||||
@@ -186,9 +125,9 @@ function PayoffHeatmapView({ heatmap, spot }: { heatmap: PayoffHeatmap; spot: nu
|
||||
{visible.map(i => (
|
||||
<td key={i}
|
||||
className={clsx('py-1 px-2 text-right font-mono text-white', isBreakeven(heatmap.prices[i]) && 'border-x border-amber-500/60')}
|
||||
style={{ backgroundColor: cellBg(row.pnl[i]) }}
|
||||
style={{ backgroundColor: cellBg(row[metric][i]) }}
|
||||
>
|
||||
{fmtMoney(row.pnl[i])}
|
||||
{fmtHeatmapCell(row[metric][i], metric)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -1116,7 +1055,7 @@ export default function StrategyBuilder() {
|
||||
const [symbol, setSymbol] = useState('')
|
||||
const [debouncedSymbol, setDebouncedSymbol] = useState('')
|
||||
const [horizonDays, setHorizonDays] = useState(8)
|
||||
const [payoffView, setPayoffView] = useState<'curve' | 'slices' | 'heatmap'>('curve')
|
||||
const [heatmapMetric, setHeatmapMetric] = useState<PayoffHeatmapMetric>('pnl')
|
||||
const [scenario, setScenario] = useState<StrategyScenario>({
|
||||
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: [],
|
||||
@@ -1580,43 +1519,21 @@ export default function StrategyBuilder() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="stat-label mb-0">Diagramme payoff</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{([['curve', 'Courbe'], ['slices', 'Paliers T+N'], ['heatmap', 'Heatmap']] as const).map(([v, label]) => (
|
||||
<button key={v} onClick={() => setPayoffView(v)}
|
||||
className={clsx('px-2.5 py-1 rounded text-[11px] font-semibold transition-colors',
|
||||
payoffView === v ? 'bg-blue-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-slate-200 border border-slate-700/50')}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<select
|
||||
value={heatmapMetric}
|
||||
onChange={(e) => setHeatmapMetric(e.target.value as PayoffHeatmapMetric)}
|
||||
className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1 text-[11px] font-semibold text-slate-200"
|
||||
>
|
||||
{HEATMAP_METRICS.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{payoffView === 'curve' && (
|
||||
<>
|
||||
<PayoffChart priced={priced} spot={priced.spot} scenarioSpot={priced.scenario_spot} horizonDays={scenario.horizon_days} />
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
{mode === 'historical'
|
||||
? <>Les deux courbes utilisent la vraie smile de volatilité capturée au jour scruté — seule la date diffère : bleu = à l'échéance de la jambe la plus proche, orange = au jour scruté (J+{scenario.horizon_days}).</>
|
||||
: <>Les deux courbes utilisent la même vue de volatilité (celle du scénario) — seule la date diffère : bleu = à l'échéance de la jambe la plus proche, orange = à J+{scenario.horizon_days}.</>}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{payoffView === 'slices' && (
|
||||
<>
|
||||
<TimeDecayChart timeSlices={priced.time_slices} spot={priced.spot} scenarioSpot={priced.scenario_spot} />
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
{mode === 'historical' ? 'Vraie smile capturée au jour scruté' : 'Même vue de volatilité (celle du scénario)'} pour chaque palier — seule la date change, du jour même à l'échéance de la jambe la plus proche : ne montre que l'effet de la valeur temps (theta), pas un changement de vue de vol.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{payoffView === 'heatmap' && (
|
||||
<>
|
||||
<PayoffHeatmapView heatmap={priced.heatmap} spot={priced.spot} />
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
{mode === 'historical' ? 'Vraie smile capturée au jour scruté' : 'Même vue de volatilité (celle du scénario)'} sur toute la grille — vert/rouge = gain/perte, l'intensité est relative au P&L max de cette grille.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<PayoffHeatmapView heatmap={priced.heatmap} spot={priced.spot} metric={heatmapMetric} />
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
{mode === 'historical' ? 'Vraie smile capturée au jour scruté' : 'Même vue de volatilité (celle du scénario)'} sur toute la grille
|
||||
{heatmapMetric === 'pnl'
|
||||
? <> — vert/rouge = gain/perte, l'intensité est relative au P&L max de cette grille.</>
|
||||
: <> — vert/rouge = valeur positive/négative de {HEATMAP_METRICS.find(m => m.value === heatmapMetric)?.label}, l'intensité est relative au max |valeur| de cette grille.</>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user