/* test_engine.js — il motore JS deve riprodurre r0807_growth_yearly.py. * * Non si confrontano i semi (non sono confrontabili fra linguaggi): si confrontano i RISULTATI * entro il rumore Monte Carlo, su due configurazioni gia' calcolate in Python, e si controlla * che le identita' deterministiche (il versato) coincidano ESATTAMENTE — quelle non hanno rumore. * * ⚠️ TRAPPOLA, in cui sono caduto scrivendo questo test: il riferimento Python e' UNA estrazione * (seme 807), non un valore vero. Confrontandoci 8 estrazioni JS ho visto "8/8 sopra il * riferimento, media +0.75%" e l'ho letto come distorsione sistematica — ma 8 segni concordi * rispetto a UN punto rumoroso non sono 8 osservazioni indipendenti: dicono solo che quel punto * sta in basso. Misurato per bene (8 semi PER LINGUAGGIO, distribuzione contro distribuzione): * differenza delle medie -0.01%, t = -0.04, e i due campionatori cadono entrambi entro 1.7 SE * dall'atteso ANALITICO della media di blocco. Le tolleranze qui sotto sono percio' tarate sul * rumore di UN confronto punto-a-punto (~1 SE = 0.7%), non su una distorsione che non esiste. */ const fs = require("fs"); const { makeEngine } = require("./engine.js"); const CFG = JSON.parse(fs.readFileSync("series.json", "utf8")); const E = makeEngine(CFG); /* valori di riferimento prodotti da Python (4.000 path, seme 807) */ const REF = { "500": { dep: 500, py: JSON.parse(fs.readFileSync("growth.json", "utf8")) }, "800": { dep: 800, py: JSON.parse(fs.readFileSync("growth800.json", "utf8")) } }; const PATHS = 4000; let fail = 0; const pct = x => (100 * x).toFixed(1) + "%"; for (const nome of Object.keys(REF)) { const { dep, py } = REF[nome]; for (const regime of ["lordo", "netto"]) { const js = E.run({ lumpEur: 5000, depEur: dep, years: 15, tax: regime === "netto" ? CFG.tax : 0, patrimoniale: regime === "netto" ? CFG.patrimoniale : 0, paths: PATHS, seed: 20260807 }); console.log(`\n--- €${dep}/mese · ${regime} ---`); console.log("anno versato PY versato JS med PY med JS scarto P PY P JS"); for (const y of [1, 5, 10, 15]) { const a = py[regime][y - 1], b = js[y - 1]; const capPy = a.versato + a.guadagno_med; const d = capPy > 0 ? b.med / capPy - 1 : 0; /* il versato e' deterministico: deve coincidere al centesimo */ const vOk = Math.abs(a.versato - b.versato) < 0.01; /* le mediane hanno rumore Monte Carlo: tolleranza 3% */ const mOk = Math.abs(d) < 0.03; /* le probabilita' hanno SE <= 0.8pp a 4.000 path: tolleranza 3pp */ const pOk = Math.abs(a.p_traguardo - b.phit) < 0.03; if (!vOk || !mOk || !pOk) fail++; console.log( String(y).padStart(4) + ("$" + Math.round(a.versato).toLocaleString("en-US")).padStart(13) + ("$" + Math.round(b.versato).toLocaleString("en-US")).padStart(14) + ("$" + Math.round(capPy).toLocaleString("en-US")).padStart(12) + ("$" + Math.round(b.med).toLocaleString("en-US")).padStart(12) + (d >= 0 ? "+" : "") + (100 * d).toFixed(1).padStart(9) + "%" + pct(a.p_traguardo).padStart(8) + pct(b.phit).padStart(8) + (vOk && mOk && pOk ? "" : " <-- FUORI TOLLERANZA")); } } } /* controllo positivo: il test deve saper fallire. Un motore con l'imposta spenta quando dovrebbe essere accesa deve risultare FUORI tolleranza rispetto al riferimento netto. */ const rotto = E.run({ lumpEur: 5000, depEur: 500, years: 15, tax: 0, patrimoniale: 0, paths: 1500, seed: 1 }); const refNetto15 = REF["500"].py.netto[14]; const capRef = refNetto15.versato + refNetto15.guadagno_med; const scartoRotto = rotto[14].med / capRef - 1; console.log("\ncontrollo positivo (motore volutamente sbagliato: fisco spento): scarto " + (100 * scartoRotto).toFixed(1) + "% -> " + (Math.abs(scartoRotto) > 0.03 ? "rilevato, il test sa fallire" : "NON RILEVATO: il test e' cieco")); if (Math.abs(scartoRotto) <= 0.03) fail++; /* velocita': l'interfaccia deve restare utilizzabile */ const t0 = Date.now(); E.run({ lumpEur: 5000, depEur: 500, years: 20, tax: CFG.tax, patrimoniale: CFG.patrimoniale, paths: 3000, seed: 5 }); console.log("\n20 anni x 3.000 path: " + (Date.now() - t0) + " ms"); console.log(fail === 0 ? "\nOK — il motore JS riproduce il Python" : "\n" + fail + " CONTROLLI FALLITI"); process.exit(fail === 0 ? 0 : 1);