news 2026/9/15 5:31:14

量化新手必看:双均线策略回测全流程详解,30分钟跑通

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
量化新手必看:双均线策略回测全流程详解,30分钟跑通

你打开编辑器,准备写自己人生中第一个量化策略的时候,多半会看到各种论坛里铺天盖地的“双均线策略”“金叉死叉”这些词。别觉得老套,双均线策略真的是最适合新手的第一个量化策略,没有之一——逻辑简单、代码量小、边界清晰,而且能完整体验“拿数据→算信号→做回测→看资金曲线”的全过程。这篇文章就是按照我自己的经验,带你30分钟把这个流程完整跑通,顺便把那些教程里不太会写的坑也抖出来。

我对这篇文章的定位是给两类人看:一类是刚接触量化、想搞懂回测是怎么回事的Python初学者,另一类是已经能写代码、但一直没想明白“策略到底怎么从信号变成真金白银”的转型选手。我不打算把双均线的原理翻来覆去地讲,而是直接给你一条最短路径,让你先把流程跑通,再回头补理论。毕竟做量化这件事,卡在“想”和“做”之间的人太多了,先动起来比什么都重要。

1. 为什么第一策略一定要是双均线

1.1 双均线策略到底在做什么

双均线策略的核心逻辑一句话就能说清:当短期均线从下方穿过长期均线时买入,当短期均线从上方穿过长期均线时卖出。前者叫“金叉”,后者叫“死叉”。

均线本身是什么?它就是过去N根K线收盘价的平均值。MA5代表最近5个交易日的平均成本,MA20代表最近20个交易日的平均成本。短期均线代表了市场最近的情绪,长期均线则反映了近一个月左右的筹码成本。当短线上穿长线,说明最近买入的人比过去一个月买入的人更愿意出高价,市场短期动能向上;反过来,短线下穿长线,说明短期抛压已经超过了前期的平均成本,动能向下。

这其实就是最朴素的趋势跟踪思路:均线把每天的价格噪声做了平滑,只留下方向性信息。你赚的是趋势行情的钱,而不是每天的波动差价。所以双均线策略本质上是在回答一个问题——现在这个品种,到底是在上涨趋势里还是在下跌趋势里?如果是在上涨趋势里,我就持仓;如果不在,我就空仓或者做空。

1.2 为什么拿它当入门策略最合适

我见过不少新人一上来就研究机器学习预测股价、搭建多因子模型,结果卡在数据清洗和因子计算上好几周,最后连一次完整的回测都没跑完。选双均线当第一个策略,有三个很实际的理由:

第一,逻辑透明。每一笔买卖都能用一句人话解释清楚,“MA5上穿MA20了,所以我买了”,复盘的时候不玄学、不黑箱,出了问题容易定位。

第二,代码量小。纯用pandas实现一个带交易明细的回测,只需要几十行代码。30分钟跑通是真实可行的,不是标题党。

第三,可扩展性强。双均线是很多策略的基本骨架,你后续加止损、加仓位管理、加波动率过滤,都是在它基础上叠加。把这个骨架吃透,后面的路会顺很多。

2. 跑通策略前的准备工作

2.1 数据从哪来:先准备一份干净的行情数据

做回测的第一步是拿到一份能用的行情数据。我不建议一上来就接实盘接口,先在本地准备一份CSV文件就够了。字段最少要有:日期(date)、开盘价(open)、最高价(high)、最低价(low)、收盘价(close)、成交量(volume)。这是最通用的OHLCV格式。

从公开数据源下载历史日线数据时,有两个地方很容易踩坑。第一个是复权问题:股票分红、送股会造成价格跳空,不复权的数据会让均线产生虚假的信号。建议使用“前复权”数据做回测,也就是把历史价格按照当前价格做了统一调整。第二个是数据里面可能有停牌日、空值,回测前要做清洗排序,不然后面的计算很容易报错。

我自己的习惯是把数据文件命名为000300.csv,放在项目根目录下,这样后面代码里读文件的路径最省事。你可以用沪深300指数、创业板ETF或者任何你熟悉的品种作为第一个实验对象。

2.2 回测环境安装:Python + pandas 足够用了

说到回测,很多人会觉得必须要上专业的量化框架。我的建议恰恰相反:第一次跑通策略,手写回测就够了,别急着用框架。原因很简单——框架帮你封装了太多细节,你反而搞不懂资金、仓位、信号这些环节是怎么串起来的。老老实实用pandas一行一行算,跑通了再换框架,你会感谢自己做了这个决定。

环境方面只需要三样东西:Python 3.8以上,pandas,以及matplotlib(用来画资金曲线)。安装命令:

pip install pandas matplotlib

如果你用的是Anaconda,一般这两个库都已经内置了。全程不需要装额外的量化库。

2.3 数据预览:先肉眼看一遍趋势

数据读进来以后,不要急着写策略,先画个图。这一步几乎是所有教程不会提但非常关键的环节。用收盘价画出价格曲线,顺手把MA5和MA20叠加上去,先肉眼确认一下金叉死叉的位置是否大致符合行情走势。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('000300.csv', parse_dates=['date']) df.set_index('date', inplace=True) df.sort_index(inplace=True) df['close'].plot(figsize=(12, 5)) plt.title('Close Price') plt.show()

为什么这个步骤重要?因为如果后面策略跑出来的结果很离谱,你至少能凭借肉眼判断是数据问题、信号问题还是逻辑问题。我见过太多人写了几个小时代码,结果跑出来的收益曲线和真实行情完全对不上,最后发现问题出在数据排序上——日期索引是倒序的,均线算反了。

3. 双均线策略的完整实现

3.1 计算均线并生成交易信号

数据准备好以后,核心代码就三步:算均线、定信号、找买卖点。

# 计算短期和长期均线 df['ma_short'] = df['close'].rolling(window=5).mean() df['ma_long'] = df['close'].rolling(window=20).mean() # 生成持仓状态:短期均线在上方时为1,否则为0 df['position'] = (df['ma_short'] > df['ma_long']).astype(int) # 找出仓位发生变化的位置 df['signal_change'] = df['position'].diff()

这里有一个非常关键的点:我把信号定义成“持仓状态”而不是“交易动作”。position列的每一行都表示“今天这个时刻,我应该持有还是空仓”,值为1表示持仓,0表示空仓。金叉发生时position从0变成1,死叉发生时position从1变成0。

为什么要这样设计?因为均线是连续变化的,金叉之后的每一天,短期均线大概率仍然在长期均线上方,信号值会连续保持1。如果你直接写成“当短期均线大于长期均线就买入”,那么一个上升趋势里你会连续买入十几次,资金根本不够用。用position+diff()的方式,只在状态变化的那一刻触发交易,才是最符合实际操作的写法。

signal_change为1的时候,就是金叉;为-1的时候,就是死叉。回测逻辑里只需要识别这两种情况。

3.2 从信号到交易:必须用次日开盘价成交

拿到买卖点之后,要做一件非常重要的事:把信号转换成真实的交易记录。这里面最大的坑,就是成交价的设定。

很多新手直接写“金叉当天按收盘价买入”,这个做法在回测里显得收益很好看,但实际做不到。原因很简单:金叉收盘之后,你才通过公式算出今天出现了金叉,此时收盘价已经确定了,你不可能用这个价格成交。就算你速度再快,最早也只能在下一个交易日开盘时买入。

所以正确做法是:信号出现的第二天,以开盘价成交。用代码翻译一下就是——当signal_change为1时,下一行的open就是你的买入成交价。

df['trade_price'] = df['open'].shift(-1) # 第二天开盘价 df['buy_flag'] = (df['signal_change'] == 1) df['sell_flag'] = (df['signal_change'] == -1)

这里使用shift(-1)把第二天的开盘价“提到”今天这一行来,相当于告诉回测引擎:“今天收盘后确定了信号,明天用这个价格去成交。”

忽略这个细节,回测收益可能虚高好几个百分点,而一旦实战你就会发现这个收益根本抓不住。这是双均线回测里最常见的幽灵收益来源之一。

3.3 手写一个最简回测:跟踪资金曲线

现在开始模拟交易。我们做一个最简单的不加杠杆、满仓进出的回测:初始资金100万,每次信号出现就全仓买入或者全部卖出。每一笔交易都记录下来,同时计算每天的资产总值。

initial_cash = 1000000.0 cash = initial_cash position = 0 # 当前持仓股数 trades = [] # 交易明细 for i in range(len(df)): if df['buy_flag'].iloc[i]: position = cash / df['trade_price'].iloc[i] cash = 0.0 trades.append({ 'date': df.index[i], 'type': 'BUY', 'price': df['trade_price'].iloc[i], 'shares': position }) elif df['sell_flag'].iloc[i] and position > 0: cash = position * df['trade_price'].iloc[i] trades.append({ 'date': df.index[i], 'type': 'SELL', 'price': df['trade_price'].iloc[i], 'shares': position }) position = 0.0 df.loc[df.index[i], 'total_asset'] = cash + position * df['close'].iloc[i]

注意到这里有个细节:买卖价用的是trade_price(次日开盘价),但每天计算资产总值时,我用的是当天的收盘价close。因为收盘价才代表你当前时间点持仓的真实市值,这个设计更贴近实际账户的每日结算规则。

这个循环虽然简单,但里面有一个新手很容易漏掉的边界条件:策略在空仓时,position为0,此时卖出一个空仓状态是无效的。所以elif里加了position > 0的判断,避免“空仓卖出”这种荒谬交易发生。

3.4 评价指标:别只看收益率

交易跑完,眼睛不能只盯着“最终赚了多少钱”,要看几个更全面的指标。我每次回测必看的有四个:累计收益率、年化收益率、最大回撤、交易次数。

  • 累计收益率:期末资产 / 初始资产 - 1,告诉你策略整个区间的收益。
  • 年化收益率:把累计收益按年化折算,便于和其他策略对比。
  • 最大回撤:从任意高点跌到后续最低点的最大幅度,它直接代表这个策略的心理折磨上限。
  • 交易次数:次数太少说明策略不敏感,太多说明被震荡行情反复打脸。

计算最大回撤有一段通用代码,顺手就能用:

df['cummax'] = df['total_asset'].cummax() df['drawdown'] = df['total_asset'] / df['cummax'] - 1 max_drawdown = df['drawdown'].min()

我拿一段沪深300历史数据跑过这个5/20双均线,大致结果是这样:区间内年化收益7%~13%不等,最大回撤8%~15%,交易次数大约20到30次。这个收益不算惊艳,但它是你亲手跑出来的第一个基线结果。后面加过滤条件、优化参数,都是以这个为基准去对比的。注意,我不给你具体数字,是因为不同时间段、不同品种差异很大,你自己跑出来才有意义。

4. 均线参数怎么看:5/20组合背后的门道

4.1 不同均线组合的性格差异

为什么双均线默认都用5和20?因为5日线大约是一周的交易平均线,20日线大约是一个月,这个组合既不过分灵敏也不过分迟钝,属于比较均衡的入门参数。但你要知道,换个参数,策略性格会完全不一样。

拿几组常见参数来说:5/10组合均线周期短,反应快,在趋势启动初期就能进场,但震荡行情里被反复打脸的概率也高;20/60组合周期长,信号极少,但每笔交易都能吃到大段趋势,空仓时间很长,考验耐心;10/30组合则介于两者之间,我更愿意把它当作5/20的“试探性替代”。

这里我不建议你靠肉眼挑参数,可以自己写一个循环,把几组均线周期都跑一遍,对比收益和回撤,看看哪组在你的数据区间里更稳定。跑完你大概率会发现一个规律:短周期参数在震荡市吃亏,长周期参数在趋势市占便宜,没有一劳永逸的最优解。

4.2 策略为什么会失效:认识胜率与盈亏比

双均线策略的胜率通常不高,很多时候只有40%左右。这意味着十笔交易里有六笔是亏钱的。但这不代表策略不赚钱,关键在盈亏比——赚的时候赚大钱,亏的时候亏小钱。

趋势跟踪策略的盈利逻辑在于:震荡期反复小亏,但一旦趋势来了,一次大赚就能覆盖之前的所有亏损。理解这一点非常重要,因为很多人跑完回测,看到胜率不到50%,就开始怀疑策略,瞎调参数,最后把一套原本合理的逻辑改得面目全非。

我做回测时特别关注资金曲线回撤段,如果回撤期之后的反弹足够有力,说明策略的盈亏比设计是对的。如果回撤之后一直爬不起来,那就要看是行情原因,还是策略逻辑本身有问题。

4.3 关于参数优化的警告

每跑完一组参数,如果某组参数的收益特别高,别急着高兴,先问一句:这是不是因为参数恰好拟合了这段历史行情?我见过有人为了追求漂亮曲线,把均线周期从5/20一路试到8/33,最后找到一组完美参数,结果换一段行情立刻亏成狗。这就是典型的过拟合。

一个常用的防过拟合手段是样本外测试:把数据分成前半段和后半段,用前半段选参数,用后半段验证。选参数只看前半段,能让后半段近似的模型才靠谱。这个方法不完美,但对新手来说已经完全够用了。

5. 常见问题与排查技巧实录

5.1 为什么信号很多,交易却很少

这是新手最常见的困惑。代码里统计出来金叉死叉各有二三十次,但交易列表里只有三五笔交易。排查思路很简单:先检查position.diff()是否写对。如果直接用“短期均线是否大于长期均线”来触发交易,那确实每天都会产出同一个方向的大量信号。正确做法是只在状态翻转当天交易,上面的signal_change方法已经天然规避了这个问题。

还有一种情况是数据区间太短。如果样本里总共只有60多个交易日,MA20在初期会产生大量的NaN值,有效信号区间不足,交易次数自然很少。这属于正常现象,多拉几年数据再测就行。

5.2 回测收益很高,但总觉得不真实

收益高得让你自己都心虚,那通常就是出了问题。最常见的元凶是使用未来数据。复盘一下这几个场景,看看你踩中了几个:

  • 用当天的收盘价信号去成交当天收盘价,实际做不到。
  • 用当天的“最高价、最低价”进行止损判断,但止损指令实际只能在下个K线确认。
  • 统计指标时用了全量数据的“平均收益”“波动率”,导致前期的回撤被平滑掉了。
  • 没有扣除手续费和滑点。

只要存在上面任意一条,回测收益就是虚高的,实盘大概率跑不出这个数值。我的习惯是在回测里至少扣掉万分之三的手续费和万分之五的滑点。

5.3 数据对齐和NaN问题怎么处理

rolling(window=20).mean()在数据前19行必然会产生NaN,这是正常的。处理方式有两种:一种是直接用dropna()丢掉前期数据,另一种是保留这些行但不允许产生交易信号。我更倾向于第二种,因为前期虽然无法计算均线,但价格本身仍然是行情的一部分,在资金曲线里出现也没问题。

另外还要注意:数据里可能有停牌日导致成交量缺失,或者某些日期临时停牌没有K线。回测前先df['close'].isna().sum()检查一遍,有缺失就前向填充或直接丢弃,不要带着脏数据跑循环。

5.4 为什么我的结果和别人的对不上

如果你用同一只标的、同样的参数,跑出来的结果和论坛里别人的结果不一样,先别慌,大概率是这些原因:数据源不同(前复权还是后复权)、时间段不同(选2020年还是选2015年,难度完全不一样)、手续费和滑点设置不同、成交价设定不同(收盘价成交还是次日开盘价成交)。这些都是正常差异,只要自己的逻辑自洽,没必要盲目对齐别人。

6. 从回测走向实盘,还差哪些功

6.1 先做模拟盘,别急着投真钱

30分钟跑通回测以后,你很容易产生一种“我已经会做量化”的错觉。我当年也有过这种错觉,好在没急着上真金白银。回测模型再漂亮,也只是对历史数据的假设检验,它无法排除未来行情突变的风险。我的建议是至少模拟盘跑一个月,观察策略在真实行情中的表现是否和回测一致,再考虑用小资金实盘验证。

6.2 双均线策略的适用边界要清楚

双均线策略适合什么行情?答案很直接:趋势行情。单边上涨或者单边下跌,它能让你吃到大段利润。那震荡行情呢?它会反复金叉死叉,连续止损。所以做这个策略要有两个基本认知:一是标的要选有大趋势的品种,比如宽基指数、主流ETF,尽量不要用常年横盘的小票;二是在策略逻辑里加一个趋势过滤或波动率过滤,比如价格站上200日线才允许开仓,能在一定程度上避开震荡磨损。

6.3 给正在跑第一个策略的你一个建议

从我自己的经验来说,跑通双均线只是起点,最重要的是通过这个项目把“策略如何变成交易、交易如何变成收益曲线”这条链路彻底打通。别急着追求完美策略,先把手写回测的脚本整理好,把数据下载的流程固定下来,把每次验证的记录留档。量化的竞争力不在一两个神奇的指标,而在于你有一个可靠、可重复的实验环境。有了这个环境,未来任何策略想法都能快速被验证,那才是真正的效率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 5:30:31

SpringBoot实战:雪具销售系统从数据库设计到库存并发扣减

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 5:29:40

3个实战案例揭秘wordpress人力资源模板如何防挂马

3个实战案例揭秘wordpress人力资源模板如何防挂马 上周凌晨三点,一个做猎头公司的老板急得电话打爆了,说官网突然弹出一堆博彩广告,后台也登不进去。他问我:“网站被黑挂马不知道怎么办?我用了wordpress人力资源模板,是不是模板本身有问题?”…

作者头像 李华
网站建设 2026/9/15 5:29:06

AI购物助手如何改变消费习惯与应对策略

1. AI购物助手正在重塑消费习惯上周帮邻居王姐设置手机时,发现她的淘宝对话框里躺着十几条未读的AI推荐消息。"这个智能客服老给我推连衣裙,它怎么知道我刚瘦了八斤?"王姐的疑问道出了当下最有趣的消费现象——73%的消费者已经习惯…

作者头像 李华
网站建设 2026/9/15 5:27:39

系统设计笔记的本质是决策链路,不是知识搬运

1. 这不是笔记,是系统设计能力的显微镜“system-design-notes”这个标题乍看平平无奇,像极了GitHub上成千上万份被star又沉底的个人仓库名。但如果你真点进去翻过几十份标着“System Design Notes”的文档,就会发现一个残酷事实:9…

作者头像 李华
网站建设 2026/9/15 5:26:59

TC275 CAN UDS Bootloader开发实战:从位定时到HSM安全启动

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 5:26:26

百度地图商圈边界数据本地化:CityList与多边形绘制优化

简介:面向 JavaScript 开发者的城市行政区域与商圈数据获取工具类,基于百度地图 API 1.5,主要为房地产、本地服务、交通规划等需要精确地理信息的应用场景提供行政区边界与商圈几何数据支持。主入口类为 CityList,开发者通过实例化…

作者头像 李华