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