0adc69a357
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>
108 lines
4.7 KiB
Python
108 lines
4.7 KiB
Python
"""Null tests: is the pre-2025 +0.027 uplift distinguishable from 'any zero-mean low-corr
|
|
noise asset dampens a Sharpe-1.3 portfolio at w=0.25'? And bootstrap CI on the uplift."""
|
|
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"
|
|
rng = np.random.default_rng(42)
|
|
|
|
|
|
def _book(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):
|
|
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 uplift(B, C, lo=None, hi=None, w=0.25):
|
|
J = pd.concat({"B": B, "C": C}, 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()
|
|
excl = ["12_rebalance_harvest"]
|
|
rows = {}
|
|
for p in sorted(AGENTS.glob("agent_*.py")):
|
|
nm = p.stem.replace("agent_", "")
|
|
if nm in excl: continue
|
|
ev = ol.eval_book(_book(p))
|
|
if ev["daily"].std() > 0:
|
|
rows[nm] = ev["daily"]
|
|
M = pd.concat(rows, axis=1, join="inner").dropna()
|
|
basket = M.mean(axis=1)
|
|
|
|
# ---- NULL 1: a synthetic zero-corr asset with the SAME standalone Sharpe & vol as the
|
|
# basket. If it gives the same ~+0.03 pre-2025 uplift, the uplift is just diversification
|
|
# math, not a specific edge.
|
|
Jb = pd.concat({"B": B, "C": basket}, axis=1, join="inner").dropna()
|
|
pre = Jb[Jb.index < pd.Timestamp("2025-01-01", tz="UTC")]
|
|
bk_pre = pre["C"]
|
|
mu, sd = bk_pre.mean(), bk_pre.std()
|
|
print(f"Pre-2025 basket: mean {mu:.5f}/day vol {sd:.5f} Sharpe {_sh(bk_pre):.3f} "
|
|
f"corr->TP01 {pre['B'].corr(pre['C']):.3f}")
|
|
print(f"Pre-2025 ACTUAL basket uplift (w=0.25): {uplift(B,basket,hi='2025-01-01'):+.3f}\n")
|
|
|
|
print("NULL: 500 synthetic zero-corr assets, same daily mean & vol as pre-2025 basket,")
|
|
print(" independent random sign each day. Pre-2025 uplift distribution:")
|
|
null_u = []
|
|
for _ in range(500):
|
|
noise = pd.Series(rng.normal(mu, sd, len(pre)), index=pre.index)
|
|
null_u.append(uplift(B, noise, hi="2025-01-01"))
|
|
null_u = np.array(null_u)
|
|
print(f" null uplift: mean {null_u.mean():+.3f} sd {null_u.std():.3f} "
|
|
f"5-95pct [{np.percentile(null_u,5):+.3f}, {np.percentile(null_u,95):+.3f}]")
|
|
actual = uplift(B, basket, hi="2025-01-01")
|
|
pctile = (null_u < actual).mean()*100
|
|
print(f" ACTUAL pre-2025 uplift {actual:+.3f} sits at the {pctile:.0f}th percentile of the null")
|
|
print(" (if ~50th pct, the pre-2025 'edge' is INDISTINGUISHABLE from a positive-drift noise asset)\n")
|
|
|
|
# ---- NULL 2: same but ZERO mean (pure diversification, no standalone return) ----
|
|
null_u0 = []
|
|
for _ in range(500):
|
|
noise = pd.Series(rng.normal(0.0, sd, len(pre)), index=pre.index)
|
|
null_u0.append(uplift(B, noise, hi="2025-01-01"))
|
|
null_u0 = np.array(null_u0)
|
|
print(f" ZERO-mean noise null uplift: mean {null_u0.mean():+.3f} "
|
|
f"5-95pct [{np.percentile(null_u0,5):+.3f}, {np.percentile(null_u0,95):+.3f}]")
|
|
print(f" => pure diversification of a zero-return asset gives ~{null_u0.mean():+.3f} uplift\n")
|
|
|
|
# ---- bootstrap CI on the FULL-sample and pre-2025 basket uplift (block bootstrap) ----
|
|
print("Block-bootstrap (30d blocks, 1000 draws) CI on uplift:")
|
|
for lbl, lo, hi in [("full", "2018-01-01", None), ("pre-2025", "2018-01-01", "2025-01-01"),
|
|
("2025+", "2025-01-01", None)]:
|
|
J = pd.concat({"B": B, "C": basket}, axis=1, join="inner").dropna()
|
|
if lo: J = J[J.index >= pd.Timestamp(lo, tz="UTC")]
|
|
if hi: J = J[J.index < pd.Timestamp(hi, tz="UTC")]
|
|
n = len(J); bl = 30; nb = n // bl
|
|
ups = []
|
|
for _ in range(1000):
|
|
starts = rng.integers(0, n - bl, nb)
|
|
idx = np.concatenate([np.arange(s, s+bl) for s in starts])
|
|
sub = J.iloc[idx]
|
|
ups.append(_sh(0.75*sub["B"]+0.25*sub["C"]) - _sh(sub["B"]))
|
|
ups = np.array(ups)
|
|
frac_pos = (ups > 0).mean()*100
|
|
print(f" {lbl:9s}: median {np.median(ups):+.3f} "
|
|
f"5-95pct [{np.percentile(ups,5):+.3f}, {np.percentile(ups,95):+.3f}] "
|
|
f"P(uplift>0)={frac_pos:.0f}%")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|