信号合成与融合:多信号加权、信号打分、信号排序
做高频交易的人,手里通常不止一个信号。
你可能有均线突破信号,有波动率异常信号,有订单流失衡信号。每个信号单独看都还行,但合在一起就打架——这个说买,那个说卖,到底听谁的?
我早年踩过这个坑。那时候我搞了七八个信号,简单平均一下就当合成信号用了。结果回测曲线漂亮得不行,实盘直接崩了。后来才发现,有些信号天生就是「噪音放大器」,你把它跟好信号等权相加,反而把好信号给污染了。
所以今天咱们聊聊,怎么把多个信号揉成一个靠谱的决策依据。
信号融合的三种思路
我个人习惯把信号融合分成三个层次:
- 加权合成:给每个信号一个权重,线性组合
- 打分排序:对每个信号独立打分,然后汇总排名
- 条件融合:不同市场环境下,用不同的信号组合
说白了,第一种最简单,第二种更稳健,第三种最灵活。咱们一个一个来。
多信号加权:别搞等权
等权加权是最常见的错误。为什么?因为信号的质量不一样。
我举个例子。你有一个趋势信号,夏普比率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会被这些极端值拉偏。排名打分天然对异常值不敏感。
信号排序:从合成到决策
打分之后,我们有了每个信号的分数。接下来怎么合成最终决策?
我见过两种做法:
- 加权求和:每个信号分数乘以权重,加起来得到总分
- 排序投票:每个信号独立给出买卖建议,然后投票决定
我个人更推荐第一种。为什么?因为投票会丢失信息量。一个信号说「强烈看多」(分数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,考虑只保留一个,或者把权重减半。
实战中的信号融合框架
下面这张图是我在实际项目中用的信号融合流程。你可以参考一下。
这个框架的核心逻辑是:先对每个原始信号做预处理(去噪、归一化),然后根据信号特性选择不同的打分方法,最后加权融合并加阈值过滤。
关于阈值的一点补充
阈值怎么设?我见过有人用固定值,比如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线,看看效果是否还能保持。如果延迟后效果大幅下降,说明你的融合模型可能过度拟合了微观结构——这在实盘里很容易翻车。