4. 信号生成逻辑:基于双均线金叉死叉生成买卖信号、信号过滤与去噪
双均线策略,说白了就是交易世界里最经典的“老黄牛”。
我刚开始做量化那会儿,第一个实盘策略就是双均线。为什么?因为它逻辑简单,参数直观,回测起来心里有底。但你别小看它——越简单的东西,越容易在细节上翻车。
今天我们就来拆解一下,怎么用双均线的金叉死叉生成买卖信号,以及怎么给这些信号“洗个澡”,去掉那些烦人的噪音。
4.1 双均线金叉死叉的核心逻辑
双均线策略,就是拿两条不同周期的移动平均线做比较。
- 金叉:短期均线上穿长期均线。意味着短期趋势转强,买入信号。
- 死叉:短期均线下穿长期均线。意味着短期趋势转弱,卖出信号。
举个例子,我习惯用 MA5 和 MA20 这对组合。MA5 代表最近5天的平均价格,反应快;MA20 代表最近20天的平均价格,反应慢。当快线从下方穿过慢线,说明短期力量压过了长期力量,该动手了。
核心公式:
金叉条件:MA_short[t-1] <= MA_long[t-1] 且 MA_short[t] > MA_long[t]
死叉条件:MA_short[t-1] >= MA_long[t-1] 且 MA_short[t] < MA_long[t]
嗯,这里要注意:判断交叉必须用前后两个时刻的值做比较,不能只看当前时刻。否则你永远抓不住交叉的那一瞬间。
4.2 代码实现:从数据到信号
我们直接用 Python 来实现。假设你已经有了 OHLC 数据,我习惯用收盘价来计算均线。
import pandas as pd
import numpy as np
def generate_ma_cross_signals(df, short_window=5, long_window=20):
"""
基于双均线金叉死叉生成买卖信号
df: 包含 'close' 列的 DataFrame
"""
df = df.copy()
# 计算均线
df['MA_short'] = df['close'].rolling(window=short_window).mean()
df['MA_long'] = df['close'].rolling(window=long_window).mean()
# 初始化信号列
df['signal'] = 0
# 生成信号:1 买入,-1 卖出
# 注意:从 long_window 之后开始判断,确保均线有值
for i in range(long_window, len(df)):
# 金叉
if (df['MA_short'].iloc[i-1] <= df['MA_long'].iloc[i-1] and
df['MA_short'].iloc[i] > df['MA_long'].iloc[i]):
df.loc[df.index[i], 'signal'] = 1
# 死叉
elif (df['MA_short'].iloc[i-1] >= df['MA_long'].iloc[i-1] and
df['MA_short'].iloc[i] < df['MA_long'].iloc[i]):
df.loc[df.index[i], 'signal'] = -1
return df
这段代码跑起来很简单。但我在项目中遇到过一个问题:如果数据里有跳空缺口,或者某天停牌,均线值会突然跳变,导致出现“假金叉”。你想想看,一个停牌后复牌直接涨停,MA5 瞬间上穿 MA20,这能算有效信号吗?显然不能。
4.3 信号过滤与去噪:别被假信号骗了
双均线最大的痛点就是——震荡行情里反复打脸。价格在均线附近来回穿梭,金叉完死叉,死叉完又金叉,手续费都能亏掉一层皮。
我总结了几种实用的去噪方法,分享给你。
4.3.1 过滤方法一:交叉确认延迟
不要求交叉当天就交易。等交叉发生后,再观察1-2根K线,确认趋势延续再入场。
def filter_by_delay(df, delay=2):
"""
延迟确认:信号出现后,等待 delay 天再确认
"""
df['signal_filtered'] = 0
raw_signals = df[df['signal'] != 0].index
for idx in raw_signals:
# 检查未来 delay 天内,均线关系是否保持
future_idx = df.index.get_loc(idx)
if future_idx + delay < len(df):
future_slice = df.iloc[future_idx+1 : future_idx+delay+1]
# 如果是金叉,检查未来几天短期均线是否一直在长期均线上方
if df.loc[idx, 'signal'] == 1:
if (future_slice['MA_short'] > future_slice['MA_long']).all():
df.loc[idx, 'signal_filtered'] = 1
# 如果是死叉,检查未来几天短期均线是否一直在长期均线下方
elif df.loc[idx, 'signal'] == -1:
if (future_slice['MA_short'] < future_slice['MA_long']).all():
df.loc[idx, 'signal_filtered'] = -1
return df
我的经验:延迟天数不要设太大。我个人习惯用2天。设大了会错过行情,设小了过滤效果不好。你可以根据品种的波动率来调。
4.3.2 过滤方法二:价格与均线的偏离度过滤
如果价格离均线太近,说明方向不明确,这时候的交叉信号可靠性低。我一般要求价格偏离均线的幅度超过某个阈值,才认为信号有效。
def filter_by_deviation(df, threshold=0.02):
"""
偏离度过滤:价格与长期均线的偏离度必须超过 threshold(2%)
"""
df['deviation'] = abs(df['close'] - df['MA_long']) / df['MA_long']
df['signal_filtered'] = np.where(
(df['signal'] != 0) & (df['deviation'] > threshold),
df['signal'],
0
)
return df
注意:偏离度阈值不能设得太高。我曾经在某个期货品种上设了5%,结果一年下来没几个信号,直接踏空大行情。建议先用历史数据回测,找到最优参数。
4.3.3 过滤方法三:成交量确认
金叉当天如果成交量放大,说明有资金进场,信号更可靠。死叉同理。
def filter_by_volume(df, volume_multiplier=1.5):
"""
成交量确认:信号当天的成交量必须大于过去20天均量的 volume_multiplier 倍
"""
df['volume_ma'] = df['volume'].rolling(window=20).mean()
df['signal_filtered'] = np.where(
(df['signal'] != 0) & (df['volume'] > df['volume_ma'] * volume_multiplier),
df['signal'],
0
)
return df
4.4 知识体系:信号生成与过滤的完整流程
下面这张图,是我自己画的一个流程图。它把整个信号生成和过滤的步骤串起来了。你看一眼就能明白整体逻辑。
4.5 三种过滤方法的对比
我把这三种方法放在一起做了个对比表,方便你根据实际情况选择。
| 过滤方法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 延迟确认 | 等待交叉后趋势延续 | 简单有效,减少假信号 | 可能错过急涨急跌 | 震荡行情、日线级别 |
| 偏离度过滤 | 价格远离均线才交易 | 过滤盘整期的无效信号 | 参数敏感,需回测优化 | 趋势行情、大周期 |
| 成交量确认 | 放量才确认信号有效 | 结合量价关系,更可靠 | 成交量数据可能失真 | 股票、期货主力合约 |
我的建议:不要三种方法全用上。过滤太狠,信号太少,赚不到钱。我一般只选1-2种组合。比如“延迟确认 + 成交量确认”这对组合,我在股指期货上用过,效果还不错。
4.6 实战中的避坑指南
最后,分享几个我踩过的坑。
- 参数过拟合:我曾经为了追求回测曲线好看,把均线参数调到了 MA7 和 MA23。结果实盘一跑,亏得亲妈都不认识。记住,参数越简单,越不容易过拟合。
- 未来函数:计算均线时,一定要用过去的数据。我见过有人不小心用了未来的收盘价算均线,回测收益高得离谱,实盘直接崩。
- 手续费没算进去:双均线策略交易频率不低。如果每笔交易的手续费和滑点没算进去,回测和实盘差距会很大。我一般按万分之三算手续费,滑点按1个最小变动价位算。
嗯,信号生成这块,今天就聊到这儿。双均线虽然基础,但把过滤做好,它依然能打。你回去可以拿自己的数据跑一跑,看看哪种过滤方法最适合你的品种。