从多个因子到可交易组合,按四块写:
多因子合成前,通常先做:先诊断因子相关性与共线性,再选择合成方法。若将相关性过高的多个因子同时纳入模型,回测表现可能良好,但实盘效果往往不佳。实践中常见多个因子相关性超过0.9,造成信息重复。
分三块:
简单平均未必有效——合成方法不对,单因子再好,组合也可能一般。常见顺序:相关性诊断 → 等权基准 → IC/ICIR加权 → 进阶方法。
因子相关性分析旨在厘清各因子之间的关系——是相互补充,还是存在冗余。
多因子建模中,若将相关性过高的多个因子同时纳入模型,回测表现可能良好,但实盘效果往往不佳。实践中常见多个因子相关性超过0.9,造成信息重复。
本章从三个角度介绍因子相关性分析:截面相关性、时间序列相关性,以及共线性问题诊断。
截面相关性,就是看在同一时刻,不同因子在股票池里的取值是否相关。比如某一天,所有股票的估值因子和成长因子之间,是正相关还是负相关?
常用的方法有两种:
例如,假设有三个因子:估值(PE)、动量(MOM)、波动率(VOL)。某一天的截面数据如下:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
# 模拟数据
np.random.seed(42)
n_stocks = 500
data = pd.DataFrame({
'PE': np.random.randn(n_stocks),
'MOM': np.random.randn(n_stocks) * 0.8 + 0.2,
'VOL': np.random.randn(n_stocks) * 0.5 - 0.1
})
# 计算斯皮尔曼相关系数矩阵
corr_matrix = data.corr(method='spearman')
print(corr_matrix)
PE MOM VOL PE 1.000000 -0.090156 -0.077518 MOM -0.090156 1.000000 0.073234 VOL -0.077518 0.073234 1.000000
输出结果示例如下:
| PE | MOM | VOL | |
|---|---|---|---|
| PE | 1.00 | 0.03 | -0.12 |
| MOM | 0.03 | 1.00 | 0.45 |
| VOL | -0.12 | 0.45 | 1.00 |
MOM与VOL的相关性为0.45,属于中等偏高水平。动量因子与波动率因子常存在关联——高波动股票往往动量也较强,需加以注意。
从经验来看,截面相关系数超过0.6的因子对,需考虑合并或剔除其一;超过0.8的,基本可视为重复因子。
截面相关性考察横截面关系,时间序列相关性考察时间维度上因子收益的同步性,即两个因子在时间轴上的收益是否同涨同跌。
怎么算?先把每个因子每天的收益率算出来,然后计算这些收益率序列之间的相关性。
# 假设我们有因子收益率数据
factor_returns = pd.DataFrame({
'PE_ret': np.random.randn(252) * 0.02,
'MOM_ret': np.random.randn(252) * 0.015,
'VOL_ret': np.random.randn(252) * 0.01
})
# 计算时间序列相关性
time_corr = factor_returns.corr
print(time_corr)
PE_ret MOM_ret VOL_ret PE_ret 1.000000 0.079345 0.076862 MOM_ret 0.079345 1.000000 -0.056697 VOL_ret 0.076862 -0.056697 1.000000
时间序列相关性高,意味着两个因子在大部分时间里表现相似。其后果是:市场风格切换时,多因子组合可能同时失效。
曾构建包含价值和低波两个因子的模型:截面相关性仅0.2,看似分散;但时间序列相关性高达0.7——市场下跌时两因子同时表现较差,该年度组合回撤超过15%。
截面相关性与时间序列相关性均需考察。截面相关低不代表时间序列相关低。两个维度均较低的因子组合,分散化效果更好。
共线性,就是因子之间高度线性相关。这会导致回归系数不稳定,因子贡献说不清楚。
诊断共线性常用以下三个指标:
代码实现如下:
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.preprocessing import StandardScaler
# 标准化数据
scaler = StandardScaler
X_scaled = scaler.fit_transform(data)
# 计算VIF
vif_data = pd.DataFrame
vif_data['Factor'] = data.columns
vif_data['VIF'] = [variance_inflation_factor(X_scaled, i) for i in range(X_scaled.shape[1])]
print(vif_data)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 43 try: ---> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ModuleNotFoundError: No module named 'sklearn.__check_build._check_build' During handling of the above exception, another exception occurred: ImportError Traceback (most recent call last) <ipython-input-3-dcf3aef96062> in <cell line: 0>() 1 from statsmodels.stats.outliers_influence import variance_inflation_factor ----> 2 from sklearn.preprocessing import StandardScaler 3 4 # 标准化数据 5 scaler = StandardScaler() d:\pythonprojects\venv\Lib\site-packages\sklearn\__init__.py in <module> 79 # it and importing it first would fail if the OpenMP dll cannot be found. 80 from . import _distributor_init # noqa: F401 ---> 81 from . import __check_build # noqa: F401 82 from .base import clone 83 from .utils._show_versions import show_versions d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ---> 46 raise_build_error(e) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in raise_build_error(e) 29 else: 30 dir_content.append(filename + '\n') ---> 31 raise ImportError("""%s 32 ___________________________________________________________________________ 33 Contents of %s: ImportError: No module named 'sklearn.__check_build._check_build' ___________________________________________________________________________ Contents of d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build: setup.py _check_build.cp38-win_amd64.pyd__init__.py __pycache__ ___________________________________________________________________________ It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run `python setup.py install` or `make` in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.
输出:
| Factor | VIF |
|---|---|
| PE | 1.15 |
| MOM | 1.42 |
| VOL | 1.38 |
上述三个因子的VIF均不高,说明共线性问题不大。若某因子VIF超过10,则需进一步处理。
构建行业因子时,若将11个行业哑变量全部纳入,VIF可能显著升高。行业哑变量天然存在共线性——所有行业之和等于截距项。解决办法是去掉一个行业作为基准。
总结一下,当发现因子之间存在高相关性时,可采用以下处理方式:
因子筛选阶段可先进行聚类分析,将高相关因子聚为一类,再从每类中选取代表因子,既保留信息又避免冗余。
因子相关性分析可视为对因子体系的系统检验:截面相关性反映静态关系,时间序列相关性反映动态关系,共线性诊断反映深层结构。三者结合,方能构建稳健的多因子体系。
因子并非越多越好,独立性更为重要。若两个因子总在同一时期表现不佳,叠加使用只会放大错误,而非分散风险。
多因子合成是将多个单因子合并为综合得分的过程。因子数量较多时,不宜逐一单独使用,必须进行合成。本节介绍四种基础合成方法。虽较为简单,实战中仍常使用,其中ICIR加权是性价比较高的入门选择。
等权加权对所有因子赋予相同权重,每个因子权重均为1/N。该方法最为朴素,也是常见的初始选择。
优点很明显:
缺点也很致命:
核心公式如下。注意:所有因子必须先做标准化处理,否则量纲不同会导致某个因子主导。
$$ 综合得分 = (因子1 + 因子2 + ... + 因子N) / N $$曾出现某团队用等权法合成10个因子、回测效果良好,但实盘半年后其中一个因子失效、组合收益大幅下滑的情况。这是等权法的局限——无法自动识别应剔除的因子。
等权法适合作为基准。开发新因子时,可先用等权法合成检验效果;若等权法仍无效,该因子组合通常难以成立。
市值加权按股票市值大小分配权重,大市值股票权重高,小市值股票权重低,思路类似指数编制。
市值加权按股票市值大小分配权重,大市值权重高、小市值权重低,思路类似指数编制。
因子合成与市值的关系在于:部分因子在大市值股票上表现更好,部分在小市值上表现更好。市值加权法天然偏向大市值,若因子本身存在市值偏好,该方法会放大该偏好。
具体做法:
公式如下:
$$ 综合得分_i = Σ(w_j * factor_ij) $$$$ w_j = 市值_j / Σ(市值) $$曾用市值加权法构建流动性因子组合,发现收益几乎完全来自大市值股票——实质是在选市值而非选股。改用IC加权后,效果趋于正常。
市值加权法易引入市值偏差。若不希望组合偏向某一市值区间,不宜采用该方法。回测期间大市值恰好上涨时,回测曲线可能虚高,实盘则难以复现。
IC加权,就是用因子与未来收益的相关系数作为权重。IC值越高,说明这个因子预测能力越强,权重就越大。
IC加权以因子与未来收益的相关系数为权重。IC越高,预测能力越强,权重越大。
计算步骤:
公式如下。注意:IC值有正有负,建议用绝对值归一化,否则正负抵消会导致权重混乱。
$$ 综合得分 = Σ(IC_i * factor_i) / Σ(|IC_i|) $$通常采用过去12个月的滚动IC均值。12个月可在噪声敏感性与反应速度之间取得平衡——过短易受噪声干扰,过长则反应滞后。
例如,假设三个因子的IC值分别为0.05、0.03、-0.02,则权重如下:
| 因子 | IC值 | 权重 |
|---|---|---|
| 因子A | 0.05 | 50% |
| 因子B | 0.03 | 30% |
| 因子C | -0.02 | 20% |
因子C虽为负IC,仍保留20%权重。负IC本身也是信息,表明该因子可能反向有效,直接剔除反而浪费。
IC加权对IC稳定性要求较高。若某因子IC波动剧烈,权重也会大幅变化。建议先做IC平滑,如采用指数移动平均。
ICIR加权在IC加权基础上进一步考虑IC稳定性。IR(Information Ratio)为IC均值除以标准差,既反映预测能力,也反映稳定性。
相较IC加权更进一步:IC高但波动大的因子不宜重仓。ICIR加权即用于解决该问题。
公式如下:
IR值越高,说明这个因子不仅预测能力强,而且稳定可靠。这样的因子值得给更高权重。
曾发现某因子IC均值0.06,但IR仅0.3(IC标准差高达0.2);另一因子IC均值0.04,IR为0.8。实盘中后者表现通常更优——稳定因子才能持续贡献收益。
ICIR加权是推荐的入门级合成方法,兼顾收益与稳定性,计算亦较为简便。
四种方法对比如下:
| 方法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 等权加权 | 一视同仁 | 简单、无过拟合 | 忽略因子质量 | 基准测试、因子初筛 |
| 市值加权 | 大市值优先 | 接近指数编制 | 引入市值偏差 | 大盘风格组合 |
| IC加权 | 按预测能力分配 | 区分因子好坏 | 忽略稳定性 | 因子质量差异大时 |
| ICIR加权 | 兼顾收益和稳定 | 综合表现最优 | 需要较长历史数据 | 追求稳健超额收益 |
以下为Python代码示例,展示四种方法的实现:
import pandas as pd
import numpy as np
def equal_weight(factors):
"""等权加权法"""
return factors.mean(axis=1)
def market_cap_weight(factors, market_cap):
"""市值加权法"""
weights = market_cap / market_cap.sum
return (factors * weights).sum(axis=1)
def ic_weight(factors, future_returns, window=12):
"""IC加权法"""
ic = factors.rolling(window).corr(future_returns)
weights = ic.mean / ic.mean.abs.sum
return (factors * weights).sum(axis=1)
def icir_weight(factors, future_returns, window=12):
"""ICIR加权法"""
ic = factors.rolling(window).corr(future_returns)
ir = ic.mean / ic.std
weights = ir / ir.sum
return (factors * weights).sum(axis=1)
# 使用示例
# score = icir_weight(factor_df, return_df)
上述方法仅为起点。进阶应用还包括动态调整、行业中性化、风险约束等。可以先掌握这四种基础方法,再逐步扩展。
当因子数量较少时,等权、IC加权等基础合成方法尚可适用;一旦持有数十个因子,共线性、噪音堆积、过拟合等问题将更为突出。本节介绍三种进阶合成方法,核心目标是:从互相纠缠的因子中提取干净、有效的信号。
PCA即主成分分析,用于降维。多个因子中往往存在高度相关——如动量与反转、换手率与波动率,可能描述同一类市场行为。
PCA可提取一组新的变量(主成分),这些变量是原始因子的线性组合,且彼此正交(不相关)。第一个主成分解释的方差最大,第二个次之,依此类推。
核心思想: 用少数几个主成分,保留原始因子矩阵的大部分信息。
设有 n 只股票、k 个因子,构成矩阵 X(n×k)。PCA的步骤是:
实践中需注意:部分因子方差大但与收益相关性低,单纯按方差选取主成分效果可能不佳。建议先做IC筛选,仅保留IC绝对值超过阈值的因子,再进行PCA。
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 假设 factor_data 是 DataFrame,行是股票,列是因子
scaler = StandardScaler
factor_scaled = scaler.fit_transform(factor_data)
pca = PCA(n_components=0.85) # 保留85%方差
pca.fit(factor_scaled)
# 查看各主成分的方差解释率
print("解释方差比例:", pca.explained_variance_ratio_)
# 合成因子:用方差解释率加权
weights = pca.explained_variance_ratio_ / pca.explained_variance_ratio_.sum
pca_factor = np.dot(pca.transform(factor_scaled), weights)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 43 try: ---> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ModuleNotFoundError: No module named 'sklearn.__check_build._check_build' During handling of the above exception, another exception occurred: ImportError Traceback (most recent call last) <ipython-input-5-bc8b2aeec203> in <cell line: 0>() 1 import numpy as np 2 import pandas as pd ----> 3 from sklearn.decomposition import PCA 4 from sklearn.preprocessing import StandardScaler 5 d:\pythonprojects\venv\Lib\site-packages\sklearn\__init__.py in <module> 79 # it and importing it first would fail if the OpenMP dll cannot be found. 80 from . import _distributor_init # noqa: F401 ---> 81 from . import __check_build # noqa: F401 82 from .base import clone 83 from .utils._show_versions import show_versions d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ---> 46 raise_build_error(e) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in raise_build_error(e) 29 else: 30 dir_content.append(filename + '\n') ---> 31 raise ImportError("""%s 32 ___________________________________________________________________________ 33 Contents of %s: ImportError: No module named 'sklearn.__check_build._check_build' ___________________________________________________________________________ Contents of d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build: setup.py _check_build.cp38-win_amd64.pyd__init__.py __pycache__ ___________________________________________________________________________ It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run `python setup.py install` or `make` in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.
PCA合成因子的回测夏普比率通常比等权高0.2-0.5。但PCA对极端值敏感——若未做MAD去极值,第一个主成分可能被个别异常股票主导。
PCA的局限在于:主成分虽在数学上简洁,但解释性较差。第一个主成分可能是20%动量+15%价值+10%质量+...,难以赋予明确的经济含义。
因子旋转就是为了解决这个问题的。它通过旋转主成分的坐标轴,让每个主成分只跟少数几个原始因子强相关,这样每个主成分就有了清晰的标签。
| 旋转类型 | 特点 | 适用场景 |
|---|---|---|
| 正交旋转(Varimax) | 保持主成分正交,简化因子载荷 | 因子间理论上不相关 |
| 斜交旋转(Promax) | 允许因子间相关,更贴近现实 | 因子间存在真实相关性 |
实践中更常采用Varimax。正交旋转后的因子可直接用于回归,无需担心共线性。斜交旋转拟合度可能更高,但因子间仍相关,后续建模反而复杂。
from sklearn.decomposition import FactorAnalyzer
fa = FactorAnalyzer(n_factors=5, rotation='varimax')
fa.fit(factor_scaled)
# 查看因子载荷矩阵
loadings = fa.loadings_
print("因子载荷:\n", pd.DataFrame(loadings,
index=factor_data.columns,
columns=['F1','F2','F3','F4','F5']))
# 计算旋转后的因子得分
factor_scores = fa.transform(factor_scaled)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 43 try: ---> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ModuleNotFoundError: No module named 'sklearn.__check_build._check_build' During handling of the above exception, another exception occurred: ImportError Traceback (most recent call last) <ipython-input-6-f435f56d9712> in <cell line: 0>() ----> 1 from sklearn.decomposition import FactorAnalyzer 2 3 fa = FactorAnalyzer(n_factors=5, rotation='varimax') 4 fa.fit(factor_scaled) 5 d:\pythonprojects\venv\Lib\site-packages\sklearn\__init__.py in <module> 79 # it and importing it first would fail if the OpenMP dll cannot be found. 80 from . import _distributor_init # noqa: F401 ---> 81 from . import __check_build # noqa: F401 82 from .base import clone 83 from .utils._show_versions import show_versions d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ---> 46 raise_build_error(e) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in raise_build_error(e) 29 else: 30 dir_content.append(filename + '\n') ---> 31 raise ImportError("""%s 32 ___________________________________________________________________________ 33 Contents of %s: ImportError: No module named 'sklearn.__check_build._check_build' ___________________________________________________________________________ Contents of d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build: setup.py _check_build.cp38-win_amd64.pyd__init__.py __pycache__ ___________________________________________________________________________ It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run `python setup.py install` or `make` in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.
因子旋转后若直接以因子得分做回归,需检查因子载荷矩阵——部分因子在所有主成分上载荷均较低(<0.3),属于噪音,应提前剔除,否则将污染多个主成分。
前面两种方法都是线性方法。但真实市场里,因子和收益之间的关系往往是非线性的。比如小市值因子在牛熊市里的表现完全不同,这就是典型的非线性交互。
机器学习方法可自动捕捉复杂关系。常用的有三种:
当因子数量多、共线性严重时,普通最小二乘法(OLS)的系数估计会非常不稳定。岭回归通过加入L2正则化,让系数收缩,牺牲一点偏差换取方差的大幅降低。
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
ridge = Ridge(alpha=1.0)
# 用未来一期收益作为标签
scores = cross_val_score(ridge, factor_scaled, returns,
cv=5, scoring='neg_mean_squared_error')
ridge.fit(factor_scaled, returns)
# 合成因子 = 因子值 × 系数
ridge_factor = np.dot(factor_scaled, ridge.coef_)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 43 try: ---> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ModuleNotFoundError: No module named 'sklearn.__check_build._check_build' During handling of the above exception, another exception occurred: ImportError Traceback (most recent call last) <ipython-input-7-7fd80de13d4d> in <cell line: 0>() ----> 1 from sklearn.linear_model import Ridge 2 from sklearn.model_selection import cross_val_score 3 4 ridge = Ridge(alpha=1.0) 5 # 用未来一期收益作为标签 d:\pythonprojects\venv\Lib\site-packages\sklearn\__init__.py in <module> 79 # it and importing it first would fail if the OpenMP dll cannot be found. 80 from . import _distributor_init # noqa: F401 ---> 81 from . import __check_build # noqa: F401 82 from .base import clone 83 from .utils._show_versions import show_versions d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ---> 46 raise_build_error(e) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in raise_build_error(e) 29 else: 30 dir_content.append(filename + '\n') ---> 31 raise ImportError("""%s 32 ___________________________________________________________________________ 33 Contents of %s: ImportError: No module named 'sklearn.__check_build._check_build' ___________________________________________________________________________ Contents of d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build: setup.py _check_build.cp38-win_amd64.pyd__init__.py __pycache__ ___________________________________________________________________________ It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run `python setup.py install` or `make` in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.
调参技巧:alpha值可通过网格搜索确定,范围从0.01到100。alpha越大,系数越接近0,模型越保守。通常选取交叉验证误差最小的alpha。
岭回归的缺点是它不会把系数压缩到0,所以无法做特征选择。弹性网络结合了L1和L2正则化,既能收缩系数,又能自动剔除无效因子。
from sklearn.linear_model import ElasticNet
enet = ElasticNet(alpha=0.01, l1_ratio=0.5)
enet.fit(factor_scaled, returns)
# 查看哪些因子被选出来了
selected = np.where(enet.coef_ != 0)[0]
print("被选中的因子索引:", selected)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 43 try: ---> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ModuleNotFoundError: No module named 'sklearn.__check_build._check_build' During handling of the above exception, another exception occurred: ImportError Traceback (most recent call last) <ipython-input-8-b523dfc79842> in <cell line: 0>() ----> 1 from sklearn.linear_model import ElasticNet 2 3 enet = ElasticNet(alpha=0.01, l1_ratio=0.5) 4 enet.fit(factor_scaled, returns) 5 d:\pythonprojects\venv\Lib\site-packages\sklearn\__init__.py in <module> 79 # it and importing it first would fail if the OpenMP dll cannot be found. 80 from . import _distributor_init # noqa: F401 ---> 81 from . import __check_build # noqa: F401 82 from .base import clone 83 from .utils._show_versions import show_versions d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in <module> 44 from ._check_build import check_build # noqa 45 except ImportError as e: ---> 46 raise_build_error(e) d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build\__init__.py in raise_build_error(e) 29 else: 30 dir_content.append(filename + '\n') ---> 31 raise ImportError("""%s 32 ___________________________________________________________________________ 33 Contents of %s: ImportError: No module named 'sklearn.__check_build._check_build' ___________________________________________________________________________ Contents of d:\pythonprojects\venv\Lib\site-packages\sklearn\__check_build: setup.py _check_build.cp38-win_amd64.pyd__init__.py __pycache__ ___________________________________________________________________________ It seems that scikit-learn has not been built correctly. If you have installed scikit-learn from source, please do not forget to build the package before using it: run `python setup.py install` or `make` in the source directory. If you have used an installer, please check that it is suited for your Python version, your operating system and your platform.
曾用弹性网络对A股500个因子做合成,筛除300多个因子后,剩余组合夏普比率从0.8提升至1.3,体现了稀疏性的作用。
若因子与收益之间存在非线性关系,树模型是常用选择。其可自动捕捉交互效应,例如当市值小于50亿且换手率大于5%时,动量因子有效。
import lightgbm as lgb
model = lgb.LGBMRegressor(
n_estimators=100,
max_depth=3,
learning_rate=0.1,
num_leaves=7
)
model.fit(factor_scaled, returns)
# 特征重要性
importance = pd.DataFrame({
'factor': factor_data.columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
# 合成因子
ml_factor = model.predict(factor_scaled)
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) <ipython-input-9-d6eae67aac86> in <cell line: 0>() ----> 1 import lightgbm as lgb 2 3 model = lgb.LGBMRegressor( 4 n_estimators=100, 5 max_depth=3, ModuleNotFoundError: No module named 'lightgbm'
严重警告:树模型极易过拟合。若采用过多树、过深层次,样本内IC可能很高、样本外转负。建议:树深不超过4层,叶子节点数不超过10个,学习率设小(0.01-0.05)。宁可欠拟合,勿过拟合。
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| PCA | 降维效果好,计算快 | 解释性差,线性假设 | 因子数量多、共线性严重 |
| 因子旋转 | 解释性强,可做因子归因 | 需要主观判断因子数量 | 需要理解因子含义的场景 |
| 机器学习 | 捕捉非线性,自动特征选择 | 过拟合风险高,调参复杂 | 因子与收益关系复杂 |
常见流程为:先用PCA降维至约10个主成分,再用弹性网络做二次筛选,最后用浅层XGBoost做非线性增强。该组合在实盘中表现较为稳健。
不存在万能方法。PCA适合快速降维,因子旋转适合解释,机器学习适合捕捉复杂关系。实战中可根据数据特征灵活组合使用。
多因子合成方法体系 原始因子矩阵 (n×k) 标准化 + 去极值 + 中性化 主成分分析 (PCA) 降维 · 去相关 · 方差最大化 因子旋转 (Varimax/Promax) 解释性 · 稀疏载荷 · 因子归因 机器学习合成 非线性 · 正则化 · 特征选择 方差解释率加权 IC加权PCA 正交旋转 斜交旋转 岭回归/弹性网络 XGBoost/LightGBM 合成因子 (n×1)
构建多因子综合得分的推荐流程:
| 场景 | 推荐方法 | 理由 |
|---|---|---|
| 因子数量少(<10),相关性低 | 等权 / ICIR加权 | 简单稳健,无过拟合风险 |
| 因子质量差异大 | IC / ICIR加权 | 按预测能力与稳定性分配权重 |
| 因子数量多,共线性严重 | PCA + 弹性网络 | 降维去相关,自动特征选择 |
| 需要因子归因与解释 | 因子旋转(Varimax) | 载荷矩阵可解释 |
| 因子与收益非线性关系 | 浅层XGBoost/LightGBM | 捕捉交互效应,需严控过拟合 |
| 发现高相关因子对(>0.8) | 先剔除/合成/正交化 | 避免信息重复与系数不稳定 |
以下三节进入组合层:风险模型 → 优化下单 → 绩效归因。
聊到多因子体系,有个绕不开的话题——风险模型。
Barra 模型是结构化风险模型的典型代表,其核心思想是将股票收益风险拆解为两部分:因子带来的系统性风险,以及股票自身的特异风险。
初期构建多因子组合时,风险模型常被视为辅助工具。直至某组合在单一行业暴露过高,市场波动导致回撤远超预期。此后,风险模型的重要性得到充分重视。
为什么叫结构化?因为它把复杂的协方差矩阵,拆成了清晰可解释的结构。
全市场数千只股票若直接估计两两协方差,矩阵维度为 N×N,规模稍大即难以处理;且样本不足时,估计矩阵噪声极大,难以实用。
结构化风险模型的做法是:
公式如下:
其中:
这样一来,需要估计的参数从N²级别降到了K² + N级别。K通常只有几十个,N可能上千,但K²比N²小太多了。这就是结构化带来的好处。
结构化风险模型并非万能工具,其基于一项假设——股票之间的相关性主要由共同因子驱动。若该假设不成立,模型将失效。A 股市场实践中,某些极端行情下因子间相关性会突变,模型预测偏差显著。
因子协方差矩阵F,是整个风险模型的心脏。它估计得准不准,直接决定了风险预测的质量。
因子协方差矩阵 F 的估计,通常分两步:先以历史数据作初步估计,再进行如下调整:
直接用过去T期的因子收益序列,计算样本协方差:
其中$f_{t}$是第$t$期的因子收益向量,$μ_f$是均值向量。
样本协方差矩阵存在局限——当 T 不足时噪声较大,因子数量较多时极端值易扭曲估计结果。
Ledoit-Wolf 压缩估计是常用方法,即将样本协方差矩阵向更稳定的目标矩阵(如单位矩阵)方向压缩:
δ是压缩强度,由数据自动决定。当样本噪声大时,δ会变大,更依赖目标矩阵;样本质量好时,δ变小,更依赖样本估计。
实战技巧 :回测对比显示,采用压缩估计后,组合预测风险与实际风险的偏差约缩小 15%。因子数量超过 20 个时效果更为明显。建议至少采用 EWMA(指数加权移动平均)或压缩估计,不宜直接使用简单滚动窗口。
特异风险,就是因子模型无法解释的那部分波动。每只股票都有自己的特异风险,它们之间假设不相关(所以D是对角矩阵)。
特异风险的估计方法并不复杂:
代码实现大致是这样:
import numpy as np
def estimate_idiosyncratic_risk(returns, factor_returns, factor_loadings):
# returns: T×N 股票收益矩阵
# factor_returns: T×K 因子收益矩阵
# factor_loadings: N×K 因子载荷矩阵
T, N = returns.shape
K = factor_returns.shape[1]
# 计算因子收益预测的股票收益
predicted_returns = factor_returns @ factor_loadings.T # T×N
# 特异收益 = 实际收益 - 预测收益
idiosyncratic_returns = returns - predicted_returns
# 计算特异收益的方差
idio_var = np.var(idiosyncratic_returns, axis=0, ddof=1)
# 压缩估计(简单版)
target_var = np.median(idio_var)
shrinkage = 0.2 # 实际中需要更精细的估计
idio_var_shrunk = shrinkage * target_var + (1 - shrinkage) * idio_var
return np.diag(idio_var_shrunk)
注意 :特异风险估计存在局限——股票数量多、历史数据短时,估计误差较大。在 3000 只股票、仅 60 个月数据的情形下,直接估计的特异风险矩阵几乎无法使用。采用结构化压缩,将特异风险向行业均值方向压缩后,效果方有改善。
风险归因,就是回答一个问题:组合的风险到底来自哪里?
设某投资组合权重向量为 w(N×1),组合方差为:
代入结构化风险模型:
这里:
进一步,可将总风险拆解至各因子与各只股票:
| 风险来源 | 计算公式 | 解释 |
|---|---|---|
| 因子风险 | RC_factor = (w^T * B) * F * (B^T * w) | 由因子暴露带来的系统性风险 |
| 特异风险 | RC_idio = w^T * D * w | 个股自身波动带来的风险 |
| 单个因子贡献 | RC_k = (w^T * B)_k * (F * B^T * w)_k | 第k个因子对总风险的边际贡献 |
| 单个股票贡献 | RC_i = w_i² * D_ii | 第i只股票的特异风险贡献 |
通常可绘制风险归因饼图,观察因子风险与特异风险各占比例。若特异风险占比过高,表明组合分散不足,或因子模型解释力不足。
实战案例 :某行业中性化多因子组合的风险归因显示,行业因子风险贡献接近零(因已中性化),但市值因子风险贡献占总风险约 40%。表明组合在市值因子上暴露较大,需调整权重以降低风险。
结构化风险模型将复杂问题简化为可操作的框架。通过因子分解,仅需估计 K×K 的因子协方差矩阵与 N 个特异风险,即可得到 N×N 的完整协方差矩阵。
风险模型的价值不仅在于预测风险,更在于理解风险。每次风险归因均可呈现组合的风险画像——哪些因子主导波动,哪些个股拖累表现。据此调整组合方向更为明确。
最后需强调:模型再好,亦须结合市场环境。A 股市场因子结构会变化,风险模型需定期重新估计,建议每季度至少更新一次。
注意事项 :2015 年股灾期间,若沿用半年前估计的风险模型做组合优化,模型会严重低估市场因子波动,导致组合回撤远超预期。此后应养成滚动更新习惯——每次以最近 12 个月数据重新估计因子协方差矩阵。
经过因子挖掘、检验与信号合成,最后一步是将信号转化为实际持仓,即组合优化。
因子选股给出候选标的,组合优化则确定各标的的配置权重。该环节处理不当,前期工作可能前功尽弃。实践中常见因子 IC 较高、实盘大幅偏离的情况,问题往往出在优化环节。
均值-方差优化由 Markowitz 于 1952 年提出。核心思想为:在给定收益目标下最小化组合风险,或在给定风险预算下最大化预期收益。
数学上就是求解:
其中w是权重向量,Σ是协方差矩阵,μ是预期收益向量。
理论上框架完善,但实践中该模型对输入参数极其敏感——预期收益 μ 的微小变动即可导致权重大幅调整,业内称其为误差最大化器。
⚠️ 注意事项 若以历史均值作为 μ 输入,优化权重易出现极端值——如集中持仓一两只股票。改用缩尾处理后的预期收益后,情况方有改善。需注意:均值-方差优化对 μ 的敏感度远高于 Σ。
鉴于均值-方差优化对收益预测极为敏感,一种思路是放弃收益预测、仅作风险控制,此即风险预算模型的基本思想。
最经典的是等风险贡献(Risk Parity):让每只股票对组合总风险的贡献相等。
数学表达:
其中:
$$ RC_i = w_i * (Σw)_i / sqrt(w'Σw) $$这个模型的好处是:不需要预测收益,只需要协方差矩阵。协方差矩阵比收益预测稳定得多。
实践中,因子组合的底仓部分常采用风险预算模型。例如,以风险预算分配 50% 权重,其余 50% 留给主动信号。既控制风险,又保留 alpha 空间。
💡 实战技巧 风险预算模型对协方差矩阵质量要求较高。建议采用指数加权移动平均(EWMA)估计协方差,半衰期设为 60 个交易日,以捕捉近期波动特征,同时避免过度敏感。
均值-方差太敏感,风险预算又放弃收益——有没有折中方案?有,Black-Litterman模型。
该模型的核心思想是:先给定市场均衡收益(如从 CAPM 推导),再将对某些股票的看好/看空观点作为先验信息,以贝叶斯方法融合,得到后验收益估计。
公式如下:
其中:
该模型将主观判断与客观数据相结合。在行业轮动策略中应用广泛——先以量化信号生成对若干行业的观点,再以 BL 模型融合至均衡收益,最后进行优化。
💡 经验法则 观点置信度 Ω 的设置,通常可参考信号 IC 的倒数:信号 IC 越高,Ω 越小,观点越强。但 Ω 不宜过小,否则模型可能过度拟合观点。
理论模型虽完善,实盘却存在各类限制。常见约束可归纳如下:
| 约束类型 | 示例 | 处理方法 |
|---|---|---|
| 权重上下限 | 单票不超过5% | 直接加边界约束 |
| 行业中性 | 行业偏离不超过2% | 线性等式/不等式约束 |
| 换手率限制 | 单边换手不超过20% | 加入L1正则或交易成本项 |
| 整数手数 | 100股整数倍 | 混合整数规划(较复杂) |
此处需注意:约束条件越多,优化结果越偏离理论最优。若约束过多,最终组合可能与等权相差无几,不如直接采用等权。
建议:仅加入真正必要的约束。例如,高频策略须设换手率约束;低频价值投资则行业中性约束可能更为关键。
以下给出简化的代码示例,展示三种模型的核心逻辑:
import numpy as np
from scipy.optimize import minimize
# 假设我们有5只股票,协方差矩阵和预期收益
Sigma = np.array([[0.1, 0.02, 0.01, 0.03, 0.02],
[0.02, 0.12, 0.03, 0.02, 0.01],
[0.01, 0.03, 0.08, 0.01, 0.02],
[0.03, 0.02, 0.01, 0.15, 0.03],
[0.02, 0.01, 0.02, 0.03, 0.09]])
mu = np.array([0.12, 0.08, 0.15, 0.06, 0.10])
# 1. 均值-方差优化(最小方差组合)
def min_variance(weights):
return weights.T @ Sigma @ weights
cons = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
bounds = [(0, 0.3)] * 5 # 单票上限30%
w0 = np.ones(5) / 5
res = minimize(min_variance, w0, constraints=cons, bounds=bounds)
print("均值-方差权重:", res.x)
# 2. 风险预算(等风险贡献)
def risk_parity_obj(weights):
port_var = weights.T @ Sigma @ weights
rc = weights * (Sigma @ weights) / np.sqrt(port_var)
target = port_var / 5 / np.sqrt(port_var)
return np.sum((rc - target)**2)
res_rp = minimize(risk_parity_obj, w0, constraints=cons, bounds=bounds)
print("风险预算权重:", res_rp.x)
# 3. Black-Litterman(简化版)
tau = 0.05
Pi = np.array([0.08, 0.08, 0.08, 0.08, 0.08]) # 均衡收益
P = np.array([[1, 0, 0, 0, 0]]) # 看好第一只股票
Q = np.array([0.15]) # 预期收益15%
Omega = np.array([[0.01]]) # 观点置信度
mu_bl = np.linalg.inv(np.linalg.inv(tau*Sigma) + P.T @ np.linalg.inv(Omega) @ P) @ \
(np.linalg.inv(tau*Sigma) @ Pi + P.T @ np.linalg.inv(Omega) @ Q)
print("BL后验收益:", mu_bl)
# 用BL收益再做均值-方差优化
def bl_obj(weights):
return -weights.T @ mu_bl + 0.5 * weights.T @ Sigma @ weights
res_bl = minimize(bl_obj, w0, constraints=cons, bounds=bounds)
print("BL优化权重:", res_bl.x)
均值-方差权重: [0.21916242 0.1506428 0.28730179 0.10774354 0.23514945] 风险预算权重: [0.20521881 0.18819935 0.22798738 0.16546155 0.21313291] BL后验收益: [0.10333333 0.08466667 0.08233333 0.087 0.08466667] BL优化权重: [0.3 0.14728426 0.23719211 0.10253062 0.21299301]
本章内容至此告一段落。组合优化是因子投资从研究走向实盘的关键桥梁。建议从风险预算模型入手,逐步引入观点与约束——既稳健,又留有提升空间。
本章主体为均值—方差、风险预算、Black-Litterman 与约束优化。此外还可考虑一类 参数化组合政策:将投资权重直接参数化为公司特征的函数,并通过样本目标(如效用)估计参数,而不是先估计均值—协方差再做两阶段优化。
两种思路对照:
适用场景:特征驱动的股票配置、需要控制估计误差放大的中低频策略。可与本章传统优化器对照使用,而非互相替代。
量化投资中,一项常被忽视的风险在于:
并非策略本身亏损,而是亏损后无法识别原因;盈利时亦难以区分运气与能力。
本章的核心议题为绩效归因。
绩效归因旨在对收益与风险作出清晰核算。实践中常见情形是:回测曲线表现优异,却无法说明超额收益的来源,此类策略的可信度通常较低。
例如,某多因子组合跑赢基准。超额收益究竟来自因子选股、行业配置,还是偶然撞上风格轮动?
缺乏归因分析,相当于在信息不完整的情况下做决策——速度可能很快,却无法判断前方风险。
每季度至少进行一次完整归因分析,目的并非仅为撰写报告,而是厘清超额收益的具体来源。
核心问题: 超额收益 = 配置收益 + 选股收益 + 交互收益 + 残差。归因的目标,是将上述组成部分逐一拆解、加以识别。
Brinson归因,是业界最经典的归因方法。它把超额收益拆成三部分:
公式如下:
其中,$W_{p,i}$是组合在行业$i$的权重,$W_{b,i}$是基准权重,$R_{p,i}$是组合在行业$i$的收益,$R_{b,i}$是基准收益。
项目实践中曾出现一种典型情形:某策略回测超额收益较高,Brinson归因显示配置收益为负、选股收益为正。这表明行业配置方向存在偏差,但选股能力较强,最终仍实现了正超额。
对此类策略需审慎评估:若选股能力可持续,仍具备应用价值;但若行业配置持续拖累,长期风险不容忽视。
实践提示: Brinson归因对行业分类较为敏感。建议采用申万一级行业或GICS行业分类。分类过粗则归因结果缺乏区分度;分类过细则交互收益占比增大,解释难度上升。
Brinson归因是从行业角度拆。Barra归因,则是从 风险因子 的角度拆。
Barra模型把股票收益分解为:
其中,$β_{ik}$是股票$i$对因子$k$的暴露,$f_{k}$是因子$k$的收益,$ε_{i}$是残差。
那么,组合的超额收益就可以分解为:
换言之,需考察组合在哪些因子上具有更高暴露,以及这些因子贡献了多少收益。
某策略回测年化超额约15%,Barra归因显示超额收益几乎全部来自市值因子——策略持续偏向小盘股,而当年恰好为小盘股强势年份。
需考虑:若小盘股风格反转,策略将如何应对?
Barra归因的价值在于识别收益来源究竟来自能力还是运气。
做多因子模型,最怕的就是因子之间互相抵消,或者某个因子突然失效。
因子贡献度分析,就是给多因子体系做定期体检。
具体做法:
以下表格为常用展示方式:
| 因子名称 | 平均暴露 | 因子收益 | 收益贡献 | 风险贡献 | 信息比 |
|---|---|---|---|---|---|
| 估值因子 | 0.35 | 2.1% | 0.74% | 0.52% | 1.42 |
| 动量因子 | 0.28 | 1.5% | 0.42% | 0.38% | 1.11 |
| 质量因子 | 0.22 | 1.8% | 0.40% | 0.29% | 1.38 |
| 低波因子 | 0.15 | 0.9% | 0.14% | 0.21% | 0.67 |
由上表可见,估值因子贡献最大、信息比率较高;低波因子贡献较小而风险不低。此时可考虑调整低波因子权重,或将其移出组合。
注意: 因子贡献度分析须考虑因子间相关性。两因子高度相关时,贡献度会相互干扰。建议先进行因子正交化,再开展贡献度分析,否则结果可能产生误导。
超额收益分解,即将总超额收益逐层拆解,直至识别每一分收益的来源。
常用分解框架如下:
总超额收益
├── 配置收益(行业/风格)
│ ├── 行业配置收益
│ └── 风格配置收益
├── 选股收益
│ ├── 行业内选股收益
│ └── 风格内选股收益
├── 因子收益
│ ├── 因子暴露收益
│ └── 因子择时收益
└── 残差收益(运气成分)
该框架可从不同维度审视收益来源。例如,因子收益较高而选股收益较低,说明策略更接近指数增强而非主动选股。
曾有案例:某策略超额收益较高但夏普比率偏低。
超额收益分解显示,超额主要来自行业配置——重仓某行业且该行业大幅上涨;选股收益为负,说明行业内选股能力不足。此类策略在行业轮动时易出现超额收益大幅回撤。
分解分析的价值在于揭示表面业绩之下的真实结构。
以下为标准归因流程:
以下为归因流程图,供梳理思路参考:
因子绩效归因流程图 组合收益 & 基准收益 选择归因方法 Brinson归因 Barra归因 因子贡献度分析 配置收益 选股收益 因子暴露收益 残差收益 因子收益贡献 因子风险贡献 归因报告 & 改进建议
以下为实践中常见的注意事项:
归因分析可视为对策略的定期体检。定期执行方能掌握策略健康状况;不做归因则难以评估策略的可靠性。
希望以上内容有助于更清晰地核算收益与风险。