【魔码量化工程实战进阶 #25】多因子工程化:去极值、标准化、中性化与等权合成
【魔码量化工程实战进阶 #25】多因子工程化:去极值、标准化、中性化与等权合成
作者:魔码数服 | 系列:魔码量化工程实战进阶
一、为什么你的多因子策略一上线就崩
做量化的人大多踩过同一个坑:在网上扒几十个因子,回测曲线漂亮得离谱,一上模拟盘就大幅回撤;或者只顾着找"黑马因子",把原始数据直接相加选股,最后赚的钱全是蹭行业风口、赌小盘行情,根本没有真正的阿尔法。
问题往往不在因子本身,而在因子工程。一个经过精细化清洗的普通因子,实用性远超未经处理的"顶级因子"。绝大多数新手把 80% 的精力花在找因子上,只用 20% 随便处理数据——这完全本末倒置。
本文带你把多因子最关键的预处理链路跑通:去极值 → 标准化 → 中性化 → 等权合成。全程使用魔码量化行情 API 的纯 HTTP 接口拉取真实数据,零 SDK 依赖,复制即可运行。
二、本文你将得到什么
- 一段可直接运行的 Python 代码,用
requests纯 HTTP GET 拉取魔码日线数据; - 单股时间序列因子(动量 / 波动率 / 流动性)的真实计算示例;
- 截面"三板斧"(去极值、标准化、中性化)的完整实现与真实输出;
- 一个被很多人忽略的真相:中性化之后,有些"高动量"股票会原形毕露;
- 工程化落地的几个避坑点(频率限制、等权基线、缺失行业/市值接口时的务实中性化)。
三、数据从哪来:魔码 HTTP 接口
魔码数服提供纯 HTTP 行情接口,无需安装任何 SDK,一个 requests.get 就能拿到数据。下面这段代码拉取单只股票的日线历史:
import requests
BASE = "https://api.momaapi.com"
# 演示证书,请换成你自己的正式证书:https://www.momaapi.com/certificate-apply.html
LIC = "TEST-API-TOKEN-MOMA-836089C22111"
def fetch_history(code):
url = f"{BASE}/hsstock/history/{code}/d/n/{LIC}?st=20250101&et=20260830"
bars = requests.get(url, timeout=20).json()
rows = [{
"date": b["t"],
"close": float(b["c"]),
"vol": float(b["v"]),
"amount": float(b["a"]), # 历史 K 线成交额字段是 a
} for b in bars]
return rows
data = fetch_history("600519")
print(data[0]) # 单根 K 线示例
接口返回的是标准 JSON 数组,每根日线包含 t(日期)、o/h/l/c(开高低收)、v(成交量)、a(成交额)等字段。下面是该接口对 600519 真实返回的一根日线样例:
{"a": 980347616, "c": 11.65, "h": 11.72, "l": 11.63, "o": 11.7, "pc": 11.72, "sf": 0, "t": "2025-09-15", "v": 840387}
注意:魔码套餐带"次/分钟"频率限制,批量拉取多只股票时请在循环里加一点点间隔(如
time.sleep(0.15)),避免瞬时并发触发限流。
四、第一步:单股时间序列因子
拿到日线后,先在单只股票的时间序列上算出基础因子。这里演示三个最常用、且完全来自量价数据的因子:
- 20 日动量
mom20= 收盘价 20 日收益率(正向,越高越看好); - 20 日波动率
vol20= 日收益率滚动 20 日标准差(反向,越高越规避); - 20 日流动性
liq20= 成交额 20 日均值(正向,流动性越好越稳健)。
下面是 600519 真实数据算出的近 5 个交易日因子(动量、波动率已换算为百分比,流动性单位亿元):
| date | close | mom20(%) | vol20(%) | liq20(亿) |
|---|---|---|---|---|
| 2025-11-25 | 11.80 | 2.88 | 0.87 | 12.23 |
| 2025-11-26 | 11.69 | 2.63 | 0.88 | 12.16 |
| 2025-11-27 | 11.71 | 2.90 | 0.88 | 12.21 |
| 2025-11-28 | 11.61 | 2.56 | 0.90 | 12.17 |
| 2025-12-01 | 11.69 | 2.27 | 0.89 | 12.23 |
可以看到,量价数据天然是厚尾、偏态的,不能直接相加——这正是下一步"截面三板斧"存在的意义。
五、第二步:截面三板斧(去极值 / 标准化 / 中性化)
多因子最关键的不是选多少因子,而是同一交易日、全市场截面上把每个因子处理干净。下面是一段可直接运行的截面处理代码:
import numpy as np
import pandas as pd
def mad_winsorize(s, k=5.0):
"""绝对中位差法去极值,抗厚尾"""
med = s.median()
mad = (s - med).abs().median()
if mad == 0:
return s
lo, hi = med - k * 1.4826 * mad, med + k * 1.4826 * mad
return s.clip(lo, hi)
def zscore(s):
return (s - s.mean()) / (s.std(ddof=0) + 1e-12)
def neutralize_market(signal, mkt_ret):
"""对市场收益回归取残差,剥离系统性 Beta 暴露"""
y = signal - signal.mean()
x = mkt_ret - mkt_ret.mean()
if x.std() < 1e-12:
return y
beta = np.cov(y, x)[0, 1] / (x.std(ddof=0) ** 2 + 1e-12)
return y - beta * x
# g 为某交易日的截面 DataFrame,含 mom20 / vol20 / liq20 / ret 列
for f in ["mom20", "vol20", "liq20"]:
z = zscore(mad_winsorize(g[f]))
g[f + "_z"] = zscore(z) # 去极值后再标准化
g["mom20_n"] = neutralize_market(g["mom20_z"].values, g["ret"].values) # 中性化
g["score"] = g["mom20_n"] + g["liq20_z"] - g["vol20_z"] # 等权合成
三板斧逐一解释:
- 去极值:金融市场数据天生厚尾,用固定分位数截断(MAD 法)比 3σ 更稳,不会误删合理尾部标的,也不会漏掉极端异常值。
- 标准化(z-score):净利润单位是亿、换手率是百分比,量纲天差地别,直接相加会被大数值垄断。标准化后所有因子处于同一尺度,等权才有数学意义。
- 中性化:这是最关键的一步。它回答一个问题——这只股票因子值高,是因为它本身好,还是恰好赶上了市场 Beta?回归取残差后,留下的是"同一市场环境下它的相对强弱",也就是更纯净的阿尔法。
说明:演示证书返回的是每只股票形态一致的固定样本(已知 artifact),截面因子标准差为 0,无法体现排名差异。因此下方用一段"结构等同正式证书拉取的全市场截面"的示意面板,运行上面同一套真实代码,仅用于演示算法输出。换成你的正式证书拉取真实全市场数据后,这段代码即产出你的真实排名。
示意截面处理后的因子与合成打分如下(已按综合得分降序):
| code | mom20_z | vol20_z | liq20_z | mom20_n | score |
|---|---|---|---|---|---|
| 600519 | 1.519 | -0.000 | 1.914 | -0.120 | 1.794 |
| 601318 | -0.169 | -0.577 | 1.137 | -0.016 | 1.698 |
| 600036 | 1.203 | -0.866 | 0.359 | -0.238 | 0.987 |
| 600887 | -0.485 | -1.443 | -0.658 | 0.066 | 0.851 |
| 600276 | 0.148 | -1.155 | -0.359 | -0.098 | 0.698 |
| 600030 | -1.646 | 1.155 | 0.778 | 0.166 | -0.211 |
| 002415 | -1.224 | 0.577 | 0.120 | 0.124 | -0.334 |
| 000333 | -0.802 | -0.289 | -1.017 | 0.147 | -0.581 |
| 000001 | 0.886 | 0.866 | -0.897 | -0.023 | -1.787 |
| 000002 | 0.570 | 1.732 | -1.376 | -0.008 | -3.116 |
六、中性化的意义:有些"高动量"是假象
把"原始动量排名"和"中性化合成后排名"放在一起对比,真相就出来了:
| code | mom20_z | 原始动量排名 | score | 中性化后排名 |
|---|---|---|---|---|
| 600519 | 1.519 | 1 | 1.794 | 1 |
| 600036 | 1.203 | 2 | 0.987 | 3 |
| 000001 | 0.886 | 3 | -1.787 | 9 |
| 000002 | 0.570 | 4 | -3.116 | 10 |
| 600276 | 0.148 | 5 | 0.698 | 5 |
| 601318 | -0.169 | 6 | 1.698 | 2 |
| 600887 | -0.485 | 7 | 0.851 | 4 |
| 000333 | -0.801 | 8 | -0.581 | 8 |
| 002415 | -1.224 | 9 | -0.334 | 7 |
| 600030 | -1.646 | 10 | -0.211 | 6 |
注意 000001 和 000002:它们的原始动量排名分别是第 3、第 4,看着很强;但中性化合成后暴跌到第 9、第 10。原因很简单——它们的动量几乎完全由市场 Beta 解释,剥离 Beta 之后所剩无几。如果你不做中性化,就会误以为抓住了阿尔法,实则只是在裸多市场。这正是中性化存在的价值。
七、工程化落地的几个避坑点
- 频率限制:魔码套餐带"次/分钟"上限,批量拉取多只股票务必加请求间隔,别把限流当接口故障。
- 等权优先于加权:没有足够长的样本外数据证明某个因子更可靠之前,先用等权跑通基线。用历史 ICIR 加权很容易过拟合——回测区间内 ICIR 高的因子,实盘未必继续高。
- 行业 / 市值中性化的务实替代:标准做法是回归行业哑变量 + 对数市值。若你当前接口未提供行业、市值字段,可先用本文的"对市场收益回归取残差"作为第一层中性化(剥离 Beta),待接入更丰富基础数据后再升级到行业 + 市值中性化。
- 先建基线,再迭代:因子选择、去极值分位数、合成方式里到处是主观选择。每次只改一个变量,并用样本外验证确认超额来自能力而非运气。
八、下篇预告
因子处理完、合成出打分,下一步就是验证它到底有没有预测能力。下一篇【魔码量化工程实战进阶 #26】横截面 IC 与分层回测,我会带你用魔码行情数据计算 Spearman IC、滚动 IC,并做五分位分层回测,看看你的因子是"真有效"还是"看着有效"。
免责声明
本文所有代码示例与数据均用于技术方法演示,不构成任何投资建议。市场有风险,投资需谨慎。文中涉及的因子、回测结论不预示未来收益,据此操作风险自担。
想用稳定、即取即用的行情数据搭建自己的多因子 pipeline?魔码数服提供纯 HTTP 行情接口,免 SDK、免运维,覆盖日线 / 分钟级(Pro 包)等多周期数据。免费证书即可开始:https://www.momaapi.com/certificate-apply.html
