Files
quant-trading/research/lab/export_curves.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

79 lines
3.6 KiB
Python

"""
Export phase-averaged equity curves of the final candidates and baselines for
the report (weekly samples), plus the US/CN correlation of the new strategies.
PYTHONPATH=. uv run python -m research.lab.export_curves
"""
import json
import os
import warnings
import numpy as np
import pandas as pd
from research.lab.evaluate import phase_runs, score_for
from research.lab.factor_scan import RESULTS, load
from research.lab.report import stats
warnings.filterwarnings("ignore")
SETS = {
"us": ("2006-09-27", "2026-09-24", {
"新: overnight+recovery": ("overnight_mom+rec_126", dict(top_n=10, every=10, buffer=2.0)),
"现: rec_mfilt+deep_upvol 月频": ("baseline:rec_mfilt+deep_upvol", dict(top_n=10, every=21)),
"现: mom7m+rec126 月频": ("baseline:mom7m+rec126", dict(top_n=10, every=21)),
"等权 S&P 500 (PIT)": ("ew", dict(top_n=10000, every=5)),
}),
"cn_hs300": ("2010-01-04", "2026-09-24", {
"新: 低风险动量4因子": ("amp_cut_mom+low_beta+pv_corr+sharpe_mom", dict(top_n=15, every=10, buffer=2.0)),
"新: low_beta+sharpe_mom": ("low_beta+sharpe_mom", dict(top_n=20, every=10, buffer=1.0)),
"现: down_resil+qual_mom 双周": ("baseline:down_resil+qual_mom", dict(top_n=10, every=10)),
"现: up_cap+quality_mom 周频": ("baseline:up_cap+quality_mom", dict(top_n=10, every=5)),
"等权 CSI 300 (PIT)": ("ew", dict(top_n=10000, every=5)),
}),
}
def main() -> None:
out, daily = {}, {}
for market, (a, b, cands) in SETS.items():
p, _ = load(market)
curves = {}
for name, (combo, kw) in cands.items():
s = score_for(p, combo, market)
runs = phase_runs(p, s, a, b, **kw)
rets = pd.concat([r.returns.loc[a:b] for r in runs], axis=1).mean(axis=1)
daily[(market, name)] = rets
eq = (1 + rets).cumprod()
wk = eq.resample("W-FRI").last().dropna()
dd = eq / eq.cummax() - 1
curves[name] = {"dates": [d.strftime("%Y-%m-%d") for d in wk.index],
"equity": [round(float(v), 4) for v in wk.values],
"drawdown": [round(float(v), 4) for v in dd.resample("W-FRI").min().dropna().values],
"stats": {k: float(v) for k, v in stats(eq).items()}}
print(market, name, {k: round(v, 3) for k, v in curves[name]["stats"].items()}, flush=True)
out[market] = curves
us = daily[("us", "新: overnight+recovery")]
cn = daily[("cn_hs300", "新: 低风险动量4因子")]
# compound each market on its own calendar before joining: the exchanges
# have different holidays, and an inner join on days would drop returns
m = pd.concat([us, cn], axis=1, keys=["us", "cn"]).dropna().loc["2016-09-27":]
mon = pd.concat({k: (1 + v.loc["2016-09-27":]).resample("ME").prod() - 1
for k, v in (("us", us), ("cn", cn))}, axis=1).dropna()
cross = {"daily_corr": float(m.corr().iloc[0, 1]), "monthly_corr": float(mon.corr().iloc[0, 1])}
for col in ["us", "cn", "5050"]:
r = mon.mean(axis=1) if col == "5050" else mon[col]
eq = (1 + r).cumprod()
cross[col] = {"cagr": float(eq.iloc[-1] ** (12 / len(r)) - 1),
"sharpe_monthly": float(r.mean() / r.std() * np.sqrt(12)),
"mdd_monthly": float((eq / eq.cummax() - 1).min())}
out["cross"] = cross
print("cross", out["cross"])
with open(os.path.join(RESULTS, "curves.json"), "w") as fh:
json.dump(out, fh, ensure_ascii=False, default=float)
if __name__ == "__main__":
main()