📚 前置已完成第 1 课 + 作业 1~4(见 lessons/01_pandas入门.md)📄 代码02_多标的回测.py(本课新增)、config.py(新增 STOCK_POOL)

第 2 课:多标的回测 —— 一个策略跑 50 只股票

前置:已完成第 1 课 + 作业 1~4(见 lessons/01_pandas入门.md) 代码文件:02_多标的回测.py(本课新增)、config.py(新增 STOCK_POOL) 学完本课 → 打开 lessons/03_单因子测试.md 开新会话学第 3 课


0. 新会话背景速览(给"接手的 AI"看的 30 秒)

  • 项目位置:D:/code/量化交易
  • 学生已学完:pandas 基础、回测框架(engine 的 shift(1))、作业 1~4(含自己写 RSI)
  • 本课已交付:02_多标的回测.py 可正常运行,config.STOCK_POOL(50 只股票)已配置
  • 运行方式:项目根目录下 python 02_多标的回测.py(第一次会下载真实行情,之后走缓存)
  • 输出:output/multi_stock_summary.csvoutput/multi_stock_excess.png(条形图)、output/multi_stock_scatter.png(散点图)
  • 框架要点:信号 1=持有 0=空仓;signal.shift(1) 防前视;backtest/metrics.pycompute_metrics 返回字符串(方便打印,不能做数值统计)

1. 本课目标

上一课只看一只股票(600519),结论容易被运气带偏。本课解决这个问题:

  1. for 循环批量跑:同一套策略跑遍 50 只股票
  2. 超额收益:策略总收益 − 买入持有总收益(策略到底有没有用的核心指标)
  3. 赢面:策略跑赢买入持有的股票占比(量化里最该盯的一个数字)
  4. 平均 vs 中位数:少数暴涨股会把平均拉高,中位数代表"普通股票"水平
  5. 出两张图:超额收益条形图 + 策略 vs 基准散点图(对角线上方 = 策略赢)

2. 概念先讲透

概念 一句话解释
单标的 vs 多标的 一只股票的结果 = 策略 + 运气;一批股票的平均/赢面才是策略的"体检报告"
超额收益 策略收益 − 买入持有收益。为正 = 择时有效果;为负 = 还不如躺着不动
赢面 跑赢基准的股票占比。50% 以下说明策略没创造价值,只有择时运气
平均 vs 中位数 平均会被少数暴涨股拉高,中位数代表"随机挑一只股票的普通结果"

3. 代码讲解(对着 02_多标的回测.py 看)

3.1 股票池(config.py

STOCK_POOL = [ ... 50  ... ]

挑选原则写在注释里:覆盖消费/金融/医药/科技/制造/其他大盘 6 个行业, 别光挑"看起来会涨"的股票——否则等于考试前先偷看了答案。

3.2 逐股分析(analyze_one

为什么不用 compute_metrics?因为它返回格式化字符串(如 "12.34%"), 字符串没法做加减平均,所以这里手算一遍数值指标——顺便复习年化/夏普/回撤公式:

strat_total = cum_strat.iloc[-1] - 1                      # 策略总收益
annual = (1 + strat_total) ** (TRADING_DAYS / n) - 1       # 年化(复利)
vol = res["strategy_ret"].std() * np.sqrt(TRADING_DAYS)    # 年化波动率
sharpe = (annual - rf) / vol
mdd = max_drawdown(cum_strat)                              # 复用 metrics 里的函数

3.3 批量收集

rows = []
for i, code in enumerate(config.STOCK_POOL, 1):
    row = analyze_one(code, load_data(code))
    rows.append(row)          # 每只股票一行,最后拼成一张总表
summary = pd.DataFrame(rows)

3.4 汇总统计(本课的核心输出)

win = summary["超额收益"] > 0
win.mean()   # 赢面 = 跑赢基准的股票占比
summary["超额收益"].mean()    # 平均(会被暴涨股拉高)
summary["超额收益"].median()  # 中位数(普通股票水平)

3.5 两张图

  • 条形图:50 只股票超额收益排序,正绿负红,零轴一眼看出赢面
  • 散点图:横轴买入持有收益、纵轴策略收益,画一条"跑平线"(对角线),线上方 = 策略赢

4. 真实运行结果(2021-01 ~ 2025-06,双均线 10/30,50 只真实行情)

这是本课跑出来的实际结果,供你复跑对照:

指标 结果
策略平均总收益 −5.05%
买入持有平均总收益 +16.68%
平均超额收益 −21.73%
中位数超额收益 −0.96%
超额最好 / 最差 +112.05% / −519.10%
赢面 50%(25/50)
策略平均最大回撤 −44.11%

这个结果本身就是最好的教材: - 双均线平均跑输买入持有 21.7%,赢面只有 50% → 择时没有创造价值 - 跑输最惨的全是单边慢牛股:中国神华(基准 +695%、超额 −519%)、 中国石油(+290%、−194%)、紫金矿业(+128%、−166%)、农业银行(+207%、−133%) → 双均线在这些股票上反复被"洗下车",一卖就涨 - 超额收益最高的:宁德时代 +112%、东方财富 +100%、隆基绿能 +76% → 都是震荡/深跌后反弹的行情,均线来回吃波段

注:此结果会随数据缓存和参数变化而不同,重点看"数量级和方向"而不是精确数字。

超额收益条形图(正绿负红,零轴上方 = 跑赢买入持有):

策略 vs 买入持有散点图(对角线上方 = 策略赢):

5. 作业 5

02_多标的回测.pyanalyze_one 的策略从双均线换成 RSI:

from strategies.rsi import rsi_signal
sig = rsi_signal(df, config.RSI_PERIOD)

重跑,回答三个问题: 1. RSI 的赢面是多少?比双均线高还是低? 2. RSI 在单边慢牛股(如中国神华)上还会大幅跑输吗?为什么? 3. 从结果反推:双均线和 RSI 各自更适合什么行情?(提示:均线吃趋势、RSI 吃震荡)

做完再看:如果 RSI 赢面还是不到一半,别急着失望—— 这正是第 4 课"参数敏感性分析"和第 3 课"单因子测试"要解决的事: 与其在单只股票上择时,不如全市场选股。

作业 5 参考答案(真实数据跑出来的,供对照)

指标 双均线(10,30) RSI(14)
策略平均总收益 −5.05% +1.44%
平均超额收益 −21.73% −15.24%
中位数超额收益 −0.96% +20.78%
赢面 50%(25/50) 62%(31/50)
平均最大回撤 −44.11% −16.86%

三个问题的答案要点: 1. 赢面 62%,高于双均线的 50%;回撤从 −44% 砍到 −17%(小一半以上),是赢面高的根源 2. 单边慢牛股上依然大幅跑输(神华超额 −714%):RSI 只在超卖(<30)时买入, 而慢牛股几乎不超卖(神华 1086 天里仅 25 天 RSI<30),策略 97.7% 时间空仓—— 踏空就是最大的跑输,它没亏大钱,但错过了整个 +695% 的行情 3. RSI(均值回归)适合震荡市/下跌市:跌深就买、反弹就跑、靠躲大跌赢; 双均线(趋势跟踪)适合单边趋势,但 10/30 参数太灵敏,震荡市反复被洗; 两者共同死穴是单边慢牛(趋势派被洗下车、反转派等不到回调,双双踏空)

作业 5 的深层教学点:平均 vs 中位数的矛盾

平均超额 −15.24% 但中位数 +20.78%:62% 的股票跑赢基准,平均却是负的—— 被神华(−714%)、石油(−281%)、农行(−207%)、紫金(−117%) 几只慢牛股拉穿。 中位数才是"随机挑一只股票的普通结果"。

作业 6(进阶,可选):状态机版 RSI

现有 rsi_signal 是点位信号(30~70 中间区域信号=0,等于 RSI 一回升到 30 就卖)。 标准玩法是状态机:跌破 30 买入 → 持有 → 冲上 70 才卖,中间维持上次状态:

signal = pd.Series(0, index=df.index)
for i in range(1, len(df)):
    if   rsi_val.iloc[i] < 30: signal.iloc[i] = 1   # 买入
    elif rsi_val.iloc[i] > 70: signal.iloc[i] = 0   # 卖出
    else: signal.iloc[i] = signal.iloc[i-1]          # 中间:维持原状

预测:慢牛股上跑输会变小(能拿住更久),但平均回撤会变大(下跌初期持仓变长)。 跑完对照你的直觉。

作业 6 参考答案(真实数据跑出来的,供对照)

指标 RSI 点位版(A) RSI 状态机版(B)
策略平均总收益 +1.44% +18.41%
平均超额收益 −15.24% +1.73%(转正!)
中位数超额收益 +20.78% +25.10%
赢面 62%(31/50) 66%(33/50)
平均最大回撤 −16.86% −40.27%

两条预测全部兑现: 1. 单边慢牛股跑输变小(拿得更久):神华 −714%→−635%、石油 −281%→−216%、 紫金 −117%→−53%、石化 −131%→−93%、农行 −207%→−199% 2. 平均回撤变大:−16.86% → −40.27%(下跌初期持仓变长)

白捡的惊喜:赢面从 62% 升到 66%,平均超额收益从 −15.24% 转正为 +1.73%—— 状态机版赚得更多、赢面更高,代价是回撤大了一倍多。

本质是风险-收益取舍: - A 版(抢反弹):只持仓 55 天/1086 天,回撤小(−17%),但交易 28 次磨损大、平均超额为负 - B 版(吃整段):持仓 376 天,回撤大(−40%),但把"超卖→超买"整段行情吃进肚,赢面 66% - 没有绝对更好的版本,只有不同性格——B 更接近教科书的 RSI,A 更像"抄底快枪手" - 下一步思路(预告第 5 课):B 的回撤能不能用仓位管理压下去? (第 5 课:波动大少买、波动小多买,而不是永远全仓)

6. 学完自检清单

  • [ ] 能说清"为什么不能只看一只股票的绩效"
  • [ ] 能说清超额收益、赢面的定义,以及为什么平均和中位数要一起看
  • [ ] 复跑过 python 02_多标的回测.py,结果和上面的表数量级一致
  • [ ] 能解释散点图里"对角线上方 = 策略赢"
  • [ ] 做完作业 5(RSI 版多标的回测),能说出双均线 vs RSI 的适用行情差异

7. 下一课预告

第 3 课:单因子测试lessons/03_单因子测试.md) 从"对一只股票选时"升级到"对全市场选股": 把股票池按某个因子(动量/反转/低波动……)分组,看高分组是否真的比低分组涨得多, 学会用 IC 和分组收益检验一个因子有没有用——这是量化研究员最日常的工作。