Event-driven long-only simulator with next-open fills, CN tradability (halts, limit-up/down, stamp duty), PIT S&P 500 / CSI 300 membership, rebalance-phase averaging, factor IC scans, maximin stepwise composite search across 5-year blocks, walk-forward LightGBM/ridge combiners, overlays, and deflated Sharpe. Fetchers pull 20y US OHLCV from Yahoo and CSI 300/500 ever-members (membership snapshot from baostock, bars from Yahoo). Research data lives in data/research/ (ignored). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
"""
|
|
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()
|