双均线策略回测大概是我见过被最多人当作“量化第一课”的项目。两条均线,一短一长,金叉做多、死叉离场,逻辑简单到可以写在一张便签纸上。但真正从零动手把它做成一次完整回测——从拿到历史行情数据,到生成交易信号,再到扣除手续费和滑点之后算出净值曲线——你会发现它几乎覆盖了量化回测里的全部基本功:数据清洗、复权处理、未来函数规避、成本建模、过拟合检验。这篇文章记录的就是我从头做“经典双均线策略回测”的完整过程,每一步怎么设计、为什么这么设计、踩过哪些坑都会写清楚。如果你正准备上手第一个回测项目,或者已经跑出一份漂亮的净值曲线却不知道结果到底靠不靠谱,这篇应该能给你一份可以直接照抄的作业。
1. 双均线策略拆解:金叉死叉背后到底在验证什么逻辑
1.1 金叉死叉的市场含义与策略代价
先补一点基础认知。移动平均线就是把过去N日收盘价求平均,它平滑掉了短期噪声,反映的是一段时间内市场参与者的平均持仓成本。MA5是最近5天的平均成本,MA20是最近20天的平均成本,别看这两个数只是简单的算术平均,它们之间的关系,在交易者眼里代表了一种市场状态。
当MA5从下方上穿MA20,形成金叉,说明短期买入者的平均成本在抬升,近期资金进场的意愿比中期趋势更强,通常被解读为趋势可能由跌转涨的启动信号。反过来,MA5下穿MA20形成死叉,说明短期成本重心在下移,近期持有者倾向离场,可能意味着趋势转弱。双均线策略做的事情,就是把这套市场状态切换翻译成两条操作指令:金叉了就持有,死叉了就空仓。
不过一定要清楚它的代价。均线天然是滞后指标,金叉确认的时候,价格往往已经离开底部一段距离;死叉发生的时候,顶部利润也已经回吐了一部分。换句话说,双均线吃的是趋势中段那段肉,指望它从头抄底逃顶是不现实的。明白了这一点,回测中出现“刚金叉就回调”或者“刚离场就反弹”的窘境时,就不会觉得是策略写错了,这是策略本身必须承受的成本。
1.2 均线参数组合的选型逻辑
参数选择是双均线的灵魂。常见组合有MA5/MA20、MA10/MA30、MA20/MA60。周期越短,金叉死叉越灵敏,越能在早期捕捉反弹,但也会在震荡行情里频繁触发,被反复打脸;周期越长,信号次数越少,但每次信号背后对应的趋势级别更大,理论上“含金量”更高。
我的基准组合选了MA5/MA20,理由很实际:日线级别上,这个组合能兼顾信号数量和信号质量,一年内能给出十几个可交易的信号,足够支撑统计分析,又不至于高频到被手续费拖垮。我同时还用MA10/MA60做了一组对照回测,用来观察不同灵敏度的策略在净值曲线上呈现出怎样的差异。
下面这个对比表我只放了规则层面的结论,具体收益数字放在第四章:
| 参数组合 | 信号频率 | 适合的市场环境 | 主要弱点 |
|---|---|---|---|
| MA5/MA20 | 较高 | 波动幅度较大的趋势行情 | 震荡市反复止损 |
| MA10/MA60 | 较低 | 慢牛或大级别趋势 | 信号少,容易错过启动段 |
| MA20/MA60 | 很低 | 月线级别大周期 | 中期回调几乎不回避 |
这里有个新手容易忽略的细节:不管参数怎么选,信号判断都应该基于收盘价计算的均线。也就是说,当日收盘后才能确认“今天是不是金叉”。如果盘中用实时价格触发信号,同样的参数在回测里会与实盘产生巨大的执行偏差。更多细节我在第三章展开。总之,选参数不要一上来就想“找最优”,先找一组业务上说得通的数字把流程跑通,后面再做参数扫描验证稳定性。
2. 回测前的准备工作:数据、工具和参数选型
2.1 回测工具选型:pandas手写还是专业框架
做双均线这个量级的回测,工具选择其实很自由。现在主流的路径有三条:直接用带回测引擎的平台(如聚宽、掘金这类在线量化平台)、用现成的Python回测框架(如backtrader)、或者用pandas从零手写。我的建议是,第一次做双均线回测,老老实实用pandas手写最划算。
原因很直接:双均线的策略逻辑本身只有几行,用框架反而要先学习那套框架的规则,比如怎么组织Account/Order对象、怎么注册指标、怎么处理交易日历,学习成本比策略本身还高。而手写的好处是每个环节都完全透明,信号是在哪一天产生的、用哪个价格成交、手续费加在哪一步,都是自己写出来的,出了问题一眼就能定位。等你把流程跑熟了,再迁移到专业框架,反而能理解框架里每个配置项在干什么。
2.2 数据获取与预处理:复权和停牌怎么处理
回测第一步是拿数据。A股日线级别的历史数据,开源渠道有很多,比如tushare、akshare、baostock。这次我用的是本地CSV格式的日线数据,字段就是date、open、high、low、close、volume六列,回测真正用到的主要是close和open。
数据拿到手不能直接用,至少做三件事。第一,检查缺失值和重复值:删除重复的交易日,把close列的NaN和0揪出来,否则均线会在这些位置出现断崖式跳变。第二,检查日期排序。数据源可能默认按时间倒序输出,必须先按date升序排好,否则后面的rolling(滚动计算)全乱套。第三,复权处理,这是新手最容易翻车的地方。
股票分红送股之后会除权除息,价格会瞬间“跳空”下跌,但这个跳空是假象,是除权造成的数字游戏。如果不做复权,均线就会在这个位置被强行拉出一个假的死叉,策略会误判卖出。回测中我统一用前复权价格来做信号和净值计算。前复权剔除了历史除权跳空,能保证信号的一致性。需要提醒的是,不同数据源的前复权基准可能不同,做多策略对比时,务必确保所有标的使用同一个数据源、同一个复权规则。
还有一个实操细节:如果持仓股票当天停牌,你挂的单根本成交不了。回测里我的处理是,把成交量为0的交易日标记出来,凡是卖出信号落在停牌日,就顺延到下一个可交易日的开盘价成交,否则遇到长期停牌的重仓股,净值曲线会严重失真。
3. 回测核心实现:信号生成、成交价格与成本模拟
3.1 信号生成与成交假设:未来函数必须规避
这是双均线回测里最牵扯精力的一环,也是决定回测结果是否可信的核心。所谓未来函数,就是你在回测中使用了当时无法获得的信息,导致策略“开挂”,回测收益率虚高。最常见的错误就是:用当天收盘价算出金叉信号,然后假设以当天收盘价买入了。
问题在哪?金叉信号是收盘之后才确认的,收盘那一刻你根本来不及用收盘价成交。除非在收盘集合竞价阶段挂单,并且严格假设成交在收盘竞价,但这在日线级回测里会引入太多执行层面的假设。最稳妥的做法是:T日收盘后确认信号,T+1日以开盘价成交。对应到代码,就是把持仓信号整体向后平移一天:
import pandas as pd df['MA5'] = df['close'].rolling(5).mean() df['MA20'] = df['close'].rolling(20).mean() # 持仓信号:1代表持仓,0代表空仓 df['signal'] = 0 df.loc[df['MA5'] > df['MA20'], 'signal'] = 1 # 关键:次日生效,避免用当日收盘价成交 df['position'] = df['signal'].shift(1) df['ret'] = df['close'].pct_change() df['strategy_ret'] = (df['position'] * df['ret']).fillna(0) df['equity'] = (1 + df['strategy_ret']).cumprod()上面这段代码里最核心的就是df['position'] = df['signal'].shift(1),它保证策略在信号出现后的下一个交易日才持有头寸,而不是“瞬移”进场。同时它也符合A股T+1制度:T日收盘确认信号,T+1日开盘买入,最早也要等到再下一个交易日才可能触发新的卖出信号,不存在当天买当天卖的问题。
提醒一句:市面不少平台默认用“信号当天收盘价成交”,如果你拿自己的回测结果和别人对比,先问清楚对方的成交假设是什么,否则收益数字没有可比性。
3.2 成本模型:手续费、印花税和滑点的量化
信号再准,扣完成本可能变成亏钱机器。双均线在震荡市的换手率不低,成本必须精打细算。我在回测中按A股实际交易成本做了如下设定:
| 成本项 | 参数 | 说明 |
|---|---|---|
| 佣金 | 万2.5 | 双边收取,按单笔金额足够大计算,5元最低佣金的影响可以忽略 |
| 印花税 | 0.05% | 2023年8月减半后执行,仅卖出时收取 |
| 滑点 | 0.05% | 每次成交额外让出0.05%,模拟开盘价跳空的误差 |
具体计算方式,我直接在收益序列里修正:开仓那天,strategy_ret额外扣掉万2.5佣金加0.05%滑点;平仓那天,再扣掉万2.5佣金、0.05%滑点和0.05%印花税。这样每个信号对应的交易摩擦都被实实在在扣在当天的净值里,而不是最后从总收益里一刀切掉。
有个很多人没意识到的点:回测里的成交价永远只是近似值。开盘价成交已经算比较保守,但开盘瞬间同样有跳空,所以额外加一笔滑点做安全垫是必要的。如果你把滑点从0.05%调到0.1%,策略就从盈利变成亏损,那只能说明这个策略很脆弱,不值得实盘押注。我后来做敏感性测试时,习惯把滑点参数单独拎出来跑一遍,看看策略对交易摩擦的容忍度到底有多高。
4. 回测结果解读:从指标到参数敏感性的完整分析
4.1 核心绩效指标:收益、回撤、夏普、胜率怎么配合看
回测跑完会得到一串收益率和一条净值曲线,但“总收益率多少”远不足以描述一个策略。我固定计算一套指标,每个指标都能从不同角度暴露策略的弱点。年化收益率回答“这笔钱一年能赚多少”,按复利年化;最大回撤回答“最惨的时候从高点亏了多少”,这决定了你在实盘里能不能拿得住仓位;夏普比率衡量风险调整后的收益,简单理解就是每承担一单位风险换来多少超额收益,A股日线策略能跑到1以上已经不错;胜率和盈亏比则用来拆解盈亏来源。双均线的胜率通常不高,可能不到40%,但盈利单的平均利润要明显大于亏损单的平均亏损,它赚钱靠的是“截断亏损、让利润奔跑”,而不是靠高胜率。
对应的指标计算,我补一段核心代码:
import numpy as np total_return = df['equity'].iloc[-1] - 1 # 252是A股一年的交易日数量 annual_return = (df['equity'].iloc[-1]) ** (252 / len(df)) - 1 cum_max = df['equity'].cummax() max_drawdown = (df['equity'] / cum_max - 1).min() daily_vol = df['strategy_ret'].std() * np.sqrt(252) sharpe = annual_return / daily_vol print(f'总收益: {total_return:.2%}') print(f'年化收益: {annual_return:.2%}') print(f'最大回撤: {max_drawdown:.2%}') print(f'夏普比率: {sharpe:.2f}')我的MA5/MA20基准回测,放在过去五年的沪深300成分股某只样本上,年化收益大概在百分之十几,最大回撤约25%,夏普比率在0.7到0.9之间。听着不算惊艳,但这是扣掉各项成本和滑点后实打实的结果。做回测不是看谁跑出来的数字惊悚,而是看这个数字是怎么来的、能不能复现。
4.2 参数敏感性分析与基准指数对比
参数敏感性分析,是检验双均线是不是在碰运气的最直接方式。做法是把快线周期从3日扫到15日、慢线从15日扫到60日,形成一个参数网格,每个组合跑一遍完整回测,最后把年化收益或夏普比率画成热力图看走势。
我实测下来,MA5/MA20附近确实有一片相对稳健的区域,但周围不是“别的参数全亏、唯我独赚”的极端状态,而是一个渐进变化的平滑曲面。如果你跑出来的结果是在某个参数上突然冒出巨大尖峰,比如MA7/MA23收益翻倍,旁边MA6/MA23直接亏损,那基本可以判定这个参数是过拟合的,换个时间段就会失效。
基准对比同样重要。沪深300指数是一个免费的参照系:如果策略跑出来的收益还不如无脑持有沪深300,那就没创造alpha价值,这套系统没有存在的意义。我习惯把策略净值和基准净值画在同一张图里,盯着超额收益的来源。双均线在单边行情里可能跑赢基准,震荡行情里跑输都很正常,关键是长期累计下来有没有稳定超越。同时,任何带择时信号的策略都要额外关注熊市里的下行保护,双均线的死叉在漫长熊市里能帮你离场,这也是它最核心的价值所在。
5. 双均线回测的坑与避坑经验
5.1 回测陷阱排查:从幸存者偏差到涨跌停约束
第一是历史区间选择。如果只拿最近两年的单边上涨数据回测,任何买入持有策略都会很好看,双均线也一样,单边行情里金叉频繁出现、死叉基本不出现,收益自然接近满仓买入。真正检验策略的是混合行情样本,我现在会把回测区间拉长到至少包含一轮完整牛熊,比如2018到2023年,让策略在下跌段有机会展示离场能力。
第二是忽略涨跌停和流动性约束。回测模型默认想买就买、想卖就卖,但A股跌停时你是卖不出去的。如果回测不考虑这个约束,大跌之后死叉信号出现时正好挂单卖出成功,净值就会比实盘漂亮。我在成交逻辑里加了一层过滤:T+1日开盘若跌停,平仓信号顺延到下一个可交易日。这个细节对回测结果的影响,比大多数人想象的要大。
第三是成本后置。有些新手先生成一条完美净值曲线,到结尾才把手续费从总收益里扣除,这样总收益看起来扣了钱,但过程中每一次信号的盈亏、最大回撤、换手率相关的风险指标全部失真。成本必须跟着交易走,哪次开平仓就扣哪次的成本,这一条建议直接照抄。
我把排查过程中比较典型的问题整理成一个速查表,各位回测出异常结果时可以对着自查:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 回测年化高得离谱 | 使用了未来数据 | 检查成交价格是否是信号日当天 |
| 股灾期间净值几乎不跌 | 涨跌停约束未建模 | 检查跌停日是否仍假设成交 |
| 加成本后由盈转亏 | 换手率过高 | 统计年均交易次数,尝试拉长均线周期 |
| 不同时间区间结果差异巨大 | 参数过拟合 | 做样本外验证和参数热力图 |
还有一个账很多人不算:把交易频率统计出来,双均线一年可能产生几十次交易,单边佣金万2.5加上卖出印花税,每笔往返成本大约千分之几,几十次下来就能吃掉几个点到十几个点的收益。这个账不算清楚,回测收益率虚高个5%都很常见。
5.2 过拟合与样本外验证:别让参数骗了你
最后重点说过拟合。双均线只有两个参数,看着简单,实际上正因为参数少好调,更容易被人反复优化到某个狭窄区间,最终收获一条曲线拟合得很好的回测。我的铁律是:优化只用训练集,验证只用测试集。历史数据按时间切成两段,前70%用于参数扫描和策略调整,后30%完全冻结,作为没碰过的样本外数据。参数一旦确定,就绝不再为了样本外结果回头调整。
实际操作中,样本外表现差于样本内很正常,关键是不能出现突然变脸:训练集年化15%、样本外年化8%,这种衰减可以接受;训练集年化30%、样本外为负数,那不是发现圣杯,是过拟合了。这种断层现象在实盘里会直接导致灾难。
我有一条主观但很好用的经验:最终参数一定选在“参数高原”上,而不是“参数尖峰”上。所谓高原,就是周围一圈参数表现都不差的位置;尖峰,则是只有你最得意的那个点收益奇高、旁边全部拉胯。选高原上的参数,代表策略逻辑是稳健的,不是被某个特殊时间段喂养出来的巧合。做完这个双均线项目,我最大的收获就是这条判断标准。
最后再补一句个人体会。双均线这个级别的简单策略,指望它实盘常年稳定盈利是不现实的,但它作为回测练手项目简直完美——逻辑足够简单,可以把全部精力放到回测本身的正确性上。我做完这个项目后,把数据清洗、复权处理、未来函数规避、成本建模、参数敏感性检验这一整套流程全部用手过了一遍,这些东西搬到任何更复杂的策略上都通用,它们才是回测真正值钱的部分。如果你正准备做第一个回测项目,我建议就从双均线开始,把它做扎实,后面你会感激自己这个决定。