feat: strategy builder
This commit is contained in:
@@ -285,15 +285,30 @@ def payoff_curves(
|
|||||||
entry_ref = priced["entry_cost"]
|
entry_ref = priced["entry_cost"]
|
||||||
|
|
||||||
lo, hi = spot * 0.6, spot * 1.4
|
lo, hi = spot * 0.6, spot * 1.4
|
||||||
prices = np.linspace(lo, hi, n)
|
# 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.
|
||||||
|
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)
|
||||||
|
prices = np.unique(np.concatenate([baseline, near_strikes]))
|
||||||
|
prices.sort()
|
||||||
eval_days_expiry = min(l["days_to_expiry"] for l in legs)
|
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 = [
|
at_expiry = [
|
||||||
{"underlying": round(float(p), 2), "pnl": round(float(value_at(legs, float(p), eval_days_expiry, surface_now, r, contract_size) - entry_ref), 2)}
|
{"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
|
for p in prices
|
||||||
]
|
]
|
||||||
at_scenario = [
|
at_scenario = [
|
||||||
{"underlying": round(float(p), 2), "pnl": round(float(value_at(legs, float(p), horizon_days, surface_scenario, r, contract_size) - entry_ref), 2)}
|
{"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
|
for p in prices
|
||||||
]
|
]
|
||||||
return {"at_expiry": at_expiry, "at_scenario": at_scenario, **priced}
|
return {"at_expiry": at_expiry, "at_scenario": at_scenario, **priced}
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ function PayoffChart({ priced, spot, scenarioSpot }: { priced: PriceCombo; spot:
|
|||||||
tickFormatter={(v) => `${v}`} />
|
tickFormatter={(v) => `${v}`} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||||
labelFormatter={(v) => `Sous-jacent: ${Number(v).toFixed(2)}`}
|
labelFormatter={(v) => `Sous-jacent: ${Number(v).toFixed(decimals)}`}
|
||||||
formatter={(v: number, name: string) => [fmtMoney(v), name]}
|
formatter={(v: number, name: string) => [fmtMoney(v), name]}
|
||||||
/>
|
/>
|
||||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
|
<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={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+8', fill: '#f59e0b', fontSize: 9, position: 'insideTopRight' }} />
|
<ReferenceLine x={scenarioSpot} stroke="#f59e0b" strokeDasharray="2 2" label={{ value: 'Scénario J+8', fill: '#f59e0b', fontSize: 9, position: 'insideTopRight' }} />
|
||||||
<Line type="monotone" dataKey="expiry" name="À échéance" stroke="#3b82f6" strokeWidth={2} dot={false} />
|
<Line type="monotone" dataKey="expiry" name="À échéance jambe proche" stroke="#3b82f6" strokeWidth={2} dot={false} />
|
||||||
<Line type="monotone" dataKey="scenario" name="À J+8 (scénario)" stroke="#f59e0b" strokeWidth={2} dot={false} />
|
<Line type="monotone" dataKey="scenario" name="À J+8 (scénario)" stroke="#f59e0b" strokeWidth={2} dot={false} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
@@ -818,6 +818,9 @@ export default function StrategyBuilder() {
|
|||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="stat-label mb-2">Diagramme payoff</div>
|
<div className="stat-label mb-2">Diagramme payoff</div>
|
||||||
<PayoffChart priced={priced} spot={priced.spot} scenarioSpot={priced.scenario_spot} />
|
<PayoffChart priced={priced} spot={priced.spot} scenarioSpot={priced.scenario_spot} />
|
||||||
|
<p className="text-[11px] text-slate-500 mt-1">
|
||||||
|
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+{horizonDays}.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user