欢迎光临
我们一直在努力

多因子轮动系列(五):机器学习辅助因子加权——让模型帮你分配权重

摘要: 本文是“多因子轮动”系列的第五篇。前面四篇我们完成了从因子构建到回测对比的全流程,并使用等权和经验权重合成了综合分数。但这些固定权重无法适应市场环境的变化。今天,我们将引入机器学习中的岭回归模型,让它根据历史数据动态学习各因子的最优权重。我们将详细讲解如何使用滚动窗口训练模型、如何避免未来函数、以及如何将模型输出的权重嵌入到现有的多因子轮动框架中。最后,我们会诚实讨论机器学习在实盘中的局限与风险。读完本文,你将拥有一个能够自适应市场的多因子轮动策略。


大家好,我是你们的老朋友。

前面四篇文章,我们完整地走完了多因子轮动的全流程。在第四篇的回测对比中,我们采用等权或经验加权来合成综合分数,取得了不错的成绩。但有一个问题始终悬而未决:四个因子的权重到底应该是多少?

动量给 40% 还是 25%?低波动因子在熊市里明显更有用,但在牛市中却拖后腿,它的权重是不是应该随市场状态动态变化?这些问题靠拍脑袋是解决不了的,我们需要一套系统性的方法来让数据自己“说话”。

今天这篇文章,我们就来引入一种简单但强大的工具——岭回归,让模型根据历史数据动态学习每个因子的最优权重。

一、为什么需要机器学习?

1.1 固定权重的局限

等权加权简单、稳健,不容易过拟合,但它假设所有因子在任何市场环境下都同等重要。经验加权(比如动量 40%,其余各 20%)融入了人的主观判断,但一旦设定好就不会改变。

现实是,因子的有效性会随着市场环境漂移。趋势强度因子在单边市中表现优异,但在震荡市中可能变成噪音;成交量因子在放量突破时信号强烈,但在缩量盘整时几乎无用。固定权重无法捕捉这种动态变化。

1.2 机器学习能做什么?

机器学习模型可以自动学习“输入”(各因子值)和“输出”(未来收益)之间的关系。我们不需要告诉模型哪个因子更重要,它自己会从数据中找到最优的权重组合。

而且,如果我们定期用最新的数据重新训练模型,它就能持续适应市场的变化。当某个因子开始失效时,模型会自动降低它的权重;当某个因子变得特别有效时,模型会给予它更多关注。

1.3 为什么选择岭回归?

在众多机器学习模型中,岭回归有几个非常适合我们场景的特点:

  • 简单透明:模型就是一组线性权重,你可以直接看到每个因子的贡献大小。
  • 防止过拟合:岭回归通过 L2 正则化惩罚过大的权重,避免模型对历史数据“死记硬背”。
  • 适合小样本:我们每周只有 10 只 ETF 的数据,样本量很小,岭回归的正则化特性在这里优势明显。
  • 计算快速:不需要 GPU,普通电脑几秒就能完成训练和预测。

二、岭回归的核心原理

2.1 普通线性回归的问题

普通线性回归的目标是找到一组权重 w,使得预测值 X * w 与真实值 y 之间的误差平方和最小。但在金融数据中,因子之间往往存在一定的相关性,普通线性回归对这种情况非常敏感,会导致某些权重被放大到不合理的程度。

2.2 岭回归的解决方案

岭回归在误差平方和的基础上,增加了一个惩罚项:所有权重的平方和乘以一个正则化参数 alpha。这个惩罚项会“压缩”过大的权重,迫使模型更均衡地使用各个因子的信息。

alpha 越大,权重被压缩得越厉害;alpha 越小,模型越接近普通线性回归。通常 alpha 设置在 0.1 到 10 之间,需要通过交叉验证来确定最优值。

三、用岭回归训练动态权重

3.1 数据准备:构建训练集

模型的目标是根据当期的因子值,预测下一期的收益率。因此,训练数据的 X(特征)是各 ETF 在某个日期的四个因子值,y(目标)是这些 ETF 在下一个交易日的收益率。

由于不同因子的量纲不同,我们在训练前需要对 X 做标准化处理。注意:标准化必须只使用训练集内的均值和标准差,不能使用全样本数据,否则会引入未来函数。

3.2 滚动窗口训练

为了模拟实盘环境,我们采用滚动窗口的方式进行训练和预测。具体步骤如下:

  • 设定一个训练窗口长度,比如 252 个交易日(约一年)。
  • 在窗口内,用所有 ETF 的因子值作为 X,下一日收益率作为 y,训练一个岭回归模型。
  • 用训练好的模型对当前日期的 ETF 因子值进行预测,得到每只 ETF 的预测收益率。
  • 将预测收益率作为综合分数,选出最高的那只 ETF。
  • 每个交易日重复以上步骤,窗口随时间向前滚动。
  • 这个过程确保了在任何时间点,模型只使用“当时已知”的数据进行训练,完全避免了未来函数。

    3.3 Python 实现

    以下是完整的滚动岭回归训练与预测代码。

    from sklearn.linear_model import Ridge
    from sklearn.preprocessing import StandardScaler

    def rolling_ridge_signal(factor_dict, returns_df, window=252, alpha=1.0):
    """
    使用滚动岭回归生成多因子轮动信号

    Parameters:
    – factor_dict: 字典,键为因子名称,值为因子DataFrame
    – returns_df: 日收益率DataFrame
    – window: 滚动训练窗口长度
    – alpha: 岭回归正则化参数

    Returns:
    – ridge_score: 每个交易日的预测收益率DataFrame
    """
    # 将所有因子合并为一个面板
    factor_names = list(factor_dict.keys())

    # 构建统一的MultiIndex DataFrame
    all_dates = returns_df.index
    all_etfs = returns_df.columns

    # 初始化输出
    ridge_score = pd.DataFrame(index=all_dates, columns=all_etfs, dtype=float)

    # 从窗口起始日开始循环
    for i in range(window, len(all_dates)):
    train_start = i window
    train_end = i 1 # 训练集结束于昨天
    predict_date = all_dates[i]

    # 收集训练数据
    X_train_list = []
    y_train_list = []

    for j in range(train_start, train_end + 1):
    train_date = all_dates[j]
    next_date = all_dates[j + 1] if j + 1 < len(all_dates) else None
    if next_date is None:
    continue

    # 提取该日期所有ETF的因子值
    factor_values = []
    for name in factor_names:
    factor_df = factor_dict[name]
    if train_date in factor_df.index:
    factor_values.append(factor_df.loc[train_date].values)
    else:
    factor_values.append(np.zeros(len(all_etfs)))

    X_train_list.append(np.column_stack(factor_values))
    y_train_list.append(returns_df.loc[next_date].values)

    if len(X_train_list) == 0:
    continue

    X_train = np.vstack(X_train_list)
    y_train = np.concatenate(y_train_list)

    # 删除含有NaN的行
    valid_mask = ~np.isnan(X_train).any(axis=1) & ~np.isnan(y_train)
    X_train = X_train[valid_mask]
    y_train = y_train[valid_mask]

    if len(X_train) < 20:
    continue

    # 标准化X
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)

    # 训练岭回归模型
    model = Ridge(alpha=alpha)
    model.fit(X_train_scaled, y_train)

    # 对当前日期进行预测
    factor_values_today = []
    for name in factor_names:
    factor_df = factor_dict[name]
    if predict_date in factor_df.index:
    factor_values_today.append(factor_df.loc[predict_date].values)
    else:
    factor_values_today.append(np.zeros(len(all_etfs)))

    X_today = np.column_stack(factor_values_today)
    valid_etfs = ~np.isnan(X_today).any(axis=0)
    X_today_valid = X_today[:, valid_etfs]

    if X_today_valid.shape[1] == 0:
    continue

    X_today_scaled = scaler.transform(X_today_valid)
    predictions = model.predict(X_today_scaled)

    # 将预测值写入输出
    valid_indices = np.where(valid_etfs)[0]
    for idx, etf_idx in enumerate(valid_indices):
    ridge_score.loc[predict_date, all_etfs[etf_idx]] = predictions[idx]

    return ridge_score

    # 使用岭回归生成综合分数
    factor_dict = {
    'momentum': momentum_20,
    'lowvol': low_vol_factor,
    'volume': volume_factor,
    'trend': trend_factor
    }

    ridge_score = rolling_ridge_signal(factor_dict, daily_returns, window=252, alpha=1.0)

    3.4 将岭回归分数嵌入轮动框架

    岭回归输出的 ridge_score 是每只 ETF 的预测收益率,数值越大代表模型越看好它。我们可以直接用 idxmax 选出最优 ETF,后面的流程与之前完全一样。

    # 用岭回归分数选出最优ETF
    best_etf_ridge = ridge_score.idxmax(axis=1)
    max_momentum = momentum_20.max(axis=1) # 风控仍用原始动量

    # 后续信号处理与之前完全相同
    signal_df_ridge = pd.DataFrame({
    'best_etf': best_etf_ridge,
    'max_momentum': max_momentum
    })
    signal_df_ridge['hold'] = signal_df_ridge['best_etf'].shift(1)
    signal_df_ridge['max_shifted'] = signal_df_ridge['max_momentum'].shift(1)
    signal_df_ridge.loc[signal_df_ridge['max_shifted'] < 0, 'hold'] = safe_asset
    signal_df_ridge.dropna(inplace=True)

    # 周频调仓
    signal_df_ridge['weekday'] = signal_df_ridge.index.dayofweek
    friday_signals_ridge = signal_df_ridge[signal_df_ridge['weekday'] == 4].copy()
    friday_signals_ridge['trade_signal'] = friday_signals_ridge['hold']
    friday_signals_ridge.loc[friday_signals_ridge['max_shifted'] < 0, 'trade_signal'] = safe_asset

    daily_signal_ridge = friday_signals_ridge['trade_signal'].reindex(price_df.index)
    daily_signal_ridge.ffill(inplace=True)
    daily_signal_ridge.fillna(safe_asset, inplace=True)

    四、回测对比:岭回归 vs 等权多因子

    4.1 绩效指标对比

    将岭回归策略加入我们第四篇的对比表中:

    指标单因子动量多因子等权岭回归动态权重
    年化收益率 18.2% 16.9% 17.5%
    最大回撤 -36.8% -28.4% -26.2%
    年化波动率 28.5% 23.6% 24.1%
    夏普比率 0.64 0.72 0.73
    卡玛比率 0.49 0.60 0.67
    年化换手率 420% 285% 350%

    4.2 结果解读

    • 收益介于两者之间:岭回归的年化收益率高于等权多因子,但略低于单因子动量。
    • 回撤控制优秀:最大回撤比等权多因子还低一些,体现了模型在风险环境中的适应能力。
    • 换手率回升:这是机器学习策略的常见特征。模型权重频繁微调会导致换仓次数增加,部分抵消了收益提升。
    • 夏普比率最高:综合来看,岭回归在风险调整收益上略优于等权方案。

    五、机器学习策略的局限与风险

    诚实地说,机器学习策略在回测中的优异表现,有相当一部分来自“运气”——模型在大量参数组合中恰好找到了适合历史数据的配置。以下是实盘前必须认清的几个风险。

    5.1 过拟合风险

    即使使用了岭回归的正则化,模型仍然可能捕捉到历史数据中的噪音。在金融领域,信噪比极低,任何复杂的模型都可能在回测中表现优异,实盘中却黯然失色。

    5.2 参数敏感性

    岭回归的 alpha 参数对结果影响很大。alpha=1.0 和 alpha=5.0 可能给出完全不同的权重分配。训练窗口长度也是一个敏感参数。这些参数的选择往往需要样本外测试来验证。

    5.3 因子失效的连锁反应

    如果某个因子突然失效(比如市场结构发生根本变化),机器学习模型可能需要一段时间才能“忘记”它。在此期间,失效因子仍然占据一定权重,拖累策略表现。

    5.4 建议

    • 用等权多因子作为主策略,机器学习版本作为辅助参考。
    • 用一部分小资金单独运行机器学习策略,观察实盘与回测的差距。
    • 定期对比两种方案的绩效,如果机器学习版本连续跑输等权版,果断切换。
    • 不要频繁调参。一旦确定了 alpha 和窗口长度,至少运行半年再做调整。

    六、本篇总结

    • 岭回归提供了一种系统性的方法,让模型根据历史数据动态学习各因子的最优权重。
    • 滚动窗口训练是避免未来函数的关键:只用“当时已知”的数据训练,用训练好的模型预测下一期。
    • 在回测中,岭回归策略在夏普比率上优于等权多因子,但换手率有所回升。
    • 机器学习策略有固有的过拟合风险,实盘应谨慎使用,建议以等权多因子为主,机器学习为辅。

    下一篇预告:多因子轮动系列(六)——实盘监控与因子生命周期管理

    下一篇将我们将讨论如何在实盘中持续监控各个因子的健康状况,建立因子淘汰与更替机制,以及如何将整套多因子轮动策略转化为一个可持续运行的交易系统。咱们下篇见!

    赞(0)
    未经允许不得转载:171主机测评 » 多因子轮动系列(五):机器学习辅助因子加权——让模型帮你分配权重
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址