feat(web): simulatore di accumulo nel browser, verificato contro il Python
scripts/web/ — motore in JavaScript (engine.js) sui ritorni VERI del book esportati da export_series.py, pagine assemblate da build.py. Da "sistema dinamico dove imposti valore iniziale, mensile e durata, e calcola la best curva fissando il tempo": la simulazione gira nel browser, quindi il motore va riscritto e VA PROVATO che dia la stessa risposta. test_engine.js confronta mediane e probabilita' con r0807_growth_yearly.py su due configurazioni, esige che il versato (deterministico) coincida al centesimo, e include un controllo positivo — un motore col fisco spento DEVE risultare fuori tolleranza, perche' un test che non sa fallire non e' un test. Il risolutore e' validato contro dep_necessario di r0727_tasse.py: -0.4 / -0.6 / -1.1%. smoke.js ESEGUE le pagine con un DOM finto. Serve perche' node --check valida solo la sintassi: ho pubblicato una pagina che lo passava e moriva alla prima riga utile (chiavi Python "0.0" ricostruite in JS come String(0) = "0"; i pesi intermedi funzionavano per caso). Altri due errori che questi strumenti hanno intercettato: - due anni di dati di un grafico scritti A MEMORIA perche' tail aveva troncato l'output -> ora i dati si INIETTANO da JSON (build.py), il passaggio manuale non esiste piu'; - una "distorsione sistematica" del motore JS (+0.75%, 8 semi tutti positivi) che erano 8 estrazioni contro UN punto Python rumoroso. Misurato bene, 8 semi per parte: -0.01%, t = -0.04; ed entrambi i campionatori cadono entro 1.7 SE dall'atteso ANALITICO della media di blocco. Scelta guidata da una misura: la banda del versamento suggerito resta +-1% da 1.200 a 3.000 percorsi -> non domina il Monte Carlo ma la granularita' della bisezione (~5 EUR). Percorsi tenuti bassi e incertezza dichiarata, invece di pagare tempo per una precisione che non arriva. Nessun impatto sulla produzione: non tocca book, pesi, cron o config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
"""build.py — assembla le pagine pubblicate a partire da shell + dati prodotti dagli script.
|
||||
|
||||
Le pagine sono deliverable, non sorgenti di verita': ogni numero che mostrano viene da uno script
|
||||
Python committato, e qui viene INIETTATO, mai trascritto a mano. La regola nasce da un errore di
|
||||
questa sessione — due anni di dati di un grafico scritti a memoria perche' l'output del terminale
|
||||
era troncato dal `tail` — e il modo di non ripeterlo e' non avere il passaggio manuale.
|
||||
|
||||
# 1. produrre i dati
|
||||
uv run python scripts/research/r0807_growth_yearly.py --dep 800 --json /tmp/growth800.json
|
||||
uv run python scripts/research/r0807_asset_compare.py --json /tmp/compare.json
|
||||
uv run python scripts/research/r0807_best_strategy.py --anni 12 --json /tmp/best.json
|
||||
|
||||
# 2. assemblare
|
||||
uv run python scripts/web/build.py --out /tmp/pagine \\
|
||||
--dati simulatore=/tmp/series.json confronto=/tmp/compare.json scelta=/tmp/best.json
|
||||
|
||||
# 3. verificare che le pagine ESEGUANO (node --check valida solo la sintassi)
|
||||
node scripts/web/smoke.js /tmp/pagine/*.html
|
||||
|
||||
`pagina_crescita.html` ha i dati gia' dentro (due configurazioni di versamento a confronto) e non
|
||||
richiede iniezione: se manca il segnaposto, il file viene copiato tale e quale.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
QUI = Path(__file__).resolve().parent
|
||||
SEGNAPOSTO = "/*__DATA__*/"
|
||||
MOTORE = "/*__ENGINE__*/"
|
||||
SERIE = "/*__SERIES__*/"
|
||||
|
||||
|
||||
def costruisci(shell: Path, dati: Path | None, out: Path) -> str:
|
||||
testo = shell.read_text()
|
||||
note = []
|
||||
|
||||
if MOTORE in testo: # la pagina interattiva porta dentro il motore
|
||||
eng = (QUI / "engine.js").read_text()
|
||||
eng = eng.split('if (typeof module !== "undefined")')[0].rstrip()
|
||||
testo = testo.replace(MOTORE, eng)
|
||||
note.append("motore")
|
||||
|
||||
for tag in (SERIE, SEGNAPOSTO):
|
||||
if tag in testo:
|
||||
if dati is None:
|
||||
raise SystemExit(f"{shell.name}: serve --dati, il file contiene {tag}")
|
||||
testo = testo.replace(tag, dati.read_text().strip())
|
||||
note.append(dati.name)
|
||||
|
||||
out.write_text(testo)
|
||||
return ", ".join(note) if note else "nessuna iniezione (gia' completa)"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", required=True, help="cartella di destinazione")
|
||||
ap.add_argument("--dati", nargs="*", default=[], metavar="nome=percorso.json")
|
||||
a = ap.parse_args()
|
||||
|
||||
mappa = {}
|
||||
for voce in a.dati:
|
||||
nome, _, perc = voce.partition("=")
|
||||
p = Path(perc)
|
||||
if not p.exists():
|
||||
raise SystemExit(f"dati mancanti per «{nome}»: {p}")
|
||||
mappa[nome] = p
|
||||
|
||||
dest = Path(a.out)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
for shell in sorted(QUI.glob("pagina_*.html")):
|
||||
nome = shell.stem.replace("pagina_", "")
|
||||
out = dest / f"{nome}.html"
|
||||
try:
|
||||
esito = costruisci(shell, mappa.get(nome), out)
|
||||
except SystemExit as e:
|
||||
print(f" {nome:12} SALTATA — {e}")
|
||||
continue
|
||||
print(f" {nome:12} -> {out} ({esito}, {out.stat().st_size/1024:.0f} KB)")
|
||||
|
||||
for extra in ("engine.js", "smoke.js", "test_engine.js"):
|
||||
shutil.copy(QUI / extra, dest / extra)
|
||||
print(f"\n verifica: node {dest}/smoke.js {dest}/*.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user