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