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>
532 lines
27 KiB
HTML
532 lines
27 KiB
HTML
<title>Crescita del capitale: quanto è versamento e quanto è guadagno</title>
|
||
|
||
<style>
|
||
:root {
|
||
color-scheme: light;
|
||
--ground: #f7f7f5;
|
||
--surface: #fdfdfc;
|
||
--rule: #dededa;
|
||
--rule-soft: #ebebe7;
|
||
--ink: #16171a;
|
||
--ink-2: #55575c;
|
||
--ink-3: #83858b;
|
||
--versato: #2a78d6;
|
||
--guadagno: #eb6834;
|
||
--grid: #e6e6e2;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
color-scheme: dark;
|
||
--ground: #17181a;
|
||
--surface: #1d1e21;
|
||
--rule: #34363a;
|
||
--rule-soft: #26282b;
|
||
--ink: #f1f1ee;
|
||
--ink-2: #a8aab0;
|
||
--ink-3: #7c7e85;
|
||
--versato: #3987e5;
|
||
--guadagno: #d95926;
|
||
--grid: #2b2d31;
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
color-scheme: dark;
|
||
--ground: #17181a;
|
||
--surface: #1d1e21;
|
||
--rule: #34363a;
|
||
--rule-soft: #26282b;
|
||
--ink: #f1f1ee;
|
||
--ink-2: #a8aab0;
|
||
--ink-3: #7c7e85;
|
||
--versato: #3987e5;
|
||
--guadagno: #d95926;
|
||
--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: 18px; }
|
||
|
||
.mono, .eyebrow, .lede-meta, .legend, .axis, table, .tile, .switch {
|
||
font-family: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
header { padding-top: 64px; display: flex; flex-direction: column; gap: 20px; }
|
||
.eyebrow {
|
||
font-size: 11.5px; letter-spacing: 0.16em; text-transform: uppercase;
|
||
color: var(--ink-3); margin: 0;
|
||
}
|
||
h1 {
|
||
font-size: clamp(30px, 4.4vw, 46px); line-height: 1.12; margin: 0;
|
||
font-weight: 400; letter-spacing: -0.015em; text-wrap: balance; max-width: 20ch;
|
||
}
|
||
.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: 16px; 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; }
|
||
strong { font-weight: 700; }
|
||
em { font-style: italic; color: var(--ink-2); }
|
||
|
||
figure { margin: 0; display: flex; flex-direction: column; gap: 10px; }
|
||
.figcap {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 12px; color: var(--ink-3); line-height: 1.7; max-width: 78ch;
|
||
}
|
||
.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: 11px; height: 11px; border-radius: 2px; display: inline-block; margin-right: 7px; vertical-align: -1px; }
|
||
.legend .band { width: 2px; height: 13px; border-radius: 0; background: var(--ink-3); margin-right: 7px; }
|
||
|
||
.stack { display: flex; flex-direction: column; gap: 30px; }
|
||
|
||
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
|
||
.tile { background: var(--surface); padding: 18px 18px 20px; display: flex; flex-direction: column; gap: 6px; }
|
||
.tile .k { font-size: 11px; letter-spacing: 0.13em; text-transform: uppercase; color: var(--ink-3); }
|
||
.tile .v { font-size: 27px; letter-spacing: -0.02em; color: var(--ink); line-height: 1.1; }
|
||
.tile .s { font-size: 12px; color: var(--ink-2); line-height: 1.6; }
|
||
.tile .v small { font-size: 15px; color: var(--ink-3); letter-spacing: 0; }
|
||
|
||
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(--versato); outline-offset: 3px; }
|
||
.tablewrap { overflow-x: auto; margin-top: 16px; }
|
||
table { border-collapse: collapse; font-size: 12.5px; width: 100%; min-width: 660px; }
|
||
th, td { padding: 6px 12px 6px 0; text-align: right; white-space: nowrap; }
|
||
th:first-child, td:first-child { text-align: left; padding-left: 0; }
|
||
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; }
|
||
tbody td { border-bottom: 1px solid var(--rule-soft); color: var(--ink-2); }
|
||
tbody tr td:first-child { color: var(--ink); }
|
||
tbody tr.mark td { background: color-mix(in srgb, var(--versato) 7%, transparent); }
|
||
caption { caption-side: top; text-align: left; font-size: 12px; color: var(--ink-3); padding-bottom: 10px; }
|
||
|
||
/* confronto fra piani */
|
||
.cmp { border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
|
||
.cmp table { min-width: 520px; }
|
||
.cmp thead th { padding: 12px 14px 10px; background: var(--surface); }
|
||
.cmp td { padding: 9px 14px; }
|
||
.cmp tbody tr:last-child td { border-bottom: 0; }
|
||
.cmp .hi { color: var(--ink); }
|
||
|
||
#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: 260px;
|
||
}
|
||
#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; vertical-align: 0; }
|
||
|
||
.bargroup { cursor: default; }
|
||
.bargroup:focus-visible { outline: none; }
|
||
.bargroup:focus-visible .hit { stroke: var(--ink); stroke-width: 1.5; }
|
||
|
||
.caveat {
|
||
border-left: 2px solid var(--guadagno); 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: 78ch;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||
</style>
|
||
|
||
<div class="wrap">
|
||
|
||
<header>
|
||
<p class="eyebrow">PythagorasGoal · piano di accumulo · 2026-08-07</p>
|
||
<h1>Quanto è versamento e quanto è guadagno</h1>
|
||
<p class="lede">
|
||
Il piano con <b>€800 al mese</b> — più i €5.000 iniziali sul conto Deribit — anno per anno,
|
||
con le due componenti del capitale separate: i bonifici, che sono decisi, e il rendimento del
|
||
book, che è estratto.
|
||
</p>
|
||
<p class="lede-meta">
|
||
book live <b>TP01 + SKH01 75/25</b> alle ancore canoniche, de-luckato <b>×0.89</b> ·
|
||
block bootstrap <b>4.000 path</b>, blocchi da 20 giorni ·
|
||
bersaglio <b>$272.061</b> (capitale-rendita per €50/giorno netti) ·
|
||
<b>scripts/research/r0807_growth_yearly.py --dep 800</b>
|
||
</p>
|
||
</header>
|
||
|
||
<section class="prose">
|
||
<h2>La forma della curva</h2>
|
||
<p>
|
||
Per i primi sette anni il capitale <em>è</em> quasi solo la somma dei bonifici: il book
|
||
aggiunge poco perché ha poco su cui lavorare. Il sorpasso — l'anno in cui il guadagno
|
||
cumulato supera tutto il denaro versato — arriva all'<strong id="x-cross-l">8º</strong> anno
|
||
nel modello lordo e all'<strong id="x-cross-n">11º</strong> col fisco dentro. Da lì la curva
|
||
cambia natura: smette di essere un piano di risparmio e diventa un capitale che lavora.
|
||
</p>
|
||
<p>
|
||
Versare di più <em>non</em> anticipa il sorpasso — lo ritarda, perché alza l'asticella che il
|
||
guadagno deve superare. A €500/mese cadeva al 7º anno; qui all'8º. Quello che i €300 in più
|
||
comprano è il <strong>traguardo</strong>, non il momento in cui la strategia prende il
|
||
sopravvento: la mediana tocca il bersaglio al <strong id="x-hit-n">12º</strong> anno col
|
||
fisco, contro il 15º a €500.
|
||
</p>
|
||
</section>
|
||
|
||
<div class="tiles" id="tiles"></div>
|
||
|
||
<section class="stack">
|
||
<figure>
|
||
<p class="chart-title">Modello lordo <span>— nessuna imposta durante l'accumulo</span></p>
|
||
<div class="legend" aria-hidden="true">
|
||
<span><i style="background:var(--versato)"></i>versato (deterministico)</span>
|
||
<span><i style="background:var(--guadagno)"></i>guadagno del percorso mediano</span>
|
||
<span><i class="band"></i>banda p25–p75 del capitale</span>
|
||
</div>
|
||
<div class="chart-box"><svg id="c-lordo" role="img" aria-label="Capitale per anno con 800 euro al mese, modello lordo, scomposto in versamenti e guadagno"></svg></div>
|
||
</figure>
|
||
|
||
<figure>
|
||
<p class="chart-title">Con il fisco d'accumulo <span>— plusvalenze 33% annue + patrimoniale 0,2%</span></p>
|
||
<div class="chart-box"><svg id="c-netto" role="img" aria-label="Capitale per anno con 800 euro al mese e imposte, scomposto in versamenti e guadagno"></svg></div>
|
||
<figcaption class="figcap">
|
||
Stessa scala verticale nei due grafici: la differenza di altezza <em>è</em> il costo del fisco.
|
||
La banda p25–p75 dice quanto il percorso può spostarsi; il guadagno disegnato è quello del
|
||
percorso mediano, non di un percorso singolo.
|
||
</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<section class="prose">
|
||
<h2>Cosa comprano i €300 in più</h2>
|
||
<p>
|
||
Il confronto è a parità di tutto il resto — stesso book, stessi path, stesso lump da €5.000 —
|
||
e va letto sulla riga della <em>probabilità</em>, non su quella del capitale: la mediana dice
|
||
dove finisce metà dei percorsi, la probabilità dice quanti ci arrivano.
|
||
</p>
|
||
</section>
|
||
|
||
<div class="cmp"><div class="tablewrap"><table id="cmp"></table></div></div>
|
||
|
||
<section class="prose">
|
||
<h2>Il vincolo dei dieci anni</h2>
|
||
<p class="caveat">
|
||
Anche €800 al mese <b>non bastano</b> per il traguardo in dieci anni: la probabilità col fisco
|
||
dentro è <b id="x-p10">25,5%</b>. Serve circa <b>€880–1.050 al mese</b> — e a quel punto si
|
||
sono versati oltre $119.000 per arrivare a $272.061, cioè <em>a orizzonte corto non fai
|
||
lavorare la strategia, compri il capitale coi bonifici</em>.
|
||
</p>
|
||
<p>
|
||
L'imposta durante l'accumulo <strong>non era mai stata contata</strong> in nessuna traiettoria
|
||
del progetto: l'aliquota compariva in un solo punto, la lordizzazione del bersaglio in fase di
|
||
<em>prelievo</em>. L'errore è composto — <strong id="x-tax5">−15%</strong> di capitale a 5 anni,
|
||
<strong id="x-tax10">−29%</strong> a 10, <strong id="x-tax15">−42%</strong> a 15 — quindi
|
||
cresce proprio sull'orizzonte su cui si sta decidendo. Le assunzioni (33% sulle plusvalenze,
|
||
minusvalenze riportabili 4 anni, 0,2% annuo sul valore) sono dichiarate e <em>non sono un
|
||
parere fiscale</em>.
|
||
</p>
|
||
</section>
|
||
|
||
<details>
|
||
<summary>Tabella completa — i due regimi, anno per anno, a €800/mese</summary>
|
||
<div class="tablewrap">
|
||
<table id="tbl">
|
||
<caption>Valori in dollari. «capitale» è la mediana fra i 4.000 percorsi; «P(bersaglio)» è la quota di percorsi che ha toccato $272.061 in qualunque momento entro quell'anno.</caption>
|
||
</table>
|
||
</div>
|
||
</details>
|
||
|
||
<footer>
|
||
Il «guadagno» di ogni riga è <b>mediana(capitale) − versato</b>: il rendimento del percorso
|
||
mediano, non di un percorso singolo — le due componenti non sono indipendenti e non vanno
|
||
sommate come se lo fossero. I versamenti entrano ogni 30 giorni. Nessun rischio di venue in
|
||
questo modello: a p=1% annuo di fallimento dell'exchange la probabilità di arrivare scende di
|
||
~2 punti e quella di <b>perdere tutto</b> sale al 18%, e a p=5% il capitale mediano a 20 anni è
|
||
zero per qualunque calendario di versamenti. E un piano si giudica sulle sue deviazioni: qui i
|
||
versamenti non si fermano mai, che è l'unico scenario che non succede.
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<div id="tip" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
(function () {
|
||
"use strict";
|
||
|
||
/* riga = [anno, versato, guadagno_med, p25, p75, P(bersaglio)] */
|
||
const D800 = {
|
||
"lordo":[[1,16514,1769,17245,19578,0.0],[2,26978,5761,30155,35973,0.0],[3,37442,12653,45263,55806,0.0],[4,47906,22674,62395,80542,0.0],[5,58370,36254,82632,109462,0.0],[6,68834,54149,105982,145050,0.001],[7,80170,77582,134771,188140,0.027],[8,90634,107277,166043,240255,0.153],[9,101098,145547,201137,302141,0.406],[10,111562,190624,245158,375704,0.663],[11,122026,246454,293656,463843,0.844],[12,132490,314762,352325,569174,0.942],[13,143826,397855,420145,697480,0.981],[14,154290,497056,504972,849886,0.995],[15,164754,611612,598478,1027590,0.998]],
|
||
"netto":[[1,16514,1150,16970,18530,0.0],[2,26978,3704,28990,32772,0.0],[3,37442,8016,42367,49006,0.0],[4,47906,14081,56895,67961,0.0],[5,58370,21953,73255,88990,0.0],[6,68834,32239,91315,113319,0.0],[7,80170,45114,112318,141436,0.001],[8,90634,60851,134505,172871,0.014],[9,101098,80839,158029,208664,0.072],[10,111562,102940,185847,248679,0.255],[11,122026,129351,216191,293736,0.508],[12,132490,160661,249046,345007,0.736],[13,143826,196548,286976,403693,0.884],[14,154290,238407,331309,470099,0.961],[15,164754,285353,377454,544074,0.99]]};
|
||
|
||
/* il piano precedente, per il confronto */
|
||
const D500 = {
|
||
"lordo":[[1,12590,1480,13204,15143,0.0],[2,19130,4439,21604,26058,0.0],[3,25670,9315,31459,39175,0.0],[4,32210,16336,42734,55555,0.0],[5,38750,25573,55941,74606,0.0],[6,45290,37658,71107,98301,0.0],[7,52375,53218,90027,126660,0.001],[8,58915,73347,110476,161177,0.012],[9,65455,98449,133388,202044,0.059],[10,71995,128781,162249,250780,0.2],[11,78535,166083,194409,309087,0.413],[12,85075,210811,232546,378373,0.625],[13,92160,266535,276493,463805,0.787],[14,98700,331836,332045,563596,0.901],[15,105240,406396,392375,682638,0.958]],
|
||
"netto":[[1,12590,964,12975,14272,0.0],[2,19130,2854,20694,23582,0.0],[3,25670,5911,29311,34169,0.0],[4,32210,10094,38695,46478,0.0],[5,38750,15457,49239,60153,0.0],[6,45290,22328,60828,76054,0.0],[7,52375,30864,74451,94333,0.0],[8,58915,41442,88742,114692,0.0],[9,65455,54390,103868,138004,0.001],[10,71995,68996,121916,164188,0.012],[11,78535,86387,141436,193292,0.051],[12,85075,107166,162790,226868,0.154],[13,92160,130846,187634,264641,0.334],[14,98700,158081,216211,308116,0.537],[15,105240,188583,245868,356333,0.732]]};
|
||
|
||
const TARGET = 272061;
|
||
const I = { ANNO:0, VERS:1, GUAD:2, P25:3, P75:4, PHIT:5 };
|
||
const D = D800;
|
||
|
||
const usd = n => "$" + Math.round(n).toLocaleString("it-IT");
|
||
const usdK = n => n === 0 ? "$0" : "$" + Math.round(n / 1000) + "k";
|
||
const pct = n => (100 * n).toFixed(1).replace(".", ",") + "%";
|
||
const ORD = ["", "1º","2º","3º","4º","5º","6º","7º","8º","9º","10º","11º","12º","13º","14º","15º"];
|
||
|
||
const at = (set, key, y) => set[key].find(r => r[I.ANNO] === y);
|
||
const cap = r => r[I.VERS] + r[I.GUAD];
|
||
/* primo anno in cui il guadagno cumulato supera tutto il versato */
|
||
const crossover = key => (D[key].find(r => r[I.GUAD] > r[I.VERS]) || {})[I.ANNO];
|
||
/* primo anno in cui il capitale MEDIANO tocca il bersaglio */
|
||
const hitYear = key => (D[key].find(r => cap(r) >= TARGET) || {})[I.ANNO];
|
||
|
||
/* ---------------- testo guidato dai dati ---------------- */
|
||
const put = (id, txt) => { const e = document.getElementById(id); if (e) e.textContent = txt; };
|
||
put("x-cross-l", ORD[crossover("lordo")]);
|
||
put("x-cross-n", ORD[crossover("netto")]);
|
||
put("x-hit-n", ORD[hitYear("netto")]);
|
||
put("x-p10", pct(at(D, "netto", 10)[I.PHIT]));
|
||
[5, 10, 15].forEach(y => {
|
||
const l = cap(at(D, "lordo", y)), n = cap(at(D, "netto", y));
|
||
put("x-tax" + y, "−" + Math.round(100 * (1 - n / l)) + "%");
|
||
});
|
||
|
||
/* ---------------- tiles ---------------- */
|
||
const tiles = [5, 10, 15].map(y => {
|
||
const n = at(D, "netto", y), l = at(D, "lordo", y);
|
||
return { k: "a " + y + " anni", v: usd(cap(n)),
|
||
s: "versati " + usd(n[I.VERS]) + " · il guadagno è il " +
|
||
Math.round(100 * n[I.GUAD] / cap(n)) + "% del capitale · lordo sarebbe " + usd(cap(l)) };
|
||
}).concat([{ k: "sorpasso", v: crossover("lordo") + "º<small> / " + crossover("netto") + "º anno</small>",
|
||
s: "l'anno in cui il guadagno cumulato supera tutto il versato — lordo / col fisco" }]);
|
||
document.getElementById("tiles").innerHTML = tiles.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("");
|
||
|
||
/* ---------------- confronto 500 vs 800 ---------------- */
|
||
const cmpRows = [
|
||
["versato a 10 anni", s => usd(at(s, "netto", 10)[I.VERS])],
|
||
["capitale a 10 anni", s => usd(cap(at(s, "netto", 10)))],
|
||
["P(bersaglio) a 10 anni", s => pct(at(s, "netto", 10)[I.PHIT]), true],
|
||
["versato a 15 anni", s => usd(at(s, "netto", 15)[I.VERS])],
|
||
["capitale a 15 anni", s => usd(cap(at(s, "netto", 15)))],
|
||
["P(bersaglio) a 15 anni", s => pct(at(s, "netto", 15)[I.PHIT]), true],
|
||
["anno del traguardo (mediana)", s => {
|
||
const r = s.netto.find(x => x[I.VERS] + x[I.GUAD] >= TARGET);
|
||
return r ? ORD[r[I.ANNO]] + " anno" : "oltre il 15º"; }, true]
|
||
];
|
||
document.getElementById("cmp").innerHTML =
|
||
"<caption>Tutti i valori sono <b>col fisco d'accumulo</b>, il regime realistico. Stesso lump da €5.000, stessi 4.000 percorsi.</caption>" +
|
||
"<thead><tr><th></th><th>€500/mese</th><th>€800/mese</th></tr></thead><tbody>" +
|
||
cmpRows.map(([k, f, hi]) =>
|
||
"<tr><td>" + k + "</td><td" + (hi ? ' class="hi"' : "") + ">" + f(D500) +
|
||
"</td><td" + (hi ? ' class="hi"' : "") + ">" + f(D800) + "</td></tr>").join("") +
|
||
"</tbody>";
|
||
|
||
/* ---------------- tooltip ---------------- */
|
||
const tip = document.getElementById("tip");
|
||
function showTip(evt, serie, row) {
|
||
tip.innerHTML =
|
||
'<span class="t">anno ' + row[I.ANNO] + " · " + serie + "</span>" +
|
||
'<div class="r"><span><span class="sw" style="background:var(--versato)"></span>versato</span><b>' + usd(row[I.VERS]) + "</b></div>" +
|
||
'<div class="r"><span><span class="sw" style="background:var(--guadagno)"></span>guadagno</span><b>' + usd(row[I.GUAD]) + "</b></div>" +
|
||
'<div class="r"><span>capitale mediano</span><b>' + usd(cap(row)) + "</b></div>" +
|
||
'<div class="r"><span>banda p25–p75</span><b>' + usdK(row[I.P25]) + " – " + usdK(row[I.P75]) + "</b></div>" +
|
||
'<div class="r"><span>P(bersaglio)</span><b>' + pct(row[I.PHIT]) + "</b></div>";
|
||
tip.style.opacity = "1";
|
||
moveTip(evt);
|
||
}
|
||
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"; };
|
||
|
||
/* ---------------- scala verticale, dai dati ---------------- */
|
||
const rawMax = Math.max(...["lordo", "netto"].flatMap(k => D[k].map(r => r[I.P75])));
|
||
const STEP = [25000, 50000, 100000, 150000, 200000, 250000, 500000]
|
||
.find(s => rawMax / s <= 8) || 1000000;
|
||
const YMAX = Math.ceil(rawMax / STEP) * STEP;
|
||
|
||
/* ---------------- chart ---------------- */
|
||
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; };
|
||
|
||
function roundedTop(x, yTop, w, h, r) {
|
||
if (h <= 0) return "";
|
||
r = Math.min(r, h, w / 2);
|
||
return "M" + x + "," + (yTop + h) + "V" + (yTop + r) +
|
||
"a" + r + "," + r + " 0 0 1 " + r + "," + (-r) +
|
||
"h" + (w - 2 * r) +
|
||
"a" + r + "," + r + " 0 0 1 " + r + "," + r +
|
||
"V" + (yTop + h) + "Z";
|
||
}
|
||
|
||
function draw(svgId, key, label) {
|
||
const rows = D[key];
|
||
const W = 960, H = 360, M = { t: 26, r: 108, b: 38, l: 74 };
|
||
const pw = W - M.l - M.r, ph = H - M.t - M.b;
|
||
const svg = document.getElementById(svgId);
|
||
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
|
||
svg.innerHTML = "";
|
||
|
||
const y = v => M.t + ph - (v / YMAX) * ph;
|
||
const step = pw / rows.length;
|
||
const bw = Math.min(38, step * 0.62);
|
||
|
||
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);
|
||
}
|
||
|
||
const ty = y(TARGET);
|
||
svg.appendChild(el("line", { x1: M.l, x2: M.l + pw, y1: ty, y2: ty,
|
||
stroke: "var(--ink-2)", "stroke-width": 1.5, "stroke-dasharray": "5 4" }));
|
||
const tl = el("text", { x: M.l + 8, y: ty - 8, class: "axis",
|
||
fill: "var(--ink-2)", "font-size": 11 });
|
||
tl.textContent = "bersaglio $272.061 — capitale-rendita";
|
||
svg.appendChild(tl);
|
||
|
||
rows.forEach((row, i) => {
|
||
const cx = M.l + step * i + step / 2;
|
||
const x = cx - bw / 2;
|
||
const vTop = y(row[I.VERS]), gTop = y(cap(row)), base = y(0);
|
||
const g = el("g", { class: "bargroup", tabindex: "0",
|
||
"aria-label": "anno " + row[I.ANNO] + ": versato " + usd(row[I.VERS]) +
|
||
", guadagno " + usd(row[I.GUAD]) });
|
||
|
||
g.appendChild(el("path", { d: roundedTop(x, vTop, bw, base - vTop, 0), fill: "var(--versato)" }));
|
||
/* distanziatore da 2px solo dove il segmento e' visibile: nei primi anni il guadagno
|
||
vale pochi pixel e il distanziatore lo cancellerebbe */
|
||
const rawH = vTop - gTop;
|
||
const gh = rawH - (rawH > 6 ? 2 : 0);
|
||
if (gh > 0.3) g.appendChild(el("path", { d: roundedTop(x, gTop, bw, gh, 4), fill: "var(--guadagno)" }));
|
||
|
||
const p25 = y(row[I.P25]), p75 = y(row[I.P75]);
|
||
g.appendChild(el("line", { x1: cx, x2: cx, y1: p75, y2: p25,
|
||
stroke: "var(--surface)", "stroke-width": 4 }));
|
||
g.appendChild(el("line", { x1: cx, x2: cx, y1: p75, y2: p25,
|
||
stroke: "var(--ink-2)", "stroke-width": 2, opacity: .85 }));
|
||
[p25, p75].forEach(yy => g.appendChild(el("line", { x1: cx - 5, x2: cx + 5, y1: yy, y2: yy,
|
||
stroke: "var(--ink-2)", "stroke-width": 2, opacity: .85 })));
|
||
|
||
const hit = el("rect", { class: "hit", x: M.l + step * i, y: M.t,
|
||
width: step, height: ph, fill: "transparent" });
|
||
g.appendChild(hit);
|
||
g.addEventListener("pointerenter", e => showTip(e, label, row));
|
||
g.addEventListener("pointermove", moveTip);
|
||
g.addEventListener("pointerleave", hideTip);
|
||
g.addEventListener("focus", () => {
|
||
const b = hit.getBoundingClientRect();
|
||
showTip({ clientX: b.left + b.width / 2, clientY: b.top + 40 }, label, row);
|
||
});
|
||
g.addEventListener("blur", hideTip);
|
||
svg.appendChild(g);
|
||
|
||
const xt = el("text", { x: cx, y: H - 14, "text-anchor": "middle", class: "axis",
|
||
fill: row[I.ANNO] === crossover(key) ? "var(--ink)" : "var(--ink-3)", "font-size": 11 });
|
||
xt.textContent = row[I.ANNO];
|
||
svg.appendChild(xt);
|
||
});
|
||
|
||
/* etichette dirette delle SERIE a destra dell'ultima barra: l'identita' non e' mai solo
|
||
colore, ed e' il relief richiesto dal contrasto dell'arancio in chiaro */
|
||
const last = rows[rows.length - 1];
|
||
const lastTop = y(cap(last)), lastMid = y(last[I.VERS]), lx = M.l + pw + 14;
|
||
[["guadagno", usd(last[I.GUAD]), (lastTop + lastMid) / 2, "var(--guadagno)"],
|
||
["versato", usd(last[I.VERS]), (lastMid + y(0)) / 2, "var(--versato)"]
|
||
].forEach(([nome, val, yy, col]) => {
|
||
const a = el("text", { x: lx, y: yy - 2, class: "axis", fill: col, "font-size": 11 });
|
||
a.textContent = nome;
|
||
svg.appendChild(a);
|
||
const b = el("text", { x: lx, y: yy + 12, class: "axis", fill: "var(--ink)", "font-size": 11.5 });
|
||
b.textContent = val;
|
||
svg.appendChild(b);
|
||
});
|
||
|
||
/* etichetta diretta: il sorpasso */
|
||
const ci = crossover(key) - 1, cRow = rows[ci];
|
||
const ccx = M.l + step * ci + step / 2, cy = y(cap(cRow));
|
||
svg.appendChild(el("line", { x1: ccx, x2: ccx, y1: cy - 6, y2: cy - 26,
|
||
stroke: "var(--guadagno)", "stroke-width": 1.5 }));
|
||
const ct = el("text", { x: ccx, y: cy - 32, "text-anchor": "middle", class: "axis",
|
||
fill: "var(--guadagno)", "font-size": 11 });
|
||
ct.textContent = "sorpasso";
|
||
svg.appendChild(ct);
|
||
|
||
const ax = el("text", { x: M.l + pw + 14, y: H - 14, class: "axis",
|
||
fill: "var(--ink-3)", "font-size": 10.5 });
|
||
ax.textContent = "anno";
|
||
svg.appendChild(ax);
|
||
}
|
||
|
||
draw("c-lordo", "lordo", "lordo");
|
||
draw("c-netto", "netto", "col fisco");
|
||
|
||
/* ---------------- tabella ---------------- */
|
||
const tbl = document.getElementById("tbl");
|
||
const head = "<thead><tr><th>anno</th><th>versato</th>" +
|
||
"<th>guad. lordo</th><th>capitale lordo</th><th>P(bers.)</th>" +
|
||
"<th>guad. netto</th><th>capitale netto</th><th>P(bers.)</th><th>costo fisco</th></tr></thead>";
|
||
const body = D.lordo.map((l, i) => {
|
||
const n = D.netto[i], cl = cap(l), cn = cap(n);
|
||
const mark = [5, 10, 15].includes(l[I.ANNO]) ? ' class="mark"' : "";
|
||
return "<tr" + mark + "><td>" + l[I.ANNO] + "</td><td>" + usd(l[I.VERS]) + "</td><td>" +
|
||
usd(l[I.GUAD]) + "</td><td>" + usd(cl) + "</td><td>" + pct(l[I.PHIT]) + "</td><td>" +
|
||
usd(n[I.GUAD]) + "</td><td>" + usd(cn) + "</td><td>" + pct(n[I.PHIT]) + "</td><td>" +
|
||
(100 * (cn / cl - 1)).toFixed(1).replace(".", ",") + "%</td></tr>";
|
||
}).join("");
|
||
tbl.insertAdjacentHTML("beforeend", head + "<tbody>" + body + "</tbody>");
|
||
})();
|
||
</script>
|