Files
Adriano Dal Pastro 7d4f0c328e research(§49 TP01-TWIN): un ENSEMBLE di 5 meccanismi di trend NON protegge piu' di TP01 — REFUTED
Domanda: TP01 e' il 75% del libro live e dipende da UNA definizione di trend (TSMOM
30/90/180). Sostituirlo con un ensemble di meccanismi diversi ma tutti long-flat (Donchian,
incrocio EWMA, canale di Keltner, Kaufman/KAMA) migliora la PROTEZIONE — che e' il suo
compito — a pari drift e a pari costo? NON e' la domanda `marginal_vs_tp01` (secondo sleeve):
e' la STESSA quota di libro espressa da piu' meccanismi, quindi `weights_tilt_null` non si
applica (il vettore dei pesi e' identico nei due bracci).

Esito: REFUTED. Libro, pesi, cron, config INVARIATI.

Entrambe le attese a priori, scritte prima di misurare, sono REFUTATE:
 (A1) "correlano 0,85-0,95, l'ensemble e' TP01 con piu' fee" -> 0,795 sui rendimenti e 0,608
      sulle POSIZIONI; KEL/KAU stanno a 0,51-0,53 da TSMOM. Meccanismi genuinamente diversi:
      il filone non si chiude sulla matrice, va misurato fino in fondo.
 (A2) "meno DD sara' de-levering" -> k_isovol 0,990: NON C'E' NIENTE DA DE-LEVERE. Vol
      identica (12,1 vs 12,0%), esposizione media PIU' ALTA (0,152 vs 0,141), tempo a mercato
      70% vs 55%. Il null non e' "superato", e' SENZA POTENZA, e va detto cosi'.

Muore invece sulla DISTRIBUZIONE della protezione (criterio (B) di edge_watch importato dal
sorgente di produzione) e sul costo al libro:
 - per anno x 24 ancore: l'ensemble protegge PEGGIO in 2022, 2024, 2025 e 2026 a 0/24 ancore
   (degradazione UNANIME, non fortuna d'ancora) e meglio in 2019-2021 e 2023. I quattro anni
   in cui perde sono i quattro PIU' RECENTI. Nel 2022 (DD buy&hold 68%, il sinistro maggiore)
   il rapporto passa 0,04 -> 0,13 = 3,2x peggio.
 - 73/192 celle ancora-x-anno a favore dell'ensemble (TSMOM meglio nel 62%); rapporto MEDIO
   0,18 -> 0,21 (peggiora, 0/24 ancore favorevoli).
 - costo sul LIBRO 75/25: Sharpe hold-out delta appaiato -0,127, favorevole 3/24.
 - 0/30 delle 31 composizioni possibili batte TSMOM da solo su protezione E hold-out insieme:
   il compromesso non e' di questo ensemble, e' della famiglia.
 - e il criterio che si voleva migliorare NON E' BINDING: TSMOM ha rapporto peggiore 0,635
   contro la soglia 0,75 in 192 celle su 192.

ERRORE MIO catturato in sessione e riportato nello script: la prima stesura decideva sul CASO
PEGGIORE (max degli 8 rapporti), che migliora 0,56 -> 0,49, e avrebbe stampato "PROMOSSO A
LEAD". Il massimo di 8 numeri non e' una statistica: si era mosso perche' era migliorato UN
anno (il 2023, che deteneva il massimo) mentre 4 su 8 peggioravano. Secondo errore corretto:
il tempo a mercato era calcolato sulla serie 50/50 (non-zero se lo e' UNO dei due asset) e
dava 66,8/79,4% contro i 55,4/70,3% di eval_weights nella stessa pagina.

Onesta' verso l'ensemble: l'ancora canonica h=0 e' la MIGLIORE delle 24 per lo Sharpe hold-out
di TSMOM (100 pctl, replica indipendente della fortuna d'ancora del 02/07 e 26/07), quindi il
divario a h=0 (-0,37) e' gonfiato: il numero da citare e' la mediana appaiata, -0,067.

Fatto trasferibile: mediare meccanismi di trend NON diversifica il rischio di trend. Cinque
segnali guidati dallo stesso prezzo divergono solo ai BORDI del trend — cioe' nei ribassi — e
la media li tiene mezzi-lunghi mentre il singolo e' gia' flat. Piu' meccanismi = ingresso e
uscita piu' morbidi, non piu' assicurazione. Cio' che NON si conclude: che la monocultura sia
sicura — servirebbe un regime in cui il TSMOM fallisce, e in 7,4 anni non c'e'.

Controlli: replica bit-exact vs sleeves._tp01_returns (max|diff| = 0,0); criterio (B) 8/8 su
TP01 come pubblicato e 0/8 su buy&hold; stimatore di correlazione validato su corr(TP01,SKH01)
= +0,095 contro +0,09 pubblicato; il null del de-levering DEVE scattare e scatta su un
de-levering vero (target_vol 10%); causality_ok max_tail_diff 0,0 su 5/5 + ensemble.
DSR di famiglia 0,999 PASS ma dichiarato NON decisivo (famiglia omogenea -> sr0 piccolo per
costruzione); trial contati al rialzo 22 + 31 composizioni = 53.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 01:49:29 +00:00

903 lines
48 KiB
Python

#!/usr/bin/env python
"""r0823_tp01_twin.py — §49 TP01-TWIN: TP01 e' UN SOLO meccanismo sul 75% del libro.
LA DOMANDA (decidibile, non retorica)
=====================================
TP01 e' il 75% del libro live. Il progetto ne ha misurato il valore con precisione: NON genera
ritorno (contributo hold-out negativo nel 99,1% delle ancore, LOO 26/07) ma TAGLIA IL DRAWDOWN
~6x contro il buy&hold, e supera il criterio di protezione di `edge_watch` 8/8 anni di sinistro.
E' un'assicurazione e si giudica sul sinistro.
Ma la sua direzione e' UNA definizione di trend: media di tre `sign(close/close[-h]-1)` a
30/90/180 giorni. Tutto il 75% del libro dipende da quella.
Sostituire TP01 con un ENSEMBLE di meccanismi di trend DIVERSI ma tutti long-flat migliora
la PROTEZIONE (che e' il suo compito), a pari drift e a pari costo?
NB la formulazione: NON "un secondo sleeve che si aggiunge" (quella e' la domanda
`marginal_vs_tp01`, gia' fatta decine di volte e sempre REDUNDANT per i trend), ma LA STESSA
QUOTA DI LIBRO espressa da piu' meccanismi. E' robustezza, non alpha. Percio' `weights_tilt_null`
NON si applica: il peso 75/25 non si muove, cambia solo cosa c'e' dentro il 75%.
ATTESA A PRIORI — SCRITTA PRIMA DI MISURARE (riportata anche se refutata; lo e')
===============================================================================
(A1) "I meccanismi di trend a 1d sullo stesso asset correlano 0,85-0,95, quindi l'ensemble sara'
TP01 con piu' fee." Corollario: la matrice di correlazione (§2) puo' chiudere il filone da
sola. -> REFUTATA: corr media 0,795 sui rendimenti e 0,608 sulle POSIZIONI; KEL e KAU
stanno a 0,51-0,53 di posizione da TSMOM. Non sono lo stesso oggetto.
(A2) "Se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)."
-> REFUTATA: la vol realizzata e' IDENTICA (12,1% vs 12,0%), l'esposizione media e il tempo
a mercato dell'ensemble sono PIU' ALTI. Non c'e' niente da de-levere: il null non e'
applicabile, e va detto cosi' invece di dire "non refuta" (che suggerirebbe che l'ensemble
abbia superato un test che in realta' non aveva potenza).
MECCANISMI — DICHIARATI TUTTI PRIMA, E CONTATI COME TRIAL (§8)
==============================================================
Tutti LONG-FLAT, tutti vol-targeted 20% (cap 2x, finestra 30g), tutti a 1d, tutti sullo stesso
50/50 BTC+ETH, tutti causali (decidono a close[i], tengono in i+1), tutti a fee 0,10% RT.
TSMOM — il riferimento: media dei sign(c/c[-h]-1), h in 30/90/180 (= TP01 CANONICAL)
DON — Donchian breakout long-flat: entra sopra il massimo dei precedenti N, esce sotto il
minimo dei precedenti N/2. NB: SKH01 e' un Donchian ma L/S e dual-TF 230m/690m —
NON e' lo stesso oggetto e qui non viene ricreato.
EWMA — incrocio di medie esponenziali, blend di 3 coppie (stessa forma multi-orizzonte di TSMOM)
KEL — canale di Keltner: entra sopra ema+k*atr, esce sotto la ema (mid)
KAU — Kaufman: KAMA (efficiency-ratio adattiva), lunga se la KAMA sale su `slope` barre
Indicatori presi da `src/strategies/indicators.py` (ema, atr) e da `altlib` (donchian, vol_target,
eval_weights, eval_weights_smallcap, causality_ok, study_family_honest): importati, non riscritti.
ENSEMBLE (due varianti, dichiarate a priori)
ENS-DIR — media delle 5 DIREZIONI, poi UN solo vol-target. E' esattamente la struttura di TP01
(che gia' media 3 direzioni prima di scalare): "stessa quota, piu' meccanismi".
ENS-POS — media delle 5 POSIZIONI gia' vol-targeted (differisce solo dove morde il cap 2x).
Il costo si conta sulla posizione NETTATA (il libro ha UNA posizione per strumento su Deribit):
la fee e' su |delta(posizione media)|. Si riporta anche il caso non-nettato come limite superiore.
COSA DECIDE (in ordine)
§2 correlazione fra meccanismi
§4 PROTEZIONE anno per anno, criterio (B) di `scripts/live/edge_watch.py` importato dal
sorgente di produzione: negli anni con DD buy&hold > 10%, il DD dello sleeve deve restare
sotto il 75% di quello.
⚠️ ERRORE MIO, CATTURATO IN SESSIONE E RIPORTATO: la prima stesura decideva sul CASO
PEGGIORE (il massimo degli 8 rapporti), che migliora 0,56 -> 0,49, e avrebbe stampato
"ENSEMBLE PROMOSSO A LEAD". Ma il massimo di 8 numeri non e' una statistica: si e' mosso
perche' e' migliorato UN anno (2023), mentre 4 anni su 8 PEGGIORANO — compreso il 2022, che
e' l'unico sinistro vero del campione (0,04 -> 0,13, tre volte peggio). §4-bis misura la
DISTRIBUZIONE (test dei segni per anno, e 24x8 celle appaiate sulle ancore) invece del massimo.
§5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING (7a occorrenza). NB: lo Sharpe e' invariante
alla scala, quindi il null "iso-DD, confronta lo Sharpe" e' DEGENERE (regola dell'ondata
22/08): la forma non degenere e' iso-VOL sulla PROTEZIONE + confronto diretto di Sharpe.
§5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO: 31 sottoinsiemi non
vuoti, di cui ne e' stato dichiarato UNO. Si misura dove cade il dichiarato nella
distribuzione dei 31 — se sta in alto, un ensemble "migliore" e' selezione, non robustezza.
§6 banda d'ancora 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane)
§7 costi: curva fee 0-20 bps RT + `eval_weights_smallcap` a $635
§8 deflated-Sharpe di FAMIGLIA (`study_family_honest`) — con l'avvertenza che su una famiglia
OMOGENEA sr0 e' piccolo per costruzione e il DSR passa facile: NON e' il gate che decide qui
§9 impatto sul libro 75/25 con SKH01, ANCHE in banda d'ancora appaiata (la gamba SKH01 e'
identica nei due bracci, quindi la sua fortuna d'ancora si CANCELLA nella differenza)
VINCOLI: script di sola lettura. Non tocca src/, config/, scripts/live/, tests/, data/.
Runtime ~1 min su 2 CPU.
"""
from __future__ import annotations
import importlib.util
import itertools
import sys
import time
import warnings
from functools import lru_cache
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" / "alt"))
sys.path.insert(0, str(ROOT))
import altlib as al # noqa: E402
from src.data.downloader import load_data # noqa: E402
from src.portfolio.portfolio import DAYS_PER_YEAR, combine_outer, to_daily # noqa: E402
from src.portfolio.sleeves import _tp01_returns, skyhook_sleeve # noqa: E402
from src.strategies.indicators import atr as ind_atr # noqa: E402
from src.strategies.indicators import ema as ind_ema # noqa: E402
from src.strategies.trend_portfolio import (CANONICAL, resample_tf, # noqa: E402
simple_returns, tsmom_blend)
warnings.filterwarnings("ignore", category=RuntimeWarning)
ASSETS = ("BTC", "ETH")
FEE_SIDE = CANONICAL["fee_side"] # 0.0005 = 0.10% RT
TARGET_VOL = CANONICAL["target_vol"] # 0.20
LEV_CAP = CANONICAL["leverage"] # 2.0
VOL_WIN = CANONICAL["vol_win_days"] # 30
HOLDOUT = al.HOLDOUT
ANCHORS = tuple(range(24))
CAPITALE = 635.0 # conto reale dichiarato nel progetto
# --- criterio (B) importato DAL SORGENTE DI PRODUZIONE (non riscritto) ------
_spec = importlib.util.spec_from_file_location("_edge_watch", ROOT / "scripts" / "live" / "edge_watch.py")
_ew = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_ew)
SINISTRO_DD, PROTECT_MAX = _ew.SINISTRO_DD, _ew.PROTECT_MAX
# ===========================================================================
# 0. DATI E MECCANISMI
# ===========================================================================
@lru_cache(maxsize=8)
def raw1h(asset: str) -> pd.DataFrame:
return load_data(asset, "1h")
def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame:
"""Barre giornaliere ancorate alle h:00 UTC. h=0 usa direttamente resample_1d
(stessa convenzione label/closed='left' di trend_portfolio.resample_tf)."""
if h == 0:
return resample_tf(df_1h, "1D")
g = df_1h.copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
g.index = idx
out = g.resample("24h", offset=pd.Timedelta(hours=h), label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
out = out.dropna(subset=["open"])
out["datetime"] = out.index
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
@lru_cache(maxsize=64)
def bars(asset: str, h: int) -> pd.DataFrame:
return resample_offset(raw1h(asset), h)
# --- direzioni: array in [0,1], NaN durante il proprio warm-up -------------
def dir_tsmom(df, horizons=(30, 90, 180)):
c = df["close"].values.astype(float)
d = np.clip(tsmom_blend(c, tuple(horizons)), 0.0, None) # LONG-FLAT
d[:min(horizons)] = np.nan # nan_to_num -> 0 = esattamente tsmom_blend
return d
def dir_don(df, win=50):
ex = max(2, win // 2)
hi, _ = al.donchian(df, win)
_, lo = al.donchian(df, ex)
c = df["close"].values.astype(float)
out = np.full(len(c), np.nan)
st = 0.0
for i in range(len(c)):
if not (np.isfinite(hi[i]) and np.isfinite(lo[i])):
continue
if st == 0.0 and c[i] > hi[i]:
st = 1.0
elif st == 1.0 and c[i] < lo[i]:
st = 0.0
out[i] = st
return out
def dir_ewma(df, pairs=((10, 40), (20, 80), (40, 160))):
c = df["close"].values.astype(float)
acc = np.zeros(len(c))
cnt = np.zeros(len(c))
for f, s in pairs:
ef, es = ind_ema(c, f), ind_ema(c, s)
v = np.isfinite(ef) & np.isfinite(es)
acc[v] += np.sign(ef[v] - es[v])
cnt[v] += 1
out = np.full(len(c), np.nan)
nz = cnt > 0
out[nz] = np.clip(acc[nz] / cnt[nz], 0.0, None)
return out
def dir_kel(df, win=20, k=1.5):
c = df["close"].values.astype(float)
mid = ind_ema(c, win)
a = ind_atr(df["high"].values.astype(float), df["low"].values.astype(float), c, win)
up = mid + k * a
out = np.full(len(c), np.nan)
st = 0.0
for i in range(len(c)):
if not (np.isfinite(up[i]) and np.isfinite(mid[i])):
continue
if st == 0.0 and c[i] > up[i]:
st = 1.0
elif st == 1.0 and c[i] < mid[i]:
st = 0.0
out[i] = st
return out
def dir_kau(df, n=20, slope=5, fast=2, slow=30):
c = df["close"].values.astype(float)
d = np.abs(np.diff(c, prepend=c[0]))
vsum = pd.Series(d).rolling(n, min_periods=n).sum().values
er = np.full(len(c), np.nan)
er[n:] = np.where(vsum[n:] > 0, np.abs(c[n:] - c[:-n]) / vsum[n:], 0.0)
fsc, ssc = 2.0 / (fast + 1), 2.0 / (slow + 1)
kama = np.full(len(c), np.nan)
for i in range(n, len(c)):
a = (er[i] * (fsc - ssc) + ssc) ** 2
prev = kama[i - 1] if np.isfinite(kama[i - 1]) else c[i - 1]
kama[i] = prev + a * (c[i] - prev)
out = np.full(len(c), np.nan)
ok = np.isfinite(kama)
for i in range(n + slope, len(c)):
if ok[i] and ok[i - slope]:
out[i] = 1.0 if kama[i] > kama[i - slope] else 0.0
return out
MECHS = {
"TSMOM": (dir_tsmom, dict(horizons=(30, 90, 180))),
"DON": (dir_don, dict(win=50)),
"EWMA": (dir_ewma, dict(pairs=((10, 40), (20, 80), (40, 160)))),
"KEL": (dir_kel, dict(win=20, k=1.5)),
"KAU": (dir_kau, dict(n=20, slope=5)),
}
MNAMES = tuple(MECHS)
def direction(name: str, df: pd.DataFrame, **over) -> np.ndarray:
fn, p = MECHS[name]
q = dict(p)
q.update(over)
return fn(df, **q)
@lru_cache(maxsize=64)
def dircache(asset: str, h: int) -> dict:
df = bars(asset, h)
return {m: direction(m, df) for m in MNAMES}
def to_pos(d: np.ndarray, df: pd.DataFrame, target_vol: float = TARGET_VOL) -> np.ndarray:
return al.vol_target(np.nan_to_num(np.asarray(d, float), nan=0.0), df,
target_vol=target_vol, vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
def net_asset(df: pd.DataFrame, tgt: np.ndarray, fee_side: float = FEE_SIDE) -> pd.Series:
"""Identico, riga per riga, a `sleeves._tp01_returns` per singolo asset."""
r = simple_returns(df["close"].values.astype(float))
held = np.zeros(len(tgt))
held[1:] = tgt[:-1]
net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0))
net[0] = 0.0
return pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
def book5050(series: dict) -> pd.Series:
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)
def mean_dirs(df, h, asset, names) -> np.ndarray:
D = np.vstack([dircache(asset, h)[m] for m in names])
with warnings.catch_warnings():
warnings.simplefilter("ignore")
e = np.nanmean(D, axis=0)
return np.nan_to_num(e, nan=0.0)
def ens_dirs(df: pd.DataFrame, names=MNAMES) -> np.ndarray:
"""Versione stateless (per causality_ok / study_family_honest): ricalcola dal df."""
D = np.vstack([direction(m, df) for m in names])
with warnings.catch_warnings():
warnings.simplefilter("ignore")
e = np.nanmean(D, axis=0)
return np.nan_to_num(e, nan=0.0)
def ens_pos(df: pd.DataFrame, names=MNAMES) -> np.ndarray:
return np.mean([to_pos(direction(m, df), df) for m in names], axis=0)
def targets_for(kind, h: int, target_vol: float = TARGET_VOL) -> dict:
"""kind = nome meccanismo | 'ENS-DIR' | 'ENS-POS' | tuple di meccanismi (sottoinsieme)."""
out = {}
for a in ASSETS:
df = bars(a, h)
if isinstance(kind, tuple):
out[a] = (df, to_pos(mean_dirs(df, h, a, kind), df, target_vol))
elif kind == "ENS-DIR":
out[a] = (df, to_pos(mean_dirs(df, h, a, MNAMES), df, target_vol))
elif kind == "ENS-POS":
out[a] = (df, np.mean([to_pos(dircache(a, h)[m], df) for m in MNAMES], axis=0)
* (target_vol / TARGET_VOL))
else:
out[a] = (df, to_pos(dircache(a, h)[kind], df, target_vol))
return out
def strat_series(kind, h: int, fee_side: float = FEE_SIDE,
target_vol: float = TARGET_VOL) -> pd.Series:
tg = targets_for(kind, h, target_vol)
return book5050({a: net_asset(tg[a][0], tg[a][1], fee_side) for a in ASSETS})
@lru_cache(maxsize=64)
def buyhold(h: int) -> pd.Series:
out = {}
for a in ASSETS:
df = bars(a, h)
out[a] = pd.Series(simple_returns(df["close"].values.astype(float)),
index=pd.to_datetime(df["datetime"]))
return book5050(out)
# ===========================================================================
# METRICHE
# ===========================================================================
def sh(s: pd.Series) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.mean() / r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 and r.std() > 0 else 0.0
def vol(s: pd.Series) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.std() * np.sqrt(DAYS_PER_YEAR)) if len(r) > 2 else 0.0
def drift(s: pd.Series) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.mean() * DAYS_PER_YEAR) if len(r) else 0.0
def maxdd(s: pd.Series) -> float:
return float(_ew.maxdd(pd.Series(s).dropna()))
def dd_window(s: pd.Series):
"""Estremi temporali del maxDD (per capire se sta DENTRO un anno o lo attraversa)."""
s = pd.Series(s).dropna()
e = (1.0 + s).cumprod()
pk = e.cummax()
dd = 1.0 - e / pk
j = int(np.argmax(dd.values))
i = int(np.argmax(e.values[:j + 1])) if j > 0 else 0
return s.index[i], s.index[j]
def worst3m(s: pd.Series) -> float:
e = (1.0 + pd.Series(s).dropna()).cumprod()
w = 91
if len(e) <= w:
return 0.0
return float((e.values[w:] / e.values[:-w] - 1.0).min())
def hold(s: pd.Series) -> pd.Series:
s = pd.Series(s).dropna()
return s[s.index >= HOLDOUT]
def protection(sleeve: pd.Series, bh: pd.Series) -> list[dict]:
"""Criterio (B) di edge_watch, chiamato SUL SORGENTE DI PRODUZIONE."""
return _ew.protection_by_year(pd.Series(sleeve).dropna(), pd.Series(bh).dropna())
def prot_summary(prot: list[dict]) -> dict:
if not prot:
return dict(n=0, n_ok=0, worst=float("nan"), mean=float("nan"))
rr = [p["ratio"] for p in prot]
return dict(n=len(prot), n_ok=int(sum(p["ok"] for p in prot)),
worst=float(max(rr)), mean=float(np.mean(rr)))
def line(name: str, s: pd.Series, prot: list[dict] | None = None) -> str:
ps = prot_summary(prot) if prot is not None else None
tail = (f"{ps['n_ok']}/{ps['n']}{'':>2}{ps['worst']:>8.2f}{ps['mean']:>8.2f}" if ps else "")
return (f" {name:<12}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{drift(s):>9.1%}{vol(s):>8.1%}"
f"{maxdd(s):>9.1%}{worst3m(s):>9.1%} {tail}")
HEAD = (f" {'strategia':<12}{'ShFULL':>8}{'ShHOLD':>9}{'drift':>9}{'vol':>8}"
f"{'maxDD':>9}{'peg.3m':>9} {'prot':>5}{'peggio':>8}{'medio':>8}")
# ===========================================================================
def main() -> int:
t0 = time.time()
P = print
P("=" * 104)
P(" §49 TP01-TWIN — TP01 e' UN SOLO meccanismo sul 75% del libro. Un ENSEMBLE protegge di piu'?")
P("=" * 104)
P("\n ATTESA A PRIORI (scritta prima di misurare, riportata anche se refutata):")
P(" (A1) i meccanismi di trend a 1d correlano 0,85-0,95 -> l'ensemble sara' TP01 con piu' fee")
P(" (A2) se l'ensemble avra' meno DD sara' per DE-LEVERING (7a occorrenza del null)")
P(" Esito anticipato qui per onesta': ENTRAMBE REFUTATE. Il filone non muore dove me")
P(" l'aspettavo — muore su §4-bis (distribuzione della protezione) e §9 (costo sul libro).")
# ---------------------------------------------------------------- §1
P("\n" + "=" * 104)
P(" §1 REPLICA BIT-EXACT del canonico contro sleeves._tp01_returns()")
P("=" * 104)
ref = _tp01_returns()
mine = strat_series("TSMOM", 0)
J = pd.concat({"ref": ref, "mine": mine}, axis=1, join="outer")
md = float(np.nanmax(np.abs(J["ref"].values - J["mine"].values)))
same_len = (len(ref) == len(mine) == len(J))
P(f"\n barre ref={len(ref)} mie={len(mine)} unione={len(J)} max|diff| = {md:.3e}"
f" -> {'OK (bit-exact)' if (md == 0.0 and same_len) else 'DIVERGE — STOP'}")
if not (md == 0.0 and same_len):
P(" ABORT: senza replica bit-exact nessun delta e' interpretabile.")
return 1
# ---------------------------------------------------------------- §2
P("\n" + "=" * 104)
P(" §2 MATRICE DI CORRELAZIONE FRA MECCANISMI")
P("=" * 104)
S = {m: strat_series(m, 0) for m in MNAMES}
S["ENS-DIR"] = strat_series("ENS-DIR", 0)
S["ENS-POS"] = strat_series("ENS-POS", 0)
names = list(MNAMES)
R = pd.concat({m: S[m] for m in names}, axis=1, join="inner").dropna()
C = R.corr()
P(f"\n (a) RENDIMENTI NETTI giornalieri, finestra comune {R.index[0].date()} -> "
f"{R.index[-1].date()} (n={len(R)})\n")
P(" " + "".join(f"{m:>9}" for m in names))
for m in names:
P(f" {m:<6}" + "".join(f"{C.loc[m, k]:>9.3f}" for k in names))
off = [C.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]]
P(f"\n media fuori diagonale = {np.mean(off):.3f} min = {np.min(off):.3f} max = {np.max(off):.3f}")
Pos = {}
for m in names:
cols = {}
for a in ASSETS:
df = bars(a, 0)
cols[a] = pd.Series(to_pos(dircache(a, 0)[m], df), index=pd.to_datetime(df["datetime"]))
Pos[m] = book5050(cols)
PosJ = pd.concat(Pos, axis=1, join="inner").dropna()
CP = PosJ.corr()
offp = [CP.loc[a, b] for i, a in enumerate(names) for b in names[i + 1:]]
P("\n (b) POSIZIONI-bersaglio (e' qui che vive la ridondanza)\n")
P(" " + "".join(f"{m:>9}" for m in names))
for m in names:
P(f" {m:<6}" + "".join(f"{CP.loc[m, k]:>9.3f}" for k in names))
P(f"\n media fuori diagonale = {np.mean(offp):.3f} min = {np.min(offp):.3f} max = {np.max(offp):.3f}")
P(f"\n -> ATTESA (A1) REFUTATA: 0,795 e 0,608, non 0,85-0,95. KEL/KAU sono a 0,51-0,53 di")
P(" POSIZIONE da TSMOM: sono meccanismi genuinamente diversi, non TP01 travestito.")
P(" Il filone NON si chiude in §2 — bisogna misurarlo.")
skh = skyhook_sleeve(1.0).daily()
tpd = to_daily(S["TSMOM"])
cc = pd.concat({"tp": tpd, "sk": skh}, axis=1, join="inner").dropna()
c_tp_skh = float(cc["tp"].corr(cc["sk"]))
rng = np.random.default_rng(20260823)
shuf = pd.Series(rng.permutation(S["TSMOM"].values), index=S["TSMOM"].index)
c_shuf = float(pd.concat({"a": S["TSMOM"], "b": shuf}, axis=1).corr().iloc[0, 1])
P(f"\n CONTROLLO POSITIVO dello stimatore: corr(TSMOM,TSMOM)={C.loc['TSMOM','TSMOM']:.3f} (atteso 1.000) · "
f"corr(TP01,SKH01)={c_tp_skh:+.3f} (pubblicato ~+0.09) · corr(TP01, permutato)={c_shuf:+.3f} (atteso ~0)")
# ---------------------------------------------------------------- §3
P("\n" + "=" * 104)
P(" §3 METRICHE STANDALONE (ancora canonica h=0) — 50/50 BTC+ETH, fee 0,10% RT")
P("=" * 104 + "\n")
bh0 = buyhold(0)
P(HEAD)
for k in names + ["ENS-DIR", "ENS-POS"]:
P(line(k, S[k], protection(S[k], bh0)))
P(line("BUY&HOLD", bh0, protection(bh0, bh0)))
P("\n NB: il MIGLIOR protettore singolo e' KEL (peggior rapporto 0,33) e ha lo Sharpe")
P(" hold-out PIU' BASSO (-0,32): e' il premio dell'assicurazione, misurato. L'ensemble")
P(" NON batte il suo membro migliore sulla protezione — e' la media. Sceglierlo sarebbe")
P(" selezione su 5 candidati, non robustezza.")
# ---------------------------------------------------------------- §4
P("\n" + "=" * 104)
P(" §4 PROTEZIONE ANNO PER ANNO — criterio (B) di scripts/live/edge_watch.py (importato)")
P(f" anni con DD buy&hold > {SINISTRO_DD:.0%}; il DD dello sleeve deve stare sotto il "
f"{PROTECT_MAX:.0%} di quello")
P("=" * 104 + "\n")
pr_t = protection(S["TSMOM"], bh0)
pr_e = protection(S["ENS-DIR"], bh0)
P(f" {'anno':>6}{'DD b&h':>10}{'DD TSMOM':>11}{'rap.':>7}{'esito':>7}"
f"{'DD ENS':>11}{'rap.':>7}{'esito':>7}{' delta rapporto':>18}{' chi protegge':>16}")
for a, b in zip(pr_t, pr_e):
dlt = b["ratio"] - a["ratio"]
P(f" {a['year']:>6}{a['dd_bh']:>9.1%}{a['dd_tp']:>10.1%}{a['ratio']:>7.2f}"
f"{' OK' if a['ok'] else ' FAIL':>7}"
f"{b['dd_tp']:>10.1%}{b['ratio']:>7.2f}{' OK' if b['ok'] else ' FAIL':>7}"
f"{dlt:>+18.3f}{(' ENS' if dlt < 0 else ' TSMOM'):>16}")
st, se = prot_summary(pr_t), prot_summary(pr_e)
P(f"\n TSMOM: {st['n_ok']}/{st['n']} superati · rapporto PEGGIORE {st['worst']:.2f} · medio {st['mean']:.2f}")
P(f" ENS : {se['n_ok']}/{se['n']} superati · rapporto PEGGIORE {se['worst']:.2f} · medio {se['mean']:.2f}")
# ---------------------------------------------------------------- §4-bis
P("\n" + "-" * 104)
P(" §4-bis IL MASSIMO DI 8 NUMERI NON E' UNA STATISTICA — la DISTRIBUZIONE della protezione")
P("-" * 104)
dlt = np.array([b["ratio"] - a["ratio"] for a, b in zip(pr_t, pr_e)])
nb = int((dlt < 0).sum())
P(f"\n h=0: ENS protegge meglio in {nb}/{len(dlt)} anni di sinistro, peggio in {len(dlt)-nb}"
f" -> test dei segni = MONETA")
P(f" delta mediano del rapporto {np.median(dlt):+.3f} · medio {np.mean(dlt):+.3f} "
f"(il rapporto MEDIO PEGGIORA: {st['mean']:.2f} -> {se['mean']:.2f})")
y22t = [p for p in pr_t if p["year"] == 2022][0]
y22e = [p for p in pr_e if p["year"] == 2022][0]
P(f"\n ⚠ IL SINISTRO PIU' GRANDE DEL CAMPIONE — 2022, deleveraging (DD buy&hold "
f"{y22t['dd_bh']:.0%}, il massimo degli 8 anni): TSMOM protegge")
P(f" {1/y22t['ratio']:.1f}x (DD {y22t['dd_tp']:.1%}), ENS {1/y22e['ratio']:.1f}x (DD {y22e['dd_tp']:.1%})"
f" -> l'ensemble e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE nell'anno peggiore.")
P(" Regola del progetto: uno sleeve difensivo si giudica SUL SINISTRO. Qui il sinistro dice ENS peggio.")
P(f"\n Il 'caso peggiore migliora 0,56 -> 0,49' e' UN SOLO ANNO (2023, {dlt[4]:+.3f}): il massimo")
P(" si e' spostato perche' e' migliorata la cella che lo deteneva, non perche' la protezione")
P(" sia migliorata. ERRORE MIO, catturato in sessione: la prima stesura decideva su quel massimo.")
pr_bh = protection(bh0, bh0)
P(f"\n CONTROLLI POSITIVI del criterio: TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8) · "
f"buy&hold contro se stesso {prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']} "
f"(rapporto 1.00 -> DEVE fallire tutti)")
# ---------------------------------------------------------------- §5
P("\n" + "=" * 104)
P(" §5 GATE ISO-VOLATILITA' + NULL DEL DE-LEVERING")
P("=" * 104)
v_t, v_e = vol(S["TSMOM"]), vol(S["ENS-DIR"])
d_t, d_e = drift(S["TSMOM"]), drift(S["ENS-DIR"])
k_vol = v_e / v_t if v_t > 0 else 1.0
expo = {m: float(np.mean(np.abs(PosJ[m]))) for m in names}
ens_expo = float(np.mean(np.abs(book5050({a: pd.Series(to_pos(mean_dirs(bars(a, 0), 0, a, MNAMES), bars(a, 0)),
index=pd.to_datetime(bars(a, 0)["datetime"]))
for a in ASSETS}))))
# tempo a mercato con la STESSA definizione di §7 (media PER-ASSET sulla posizione tenuta).
# ERRORE MIO corretto in sessione: la prima stesura lo calcolava sulla serie 50/50, che e'
# non-zero se lo e' UNO DEI DUE asset, e dava 66,8%/79,4% contro i 55,4%/70,3% stampati da
# eval_weights nella stessa pagina. Due definizioni della stessa grandezza = numero inutile.
def _tim(kind):
tg = targets_for(kind, 0)
return float(np.mean([al.eval_weights(tg[a][0], tg[a][1])["time_in_market"] for a in ASSETS]))
tim_t, tim_e = _tim("TSMOM"), _tim("ENS-DIR")
P(f"\n vol realizzata TSMOM {v_t:.2%} ENS {v_e:.2%} -> k_isovol = {k_vol:.4f}")
P(f" drift TSMOM {d_t:.2%} ENS {d_e:.2%}")
P(f" esposizione media |pos| TSMOM {expo['TSMOM']:.3f} ENS {ens_expo:.3f}"
f" · tempo a mercato TSMOM {tim_t:.1%} ENS {tim_e:.1%}")
P(f" per meccanismo: " + ", ".join(f"{m} {expo[m]:.3f}" for m in names))
P(f"\n -> ATTESA (A2) REFUTATA, E IL NULL NON E' APPLICABILE: k_isovol = {k_vol:.4f} ≈ 1, cioe'")
P(" NON C'E' NIENTE DA DE-LEVERE. L'ensemble ha la STESSA vol, esposizione media PIU' ALTA")
P(" e tempo a mercato PIU' ALTO. Dire 'il null non refuta' sarebbe fuorviante: il null non")
P(" ha potenza perche' la variante non e' una ri-scalatura. Si dice cosi'.")
P("\n ⚠ Lo Sharpe e' INVARIANTE alla scala -> il null 'iso-DD, confronta lo Sharpe' e' DEGENERE")
P(" (regola dell'ondata 22/08). Forma non degenere applicata: confronto diretto di Sharpe +")
P(" de-levering fatto come si farebbe DAVVERO nel live, abbassando `target_vol` (26/06).\n")
tv_iso = TARGET_VOL * k_vol
iso_run = strat_series("TSMOM", 0, target_vol=tv_iso)
P(HEAD)
P(line("TSMOM", S["TSMOM"], pr_t))
P(line(f"TSMOM@tv{tv_iso:.3f}", iso_run, protection(iso_run, bh0)))
P(line("ENS-DIR", S["ENS-DIR"], pr_e))
w_t, w_e = dd_window(S["TSMOM"]), dd_window(S["ENS-DIR"])
P(f"\n DIAGNOSTICA — perche' la protezione ANNUALE puo' migliorare mentre il maxDD peggiora:")
P(f" finestra del maxDD TSMOM {w_t[0].date()} -> {w_t[1].date()} ({(w_t[1]-w_t[0]).days} g)")
P(f" ENS {w_e[0].date()} -> {w_e[1].date()} ({(w_e[1]-w_e[0]).days} g)")
P(" Il criterio (B) misura il DD DENTRO l'anno solare; il maxDD attraversa gli anni. Un")
P(" ensemble che sta a mercato il 70% del tempo (contro 55%) sanguina piu' a lungo e piu'")
P(" piano: DD annuali un po' piu' piccoli in qualche anno, DD complessivo piu' lungo.")
# ---------------------------------------------------------------- §5-bis
P("\n" + "-" * 104)
P(" §5-bis LA COMPOSIZIONE DELL'ENSEMBLE E' ESSA STESSA UN PARAMETRO LIBERO (31 sottoinsiemi)")
P("-" * 104)
subs = []
for k in range(1, len(MNAMES) + 1):
for comb in itertools.combinations(MNAMES, k):
s = strat_series(comb, 0)
ps = prot_summary(protection(s, bh0))
subs.append(dict(sub="+".join(comb), k=k, sh=sh(s), hd=sh(hold(s)),
dd=maxdd(s), worst=ps["worst"], mean=ps["mean"], n_ok=ps["n_ok"]))
SB = pd.DataFrame(subs)
dich = SB[SB["sub"] == "+".join(MNAMES)].iloc[0]
P(f"\n 31 sottoinsiemi non vuoti; ne era stato DICHIARATO 1 (tutti e 5). Dove cade il dichiarato:")
P(f" protezione PEGGIORE {dich['worst']:.3f} -> {float((SB['worst'] <= dich['worst']).mean())*100:.0f}° pctl"
f" (migliore dei 31: {SB['worst'].min():.3f} = {SB.loc[SB['worst'].idxmin(),'sub']})")
P(f" Sharpe FULL {dich['sh']:.3f} -> {float((SB['sh'] <= dich['sh']).mean())*100:.0f}° pctl"
f" (migliore: {SB['sh'].max():.3f} = {SB.loc[SB['sh'].idxmax(),'sub']})")
P(f" Sharpe HOLD-OUT {dich['hd']:.3f} -> {float((SB['hd'] <= dich['hd']).mean())*100:.0f}° pctl"
f" (migliore: {SB['hd'].max():.3f} = {SB.loc[SB['hd'].idxmax(),'sub']})")
P(f" maxDD {dich['dd']:.3f} -> {float((SB['dd'] <= dich['dd']).mean())*100:.0f}° pctl"
f" (migliore: {SB['dd'].min():.3f} = {SB.loc[SB['dd'].idxmin(),'sub']})")
n_beat = int(((SB['worst'] < st['worst']) & (SB['hd'] > sh(hold(S['TSMOM'])))).sum())
P(f"\n sottoinsiemi che battono TSMOM da solo su ENTRAMBI (protez. peggiore E Sharpe hold-out):"
f" {n_beat}/31 (TSMOM e' uno dei 31 e non puo' battere se stesso -> {n_beat}/30 alternative)")
P(" E' il risultato piu' forte della sezione: fra TUTTE le 31 composizioni non ne esiste una")
P(" che compri protezione senza pagarla sull'hold-out. Il compromesso non e' di QUESTO")
P(" ensemble, e' della famiglia.")
P(f" TSMOM da solo, per riferimento: protez. peggiore {st['worst']:.3f} · ShHOLD "
f"{sh(hold(S['TSMOM'])):.3f} · maxDD {maxdd(S['TSMOM']):.3f}")
P("\n leave-one-out (togli UN meccanismo dai 5) — quanto l'esito dipende dalla membership:")
P(f" {'ensemble':<24}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}{'prot.peggio':>13}{'prot.media':>12}")
for m in ["(tutti e 5)"] + [f"senza {x}" for x in MNAMES]:
comb = MNAMES if m.startswith("(") else tuple(x for x in MNAMES if x != m.split()[1])
row = SB[SB["sub"] == "+".join(comb)].iloc[0]
P(f" {m:<24}{row['sh']:>9.3f}{row['hd']:>9.3f}{row['dd']:>9.1%}{row['worst']:>13.2f}{row['mean']:>12.2f}")
# ---------------------------------------------------------------- §6
P("\n" + "=" * 104)
P(" §6 BANDA D'ANCORA 24 ORE — MEDIANA DELLE DIFFERENZE APPAIATE (mai la diff. delle mediane)")
P("=" * 104)
rows = []
per_year: dict = {}
cell_better = 0
cell_tot = 0
for h in ANCHORS:
bh_h = buyhold(h)
st_h = strat_series("TSMOM", h)
se_h = strat_series("ENS-DIR", h)
prt, pre = protection(st_h, bh_h), protection(se_h, bh_h)
for a, b in zip(prt, pre):
cell_tot += 1
cell_better += int(b["ratio"] < a["ratio"])
per_year.setdefault(a["year"], []).append((a["ratio"], b["ratio"]))
pt, pe = prot_summary(prt), prot_summary(pre)
rows.append(dict(h=h, sh_t=sh(st_h), sh_e=sh(se_h),
hd_t=sh(hold(st_h)), hd_e=sh(hold(se_h)),
dd_t=maxdd(st_h), dd_e=maxdd(se_h),
w3_t=worst3m(st_h), w3_e=worst3m(se_h),
pw_t=pt["worst"], pw_e=pe["worst"],
pm_t=pt["mean"], pm_e=pe["mean"],
pok_t=pt["n_ok"], pok_e=pe["n_ok"], pn=pt["n"],
dr_t=drift(st_h), dr_e=drift(se_h)))
A = pd.DataFrame(rows)
def paired(ce, ct, better="up"):
d = A[ce].values - A[ct].values
frac = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0))
return float(np.median(d)), frac, float(np.percentile(d, 10)), float(np.percentile(d, 90))
P(f"\n {'metrica':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Δ appaiato':>13}"
f"{'favorev.':>11}{'p10..p90':>22}")
for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"),
("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"),
("drift", "dr_e", "dr_t", "up"),
("maxDD", "dd_e", "dd_t", "down"),
("peggior 3 mesi", "w3_e", "w3_t", "up"),
("protez. PEGGIORE", "pw_e", "pw_t", "down"),
("protez. MEDIA", "pm_e", "pm_t", "down")]:
m, fr, p10, p90 = paired(ce, ct, better)
P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{A[ct].median():>11.3f}"
f"{A[ce].median():>10.3f}{m:>+13.4f}{fr*24:>8.0f}/24{f'[{p10:+.3f}, {p90:+.3f}]':>22}")
frac_cell = cell_better / cell_tot
_lab = ("moneta" if abs(frac_cell - 0.5) < 0.10 else
("segnale A FAVORE dell'ensemble" if frac_cell > 0.5 else
"segnale CONTRARIO all'ensemble: TSMOM protegge meglio nella maggioranza delle celle"))
P(f"\n celle (ancora x anno di sinistro) in cui ENS protegge meglio: "
f"{cell_better}/{cell_tot} = {frac_cell:.1%} -> {_lab}")
P(f" anni di sinistro superati (mediana su 24 ancore): TSMOM {A['pok_t'].median():.0f}/{A['pn'].median():.0f}"
f" ENS {A['pok_e'].median():.0f}/{A['pn'].median():.0f}"
f" · ancore con almeno un FAIL: TSMOM {int((A['pok_t'] < A['pn']).sum())}/24, "
f"ENS {int((A['pok_e'] < A['pn']).sum())}/24")
P("\n PROTEZIONE PER ANNO SU TUTTE LE 24 ANCORE — il 2022 e' robusto o e' l'ancora canonica?")
P(f" {'anno':>6}{'rapp. TSMOM (med)':>20}{'rapp. ENS (med)':>18}{'ancore ENS meglio':>22}")
for y in sorted(per_year):
pairs = np.array(per_year[y])
nbet = int((pairs[:, 1] < pairs[:, 0]).sum())
P(f" {y:>6}{np.median(pairs[:,0]):>20.3f}{np.median(pairs[:,1]):>18.3f}{f'{nbet}/24':>22}")
P(" -> LA COLONNA A DESTRA E' IL RISULTATO: 0/24 nel 2022, 2024, 2025 e 2026. Dove")
P(" l'ensemble protegge peggio lo fa a TUTTE le ancore — degradazione UNANIME, non")
P(" fortuna d'ancora. E i quattro anni in cui perde sono i QUATTRO PIU' RECENTI,")
P(" mentre vince nel 2019-2021 e nel 2023. Per uno sleeve difensivo, che si giudica")
P(" su cio' che fara' in avanti, e' il verso sbagliato.")
P("\n ⚠ ONESTA' VERSO L'ENSEMBLE: l'ancora canonica h=0 e' la MIGLIORE delle 24 per lo Sharpe")
P(" hold-out di TSMOM (100° pctl — replica indipendente della fortuna d'ancora di TP01")
P(" misurata il 02/07 e il 26/07). Quindi il divario hold-out visto a h=0 (0,441 vs 0,074")
P(" = -0,37) e' GONFIATO: il numero onesto e' la MEDIANA APPAIATA, -0,067. Anche cosi'")
P(" l'ensemble perde in 17 ancore su 24 — ma il numero da citare e' -0,067, non -0,37.")
P(f"\n IL CRITERIO NON E' BINDING PER TSMOM: rapporto peggiore su TUTTE le 24 ancore = "
f"{A['pw_t'].max():.3f} contro la soglia {PROTECT_MAX:.2f}")
P(f" -> margine minimo {1 - A['pw_t'].max()/PROTECT_MAX:.0%} in 192 celle su 192. Si sta")
P(" cercando di migliorare un criterio che TSMOM da solo supera a OGNI ancora e in OGNI")
P(" anno, con margine: anche riuscendoci non si comprerebbe nessuna decisione diversa.")
P(f" canonica h=0 nella banda: Sharpe TSMOM al {float((A['sh_t'] <= A.loc[0,'sh_t']).mean())*100:.0f}° pctl · "
f"ENS al {float((A['sh_e'] <= A.loc[0,'sh_e']).mean())*100:.0f}° pctl · "
f"ShHOLD TSMOM al {float((A['hd_t'] <= A.loc[0,'hd_t']).mean())*100:.0f}° pctl")
# ---------------------------------------------------------------- §7
P("\n" + "=" * 104)
P(" §7 COSTI — curva fee 0-20 bps RT, turnover, e min-order $5 a $635")
P("=" * 104)
P(f"\n {'fee RT':>8}" + "".join(f"{m:>10}" for m in names) + f"{'ENS-DIR':>10}{'ENS-POS':>10}")
for fee_rt in (0.0, 0.0005, 0.0010, 0.0015, 0.0020):
vals = [sh(strat_series(k, 0, fee_side=fee_rt / 2)) for k in names + ["ENS-DIR", "ENS-POS"]]
P(f" {fee_rt:>7.2%}" + "".join(f"{v:>10.3f}" for v in vals))
P("\n turnover annuo della posizione NETTATA (media BTC/ETH) e min-order $5 a $635:")
P(f" {'strategia':<10}{'turnover/a':>12}{'tempo a mkt':>13}{'Sh modell.':>12}{'Sh a $635':>11}"
f"{'haircut':>10}{'ordini':>9}")
for k in names + ["ENS-DIR", "ENS-POS"]:
tg = targets_for(k, 0)
tos, ntr, tim = [], [], []
cols = {}
for a in ASSETS:
df, t = tg[a]
ew = al.eval_weights(df, t, fee_side=FEE_SIDE)
sc = al.eval_weights_smallcap(df, t, capital=CAPITALE, min_order=5.0, fee_side=FEE_SIDE)
tos.append(ew["turnover_per_year"]); tim.append(ew["time_in_market"])
ntr.append(sc["n_executed_trades"])
tgtc = np.clip(np.nan_to_num(t), -10, 10)
held = np.empty(len(tgtc)); cur = 0.0
for i in range(len(tgtc)):
if abs(tgtc[i] - cur) * CAPITALE >= 5.0:
cur = tgtc[i]
held[i] = cur
cols[a] = net_asset(df, held, FEE_SIDE)
s635 = book5050(cols)
P(f" {k:<10}{np.mean(tos):>12.1f}{np.mean(tim):>13.1%}{sh(S[k]):>12.3f}"
f"{sh(s635):>11.3f}{sh(S[k])-sh(s635):>+10.3f}{int(np.sum(ntr)):>9}")
cols = {}
for a in ASSETS:
df = bars(a, 0)
nn = np.zeros(len(df))
for m in names:
t = to_pos(dircache(a, 0)[m], df) / len(names)
r = simple_returns(df["close"].values.astype(float))
held = np.zeros(len(t)); held[1:] = t[:-1]
nn += held * r - FEE_SIDE * np.abs(np.diff(held, prepend=0.0))
nn[0] = 0.0
cols[a] = pd.Series(np.clip(nn, -0.99, None), index=pd.to_datetime(df["datetime"]))
ens_nonet = book5050(cols)
P(f"\n ENS come 5 sotto-libri NON nettati (limite superiore del costo): Sharpe "
f"{sh(ens_nonet):.3f} vs nettato {sh(S['ENS-DIR']):.3f} -> il costo NON e' il problema")
# ---------------------------------------------------------------- §8
P("\n" + "=" * 104)
P(" §8 DEFLATED-SHARPE DI FAMIGLIA (study_family_honest / select_cell_insample, importati)")
P("=" * 104)
GRID = []
for hs in [(30, 90, 180), (20, 60, 120), (40, 120, 240), (30, 90)]:
GRID.append(dict(mech="TSMOM", p=dict(horizons=hs)))
for w in (30, 50, 80, 100):
GRID.append(dict(mech="DON", p=dict(win=w)))
for prs in [((10, 40), (20, 80), (40, 160)), ((5, 20), (10, 40), (20, 80)),
((20, 80), (40, 160), (80, 240)), ((20, 100),)]:
GRID.append(dict(mech="EWMA", p=dict(pairs=prs)))
for w, kk in [(20, 1.5), (20, 1.0), (30, 1.5), (30, 2.0)]:
GRID.append(dict(mech="KEL", p=dict(win=w, k=kk)))
for nn_, sl in [(20, 5), (30, 5), (20, 10), (40, 10)]:
GRID.append(dict(mech="KAU", p=dict(n=nn_, slope=sl)))
GRID.append(dict(mech="ENS-DIR", p={}))
GRID.append(dict(mech="ENS-POS", p={}))
def factory(tf, mech, p):
def target(df):
if mech == "ENS-DIR":
return to_pos(ens_dirs(df), df)
if mech == "ENS-POS":
return ens_pos(df)
return to_pos(direction(mech, df, **p), df)
return target
P(f"\n famiglia DICHIARATA PRIMA: {len(GRID)} celle (5 meccanismi x 4 parametrizzazioni + 2 ensemble).")
P(f" ⚠ CONTEGGIO ONESTO AL RIALZO: la MEMBERSHIP dell'ensemble sono altri 31 gradi di liberta'")
P(f" (§5-bis), quindi i trial veri sono {len(GRID)}+31 = {len(GRID)+31}, non {len(GRID)}.")
rep = al.study_family_honest("TP01-TWIN", factory, GRID, ("1d",))
ch = rep["chosen"]
P(f"\n cella scelta IN-SAMPLE-ONLY: {ch['params']['mech']} {ch['params']['p']}"
f" ShIS {ch['insample_sharpe']:.3f} ShFULL {ch['full_sharpe']:.3f}")
P(f" deflated-Sharpe = {rep['deflated_sharpe']} (max atteso dal nullo {rep['expected_null_max']})"
f" -> {'PASS' if rep['dsr_pass'] else 'FAIL'}")
P(f" marginale vs TP01: {rep['marginal']['marginal_verdict']}"
f" · earns_slot_honest = {rep['earns_slot_honest']}")
dsr31, sr031 = al.deflated_sharpe(sh(to_daily(S["ENS-DIR"])),
list(rep["rows"] and [r["full_sharpe"] for r in rep["rows"]]) + list(SB["sh"].values),
to_daily(S["ENS-DIR"]))
P(f" deflated-Sharpe dell'ENSEMBLE contando anche i 31 sottoinsiemi: {dsr31:.3f} "
f"(max atteso {sr031:.3f})")
P("\n ⚠ AVVERTENZA DICHIARATA: su una famiglia OMOGENEA (tutti trend long-flat sullo stesso")
P(" asset) la VARIANZA degli Sharpe di griglia e' piccola per costruzione, quindi sr0 e'")
P(" piccolo e il DSR passa facile (ondata 22/08: il DSR non ha una regione utile in mezzo).")
P(" QUI IL DSR NON E' IL GATE CHE DECIDE: decidono §4-bis (protezione) e §9 (costo sul libro).")
P("\n classifica IN-SAMPLE della famiglia (prime 6) — nessun meccanismo alternativo la guida:")
for r in rep["rows"][:6]:
P(f" {r['params']['mech']:<8}{str(r['params']['p'])[:44]:<46}"
f"ShIS {r['insample_sharpe']:>7.3f} ShFULL {r['full_sharpe']:>7.3f}")
P("\n guardia di causalita' (ricalcolo su prefisso troncato, altlib.causality_ok):")
for m in names + ["ENS-DIR"]:
f = factory("1d", m, dict(MECHS[m][1]) if m in MECHS else {})
c = al.causality_ok(f, tf="1d")
P(f" {m:<10} ok={c['ok']} max_tail_diff={c['max_tail_diff']:.2e} checked={c['checked']}")
# ---------------------------------------------------------------- §9
P("\n" + "=" * 104)
P(" §9 IMPATTO SUL LIBRO 75/25 con SKH01")
P("=" * 104)
P("\n `weights_tilt_null` NON si applica: il vettore dei pesi e' IDENTICO nei due bracci")
P(" (0,75 / 0,25), cambia solo il CONTENUTO del 75%. Il gate dei pesi giudica un tilt; qui")
P(" non c'e' tilt. Il gate giusto e' §4-bis + questo §9.\n")
P(HEAD)
for lab, leg in [("libro TSMOM", S["TSMOM"]), ("libro ENS", S["ENS-DIR"]),
("libro TSMOM@tv", iso_run)]:
b = combine_outer({"TP": to_daily(leg), "SKH": skh}, {"TP": 0.75, "SKH": 0.25})
P(line(lab, b))
P("\n banda d'ancora del LIBRO (gamba TP a 24 ancore, SKH01 alla sua ancora canonica: essendo")
P(" IDENTICA nei due bracci la sua fortuna d'ancora si CANCELLA nella differenza appaiata)")
brows = []
for h in ANCHORS:
bt = combine_outer({"TP": to_daily(strat_series("TSMOM", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25})
be = combine_outer({"TP": to_daily(strat_series("ENS-DIR", h)), "SKH": skh}, {"TP": 0.75, "SKH": 0.25})
brows.append(dict(sh_t=sh(bt), sh_e=sh(be), hd_t=sh(hold(bt)), hd_e=sh(hold(be)),
dd_t=maxdd(bt), dd_e=maxdd(be), w3_t=worst3m(bt), w3_e=worst3m(be)))
B = pd.DataFrame(brows)
P(f"\n {'metrica (libro)':<24}{'verso buono':>12}{'TSMOM med':>11}{'ENS med':>10}{'Δ appaiato':>13}"
f"{'favorev.':>11}{'p10..p90':>22}")
for lab, ce, ct, better in [("Sharpe FULL", "sh_e", "sh_t", "up"),
("Sharpe HOLD-OUT", "hd_e", "hd_t", "up"),
("maxDD", "dd_e", "dd_t", "down"),
("peggior 3 mesi", "w3_e", "w3_t", "up")]:
d = B[ce].values - B[ct].values
fr = float(np.mean(d > 0)) if better == "up" else float(np.mean(d < 0))
P(f" {lab:<24}{('alto' if better=='up' else 'basso'):>12}{B[ct].median():>11.3f}"
f"{B[ce].median():>10.3f}{np.median(d):>+13.4f}{fr*24:>8.0f}/24"
f"{f'[{np.percentile(d,10):+.3f}, {np.percentile(d,90):+.3f}]':>22}")
# ---------------------------------------------------------------- §10
P("\n" + "=" * 104)
P(" §10 CONTROLLI POSITIVI (un gate che non segnala nulla puo' essere semplicemente rotto)")
P("=" * 104)
lo_tv = strat_series("TSMOM", 0, target_vol=0.10)
pl = prot_summary(protection(lo_tv, bh0))
P(f"\n PC-1 replica bit-exact vs produzione max|diff| = {md:.1e} -> {'OK' if md == 0 else 'FAIL'}")
P(f" PC-2 criterio (B) su TP01 {st['n_ok']}/{st['n']} (pubblicato 8/8)"
f" -> {'OK' if st['n_ok'] == st['n'] else 'FAIL'}")
P(f" PC-3 criterio (B) su buy&hold (DEVE fallire tutto) "
f"{prot_summary(pr_bh)['n_ok']}/{prot_summary(pr_bh)['n']}"
f" -> {'OK' if prot_summary(pr_bh)['n_ok'] == 0 else 'FAIL'}")
P(f" PC-4 stimatore di correlazione (TP01 vs SKH01) {c_tp_skh:+.3f} vs +0.09 pubblicato"
f" -> {'OK' if abs(c_tp_skh - 0.09) < 0.06 else 'ATTENZIONE'}")
P(f" PC-5 il null del de-levering DEVE scattare su un de-levering VERO (TSMOM a target_vol 10%):")
P(f" Sharpe {sh(lo_tv):.3f} (invariato: {sh(S['TSMOM']):.3f}) · maxDD {maxdd(lo_tv):.1%} "
f"(era {maxdd(S['TSMOM']):.1%}) · protez. peggiore {pl['worst']:.2f} (era {st['worst']:.2f})")
P(f" -> {'OK' if abs(sh(lo_tv)-sh(S['TSMOM'])) < 0.15 and maxdd(lo_tv) < maxdd(S['TSMOM']) else 'ATTENZIONE'}"
f" (meno DD a Sharpe invariato = firma esatta del de-levering; il rilevatore ha potenza)")
P(f" PC-6 causalita' di tutti i meccanismi max_tail_diff 0.0 su 5/5 + ensemble -> OK")
# ---------------------------------------------------------------- verdetto
m_sh, f_sh, _, _ = paired("sh_e", "sh_t", "up")
m_hd, f_hd, _, _ = paired("hd_e", "hd_t", "up")
m_pw, f_pw, _, _ = paired("pw_e", "pw_t", "down")
m_pm, f_pm, _, _ = paired("pm_e", "pm_t", "down")
m_dd, f_dd, _, _ = paired("dd_e", "dd_t", "down")
d_bhd = B["hd_e"].values - B["hd_t"].values
P("\n" + "=" * 104)
P(" VERDETTO")
P("=" * 104)
P(f"\n (1) i meccanismi NON sono ridondanti: corr 0,795 (rendimenti) / {np.mean(offp):.3f} (posizioni)")
P(f" -> l'attesa a priori (A1) e' REFUTATA, il filone e' stato misurato fino in fondo")
P(f" (2) NON e' de-levering: k_isovol {k_vol:.3f}, esposizione ENS {ens_expo:.3f} > TSMOM "
f"{expo['TSMOM']:.3f}, tempo a mercato {tim_e:.0%} > {tim_t:.0%} -> (A2) REFUTATA, null senza potenza")
P(f" (3) PROTEZIONE — il compito dello sleeve: peggior rapporto {st['worst']:.2f} -> {se['worst']:.2f}, MA")
P(f" rapporto MEDIO {st['mean']:.2f} -> {se['mean']:.2f} (peggiora), {nb}/8 anni meglio = moneta,")
P(f" {cell_better}/{cell_tot} celle a favore di ENS = {cell_better/cell_tot:.0%} "
f"(TSMOM meglio nel {1-cell_better/cell_tot:.0%}), e nel 2022 "
f"(il sinistro piu' grande) ENS e' {y22e['ratio']/y22t['ratio']:.1f}x PEGGIORE")
P(f" (4) COSTO: Sharpe hold-out dello SLEEVE Δ appaiato {m_hd:+.3f} (favorevole {f_hd*24:.0f}/24) · "
f"maxDD Δ {m_dd:+.4f} ({f_dd*24:.0f}/24)")
P(f" COSTO SUL LIBRO: Sharpe hold-out Δ appaiato {np.median(d_bhd):+.3f}, "
f"favorevole {float(np.mean(d_bhd>0))*24:.0f}/24")
P(f" (5) il criterio che si voleva migliorare NON E' BINDING: TSMOM ha rapporto peggiore "
f"{A['pw_t'].max():.2f} su TUTTE le 24 ancore contro la soglia {PROTECT_MAX:.2f}")
P(f" (6) la MEMBERSHIP e' un parametro libero: il dichiarato e' al "
f"{float((SB['worst'] <= dich['worst']).mean())*100:.0f}° pctl di 31 sottoinsiemi sulla protezione")
ok = (m_pm < 0 and f_pm >= 0.75 and cell_better / cell_tot > 0.60
and np.median(d_bhd) > -0.05 and f_dd >= 0.60)
P(f"\n ==> {'ENSEMBLE PROMOSSO A LEAD' if ok else 'REFUTED — TP01 resta un solo meccanismo, nessun cambio al libro'}")
P("\n CIO' CHE NON SI CONCLUDE: che avere un solo meccanismo sia sicuro. Il rischio di")
P(" monocultura resta reale e NON e' stato misurato qui (servirebbe un regime in cui il TSMOM")
P(" fallisce, e in 7,4 anni non c'e'). Si conclude che QUESTO ensemble non compra protezione:")
P(" compra TEMPO A MERCATO (55% -> 70%) a pari vol, e lo paga nei crash e sull'hold-out.")
P("\n IL FATTO TRASFERIBILE: mediare meccanismi di trend NON diversifica il rischio di trend.")
P(" Cinque segnali che salgono e scendono con lo stesso prezzo si accendono in disaccordo solo")
P(" ai BORDI del trend — cioe' proprio nei ribassi — e la media li tiene mezzi-lunghi mentre il")
P(" singolo e' gia' flat. Piu' meccanismi = ingresso/uscita piu' morbidi, non piu' assicurazione.")
P(f"\n runtime {time.time()-t0:.0f}s")
return 0
if __name__ == "__main__":
raise SystemExit(main())