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>
This commit is contained in:
@@ -59,6 +59,11 @@ def build_everything():
|
||||
t = tsmom_sim()
|
||||
tsm = {"TSM01": daily_from(t["eq_ts"], t["eq_v"])}
|
||||
shape = {f"SH_{a}": _norm(shape_daily_equity(a, IDX)) for a in ("BTC", "ETH")}
|
||||
# XS01 — reversione cross-sectional (8 asset, market-neutral). Engine canonico
|
||||
# scripts.strategies.XS01_cross_sectional (worker validato == backtest).
|
||||
from scripts.strategies.XS01_cross_sectional import xsec_sim
|
||||
x = xsec_sim()
|
||||
tsm["XS01"] = daily_from(x["eq_ts"], x["eq_v"])
|
||||
return S, pairs, tsm, shape
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Valida il CrossSectionalWorker: replay bar-per-bar == backtest XS01.xsec_sim?
|
||||
|
||||
Come validate_worker_pairs: alimenta il worker con finestre trailing crescenti del
|
||||
pannello 8-asset e confronta capitale finale e n.trade col backtest di riferimento
|
||||
scripts.strategies.XS01_cross_sectional.xsec_sim. Se combaciano, la semantica live e' fedele.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.live.xsec_worker import CrossSectionalWorker
|
||||
from scripts.strategies.XS01_cross_sectional import aligned_panel, xsec_sim, UNIVERSE, LB, HOLD
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 88)
|
||||
print(" VALIDAZIONE CrossSectionalWorker — replay live vs backtest xsec_sim (fee 0.10% RT/book)")
|
||||
print("=" * 88)
|
||||
M = aligned_panel(UNIVERSE)
|
||||
dfs = {a: pd.DataFrame({"timestamp": M.index.values, "close": M[a].values}) for a in UNIVERSE}
|
||||
n = len(M)
|
||||
tmp = Path(tempfile.mkdtemp(prefix="xsec_val_"))
|
||||
try:
|
||||
w = CrossSectionalWorker(UNIVERSE, tf="1h", params={"lb": LB, "hold": HOLD},
|
||||
fee_rt=0.0005, data_dir=tmp)
|
||||
w._save = lambda: None; w._log = lambda *a, **k: None; w._notify = lambda *a, **k: None
|
||||
window = LB + 6
|
||||
for k in range(LB + 1, n + 1): # prima finestra = lb+1 barre -> ingresso al bar lb
|
||||
lo = max(0, k - window)
|
||||
w.tick({a: dfs[a].iloc[lo:k] for a in UNIVERSE})
|
||||
bt = xsec_sim(UNIVERSE)
|
||||
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
|
||||
cap_ok = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
|
||||
trd_ok = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
|
||||
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
|
||||
print(f"\n {'':<6}{'cap':>14}{'trades':>8}{'win%':>7}")
|
||||
print(f" WORKER{w.capital:>14.0f}{w.total_trades:>8d}{ww:>7.1f}")
|
||||
print(f" BCKTST{bt_cap:>14.0f}{bt['trades']:>8d}{bt['win']:>7.1f}")
|
||||
print(f"\n ESITO: {'OK (replay == backtest)' if (cap_ok and trd_ok) else 'DIFF -> INDAGARE'}")
|
||||
print(" (diff minime attese da bar finale aperta / troncamento)")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""GATE PORT06 — XS01 (reversione cross-sectional 8 asset), candidato trovato in sessione.
|
||||
|
||||
XS01: ogni HOLD ore, long i perdenti relativi / short i vincenti su 8 asset (lb LB),
|
||||
market-neutral gross 1, fee 0.10% RT/book. Decorrelato (~0) dai pairs. Domanda: aggiunto
|
||||
a PORT06 migliora Sharpe/DD? (criterio del progetto: OOS Sharpe non peggiora E DD scende.)
|
||||
|
||||
uv run python scripts/analysis/xsec_port06_gate.py
|
||||
"""
|
||||
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
|
||||
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE
|
||||
from scripts.analysis.report_families import daily_from
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
from src.portfolio import weighting as W
|
||||
|
||||
ASSETS = ["BTC", "ETH", "LTC", "ADA", "SOL", "BNB", "XRP", "DOGE"]
|
||||
LB, HOLD, FEE = 48, 12, 0.0005
|
||||
|
||||
|
||||
def xsec_equity(pos=0.15, lev=3.0):
|
||||
dfs = {a: load_data(a, "1h")[["timestamp", "close"]].rename(columns={"close": a}).set_index("timestamp")
|
||||
for a in ASSETS}
|
||||
M = pd.concat(dfs.values(), axis=1, join="inner").sort_index()
|
||||
C = M[ASSETS].values
|
||||
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
||||
n = len(C); logC = np.log(C)
|
||||
cap = 1000.0; eq_ts, eq_v, rets = [], [], []
|
||||
last = -1; i = LB
|
||||
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
|
||||
net = np.sum(w * (logC[i + HOLD] - logC[i])) - FEE * np.sum(np.abs(w)) * 2
|
||||
cap = max(cap + cap * pos * lev * net, 10.0)
|
||||
rets.append(net); eq_ts.append(ts[i + HOLD]); eq_v.append(cap)
|
||||
last = i + HOLD; i += 1
|
||||
return daily_from(eq_ts, eq_v), np.array(rets)
|
||||
|
||||
|
||||
def port_metrics(members, ids, clusters, caps):
|
||||
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||
w = W.weight_vector("cap", ids, dr, caps=caps, clusters=clusters)
|
||||
drp = port_returns({i: members[i] for i in ids}, w)
|
||||
return metrics(drp), metrics(drp, lo=SPLIT), w
|
||||
|
||||
|
||||
def main():
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
print("=" * 92)
|
||||
print(" GATE PORT06 — XS01 reversione cross-sectional (8 asset) | OOS da", OOS_DATE)
|
||||
print("=" * 92)
|
||||
|
||||
for pos, lbl in [(0.15, "XS01 pos0.15"), (0.075, "XS01 pos0.075 (mezza)")]:
|
||||
e, r = xsec_equity(pos=pos)
|
||||
# correlazione con i pairs e i fade
|
||||
cors = {}
|
||||
for ref in ("PR_ETHBTC", "MR02_ETH"):
|
||||
j = pd.concat([e.pct_change(), eq_base[ref].pct_change()], axis=1).dropna()
|
||||
cors[ref] = round(j.iloc[:, 0].corr(j.iloc[:, 1]), 3)
|
||||
ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps
|
||||
f0, o0, _ = port_metrics(eq_base, ids0, cl0, caps)
|
||||
mem = dict(eq_base); mem["XS01"] = e
|
||||
ids1 = ids0 + ["XS01"]; cl1 = dict(cl0); cl1["XS01"] = "xsec"
|
||||
f1, o1, w1 = port_metrics(mem, ids1, cl1, caps)
|
||||
# risk contribution di XS01
|
||||
drm = pd.DataFrame({i: mem[i].pct_change().fillna(0.0) for i in ids1})
|
||||
cov = drm.cov(); wv = np.array([w1[i] for i in ids1])
|
||||
pv = float(wv @ cov.values @ wv)
|
||||
rc = {i: float(w1[i] * (cov.values[k] @ wv) / pv * 100) for k, i in enumerate(ids1)}
|
||||
print(f"\n[{lbl}] corr XS01 vs {cors} | peso XS01 {w1['XS01']*100:.1f}% | "
|
||||
f"risk-contrib XS01 {rc['XS01']:.1f}%")
|
||||
print(f" {'config':<16}{'FULL Sh':>8}{'FULL DD%':>9}{'OOS Sh':>8}{'OOS DD%':>8}")
|
||||
print(f" {'ATTUALE':<16}{f0['sharpe']:>8.2f}{f0['dd']:>9.2f}{o0['sharpe']:>8.2f}{o0['dd']:>8.2f}")
|
||||
print(f" {'+XS01':<16}{f1['sharpe']:>8.2f}{f1['dd']:>9.2f}{o1['sharpe']:>8.2f}{o1['dd']:>8.2f}")
|
||||
ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9
|
||||
and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9)
|
||||
print(f" => {'PROMOSSO' if ok else 'non passa il criterio stretto (vedi numeri)'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -101,6 +101,13 @@ TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend",
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape",
|
||||
params={"last_block_only": True})
|
||||
for a in ("BTC", "ETH")]
|
||||
# XS01 — reversione CROSS-SECTIONAL (8 asset, market-neutral). Famiglia nuova, scorrelata
|
||||
# (~0) da pairs e fade. Gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46.
|
||||
# 8 gambe -> niente esecuzione reale: gira PAPER (come TR01/ROT02/TSM01). Worker validato
|
||||
# (validate_xsec_worker: replay == backtest esatto). Diario 2026-06-09.
|
||||
XSEC = [SleeveSpec(kind="xsec", name="XS01", sid="XS01", cluster="xsec",
|
||||
params={"universe": ["BTC", "ETH", "LTC", "ADA", "SOL", "BNB", "XRP", "DOGE"],
|
||||
"tf": "1h", "lb": 48, "hold": 12})]
|
||||
|
||||
PORTFOLIOS = {
|
||||
"PORT01": Portfolio("PORT01", "Honest", HONEST, weighting="equal"),
|
||||
@@ -115,6 +122,6 @@ PORTFOLIOS = {
|
||||
# che NESSUNO stop taglia la coda ETH senza rompere l'edge -> si dimezza l'esposizione
|
||||
# (costo backtest ~0: FULL 6.47->6.43, OOS 8.82->8.58, FULL DD 4.10->3.96). Vedi
|
||||
# docs/diary/2026-06-05-sh01-sl-research.md.
|
||||
"PORT06": Portfolio("PORT06", "Master + shape", FADE + HONEST + PAIRS + TSM + SHAPE,
|
||||
"PORT06": Portfolio("PORT06", "Master + shape", FADE + HONEST + PAIRS + TSM + SHAPE + XSEC,
|
||||
weighting="cap", caps={"PAIRS": 0.33, "SHAPE": 0.0588}, leverage=2.0),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user