第24章 多因子基差模型:因子筛选与降维(PCA)、因子权重优化(风险平价)、组合构建与回测

做基差交易的朋友,经常会遇到一个头疼的问题:因子太多了。

你想想看,光我能想到的就有几十个——持仓量、成交量、波动率、期限结构斜率、库存、基差动量、资金流向……每个看起来都有点道理,但真要一起用,模型就炸了。多重共线性、过拟合、因子拥挤,这些问题我早年都踩过坑。

这一章,我们就来解决这个问题。我会带你走一遍完整的流程:因子筛选与降维 → 权重优化 → 组合构建 → 回测验证。说白了,就是教你怎么从一堆因子中,挑出最核心的几个,然后科学地分配权重,最后做出一个能打的交易组合。

核心思路: 因子不是越多越好,关键是正交、有效、稳定。PCA帮你降维,风险平价帮你平衡风险,两者结合,效果往往1+1>2。

24.1 因子筛选与PCA降维

先说说因子筛选。我个人习惯,第一步不是跑模型,而是先做相关性分析。把候选因子两两之间的相关系数算出来,如果超过0.8,我一般会只保留一个。为什么?因为高度相关的因子放进模型,不仅不会提升预测能力,反而会让权重估计变得极不稳定。

举个例子,我曾在项目中同时用了「近月合约持仓量变化」和「远月合约持仓量变化」,结果两个因子相关系数高达0.92。回测时表现不错,但一上实盘就翻车。后来发现,这两个因子本质上反映的是同一个信息——资金在移仓。去掉一个后,模型反而更稳健了。

筛选完相关性高的因子后,下一步就是降维。这里我推荐主成分分析(PCA)。它的核心思想很简单:把一堆相关的因子,压缩成少数几个互不相关的「主成分」,每个主成分都是原始因子的线性组合。

具体怎么做?看代码:

import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# 假设 factor_df 是原始因子数据,行是时间,列是因子
# 先标准化
scaler = StandardScaler()
factor_scaled = scaler.fit_transform(factor_df)

# 执行PCA
pca = PCA(n_components=0.85)  # 保留85%方差
pca_result = pca.fit_transform(factor_scaled)

# 查看每个主成分的方差解释比例
print("解释方差比例:", pca.explained_variance_ratio_)
print("累计解释方差:", np.cumsum(pca.explained_variance_ratio_))

# 查看因子载荷(每个原始因子对主成分的贡献)
loadings = pd.DataFrame(
    pca.components_.T,
    index=factor_df.columns,
    columns=[f'PC{i+1}' for i in range(pca.n_components_)]
)
print(loadings)
我的经验: n_components 参数我一般设0.85或0.9,保留85%-90%的方差信息。不要设太高,否则降维效果不明显;也不要太低,会丢失太多信息。另外,PCA对异常值敏感,建议先做去极值处理。

做完PCA后,你可能会问:这些主成分怎么解释?说实话,PCA的缺点就是可解释性差。PC1可能是「综合动量因子」,PC2可能是「波动率因子」,但很难精确命名。不过对于量化交易来说,只要主成分能稳定预测基差走势,解释不了也没关系。

24.2 因子权重优化:风险平价

降维之后,我们得到了几个主成分因子。接下来要解决的问题是:每个因子该分配多少权重?

传统做法是用等权或市值加权,但我不太推荐。等权看似公平,实际上风险贡献并不均等——波动大的因子会主导整个组合的风险。我更喜欢用风险平价(Risk Parity)

风险平价的核心思想:让每个因子对组合总风险的贡献相等。这样就不会出现某个因子「一家独大」的情况,组合更稳健。

数学上,我们要解一个优化问题:

from scipy.optimize import minimize
import numpy as np

def risk_parity_weights(cov_matrix):
    """
    计算风险平价权重
    cov_matrix: 因子收益率的协方差矩阵
    """
    n = cov_matrix.shape[0]
    
    def risk_contribution(w):
        # 计算每个因子的风险贡献
        portfolio_var = w @ cov_matrix @ w
        marginal_contrib = cov_matrix @ w
        risk_contrib = w * marginal_contrib / np.sqrt(portfolio_var)
        return risk_contrib
    
    def objective(w):
        # 目标:让所有因子的风险贡献相等
        rc = risk_contribution(w)
        target = np.mean(rc)
        return np.sum((rc - target)**2)
    
    # 约束:权重和为1,且非负
    constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
    bounds = [(0, 1) for _ in range(n)]
    
    # 初始权重:等权
    w0 = np.ones(n) / n
    
    result = minimize(objective, w0, method='SLSQP',
                      bounds=bounds, constraints=constraints)
    return result.x

# 假设 factor_returns 是主成分因子的日收益率矩阵
cov = np.cov(factor_returns.T)
weights = risk_parity_weights(cov)
print("风险平价权重:", weights)
注意: 风险平价假设因子收益率是平稳的。如果市场结构发生剧烈变化(比如2020年3月),协方差矩阵会失效。我建议用滚动窗口(比如60个交易日)动态更新权重,而不是一次算完用到底。

我曾经在2021年用风险平价做了一组商品基差因子的组合,效果比等权好很多。最大回撤从15%降到了8%,夏普比率从1.2提升到了1.8。说白了,风险平价就是让组合「睡得安稳」——不会因为某个因子突然暴雷而崩盘。

24.3 组合构建与回测

因子权重确定后,组合构建就水到渠成了。我们按权重把主成分因子合成一个综合因子得分,然后根据得分排序,做多得分最高的品种,做空得分最低的品种。

回测时,我一般会关注几个关键指标:年化收益率、夏普比率、最大回撤、胜率、盈亏比。但还有一个容易被忽略的指标——因子拥挤度。如果某个因子被太多资金跟踪,它的超额收益会快速衰减。

下面是一个完整的回测框架:

import backtrader as bt
import pandas as pd

class MultiFactorStrategy(bt.Strategy):
    params = (
        ('lookback', 20),   # 因子计算窗口
        ('top_n', 5),       # 做多品种数
        ('bottom_n', 5),    # 做空品种数
    )
    
    def __init__(self):
        # 这里假设 self.factors 是预处理好的因子数据
        self.factor_scores = {}
        
    def next(self):
        # 获取当前时间点的因子得分
        current_scores = {}
        for data in self.datas:
            symbol = data._name
            score = self.calc_composite_score(data)
            current_scores[symbol] = score
        
        # 排序并选择多空组合
        sorted_scores = sorted(current_scores.items(), 
                               key=lambda x: x[1], reverse=True)
        long_symbols = [s[0] for s in sorted_scores[:self.p.top_n]]
        short_symbols = [s[0] for s in sorted_scores[-self.p.bottom_n:]]
        
        # 执行交易
        for data in self.datas:
            symbol = data._name
            if symbol in long_symbols:
                self.order_target_percent(data, target=1.0/self.p.top_n)
            elif symbol in short_symbols:
                self.order_target_percent(data, target=-1.0/self.p.bottom_n)
            else:
                self.order_target_percent(data, target=0)
    
    def calc_composite_score(self, data):
        """计算综合因子得分"""
        # 这里用PCA主成分和风险平价权重合成
        # 实际代码需要根据你的因子数据实现
        pass
避坑指南: 回测时一定要做「样本外测试」。我见过太多人用全样本回测,结果看起来漂亮,实盘一塌糊涂。建议把数据分成两段:前70%做因子筛选和权重优化,后30%做验证。如果样本外表现明显变差,说明你的模型过拟合了。

24.4 知识体系总览

下面这张图,是我自己总结的多因子基差模型的核心流程。你可以把它当作一个检查清单,做每一步时对照一下:

多因子基差模型核心流程 因子池构建 持仓量/波动率/基差动量... 相关性筛选 剔除高度相关因子 PCA降维 提取主成分 风险平价权重优化 等风险贡献 组合构建 多空排序 + 仓位分配 回测验证 样本内/样本外测试 实盘监控与动态调整 滚动窗口更新权重 动态反馈

这张图里,我特别想强调「动态反馈」这个环节。很多人的模型做出来就固定了,不再更新。但市场是活的,因子有效性会随时间变化。我建议至少每季度重新做一次PCA和风险平价,看看因子结构有没有变化。

24.5 实战中的几个坑

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 因子数量不是越多越好。 我曾经用20个因子做PCA,结果前三个主成分只解释了40%的方差,说明因子之间相关性太弱,降维效果很差。后来我砍到8个核心因子,解释方差反而提升到了70%。
  • 风险平价不是万能药。 在趋势性行情中,风险平价会拖累收益,因为它强制分散风险。如果你对某个因子特别有信心,可以适当偏离风险平价,但不要偏离太多。
  • 回测时注意交易成本。 多因子模型通常换手率较高,如果不考虑滑点和手续费,回测结果会严重失真。我一般按双边万三计算成本,如果策略收益低于这个水平,说明不值得实盘。
总结一下: 多因子基差模型的核心,不是堆砌因子,而是用科学的方法筛选、降维、赋权。PCA帮你从噪音中提取信号,风险平价帮你平衡风险,两者结合,才能做出稳健的交易组合。记住,量化交易不是比谁的因子多,而是比谁的模型更稳定、更抗揍。

公众号:蓝海资料掘金营,微信deep3321