1. 双均线策略的核心逻辑与方案选型
1.1 为什么选择双均线作为入门量化策略
双均线策略是量化交易领域最经典的入门策略之一,核心逻辑非常朴素:用两条不同周期的移动平均线来判断趋势方向。短期均线代表近期价格走势,长期均线代表中长期趋势,当短期均线上穿长期均线时视为买入信号,下穿时视为卖出信号。
这个策略之所以适合入门,原因有三。第一,逻辑直观,不需要复杂的数学推导就能理解。第二,参数少,只有两个核心参数——短周期和长周期,调参空间可控。第三,它天然具备趋势跟踪的特性,在单边行情中表现不错,能帮助新手建立对量化交易的基本认知。
但我要提前说清楚:双均线策略不是印钞机。它在震荡行情中会频繁发出假信号,导致反复止损。理解这一点很重要,否则你回测出一个漂亮曲线就以为找到了圣杯,实盘会教你做人。
1.2 技术栈选型:pandas + tushare 的组合优势
选择 pandas 搭配 tushare 这套组合,是我经过多次实践后认为最适合个人量化入门者的方案。
pandas 负责数据处理和策略计算。它的 DataFrame 结构天然适合处理时间序列数据,rolling 方法可以一行代码算出移动平均线,向量化运算比循环快几个数量级。你不需要自己去写循环遍历每一行数据,这对新手来说大大降低了门槛。
tushare 负责数据获取。它提供了 A 股市场的日线、周线、分钟线等行情数据,接口简单,返回的就是 DataFrame 格式,和 pandas 无缝衔接。对于个人研究者来说,tushare 的免费额度基本够用,日常回测几个标的完全没问题。
注意:tushare 的部分接口需要注册后获取 token,并且不同积分等级能调用的接口不同。日线行情接口对积分要求较低,适合入门使用。
1.3 整体架构设计思路
整个策略的实现分为四个层次:
- 数据层:通过 tushare 获取标的的日线行情数据,包括开盘价、收盘价、最高价、最低价、成交量等字段。
- 计算层:用 pandas 计算短期和长期移动平均线,并生成买卖信号。
- 回测层:模拟交易过程,计算每次交易的盈亏,统计策略的整体表现。
- 评估层:计算年化收益、最大回撤、夏普比率等指标,判断策略是否值得进一步优化。
这个分层设计的好处是每一层职责清晰,后续想换数据源、改策略逻辑或者加评估指标,都只需要动对应的那一层,不会牵一发而动全身。
2. 数据获取与预处理的实操细节
2.1 tushare 环境配置与数据拉取
首先你需要安装必要的库:
pip install tushare pandas numpy matplotlib然后初始化 tushare 接口:
import tushare as ts import pandas as pd import numpy as np # 设置你的 token ts.set_token('你的token') pro = ts.pro_api()拉取日线数据的代码:
# 获取某只股票在某段时间的日线数据 df = pro.daily(ts_code='000001.SZ', start_date='20200101', end_date='20231231') # 数据默认按日期降序排列,需要转为升序 df = df.sort_values('trade_date').reset_index(drop=True) # 将日期列转为 datetime 类型 df['trade_date'] = pd.to_datetime(df['trade_date']) df.set_index('trade_date', inplace=True)这里有几个容易踩的坑。第一,tushare 返回的数据默认是降序的,也就是最新的日期在最前面,如果你不排序直接用,均线计算会完全错误。第二,trade_date 字段是字符串格式,必须转成 datetime 才能正确做时间序列操作。第三,记得把日期设为索引,后续 pandas 的 rolling 操作和绘图都会方便很多。
2.2 数据清洗与缺失值处理
实际拉下来的数据往往不干净。常见问题包括停牌期间没有数据、某些字段存在空值、复权问题等。
停牌导致的数据缺失最容易被忽略。如果一只股票停牌了半个月,你的数据里就直接少了这半个月的记录,均线计算时会把停牌前后的价格直接连起来算,导致信号失真。处理办法是检查日期是否连续:
# 生成完整的交易日序列 all_dates = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B') missing = all_dates.difference(df.index) print(f"缺失的交易日数量: {len(missing)}")如果缺失较多,可以考虑用前值填充或者直接换一个标的。对于入门回测,建议选择流动性好、停牌少的大盘股,减少数据问题带来的干扰。
复权问题也值得注意。tushare 的 daily 接口返回的是未复权价格,如果标的在回测期间有分红送股,价格会出现跳空,均线会被扭曲。建议使用 pro_bar 接口并设置 adj='qfq' 获取前复权数据:
df = ts.pro_bar(ts_code='000001.SZ', adj='qfq', start_date='20200101', end_date='20231231')2.3 数据字段的选择与衍生
原始数据有很多字段,但双均线策略核心只用到收盘价。不过我建议保留开盘价、最高价、最低价和成交量,原因有两个:一是后续如果想加入其他过滤条件(比如用成交量确认信号),这些字段直接可用;二是计算最大回撤时需要用到每日的实际价格波动。
我通常会额外计算一个日收益率字段,方便后续统计:
df['returns'] = df['close'].pct_change()这个字段在计算夏普比率和累计收益曲线时非常有用。
3. 双均线策略的代码实现与参数计算
3.1 移动平均线的计算原理与 pandas 实现
移动平均线的本质是对过去 N 天的收盘价取算术平均。以 5 日均线为例,第 t 天的 5 日均线值等于第 t-4 天到第 t 天收盘价之和除以 5。随着时间推移,这个窗口不断向前滑动,所以叫"移动"平均线。
用 pandas 实现非常简单:
df['ma_short'] = df['close'].rolling(window=5).mean() df['ma_long'] = df['close'].rolling(window=20).mean()rolling(window=N).mean() 这一行代码背后做的事情是:对每一个位置,取包括当前行在内的前 N 行数据,计算均值。前 N-1 行因为数据不够,结果会是 NaN,这是正常的。
这里有个细节:window 参数的选择直接影响策略表现。短周期越小,信号越灵敏但假信号越多;长周期越大,趋势判断越稳定但信号越滞后。常见的组合有 5/20、10/30、5/60 等,没有绝对最优,需要根据标的特性和回测结果来定。
3.2 买卖信号的生成逻辑
信号生成的核心是判断两条均线的相对位置变化:
# 当短期均线在长期均线之上时,signal 为 1,否则为 0 df['signal'] = np.where(df['ma_short'] > df['ma_long'], 1, 0) # 信号变化点即为买卖点 df['position'] = df['signal'].diff()position 为 1 表示金叉(短期上穿长期),为 -1 表示死叉(短期下穿长期),为 0 表示无变化。
但这里有一个实战中必须处理的问题:均线计算初期的 NaN 值会导致信号异常。前 19 天(以 20 日均线为例)的 ma_long 是 NaN,比较结果会是 False,signal 为 0,这本身没问题。但如果你在 NaN 期间就产生了交易信号,那肯定是错的。所以建议在计算完均线后,先去掉 NaN 行再做信号判断:
df.dropna(subset=['ma_short', 'ma_long'], inplace=True)3.3 参数选择的量化依据
很多人调参就是凭感觉试,今天用 5/20,明天用 10/30,没有一个系统的方法。我建议用网格搜索的方式,把短周期和长周期的组合都跑一遍,用夏普比率或者年化收益作为评价标准。
results = [] for short in [5, 10, 15, 20]: for long in [30, 40, 50, 60]: if short >= long: continue df['ma_s'] = df['close'].rolling(short).mean() df['ma_l'] = df['close'].rolling(long).mean() # ... 执行回测逻辑 # results.append({'short': short, 'long': long, 'sharpe': sharpe})但要注意过拟合问题。如果你在同一个时间段上反复调参,找到的"最优参数"很可能只是拟合了那段历史的噪声。正确的做法是划分训练集和测试集,在训练集上选参数,在测试集上验证。
4. 回测框架的搭建与绩效评估
4.1 简易回测引擎的实现
回测的核心是模拟"如果按照信号交易,资金曲线会怎么走"。最简化的版本不考虑手续费和滑点:
initial_capital = 100000 cash = initial_capital shares = 0 portfolio_values = [] for i in range(len(df)): price = df['close'].iloc[i] if df['position'].iloc[i] == 1 and cash > 0: shares = cash / price cash = 0 elif df['position'].iloc[i] == -1 and shares > 0: cash = shares * price shares = 0 portfolio_values.append(cash + shares * price) df['portfolio'] = portfolio_values这段代码的逻辑是:金叉时全仓买入,死叉时全部卖出。每次交易都是全仓进出,这是最简单的仓位管理方式。
但实际回测中必须考虑手续费。A 股买卖通常有万分之几的佣金和千分之一的印花税(卖出时收取)。加上这些成本后,高频交易策略的收益会明显下降:
commission = 0.0003 # 佣金 stamp_tax = 0.001 # 印花税(仅卖出)4.2 绩效指标的计算与解读
回测跑完后,你需要几个关键指标来判断策略好坏:
| 指标 | 计算方式 | 含义 |
|---|---|---|
| 累计收益率 | (最终市值 - 初始资金) / 初始资金 | 整体赚了多少 |
| 年化收益率 | (1 + 累计收益率)^(252/交易天数) - 1 | 折算到每年的收益 |
| 最大回撤 | max(1 - 当前市值/历史最高市值) | 最坏情况下亏多少 |
| 夏普比率 | (年化收益 - 无风险利率) / 年化波动率 | 单位风险获得的超额收益 |
| 胜率 | 盈利交易次数 / 总交易次数 | 交易的胜算有多大 |
最大回撤是我最看重的指标。一个策略哪怕年化收益很高,如果最大回撤超过 50%,实盘时你很可能在最低点割肉离场。一般来说,最大回撤控制在 20% 以内比较舒适。
夏普比率大于 1 算不错,大于 2 算优秀。但要注意,夏普比率高不一定代表策略好,如果交易次数太少,统计意义不足,夏普比率再高也不可信。
4.3 交易明细的记录与分析
光看总收益不够,还需要记录每一笔交易的细节:
trades = [] entry_price = 0 entry_date = None for i in range(len(df)): if df['position'].iloc[i] == 1: entry_price = df['close'].iloc[i] entry_date = df.index[i] elif df['position'].iloc[i] == -1 and entry_price > 0: exit_price = df['close'].iloc[i] profit = (exit_price - entry_price) / entry_price trades.append({ 'entry_date': entry_date, 'exit_date': df.index[i], 'entry_price': entry_price, 'exit_price': exit_price, 'profit': profit }) entry_price = 0 trades_df = pd.DataFrame(trades)有了交易明细,你可以分析:平均持仓周期多长?盈利交易和亏损交易的分布如何?是不是某几笔大额盈利撑起了整体收益?这些分析能帮你判断策略的收益来源是否可持续。
5. 常见问题与实战避坑指南
5.1 回测结果与实盘差异的根源
很多人回测出一条漂亮的资金曲线,实盘却亏得一塌糊涂。差异主要来自几个方面。
未来函数问题是最隐蔽的杀手。如果你的信号计算用到了当天收盘价,但交易执行也是按当天收盘价,那实际上你是在用当天收盘后才知道的信息做当天的交易,这在实盘中不可能实现。正确的做法是:用当天收盘价计算信号,第二天开盘时执行交易。
滑点是另一个被忽视的因素。回测中你假设能按收盘价精确成交,但实盘中大额买卖会推动价格变动,尤其是流动性差的标的。建议在回测中人为加入 0.1% 到 0.2% 的滑点。
手续费累积也不可小觑。双均线策略在震荡市中会频繁交易,如果每次交易成本是 0.13%(佣金加印花税),一年交易 20 次就是 2.6% 的成本,直接吃掉大部分利润。
5.2 震荡行情中的假信号过滤
双均线策略最大的痛点就是震荡行情中反复被打脸。金叉买入后马上死叉,死叉卖出后马上金叉,来回几次本金就磨损了不少。
几种常见的过滤方法:
- 加入趋势过滤:只有当长期均线本身向上时才考虑买入信号,长期均线向下时忽略金叉。
- 加入成交量确认:金叉当天如果成交量明显放大,信号更可靠。
- 加入价格过滤:要求金叉时价格已经站上长期均线一定幅度,避免均线纠缠时的假突破。
- 加入时间过滤:两次交易之间设置最小间隔,避免一天内反复进出。
这些过滤条件没有绝对的好坏,需要根据标的特性回测验证。我的经验是,加入趋势过滤后,交易次数会明显减少,胜率提升,但单边行情中可能会错过一部分利润。
5.3 参数过拟合的识别与规避
过拟合是量化策略的头号敌人。识别过拟合有几个信号:
第一,参数敏感度太高。如果短周期从 5 改成 6,收益就从 30% 掉到 5%,说明这个参数是在拟合噪声,不是真实规律。
第二,样本内外表现差异大。在 2020 年到 2022 年数据上表现优异,但 2023 年数据上完全失效,说明策略没有泛化能力。
第三,交易次数太少。如果三年只交易了 5 次,那统计上根本不足以判断策略有效性,高收益可能只是运气。
规避过拟合的方法:用滚动窗口回测,每次用前 N 年数据选参数,后 M 个月验证;或者用多只标的做交叉验证,看策略是否在不同标的上都有效。
5.4 常见报错与排查速查表
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
| KeyError: 'trade_date' | 字段名拼写错误或接口返回字段不同 | 打印 df.columns 确认实际字段名 |
| ValueError: window must be an integer | rolling 参数传了浮点数 | 确保 window 参数是整数 |
| 均线全为 NaN | 数据量不足或日期未排序 | 检查数据行数是否大于窗口大小 |
| 信号全部为 0 | 均线计算前未去除 NaN 或数据未排序 | 先 dropna 再计算信号 |
| 回测收益异常高 | 存在未来函数或未考虑交易成本 | 检查信号与执行的时间对齐 |
提示:每次修改代码后,先打印前 20 行数据肉眼检查,比直接跑回测更容易发现低级错误。
6. 策略优化的几个实用方向
6.1 从固定参数到自适应参数
固定参数的双均线在不同市场环境下表现差异很大。一个改进方向是让参数自适应:用波动率或者趋势强度来动态调整均线周期。比如波动率大的时候用更长的周期过滤噪声,波动率小的时候用更短的周期提高灵敏度。
实现上可以用 ATR(平均真实波幅)来衡量波动率,然后根据 ATR 的分位数来切换参数组。这个思路不复杂,但能明显改善策略在不同市场状态下的适应性。
6.2 加入止损止盈逻辑
纯双均线策略依赖死叉来平仓,但死叉出现时往往已经回吐了不少利润。加入固定比例的止损止盈可以改善这一点:
stop_loss = 0.05 # 亏损 5% 止损 take_profit = 0.15 # 盈利 15% 止盈在持仓期间,每天检查是否触发止损止盈条件,触发就立即平仓,不等死叉。这样能锁定利润、控制单笔亏损,但也会在某些情况下过早离场,错过后续大涨。需要回测对比两种方式的表现。
6.3 多标的分散与仓位管理
单标的双均线策略的收益波动很大,一个改进方向是同时跑多只标的,每只分配一部分资金。这样单只标的的假信号对整体组合的影响就被稀释了。
仓位管理上,可以等权分配,也可以根据波动率倒数分配(波动率低的标的配更多资金)。后者能让组合的整体波动更平稳。
6.4 从日线到分钟线的注意事项
有人想把双均线用到分钟线上做日内交易。技术上可行,但有几个坑要注意。分钟线的噪声远大于日线,假信号更频繁;交易成本占比更高,因为交易次数大幅增加;tushare 的分钟线接口对积分要求更高,数据获取可能受限。如果没有充分的准备,不建议新手直接上分钟线。
7. 我个人在实际操作中的几点体会
跑过几十个双均线回测之后,我最大的感受是:策略本身很简单,难的是对结果的理解和取舍。
第一,不要追求回测曲线最漂亮的参数。那条曲线大概率是过拟合的产物。我更倾向于选择参数平原——也就是周围几个参数组合表现都不错的区域,而不是孤立的最高点。
第二,交易成本一定要算进去。我早期回测时忽略了手续费,看到一个策略年化 25% 很兴奋,加上成本后直接掉到 12%,心态完全不一样。
第三,双均线策略适合趋势明显的标的。如果你选的标的长期横盘震荡,再好的参数也救不了。选标的比调参数重要得多。
第四,回测只是起点,不是终点。真正能让你进步的是把策略跑在模拟盘上,观察信号触发时的市场环境,理解为什么这次金叉有效、那次金叉无效。这种盘感是回测给不了的。
最后分享一个小技巧:在回测代码里加一个信号标记图,把买卖点画在 K 线图上。肉眼看一下信号位置是否合理,比看一堆数字指标更直观。很多时候你一眼就能看出某个信号明显是噪声,然后针对性去优化过滤条件。