Files
PythagorasGoal/scripts/research/r0726_decadimento.py
T
Adriano Dal Pastro 1296fa2d96 research: "dai per scontato che non perdiamo mai?" — meta' infondata, meta' e' il punto piu' debole del piano
Obiezione dell'operatore, presa sul serio. Due parti con risposte opposte.

PARTE 1 (infondata): le perdite SONO nel modello. Il block bootstrap ricampiona i ritorni
reali a blocchi di 20 giorni e conserva la forma dei drawdown. Serie reale: 35.6% giorni
in perdita, 27.5% flat, 36.9% in guadagno; giorno peggiore -3.94%, peggior mese -5.76%.
Sui 5.000 percorsi: maxDD mediano 14.4%, p90 19.8%, PEGGIORE 44.2%; anni-calendario in
perdita 5.8% su 95.000 anni simulati.

⚠️ ERRORE MIO CATTURATO PRIMA DI PUBBLICARE: la prima stesura diceva 63.9% di giorni in
perdita. Artefatto — il de-luck sottrae una costante a OGNI giorno (e' cosi' che riduce il
drift lasciando la vol invariata, come prescrive la misura d'ancora), quindi trasforma il
27.5% di giorni FLAT in piccoli negativi. REGOLA: una correzione uniforme sul drift e'
giusta per le domande sul drift e sbagliata per quelle sulla distribuzione — la stessa
serie dice 36% o 64% a seconda di quale si guarda, senza che sia cambiato niente.

PARTE 2 (coglie il punto): l'assunzione ottimista c'e' ed e' che l'EDGE CONTINUI A
ESISTERE per vent'anni. Il bootstrap assume che il futuro sia il passato rimescolato;
nessuna parte del progetto misura il decadimento dell'alpha. Misurato ora:
  edge intatto            -> 11.6a, P(20a) 99.9%
  edge dimezzato          -> 15.8a, P 76.6%
  decade a zero in 20a    -> 14.3a, P 65.5%
  decade a zero in 10a    -> oltre 20a, P 18.4%
  morto dall'anno 10      -> oltre 20a, P 42.0%
  morto dall'anno 5       -> oltre 20a, P 0.0%
Il piano NON e' fragile a un dimezzamento dell'edge, lo e' alla sua morte. E i due casi
non sono distinguibili in anticipo: e' la ragione per cui esistono i gate pre-registrati.

IL LIMITE STRUTTURALE: il peggior BIENNIO dell'intero campione e' +3.0%, cioe' POSITIVO.
Sette anni di storia crypto contengono due tori: un decennio davvero brutto non e' mai
successo, quindi il bootstrap non lo puo' estrarre. Se i 20 anni fossero tutti come quel
biennio, P(traguardo) 2.9% e capitale finale $149.014 contro $136.847 versati — il piano
non fallirebbe, semplicemente non renderebbe. Non e' un parametro da alzare: non si puo'
simulare un regime peggiore di qualunque cosa ci sia nel campione.

Book, pesi, piano INVARIATI. Cambia cosa si sorveglia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:06:53 +00:00

193 lines
9.3 KiB
Python

#!/usr/bin/env python
"""r0726_decadimento.py — "dai per scontato che non perdiamo mai con i trade?"
L'obiezione dell'operatore, presa sul serio. Si divide in due parti che hanno risposte OPPOSTE.
PARTE 1 — LE PERDITE SONO NEL MODELLO. Il block bootstrap ricampiona i ritorni REALI del book
(blocchi da 20 giorni, quindi conserva l'autocorrelazione e la forma dei drawdown): giorni negativi,
mesi negativi e drawdown ci sono tutti. Qui si mostrano espliciti, invece di lasciarlo assumere.
PARTE 2 — MA C'E' UN'ASSUNZIONE OTTIMISTA, ED E' UN'ALTRA. Il bootstrap assume che **il futuro sia
il passato rimescolato**: che l'edge continui a esistere, alla stessa taglia, per vent'anni. Questo
NON e' misurato da nessuna parte del progetto, ed e' l'ipotesi piu' forte di tutto il piano.
Tre cose che il modello NON contiene:
(a) DECADIMENTO dell'alpha — il TSMOM e' pubblicato dagli anni '80 e in molti mercati si e'
eroso; il modello lo tiene costante;
(b) ROTTURA DI REGIME — 7 anni di storia crypto contengono DUE tori. Un decennio piatto non c'e'
mai stato nel campione, quindi il bootstrap non lo puo' estrarre;
(c) MORTE della strategia — il caso in cui semplicemente smette di funzionare.
Qui si misura il costo di ognuna.
uv run python scripts/research/r0726_decadimento.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 r0726_capwall_refresh as WR # noqa: E402
import r0726_deposits as DP # noqa: E402
START = 596.92 + 5_000 * CC.EURUSD # il piano dichiarato: conto + €5.000
DEP = 500.0 * CC.EURUSD
N_PATHS = 5000
BLOCK = 20
YEARS = 20
SEED = 20260726
def maxdd(equity: np.ndarray) -> np.ndarray:
"""Max drawdown per path su una matrice (path x tempo) di equity."""
peak = np.maximum.accumulate(equity, axis=1)
return (1.0 - equity / peak).max(axis=1)
def simulate(paths: np.ndarray, wall: float, decay_years: float | None = None,
dead_from: float | None = None, drift_mult: float = 1.0) -> dict:
"""Traiettoria con versamenti, opzionalmente con alpha che decade o che muore.
decay_years: il DRIFT decade linearmente a zero in tanti anni (la vol resta).
dead_from: dall'anno indicato il book rende zero (resta solo il versamento).
drift_mult: moltiplicatore costante sul drift (per lo scenario 'meta' edge').
"""
n_paths, n_days = paths.shape
mu = float(paths.mean())
cap = np.full(n_paths, START)
hit = np.full(n_paths, -1, int)
eq = np.empty((n_paths, n_days))
for t in range(n_days):
yr = t / 365.0
r = paths[:, t]
if drift_mult != 1.0:
r = r - (1.0 - drift_mult) * mu
if decay_years is not None:
k = max(0.0, 1.0 - yr / decay_years) # 1 -> 0 in decay_years
r = r - (1.0 - k) * mu
if dead_from is not None and yr >= dead_from:
r = np.zeros_like(r)
cap = cap * (1.0 + r)
if t % 30 == 0 and t > 0:
cap += DEP
eq[:, t] = cap
newly = (hit < 0) & (cap >= wall)
hit[newly] = t
yrs = np.where(hit >= 0, hit / 365.0, np.nan)
return dict(cap=cap, yrs=yrs, eq=eq)
def main() -> None:
print("=" * 100)
print(' "dai per scontato che non perdiamo mai con i trade?"')
print("=" * 100)
r = DP.deluck_returns()
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
_, perp, wall = WR.perp_and_wall(r, 1.0, gross)
# ---------------------------------------------------------------- parte 1
print("\n" + "=" * 100)
print(" PARTE 1 — QUANTO SI PERDE, NEL MODELLO CHE HO GIA' USATO")
print("=" * 100)
# ⚠️ Le statistiche DISTRIBUTIVE vanno lette sulla serie GREZZA. Il de-luck sottrae una
# costante a OGNI giorno (e' cosi' che riduce il drift lasciando la vol invariata, che e'
# esattamente cio' che la misura d'ancora del 26/07 dice di fare): questo trasforma i giorni
# FLAT in piccoli negativi, e "% di giorni in perdita" sulla serie de-luckata sale dal 36% al
# 64% senza che sia successo niente. Giusto per il drift, fuorviante per la forma.
raw = CC.book_series("hourly").values.astype(float)
neg_d, zero_d = float((raw < 0).mean()), float((raw == 0).mean())
print(f"\n Sulla serie REALE del book, {len(raw):,} giorni (statistiche di FORMA -> serie")
print(f" grezza, non de-luckata: vedi nota nel codice):")
print(f" giorni in perdita : {neg_d:.1%}")
print(f" giorni flat (book fuori) : {zero_d:.1%}")
print(f" giorni in guadagno : {float((raw > 0).mean()):.1%}")
print(f" giorno peggiore : {raw.min()*100:+.2f}%")
print(f" peggior mese (30g rolling) : "
f"{min(np.prod(1+raw[i:i+30])-1 for i in range(len(raw)-30))*100:+.2f}%")
rng = np.random.default_rng(SEED)
n_days = 365 * YEARS
paths = CC._boot_paths(r, N_PATHS, n_days, BLOCK, rng)
base = simulate(paths, wall)
dd = maxdd(base["eq"])
print(f"\n Sui {N_PATHS:,} percorsi simulati (20 anni ciascuno):")
print(f" drawdown massimo mediano {np.median(dd):.1%} p90 {np.percentile(dd,90):.1%}"
f" PEGGIORE {dd.max():.1%}")
# anni in perdita: quota di anni-calendario chiusi sotto l'anno precedente
ann = base["eq"][:, ::365]
yr_ret = ann[:, 1:] / ann[:, :-1] - 1.0
print(f" anni-calendario in perdita : {float((yr_ret < 0).mean()):.1%} "
f"(su {yr_ret.size:,} anni simulati)")
# il peggior percorso
worst = int(np.argmin(base["cap"]))
print(f" percorso PEGGIORE dei {N_PATHS:,}: capitale finale ${base['cap'][worst]:,.0f} "
f"contro ${DP.START + 0:,.0f}+versamenti = ${START + DEP*12*YEARS:,.0f} messi dentro")
print(f"\n -> No, il modello non assume che non si perda: perde nel {neg_d:.0%} dei giorni,")
print(f" ha drawdown mediani del {np.median(dd):.0%} (peggiore {dd.max():.0%}) e anni")
print(f" negativi nel {float((yr_ret < 0).mean()):.0%} dei casi.")
print(f" Cio' che il modello NON contiene e' un'altra cosa -> parte 2.")
# ---------------------------------------------------------------- parte 2
print("\n" + "=" * 100)
print(" PARTE 2 — L'ASSUNZIONE OTTIMISTA C'E', MA E' UN'ALTRA")
print("=" * 100)
print("\n Il bootstrap ricampiona il PASSATO: assume che l'edge esista ancora, alla stessa")
print(" taglia, per vent'anni. Questo non e' misurato da nessuna parte. Quanto costa se e'")
print(" falso:")
scen = [
("edge intatto (cio' che ho mostrato finora)", dict()),
("edge DIMEZZATO da subito", dict(drift_mult=0.5)),
("edge che decade a zero in 20 anni", dict(decay_years=20.0)),
("edge che decade a zero in 10 anni", dict(decay_years=10.0)),
("edge MORTO dall'anno 10 (rende 0)", dict(dead_from=10.0)),
("edge MORTO dall'anno 5", dict(dead_from=5.0)),
]
print(f"\n {'scenario':<42}{'mediana traguardo':>19}{'P(entro 20a)':>14}{'cap. mediano 20a':>19}")
for lab, kw in scen:
s = simulate(paths, wall, **kw)
f = s["yrs"][~np.isnan(s["yrs"])]
med = f"{np.percentile(f,50):.1f}a" if len(f) > N_PATHS * 0.5 else "oltre 20a"
print(f" {lab:<42}{med:>19}{float((s['yrs'] >= 0).mean() if False else (~np.isnan(s['yrs'])).mean()):>13.1%}"
f"${np.median(s['cap']):>18,.0f}")
print("\n Lettura: il piano NON e' fragile a un dimezzamento dell'edge (traguardo da ~11.6 a")
print(" ~15 anni), ma lo e' alla MORTE dell'edge. E la differenza fra i due casi non e'")
print(" misurabile in anticipo: e' la ragione per cui il progetto ha gate pre-registrati")
print(" (kill di DVOLSPREAD, decisione XSR01) invece di aspettarsi che le cose funzionino.")
# ---------------------------------------------------------------- il peggior decennio
print("\n" + "=" * 100)
print(" E SE I PROSSIMI 10 ANNI FOSSERO IL PEGGIOR TRATTO DELLA STORIA?")
print("=" * 100)
# peggior finestra contigua di 2 anni nella serie reale, ricampionata a blocchi
W = 365 * 2
if len(r) > W:
cums = np.array([np.prod(1 + r[i:i + W]) - 1 for i in range(0, len(r) - W, 30)])
i_bad = int(np.argmin(cums)) * 30
bad = r[i_bad:i_bad + W]
print(f"\n Peggior biennio del campione: rendimento {cums.min()*100:+.1f}% "
f"({len(bad)} giorni)")
rng2 = np.random.default_rng(SEED + 5)
bad_paths = CC._boot_paths(bad, N_PATHS, n_days, BLOCK, rng2)
sb = simulate(bad_paths, wall)
f = sb["yrs"][~np.isnan(sb["yrs"])]
print(f" Se TUTTI i 20 anni fossero fatti cosi':")
print(f" P(traguardo entro 20a) {(~np.isnan(sb['yrs'])).mean():>6.1%} "
f"capitale mediano ${np.median(sb['cap']):>12,.0f} "
f"(versati ${START + DEP*12*YEARS:,.0f})")
print(f"\n ⚠️ Questo NON e' il caso peggiore possibile: e' il peggiore CHE SIA GIA'")
print(f" SUCCESSO. Un regime peggiore di qualunque cosa nel campione non e'")
print(f" simulabile ricampionando il campione — e' il limite strutturale del")
print(f" metodo, non un parametro da alzare.")
if __name__ == "__main__":
main()