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:
@@ -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()
|
||||
Reference in New Issue
Block a user