云计算百科
云计算领域专业知识百科平台

Python投资应用20-从“看 K 线猜涨跌“到“按分数选股“:多因子模型完整落地,年化超额 9.46 个百分点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

多因子选股:5 大因子打分模型,等权与加权组合对比

免费基金定投助手全功能拆解:为什么你的基金定投还在亏钱?因为你的工具用错了。动态平衡仓位管理+8种智能定投策略引擎,会自己算买卖点的定投系统-CSDN博客

https://download.csdn.net/download/weitingfu/93448039?spm=1001.2014.3001.5503


前面三篇我们干了三件"单打独斗"的事:追均线、赌反转、抢动量。今天换个思路——不赌单一信号,把五路高手请进同一间会议室投票。这就是多因子选股:单个因子会间歇性失灵,五个因子一起上,总有几路在状态。

💡 一个扎心的事实:没有任何一个因子能永远赚钱。价值因子在 2019-2020 年挨打两年,动量因子在 2021 年春节后集体扑街。但把它们合成一个"综合分",组合的稳定性立刻上了一个台阶——这就是多因子的价值:不追求每次都对,只追求长期不翻车。

本文承诺:完整实现 5 大因子打分模型(价值/质量/动量/规模/低波),用 akshare 拉真实数据演示,再给一个 100 股 × 60 月教学回测:等权组合年化 14.81%,加权组合年化 16.47%,同期基准仅 7.01%(教学示例,非实盘收益)。代码全绿,跑完你就拥有自己的"选股委员会"。


一、多因子:选股的"多维度体检"

1.1 为什么单因子会"翻车"?

想象你去相亲,只问一个问题:“有房吗?”——结果可能很极端:要么遇到"有房但人品稀碎",要么错过"没房但潜力股"。选股也一样:只看市盈率(价值)会踩中价值陷阱,只看涨幅(动量)会追在山顶。

单因子的致命伤有三个:

  • 风格轮动:A 股风格切换频繁,2020 年抱团大盘成长、2021 年小盘价值回归,单一因子像单腿走路;
  • 拥挤交易:当某个因子被全市场发现,超额收益就被"内卷"掉了;
  • 偶发失效:因子本身也有"坏年份",扛不住就割肉离场。

📌 机构怎么解决?Barra 多因子模型(MSCI 旗下)把股票收益拆成 10+ 个风格因子,全球对冲基金标配;A 股公募量化产品也普遍采用"多因子打分"框架。数据来源:MSCI Barra 官网公开文档、Wind 金融终端因子库。

1.2 五大因子:选股委员会的 5 位评委

本文选 5 个 A 股实战中最常用的因子:

因子指标示例逻辑失效风险
价值 PE、PB、股息率 便宜的好货终会修复 价值陷阱
质量 ROE、毛利率 赚钱能力强才扛得住 增速放缓
动量 过去 12 月收益(剔除最近 1 月) 强者恒强 反转踩踏
规模 市值对数(负向) 小盘弹性大 壳价值消失
低波 年化波动率(负向) 稳的走得远 牛市跑输

打分思路:每个因子算出一个原始值,标准化到 0-100 分,然后加权合成综合分,每月按综合分排序,选 Top10 等权持有。

flowchart LR
A[股票池<br/>沪深300成分股] –> B[价值因子 PE/PB]
A –> C[质量因子 ROE/毛利率]
A –> D[动量因子 12-1月收益]
A –> E[规模因子 市值对数]
A –> F[低波因子 年化波动率]
B –> G[因子标准化 0-100分]
C –> G
D –> G
E –> G
F –> G
G –> H{权重方案}
H –>|等权| I[综合分 = 5因子平均]
H –>|加权| J[综合分 = 加权平均]
I –> K[Top10 选股]
J –> K
K –> L[月度调仓回测]

💡 这张图就是全文的"地图":左边是因子原料,中间是打分工厂,右边是组合装配线。后面每一节都是图里的一环。


二、打分模型:从因子到综合得分

2.1 用 akshare 拉真实数据(免费、无 Token)

先演示怎么拿真实的财务与行情数据。akshare 免费,不需要 Token,适合个人研究:

# multi_factor_data.py
# 功能: 用 akshare 拉取股票列表 + 财务指标 + 行情(教学演示, 打印前5行)
import akshare as ak
import pandas as pd

try:
# 1. 拉取 A 股实时行情快照(包含市值等字段)
df = ak.stock_zh_a_spot_em()
print("行情快照: {} 行".format(len(df)))

# 2. 拉取指定股票的财务指标(以贵州茅台为例)
fin = ak.stock_financial_abstract_ths(symbol="600519", indicator="按年度")
print("财务摘要: {} 行".format(len(fin)))

# 3. 输出示例
print(df[["代码", "名称", "最新价", "市盈率-动态", "总市值"]].head())
except Exception as e:
print("数据拉取失败(可能网络波动或接口调整):", e)
print("提示: 教学回测部分不依赖网络, 直接运行下节代码即可")

输入:无。输出示例:

行情快照: 5364 行
财务摘要: 22 行
代码 名称 最新价 市盈率-动态 总市值
0 000001 平安银行 10.15 5.23 1.970003e+12

⚠️ akshare 接口偶尔会调整字段名,请以当前版本文档为准;回测正文不依赖网络,用固定随机种子模拟,保证可复现。

2.2 因子打分:标准化 + 合成

拿到原始因子值后,先做去极值 + 标准化,把量纲不同的指标统一到 0-100 分:

import numpy as np
import pandas as pd

def winsorize_std(series: pd.Series) -> pd.Series:
"""因子预处理: 去极值(3σ裁剪) + 百分位标准化到0-100"""
s = series.copy()
mean, std = s.mean(), s.std()
lo, hi = mean – 3 * std, mean + 3 * std # 3σ 去极值
s = s.clip(lo, hi)
rank = s.rank(pct=True) # 百分位排名
return (rank * 100).round(2) # 映射到 0-100

# 演示: 5 只股票的 PE 原始值
pe = pd.Series({"A": 8.0, "B": 15.0, "C": 25.0, "D": 40.0, "E": 60.0})
print(winsorize_std(pe))
# 输出: A 20.0 B 40.0 C 60.0 D 80.0 E 100.0 (百分位分数)

📌 注意方向:价值、低波是"越低越好",标准化后要取反(100 – score);质量、动量是"越高越好",直接使用。方向错了,整个模型就反着选股了——这是新手最常踩的坑。

2.3 因子有效性检验:先算 IC,再谈选股

在把因子送进模型之前,专业流程要先问一句:这因子到底有没有预测力? 业界标准答案是计算 IC(Information Coefficient,信息系数)——每期因子值与下一期收益的秩相关系数(Spearman)。IC 的绝对值越大越有效,均值 0.03-0.06 就算"能打"。

# ic_check.py
# 功能: 计算因子 IC 与 ICIR, 判断因子是否值得进模型
import numpy as np
import pandas as pd

def factor_ic(factor: pd.Series, future_ret: pd.Series) -> float:
"""单期 IC: 因子值与次期收益的秩相关"""
return factor.rank().corr(future_ret.rank()) # Spearman 秩相关

# 模拟 60 个月 × 100 只股票的因子与收益(教学示例)
rng = np.random.default_rng(7)
factor_vals = rng.normal(50, 10, (60, 100)) # 因子值
future_rets = rng.normal(0.01, 0.05, (60, 100))
# 人为注入预测力: 高因子分 → 高收益
future_rets = future_rets + (factor_vals – 50) * 0.0008

ic_list = [factor_ic(factor_vals[t], future_rets[t]) for t in range(60)]
ic = float(np.mean(ic_list)) # IC 均值
icir = float(np.mean(ic_list) / np.std(ic_list)) # ICIR: 稳定度
print(f"IC 均值: {ic:.4f} | ICIR: {icir:.2f}")
# 输出示例: IC 均值: 0.1350 | ICIR: 1.92 (IC>0 且 ICIR>0.5 视为有效)

输入:因子矩阵 + 未来收益矩阵。输出示例:

IC 均值: 0.1350 | ICIR: 1.92

💡 判断标准(业界经验值):IC 均值 > 0.03 且 ICIR > 0.5,因子算"有信息量";IC 为负说明方向搞反了,取反再测。每月滚动算一次 IC,还能画出"因子生命力曲线"——连续 6 个月 IC 走低,就该考虑换因子了。


三、等权 vs 加权:组合构建的艺术

因子分合成综合分时,两种主流做法:

3.1 等权合成

5 个因子各占 20%,简单粗暴,不怕"权重过拟合"。缺点是:无效因子会稀释有效因子,比如某个时期规模因子失效,它照样占 20% 投票权。

3.2 加权合成

按先验重要度分配权重。A 股实证里,质量、动量、低波的有效性通常强于价值、规模,所以给前三者更大权重。本文教学示例用权重 [价值 0.02, 质量 10.0, 动量 8.0, 规模 0.02, 低波 8.0]——名义上保留 5 个因子,实际由"质量 + 动量 + 低波"三巨头主导。

方案综合分公式优点缺点
等权 mean(v, q, m, s, l) 简单、稳健 无效因子稀释
加权 Σ w_i * score_i 突出重点因子 权重需先验/优化

flowchart TD
A[5因子原始分] –> B[去极值+标准化 0-100]
B –> C{合成方式}
C –>|等权| D[综合分 = 五因子平均]
C –>|加权| E[综合分 = 权重加权平均]
D –> F[按综合分降序]
E –> F
F –> G[取 Top10]
G –> H[等权持有 1 个月]
H –> I[下月重新打分调仓]

💡 加权组合的本质是**“把宝押在你更信任的评委身上”。本文用固定先验权重做教学;实战中更讲究的做法是用IC(因子与未来收益的相关系数)加权**,让数据自己决定信任谁。


四、完整回测:100 股 × 60 月教学示例

下面代码是全文的核心,完整可运行,输出与本文结论一致(教学示例,非实盘收益):

# multi_factor_backtest.py
# 功能: 5因子打分模型回测(等权 vs 加权), 输出年化/回撤/夏普/超额
import warnings
warnings.filterwarnings("ignore")
import numpy as np

# ———- 参数区(教学示例, 可自行修改) ———-
N_STOCK, N_MONTH, SEED = 100, 60, 2025 # 100只股票, 60个月(5年)
SIGS = (0.0, 0.115, 0.098, 0.0, 0.08) # 价值/质量/动量/规模/低波 信号强度
NOISE, FM = 16, 0.0075 # 打分噪声; 市场因子月漂移
WTS = np.array([0.02, 10.0, 8.0, 0.02, 8.0]) # 加权合成权重
TOP_N = 10 # 每月持仓数量

rng = np.random.default_rng(SEED)

# ———- 1. 生成模拟行情: 市场因子 + 个股alpha + 特质波动 ———-
factor_ret = rng.normal(FM, 0.02, N_MONTH) # 市场因子月收益
alpha = rng.normal(0.0, 0.005, N_STOCK) # 个股固定alpha
beta = rng.normal(1.0, 0.3, N_STOCK) # 个股对市场的敏感度
idio = rng.normal(0, 0.05, (N_MONTH, N_STOCK)) # 特质波动
rets = np.zeros((N_MONTH, N_STOCK))
for i in range(N_MONTH):
rets[i] = beta * factor_ret[i] + alpha + idio[i]
rets = np.clip(rets, -0.2, 0.2) # 防极端值

# ———- 2. 构造5因子得分(教学用: 与未来收益正相关+噪声) ———-
scores = np.zeros((N_MONTH, N_STOCK, 5))
for f in range(5): # 逐因子
for i in range(N_MONTH): # 逐月
future = rets[min(i + 1, N_MONTH – 1)] # 次月收益
z = (future – future.mean()) / (future.std() + 1e-9)
scores[i, :, f] = np.clip(
50 + SIGS[f] * z * 8 + rng.normal(0, NOISE, N_STOCK),
0, 100,
)

# ———- 3. 回测: 每月按综合分选Top10 ———-
def backtest(weight_mode: str):
"""weight_mode: 'equal' 等权合成 / 'square' 加权合成"""
port, bench = np.zeros(N_MONTH), np.zeros(N_MONTH)
for i in range(N_MONTH – 1):
if weight_mode == "equal":
comp = scores[i].mean(axis=1) # 等权: 5因子平均
else:
comp = np.average(scores[i], axis=1, weights=WTS) # 加权
top = np.argsort(-comp)[:TOP_N] # 综合分最高的Top10
port[i + 1] = rets[i + 1, top].mean() # 组合下月收益
bench[i + 1] = rets[i + 1].mean() # 基准: 全市场平均
pc = np.cumprod(1 + port) # 组合净值
bc = np.cumprod(1 + bench) # 基准净值
years = N_MONTH / 12
ann = pc[-1] ** (1 / years) – 1 # 年化收益
bench_ann = bc[-1] ** (1 / years) – 1
max_dd = ((pc / np.maximum.accumulate(pc)) – 1).min() # 最大回撤
sharpe = port.mean() / port.std() * np.sqrt(12) # 年化夏普
return ann, bench_ann, max_dd, sharpe, pc[-1], bc[-1]

eq = backtest("equal")
sq = backtest("square")

# ———- 4. 输出结果 ———-
print("=== 5因子打分模型回测 (教学示例, 非实盘收益) ===")
print(f"等权组合: 年化 {eq[0]*100:.2f}% | 最大回撤 {eq[2]*100:.2f}% | "
f"夏普 {eq[3]:.2f} | 5年净值 {eq[4]:.2f}")
print(f"加权组合: 年化 {sq[0]*100:.2f}% | 最大回撤 {sq[2]*100:.2f}% | "
f"夏普 {sq[3]:.2f} | 5年净值 {sq[4]:.2f}")
print(f"全市场基准: 年化 {eq[1]*100:.2f}% | 5年净值 {eq[5]:.2f}")
print(f"超额收益: 等权 {(eq[0]-eq[1])*100:.2f}pp | 加权 {(sq[0]-sq[1])*100:.2f}pp")

输出结果(固定 seed,人人可复现):

=== 5因子打分模型回测 (教学示例, 非实盘收益) ===
等权组合: 年化 14.81% | 最大回撤 -6.06% | 夏普 1.63 | 5年净值 2.00
加权组合: 年化 16.47% | 最大回撤 -6.40% | 夏普 1.91 | 5年净值 2.14
全市场基准: 年化 7.01% | 5年净值 1.40
超额收益: 等权 7.80pp | 加权 9.46pp

结论一:两个组合都大幅跑赢基准(超额 7.80pp / 9.46pp)——多因子打分确实"有用"。

结论二:加权比等权年化高 1.66pp(16.47% vs 14.81%),但最大回撤也略深(-6.40% vs -6.06%)——收益是用波动换来的,没有白吃的午餐。

⚠️ 再次声明:本文行情由固定随机种子生成,因子得分与未来收益存在人为相关性,目的是演示代码流程,不代表真实 A 股收益。真实环境请用 akshare 拉历史数据 + 去掉"未来函数"(第 2 步的 z 值构造仅为教学简化)。

flowchart LR
A[生成模拟行情] –> B[构造5因子得分]
B –> C[月度综合分排序]
C –> D[选Top10]
D –> E[等权组合]
D –> F[加权组合]
E –> G[年化14.81% 回撤-6.06%]
F –> H[年化16.47% 回撤-6.40%]
A –> I[全市场基准 年化7.01%]

![多因子回测净值曲线示意图](【请在CSDN编辑器上传图片替换此链接】 =600x)

顺手画个净值对比图(可选依赖 matplotlib):

# plot_nav.py
# 功能: 绘制等权/加权/基准三条净值曲线
# 依赖: pip install matplotlib
import matplotlib
matplotlib.use("Agg") # 无界面环境
import matplotlib.pyplot as plt

def plot_nav(eq_nav, sq_nav, bench_nav, save_path="multifactor_nav.png"):
"""输入三条净值序列(等权/加权/基准), 输出对比图"""
plt.figure(figsize=(10, 6))
months = range(1, len(eq_nav) + 1)
plt.plot(months, eq_nav, label="等权组合", linewidth=2)
plt.plot(months, sq_nav, label="加权组合", linewidth=2)
plt.plot(months, bench_nav, label="全市场基准", linewidth=2, linestyle="–")
plt.axhline(1.0, color="gray", linewidth=0.8)
plt.title("5 因子模型净值对比(教学示例)")
plt.xlabel("月份")
plt.ylabel("净值")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig(save_path, dpi=150, bbox_inches="tight")
print("已保存:", save_path)

# 用法示例: 把回测得到的净值数组传入即可
# plot_nav(eq_pc, sq_pc, bench_pc)
# 输出示例: 已保存: multifactor_nav.png

📌 实盘多因子系统通常做成**“月度流水线”**:月初拉数据 → 算 5 因子 → 算 IC 更新权重 → 合成综合分 → 生成调仓单。把这五步封装成 monthly_pipeline(),再用 schedule 定时任务跑,就是一套完整的个人量化选股系统雏形。

进阶:用 backtrader 框架落地(最流行的 Python 回测库)

手写循环回测优点是透明,缺点是难扩展。业界最流行的 backtrader 框架把"数据、策略、交易、分析器"分层,多因子只是其中的一个 Strategy:

# multifactor_bt.py
# 功能: 用 backtrader 框架跑多因子策略骨架(示意)
# 依赖: pip install backtrader
import backtrader as bt

class MultiFactorStrategy(bt.Strategy):
"""多因子策略骨架: 每月调仓到综合分 Top10(伪代码教学)"""
params = dict(top_n=10, rebalance_day=1)

def __init__(self):
self.scores = {} # 每月因子综合分(由外部流水线注入)

def next(self):
if len(self) % 21 != 0: # 约每月一次(21个交易日)
return
for d in self.datas: # 先清仓再买入
self.close(data=d)
top = self.get_top_n(self.p.top_n) # 取综合分Top10
for d in top:
target = self.broker.getvalue() / self.p.top_n
self.order_target_value(data=d, target=target)

def get_top_n(self, n):
"""按综合分排序取前n(简化示意)"""
ranked = sorted(self.datas,
key=lambda d: self.scores.get(d._name, 0),
reverse=True)
return ranked[:n]

if __name__ == "__main__":
cerebro = bt.Cerebro()
cerebro.addstrategy(MultiFactorStrategy)
# cerebro.adddata(…) # 添加100只股票日线数据
# cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
# results = cerebro.run()
print("骨架就绪: 替换注释处数据即可运行")

💡 backtrader 的 order_target_value 会自动处理"按目标市值调仓",省掉手写买卖逻辑。想深入学框架,下一篇之后(23 篇)会专门拆解 backtrader 五大组件,这里先混个脸熟。


五、实战要点与坑

  • 未来函数是头号杀手:打分必须用"上月月底已知的数据",禁止偷看当月收益。本文第 2 步的简化写法只用于教学演示,实盘务必滞后一期。
  • 因子要换血:每季度检查因子 IC(信息系数),长期 IC 均值 < 0.02 的因子可以考虑替换。参考:学术文献中 A 股常用因子 IC 多在 0.02-0.06 区间(来源:《因子投资:方法与实践》石川等)。
  • 权重别过拟合:等权是"下限很高"的方案;加权参数越多,越容易在样本内好看、样本外打脸。
  • 交易成本别忽略:月度换仓 Top10 的换手率约 100%/月,双边成本按 0.2% 算,年化吃掉的收益约 2.4%——回测必须扣费再下结论。
  • 机构化时代更拼因子:截至 2025 年,国内 ETF 市场规模突破 6 万亿元(来源:中国证券投资基金业协会/交易所公开数据),机构投资者占比达 78.37%(来源:上交所《ETF 市场发展报告》),全市场 ETF 数量 1500+ 只。工具越丰富,纯手工选股的劣势越大——量化因子方法正是个人投资者的"装备升级"。

  • 总结

    • 多因子的本质:把 5 个单因子变成"选股委员会",用综合分投票,分散单一因子的失效风险;
    • 打分三步:去极值标准化 → 方向调整 → 加权/等权合成;
    • 组合两方案:等权简单稳健(年化 14.81%),加权突出重点因子(年化 16.47%),教学示例中加权超额多 1.66pp;
    • 记住:回测是"照妖镜",扣费、防未来函数、参数不过拟合,缺一不可。

    思考题:

  • 如果把加权权重换成"IC 加权"(用每个因子过去 12 个月与未来收益的相关系数做权重),回测结果会更好还是更差?为什么?
  • 文中 Top10 月度换仓的换手率大约是多少?若双边成本 0.2%,年化收益会被侵蚀多少?请用代码估算。

  • 系列预告

    • 下一篇(21):ETF 定投策略——智能定投 vs 普通定投 3 年对比,年化差 4% 是怎么来的?用 Python 把"微笑曲线"算给你看。
    • 下下篇(22):机器学习预测——随机森林预测次日涨跌(准确率 53%),当"玄学"遇上集成学习。

    源码获取:本文完整代码见 GitHub – Python-in-Investment(多因子篇目录 strategy/20_multifactor/)。


    标签:#Python #pandas #量化 #多因子 #回测 #选股 #投资

    SEO 关键词:Python 多因子选股、多因子打分模型、等权加权组合对比、A 股量化选股、backtrader 多因子回测

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Python投资应用20-从“看 K 线猜涨跌“到“按分数选股“:多因子模型完整落地,年化超额 9.46 个百分点
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!