ricerca: il piano a 10 anni dal conto vero, l'ETF-quando-flat SCARTATO, slippage rifatto

Giornata partita da "stato trades" e finita sul disegno del sistema. Versamento di
1.400 USDC atterrato alle 11:03:35Z (equity $667,88 -> $2.066,88); il giro delle 11:47
ha ribilanciato correttamente e ha ripiazzato i disaster-SL alla taglia nuova -- prima
volta che il ramo riparato stamattina gira sul serio.

(1) PIANO A 10 ANNI DAL CONTO VERO -- r0825_piano_10a_500.py (nuovo)
Le tabelle pubblicate partono da $600/$635: rifatte su $2.067, lente L3 CONGIUNTA.
Replica superata: chiede EUR 1.718/m dove la tabella da $635 chiedeva EUR 1.733.
EUR 500/mese per 10 anni -> mediana $114.934, rendita 18,35 EUR/g, P(>=50 EUR/g) = 0,0%
su 3.000 traiettorie. Il bersaglio con EUR 500/m arriva al 17o anno.

  L'EQUIVALENZA CHE ORDINA IL PIANO: EUR 100/mese in piu' == +4,07%/anno di drift,
  cioe' +27% su TUTTO il drift del libro. A 10 anni i bonifici fanno il 59%.
  Tutta la leva autorizzabile vale quanto EUR 100-150/mese: k=1,25 (+15,6%) vale MENO
  di EUR 100/mese in piu' (+19,1%), e si porta dietro il peggior giorno al 21,48%.

  E il libro a k=1 rende MENO dell'S&P (15,19% vs 17,40%): il vantaggio sta nello
  Sharpe (1,35 vs 0,89) e senza leva NON si converte in rendimento. Senza leva il libro
  non si giustifica come veicolo di ACCUMULO -- si giustifica come veicolo di RENDITA,
  dove serve 2,5x meno capitale ($254k contro $646k) perche' la perpetua vive sul DD.

(2) "ETF QUANDO IL LIBRO E' FLAT" -- SCARTATO, r0825_capitale_fermo.py (nuovo)
Il libro e' flat il 28,0% dei giorni (2,7% nel 2021, 77,0% nel 2022, 75,6% nel 2026);
live, esposto 14 giorni su 64, leva lorda mediana 0,00x e max 0,52x su un tetto di 1,0x
-- il cap non ha mai morso, il vincolo e' il segnale.
A ISO-RISCHIO il dinamico PERDE: Sharpe 1,01 contro 1,40 del 50/50 e 1,35 del libro. E
il null a maschera casuale (400 estrazioni, stessa quota, blocchi 20g) lo mette al 30o
percentile: fa PEGGIO di commutare a caso.
A iso-nozionale sembrava vincere (drift 17,09%, il piu' alto) perche' aveva la vol piu'
alta: e' la trappola di M6, il de-levering e' il PRIMO test.

  E IL MECCANISMO CHE SEMBRAVA OVVIO NON ESISTE. Aggregato: SPY 4,91% nei giorni flat
  contro 21,48% negli altri (-16,57%), e la storia si scrive da sola. FALSA: scomposta
  per anno il segno ALTERNA (3 su, 4 giu') e il 2022 -- l'anno che doveva reggerla --
  ha il segno OPPOSTO. Artefatto di composizione: i giorni flat stanno negli ANNI brutti
  per l'azionario, non nei GIORNI brutti. M9 ha fatto il suo lavoro su di me.

(3) SLIPPAGE RIFATTO ALLA TAGLIA VERA -- r0822_slip_audit.py riparato
26 fill (erano 18), $6-$319. I due piu' grandi mai eseguiti sono di oggi e hanno preso
1,01% e 0,54% della loro barra 5m, con il print a meta' del range (q=0,50). Il caso
peggiore resta un fill da $74 del 18/07 al 21,9%: un sabato, nastro inesistente.
L'attrito non e' funzione della TAGLIA ma di taglia/volume-della-barra -- l'estrapolazione
lineare che prevedeva ~71% a questo capitale e' refutata dalla misura diretta.
NB non e' "assente", e' "non ancora testato": manca un fill grande in una barra sottile,
e il weekend e' dove TP01 fa il 38% del proprio gross.

  DIFETTO P1 RIPARATO: l'equity era CABLATA a 636.0 con un commento che diceva di
  leggerla dal watermark. Il giorno del versamento avrebbe stampato $636 sbagliando di
  3,3x proprio la sezione che esiste per dire QUANDO la misura scade. Riparata leggendo
  il watermark -- e poi riparata di nuovo, perche' i fill del campione sono stati
  eseguiti a conti diversi ($597-$2.067) e una base sola e' sbagliata comunque la si
  scelga. Ora la normalizzazione e' PER FILL, e a $600 riproduce il 22,0% originale.

DUE ERRORI MIEI, CATTURATI PRIMA DI PUBBLICARLI
- maschera VUOTA vestita da risultato: la prima stesura di r0825_capitale_fermo prendeva
  i giorni flat dalla serie DE-LUCKATA, ma `deluck` sottrae una costante e lo zero esatto
  sparisce -> maschera vuota, dinamico identico al book, e lo script ha stampato tabelle
  piene e un p-value. Lo ha rivelato solo la riga "0 = 0.0%".
  REGOLA: stampare la CARDINALITA' di una maschera prima di usarla.
- il meccanismo falso della (2), demolito dalla scomposizione per anno.

Suite: 730 passati, 1 fallito -- quello gia' noto di 5.9 (deriva dati, non codice).
Watermark del libro live sopravvissuto intatto alla suite (fixture autouse di stamattina).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
Adriano Dal Pastro
2026-08-25 18:13:15 +00:00
parent 14e1567125
commit ee5c6ed539
7 changed files with 647 additions and 7 deletions
+71 -7
View File
@@ -212,6 +212,54 @@ def _containing(bars: pd.DataFrame, t) -> pd.Series | None:
# ----------------------------------------------------------------------------- stats
def _equity_per_fill(ts_list) -> "pd.Series":
"""Equity del conto AL MOMENTO di ciascun fill, dal libro di bordo (`data/live/trades.db`).
⚠️ PERCHE' SERVE (2026-08-25). La sezione SCALA divideva TUTTE le partecipazioni per UNA
equity sola. Andava bene finche' il conto stava fermo a ~$636; il giorno del versamento
($667 -> $2.067) qualunque scelta di quel singolo numero diventa sbagliata, perche' i fill
del campione sono stati eseguiti a taglie di conto DIVERSE. Il caso peggiore osservato
(22% di una barra 5m) e' un fill da $74 fatto a ~$650: normalizzarlo sull'equity di oggi lo
sottostima di 3,2x, sull'equity di allora sovrastima gli altri.
La grandezza invariante e' **partecipazione per dollaro di equity**, e si ottiene solo
appaiando ogni fill alla SUA equity.
"""
import sqlite3
import pandas as _pd
f = ROOT / "data" / "live" / "trades.db"
ts = _pd.DatetimeIndex(_pd.to_datetime(list(ts_list), utc=True))
try:
with sqlite3.connect(f"file:{f}?mode=ro", uri=True) as con:
e = _pd.read_sql("SELECT ts_utc, equity FROM equity ORDER BY ts_utc", con)
e["ts_utc"] = _pd.to_datetime(e["ts_utc"], utc=True)
e = e.dropna().set_index("ts_utc")["equity"].sort_index()
# BACKWARD: l'equity in vigore quando il fill e' avvenuto, mai una lettura futura.
out = _pd.Series(e.reindex(ts, method="ffill").values, index=ts)
if out.notna().any():
return out
except Exception as ex: # noqa: BLE001
print(f" ⚠️ equity per fill non leggibile ({type(ex).__name__}): "
f"ricado sull'equity unica")
return _pd.Series([float("nan")] * len(ts), index=ts)
def _equity_osservata(default: float = 636.0) -> float:
"""L'equity reale dal watermark del libro live — DERIVATA, non ridichiarata (P1).
Se il file non c'e' o non e' leggibile si ricade sul valore storico e **lo si dice**: un
default silenzioso qui rimetterebbe esattamente il difetto che questa funzione ripara.
"""
import json
f = ROOT / "data" / "live" / "equity_seen.json"
try:
v = float(json.loads(f.read_text())["real_equity"])
if v > 0:
return v
except Exception as e: # noqa: BLE001
print(f" ⚠️ watermark non leggibile ({type(e).__name__}): uso il default ${default:,.0f}")
return default
def ci_mean(x, boot=True):
x = np.asarray(x, float)
x = x[np.isfinite(x)]
@@ -515,18 +563,34 @@ def main():
# -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no.
print(f"\n SCALA — l'unica parte di questa misura che ha una data di scadenza:")
eq = 636.0 # equity reale osservata (data/live/equity_seen.json)
print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).")
# ⚠️ 2026-08-25: era CABLATA a 636.0 con un commento che diceva "equity reale osservata
# (data/live/equity_seen.json)" — cioe' DICHIARAVA la fonte senza leggerla. E' P1: un
# misuratore deve DERIVARE il proprio bersaglio, mai ridichiararlo. Il giorno del versamento
# (equity $667 -> $2.067) la riga avrebbe continuato a stampare $636 e l'intera colonna
# "scala" sarebbe stata sbagliata di 3,3x senza che nulla lo segnalasse — proprio nella
# sezione che esiste per dire quando la misura scade.
eq = _equity_osservata()
eq_fill = _equity_per_fill(R.loc[pr.index, "fill_ts"])
if eq_fill.notna().all():
# partecipazione PER DOLLARO di equity: invariante alla taglia del conto al momento
ppd = pr.values / eq_fill.values
base = "per-fill (ogni fill sulla SUA equity)"
else:
ppd = pr.values / eq
base = f"equity unica ${eq:,.0f} — RIPIEGO, i fill hanno taglie di conto diverse"
print(f" la partecipazione scala lineare col capitale. Normalizzazione: {base}.")
print(f" equity al momento dei fill: ${np.nanmin(eq_fill):,.0f} - ${np.nanmax(eq_fill):,.0f}"
if eq_fill.notna().any() else "")
print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}")
for cap in (600, 5_000, 20_000, 100_000, 272_000):
k = cap / eq
mx = pr.max() * k * 100
print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% "
f"{pr.quantile(.9) * k * 100:>8.2f}% "
mx = float(np.nanmax(ppd)) * cap * 100
print(f" {'$' + format(cap, ','):>10} {float(np.nanmedian(ppd)) * cap * 100:>18.2f}% "
f"{float(np.nanpercentile(ppd, 90)) * cap * 100:>8.2f}% "
+ (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}"))
print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.")
_mx = float(np.nanmax(ppd))
print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k")
print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. "
print(f" diventa {_mx * 5000 * 100:.0f}%, a $20k {_mx * 20000 * 100:.0f}%. "
f"**La misura di oggi NON si estrapola**")
print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa")
print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma")
+210
View File
@@ -0,0 +1,210 @@
"""r0825_capitale_fermo.py — il capitale che il libro NON usa: occasione o illusione?
DOMANDA (operatore, 2026-08-25). Il libro live sta **flat** buona parte del tempo (28,0% dei giorni
sull'intera storia; **75,6% nel 2026** e 77,0% nel 2022). Il 07/08 il progetto aveva gia' scelto un
**50/50 book/ETF** per *rimpianto minimo*, su base statistica. La domanda nuova e' meccanica:
**mettere l'ETF proprio nei giorni in cui il libro e' flat** batte il 50/50 statico?
⚠️ ATTENZIONE A COSA SI STA MISURANDO. "ETF quando TP01 e' flat" e' a tutti gli effetti una
**strategia di timing sull'azionario guidata da un segnale cripto**. Non c'e' nessuna ragione a
priori perche' funzioni, e il rischio e' di misurare due cose diverse credendo di misurarne una:
(a) **UTILIZZO DEL CAPITALE** — stare nell'ETF invece che fermi aggiunge rendimento a
prescindere da QUANDO lo si fa. E' aritmetica, non informazione.
(b) **TIMING** — la flat-ness di TP01 predice i rendimenti azionari.
Il confronto ingenuo (dinamico vs 50/50) li somma. Per separarli serve il **null a maschera
casuale**: si rifa' la stessa strategia con una maschera flat FINTA della stessa lunghezza e della
stessa struttura a blocchi. Se il dinamico vero non batte quella nuvola, di (b) non c'e' niente e
resta solo (a) — che e' comunque un risultato, ma **un altro** risultato.
REGOLE APPLICATE:
* **M6/M5** — il confronto e' a **ISO-RISCHIO** (ogni linea riscalata alla stessa vol bersaglio),
mai a iso-nozionale: un veicolo a CAGR piu' basso ma vol piu' bassa perde sempre a iso-nozionale
per costruzione. Il de-levering e' il PRIMO test, non l'ultimo.
* **M14** — il null e' a **struttura preservata** (blocchi), non i.i.d.: la flat-ness del libro e'
fortemente autocorrelata (interi mesi), e un null i.i.d. sarebbe banale da battere.
* **⚠️ finestra comune** — un mix esiste solo dove esistono ENTRAMBE le serie. L'errore gia'
commesso e corretto il 07/08 (dichiarava 30 anni e ne usava 7) qui non si ripete: si taglia
sull'**intersezione** e la si stampa.
* **convenzione GTAA01** per l'azionario: ritorni su griglia di calendario con **0.0 a borsa
chiusa** (senza, lo Sharpe azionario sale del 20% — lezione 25/07).
uv run python scripts/research/r0825_capitale_fermo.py
"""
from __future__ import annotations
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 r0807_asset_compare as AC # noqa: E402
import r0822d_piano_vero as PV # noqa: E402
VOL_TARGET = 0.11 # ~la vol del libro: la lente iso-rischio del confronto
N_NULL = 400 # estrazioni della maschera finta
BLOCK = 20 # giorni: la stessa taglia di blocco del resto del progetto
SEED = 20260825
def metriche(r: pd.Series | np.ndarray, nome: str = "") -> dict:
r = np.asarray(r, float)
mu, sd = r.mean() * 365.0, r.std() * 365.0 ** 0.5
eq = np.cumprod(1.0 + r)
dd = float((eq / np.maximum.accumulate(eq) - 1.0).min())
return dict(nome=nome, drift=mu, vol=sd, sharpe=mu / sd if sd > 0 else np.nan, maxdd=dd)
def iso(r: np.ndarray, vol_target: float = VOL_TARGET) -> np.ndarray:
"""Riscala la serie alla vol bersaglio. E' il de-levering di M6: senza, un veicolo a vol
bassa perde per costruzione e il confronto non dice niente."""
sd = r.std() * 365.0 ** 0.5
return r * (vol_target / sd) if sd > 0 else r
def maschera_a_blocchi(n: int, quota: float, rng: np.random.Generator,
block: int = BLOCK) -> np.ndarray:
"""Maschera booleana casuale con ~`quota` di True, in BLOCCHI di `block` giorni.
La flat-ness vera del libro dura mesi interi: un null i.i.d. avrebbe una struttura temporale
completamente diversa e sarebbe facile da battere per la ragione sbagliata."""
nb = int(np.ceil(n / block))
scelti = rng.random(nb) < quota
return np.repeat(scelti, block)[:n]
def main() -> None:
print("=" * 100)
print(" r0825 — IL CAPITALE FERMO: l'ETF nei giorni in cui il libro e' flat")
print("=" * 100)
# --- le due serie, sulla FINESTRA COMUNE -------------------------------------------------
B = PV.costruisci_serie()
ix = pd.to_datetime(B.index).tz_localize(None).normalize()
# ⚠️ LA MASCHERA FLAT SI PRENDE DALLA SERIE GREZZA, MAI DA QUELLA DE-LUCKATA.
# `deluck` sottrae una COSTANTE (r - (1-0.89)*r.mean()): un giorno a rendimento esattamente
# zero diventa un numero piccolo ma non nullo, e `abs(r) < 1e-12` non trova piu' NIENTE.
# Prima versione di questo script: maschera VUOTA, dinamico identico al book, e un p-value
# stampato lo stesso. Se ne e' accorta solo la riga che stampa la quota — che e' il motivo
# per cui la si stampa.
grezzo = pd.Series(B["fund"].values.astype(float), index=ix)
book = pd.Series(PV.deluck(B["fund"].values.astype(float)), index=ix)
grezzo = grezzo[~grezzo.index.duplicated(keep="last")]
book = book[~book.index.duplicated(keep="last")]
spy = AC._calendar_daily(EQ.load_eq("SPY")["close"])
idx = book.index.intersection(spy.index)
book, spy, grezzo = book.reindex(idx), spy.reindex(idx), grezzo.reindex(idx)
flat = (grezzo.abs() < 1e-12).values
assert flat.sum() > 0, "maschera flat vuota: e' stata presa dalla serie de-luckata?"
print(f"\n finestra COMUNE: {idx[0].date()} -> {idx[-1].date()} ({len(idx)} giorni, "
f"{len(idx)/365.25:.1f} anni)")
print(f" giorni in cui il LIBRO e' flat: {flat.sum()} = {flat.mean():.1%}")
print(f" quota per anno: " + " · ".join(
f"{y}: {g.mean():.0%}" for y, g in pd.Series(flat, index=idx).groupby(idx.year)))
b, s = book.values, spy.values
linee = {
"BOOK da solo": b,
"ETF da solo": s,
"50/50 statico": 0.5 * b + 0.5 * s,
"DINAMICO (ETF se il libro e' flat)": np.where(flat, s, b),
}
# --- (1) confronto a ISO-NOZIONALE (come lo si guarderebbe d'istinto) --------------------
print(f"\n{'='*100}\n (1) ISO-NOZIONALE — come lo si guarda d'istinto. ⚠️ NON e' il test."
f"\n{'='*100}")
print(f" {'linea':<38}{'drift':>9}{'vol':>9}{'Sharpe':>9}{'maxDD':>9}")
for nome, r in linee.items():
m = metriche(r, nome)
print(f" {nome:<38}{m['drift']:>8.2%}{m['vol']:>9.2%}{m['sharpe']:>9.2f}{m['maxdd']:>9.1%}")
# --- (2) ISO-RISCHIO: il test vero (M6) --------------------------------------------------
print(f"\n{'='*100}\n (2) ISO-RISCHIO a vol {VOL_TARGET:.0%} — il confronto che conta (M6)"
f"\n{'='*100}")
print(f" {'linea':<38}{'drift':>9}{'vol':>9}{'Sharpe':>9}{'maxDD':>9}{'leva impl.':>12}")
iso_m = {}
for nome, r in linee.items():
k = VOL_TARGET / (r.std() * 365 ** 0.5)
m = metriche(iso(r), nome)
iso_m[nome] = m
print(f" {nome:<38}{m['drift']:>8.2%}{m['vol']:>9.2%}{m['sharpe']:>9.2f}"
f"{m['maxdd']:>9.1%}{k:>11.2f}x")
# --- (3) IL NULL: timing o solo utilizzo del capitale? -----------------------------------
print(f"\n{'='*100}\n (3) NULL A MASCHERA CASUALE — separa il TIMING dall'UTILIZZO"
f"\n{'='*100}")
print(f" {N_NULL} maschere finte, stessa quota ({flat.mean():.1%}) e stessa struttura "
f"a blocchi di {BLOCK}g.")
rng = np.random.default_rng(SEED)
nulli = []
for _ in range(N_NULL):
fk = maschera_a_blocchi(len(b), flat.mean(), rng)
nulli.append(metriche(iso(np.where(fk, s, b)))["sharpe"])
nulli = np.array(nulli)
vero = iso_m["DINAMICO (ETF se il libro e' flat)"]["sharpe"]
pct = float((nulli < vero).mean())
print(f" Sharpe iso-rischio del DINAMICO vero : {vero:.3f}")
print(f" nuvola del null: mediana {np.median(nulli):.3f} · "
f"p5 {np.percentile(nulli,5):.3f} · p95 {np.percentile(nulli,95):.3f}")
print(f" percentile del vero dentro il null : {pct:.1%}")
print(f" p-value one-sided (vero <= null) : {1-pct:.3f}")
# --- (3b) IL "MECCANISMO" SI SCOMPONE PER ANNO PRIMA DI CREDERCI (M9) --------------------
print(f"\n{'='*100}\n (3b) L'azionario nei giorni flat — e la scomposizione che la smonta"
f"\n{'='*100}")
sf, sn = spy.values[flat], spy.values[~flat]
print(f" aggregato: SPY|flat {sf.mean()*365:>7.2%} (vol {sf.std()*365**0.5:.2%}) · "
f"SPY|dentro {sn.mean()*365:>7.2%} (vol {sn.std()*365**0.5:.2%}) · "
f"differenza {(sf.mean()-sn.mean())*365:>+7.2%}")
print(f" ⚠️ Sembra un meccanismo («il libro va flat quando l'azionario soffre»). Non lo e':")
print(f" {'anno':>6}{'gg flat':>9}{'differenza SPY (flat - dentro)':>34}")
segni = []
for y, m in spy.groupby(spy.index.year):
mk = flat[spy.index.year == y]
a, b = m.values[mk], m.values[~mk]
if len(a) < 15 or len(b) < 15:
print(f" {y:>6}{len(a):>9}{'campione insufficiente':>34}")
continue
d = (a.mean() - b.mean()) * 365
segni.append(np.sign(d))
print(f" {y:>6}{len(a):>9}{d:>+33.1%}")
su, giu = int(sum(1 for x in segni if x > 0)), int(sum(1 for x in segni if x < 0))
print(f"\n => il segno ALTERNA: {su} anni 'flat meglio', {giu} anni 'flat peggio'.")
print(f" L'aggregato e' un artefatto di COMPOSIZIONE: i giorni flat sono concentrati")
print(f" negli anni a basso rendimento azionario, non nei GIORNI a basso rendimento.")
print(f" Dentro ogni anno l'effetto non c'e'. (M9: un contributo si scompone per anno")
print(f" prima di crederci — '24/24 ancore positive' puo' essere un anno solo.)")
# --- (4) VERDETTO calcolato a runtime (N11) ----------------------------------------------
print(f"\n{'='*100}\n VERDETTO (calcolato, non scritto a mano)\n{'='*100}")
din = iso_m["DINAMICO (ETF se il libro e' flat)"]["sharpe"]
sta = iso_m["50/50 statico"]["sharpe"]
bk = iso_m["BOOK da solo"]["sharpe"]
print(f" a iso-rischio: BOOK {bk:.3f} · 50/50 {sta:.3f} · DINAMICO {din:.3f}")
if din <= sta:
print(f" (a) Il dinamico NON batte il 50/50 statico ({din:.3f} <= {sta:.3f}): "
f"la commutazione non aggiunge nulla che il mix non dia gia'.")
else:
print(f" (a) Il dinamico batte il 50/50 statico di {din-sta:+.3f} di Sharpe.")
if pct < 0.95:
print(f" (b) E il TIMING non e' dimostrato: il vero sta al {pct:.0%} del null, "
f"sotto la soglia del 95%.")
print(f" Quel che si vede e' UTILIZZO DEL CAPITALE, non informazione: qualunque")
print(f" maschera con la stessa quota fa altrettanto.")
else:
print(f" (b) Il timing SOPRAVVIVE al null ({pct:.0%} >= 95%): la flat-ness del libro")
print(f" porta informazione sull'azionario. Da riprovare fuori campione.")
print(f"\n NB: 'battere a iso-rischio' qui vuol dire leva implicita diversa per ogni linea")
print(f" (colonna 'leva impl.'), e la leva NON e' autorizzata oggi (§3, k max 1.40).")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
"""r0825_piano_10a_500.py — €500/mese per 10 anni, dal conto VERO di oggi.
DOMANDA (operatore, 2026-08-25): *"dammi strategia a 10 anni con 500 al mese"*.
PERCHE' NON BASTAVA CITARE LA TABELLA. Le tabelle pubblicate del piano partono da **$600/$635**.
Oggi il conto e' **$2.067** (versamento di 1.400 USDC atterrato alle 11:03:35Z). Il lump e' la
leva piu' grande misurata dopo il versamento mensile stesso (*"€10.000 oggi + €250/m porta P(20a)
da 14% a 45%"*), quindi la riga vecchia sottostima il caso di oggi e non si puo' riusare.
LENTE: **L3 CONGIUNTA** di `r0822d_piano_vero` — libro live 75/25, **funding esatto dentro la
serie**, ancora de-luckata x0.89, **fisco d'accumulo** ogni anno dentro il portafoglio (33% sulla
variazione annua di valore, minusvalenze in carry 4 anni, patrimoniale 0,2%). E' la piu' severa
che il progetto abbia, ed e' quella che ha prodotto muro $313k e *"€1.733/mese per 10 anni"*.
⚠️ DIFETTI EREDITATI, dichiarati perche' vanno **a favore del piano** (registrati il 23/08):
1. `PN.accumula` versa ogni **30 giorni** -> a 10 anni sono **121** versamenti, non 120 (+0,83%);
2. il contatore `versato` e' uno **scalare che non si ferma al traguardo** -> il "versato" a
denominatore e' quello dell'INTERO orizzonte, non quello fino all'arrivo.
Non li ho riparati qui: ripararli cambierebbe i numeri di riferimento e li renderebbe non
confrontabili con tutto il resto della memoria. Vanno LETTI, non dimenticati.
⚠️ E IL NUMERO DA NON DIMENTICARE MAI accanto a questi: la SE del drift L3 e' **5,151%/anno**, e
il muro e' `prelievo/perpetua` = un **1/x su una quantita' che va a zero** -> la banda e'
esplosiva e **asimmetrica verso l'alto**: $187k (p90) · **$313k (punto)** · $1,14M (p10) ·
a **2 SE il traguardo non esiste a nessun capitale**. Percio' la regola del progetto:
**il piano si dimensiona sul VERSAMENTO, che e' certo, non sul muro** (N1).
uv run python scripts/research/r0825_piano_10a_500.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import r0725_capcurve as CC # noqa: E402
import r0807_piano_netto as PN # noqa: E402
import r0822d_piano_vero as PV # noqa: E402
import r0823_deposit_timing as DT # noqa: E402
# --- lo stato VERO di oggi, non un nominale -------------------------------------------------
START_OGGI = 2066.88 # equity Deribit letta alle 11:47:01Z del 2026-08-25
START_PUBBL = PV.START # 635.0 — la base delle tabelle pubblicate, per il confronto
DEP = 500.0 # EUR/mese
ORIZZONTI = (5, 10, 15, 20)
N_PATHS = 3000
SEED = PV.SEED_TRAJ # 725: stesso seme delle traiettorie pubblicate
BLOCK = PV.BLOCK # 20 giorni
def rendita_eur_giorno(cap: np.ndarray, perp: float) -> np.ndarray:
"""€/giorno NETTI da un capitale, alla perpetua della lente.
⚠️ La perpetua L3 (6,35%) ha gia' le imposte dentro: **non si lordizza una seconda volta**
(errore corretto il 07/08)."""
return cap * perp / 365.0 / CC.EURUSD
def main() -> None:
print("=" * 100)
print(" r0825 — €500/MESE PER 10 ANNI, dal conto vero di oggi ($2.067)")
print("=" * 100)
B = PV.costruisci_serie()
r = PV.deluck(B["fund"].values.astype(float)) # L3 CONGIUNTA, ancora x0.89
lente = PV.Lente("L3 congiunta", r, lordizza=False)
lente.perp, lente.muro = PV.muro_di(lente)
print(f"\n serie: {len(r)} giorni · drift {lente.drift:.2%}/anno · vol {lente.vol:.2%}/anno")
print(f" perpetua {lente.perp:.2%} · MURO ${lente.muro:,.0f} "
f"[banda del progetto: $187k (p90) — $1,14M (p10); a 2 SE non esiste]")
print(f" bersaglio di rendita: €{CC.TARGET_EUR_DAY:.0f}/giorno netti")
print(f"\n{'='*100}\n (1) €{DEP:.0f}/MESE DA ${START_OGGI:,.0f} — cosa consegna, per orizzonte"
f"\n{'='*100}")
print(f" {'oriz.':>6} {'capitale mediano':>18} {'p10':>12} {'p90':>13} "
f"{'€/g mediani':>13} {'P(≥50 €/g)':>12} {'versato':>11} {'fin/vers':>9}")
print(" " + "-" * 98)
righe = {}
for anni in ORIZZONTI:
rng = np.random.default_rng(SEED)
ii = DT.boot_idx(len(r), N_PATHS, anni * 365, BLOCK, rng)
paths = r[ii]
out = PN.accumula(paths, DEP, lente.muro, lente.aliq, lente.patr, start=START_OGGI)
cap, versato = out["cap"], out["versato"]
eur = rendita_eur_giorno(cap, lente.perp)
p50 = float((eur >= CC.TARGET_EUR_DAY).mean())
righe[anni] = dict(med=np.median(cap), eur=np.median(eur), p50=p50, versato=versato)
print(f" {anni:>4}a ${np.median(cap):>16,.0f} ${np.percentile(cap,10):>10,.0f} "
f"${np.percentile(cap,90):>11,.0f} {np.median(eur):>12.2f} {p50:>11.1%} "
f"${versato:>9,.0f} {np.median(cap)/versato:>8.2f}x")
print(f"\n{'='*100}\n (2) COSA HA COMPRATO IL VERSAMENTO DI OGGI — $635 (tabelle pubbl.) vs "
f"$2.067 (reale)\n{'='*100}")
print(f" {'oriz.':>6} {'da $635':>14} {'da $2.067':>14} {'guadagno':>11} "
f"{'€/g da $635':>13} {'€/g da $2.067':>15}")
print(" " + "-" * 78)
for anni in ORIZZONTI:
rng = np.random.default_rng(SEED)
ii = DT.boot_idx(len(r), N_PATHS, anni * 365, BLOCK, rng)
paths = r[ii]
a = PN.accumula(paths, DEP, lente.muro, lente.aliq, lente.patr, start=START_PUBBL)["cap"]
b = PN.accumula(paths, DEP, lente.muro, lente.aliq, lente.patr, start=START_OGGI)["cap"]
ea, eb = rendita_eur_giorno(a, lente.perp), rendita_eur_giorno(b, lente.perp)
print(f" {anni:>4}a ${np.median(a):>12,.0f} ${np.median(b):>12,.0f} "
f"{np.median(b)/np.median(a)-1:>10.1%} {np.median(ea):>12.2f} {np.median(eb):>14.2f}")
print(f"\n{'='*100}\n (3) QUANDO ARRIVA IL BERSAGLIO (€50/g = ${lente.muro:,.0f})"
f"\n{'='*100}")
rng = np.random.default_rng(SEED)
ii = DT.boot_idx(len(r), N_PATHS, 25 * 365, BLOCK, rng)
paths25 = r[ii]
for dep in (0, 250, 500, 800, 1_000):
out = PN.accumula(paths25, float(dep), lente.muro, lente.aliq, lente.patr,
start=START_OGGI)
d = PV.leggi(out["colpito"], anni_sim=25)
med = "mai" if not np.isfinite(d["med"]) else f"{d['med']:.1f}a"
cond = "n/d" if not np.isfinite(d["med_cond"]) else f"{d['med_cond']:.1f}a"
print(f"{dep:>5}/m -> mediana {med:>6} (condizionata all'arrivo {cond:>6}, "
f"arriva il {d['p_arr']:.0%}) · P(entro 20a) {d['p20']:>5.0%}")
print(f"\n{'='*100}\n (4) IL VERSAMENTO CHE SERVE DAVVERO PER €50/g, da ${START_OGGI:,.0f}"
f"\n{'='*100}")
for anni in (10, 15, 20):
rng = np.random.default_rng(PV.SEED_DEP)
ii = DT.boot_idx(len(r), PV.N_DEP, anni * 365, BLOCK, rng)
p = r[ii]
riga = []
for conf in (0.50, 0.75, 0.90):
d, tot = PV.dep_per_conf(p, lente, conf, start=START_OGGI)
riga.append(f"P={conf:.0%}: €{d:,.0f}/m" + (f" (tot ${tot:,.0f})" if conf == 0.90 else ""))
print(f" {anni:>2} anni -> " + " · ".join(riga))
print(f"\n{'='*100}\n VERDETTO (calcolato a runtime, non scritto a mano)\n{'='*100}")
d10 = righe[10]
print(f" €500/mese per 10 anni da ${START_OGGI:,.0f}:")
print(f" capitale mediano ${d10['med']:,.0f} · rendita mediana {d10['eur']:.2f} €/giorno")
print(f" P(≥50 €/g a 10 anni) = {d10['p50']:.1%}")
print(f" versato ${d10['versato']:,.0f} -> il rendimento fa il "
f"{max(0.0, 1 - d10['versato']/d10['med']):.0%} del risultato, i bonifici il resto")
verdetto = ("CENTRA" if d10["p50"] >= 0.5 else "NON CENTRA")
print(f"\n => a 10 anni €500/mese {verdetto} il bersaglio €50/giorno.")
print(f" => quello che centra a 10 anni sta nella sezione (4); quello che €500/mese centra,")
print(f" e quando, sta nella (3).")
if __name__ == "__main__":
main()