feat: CSI500 universe + reversal_vol signal + argparse CLI

This commit is contained in:
Yuxuan Yan
2026-06-07 10:01:41 +08:00
parent aedc019d23
commit 241683cc54
4 changed files with 93 additions and 11 deletions
+30 -9
View File
@@ -1,6 +1,12 @@
#!/usr/bin/env python3
"""End-to-end pipeline: HS300 universe -> reversal signal -> cross-sectional IC
-> multi-stock backtest (AlphaStrategy + RankEqualWeightBuilder) -> reports."""
"""End-to-end pipeline: universe -> signal -> cross-sectional IC
-> multi-stock backtest (AlphaStrategy + RankEqualWeightBuilder) -> reports.
Usage:
python3 run_example.py --universe hs300 --signal reversal
python3 run_example.py --universe csi500 --signal reversal_vol
"""
import argparse
import logging
import pandas as pd
@@ -9,10 +15,11 @@ from analysis.report import generate_report
from backtest.config import BacktestConfig
from backtest.runner import BacktestRunner
from data.downloader import download_batch
from data.universe import SYMBOLS
from data.universe import SYMBOLS, CSI500_SYMBOLS
from eval.metrics import evaluate_cross_sectional
from portfolio.builder import RankEqualWeightBuilder
from signals.reversal import ReversalSignal
from signals.reversal_vol import ReversalVolSignal
from strategies.alpha_strategy import AlphaStrategy
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
@@ -29,18 +36,27 @@ def _forward_returns(data: dict[str, pd.DataFrame], horizon: int) -> pd.DataFram
return pd.DataFrame(forward_returns)
def main(forward_horizon: int = 5):
symbols = SYMBOLS[:30]
def main(forward_horizon: int = 5, universe: str = "csi500", signal_name: str = "reversal_vol"):
universes = {"hs300": SYMBOLS, "csi500": CSI500_SYMBOLS}
symbols = universes.get(universe, CSI500_SYMBOLS)[:30]
signals = {
"reversal": ReversalSignal(lookback=5),
"reversal_vol": ReversalVolSignal(lookback=5, vol_window=20),
}
signal = signals.get(signal_name, ReversalVolSignal(lookback=5, vol_window=20))
start, end = "2023-01-01", "2024-12-31"
initial_cash = 1_000_000
logger.info(f"Universe: {universe} ({len(symbols)} stocks), Signal: {signal.name}")
# 1-2. Download daily data for the universe.
data = download_batch(symbols, start, end)
data = {s: df for s, df in data.items() if df is not None and not df.empty}
logger.info(f"Downloaded {len(data)}/{len(symbols)} symbols")
# 3. Compute the reversal signal per stock.
signal = ReversalSignal(lookback=5)
# 3. Compute the signal per stock.
signal_series: dict[str, pd.Series] = {}
for sym, df in data.items():
sig = signal.compute(df)
@@ -52,7 +68,7 @@ def main(forward_horizon: int = 5):
returns_df = _forward_returns(data, forward_horizon)
signal_eval = evaluate_cross_sectional(signals_df, returns_df)
# 4b. Multi-horizon IC to show which horizon the signal works at.
# 4b. Multi-horizon IC.
horizon_evals = {
h: evaluate_cross_sectional(signals_df, _forward_returns(data, h))
for h in (1, 5, 20)
@@ -84,6 +100,7 @@ def main(forward_horizon: int = 5):
# 8. Print summary.
print("\nSIGNAL IC")
print("=" * 50)
print(f"Universe: {universe} | Signal: {signal.name}")
print(f"IC mean / std / IR: {signal_eval['ic_mean']:.4f} / "
f"{signal_eval['ic_std']:.4f} / {signal_eval['ir']:.4f}")
print(f"Rank IC mean / std / IR: {signal_eval['rank_ic_mean']:.4f} / "
@@ -102,4 +119,8 @@ def main(forward_horizon: int = 5):
if __name__ == "__main__":
main()
parser = argparse.ArgumentParser(description="Chinese equity quant backtest")
parser.add_argument("--universe", default="csi500", choices=["hs300", "csi500"])
parser.add_argument("--signal", default="reversal_vol", choices=["reversal", "reversal_vol"])
args = parser.parse_args()
main(universe=args.universe, signal_name=args.signal)