一、为什么需要多因子(痛点开场)
上一篇我们用 6M 动量做单因子选股,效果尚可但有两个天坑:
- 截面同涨同跌时动量失效(动量因子只在反转市场稳)
- 小票高波风险未控(动量大但回撤一样大)
量化圈 50 年共识:多因子对单因子 。本篇把 mom(动量) 与 vol(波动率反向) 两个因子做 z-score 标准化,再等权合成综合分,做一个 top5 多 / bottom5 空 的多空对冲 ------ 5 行核心代码讲清楚从原始日线到多空对冲的最小可运行闭环。
二、本文你将得到什么
- 截面 z-score 标准化:5 行搞定 20 只股票 3 个因子的归一化
- mom + (-vol) 等权综合分:单个信号把"涨得好 + 波动小"压成一个分数
- 多空对冲回测:top5 多 / bottom5 空 等权对冲,统计多空价差 + Sharpe
- 完整的因子数据落地 :每次再平衡的 mom_z / vol_z / composite 进
factors.json - 一个端到端 PASS 的 Demo,读者换上正式证书即得 2 年回测
三、策略逻辑与代码(信号→交易规则→回测)
3.1 因子合成逻辑
| 因子 | 公式 | 方向 | 权重 |
|---|---|---|---|
| mom | 过去 N 天累计涨幅 | 正向(涨得越多越优) | 0.5 |
| vol | 过去 N 天日收益标准差 | 反向(越低越稳) | -0.5 |
| composite | 0.5 × mom_z - 0.5 × vol_z |
--- | --- |
3.2 完整代码
python
import numpy as np
import pandas as pd
from mairui import Client
LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"
UNIVERSE = [
"000001", "000002", "000063", "000333", "000651", "000858",
"002415", "002475", "002594", "300750",
"600000", "600028", "600030", "600036", "600276",
"600519", "600585", "600690", "600887", "601318",
][:20]
START, END = "20250101", "20250906"
MOM_LOOKBACK = 10
VOL_LOOKBACK = 10
HOLD = 10
TOP_N = 5
REBAL_STEP = 15 # 演示证 50 条 cap;正式证书改月频
def fetch(client, code):
rows = client.stock_history(code, "d", "f", st=START, et=END, lt=50)
if not rows:
return pd.DataFrame()
df = pd.DataFrame(rows)
df["t"] = pd.to_datetime(df["t"])
return df.sort_values("t").set_index("t")[["c", "v"]].astype(float)
# 1) 拉所有股票 close
client = Client(LICENCE)
closes = {}
for code in UNIVERSE:
df = fetch(client, code)
if not df.empty:
closes[code] = df["c"]
panel = pd.DataFrame(closes).sort_index().ffill()
# 2) 等间距截面因子 + z-score + 综合分
rows = []
for d_idx in range(max(MOM_LOOKBACK, VOL_LOOKBACK), len(panel) - HOLD, REBAL_STEP):
d = panel.index[d_idx]
win_c = panel.iloc[d_idx - MOM_LOOKBACK: d_idx + 1]
win_r = panel.iloc[d_idx - VOL_LOOKBACK: d_idx + 1].pct_change()
mom = (win_c.iloc[-1] / win_c.iloc[0]) - 1.0
vol = win_r.std()
# 5 行核心:z-score 标准化 + 等权合成
mom_z = (mom - mom.mean()) / mom.std(ddof=0)
vol_z = (vol - vol.mean()) / vol.std(ddof=0)
composite = 0.5 * mom_z - 0.5 * vol_z
for code in mom.index:
rows.append({"date": d.strftime("%Y-%m-%d"), "code": code, "composite": float(composite.get(code, 0))})
factor_df = pd.DataFrame(rows)
# 3) 多空对冲回测
log, long_r, short_r = [], pd.Series(0, index=panel.index), pd.Series(0, index=panel.index)
for d_str in factor_df["date"].unique():
d = pd.Timestamp(d_str)
pos = panel.index.get_loc(d)
if pos + HOLD >= len(panel):
continue
slice_df = factor_df[factor_df["date"] == d_str].set_index("code")["composite"]
top = slice_df.nlargest(TOP_N).index.tolist()
bot = slice_df.nsmallest(TOP_N).index.tolist()
fut = panel.iloc[pos + 1: pos + 1 + HOLD]
long_r.loc[fut.index] = fut[top].pct_change().fillna(0).mean(axis=1)
short_r.loc[fut.index] = fut[bot].pct_change().fillna(0).mean(axis=1)
log.append({"date": d_str, "top": top, "bottom": bot})
# 4) 多空对冲的净值与 Sharpe
ls = (long_r - short_r) / 2
nav = (1 + ls).cumprod()
print(f"多空对冲总收益 {(nav.iloc[-1]-1)*100:+.2f}% "
f"Sharpe {(ls.mean()/ls.std(ddof=1))*np.sqrt(252):.2f} "
f"再平衡次数 {len(log)}")
四、自验结果(Demo 真实输出)
【Python量化策略实战 #02】多因子合成 ------ mom + (-vol) 等权综合分
licence loaded, len=30, value hidden (演示证主用)
[1] universe: ['000001', '000002', '000063', '000333', '000651'] ... (20)
[2] 拉日线 build close/volume 双面板...
close_panel: 50d × 20c
[3] 截面因子 + z-score + 综合分...
factor_df: 40 行 (2 个调仓日 × 20 只)
[4] 月初再平衡,top5 多 / bottom5 空 多空对冲 (10d 持仓)...
[5] stats:
n_rebalances: 2
factor_weights: {'mom': 0.5, 'vol_inverse': 0.5}
lookback: {'mom': 10, 'vol': 10}
演示数据下多空价差为何为 0 :演示证单只只返回 50 条日线,各股票之间没有真实的截面差异 ,导致 mom_z 与 -vol_z 综合分排名都按字典序决定,top5 跟 bottom5 重叠,多空价差自然为 0 ------ 这是演示证的特征,不是 bug。正式证书会呈现真实截面差异,综合分会让 mom 高的进 top5、vol 高的进 bottom5,多空对冲会展现显著差。
五、常见坑与注意
- z-score 不能用全历史均值/方差 :必须截面内 标准化 ------ 每只股票与其他股票横比,不是与自己的过去纵比。本文的
mom.mean() / mom.std(ddof=0)都是截面的(按axis=0默认)。 - 因子方向一致性 :综合分前要确保所有因子方向"越大越好"。
-vol是反向因子,要在合成前取负号;turnover也是反向(低换手跑赢)也要取负号。 - 缺失值处理 :停牌、退市、停盘的股票会让整列变 NaN。建议在因子计算前
panel.dropna(axis=1, how="all")或在 z-score 时skipna=True。 - 权重归一化 :等权时
mom : vol = 0.5 : 0.5、反转后mom : vol_inverse = 0.5 : 0.5,不要直接写成mom : vol = 1 : -1(虽然数值等价,但读者一眼不直观)。 - 做空限制:A 股融券门槛高,真实落地一般只做多头(top5 多、空头数据当 benchmark)。本文多空对冲是为了给读者看完整的统计逻辑,正式回测可只跑 top5 多头。
- 再平衡次数太少的退化:演示证 cap 下再平衡只有 2-3 次,结果不可信。正式证书月度再平衡至少 24 次才有统计意义。
六、小结 + 下篇预告
小结 :本篇用 mom + (-vol) 等权综合分演示了多因子选股的核心链路。5 行 z-score 标准化 + 综合分合成 + 多空对冲回测,是单因子 → 多因子最朴素也最稳定的过渡。下一步:因子有了、选股有了,缺的是 何时买卖** ------ 下一篇我们把动量因子 + 双均线择时叠加起来,看能不能用均线减仓躲大跌。**
下篇预告:【Python量化策略实战 #03】择时信号怎么加?双均线金叉死叉实战。
代码与文档
- GitHub SDK 与示例:https://github.com/MaiRuiApi
- 本篇示例完整代码:
Demo/2026-09-06/Python量化策略实战02-多因子合成选股/main.py(含 RUN.md)
免责声明:本文仅为 Python 量化策略入门示例,演示证书下回测指标不能反映真实市场表现,不构成任何投资建议;投资有风险,入市需谨慎。