feat(xsec): dispersion-gate XS01 live (disp_min=0.0313) — Sharpe 3.46, PORT06 OOS 10.07->10.37; FC01 funding-carry scartata
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""FC01 — Funding-carry market-neutral (ricerca, 2026-06-10).
|
||||
|
||||
Idea: su Deribit i long pagano gli short quando il funding e' positivo (e
|
||||
viceversa). W12 (scartata) shortava il perp su funding alto = direzionale.
|
||||
Qui il meccanismo NUOVO e' il CARRY NEUTRALE: short della gamba con funding
|
||||
alto / long della gamba con funding basso (BTC vs ETH, dollar-neutral),
|
||||
incassando il DIFFERENZIALE di funding con esposizione residua = solo lo
|
||||
spread ETH/BTC (correlazione ~0.95).
|
||||
|
||||
Dati REALI: data/regime/{btc,eth}_funding.parquet (orario, 2019-12 -> 2026-06,
|
||||
interest_1h effettivo + index_price). Causale: decisione al close t con
|
||||
funding noto fino a t; accrual dal bar t+1; fee 0.10% RT per GAMBA.
|
||||
|
||||
Varianti:
|
||||
FC-A spread-carry 2 gambe (il candidato): entra quando lo spread di funding
|
||||
smussato supera la soglia, esce quando rientra / max_bars.
|
||||
FC-B single-asset carry direzionale (confronto onesto con W12): short se
|
||||
funding smussato > thr, long se < -thr.
|
||||
|
||||
Protocollo: TRAIN fino a OOS_DATE (2023-11-01) per scegliere la config,
|
||||
OOS dopo; griglia robustezza; sweep fee; breakdown annuale.
|
||||
|
||||
uv run python scripts/analysis/funding_carry_research.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))
|
||||
|
||||
FEE_RT = 0.001 # 0.10% RT per gamba (taker, baseline progetto)
|
||||
OOS_DATE = "2023-11-01"
|
||||
HRS_YEAR = 24 * 365
|
||||
|
||||
|
||||
def load_panel():
|
||||
btc = pd.read_parquet("data/regime/btc_funding.parquet")
|
||||
eth = pd.read_parquet("data/regime/eth_funding.parquet")
|
||||
for d in (btc, eth):
|
||||
d["dt"] = pd.to_datetime(d["timestamp"], unit="ms")
|
||||
m = btc.set_index("dt")[["interest_1h", "index_price"]].rename(
|
||||
columns={"interest_1h": "f_btc", "index_price": "p_btc"}).join(
|
||||
eth.set_index("dt")[["interest_1h", "index_price"]].rename(
|
||||
columns={"interest_1h": "f_eth", "index_price": "p_eth"}),
|
||||
how="inner").sort_index()
|
||||
m = m.dropna()
|
||||
return m
|
||||
|
||||
|
||||
def explore(m):
|
||||
print("=" * 96)
|
||||
print(" [0] ESPLORAZIONE — funding orario reale Deribit, "
|
||||
f"{m.index[0].date()} -> {m.index[-1].date()} ({len(m)} ore)")
|
||||
print("=" * 96)
|
||||
for a in ("btc", "eth"):
|
||||
f = m[f"f_{a}"] * HRS_YEAR * 100 # annualizzato %
|
||||
print(f" {a.upper()}: funding annualizzato mean {f.mean():+6.2f}% "
|
||||
f"med {f.median():+6.2f}% p10 {f.quantile(.1):+7.2f}% "
|
||||
f"p90 {f.quantile(.9):+7.2f}% %ore>0 {100*(f>0).mean():.0f}%")
|
||||
sp = (m["f_eth"] - m["f_btc"]) * HRS_YEAR * 100
|
||||
print(f" SPREAD ETH-BTC annualizzato: mean {sp.mean():+6.2f}% "
|
||||
f"p10 {sp.quantile(.1):+7.2f}% p90 {sp.quantile(.9):+7.2f}%")
|
||||
# persistenza: autocorr dello spread smussato 24h a vari lag
|
||||
s24 = (m["f_eth"] - m["f_btc"]).rolling(24).mean()
|
||||
for lag in (24, 72, 168):
|
||||
c = s24.autocorr(lag)
|
||||
print(f" autocorr spread(24h-smooth) lag {lag:>4}h: {c:+.3f}")
|
||||
# quanto duramo sopra soglia? episodi |spread ann| > 10%
|
||||
thr = 0.10 / HRS_YEAR
|
||||
above = (s24.abs() > thr).astype(int)
|
||||
runs = (above.groupby((above != above.shift()).cumsum()).sum())
|
||||
runs = runs[runs > 0]
|
||||
if len(runs):
|
||||
print(f" episodi |spread|>10% ann: {len(runs)} durata mediana "
|
||||
f"{runs.median():.0f}h p90 {runs.quantile(.9):.0f}h")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backtest FC-A: spread-carry 2 gambe
|
||||
# ---------------------------------------------------------------------------
|
||||
def carry_pair(m, smooth=72, thr_ann=10.0, exit_frac=0.0, max_bars=24 * 30,
|
||||
fee_rt=FEE_RT, sl=None):
|
||||
"""Entra quando |spread smussato| > thr (annualizzato %); short la gamba
|
||||
col funding alto, long l'altra, 1x notional per gamba. Esce quando lo
|
||||
spread smussato scende sotto exit_frac*thr (o cambia segno) o max_bars.
|
||||
Ritorna array di net-return per trade + serie equity oraria (additiva)."""
|
||||
f_sp = (m["f_eth"] - m["f_btc"]).rolling(smooth).mean().to_numpy()
|
||||
fe = m["f_eth"].to_numpy()
|
||||
fb = m["f_btc"].to_numpy()
|
||||
pe = m["p_eth"].to_numpy()
|
||||
pb = m["p_btc"].to_numpy()
|
||||
n = len(m)
|
||||
thr = thr_ann / 100 / HRS_YEAR
|
||||
ex = exit_frac * thr
|
||||
sli = m.index[:n] if sl is None else None
|
||||
rets, lens, accs = [], [], []
|
||||
eq = np.zeros(n)
|
||||
i = smooth
|
||||
while i < n - 1:
|
||||
s = f_sp[i]
|
||||
if not np.isfinite(s) or abs(s) <= thr:
|
||||
i += 1
|
||||
continue
|
||||
d = -1 if s > 0 else 1 # s>0: ETH paga di piu' -> short ETH/long BTC
|
||||
e_eth, e_btc = pe[i], pb[i]
|
||||
acc = 0.0
|
||||
j = i + 1
|
||||
end = min(n - 1, i + max_bars)
|
||||
while j <= end:
|
||||
# accrual del funding sull'ora j: short riceve +f, long paga f
|
||||
acc += (-d) * fe[j] + d * fb[j]
|
||||
if abs(f_sp[j]) <= ex or np.sign(f_sp[j]) != np.sign(s):
|
||||
break
|
||||
j += 1
|
||||
j = min(j, end)
|
||||
price_leg = d * (pe[j] - e_eth) / e_eth - d * (pb[j] - e_btc) / e_btc
|
||||
net = price_leg + acc - 2 * fee_rt
|
||||
rets.append(net)
|
||||
lens.append(j - i)
|
||||
accs.append(acc)
|
||||
eq[j] += net
|
||||
i = j + 1
|
||||
rets = np.array(rets)
|
||||
eqs = pd.Series(eq, index=m.index).cumsum()
|
||||
return rets, np.array(lens), np.array(accs), eqs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backtest FC-B: carry direzionale single-asset (confronto/W12 onesto)
|
||||
# ---------------------------------------------------------------------------
|
||||
def carry_single(m, asset="eth", smooth=72, thr_ann=20.0, exit_frac=0.0,
|
||||
max_bars=24 * 30, fee_rt=FEE_RT):
|
||||
f = m[f"f_{asset}"].rolling(smooth).mean().to_numpy()
|
||||
fr = m[f"f_{asset}"].to_numpy()
|
||||
p = m[f"p_{asset}"].to_numpy()
|
||||
n = len(m)
|
||||
thr = thr_ann / 100 / HRS_YEAR
|
||||
ex = exit_frac * thr
|
||||
rets = []
|
||||
i = smooth
|
||||
while i < n - 1:
|
||||
s = f[i]
|
||||
if not np.isfinite(s) or abs(s) <= thr:
|
||||
i += 1
|
||||
continue
|
||||
d = -1 if s > 0 else 1 # funding alto -> short (incassa)
|
||||
e = p[i]
|
||||
acc = 0.0
|
||||
j = i + 1
|
||||
end = min(n - 1, i + max_bars)
|
||||
while j <= end:
|
||||
acc += (-d) * fr[j]
|
||||
if abs(f[j]) <= ex or np.sign(f[j]) != np.sign(s):
|
||||
break
|
||||
j += 1
|
||||
j = min(j, end)
|
||||
net = d * (p[j] - e) / e + acc - fee_rt
|
||||
rets.append(net)
|
||||
i = j + 1
|
||||
return np.array(rets)
|
||||
|
||||
|
||||
def stats(rets, idx_len_hours, label="", lens=None, accs=None):
|
||||
if len(rets) == 0:
|
||||
return f" {label:<28s} 0 trade"
|
||||
yrs = idx_len_hours / HRS_YEAR
|
||||
pnl = rets.sum() * 100
|
||||
win = (rets > 0).mean() * 100
|
||||
tpy = len(rets) / yrs
|
||||
sh = rets.mean() / (rets.std() + 1e-12) * np.sqrt(max(tpy, 1e-9))
|
||||
extra = ""
|
||||
if lens is not None and len(lens):
|
||||
extra = f" | hold med {np.median(lens):.0f}h"
|
||||
if accs is not None and len(accs):
|
||||
extra += f" | carry quota {100*np.sum(accs)/max(np.sum(rets),1e-9):.0f}%"
|
||||
return (f" {label:<28s} {len(rets):>4d} tr | win {win:>4.0f}% | "
|
||||
f"PnL {pnl:>+7.1f}% | {tpy:>5.1f} tr/anno | Sh {sh:>5.2f}{extra}")
|
||||
|
||||
|
||||
def main():
|
||||
m = load_panel()
|
||||
explore(m)
|
||||
cut = m.index.searchsorted(pd.Timestamp(OOS_DATE))
|
||||
mtr, moo = m.iloc[:cut], m.iloc[cut:]
|
||||
print(f"\n TRAIN {m.index[0].date()} -> {OOS_DATE} | OOS -> {m.index[-1].date()}")
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
print(" [1] FC-A spread-carry 2 gambe (fee 0.10% RT x2 gambe) — griglia su TRAIN")
|
||||
print("=" * 96)
|
||||
grid = []
|
||||
for smooth in (24, 72, 168):
|
||||
for thr in (5.0, 10.0, 20.0):
|
||||
r, ln, ac, _ = carry_pair(mtr, smooth=smooth, thr_ann=thr)
|
||||
grid.append((smooth, thr, r))
|
||||
print(stats(r, len(mtr), f"TRAIN s{smooth} thr{thr:.0f}%", ln, ac))
|
||||
|
||||
print("\n Le stesse config in OOS (mai usate per scegliere):")
|
||||
for smooth in (24, 72, 168):
|
||||
for thr in (5.0, 10.0, 20.0):
|
||||
r, ln, ac, _ = carry_pair(moo, smooth=smooth, thr_ann=thr)
|
||||
print(stats(r, len(moo), f"OOS s{smooth} thr{thr:.0f}%", ln, ac))
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
print(" [2] FC-B carry direzionale single-asset (confronto, fee 0.10% RT)")
|
||||
print("=" * 96)
|
||||
for a in ("btc", "eth"):
|
||||
for thr in (10.0, 30.0):
|
||||
rtr = carry_single(mtr, a, thr_ann=thr)
|
||||
roo = carry_single(moo, a, thr_ann=thr)
|
||||
print(stats(rtr, len(mtr), f"TRAIN {a} thr{thr:.0f}%"))
|
||||
print(stats(roo, len(moo), f"OOS {a} thr{thr:.0f}%"))
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
print(" [3] FC-A: sweep fee (config mediana s72 thr10) e breakdown annuale")
|
||||
print("=" * 96)
|
||||
for fee in (0.0005, 0.001, 0.002):
|
||||
r, ln, ac, _ = carry_pair(m, smooth=72, thr_ann=10.0, fee_rt=fee)
|
||||
print(stats(r, len(m), f"FULL fee {fee*100:.2f}% RT/gamba", ln, ac))
|
||||
_, _, _, eq = carry_pair(m, smooth=72, thr_ann=10.0)
|
||||
yr = eq.groupby(eq.index.year).apply(lambda s: (s.iloc[-1] - s.iloc[0]) * 100)
|
||||
print(" annuale (PnL additivo %):",
|
||||
{int(k): round(float(v), 1) for k, v in yr.items()})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,189 @@
|
||||
"""XS01 dispersion-gate — la reversione cross-sectional va accesa solo in certi regimi?
|
||||
|
||||
Motivazione: l'edge XS01 e' concentrato (2025 domina, 2023 debole). Ipotesi da
|
||||
testare: il fattore reversione cross-sezionale paga quando c'e' DISPERSIONE da
|
||||
far rientrare (spread cross-section largo) e/o correlazione media alta (mosse
|
||||
idiosincratiche = rumore che rientra), e perde nei regime-break (dispersione da
|
||||
trend divergente, es. melt-up di un singolo asset).
|
||||
|
||||
Metodo (anti-multiple-testing):
|
||||
[1] DIAGNOSTICA: engine XS01 canonico SENZA gate, registrando per ogni trade
|
||||
il valore di 3 feature di regime alla barra di ENTRY (tutte causali,
|
||||
calcolate dallo stesso panel closes <= i):
|
||||
g_disp = std cross-section del segnale stesso (logC[i]-logC[i-lb])
|
||||
g_corr = correlazione media pairwise 72h (identita' var dell'indice)
|
||||
g_vol = vol realizzata BTC 168h
|
||||
Bucket per quintili (quintili dal TRAIN) -> mean net per bucket,
|
||||
TRAIN e OOS SEPARATI. Si prosegue solo se la relazione e' monotona
|
||||
e con lo stesso segno in entrambe le finestre.
|
||||
[2] GATE: per la feature promossa, sweep soglie (percentili TRAIN
|
||||
30/40/50/60/70) -> TRAIN/OOS Sharpe/PnL/DD vs base. Serve PLATEAU.
|
||||
[3] Solo se [2] regge: gate PORT06 (swap equity sleeve XS01).
|
||||
|
||||
uv run python scripts/analysis/xs01_dispersion_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 scripts.strategies.XS01_cross_sectional import (
|
||||
aligned_panel, UNIVERSE, FEE_RT, LEV, POS, OOS_FRAC, LB, HOLD)
|
||||
|
||||
N_A = len(UNIVERSE)
|
||||
|
||||
|
||||
def build_features(M, lb=LB):
|
||||
"""Feature di regime causali dal panel closes (nessun feed esterno)."""
|
||||
logC = np.log(M.values)
|
||||
r = np.diff(logC, axis=0, prepend=logC[:1]) # ret orari (r[0]=0)
|
||||
R = pd.DataFrame(r, index=M.index)
|
||||
# g_disp: std cross-section del momentum lb (il segnale che fadiamo)
|
||||
D = pd.DataFrame(logC).diff(lb).to_numpy()
|
||||
g_disp = np.nanstd(D, axis=1)
|
||||
# g_corr 72h: avg pairwise corr via identita' della varianza dell'indice
|
||||
w = 72
|
||||
idx_var = R.mean(axis=1).rolling(w).var().to_numpy()
|
||||
mean_var = R.rolling(w).var().mean(axis=1).to_numpy()
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
g_corr = (N_A * idx_var / mean_var - 1) / (N_A - 1)
|
||||
# g_vol: vol BTC 168h annualizzata
|
||||
b = UNIVERSE.index("BTC")
|
||||
g_vol = R[b].rolling(168).std().to_numpy() * np.sqrt(24 * 365)
|
||||
return dict(g_disp=g_disp, g_corr=g_corr, g_vol=g_vol)
|
||||
|
||||
|
||||
def sim_with_trace(M, feats, gate=None, lb=LB, hold=HOLD, fee_rt=FEE_RT,
|
||||
lev=LEV, pos=POS):
|
||||
"""Engine XS01 canonico (stessa logica/ordine di XS01_cross_sectional.xsec_sim)
|
||||
+ trace per-trade (entry index, net, feature) + gate opzionale bool[i]."""
|
||||
C = M.values
|
||||
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
||||
n = len(C)
|
||||
logC = np.log(C)
|
||||
cap = peak = 1000.0
|
||||
dd = 0.0
|
||||
rows = []
|
||||
eq_ts, eq_v = [], []
|
||||
last = -1
|
||||
i = lb
|
||||
fee = 2 * fee_rt
|
||||
while i < n - hold:
|
||||
if i <= last:
|
||||
i += 1
|
||||
continue
|
||||
if gate is not None and not gate[i]:
|
||||
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
|
||||
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)
|
||||
rows.append((i, int(ts[i].year), net,
|
||||
feats["g_disp"][i], feats["g_corr"][i], feats["g_vol"][i]))
|
||||
eq_ts.append(ts[i + hold])
|
||||
eq_v.append(cap)
|
||||
last = i + hold
|
||||
i += 1
|
||||
tr = pd.DataFrame(rows, columns=["i", "year", "net", "g_disp", "g_corr", "g_vol"])
|
||||
yrs_span = (ts[-1] - ts[0]).days / 365.25 or 1
|
||||
out = dict(trades=len(tr), cap=cap, dd=dd * 100, eq_ts=eq_ts, eq_v=eq_v, tr=tr)
|
||||
if len(tr) > 1 and tr["net"].std() > 0:
|
||||
out["sharpe"] = float(tr["net"].mean() / tr["net"].std()
|
||||
* np.sqrt(len(tr) / yrs_span))
|
||||
else:
|
||||
out["sharpe"] = 0.0
|
||||
out["pnl_add"] = float(tr["net"].sum() * 100) if len(tr) else 0.0
|
||||
out["win"] = float((tr["net"] > 0).mean() * 100) if len(tr) else 0.0
|
||||
out["tpm"] = len(tr) / (yrs_span * 12)
|
||||
return out
|
||||
|
||||
|
||||
def metrics_window(tr, lo, hi, yrs_span):
|
||||
t = tr[(tr["i"] >= lo) & (tr["i"] < hi)]
|
||||
if len(t) < 2 or t["net"].std() == 0:
|
||||
return dict(n=len(t), pnl=0.0, sh=0.0, win=0.0)
|
||||
sh = float(t["net"].mean() / t["net"].std() * np.sqrt(len(t) / yrs_span))
|
||||
return dict(n=len(t), pnl=float(t["net"].sum() * 100), sh=sh,
|
||||
win=float((t["net"] > 0).mean() * 100))
|
||||
|
||||
|
||||
def main():
|
||||
M = aligned_panel()
|
||||
n = len(M)
|
||||
cut = int(n * (1 - OOS_FRAC))
|
||||
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
||||
feats = build_features(M)
|
||||
print("=" * 96)
|
||||
print(f" XS01 dispersion-gate | panel {ts[0].date()} -> {ts[-1].date()} "
|
||||
f"({n} ore, 8 asset) | TRAIN 70% (-> {ts[cut].date()}) / OOS 30%")
|
||||
print("=" * 96)
|
||||
|
||||
base = sim_with_trace(M, feats)
|
||||
tr = base["tr"]
|
||||
yrs_tr = (ts[cut] - ts[0]).days / 365.25
|
||||
yrs_oo = (ts[-1] - ts[cut]).days / 365.25
|
||||
|
||||
# [1] DIAGNOSTICA per quintili (quintili dal TRAIN)
|
||||
print("\n[1] DIAGNOSTICA — mean net per trade (bps) per quintile feature @entry")
|
||||
ttr = tr[tr["i"] < cut]
|
||||
too = tr[tr["i"] >= cut]
|
||||
for g in ("g_disp", "g_corr", "g_vol"):
|
||||
qs = ttr[g].quantile([0.2, 0.4, 0.6, 0.8]).to_numpy()
|
||||
def bucket(x):
|
||||
return int(np.searchsorted(qs, x))
|
||||
print(f" {g:<7s} | " + " | ".join(
|
||||
f"Q{q+1} TR {ttr[ttr[g].apply(bucket) == q]['net'].mean()*1e4:+6.1f} "
|
||||
f"OOS {too[too[g].apply(bucket) == q]['net'].mean()*1e4:+6.1f}"
|
||||
for q in range(5)) +
|
||||
f" (n TR {len(ttr)}, OOS {len(too)})")
|
||||
|
||||
# [2] GATE sweep — per ogni feature, tieni SOPRA o SOTTO il percentile
|
||||
print("\n[2] GATE — TRAIN/OOS vs base (soglie = percentili del TRAIN; "
|
||||
"side scelto dal segno della diagnostica TRAIN)")
|
||||
b_tr = metrics_window(tr, 0, cut, yrs_tr)
|
||||
b_oo = metrics_window(tr, cut, n, yrs_oo)
|
||||
print(f" {'BASE':<24s} TRAIN n {b_tr['n']:>4} pnl {b_tr['pnl']:>+7.1f}% "
|
||||
f"Sh {b_tr['sh']:>5.2f} | OOS n {b_oo['n']:>4} pnl {b_oo['pnl']:>+7.1f}% "
|
||||
f"Sh {b_oo['sh']:>5.2f}")
|
||||
for g in ("g_disp", "g_corr", "g_vol"):
|
||||
# segno dal TRAIN: correlazione quintile->ret
|
||||
qs5 = ttr[g].quantile([0.2, 0.4, 0.6, 0.8]).to_numpy()
|
||||
means = [ttr[ttr[g].apply(lambda x: int(np.searchsorted(qs5, x))) == q]["net"].mean()
|
||||
for q in range(5)]
|
||||
side = "above" if means[-1] > means[0] else "below"
|
||||
for pct in (30, 40, 50, 60, 70):
|
||||
thr = float(np.nanpercentile(feats[g][:cut], pct))
|
||||
gv = feats[g]
|
||||
gate = (gv >= thr) if side == "above" else (gv <= thr)
|
||||
gate = np.nan_to_num(gate, nan=False).astype(bool)
|
||||
r = sim_with_trace(M, feats, gate=gate)
|
||||
g_tr = metrics_window(r["tr"], 0, cut, yrs_tr)
|
||||
g_oo = metrics_window(r["tr"], cut, n, yrs_oo)
|
||||
print(f" {g} {side} p{pct:<3d}{'':<6s} TRAIN n {g_tr['n']:>4} "
|
||||
f"pnl {g_tr['pnl']:>+7.1f}% Sh {g_tr['sh']:>5.2f} | "
|
||||
f"OOS n {g_oo['n']:>4} pnl {g_oo['pnl']:>+7.1f}% Sh {g_oo['sh']:>5.2f}")
|
||||
|
||||
# breakdown annuale base (riferimento concentrazione)
|
||||
print("\n base, net additivo per anno (%):",
|
||||
{int(y): round(float(v * 100), 1)
|
||||
for y, v in tr.groupby("year")["net"].sum().items()})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -105,9 +105,14 @@ SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="sha
|
||||
# (~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.
|
||||
# DISPERSION-GATE (2026-06-10): entry solo se std cross-section del momentum lb >= disp_min
|
||||
# (p50 TRAIN = 0.0313). Diagnostica monotona TRAIN+OOS, plateau p30-p70, ogni anno migliora
|
||||
# (standalone Sharpe 2.50->3.46, regge fee 2x), PORT06 OOS Sh 10.07->10.37 a DD pari. Solo
|
||||
# path LIVE (backtest canonico NON filtrato, come trend/hurst sulle fade) -> il live fara'
|
||||
# meglio del backtest. Diario 2026-06-10, gate scripts/analysis/xs01_dispersion_gate.py.
|
||||
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})]
|
||||
"tf": "1h", "lb": 48, "hold": 12, "disp_min": 0.0313})]
|
||||
|
||||
PORTFOLIOS = {
|
||||
"PORT01": Portfolio("PORT01", "Honest", HONEST, weighting="equal"),
|
||||
|
||||
Reference in New Issue
Block a user