Files
Adriano Dal Pastro b1c3ff1bb8 docs(fisco): quadro fiscale verificato sulle fonti — la citazione normativa era sbagliata
Verificato sulle fonti (Fisco Oggi dell'Agenzia, Eutekne, Fiscomania, Circolare AdE 30/E
del 27/10/2023, guide professionali) cio' che il progetto assumeva senza averlo mai
controllato. NON e' un parere fiscale.

CONFERMATO, e nessun numero del piano cambia: 33% sulle plusvalenze cripto realizzate dal
1/1/2026 (art. 67 c.1 lett. c-sexies TUIR); franchigia EUR 2.000 abolita dal 2025;
minusvalenze riportabili 4 periodi ma solo contro plusvalenze cripto (art. 68 c. 9-bis);
patrimoniale 2 per mille sul valore al 31/12; regime dichiarativo per gli exchange esteri.

CORRETTO: il progetto citava «L.199/2025» come origine del 33% in 5 punti (r0725_capcurve,
r0725_ib10k x2, r0727_tasse, r0807_asset_compare, diario 24/07). E' falso. Il 33% dal 2026
e l'abolizione della franchigia vengono dalla L. 207/2024 art. 1 c. 23-29. La L. 199/2025
art. 1 c. 28 ritaglia il 26% per i soli token e-money denominati in EURO: BTC/ETH e le
stablecoin in dollari restano al 33%.

RESTA APERTA la domanda che vale $22k di muro: la Circolare 30/E non tratta i derivati, e
le fonti professionali collocano i derivati su cripto fuori dalle cripto-attivita'
(c-quater, RT Sez. II, 26%) — ma parlando di CFD di broker UE regolati in euro, non di
contratti inverse marginati e regolati IN CRIPTO su sede extra-UE. Registrata la domanda
da porre al commercialista nei termini esatti.

Trovata per strada una conseguenza modellistica: se i derivati sono c-quater, sono un
comparto di compensazione separato → il buffer di carry UNICO di r0807_piano_netto e
r0727_tasse e' ottimistico sulla coda (non sull'aliquota).

REGOLA: una fonte normativa citata in un commento di codice si verifica come un numero —
questa era sbagliata da settimane in 5 file e nessun test poteva accorgersene.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:51:50 +00:00

280 lines
14 KiB
Python

#!/usr/bin/env python
"""r0807_asset_compare.py — e se invece del book ci mettessi un ETF S&P 500 o MSCI World?
DOMANDA DELL'OPERATORE. Confronto onesto fra tre motori per lo STESSO piano di accumulo:
BOOK il book live Deribit (TP01 75% + SKH01 25%) alle ancore canoniche, de-luckato x0.89
SP500 SPY, ADJUSTED_LAST (dividendi reinvestiti), 1996+ -> 30 anni
WORLD MSCI World: URTH se certificato, altrimenti proxy 70% SPY + 30% EFA
Il confronto e' interessante SOLO se sono comparabili quattro cose, e tre di queste il progetto
le ha gia' sbagliate almeno una volta:
(1) LA GRIGLIA. Le azioni hanno ~252 barre/anno, il book 365. Una serie di borsa passata a un
motore che annualizza a 365 esce con Sharpe x1.20 e CAGR x1.45 (lezione del 25/07 su GTAA01).
Qui le azioni vanno su griglia di CALENDARIO con 0.0 nei giorni di chiusura — la convenzione
gia' adottata per GTAA01: il capitale e' fermo, non riciclato.
(2) LA FINESTRA. Il book ha 7.4 anni (2019-2026, due tori cripto); SPY ne ha 30 (2000, 2008,
2020, 2022 dentro). Confrontare 7 anni scelti dalla storia recente contro 30 anni e' un
confronto truccato a favore del book -> si riporta ANCHE la finestra comune.
(3) IL FISCO, che NON e' lo stesso e non e' un dettaglio:
book -> realizza in continuo, imposta ANNUA sulle plusvalenze (33% assunto) + 0.2% sul valore
ETF -> UCITS ad accumulazione: nessuna distribuzione, imposta DIFFERITA alla vendita (26%)
+ 0.2% annuo sul valore
Il differimento e' un vantaggio strutturale dell'ETF che il book non ha, e va nel modello.
(4) IL BERSAGLIO. $272.061 e' il capitale-rendita calcolato con la rendita perpetua DEL BOOK e
l'aliquota del 33%. Un ETF ha un'altra distribuzione e un'altra aliquota -> ha un ALTRO
bersaglio, che qui si ricalcola per ciascun motore invece di riusare il numero del book.
⚠️ Le assunzioni fiscali sono dichiarate e NON sono un parere fiscale.
uv run python scripts/research/r0807_asset_compare.py [--dep 500] [--lump 5000] [--anni 15]
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib as EQ # noqa: E402
import r0725_capcurve as CC # noqa: E402
import r0726_venue_risk as VR # noqa: E402
import r0726_capwall_refresh as WR # noqa: E402
import r0727_lumpsum_split as LS # noqa: E402
import r0807_growth_yearly as G # noqa: E402
TAX_CRYPTO = 0.33 # L. 207/2024 art. 1 c.23-29 (assunzione gia' cablata nel progetto)
TAX_ETF = 0.26 # redditi diversi di natura finanziaria
PATRIM = 0.002 # 0.2% annuo sul valore (IVAFE / imposta di bollo)
COMM_EUR = 2.0 # commissione per acquisto mensile su un broker retail europeo
N_PATHS = 4000
SEED = 807
# ------------------------------------------------------------------ serie
def _calendar_daily(close: pd.Series) -> pd.Series:
"""Ritorni giornalieri su griglia di CALENDARIO: 0.0 quando la borsa e' chiusa.
E' la convenzione di GTAA01 (capitale fermo nel weekend, non riciclato) ed e' cio' che rende
la serie confrontabile con quella del book, che ha 365 barre l'anno.
"""
r = close.astype(float).pct_change().dropna()
r.index = pd.to_datetime(r.index).tz_convert(None).normalize()
r = r[~r.index.duplicated(keep="last")]
idx = pd.date_range(r.index[0], r.index[-1], freq="D")
return r.reindex(idx).fillna(0.0)
def carica_serie() -> dict:
out = {}
b = VR.venue_series()["Deribit"]
b.index = pd.to_datetime(b.index).tz_localize(None).normalize()
out["BOOK"] = b.astype(float)
out["SP500"] = _calendar_daily(EQ.load_eq("SPY")["close"])
# MSCI World: il vero ETF se e' su disco, altrimenti il proxy dichiarato
world_path = ROOT / "data" / "raw" / "eq_urth_1d.parquet"
if world_path.exists():
out["WORLD"] = _calendar_daily(EQ.load_eq("URTH")["close"])
out["_world_fonte"] = "URTH (iShares MSCI World, reale)"
else:
spy, efa = _calendar_daily(EQ.load_eq("SPY")["close"]), _calendar_daily(EQ.load_eq("EFA")["close"])
j = pd.concat({"SPY": spy, "EFA": efa}, axis=1).dropna()
out["WORLD"] = (0.70 * j["SPY"] + 0.30 * j["EFA"])
out["_world_fonte"] = "PROXY 70% SPY + 30% EFA (ribil. giornaliero) — non e' l'indice"
return out
def metriche(r: pd.Series) -> dict:
eq = (1 + r).cumprod()
dd = float((eq / eq.cummax() - 1).min())
mu, sd = float(r.mean()) * 365, float(r.std()) * np.sqrt(365)
return dict(n=len(r), da=str(r.index[0].date()), a=str(r.index[-1].date()),
anni=len(r) / 365.0, drift=mu, vol=sd, sharpe=mu / sd if sd > 0 else 0.0, maxdd=dd)
# ------------------------------------------------------------------ accumulo
def accumula(r: np.ndarray, lump_eur: float, dep_eur: float, anni: int,
differita: bool, aliquota: float, comm_eur: float) -> list[dict]:
"""Traiettoria annuale con due regimi fiscali.
* `differita=False` (book): imposta ANNUA sulle plusvalenze realizzate + patrimoniale.
E' esattamente `r0807_growth_yearly.traiettoria`, riusata senza riscriverla.
* `differita=True` (ETF ad accumulazione): nessuna imposta annua sul reddito, solo la
patrimoniale; l'imposta sul guadagno si paga alla VENDITA -> la curva riportata e' il
valore di LIQUIDAZIONE, `cap - aliquota * max(0, cap - versato)`, che e' l'unica cosa
confrontabile con un book gia' tassato ogni anno.
La commissione d'acquisto si modella come deposito netto piu' piccolo, ma il `versato`
riportato resta quello USCITO DI TASCA: cosi' la commissione pesa sul guadagno, dov'e' giusto.
"""
dep_netto = max(0.0, dep_eur - comm_eur) if dep_eur > 0 else 0.0
righe = G.traiettoria(r, lump_eur, dep_netto, anni,
0.0 if differita else aliquota, PATRIM,
n_paths=N_PATHS, seed=SEED)
out = []
for x in righe:
n_dep = round((x["versato"] - LS.START - lump_eur * CC.EURUSD) / (dep_netto * CC.EURUSD)) \
if dep_netto > 0 else 0
versato_reale = LS.START + lump_eur * CC.EURUSD + n_dep * dep_eur * CC.EURUSD
med, p25, p75 = x["capitale_med"], x["capitale_p25"], x["capitale_p75"]
if differita:
netto = lambda c: c - aliquota * max(0.0, c - versato_reale) # noqa: E731
med, p25, p75 = netto(med), netto(p25), netto(p75)
out.append(dict(anno=x["anno"], versato=versato_reale, med=med, p25=p25, p75=p75,
guadagno=med - versato_reale, phit_book=x["p_traguardo"]))
return out
def bersaglio(r: np.ndarray, aliquota: float) -> tuple[float, float]:
"""(rendita perpetua, capitale necessario) per €50/giorno NETTI con l'aliquota di quel motore."""
lordo = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - aliquota)
_, perp, cap = WR.perp_and_wall(r, 1.0, lordo)
return perp, cap
# ------------------------------------------------------------------ main
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--lump", type=float, default=5000.0)
ap.add_argument("--dep", type=float, default=500.0)
ap.add_argument("--anni", type=int, default=15)
ap.add_argument("--json", type=str, default="")
a = ap.parse_args()
S = carica_serie()
motori = ["BOOK", "SP500", "WORLD"]
print("=" * 104)
print(" r0807 — BOOK vs S&P 500 vs MSCI WORLD, stesso piano di accumulo")
print("=" * 104)
print(f" piano: ${LS.START:,.0f} + €{a.lump:,.0f} + €{a.dep:,.0f}/mese · {a.anni} anni · "
f"{N_PATHS:,} path, blocchi 20g")
print(f" MSCI World = {S['_world_fonte']}")
print("\n (1) LE SERIE — griglia di calendario, 0.0 a borsa chiusa")
print(f" {'motore':8}{'barre':>8}{'da':>13}{'a':>13}{'anni':>7}{'drift':>9}{'vol':>8}"
f"{'Sharpe':>8}{'maxDD':>8}")
for m in motori:
x = metriche(S[m])
print(f" {m:8}{x['n']:>8}{x['da']:>13}{x['a']:>13}{x['anni']:>7.1f}"
f"{x['drift']:>8.1%}{x['vol']:>8.1%}{x['sharpe']:>8.2f}{x['maxdd']:>8.1%}")
# finestra comune: il confronto onesto sul REGIME
lo = max(S[m].index[0] for m in motori)
hi = min(S[m].index[-1] for m in motori)
print(f"\n (2) STESSA FINESTRA {lo.date()}{hi.date()} — toglie il vantaggio di aver scelto "
f"gli anni")
print(f" {'motore':8}{'anni':>7}{'drift':>9}{'vol':>8}{'Sharpe':>8}{'maxDD':>8}")
comune = {}
for m in motori:
comune[m] = S[m].loc[lo:hi]
x = metriche(comune[m])
print(f" {m:8}{x['anni']:>7.1f}{x['drift']:>8.1%}{x['vol']:>8.1%}{x['sharpe']:>8.2f}"
f"{x['maxdd']:>8.1%}")
# bersagli propri
print("\n (3) IL BERSAGLIO E' DIVERSO PER OGNI MOTORE (rendita perpetua + aliquota propria)")
print(f" {'motore':8}{'aliquota':>10}{'rendita perpetua':>19}{'capitale per €50/g netti':>27}")
berg = {}
for m in motori:
al = TAX_CRYPTO if m == "BOOK" else TAX_ETF
perp, cap = bersaglio(S[m].values.astype(float), al)
berg[m] = dict(perp=perp, cap=cap, aliquota=al)
cs = f"${cap:,.0f}" if np.isfinite(cap) else "non sostenibile"
print(f" {m:8}{al:>9.0%}{perp:>18.2%}{cs:>27}")
# bersaglio anche sulla finestra comune — altrimenti si confronta un accumulo di 7 anni
# con un bersaglio stimato su 30. ⚠️ Questa colonna e' la MENO affidabile delle due: un
# prelievo perpetuo e' una domanda a 20 anni e 7 anni di storia non la risolvono.
print("\n (3-bis) LO STESSO BERSAGLIO, ma stimato sulla sola finestra comune (poco "
"affidabile: 7 anni)")
print(f" {'motore':8}{'rendita perpetua':>19}{'capitale':>14}{'vs finestra piena':>20}")
berg_c = {}
for m in motori:
al = berg[m]["aliquota"]
perp, cap = bersaglio(comune[m].values.astype(float), al)
berg_c[m] = dict(perp=perp, cap=cap, aliquota=al)
rel = cap / berg[m]["cap"] - 1 if np.isfinite(cap) and np.isfinite(berg[m]["cap"]) else float("nan")
cs = f"${cap:,.0f}" if np.isfinite(cap) else "non sostenibile"
print(f" {m:8}{perp:>18.2%}{cs:>14}{rel:>19.0%}")
# accumulo, ciascuno col proprio regime
print(f"\n (4) ACCUMULO a {a.anni} anni — ciascuno col PROPRIO fisco e i propri costi")
print(f" {'motore':8}{'versato':>13}{'capitale med':>15}{'p25':>13}{'p75':>13}"
f"{'guadagno':>13}{'% del cap.':>12}")
traj = {}
for m in motori:
t = accumula(S[m].values.astype(float), a.lump, a.dep, a.anni,
differita=(m != "BOOK"),
aliquota=TAX_CRYPTO if m == "BOOK" else TAX_ETF,
comm_eur=0.0 if m == "BOOK" else COMM_EUR)
traj[m] = t
L = t[-1]
print(f" {m:8}{L['versato']:>12,.0f}${L['med']:>14,.0f}${L['p25']:>12,.0f}$"
f"{L['p75']:>12,.0f}${L['guadagno']:>12,.0f}${L['guadagno']/L['med']:>11.0%}")
# (4-bis) l'accumulo sulla STESSA finestra: e' il confronto che toglie il regime
print(f"\n (4-bis) LO STESSO ACCUMULO, ma ricampionando SOLO {lo.date()}{hi.date()} "
f"per tutti e tre")
print(f" {'motore':8}{'capitale med':>15}{'p25':>13}{'p75':>13}{'guadagno':>13}"
f"{'vs finestra piena':>20}")
traj_c = {}
for m in motori:
t = accumula(comune[m].values.astype(float), a.lump, a.dep, a.anni,
differita=(m != "BOOK"),
aliquota=TAX_CRYPTO if m == "BOOK" else TAX_ETF,
comm_eur=0.0 if m == "BOOK" else COMM_EUR)
traj_c[m] = t
L, F = t[-1], traj[m][-1]
print(f" {m:8}{L['med']:>14,.0f}${L['p25']:>12,.0f}${L['p75']:>12,.0f}$"
f"{L['guadagno']:>12,.0f}${L['med']/F['med']-1:>19.0%}")
print("\n (5) QUOTA DEL PROPRIO BERSAGLIO RAGGIUNTA (ognuno il suo)")
for m in motori:
L, c = traj[m][-1], berg[m]["cap"]
if np.isfinite(c) and c > 0:
print(f" {m:8} capitale ${L['med']:>10,.0f} · bersaglio ${c:>10,.0f}"
f" · {L['med'] / c:>6.0%}")
else:
print(f" {m:8} capitale ${L['med']:>10,.0f} · bersaglio non sostenibile")
# sensibilita' del proxy World: la quota USA dell'indice e' cambiata molto nel tempo
if "PROXY" in S["_world_fonte"]:
spy, efa = _calendar_daily(EQ.load_eq("SPY")["close"]), _calendar_daily(EQ.load_eq("EFA")["close"])
j = pd.concat({"SPY": spy, "EFA": efa}, axis=1).dropna()
print("\n (6) SENSIBILITA' DEL PROXY — la quota USA di MSCI World e' passata da ~50% "
"(2008) a ~72% (oggi)")
print(f" {'peso USA':>10}{'drift':>9}{'vol':>8}{'Sharpe':>8}{'maxDD':>8}")
for w in (0.5, 0.6, 0.7, 0.8):
x = metriche(w * j["SPY"] + (1 - w) * j["EFA"])
print(f" {w:>9.0%}{x['drift']:>8.1%}{x['vol']:>8.1%}{x['sharpe']:>8.2f}{x['maxdd']:>8.1%}")
if a.json:
Path(a.json).write_text(json.dumps(dict(
piano=dict(lump=a.lump, dep=a.dep, anni=a.anni, start=LS.START, eurusd=CC.EURUSD,
n_paths=N_PATHS, comm_eur=COMM_EUR, tax_crypto=TAX_CRYPTO,
tax_etf=TAX_ETF, patrim=PATRIM, world=S["_world_fonte"]),
metriche={m: metriche(S[m]) for m in motori},
metriche_comune={m: metriche(comune[m]) for m in motori},
finestra_comune=dict(da=str(lo.date()), a=str(hi.date())),
bersaglio={m: berg[m] for m in motori},
bersaglio_comune={m: berg_c[m] for m in motori},
traiettoria=traj, traiettoria_comune=traj_c), indent=1))
print(f"\n scritto {a.json}")
if __name__ == "__main__":
main()