Files
quant-trading/research/lab/construction.py
Gahow Wang 0e378efa67 research: point-in-time factor research harness (research/lab)
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>
2026-09-25 19:11:59 +08:00

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