Files
PythagorasGoal/scripts/research/r0822d_piano_vero.py
T

696 lines
38 KiB
Python

#!/usr/bin/env python
"""r0822d_piano_vero.py — LA TABELLA DEL PIANO CON TUTTE LE CORREZIONI DENTRO, UNA VOLTA SOLA.
IL BUCO. L'obiettivo dichiarato del progetto e' 50 EUR/giorno e la domanda operativa e' «quanto
ci vuole, versando quanto». La tabella pubblicata che risponde e' stata corretta DUE VOLTE, ogni
volta partendo dallo stesso numero LORDO e mai dall'altra correzione:
riga «250 EUR/mese», P(traguardo entro 20 anni)
* 92% al LORDO (26/07, fattore d'ancora x0.89)
* 52% col FISCO d'accumulo (07/08, r0807_piano_netto)
* 64% col FUNDING (22/08, r0822d_funding)
* ??? con ENTRAMBI <- non esiste
Le due lenti sono state applicate SEPARATAMENTE. Questo script produce la lente unica, coerente,
con dentro tutte le correzioni MISURATE (nessuna scelta a mano), e dice quanto ci vuole davvero.
L'ORDINE DI APPLICAZIONE, e perche' quello.
1. FUNDING — e' un costo sul rendimento LORDO: si paga sul nozionale, ogni ora, prima che
esista una plusvalenza. Entra DENTRO la serie dei ritorni, giorno per giorno
(`r0822d_funding.book_75_25(rate)`), non come haircut a valle.
2. ANCORA x0.89 — e' una correzione sul DRIFT della serie (fortuna di calendario misurata,
r0726_deluck_factor). Si applica alla serie, dopo il funding, come fa
`r0822d_funding.sezione4c` — cosi' i numeri restano confrontabili con quelli pubblicati.
La convenzione alternativa (de-luckare il lordo e POI togliere il funding) e' riportata
come sensibilita': vale 0.24pp di drift.
3. FISCO — tassa la PLUSVALENZA, a fine anno, sul percorso GIA' netto di funding. L'ordine
inverso (tassare la plusvalenza lorda e poi sottrarre il funding dal capitale gia' tassato)
farebbe pagare il 33% su un guadagno mai incassato.
Da qui l'effetto di SECONDO ORDINE che la somma delle due correzioni non contiene: meno drift
-> meno plusvalenza -> meno imposta. Il congiunto e' meno severo della somma, e di quanto e'
una MISURA (sezione 3), non un'assunzione.
COSA NON C'E' DENTRO, DI PROPOSITO. Il rischio di venue. Non e' un costo, e' un rischio di
ROVINA: a p=5% annuo il capitale mediano a 20 anni e' ZERO per qualunque calendario di
versamenti (26/07). Sta su un asse separato e ci resta; metterlo qui trasformerebbe una tabella
di pianificazione in una tabella di sopravvivenza.
ASSUNZIONI FISCALI, dichiarate e NON un parere fiscale (le stesse del 27/07 e del 07/08):
33% sulle plusvalenze cripto realizzate, minusvalenze in carry 4 anni, patrimoniale 0.2%/anno
sul valore. Il modello tassa la variazione ANNUA di valore = limite superiore, stretto perche'
il libro realizza quasi tutto entro l'anno.
TRAPPOLE GIA' PAGATE DA QUESTO PROGETTO, evitate qui per costruzione:
* un NON-ARRIVO si codifica +inf, mai -1 (con -1 la mediana MIGLIORA quando la colonna
peggiora: errore commesso il 07/08). Qui si stampano DUE mediane: quella incondizionata
(non-arrivo = +inf, quindi «>25a») e quella condizionata, che non compare MAI senza la sua
probabilita' accanto.
* un Monte Carlo ha una RISOLUZIONE: misurata su 3 semi e dichiarata (sezione 2).
* la serie del libro e' su giorni di CALENDARIO (cripto, 365/anno): l'annualizzazione a 365 e'
corretta e `to_daily()` non serve. Sarebbe servito su una serie di giorni di borsa.
uv run python scripts/research/r0822d_piano_vero.py
uv run python scripts/research/r0822d_piano_vero.py --rebuild # ignora la cache di serie
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT))
import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths, book_series
import r0726_capwall_refresh as WR # noqa: E402 perp_and_wall, trajectory (2000 path)
import r0727_lumpsum_split as LS # noqa: E402 TARGET pubblicato
import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI
import r0807_piano_netto as PN # noqa: E402 passo_fiscale, sopravvivenza, accumula
# ---------------------------------------------------------------------------
# Costanti dichiarate
# ---------------------------------------------------------------------------
DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio
START = 635.0 # il conto Deribit VERO di oggi (le tabelle pubblicate usano 600)
START_PUB = 600.0 # solo per la replica
N_WALL = 4000 # path per il muro (la taglia a cui furono calcolati i pubblicati)
N_TRAJ = 3000 # path per le traiettorie (idem, seed 725)
N_DEP = 3000 # path per la bisezione sul versamento
BLOCK = 20
SEED_TRAJ = 725
SEED_DEP = 20260822
SEED_WALL = PN.SEED_MURO # 725
ANNI_TRAJ = 25 # 20 troncherebbe la coda: convenzione di WR.trajectory
VERSAMENTI = (0, 250, 500, 800, 1_000, 2_000)
LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0)
ORIZZONTI = (10, 15, 20)
CONFIDENZE = (0.50, 0.75, 0.90)
VINTAGE_PUB = "2026-08-07" # la data in cui furono calcolati $272.061 / $258.338
COSTO_STRUMENTO_VERO = 0.013931 # -1.39%/anno: funding misurato sulla sola finestra >= 2022-03
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero")
if not CACHE.parent.parent.exists():
CACHE = Path("/tmp/pythagoras_pianovero")
REBUILD = "--rebuild" in sys.argv
T0 = time.time()
def sez(t: str) -> None:
print("\n" + "=" * 100)
print(f" {t}")
print("=" * 100)
def clock() -> str:
return f"[{time.time() - T0:5.0f}s]"
# ===========================================================================
# 0) LE SERIE
# ===========================================================================
def costruisci_serie() -> pd.DataFrame:
"""Le due serie giornaliere del libro live 75/25: senza e con funding.
Nessuna re-implementazione: `r0822d_funding.book_75_25` e' la stessa lente di
`CC.book_series('hourly')` e la replica bit-exact e' verificata qui sotto.
"""
f = CACHE / "book.parquet"
if f.exists() and not REBUILD:
return pd.read_parquet(f)
# Se la cache dello storico funding c'e', si usa quella: nessuna chiamata al venue (il
# progetto gira su una VPS con soldi veri e un rate limit Deribit per-IP gia' saturato una
# volta, 29/07). Se NON c'e', si lascia che `fetch_funding` la scarichi: cosi' lo script
# gira anche da solo, su una macchina pulita.
for c in (Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund"),
Path("/tmp/pythagoras_funding_cache")):
if any(c.glob("*.parquet")):
sys.argv.append("--no-net")
break
import r0822d_funding as FD # noqa: E402 (USE_NET si decide all'import)
F = {}
for instr, (start, _asset, _kind) in FD.INSTR.items():
d = FD.fetch_funding(instr, start).copy()
d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True)
F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt")
rate = FD.build_rate(F)
A, B = FD.book_75_25(None), FD.book_75_25(rate)
out = pd.DataFrame({"nofund": A, "fund": B.reindex(A.index)})
CACHE.mkdir(parents=True, exist_ok=True)
out.to_parquet(f)
return out
def deluck(r: np.ndarray) -> np.ndarray:
"""Toglie dal DRIFT la fortuna d'ancora misurata. La vol non si tocca: fra le ancore era
invariata (7.80% -> 7.76%), la fortuna sta tutta nel drift (r0726_deluck_factor)."""
r = np.asarray(r, float)
return r - (1.0 - DELUCK) * r.mean()
# ===========================================================================
# LE LENTI
# ===========================================================================
class Lente:
"""Una lente = (serie di ritorni, convenzione di prelievo, regime fiscale d'accumulo).
`lordizza=True` -> convenzione PUBBLICATA: il prelievo si lordizza al 33% e il capitale
investito non paga mai imposte. Incoerente (r0807), tenuta per replica.
`lordizza=False` -> convenzione COERENTE: imposte ogni anno dentro il portafoglio, prelievo
gia' netto.
"""
def __init__(self, nome: str, r: np.ndarray, lordizza: bool, nota: str = ""):
self.nome, self.r, self.lordizza, self.nota = nome, np.asarray(r, float), lordizza, nota
self.aliq = 0.0 if lordizza else CC.TAX_RATE
self.patr = 0.0 if lordizza else TX.PATRIMONIALE
self.perp = float("nan")
self.muro = float("nan")
@property
def prelievo(self) -> float:
netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD
return netto / (1 - CC.TAX_RATE) if self.lordizza else netto
@property
def drift(self) -> float:
return float(self.r.mean()) * 365.0
@property
def vol(self) -> float:
return float(self.r.std()) * 365.0 ** 0.5
def muro_di(lente: Lente, n_paths: int = N_WALL, seed: int = SEED_WALL) -> tuple[float, float]:
"""(rendita perpetua, capitale necessario). Perpetua = il prelievo piu' alto con
P(capitale a 20 anni >= capitale iniziale) >= 90% — definizione del 25/07, invariata.
Bisezione identica a `PN.muro`, esposta solo per poter cambiare SEME e misurare la
risoluzione Monte Carlo. Il controllo positivo (a seme 725 deve dare esattamente `PN.muro`)
e' in sezione 2.
"""
lo, hi = 0.0, 0.40
for _ in range(13):
mid = (lo + hi) / 2
s = PN.sopravvivenza(lente.r, 1e6, 1e6 * mid, 20, lente.aliq, lente.patr,
n_paths=n_paths, seed=seed)
if s["p10_end"] >= 1e6:
lo = mid
else:
hi = mid
return lo, (lente.prelievo / lo if lo > 0.002 else float("inf"))
# ===========================================================================
# lettura onesta di un vettore di primi passaggi
# ===========================================================================
def leggi(colpito: np.ndarray, anni_sim: int = ANNI_TRAJ) -> dict:
"""Mediana INCONDIZIONATA (non-arrivo = +inf) + mediana condizionata + P(20 anni).
⚠️ Il non-arrivo e' +inf, MAI -1: con -1 la mediana scende quanto peggio va la colonna
(errore commesso il 07/08 e corretto lo stesso giorno). E la mediana condizionata non si
stampa mai da sola: descrive solo chi arriva.
"""
y = np.where(colpito >= 0, colpito / 365.0, np.inf)
arr = y[np.isfinite(y)]
return dict(
med=float(np.median(y)), # +inf se meno di meta' arriva
med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"),
p20=float((y <= 20.0).mean()),
p_arr=float(np.isfinite(y).mean()),
anni_sim=anni_sim)
def fmt_med(d: dict) -> str:
m = d["med"]
return f">{d['anni_sim']}a" if not np.isfinite(m) else f"{m:.1f}a"
def fmt_cond(d: dict) -> str:
c = d["med_cond"]
return "—" if np.isnan(c) else f"{c:.1f}a"
# ===========================================================================
# bisezione sul versamento (start parametrico: le tabelle pubblicate partono da $600)
# ===========================================================================
def dep_per_conf(paths: np.ndarray, lente: Lente, conf: float, start: float,
lump_eur: float = 0.0) -> tuple[float, float]:
"""(versamento mensile EUR minimo per P(traguardo entro l'orizzonte) >= conf, totale versato).
I path si estraggono UNA volta e si riusano a ogni passo: con campioni diversi per passo la
funzione non sarebbe monotona e la bisezione oscillerebbe (lezione WR.solve_deposit).
"""
def p(dep: float) -> float:
c = PN.accumula(paths, dep, lente.muro, lente.aliq, lente.patr,
start=start, lump_eur=lump_eur)["colpito"]
return float((c >= 0).mean())
lo, hi = 0.0, 6_000.0
if p(hi) < conf:
return float("inf"), float("nan")
for _ in range(12):
mid = (lo + hi) / 2
if p(mid) >= conf:
hi = mid
else:
lo = mid
versato = PN.accumula(paths, hi, lente.muro, lente.aliq, lente.patr,
start=start, lump_eur=lump_eur)["versato"]
return hi, versato
# ===========================================================================
def main() -> None:
print("=" * 100)
print(" r0822d — IL PIANO VERO: fisco d'accumulo E funding nella STESSA tabella")
print("=" * 100)
B = costruisci_serie()
idx = pd.DatetimeIndex(B.index)
A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float)
print(f"\n serie giornaliera del libro LIVE (TP01 75 + SKH01 25, lente 'hourly', path live)")
print(f" {len(B)} giorni, dal {idx.min().date()} al {idx.max().date()} {clock()}")
print(f" drift lordo {A_raw.mean() * 365:.3%} · drift netto funding {F_raw.mean() * 365:.3%}")
print(" ⚠️ giorni di CALENDARIO (cripto): annualizzazione a 365 corretta, `to_daily()` non")
print(" serve qui — servirebbe su una serie di giorni di borsa (lezione 25/07 su GTAA01).")
# =====================================================================
sez("1) CONTROLLO DI REPLICA — prima di pubblicare un numero nuovo, riprodurre i vecchi")
print("\n Regola del progetto (07/08): «prima di pubblicare un numero nuovo, far riprodurre")
print(" alla macchina quello vecchio». Cinque controlli, tutti su codice IMPORTATO.\n")
ok = {}
# (R1) la serie senza funding e' bit-exact la serie del libro gia' pubblicata
C = CC.book_series("hourly")
d = float(np.max(np.abs(A_raw - C.reindex(idx).values)))
ok["R1"] = d == 0.0
print(f" R1 serie 'senza funding' vs CC.book_series('hourly') max|dif| = {d:.3e} "
f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}")
# (R2) il costo del funding per finestra
print(f"\n R2 costo del funding per finestra (pubblicato 22/08: -2,16% / -1,39% / -0,55%)")
fin = [("piena (proxy inverse pre-2022)", None, -0.0216),
(">= 2022-03-15 (strumento vero)", "2022-03-15", -0.0139),
(">= 2025-01-01", "2025-01-01", -0.0055)]
r2 = True
for lab, lo, atteso in fin:
m = np.ones(len(B), bool) if lo is None else np.asarray(idx >= pd.Timestamp(lo, tz="UTC"))
got = -(A_raw[m].mean() - F_raw[m].mean()) * 365
r2 &= abs(got - atteso) < 5e-4
print(f" {lab:<34} n={m.sum():>5} misurato {got:+.4%} atteso {atteso:+.2%}")
ok["R2"] = r2
# (R3-R4) i muri pubblicati, sul VINTAGE su cui furono calcolati
print(f"\n R3/R4 i muri pubblicati ($272.061 lordo / $258.338 netto fisco) sul vintage")
print(f" {VINTAGE_PUB} — la data in cui furono calcolati.")
mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC"))
Lv0 = Lente("vintage LORDO", deluck(A_raw[mv]), True)
Lv1 = Lente("vintage +FISCO", deluck(A_raw[mv]), False)
Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0)
Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr)
ok["R3"] = abs(Lv0.muro - LS.TARGET) < 1.0
ok["R4"] = abs(Lv1.muro - 258_338.0) < 1.0
print(f" LORDO perpetua {Lv0.perp:.4%} muro ${Lv0.muro:,.0f} atteso $272,061 "
f"{'ESATTO AL DOLLARO' if ok['R3'] else 'DIVERSO'}")
print(f" NETTO perpetua {Lv1.perp:.4%} muro ${Lv1.muro:,.0f} atteso $258,338 "
f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'} {clock()}")
# (R5) i numeri del funding, alla SUA taglia MC (2000 path)
print(f"\n R5 i numeri del filone funding (22/08), alla sua taglia dichiarata (2000 path):")
g = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
rA, rF = deluck(A_raw), deluck(F_raw)
_, pA, wA = WR.perp_and_wall(rA, 1.0, g)
_, pF, wF = WR.perp_and_wall(rF, 1.0, g)
tA = WR.trajectory(rA, wA, n_paths=2000)[250]
tF = WR.trajectory(rF, wF, n_paths=2000)[250]
ok["R5"] = abs(wF / wA - 1 - 0.175) < 0.004 and abs(pF - 0.0914) < 3e-4
print(f" perpetua {pA:.2%} -> {pF:.2%} (pubblicato 10,73% -> 9,14%)")
print(f" muro ${wA:,.0f} -> ${wF:,.0f} = {wF / wA - 1:+.1%} (pubblicato $276,6k -> "
f"$325,0k = +17,5%)")
print(f" traiettoria EUR250/mese {tA['p50']:.1f}a P20 {tA['p20y']:.0%} -> "
f"{tF['p50']:.1f}a P20 {tF['p20y']:.0%} (pubblicato 16,4a/90% -> 18,7a/64%) {clock()}")
# (R6) la colonna LORDA e la colonna NETTA delle traiettorie, sul vintage, da $600
print(f"\n R6 le due colonne di traiettoria pubblicate, sul vintage {VINTAGE_PUB}, da $600:")
rng = np.random.default_rng(SEED_TRAJ)
pv = CC._boot_paths(Lv0.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng)
att_l = {250: 16.3, 500: 12.4, 800: 10.0, 1_000: 9.0, 2_000: 6.0}
att_n = {250: 19.8, 500: 14.7, 800: 11.4, 1_000: 10.0, 2_000: 6.4}
print(f" {'EUR/m':>7}{'LORDO mis.':>13}{'atteso':>9}{'P20':>7}"
f"{' |':>3}{'NETTO mis.':>13}{'atteso':>9}{'P20':>7}")
r6 = True
for dep in (250, 500, 800, 1_000, 2_000):
l = leggi(PN.accumula(pv, dep, Lv0.muro, 0.0, 0.0, start=START_PUB)["colpito"])
n = leggi(PN.accumula(pv, dep, Lv1.muro, Lv1.aliq, Lv1.patr, start=START_PUB)["colpito"])
r6 &= abs(l["med_cond"] - att_l[dep]) < 0.25 and abs(n["med_cond"] - att_n[dep]) < 0.25
print(f" {dep:>7}{fmt_cond(l):>13}{att_l[dep]:>9.1f}{l['p20']:>7.0%}"
f"{' |':>3}{fmt_cond(n):>13}{att_n[dep]:>9.1f}{n['p20']:>7.0%}")
ok["R6"] = r6
del pv
print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} — "
+ ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items()))
if not all(ok.values()):
raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.")
print("""
⚠️ E IL FATTO CHE VA DETTO: la replica e' esatta AL DOLLARO solo sul vintage del 07/08.
Su TUTTA la serie di oggi lo stesso codice, senza una riga cambiata, da' muri diversi:""")
L0t = Lente("oggi LORDO", rA, True)
L0t.perp, L0t.muro = PN.muro(L0t.r, L0t.prelievo, 0.0, 0.0)
print(f" muro LORDO: ${Lv0.muro:,.0f} (vintage {VINTAGE_PUB}, {mv.sum()} giorni) -> "
f"${L0t.muro:,.0f} (oggi, {len(B)} giorni) = {L0t.muro / Lv0.muro - 1:+.1%}")
print(" `data/raw/` e' gitignored e il cron lo allunga ogni notte: 15 giorni in piu' di")
print(" dati spostano il muro del ~2%, cioe' PIU' della risoluzione Monte Carlo. E' la")
print(" stessa lezione del 07/08 sul gate di GTAA01 (un numero cambia senza che il codice")
print(" cambi = i dati non sono versionati). Percio' qui sotto ogni confronto fra lenti e'")
print(" fatto sullo STESSO vintage: un grado di liberta' per volta.")
# =====================================================================
sez("2) LE QUATTRO LENTI E IL MURO — la riga che non esisteva")
print("""
Tutte de-luckate x0.89. Le prime due sono le lenti pubblicate; la terza e' quella del filone
funding di stanotte; la quarta e' la lente unica con dentro TUTTO.""")
Lo = {
"L0 LORDO (26/07)": Lente("L0", rA, True, "prelievo lordizzato, nessuna imposta in accumulo"),
"L1 +FISCO (07/08)": Lente("L1", rA, False, "imposte ogni anno nel portafoglio"),
"L2 +FUNDING (22/08)": Lente("L2", rF, True, "funding esatto, convenzione pubblicata"),
"L3 CONGIUNTA": Lente("L3", rF, False, "funding esatto + fisco coerente"),
}
rFb = deluck(A_raw - COSTO_STRUMENTO_VERO / 365.0)
Lo["L3b CONGIUNTA (str. vero)"] = Lente("L3b", rFb, False,
"funding -1,39%/anno (finestra >= 2022-03)")
print(f"\n {'lente':<27}{'drift':>9}{'vol':>8}{'prelievo/a':>13}{'perpetua':>10}{'muro':>13}")
for k, L in Lo.items():
L.perp, L.muro = muro_di(L)
print(f" {k:<27}{L.drift:>9.2%}{L.vol:>8.2%}{L.prelievo:>13,.0f}"
f"{L.perp:>10.2%}{L.muro:>13,.0f}")
print(f" {clock()}")
# controllo positivo sulla bisezione con seme + risoluzione Monte Carlo
p_ctrl, w_ctrl = PN.muro(Lo["L3 CONGIUNTA"].r, Lo["L3 CONGIUNTA"].prelievo,
CC.TAX_RATE, TX.PATRIMONIALE)
same = abs(w_ctrl - Lo["L3 CONGIUNTA"].muro) < 1e-6
print(f"\n controllo positivo: `muro_di` a seme {SEED_WALL} == `PN.muro` importato "
f"${w_ctrl:,.0f} vs ${Lo['L3 CONGIUNTA'].muro:,.0f} {'OK' if same else 'DIVERGE'}")
semi = [Lo["L3 CONGIUNTA"].muro]
for s in (2026, 8_22):
semi.append(muro_di(Lo["L3 CONGIUNTA"], seed=s)[1])
sp = (max(semi) - min(semi)) / np.mean(semi)
print(f" RISOLUZIONE MONTE CARLO del muro, misurata su 3 semi a {N_WALL} path: "
+ " / ".join(f"${x:,.0f}" for x in semi))
print(f" ampiezza {sp:.1%} -> si citano TRE cifre significative (${Lo['L3 CONGIUNTA'].muro/1000:.0f}k), "
f"non il dollaro. {clock()}")
# sensibilita' sull'ordine de-luck / funding
alt = Lente("alt", deluck(A_raw) - (A_raw - F_raw).mean(), False)
alt.perp, alt.muro = muro_di(alt)
print(f"\n sensibilita' all'ORDINE de-luck/funding (convenzione alternativa: de-luckare il")
print(f" drift LORDO e poi togliere il funding): drift {alt.drift:.2%} contro "
f"{Lo['L3 CONGIUNTA'].drift:.2%}, muro ${alt.muro:,.0f} contro "
f"${Lo['L3 CONGIUNTA'].muro:,.0f} ({alt.muro / Lo['L3 CONGIUNTA'].muro - 1:+.1%}).")
print(" Sotto la risoluzione MC: la scelta di convenzione non decide niente. Si tiene quella")
print(" di `r0822d_funding.sezione4c`, per confrontabilita' con i numeri gia' pubblicati.")
# =====================================================================
sez("3) LE DUE CORREZIONI SI SOMMANO? — l'effetto di secondo ordine, misurato")
print("""
ATTESA DICHIARATA PRIMA DI MISURARE: meno drift -> meno plusvalenza -> meno imposta, quindi
il congiunto dovrebbe essere MENO severo della somma delle due correzioni prese da sole.
Misurato come interazione di un fattoriale 2x2 (funding OFF/ON x fisco OFF/ON) in TRE monete,
perche' un'interazione piccola puo' cambiare SEGNO a seconda di come la si misura.""")
# (3a) accumulo puro: il capitale a 20 anni non dipende dal muro -> isola il meccanismo
print("\n (3a) SUL CAPITALE ACCUMULATO — la domanda pura sul meccanismo.")
print(" Capitale mediano a 20 anni, EUR250/mese, partenza $%.0f. Non dipende dal muro:" % START)
print(" qui dentro c'e' SOLO l'accumulo, cioe' l'unico posto dove il meccanismo vive.")
capf = {}
for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"):
L = Lo[k]
rr = np.random.default_rng(SEED_DEP)
pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr)
capf[k] = float(np.median(PN.accumula(pp, 250, L.muro, L.aliq, L.patr,
start=START)["cap"]))
del pp
print(f"\n {'':<14}{'fisco OFF':>16}{'fisco ON':>16}{'rapporto ON/OFF':>19}")
for lab, a, b in (("funding OFF", "L0 LORDO (26/07)", "L1 +FISCO (07/08)"),
("funding ON ", "L2 +FUNDING (22/08)", "L3 CONGIUNTA")):
print(f" {lab:<14}{capf[a]:>16,.0f}{capf[b]:>16,.0f}"
f"{capf[b] / capf[a]:>19.4f}")
dragOFF = capf["L1 +FISCO (07/08)"] / capf["L0 LORDO (26/07)"]
dragON = capf["L3 CONGIUNTA"] / capf["L2 +FUNDING (22/08)"]
inter_log = (np.log(capf["L3 CONGIUNTA"]) - np.log(capf["L1 +FISCO (07/08)"])
- np.log(capf["L2 +FUNDING (22/08)"]) + np.log(capf["L0 LORDO (26/07)"]))
verso_drag = "MENO" if dragON > dragOFF else "DI PIU'"
print(f"\n Il fisco toglie il {1 - dragOFF:.1%} del capitale a funding OFF e il "
f"{1 - dragON:.1%} a funding ON.")
esito_mecc = ("ESISTE ed e' nel verso previsto" if dragON > dragOFF else "NON si vede")
print(f" -> il meccanismo {esito_mecc}: con meno drift il fisco morde {verso_drag}.")
print(f" Interazione moltiplicativa (log-capitale): {inter_log:+.4f} = "
f"{np.expm1(inter_log):+.2%} di capitale.")
print(" ⚠️ Ma e' UNA FRAZIONE: il fisco resta la correzione dominante in entrambe le")
print(" colonne, e la 'compensazione' non ne recupera che una parte trascurabile.")
# (3b) il muro
inter_w = (Lo["L3 CONGIUNTA"].muro - Lo["L1 +FISCO (07/08)"].muro
- Lo["L2 +FUNDING (22/08)"].muro + Lo["L0 LORDO (26/07)"].muro)
print("\n (3b) SUL MURO — la fase di prelievo.")
print(f" ${Lo['L0 LORDO (26/07)'].muro:,.0f} (L0) · ${Lo['L1 +FISCO (07/08)'].muro:,.0f} (L1) · "
f"${Lo['L2 +FUNDING (22/08)'].muro:,.0f} (L2) · ${Lo['L3 CONGIUNTA'].muro:,.0f} (L3)")
print(f" interazione ${inter_w:+,.0f} = {abs(inter_w) / Lo['L3 CONGIUNTA'].muro:.2%} del muro "
f"congiunto, cioe' SOTTO la risoluzione Monte Carlo ({sp:.1%}): indistinguibile da zero.")
# (3c) il versamento necessario
print("\n (3c) SUL VERSAMENTO NECESSARIO — la moneta in cui si decide.")
print(f" Orizzonte 20 anni, P=90%, partenza ${START:.0f}. Ogni lente col PROPRIO muro.")
# ⚠️ ogni lente ha la SUA serie di ritorni: i path si riestraggono per lente (stesso seme,
# quindi stessi INDICI di blocco: il confronto fra lenti resta appaiato) e non si riusano.
dep90 = {}
for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"):
L = Lo[k]
rr = np.random.default_rng(SEED_DEP)
pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr)
dep90[k], _ = dep_per_conf(pp, L, 0.90, START)
del pp
b = dep90["L0 LORDO (26/07)"]
d_fisco = dep90["L1 +FISCO (07/08)"] - b
d_fund = dep90["L2 +FUNDING (22/08)"] - b
d_joint = dep90["L3 CONGIUNTA"] - b
inter = d_joint - d_fisco - d_fund
print(f"\n {'lente':<27}{'EUR/mese per P=90% a 20 anni':>32}{'vs L0':>10}")
for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"):
print(f" {k:<27}{dep90[k]:>32,.0f}{dep90[k] - b:>+10,.0f}")
print(f"\n somma ingenua : {d_fisco:+,.0f} {d_fund:+,.0f} = {d_fisco + d_fund:+,.0f} EUR/mese")
print(f" MISURATO : {d_joint:+,.0f} EUR/mese")
print(f" INTERAZIONE : {inter:+,.0f} EUR/mese ({inter / (d_fisco + d_fund):+.0%} della somma)")
# ⚠️ un'interazione piccola e' una DIFFERENZA DI DIFFERENZE: prima di dichiararne il SEGNO
# va misurato il rumore Monte Carlo che la circonda, o si pubblica un segno di rumore.
inters = [inter]
for sd in (4_242, 90_909):
dd = {}
for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"):
L = Lo[k]
rr = np.random.default_rng(sd)
pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr)
dd[k], _ = dep_per_conf(pp, L, 0.90, START)
del pp
inters.append(dd["L3 CONGIUNTA"] - dd["L1 +FISCO (07/08)"]
- dd["L2 +FUNDING (22/08)"] + dd["L0 LORDO (26/07)"])
segno_ok = all(x > 0 for x in inters) or all(x < 0 for x in inters)
et_segno = "STABILE" if segno_ok else "NON stabile: e' rumore"
print(" su 3 semi: " + " / ".join(f"{x:+,.0f}" for x in inters)
+ f" -> segno {et_segno}"
+ f" (bisezione a {6000 / 2 ** 12:.1f} EUR/m di risoluzione)")
verso = "MENO" if np.median(inters) < 0 else "PIU'"
print(f"\n 📌 ESITO, e la mia attesa era SBAGLIATA nella moneta che conta. Il meccanismo")
print(f" esiste (3a: il fisco morde meno dove il drift e' minore) ma nel VERSAMENTO")
if segno_ok:
print(f" necessario il congiunto e' {verso} severo della somma ({np.median(inters):+,.0f} EUR/m,")
print(" segno stabile su 3 semi): la funzione «versamento -> probabilita'» e' convessa")
print(" e ribalta il segno di un'interazione gia' piccola.")
else:
print(f" necessario l'interazione NON ha segno stabile fra i semi ({inters[0]:+,.0f} / "
f"{inters[1]:+,.0f} / {inters[2]:+,.0f} EUR/m):")
print(" e' rumore Monte Carlo, e va detto invece di pubblicarne il segno.")
print(" Sul MURO l'interazione e' sotto la risoluzione MC.")
print(f" REGOLA OPERATIVA: NON contare su una compensazione fra le due correzioni —")
print(f" si sommano, e il congiunto resta {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due")
print(f" presa da sola. {clock()}")
sez(f"4) TRAIETTORIA da ${START:.0f} — anni al capitale-rendita, nelle quattro lenti")
print(f"""
Partenza ${START:.0f} = il conto Deribit VERO (le tabelle pubblicate partono da $600).
Ogni lente usa il PROPRIO muro. {N_TRAJ} path, blocchi da {BLOCK} giorni, orizzonte {ANNI_TRAJ} anni.
DUE mediane: 'mediana' = incondizionata (non-arrivo = +inf -> '>{ANNI_TRAJ}a'), 'cond.' = solo su chi
arriva — e la seconda non si legge senza la P accanto.""")
traj = {}
for k in Lo:
L = Lo[k]
rr = np.random.default_rng(SEED_TRAJ)
pp = CC._boot_paths(L.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr)
traj[k] = {dep: leggi(PN.accumula(pp, dep, L.muro, L.aliq, L.patr,
start=START)["colpito"]) for dep in VERSAMENTI}
del pp
hdr = f" {'EUR/m':>7}"
for k in Lo:
hdr += f"{k.split()[0]:>22}"
print("\n" + hdr)
print(f" {'':>7}" + "".join(f"{'mediana cond. P20':>22}" for _ in Lo))
for dep in VERSAMENTI:
row = f" {dep:>7}"
for k in Lo:
d = traj[k][dep]
row += f"{fmt_med(d):>9}{fmt_cond(d):>7}{d['p20']:>6.0%}"
print(row)
print(f" {clock()}")
d250 = {k: traj[k][250] for k in Lo}
print(f"""
📌 LA RIGA CHE MANCAVA. «EUR250/mese» P(traguardo entro 20 anni):
L0 LORDO {d250['L0 LORDO (26/07)']['p20']:>5.0%} (pubblicato 92% — qui {d250['L0 LORDO (26/07)']['p20']:.0%} su serie piu' lunga e da $635)
L1 +FISCO {d250['L1 +FISCO (07/08)']['p20']:>5.0%} (pubblicato 52%)
L2 +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:>5.0%} (pubblicato 64%)
L3 CONGIUNTA {d250['L3 CONGIUNTA']['p20']:>5.0%} <- IL NUMERO ONESTO
L3b str. vero {d250['L3b CONGIUNTA (str. vero)']['p20']:>5.0%} (col funding misurato solo dove lo strumento esiste)
⚠️ Le due colonne di funding sono una BANDA, non una scelta: -2,16%/anno usa il proxy
inverse sul 40% del campione (conservativa), -1,39%/anno usa solo la finestra in cui il
perpetual lineare USDC esiste davvero. La verita' sta fra 14% e 26%, non su un numero.
P(traguardo entro i {ANNI_TRAJ} anni simulati), stessa riga: L3 {d250['L3 CONGIUNTA']['p_arr']:.0%} · L3b {d250['L3b CONGIUNTA (str. vero)']['p_arr']:.0%}
— cioe' anche allungando di 5 anni l'orizzonte una quota non trascurabile non arriva.""")
# --- il lump: i soldi gia' fermi ---
sez(f"4-bis) E SE PARTO CON UN VERSAMENTO INIZIALE — ${START:.0f} + un lump, lente CONGIUNTA")
print(f"""
Il progetto ha misurato (27/07) che EUR10.000 fermi oggi valgono 2,45x, ma quel lump non e'
MAI entrato nelle tabelle nette. Qui e' nella lente congiunta. Lo stesso muro, gli stessi path.""")
L3 = Lo["L3 CONGIUNTA"]
rr = np.random.default_rng(SEED_TRAJ)
p3 = CC._boot_paths(L3.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr)
print(f"\n {'lump EUR':>10}" + "".join(f"{f'EUR{d}/mese':>26}" for d in (0, 250, 500)))
print(f" {'':>10}" + "".join(f"{'mediana cond. P20':>26}" for _ in range(3)))
for lump in LUMPS_EUR:
row = f" {lump:>10,.0f}"
for dep in (0, 250, 500):
d = leggi(PN.accumula(p3, dep, L3.muro, L3.aliq, L3.patr,
start=START, lump_eur=lump)["colpito"])
row += f"{fmt_med(d):>11}{fmt_cond(d):>8}{d['p20']:>7.0%}"
print(row)
del p3
print(f" {clock()}")
print("\n ⚠️ Un lump non e' capitale disponibile se e' il fondo d'emergenza (nota 27/07),")
print(" e a EUR10.000 sul conto si riapre la decisione di venue (soglia dichiarata $20k,")
print(" ma il piano cambia -> CLAUDE.md dice di riaprire PRIMA, non dopo).")
# =====================================================================
sez("5) QUANTO VERSARE PER UN ORIZZONTE DATO — lente CONGIUNTA, e il totale versato accanto")
print(f"""
bersaglio ${L3.muro:,.0f} (lente congiunta) · partenza ${START:.0f} · nessun lump
La colonna 'tot. versato' e' il confronto che conta: quanto del bersaglio lo COMPRI coi
bonifici invece di farlo produrre alla strategia.""")
L1 = Lo["L1 +FISCO (07/08)"]
print(f"\n {'orizzonte':>10}" + "".join(f"{f'P={c:.0%}':>13}" for c in CONFIDENZE)
+ f"{'tot.versato@90%':>18}{'% bersaglio':>13}{' | solo fisco @90%':>21}")
riga10 = None
for h in ORIZZONTI:
rr = np.random.default_rng(SEED_DEP)
ph = CC._boot_paths(L3.r, N_DEP, h * 365, BLOCK, rr)
celle, tot90, dep90h = [], float("nan"), float("nan")
for c in CONFIDENZE:
v, vers = dep_per_conf(ph, L3, c, START)
celle.append(f"EUR{v:,.0f}/m" if np.isfinite(v) else "irraggiung.")
if c == 0.90:
tot90, dep90h = vers, v
del ph
rr = np.random.default_rng(SEED_DEP)
p1 = CC._boot_paths(L1.r, N_DEP, h * 365, BLOCK, rr)
v1, _ = dep_per_conf(p1, L1, 0.90, START)
del p1
quota = tot90 / L3.muro if np.isfinite(tot90) else float("nan")
if h == 10:
rr = np.random.default_rng(SEED_DEP)
pb = CC._boot_paths(Lo["L3b CONGIUNTA (str. vero)"].r, N_DEP, h * 365, BLOCK, rr)
vb, versb = dep_per_conf(pb, Lo["L3b CONGIUNTA (str. vero)"], 0.90, START)
del pb
riga10 = (dep90h, tot90, quota, vb, versb)
print(f" {f'{h} anni':>10}" + "".join(f"{x:>13}" for x in celle)
+ f"{f'${tot90:,.0f}':>18}{quota:>13.0%}"
+ f"{f' | EUR{v1:,.0f}/m ({dep90h / v1:.2f}x)':>21}")
print(f" {clock()}")
print("""
📌 A 10 anni il rendimento fa la minoranza del lavoro: il resto lo fanno i bonifici. E'
la lezione gia' scritta il 26/07, qui con le due correzioni dentro: comprimere
l'orizzonte non fa lavorare la strategia, COMPRA il capitale.""")
# =====================================================================
sez("6) LA DOMANDA INVERSA — che rendita compra quello che posso permettermi")
print(f"""
Rendita NETTA mediana in EUR/giorno = capitale x perpetua({L3.perp:.2%}). Le imposte sono gia'
dentro la perpetua: il numero e' netto e NON va lordizzato una seconda volta.
Lente CONGIUNTA, partenza ${START:.0f}, nessun lump.""")
print(f"\n {'EUR/mese':>9}" + "".join(f"{f'{y}a':>12}" for y in (5, 10, 15, 20))
+ f"{'P(>=EUR50/g a 20a)':>21}")
for dep in VERSAMENTI:
celle, p50 = [], 0.0
for y in (5, 10, 15, 20):
rr = np.random.default_rng(SEED_DEP)
py = CC._boot_paths(L3.r, N_DEP, y * 365, BLOCK, rr)
cap = PN.accumula(py, dep, L3.muro, L3.aliq, L3.patr, start=START)["cap"]
del py
rend = cap * L3.perp / 365.0 / CC.EURUSD
celle.append(f"{np.median(rend):,.2f}")
if y == 20:
p50 = float((rend >= CC.TARGET_EUR_DAY).mean())
print(f" {dep:>9}" + "".join(f"{x:>12}" for x in celle) + f"{p50:>21.1%}")
print(f" {clock()}")
# =====================================================================
sez("VERDETTO")
j250 = traj["L3 CONGIUNTA"][250]
d10, t10, q10, d10b, t10b = riga10
print(f"""
1. LA TABELLA ORA ESISTE, ed e' peggiore di entrambe le lenti separate:
EUR250/mese, P(entro 20 anni) — LORDO {d250['L0 LORDO (26/07)']['p20']:.0%} · +FISCO {d250['L1 +FISCO (07/08)']['p20']:.0%} · +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:.0%} · CONGIUNTA {j250['p20']:.0%}.
Il muro passa da ${Lo['L0 LORDO (26/07)'].muro:,.0f} a ${L3.muro:,.0f} ({L3.muro / Lo['L0 LORDO (26/07)'].muro - 1:+.0%}).
2. LE DUE CORREZIONI SI SOMMANO — la mia attesa («meno drift -> meno imposta -> congiunto
meno severo della somma») e' MISURATA e vale poco, e nella moneta che decide NON la
conferma. Interazione: {np.expm1(inter_log):+.2%} sul capitale accumulato (li' il meccanismo
c'e'), ${inter_w:+,.0f} sul muro (sotto la risoluzione MC), {np.median(inters):+,.0f} EUR/mese sul
versamento necessario ({np.median(inters) / (d_fisco + d_fund):+.0%} della somma, segno {"stabile" if segno_ok else "NON stabile"}
su 3 semi). Il congiunto resta
{d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due correzioni presa da sola.
3. COSA NON CAMBIA A NESSUNA LENTE: senza versamenti il capitale-rendita non si raggiunge
MAI (P=0% a 20 anni in tutte e cinque le colonne). L'accumulo viene dai bonifici, non dal
rendimento — e l'ordine delle leve del piano e' invariato.
4. IL RISCHIO DI VENUE RESTA FUORI, DI PROPOSITO: e' rovina, non costo. A p=5% annuo il
capitale mediano a 20 anni e' ZERO per ogni riga di ogni tabella qui sopra. Nessuna di
queste righe e' valida se il conto sparisce.
5. L'OBIETTIVO DICHIARATO, senza addolcirlo: EUR50/giorno NON e' raggiungibile con versamenti
plausibili in un orizzonte breve. A 10 anni servono EUR{d10:,.0f}/mese (P=90%), cioe' ${t10:,.0f}
versati per arrivare a ${L3.muro:,.0f}: il {q10:.0%} del bersaglio lo compri coi bonifici.
La tabella utile e' quella INVERSA (sezione 6): non «quando arrivo a 50», ma «cosa compro
con quello che ho».
""")
print(f"\nCON TUTTE LE CORREZIONI, EUR250/MESE DA ${START:.0f}: {fmt_med(j250)[:-1]} anni "
f"(mediana incondizionata; cond. all'arrivo {fmt_cond(j250)}), "
f"P(20a) {j250['p20']:.0%}")
print(f"PER EUR50/GIORNO IN 10 ANNI SERVONO: EUR{d10:,.0f}/mese (P=90%), "
f"totale versato ${t10:,.0f}")
print(f" [banda di funding: lente conservativa -2,16%/a come sopra; con il funding dello")
print(f" strumento vero -1,39%/a -> EUR250/mese fa {fmt_med(d250['L3b CONGIUNTA (str. vero)'])} "
f"con P(20a) {d250['L3b CONGIUNTA (str. vero)']['p20']:.0%}, e i 10 anni costano "
f"EUR{d10b:,.0f}/mese (${t10b:,.0f})]")
print(f"\n{clock()} fine.")
if __name__ == "__main__":
main()