/* ============================================================ Futures simulator — daily mark-to-market + margin calls ============================================================ */ const CONTRACTS = { 'WDO': { name: 'WDO · Mini-dólar', size: 10, im: 800, mm: 600, ticker: 'WDO', basePx: 5.10, vol: 0.12 }, 'WIN': { name: 'WIN · Mini-índice', size: 0.2, im: 200, mm: 150, ticker: 'WIN', basePx: 138000, vol: 0.18 }, 'BGI': { name: 'BGI · Boi gordo', size: 330, im: 4500, mm: 3500, ticker: 'BGI', basePx: 320, vol: 0.14 }, 'ICF': { name: 'ICF · Café arábica', size: 100, im: 6800, mm: 5200, ticker: 'ICF', basePx: 1482, vol: 0.22 }, 'CCM': { name: 'CCM · Milho', size: 450, im: 1200, mm: 900, ticker: 'CCM', basePx: 72, vol: 0.16 }, }; function CalcFuturosPage() { const [contractKey, setContractKey] = useState('WDO'); const [side, setSide] = useState('long'); const [qty, setQty] = useState(5); const [days, setDays] = useState(30); const [drift, setDrift] = useState(0); const [vol, setVol] = useState(12); const [seed, setSeed] = useState(42); const [scenario, setScenario] = useState('random'); const contract = CONTRACTS[contractKey]; const result = useMemo(() => { let priceSeries; if (scenario === 'random') { priceSeries = IBDMath.gbmSeries(contract.basePx, drift / 100, vol / 100, days, seed); } else if (scenario === 'up') { priceSeries = Array.from({length: days + 1}, (_, i) => contract.basePx * (1 + 0.001 * i + (i === 0 ? 0 : Math.sin(i * 0.5) * 0.003))); } else if (scenario === 'down') { priceSeries = Array.from({length: days + 1}, (_, i) => contract.basePx * (1 - 0.0015 * i + (i === 0 ? 0 : Math.sin(i * 0.5) * 0.003))); } else if (scenario === 'crash') { priceSeries = Array.from({length: days + 1}, (_, i) => { const crashDay = Math.floor(days * 0.4); if (i < crashDay) return contract.basePx * (1 + Math.sin(i * 0.4) * 0.005); const drop = Math.min((i - crashDay) * 0.012, 0.18); return contract.basePx * (1 - drop + Math.sin(i * 0.4) * 0.003); }); } return IBDMath.simulateFutures({ side, qty: +qty, entryPrice: contract.basePx, contractSize: contract.size, initialMargin: contract.im, maintenanceMargin: contract.mm, priceSeries }); }, [contractKey, side, qty, days, drift, vol, seed, scenario]); return (
Resultado · {contract.name} · {qty} {qty > 1 ? 'contratos' : 'contrato'} · {side === 'long' ? 'comprado' : 'vendido'} · {days} dias
= 0 ? 'pos' : 'neg'} /> = 0 ? 'pos' : 'neg'} /> 0 ? 'neg' : null} />

Preço, P&L acumulado e patrimônio em margem

D+0 → D+{days}

Fluxo financeiro diário · D+0 a D+{Math.min(days, 14)}

{days > 14 && Mostrando primeiros 15 dias}
{result.rows.slice(0, 15).map(r => ( ))}
Dia Preço Ajuste diário P&L acumulado Patrimônio em margem Chamada
D+{r.day} {r.price.toLocaleString('pt-BR', {maximumFractionDigits: 3})} = 0 ? 'var(--success)' : 'var(--danger)'}}> {r.variation > 0 ? '+' : ''}{IBDMath.fmtBRL(r.variation)} = 0 ? 'var(--success)' : 'var(--danger)'}}> {IBDMath.fmtBRL(r.cumPnL)} {IBDMath.fmtBRL(r.equity)} 0 ? 'var(--danger)' : 'var(--fg-subtle)'}}> {r.marginCall > 0 ? IBDMath.fmtBRL(r.marginCall) : '—'}
Metodologia. O ajuste diário é calculado como variação de preço × tamanho do contrato × quantidade × sinal da posição. Quando o patrimônio em margem cai abaixo da mantenedora, simula-se chamada de margem para reposição até a margem inicial. Preços aleatórios seguem GBM com base 252 dias úteis/ano.
); } function FuturesChart({ rows, contract }) { const w = 800, h = 320, pad = { l: 60, r: 60, t: 20, b: 36 }; const iw = w - pad.l - pad.r, ih = h - pad.t - pad.b; const prices = rows.map(r => r.price); const pnls = rows.map(r => r.cumPnL); const equities = rows.map(r => r.equity); const pMin = Math.min(...prices) * 0.99; const pMax = Math.max(...prices) * 1.01; const yMin = Math.min(0, Math.min(...pnls)); const yMax = Math.max(0, Math.max(...pnls), Math.max(...equities)); const yMinAll = Math.min(yMin, 0); const x = i => pad.l + (i / (rows.length - 1)) * iw; const yP = v => pad.t + ih - ((v - pMin) / (pMax - pMin)) * ih; const yL = v => pad.t + ih - ((v - yMinAll) / (yMax - yMinAll)) * ih; const pricePath = rows.map((r, i) => (i === 0 ? 'M' : 'L') + x(i).toFixed(2) + ',' + yP(r.price).toFixed(2)).join(' '); const pnlPath = rows.map((r, i) => (i === 0 ? 'M' : 'L') + x(i).toFixed(2) + ',' + yL(r.cumPnL).toFixed(2)).join(' '); const eqPath = rows.map((r, i) => (i === 0 ? 'M' : 'L') + x(i).toFixed(2) + ',' + yL(r.equity).toFixed(2)).join(' '); const zeroY = yL(0); const callDays = rows.filter(r => r.marginCall > 0); return (
{/* grid */} {Array.from({length: 6}, (_, i) => { const yy = pad.t + (ih / 5) * i; return ; })} {/* day labels */} {Array.from({length: 6}, (_, i) => { const dayIdx = Math.round((rows.length - 1) * (i / 5)); const xx = x(dayIdx); return ( D+{dayIdx} ); })} {/* zero line for P&L */} {/* margin call markers */} {callDays.map(r => ( ))} {/* price (right axis) */} {/* equity */} {/* P&L (main) */} {/* left axis (P&L) */} {Array.from({length: 6}, (_, i) => { const yy = pad.t + (ih / 5) * i; const v = yMax - ((yMax - yMinAll) / 5) * i; return {Math.round(v).toLocaleString('pt-BR')}; })} {/* right axis (price) */} {Array.from({length: 6}, (_, i) => { const yy = pad.t + (ih / 5) * i; const v = pMax - ((pMax - pMin) / 5) * i; return {v.toFixed(2)}; })} {/* axis titles */} DIAS DE PREGÃO P&L · R$ PREÇO · {contract.ticker}
P&L acumulado Patrimônio em margem Preço do contrato Chamada de margem
); } window.CalcFuturosPage = CalcFuturosPage;