research(wave-0702): ondata timing + CRT — 8 filoni, 0 nuovi sleeve, finding anchor timing-luck TP01

Goal: "altre strategie su Deribit con timing differenti". 8 filoni multi-agente + scettico:
- event-clock bars, expiry calendar Deribit, clock lenti/bande, regime-speed: SCARTATI
- CRT (Candle Range Theory) base/multi-TF/contesto: SCARTATA 3/3 (DSR~0, ritest =
  informazione negativa; sottoprodotto: FOLLOW>FADE sui livelli prior-day ogni anno,
  conferma il lead prevday)
- FINDING (confermato da scettico indipendente): hold-out 0.31 di TP01 = migliore delle
  24 ancore orarie (mediana 0.04, banda [-0.13,+0.30]) -> narrativa corretta in CLAUDE.md
  e docstring: l'hold-out non risolve l'edge di ritorno, regge il taglio DD a ogni ancora.
  Tranching K=2/4 = solo varianza della stima, no deploy a $600. Audit d'ancora pendente
  su XS01/SKH01. Book live e portafoglio INVARIATI. Test 168/168.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-02 22:12:22 +00:00
parent e6657fcb16
commit 73d74c5e53
12 changed files with 4205 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env python
"""r0702_tp01_offset.py — TIMING-LUCK del ribilanciamento giornaliero di TP01.
TP01 CANONICAL (PORT LF1d) decide sulla barra daily chiusa alle 00:00 UTC. L'ancora e'
arbitraria (Hoffstein, "rebalance timing luck"): la STESSA strategia con parametri IDENTICI
ancorata alle h:00 (h=0..23) puo' dare Sharpe diversi. Questo script:
1. costruisce 24 serie daily (resample 24h del 1h certificato, offset h, label/closed left,
stessa convenzione di trend_portfolio.resample_tf) — SANITY OBBLIGATORIO: h=0 riproduce
ESATTAMENTE al.tp01_baseline_daily() (stesso Sharpe FULL/HOLD);
2. misura Sharpe/CAGR/maxDD FULL, IS (pre-2025) e HOLD-OUT per offset -> percentile di h=0;
3. ENSEMBLE (tranching 1/K del capitale per ancora): 24-way + K=2 (0,12), K=3 (0,8,16),
K=4 (0,6,12,18) — scelte A PRIORI simmetriche, zero tuning per-offset, zero selezione
sull'hold-out. L'ensemble e' valutato sul BOOK AGGREGATO su griglia 1h (posizione =
media delle tranche, fee sul turnover netto reale) — non media di equity separate;
4. dispersione: std/range dello Sharpe fra le 24 ancore vs fra TUTTE le rotazioni possibili
di K=2 (12), K=3 (8), K=4 (6) — la riduzione di varianza e' il criterio strutturale;
5. small-cap: haircut min-order $5 a capitale 600/2k/10k per K=1 vs K=2/4/24
(il tranching divide gli ordini per K -> piu' rebalance sotto min-order).
Causalita': targets TP01 causali per costruzione; guardia ricalcolo-su-prefisso sia sul
daily resampled sia sul troncamento del 1h; mappatura daily->1h via merge_asof backward su
EPOCA MS ESPLICITA (mai .view su tz-aware); eval_weights fa lo shift (held durante t+1).
Vincoli: nessun file toccato fuori da questo script. Runtime ~1-2 min.
"""
from __future__ import annotations
import sys
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 CANONICAL, TrendPortfolio # noqa: E402
TP = TrendPortfolio(**CANONICAL)
HOLDOUT = al.HOLDOUT
ASSETS = ("BTC", "ETH")
MS_H = 3_600_000
MS_D = 86_400_000
# ancore a priori, simmetriche, NON ottimizzate
HEADLINE = {
"K=1 (canonico h=0)": (0,),
"K=2 (0,12)": (0, 12),
"K=3 (0,8,16)": (0, 8, 16),
"K=4 (0,6,12,18)": (0, 6, 12, 18),
"K=24 (tutte)": tuple(range(24)),
}
# ---------------------------------------------------------------------------
# Resample con ancora h — stessa convenzione di trend_portfolio.resample_tf
# ---------------------------------------------------------------------------
def resample_offset(df_1h: pd.DataFrame, h: int) -> pd.DataFrame:
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=8)
def get1h(asset: str) -> pd.DataFrame:
return al.get(asset, "1h")
@lru_cache(maxsize=64)
def daily_off(asset: str, h: int) -> pd.DataFrame:
return resample_offset(get1h(asset), h)
# ---------------------------------------------------------------------------
# Metriche su serie daily (convenzione identica al baseline: _to_daily + _sh)
# ---------------------------------------------------------------------------
def dmetrics(s: pd.Series) -> dict:
s = s.dropna()
is_ = s[s.index < HOLDOUT]
ho = s[s.index >= HOLDOUT]
eq = float(np.prod(1.0 + s.values))
yrs = len(s) / 365.25
cagr = eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0
return dict(sh_full=al._sh(s), sh_is=al._sh(is_), sh_hold=al._sh(ho),
cagr=cagr, dd=al._dd_ret(s), dd_hold=al._dd_ret(ho), n=len(s))
# ---------------------------------------------------------------------------
# Path DAILY-NATIVO per offset (counterfactual "e se l'ancora fosse h")
# ---------------------------------------------------------------------------
def port_daily_native(h: int) -> pd.Series:
series = {}
for a in ASSETS:
df = daily_off(a, h)
net, ts = TP.net_returns(df)
series[a] = pd.Series(net, index=pd.DatetimeIndex(pd.to_datetime(ts.values, utc=True)))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
# ---------------------------------------------------------------------------
# Path 1h AGGREGATO (book unico): target daily-offset mappato causale sul 1h
# ---------------------------------------------------------------------------
@lru_cache(maxsize=64)
def hourly_target(asset: str, h: int) -> tuple:
"""Target TP01 (ancora h) sul grid 1h: per ogni barra 1h prendi il target dell'ultima
barra daily-offset il cui CLOSE nominale (label+24h, epoca ms) e' <= close della barra 1h
(ts+1h). merge_asof backward su int ms espliciti. eval_weights poi SHIFTA (held t+1)."""
d = daily_off(asset, h)
tgt = TP.target_series(d)
right = pd.DataFrame({"cms": d["timestamp"].values.astype("int64") + MS_D,
"tgt": tgt})
left = pd.DataFrame({"cms": get1h(asset)["timestamp"].values.astype("int64") + MS_H})
m = pd.merge_asof(left, right, on="cms", direction="backward")
return tuple(np.nan_to_num(m["tgt"].values, nan=0.0))
def ens_target(asset: str, hs: tuple) -> np.ndarray:
return np.mean([np.asarray(hourly_target(asset, h)) for h in hs], axis=0)
def port_hourly(hs: tuple) -> tuple[pd.Series, float]:
"""Serie daily del book aggregato (0.5 BTC + 0.5 ETH su grid 1h) + turnover/anno."""
nets, turns = {}, 0.0
for a in ASSETS:
df = get1h(a)
ev = al.eval_weights(df, ens_target(a, hs))
nets[a] = pd.Series(ev["net"], index=ev["idx"])
turns += 0.5 * ev["turnover_per_year"]
J = pd.concat(nets, axis=1, join="inner").fillna(0.0)
return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]), turns
# ---------------------------------------------------------------------------
# Small-cap: min-order $5, capitale condiviso 50/50 fra i 2 asset
# ---------------------------------------------------------------------------
def smallcap_net(df: pd.DataFrame, tgt: np.ndarray, capital: float,
min_order: float = 5.0) -> tuple[pd.Series, int]:
"""Copia locale della logica di al.eval_weights_smallcap che restituisce la serie net
(serve per combinare il book 2-asset). Cambi di nozionale < min_order NON eseguiti."""
c = df["close"].values.astype(float)
tgt = np.clip(np.nan_to_num(np.asarray(tgt, float)), -10, 10)
held = np.empty(len(tgt))
cur, n_tr = 0.0, 0
for i in range(len(tgt)):
if abs(tgt[i] - cur) * capital >= min_order:
cur = tgt[i]
n_tr += 1
held[i] = cur
r = al.simple_returns(c)
pos = np.zeros(len(held))
pos[1:] = held[:-1]
turn = np.abs(np.diff(pos, prepend=0.0))
net = pos * r - al.FEE_SIDE * turn
net[0] = 0.0
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
return pd.Series(net, index=idx), n_tr
def smallcap_port(hs: tuple, capital: float) -> dict:
"""Book realistico a `capital`: target per-asset = 0.5*ensemble (quota 50/50).
modeled = stesso book senza vincolo min-order (fee identiche proporzionali)."""
nets_r, nets_m, ntr = {}, {}, 0
for a in ASSETS:
df = get1h(a)
t = 0.5 * ens_target(a, hs)
nr, n = smallcap_net(df, t, capital)
nets_r[a] = nr
ntr += n
ev = al.eval_weights(df, t)
nets_m[a] = pd.Series(ev["net"], index=ev["idx"])
Jr = pd.concat(nets_r, axis=1, join="inner").fillna(0.0)
Jm = pd.concat(nets_m, axis=1, join="inner").fillna(0.0)
dr = al._to_daily(Jr[ASSETS[0]] + Jr[ASSETS[1]])
dm = al._to_daily(Jm[ASSETS[0]] + Jm[ASSETS[1]])
yrs = len(dr) / 365.25
return dict(sh_real=al._sh(dr), sh_model=al._sh(dm),
haircut=al._sh(dm) - al._sh(dr),
dd_real=al._dd_ret(dr), trades_per_year=ntr / yrs)
# ---------------------------------------------------------------------------
# Guardie
# ---------------------------------------------------------------------------
def sanity_h0() -> None:
"""h=0 deve riprodurre ESATTAMENTE il baseline (dati + strategia + metriche)."""
for a in ASSETS:
d0 = daily_off(a, 0)
ref = al.get(a, "1d")
for col in ("timestamp", "open", "high", "low", "close", "volume"):
assert np.allclose(d0[col].values.astype(float),
ref[col].values.astype(float), atol=0, rtol=0), \
f"resample_offset(h=0) != al.get('{a}','1d') su {col}"
mine = port_daily_native(0)
base = al.tp01_baseline_daily()
assert len(mine) == len(base) and np.allclose(mine.values, base.values, atol=1e-12), \
"portafoglio h=0 != tp01_baseline_daily"
mm, bb = dmetrics(mine), dmetrics(base)
print(f"[SANITY] h=0 == baseline: OK (Sharpe FULL {mm['sh_full']:.4f} == "
f"{bb['sh_full']:.4f}, HOLD {mm['sh_hold']:.4f} == {bb['sh_hold']:.4f})")
def causality_guards() -> None:
"""(a) prefix-recompute sul daily resampled: target[i] non cambia aggiungendo futuro.
(b) troncando il 1h, le barre daily complete restano identiche (solo l'ultima e' parziale)."""
for a in ASSETS:
for h in (0, 7, 13, 21):
d = daily_off(a, h)
t_full = TP.target_series(d)
cut = len(d) - 250
t_pref = TP.target_series(d.iloc[:cut].reset_index(drop=True))
assert np.allclose(t_full[:cut], t_pref, atol=1e-12), \
f"prefix-recompute FAIL {a} h={h}"
df1h = get1h(a)
d_tr = resample_offset(df1h.iloc[:-500].reset_index(drop=True), h)
k = len(d_tr) - 1 # l'ultima barra del troncato e' parziale per costruzione
for col in ("timestamp", "close"):
assert np.allclose(d_tr[col].values[:k].astype(float),
d[col].values[:k].astype(float)), \
f"1h-truncation FAIL {a} h={h} {col}"
print("[SANITY] guardie causalita' (prefix-recompute daily + troncamento 1h): OK")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print("=" * 96)
print("r0702 — TP01 rebalance timing-luck: 24 ancore daily + ensemble tranching")
print(f"CANONICAL={CANONICAL} fee 0.10% RT HOLD-OUT >= {HOLDOUT.date()}")
print("=" * 96)
sanity_h0()
causality_guards()
# ---- (1) per-offset, path daily-nativo --------------------------------
rows = []
for h in range(24):
m = dmetrics(port_daily_native(h))
rows.append(dict(h=h, **m))
T = pd.DataFrame(rows).set_index("h")
print("\n--- (1) PER-OFFSET (daily nativo, ancora h:00 UTC) ---")
print(f"{'h':>3} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'CAGR':>7} {'maxDD':>7}")
for h, r in T.iterrows():
tag = " <- canonico" if h == 0 else ""
print(f"{h:>3} {r.sh_full:>7.3f} {r.sh_is:>7.3f} {r.sh_hold:>7.3f} "
f"{r.cagr:>6.1%} {r.dd:>6.1%}{tag}")
def pctl(col: str) -> float:
v = T[col].values
return float((v < v[0]).mean() + 0.5 * (v == v[0]).mean()) * 100
print("\nDistribuzione fra le 24 ancore (min / mediana / max / std) [percentile di h=0]:")
for col, lbl in (("sh_full", "Sharpe FULL"), ("sh_is", "Sharpe IS(pre-2025)"),
("sh_hold", "Sharpe HOLD"), ("dd", "maxDD"), ("cagr", "CAGR")):
v = T[col]
print(f" {lbl:<20} {v.min():>7.3f} / {v.median():>7.3f} / {v.max():>7.3f} "
f"/ std {v.std():.3f} h=0 al {pctl(col):.0f}° pctl (val {v.iloc[0]:.3f})")
# ---- (2) ensemble headline, book aggregato su 1h ----------------------
print("\n--- (2) ENSEMBLE (book aggregato su grid 1h, fee su turnover netto) ---")
print(f"{'config':<22} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'CAGR':>7} "
f"{'maxDD':>7} {'DDhold':>7} {'turn/y':>7}")
head = {}
for name, hs in HEADLINE.items():
s, tpy = port_hourly(hs)
m = dmetrics(s)
head[name] = m
print(f"{name:<22} {m['sh_full']:>7.3f} {m['sh_is']:>7.3f} {m['sh_hold']:>7.3f} "
f"{m['cagr']:>6.1%} {m['dd']:>6.1%} {m['dd_hold']:>6.1%} {tpy:>7.1f}")
print("(nota: 'K=1 (canonico h=0)' qui e' lo stesso book valutato sul grid 1h — "
"differenze vs riga h=0 sopra = sola granularita' di compounding, non strategia)")
# ---- (3) varianza della stima: rotazioni complete per famiglia --------
print("\n--- (3) DISPERSIONE fra rotazioni (nessuna selezione: TUTTE le rotazioni) ---")
fams = {
"singole (24)": [(h,) for h in range(24)],
"K=2 h,h+12 (12)": [(h, h + 12) for h in range(12)],
"K=3 h,h+8,h+16 (8)": [(h, h + 8, h + 16) for h in range(8)],
"K=4 h,h+6,.. (6)": [tuple(h + 6 * j for j in range(4)) for h in range(6)],
}
print(f"{'famiglia':<20} {'ShFULL μ':>9} {'σ':>6} {'range':>13} "
f"{'ShHOLD μ':>9} {'σ':>6} {'range':>13}")
for name, rot in fams.items():
mf = [dmetrics(port_hourly(hs)[0]) for hs in rot]
f = np.array([m["sh_full"] for m in mf])
ho = np.array([m["sh_hold"] for m in mf])
print(f"{name:<20} {f.mean():>9.3f} {f.std():>6.3f} "
f"[{f.min():>5.3f},{f.max():>5.3f}] "
f"{ho.mean():>9.3f} {ho.std():>6.3f} [{ho.min():>5.3f},{ho.max():>5.3f}]")
# ---- (4) small-cap: haircut min-order per capitale --------------------
print("\n--- (4) SMALL-CAP (min order $5, capitale 50/50 sui 2 asset) ---")
print(f"{'config':<22} {'cap':>7} {'Sh model':>9} {'Sh real':>8} {'haircut':>8} "
f"{'DD real':>8} {'trade/y':>8}")
for name, hs in HEADLINE.items():
for cap in (600, 2000, 10000):
r = smallcap_port(hs, cap)
print(f"{name:<22} {cap:>7} {r['sh_model']:>9.3f} {r['sh_real']:>8.3f} "
f"{r['haircut']:>8.3f} {r['dd_real']:>7.1%} {r['trades_per_year']:>8.1f}")
print("\nFatto. Nessuna selezione sull'hold-out: ensemble a priori, giudizio su "
"struttura (varianza) + IS pre-2025; l'hold-out e' solo riportato.")
if __name__ == "__main__":
main()