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>
This commit is contained in:
@@ -293,3 +293,73 @@ configurazione.** Qui lo stesso numero era sbagliato in un verso prima del depos
|
||||
dopo: la soluzione non era scegliere il valore giusto, era **legarlo alla grandezza che lo
|
||||
determina**. Il segnale che serviva questa correzione era gia' visibile: l'azione manuale era stata
|
||||
pre-registrata 24 giorni prima e non era mai stata eseguita.
|
||||
|
||||
---
|
||||
|
||||
# Addendum 2 — "dai per scontato che non perdiamo mai con i trade?"
|
||||
|
||||
Obiezione dell'operatore. Si divide in due parti che hanno risposte **opposte**:
|
||||
la prima e' infondata, la seconda coglie l'ipotesi piu' fragile di tutto il piano.
|
||||
Script `r0726_decadimento.py`.
|
||||
|
||||
## Parte 1 — no: le perdite sono nel modello
|
||||
|
||||
Il block bootstrap ricampiona i ritorni **reali** del book a blocchi di 20 giorni, quindi conserva
|
||||
autocorrelazione e forma dei drawdown.
|
||||
|
||||
```
|
||||
serie reale (2.692 giorni): in perdita 35.6% · flat 27.5% · in guadagno 36.9%
|
||||
giorno peggiore -3.94% · peggior mese -5.76%
|
||||
5.000 percorsi simulati (20a): maxDD mediano 14.4% · p90 19.8% · PEGGIORE 44.2%
|
||||
anni-calendario in perdita 5.8% (su 95.000 anni)
|
||||
```
|
||||
|
||||
⚠️ **Errore mio catturato scrivendo questo**: la prima stesura riportava **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**. Corretto per il drift, fuorviante per la forma.
|
||||
**REGOLA: una correzione uniforme sul drift e' giusta per le domande sul drift e sbagliata per le
|
||||
domande sulla distribuzione** — la stessa serie dice 36% o 64% di giorni in perdita a seconda di
|
||||
quale delle due si guarda, senza che sia cambiato niente.
|
||||
|
||||
## Parte 2 — si', ma l'assunzione e' un'altra: che l'edge continui a esistere
|
||||
|
||||
Il bootstrap assume che **il futuro sia il passato rimescolato**. Nessuna parte del progetto misura
|
||||
il decadimento dell'alpha. Quanto costa se e' falso (piano €5.000 + €500/mese):
|
||||
|
||||
```
|
||||
scenario mediana traguardo P(entro 20a) cap. mediano 20a
|
||||
edge intatto (cio' che ho mostrato finora) 11.6a 99.9% $1,115,643
|
||||
edge DIMEZZATO da subito 15.8a 76.6% $342,224
|
||||
edge che decade a zero in 20 anni 14.3a 65.5% $269,970
|
||||
edge che decade a zero in 10 anni oltre 20a 18.4% $157,856
|
||||
edge MORTO dall'anno 10 (rende 0) oltre 20a 42.0% $259,553
|
||||
edge MORTO dall'anno 5 oltre 20a 0.0% $163,377
|
||||
```
|
||||
|
||||
**Il piano NON e' fragile a un dimezzamento dell'edge** (11.6 → 15.8 anni, P ancora 77%). **Lo e'
|
||||
alla morte dell'edge.** E la differenza fra i due casi non e' distinguibile in anticipo — e' la
|
||||
ragione per cui il progetto ha gate pre-registrati con date e soglie invece di aspettarsi che le
|
||||
cose continuino a funzionare.
|
||||
|
||||
## Il limite strutturale del metodo
|
||||
|
||||
```
|
||||
Peggior BIENNIO dell'intero campione: +3.0%
|
||||
Se tutti i 20 anni fossero fatti cosi': P(traguardo) 2.9%, capitale mediano $149.014
|
||||
(a fronte di $136.847 versati)
|
||||
```
|
||||
|
||||
**Il peggior biennio della storia del book e' POSITIVO.** Sette anni di storia crypto contengono
|
||||
due mercati toro: un decennio davvero brutto **non e' mai successo**, quindi il bootstrap non lo
|
||||
puo' estrarre. Non e' un parametro da alzare, e' il limite del ricampionamento: **non si puo'
|
||||
simulare un regime peggiore di qualunque cosa ci sia nel campione.**
|
||||
|
||||
Se i prossimi 20 anni somigliassero al peggior biennio gia' visto, il capitale finirebbe **appena
|
||||
sopra i soldi versati**: il piano non fallirebbe, semplicemente non renderebbe.
|
||||
|
||||
## Cosa se ne fa
|
||||
|
||||
Nessun cambio a book, pesi o piano. Cambia **cosa si sorveglia**: la domanda "l'edge c'e' ancora?"
|
||||
non ha una risposta nel backtest, ce l'ha solo nel forward — che e' esattamente cio' che i monitor
|
||||
paper e i gate pre-registrati esistono per misurare.
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user