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>
490 lines
27 KiB
HTML
490 lines
27 KiB
HTML
<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&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&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 p25–p75, 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 − (1−0,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 p25–p75</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&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&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 p25–p75 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&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>
|