/* engine.js — il motore di accumulo, ri-implementato per il browser. * * Deve dare LA STESSA RISPOSTA di scripts/research/r0807_growth_yearly.py: stessa costruzione * del block bootstrap, stessa cadenza dei versamenti (ogni 30 giorni), stessa contabilita' * fiscale (imposta annua sulla variazione di valore al netto dei versamenti, minusvalenze in * carry 4 anni, patrimoniale sul valore). Il seme non e' confrontabile fra i due linguaggi: * l'accordo si verifica sui RISULTATI, entro il rumore Monte Carlo (test_engine.js). */ function makeEngine(CFG) { const R = CFG.r, NR = R.length, BLOCK = CFG.block, CARRY = CFG.carry; /* PRNG seminato (mulberry32): serve la riproducibilita', non la qualita' crittografica */ function rng(seed) { let a = seed >>> 0; return function () { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /* Un accumulo completo. Ritorna, per ogni anno, la distribuzione del capitale fra i path. * opts: { lumpEur, depEur, years, tax, patrimoniale, paths, seed } */ function run(o) { const nDays = o.years * 365; const rand = rng(o.seed || 807); const dep = o.depEur * CFG.eurusd; const cap0 = CFG.start_usd + o.lumpEur * CFG.eurusd; const nPaths = o.paths; /* capitale a fine di ogni anno, per path */ const snap = []; for (let y = 0; y < o.years; y++) snap.push(new Float64Array(nPaths)); const hitBy = new Int32Array(o.years); /* quanti path hanno toccato il bersaglio entro l'anno y */ const versatoY = new Float64Array(o.years); /* deterministico, ma CONTATO come in Python */ const carry = new Float64Array(CARRY); for (let p = 0; p < nPaths; p++) { let cap = cap0, annoStart = cap0, versatoAnno = 0, hitYear = -1, versato = cap0; carry.fill(0); let blockPos = BLOCK, base = 0; for (let t = 0; t < nDays; t++) { if (blockPos === BLOCK) { /* nuovo blocco: stessa costruzione di numpy */ base = Math.floor(rand() * (NR - BLOCK)); blockPos = 0; } cap *= 1 + R[base + blockPos]; blockPos++; if (hitYear < 0 && cap >= CFG.target) hitYear = Math.floor(t / 365); if (t % 30 === 0 && t > 0 && dep > 0) { cap += dep; versato += dep; versatoAnno += dep; } if ((t + 1) % 365 === 0) { const plus = cap - annoStart - versatoAnno; if (o.tax > 0) { let disponibili = 0; for (let k = 0; k < CARRY; k++) disponibili += carry[k]; const positivo = plus > 0 ? plus : 0; const usate = Math.min(positivo, disponibili); let resid = usate; for (let k = CARRY - 1; k >= 0; k--) { /* dalla piu' VECCHIA, che scade prima */ const presa = Math.min(carry[k], resid); carry[k] -= presa; resid -= presa; } const dovuta = (positivo - usate) * o.tax; cap -= Math.min(dovuta, cap); for (let k = CARRY - 1; k > 0; k--) carry[k] = carry[k - 1]; /* invecchia */ carry[0] = plus < 0 ? -plus : 0; } if (o.patrimoniale > 0) cap -= Math.min(cap * o.patrimoniale, cap); annoStart = cap; versatoAnno = 0; const yi = (t + 1) / 365 - 1; snap[yi][p] = cap; if (p === 0) versatoY[yi] = versato; } } if (hitYear >= 0) for (let y = hitYear; y < o.years; y++) hitBy[y]++; } const out = []; for (let y = 1; y <= o.years; y++) { const col = snap[y - 1].slice().sort(); const med = quantile(col, 0.5); out.push({ anno: y, versato: versatoY[y - 1], guadagno: med - versatoY[y - 1], p25: quantile(col, 0.25), med: med, p75: quantile(col, 0.75), phit: hitBy[y - 1] / nPaths }); } return out; } /* percentile con interpolazione lineare, come np.percentile (default 'linear') */ function quantile(sorted, f) { const n = sorted.length; if (n === 0) return 0; const pos = f * (n - 1), lo = Math.floor(pos), hi = Math.ceil(pos); return lo === hi ? sorted[lo] : sorted[lo] + (pos - lo) * (sorted[hi] - sorted[lo]); } /* Il versamento mensile che porta P(bersaglio entro `years`) alla confidenza voluta. * Bisezione, come dep_necessario() in r0727_tasse.py. Ritorna null se non basta nemmeno * il tetto: cosi' l'interfaccia dice "fuori portata" invece di stampare un numero enorme. */ function solveDeposit(o, pTarget, iters) { let lo = 0, hi = 20000; const P = d => run(Object.assign({}, o, { depEur: d })).slice(-1)[0].phit; if (P(hi) < pTarget) return null; for (let i = 0; i < (iters || 13); i++) { const mid = 0.5 * (lo + hi); if (P(mid) >= pTarget) hi = mid; else lo = mid; } return 0.5 * (lo + hi); } return { run, solveDeposit, quantile }; } if (typeof module !== "undefined") module.exports = { makeEngine };