Files
PythagorasGoal/scripts/strategies/XS01_cross_sectional.py
T
Adriano Dal Pastro a85289d7c7 feat(xsec): XS01 reversione cross-sectional (8 asset) -> PORT06 PAPER
Famiglia NUOVA trovata in sessione (dopo aver scartato trend/breakout/seasonal/
opzioni/funding come rumore): ogni 12h long i perdenti relativi / short i vincenti
su 8 asset, market-neutral. Scorrelata (~0) da pairs e fade -> diversificatore.

- engine canonico scripts/strategies/XS01_cross_sectional.py (no look-ahead, plateau
  OOS Sharpe 2-3.9, 5/5 anni+, edge concentrato 2025, cost-sensitive ~0.35% RT).
- src/live/xsec_worker.py CrossSectionalWorker: validate_xsec_worker == backtest ESATTO
  (4993/1427 trade). Mirror della cadenza engine (entry-to-entry = hold+1).
- gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46 (OOS DD +0.17pp,
  risk-contrib 2.2%). xsec_port06_gate.py.
- wiring: _defs XSEC in PORT06 (19 sleeve, family XSEC), build_everything, runner
  kind=xsec, asset_days da supported (fix fetch alt anche per paper sleeves), paper.
- 8 gambe -> niente exec reale -> gira PAPER. Regression-lock 18->19, FULL 7.20->7.34,
  OOS 9.66->10.07. 93 test verdi. Diario 2026-06-09-xs01-cross-sectional.md.

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

106 lines
4.4 KiB
Python

"""XS01 — Cross-Sectional Reversion (market-neutral su 8 cripto). FAMIGLIA NUOVA.
Distinta dai pairs PR01 (pairwise) e dai fade (single-asset): ogni HOLD ore classifica
gli 8 asset per rendimento su LB ore e va LONG i perdenti relativi / SHORT i vincenti
(peso ∝ -(ret - media_cross-section)), market-neutral gross 1. Cattura il FATTORE
reversione cross-sezionale. Scorrelato (~0) da pairs e fade -> diversificatore.
Engine ONESTO (no look-ahead, verificato): pesi a barra i da close[<=i]; ingresso a
close[i], uscita a close[i+HOLD]; roll NON sovrapposto (riallinea ogni HOLD barre).
Fee = 0.10% RT/book (turnover gross 1 -> 2*fee_rt). PnL su capitale composto (pos, lev).
Validazione (sessione 2026-06-09, lb48 hold12, fee 0.10% RT, OOS ultimo 30%):
FULL Sharpe ~3.3 / OOS ~3.4, plateau lb 12-72 x hold 6-24 (OOS 2-3.9), 4/5 anni+.
Decorrelato (-0.006 da PR01 ETH/BTC). Cost-sensitive: muore ~0.35% RT/book.
Gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46 (OOS DD +0.06pp a mezza size).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.data.downloader import load_data
UNIVERSE = ["BTC", "ETH", "LTC", "ADA", "SOL", "BNB", "XRP", "DOGE"]
FEE_RT, LEV, POS, OOS_FRAC = 0.0005, 3.0, 0.15, 0.30
LB, HOLD = 48, 12
def aligned_panel(assets=UNIVERSE, tf="1h"):
dfs = {a: load_data(a, tf)[["timestamp", "close"]].rename(columns={"close": a}).set_index("timestamp")
for a in assets}
M = pd.concat(dfs.values(), axis=1, join="inner").sort_index()
return M[assets]
def xsec_sim(assets=UNIVERSE, tf="1h", lb=LB, hold=HOLD, fee_rt=FEE_RT, lev=LEV,
pos=POS, split_frac=0.0):
M = aligned_panel(assets, tf)
C = M[assets].values
ts = pd.to_datetime(M.index, unit="ms", utc=True)
n = len(C); logC = np.log(C)
split = int(n * split_frac)
cap = peak = 1000.0; dd = 0.0
trades = wins = 0; rets = []; yearly = {}; yearly_n = {}
eq_ts, eq_v = [], []
last = -1; i = max(lb, split)
fee = 2 * fee_rt # gross 1 -> turnover 2 (entra+esce)
while i < n - hold:
if i <= last:
i += 1; continue
dm = (logC[i] - logC[i - lb]); dm = dm - dm.mean()
w = -dm; gw = np.sum(np.abs(w))
if gw < 1e-9:
i += 1; continue
w = w / gw # market-neutral, gross 1
book = float(np.sum(w * (logC[i + hold] - logC[i])))
net = book - fee
cap = max(cap + cap * pos * lev * net, 10.0)
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
trades += 1; wins += net > 0; rets.append(net * pos); last = i + hold
eq_ts.append(ts[i + hold]); eq_v.append(cap)
yearly[ts[i].year] = yearly.get(ts[i].year, 0.0) + net * 100
yearly_n[ts[i].year] = yearly_n.get(ts[i].year, 0) + 1
i += 1
yrs_span = (ts[-1] - ts[max(split, 0)]).days / 365.25 or 1
sharpe = 0.0
if len(rets) > 1 and np.std(rets) > 0:
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs_span))
ret_tot = (cap / 1000 - 1) * 100
cagr = ((cap / 1000) ** (1 / yrs_span) - 1) * 100 if cap > 0 else -100
return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot,
cagr=cagr, dd=dd * 100, sharpe=sharpe, yearly=yearly, yearly_n=yearly_n,
eq_ts=eq_ts, eq_v=eq_v)
def check_no_lookahead():
M = aligned_panel(); logC = np.log(M.values); i = 1000
a = (logC[i] - logC[i - LB])
Cp = logC.copy(); Cp[i + 1:] += 0.5
b = (Cp[i] - Cp[i - LB])
print(f" no-look-ahead: segnale invariato col futuro perturbato -> "
f"{'OK' if np.allclose(a, b) else 'VIOLAZIONE'}")
def run():
print("=" * 84)
print(" XS01 — Cross-Sectional Reversion (8 asset, market-neutral) | netto fee 0.10% RT/book")
print("=" * 84)
check_no_lookahead()
f = xsec_sim()
o = xsec_sim(split_frac=1 - OOS_FRAC)
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
print(f" trade {f['trades']} | win {f['win']:.1f}% | CAGR {f['cagr']:.0f}% | DD {f['dd']:.0f}% | "
f"Sharpe FULL {f['sharpe']:.2f} / OOS {o['sharpe']:.2f} | anni+ {pos_y}/{len(yrs)}")
print(" per anno:", " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(yrs.items())))
if __name__ == "__main__":
run()