Files
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

134 lines
6.5 KiB
Python

"""ortholib — lab for strategies ORTHOGONAL to TP01 (the only thing worth a NEW live slot).
The blind fleet proved (again) that directional BTC/ETH is trend-beta of TP01 — a ~1.3
ceiling, nothing new to deploy. The ONLY way to earn a live slot is a mechanism whose
returns are NOT explained by TP01's trend. The most promising one that is ALSO executable
at our real ~$600 (unlike XS01's 19-leg book, which needs ~$20k) is a **2-leg BTC/ETH
relative-value book**: long one leg / short (or under-weight) the other. Being roughly
market-neutral, its correlation to TP01 (a long-flat trend on the SUM) is naturally low.
A candidate here is a BOOK function:
def book(btc, eth) -> (w_btc, w_eth)
where w_*[i] in [-1,1] is the per-leg weight (fraction of equity, sign = long/short),
decided causally with data <= close[i]. The evaluator SHIFTS both legs (held during bar
i+1), charges fees on BOTH legs' turnover, and:
* builds the book's net DAILY return series,
* scores it MARGINALLY vs TP01 via altlib.marginal_vs_tp01 (corr, OOS blend uplift,
residual alpha, robust_oos jackknife) — the verdict that matters is ADDS, not Sharpe,
* checks EXECUTABILITY at $600 (per-leg notional within the live cap, turnover sane),
* runs the same online causality guard as the blind lab.
Judge a candidate on: marginal_verdict == 'ADDS' AND robust_oos AND executable. Absolute
PnL/DD are reported but are NOT the gate (a market-neutral sleeve has modest absolute
numbers by design; its job is to improve the PORTFOLIO, not to win alone).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
FEE_SIDE = al.FEE_SIDE
CAPITAL = 600.0 # real mainnet capital (NOT the 2000 paper nominal)
CAP_NOTIONAL = 300.0 # live guardrail: $300 notional / asset
MIN_ORDER = 5.0
CAP_FRAC = CAP_NOTIONAL / CAPITAL # 0.5 of capital per leg
def aligned():
"""BTC & ETH 1d on COMMON dates (inner join), as two parallel DataFrames with a
shared 'datetime'. Relative-value needs both legs at the same bar."""
b = al.get("BTC", "1d").copy()
e = al.get("ETH", "1d").copy()
b["dt"] = pd.to_datetime(b["datetime"], utc=True)
e["dt"] = pd.to_datetime(e["datetime"], utc=True)
common = pd.Index(b["dt"]).intersection(pd.Index(e["dt"]))
b = b[b["dt"].isin(common)].sort_values("dt").reset_index(drop=True)
e = e[e["dt"].isin(common)].sort_values("dt").reset_index(drop=True)
return b, e
def eval_book(book_fn, fee_side: float = FEE_SIDE) -> dict:
"""Honest backtest of a 2-leg BTC/ETH book. Weights decided at close[i] are HELD
during bar i+1 (shift here -> no leak). Fee on both legs' turnover. Returns standalone
metrics, the net DAILY series (for marginal scoring), and executability stats."""
btc, eth = aligned()
wb, we = book_fn(btc, eth)
wb = np.clip(np.nan_to_num(np.asarray(wb, float), nan=0.0), -1, 1)
we = np.clip(np.nan_to_num(np.asarray(we, float), nan=0.0), -1, 1)
n = len(btc)
rb = al.simple_returns(btc["close"].values.astype(float))
re = al.simple_returns(eth["close"].values.astype(float))
pb = np.zeros(n); pb[1:] = wb[:-1]
pe = np.zeros(n); pe[1:] = we[:-1]
gross = pb * rb + pe * re
turn = np.abs(np.diff(pb, prepend=0.0)) + np.abs(np.diff(pe, prepend=0.0))
net = gross - fee_side * turn
net[0] = 0.0
idx = pd.DatetimeIndex(btc["dt"])
m = al._metrics_from_net(net, idx)
daily = al._to_daily(pd.Series(net, index=idx))
# executability: worst per-leg notional fraction, and net market exposure
max_leg = float(np.max(np.maximum(np.abs(pb), np.abs(pe)))) if n else 0.0
gross_lev = float(np.max(np.abs(pb) + np.abs(pe))) if n else 0.0
net_beta = float(np.mean(pb + pe)) # ~0 => market-neutral
turnover_yr = float(turn.sum() / (len(turn) / 365.25))
return dict(
pnl=m["ret"], maxdd=m["maxdd"], sharpe=m["sharpe"], cagr=m["cagr"],
net=net, idx=idx, daily=daily,
max_leg_frac=round(max_leg, 3), gross_lev=round(gross_lev, 3),
net_beta=round(net_beta, 3), turnover_per_year=round(turnover_yr, 1),
executable=bool(max_leg <= CAP_FRAC + 1e-9 and max_leg * CAPITAL >= MIN_ORDER),
)
def marginal(book_fn, fee_side: float = FEE_SIDE) -> dict:
"""eval_book -> altlib.marginal_vs_tp01 on the book's daily returns. The real verdict."""
ev = eval_book(book_fn, fee_side)
marg = al.marginal_vs_tp01(ev["daily"])
return dict(book=ev, marginal=marg)
def causality_ok(book_fn, tail: int = 60, tol: float = 1e-4) -> dict:
"""Online-consistency guard: book(btc[:cut], eth[:cut]) must match book(full)[:cut]
on the tail before each cut. Catches any look-ahead / global fit in either leg."""
btc, eth = aligned()
wbf, wef = book_fn(btc, eth)
wbf = np.nan_to_num(np.asarray(wbf, float)); wef = np.nan_to_num(np.asarray(wef, float))
n = len(btc)
max_diff = 0.0; frac_bad = 0.0; checked = []
for cut in (int(n * 0.80), int(n * 0.92)):
if cut <= tail + 5 or cut >= n:
continue
wbs, wes = book_fn(btc.iloc[:cut].reset_index(drop=True),
eth.iloc[:cut].reset_index(drop=True))
wbs = np.nan_to_num(np.asarray(wbs, float)); wes = np.nan_to_num(np.asarray(wes, float))
if len(wbs) != cut or len(wes) != cut:
return dict(ok=False, reason=f"book returned wrong length on prefix {cut}",
max_diff=9.99, frac_bad=1.0)
d = np.maximum(np.abs(wbs[cut - tail:cut] - wbf[cut - tail:cut]),
np.abs(wes[cut - tail:cut] - wef[cut - tail:cut]))
max_diff = max(max_diff, float(np.max(d)) if len(d) else 0.0)
frac_bad = max(frac_bad, float(np.mean(d > tol)) if len(d) else 0.0)
checked.append(cut)
ok = (max_diff <= max(tol * 10, 1e-3)) and (frac_bad <= 0.02)
return dict(ok=bool(ok), max_diff=round(max_diff, 6), frac_bad=round(frac_bad, 4),
checked_at=checked)
# convenient causal helpers re-exported (same as blind/altlib)
simple_returns = al.simple_returns
log_returns = al.log_returns
ema = al.ema; sma = al.sma; rolling_std = al.rolling_std; zscore = al.zscore
rsi = al.rsi; realized_vol = al.realized_vol; donchian = al.donchian; bbands = al.bbands
vol_target = al.vol_target
__all__ = ["aligned", "eval_book", "marginal", "causality_ok", "CAPITAL", "CAP_FRAC",
"FEE_SIDE", "simple_returns", "log_returns", "ema", "sma", "rolling_std",
"zscore", "rsi", "realized_vol", "donchian", "bbands", "vol_target"]