信号合成与融合:多信号加权、信号打分、信号排序

做高频交易的人,手里通常不止一个信号。

你可能有均线突破信号,有波动率异常信号,有订单流失衡信号。每个信号单独看都还行,但合在一起就打架——这个说买,那个说卖,到底听谁的?

我早年踩过这个坑。那时候我搞了七八个信号,简单平均一下就当合成信号用了。结果回测曲线漂亮得不行,实盘直接崩了。后来才发现,有些信号天生就是「噪音放大器」,你把它跟好信号等权相加,反而把好信号给污染了。

所以今天咱们聊聊,怎么把多个信号揉成一个靠谱的决策依据。

信号融合的三种思路

我个人习惯把信号融合分成三个层次:

  1. 加权合成:给每个信号一个权重,线性组合
  2. 打分排序:对每个信号独立打分,然后汇总排名
  3. 条件融合:不同市场环境下,用不同的信号组合

说白了,第一种最简单,第二种更稳健,第三种最灵活。咱们一个一个来。

多信号加权:别搞等权

等权加权是最常见的错误。为什么?因为信号的质量不一样。

我举个例子。你有一个趋势信号,夏普比率1.5;还有一个反转信号,夏普比率0.6。你给它们各50%权重,合成信号的夏普比率大概率在1.0左右——被差的拖累了。

正确的做法是:用历史表现来定权重

import numpy as np
import pandas as pd

def calc_optimal_weights(signals: pd.DataFrame, returns: pd.Series):
    """
    基于信号与未来收益的相关性,计算最优权重
    signals: 每列是一个信号值,已归一化
    returns: 未来N期的收益率
    """
    # 计算每个信号的信息系数(IC)
    ic = signals.corrwith(returns)
    
    # 用IC作为权重,并归一化
    weights = ic / ic.sum()
    
    return weights

# 示例:三个信号
signals = pd.DataFrame({
    'trend': [0.8, 0.3, -0.2, -0.6],
    'momentum': [0.5, 0.1, -0.3, -0.4],
    'volatility': [-0.2, 0.4, 0.1, -0.1]
})
future_returns = pd.Series([0.02, 0.01, -0.01, -0.03])

weights = calc_optimal_weights(signals, future_returns)
print(weights)

关键点:权重不是一成不变的。我建议每两周重新计算一次,用滚动窗口的方式。太频繁了容易过拟合,太久了跟不上市场变化。

信号打分:把不同量纲统一起来

你想想看,一个信号的值可能是0.8到-0.8,另一个信号可能是100到-100。直接加权?那第二个信号会完全主导结果。

所以我们需要打分——把每个信号映射到同一个尺度上。

我常用的打分方法有三种:

方法 适用场景 做法
分位数打分 信号分布稳定 按历史分位数给1-100分
Z-score打分 信号近似正态 (x - mean) / std
排名打分 信号有极端值 按大小排名,归一化到0-1
def rank_score(signal_series: pd.Series):
    """
    排名打分:把信号转为0-1之间的分数
    """
    ranks = signal_series.rank()
    return (ranks - 1) / (len(ranks) - 1)

def zscore_score(signal_series: pd.Series):
    """
    Z-score打分,然后压缩到[-3, 3]区间
    """
    z = (signal_series - signal_series.mean()) / signal_series.std()
    return np.clip(z, -3, 3) / 3  # 归一化到[-1, 1]

我的经验:对于高频数据,我偏爱排名打分。因为高频信号经常有极端值(比如瞬间的订单流冲击),Z-score会被这些极端值拉偏。排名打分天然对异常值不敏感。

信号排序:从合成到决策

打分之后,我们有了每个信号的分数。接下来怎么合成最终决策?

我见过两种做法:

  1. 加权求和:每个信号分数乘以权重,加起来得到总分
  2. 排序投票:每个信号独立给出买卖建议,然后投票决定

我个人更推荐第一种。为什么?因为投票会丢失信息量。一个信号说「强烈看多」(分数0.9),另一个说「轻微看空」(分数-0.1),投票结果是平局。但加权求和会得到0.8,明显偏多——这才是真实的信息。

def signal_fusion(scores: pd.DataFrame, weights: np.array):
    """
    信号融合:加权求和 + 阈值过滤
    scores: 每列是一个信号的分数,范围[-1, 1]
    weights: 每个信号的权重,和为1
    """
    # 加权总分
    total_score = scores @ weights
    
    # 阈值过滤:只有超过阈值才交易
    threshold = 0.3
    decision = np.where(total_score > threshold, 1,
                        np.where(total_score < -threshold, -1, 0))
    
    return total_score, decision

# 示例
scores = pd.DataFrame({
    'signal_a': [0.8, -0.2, 0.1],
    'signal_b': [0.3, 0.5, -0.6],
    'signal_c': [-0.1, 0.2, 0.4]
})
weights = np.array([0.5, 0.3, 0.2])

total, decision = signal_fusion(scores, weights)
print("总分:", total)
print("决策:", decision)

避坑指南:我曾经犯过一个错误——把相关性很高的两个信号都给了高权重。比如趋势信号和动量信号,它们本质上是一回事。结果合成信号方差极大,回撤惨不忍睹。所以加权之前,一定要检查信号之间的相关性。如果两个信号相关系数超过0.7,考虑只保留一个,或者把权重减半。

实战中的信号融合框架

下面这张图是我在实际项目中用的信号融合流程。你可以参考一下。

信号融合实战框架 原始信号1 原始信号2 原始信号3 去噪 + 归一化 去噪 + 归一化 去噪 + 归一化 排名打分 Z-score打分 分位数打分 加权融合 + 阈值过滤 最终交易信号

这个框架的核心逻辑是:先对每个原始信号做预处理(去噪、归一化),然后根据信号特性选择不同的打分方法,最后加权融合并加阈值过滤。

关于阈值的一点补充

阈值怎么设?我见过有人用固定值,比如0.3。但市场波动大的时候,0.3可能太容易触发;波动小的时候,0.3又可能太严格。

我建议用动态阈值——根据近期信号的波动率来调整。比如:

def dynamic_threshold(total_score: pd.Series, window: int = 20):
    """
    动态阈值:基于滚动标准差
    """
    rolling_std = total_score.rolling(window).std()
    threshold = 1.5 * rolling_std  # 1.5倍标准差
    return threshold

嗯,这样阈值就能自适应市场环境了。牛市里信号普遍偏高,阈值自动上移;熊市里信号偏低,阈值自动下移。不会出现「牛市里频繁交易、熊市里一动不动」的尴尬局面。

最后一个小技巧:信号融合之后,别忘了做一次「信号衰减」测试。就是把合成信号延迟1根K线、2根K线,看看效果是否还能保持。如果延迟后效果大幅下降,说明你的融合模型可能过度拟合了微观结构——这在实盘里很容易翻车。