Files
Adriano Dal Pastro 73d74c5e53 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>
2026-07-02 22:12:22 +00:00

320 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""r0702_slow_clock.py — FILONE: clock più lenti del daily + banded rebalancing per TP01.
Due idee di TIMING DI ESECUZIONE (non di segnale) sul TP01 CANONICAL (PORT LF1d):
(A) CLOCK LENTI — segnale calcolato daily, posizione aggiornata solo ogni N giorni
(N in {2,3,5,7}). ⚠ timing luck: si riportano TUTTE le N fasi (min/med/max) e
l'ENSEMBLE delle fasi (media dei libri sfasati), MAI la fase migliore da sola.
(B) BANDE DI ISTERESI — decisione daily, si esegue solo se |target posizione| > banda
(banda in frazione di equity PER ASSET, in {0, .025, .05, .10, .20}); quando si
esegue si va al target pieno.
Onestà:
- selezione cella SOLO in-sample pre-2025 (pattern al.select_cell_insample); l'hold-out
della cella scelta si RIPORTA, non si sceglie.
- deflated Sharpe (al.deflated_sharpe) su TUTTI i trial esplorati (fasi incluse).
- Sharpe LORDO (fee=0) accanto al netto: una variante di esecuzione onesta ha lordo
~uguale al canonico e netto >= (il guadagno è meccanico-di-costo, non fitting).
- executability: replica di eval_weights_smallcap a capitale 600/2000/10000 (min order
$5, capitale per-asset = C/2) per baseline vs variante scelta — a $600 la banda
implicita min-order è 5/(600/2) ≈ 0.0167 di peso per asset.
- causalità: target TP01 causale (verificato altrove); i filtri di esecuzione usano solo
stato passato; eval_weights fa lo shift +1; check prefix-consistency inline sulla
cella scelta. Nessun ffill mixed-TF, nessun .view("int64") su tz-aware.
Convenzione (stessa di eval_weights/TrendPortfolio): il peso resta costante tra i
ribilanciamenti (fee solo su |Δpeso|); il drift del peso intra-periodo non è modellato
(secondo ordine a N<=7 giorni) — dichiarato nei caveat.
Run: uv run python scripts/research/r0702_slow_clock.py
"""
from __future__ import annotations
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
import numpy as np # noqa: E402
import pandas as pd # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
HOLDOUT = al.HOLDOUT
FEE = al.FEE_SIDE
EPOCH = pd.Timestamp("1970-01-01", tz="UTC")
CLOCK_NS = (2, 3, 5, 7)
BANDS = (0.0, 0.025, 0.05, 0.10, 0.20)
CAPITALS = (600.0, 2000.0, 10000.0)
MIN_ORDER = 5.0
# ---------------------------------------------------------------------------
# targets & execution filters (tutti causali: stato = solo passato)
# ---------------------------------------------------------------------------
def daily_targets() -> dict[str, tuple[pd.DataFrame, np.ndarray]]:
tp = TrendPortfolio(**CANONICAL)
out = {}
for a in al.CERTIFIED:
df = al.get(a, "1d")
out[a] = (df, tp.target_series(df))
return out
def epoch_days(df: pd.DataFrame) -> np.ndarray:
dt = pd.to_datetime(df["datetime"], utc=True)
return ((dt.dt.floor("D") - EPOCH) // pd.Timedelta(days=1)).values.astype(int)
def slow_clock_exec(df: pd.DataFrame, tgt: np.ndarray, N: int, phase: int) -> np.ndarray:
"""Aggiorna la posizione solo nei giorni con epoch_day % N == phase (ancoraggio a
calendario -> prefix-consistent, entrambe le gambe ribilanciano lo stesso giorno)."""
days = epoch_days(df)
out = np.empty(len(tgt))
cur = 0.0
for i in range(len(tgt)):
if days[i] % N == phase:
cur = tgt[i]
out[i] = cur
return out
def band_exec(tgt: np.ndarray, band: float) -> np.ndarray:
"""Esegue (va al target pieno) solo se |target posizione corrente| > band."""
out = np.empty(len(tgt))
cur = 0.0
for i in range(len(tgt)):
if abs(tgt[i] - cur) > band:
cur = tgt[i]
out[i] = cur
return out
def smallcap_exec(tgt: np.ndarray, capital_per_asset: float,
min_order: float = MIN_ORDER) -> np.ndarray:
"""Replica della logica di al.eval_weights_smallcap (serve la SERIE, non solo le
metriche): un Δpeso il cui nozionale < min_order NON si esegue."""
out = np.empty(len(tgt))
cur = 0.0
for i in range(len(tgt)):
if abs(tgt[i] - cur) * capital_per_asset >= min_order:
cur = tgt[i]
out[i] = cur
return out
# ---------------------------------------------------------------------------
# valutazione book 50/50 (netto + lordo)
# ---------------------------------------------------------------------------
def _series(df: pd.DataFrame, et: np.ndarray, fee_side: float) -> pd.Series:
ev = al.eval_weights(df, et, fee_side=fee_side)
return pd.Series(ev["net"], index=ev["idx"])
def book_eval(pairs: dict[str, tuple[pd.DataFrame, np.ndarray]]) -> dict:
"""pairs: {asset: (df, exec_target)} -> metriche book 50/50 nette e lorde."""
net_s, gro_s = {}, {}
turn_book = 0.0
orders_y = 0.0
for a, (df, et) in pairs.items():
net_s[a] = _series(df, et, FEE)
gro_s[a] = _series(df, et, 0.0)
years = (df["datetime"].iloc[-1] - df["datetime"].iloc[0]).total_seconds() / 86400 / 365.25
pos = np.zeros(len(et)); pos[1:] = et[:-1]
turn = np.abs(np.diff(pos, prepend=0.0))
turn_book += 0.5 * turn.sum() / years # in unità di equity del book
orders_y += float(np.sum(turn > 1e-12) / years) # ordini reali (entrambe le gambe)
NET = pd.concat(net_s, axis=1, join="inner").fillna(0.0)
GRO = pd.concat(gro_s, axis=1, join="inner").fillna(0.0)
assets = list(pairs)
net = 0.5 * NET[assets[0]] + 0.5 * NET[assets[1]]
gro = 0.5 * GRO[assets[0]] + 0.5 * GRO[assets[1]]
def _hold(s): return s[s.index >= HOLDOUT]
def _ins(s): return s[s.index < HOLDOUT]
eqn = np.cumprod(1.0 + np.clip(net.values, -0.99, None))
span_y = (net.index[-1] - net.index[0]).total_seconds() / 86400 / 365.25
cagr = eqn[-1] ** (1 / span_y) - 1 if span_y > 0 else 0.0
return dict(
sh_full_net=al._sh(net), sh_hold_net=al._sh(_hold(net)), sh_ins_net=al._sh(_ins(net)),
sh_full_gro=al._sh(gro), sh_hold_gro=al._sh(_hold(gro)),
maxdd=al._dd_ret(net), cagr=cagr,
turnover_y=turn_book, orders_y=orders_y, net=net, gross=gro,
)
def row(tag: str, m: dict) -> str:
return (f"{tag:<26} | net F {m['sh_full_net']:5.2f} H {m['sh_hold_net']:5.2f} "
f"(IS {m['sh_ins_net']:5.2f}) | gross F {m['sh_full_gro']:5.2f} "
f"H {m['sh_hold_gro']:5.2f} | DD {m['maxdd']*100:4.1f}% | CAGR {m['cagr']*100:5.1f}% "
f"| turn/y {m['turnover_y']:5.1f} | ord/y {m['orders_y']:6.1f}")
# ---------------------------------------------------------------------------
def main() -> None:
pairs = daily_targets()
# ---- sanity check: riproduci al.tp01_baseline_daily() -------------------
base = book_eval(pairs)
ref = al.tp01_baseline_daily()
common = base["net"].index.intersection(ref.index)
diff = float(np.max(np.abs(base["net"].reindex(common).values - ref.reindex(common).values)))
print("=" * 118)
print("SANITY — baseline daily vs al.tp01_baseline_daily():",
f"max|Δdaily ret| = {diff:.2e}",
f"(Sharpe qui {base['sh_full_net']:.3f} / ref {al._sh(ref):.3f})")
assert diff < 1e-9, "baseline non riprodotta!"
print(row("BASELINE daily band=0", base))
all_trial_sharpes: list[float] = [base["sh_full_net"]]
candidates: dict[str, dict] = {"baseline_daily": base}
# ---- (A) CLOCK LENTI: tutte le fasi + ensemble ---------------------------
print("\n" + "=" * 118)
print("(A) CLOCK LENTI — TP01 daily-signal, ribilanciamento ogni N giorni "
"(tutte le fasi: min/med/max; ensemble = media dei libri sfasati)")
print("=" * 118)
clock_tbl = {}
for N in CLOCK_NS:
phase_ms, phase_nets, phase_gros = [], [], []
for p in range(N):
pp = {a: (df, slow_clock_exec(df, tgt, N, p)) for a, (df, tgt) in pairs.items()}
m = book_eval(pp)
phase_ms.append(m)
phase_nets.append(m["net"]); phase_gros.append(m["gross"])
all_trial_sharpes.append(m["sh_full_net"])
ens_net = pd.concat(phase_nets, axis=1, join="inner").mean(axis=1)
ens_gro = pd.concat(phase_gros, axis=1, join="inner").mean(axis=1)
eqn = np.cumprod(1.0 + np.clip(ens_net.values, -0.99, None))
span_y = (ens_net.index[-1] - ens_net.index[0]).total_seconds() / 86400 / 365.25
ens = dict(
sh_full_net=al._sh(ens_net),
sh_hold_net=al._sh(ens_net[ens_net.index >= HOLDOUT]),
sh_ins_net=al._sh(ens_net[ens_net.index < HOLDOUT]),
sh_full_gro=al._sh(ens_gro), sh_hold_gro=al._sh(ens_gro[ens_gro.index >= HOLDOUT]),
maxdd=al._dd_ret(ens_net), cagr=eqn[-1] ** (1 / span_y) - 1,
turnover_y=float(np.mean([m["turnover_y"] for m in phase_ms])),
orders_y=float(np.mean([m["orders_y"] for m in phase_ms])),
net=ens_net, gross=ens_gro,
)
all_trial_sharpes.append(ens["sh_full_net"])
candidates[f"clock_N{N}_ensemble"] = ens
clock_tbl[N] = (phase_ms, ens)
fn = [m["sh_full_net"] for m in phase_ms]
hn = [m["sh_hold_net"] for m in phase_ms]
fg = [m["sh_full_gro"] for m in phase_ms]
hg = [m["sh_hold_gro"] for m in phase_ms]
dd = [m["maxdd"] for m in phase_ms]
oy = [m["orders_y"] for m in phase_ms]
print(f"N={N} fasi ({N}): net FULL min/med/max {min(fn):.2f}/{np.median(fn):.2f}/{max(fn):.2f}"
f" HOLD {min(hn):.2f}/{np.median(hn):.2f}/{max(hn):.2f}"
f" | gross FULL {min(fg):.2f}/{np.median(fg):.2f}/{max(fg):.2f}"
f" HOLD {min(hg):.2f}/{np.median(hg):.2f}/{max(hg):.2f}"
f" | DD {min(dd)*100:.1f}-{max(dd)*100:.1f}% | ord/y {min(oy):.0f}-{max(oy):.0f}")
print(row(f" N={N} ENSEMBLE", ens))
# ---- (B) BANDE DI ISTERESI ----------------------------------------------
print("\n" + "=" * 118)
print("(B) BANDE DI ISTERESI — decisione daily, esecuzione solo se |targetpos| > banda "
"(frazione di equity per asset); si va al target pieno")
print("=" * 118)
band_tbl = {}
for b in BANDS:
pp = {a: (df, band_exec(tgt, b)) for a, (df, tgt) in pairs.items()}
m = book_eval(pp)
band_tbl[b] = m
all_trial_sharpes.append(m["sh_full_net"])
if b > 0:
candidates[f"band_{b:g}"] = m
saved = base["turnover_y"] - m["turnover_y"]
print(row(f"banda {b:5.3f}", m) +
f" | turn risparmiato {saved:5.1f}/y (fee ~{saved*FEE*100*2:.2f}%/y su RT)")
# ---- selezione IN-SAMPLE (pre-2025) e hold-out riportato -----------------
print("\n" + "=" * 118)
print("SELEZIONE CELLA — solo in-sample pre-2025 (l'hold-out si riporta, non si sceglie)")
print("=" * 118)
ranked = sorted(candidates.items(), key=lambda kv: kv[1]["sh_ins_net"], reverse=True)
for name, m in ranked:
print(f" IS {m['sh_ins_net']:5.3f} | HOLD {m['sh_hold_net']:5.3f} | FULL {m['sh_full_net']:5.3f} {name}")
chosen_name, chosen = ranked[0]
n_trials = len(all_trial_sharpes)
dsr, sr0 = al.deflated_sharpe(chosen["sh_full_net"], all_trial_sharpes, chosen["net"])
print(f"\nCELLA SCELTA IN-SAMPLE: {chosen_name}")
print(row(" scelta", chosen))
print(f" trials totali esplorati: {n_trials} (fasi singole incluse)")
print(f" deflated Sharpe (vs {n_trials} trial): DSR={dsr:.3f}, null-max atteso={sr0:.3f} "
f"(NB: candidato = variante di TP01, correlatissima al baseline — l'asticella "
f"giusta è lordo~uguale/netto-migliore, non earns_slot)")
dgro = chosen["sh_full_gro"] - base["sh_full_gro"]
dnet = chosen["sh_full_net"] - base["sh_full_net"]
dgro_h = chosen["sh_hold_gro"] - base["sh_hold_gro"]
dnet_h = chosen["sh_hold_net"] - base["sh_hold_net"]
print(f" Δ vs baseline — FULL: gross {dgro:+.3f} / net {dnet:+.3f} "
f"HOLD: gross {dgro_h:+.3f} / net {dnet_h:+.3f}")
print(f" fee drag baseline: turn {base['turnover_y']:.1f}/y × {2*FEE*100:.2f}%RT "
f"≈ {base['turnover_y']*FEE*100:.2f}%/y di equity — questo è il TETTO del guadagno meccanico")
# ---- prefix-consistency (causalità dell'exec filter) ---------------------
ok = True
for a, (df, tgt) in pairs.items():
if chosen_name.startswith("band"):
b = float(chosen_name.split("_")[1])
full_e = band_exec(tgt, b)
cut = int(len(df) * 0.8)
sub = df.iloc[:cut].reset_index(drop=True)
sub_t = TrendPortfolio(**CANONICAL).target_series(sub)
sub_e = band_exec(sub_t, b)
elif chosen_name.startswith("clock"):
N = int(chosen_name.split("_")[1][1:])
full_e = slow_clock_exec(df, tgt, N, 0)
cut = int(len(df) * 0.8)
sub = df.iloc[:cut].reset_index(drop=True)
sub_t = TrendPortfolio(**CANONICAL).target_series(sub)
sub_e = slow_clock_exec(sub, sub_t, N, 0)
else:
continue
d = float(np.max(np.abs(sub_e[-60:] - full_e[cut - 60:cut])))
ok &= d < 1e-9
print(f" prefix-consistency exec-filter (fase 0 per i clock): {'OK' if ok else 'FAIL'}")
# ---- (6) EXECUTABILITY small-cap a 600 / 2000 / 10000 --------------------
print("\n" + "=" * 118)
print("(6) EXECUTABILITY — min order $5, capitale per-asset = C/2 "
"(banda implicita = 5/(C/2) in peso per asset)")
print("=" * 118)
def chosen_exec(a, df, tgt):
if chosen_name.startswith("band"):
return band_exec(tgt, float(chosen_name.split("_")[1]))
if chosen_name.startswith("clock"):
N = int(chosen_name.split("_")[1][1:])
# deploy reale = UNA fase; usiamo fase 0 e dichiariamo la timing luck
return slow_clock_exec(df, tgt, N, 0)
return tgt.copy()
for C in CAPITALS:
cpa = C / 2.0
implicit = MIN_ORDER / cpa
print(f"\ncapitale ${C:.0f} (banda implicita min-order = {implicit:.4f} peso/asset)")
for label, mk in (("baseline daily", lambda a, df, t: t.copy()),
(f"variante [{chosen_name}]", chosen_exec)):
pp = {a: (df, smallcap_exec(mk(a, df, tgt), cpa))
for a, (df, tgt) in pairs.items()}
m = book_eval(pp)
# cross-check con l'utility ufficiale (per-asset, solo full)
hc = {a: al.eval_weights_smallcap(df, mk(a, df, tgt), capital=cpa)["sharpe_haircut"]
for a, (df, tgt) in pairs.items()}
print(row(f" {label}", m) +
f" | haircut/asset vs modellato: " +
", ".join(f"{a} {h:+.3f}" for a, h in hc.items()))
print("\nNOTA: se la banda ottimale ≈ banda implicita a $600 (0.0167), il vincolo "
"small-cap del libro live sta GIÀ facendo il lavoro della banda.")
if __name__ == "__main__":
main()