Files
Adriano Dal Pastro ed2532b991 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>
2026-08-07 18:44:13 +00:00

394 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>Quale strategia — book, ETF o un mix</title>
<style>
:root {
color-scheme: light;
--ground: #f7f7f5; --surface: #fdfdfc; --sunken: #f1f1ee;
--rule: #dededa; --rule-soft: #ebebe7;
--ink: #16171a; --ink-2: #55575c; --ink-3: #83858b;
--s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --s4: #eda100; --grid: #e6e6e2;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--ground: #17181a; --surface: #1d1e21; --sunken: #232427;
--rule: #34363a; --rule-soft: #26282b;
--ink: #f1f1ee; --ink-2: #a8aab0; --ink-3: #7c7e85;
--s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --grid: #2b2d31;
}
}
:root[data-theme="dark"] {
color-scheme: dark;
--ground: #17181a; --surface: #1d1e21; --sunken: #232427;
--rule: #34363a; --rule-soft: #26282b;
--ink: #f1f1ee; --ink-2: #a8aab0; --ink-3: #7c7e85;
--s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --grid: #2b2d31;
}
*, *::before, *::after { box-sizing: border-box; }
body { background: var(--ground); color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
font-size: 17px; line-height: 1.62; margin: 0; padding: 0 24px 96px; -webkit-font-smoothing: antialiased; }
.wrap { max-width: 1080px; margin: 0 auto; display: flex; flex-direction: column; gap: 44px; }
.prose { max-width: 68ch; display: flex; flex-direction: column; gap: 16px; }
.mono, .eyebrow, .lede-meta, .legend, .axis, table, .tile {
font-family: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
font-variant-numeric: tabular-nums; }
header { padding-top: 60px; display: flex; flex-direction: column; gap: 18px; }
.eyebrow { font-size: 11.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-3); margin: 0; }
h1 { font-size: clamp(29px, 4.2vw, 44px); line-height: 1.12; margin: 0; font-weight: 400;
letter-spacing: -0.015em; text-wrap: balance; max-width: 21ch; }
.lede { font-size: 19px; color: var(--ink-2); margin: 0; max-width: 62ch; }
.lede b { color: var(--ink); font-weight: 700; }
.lede-meta { font-size: 12.5px; color: var(--ink-3); line-height: 1.9; margin: 0;
padding-top: 14px; border-top: 1px solid var(--rule); }
.lede-meta b { color: var(--ink-2); font-weight: 400; }
h2 { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
letter-spacing: 0.15em; text-transform: uppercase; color: var(--ink-2); font-weight: 500;
margin: 0 0 4px; padding-bottom: 10px; border-bottom: 1px solid var(--rule); }
p { margin: 0; }
em { font-style: italic; color: var(--ink-2); }
strong { font-weight: 700; }
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 1px;
background: var(--rule); border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
.tile { background: var(--surface); padding: 17px 18px 19px; display: flex; flex-direction: column; gap: 5px; }
.tile .k { font-size: 11px; letter-spacing: 0.13em; text-transform: uppercase; color: var(--ink-3); }
.tile .v { font-size: 26px; letter-spacing: -0.02em; color: var(--ink); line-height: 1.12; }
.tile .v small { font-size: 14px; color: var(--ink-3); letter-spacing: 0; }
.tile .s { font-size: 12px; color: var(--ink-2); line-height: 1.55; }
.tablewrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: 3px;
background: var(--surface); padding: 14px 16px; }
table.data { border-collapse: collapse; font-size: 12.5px; width: 100%; min-width: 620px; }
table.data th, table.data td { padding: 9px 14px 9px 0; text-align: right; white-space: nowrap; }
table.data th:first-child, table.data td:first-child { text-align: left; }
table.data thead th { color: var(--ink-3); font-weight: 400; border-bottom: 1px solid var(--rule);
font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; }
table.data tbody td { border-bottom: 1px solid var(--rule-soft); color: var(--ink-2); }
table.data tbody td:first-child { color: var(--ink); }
table.data tbody tr.best td { background: color-mix(in srgb, var(--s1) 9%, transparent); color: var(--ink); }
caption { caption-side: top; text-align: left; font-size: 12px; color: var(--ink-3); padding-bottom: 12px; }
.cell { display: inline-block; min-width: 46px; text-align: right; }
figure { margin: 0; display: flex; flex-direction: column; gap: 10px; }
.chart-title { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink); margin: 0; }
.chart-title span { color: var(--ink-3); text-transform: none; letter-spacing: 0; }
.chart-box { background: var(--surface); border: 1px solid var(--rule); border-radius: 3px;
padding: 18px 16px 10px; overflow-x: auto; }
.chart-box svg { display: block; width: 100%; min-width: 620px; height: auto; }
.legend { display: flex; gap: 20px; flex-wrap: wrap; font-size: 12px; color: var(--ink-2); align-items: center; }
.legend i { width: 20px; height: 3px; border-radius: 2px; display: inline-block; margin-right: 8px; vertical-align: 3px; }
.figcap { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
color: var(--ink-3); line-height: 1.7; max-width: 80ch; }
.caveat { border-left: 2px solid var(--s2); padding: 2px 0 2px 18px; color: var(--ink-2);
font-size: 16px; max-width: 64ch; }
.caveat b { color: var(--ink); font-weight: 700; }
#tip { position: fixed; z-index: 40; pointer-events: none; opacity: 0; transition: opacity .1s ease;
background: var(--surface); border: 1px solid var(--rule); border-radius: 3px; padding: 10px 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.75;
color: var(--ink-2); box-shadow: 0 6px 22px rgba(0,0,0,.14); font-variant-numeric: tabular-nums; max-width: 270px; }
#tip .t { color: var(--ink); letter-spacing: 0.08em; text-transform: uppercase; font-size: 10.5px; display: block; margin-bottom: 5px; }
#tip .r { display: flex; justify-content: space-between; gap: 18px; }
#tip .r b { font-weight: 400; color: var(--ink); }
#tip .sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; }
footer { border-top: 1px solid var(--rule); padding-top: 18px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
color: var(--ink-3); line-height: 1.85; max-width: 80ch; }
footer b { color: var(--ink-2); font-weight: 400; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
</style>
<div class="wrap">
<header>
<p class="eyebrow">PythagorasGoal · scelta di portafoglio · 2026-08-07</p>
<h1 id="h1">Quale strategia, a 12 anni</h1>
<p class="lede" id="lede"></p>
<p class="lede-meta" id="meta"></p>
</header>
<section class="prose">
<h2>1 · Perché la domanda non ha una risposta sola</h2>
<p id="p-intro"></p>
<p class="caveat" id="p-asim"></p>
</section>
<div class="tiles" id="tiles"></div>
<figure>
<p class="chart-title">Quota del proprio capitale-rendita raggiunta <span>— per peso sul book, in ciascuno scenario</span></p>
<div class="legend" id="leg"></div>
<div class="chart-box"><svg id="c1" role="img" aria-label="Quota del bersaglio raggiunta per peso sul book, quattro scenari"></svg></div>
<figcaption class="figcap" id="cap1"></figcaption>
</figure>
<section class="prose">
<h2>2 · La tabella della decisione</h2>
<p id="p-tab"></p>
</section>
<div class="tablewrap"><table class="data" id="t-dec"></table></div>
<section class="prose">
<h2>3 · Perché il mix non è un compromesso</h2>
<p id="p-corr"></p>
</section>
<div class="tablewrap"><table class="data" id="t-det"></table></div>
<section class="prose">
<h2>4 · La risposta</h2>
<p class="caveat" id="p-risp"></p>
<p id="p-limiti"></p>
</section>
<footer id="foot"></footer>
</div>
<div id="tip" role="status" aria-live="polite"></div>
<script id="best-data" type="application/json">/*__DATA__*/</script>
<script>
(function () {
"use strict";
const D = JSON.parse(document.getElementById("best-data").textContent);
const $ = id => document.getElementById(id);
const P = D.piano;
const SCEN = Object.keys(D.scenari);
const COL = ["var(--s1)", "var(--s3)", "var(--s2)", "var(--s4)"];
/* ⚠️ le chiavi dei pesi si LEGGONO dal JSON, non si ricostruiscono: Python le scrive come
"0.0"/"1.0", mentre String(P.pesi[0]) in JS darebbe "0" — mismatch silenzioso che rendeva
ogni lookup undefined. Vale per qualunque dizionario Python con chiavi numeriche. */
const W = Object.keys(D.scenari[SCEN[0]]).sort((a, b) => Number(a) - Number(b));
const kOf = w => W.find(k => Number(k) === Number(w));
const usd = n => "$" + Math.round(n).toLocaleString("it-IT");
const usdK = n => "$" + Math.round(n / 1000) + "k";
const pc = (x, d) => (100 * x).toFixed(d === undefined ? 0 : d).replace(".", ",") + "%";
const wlab = w => Number(w) === 0 ? "solo ETF" : (Number(w) === 1 ? "solo book" :
pc(Number(w)) + " book");
const S = (n, w) => D.scenari[n][kOf(w)];
/* ---------- intestazione ---------- */
$("h1").textContent = "Quale strategia, a " + P.anni + " anni";
$("lede").innerHTML =
"Con <b>€" + P.lump.toLocaleString("it-IT") + "</b> iniziali e <b>€" +
P.dep.toLocaleString("it-IT") + "/mese</b> per <b>" + P.anni + " anni</b>: tutto sul book, " +
"tutto su un ETF S&amp;P 500, o un mix? La risposta dipende da un'assunzione che " +
"<b>nessun dato può risolvere</b> — e allora si sceglie ciò che regge comunque.";
$("meta").innerHTML =
"finestra <b>" + D.finestra.da + " → " + D.finestra.a + "</b> (" + D.finestra.giorni +
" giorni): è l'unica dove un mix è definito · drift misurati: book <b>" + pc(D.drift.book, 1) +
"</b>, S&amp;P su questa finestra <b>" + pc(D.drift.eq_comune, 1) + "</b>, S&amp;P su 30 anni <b>" +
pc(D.drift.eq_30, 1) + "</b> · " + P.n_paths.toLocaleString("it-IT") + " percorsi · " +
"fisco cripto <b>" + pc(P.tax_crypto) + " annuo</b>, ETF <b>" + pc(P.tax_etf) + " differito</b>";
$("p-intro").innerHTML =
"Il book ha <b>" + (D.finestra.giorni / 365).toFixed(1) + " anni</b> di storia in tutto, " +
"l'indice ne ha trenta. Sugli stessi sette anni i due rendono quasi uguale (" +
pc(D.drift.book, 1) + " contro " + pc(D.drift.eq_comune, 1) + "); ma quei trent'anni " +
"contengono il 2000 e il 2008, e lì l'indice rende " + pc(D.drift.eq_30, 1) + ". " +
"Quindi la domanda vera non è «quale ha reso di più», è <em>quanto di ciò che ho misurato " +
"sopravvive ai prossimi dodici anni</em>. Non essendoci un dato che lo decida, si guarda la " +
"stessa scelta sotto quattro scenari e si tiene quella che non crolla in nessuno.";
$("p-asim").innerHTML =
"⚠️ <b>L'asimmetria fra i due scenari prudenti è essa stessa il risultato.</b> Quello " +
"sull'indice è <b>misurato</b> — trent'anni di storia esistono e dicono " + pc(D.drift.eq_30, 1) +
". Quello sul book è <b>giudiziale</b>: metà del drift, scelto a mano, perché " +
(D.finestra.giorni / 365).toFixed(1) + " anni sono tutta la storia che ha e non c'è nulla con " +
"cui stressarlo. Si può discutere la taglia di quel taglio; non lo si può togliere.";
/* ---------- tiles ---------- */
const peg = D.peggiori, rim = D.rimpianti;
const wMax = D.minimax, wReg = D.minregret;
const base = SCEN[0];
/* il criterio del caso peggiore puo' non distinguere due pesi: se lo scarto e' dentro il
rumore Monte Carlo (~1%) va detto, invece di far sembrare che uno vinca */
const ordPeg = W.slice().sort((a, b) => peg[b] - peg[a]);
const pareggio = Math.abs(peg[ordPeg[0]] - peg[ordPeg[1]]) < 0.01;
$("tiles").innerHTML = [
{ k: "massimizza il caso peggiore", v: pareggio ? wlab(ordPeg[0]) + " ≈ " + wlab(ordPeg[1]) : wlab(wMax),
s: pareggio
? "pareggio dentro il rumore (" + pc(peg[ordPeg[0]], 1) + " contro " + pc(peg[ordPeg[1]], 1) +
"): questo criterio non li distingue"
: "nello scenario a lei più ostile arriva al " + pc(peg[kOf(wMax)]) + " del proprio bersaglio" },
{ k: "minimizza il rimpianto", v: wlab(wReg),
s: "non perde mai più di " + pc(rim[kOf(wReg)]) + " rispetto alla scelta migliore di ogni scenario" },
{ k: "correlazione book ↔ indice", v: D.correlazione.aperta.toFixed(2).replace(".", ","),
s: "nei giorni di ribasso " + D.correlazione.ribasso.toFixed(2).replace(".", ",") +
"; nel 5% peggiore dell'indice (" + pc(D.correlazione.coda_indice, 2) + ") il book fa " +
pc(D.correlazione.coda_book, 2) },
{ k: "scenario base · solo book", v: pc(S(base, 1).quota),
s: "capitale " + usd(S(base, 1).med) + " contro un bersaglio di " + usd(S(base, 1).cap) }
].map(t => '<div class="tile"><span class="k">' + t.k + '</span><span class="v">' + t.v +
'</span><span class="s">' + t.s + "</span></div>").join("");
/* ---------- grafico: quota vs peso ---------- */
$("leg").innerHTML = SCEN.map((n, i) =>
"<span><i style='background:" + COL[i] + "'></i>" + n + "</span>").join("");
(function () {
const svg = $("c1");
const Wd = 960, H = 400, M = { t: 26, r: 150, b: 46, l: 66 };
const pw = Wd - M.l - M.r, ph = H - M.t - M.b;
svg.setAttribute("viewBox", "0 0 " + Wd + " " + H);
svg.innerHTML = "";
const NS = "http://www.w3.org/2000/svg";
const el = (n, a) => { const e = document.createElementNS(NS, n); for (const k in a) e.setAttribute(k, a[k]); return e; };
const vals = SCEN.flatMap(n => W.map(w => S(n, w).quota));
const YMAX = Math.ceil(Math.max(1.0, ...vals) / 0.2) * 0.2;
const x = w => M.l + Number(w) * pw;
const y = v => M.t + ph - (v / YMAX) * ph;
for (let v = 0; v <= YMAX + 1e-9; v += 0.2) {
svg.appendChild(el("line", { x1: M.l, x2: M.l + pw, y1: y(v), y2: y(v),
stroke: v === 0 ? "var(--ink-3)" : "var(--grid)", "stroke-width": 1 }));
const t = el("text", { x: M.l - 12, y: y(v) + 4, "text-anchor": "end", class: "axis",
fill: "var(--ink-3)", "font-size": 11 });
t.textContent = pc(v); svg.appendChild(t);
}
/* la riga del 100%: il bersaglio raggiunto */
svg.appendChild(el("line", { x1: M.l, x2: M.l + pw, y1: y(1), y2: y(1),
stroke: "var(--ink-2)", "stroke-width": 1.5, "stroke-dasharray": "5 4" }));
const bl = el("text", { x: M.l + 8, y: y(1) - 8, class: "axis", fill: "var(--ink-2)", "font-size": 11 });
bl.textContent = "bersaglio raggiunto"; svg.appendChild(bl);
W.forEach(w => {
const t = el("text", { x: x(w), y: H - 20, "text-anchor": "middle", class: "axis",
fill: "var(--ink-3)", "font-size": 11 });
t.textContent = pc(Number(w)); svg.appendChild(t);
});
const ax = el("text", { x: M.l + pw / 2, y: H - 4, "text-anchor": "middle", class: "axis",
fill: "var(--ink-3)", "font-size": 10.5 });
ax.textContent = "quota sul book (il resto su ETF S&P 500)"; svg.appendChild(ax);
SCEN.forEach((n, i) => {
const pts = W.map(w => x(w) + "," + y(S(n, w).quota)).join(" ");
svg.appendChild(el("polyline", { points: pts, fill: "none", stroke: COL[i],
"stroke-width": 2.5, "stroke-linejoin": "round" }));
W.forEach(w => svg.appendChild(el("circle", { cx: x(w), cy: y(S(n, w).quota), r: 3.5, fill: COL[i] })));
const last = S(n, W[W.length - 1]);
const a = el("text", { x: M.l + pw + 12, y: y(last.quota) - 3, class: "axis", fill: COL[i], "font-size": 11 });
a.textContent = n; svg.appendChild(a);
const b = el("text", { x: M.l + pw + 12, y: y(last.quota) + 12, class: "axis", fill: "var(--ink)", "font-size": 11.5 });
b.textContent = pc(last.quota); svg.appendChild(b);
});
/* hover per colonna */
const tip = $("tip");
const move = evt => { const r = tip.getBoundingClientRect();
let X = evt.clientX + 16, Y = evt.clientY - r.height - 12;
if (X + r.width > window.innerWidth - 8) X = evt.clientX - r.width - 16;
if (Y < 8) Y = evt.clientY + 18;
tip.style.left = X + "px"; tip.style.top = Y + "px"; };
W.forEach(w => {
const g = el("g", { tabindex: "0", "aria-label": wlab(w) });
g.appendChild(el("rect", { x: x(w) - pw / (W.length * 2), y: M.t,
width: pw / (W.length - 1) * 0.9, height: ph, fill: "transparent" }));
const show = evt => {
tip.innerHTML = '<span class="t">' + wlab(w) + "</span>" +
SCEN.map((n, i) => '<div class="r"><span><span class="sw" style="background:' + COL[i] +
'"></span>' + n + "</span><b>" + pc(S(n, w).quota) + "</b></div>").join("") +
'<div class="r" style="margin-top:5px"><span>capitale (base)</span><b>' +
usd(S(base, w).med) + "</b></div>" +
'<div class="r"><span>bersaglio (base)</span><b>' + usd(S(base, w).cap) + "</b></div>";
tip.style.opacity = "1"; move(evt); };
g.addEventListener("pointerenter", show);
g.addEventListener("pointermove", move);
g.addEventListener("pointerleave", () => tip.style.opacity = "0");
g.addEventListener("focus", () => show({ clientX: x(w) + 40, clientY: 320 }));
g.addEventListener("blur", () => tip.style.opacity = "0");
svg.appendChild(g);
});
$("cap1").innerHTML =
"Ogni punto è: capitale mediano a " + P.anni + " anni diviso il capitale-rendita di " +
"<em>quella</em> composizione — perché il bersaglio cambia col portafoglio, non è un numero " +
"fisso. Il 100% è la linea da superare. Le quattro curve sono lo stesso calcolo sotto quattro " +
"assunzioni di rendimento; le volatilità e la forma del rischio non sono mai state toccate.";
})();
/* ---------- tabella decisione ---------- */
$("t-dec").innerHTML =
"<caption>Quota del proprio capitale-rendita raggiunta a " + P.anni + " anni. «Caso peggiore» " +
"= il minimo della riga. «Rimpianto massimo» = quanto si perde, nello scenario in cui va " +
"peggio, rispetto alla scelta che <em>lì</em> sarebbe stata la migliore.</caption>" +
"<thead><tr><th>scelta</th>" + SCEN.map(n => "<th>" + n + "</th>").join("") +
"<th>caso peggiore</th><th>rimpianto max</th></tr></thead><tbody>" +
W.map(w => {
const cls = (kOf(wMax) === w || kOf(wReg) === w) ? ' class="best"' : "";
return "<tr" + cls + "><td>" + wlab(w) + "</td>" +
SCEN.map(n => "<td><span class='cell'>" + pc(S(n, w).quota) + "</span></td>").join("") +
"<td><span class='cell'>" + pc(peg[w]) + "</span></td>" +
"<td><span class='cell'>" + pc(rim[w]) + "</span></td></tr>"; }).join("") + "</tbody>";
$("p-tab").innerHTML =
"Le colonne non sono probabilità: sono quattro mondi possibili, e non si sa quale sia il " +
"nostro. Una scelta buona è una che sta in alto in tutti — non una che vince nel mondo che " +
"preferiamo.";
/* ---------- perché il mix ---------- */
$("p-corr").innerHTML =
"La correlazione fra book e indice è <b>" + D.correlazione.aperta.toFixed(2).replace(".", ",") +
"</b> nei giorni di borsa aperta e <b>" + D.correlazione.ribasso.toFixed(2).replace(".", ",") +
"</b> nei giorni di ribasso. Nel 5% di giornate peggiori dell'indice — quelle in cui perde in " +
"media <b>" + pc(D.correlazione.coda_indice, 2) + "</b> — il book fa <b>" +
pc(D.correlazione.coda_book, 2) + "</b>. Per questo un mix non è «un po' di ciascuno per non " +
"sbagliare»: due motori scorrelati messi insieme hanno una volatilità più bassa della media " +
"delle loro, e la rendita sostenibile dipende dal drawdown, non dal rendimento medio. È il " +
"motivo per cui nella tabella qui sotto il <em>bersaglio</em> di un mix può essere più basso " +
"di quello di entrambi i puri.";
$("t-det").innerHTML =
"<caption>Dettaglio nello scenario «" + base + "». La rendita perpetua è il prelievo annuo " +
"che mantiene P(capitale a 20 anni ≥ capitale iniziale) ≥ 90%.</caption>" +
"<thead><tr><th>scelta</th><th>rendita perpetua</th><th>capitale-rendita</th>" +
"<th>capitale a " + P.anni + " anni</th><th>banda p25p75</th><th>quota</th></tr></thead><tbody>" +
W.map(w => { const r = S(base, w);
return "<tr><td>" + wlab(w) + "</td><td>" + pc(r.perp, 2) + "</td><td>" + usd(r.cap) +
"</td><td>" + usd(r.med) + "</td><td>" + usdK(r.p25) + " " + usdK(r.p75) +
"</td><td>" + pc(r.quota) + "</td></tr>"; }).join("") + "</tbody>";
/* ---------- risposta ---------- */
$("p-risp").innerHTML = ""; /* riempita sotto, dipende dai numeri */
const qBookPeg = peg[kOf(1)];
const qEtfPeg = peg[kOf(0)];
$("p-risp").innerHTML =
"<b>" + wlab(wReg) + "</b>. Non perché vinca — vince solo nello scenario base — ma perché è la " +
"scelta il cui <b>rimpianto massimo</b> è più basso (" + pc(rim[kOf(wReg)]) + " contro il " +
pc(rim[kOf(1)]) + " del book puro e il " + pc(rim[kOf(0)]) +
" dell'ETF puro): qualunque dei quattro mondi si avveri, resta vicina alla scelta che sarebbe " +
"stata giusta lì. Tutto sul book arriva al " + pc(qBookPeg) + " del bersaglio nel mondo che " +
"gli è ostile, tutto sull'ETF al " + pc(qEtfPeg) + " nel suo. " +
(pareggio ? "⚠️ Il criterio del solo caso peggiore <em>non</em> distingue " + wlab(ordPeg[0]) +
" da " + wlab(ordPeg[1]) + " (" + pc(peg[ordPeg[0]], 1) + " contro " + pc(peg[ordPeg[1]], 1) +
", dentro il rumore): a separarli è il rimpianto, non il caso peggiore." : "");
$("p-limiti").innerHTML =
"⚠️ Cosa questa analisi <em>non</em> dice. Non c'è il rischio di venue (a p=1% annuo la " +
"probabilità di perdere tutto il lato cripto è ~18% su vent'anni, e sul lato ETF quel rischio " +
"praticamente non esiste — il che spinge ancora verso il mix, ma non è nel conto). Il mix " +
"assume due salvadanai separati senza ribilanciamento, perché ribilanciare vuol dire vendere " +
"e realizzare. Il taglio del 50% al drift del book è una scelta, non una misura. E gli scenari " +
"sono quattro mondi, non quattro probabilità: la tabella aiuta a non farsi male, non a " +
"indovinare.";
$("foot").innerHTML =
"Script: <b>scripts/research/r0807_best_strategy.py</b> (decisione) e " +
"<b>r0807_asset_compare.py</b> (serie e fisco). Il book è TP01 75% + SKH01 25% alle ancore " +
"canoniche, de-luckato ×0.89; l'ETF è SPY con <code>ADJUSTED_LAST</code> da Interactive " +
"Brokers, su griglia di calendario con 0,0 a borsa chiusa. Fisco: cripto " + pc(P.tax_crypto) +
" annuo sulle plusvalenze più " + pc(0.002, 1) + " sul valore; ETF UCITS ad accumulazione " +
pc(P.tax_etf) + " differito alla vendita più " + pc(0.002, 1) + " — assunzioni dichiarate, " +
"<b>non un parere fiscale</b>. Commissione d'acquisto ETF €" + P.comm + "/mese.";
})();
</script>