Files
PythagorasGoal/scripts/research/ortho/skeptic_basket.py
Adriano Dal Pastro 0adc69a357 research(ortho): caccia all'ortogonale a TP01 — relative-value BTC/ETH reale ma NON deployabile (hedge mono-regime)
18 agenti su book market-neutral a 2 gambe BTC/ETH (eseguibili a $600, a differenza di XS01),
giudicati sul MARGINALE vs TP01 (altlib.marginal_vs_tp01), non sullo Sharpe assoluto.

Lab: ortholib.py (eval_book leak-free a 2 gambe + causalità + eseguibilità@600), ortho_score.py
(giudice), meta_ortho.py (corr mutua + persistenza multi-cut), sleeve_rv.py (curated, SELECTION-
BIASED, non deployare).

Esito: 17/18 "ADDS" -> gonfiato dall'hold-out corto fisso-2025 (finestra ETH-bleed dove TP01 è
debole). Diagnosi orchestratore: collassano a 8 bet (corr 0.43); persistenza multi-cut e selezione
walk-forward smascherano i 2025-only (kalman/xs2). Scettico indipendente: basket selection-free ha
uplift pre-2025 +0.027 = 49° percentile di asset-rumore corr-zero (matematica di diversificazione,
non segnale); corr(Sharpe-TP01, uplift) -0.87 (è un HEDGE dei drawdown di TP01); muore a 0.30% RT.

Verdetto: NIENTE in live. Resta solo TP01. Lezione: lo scorer marginale va indurito (multi-cut +
null-asset-rumore + distinguere hedge da alpha). Diario 2026-06-21-ortho-tp01-relative-value.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:35:48 +00:00

155 lines
6.6 KiB
Python

"""Adversarial dissection of the SELECTION-FREE relative-value basket vs TP01."""
from __future__ import annotations
import importlib.util, sys
from pathlib import Path
import numpy as np
import pandas as pd
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import ortholib as ol
import altlib as al
AGENTS = HERE / "agents"
def _book(path: Path):
spec = importlib.util.spec_from_file_location(path.stem, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.book
def _sh(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if len(r) > 2 and np.std(r) > 0 else 0.0
def _dd(s) -> float:
eq = (1.0 + pd.Series(s).dropna()).cumprod()
return float((eq / eq.cummax() - 1.0).min())
def uplift_at(cand: pd.Series, B: pd.Series, lo=None, hi=None, w=0.25) -> float:
J = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna()
if lo is not None:
J = J[J.index >= pd.Timestamp(lo, tz="UTC")]
if hi is not None:
J = J[J.index < pd.Timestamp(hi, tz="UTC")]
if len(J) < 30:
return float("nan")
return _sh((1 - w) * J["B"] + w * J["C"]) - _sh(J["B"])
def main():
B = al.tp01_baseline_daily()
# ---- collect ALL books, classify ----
rows = {}
meta = {}
for p in sorted(AGENTS.glob("agent_*.py")):
try:
ev = ol.eval_book(_book(p))
except Exception as e:
print(f"skip {p.stem}: {e}"); continue
d = ev["daily"]
if d.std() == 0:
print(f"skip degenerate {p.stem}"); continue
rows[p.stem.replace("agent_", "")] = d
meta[p.stem.replace("agent_", "")] = ev
names = list(rows)
M = pd.concat(rows, axis=1, join="inner").dropna()
print(f"\nBooks loaded: {len(names)} common-date matrix: {M.shape[0]} days "
f"[{M.index.min().date()} .. {M.index.max().date()}]")
# selection-free: ALL non-degenerate, market-neutral. exclude only the OBVIOUS
# degenerate DILUTE (rebalance_harvest, sharpe<0 standalone & negative every metric).
excl = ["12_rebalance_harvest"]
keep = [n for n in names if n not in excl]
print(f"Basket = equal-weight of {len(keep)} books (excluded only {excl})")
basket = M[keep].mean(axis=1)
basket_all = M.mean(axis=1) # truly ALL incl rebalance_harvest
for label, bk in [("BASKET(excl harvest)", basket), ("BASKET(all 18)", basket_all)]:
print(f"\n===== {label} =====")
Bal = pd.concat({"B": B, "C": bk}, axis=1, join="inner").dropna()
corr = Bal["B"].corr(Bal["C"])
print(f" standalone Sharpe {_sh(bk):.3f} maxDD {_dd(bk):.3f} corr->TP01 {corr:.3f} "
f"n={len(bk)}")
# full + per-cut uplift (forward windows from cut)
print(f" uplift (w=0.25, window = [cut, end)):")
for cut in ["2018-01-01", "2021-01-01", "2022-01-01", "2023-01-01", "2024-01-01", "2025-01-01"]:
u = uplift_at(bk, B, lo=cut)
print(f" from {cut[:4]}+ : {u:+.3f}")
# PRE-2025 ONLY (exclude suspect window)
u_pre = uplift_at(bk, B, hi="2025-01-01")
u_2025 = uplift_at(bk, B, lo="2025-01-01")
print(f" uplift 2019-2024 ONLY (exclude suspect window): {u_pre:+.3f}")
print(f" uplift 2025+ ONLY : {u_2025:+.3f}")
# per-year standalone returns & per-year uplift
print(f" per-year: standalone ret | blend-uplift(w0.25, that year only)")
for y in sorted(set(bk.index.year)):
sub = bk[bk.index.year == y]
ret = float((1 + sub).prod() - 1)
uy = uplift_at(bk, B, lo=f"{y}-01-01", hi=f"{y+1}-01-01")
print(f" {y}: ret {ret:+7.2%} uplift {uy:+.3f}")
# ---- decompose full-sample uplift: pre vs post 2025 ----
print("\n===== UPLIFT DECOMPOSITION (basket excl harvest, w=0.25) =====")
full = uplift_at(basket, B, lo="2018-01-01")
print(f" full-sample uplift {full:+.3f}")
print(f" pre-2025 (2019-2024) {uplift_at(basket,B,hi='2025-01-01'):+.3f}")
print(f" 2025+ only {uplift_at(basket,B,lo='2025-01-01'):+.3f}")
# ---- fee sensitivity: rebuild basket at higher fees ----
print("\n===== FEE SENSITIVITY (rebuild every book's daily net at higher RT fee) =====")
for fee_side in [0.0005, 0.0010, 0.0015]:
dd = {}
for p in sorted(AGENTS.glob("agent_*.py")):
nm = p.stem.replace("agent_", "")
if nm in excl: continue
try:
ev = ol.eval_book(_book(p), fee_side=fee_side)
if ev["daily"].std() > 0:
dd[nm] = ev["daily"]
except Exception:
pass
Mf = pd.concat(dd, axis=1, join="inner").dropna()
bk = Mf.mean(axis=1)
u_full = uplift_at(bk, B, lo="2018-01-01")
u_pre = uplift_at(bk, B, hi="2025-01-01")
u_25 = uplift_at(bk, B, lo="2025-01-01")
print(f" RT={2*fee_side*100:.2f}% standalone Sh {_sh(bk):+.3f} "
f"uplift full {u_full:+.3f} | pre25 {u_pre:+.3f} | 25+ {u_25:+.3f}")
# ---- turnover / executability of the basket ----
print("\n===== EXECUTION REALISM (per-book at $600, basket-averaged legs) =====")
tns = [meta[n]["turnover_per_year"] for n in keep]
print(f" per-book turnover/yr: min {min(tns):.1f} med {np.median(tns):.1f} max {max(tns):.1f}")
print(f" Basket = mean of {len(keep)} books; each leg capped 0.5. Averaged turnover lower.")
# build averaged book legs to measure REAL basket turnover & notional
btc, eth = ol.aligned()
wbs, wes = [], []
for n in keep:
wb, we = _book(AGENTS / f"agent_{n}.py")(btc, eth)
wbs.append(np.nan_to_num(np.asarray(wb, float)))
wes.append(np.nan_to_num(np.asarray(we, float)))
wb = np.clip(np.mean(wbs, axis=0), -0.5, 0.5)
we = np.clip(np.mean(wes, axis=0), -0.5, 0.5)
pb = np.zeros(len(wb)); pb[1:] = wb[:-1]
pe = np.zeros(len(we)); pe[1:] = we[:-1]
turn = np.abs(np.diff(pb, prepend=0.0)) + np.abs(np.diff(pe, prepend=0.0))
turn_yr = turn.sum() / (len(turn) / 365.25)
max_leg = float(np.max(np.maximum(np.abs(pb), np.abs(pe))))
print(f" AVERAGED-BASKET book: turnover/yr {turn_yr:.1f} max-leg-frac {max_leg:.3f} "
f"max per-leg notional @$600 = ${max_leg*600:.0f}")
print(f" typical daily notional traded @$600 satellite (say 20% slot=$120): "
f"${turn_yr/365.25*120:.2f}/day both legs")
if __name__ == "__main__":
main()