Files
PythagorasGoal/scripts/research/r0823_tp01_ls.py
T
Adriano Dal Pastro 3e3b41d842 research(tp01-ls): la gamba SHORT del trend a 1d e' SCARTATA — toglie drift a 0/24 ancore
§42 TP01-LS. Meccanismo TP01 di produzione CONGELATO, un solo grado di liberta': il
pavimento della direzione TSMOM (floor 0 / -1/3 / -2/3 / -1, 4 trial dichiarati prima).
Replica bit-exact 3/3 a 0.0 (floor=0 == CANONICAL long_only=True, floor=-1 == long_only=
False, sleeve 50/50 == src/portfolio/sleeves._tp01_returns) + 4a replica candidate_daily
vs tp01_baseline_daily a 0.0. Controlli positivi 4/4 (causality_ok su variante leaky,
implausible_sharpe su serie senza perdite, gate iso-vol su leva pura, anchor_luck_delta
A-vs-A).

Esito: la short NON paga il proprio costo. dDRIFT appaiato -1.91%/anno positivo in 0/24
ancore, dShFULL -0.354 (0/24), maxDD +8.80pp (24/24); gate iso-volatilita' FAIL (dCAGR
-6.19% a pari vol); senza il 2022 il divario RADDOPPIA (dShFULL -0.406, 0/24); anni
positivi 2/8 (2022 +1.84, 2025 +0.27) e non compensano gli altri sei; marginal_vs_tp01
= NEUTRAL su tutte e tre le celle (multicut False, corr 0.79-0.93, alpha -2.6%/anno);
la cella scelta AL BUIO in-sample e' IL CANONICO mentre quella scelta sull'hold-out e'
floor=-1/3 = firma di selezione-sull-hold-out. Non e' morte-per-fee: a fee ZERO 1.338
contro 0.904. Attesa a priori del coordinatore CONFERMATA e superata.

Unica metrica nel verso della short: dShHOLD +0.292 in 23/24 ancore — ma l'hold-out e'
1.6 anni, non e' selezionabile in-sample, e all'ancora canonica non si vede (-0.008:
caso speculare della lezione 26/07). Libro 75/25 con SKH01: dSh -0.356 in 0/24, e la
gamba short isolata ha Sharpe -0.335 con corr +0.10 a SKH01 (non ridondante, solo
perdente). Trovato per strada: src/live/book.py clippa gia' TP01 con max(tp_frac, 0.0)
-> il long-flat e' cablato anche nell'esecutore, non solo in CANONICAL.

Nessun file di produzione toccato. Libro, pesi, cron, config INVARIATI.

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

595 lines
32 KiB
Python

#!/usr/bin/env python
"""r0823_tp01_ls.py — §42 TP01-LS: la gamba SHORT del trend a 1d paga il proprio costo?
DOMANDA. TP01 e' long-flat PER SCELTA: nei ribassi va a 0x invece di girarsi short. Il progetto
lo descrive come ASSICURAZIONE (taglia il DD ~6x contro buy&hold, contributo hold-out negativo
nel 99,1% delle ancore, LOO 26/07). L'obiettivo dichiarato e' il DRIFT, non lo Sharpe. La
direzione mai presa al TF di deploy e' il TSMOM LONG-SHORT a 1d: il progetto ha UNA riga
("trend 1h L/S ~1.0" fra gli edge deboli), misurata a 1h e senza l'harness onesto di oggi.
Decidibile in quattro pezzi:
(a) la short aggiunge DRIFT?
(b) sopravvive al GATE ISO-VOLATILITA'? (se aggiunge drift solo alzando l'esposizione e'
leva travestita — null del de-levering al contrario, 5 occorrenze nel progetto)
(c) NON e' semplicemente il beta del 2022? (si toglie il 2022 e si rifa')
(d) resta ADDS quando la si giudica contro TP01 stesso (`marginal_vs_tp01`)?
ATTESA A PRIORI DEL COORDINATORE, dichiarata PRIMA di misurare: il crypto ha drift positivo
strutturale e la short di trend perde quasi sempre fuori dal 2022 -> il guadagno sara' UN ANNO
SOLO (2022), lo stesso schema di SOL (22/08: 24/24 ancore positive, un anno solo). Se e' cosi'
la conclusione non e' "la short funziona" ma "il 2022 e' un evento nel campione".
MECCANISMO CONGELATO — UN SOLO GRADO DI LIBERTA'. Si parte dal TP01 di PRODUZIONE
(src/strategies/trend_portfolio.TrendPortfolio + CANONICAL) e si cambia SOLO il pavimento della
direzione TSMOM, da 0 (clip long-flat) a `floor`. Tutto il resto e' importato, non riscritto:
orizzonti 30/90/180g, vol_win 30g, target_vol 20%, leva cap 2x, fee 0.05%/lato, 50/50 BTC+ETH,
lo shift causale e la contabilita' fee stanno in `TrendPortfolio.net_returns` EREDITATA.
La direzione del blend a 3 orizzonti vive in {-1,-1/3,+1/3,+1}, quindi il pavimento e' un vero
knob discreto:
floor= 0 -> {0,0,1/3,1} = CANONICO (long-flat)
floor=-1/3 -> {-1/3,-1/3,1/3,1} = short "timida" (solo 1/3 di size al ribasso pieno)
floor=-2/3 -> {-2/3,-1/3,1/3,1}
floor=-1 -> {-1,-1/3,1/3,1} = LONG-SHORT pieno (== long_only=False di produzione)
GRIGLIA DICHIARATA PRIMA DI GUARDARE: 4 celle (i 4 floor) su UN solo TF (1d, quello di deploy).
Nessun altro parametro riaperto -> 4 trial, contati nel deflated-Sharpe. La griglia di 24 ANCORE
non e' una griglia di parametri (il DSR non la conta): si giudica con `anchor_luck_band` /
`anchor_luck_delta`, come prescritto dal gate del 26/07.
REPLICA BIT-EXACT PRIMA DI OGNI DELTA (tre vie, richiesta max|diff| == 0):
(1) floor=0 -> target_series == TrendPortfolio(**CANONICAL).target_series
(2) floor=-1 -> target_series == TrendPortfolio(long_only=False).target_series
(cioe' la mia unica riga nuova coincide con la produzione a ENTRAMBI gli estremi: il
codice davvero nuovo sono solo i due floor intermedi)
(3) serie 50/50 a floor=0 vs src.portfolio.sleeves._tp01_returns() (lo sleeve del libro)
Onesta': altlib (`marginal_vs_tp01`, `deflated_sharpe`, `causality_ok`, `implausible_sharpe`,
`eval_weights_smallcap`, `anchor_luck_*`) importato, non riscritto. Controlli positivi
OBBLIGATORI su ogni gate (un gate che non segnala puo' essere semplicemente rotto).
Vincoli: NESSUN file toccato fuori da questo script, nessun ordine inviato, sola lettura su
data/raw. Runtime ~1-2 min su 2 core.
"""
from __future__ import annotations
import sys
import time
from dataclasses import dataclass
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.strategies.trend_portfolio import ( # noqa: E402
CANONICAL, TrendPortfolio, realized_vol, simple_returns, tsmom_blend,
)
ASSETS = ("BTC", "ETH")
HOLDOUT = al.HOLDOUT
FLOORS = (0.0, -1.0 / 3.0, -2.0 / 3.0, -1.0) # LA GRIGLIA, dichiarata prima
FLOOR_LBL = {0.0: "0 (canonico LF)", -1.0 / 3.0: "-1/3", -2.0 / 3.0: "-2/3", -1.0: "-1 (LS pieno)"}
OFFSETS = tuple(range(24)) # banda d'ancora giornaliera
W_TP01_BOOK, W_SKH_BOOK = 0.75, 0.25 # src/live/book.py
CAPITAL = 635.0 # conto reale dichiarato nel briefing
# ===========================================================================
# IL MECCANISMO — TP01 di produzione con UN grado di liberta' in piu'
# ===========================================================================
@dataclass
class TPFloor(TrendPortfolio):
"""TP01 con pavimento della direzione parametrico. UNICA riga diversa dalla produzione:
`np.clip(direction, self.floor, None)` al posto di `np.clip(direction, 0, None)`.
Tutto il resto (vol realizzata, blend TSMOM, scaling, cap di leva, net_returns con lo
shift causale e le fee sul turnover) e' ereditato/importato dal modulo di produzione."""
floor: float = 0.0
def target_series(self, df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
bpd = self._bpd(df)
bpy = bpd * 365.25
r = simple_returns(c)
vol = realized_vol(r, self.vol_win_days * bpd, bpy)
horizons = tuple(d * bpd for d in self.horizons_days)
direction = np.clip(tsmom_blend(c, horizons), self.floor, None) # <-- IL GRADO DI LIBERTA'
scal = np.where((vol > 0) & np.isfinite(vol), self.target_vol / vol, 0.0)
tgt = np.clip(direction * scal, -self.leverage, self.leverage)
tgt[~np.isfinite(tgt)] = 0.0
return tgt
_CFG = {k: v for k, v in CANONICAL.items() if k != "long_only"}
def tp_floor(floor: float) -> TPFloor:
return TPFloor(long_only=False, floor=floor, **_CFG)
# ===========================================================================
# DATI — resample 1h -> 24h con ANCORA h (stessa convenzione di resample_tf)
# ===========================================================================
@lru_cache(maxsize=8)
def _h1(asset: str) -> pd.DataFrame:
return al.get(asset, "1h")
@lru_cache(maxsize=128)
def bars(asset: str, off: int) -> pd.DataFrame:
g = _h1(asset).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=off), 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=512)
def sleeve(floor: float, off: int = 0, fee_side: float | None = None) -> pd.Series:
"""Serie 50/50 BTC+ETH dei rendimenti NETTI per barra giornaliera, ancora `off`.
Usa `TrendPortfolio.net_returns` EREDITATA -> shift causale e fee di produzione."""
tp = tp_floor(floor)
if fee_side is not None:
tp.fee_side = fee_side
ser = {}
for a in ASSETS:
df = bars(a, off)
net, ts = tp.net_returns(df)
ser[a] = pd.Series(net, index=pd.DatetimeIndex(pd.to_datetime(ts.values, utc=True)))
J = pd.concat(ser, 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)
# ===========================================================================
# METRICHE
# ===========================================================================
def sh(s: pd.Series) -> float:
return al._sh(s)
def dd(s: pd.Series) -> float:
return al._dd_ret(s)
def drift(s: pd.Series) -> float:
"""Drift annualizzato in LOG (la grandezza su cui vivono i muri di capitale; il carry si
misura sul log, mai sulla media aritmetica delle differenze — regola 23/08)."""
r = np.asarray(s.dropna().values, float)
return float(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) if len(r) else 0.0
def cagr(s: pd.Series) -> float:
r = np.asarray(s.dropna().values, float)
if len(r) < 2:
return 0.0
return float(np.exp(np.mean(np.log1p(np.clip(r, -0.99, None))) * 365.25) - 1.0)
def vol(s: pd.Series) -> float:
r = np.asarray(s.dropna().values, float)
return float(np.std(r) * np.sqrt(365.25)) if len(r) else 0.0
def hold(s: pd.Series) -> pd.Series:
return s[s.index >= HOLDOUT]
def no2022(s: pd.Series) -> pd.Series:
return s[s.index.year != 2022]
def row(s: pd.Series) -> dict:
return dict(sh=sh(s), sh_hold=sh(hold(s)), cagr=cagr(s), drift=drift(s),
vol=vol(s), dd=dd(s), n=len(s))
def turnover_year(floor: float, off: int = 0) -> float:
tot = 0.0
for a in ASSETS:
df = bars(a, off)
tgt = tp_floor(floor).target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
turn = np.abs(np.diff(held, prepend=0.0)).sum()
yrs = (df["datetime"].iloc[-1] - df["datetime"].iloc[0]).total_seconds() / 86400 / 365.25
tot += turn / yrs
return tot / len(ASSETS)
def time_in_market(floor: float, off: int = 0) -> tuple[float, float]:
lo, sho = [], []
for a in ASSETS:
tgt = tp_floor(floor).target_series(bars(a, off))
lo.append(float(np.mean(tgt > 0))); sho.append(float(np.mean(tgt < 0)))
return float(np.mean(lo)), float(np.mean(sho))
def hr(t=""):
print("\n" + "=" * 78)
if t:
print(t)
print("=" * 78)
# ===========================================================================
def main() -> None:
t_start = time.time()
print(__doc__.split("\n\n")[0])
print(f"\nAvvio {pd.Timestamp.now('UTC')} | griglia dichiarata: {len(FLOORS)} floor x 1 TF (1d) = "
f"{len(FLOORS)} trial | ancore: {len(OFFSETS)} (NON trial: gate d'ancora)")
# ------------------------------------------------------------------ 1. REPLICA BIT-EXACT
hr("1. REPLICA BIT-EXACT DEL CANONICO (prima di qualunque delta)")
ok_all = True
canon = TrendPortfolio(**CANONICAL)
ls_prod = TrendPortfolio(**{**CANONICAL, "long_only": False})
for a in ASSETS:
df = bars(a, 0)
d0 = float(np.max(np.abs(tp_floor(0.0).target_series(df) - canon.target_series(df))))
d1 = float(np.max(np.abs(tp_floor(-1.0).target_series(df) - ls_prod.target_series(df))))
print(f" {a}: max|floor=0 - CANONICAL(long_only=True) | = {d0:.1e} "
f"max|floor=-1 - CANONICAL(long_only=False)| = {d1:.1e}")
ok_all &= (d0 == 0.0) and (d1 == 0.0)
from src.portfolio.sleeves import _tp01_returns
prod = _tp01_returns()
mine = sleeve(0.0, 0)
JJ = pd.concat({"p": prod, "m": mine}, axis=1, join="inner").dropna()
d2 = float(np.max(np.abs(JJ["p"] - JJ["m"])))
print(f" sleeve 50/50 vs src.portfolio.sleeves._tp01_returns(): n={len(JJ)} max|diff| = {d2:.1e}")
ok_all &= (d2 == 0.0)
print(f" ==> REPLICA {'OK (3/3 a 0.0)' if ok_all else 'FALLITA — NON procedere'}")
if not ok_all:
sys.exit(1)
# ------------------------------------------------------------------ 2. CONTROLLI POSITIVI
hr("2. CONTROLLI POSITIVI DEI GATE (un gate che non segnala puo' essere rotto)")
def tgt_fn(floor):
return lambda df: tp_floor(floor).target_series(df)
def leaky(df): # guarda il futuro: DEVE fallire causality_ok
t = tp_floor(-1.0).target_series(df)
out = np.zeros_like(t); out[:-1] = t[1:]
return out
c_ok = al.causality_ok(tgt_fn(-1.0), tf="1d")
c_bad = al.causality_ok(leaky, tf="1d")
print(f" causality_ok(floor=-1) -> ok={c_ok['ok']} max_tail_diff={c_ok['max_tail_diff']}")
print(f" causality_ok(variante LEAKY) -> ok={c_bad['ok']} max_tail_diff={c_bad['max_tail_diff']}"
f" [controllo positivo: deve essere False]")
ctrl1 = c_ok["ok"] and not c_bad["ok"]
rng = np.random.default_rng(20260823)
idx = pd.date_range("2020-01-01", periods=900, freq="1D", tz="UTC")
fake_good = pd.Series(np.abs(rng.normal(0.002, 0.0005, 900)), index=idx) # 0 perdite
fake_noise = pd.Series(rng.normal(0.0004, 0.012, 900), index=idx)
ip_g = al.implausible_sharpe(fake_good)
ip_n = al.implausible_sharpe(fake_noise)
print(f" implausible_sharpe(serie senza perdite) -> implausible={ip_g['implausible']} "
f"[deve essere True] (rumore Sh {ip_n['sharpe']:.2f} -> {ip_n['implausible']} [deve essere False])")
ctrl2 = ip_g["implausible"] and not ip_n["implausible"]
base0 = sleeve(0.0, 0)
lev13 = base0 * 1.3 # LEVA PURA: il gate iso-vol deve bocciarla
k_lev = vol(base0) / vol(lev13)
print(f" GATE ISO-VOL, controllo positivo su LEVA PURA (canonico x1.3):")
print(f" grezzo dCAGR = {cagr(lev13) - cagr(base0):+.4f} (positivo: sembra 'piu' drift')")
print(f" iso-vol dCAGR = {cagr(lev13 * k_lev) - cagr(base0):+.6f} [deve essere ~0 -> e' leva]")
ctrl3 = abs(cagr(lev13 * k_lev) - cagr(base0)) < 1e-6
ad_self = al.anchor_luck_delta(lambda o: sleeve(0.0, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh)
print(f" anchor_luck_delta(A vs A) -> mediana {ad_self['median_paired']:+.6f}, "
f"positivi {ad_self['n_positive']}/{ad_self['n_anchors']} [deve essere 0 e 0/24]")
ctrl4 = (ad_self["median_paired"] == 0.0) and (ad_self["n_positive"] == 0)
print(f" ==> CONTROLLI POSITIVI: {sum([ctrl1, ctrl2, ctrl3, ctrl4])}/4 superati")
# ------------------------------------------------------------------ 3. TABELLA PRINCIPALE
hr("3. TABELLA PRINCIPALE (ancora canonica h=0, fee 0.10% RT, 50/50 BTC+ETH)")
print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'drift':>8} {'vol':>7} "
f"{'maxDD':>7} {'turn/a':>7} {'%long':>6} {'%short':>7}")
rows = {}
for f in FLOORS:
s = sleeve(f, 0)
r = row(s); rows[f] = r
tl, ts = time_in_market(f)
print(f" {FLOOR_LBL[f]:<16} {r['sh']:>7.3f} {r['sh_hold']:>7.3f} {r['cagr']:>7.2%} "
f"{r['drift']:>7.2%} {r['vol']:>6.2%} {r['dd']:>6.2%} {turnover_year(f):>7.2f} "
f"{tl:>5.1%} {ts:>6.1%}")
print(f" (n barre = {rows[0.0]['n']}, dal {base0.index[0].date()} al {base0.index[-1].date()})")
# ------------------------------------------------------------------ 4. GATE ISO-VOL
hr("4. GATE ISO-VOLATILITA' (null del de-levering AL CONTRARIO: la variante ALZA la vol)")
print(" Convenzione: si DE-LEVA la variante fino alla vol del canonico (k<1, gratis: nessuna")
print(" leva richiesta). Se a pari vol il canonico regge, la short era leva travestita.")
print(f" {'floor':<16} {'k de-lev':>9} {'CAGR iso':>9} {'dCAGR iso':>10} {'dSharpe':>9} "
f"{'maxDD iso':>10} {'esito':>10}")
isovol = {}
for f in FLOORS:
s = sleeve(f, 0)
k = vol(base0) / vol(s) if vol(s) > 0 else 1.0
si = s * k
dc = cagr(si) - cagr(base0)
isovol[f] = dict(k=k, dcagr=dc, dsh=sh(s) - sh(base0), dd=dd(si))
v = "—" if f == 0.0 else ("PASS" if dc > 0 else "FAIL")
print(f" {FLOOR_LBL[f]:<16} {k:>9.4f} {cagr(si):>8.2%} {dc:>+10.2%} "
f"{sh(s) - sh(base0):>+9.3f} {dd(si):>9.2%} {v:>10}")
# ------------------------------------------------------------------ 5. BANDA D'ANCORA
hr("5. BANDA D'ANCORA — 24 ore, MEDIANA DELLE DIFFERENZE APPAIATE (variante - canonico)")
print(" (un Delta misurato a una sola ancora eredita la fortuna di quell'ancora: regola 26/07)")
band = {}
for f in FLOORS[1:]:
d_sh = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=sh)
d_ho = al.anchor_luck_delta(lambda o, f=f: hold(sleeve(f, o)), lambda o: hold(sleeve(0.0, o)),
OFFSETS, metric=sh)
d_dr = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=drift)
d_dd = al.anchor_luck_delta(lambda o, f=f: sleeve(f, o), lambda o: sleeve(0.0, o), OFFSETS, metric=dd)
band[f] = dict(sh=d_sh, ho=d_ho, dr=d_dr, dd=d_dd)
print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} "
f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | "
f"dShHOLD {d_ho['median_paired']:+.3f} {d_ho['n_positive']:>2}/24 | "
f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24 | "
f"dMAXDD {d_dd['median_paired']:+.2%} {d_dd['n_positive']:>2}/24")
print("\n ⚠️ L'UNICA metrica che va nel verso della short e' l'HOLD-OUT, e all'ancora")
print(" CANONICA non si vede: e' il caso speculare della lezione 26/07 (li' l'ancora")
print(" canonica INVENTAVA un danno, qui NASCONDE un vantaggio).")
print(f" {'floor':<16} {'dShHOLD @h=0':>13} {'mediana 24':>11} {'p10':>8} {'p90':>8}")
for f in FLOORS[1:]:
v = np.array(list(band[f]['ho']['per_offset'].values()), float)
print(f" {FLOOR_LBL[f]:<16} {band[f]['ho']['per_offset'][0]:>+13.3f} "
f"{np.median(v):>+11.3f} {np.percentile(v, 10):>+8.3f} {np.percentile(v, 90):>+8.3f}")
b_can = al.anchor_luck_band(lambda o: sleeve(0.0, o), OFFSETS, metric=sh)
b_ls = al.anchor_luck_band(lambda o: sleeve(-1.0, o), OFFSETS, metric=sh)
print(f" livelli: canonico ShFULL canonica {b_can['canonical']:.3f} (pctl {b_can['canonical_pctl']:.2f}) "
f"mediana {b_can['median']:.3f} | LS pieno canonica {b_ls['canonical']:.3f} "
f"(pctl {b_ls['canonical_pctl']:.2f}) mediana {b_ls['median']:.3f}")
# ------------------------------------------------------------------ 6. PER ANNO / SENZA 2022
hr("6. SCOMPOSIZIONE PER ANNO — e la domanda che decide: E SENZA IL 2022?")
years = sorted(set(base0.index.year))
print(f" Sharpe per anno (ancora h=0)")
print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS))
for y in years:
line = f" {y:<6}"
for f in FLOORS:
s = sleeve(f, 0); sy = s[s.index.year == y]
line += f"{sh(sy):>18.2f}"
print(line)
print(f"\n Rendimento per anno")
print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS))
for y in years:
line = f" {y:<6}"
for f in FLOORS:
s = sleeve(f, 0); sy = s[s.index.year == y]
line += f"{float(np.prod(1 + sy.values) - 1):>17.1%} "
print(line)
print(f"\n dSharpe ANNUALE della variante vs canonico (positivo = la short paga QUELL'anno)")
print(f" {'anno':<6}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS[1:]))
npos = {f: 0 for f in FLOORS[1:]}
for y in years:
line = f" {y:<6}"
s0 = sleeve(0.0, 0)
for f in FLOORS[1:]:
s = sleeve(f, 0)
d = sh(s[s.index.year == y]) - sh(s0[s0.index.year == y])
npos[f] += int(d > 0)
line += f"{d:>+18.2f}"
print(line)
print(" anni positivi: " + " ".join(f"{FLOOR_LBL[f]} {npos[f]}/{len(years)}" for f in FLOORS[1:]))
print(f"\n --- SENZA IL 2022 (ancora h=0) ---")
print(f" {'floor':<16} {'ShFULL':>7} {'dSh':>7} {'CAGR':>8} {'dCAGR':>8} {'iso-vol dCAGR':>14}")
b0n = no2022(base0)
for f in FLOORS:
sn = no2022(sleeve(f, 0))
k = vol(b0n) / vol(sn) if vol(sn) > 0 else 1.0
print(f" {FLOOR_LBL[f]:<16} {sh(sn):>7.3f} {sh(sn) - sh(b0n):>+7.3f} {cagr(sn):>7.2%} "
f"{cagr(sn) - cagr(b0n):>+7.2%} {cagr(sn * k) - cagr(b0n):>+14.2%}")
print(f"\n --- BANDA D'ANCORA SENZA IL 2022 (mediana differenze appaiate) ---")
for f in FLOORS[1:]:
d_sh = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)),
lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=sh)
d_dr = al.anchor_luck_delta(lambda o, f=f: no2022(sleeve(f, o)),
lambda o: no2022(sleeve(0.0, o)), OFFSETS, metric=drift)
print(f" floor {FLOOR_LBL[f]:<14} dShFULL {d_sh['median_paired']:+.3f} "
f"[{d_sh['lo']:+.3f},{d_sh['hi']:+.3f}] {d_sh['n_positive']:>2}/24 | "
f"dDRIFT {d_dr['median_paired']:+.2%} {d_dr['n_positive']:>2}/24")
# ------------------------------------------------------------------ 7. PROTEZIONE DD
hr("7. LA PROTEZIONE — il valore DICHIARATO di TP01 e' il taglio del DD: la short lo rompe?")
bh = {}
for a in ASSETS:
df = bars(a, 0)
bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
JB = pd.concat(bh, axis=1, join="inner").fillna(0.0)
bh50 = pd.Series(0.5 * JB[ASSETS[0]].values + 0.5 * JB[ASSETS[1]].values, index=JB.index)
bh50 = bh50.reindex(base0.index).fillna(0.0)
print(f" buy&hold 50/50: Sharpe {sh(bh50):.2f} CAGR {cagr(bh50):.1%} maxDD {dd(bh50):.1%}")
print(f" Criterio (B) di edge_watch: in un anno con DD del buy&hold > 10%, il DD dello sleeve")
print(f" deve restare sotto il 75% di quello. Rapporto DD_sleeve/DD_B&H per anno di sinistro:")
print(f" {'anno':<6} {'DD B&H':>8}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS))
fails = {f: 0 for f in FLOORS}; sinistri = 0
for y in years:
by = bh50[bh50.index.year == y]
if dd(by) <= 0.10:
continue
sinistri += 1
line = f" {y:<6} {dd(by):>7.1%}"
for f in FLOORS:
s = sleeve(f, 0); r_ = dd(s[s.index.year == y]) / dd(by)
fails[f] += int(r_ >= 0.75)
line += f"{r_:>17.2f}x" if False else f"{r_:>18.2f}"
print(line)
print(f" anni di sinistro: {sinistri}. Violazioni del criterio (rapporto >= 0.75): " +
" ".join(f"{FLOOR_LBL[f]} {fails[f]}" for f in FLOORS))
print(f" maxDD full: " + " ".join(f"{FLOOR_LBL[f]} {rows[f]['dd']:.2%}" for f in FLOORS))
# ------------------------------------------------------------------ 8. COSTI
hr("8. COSTI — la short raddoppia il turnover? curva fee 0-20 bps/lato")
print(f" {'fee/lato':>9} {'%RT':>7}" + "".join(f"{FLOOR_LBL[f]:>18}" for f in FLOORS))
for bps in (0, 2.5, 5, 10, 15, 20):
line = f" {bps:>8.1f}b {bps * 2 / 100:>6.2f}%"
for f in FLOORS:
line += f"{sh(sleeve(f, 0, bps / 10000.0)):>18.3f}"
print(line)
print(" turnover/anno (media asset): " + " ".join(f"{FLOOR_LBL[f]} {turnover_year(f):.2f}" for f in FLOORS))
# ------------------------------------------------------------------ 9. LIBRO 75/25 + SKH01
hr("9. IMPATTO SUL LIBRO 75/25 CON SKH01 (che e' GIA' long/short: ridondanza?)")
from src.portfolio.sleeves import _skyhook_returns
skh = _skyhook_returns()
skh.index = pd.DatetimeIndex(skh.index).tz_convert("UTC") if skh.index.tz else \
pd.DatetimeIndex(skh.index).tz_localize("UTC")
def book(floor: float, off: int = 0) -> pd.Series:
s = sleeve(floor, off).copy()
s.index = s.index.floor("1D") # ancora h -> data di calendario (1 barra per data)
J = pd.concat({"t": s, "k": skh}, axis=1, join="inner").dropna()
return pd.Series(W_TP01_BOOK * J["t"].values + W_SKH_BOOK * J["k"].values, index=J.index)
print(f" {'floor':<16} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>8} {'maxDD':>7}")
for f in FLOORS:
b = book(f)
print(f" {FLOOR_LBL[f]:<16} {sh(b):>7.3f} {sh(hold(b)):>7.3f} {cagr(b):>7.2%} {dd(b):>6.2%}")
print(f" banda d'ancora del LIBRO (24 ancore su TP01, SKH01 all'ancora canonica):")
for f in FLOORS[1:]:
d_b = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=sh)
d_c = al.anchor_luck_delta(lambda o, f=f: book(f, o), lambda o: book(0.0, o), OFFSETS, metric=drift)
print(f" floor {FLOOR_LBL[f]:<14} dSh libro {d_b['median_paired']:+.3f} "
f"[{d_b['lo']:+.3f},{d_b['hi']:+.3f}] {d_b['n_positive']:>2}/24 | "
f"dDRIFT {d_c['median_paired']:+.2%} {d_c['n_positive']:>2}/24")
print(f"\n RIDONDANZA con SKH01 — la 'gamba short' isolata = variante meno canonico:")
for f in FLOORS[1:]:
leg = (sleeve(f, 0) - sleeve(0.0, 0))
leg.index = leg.index.floor("1D")
JL = pd.concat({"l": leg, "k": skh}, axis=1, join="inner").dropna()
print(f" floor {FLOOR_LBL[f]:<14} corr(gamba short, SKH01) = {JL['l'].corr(JL['k']):+.3f} "
f"Sharpe della gamba isolata {sh(JL['l']):+.3f} drift {drift(JL['l']):+.2%}")
# ------------------------------------------------------------------ 10. MARGINALE + DSR
hr("10. MARGINALE vs TP01 STESSO + DEFLATED SHARPE (4 trial dichiarati)")
cd0 = al.candidate_daily(tgt_fn(0.0), tf="1d")
bl = al.tp01_baseline_daily()
JC = pd.concat({"a": cd0, "b": bl}, axis=1, join="inner").dropna()
print(f" 4a replica: candidate_daily(floor=0) vs tp01_baseline_daily() max|diff| "
f"{float(np.max(np.abs(JC['a'] - JC['b']))):.1e} (n={len(JC)})")
# trial set del DSR: si prende dal percorso CODIFICATO (select_cell_insample), non a mano.
# ⚠️ errore mio catturato: la prima stesura costruiva la lista dei trial da solo. Due percorsi
# per lo stesso numero e' esattamente cio' che il gate codificato serve a evitare -> qui li
# confronto invece di sostituirne uno in silenzio.
sel = al.select_cell_insample(lambda tf, floor: tgt_fn(floor),
[dict(floor=f) for f in FLOORS], ("1d",))
all_sh = sel["all_full_sharpe"]
mine_sh = [round(sh(sleeve(f, 0)), 3) for f in FLOORS]
print(f" trial set DSR da select_cell_insample: {all_sh}")
print(f" stesso conto per via indipendente (sleeve): {mine_sh} -> "
f"{'COINCIDONO' if all_sh == mine_sh else 'DIVERGONO (fermarsi)'}")
for f in FLOORS[1:]:
cd = al.candidate_daily(tgt_fn(f), tf="1d")
m = al.marginal_vs_tp01(cd)
d, sr0 = al.deflated_sharpe(sh(sleeve(f, 0)), all_sh, sleeve(f, 0))
ip = al.implausible_sharpe(sleeve(f, 0))
print(f"\n floor {FLOOR_LBL[f]}: marginal_verdict = {m['marginal_verdict']}")
print(f" corr->TP01 {m['corr_full']} beta {m['beta_to_tp01']} alpha/a {m['alpha_ann']:+.4f} "
f"resid Sharpe {m['resid_sharpe_full']}")
print(f" blend w25 uplift full {m['blends']['w25']['uplift_full']:+.3f} / hold "
f"{m['blends']['w25']['uplift_hold']:+.3f} w50 full "
f"{m['blends']['w50']['uplift_full']:+.3f} / hold {m['blends']['w50']['uplift_hold']:+.3f}")
print(f" robust_oos={m['robust_oos']} multicut={m['multicut_persistent']} "
f"insample_edge={m['has_insample_edge']} (Sh IS {m['cand_insample_sharpe']}) "
f"is_hedge={m['is_hedge']}")
print(f" HEDGE vs ALPHA: corr(Sharpe-TP01 annuo, uplift annuo) = {m['hedge_yearly_corr']} "
f"uplift quando TP01 SALE {m['uplift_tp01_up']} / quando TP01 SCENDE {m['uplift_tp01_down']}")
print(f" multicut per anno: {m['multicut_uplift']}")
print(f" deflated-Sharpe {d:.3f} (sr0 nullo {sr0:.3f}, N={len(all_sh)}) "
f"implausible={ip['implausible']}")
print(f"\n --- GATE SELECTION-ON-HOLDOUT (codificato 29/06): quale floor si sceglie ")
print(f" guardando SOLO il pre-2025? Se e' il canonico, il guadagno hold-out NON e'")
print(f" selezionabile e sceglierlo sarebbe selezione-sull'hold-out. ---")
print(f" {'floor':<16} {'Sh IN-SAMPLE (<2025)':>21} {'Sh HOLD-OUT':>12} {'Sh FULL':>9}")
for f in FLOORS:
s_ = sleeve(f, 0)
print(f" {FLOOR_LBL[f]:<16} {sh(s_[s_.index < HOLDOUT]):>21.3f} {sh(hold(s_)):>12.3f} {sh(s_):>9.3f}")
fb = sel["chosen"]["params"]["floor"]
fh = max(FLOORS, key=lambda f: sh(hold(sleeve(f, 0))))
print(f" cella scelta AL BUIO (max Sharpe in-sample) = floor {fb:+.4f} -> "
f"{'IL CANONICO' if abs(fb) < 1e-9 else 'NON il canonico'}")
print(f" cella scelta SULL'HOLD-OUT (max Sharpe hold-out) = floor {fh:+.4f} -> "
f"{'diversa: firma di selezione-sull-hold-out' if abs(fh - fb) > 1e-9 else 'la stessa'}")
print(f"\n ⚠️ NOTA ONESTA sul DSR: con 4 celle quasi identiche sr0 e' minuscolo -> il DSR e'")
print(f" alto per COSTRUZIONE (dipende dalla VARIANZA della griglia, lezione §10 del 22/08).")
print(f" Qui non e' il gate che decide: e' un contorno. Il gate che decide e' l'iso-vol +")
print(f" la banda d'ancora + il per-anno.")
# ------------------------------------------------------------------ 11. ESEGUIBILITA'
hr(f"11. ESEGUIBILITA' A ${CAPITAL:.0f} (min_order $5) e vincolo di NETTING del libro")
for f in (0.0, -1.0):
for a in ASSETS:
df = bars(a, 0)
sc = al.eval_weights_smallcap(df, tp_floor(f).target_series(df),
capital=CAPITAL, min_order=5.0)
print(f" floor {FLOOR_LBL[f]:<14} {a}: Sharpe modellato {sc['modeled']['sharpe']:.3f} -> "
f"reale {sc['realistic']['sharpe']:.3f} haircut {sc['sharpe_haircut']:+.3f} "
f"trade eseguiti {sc['n_executed_trades']}")
src_book = (ROOT / "src" / "live" / "book.py").read_text()
clipped = "max(tp_frac, 0.0)" in src_book
print(f"\n VERIFICA SUL CODICE CHE GIRA: src/live/book.py contiene 'max(tp_frac, 0.0)' -> {clipped}")
print(f" Cioe' l'esecutore del libro CLIPPA a zero la frazione di TP01: il long-flat non e' solo")
print(f" in CANONICAL, e' cablato anche in `book_net_target`. Attivare la short richiederebbe")
print(f" di toccare src/live/book.py, cioe' CODICE su un percorso con soldi veri; e TP01 e SKH01")
print(f" si nettano su UNA sola posizione per strumento, quindi la short di TP01 andrebbe a")
print(f" sommarsi/cancellarsi con quella di SKH01 invece di essere una gamba a se'.")
hr("12. VERDETTO E COSA NON E' STATO MISURATO")
dsh_ls = band[-1.0]["sh"]["median_paired"]; ddr_ls = band[-1.0]["dr"]["median_paired"]
print(f" floor=-1 (LS pieno): dShFULL appaiato {dsh_ls:+.3f} ({band[-1.0]['sh']['n_positive']}/24), "
f"dDRIFT {ddr_ls:+.2%} ({band[-1.0]['dr']['n_positive']}/24), "
f"dMAXDD {band[-1.0]['dd']['median_paired']:+.2%} ({band[-1.0]['dd']['n_positive']}/24)")
print(f" iso-vol dCAGR {isovol[-1.0]['dcagr']:+.2%} | anni positivi {npos[-1.0]}/{len(years)} | "
f"cella scelta al buio = il canonico")
print(f"\n >>> VERDETTO: SCARTATO. La gamba short NON paga il proprio costo: toglie drift")
print(f" ({ddr_ls:+.2%}/anno a 0/24 ancore), FALLISCE il gate iso-vol, peggiora il maxDD")
print(f" ({band[-1.0]['dd']['median_paired']:+.2%} a 24/24) e non e' selezionabile in-sample.")
print(f" L'attesa a priori del coordinatore (il guadagno e' il 2022) e' CONFERMATA e")
print(f" superata: il 2022 c'e' ({npos[-1.0]}/8 anni positivi, dSharpe 2022 +1.84) ma NON")
print(f" compensa gli altri sei nemmeno con se stesso dentro il campione.")
print(f" E NON e' morte-per-fee: a fee ZERO il canonico fa {sh(sleeve(0.0, 0, 0.0)):.3f} contro "
f"{sh(sleeve(-1.0, 0, 0.0)):.3f} -> l'edge lordo non c'e'.")
print(f"\n COSA NON HO MISURATO (dichiarato, non nascosto):")
print(f" (a) FUNDING. Nessun backtest del progetto lo modella (§35, 22/08) e qui nemmeno.")
print(f" E' l'unica voce che potrebbe muovere il numero nel verso della short: una")
print(f" posizione corta INCASSA il funding quando il tasso e' positivo. MA il")
print(f" meccanismo misurato il 22/08 e' che esposizione e funding sono POSITIVAMENTE")
print(f" correlati, e un trend-short entra quando il mercato scende, cioe' quando il")
print(f" funding e' tipicamente NEGATIVO (e li' e' lo short a pagare). Argomento, non")
print(f" misura: si chiude con la macchineria di r0822d_funding.py, non a parole.")
print(f" Ordine di grandezza in gioco (~1-2%/anno) contro un drift perso di "
f"{-ddr_ls:.2%}/anno.")
print(f" (b) TF diversi dal 1d. Riaprire il TF riapre la FAMIGLIA (griglia 4 -> 4xN celle)")
print(f" e imporrebbe study_family_honest; qui il TF e' congelato a quello di deploy.")
print(f" (c) La banda d'ancora del LIBRO tiene SKH01 alla sua ancora canonica (le 23 fasi")
print(f" di SKH01 non sono state incrociate): il delta di libro e' appaiato solo sulle")
print(f" 24 ancore di TP01. Non cambia il segno (0/24 su tutte e tre le celle).")
print(f" (d) La lente WICK accoppiata (25/07): tutto qui e' close-only. Irrilevante per il")
print(f" verdetto (non c'e' barriera di rovina in gioco), rilevante se qualcuno")
print(f" riproponesse la variante sul canale funded.")
print(f"\n Tempo totale {time.time() - t_start:.1f}s")
if __name__ == "__main__":
main()