""" Construction sweep: holdings count, buffer (hysteresis), rebalance interval and weighting for a fixed score, reported per ~5-year block. PYTHONPATH=. uv run python -m research.lab.construction --market us --combo overnight_mom+rec_126 """ import argparse import itertools import warnings import numpy as np import pandas as pd from research.lab.baselines import baseline_scores from research.lab.factor_scan import load, phase_avg from research.lab.factors import FACTORS, zrank from research.lab.stepwise import SUBPERIODS warnings.filterwarnings("ignore") def combo_score(p, combo: str, market: str) -> pd.DataFrame: if combo.startswith("baseline:"): return baseline_scores(p, market)[combo.split(":", 1)[1]] parts = combo.split("+") return sum(zrank(FACTORS[n](p), p.member) for n in parts) / len(parts) def block_stats(p, score, subs, **kw) -> dict: per = [phase_avg(p, score, a, b, **kw) for a, b in subs] return {"sharpe": np.mean([x["sharpe"] for x in per]), "min_sh": min(x["sharpe"] for x in per), "cagr": np.mean([x["cagr"] for x in per]), "mdd": min(x["mdd"] for x in per), "turn": np.mean([x["turnover"] for x in per]), "blocks": " ".join(f"{x['sharpe']:.2f}/{x['cagr']*100:.0f}%" for x in per)} def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--market", default="us") ap.add_argument("--combo", nargs="+", required=True) ap.add_argument("--tops", nargs="*", type=int, default=[10, 15, 20, 30]) ap.add_argument("--buffers", nargs="*", type=float, default=[0, 0.5, 1.0, 2.0]) ap.add_argument("--everys", nargs="*", type=int, default=[5, 10]) ap.add_argument("--weightings", nargs="*", default=["equal", "invvol"]) args = ap.parse_args() mk = "us" if args.market == "us" else "cn" p, _ = load(args.market) subs = SUBPERIODS[mk] for combo in args.combo: s = combo_score(p, combo, args.market) print(f"=== {combo}") for top, buf, every, wt in itertools.product(args.tops, args.buffers, args.everys, args.weightings): st = block_stats(p, s, subs, top_n=top, every=every, buffer=buf, weighting=wt) print(f" top{top:<3d} buf{buf:<4} every{every:<3d} {wt:7s} sh {st['sharpe']:.2f} min {st['min_sh']:.2f} " f"cagr {st['cagr']*100:5.1f}% worstMDD {st['mdd']*100:5.1f}% turn {st['turn']:4.1f}x [{st['blocks']}]", flush=True) if __name__ == "__main__": main()