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
+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))