【Python量化策略实战 #02】多因子合成mom和vol两因子打分合成

一、为什么需要多因子(痛点开场)

上一篇我们用 6M 动量做单因子选股,效果尚可但有两个天坑:

  • 截面同涨同跌时动量失效(动量因子只在反转市场稳)
  • 小票高波风险未控(动量大但回撤一样大)

量化圈 50 年共识:多因子对单因子 。本篇把 mom(动量) 与 vol(波动率反向) 两个因子做 z-score 标准化,再等权合成综合分,做一个 top5 多 / bottom5 空 的多空对冲 ------ 5 行核心代码讲清楚从原始日线到多空对冲的最小可运行闭环。

二、本文你将得到什么

  1. 截面 z-score 标准化:5 行搞定 20 只股票 3 个因子的归一化
  2. mom + (-vol) 等权综合分:单个信号把"涨得好 + 波动小"压成一个分数
  3. 多空对冲回测:top5 多 / bottom5 空 等权对冲,统计多空价差 + Sharpe
  4. 完整的因子数据落地 :每次再平衡的 mom_z / vol_z / composite 进 factors.json
  5. 一个端到端 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,多空对冲会展现显著差。

五、常见坑与注意

  1. z-score 不能用全历史均值/方差 :必须截面内 标准化 ------ 每只股票与其他股票横比,不是与自己的过去纵比。本文的 mom.mean() / mom.std(ddof=0) 都是截面的(按 axis=0 默认)。
  2. 因子方向一致性 :综合分前要确保所有因子方向"越大越好"。-vol 是反向因子,要在合成前取负号;turnover 也是反向(低换手跑赢)也要取负号。
  3. 缺失值处理 :停牌、退市、停盘的股票会让整列变 NaN。建议在因子计算前 panel.dropna(axis=1, how="all") 或在 z-score 时 skipna=True。
  4. 权重归一化 :等权时 mom : vol = 0.5 : 0.5、反转后 mom : vol_inverse = 0.5 : 0.5,不要直接写成 mom : vol = 1 : -1(虽然数值等价,但读者一眼不直观)。
  5. 做空限制:A 股融券门槛高,真实落地一般只做多头(top5 多、空头数据当 benchmark)。本文多空对冲是为了给读者看完整的统计逻辑,正式回测可只跑 top5 多头。
  6. 再平衡次数太少的退化:演示证 cap 下再平衡只有 2-3 次,结果不可信。正式证书月度再平衡至少 24 次才有统计意义。

六、小结 + 下篇预告

小结 :本篇用 mom + (-vol) 等权综合分演示了多因子选股的核心链路。5 行 z-score 标准化 + 综合分合成 + 多空对冲回测,是单因子 → 多因子最朴素也最稳定的过渡。下一步:因子有了、选股有了,缺的是 何时买卖** ------ 下一篇我们把动量因子 + 双均线择时叠加起来,看能不能用均线减仓躲大跌。**

下篇预告:【Python量化策略实战 #03】择时信号怎么加?双均线金叉死叉实战。


代码与文档


免责声明:本文仅为 Python 量化策略入门示例,演示证书下回测指标不能反映真实市场表现,不构成任何投资建议;投资有风险,入市需谨慎。

相关推荐
思麟呀1 小时前
位运算:从一个整数到一个集合
c语言·数据结构·c++·算法
m0_380743872 小时前
PHP7.3和7.2字符串功能有什么不同
开发语言·php
FYKJ_20102 小时前
SSM校园失物招领系统41452-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·typescript·spark
All for pursuit.3 小时前
【分治-3】347.前K个高频元素
数据结构·c++·算法·leetcode
m0_380743873 小时前
PHP 8.4的新语法怎么用才规范
开发语言·php
头发够用的程序员3 小时前
TensorRT 自定义算子插件实战(一):从零手写 customScaledTanh
c++·人工智能·pytorch·python·深度学习·边缘计算·jetson
哭泣方源炼蛊3 小时前
正则表达式c++内容汇集
数据库·c++·mysql·正则表达式
阿狗童鞋3 小时前
Java并发编程实战指南
java·开发语言
xiaoqi01953 小时前
期货量化软件回测方式深度横评:期魔方、文华财经、无限易、TB开拓者、金字塔全面对比
python·机器学习