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

490 lines
27 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>Da dove viene il guadagno — e se invece fosse un ETF</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; --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; --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; --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, .step, .dotrow {
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: 22ch; }
.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; }
/* passi del calcolo */
.steps { display: flex; flex-direction: column; gap: 1px; background: var(--rule);
border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
.step { background: var(--surface); padding: 15px 18px; display: grid;
grid-template-columns: 28px 1fr; gap: 14px; align-items: baseline; font-size: 13.5px;
color: var(--ink-2); line-height: 1.65; }
.step .n { color: var(--ink-3); font-size: 11px; letter-spacing: 0.1em; }
.step b { color: var(--ink); font-weight: 400; }
.step code { background: var(--sunken); padding: 1px 5px; border-radius: 2px; font-size: 12.5px; color: var(--ink); }
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: 22px; 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; }
.legend .dash { background: none; border-top: 2px dashed var(--ink-3); height: 0; }
.figcap { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
color: var(--ink-3); line-height: 1.7; max-width: 80ch; }
table.data { border-collapse: collapse; font-size: 12.5px; width: 100%; min-width: 560px; }
table.data th, table.data td { padding: 8px 14px 8px 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 td .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 8px; }
.tablewrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: 3px; background: var(--surface); padding: 14px 16px; }
caption { caption-side: top; text-align: left; font-size: 12px; color: var(--ink-3); padding-bottom: 12px; }
/* banda finale */
.dots { border: 1px solid var(--rule); border-radius: 3px; background: var(--surface); padding: 18px 18px 8px; }
.dotrow { display: grid; grid-template-columns: 90px 1fr; gap: 14px; align-items: center; margin-bottom: 14px; font-size: 12px; color: var(--ink-2); }
details { border-top: 1px solid var(--rule); padding-top: 14px; }
summary { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-2); cursor: pointer; padding: 4px 0; }
summary:focus-visible { outline: 2px solid var(--s1); outline-offset: 3px; }
#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: 280px; }
#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; }
.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; }
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 · 2026-08-07</p>
<h1>Da dove viene il guadagno, e se invece fosse un ETF</h1>
<p class="lede">
Due domande. La prima: <b>quel guadagno come si calcola?</b> Non è un tasso ipotizzato — è
quello che producono i ritorni veri del book, ricampionati. La seconda: <b>e se gli stessi
soldi andassero su un S&amp;P 500 o un MSCI World?</b>
</p>
<p class="lede-meta" id="meta"></p>
</header>
<section class="prose">
<h2>1 · Come si calcola il guadagno</h2>
<p>
Da nessuna parte c'è un «rendimento atteso» scritto a mano. C'è una sequenza di ritorni
giornalieri realmente prodotti dal book live, e la si rimescola.
</p>
</section>
<div class="steps" id="steps"></div>
<section class="prose">
<p>
Il rendimento non è un'ipotesi, ma <em>la scelta della sequenza sì</em>: rimescolare gli
ultimi 7 anni assume che il futuro somigli a quel campione. È l'assunzione più forte di tutto
il modello, ed è esattamente ciò che la seconda domanda mette alla prova.
</p>
</section>
<section class="prose">
<h2>2 · I tre motori</h2>
<p>
Stesso piano, stessa macchina, tre sorgenti di ritorni. Le azioni vanno su griglia di
calendario con <em>0,0 nei giorni di borsa chiusa</em> — la convenzione già usata per lo
sleeve azionario: il capitale è fermo, non riciclato. Senza quella correzione una serie di
borsa passata a un motore che annualizza a 365 esce con Sharpe ×1,20.
</p>
</section>
<div class="tablewrap"><table class="data" id="t-serie"></table></div>
<section class="prose">
<p class="caveat">
La riga che conta non è la prima, è la seconda: <b>sulla stessa finestra</b> il book e
l'S&amp;P 500 hanno quasi lo stesso rendimento — <span id="x-drift"></span>. Tutta la
differenza è nel <b>rischio</b>: volatilità <span id="x-vol"></span> e drawdown massimo
<span id="x-dd"></span>. Il book non guadagna di più: perde di meno.
</p>
</section>
<figure>
<p class="chart-title">Capitale mediano per anno <span>— ciascuno ricampionato dalla PROPRIA storia piena</span></p>
<div class="legend" id="leg1"></div>
<div class="chart-box"><svg id="c1" role="img" aria-label="Curve del capitale mediano per anno, storia piena"></svg></div>
<figcaption class="figcap" id="cap1"></figcaption>
</figure>
<figure>
<p class="chart-title">Capitale mediano per anno <span>— tutti e tre ricampionati dalla STESSA finestra</span></p>
<div class="chart-box"><svg id="c2" role="img" aria-label="Curve del capitale mediano per anno, finestra comune"></svg></div>
<figcaption class="figcap" id="cap2"></figcaption>
</figure>
<section class="prose">
<h2>3 · Il bersaglio non è lo stesso per tutti</h2>
<p>
$272.061 è il capitale che serve <em>con la rendita perpetua del book e con l'aliquota del
33%</em>. Un ETF ha un'altra distribuzione e un'altra aliquota, quindi ha un altro bersaglio:
la rendita sostenibile dipende dal drawdown, non dal rendimento medio. Ricalcolato per
ciascuno, con lo stesso metodo.
</p>
</section>
<div class="tablewrap"><table class="data" id="t-berg"></table></div>
<section class="prose">
<h2>4 · La banda, non il numero</h2>
<p>
Le curve sopra sono mediane. Ecco dove sta in realtà il capitale finale: la barra è
l'intervallo p25p75, il segno è la mediana, la linea tratteggiata è il bersaglio di
quel motore.
</p>
</section>
<div class="dots" id="dots"></div>
<section class="prose">
<h2>5 · La lettura onesta</h2>
<p class="caveat" id="lettura"></p>
<p>
Il fisco è modellato diverso perché <em>è</em> diverso: il book realizza in continuo e paga
ogni anno il 33%; un ETF UCITS ad accumulazione non distribuisce nulla e paga il 26% alla
vendita. Il differimento è un vantaggio strutturale dell'ETF, ed è dentro i numeri qui sopra
(le curve degli ETF sono valori di <em>liquidazione</em>, già al netto dell'imposta latente).
Sono assunzioni dichiarate e <em>non sono un parere fiscale</em>.
</p>
</section>
<details>
<summary>Tabella completa — capitale mediano anno per anno, entrambe le lenti</summary>
<div class="tablewrap" style="margin-top:14px"><table class="data" id="t-full"></table></div>
</details>
<footer id="foot"></footer>
</div>
<div id="tip" role="status" aria-live="polite"></div>
<script id="cmp-data" type="application/json">/*__DATA__*/</script>
<script>
(function () {
"use strict";
const D = JSON.parse(document.getElementById("cmp-data").textContent);
const $ = id => document.getElementById(id);
const MOT = ["BOOK", "SP500", "WORLD"];
const NOME = { BOOK: "book live (cripto)", SP500: "S&P 500 (SPY)", WORLD: "MSCI World (proxy)" };
const COL = { BOOK: "var(--s1)", SP500: "var(--s2)", WORLD: "var(--s3)" };
const usd = n => "$" + Math.round(n).toLocaleString("it-IT");
const usdK = n => n === 0 ? "$0" : (Math.abs(n) >= 1e6
? "$" + (n / 1e6).toFixed(n >= 1e7 ? 0 : 1).replace(".", ",") + "M"
: "$" + Math.round(n / 1000) + "k");
const pc = (x, d) => (100 * x).toFixed(d === undefined ? 1 : d).replace(".", ",") + "%";
/* ---------- intestazione ---------- */
const P = D.piano;
$("meta").innerHTML =
"piano: <b>$" + P.start + "</b> + <b>€" + P.lump.toLocaleString("it-IT") + "</b> + <b>€" +
P.dep.toLocaleString("it-IT") + "/mese</b> per <b>" + P.anni + " anni</b> · " +
P.n_paths.toLocaleString("it-IT") + " percorsi, blocchi da 20 giorni · " +
"fisco: cripto <b>" + pc(P.tax_crypto, 0) + " annuo</b>, ETF <b>" + pc(P.tax_etf, 0) +
" differito alla vendita</b>, patrimoniale <b>" + pc(P.patrim, 1) + "</b> · " +
"commissione d'acquisto ETF <b>€" + P.comm_eur + "/mese</b> · MSCI World = " + P.world;
/* ---------- passi ---------- */
const mB = D.metriche.BOOK;
const passi = [
["01", "Si parte dai <b>ritorni giornalieri veri</b> del book live (TP01 75% + SKH01 25%): " +
mB.n.toLocaleString("it-IT") + " giorni dal " + mB.da + " al " + mB.a + ". Non sono simulati: " +
"sono quelli che la strategia ha prodotto, già al netto delle commissioni."],
["02", "Si toglie la <b>fortuna d'ancora</b>: <code>r (10,89)·media(r)</code>. Il book è " +
"stato misurato al 97º percentile delle sue possibili ancore di calendario, e il fattore " +
"0,89 è la correzione misurata, non stimata a occhio."],
["03", "Si <b>rimescolano a blocchi di 20 giorni</b>: si pescano a caso spezzoni di 20 giorni " +
"consecutivi e li si incolla fino a coprire gli anni richiesti. I blocchi servono a non " +
"distruggere il raggruppamento della volatilità — un rimescolamento giorno per giorno " +
"renderebbe il percorso troppo docile e la sopravvivenza troppo alta."],
["04", "Si <b>compone</b>: <code>capitale ×= (1 + r)</code> ogni giorno, più il bonifico ogni " +
"30 giorni, meno l'imposta a fine anno. Ripetuto su " + P.n_paths.toLocaleString("it-IT") +
" percorsi indipendenti."],
["05", "Il <b>guadagno</b> di un anno è <code>mediana(capitale) versato</code>. Il versato è " +
"esatto (i bonifici sono decisi); il resto è quello che ha prodotto il ricampionamento. " +
"Drift implicito del book: <b>" + pc(mB.drift) + " annuo</b> con volatilità <b>" +
pc(mB.vol) + "</b> e drawdown massimo <b>" + pc(Math.abs(mB.maxdd)) + "</b>."]
];
$("steps").innerHTML = passi.map(([n, t]) =>
'<div class="step"><span class="n">' + n + "</span><span>" + t + "</span></div>").join("");
/* ---------- tabella serie ---------- */
const F = D.finestra_comune;
$("t-serie").innerHTML =
"<caption>Le prime tre righe usano la storia piena di ciascuno; le seconde tre la sola " +
"finestra comune <b>" + F.da + " → " + F.a + "</b>. Drift e volatilità sono annualizzati a 365 " +
"giorni su griglia di calendario.</caption>" +
"<thead><tr><th>motore</th><th>anni</th><th>da</th><th>rendimento</th><th>volatilità</th>" +
"<th>Sharpe</th><th>drawdown max</th></tr></thead><tbody>" +
MOT.map(m => { const x = D.metriche[m];
return "<tr><td><span class='sw' style='background:" + COL[m] + "'></span>" + NOME[m] +
"</td><td>" + x.anni.toFixed(1) + "</td><td>" + x.da + "</td><td>" + pc(x.drift) +
"</td><td>" + pc(x.vol) + "</td><td>" + x.sharpe.toFixed(2).replace(".", ",") +
"</td><td>" + pc(x.maxdd) + "</td></tr>"; }).join("") +
MOT.map(m => { const x = D.metriche_comune[m];
return "<tr><td style='color:var(--ink-3)'><span class='sw' style='background:" + COL[m] +
";opacity:.5'></span>" + NOME[m] + " · stessa finestra</td><td>" + x.anni.toFixed(1) +
"</td><td>" + x.da + "</td><td>" + pc(x.drift) + "</td><td>" + pc(x.vol) + "</td><td>" +
x.sharpe.toFixed(2).replace(".", ",") + "</td><td>" + pc(x.maxdd) + "</td></tr>"; }).join("") +
"</tbody>";
const cB = D.metriche_comune.BOOK, cS = D.metriche_comune.SP500;
$("x-drift").textContent = pc(cB.drift) + " contro " + pc(cS.drift);
$("x-vol").textContent = pc(cB.vol) + " contro " + pc(cS.vol);
$("x-dd").textContent = pc(Math.abs(cB.maxdd)) + " contro " + pc(Math.abs(cS.maxdd));
/* ---------- legenda ---------- */
$("leg1").innerHTML = MOT.map(m =>
"<span><i style='background:" + COL[m] + "'></i>" + NOME[m] + "</span>").join("") +
"<span><i class='dash'></i>capitale-rendita di quel motore</span>";
/* ---------- grafico a linee ---------- */
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 tip = $("tip");
function moveTip(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";
}
const hideTip = () => { tip.style.opacity = "0"; };
function drawLines(svgId, traj, berg, capId, nota) {
const svg = $(svgId);
const W = 960, H = 400, M = { t: 26, r: 150, b: 40, l: 78 };
const pw = W - M.l - M.r, ph = H - M.t - M.b;
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
svg.innerHTML = "";
const anni = traj.BOOK.length;
const finiti = MOT.map(m => berg[m].cap).filter(c => isFinite(c) && c > 0);
const rawMax = Math.max(...MOT.flatMap(m => traj[m].map(r => r.med)), ...finiti);
const STEP = [25000, 50000, 100000, 150000, 200000, 250000, 500000].find(s => rawMax / s <= 8) || 1000000;
const YMAX = Math.ceil(rawMax / STEP) * STEP;
const x = a => M.l + (a - 1) / Math.max(1, anni - 1) * pw;
const y = v => M.t + ph - (v / YMAX) * ph;
for (let v = 0; v <= YMAX; v += STEP) {
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 = usdK(v); svg.appendChild(t);
}
for (let a = 1; a <= anni; a++) {
if (anni > 12 && a % 2 === 0 && a !== anni) continue;
const t = el("text", { x: x(a), y: H - 16, "text-anchor": "middle", class: "axis",
fill: "var(--ink-3)", "font-size": 11 });
t.textContent = a; svg.appendChild(t);
}
const ax = el("text", { x: M.l + pw + 12, y: H - 16, class: "axis", fill: "var(--ink-3)", "font-size": 10.5 });
ax.textContent = "anno"; svg.appendChild(ax);
/* bersagli: tratteggio orizzontale + etichetta a destra */
MOT.forEach(m => {
const c = berg[m].cap;
if (!isFinite(c) || c <= 0 || c > YMAX) return;
svg.appendChild(el("line", { x1: M.l, x2: M.l + pw, y1: y(c), y2: y(c), stroke: COL[m],
"stroke-width": 1.5, "stroke-dasharray": "4 4", opacity: .55 }));
});
/* linee */
MOT.forEach(m => {
const pts = traj[m].map(r => x(r.anno) + "," + y(r.med)).join(" ");
svg.appendChild(el("polyline", { points: pts, fill: "none", stroke: COL[m],
"stroke-width": 2.5, "stroke-linejoin": "round", "stroke-linecap": "round" }));
const last = traj[m][traj[m].length - 1];
svg.appendChild(el("circle", { cx: x(last.anno), cy: y(last.med), r: 4, fill: COL[m] }));
/* etichetta diretta: identita' mai solo colore */
const lx = M.l + pw + 12;
const a = el("text", { x: lx, y: y(last.med) - 3, class: "axis", fill: COL[m], "font-size": 11 });
a.textContent = NOME[m]; svg.appendChild(a);
const b = el("text", { x: lx, y: y(last.med) + 12, class: "axis", fill: "var(--ink)", "font-size": 11.5 });
b.textContent = usd(last.med); svg.appendChild(b);
});
/* colonne di hover */
for (let a = 1; a <= anni; a++) {
const g = el("g", { tabindex: "0", "aria-label": "anno " + a });
const w = pw / anni;
g.appendChild(el("rect", { x: x(a) - w / 2, y: M.t, width: w, height: ph, fill: "transparent" }));
MOT.forEach(m => g.appendChild(el("circle", { cx: x(a), cy: y(traj[m][a - 1].med), r: 2.5,
fill: COL[m], opacity: .0 })));
const mostra = evt => {
tip.innerHTML = '<span class="t">anno ' + a + "</span>" +
MOT.map(m => { const r = traj[m][a - 1];
return '<div class="r"><span><span class="sw" style="background:' + COL[m] + '"></span>' +
NOME[m].split(" (")[0] + "</span><b>" + usd(r.med) + "</b></div>" +
'<div class="r" style="opacity:.65"><span> banda p25p75</span><b>' +
usdK(r.p25) + " " + usdK(r.p75) + "</b></div>"; }).join("") +
'<div class="r" style="margin-top:5px"><span>versato</span><b>' +
usd(traj.BOOK[a - 1].versato) + "</b></div>";
tip.style.opacity = "1"; moveTip(evt);
};
g.addEventListener("pointerenter", mostra);
g.addEventListener("pointermove", moveTip);
g.addEventListener("pointerleave", hideTip);
g.addEventListener("focus", () => mostra({ clientX: x(a) + 60, clientY: 300 }));
g.addEventListener("blur", hideTip);
svg.appendChild(g);
}
$(capId).innerHTML = nota;
}
drawLines("c1", D.traiettoria, D.bersaglio, "cap1",
"Ogni motore ricampiona la propria storia: il book 7,4 anni, l'S&amp;P 500 trent'anni che " +
"contengono il 2000, il 2008 e il 2022. Le linee tratteggiate sono i tre capitali-rendita, " +
"diversi fra loro. È la lente che <b>favorisce il book</b>: gli si chiede di battere indici " +
"misurati anche nei loro anni peggiori, mai vissuti dalla strategia.");
drawLines("c2", D.traiettoria_comune, D.bersaglio_comune, "cap2",
"Stessa finestra per tutti (" + F.da + " → " + F.a + "), quindi stesso regime di mercato. " +
"Qui l'S&amp;P 500 <b>accumula più del book</b>. I bersagli tratteggiati sono anch'essi " +
"ricalcolati su questa finestra — e vanno presi con le molle: una rendita perpetua è una " +
"domanda a vent'anni, e sette anni non la risolvono.");
/* ---------- tabella bersagli ---------- */
$("t-berg").innerHTML =
"<caption>«Rendita perpetua» = prelievo annuo che mantiene P(capitale a 20 anni ≥ capitale " +
"iniziale) ≥ 90%. Il capitale è quello che serve per <b>€50/giorno netti</b> con l'aliquota di " +
"quel motore.</caption>" +
"<thead><tr><th>motore</th><th>aliquota</th><th>rendita perpetua</th><th>capitale necessario</th>" +
"<th>capitale a " + P.anni + " anni</th><th>quota raggiunta</th></tr></thead><tbody>" +
MOT.map(m => {
const b = D.bersaglio[m], L = D.traiettoria[m][D.traiettoria[m].length - 1];
const q = isFinite(b.cap) && b.cap > 0 ? L.med / b.cap : NaN;
return "<tr><td><span class='sw' style='background:" + COL[m] + "'></span>" + NOME[m] +
"</td><td>" + pc(b.aliquota, 0) + "</td><td>" + pc(b.perp, 2) + "</td><td>" +
(isFinite(b.cap) ? usd(b.cap) : "non sostenibile") + "</td><td>" + usd(L.med) + "</td><td>" +
(isFinite(q) ? pc(q, 0) : "—") + "</td></tr>"; }).join("") + "</tbody>";
/* ---------- banda finale ---------- */
const allMax = Math.max(...MOT.flatMap(m => [D.traiettoria[m].slice(-1)[0].p75,
isFinite(D.bersaglio[m].cap) ? D.bersaglio[m].cap : 0]));
$("dots").innerHTML = MOT.map(m => {
const L = D.traiettoria[m].slice(-1)[0], b = D.bersaglio[m];
const f = v => (100 * v / allMax).toFixed(2) + "%";
const berg = isFinite(b.cap) && b.cap <= allMax
? '<span style="position:absolute;left:' + f(b.cap) + ';top:-6px;bottom:-6px;border-left:2px dashed var(--ink-3)"></span>' : "";
return '<div class="dotrow"><span>' + NOME[m].split(" (")[0] + '</span>' +
'<span style="position:relative;display:block;height:22px">' +
'<span style="position:absolute;left:' + f(L.p25) + ';width:' + f(L.p75 - L.p25) +
';top:7px;height:8px;border-radius:4px;background:' + COL[m] + ';opacity:.32"></span>' +
'<span style="position:absolute;left:calc(' + f(L.med) + ' - 1px);top:3px;width:3px;height:16px;background:' + COL[m] + '"></span>' +
berg + "</span></div>"; }).join("") +
'<p class="figcap" style="margin-top:2px">Barra = intervallo p25p75 del capitale a ' + P.anni +
' anni · segno pieno = mediana · tratteggio = capitale-rendita di quel motore. Scala comune, ' +
'da $0 a ' + usdK(allMax) + '.</p>';
/* ---------- lettura ---------- */
const qB = D.traiettoria.BOOK.slice(-1)[0].med / D.bersaglio.BOOK.cap;
const qS = D.traiettoria.SP500.slice(-1)[0].med / D.bersaglio.SP500.cap;
const qBc = D.traiettoria_comune.BOOK.slice(-1)[0].med / D.bersaglio_comune.BOOK.cap;
const qSc = D.traiettoria_comune.SP500.slice(-1)[0].med / D.bersaglio_comune.SP500.cap;
$("lettura").innerHTML =
"Sulla storia piena di ciascuno il book arriva al <b>" + pc(qB, 0) + "</b> del proprio " +
"bersaglio e l'S&amp;P 500 al <b>" + pc(qS, 0) + "</b>. Sulla stessa finestra diventano <b>" +
pc(qBc, 0) + "</b> e <b>" + pc(qSc, 0) + "</b>: quasi pari. <b>La differenza fra le due lenti " +
"è tutta la domanda</b> — i sette anni del book rappresentano il futuro, o sono una finestra " +
"fortunata? L'indice ha attraversato due crolli che la strategia non ha mai vissuto, e il " +
"book non ha una storia lunga da opporre: ne ha " + D.metriche.BOOK.anni.toFixed(1) + " anni in tutto.";
/* ---------- tabella completa ---------- */
$("t-full").innerHTML =
"<thead><tr><th>anno</th><th>versato</th>" +
MOT.map(m => "<th>" + NOME[m].split(" (")[0] + "</th>").join("") +
MOT.map(m => "<th>" + NOME[m].split(" (")[0] + " · stessa fin.</th>").join("") +
"</tr></thead><tbody>" +
D.traiettoria.BOOK.map((r, i) =>
"<tr><td>" + r.anno + "</td><td>" + usd(r.versato) + "</td>" +
MOT.map(m => "<td>" + usd(D.traiettoria[m][i].med) + "</td>").join("") +
MOT.map(m => "<td style='opacity:.7'>" + usd(D.traiettoria_comune[m][i].med) + "</td>").join("") +
"</tr>").join("") + "</tbody>";
$("foot").innerHTML =
"Fonti: book live dalle serie d'ancora canoniche del progetto; <b>SPY</b> e <b>EFA</b> da " +
"Interactive Brokers, <code>ADJUSTED_LAST</code> (dividendi reinvestiti), certificati. " +
"MSCI World è un <b>proxy</b>, non l'indice: gli ETF reali (URTH, ACWI, VT) non sono " +
"nell'abbonamento dati del conto, quindi è ricostruito come 70% SPY + 30% EFA — e la quota USA " +
"dell'indice vero è passata da ~50% nel 2008 a ~72% oggi, quindi il peso fisso è " +
"un'approssimazione (fra 50% e 80% di USA il rendimento si muove di 0,7 punti, il Sharpe di " +
"0,05). Il prezzo di un ETF è già al netto del suo TER. Non c'è rischio di cambio nel modello: " +
"tutto è in dollari, e un investitore in euro lo corre su entrambi i lati. " +
"Script: <b>scripts/research/r0807_asset_compare.py</b>.";
})();
</script>