research(xsec): sweep cross-sectional su Hyperliquid (43 script/257 config) + verifica avversariale

Nuova harness condivisa xslib.py (panel HL certificato, score per-asset causale, book
long-k/short-k vol-targeted leak-free) + 43 script in runs/ su 11 famiglie (MOM/REV/VOL/
DIST/LIQ/VAL/STRUCT/UNIV). Scoring = earns_slot (full>0 AND hold-out>0 AND marginal ADDS
al portafoglio live AND corr XS01<0.6, con jackknife drop-one-month).

Find: 42/257 config earns_slot=True, ma TUTTE con corr TP01 -0.2..-0.4 e PnL ~solo 2025.
Verify (verify_survivors.py, 3 scettici deterministici):
 - S1 redundancy: cluster low-vol = UNA scommessa (XV01=XU02=1.00, XV02/XV03 r 0.44-0.67);
   XM09/XL02/XS06b/XR02 distinti (corr media off-diag +0.20).
 - S2 short-beta: cluster low-vol carica 0.44-0.70 su short-market -> NON market-neutral,
   e' un tilt short-alt-beta di regime. XM09(0.08)/XR02(-0.21) NON short-beta.
 - S3 per-anno: cluster low-vol decade (XV01/XU02 2026 -0.09); XL02 morto (2025 -0.14,
   2026 -0.43); XM09 (0.82/0.50/0.74) e XR02 (0.84/0.40/2.68) positivi in tutti e 3 gli anni.

Esito: nessuna sleeve nuova. Cluster low-vol RIGETTATO (regime-bet), XL02 RIGETTATO (overfit).
2 LEAD genuini (XM09 trend-gated x-sec momentum, XR02 reversal vol-gated) -> forward-monitor,
non deployabili (panel 2.5y regime unico + STAT-MODE esecuzione). Portafoglio live invariato.

Incluso anche options_vrp_managed.py (A/B VRP01 hold-to-expiry vs gestione attiva del doc
credit-spread): la gestione attiva DISTRUGGE l'edge (combo FULL managed Sh -1.29 vs HtE +0.96,
il delta-exit taglia i vincenti) -> scartata, VRP01 resta hold-to-expiry.

Diari: 2026-06-20-xsec-strategies-sweep.md, 2026-06-20-vrp-active-management.md.
gitignore: data/paper_portfolio/ (stato runtime paper) + scripts/research/xsec/runs/out/ (output rigenerabile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-20 21:36:57 +00:00
parent 5ac4e16af8
commit 9612560479
50 changed files with 5457 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
"""VRP01 + GESTIONE ATTIVA (test del doc 'strategia-credit-spread-eth', 2026-06-20).
Innesta sul put credit spread di VRP01 le regole di gestione intra-trade del documento:
- profit-take 50% del credito
- stop-loss stretto 1.5x il credito (debito di chiusura)
- VOL-STOP: chiudi se DVOL sale >=10 punti dall'apertura (regola crypto-specifica, NUOVA)
- delta-exit: chiudi se |delta| dello short put >= 0.30 (niente rolling/difesa)
- time-stop 7 DTE
Confronto A/B ONESTO sugli STESSI ingressi gated (VRP>0 + IV-rank>0.30) e dati certificati:
BASE = hold-to-expiry (come VRP01) vs MANAGED = stesso trade con la gestione attiva.
Il MTM giornaliero dello spread usa BS sul path certificato + DVOL reale (causale: decisione al
giorno j con dati <= j). CAVEAT invariato: premio MODELLATO su DVOL ATM (no skew), nessun fill di
stress reale -> LEAD, non deploy. Qui misuriamo solo SE la gestione attiva taglia la coda.
uv run python scripts/research/options_vrp_managed.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from src.data.downloader import load_data
from src.strategies.trend_portfolio import resample_1d
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import _bs_put, _strike_from_delta, VRP_CFG, _HL_DIR
CFG = dict(VRP_CFG) # short_delta -0.28, long_delta -0.10, f 1.0, gate_ivr 0.30, crash_skip 0.90, fee_frac 0.125
def _put_delta_mag(S, K, T, sig):
if T <= 0 or sig <= 0:
return 1.0 if S < K else 0.0
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
return float(norm.cdf(-d1)) # |delta| dello short put (=N(-d1))
def simulate(asset: str, tenor_d: int, mode: str = "hte"):
"""mode: 'hte' hold-to-expiry | 'full' tutte le regole | 'volstop' solo vol-stop DVOL+10 (+PT50).
Ritorna (serie rendimenti per-trade indicizzata alla data di uscita, dict conteggio exit)."""
manage = mode != "hte"
full = mode == "full"
df = resample_1d(load_data(asset, "1h"))
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
px = J["px"].values
dvf = J["dvol"].values / 100.0
idx = J.index
n = len(px)
tn = tenor_d
f, fee = CFG["f"], CFG["fee_frac"]
rets, exits = {}, {}
i = 60
while i + tn < n:
S0, sig0 = px[i], dvf[i]
# --- gates d'ingresso identici a VRP01 (causali) ---
skip = False
if i >= 31:
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
if (sig0 - rv) <= 0: # VRP>0
skip = True
if not skip and i >= 60:
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale
if ivr < CFG["gate_ivr"] or ivr > CFG["crash_skip"]:
skip = True
if skip:
i += tn
continue
T0 = tn / 365.25
Ks = _strike_from_delta(S0, T0, sig0, CFG["short_delta"])
Kl = _strike_from_delta(S0, T0, sig0, CFG["long_delta"])
net_prem = (_bs_put(S0, Ks, T0, sig0) - _bs_put(S0, Kl, T0, sig0)) * f
if net_prem <= 0:
i += tn
continue
reason, pnl, exit_j = None, None, i + tn
if manage:
for j in range(i + 1, i + tn): # giorni STRETTAMENTE prima della scadenza
Trem = (i + tn - j) / 365.25
Sj, sigj = px[j], dvf[j]
sval = _bs_put(Sj, Ks, Trem, sigj) - _bs_put(Sj, Kl, Trem, sigj) # MTM dello spread
if sval <= 0.5 * net_prem:
reason, pnl, exit_j = "PT50", net_prem - sval, j; break
if (sigj - sig0) >= 0.10: # VOL-STOP (la regola crypto nuova del doc)
reason, pnl, exit_j = "VOLSTOP", net_prem - sval, j; break
if full and sval >= 1.5 * net_prem:
reason, pnl, exit_j = "SL150", net_prem - sval, j; break
if full and _put_delta_mag(Sj, Ks, Trem, sigj) >= 0.30:
reason, pnl, exit_j = "DELTA", net_prem - sval, j; break
if full and (i + tn - j) <= 7:
reason, pnl, exit_j = "TIME7", net_prem - sval, j; break
if reason is None: # scadenza
S1 = px[i + tn]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
pnl, reason, exit_j = net_prem - payoff, "expiry", i + tn
pnl -= fee * abs(net_prem) # fee d'ingresso (su entrambe le gambe via net_prem)
if reason != "expiry":
pnl -= fee * abs(net_prem) # fee di chiusura anticipata (ricompro lo spread)
rets[idx[exit_j]] = pnl / Ks
exits[reason] = exits.get(reason, 0) + 1
i += tn
return pd.Series(rets).sort_index(), exits
def daily(series):
if series.empty:
return series
days = pd.date_range(series.index.min().normalize(), series.index.max().normalize(), freq="1D", tz="UTC")
out = pd.Series(0.0, index=days)
out.loc[series.index.normalize()] = series.values
return out
def report(label, perTrade):
dl = to_daily(daily(perTrade))
m = metrics(dl)
mh = metrics(dl[dl.index >= HOLDOUT])
wins = float((perTrade > 0).mean()) * 100
worst = float(perTrade.min()) * 100
print(f" {label:<22s} n={len(perTrade):>3d} win={wins:>4.0f}% ret={m['ret']*100:>+6.0f}% "
f"Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% HOLD Sh={mh['sharpe']:>+5.2f} "
f"worst-trade={worst:>+5.1f}%")
return dl
def main():
print("=" * 100)
print(" VRP01 hold-to-expiry vs GESTIONE ATTIVA (vol-stop DVOL+10, SL 1.5x, PT50, delta-exit, 7DTE)")
print(" Stessi ingressi gated (VRP>0 + IV-rank>0.30), dati certificati, premio MODELLATO su DVOL (no skew)")
print("=" * 100)
combos = {}
for asset in ("ETH", "BTC"):
print(f"\n--- {asset} ---")
report("VRP01 live (7d HtE)", simulate(asset, 7, "hte")[0]) # riferimento live
# confronto equo a tenor 14 (range del doc), STESSI ingressi
b14, _ = simulate(asset, 14, "hte")
v14, exv = simulate(asset, 14, "volstop") # SOLO vol-stop (la regola nuova)
m14, exm = simulate(asset, 14, "full") # tutte le regole del doc
report("14d hold-to-expiry", b14)
report("14d +vol-stop only", v14); print(f" exit volstop: {exv}")
report("14d FULL managed", m14); print(f" exit full: {exm}")
combos[asset] = dict(base14=daily(b14), vol14=daily(v14), man14=daily(m14))
# combo 50/50 BTC+ETH (come lo sleeve VRP01) — il confronto che conta per il portafoglio
print("\n--- COMBO 50/50 BTC+ETH (sleeve-level) ---")
for tag, key in (("14d hold-to-expiry", "base14"), ("14d +vol-stop only", "vol14"), ("14d FULL managed", "man14")):
J = pd.concat({"B": combos["BTC"][key], "E": combos["ETH"][key]}, axis=1, join="outer").fillna(0.0)
combo = to_daily(0.5 * J["B"] + 0.5 * J["E"])
m, mh = metrics(combo), metrics(combo[combo.index >= HOLDOUT])
print(f" {tag:<22s} Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% ret={m['ret']*100:>+6.0f}% "
f"HOLD Sh={mh['sharpe']:>+5.2f}")
print("\n Lettura: la gestione attiva VALE se taglia maxDD e worst-trade SENZA distruggere Sharpe/ritorno.")
print(" Caveat invariato: premio modellato su DVOL ATM (no skew) + nessun fill di stress reale -> LEAD, non deploy.")
if __name__ == "__main__":
main()
+109
View File
@@ -0,0 +1,109 @@
"""XD01 — Low-skew / anti-lottery cross-sectional strategy.
Score = -roll_skew(ret, 60): short high-skew "lottery" alts, long low-skew alts.
Rationale: lottery-preference premium — investors overpay for positive-skew assets
(right-tail lottery tickets), so they should earn lower returns; negative-skew assets
are underpriced relative to their systematic risk.
Grid (<=5 calls):
1. Baseline: "majors" (19 XS01 universe), H=10, k=5, L/S
2. Wider universe: "all" (~49 alts), H=10, k=5, L/S
3. Vary rebalance period: "all", H=5, k=5, L/S (more frequent)
4. Vary top-k: "all", H=10, k=7, L/S (more diversified)
5. Combined: -skew60 + -skew30 blend (multi-horizon), "all", H=10, k=5, L/S
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
SKEW_WIN = 60 # lookback for rolling skew (days)
SKEW_WIN2 = 30 # shorter lookback for blend
def score_anti_lottery(P, win=SKEW_WIN):
"""Anti-lottery score: negate rolling skew so LOW-skew assets score HIGH (long)."""
sk = xs.roll_skew(P.ret, win) # (n_days x n_assets); higher skew = lottery
return -sk # higher = lower skew = long
def score_anti_lottery_blend(P, w1=SKEW_WIN, w2=SKEW_WIN2):
"""Multi-horizon blend of negated skews (cross-sectionally z-scored before blend)."""
sk1 = xs.xs_zscore(-xs.roll_skew(P.ret, w1))
sk2 = xs.xs_zscore(-xs.roll_skew(P.ret, w2))
return 0.5 * sk1 + 0.5 * sk2
if __name__ == "__main__":
results = []
# --- Run 1: majors universe, H=10, k=5, L/S ---
print("Running XD01-v1: majors, H=10, k=5, L/S ...")
rep1 = xs.study_xs(
"XD01-v1-majors",
lambda P: score_anti_lottery(P, 60),
universe="majors",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep1))
results.append(rep1)
# --- Run 2: all universe, H=10, k=5, L/S ---
print("\nRunning XD01-v2: all, H=10, k=5, L/S ...")
rep2 = xs.study_xs(
"XD01-v2-all",
lambda P: score_anti_lottery(P, 60),
universe="all",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep2))
results.append(rep2)
# --- Run 3: all, H=5 (more frequent rebalance), k=5, L/S ---
print("\nRunning XD01-v3: all, H=5, k=5, L/S ...")
rep3 = xs.study_xs(
"XD01-v3-H5",
lambda P: score_anti_lottery(P, 60),
universe="all",
H=5, k=5, long_short=True,
)
print(xs.fmt(rep3))
results.append(rep3)
# --- Run 4: all, H=10, k=7, L/S (more diversified) ---
print("\nRunning XD01-v4: all, H=10, k=7, L/S ...")
rep4 = xs.study_xs(
"XD01-v4-k7",
lambda P: score_anti_lottery(P, 60),
universe="all",
H=10, k=7, long_short=True,
)
print(xs.fmt(rep4))
results.append(rep4)
# --- Run 5: blend multi-horizon skew, all, H=10, k=5, L/S ---
print("\nRunning XD01-v5: blend skew30+60, all, H=10, k=5, L/S ...")
rep5 = xs.study_xs(
"XD01-v5-blend",
lambda P: score_anti_lottery_blend(P, 60, 30),
universe="all",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep5))
results.append(rep5)
# --- Pick best config by: earns_slot > holdout sharpe > full sharpe > distinctness ---
def rank_key(r):
earns = int(r["earns_slot"])
h_sh = r["holdout"].get("sharpe", -99)
f_sh = r["full"]["sharpe"]
distinct = 1.0 - abs(r["corr_xs01"] or 1.0) # higher = more distinct
verdict_score = {"ADDS": 3, "NEUTRAL": 2, "DILUTES": 1, "REDUNDANT": 0, "N/A": 0}.get(
r["marginal"].get("verdict", "N/A"), 0)
return (earns, verdict_score, h_sh, f_sh, distinct)
best = max(results, key=rank_key)
print("\n" + "=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+80
View File
@@ -0,0 +1,80 @@
"""XD02 — High-skew momentum (POSITIVE sign).
Mechanism: Score = +roll_skew(ret, 60).
Idea: positive skew = right-tailed distribution = asset had big up-moves.
Does positive skew predict cross-sectional outperformance in crypto alts?
(XD01 tested negative skew; this tests the opposite hypothesis.)
Grid (<= 5 runs):
1. majors, H=10, k=5, LS (baseline)
2. all, H=10, k=5, LS (wider universe)
3. majors, H=5, k=5, LS (faster rebalance)
4. majors, H=10, k=5, LS, win=30 (shorter lookback)
5. majors, H=10, k=3, LS (concentrated book)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# Score: positive rolling skewness of daily returns
# Higher skew -> more right-tailed -> long this asset
def score_skew(P, win=60):
return xs.roll_skew(P.ret, win)
print("=" * 60)
print("XD02 — HIGH-SKEW MOMENTUM (positive sign, does positive skew pay?)")
print("=" * 60)
# Run 1: majors, H=10, k=5, LS, win=60
r1 = xs.study_xs("XD02-MJ-H10-k5-w60", lambda P: score_skew(P, 60),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
print()
# Run 2: all universe, H=10, k=5, LS, win=60
r2 = xs.study_xs("XD02-ALL-H10-k5-w60", lambda P: score_skew(P, 60),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
print()
# Run 3: majors, H=5, k=5, LS, win=60 (faster rebalance)
r3 = xs.study_xs("XD02-MJ-H5-k5-w60", lambda P: score_skew(P, 60),
universe="majors", H=5, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
print()
# Run 4: majors, H=10, k=5, LS, win=30 (shorter lookback)
r4 = xs.study_xs("XD02-MJ-H10-k5-w30", lambda P: score_skew(P, 30),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
print()
# Run 5: majors, H=10, k=3, LS, win=60 (concentrated)
r5 = xs.study_xs("XD02-MJ-H10-k3-w60", lambda P: score_skew(P, 60),
universe="majors", H=10, k=3, long_short=True)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
print()
# Select best by: earns_slot > holdout sharpe > corr_xs01 (lower is better)
results = [r1, r2, r3, r4, r5]
earners = [r for r in results if r["earns_slot"]]
if earners:
best = max(earners, key=lambda r: r["holdout"].get("sharpe", 0))
else:
# fallback: highest holdout + positive full, then lowest xs01 corr
pos = [r for r in results if r["full"]["sharpe"] > 0 and r["holdout"].get("sharpe", 0) > 0]
if pos:
best = max(pos, key=lambda r: r["holdout"].get("sharpe", 0) - abs(r.get("corr_xs01") or 0))
else:
best = max(results, key=lambda r: r["holdout"].get("sharpe", -99))
print("=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+136
View File
@@ -0,0 +1,136 @@
"""XD03 — Coskewness with Market
Mechanism: For each asset, compute rolling coskewness of asset returns
with the equal-weight market return. Assets with LOW coskewness (they do
not co-skew positively with the market) tend to earn a premium because
investors disfavor assets with negative coskewness (they hurt in crashes
when skewness matters most). Classic Harvey & Siddique (2000) anomaly.
Coskew(i, M) = E[(r_i - mu_i)(r_M - mu_M)^2] / (sigma_i * sigma_M^2)
Causally computed. LOWER coskew = LONG signal.
Grid: 5 backtests varying (win, H, k, universe, long_short).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
import pandas as pd
def coskew_score(ret: np.ndarray, win: int = 60) -> np.ndarray:
"""Rolling coskewness of each asset with the equal-weight market.
coskew(i, M) = E[(r_i - mu_i)(r_M - mu_M)^2] / (std_i * std_M^2)
Returns (n_days x n_assets). LOWER = should be LONG (earns premium).
So for long-low, negate: score = -coskew
"""
n, A = ret.shape
mkt = xs.market_ret(ret) # (n,)
out = np.full((n, A), np.nan)
# Use pandas rolling for causality
mkt_s = pd.Series(mkt)
for a in range(A):
asset_s = pd.Series(ret[:, a])
# Rolling window stats
mu_a = asset_s.rolling(win, min_periods=max(10, win // 3)).mean()
mu_m = mkt_s.rolling(win, min_periods=max(10, win // 3)).mean()
std_a = asset_s.rolling(win, min_periods=max(10, win // 3)).std()
std_m = mkt_s.rolling(win, min_periods=max(10, win // 3)).std()
# Centered series (element-wise)
da = asset_s - mu_a
dm = mkt_s - mu_m
# coskew numerator = mean(da * dm^2)
coskew_num = (da * dm ** 2).rolling(win, min_periods=max(10, win // 3)).mean()
# Normalize by std_a * std_m^2
denom = std_a * std_m ** 2
denom = denom.replace(0, np.nan)
coskew = coskew_num / denom
out[:, a] = coskew.values
return out
def score_fn_60(P):
"""Long low-coskew: negate so that lower coskew = higher score."""
return -coskew_score(P.ret, win=60)
def score_fn_90(P):
"""Longer lookback for coskewness."""
return -coskew_score(P.ret, win=90)
def score_fn_30(P):
"""Shorter lookback — more reactive."""
return -coskew_score(P.ret, win=30)
if __name__ == "__main__":
print("=== XD03: Coskewness with Market ===\n")
results = []
# Run 1: baseline config (win=60, all, H=10, k=5, LS)
print("Run 1/5: win=60, universe=all, H=10, k=5, long_short=True")
r1 = xs.study_xs("XD03-w60-H10-k5-LS", score_fn_60,
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# Run 2: vary rebalance period (H=20, looser)
print("\nRun 2/5: win=60, universe=all, H=20, k=5, long_short=True")
r2 = xs.study_xs("XD03-w60-H20-k5-LS", score_fn_60,
universe="all", H=20, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# Run 3: longer win=90 (more stable coskewness estimate)
print("\nRun 3/5: win=90, universe=all, H=10, k=5, long_short=True")
r3 = xs.study_xs("XD03-w90-H10-k5-LS", score_fn_90,
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# Run 4: majors only (19 assets, cleaner signal)
print("\nRun 4/5: win=60, universe=majors, H=10, k=5, long_short=True")
r4 = xs.study_xs("XD03-w60-H10-k5-LS-maj", score_fn_60,
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# Run 5: long-only on majors (captures risk-premium differently)
print("\nRun 5/5: win=60, universe=majors, H=10, k=5, long_only")
r5 = xs.study_xs("XD03-w60-H10-k5-LO-maj", score_fn_60,
universe="majors", H=10, k=5, long_short=False)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# Summary: pick best by (earns_slot, then hold-out sharpe, then full sharpe)
def rank_key(r):
es = 1 if r["earns_slot"] else 0
hs = r["holdout"].get("sharpe", -99)
fs = r["full"]["sharpe"]
corr_ok = (r.get("corr_xs01") or 1.0) < 0.6
return (es, int(corr_ok), hs, fs)
best = max(results, key=rank_key)
print("\n\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+114
View File
@@ -0,0 +1,114 @@
"""XL01 — Amihud Illiquidity Premium (cross-sectional).
Score = rolling mean of |ret| / (close * volume) over W days (Amihud ratio).
Higher score = more illiquid.
We test both signs:
- Long illiquid (higher score = long): illiquidity premium hypothesis
- Short illiquid (higher score = short): liquidity premium, more liquid = better
Grid (<=5 calls):
1. LS W=30, all universe
2. LS W=30, majors
3. LS W=30, short sign (liquidity premium, flip sign)
4. LS W=30, H=20 (slower rebal), all universe
5. LS W=60, all universe
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def amihud_score(close, vol, ret, W=30):
"""Amihud illiquidity ratio: mean(|ret| / (close * volume)) over W days.
Higher = more illiquid.
Values at bar i use data <= i (causal).
"""
# dollar volume = close * volume (notional traded)
dollar_vol = close * vol # (n, A)
# |return| / dollar_vol
abs_ret = np.abs(ret) # (n, A)
# avoid division by zero
dv_safe = np.where(dollar_vol > 0, dollar_vol, np.nan)
amihud_raw = abs_ret / dv_safe # (n, A)
# rolling mean (causal)
score = xs.roll_mean(amihud_raw, W)
return score
def score_illiquid(W=30):
"""Long illiquid (high Amihud = illiquid -> buy)."""
def fn(P):
return amihud_score(P.close, P.vol, P.ret, W=W)
return fn
def score_liquid(W=30):
"""Long liquid (flip sign: low Amihud = liquid -> buy)."""
def fn(P):
return -amihud_score(P.close, P.vol, P.ret, W=W)
return fn
if __name__ == "__main__":
print("XL01 — Amihud Illiquidity Premium")
print("="*60)
# 1. Baseline: long illiquid, W=30, all universe
print("\n[1] Long ILLIQUID, W=30, universe=all, H=10, k=5, LS")
r1 = xs.study_xs("XL01-ILL-30-all", score_illiquid(30),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
# 2. Long illiquid, W=30, majors only
print("\n[2] Long ILLIQUID, W=30, universe=majors, H=10, k=5, LS")
r2 = xs.study_xs("XL01-ILL-30-maj", score_illiquid(30),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
# 3. Long LIQUID (flip sign), W=30, all universe
print("\n[3] Long LIQUID (flip sign), W=30, universe=all, H=10, k=5, LS")
r3 = xs.study_xs("XL01-LIQ-30-all", score_liquid(30),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
# 4. Long illiquid, W=30, H=20 (slower rebal), all
print("\n[4] Long ILLIQUID, W=30, universe=all, H=20, k=5, LS")
r4 = xs.study_xs("XL01-ILL-30-H20", score_illiquid(30),
universe="all", H=20, k=5, long_short=True)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
# 5. Long illiquid, W=60, all universe
print("\n[5] Long ILLIQUID, W=60, universe=all, H=10, k=5, LS")
r5 = xs.study_xs("XL01-ILL-60-all", score_illiquid(60),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
# Summary
results = [r1, r2, r3, r4, r5]
print("\n" + "="*60)
print("SUMMARY — pick best by: earns_slot > holdout > distinctness")
for r in results:
es = r["earns_slot"]
fsh = r["full"]["sharpe"]
hsh = r["holdout"].get("sharpe", 0)
cxs = r["corr_xs01"]
v = r["marginal"]["verdict"]
print(f" {r['name']:30s} FULL={fsh:+.2f} HOLD={hsh:+.2f} corr_xs01={cxs} "
f"verdict={v} earns_slot={es}")
# Pick best: prefer earns_slot, then hold sharpe
best = max(results, key=lambda r: (
r["earns_slot"],
r["holdout"].get("sharpe", -99),
r["full"]["sharpe"],
))
print(f"\nBEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("BEST JSON:", xs.as_json(best))
+118
View File
@@ -0,0 +1,118 @@
"""XL02 [LIQ] — Volume-trend momentum
IDEA: Score = volume_z(vol, 30) combined with positive return (rising-volume winners).
Assets with above-average volume AND positive momentum rank highest.
Assets with above-average volume AND negative momentum rank lowest (i.e., short).
Mechanism intuition:
- Volume surge signals conviction / participation.
- When paired with rising price (trend direction) it confirms breakout.
- When paired with falling price it confirms distribution / breakdown.
- Pure volume without price direction is ambiguous (could be capitulation or breakout).
Score variants explored (<=5 total):
1. vol_z(30) * ret(10) -- product: vol-amplified short-term return
2. vol_z(30) * ret(30) -- product: vol-amplified medium return
3. blend: 0.5*xs_z(vol_z*ret10) + 0.5*xs_z(ret30) -- add momentum anchor
4. Same blend but long-only (avoid short vol-breakdown which may just be panic)
5. vol_z(60) * ret(20) -- wider lookback, majors universe
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ── Score factory ────────────────────────────────────────────────────────────
def score_vol_trend(P, vol_win=30, ret_win=10):
"""product: volume_z * past_return — higher = rising on high volume"""
vz = xs.volume_z(P.vol, vol_win) # (n, A) causal
rr = xs.past_return(P.close, ret_win) # (n, A) causal
score = vz * rr
return score
def score_vol_trend_blend(P, vol_win=30, ret_win_short=10, ret_win_long=30, w_blend=0.5):
"""Blend vol*ret with standalone momentum to add a stable anchor"""
vz = xs.volume_z(P.vol, vol_win)
rr_short = xs.past_return(P.close, ret_win_short)
rr_long = xs.past_return(P.close, ret_win_long)
signal1 = xs.xs_zscore(vz * rr_short)
signal2 = xs.xs_zscore(rr_long)
return w_blend * signal1 + (1 - w_blend) * signal2
# ── Grid (5 calls) ───────────────────────────────────────────────────────────
results = []
# 1. vol_z(30) * ret(10) — LS, all universe
r1 = xs.study_xs(
"XL02-vz30r10",
lambda P: score_vol_trend(P, vol_win=30, ret_win=10),
universe="all", H=10, k=5, long_short=True,
)
results.append(r1)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
print()
# 2. vol_z(30) * ret(30) — LS, all universe
r2 = xs.study_xs(
"XL02-vz30r30",
lambda P: score_vol_trend(P, vol_win=30, ret_win=30),
universe="all", H=10, k=5, long_short=True,
)
results.append(r2)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
print()
# 3. blend: vol*ret(10) + mom(30) — LS, all universe
r3 = xs.study_xs(
"XL02-blend",
lambda P: score_vol_trend_blend(P, vol_win=30, ret_win_short=10, ret_win_long=30, w_blend=0.5),
universe="all", H=10, k=5, long_short=True,
)
results.append(r3)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
print()
# 4. blend long-only (avoid shorting high-vol breakdowns)
r4 = xs.study_xs(
"XL02-blend-LO",
lambda P: score_vol_trend_blend(P, vol_win=30, ret_win_short=10, ret_win_long=30, w_blend=0.5),
universe="all", H=10, k=5, long_short=False,
)
results.append(r4)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
print()
# 5. vol_z(60) * ret(20) — majors universe, tighter
r5 = xs.study_xs(
"XL02-vz60r20-maj",
lambda P: score_vol_trend(P, vol_win=60, ret_win=20),
universe="majors", H=10, k=5, long_short=True,
)
results.append(r5)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
print()
# ── Pick best ────────────────────────────────────────────────────────────────
def score_result(r):
"""Higher is better: prefer earns_slot, then hold-out, then full."""
m = r["marginal"]
earns = int(r["earns_slot"]) * 10
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"]["sharpe"]
distinct = 1 if (r["corr_xs01"] or 1.0) < 0.6 else 0
return earns + distinct + hold_sh + 0.3 * full_sh
best = max(results, key=score_result)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+93
View File
@@ -0,0 +1,93 @@
"""XL03 [LIQ] — Low-turnover anomaly.
Score = -roll_mean(close * volume, 30) : long low dollar-volume names.
Idea: low-liquidity assets carry a liquidity premium and may outperform
high-liquidity names on a risk-adjusted basis.
Grid (<=5 runs):
1. baseline: universe=all, H=10, k=5, long_short=True, win=30
2. shorter window win=10 (faster signal)
3. longer window win=60 (more stable ranking)
4. long-only version (long low-liq only, no shorting high-liq names)
5. majors universe (check if effect holds in liquid-only subspace)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# --- score factory -----------------------------------------------------------
def liq_score(P, win=30):
"""Score = -roll_mean(close * dollar_vol, win).
CAUSAL: roll_mean at row i uses data[i-win+1..i].
Higher score = LOWER liquidity = LONG.
"""
dollar_vol = P.close * P.vol # (n, A) daily dollar volume
avg_dvol = xs.roll_mean(dollar_vol, win) # rolling mean, causal
return -avg_dvol # negate: lower dvol -> higher score -> long
# --- grid -------------------------------------------------------------------
print("=" * 70)
print("XL03 [LIQ] Low-turnover anomaly — grid search")
print("=" * 70)
results = []
# Run 1: baseline (all, H=10, k=5, LS, win=30)
r1 = xs.study_xs("XL03-w30-all-LS",
lambda P: liq_score(P, 30),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# Run 2: shorter window win=10
r2 = xs.study_xs("XL03-w10-all-LS",
lambda P: liq_score(P, 10),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# Run 3: longer window win=60
r3 = xs.study_xs("XL03-w60-all-LS",
lambda P: liq_score(P, 60),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# Run 4: long-only (long low-liq, no short)
r4 = xs.study_xs("XL03-w30-all-LO",
lambda P: liq_score(P, 30),
universe="all", H=10, k=5, long_short=False)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# Run 5: majors universe only
r5 = xs.study_xs("XL03-w30-majors-LS",
lambda P: liq_score(P, 30),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# --- pick best config -------------------------------------------------------
print("\n" + "=" * 70)
print("SUMMARY — best by: earns_slot first, then hold-out Sharpe, then corr_xs01 < 0.6")
print("=" * 70)
def rank_key(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9) or -9
xs01_corr = abs(r.get("corr_xs01") or 1.0)
return (earns, hold_sh, -xs01_corr)
best = max(results, key=rank_key)
print(f"\nBEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("\nJSON (best):", xs.as_json(best))
+109
View File
@@ -0,0 +1,109 @@
"""XL04 [LIQ] — Dollar-volume momentum.
Score = past_return of dollar-volume (close * volume) over W=30 days.
Idea: assets gaining LIQUIDITY / ATTENTION relative to peers will outperform.
This is the OPPOSITE of XL03 (which went long LOW dollar-volume names).
Mechanism:
dvol[i] = close[i] * vol[i] (daily dollar volume)
score[i] = dvol[i] / dvol[i-W] - 1 (W-day return of dollar volume)
-> long assets whose dollar volume is GROWING the fastest
Grid (<=5 runs):
1. baseline: universe=all, H=10, k=5, long_short=True, W=30
2. shorter window W=10 (faster attention signal)
3. longer window W=60 (more stable)
4. majors universe (19 XS01 assets — check distinctness from XS01)
5. long-only version (long attention gainers, no shorting attention losers)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# --- score factory -----------------------------------------------------------
def dvol_momentum_score(P, W=30):
"""Score = W-day past return of dollar volume (close * volume).
CAUSAL: dvol_return[i] uses dvol[i] / dvol[i-W] - 1.
Higher score = dollar volume growing faster = LONG.
"""
dvol = P.close * P.vol # (n, A) daily dollar volume
score = np.full_like(dvol, np.nan)
# past_return style: score[i] = dvol[i] / dvol[i-W] - 1
# guard: if dvol[i-W] == 0 -> NaN
denom = dvol[:-W] # dvol[i-W]
numer = dvol[W:] # dvol[i]
with np.errstate(invalid="ignore", divide="ignore"):
ratio = np.where(denom > 0, numer / denom - 1.0, np.nan)
score[W:] = ratio
return score
# --- grid -------------------------------------------------------------------
print("=" * 70)
print("XL04 [LIQ] Dollar-volume momentum — grid search")
print("=" * 70)
results = []
# Run 1: baseline (all, H=10, k=5, LS, W=30)
r1 = xs.study_xs("XL04-W30-all-LS",
lambda P: dvol_momentum_score(P, 30),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# Run 2: shorter window W=10 (faster attention surge)
r2 = xs.study_xs("XL04-W10-all-LS",
lambda P: dvol_momentum_score(P, 10),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# Run 3: longer window W=60 (sustained attention)
r3 = xs.study_xs("XL04-W60-all-LS",
lambda P: dvol_momentum_score(P, 60),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# Run 4: majors universe only (19 XS01 assets)
r4 = xs.study_xs("XL04-W30-majors-LS",
lambda P: dvol_momentum_score(P, 30),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# Run 5: long-only (attention gainers only, no shorting losers)
r5 = xs.study_xs("XL04-W30-all-LO",
lambda P: dvol_momentum_score(P, 30),
universe="all", H=10, k=5, long_short=False)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# --- pick best config -------------------------------------------------------
print("\n" + "=" * 70)
print("SUMMARY — best by: earns_slot first, then hold-out Sharpe, then distinctness from XS01")
print("=" * 70)
def rank_key(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9) or -9
xs01_corr = abs(r.get("corr_xs01") or 1.0)
full_sh = r["full"].get("sharpe", -9) or -9
return (earns, hold_sh, full_sh, -xs01_corr)
best = max(results, key=rank_key)
print(f"\nBEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("\nJSON (best):", xs.as_json(best))
+82
View File
@@ -0,0 +1,82 @@
"""XM01 — Single-L Momentum Sweep
MECHANISM: Score = past_return(close, L). Long top-k / short bottom-k cross-sectionally.
Grid: L in {20, 30, 60, 90, 120}; universe in {all, majors}; test long-short and long-only.
Known prior: plain momentum on full 49-universe (XS01 uses 19 majors with L blend 30+90).
Goal: confirm negative on full universe, find whether single-L differs from XS01 blend.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XM01 — Single-L Momentum Sweep")
print("=" * 60)
# --- 5 targeted backtests ---
# 1) Full 49-universe, medium lookback L=60, LS — expected to be negative (known prior)
rep1 = xs.study_xs(
"XM01_ALL_L60",
lambda P: xs.past_return(P.close, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Full universe, short lookback L=20 — does short-term momentum work?
rep2 = xs.study_xs(
"XM01_ALL_L20",
lambda P: xs.past_return(P.close, 20),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Full universe, long lookback L=120, LS — intermediate/long momentum
rep3 = xs.study_xs(
"XM01_ALL_L120",
lambda P: xs.past_return(P.close, 120),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Majors only (XS01 turf), single L=60 — compare single-L vs XS01 blend on same universe
rep4 = xs.study_xs(
"XM01_MAJORS_L60",
lambda P: xs.past_return(P.close, 60),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Full universe, L=90, long-only top-k — momentum as selection filter (long-only)
rep5 = xs.study_xs(
"XM01_ALL_L90_LO",
lambda P: xs.past_return(P.close, 90),
universe="all", H=10, k=5, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Pick best: prefer earns_slot, then hold-out sharpe, then distinctness from XS01
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9)
full_sh = r["full"]["sharpe"]
corr_xs01 = r["corr_xs01"] or 1.0
distinctness = 1 - abs(corr_xs01) # higher = more distinct
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+88
View File
@@ -0,0 +1,88 @@
"""XM02 — Multi-L z-blend momentum
Score = mean of xs_zscore(past_return(close, L)) over a set of lookback windows L.
Compare two window sets: {30,90} (XS01-like) vs {20,60,120} (extended).
Grid: 5 study_xs calls total — vary universe / windows / H.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ── score helpers ─────────────────────────────────────────────────────────────
def blend_mom(close, lookbacks):
"""Mean of xs_zscore(past_return(close, L)) for each L in lookbacks."""
scores = [xs.xs_zscore(xs.past_return(close, L)) for L in lookbacks]
stacked = np.stack(scores, axis=2) # (n_days, n_assets, n_L)
return np.nanmean(stacked, axis=2) # (n_days, n_assets)
L_SHORT = [30, 90] # mirrors XS01 blend
L_LONG = [20, 60, 120] # extended set
L_WIDE = [20, 60, 90, 120] # even wider blend
# ── 5 backtests ───────────────────────────────────────────────────────────────
results = []
# 1. XS01-equivalent blend {30,90} on ALL universe — baseline reference
rep1 = xs.study_xs(
"XM02-3090-all",
lambda P: blend_mom(P.close, [30, 90]),
universe="all", H=10, k=5, long_short=True,
)
print(xs.fmt(rep1))
results.append(rep1)
# 2. Extended blend {20,60,120} on ALL universe
rep2 = xs.study_xs(
"XM02-206012-all",
lambda P: blend_mom(P.close, [20, 60, 120]),
universe="all", H=10, k=5, long_short=True,
)
print(xs.fmt(rep2))
results.append(rep2)
# 3. Extended blend {20,60,120} on MAJORS (19 alts — XS01 universe)
rep3 = xs.study_xs(
"XM02-206012-majors",
lambda P: blend_mom(P.close, [20, 60, 120]),
universe="majors", H=10, k=5, long_short=True,
)
print(xs.fmt(rep3))
results.append(rep3)
# 4. Wide blend {20,60,90,120} on ALL, shorter rebalance H=5
rep4 = xs.study_xs(
"XM02-wide-H5-all",
lambda P: blend_mom(P.close, [20, 60, 90, 120]),
universe="all", H=5, k=5, long_short=True,
)
print(xs.fmt(rep4))
results.append(rep4)
# 5. Wide blend on ALL, longer H=20 (less turnover)
rep5 = xs.study_xs(
"XM02-wide-H20-all",
lambda P: blend_mom(P.close, [20, 60, 90, 120]),
universe="all", H=20, k=5, long_short=True,
)
print(xs.fmt(rep5))
results.append(rep5)
# ── pick BEST by: earns_slot > hold-out sharpe > distinctness ────────────────
def _score(r):
earns = 1 if r["earns_slot"] else 0
verdict = 1 if r["marginal"].get("verdict") == "ADDS" else 0
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"]["sharpe"]
dist = 1 if (r["corr_xs01"] or 1.0) < 0.6 else 0
return (earns, verdict, hold_sh, full_sh, dist)
best = max(results, key=_score)
print("\n" + "=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+98
View File
@@ -0,0 +1,98 @@
"""XM03 — Vol-Scaled (Risk-Adjusted) Momentum
MECHANISM: Score = past_return(close, L) / roll_std(ret, L)
This is a Sharpe-like signal: normalises raw momentum by the volatility of that asset
over the same window. Should favour assets that moved up *smoothly* (high Sharpe trend)
over those that had large one-off jumps (noisy high return).
Grid: L in {30, 60, 90}; universe in {all, majors}; long_short True/False.
Goal: test if risk-adjusted scoring is DISTINCT from plain XS01 momentum and ADDS to the
live TP01+XS01+VRP01 portfolio.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def vol_adj_momentum(P, L: int) -> np.ndarray:
"""Causal Sharpe-like score: past_return / roll_std(ret, L).
Higher = long. Returns (n_days x n_assets).
Avoid divide-by-zero by replacing 0-vol rows with NaN -> harness treats NaN as neutral.
"""
pr = xs.past_return(P.close, L) # causal past return over L days
rv = xs.roll_std(P.ret, L) # causal rolling std of daily returns
# Replace zeros/near-zeros with NaN to avoid Inf
rv_safe = np.where(rv < 1e-8, np.nan, rv)
score = pr / rv_safe
return score
print("XM03 — Vol-Scaled (Risk-Adjusted) Momentum")
print("=" * 60)
# 1) All universe, L=30 (short horizon vol-adj)
rep1 = xs.study_xs(
"XM03_ALL_L30",
lambda P: vol_adj_momentum(P, 30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) All universe, L=60 (medium horizon)
rep2 = xs.study_xs(
"XM03_ALL_L60",
lambda P: vol_adj_momentum(P, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) All universe, L=90 (long horizon)
rep3 = xs.study_xs(
"XM03_ALL_L90",
lambda P: vol_adj_momentum(P, 90),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Majors only (same universe as XS01), L=60 — can vol-adj beat plain MOM on XS01 turf?
rep4 = xs.study_xs(
"XM03_MAJORS_L60",
lambda P: vol_adj_momentum(P, 60),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) All universe, L=60, long-only — does vol-adj work as selection filter?
rep5 = xs.study_xs(
"XM03_ALL_L60_LO",
lambda P: vol_adj_momentum(P, 60),
universe="all", H=10, k=5, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
def score_rep(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9)
full_sh = r["full"]["sharpe"]
corr_xs01 = r.get("corr_xs01") or 1.0
distinctness = 1 - abs(corr_xs01)
return (earns, hold_sh, full_sh, distinctness)
all_reps = [rep1, rep2, rep3, rep4, rep5]
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+72
View File
@@ -0,0 +1,72 @@
"""XM04 — Residual / Idiosyncratic Momentum
IDEA: Instead of raw past return, score = cumulative idiosyncratic (beta-removed) return
over the last L days. Should be a cleaner momentum signal: strips out the common market
component and scores assets on their STOCK-SPECIFIC performance.
Signal: for each day i and asset a, sum the daily residual_returns over [i-L+1 .. i].
residual_ret[t,a] = ret[t,a] - beta_t_a * market_ret[t]
score[i,a] = sum(residual_ret[i-L+1:i+1, a]) (causal: uses data <= i)
Grid (<=5 calls):
1. XM04-L30-maj : majors universe, L=30, H=10, k=5, LS
2. XM04-L60-maj : majors universe, L=60, H=10, k=5, LS
3. XM04-L30-all : all universe, L=30, H=10, k=5, LS
4. XM04-L30-maj-H5: majors, L=30, H=5, k=5, LS (faster rebal)
5. XM04-L30-maj-LO: majors, L=30, H=10, k=5, long-only (LO)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def resid_mom_score(P, L=30, beta_win=60):
"""Cumulative residual return over the last L days.
residual_ret[t,a] = ret[t,a] - beta(win)*market_ret[t]
score[i,a] = rolling sum of residual_ret over window L.
All causal: roll_beta uses data <=i, rolling sum uses [i-L+1..i].
"""
# daily idiosyncratic returns (n_days x n_assets), causal
resid = xs.residual_return(P.ret, win=beta_win)
# rolling sum over L days = cumulative idiosyncratic momentum
score = xs.roll_mean(resid, L) * L # equiv to rolling sum (roll_mean * win)
return score
configs = [
# name, universe, L, H, k, long_short
("XM04-L30-maj", "majors", 30, 10, 5, True),
("XM04-L60-maj", "majors", 60, 10, 5, True),
("XM04-L30-all", "all", 30, 10, 5, True),
("XM04-L30-maj-H5", "majors", 30, 5, 5, True),
("XM04-L30-maj-LO", "majors", 30, 10, 5, False),
]
results = []
for name, univ, L, H, k, ls in configs:
print(f"\nRunning {name} ...")
rep = xs.study_xs(
name,
lambda P, _L=L: resid_mom_score(P, L=_L, beta_win=60),
universe=univ,
H=H,
k=k,
long_short=ls,
)
print(xs.fmt(rep))
results.append(rep)
# Pick best: prefer earns_slot, then highest hold-out Sharpe, then most distinct from XS01
def score_config(r):
earns = r["earns_slot"]
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"]["sharpe"]
corr_xs = r["corr_xs01"] or 1.0
# primary: earns_slot; secondary: holdout Sharpe; tiebreak: distinctness
return (earns, hold_sh, full_sh, -abs(corr_xs))
best = max(results, key=score_config)
print("\n" + "="*60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+98
View File
@@ -0,0 +1,98 @@
"""XM05 — Momentum Acceleration
MECHANISM: Score = past_return(close, L_short) - past_return(close, L_long)
i.e. is momentum ACCELERATING? The idea: assets that are outperforming
recently vs. their longer-run momentum are gaining momentum -> rank them
high. Assets that were strong long-term but are slowing down -> rank low.
L_short=20, L_long=60 (canonical config).
Grid: vary universe (all/majors), H (5/10), and L_short param
to find the best config within <=5 backtests.
Distinctness target: if score is correlated to raw momentum (XS01), it's just XS01.
If acceleration captures something different (regime change, reversal of leaders), it
could be distinct and add to portfolio.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XM05 — Momentum Acceleration (L_short - L_long)")
print("=" * 60)
def mom_accel(close, L_short, L_long):
"""Score = short-term return minus long-term return (causal). Higher = accelerating."""
r_short = xs.past_return(close, L_short)
r_long = xs.past_return(close, L_long)
return r_short - r_long
# --- 5 targeted backtests ---
# 1) Canonical config: all universe, L_short=20, L_long=60, H=10, LS
rep1 = xs.study_xs(
"XM05_ALL_20_60",
lambda P: mom_accel(P.close, 20, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Majors universe (19 XS01 assets), same canonical L_short=20, L_long=60
rep2 = xs.study_xs(
"XM05_MAJ_20_60",
lambda P: mom_accel(P.close, 20, 60),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) All universe, shorter window: L_short=10, L_long=30 (faster acceleration signal)
rep3 = xs.study_xs(
"XM05_ALL_10_30",
lambda P: mom_accel(P.close, 10, 30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) All universe, L_short=20, L_long=60, longer holding period H=20
rep4 = xs.study_xs(
"XM05_ALL_20_60_H20",
lambda P: mom_accel(P.close, 20, 60),
universe="all", H=20, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) All universe, longer windows: L_short=30, L_long=90 (medium-term acceleration)
rep5 = xs.study_xs(
"XM05_ALL_30_90",
lambda P: mom_accel(P.close, 30, 90),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Pick best: prefer earns_slot, then hold-out sharpe, then distinctness from XS01
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9)
full_sh = r["full"]["sharpe"]
corr_xs01 = r.get("corr_xs01") or 1.0
distinctness = 1 - abs(corr_xs01) # higher = more distinct
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+79
View File
@@ -0,0 +1,79 @@
"""XM06 — 52-day-high proximity (closeness-to-recent-high momentum).
IDEA: Score = close / rolling_max(high, W) [closeness to recent high].
Assets near their recent high are "in momentum"; rank them cross-sectionally.
W in {60, 90}. Causal: rolling_max up through bar i only.
Grid: 5 calls max
1. W=60, majors, H=10, k=5, L/S
2. W=90, majors, H=10, k=5, L/S
3. W=60, all, H=10, k=5, L/S (best-W on wider universe)
4. W=60, all, H=5, k=5, L/S (faster rebalance)
5. W=60, all, H=10, k=7, L/S (wider book)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_proximity(P, W):
"""Causal: close[i] / max(high[i-W+1 .. i]). Higher = closer to recent high = long."""
n, A = P.close.shape
out = np.full((n, A), np.nan)
# rolling max of high, causal window [i-W+1 .. i]
high_df = __import__("pandas").DataFrame(P.high)
roll_max = high_df.rolling(W, min_periods=max(2, W // 2)).max().values
# proximity ratio: close / recent_high (always in (0,1] if no gap-up above window)
out = P.close / roll_max
return out
# ---- run grid ----
results = []
# 1. W=60, majors, H=10, k=5, L/S
rep1 = xs.study_xs("XM06_W60_majors", lambda P: score_proximity(P, 60),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(rep1))
results.append(rep1)
# 2. W=90, majors, H=10, k=5, L/S
rep2 = xs.study_xs("XM06_W90_majors", lambda P: score_proximity(P, 90),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(rep2))
results.append(rep2)
# 3. W=60, all, H=10, k=5, L/S
rep3 = xs.study_xs("XM06_W60_all", lambda P: score_proximity(P, 60),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(rep3))
results.append(rep3)
# 4. W=60, all, H=5, k=5, L/S (faster rebalance)
rep4 = xs.study_xs("XM06_W60_all_H5", lambda P: score_proximity(P, 60),
universe="all", H=5, k=5, long_short=True)
print(xs.fmt(rep4))
results.append(rep4)
# 5. W=60, all, H=10, k=7, L/S (wider book)
rep5 = xs.study_xs("XM06_W60_all_k7", lambda P: score_proximity(P, 60),
universe="all", H=10, k=7, long_short=True)
print(xs.fmt(rep5))
results.append(rep5)
# ---- pick best by: earns_slot > hold-out sharpe > distinctness ----
def score_result(r):
earns = 1 if r["earns_slot"] else 0
adds = 1 if r["marginal"].get("verdict") == "ADDS" else 0
hold_sh = r["holdout"].get("sharpe", -999)
full_sh = r["full"]["sharpe"]
corr_xs01 = abs(r.get("corr_xs01") or 1.0)
distinct = 1 if corr_xs01 < 0.6 else 0
return (earns, adds, hold_sh, full_sh, distinct)
best = max(results, key=score_result)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+87
View File
@@ -0,0 +1,87 @@
"""XM07 — Sharpe-rank momentum cross-sectional strategy.
Score = roll_mean(ret, L) / roll_std(ret, L) (realized Sharpe ratio over L days)
Rank assets cross-sectionally each H days, long top-k / short bottom-k.
Grid: L in {30, 60, 90}, then vary universe/H/k around the best L.
<=5 study_xs calls total.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def sharpe_score(P, L):
"""Causal realized Sharpe = roll_mean(ret, L) / roll_std(ret, L).
Uses daily returns (P.ret). Higher = stronger risk-adjusted momentum -> long.
"""
mu = xs.roll_mean(P.ret, L)
sigma = xs.roll_std(P.ret, L)
# avoid division by near-zero vol; set to NaN if sigma too small
score = mu / np.where(sigma > 1e-8, sigma, np.nan)
return score # (n_days x n_assets), higher = long
# ---- Grid (5 calls) --------------------------------------------------------
# Step 1: sweep L on "majors" universe with fixed H=10, k=5, long_short=True
print("=" * 60)
print("XM07 Sharpe-rank momentum — grid search")
print("=" * 60)
results = {}
# Call 1: L=30, majors
r1 = xs.study_xs("XM07_L30_majors", lambda P: sharpe_score(P, 30),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r1))
results["L30_majors"] = r1
# Call 2: L=60, majors
r2 = xs.study_xs("XM07_L60_majors", lambda P: sharpe_score(P, 60),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r2))
results["L60_majors"] = r2
# Call 3: L=90, majors
r3 = xs.study_xs("XM07_L90_majors", lambda P: sharpe_score(P, 90),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r3))
results["L90_majors"] = r3
# Pick best L by hold-out Sharpe among the 3
best_L_key = max(["L30_majors", "L60_majors", "L90_majors"],
key=lambda k: results[k]["holdout"]["sharpe"])
best_L = int(best_L_key.split("_")[0][1:]) # extract integer
print(f"\nBest L = {best_L} (by hold-out Sharpe)")
# Call 4: best L on "all" universe (49 alts) to test breadth
r4 = xs.study_xs(f"XM07_L{best_L}_all", lambda P: sharpe_score(P, best_L),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r4))
results[f"L{best_L}_all"] = r4
# Call 5: best L on majors, try H=20 (less frequent rebalance, lower fee drag)
r5 = xs.study_xs(f"XM07_L{best_L}_H20", lambda P: sharpe_score(P, best_L),
universe="majors", H=20, k=5, long_short=True)
print(xs.fmt(r5))
results[f"L{best_L}_H20"] = r5
# ---- Pick overall best config -----------------------------------------------
print("\n" + "=" * 60)
print("SUMMARY — picking best config")
print("=" * 60)
def score_config(r):
"""Prefer: earns_slot, then hold-out, then full Sharpe, then distinctness."""
earns = int(r.get("earns_slot", False))
ho = r["holdout"]["sharpe"]
full = r["full"]["sharpe"]
dist = 1.0 - abs(r.get("corr_xs01", 1.0)) # higher = more distinct
return (earns, ho, full, dist)
best_key = max(results.keys(), key=lambda k: score_config(results[k]))
best = results[best_key]
print(f"Best config: {best_key}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+107
View File
@@ -0,0 +1,107 @@
"""XM08 — Momentum Consistency (Frog-in-Pan)
Score = past_return(close, L) * fraction_of_up_days(ret, L)
Smooth momentum beats jumpy. "Frog-in-pan" from Ang, Goetzmann, Schaefer (2012):
consistent trends accumulating through many small daily gains dominate short sharp jumps.
Score is higher (more long) when returns over L days are both large AND consistent.
Grid: L=60 fixed (canonical), vary universe / H / k / long_short (<=5 calls total).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ---------------------------------------------------------------------------
# SCORE: causal frog-in-pan
# ---------------------------------------------------------------------------
def fip_score(P, L=60):
"""
score[i, a] = past_return(close[i], L) * frac_up_days(ret[i-L+1..i], L)
Causal: only uses ret and close up to row i.
"""
close = P.close # (n, A)
ret = P.ret # (n, A) simple daily returns
n, A = close.shape
# past return over L days (causal)
pr = xs.past_return(close, L) # (n, A), nan for i < L
# fraction of positive days over rolling window L
pos = (ret > 0).astype(float) # 1 if up day
frac_up = xs.roll_mean(pos, L) # causal rolling mean -> (n, A)
score = pr * frac_up
return score
# ---------------------------------------------------------------------------
# GRID (<=5 calls)
# ---------------------------------------------------------------------------
results = []
# 1. Base: majors, L=60, H=10, k=5, long_short
rep1 = xs.study_xs(
"XM08_majors_H10_k5_ls",
lambda P: fip_score(P, 60),
universe="majors", H=10, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
results.append(rep1)
# 2. All assets, L=60, H=10, k=5, long_short
rep2 = xs.study_xs(
"XM08_all_H10_k5_ls",
lambda P: fip_score(P, 60),
universe="all", H=10, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
results.append(rep2)
# 3. All assets, H=20, k=5, long_short (slower rebal)
rep3 = xs.study_xs(
"XM08_all_H20_k5_ls",
lambda P: fip_score(P, 60),
universe="all", H=20, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
results.append(rep3)
# 4. Majors, H=10, k=5, long-only
rep4 = xs.study_xs(
"XM08_majors_H10_k5_lo",
lambda P: fip_score(P, 60),
universe="majors", H=10, k=5, long_short=False, target_vol=0.20
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
results.append(rep4)
# 5. All assets, H=10, k=7, long_short (wider top/bottom bucket)
rep5 = xs.study_xs(
"XM08_all_H10_k7_ls",
lambda P: fip_score(P, 60),
universe="all", H=10, k=7, long_short=True, target_vol=0.20
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
results.append(rep5)
# ---------------------------------------------------------------------------
# PICK BEST
# ---------------------------------------------------------------------------
def score_result(r):
"""Prefer earns_slot, then hold-out sharpe, then distinctness."""
earns = r.get("earns_slot", False)
ho = r.get("holdout", {}).get("sharpe", -999)
corr = abs(r.get("corr_xs01", 1.0))
return (int(earns), ho, -corr)
best = max(results, key=score_result)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+127
View File
@@ -0,0 +1,127 @@
"""XM09 — Market-trend-gated momentum
Score = XS momentum (past_return L=60) but ACTIVE only when the equal-weight
market return trailing sum over L days is > 0; else 0 (flat).
Idea: plain cross-sectional momentum tends to fail during broad market downtrends
(all alts fall together, 'market neutral' still bleeds). Gate it off when the market
equal-weight trend is negative. Distinct from XS01 (plain XS mom) because it selectively
silences the strategy in bear regimes, producing a different return pattern.
Grid (<=5 calls): vary universe / H / k.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ---------------------------------------------------------------------------
# SCORE: market-trend-gated momentum
# ---------------------------------------------------------------------------
def xm09_score(P, L=60):
"""
score[i, a] = past_return(close, L)[i, a] * market_up[i]
market_up[i] = 1 if trailing-L sum of equal-weight market daily returns > 0, else 0.
Fully causal: uses close and ret up to row i only.
"""
close = P.close # (n, A)
ret = P.ret # (n, A) simple daily returns
n, A = close.shape
# Base momentum score (causal)
pr = xs.past_return(close, L) # (n, A), nan for i < L
# Equal-weight market return per day (causal, mean across assets ignoring NaN)
mret = xs.market_ret(ret) # (n,) equal-weight market return
# Trailing L-day cumulative market return (causal rolling sum)
# roll_mean(mat, win) works on 2D; use it on a column vector
mret_2d = mret.reshape(-1, 1) # (n, 1)
mkt_trail = xs.roll_mean(mret_2d, L) * L # approximate trailing sum via roll_mean * L
# Actually compute exact rolling sum using cumsum trick (causal)
mret_cumsum = np.cumsum(mret) # (n,)
mkt_rolling_sum = np.empty(n)
mkt_rolling_sum[:] = np.nan
for i in range(L - 1, n):
mkt_rolling_sum[i] = mret_cumsum[i] - (mret_cumsum[i - L] if i >= L else 0.0)
# Market uptrend gate: 1 when trailing sum > 0, else 0
market_up = (mkt_rolling_sum > 0).astype(float) # (n,)
market_up[:L - 1] = np.nan # not enough history
# Broadcast: score is 0 (flat) when market is down
score = pr * market_up[:, None] # (n, A)
return score
# ---------------------------------------------------------------------------
# GRID (<=5 calls)
# ---------------------------------------------------------------------------
results = []
# 1. Base: majors, L=60, H=10, k=5, long_short
rep1 = xs.study_xs(
"XM09_majors_H10_k5_ls",
lambda P: xm09_score(P, 60),
universe="majors", H=10, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
results.append(rep1)
# 2. All assets, L=60, H=10, k=5, long_short
rep2 = xs.study_xs(
"XM09_all_H10_k5_ls",
lambda P: xm09_score(P, 60),
universe="all", H=10, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
results.append(rep2)
# 3. Majors, H=10, k=5, long-only (when market is up, just go long top-k)
rep3 = xs.study_xs(
"XM09_majors_H10_k5_lo",
lambda P: xm09_score(P, 60),
universe="majors", H=10, k=5, long_short=False, target_vol=0.20
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
results.append(rep3)
# 4. All assets, H=20, k=5, long_short (slower rebalance)
rep4 = xs.study_xs(
"XM09_all_H20_k5_ls",
lambda P: xm09_score(P, 60),
universe="all", H=20, k=5, long_short=True, target_vol=0.20
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
results.append(rep4)
# 5. Majors, H=10, k=7, long_short (wider buckets on smaller universe)
rep5 = xs.study_xs(
"XM09_majors_H10_k7_ls",
lambda P: xm09_score(P, 60),
universe="majors", H=10, k=7, long_short=True, target_vol=0.20
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
results.append(rep5)
# ---------------------------------------------------------------------------
# PICK BEST
# ---------------------------------------------------------------------------
def score_result(r):
"""Prefer earns_slot, then hold-out sharpe, then distinctness from XS01."""
earns = r.get("earns_slot", False)
ho = r.get("holdout", {}).get("sharpe", -999)
corr = abs(r.get("corr_xs01", 1.0))
return (int(earns), ho, -corr)
best = max(results, key=score_result)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+127
View File
@@ -0,0 +1,127 @@
"""XM10 — Rank-weighted continuous momentum (demeaned xs_rank).
MECHANISM: Instead of top-k/bottom-k binary selection, weight ALL assets
proportionally to their demeaned cross-sectional rank of past return.
rank_i in [0,1] -> demeaned rank = rank_i - 0.5 -> scores in [-0.5, +0.5].
L=60 (lookback ~2 months). Continuous book approximated via large k (A//2)
and fine score (continuous rank, not discrete order).
The study_xs() engine still uses top-k/bottom-k for the actual rebalance,
but by setting k=A//2 (half the universe) and using xs_rank as the score,
the effective weight profile is nearly linear across the full distribution.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# -----------------------------------------------------------------------
# Score: demeaned cross-sectional rank of 60-day past return
# Higher score = longer weight. Causal: uses data up to and including bar i.
# -----------------------------------------------------------------------
L = 60 # lookback
def score_rank_mom(P, L=60):
"""Continuous rank-weighted momentum score.
xs_rank -> [0,1]; demean -> [-0.5, +0.5] so it's symmetric long/short.
"""
pr = xs.past_return(P.close, L) # (n_days, n_assets)
ranked = xs.xs_rank(pr) # [0,1] cross-sectionally per row
return ranked - 0.5 # demeaned: positive = long
# -----------------------------------------------------------------------
# Small grid: 5 studies
# 1) majors, H=10, k=large (9 ~ A//2 of 19)
# 2) all, H=10, k=large (24 ~ A//2 of 49)
# 3) all, H=5, k=large (24) — faster rebalance
# 4) all, H=10, k=large (24), L=30 — shorter lookback
# 5) all, H=20, k=large (24) — slower rebalance
# -----------------------------------------------------------------------
results = []
print("=== XM10 Rank-Weighted Continuous Momentum ===\n")
# 1) Majors universe, H=10, k=9 (A//2 of 19)
r1 = xs.study_xs(
"XM10-majors-H10-k9-L60",
lambda P: score_rank_mom(P, L=60),
universe="majors",
H=10, k=9, long_short=True
)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
print()
results.append(r1)
# 2) All (49 alts), H=10, k=24 (A//2)
r2 = xs.study_xs(
"XM10-all-H10-k24-L60",
lambda P: score_rank_mom(P, L=60),
universe="all",
H=10, k=24, long_short=True
)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
print()
results.append(r2)
# 3) All, H=5, k=24 — faster rebalance
r3 = xs.study_xs(
"XM10-all-H5-k24-L60",
lambda P: score_rank_mom(P, L=60),
universe="all",
H=5, k=24, long_short=True
)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
print()
results.append(r3)
# 4) All, H=10, k=24, L=30 shorter lookback
r4 = xs.study_xs(
"XM10-all-H10-k24-L30",
lambda P: score_rank_mom(P, L=30),
universe="all",
H=10, k=24, long_short=True
)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
print()
results.append(r4)
# 5) All, H=20, k=24, L=60 slower rebalance
r5 = xs.study_xs(
"XM10-all-H20-k24-L60",
lambda P: score_rank_mom(P, L=60),
universe="all",
H=20, k=24, long_short=True
)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
print()
results.append(r5)
# -----------------------------------------------------------------------
# Pick best by: earns_slot first, then hold-out sharpe, then distinctness
# -----------------------------------------------------------------------
def score_result(r):
es = int(r.get("earns_slot", False))
hold_sh = r["holdout"].get("sharpe", -99)
corr_xs01 = abs(r.get("corr_xs01") or 1.0)
distinctness = 1.0 - corr_xs01 # higher is more distinct
# marginal verdict
verdict = r["marginal"].get("verdict", "")
verdict_score = {"ADDS": 3, "NEUTRAL": 1, "DILUTES": 0, "REDUNDANT": 0, "N/A": 0}.get(verdict, 0)
return (es, verdict_score, hold_sh, distinctness)
results_sorted = sorted(results, key=score_result, reverse=True)
best = results_sorted[0]
print("\n" + "=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+68
View File
@@ -0,0 +1,68 @@
"""XR01 — Short-term Reversal on the Hyperliquid certified alt panel.
Score = -past_return(close, L) (long losers / short winners)
Grid: L in {1, 3, 5, 7}
Known prior: REV5 negative — confirm / diagnose.
We try <=5 study_xs calls:
1. L=1 majors H=5 k=5 L/S
2. L=3 majors H=5 k=5 L/S
3. L=5 majors H=5 k=5 L/S (baseline to confirm negative prior)
4. L=7 majors H=5 k=5 L/S
5. best L (by hold-out) on universe="all" (same H/k)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
UNIVERSE_BASE = "majors"
H = 5
K = 5
results = []
for L in [1, 3, 5, 7]:
def score_fn(P, L=L):
return -xs.past_return(P.close, L)
rep = xs.study_xs(
f"XR01_L{L}_maj",
score_fn,
universe=UNIVERSE_BASE,
H=H,
k=K,
long_short=True,
target_vol=0.20,
)
print(xs.fmt(rep))
print("JSON:", xs.as_json(rep))
results.append((L, rep))
# Pick best L by hold-out Sharpe
best_L, best_rep = max(results, key=lambda x: x[1]["holdout"]["sharpe"])
print(f"\n=== Best L on majors: L={best_L} (hold-out Sharpe={best_rep['holdout']['sharpe']:.3f}) ===\n")
# Run the best L on "all" universe
def score_best(P, L=best_L):
return -xs.past_return(P.close, L)
rep_all = xs.study_xs(
f"XR01_L{best_L}_all",
score_best,
universe="all",
H=H,
k=K,
long_short=True,
target_vol=0.20,
)
print(xs.fmt(rep_all))
print("JSON:", xs.as_json(rep_all))
# Final summary: pick the overall best (by earns_slot, then hold-out)
all_reps = [r for _, r in results] + [rep_all]
all_reps_sorted = sorted(all_reps, key=lambda r: (r.get("earns_slot", False), r["holdout"]["sharpe"]), reverse=True)
final = all_reps_sorted[0]
print("\n=== FINAL BEST ===")
print(xs.fmt(final))
print("JSON:", xs.as_json(final))
+171
View File
@@ -0,0 +1,171 @@
"""XR02 — Short-term Reversal gated by high-vol regime (L=3).
MECHANISM:
Plain reversal: short the recent winners, long the recent losers (L=3 days lookback).
GATE: only active when market volatility is HIGH — defined as the 30-day rolling std of
the equal-weight market return exceeding its own 90-day expanding percentile (p70 threshold).
In low-vol / calm regimes, the book is flat (score = NaN -> no position).
Rationale: short-term reversal is a classic effect but is often diluted by trend in calm
regimes. In panic / high-vol regimes (sharp market moves), mean-reversion / liquidity
provision logic is stronger (overshoot + reversal). Gate concentrates the signal in those
regimes while avoiding trend-contamination in smooth uptrends.
Causal: all quantities computed at close[i], applied to return of bar i+1.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
import pandas as pd
def rev_gate_score(P, L=3, vol_win=30, baseline_win=90, vol_pct=70, use_residual=False):
"""Short-term reversal score gated by high market-vol regime.
Parameters
----------
P : Panel
L : int
Reversal lookback in days (price L days ago vs today).
vol_win : int
Rolling window for realised market-vol (std of equal-weight market return).
baseline_win : int
Expanding window for computing the percentile threshold on market-vol.
vol_pct : float
Percentile threshold: market vol must exceed this percentile to be active.
use_residual : bool
If True, compute reversal on idiosyncratic (market-beta-neutral) returns instead of raw.
"""
n, A = P.close.shape
# --- 1. Reversal score: negative of L-day return (higher = long the loser) ---
raw_ret = xs.past_return(P.close, L) # (n, A), causal: uses close[i-L..i]
if use_residual:
# use idiosyncratic cumulative return instead of total
resid = xs.residual_return(P.ret, 30) # (n, A), causal
# cumulate idiosyncratic over L days
resid_cum = np.full_like(raw_ret, np.nan)
for lag in range(1, L + 1):
shifted = np.roll(resid, lag, axis=0)
shifted[:lag] = np.nan
resid_cum = np.nansum([resid_cum, resid], axis=0)
signal = -resid_cum
else:
signal = -raw_ret # reversal: short winners, long losers
# --- 2. Market-vol regime gate (expanding percentile, causal) ---
mkt = xs.market_ret(P.ret) # (n,) equal-weight mkt return
mkt_vol = pd.Series(mkt).rolling(vol_win, min_periods=max(5, vol_win // 2)).std().values
# expanding percentile of mkt_vol up to each row i (causal)
thresh = np.full(n, np.nan)
for i in range(baseline_win, n):
hist = mkt_vol[max(0, i - baseline_win):i + 1]
hist = hist[np.isfinite(hist)]
if len(hist) >= 10:
thresh[i] = np.nanpercentile(hist, vol_pct)
# gate: active only when mkt_vol > threshold (high-vol regime)
active = (mkt_vol > thresh) # (n,) boolean, NaN -> False
active[~np.isfinite(mkt_vol) | ~np.isfinite(thresh)] = False
# --- 3. Apply gate: set score to NaN when flat ---
score = signal.copy()
score[~active, :] = np.nan
return score
# ---------------------------------------------------------------------------
# GRID — <=5 study_xs calls
# Config space: L in {3, 5}, vol_pct in {60, 70}, universe in {majors, all}
# ---------------------------------------------------------------------------
print("=" * 70)
print("XR02: Short-term Reversal gated by high-vol regime")
print("=" * 70)
results = []
# Config 1: L=3, pct=70, majors (baseline config)
print("\n[1/5] L=3, vol_pct=70, universe=majors")
rep1 = xs.study_xs(
"XR02-L3-p70-maj",
lambda P: rev_gate_score(P, L=3, vol_win=30, baseline_win=90, vol_pct=70),
universe="majors",
H=3,
k=5,
long_short=True,
)
print(xs.fmt(rep1))
results.append(rep1)
# Config 2: L=3, pct=70, all (wider universe)
print("\n[2/5] L=3, vol_pct=70, universe=all")
rep2 = xs.study_xs(
"XR02-L3-p70-all",
lambda P: rev_gate_score(P, L=3, vol_win=30, baseline_win=90, vol_pct=70),
universe="all",
H=3,
k=5,
long_short=True,
)
print(xs.fmt(rep2))
results.append(rep2)
# Config 3: L=3, pct=60 (more permissive gate), majors
print("\n[3/5] L=3, vol_pct=60, universe=majors")
rep3 = xs.study_xs(
"XR02-L3-p60-maj",
lambda P: rev_gate_score(P, L=3, vol_win=30, baseline_win=90, vol_pct=60),
universe="majors",
H=3,
k=5,
long_short=True,
)
print(xs.fmt(rep3))
results.append(rep3)
# Config 4: L=5, pct=70, majors (longer lookback)
print("\n[4/5] L=5, vol_pct=70, universe=majors")
rep4 = xs.study_xs(
"XR02-L5-p70-maj",
lambda P: rev_gate_score(P, L=5, vol_win=30, baseline_win=90, vol_pct=70),
universe="majors",
H=5,
k=5,
long_short=True,
)
print(xs.fmt(rep4))
results.append(rep4)
# Config 5: L=3, pct=70, majors, H=5 (slower rebalance)
print("\n[5/5] L=3, vol_pct=70, universe=majors, H=5")
rep5 = xs.study_xs(
"XR02-L3-p70-maj-H5",
lambda P: rev_gate_score(P, L=3, vol_win=30, baseline_win=90, vol_pct=70),
universe="majors",
H=5,
k=5,
long_short=True,
)
print(xs.fmt(rep5))
results.append(rep5)
# ---------------------------------------------------------------------------
# BEST: pick by earns_slot, then holdout sharpe, then distinctness
# ---------------------------------------------------------------------------
def score_result(r):
earns = int(r.get("earns_slot", False))
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"].get("sharpe", -99)
corr_xs01 = r.get("corr_xs01") or 1.0
# prefer earns_slot, then hold-out, then distinctness, then full
return (earns, hold_sh, full_sh, -abs(corr_xs01))
best = max(results, key=score_result)
print("\n" + "=" * 70)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+92
View File
@@ -0,0 +1,92 @@
"""XR03 — Residual Short-Term Reversal
Score = -(sum of residual_return over last L days)
Idiosyncratic reversal: removes market beta before computing the short-term reversal signal.
L in {3, 5}; beta window fixed at 60d.
Grid (<= 5 study_xs calls):
1. L=3, majors, H=5, k=5, long_short=True
2. L=5, majors, H=5, k=5, long_short=True
3. L=3, all, H=5, k=5, long_short=True
4. L=5, all, H=5, k=5, long_short=True
5. Best-L from above, all, H=10, k=5, long_short=True
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
BETA_WIN = 60 # rolling beta window for residual computation
def score_xr03(P, L):
"""Causal residual reversal score.
residual[i] = ret[i] - beta_rolling[i] * market_ret[i]
score[i] = -sum(residual[i-L+1 .. i])
HIGHER score = more negative recent idio returns = long (reversal)
"""
res = xs.residual_return(P.ret, BETA_WIN) # (n_days, n_assets)
# rolling sum of last L residuals (causal: sum of rows [i-L+1..i])
res_sum = xs.roll_mean(res, L) * L # roll_mean * L = roll_sum
# reversal: negative of cumulative idio return
score = -res_sum
return score
# --- Grid ---
results = []
# 1. L=3, majors
r1 = xs.study_xs("XR03_L3_maj", lambda P: score_xr03(P, 3),
universe="majors", H=5, k=5, long_short=True)
results.append(r1)
print("=== XR03 L3 majors H5 ===")
print(xs.fmt(r1))
# 2. L=5, majors
r2 = xs.study_xs("XR03_L5_maj", lambda P: score_xr03(P, 5),
universe="majors", H=5, k=5, long_short=True)
results.append(r2)
print("=== XR03 L5 majors H5 ===")
print(xs.fmt(r2))
# 3. L=3, all
r3 = xs.study_xs("XR03_L3_all", lambda P: score_xr03(P, 3),
universe="all", H=5, k=5, long_short=True)
results.append(r3)
print("=== XR03 L3 all H5 ===")
print(xs.fmt(r3))
# 4. L=5, all
r4 = xs.study_xs("XR03_L5_all", lambda P: score_xr03(P, 5),
universe="all", H=5, k=5, long_short=True)
results.append(r4)
print("=== XR03 L5 all H5 ===")
print(xs.fmt(r4))
# 5. Best-L (by hold-out sharpe) with H=10
# pick best L from runs 1-4
best_so_far = max(results, key=lambda r: r["holdout"]["sharpe"])
best_L = 3 if "L3" in best_so_far["name"] else 5
best_univ = "all" if "all" in best_so_far["name"] else "majors"
r5 = xs.study_xs(f"XR03_L{best_L}_{best_univ}_H10",
lambda P: score_xr03(P, best_L),
universe=best_univ, H=10, k=5, long_short=True)
results.append(r5)
print(f"=== XR03 L{best_L} {best_univ} H10 ===")
print(xs.fmt(r5))
# --- Pick best overall by marginal robustness then hold-out ---
def score_key(r):
earns = r.get("earns_slot", False)
oos = r.get("marginal", {}).get("robust_oos", False)
verdict = r.get("marginal", {}).get("verdict", "")
adds = verdict == "ADDS"
hold = r["holdout"]["sharpe"]
full = r["full"]["sharpe"]
return (int(earns), int(adds), int(oos), hold, full)
best = max(results, key=score_key)
print("\n========== BEST CONFIG ==========")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+101
View File
@@ -0,0 +1,101 @@
"""XR04 — Volume-shock reversal.
IDEA: Long recent losers that ALSO had a volume spike.
score = -past_return(L) * (volume_z > 1)
The intuition: large volume + price drop signals capitulation/panic selling.
The oversold name with elevated volume is more likely to bounce vs a name
that drifted down quietly. L=3 is the suggested lookback.
Grid (<=5 calls):
1. majors H=5 k=3 LS=True L=3 (baseline config)
2. majors H=5 k=5 LS=True L=3 (more positions)
3. all H=5 k=5 LS=True L=3 (wider universe)
4. majors H=3 k=3 LS=True L=5 (slightly longer reversal)
5. majors H=5 k=3 LS=True L=3 with volume_z threshold = 0.5 (lower bar)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def vol_shock_reversal_score(P, L=3, vz_thresh=1.0):
"""score = -past_return(L) when volume_z > vz_thresh, else 0.
Higher score = more reversal candidate with volume spike.
Causally computed: all data at row i uses data <=i."""
ret_L = xs.past_return(P.close, L) # (n, A)
vz = xs.volume_z(P.vol, 20) # rolling 20d volume z-score
# Only count the reversal signal when volume is elevated
mask = vz > vz_thresh # bool (n, A)
score = np.where(mask, -ret_L, 0.0)
# Where no data (NaN), set to NaN so harness skips
score = np.where(np.isfinite(ret_L) & np.isfinite(vz), score, np.nan)
return score
results = []
# 1. Baseline: majors, H=5, k=3, L=3, vz_thresh=1.0
r1 = xs.study_xs(
"XR04-majors-H5k3-L3",
lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0),
universe="majors", H=5, k=3, long_short=True
)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# 2. More positions: majors, H=5, k=5, L=3
r2 = xs.study_xs(
"XR04-majors-H5k5-L3",
lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0),
universe="majors", H=5, k=5, long_short=True
)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# 3. Wider universe: all, H=5, k=5, L=3
r3 = xs.study_xs(
"XR04-all-H5k5-L3",
lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# 4. Slightly longer reversal window: majors, H=3, k=3, L=5
r4 = xs.study_xs(
"XR04-majors-H3k3-L5",
lambda P: vol_shock_reversal_score(P, L=5, vz_thresh=1.0),
universe="majors", H=3, k=3, long_short=True
)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# 5. Lower volume threshold: majors, H=5, k=3, L=3, vz_thresh=0.5
r5 = xs.study_xs(
"XR04-majors-H5k3-L3-vz05",
lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=0.5),
universe="majors", H=5, k=3, long_short=True
)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# Pick best by: earns_slot first, then hold-out sharpe, then distinctness
def rank_key(r):
earns = int(r.get("earns_slot", False))
h_sh = r["holdout"].get("sharpe", -99)
f_sh = r["full"].get("sharpe", -99)
corr_xs01 = r.get("corr_xs01") or 1.0
distinct = 1 if corr_xs01 < 0.6 else 0
return (earns, h_sh, f_sh, distinct)
best = max(results, key=rank_key)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+78
View File
@@ -0,0 +1,78 @@
"""XR05 — Overreaction Reversal (mid-horizon)
IDEA: Score = -past_return(close, L) for L in {20, 30}.
Assets that ran up the most over the past 20-30 days are SHORTED (expected to mean-revert);
assets that dropped the most are LONGED. Pure cross-sectional contrarian on multi-week moves.
Grid (<= 5 calls):
1. L=20, H=10, k=5, LS, universe=majors
2. L=30, H=10, k=5, LS, universe=majors
3. L=20, H=5, k=5, LS, universe=majors (faster rebal)
4. blend -PR20 and -PR30 (mean z-score), H=10, k=5, LS, universe=majors
5. blend -PR20 and -PR30, H=10, k=5, LS, universe=all (broader universe)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ------------------------------------------------------------------
# Score helpers (causal: close[i] only uses data up to bar i)
# ------------------------------------------------------------------
def score_rev20(P):
return -xs.past_return(P.close, 20)
def score_rev30(P):
return -xs.past_return(P.close, 30)
def score_rev20_fast(P):
return -xs.past_return(P.close, 20)
def score_blend_majors(P):
z20 = xs.xs_zscore(-xs.past_return(P.close, 20))
z30 = xs.xs_zscore(-xs.past_return(P.close, 30))
return (z20 + z30) / 2.0
def score_blend_all(P):
z20 = xs.xs_zscore(-xs.past_return(P.close, 20))
z30 = xs.xs_zscore(-xs.past_return(P.close, 30))
return (z20 + z30) / 2.0
# ------------------------------------------------------------------
# Grid
# ------------------------------------------------------------------
configs = [
dict(name="XR05-REV20-H10-k5-majors", fn=score_rev20, universe="majors", H=10, k=5, long_short=True),
dict(name="XR05-REV30-H10-k5-majors", fn=score_rev30, universe="majors", H=10, k=5, long_short=True),
dict(name="XR05-REV20-H5-k5-majors", fn=score_rev20_fast, universe="majors", H=5, k=5, long_short=True),
dict(name="XR05-BLENDz-H10-k5-majors", fn=score_blend_majors, universe="majors", H=10, k=5, long_short=True),
dict(name="XR05-BLENDz-H10-k5-all", fn=score_blend_all, universe="all", H=10, k=5, long_short=True),
]
results = []
for c in configs:
print(f"\nRunning {c['name']} ...")
rep = xs.study_xs(
c["name"],
c["fn"],
universe=c["universe"],
H=c["H"],
k=c["k"],
long_short=c["long_short"],
)
print(xs.fmt(rep))
results.append(rep)
# ------------------------------------------------------------------
# Pick best config: earns_slot first, then hold-out sharpe, then distinctness
# ------------------------------------------------------------------
def _sort_key(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -99)
corr_xs01 = abs(r["corr_xs01"] or 1.0)
return (earns, hold_sh, -corr_xs01)
best = max(results, key=_sort_key)
print("\n" + "="*60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+58
View File
@@ -0,0 +1,58 @@
"""XS01b — Double-sort Momentum × Low-Vol
Score = xs_zscore(past_return(close, 60)) + xs_zscore(-roll_std(ret, 30))
Combines cross-sectional momentum with low-vol preference (lower realized vol = higher score).
Grid: universe x H x k variations, <=5 total backtests.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# --- score factory ---
def score_mom_lowvol(mom_L=60, vol_win=30):
"""Double-sort: momentum z + low-vol z. Both causal (data <= close[i])."""
def _score(P):
mom = xs.xs_zscore(xs.past_return(P.close, mom_L))
# low vol = higher score -> negate std
lowvol = xs.xs_zscore(-xs.roll_std(P.ret, vol_win))
return mom + lowvol
return _score
# Grid (<=5 calls total):
# 1. Baseline: majors H10 k5 LS (19 assets, closest to XS01 universe)
# 2. All universe H10 k5 LS
# 3. All universe H5 k5 LS (faster rebalance)
# 4. Majors H10 k5 LS with longer mom window (90d) to differ from XS01
# 5. All universe H10 k7 LS (wider book)
configs = [
dict(name="XS01b-MAJ-H10-k5", universe="majors", H=10, k=5, long_short=True, fn=score_mom_lowvol(60,30)),
dict(name="XS01b-ALL-H10-k5", universe="all", H=10, k=5, long_short=True, fn=score_mom_lowvol(60,30)),
dict(name="XS01b-ALL-H5-k5", universe="all", H=5, k=5, long_short=True, fn=score_mom_lowvol(60,30)),
dict(name="XS01b-MAJ-H10-MOM90", universe="majors", H=10, k=5, long_short=True, fn=score_mom_lowvol(90,30)),
dict(name="XS01b-ALL-H10-k7", universe="all", H=10, k=7, long_short=True, fn=score_mom_lowvol(60,30)),
]
results = []
for cfg in configs:
print(f"\nRunning {cfg['name']} ...")
fn = cfg.pop("fn")
rep = xs.study_xs(score_fn=fn, **cfg)
results.append(rep)
print(xs.fmt(rep))
print()
# --- pick best: prefer earns_slot, then hold-out sharpe, then corr_xs01 < 0.6
def score_result(r):
earns = 1 if r["earns_slot"] else 0
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"]["sharpe"]
distinct = 1 if (r["corr_xs01"] or 1.0) < 0.6 else 0
return (earns, hold_sh, full_sh, distinct)
best = max(results, key=score_result)
print("\n" + "="*60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+88
View File
@@ -0,0 +1,88 @@
"""XS02b — Long-mom + short-rev multi-horizon
Score = xs_zscore(past_return(close, 90)) + xs_zscore(-past_return(close, 5))
Long-term winners (90d) that have recently dipped (5d reversal).
This is structurally distinct from plain XS01 momentum because it FADES the very-recent move
while keeping the intermediate-term trend, blending momentum with mean-reversion.
Grid: universe x H x k (<=5 study_xs calls).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_xs02b(P):
"""Score = xs_zscore(90d mom) + xs_zscore(-5d return).
Higher = long: intermediate-term winner AND short-term dipper.
Fully causal: past_return(close, L) at row i uses close[i-L..i].
"""
mom_long = xs.xs_zscore(xs.past_return(P.close, 90)) # 90d momentum
rev_short = xs.xs_zscore(-xs.past_return(P.close, 5)) # 5d reversal (negate: dip = good)
return mom_long + rev_short
if __name__ == "__main__":
results = []
# Run 1: majors, H=10, k=5, L/S — canonical XS01-like setup but new signal
r1 = xs.study_xs("XS02b_maj_H10_k5_LS", score_xs02b,
universe="majors", H=10, k=5, long_short=True, target_vol=0.20)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# Run 2: all (49 alts), H=10, k=5, L/S — broader universe
r2 = xs.study_xs("XS02b_all_H10_k5_LS", score_xs02b,
universe="all", H=10, k=5, long_short=True, target_vol=0.20)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# Run 3: majors, H=5, k=5, L/S — faster rebalance
r3 = xs.study_xs("XS02b_maj_H5_k5_LS", score_xs02b,
universe="majors", H=5, k=5, long_short=True, target_vol=0.20)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# Run 4: all, H=5, k=7, L/S — broader universe, faster, wider basket
r4 = xs.study_xs("XS02b_all_H5_k7_LS", score_xs02b,
universe="all", H=5, k=7, long_short=True, target_vol=0.20)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# Run 5: majors, H=10, k=5, long-only — for comparison
r5 = xs.study_xs("XS02b_maj_H10_k5_LO", score_xs02b,
universe="majors", H=10, k=5, long_short=False, target_vol=0.20)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# ---- Summary ----
print("\n\n=== XS02b GRID SUMMARY ===")
for r in results:
f = r["full"]
h = r["holdout"]
m = r.get("marginal", {})
print(f" {r['name']:35s} FULL Sh={f['sharpe']:+.2f} DD={f['maxdd']:.1%}"
f" HOLD Sh={h['sharpe']:+.2f}"
f" corr_xs01={r.get('corr_xs01',float('nan')):+.2f}"
f" verdict={m.get('verdict','?')}"
f" earns_slot={r.get('earns_slot','?')}")
# Pick best by: earns_slot > hold-out > corr distinctness
def sort_key(r):
es = 1 if r.get("earns_slot") else 0
mv = 1 if r.get("marginal", {}).get("verdict") == "ADDS" else 0
ho = r["holdout"]["sharpe"]
cxs = abs(r.get("corr_xs01", 1.0))
return (es, mv, ho, -cxs)
best = max(results, key=sort_key)
print(f"\nBEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+126
View File
@@ -0,0 +1,126 @@
"""XS03b — Beta-hedged momentum.
IDEA: Instead of plain cross-sectional momentum (XS01), use RESIDUAL momentum:
score = cumulative idiosyncratic return over lookback L.
The residual is ret - beta*mkt_ret (rolling beta vs equal-weight panel),
so each asset's score reflects ONLY its idiosyncratic drift, stripping
out shared market moves. The resulting book is already dollar-neutral
(long-short) but also implicitly market-beta-neutral because the signal
itself filters out mkt co-movement.
WHY DISTINCT FROM XS01: Plain XS01 ranks on raw momentum; the top assets
in a bull market are often the highest-beta assets (not idiosyncratic winners).
Beta-hedged momentum ranks on WHAT IS LEFT after removing mkt factor:
- In bull: avoids accidental overweight of market beta
- In bear: avoids accidental short of low-beta (defensive) assets
- Net: the book is more idiosyncratic and less correlated to raw XS momentum.
GRID (5 backtests max):
1. majors, L=30, beta_win=90, H=10, k=5, LS
2. majors, L=60, beta_win=90, H=10, k=5, LS
3. all, L=30, beta_win=90, H=10, k=5, LS
4. all, L=60, beta_win=90, H=10, k=5, LS
5. all, blend L=[30,60] residuals, beta_win=90, H=10, k=5, LS (like XS01 blend)
Pick best by: earns_slot > holdout_sharpe > distinctness from XS01.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def residual_momentum(P, L, beta_win=90):
"""Cumulative idiosyncratic return over L days (causal).
Residual daily ret = ret - rolling_beta * market_ret.
Cumulate over L days to get momentum score on idiosyncratic drift.
"""
resid = xs.residual_return(P.ret, beta_win) # (n_days x n_assets)
# Cumulate residuals over L days (causal: sum of past L residual daily rets)
n, A = resid.shape
cum = np.full((n, A), np.nan)
for i in range(L, n):
cum[i] = np.nansum(resid[i - L:i], axis=0)
return cum
def blend_residual_mom(P, Ls=(30, 60), beta_win=90):
"""Cross-sectional z-score blend of multiple lookback residual momentums."""
scores = []
for L in Ls:
s = residual_momentum(P, L, beta_win)
scores.append(xs.xs_zscore(s))
return np.nanmean(scores, axis=0)
print("=== XS03b: Beta-hedged Momentum ===\n")
results = []
# 1. majors, L=30
print("Run 1/5: majors, L=30, beta_win=90, H=10, k=5 LS")
r1 = xs.study_xs(
"XS03b-MAJ-L30",
lambda P: residual_momentum(P, 30, 90),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(r1))
results.append(r1)
# 2. majors, L=60
print("\nRun 2/5: majors, L=60, beta_win=90, H=10, k=5 LS")
r2 = xs.study_xs(
"XS03b-MAJ-L60",
lambda P: residual_momentum(P, 60, 90),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(r2))
results.append(r2)
# 3. all, L=30
print("\nRun 3/5: all, L=30, beta_win=90, H=10, k=5 LS")
r3 = xs.study_xs(
"XS03b-ALL-L30",
lambda P: residual_momentum(P, 30, 90),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(r3))
results.append(r3)
# 4. all, L=60
print("\nRun 4/5: all, L=60, beta_win=90, H=10, k=5 LS")
r4 = xs.study_xs(
"XS03b-ALL-L60",
lambda P: residual_momentum(P, 60, 90),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(r4))
results.append(r4)
# 5. all, blend [30,60]
print("\nRun 5/5: all, blend L=[30,60], beta_win=90, H=10, k=5 LS")
r5 = xs.study_xs(
"XS03b-ALL-BLEND",
lambda P: blend_residual_mom(P, (30, 60), 90),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(r5))
results.append(r5)
# --- Pick best ---
def score_config(r):
"""Priority: earns_slot > holdout_sharpe > full_sharpe > distinctness."""
slot = 1 if r.get("earns_slot") else 0
hs = r["holdout"].get("sharpe", -9)
fs = r["full"]["sharpe"]
corr = r.get("corr_xs01") or 1.0
distinct = 1.0 - abs(corr)
return (slot, hs, fs, distinct)
best = max(results, key=score_config)
print("\n\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+83
View File
@@ -0,0 +1,83 @@
"""XS04b — Ensemble z-vote cross-sectional strategy.
Score = mean of xs_zscore over {momentum90, -vol30, -skew60, -beta60}.
Each component is z-scored cross-sectionally per row, then averaged.
Diversified signal: momentum (strong assets), low vol (stable), negative skew
(avoid lottery stocks), low beta (idiosyncratic leaders).
Grid: universe x H x k — 5 calls max.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_matrix(P: xs.Panel) -> np.ndarray:
"""Ensemble z-vote: mean of four xs_zscored components (causal)."""
# 1. Momentum 90d: higher = stronger recent trend
mom90 = xs.past_return(P.close, 90)
z_mom = xs.xs_zscore(mom90)
# 2. Negative vol 30d: lower vol = more stable = prefer
vol30 = xs.roll_std(P.ret, 30)
z_vol = xs.xs_zscore(-vol30) # negative: lower vol -> higher score
# 3. Negative skew 60d: negative skew = avoid lottery/pump; prefer normal/negative-skew
skew60 = xs.roll_skew(P.ret, 60)
z_skew = xs.xs_zscore(-skew60) # negative: lower skew -> higher score
# 4. Negative beta 60d: low-beta assets have idiosyncratic edge in cross-section
beta60 = xs.roll_beta(P.ret, 60)
z_beta = xs.xs_zscore(-beta60) # negative: lower beta -> higher score
# Ensemble: simple mean across components (NaN-safe per cell)
stack = np.stack([z_mom, z_vol, z_skew, z_beta], axis=0)
score = np.nanmean(stack, axis=0)
return score
# ── Grid (5 calls max) ──────────────────────────────────────────────────────
results = []
# 1. Majors, H=10, k=5, L/S
r = xs.study_xs("XS04b_maj_H10_k5_ls", score_matrix, universe="majors",
H=10, k=5, long_short=True)
print(xs.fmt(r)); print("JSON:", xs.as_json(r))
results.append(r)
# 2. Majors, H=5, k=5, L/S (faster rebalance)
r = xs.study_xs("XS04b_maj_H5_k5_ls", score_matrix, universe="majors",
H=5, k=5, long_short=True)
print(xs.fmt(r)); print("JSON:", xs.as_json(r))
results.append(r)
# 3. All, H=10, k=5, L/S
r = xs.study_xs("XS04b_all_H10_k5_ls", score_matrix, universe="all",
H=10, k=5, long_short=True)
print(xs.fmt(r)); print("JSON:", xs.as_json(r))
results.append(r)
# 4. All, H=10, k=7, L/S (wider book)
r = xs.study_xs("XS04b_all_H10_k7_ls", score_matrix, universe="all",
H=10, k=7, long_short=True)
print(xs.fmt(r)); print("JSON:", xs.as_json(r))
results.append(r)
# 5. Majors, H=10, k=5, long-only (avoid short-side noise)
r = xs.study_xs("XS04b_maj_H10_k5_lo", score_matrix, universe="majors",
H=10, k=5, long_short=False)
print(xs.fmt(r)); print("JSON:", xs.as_json(r))
results.append(r)
# ── Pick best by: earns_slot > holdout > corr_xs01 distance ─────────────────
def score_rep(r):
es = 1 if r.get("earns_slot") else 0
ho = r.get("holdout", {}).get("sharpe", -99)
dist = 1 - abs(r.get("corr_xs01", 1)) # distinctness
return (es, ho, dist)
best = max(results, key=score_rep)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+132
View File
@@ -0,0 +1,132 @@
"""XS05b — Risk-parity momentum (inverse-vol weighted legs).
MECHANISM: Select top-k / bottom-k by plain 60-day momentum (same as XS01),
but instead of equal-weighting within long/short legs, weight each asset by
INVERSE of its own recent volatility (60-day rolling std of daily returns).
This approximates risk-parity within the cross-sectional book: lower-vol
assets get larger weight, so each leg contributes roughly equal risk.
LIMITATION / CAVEAT:
- xslib.study_xs always equal-weights within legs (the score only determines
SELECTION, not position sizing). We cannot pass per-asset weights directly
through the study_xs interface.
- Workaround: encode the inverse-vol signal INTO the score. After selecting
the top-k / bottom-k by momentum rank, the harness will still equal-weight
— but by blending the momentum z-score with the inverse-vol z-score we bias
the SELECTION toward low-vol winners (i.e., the most risk-efficient longs
rank higher). This is a partial approximation: true risk-parity would rescale
weights post-selection; here we rescale the ranking pre-selection.
- The blend is: score = z(mom60) + alpha * z(1/vol60), where alpha=1 gives
equal weight to momentum rank and inverse-vol rank.
GRID (<=5 calls):
1. XS05b-base : majors, H=10, k=5, L=60, alpha=1 (blend)
2. XS05b-all : all (49 alts), H=10, k=5, L=60, alpha=1
3. XS05b-a05 : majors, H=10, k=5, L=60, alpha=0.5 (lighter inv-vol)
4. XS05b-a2 : majors, H=10, k=5, L=60, alpha=2.0 (heavier inv-vol)
5. XS05b-H5 : majors, H=5, k=5, L=60, alpha=1 (faster rebalance)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_xs05b(P, L=60, alpha=1.0):
"""Risk-parity momentum score (causal).
score = z_cross(mom_L) + alpha * z_cross(inv_vol_L)
Higher score -> more risk-efficient momentum winner -> long.
Lower score -> more risk-efficient momentum loser -> short.
"""
# 1. momentum signal (L-day return, causal)
mom = xs.past_return(P.close, L) # (n_days, n_assets), uses close[i-L:i]
z_mom = xs.xs_zscore(mom)
# 2. inverse-vol signal (rolling std of daily returns, causal)
vol = xs.roll_std(P.ret, L) # (n_days, n_assets)
inv_vol = np.where(vol > 0, 1.0 / vol, np.nan)
z_inv_vol = xs.xs_zscore(inv_vol)
# 3. blend
score = z_mom + alpha * z_inv_vol
return score
results = {}
# --- Config 1: majors, H=10, k=5, alpha=1 (baseline blend) ---
rep1 = xs.study_xs(
"XS05b-base",
lambda P: score_xs05b(P, L=60, alpha=1.0),
universe="majors",
H=10, k=5, long_short=True
)
results["XS05b-base"] = rep1
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# --- Config 2: all alts, H=10, k=5, alpha=1 ---
rep2 = xs.study_xs(
"XS05b-all",
lambda P: score_xs05b(P, L=60, alpha=1.0),
universe="all",
H=10, k=5, long_short=True
)
results["XS05b-all"] = rep2
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# --- Config 3: majors, H=10, k=5, alpha=0.5 (lighter inv-vol) ---
rep3 = xs.study_xs(
"XS05b-a05",
lambda P: score_xs05b(P, L=60, alpha=0.5),
universe="majors",
H=10, k=5, long_short=True
)
results["XS05b-a05"] = rep3
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# --- Config 4: majors, H=10, k=5, alpha=2.0 (heavier inv-vol) ---
rep4 = xs.study_xs(
"XS05b-a2",
lambda P: score_xs05b(P, L=60, alpha=2.0),
universe="majors",
H=10, k=5, long_short=True
)
results["XS05b-a2"] = rep4
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# --- Config 5: majors, H=5, k=5, alpha=1 (faster rebalance) ---
rep5 = xs.study_xs(
"XS05b-H5",
lambda P: score_xs05b(P, L=60, alpha=1.0),
universe="majors",
H=5, k=5, long_short=True
)
results["XS05b-H5"] = rep5
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# --- Summary ---
print("=" * 60)
print("SUMMARY — XS05b grid")
print("=" * 60)
fmt_h = f"{'Config':<16} {'FullSh':>7} {'HoldSh':>7} {'MaxDD':>7} {'CorrXS01':>9} {'EarnsSlot':>10} {'Verdict':>10}"
print(fmt_h)
print("-" * 70)
for name, r in results.items():
fs = r["full"]["sharpe"]
hs = r["holdout"]["sharpe"]
dd = r["full"]["maxdd"]
cxs = r.get("corr_xs01", float("nan"))
es = r.get("earns_slot", False)
vd = r.get("marginal", {}).get("verdict", "N/A")
print(f"{name:<16} {fs:>7.2f} {hs:>7.2f} {dd:>7.2f} {cxs:>9.3f} {str(es):>10} {vd:>10}")
+88
View File
@@ -0,0 +1,88 @@
"""XS06b — Correlation-to-market diversifier.
Score = -rolling_corr(asset_ret, market_ret, 60)
Long the assets LEAST correlated to the equal-weight market (the "divergers"),
short the most-correlated ones. win=60 days.
Idea: if cross-sectional momentum (XS01) selects by recent past return, this
selects by structural independence from the pack — a fundamentally different
axis. The two should be weakly correlated.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
import pandas as pd
def score_corr_diversifier(P, win=60):
"""Score = -rolling_corr(asset_ret, market_ret, win). Causal."""
n, A = P.ret.shape
mkt = xs.market_ret(P.ret) # (n,) equal-weight market
out = np.full((n, A), np.nan)
mkt_s = pd.Series(mkt)
for a in range(A):
asset_s = pd.Series(P.ret[:, a])
# rolling correlation — pandas rolling corr is causal
corr = asset_s.rolling(win, min_periods=max(10, win // 2)).corr(mkt_s)
# score = NEGATIVE correlation: higher => less correlated => long
out[:, a] = -corr.values
return out
# ---------------------------------------------------------------------------
# Grid: 5 study_xs calls max
# - vary universe (all vs majors)
# - vary H (rebalance freq)
# - vary long_short
# ---------------------------------------------------------------------------
print("=" * 70)
print("XS06b — Correlation-to-market diversifier (score = -roll_corr_60)")
print("=" * 70)
best = None
best_earns = False
best_ho = -999
configs = [
# (label_suffix, universe, H, k, long_short)
("all_H10_k5_ls", "all", 10, 5, True),
("maj_H10_k5_ls", "majors", 10, 5, True),
("all_H5_k5_ls", "all", 5, 5, True),
("all_H10_k5_lo", "all", 10, 5, False),
("all_H20_k5_ls", "all", 20, 5, True),
]
results = []
for (suffix, universe, H, k, ls) in configs:
name = f"XS06b_{suffix}"
print(f"\n--- {name} ---")
rep = xs.study_xs(
name,
lambda P: score_corr_diversifier(P, win=60),
universe=universe,
H=H,
k=k,
long_short=ls,
target_vol=0.20,
)
print(xs.fmt(rep))
print("JSON:", xs.as_json(rep))
results.append(rep)
# track best: earns_slot first, then hold-out sharpe
earns = rep.get("earns_slot", False)
ho_sh = rep.get("holdout", {}).get("sharpe", -999)
if (earns and not best_earns) or (earns == best_earns and ho_sh > best_ho):
best = rep
best_earns = earns
best_ho = ho_sh
print("\n" + "=" * 70)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+85
View File
@@ -0,0 +1,85 @@
"""XS07b — Trend-quality (R^2) ranking.
IDEA: Score each asset by the R^2 of a linear fit of log-price over the last W bars,
signed by the direction of the trend (positive slope = long candidate).
Score = sign(slope) * R^2
High R^2 + upward slope -> strong smooth uptrend -> long.
High R^2 + downward slope -> strong smooth downtrend -> short.
Low R^2 -> noisy / not trending -> near-zero score.
W=60 canonical, but we try W=30 and W=90 too. The score is CAUSAL: for row i,
we fit on close[i-W+1 .. i] (inclusive), using only past data.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def r2_trend_score(close, W=60):
"""
Per-asset, rolling R^2 of linear fit on log(close), signed by slope direction.
Returns (n_days x n_assets) matrix. Causal: row i uses close[i-W+1..i].
"""
n, A = close.shape
out = np.full((n, A), np.nan)
x = np.arange(W, dtype=float)
x -= x.mean() # center x for numerical stability
xss = (x ** 2).sum()
for i in range(W - 1, n):
log_p = np.log(close[i - W + 1: i + 1, :]) # (W, A)
# For each asset: fit log_p = a + b*x
# b = cov(x, log_p) / var(x)
mean_y = log_p.mean(axis=0) # (A,)
b = (x[:, None] * (log_p - mean_y)).sum(axis=0) / xss # (A,)
y_hat = x[:, None] * b + mean_y # (W, A)
ss_res = ((log_p - y_hat) ** 2).sum(axis=0)
ss_tot = ((log_p - mean_y) ** 2).sum(axis=0)
r2 = np.where(ss_tot > 0, 1.0 - ss_res / ss_tot, 0.0)
# Score = sign(slope) * R^2. Ranges in [-1, 1].
out[i] = np.sign(b) * r2
return out
def make_score_fn(W=60):
def score_fn(P):
return r2_trend_score(P.close, W=W)
return score_fn
if __name__ == "__main__":
# Grid: (W, universe, H, k, long_short)
# Keep <= 5 backtests total
configs = [
# Canonical: W=60, all assets, H=10, k=5, LS
dict(name="XS07b_W60_all_H10_k5_LS", W=60, universe="all", H=10, k=5, long_short=True),
# Shorter trend window
dict(name="XS07b_W30_all_H10_k5_LS", W=30, universe="all", H=10, k=5, long_short=True),
# Longer trend window
dict(name="XS07b_W90_all_H10_k5_LS", W=90, universe="all", H=10, k=5, long_short=True),
# Majors only (less noisy universe)
dict(name="XS07b_W60_maj_H10_k5_LS", W=60, universe="majors", H=10, k=5, long_short=True),
# Long-only variant (majors)
dict(name="XS07b_W60_maj_H10_k3_LO", W=60, universe="majors", H=10, k=3, long_short=False),
]
best_rep = None
best_key = (-999, -999, False) # (earns_slot, hold_sharpe, robust_oos)
for cfg in configs:
W = cfg.pop("W")
name = cfg.pop("name")
rep = xs.study_xs(name, make_score_fn(W=W), **cfg)
print(xs.fmt(rep))
hold_sh = rep["holdout"].get("sharpe", -999)
earns = int(rep["earns_slot"])
robust = int(rep["marginal"].get("robust_oos", False))
key = (earns, robust, hold_sh)
if key > best_key:
best_key = key
best_rep = rep
print("\n=== BEST CONFIG ===")
print(xs.fmt(best_rep))
print("JSON:", xs.as_json(best_rep))
+125
View File
@@ -0,0 +1,125 @@
"""XS08b — Lead-lag vs BTC.
IDEA: Score = past_return(alt, L=10) of alts CONDITIONAL on BTC having risen over the same
window. The hypothesis: alts that lagged BTC during a BTC up-move will catch up.
Score at bar i:
btc_ret_L = BTC.close[i] / BTC.close[i-L] - 1 (BTC rose L days ago to now)
alt_ret_L = alt.close[i] / alt.close[i-L] - 1 (how much alt has moved)
If btc_ret_L > 0:
score = alt_ret_L (lag = low score -> buy the laggards -> REVERSE ranking needed)
Actually: we want alts that HAVEN'T moved yet, i.e. low alt_ret when BTC is up.
So score = -alt_ret_L (lower alt return during BTC up = more upside potential).
If btc_ret_L <= 0:
score = NaN (flat; no lead-lag expected when BTC is down).
Alternative formulation (XS08b-v2): score = btc_ret - alt_ret (gap; higher = more lag = more catch-up).
Grid (<=5 calls):
1. L=10, majors, H=10, k=5, long_short=True — baseline
2. L=10, majors, H=5, k=5, long_short=True — faster rebalance
3. L=10, "all", H=10, k=5, long_short=True — wider universe
4. L=10, majors, H=10, k=5, long_short=False — long-only variant
5. L=20, majors, H=10, k=5, long_short=True — longer lookback
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ---------------------------------------------------------------------------
# Score factory
# ---------------------------------------------------------------------------
def make_score(L=10):
"""Score: BTC-alt gap during BTC up-moves. Causal."""
def score_fn(P: xs.Panel) -> np.ndarray:
syms = P.syms
n, A = P.close.shape
# BTC column index (BTC should be in the majors panel)
if "BTC" not in syms:
raise ValueError("BTC not in panel — use 'majors' or a universe containing BTC")
btc_idx = syms.index("BTC")
# past return over L days (causal)
pr = xs.past_return(P.close, L) # (n, A)
btc_pr = pr[:, btc_idx] # (n,) BTC L-day return
# score = BTC_return - alt_return (gap; higher gap = alt lagged more = more catch-up)
# Only when BTC is up (btc_pr > 0); else NaN (flat)
score = np.full((n, A), np.nan)
btc_up = btc_pr > 0 # (n,) boolean mask
gap = btc_pr[:, None] - pr # (n, A): positive when alt lagged BTC
score[btc_up] = gap[btc_up]
return score
return score_fn
# ---------------------------------------------------------------------------
# Grid
# ---------------------------------------------------------------------------
results = []
print("=" * 60)
print("XS08b — Lead-lag vs BTC")
print("=" * 60)
# 1. Baseline: L=10, majors, H=10, k=5, long_short
print("\n[1/5] L=10, majors, H=10, k=5, long_short=True")
r1 = xs.study_xs("XS08b-base", make_score(L=10),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# 2. Faster rebalance: H=5
print("\n[2/5] L=10, majors, H=5, k=5, long_short=True")
r2 = xs.study_xs("XS08b-H5", make_score(L=10),
universe="majors", H=5, k=5, long_short=True)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# 3. Wider universe: all
print("\n[3/5] L=10, all, H=10, k=5, long_short=True")
r3 = xs.study_xs("XS08b-all", make_score(L=10),
universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# 4. Long-only: majors, H=10
print("\n[4/5] L=10, majors, H=10, k=5, long_short=False")
r4 = xs.study_xs("XS08b-LO", make_score(L=10),
universe="majors", H=10, k=5, long_short=False)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# 5. Longer lookback: L=20
print("\n[5/5] L=20, majors, H=10, k=5, long_short=True")
r5 = xs.study_xs("XS08b-L20", make_score(L=20),
universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# ---------------------------------------------------------------------------
# Pick best by: earns_slot first, then hold-out Sharpe, then corr_xs01 < 0.6
# ---------------------------------------------------------------------------
def score_result(r):
earns = r.get("earns_slot", False)
ho = (r.get("holdout") or {}).get("sharpe", -999)
full = (r.get("full") or {}).get("sharpe", -999)
corr = r.get("corr_xs01", 1.0)
distinct = corr is None or abs(corr) < 0.6
return (int(earns), int(distinct and ho > 0 and full > 0), ho)
best = max(results, key=score_result)
print("\n" + "=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+102
View File
@@ -0,0 +1,102 @@
"""XU01 — Momentum Universe Sweep
MECHANISM: Best momentum z-blend (blend of past_return z-scores at L=30 and L=90),
run on different universe sizes: majors (19), top20, top30, all (49).
Goal: map where cross-sectional momentum alpha lives — does expanding to top20/top30/all
help or hurt vs the tight 19-major universe of XS01?
Grid (<=5 backtests):
1. majors (19) — baseline reference, should approach XS01
2. top20 — add one more liquid alt
3. top30 — mid-tier liquidity
4. all (49) — known to dilute (confirm)
5. top30, long-only (best mid-tier config variant)
Signal: xs_zscore(past_return(close,30)) + xs_zscore(past_return(close,90)) — same blend as XS01.
H=10, k=5, long_short=True (except run 5 long-only).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XU01 — Momentum Universe Sweep")
print("=" * 60)
def blend_score(P):
"""Z-blend of 30d and 90d momentum — same signal as XS01 but on any universe."""
z30 = xs.xs_zscore(xs.past_return(P.close, 30))
z90 = xs.xs_zscore(xs.past_return(P.close, 90))
return np.nanmean(np.stack([z30, z90], axis=0), axis=0)
# 1) Majors (19) — baseline
rep1 = xs.study_xs(
"XU01_MAJORS",
blend_score,
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Top-20 by $-volume
rep2 = xs.study_xs(
"XU01_TOP20",
blend_score,
universe=20, H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Top-30 by $-volume
rep3 = xs.study_xs(
"XU01_TOP30",
blend_score,
universe=30, H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) All (49) — expected dilution
rep4 = xs.study_xs(
"XU01_ALL",
blend_score,
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Top-30, long-only — does dropping the short leg help with mid-tier names?
rep5 = xs.study_xs(
"XU01_TOP30_LO",
blend_score,
universe=30, H=10, k=5, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Pick best: prefer earns_slot, then hold-out sharpe, then full sharpe, then distinctness
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r.get("earns_slot", False))
hold_sh = (r.get("holdout") or {}).get("sharpe", -9)
full_sh = (r.get("full") or {}).get("sharpe", -9)
corr_xs01 = r.get("corr_xs01") or 1.0
distinctness = 1 - abs(corr_xs01)
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+120
View File
@@ -0,0 +1,120 @@
"""XU02 — Rebalance/Holding Sweep (Low-Vol + Momentum)
MECHANISM: Study how holding period H and portfolio size k interact with signal quality.
Two signals: (1) pure momentum blend (z30+z90 same as XS01), (2) low-vol rank (short volatile, long stable).
Goal: find whether a DIFFERENT H/k pair or signal gives something DISTINCT from XS01.
Hypothesis:
- XS01 uses H=10, k=5 (momentum). A longer H reduces turnover, captures slower signal decay.
- Low-vol selection (long stable alts, short volatile ones) is conceptually orthogonal to momentum.
- Sweep: H in {5,10,20,30}, k in {3,5,8}, signal in {momentum, low_vol}.
- Keep <=5 backtests: focus on the most contrasting configs.
Grid (5 backtests):
1. MOM H=5 k=5 — fast rebalance, same as XS01 direction, more turnover
2. MOM H=30 k=5 — slow rebalance, lower turnover, tests signal persistence
3. LVOL H=10 k=5 — low-vol signal at standard H/k (conceptually distinct from momentum)
4. LVOL H=20 k=5 — low-vol with slower rebalance
5. LVOL H=10 k=3 — low-vol tight portfolio, more concentrated
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XU02 — Rebalance/Holding Sweep (Low-Vol + Momentum)")
print("=" * 60)
def mom_blend(P):
"""Z-blend of 30d and 90d momentum (same signal as XS01)."""
z30 = xs.xs_zscore(xs.past_return(P.close, 30))
z90 = xs.xs_zscore(xs.past_return(P.close, 90))
return np.nanmean(np.stack([z30, z90], axis=0), axis=0)
def low_vol(P):
"""Low-vol signal: score = -rolling_std(ret, 30). Higher score = lower vol = long.
Cross-sectionaly ranks alts: stable (low realized vol) go long, volatile go short."""
rv = xs.roll_std(P.ret, 30)
return -rv # negate: higher = lower vol = prefer long
# 1) Momentum H=5 k=5 — fast rebalance (more turnover, tests short-term signal)
rep1 = xs.study_xs(
"XU02_MOM_H5k5",
mom_blend,
universe="majors", H=5, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Momentum H=30 k=5 — slow rebalance, lower turnover, tests signal persistence
rep2 = xs.study_xs(
"XU02_MOM_H30k5",
mom_blend,
universe="majors", H=30, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Low-vol H=10 k=5 — standard H/k but conceptually distinct signal
rep3 = xs.study_xs(
"XU02_LVOL_H10k5",
low_vol,
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Low-vol H=20 k=5 — slower rebalance, low-vol is a structural trait (changes slowly)
rep4 = xs.study_xs(
"XU02_LVOL_H20k5",
low_vol,
universe="majors", H=20, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Low-vol H=10 k=3 — tighter portfolio (top/bottom 3 most extreme)
rep5 = xs.study_xs(
"XU02_LVOL_H10k3",
low_vol,
universe="majors", H=10, k=3, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Summary
print("=" * 60)
print("SUMMARY: All 5 configs ranked by hold-out Sharpe")
all_reps = [rep1, rep2, rep3, rep4, rep5]
ranked = sorted(all_reps, key=lambda r: r["holdout"].get("sharpe", -999), reverse=True)
for r in ranked:
h_sh = r["holdout"].get("sharpe", 0)
f_sh = r["full"]["sharpe"]
c_xs01 = r["corr_xs01"]
verdict = r["marginal"].get("verdict", "N/A")
earns = r["earns_slot"]
print(f" {r['name']:25s} FULL={f_sh:+.2f} HOLD={h_sh:+.2f} corr_xs01={c_xs01} "
f"verdict={verdict} earns_slot={earns}")
# Pick best by marginal robustness -> earns_slot -> hold-out -> distinctness
best = None
for r in ranked:
if r["earns_slot"]:
best = r
break
if best is None:
# fallback: best hold-out with corr_xs01 < 0.6
candidates = [r for r in ranked if (r.get("corr_xs01") or 1.0) < 0.6 and r["holdout"].get("sharpe", 0) > 0]
best = candidates[0] if candidates else ranked[0]
print()
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+137
View File
@@ -0,0 +1,137 @@
"""XU03 — Long-Only Top-k (Alt Selection)
MECHANISM: Low-vol / momentum LONG-ONLY top-k alt selection.
- NOT market-neutral: goes long only the top-k alts by combined score, flat otherwise.
- Captures alt-beta + selection effect (distinct from XS01 which is market-neutral).
- Executable at small capital (k legs, no short book needed).
Signal: blend of momentum (z30+z90) and low-vol (-rv30) in a composite score.
The combined signal selects alts that are trending UP and relatively stable.
long_short=False -> long-only top-k, no short leg.
Grid (5 backtests):
1. MOM_LO H=10 k=5 universe=majors — baseline long-only momentum
2. MOM_LO H=10 k=5 universe=all — wider universe, more selection power
3. COMBO H=10 k=5 universe=majors — blend momentum + low-vol (composite)
4. COMBO H=20 k=5 universe=majors — slower rebalance, lower turnover
5. COMBO H=10 k=3 universe=majors — tighter portfolio (top-3 only)
Hypothesis: long-only selection will have high corr_tp01 (market beta) but low corr_xs01
(market-neutral XS01 cancels market beta). If the composite score selects quality alts that
outperform TP01 (BTC/ETH only), it adds informational value.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XU03 — Long-Only Top-k (Alt Selection: Momentum + Low-Vol Composite)")
print("=" * 70)
def mom_blend(P):
"""Z-blend of 30d and 90d momentum — same signal as XS01 but long-only."""
z30 = xs.xs_zscore(xs.past_return(P.close, 30))
z90 = xs.xs_zscore(xs.past_return(P.close, 90))
return np.nanmean(np.stack([z30, z90], axis=0), axis=0)
def combo_score(P):
"""Composite: momentum blend + low-vol preference.
Selects alts that are trending up AND have lower realized volatility.
Both components are cross-sectionally z-scored before blending.
"""
# Momentum: 30d + 90d blend
z30 = xs.xs_zscore(xs.past_return(P.close, 30))
z90 = xs.xs_zscore(xs.past_return(P.close, 90))
z_mom = np.nanmean(np.stack([z30, z90], axis=0), axis=0)
# Low-vol: prefer stable alts (negate RV so higher = lower vol = preferred)
rv = xs.roll_std(P.ret, 30)
z_lvol = xs.xs_zscore(-rv)
# Equal blend: 50% momentum + 50% low-vol
combo = np.nanmean(np.stack([z_mom, z_lvol], axis=0), axis=0)
return combo
# 1) Pure momentum long-only, majors universe — baseline
rep1 = xs.study_xs(
"XU03_MOM_LO_H10k5_majors",
mom_blend,
universe="majors", H=10, k=5, long_short=False
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Pure momentum long-only, all universe — tests wider selection
rep2 = xs.study_xs(
"XU03_MOM_LO_H10k5_all",
mom_blend,
universe="all", H=10, k=5, long_short=False
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Composite (mom + low-vol) long-only, majors — main hypothesis
rep3 = xs.study_xs(
"XU03_COMBO_H10k5_majors",
combo_score,
universe="majors", H=10, k=5, long_short=False
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Composite, slower rebalance H=20 — lower turnover, more patient selection
rep4 = xs.study_xs(
"XU03_COMBO_H20k5_majors",
combo_score,
universe="majors", H=20, k=5, long_short=False
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Composite, tighter k=3 — more concentrated, highest-conviction picks only
rep5 = xs.study_xs(
"XU03_COMBO_H10k3_majors",
combo_score,
universe="majors", H=10, k=3, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Summary
print("=" * 70)
print("SUMMARY: All 5 configs ranked by hold-out Sharpe")
all_reps = [rep1, rep2, rep3, rep4, rep5]
ranked = sorted(all_reps, key=lambda r: r["holdout"].get("sharpe", -999), reverse=True)
for r in ranked:
h_sh = r["holdout"].get("sharpe", 0)
f_sh = r["full"]["sharpe"]
c_xs01 = r["corr_xs01"]
c_tp01 = r["corr_tp01"]
verdict = r["marginal"].get("verdict", "N/A")
earns = r["earns_slot"]
print(f" {r['name']:35s} FULL={f_sh:+.2f} HOLD={h_sh:+.2f} "
f"corr_xs01={c_xs01:+.2f} corr_tp01={c_tp01:+.2f} "
f"verdict={verdict} earns_slot={earns}")
# Pick best config by: earns_slot first, then hold-out > 0 + distinct, then hold-out
best = None
for r in ranked:
if r["earns_slot"]:
best = r
break
if best is None:
candidates = [r for r in ranked
if (r.get("corr_xs01") or 1.0) < 0.6 and r["holdout"].get("sharpe", 0) > 0]
best = candidates[0] if candidates else ranked[0]
print()
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+138
View File
@@ -0,0 +1,138 @@
"""XU04 — Liquidity-filtered momentum
MECHANISM: Cross-sectional momentum, but restrict to the top-N assets by RECENT (rolling 60d)
median dollar-volume rather than the static all-panel. The idea: momentum signal is cleaner on
liquid names; illiquid tail adds noise. Compare:
1. Dynamic top-20 by rolling $-vol (vs static top-20 from XU01)
2. Dynamic top-20, adjusted momentum (skip 1d to reduce microstructure noise): L=2..31
3. Static majors (19) with skip-1 momentum — XS01-style but skip-1 to reduce echo
4. Dynamic top-25 rolling-liquidity blend [30,90] — slightly wider universe
5. Dynamic top-20 rolling-liquidity blend [30,90], H=5 (faster rebalance)
Key difference from XS01: the UNIVERSE is determined dynamically (rolling 60d dollar-volume
rank) rather than the fixed 19-major list. This may improve distinctness and resilience to
liquidity shifts.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XU04 — Liquidity-filtered momentum")
print("=" * 60)
def rolling_liq_score(P, lookbacks=(30, 90), skip=0):
"""Momentum blend on the panel, with optional skip-1 for microstructure."""
scores = []
for L in lookbacks:
if skip > 0:
# use close[i-skip] / close[i-skip-L] - 1 (causal, skip most recent bars)
c = P.close
out = np.full_like(c, np.nan)
# at row i: return from i-L-skip to i-skip
for i in range(L + skip, len(c)):
out[i] = c[i - skip] / c[i - L - skip] - 1.0
else:
out = xs.past_return(P.close, L)
scores.append(xs.xs_zscore(out))
return np.nanmean(np.stack(scores, axis=0), axis=0)
def score_dyn20_blend(P):
"""Dynamic top-20 by rolling $-vol — blend [30,90], no skip."""
# P already filtered to top-20 by static median; this fn gets whatever panel is loaded.
# We do dynamic re-weighting via volume z-score gating:
# compute rolling 60d dollar volume rank per asset; assets below median get half-weight score
dv = P.close * P.vol # dollar volume matrix (n_days x n_assets)
dv_roll = xs.roll_mean(dv, 60) # rolling 60d mean $-vol
# rank liquidity cross-sectionally
liq_rank = xs.xs_rank(dv_roll) # 0..1, higher = more liquid
# momentum signal
mom = rolling_liq_score(P, lookbacks=(30, 90), skip=0)
# attenuate score of less-liquid assets (liq_rank < 0.5 -> half score)
liq_weight = np.where(liq_rank >= 0.5, 1.0, 0.5)
return mom * liq_weight
def score_skip1(P):
"""Majors, momentum blend [30,90] with 1-day skip (microstructure reduction)."""
return rolling_liq_score(P, lookbacks=(30, 90), skip=1)
def score_top25_blend(P):
"""Top-25 universe, plain blend [30,90]."""
return rolling_liq_score(P, lookbacks=(30, 90), skip=0)
def score_dyn20_fast(P):
"""Dynamic top-20 + blend [30,90], faster H=5 rebalance."""
return score_dyn20_blend(P)
# 1) Top-20 with dynamic liquidity weighting, H=10, k=5
rep1 = xs.study_xs(
"XU04_DYN20_H10",
score_dyn20_blend,
universe=20, H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Majors (19) with skip-1 momentum — reduces microstructure vs XS01
rep2 = xs.study_xs(
"XU04_MAJ_SKIP1",
score_skip1,
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Top-20 plain blend [30,90] no weighting, H=10 (clean baseline vs XU01)
rep3 = xs.study_xs(
"XU04_TOP20_PLAIN",
lambda P: rolling_liq_score(P, lookbacks=(30, 90), skip=0),
universe=20, H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Top-25, blend [30,90], H=10
rep4 = xs.study_xs(
"XU04_TOP25_H10",
score_top25_blend,
universe=25, H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Top-20 dynamic liq-weighted, H=5 (faster)
rep5 = xs.study_xs(
"XU04_DYN20_H5",
score_dyn20_fast,
universe=20, H=5, k=5, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Pick best
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r.get("earns_slot", False))
hold_sh = (r.get("holdout") or {}).get("sharpe", -9)
full_sh = (r.get("full") or {}).get("sharpe", -9)
corr_xs01 = r.get("corr_xs01") or 1.0
distinctness = 1 - abs(corr_xs01)
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+83
View File
@@ -0,0 +1,83 @@
"""XV01 — Low Realized-Volatility Anomaly
MECHANISM: Score = -roll_std(ret, W) (long low-vol / short high-vol alts).
The low-vol anomaly: lower-volatility assets tend to outperform on a risk-adjusted basis.
Grid: W in {20, 30, 60}; universe in {all, majors}; long-short AND long-only.
Goal: find a DISTINCT signal from XS01 (plain momentum) that ADDS to the live portfolio.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XV01 — Low Realized-Volatility Anomaly")
print("=" * 60)
# --- 5 targeted backtests ---
# 1) Full universe, W=20 (short-term vol), LS — baseline low-vol on all alts
rep1 = xs.study_xs(
"XV01_ALL_W20_LS",
lambda P: -xs.roll_std(P.ret, 20),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) Full universe, W=30, LS — medium-window vol (the main hypothesis)
rep2 = xs.study_xs(
"XV01_ALL_W30_LS",
lambda P: -xs.roll_std(P.ret, 30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Full universe, W=60, LS — longer-window vol
rep3 = xs.study_xs(
"XV01_ALL_W60_LS",
lambda P: -xs.roll_std(P.ret, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) Majors only (19), W=30, LS — smaller universe, less noise
rep4 = xs.study_xs(
"XV01_MAJORS_W30_LS",
lambda P: -xs.roll_std(P.ret, 30),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) Full universe, W=30, long-only top-k (lowest vol, long only)
# The "defensive" alt selection: pick alts with lowest realized vol
rep5 = xs.study_xs(
"XV01_ALL_W30_LO",
lambda P: -xs.roll_std(P.ret, 30),
universe="all", H=10, k=5, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# Pick best: prefer earns_slot, then hold-out sharpe, then full sharpe, then distinctness from XS01
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9)
full_sh = r["full"]["sharpe"]
corr_xs01 = r["corr_xs01"] if r["corr_xs01"] is not None else 1.0
distinctness = 1 - abs(corr_xs01) # higher = more distinct
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+121
View File
@@ -0,0 +1,121 @@
"""XV02 — Low Idiosyncratic Volatility Anomaly.
Score = -roll_std(residual_return(ret, beta_win=60), 30)
(negative idiosyncratic volatility: low idio-vol = long, high idio-vol = short).
Distinct from total-vol because we strip the market factor first (beta*mkt),
keeping only the firm-specific noise. In equities this is the "low idio-vol" anomaly
(Ang et al. 2006): low idiosyncratic volatility stocks outperform. Testing if the
same holds cross-sectionally on the HL alt panel.
Grid (<=5 calls total):
1. majors H=10 k=5 LS (baseline)
2. all H=10 k=5 LS (broader universe)
3. majors H=5 k=5 LS (faster rebalance)
4. majors H=10 k=4 LS (narrower book)
5. majors H=10 k=5 LS, shorter beta window (30d) [to test sensitivity]
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
# ---------------------------------------------------------------------------
# SCORE: negative idiosyncratic vol over last 30 days
# (idio ret = ret - rolling_beta_60d * market_ret, then 30d rolling std)
# ---------------------------------------------------------------------------
def score_idiovol(P, beta_win=60, vol_win=30):
"""Low idiosyncratic volatility score (higher = lower idio-vol = long)."""
idio = xs.residual_return(P.ret, beta_win) # n_days x n_assets
idio_vol = xs.roll_std(idio, vol_win) # rolling std of idio ret
# negate: lower vol → higher score → long
return -idio_vol
results = []
# ---- 1. Baseline: majors, H=10, k=5, LS (beta_win=60, vol_win=30) ----
rep1 = xs.study_xs(
"XV02_majors_H10k5",
lambda P: score_idiovol(P, beta_win=60, vol_win=30),
universe="majors",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
results.append(rep1)
# ---- 2. Broader universe: all, H=10, k=5, LS ----
rep2 = xs.study_xs(
"XV02_all_H10k5",
lambda P: score_idiovol(P, beta_win=60, vol_win=30),
universe="all",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
results.append(rep2)
# ---- 3. Faster rebalance: majors, H=5, k=5, LS ----
rep3 = xs.study_xs(
"XV02_majors_H5k5",
lambda P: score_idiovol(P, beta_win=60, vol_win=30),
universe="majors",
H=5, k=5, long_short=True,
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
results.append(rep3)
# ---- 4. Narrower book: majors, H=10, k=4, LS ----
rep4 = xs.study_xs(
"XV02_majors_H10k4",
lambda P: score_idiovol(P, beta_win=60, vol_win=30),
universe="majors",
H=10, k=4, long_short=True,
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
results.append(rep4)
# ---- 5. Shorter beta window: majors, H=10, k=5, LS, beta_win=30 ----
rep5 = xs.study_xs(
"XV02_majors_H10k5_bw30",
lambda P: score_idiovol(P, beta_win=30, vol_win=30),
universe="majors",
H=10, k=5, long_short=True,
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
results.append(rep5)
# ---- Summary ----
print("\n=== XV02 GRID SUMMARY ===")
for r in results:
earns = r["earns_slot"]
print(
f" {r['name']:30s} FULL {r['full']['sharpe']:+.2f} "
f"HOLD {r['holdout'].get('sharpe', 0):+.2f} "
f"corr_xs01 {r['corr_xs01']} "
f"marginal={r['marginal']['verdict']} "
f"earns_slot={earns}"
)
# ---- Best config: pick by earns_slot first, then hold-out ----
earners = [r for r in results if r["earns_slot"]]
if earners:
best = max(earners, key=lambda r: r["holdout"].get("sharpe", -999))
print(f"\nBEST (earns_slot): {best['name']}")
else:
# fallback: best hold-out Sharpe with distinct XS01 corr
distinct = [r for r in results if (r["corr_xs01"] or 1.0) < 0.6]
if distinct:
best = max(distinct, key=lambda r: r["holdout"].get("sharpe", -999))
else:
best = max(results, key=lambda r: r["holdout"].get("sharpe", -999))
print(f"\nBEST (hold-out, no earns_slot): {best['name']}")
print("\n--- BEST CONFIG ---")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+106
View File
@@ -0,0 +1,106 @@
"""XV03 — Betting Against Beta (BAB) — Low-beta anomaly
MECHANISM: Score = -roll_beta(ret, W) (long low-beta alts / short high-beta alts).
The BAB anomaly (Frazzini & Pedersen 2014): within an asset cross-section, lower-beta
assets deliver higher risk-adjusted returns — because levered/constrained investors
bid up high-beta assets above fair value. Score = NEGATIVE rolling beta to equal-weight
market, so top-ranked = lowest beta.
Grid: beta window W in {30, 60}; universe in {all, majors}; long-short.
Also test: blend BAB + dispersion condition (only enter if cross-sectional vol is high).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
print("XV03 — Betting Against Beta (BAB)")
print("=" * 60)
# --------------------------------------------------------------------------
# Score functions
# --------------------------------------------------------------------------
def score_bab(ret, win):
"""BAB score: negative rolling beta to equal-weight market (causal)."""
beta = xs.roll_beta(ret, win) # (n,A): higher = more market exposure
return -beta # higher score = lower beta = long candidate
def score_bab_beta_adj(ret, win):
"""BAB score adjusted: z-score the negative beta cross-sectionally."""
return xs.xs_zscore(-xs.roll_beta(ret, win))
# --------------------------------------------------------------------------
# Run 5 targeted backtests
# --------------------------------------------------------------------------
# 1) All alts, W=30 (shorter window — more reactive), LS
rep1 = xs.study_xs(
"XV03_ALL_W30_LS",
lambda P: score_bab(P.ret, 30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()
# 2) All alts, W=60 (longer window — more stable beta estimates), LS
rep2 = xs.study_xs(
"XV03_ALL_W60_LS",
lambda P: score_bab(P.ret, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()
# 3) Majors only (19 XS01 assets), W=60, LS
# Cleaner universe: major liquid alts, should reduce noise
rep3 = xs.study_xs(
"XV03_MAJORS_W60_LS",
lambda P: score_bab(P.ret, 60),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()
# 4) All alts, W=60, XS-zscored BAB, shorter rebalance H=5
# XS z-score normalizes the beta signal each day cross-sectionally
rep4 = xs.study_xs(
"XV03_ALL_W60_ZS_H5",
lambda P: score_bab_beta_adj(P.ret, 60),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()
# 5) BAB blend: combine W=30 and W=60 betas (multi-horizon, inspired by XS01 blend)
# Average the two z-scored BAB signals
rep5 = xs.study_xs(
"XV03_ALL_BLEND3060_LS",
lambda P: xs.xs_zscore(score_bab(P.ret, 30)) + xs.xs_zscore(score_bab(P.ret, 60)),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()
# --------------------------------------------------------------------------
# Pick best: earns_slot > hold-out sharpe > distinctness from XS01
# --------------------------------------------------------------------------
all_reps = [rep1, rep2, rep3, rep4, rep5]
def score_rep(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -9)
full_sh = r["full"]["sharpe"]
corr_xs01 = r["corr_xs01"] if r["corr_xs01"] is not None else 1.0
distinctness = 1 - abs(corr_xs01)
return (earns, hold_sh, full_sh, distinctness)
best = max(all_reps, key=score_rep)
print("=" * 60)
print(f"BEST CONFIG: {best['name']}")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+88
View File
@@ -0,0 +1,88 @@
"""XV04 — Low Downside-Vol / Semivariance
Score = -roll_std(min(ret, 0), W)
Only downside dispersion is penalized; upside is irrelevant.
Buy lowest semivariance (most defensive), short highest.
W=30 canonical; small grid over universe/H/k.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_xv04(P, W=30):
"""Score = -roll_std(min(ret, 0), W).
Causal: each row uses only past W returns.
Higher score = lower downside vol = more preferred (long).
"""
# clip positive returns to 0 so only downside contributes
down = np.minimum(P.ret, 0.0)
# rolling std of downside returns (semideviation)
semi = xs.roll_std(down, W)
# negate: lower semideviation = higher score = long bias
return -semi
# --- Grid: 5 studies max ---
# 1) Canonical: all universe, W=30, H=10, k=5, L/S
rep1 = xs.study_xs(
"XV04-W30-H10-k5-LS",
lambda P: score_xv04(P, W=30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
# 2) Majors universe (higher liquidity, 19 assets)
rep2 = xs.study_xs(
"XV04-W30-H10-k5-LS-majors",
lambda P: score_xv04(P, W=30),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
# 3) Longer window W=60, all universe
rep3 = xs.study_xs(
"XV04-W60-H10-k5-LS",
lambda P: score_xv04(P, W=60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
# 4) W=30, faster rebalance H=5
rep4 = xs.study_xs(
"XV04-W30-H5-k5-LS",
lambda P: score_xv04(P, W=30),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
# 5) W=30, k=3 (more concentrated)
rep5 = xs.study_xs(
"XV04-W30-H10-k3-LS",
lambda P: score_xv04(P, W=30),
universe="all", H=10, k=3, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
# Pick best by: earns_slot first, then holdout Sharpe, then distinctness
reps = [rep1, rep2, rep3, rep4, rep5]
earners = [r for r in reps if r["earns_slot"]]
if earners:
best = max(earners, key=lambda r: r["holdout"].get("sharpe", -999))
else:
# fallback: positive full + hold-out + corr_xs01 < 0.6
candidates = [r for r in reps if r["full"]["sharpe"] > 0 and r["holdout"].get("sharpe", 0) > 0
and (r["corr_xs01"] or 1.0) < 0.6]
if candidates:
best = max(candidates, key=lambda r: r["holdout"].get("sharpe", -999))
else:
best = max(reps, key=lambda r: r["full"]["sharpe"])
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+101
View File
@@ -0,0 +1,101 @@
"""XV05 — Low Max-Drawdown Anomaly
Score = -rolling_maxdrawdown(close, W) over the past W bars.
Prefer assets with smooth price history (low drawdown) for long,
prefer highly-drawn-down assets for short.
Grid: vary W (30, 60, 90), universe (majors, all), H (10).
<=5 study_xs calls total.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def rolling_maxdd(close, W):
"""Causal rolling max-drawdown over the past W bars.
At row i: max drawdown of the window [i-W+1 .. i].
Returns matrix (n_days x n_assets). NaN for first W-1 rows.
Higher = worse drawdown (more negative).
"""
n, A = close.shape
out = np.full((n, A), np.nan)
for i in range(W - 1, n):
window = close[i - W + 1: i + 1] # shape (W, A) — causal: data <= i
# rolling peak up to each bar within window
peak = np.maximum.accumulate(window, axis=0)
dd = (window - peak) / peak # drawdown at each bar (<=0)
out[i] = np.nanmin(dd, axis=0) # worst drawdown in window (most negative)
return out
def score_fn_w60(P):
"""Score = -maxDD(W=60): prefer LOW drawdown (smooth equity)."""
return -rolling_maxdd(P.close, 60)
def score_fn_w30(P):
"""Score = -maxDD(W=30): shorter memory."""
return -rolling_maxdd(P.close, 30)
def score_fn_w90(P):
"""Score = -maxDD(W=90): longer memory."""
return -rolling_maxdd(P.close, 90)
def score_fn_w60_blend(P):
"""Blend: average score from W=30 and W=90 (multi-horizon like XS01 blend)."""
s30 = -rolling_maxdd(P.close, 30)
s90 = -rolling_maxdd(P.close, 90)
return xs.xs_zscore(s30) + xs.xs_zscore(s90)
if __name__ == "__main__":
print("=== XV05: Low Max-Drawdown Anomaly ===\n")
# Run 1: canonical W=60, majors universe, H=10, k=5, long-short
print("--- Run 1: W=60, majors, H=10, k=5, LS ---")
r1 = xs.study_xs("XV05-W60-maj", score_fn_w60, universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r1))
print()
# Run 2: W=60, all universe (49 alts)
print("--- Run 2: W=60, all, H=10, k=5, LS ---")
r2 = xs.study_xs("XV05-W60-all", score_fn_w60, universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r2))
print()
# Run 3: W=30, majors
print("--- Run 3: W=30, majors, H=10, k=5, LS ---")
r3 = xs.study_xs("XV05-W30-maj", score_fn_w30, universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r3))
print()
# Run 4: W=90, majors
print("--- Run 4: W=90, majors, H=10, k=5, LS ---")
r4 = xs.study_xs("XV05-W90-maj", score_fn_w90, universe="majors", H=10, k=5, long_short=True)
print(xs.fmt(r4))
print()
# Run 5: blend W=30+W=90, all universe
print("--- Run 5: Blend W30+W90, all, H=10, k=5, LS ---")
r5 = xs.study_xs("XV05-BLENDall", score_fn_w60_blend, universe="all", H=10, k=5, long_short=True)
print(xs.fmt(r5))
print()
# Pick best by earns_slot, then hold-out sharpe, then distinctness from XS01
results = [r1, r2, r3, r4, r5]
names = ["W60-maj", "W60-all", "W30-maj", "W90-maj", "Blend-all"]
def score_key(r):
earns = 1 if r["earns_slot"] else 0
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"]["sharpe"]
xs01_corr = abs(r["corr_xs01"] or 1.0)
return (earns, hold_sh, full_sh, -xs01_corr)
best = max(results, key=score_key)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+89
View File
@@ -0,0 +1,89 @@
"""XV06 — Low Vol-of-Vol (stability of volatility)
Score = -roll_std(roll_std(ret, inner_win), outer_win)
Idea: assets whose volatility is most STABLE (predictable) are preferred long;
assets with high vol-of-vol (erratic/spiky volatility) are shorted.
Lower vol-of-vol = higher score = long bias.
Canonical: inner_win=10, outer_win=30.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_xv06(P, inner=10, outer=30):
"""Score = -roll_std(roll_std(ret, inner), outer).
Causal: each row uses only past data (rolling windows, no future leakage).
Higher score = lower vol-of-vol = more stable volatility = preferred long.
"""
# inner rolling std: daily vol estimate
inner_vol = xs.roll_std(P.ret, inner)
# outer rolling std of that vol: vol-of-vol
vov = xs.roll_std(inner_vol, outer)
# negate: lower vov = higher score = long
return -vov
# --- Grid: 5 studies max ---
# 1) Canonical: inner=10, outer=30, all universe, H=10, k=5, L/S
rep1 = xs.study_xs(
"XV06-i10-o30-H10-k5-LS",
lambda P: score_xv06(P, inner=10, outer=30),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
# 2) Majors only (19 assets, better liquidity)
rep2 = xs.study_xs(
"XV06-i10-o30-H10-k5-LS-majors",
lambda P: score_xv06(P, inner=10, outer=30),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
# 3) Wider outer window: inner=10, outer=60
rep3 = xs.study_xs(
"XV06-i10-o60-H10-k5-LS",
lambda P: score_xv06(P, inner=10, outer=60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
# 4) Faster rebalance H=5
rep4 = xs.study_xs(
"XV06-i10-o30-H5-k5-LS",
lambda P: score_xv06(P, inner=10, outer=30),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
# 5) More concentrated k=3
rep5 = xs.study_xs(
"XV06-i10-o30-H10-k3-LS",
lambda P: score_xv06(P, inner=10, outer=30),
universe="all", H=10, k=3, long_short=True
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
# Pick best by: earns_slot first, then holdout Sharpe, then distinctness
reps = [rep1, rep2, rep3, rep4, rep5]
earners = [r for r in reps if r["earns_slot"]]
if earners:
best = max(earners, key=lambda r: r["holdout"].get("sharpe", -999))
else:
# fallback: positive full + hold-out + corr_xs01 < 0.6
candidates = [r for r in reps if r["full"]["sharpe"] > 0 and r["holdout"].get("sharpe", 0) > 0
and (r.get("corr_xs01") or 1.0) < 0.6]
if candidates:
best = max(candidates, key=lambda r: r["holdout"].get("sharpe", -999))
else:
best = max(reps, key=lambda r: r["full"]["sharpe"])
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+85
View File
@@ -0,0 +1,85 @@
"""XVa1 — Distance-from-MA value signal.
Score = -(close / roll_mean(close, W) - 1)
Long assets furthest BELOW their rolling MA (cheap / mean-reverting).
Short assets furthest ABOVE their rolling MA (expensive).
Grid: W in {60, 100}, universe all/majors, H in {10, 20}.
Max 5 study_xs calls.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_val(close, W):
"""Causal value score: -(close / MA - 1). Higher = more below MA = long."""
ma = xs.roll_mean(close, W)
return -(close / ma - 1.0)
results = []
# Config 1: W=60, all assets, H=10, k=5, LS
r1 = xs.study_xs(
"XVa1-W60-all-H10",
lambda P: score_val(P.close, 60),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
results.append(r1)
# Config 2: W=100, all assets, H=10, k=5, LS
r2 = xs.study_xs(
"XVa1-W100-all-H10",
lambda P: score_val(P.close, 100),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
results.append(r2)
# Config 3: W=60, majors, H=10, k=5, LS
r3 = xs.study_xs(
"XVa1-W60-majors-H10",
lambda P: score_val(P.close, 60),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
results.append(r3)
# Config 4: W=60, all assets, H=20, k=5, LS (slower rebal)
r4 = xs.study_xs(
"XVa1-W60-all-H20",
lambda P: score_val(P.close, 60),
universe="all", H=20, k=5, long_short=True
)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
results.append(r4)
# Config 5: W=100, all assets, H=20, k=5, LS
r5 = xs.study_xs(
"XVa1-W100-all-H20",
lambda P: score_val(P.close, 100),
universe="all", H=20, k=5, long_short=True
)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
results.append(r5)
# Pick best by: earns_slot first, then hold-out Sharpe, then full Sharpe
def rank_key(r):
earns = int(r["earns_slot"])
hold_sh = r["holdout"].get("sharpe", -99)
full_sh = r["full"].get("sharpe", -99)
return (earns, hold_sh, full_sh)
best = max(results, key=rank_key)
print("\n=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+90
View File
@@ -0,0 +1,90 @@
"""XVa2 — Cross-sectional RSI reversal.
Idea: compute RSI(14) per asset; score = -RSI so oversold assets go long (low RSI = long).
This is a mean-reversion signal: buy the most oversold, short the most overbought.
Grid (<=5 calls):
1. RSI(14) reversal, majors, H=10, k=5, LS
2. RSI(14) reversal, all, H=10, k=5, LS
3. RSI(14) reversal, all, H=5, k=5, LS (faster rebalance)
4. RSI(7) reversal, all, H=5, k=5, LS (shorter RSI period)
5. RSI(14) reversal, all, H=10, k=7, LS (wider basket)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
def rsi_score(close: np.ndarray, win: int = 14) -> np.ndarray:
"""Compute -RSI(win) per asset column causally. Returns (n_days, n_assets) score matrix."""
n, A = close.shape
out = np.full((n, A), np.nan)
for a in range(A):
out[:, a] = -al.rsi(close[:, a], win)
return out
results = []
# 1. RSI(14) on majors, H=10, k=5, LS
rep1 = xs.study_xs(
"XVa2-RSI14-majors-H10-k5",
lambda P: rsi_score(P.close, 14),
universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
results.append(rep1)
# 2. RSI(14) on all, H=10, k=5, LS
rep2 = xs.study_xs(
"XVa2-RSI14-all-H10-k5",
lambda P: rsi_score(P.close, 14),
universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
results.append(rep2)
# 3. RSI(14) on all, H=5, k=5, LS (faster rebalance)
rep3 = xs.study_xs(
"XVa2-RSI14-all-H5-k5",
lambda P: rsi_score(P.close, 14),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(rep3))
results.append(rep3)
# 4. RSI(7) on all, H=5, k=5, LS (shorter RSI)
rep4 = xs.study_xs(
"XVa2-RSI7-all-H5-k5",
lambda P: rsi_score(P.close, 7),
universe="all", H=5, k=5, long_short=True
)
print(xs.fmt(rep4))
results.append(rep4)
# 5. RSI(14) on all, H=10, k=7, LS (wider basket)
rep5 = xs.study_xs(
"XVa2-RSI14-all-H10-k7",
lambda P: rsi_score(P.close, 14),
universe="all", H=10, k=7, long_short=True
)
print(xs.fmt(rep5))
results.append(rep5)
# Pick best by: earns_slot first, then hold-out Sharpe, then distinctness from XS01
def score_rep(r):
earns = 1 if r["earns_slot"] else 0
hold_sh = r["holdout"].get("sharpe", -999) or -999
xs01_corr = abs(r["corr_xs01"] or 1.0)
full_sh = r["full"].get("sharpe", -999) or -999
return (earns, hold_sh, full_sh, -xs01_corr)
best = max(results, key=score_rep)
print("\n" + "=" * 60)
print("BEST CONFIG:")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+94
View File
@@ -0,0 +1,94 @@
"""XVa3 — Price-to-high value (mean reversion from recent highs).
IDEA: Score = -(close / rolling_max(close, W))
Long the most beaten-down assets vs their rolling high (W=90).
Negative sign: lower ratio (more beaten down) -> higher score -> long.
CAUSAL: rolling_max at row i uses only data[i-W+1 .. i] (pandas rolling handles this).
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np
def score_pth(close, W):
"""Price-to-high score: -(close / rolling_max(close, W)), causal."""
import pandas as pd
df = pd.DataFrame(close)
roll_max = df.rolling(W, min_periods=W // 2).max().values
ratio = close / np.where(roll_max > 0, roll_max, np.nan)
return -ratio # lower ratio (more beaten down) -> higher score -> long
# --- Grid: 5 backtests total ---
# Config 1: canonical W=90, H=10, k=5, long-short, all universe
r1 = xs.study_xs(
"XVa3-W90-H10-k5-LS-all",
lambda P: score_pth(P.close, 90),
universe="all", H=10, k=5, long_short=True,
)
print(xs.fmt(r1))
print("JSON:", xs.as_json(r1))
print()
# Config 2: W=60 (shorter lookback), H=10, k=5, long-short, all universe
r2 = xs.study_xs(
"XVa3-W60-H10-k5-LS-all",
lambda P: score_pth(P.close, 60),
universe="all", H=10, k=5, long_short=True,
)
print(xs.fmt(r2))
print("JSON:", xs.as_json(r2))
print()
# Config 3: W=90, H=5 (faster rebalance), k=5, long-short, all
r3 = xs.study_xs(
"XVa3-W90-H5-k5-LS-all",
lambda P: score_pth(P.close, 90),
universe="all", H=5, k=5, long_short=True,
)
print(xs.fmt(r3))
print("JSON:", xs.as_json(r3))
print()
# Config 4: W=90, H=10, k=5, majors only (more liquid)
r4 = xs.study_xs(
"XVa3-W90-H10-k5-LS-majors",
lambda P: score_pth(P.close, 90),
universe="majors", H=10, k=5, long_short=True,
)
print(xs.fmt(r4))
print("JSON:", xs.as_json(r4))
print()
# Config 5: W=120 (longer lookback), H=10, k=5, long-short, all
r5 = xs.study_xs(
"XVa3-W120-H10-k5-LS-all",
lambda P: score_pth(P.close, 120),
universe="all", H=10, k=5, long_short=True,
)
print(xs.fmt(r5))
print("JSON:", xs.as_json(r5))
print()
# --- Pick best config ---
# Prefer: earns_slot first, then holdout sharpe, then distinctness
results = [r1, r2, r3, r4, r5]
earns = [r for r in results if r["earns_slot"]]
if earns:
best = max(earns, key=lambda r: r["holdout"].get("sharpe", -999))
else:
# Fall back to positive full+hold, distinct from XS01
candidates = [r for r in results
if r["full"]["sharpe"] > 0
and r["holdout"].get("sharpe", 0) > 0
and (r["corr_xs01"] or 1.0) < 0.6]
if candidates:
best = max(candidates, key=lambda r: r["holdout"].get("sharpe", -999))
else:
best = max(results, key=lambda r: r["holdout"].get("sharpe", -999))
print("=== BEST CONFIG ===")
print(xs.fmt(best))
print("JSON:", xs.as_json(best))
+145
View File
@@ -0,0 +1,145 @@
"""verify_survivors — adversarial 'Verify' phase for the xsec sweep (2026-06-20).
The Find phase flagged 42/257 cross-sectional configs as earns_slot=True on the certified
Hyperliquid panel. ALL the slot-earners share two tells: (a) strongly NEGATIVE corr to TP01
(-0.2..-0.4), (b) PnL concentrated in 2025. Hypothesis under test (the only thing that matters
before promoting any of them to a sleeve):
"These are not N independent edges. They are ONE regime bet — short the high-beta alt junk
during the 2024-26 alt-bear — wearing many masks (low-vol, low-beta, low-corr, reversal,
trend-gated-mom). The drop-one-month jackknife is robust only WITHIN that single regime."
Three skeptics, deterministic (no agents):
S1 (distinctness/redundancy): mutual correlation matrix of the strongest survivor per family.
If they're all mutually >0.6 correlated -> one bet, not many.
S2 (short-beta tell): correlation of each survivor to two reference factors built on the SAME
panel: SHORTBETA = book ranking by -roll_beta; SHORTMKT = -equal-weight alt-market return.
A genuinely market-neutral factor should NOT load heavily on "short the market".
S3 (single-regime): per-calendar-year Sharpe. If the edge is ~entirely 2025, the 2.5y panel
has ONE up-for-the-factor regime and the hold-out (2025-26) cannot prove robustness.
Run: uv run python scripts/research/xsec/verify_survivors.py
"""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent))
import xslib as xs # noqa: E402
import altlib as al # noqa: E402 (via xslib sys.path)
def roll_corr_to_market(ret, win):
"""Rolling corr of each asset's return to the equal-weight market (causal)."""
mkt = pd.Series(xs.market_ret(ret))
out = np.full_like(ret, np.nan)
for a in range(ret.shape[1]):
out[:, a] = pd.Series(ret[:, a]).rolling(win, min_periods=max(5, win // 2)).corr(mkt).values
return out
def book(universe, score_fn, H=10, k=5, long_short=True):
p = xs.load_panel(universe)
return xs.xs_backtest(p, score_fn(p), H=H, k=k, long_short=long_short)
# ── strongest representative survivor per family (from the Find-phase output) ──
SURV = {
"XV02_lowidiovol": lambda: book("majors", lambda P: -xs.roll_std(xs.residual_return(P.ret, 60), 30)),
"XV01_lowvol": lambda: book("majors", lambda P: -xs.roll_std(P.ret, 30)),
"XV03_lowbeta": lambda: book("all", lambda P: -xs.roll_beta(P.ret, 60)),
"XS06b_lowcorr": lambda: book("all", lambda P: -roll_corr_to_market(P.ret, 60)),
"XU02_lowvol_maj": lambda: book("majors", lambda P: -xs.roll_std(P.ret, 30), k=5),
"XM09_trendgmom": lambda: book("all", lambda P: _trend_gated_mom(P, 60)),
"XL02_voltrendmom": lambda: book("majors", lambda P: xs.xs_zscore(xs.past_return(P.close, 60)) + xs.xs_zscore(xs.volume_z(P.vol, 30))),
"XR02_revgated": lambda: book("majors", lambda P: _vol_gated_rev(P, 3), H=3),
}
def _trend_gated_mom(P, L):
"""XS momentum, but zeroed on days the equal-weight market trailing-sum is non-positive."""
s = xs.past_return(P.close, L)
mkt = xs.market_ret(P.ret)
up = pd.Series(mkt).rolling(L, min_periods=L // 2).sum().values > 0
out = s.copy()
out[~up, :] = np.nan # flat (no ranking) when market not trending up
return out
def _vol_gated_rev(P, L):
"""Short-term reversal, active only when market realized vol is in its high regime."""
rev = -xs.past_return(P.close, L)
mvol = pd.Series(xs.market_ret(P.ret)).rolling(20, min_periods=10).std()
thr = mvol.expanding(min_periods=60).quantile(0.70).values
hi = (mvol.values > thr)
out = rev.copy()
out[~hi, :] = np.nan
return out
# reference factors (the suspected single underlying bet)
REF = {
"SHORTBETA": lambda: book("all", lambda P: -xs.roll_beta(P.ret, 60)), # explicit short-high-beta
"SHORTMKT": None, # -equal-weight alt market
}
def main():
print("=" * 96)
print(" ADVERSARIAL VERIFY — are the xsec survivors one regime bet (short alt-beta) or N edges?")
print("=" * 96)
series = {n: al._to_daily(f()) for n, f in SURV.items()}
# SHORTMKT reference = negative equal-weight alt-market daily return (vol-targeted like a book)
p_all = xs.load_panel("all")
mkt = pd.Series(-xs.market_ret(p_all.ret), index=p_all.index)
series["SHORTBETA"] = al._to_daily(book("all", lambda P: -xs.roll_beta(P.ret, 60)))
series["SHORTMKT"] = al._to_daily(mkt)
df = pd.DataFrame(series).dropna(how="all")
names = list(SURV.keys())
# ── S1: mutual correlation matrix ─────────────────────────────────────────
print("\n[S1] Mutual correlation matrix of survivors (>0.6 = same bet):")
C = df[names].corr()
hdr = " " + " ".join(f"{n[:8]:>8s}" for n in names)
print(hdr)
for n in names:
row = " ".join(f"{C.loc[n, m]:>8.2f}" for m in names)
print(f" {n:<16s} {row}")
iu = [(a, b) for i, a in enumerate(names) for b in names[i + 1:]]
pair_corrs = [C.loc[a, b] for a, b in iu]
print(f" --> mean off-diagonal corr = {np.mean(pair_corrs):+.2f} "
f"(share |r|>0.6: {np.mean([abs(x) > 0.6 for x in pair_corrs]) * 100:.0f}%)")
# ── S2: load on short-beta / short-market ─────────────────────────────────
print("\n[S2] Correlation of each survivor to the suspected single bet:")
print(f" {'survivor':<18s} {'corr_SHORTBETA':>15s} {'corr_SHORTMKT':>15s}")
for n in names:
cb = df[n].corr(df["SHORTBETA"])
cm = df[n].corr(df["SHORTMKT"])
print(f" {n:<18s} {cb:>15.2f} {cm:>15.2f}")
# ── S3: per-year Sharpe (single-regime test) ──────────────────────────────
print("\n[S3] Per-calendar-year Sharpe (is the edge ~entirely 2025?):")
print(f" {'survivor':<18s} {'2024':>8s} {'2025':>8s} {'2026':>8s}")
for n in names:
s = df[n].dropna()
cells = []
for y in (2024, 2025, 2026):
sy = s[s.index.year == y]
cells.append(f"{al._sh(sy):>8.2f}" if len(sy) > 20 else f"{'--':>8s}")
print(f" {n:<18s} " + " ".join(cells))
print("\n" + "=" * 96)
print(" VERDICT logic: high mutual corr + high SHORTBETA/SHORTMKT load + 2025-only Sharpe")
print(" => one short-alt-beta regime bet on a single-regime 2.5y panel. LEAD/forward-monitor,")
print(" NOT a sleeve (cannot prove it survives an alt-bull regime flip).")
print("=" * 96)
if __name__ == "__main__":
main()
+237
View File
@@ -0,0 +1,237 @@
export const meta = {
name: 'xsec-strategies-hyperliquid',
description: 'Search NEW cross-sectional / multi-asset strategies on the 51 certified Hyperliquid alts, distinct from XS01: honest backtest each, verify, score marginal vs the live TP01+XS01+VRP01 stack',
phases: [
{ title: 'Find', detail: 'one agent per cross-sectional mechanism via shared xslib' },
{ title: 'Verify', detail: '3 adversarial skeptics per promising finding (overfit/distinctness/short-history)' },
{ title: 'Synthesize', detail: 'rank survivors, marginal contribution to the live portfolio' },
],
}
// fam | id | name | kind hint | idea
const CATALOG = [
// --- MOM: cross-sectional momentum variants ---
['MOM','XM01','Single-L momentum sweep','Score = past_return(close,L); long top-k/short bottom-k. Grid L in {20,30,60,90,120}. Try universe "all","majors",top20. (Known: momentum on full 49-universe is NEGATIVE — confirm; majors is XS01 turf.)'],
['MOM','XM02','Multi-L z-blend momentum','Score = mean of xs_zscore(past_return(close,L)) over L in {30,90} (and try {20,60,120}). Like XS01 blend. Compare "all" vs "majors".'],
['MOM','XM03','Vol-scaled (risk-adj) momentum','Score = past_return(close,L) / roll_std(ret,L). Risk-adjusted momentum (Sharpe-like). Grid L in {30,60,90}.'],
['MOM','XM04','Residual / idiosyncratic momentum','Score = cumulative residual_return(ret, win) over last L (beta-removed momentum). Cleaner than raw momentum? win=60, L in {30,60}.'],
['MOM','XM05','Momentum acceleration','Score = past_return(close,L_short) - past_return(close,L_long) (is momentum accelerating). L_short=20,L_long=60.'],
['MOM','XM06','52-day-high proximity','Score = close / rolling_max(high,W) (closeness to recent high). W in {60,90}.'],
['MOM','XM07','Sharpe-rank momentum','Score = roll_mean(ret,L) / roll_std(ret,L). Rank by realized Sharpe. L in {30,60,90}.'],
['MOM','XM08','Momentum consistency (frog-in-pan)','Score = past_return(close,L) * fraction_of_up_days(ret,L) (smooth momentum beats jumpy). L=60.'],
['MOM','XM09','Market-trend-gated momentum','XS momentum but only ACTIVE when the equal-weight market (market_ret) is in an uptrend (trailing sum>0); else flat. L=60.'],
['MOM','XM10','Rank-weighted continuous momentum','Instead of top-k/bottom-k, weight ALL assets by demeaned xs_rank(past_return) (continuous book). Implement weights yourself via a fine score + large k≈A/2, or note xslib top-k is the proxy. L=60.'],
// --- REV: reversal ---
['REV','XR01','Short-term reversal','Score = -past_return(close,L) (long losers/short winners). Grid L in {1,3,5,7}. (Known smoke: REV5 negative — confirm/diagnose.)'],
['REV','XR02','Reversal gated by high-vol regime','Short-term reversal active only when market vol is high (panic) else flat. L=3.'],
['REV','XR03','Residual short-term reversal','Score = -(sum of residual_return over last L). Idiosyncratic reversal (beta-removed). L in {3,5}.'],
['REV','XR04','Volume-shock reversal','Long recent losers that ALSO had a volume spike (volume_z high): score = -past_return*(volume_z>1). L=3.'],
['REV','XR05','Overreaction reversal (mid-horizon)','Score = -past_return(close,L) for L in {20,30} (mean-reversion of multi-week moves).'],
// --- VOL/RISK anomalies (the frontier) ---
['VOL','XV01','Low realized-vol anomaly','Score = -roll_std(ret,W) (long low-vol / short high-vol alts). Grid W in {20,30,60}, universe all/majors/top20, long-short AND long-only. (Smoke: ADDS — verify hard.)'],
['VOL','XV02','Low idiosyncratic-vol anomaly','Score = -roll_std(residual_return(ret,60), 30) (low idio vol). Distinct from total vol?'],
['VOL','XV03','Low-beta anomaly (BAB)','Score = -roll_beta(ret,60) (long low-beta / short high-beta). Betting-against-beta.'],
['VOL','XV04','Low downside-vol / semivariance','Score = -roll_std(min(ret,0), W) (only downside dispersion). W=30.'],
['VOL','XV05','Low max-drawdown anomaly','Score = -rolling_maxdrawdown(close,W) (prefer smooth equity). W=60.'],
['VOL','XV06','Low vol-of-vol','Score = -roll_std(roll_std(ret,10), 30). Stability of volatility.'],
// --- DIST: distribution shape ---
['DIST','XD01','Low-skew / anti-lottery','Score = -roll_skew(ret,60) (short high-skew lottery alts, long low-skew). Lottery-preference premium.'],
['DIST','XD02','High-skew momentum (opposite)','Score = +roll_skew(ret,60). Test the OTHER sign (does positive skew pay in crypto?).'],
['DIST','XD03','Coskewness with market','Rank by rolling coskewness of asset returns with market; long low-coskew. win=60.'],
// --- LIQ: volume / liquidity ---
['LIQ','XL01','Amihud illiquidity premium','Score = mean(|ret| / (close*volume)) over W (illiquidity). Long illiquid? Test both signs. W=30.'],
['LIQ','XL02','Volume-trend momentum','Score = volume_z(vol,30) combined with positive return (rising-volume winners). '],
['LIQ','XL03','Low-turnover anomaly','Score = -roll_mean(close*volume, 30) (long low dollar-volume names). Test sign.'],
['LIQ','XL04','Dollar-volume momentum','Score = past_return of dollar-volume (assets gaining liquidity/attention). W=30.'],
// --- VAL: value / mean-reversion to anchor ---
['VAL','XVa1','Distance-from-MA value','Score = -(close/roll_mean(close,W) - 1) (long the ones furthest BELOW their MA = cheap). W in {60,100}.'],
['VAL','XVa2','Cross-sectional RSI reversal','Compute RSI(14) per asset (use al.rsi per column); score = -RSI (long oversold). '],
['VAL','XVa3','Price-to-high value','Score = -(close / rolling_max(close,W)) (long the most beaten-down vs their high). W=90.'],
// --- STRUCT: structure / combos / construction ---
['STRUCT','XS01b','Double-sort momentum × low-vol','Score = xs_zscore(past_return(close,60)) + xs_zscore(-roll_std(ret,30)). Combine momentum and low-vol.'],
['STRUCT','XS02b','Long-mom + short-rev multi-horizon','Score = xs_zscore(past_return(close,90)) + xs_zscore(-past_return(close,5)). Long-term winners that dipped short-term.'],
['STRUCT','XS03b','Beta-hedged momentum','XS momentum book but subtract market beta exposure (score=residual momentum; or note xslib book is already ~dollar-neutral). Compare net vs market-hedged.'],
['STRUCT','XS04b','Ensemble z-vote','Score = mean of xs_zscore over {momentum90, -vol30, -skew60, -beta60}. Diversified cross-sectional signal.'],
['STRUCT','XS05b','Risk-parity legs (inverse-vol)','Momentum selection but weight legs by inverse own-vol (approximate via score = past_return and rely on xslib; document the limitation). L=60.'],
['STRUCT','XS06b','Correlation-to-market diversifier','Score = -rolling_corr(asset_ret, market_ret, 60) (long alts least correlated to the pack). win=60.'],
['STRUCT','XS07b','Trend-quality (R^2) ranking','Score = R^2 of a linear fit of log price over last W (smooth trenders). Long high-R2-up. W=60.'],
['STRUCT','XS08b','Lead-lag vs BTC','Score = past_return(close,L) of alts conditional on BTC having risen (alts that lag BTC catch up). L=10.'],
// --- UNIV: universe / rebalance sensitivity (same core signal, vary the frame) ---
['UNIV','XU01','Momentum universe sweep','Best momentum z-blend, run on universe in {majors, top20, top30, all}. Where does x-sec momentum live? (Maps the small-cap dilution.)'],
['UNIV','XU02','Rebalance/holding sweep','Low-vol or momentum with H in {5,10,20,30} and k in {3,5,8}. Turnover vs signal decay.'],
['UNIV','XU03','Long-only top-k (alt selection)','Low-vol / momentum LONG-ONLY top-k (captures alt-beta + selection, executable at small capital unlike the 38-leg book). Note: NOT market-neutral.'],
['UNIV','XU04','Liquidity-filtered momentum','Momentum but only on the top-20 by median dollar-volume (avoid illiquid noise). Compare to "all".'],
]
const ROOT = '/opt/docker/PythagorasGoal'
const CHEAT = `SHARED LIB (built & validated): ${ROOT}/scripts/research/xsec/xslib.py
Top of your script: import sys; sys.path.insert(0, "${ROOT}/scripts/research/xsec"); import xslib as xs; import numpy as np
PANEL: xs.load_panel(universe) -> Panel(.syms, .index, .close, .open, .high, .low, .vol, .ret) all numpy (n_days x n_assets).
universe: "all" (49 alts, >=700d), "majors" (19 XS01 majors), a list of syms, or an int N (top-N by $-volume).
Certified Hyperliquid 1d, 2024-2026 (~900 days). DVOL not here (that's BTC/ETH only).
CAUSAL HELPERS (value at row i uses data <= i): xs.past_return(close,L), xs.roll_std/roll_mean/roll_skew/ewm_mean(mat,win),
xs.xs_zscore(mat) (cross-sectional z per row), xs.xs_rank(mat), xs.market_ret(ret), xs.roll_beta(ret,win),
xs.residual_return(ret,win) (idiosyncratic), xs.volume_z(vol,win). For per-asset TA (RSI etc.) loop columns with altlib (sys.path has it: import altlib as al; al.rsi(col)).
BACKTEST/EVAL (no look-ahead: weight at bar i earns return of bar i+1 — built in):
xs.study_xs("NAME", lambda P: score_matrix(P), universe="all", H=10, k=5, long_short=True, target_vol=0.20)
score_matrix(P) -> np.ndarray (n_days x n_assets), HIGHER = long. Ranked cross-sectionally each H days;
long top-k / short bottom-k (long_short) or long-only top-k. Vol-targeted, fee 0.10% RT on turnover.
Returns {name,universe,H,k,long_short,n_assets,n_days, full:{sharpe,maxdd,ret,cagr}, holdout:{sharpe,...} (2025+),
yearly, corr_tp01, corr_xs01, corr_active, marginal:{verdict(ADDS/REDUNDANT/DILUTES/NEUTRAL),corr,
holdout_uplift_w20, jackknife_min_uplift, robust_oos}, earns_slot}.
PRINT xs.fmt(rep) and print("JSON:", xs.as_json(rep)).
THE BAR: a finding matters only if it is (1) positive FULL & hold-out 2025+, (2) DISTINCT from XS01 (corr_xs01 < 0.6 —
else it is just XS01), (3) marginal verdict ADDS to the live portfolio with robust_oos=True (survives the OOS jackknife).
earns_slot encodes exactly this. HONESTY: the panel is ~2.5 YEARS -> every result is SUGGESTIVE, not robust; a single
good config or one lucky quarter is NOT an edge. Report negatives plainly (most cross-sectional signals will fail here).`
function finderPrompt([fam, id, name, idea]) {
return `You are studying ONE cross-sectional / multi-asset trading mechanism on the certified Hyperliquid alt panel for PythagorasGoal. Goal of the wave: find something DISTINCT from the existing XS01 (plain cross-sectional momentum) that ADDS to the live TP01+XS01+VRP01 portfolio. Implement honestly with the shared library, backtest, report STRUCTURED results.
MECHANISM ${id} [${fam}] — ${name}
IDEA: ${idea}
CHEATSHEET
${CHEAT}
STEPS
1. Write ${ROOT}/scripts/research/xsec/runs/${id}.py: import xslib as xs, implement the score_matrix CAUSALLY, try a SMALL grid (<=5 study_xs calls total: vary universe / H / k / long_short / a param), pick the BEST config by marginal robustness (prefer earns_slot, then hold-out, then distinctness from XS01). Print xs.fmt(rep)+"JSON:"+xs.as_json(rep) for the best.
2. Run: cd ${ROOT} && uv run python scripts/research/xsec/runs/${id}.py (fix NaN/shape errors and re-run until it produces numbers).
3. Fill the schema from your BEST config, HONESTLY. promising=true ONLY if earns_slot is true OR (full>0 AND hold-out>0 AND corr_xs01<0.6 AND marginal verdict is ADDS). Remember the ~2.5y caveat — be skeptical.
CONSTRAINTS: keep <=5 backtests (each scans ~49 assets x 900 days). Score matrices must be (n_days x n_assets), higher=long, causal. Don't fabricate — every number from a real run.
Your final message IS the schema (data row), not prose.`
}
const FIND_SCHEMA = {
type: 'object',
required: ['id','name','family','implemented','best_universe','best_H','best_k','long_short','full_sharpe','holdout_sharpe','worst_maxdd','corr_xs01','corr_tp01','marginal_verdict','robust_oos','earns_slot','promising','summary'],
properties: {
id: { type: 'string' }, name: { type: 'string' }, family: { type: 'string' },
implemented: { type: 'boolean' },
best_universe: { type: 'string' }, best_H: { type: 'number' }, best_k: { type: 'number' },
long_short: { type: 'boolean' },
full_sharpe: { type: 'number' }, holdout_sharpe: { type: 'number' },
worst_maxdd: { type: 'number' },
corr_xs01: { type: 'number', description: 'correlation to existing XS01 (must be <0.6 to be distinct)' },
corr_tp01: { type: 'number' },
marginal_verdict: { type: 'string', enum: ['ADDS','REDUNDANT','DILUTES','NEUTRAL','N/A'] },
holdout_uplift_w20: { type: 'number' },
robust_oos: { type: 'boolean', description: 'survives the OOS drop-best-month jackknife' },
earns_slot: { type: 'boolean' },
promising: { type: 'boolean' },
summary: { type: 'string' },
caveats: { type: 'string' },
script_path: { type: 'string' },
},
}
function verifyPrompt(spec, find, kk) {
const [fam, id, name] = spec
const angles = [
'OVERFIT TO 2.5y / SHORT-HISTORY: the panel is only 2024-2026 with a ~1.5y hold-out. Re-run the best config and its neighbors (other universe/H/k). Is the edge a plateau or one lucky cell? Split the hold-out: is it carried by ONE quarter or the partial-2026 stub? Re-check jackknife (drop-best-month). Default real=false if it leans on a short window or single config.',
'DISTINCTNESS FROM XS01 & LEAK: is corr_xs01 really < 0.6, or is this XS01 in disguise (same momentum signal re-skinned)? Read xslib to confirm the score is causal (no future bar in rolling/beta/residual; weight at i applies to i+1). Confirm the mechanism is economically DIFFERENT from cross-sectional momentum. Default real=false if redundant with XS01 or leaky.',
'MARGINAL & EXECUTABILITY: re-verify it ADDS to the LIVE active portfolio (marginal uplift hold-out positive AND robust_oos) — not just standalone-positive. Is the book executable (a 10-leg market-neutral alt book needs ~20k capital; a long-only top-k is lighter)? Is turnover/fee realistic? For volume/illiquidity signals, are they an artifact of thin alts? Default real=false if it does not robustly improve the live stack.',
]
return `You are an ADVERSARIAL SKEPTIC (#${kk + 1}) for PythagorasGoal. A finder claims cross-sectional mechanism ${id} [${fam}] "${name}" is promising on the Hyperliquid alt panel. REFUTE it — this project was wrecked once by fake edges, and here the history is only ~2.5 years so the overfit risk is HIGH. Assume false-positive until proven otherwise.
FINDER'S CLAIM:
${JSON.stringify(find)}
Run script: ${find.script_path || ROOT + '/scripts/research/xsec/runs/' + id + '.py'}
Trusted leak-free lib: ${ROOT}/scripts/research/xsec/xslib.py
YOUR ANGLE: ${angles[kk % 3]}
Read the script, run your own checks (cd ${ROOT} && uv run python ...), quote the numbers you produce, and decide. Default to real=false when uncertain. Return ONLY the schema.`
}
const VERIFY_SCHEMA = {
type: 'object',
required: ['id','real','confidence','reason'],
properties: {
id: { type: 'string' },
real: { type: 'boolean', description: 'true only if the edge survives your adversarial check AND robustly adds to the live stack' },
confidence: { type: 'number' },
overfit_short_history: { type: 'boolean' },
redundant_with_xs01: { type: 'boolean' },
leak_suspected: { type: 'boolean' },
corrected_full_sharpe: { type: 'number' },
corrected_holdout_sharpe: { type: 'number' },
reason: { type: 'string', description: 'specific, with numbers you produced' },
},
}
// ===========================================================================
phase('Find')
log(`Searching ${CATALOG.length} cross-sectional mechanisms on the 51-alt Hyperliquid panel, one agent each. Frontier: distinct from XS01, additive to the live stack.`)
const results = await pipeline(
CATALOG,
(spec) => agent(finderPrompt(spec), { label: `find:${spec[1]}`, phase: 'Find', schema: FIND_SCHEMA, model: 'sonnet', effort: 'medium' }),
(find, spec) => {
if (!find) return { id: spec[1], name: spec[2], family: spec[0], promising: false, verify: [] }
if (!find.promising) return { ...find, verify: [] }
return parallel([0, 1, 2].map((kk) => () =>
agent(verifyPrompt(spec, find, kk), { label: `verify:${spec[1]}.${kk}`, phase: 'Verify', schema: VERIFY_SCHEMA, effort: 'high' })
)).then((votes) => ({ ...find, verify: votes.filter(Boolean) }))
}
)
phase('Synthesize')
const clean = results.filter(Boolean)
const enriched = clean.map((r) => {
const v = r.verify || []
const realVotes = v.filter((x) => x && x.real).length
const survived = r.promising && v.length >= 2 && realVotes >= Math.ceil(v.length / 2)
return { ...r, real_votes: realVotes, n_verify: v.length, survived }
})
const survivors = enriched.filter((r) => r.survived)
const killed = enriched.filter((r) => r.promising && !r.survived)
log(`Find done: ${clean.length} studied. Promising: ${enriched.filter(r => r.promising).length}. Survived adversarial verify: ${survivors.length}.`)
const compact = enriched.map((r) => ({
id: r.id, name: r.name, family: r.family, universe: r.best_universe, H: r.best_H, k: r.best_k, ls: r.long_short,
full: r.full_sharpe, hold: r.holdout_sharpe, dd: r.worst_maxdd, corr_xs01: r.corr_xs01, corr_tp01: r.corr_tp01,
marginal: r.marginal_verdict, robust: r.robust_oos, earns_slot: r.earns_slot, promising: r.promising,
survived: r.survived, real_votes: r.real_votes, summary: r.summary,
verify: (r.verify || []).map((x) => x ? `[real=${x.real} conf=${x.confidence}] ${x.reason}` : '').filter(Boolean),
}))
const SYNTH_SCHEMA = {
type: 'object',
required: ['headline', 'survivors', 'ranking', 'recommendations', 'dead_families'],
properties: {
headline: { type: 'string', description: '2-4 sentences: did a NEW cross-sectional mechanism, distinct from XS01 and additive to the live stack, emerge — net of the ~2.5y caveat?' },
survivors: { type: 'array', items: { type: 'object', required: ['id', 'name', 'why', 'suggested_role'], properties: {
id: { type: 'string' }, name: { type: 'string' }, why: { type: 'string' },
suggested_role: { type: 'string', description: 'new sleeve candidate / lead to forward-monitor / needs longer history' },
distinct_from_xs01: { type: 'string' } } } },
ranking: { type: 'array', items: { type: 'string' } },
recommendations: { type: 'string', description: 'concrete: what (if anything) to deep-validate or add, weight, and how to handle the short history' },
dead_families: { type: 'array', items: { type: 'string' } },
},
}
const synthPrompt = `You are the SYNTHESIZER for a PythagorasGoal wave that searched ${CATALOG.length} CROSS-SECTIONAL / multi-asset mechanisms on the 51 certified Hyperliquid alts (1d, 2024-2026), then adversarially verified every promising one. This is the frontier the previous BTC/ETH sweep pointed to (single-asset directional is exhausted at the ~1.3 ceiling).
LIVE stack (do not re-derive): TP01 (TSMOM trend BTC/ETH, defensive), XS01 (cross-sectional MOMENTUM on 19 HL majors, top5/bottom5, blend+dispersion-gate, vol-target — corr ~-0.12 to TP01), VRP01 (modeled options short-vol). A NEW cross-sectional sleeve is only valuable if it is (1) robust despite the SHORT ~2.5y history, (2) DISTINCT from XS01 (corr < 0.6 — not momentum re-skinned), and (3) ADDS to the live active portfolio out-of-sample (marginal uplift + robust_oos jackknife). Honesty is prime: on 2.5 years, be very skeptical; a clean set of negatives is an acceptable outcome.
Full result table (verify = the skeptics' findings):
${JSON.stringify(compact)}
Survivors (passed adversarial verify): ${JSON.stringify(survivors.map((s) => ({ id: s.id, name: s.name, full: s.full_sharpe, hold: s.holdout_sharpe, corr_xs01: s.corr_xs01, corr_tp01: s.corr_tp01, marginal: s.marginal_verdict, real_votes: s.real_votes })))}
Promising-but-killed: ${JSON.stringify(killed.map((s) => ({ id: s.id, name: s.name, why: (s.verify || []).map((v) => v && v.reason).filter(Boolean) })))}
Produce the synthesis. Be concrete and skeptical about the short history. If a genuinely distinct, additive mechanism survived (e.g. a risk/low-vol anomaly orthogonal to momentum), say what it is, whether it is a sleeve candidate or a lead needing more history, and its correlation profile. If nothing robust survived, say so plainly.`
const synthesis = await agent(synthPrompt, { schema: SYNTH_SCHEMA, effort: 'high', label: 'synthesize' })
return {
n_studied: clean.length,
n_promising: enriched.filter((r) => r.promising).length,
n_survived: survivors.length,
survivors: survivors.map((s) => ({ id: s.id, name: s.name, family: s.family, full: s.full_sharpe, hold: s.holdout_sharpe, corr_xs01: s.corr_xs01, corr_tp01: s.corr_tp01, marginal: s.marginal_verdict, real_votes: s.real_votes, summary: s.summary })),
promising_killed: killed.map((s) => ({ id: s.id, name: s.name })),
all_grades: clean.map((r) => ({ id: r.id, name: r.name, full: r.full_sharpe, hold: r.holdout_sharpe, corr_xs01: r.corr_xs01, marginal: r.marginal_verdict, earns_slot: r.earns_slot, promising: r.promising })),
synthesis,
}
+358
View File
@@ -0,0 +1,358 @@
"""xslib — SHARED CROSS-SECTIONAL research harness over the certified Hyperliquid alt panel.
Built for the "cerca altre strategie" wave (2026-06-20, follow-up to the 104-hypothesis BTC/ETH
sweep that exhausted the single-asset directional space). The frontier the prior synthesis pointed
to: CROSS-SECTIONAL / multi-asset mechanisms on the 51 certified Hyperliquid alts (1d, 2024-2026),
where the ~1.3 BTC/ETH-directional ceiling does NOT bind, and DISTINCT from XS01 (plain x-sec momentum).
Why a new harness: the panel is N assets × ~900 days. A strategy = a per-asset SCORE computed
causally (data <= close[i]); the harness ranks it cross-sectionally each rebalance, goes long the
top-k / short the bottom-k (market-neutral) or long-only top-k, vol-targets, charges fee on turnover,
and — crucially — the weight decided at bar i is applied to the return of bar i+1, so look-ahead is
structurally impossible (same convention as src.portfolio xs_book / sleeves._xsec_returns).
A candidate only matters if it (a) is robust (positive FULL + hold-out 2025+ + jackknife), AND
(b) is DISTINCT from XS01 (low correlation), AND (c) ADDS to the live TP01+XS01+VRP01 portfolio.
CAVEAT baked in: the panel is ~2.5 years — every result is SUGGESTIVE, not robust like 6y BTC/ETH.
Quick start (agent script):
import sys; sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs, numpy as np
p = xs.load_panel("all") # or "majors", a list, or an int N (top-N liquidity)
score = xs.past_return(p.close, 30) # momentum: higher = long
rep = xs.study_xs("MOM30", lambda P: xs.past_return(P.close, 30), H=10, k=5)
print(xs.fmt(rep)); print("JSON:", xs.as_json(rep))
"""
from __future__ import annotations
import glob
import json
import sys
import warnings
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
# panel research has many all-NaN edge windows (rolling beta/vol on first rows) -> benign
warnings.filterwarnings("ignore", category=RuntimeWarning)
_ROOT = Path(__file__).resolve().parents[3]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt"))
import altlib as al # noqa: E402 (reuse _sh, _dd_ret, _to_daily, HOLDOUT, metric helpers)
RAW = _ROOT / "data" / "raw"
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
FEE = 0.001 # round-trip; charged /2 per side on turnover
MAJORS = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
# ===========================================================================
# PANEL
# ===========================================================================
@dataclass
class Panel:
syms: list
index: pd.DatetimeIndex
close: np.ndarray
open: np.ndarray
high: np.ndarray
low: np.ndarray
vol: np.ndarray
ret: np.ndarray # daily simple returns, ret[0]=0
@lru_cache(maxsize=16)
def load_panel(universe="all", min_rows: int = 700) -> Panel:
"""Common-date OHLCV panel of the certified HL alts (1d). `universe`:
'all' -> every alt with >= min_rows of history (drops short ones e.g. ALGO/SAND),
'majors' -> the 19 XS01 majors, a list of symbols, or an int N (top-N by median $-volume)."""
close, vol, high, low, opn = {}, {}, {}, {}, {}
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(f)
if len(d) < min_rows:
continue
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
close[sym] = pd.Series(d["close"].values.astype(float), index=idx)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=idx)
high[sym] = pd.Series(d["high"].values.astype(float), index=idx)
low[sym] = pd.Series(d["low"].values.astype(float), index=idx)
opn[sym] = pd.Series(d["open"].values.astype(float), index=idx)
C = pd.concat(close, axis=1, join="inner").sort_index().dropna()
syms = list(C.columns)
if universe == "majors":
syms = [s for s in MAJORS if s in syms]
elif isinstance(universe, (list, tuple)):
syms = [s for s in universe if s in syms]
elif isinstance(universe, int):
dollar = {s: float(np.nanmedian(C[s].values * pd.concat(vol, axis=1)[s].reindex(C.index).values))
for s in syms}
syms = sorted(syms, key=lambda s: -dollar[s])[:universe]
C = C[syms]
idx = C.index
def stack(dd):
return pd.concat(dd, axis=1).reindex(index=idx)[syms].values.astype(float)
cl = C.values
ret = np.zeros_like(cl)
ret[1:] = cl[1:] / cl[:-1] - 1.0
return Panel(syms, idx, cl, stack(opn), stack(high), stack(low), stack(vol), ret)
# ===========================================================================
# CAUSAL CROSS-SECTIONAL HELPERS (value at row i uses data <= i)
# ===========================================================================
def past_return(close, L):
out = np.full_like(close, np.nan)
out[L:] = close[L:] / close[:-L] - 1.0
return out
def roll_std(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(2, win // 2)).std().values
def roll_mean(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(2, win // 2)).mean().values
def roll_skew(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(3, win // 2)).skew().values
def ewm_mean(mat, span):
return pd.DataFrame(mat).ewm(span=span, adjust=False).mean().values
def xs_zscore(mat):
"""Cross-sectional z-score per row (across assets). NaN-safe."""
m = np.nanmean(mat, axis=1, keepdims=True)
s = np.nanstd(mat, axis=1, keepdims=True)
return (mat - m) / np.where(s > 0, s, np.nan)
def xs_rank(mat):
"""Cross-sectional rank in [0,1] per row (0=lowest)."""
out = np.full_like(mat, np.nan, dtype=float)
for i in range(mat.shape[0]):
row = mat[i]
ok = np.isfinite(row)
if ok.sum() >= 2:
r = pd.Series(row[ok]).rank().values
out[i, ok] = (r - 1) / (ok.sum() - 1)
return out
def market_ret(ret):
"""Equal-weight market return per day (n,)."""
return np.nanmean(ret, axis=1)
def roll_beta(ret, win):
"""Rolling beta of each asset to the equal-weight market (n,A), causal."""
mkt = market_ret(ret)
ms = pd.Series(mkt)
var = ms.rolling(win, min_periods=max(5, win // 2)).var()
out = np.full_like(ret, np.nan)
for a in range(ret.shape[1]):
cov = pd.Series(ret[:, a]).rolling(win, min_periods=max(5, win // 2)).cov(ms)
out[:, a] = (cov / var.replace(0, np.nan)).values
return out
def residual_return(ret, win):
"""Idiosyncratic daily return = ret - beta*market (beta rolling, causal)."""
beta = roll_beta(ret, win)
mkt = market_ret(ret)[:, None]
return ret - beta * mkt
def volume_z(vol, win):
m = roll_mean(vol, win)
s = roll_std(vol, win)
return (vol - m) / np.where(s > 0, s, np.nan)
# ===========================================================================
# BACKTEST — generic cross-sectional book from a per-asset SCORE matrix.
# score[i] (data <= i) -> rank assets -> long top-k / short bottom-k; W[i] earns dret[i+1].
# ===========================================================================
def xs_backtest(panel: Panel, score, H=10, k=5, long_short=True, target_vol=0.20,
fee=FEE, vt_cap=3.0):
px = panel.close
n, A = px.shape
dret = panel.ret
score = np.asarray(score, float)
if score.shape != (n, A):
raise ValueError(f"score shape {score.shape} != panel {(n, A)}")
W = np.zeros((n, A))
w = np.zeros(A)
for i in range(n):
if i % H == 0:
row = score[i]
fin = np.isfinite(row)
if fin.sum() >= 2 * k:
ranked = np.where(fin, row, -np.inf)
order = np.argsort(ranked)
order = order[np.isfinite(ranked[order])]
lo, hi = order[:k], order[-k:]
w = np.zeros(A)
if long_short:
w[hi] = 0.5 / k
w[lo] = -0.5 / k
else:
w[hi] = 1.0 / k
W[i] = w
gross = np.zeros(n)
gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n)
turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (fee / 2.0)
s = pd.Series(net, index=panel.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, vt_cap)
return pd.Series(s.values * scale, index=panel.index)
# ===========================================================================
# BASELINES (live stack) + MARGINAL scoring
# ===========================================================================
@lru_cache(maxsize=1)
def baselines():
"""Daily returns of the LIVE stack: TP01, XS01, and the combined active portfolio."""
from src.portfolio.portfolio import StrategyPortfolio, to_daily
from src.portfolio.sleeves import _tp01_returns, _xsec_returns, active_sleeves
tp = to_daily(_tp01_returns())
xs01 = to_daily(_xsec_returns())
active = StrategyPortfolio(active_sleeves()).combined_daily()
return dict(tp01=tp, xs01=xs01, active=active)
def _corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return round(float(J["a"].corr(J["b"])), 3) if len(J) > 5 else None
def marginal_vs(cand, base, weights=(0.2, 0.35)):
"""Does `cand` improve `base`? blend uplift (full & hold-out), + OOS jackknife robustness."""
J = pd.concat({"B": base, "C": cand}, axis=1, join="inner").dropna()
if len(J) < 30:
return dict(verdict="N/A", reason="overlap < 30d")
JH = J[J.index >= HOLDOUT]
has_h = len(JH) > 20
out = dict(corr=_corr(J["B"], J["C"]), base_full=round(al._sh(J["B"]), 3),
base_hold=round(al._sh(JH["B"]), 3) if has_h else None,
cand_full=round(al._sh(J["C"]), 3), cand_hold=round(al._sh(JH["C"]), 3) if has_h else None,
blends={})
for w in weights:
bf, bh = (1 - w) * J["B"] + w * J["C"], (1 - w) * JH["B"] + w * JH["C"]
out["blends"][f"w{int(w * 100)}"] = dict(
uplift_full=round(al._sh(bf) - al._sh(J["B"]), 3),
uplift_hold=round(al._sh(bh) - al._sh(JH["B"]), 3) if has_h else None,
dd=round(al._dd_ret(bf), 4))
# OOS jackknife at w=0.2
robust = False
cu = jk = None
if has_h:
def _u(sub):
return al._sh(0.8 * sub["B"] + 0.2 * sub["C"]) - al._sh(sub["B"])
months = sorted(set(zip(JH.index.year, JH.index.month)))
cu = round(_u(JH), 3)
jk = round(min(_u(JH[~((JH.index.year == y) & (JH.index.month == m))]) for y, m in months), 3) \
if len(months) > 1 else cu
robust = bool(cu > 0.02 and jk > 0.0)
out["holdout_uplift_w20"] = cu
out["jackknife_min_uplift"] = jk
out["robust_oos"] = robust
up = out["blends"][f"w{int(weights[0] * 100)}"]["uplift_hold"]
cc = out["corr"] if out["corr"] is not None else 0.0
if cc is not None and cc > 0.85 and (up is None or abs(up) < 0.05):
out["verdict"] = "REDUNDANT"
elif up is not None and up >= 0.05 and robust:
out["verdict"] = "ADDS"
elif up is not None and up <= -0.05:
out["verdict"] = "DILUTES"
else:
out["verdict"] = "NEUTRAL"
return out
# ===========================================================================
# DRIVER
# ===========================================================================
def study_xs(name, score_fn, universe="all", H=10, k=5, long_short=True,
target_vol=0.20, min_rows=700) -> dict:
"""Backtest one cross-sectional hypothesis and score it honestly:
FULL + hold-out 2025+ + yearly, correlation to TP01 & XS01 (distinctness),
and marginal contribution to the LIVE active portfolio. `score_fn(panel) -> (n,A)`
per-asset score (higher = long), computed CAUSALLY (data <= close[i])."""
p = load_panel(universe, min_rows=min_rows)
score = score_fn(p)
daily = al._to_daily(xs_backtest(p, score, H=H, k=k, long_short=long_short, target_vol=target_vol))
net = daily.values
idx = daily.index
full = al._metrics_from_net(net, idx)
hmask = idx >= HOLDOUT
hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 20 else dict(sharpe=0.0, n=int(hmask.sum()))
bl = baselines()
marg = marginal_vs(daily, bl["active"])
earns_slot = (full["sharpe"] > 0 and hold.get("sharpe", 0) > 0
and marg.get("verdict") == "ADDS"
and (_corr(daily, bl["xs01"]) or 0) < 0.6) # distinct from existing x-sec
return dict(
name=name, universe=str(universe), H=H, k=k, long_short=long_short,
n_assets=len(p.syms), n_days=int(len(idx)),
full=full, holdout=hold, yearly=al._yearly(net, idx),
corr_tp01=_corr(daily, bl["tp01"]), corr_xs01=_corr(daily, bl["xs01"]),
corr_active=_corr(daily, bl["active"]),
marginal=marg, earns_slot=earns_slot,
caveat="panel ~2.5y (2024-26): suggestive, not robust",
)
def _clean(o):
if isinstance(o, dict):
return {k: _clean(v) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [_clean(x) for x in o]
if isinstance(o, (np.floating,)):
return round(float(o), 4)
if isinstance(o, (np.integer,)):
return int(o)
if isinstance(o, (np.bool_,)):
return bool(o)
return o
def as_json(rep):
return json.dumps(_clean(rep), default=str)
def fmt(rep):
m = rep["marginal"]
yr = " ".join(f"{y}:{d['ret'] * 100:+.0f}%" for y, d in rep["yearly"].items())
return (f"=== {rep['name']} [{rep['universe']} H{rep['H']} k{rep['k']} "
f"{'LS' if rep['long_short'] else 'LO'}] EARNS_SLOT={rep['earns_slot']}\n"
f" FULL Sh {rep['full']['sharpe']:+.2f} DD {rep['full']['maxdd'] * 100:.0f}% "
f"ret {rep['full']['ret'] * 100:+.0f}% | HOLD Sh {rep['holdout'].get('sharpe', 0):+.2f} "
f"| corr TP01 {rep['corr_tp01']} XS01 {rep['corr_xs01']}\n"
f" marginal vs active: {m.get('verdict')} (corr {m.get('corr')}, "
f"holdUplift_w20 {m.get('holdout_uplift_w20')}, jackknife {m.get('jackknife_min_uplift')}, "
f"robust_oos {m.get('robust_oos')}) | {yr}")
if __name__ == "__main__":
print("--- SMOKE TEST xslib ---")
# 1) x-sec momentum (should resemble XS01 ballpark) ; 2) short-term reversal ; 3) low-vol
print(fmt(study_xs("MOM30-90", lambda P: xs_zscore(past_return(P.close, 30)) + xs_zscore(past_return(P.close, 90)), H=10, k=5)))
print(fmt(study_xs("REV5", lambda P: -past_return(P.close, 5), H=5, k=5)))
print(fmt(study_xs("LOWVOL", lambda P: -roll_std(P.ret, 30), H=10, k=5)))
print("\nJSON sample:", as_json(study_xs("MOM30", lambda P: past_return(P.close, 30)))[:240])