/* ============================================================ Payoff diagram of multi-leg option strategies ============================================================ */ const PRESETS = { 'bull-call': { name: 'Bull Call Spread', legs: [ { side: 'long', type: 'call', strike: 60, premium: 5.50, qty: 1 }, { side: 'short', type: 'call', strike: 70, premium: 1.80, qty: 1 }, ], spot: 62 }, 'bear-put': { name: 'Bear Put Spread', legs: [ { side: 'long', type: 'put', strike: 70, premium: 5.20, qty: 1 }, { side: 'short', type: 'put', strike: 60, premium: 1.60, qty: 1 }, ], spot: 65 }, 'straddle': { name: 'Long Straddle', legs: [ { side: 'long', type: 'call', strike: 100, premium: 4.20, qty: 1 }, { side: 'long', type: 'put', strike: 100, premium: 3.80, qty: 1 }, ], spot: 100 }, 'iron-condor': { name: 'Iron Condor', legs: [ { side: 'long', type: 'put', strike: 80, premium: 0.60, qty: 1 }, { side: 'short', type: 'put', strike: 90, premium: 2.10, qty: 1 }, { side: 'short', type: 'call', strike: 110, premium: 2.40, qty: 1 }, { side: 'long', type: 'call', strike: 120, premium: 0.80, qty: 1 }, ], spot: 100 }, 'collar': { name: 'Collar (protetor)', legs: [ { side: 'long', type: 'put', strike: 95, premium: 2.40, qty: 1 }, { side: 'short', type: 'call', strike: 110, premium: 2.10, qty: 1 }, ], spot: 100 }, 'covered-call': { name: 'Covered Call', legs: [ { side: 'long', type: 'future', strike: 100, premium: 0, qty: 1 }, { side: 'short', type: 'call', strike: 105, premium: 3.20, qty: 1 }, ], spot: 100 } }; function CalcPayoffPage() { const [preset, setPreset] = useState('bull-call'); const [legs, setLegs] = useState(PRESETS['bull-call'].legs); const [spot, setSpot] = useState(PRESETS['bull-call'].spot); const loadPreset = (key) => { setPreset(key); setLegs(JSON.parse(JSON.stringify(PRESETS[key].legs))); setSpot(PRESETS[key].spot); }; const updateLeg = (i, patch) => setLegs(legs.map((l, j) => j === i ? { ...l, ...patch } : l)); const removeLeg = (i) => setLegs(legs.filter((_, j) => j !== i)); const addLeg = () => setLegs([...legs, { side: 'long', type: 'call', strike: +spot, premium: 1, qty: 1 }]); // Analysis const minStrike = Math.min(...legs.map(l => l.strike)); const maxStrike = Math.max(...legs.map(l => l.strike)); const sMin = Math.max(0, Math.min(minStrike, spot) * 0.7); const sMax = Math.max(maxStrike, spot) * 1.3; const curve = IBDMath.payoffCurve(legs, sMin, sMax, 240); const bes = IBDMath.findBreakevens(legs, sMin, sMax, 800); const currentPL = IBDMath.strategyPayoff(legs, +spot); const maxP = Math.max(...curve.map(p => p.p)); const minP = Math.min(...curve.map(p => p.p)); const netDebit = legs.reduce((acc, l) => { if (l.type === 'future') return acc; return acc + (l.side === 'long' ? l.premium : -l.premium) * l.qty; }, 0); return (
Resultado · {PRESETS[preset]?.name || 'Estratégia customizada'}
= 0 ? 'pos' : 'neg'} /> 1e6 ? 'Ilimitado' : IBDMath.fmtBRL(maxP)} sign="pos" /> = 0 ? 'Débito líquido' : 'Crédito líquido'} value={IBDMath.fmtBRL(Math.abs(netDebit))} />

Payoff no vencimento

Breakevens: {bes.length === 0 ? '—' : bes.map(b => b.toFixed(2)).join(' · ')}

Tabela de payoff por cenário

{[-0.20, -0.10, -0.05, 0, 0.05, 0.10, 0.20].map(d => { const ST = +spot * (1 + d); const p = IBDMath.strategyPayoff(legs, ST); return ( ); })}
Cenário Preço (ST) Variação vs. à vista P&L da estratégia Resultado
{d === 0 ? 'À vista' : (d > 0 ? '+' : '') + (d * 100).toFixed(0) + '%'} {ST.toFixed(2)} {(d * 100).toFixed(1)}% = 0 ? 'var(--success)' : 'var(--danger)', fontWeight: 500}}>{IBDMath.fmtBRL(p)} = 0 ? 'var(--success)' : 'var(--danger)', color: p >= 0 ? 'var(--success)' : 'var(--danger)'}}> {p > 0 ? 'LUCRO' : p < 0 ? 'PERDA' : 'NULO'}
); } function MiniField({ label, value, onChange, disabled }) { return (
{label}
onChange(e.target.value)} disabled={disabled} className="mono" style={{ width: '100%', padding: 8, border: '1px solid var(--rule-strong)', background: disabled ? 'var(--bg-sunken)' : 'var(--bg-elev)', color: disabled ? 'var(--fg-subtle)' : 'var(--fg)', fontSize: 13 }} />
); } function KV({ label, value, sign }) { return (
{label}
{value}
); } function PayoffChart({ curve, spot, breakevens, legs }) { const w = 800, h = 360, pad = { l: 56, r: 20, t: 20, b: 36 }; const iw = w - pad.l - pad.r, ih = h - pad.t - pad.b; const sMin = curve[0].s, sMax = curve[curve.length - 1].s; const yMin0 = Math.min(...curve.map(p => p.p)); const yMax0 = Math.max(...curve.map(p => p.p)); const yRange = Math.max(Math.abs(yMin0), Math.abs(yMax0)); const yMin = -yRange * 1.1, yMax = yRange * 1.1; const x = s => pad.l + ((s - sMin) / (sMax - sMin)) * iw; const y = v => pad.t + ih - ((v - yMin) / (yMax - yMin)) * ih; const y0 = y(0); // build path with area split (positive green, negative red) const path = curve.map((p, i) => (i === 0 ? 'M' : 'L') + x(p.s).toFixed(2) + ',' + y(p.p).toFixed(2)).join(' '); // areas const posArea = [ `M ${pad.l},${y0}`, ...curve.map(p => `L ${x(p.s).toFixed(2)},${y(Math.max(p.p, 0)).toFixed(2)}`), `L ${pad.l + iw},${y0}`, 'Z' ].join(' '); const negArea = [ `M ${pad.l},${y0}`, ...curve.map(p => `L ${x(p.s).toFixed(2)},${y(Math.min(p.p, 0)).toFixed(2)}`), `L ${pad.l + iw},${y0}`, 'Z' ].join(' '); const xTicks = 8, yTicks = 6; return (
{/* grid */} {Array.from({length: yTicks + 1}, (_, i) => { const yy = pad.t + (ih / yTicks) * i; const v = yMax - ((yMax - yMin) / yTicks) * i; return ( {v.toFixed(0)} ); })} {Array.from({length: xTicks + 1}, (_, i) => { const xx = pad.l + (iw / xTicks) * i; const v = sMin + ((sMax - sMin) / xTicks) * i; return ( {v.toFixed(0)} ); })} {/* zero line */} {/* areas */} {/* main curve */} {/* strikes */} {legs.map((l, i) => ( K{i+1} ))} {/* breakevens */} {breakevens.map((b, i) => ( BE {b.toFixed(2)} ))} {/* spot marker */} S₀ {spot.toFixed(2)} PREÇO NO VENCIMENTO (S_T) P&L (R$)
Payoff total S à vista / breakeven Lucro Perda
); } window.CalcPayoffPage = CalcPayoffPage;