ed2532b991
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>
394 lines
23 KiB
HTML
394 lines
23 KiB
HTML
<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&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&P su questa finestra <b>" + pc(D.drift.eq_comune, 1) + "</b>, S&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 p25–p75</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>
|