/* ============================================================
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 (
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'}