Python双均线策略回测实战:从零搭建量化交易入门模型
2026/9/16 6:58:30 网站建设 项目流程

1. 先搞明白,双均线策略为什么能当“第一个策略”

很多人学量化,最容易犯的错误就是一上来就整一堆机器学习、LSTM、多因子模型,结果数据还没跑通就劝退了。我一直觉得,双均线策略是普通人接触量化最适合的入门起点,没有之一。原因很简单:它够简单、够直观、逻辑完全透明,你甚至不需要懂高深的数学,就能在半小时内写出一份能跑、能看的回测结果。

双均线策略的核心逻辑,用一句话讲就是:当短期均线从下方穿越长期均线时,说明短期价格重心已经超过长期成本重心,趋势大概率向上,买入;反过来,短期均线从上方下穿长期均线,说明动能转弱,卖出或做空。

均线本质上是“在某个时间窗口内收盘价的平均值”。5日均线代表过去5天的平均持仓成本,20日均线代表过去20天的平均持仓成本。它们的交叉,本质上是在比较“最近一小波人愿意花多少钱买”和“过去一大波人花了多少钱买”。短期均线高于长期均线,意味着新进场的资金愿意出更高的价格,市场情绪是偏乐观的。

这个策略顺应了“动量效应”——资产价格在短期内往往会延续原有趋势。不是所有人都认可这一点,但它确实是目前市场上大量趋势类策略的底层逻辑。即便你以后去学更复杂的策略,你会发现很多模型里依然带着均线的影子,甚至CTA领域很多经典策略就是均线体系的变种。

我要强调一下,说它适合当第一个策略,还有一个重要原因:它能让你的回测结果“看起来合理”。如果你一开始就搞高频或复杂的统计套利,回测出来的权益曲线可能是灾难性的,新手很容易被劝退;而双均线在大多数指数或股票上,至少能跑出一个还算平稳的净值曲线,这对建立信心非常重要。

2. 跑策略之前,环境和数据怎么准备

2.1 你需要的东西其实就三样

在动手写代码之前,先把工具链理清楚。其实跑一个简单的双均线回测,并不需要多专业的环境,你的电脑上只要有Python、pandas和一份历史数据就够了。

具体来说,我建议的环境配置如下:

组件方案说明
Python3.9+,Anaconda安装Anaconda自带Jupyter,适合逐段调试
数据处理pandas、numpy计算均线、生成信号、算收益都靠它们
数据源akshare或tushare都是免费开源库,akshare无需注册即可用
可视化matplotlib画净值曲线和信号图,直观看出策略行为
回测框架暂时不需要30分钟快速跑通用自写循环就够了

不少教程一上来就推荐backtrader,我不反对,但第一遍跑通时真没必要碰框架。框架有学习成本,你需要理解它的杠杆、订单、滑点、佣金模型等概念,半小时根本不够用。用pandas做向量化回测,几行代码就能算出结果,先让自己看到“策略能跑出什么”,再谈工程化。

2.2 数据从哪里取,取哪种数据

数据这块我用的是akshare,原因很现实:免费、开放、不需要Token,pip install akshare就能直接用。取沪深300指数日线数据的话,一行代码就搞定:

import akshare as ak df = ak.stock_zh_index_daily(symbol="sh000300")

这样拿到的DataFrame里至少有date、open、high、low、close、volume这六列,做日频双均线策略完全够用。

如果你用的是tushare,需要先注册获取token,但也只是多几步而已。唯一需要注意的就是统一复权因子。做策略回测时,如果涉及个股,除权除息会导致价格跳空,均线计算就会失真。指数本身不存在这个问题,所以初学者用指数练手最省心。

还有一点需要提醒:别用你从行情软件里导出、时间不连续的数据。停牌日、未上市日的数据缺失会导致rolling窗口计算错误,均线数值很可能出现巨大偏差。索引数据还好一点,个股尤其容易踩坑。所以我的习惯是:做回测前,先检查数据的时间索引是否连续、有没有NaN值,再开始计算。

2.3 为什么用日线而不是分钟线

双均线策略可以用在任意周期的K线上,但作为新手第一个回测,我强烈建议用日线。原因有两方面:

第一,日线数据量小、回测速度快。一份指数十年日线数据也就两千多行,你用pandas的向量化计算几乎是毫秒级返回结果。如果用分钟线,光数据清洗就要花不少时间,而且分钟线里噪声更多,信号频繁,回测结果更容易受手续费影响。

第二,双均线本身就是趋势型策略,天然适合在日线级别的波段上做文章。分钟级别上双均线的参数需要重新调优,而且高频噪声会让金叉死叉变得过于频繁,最后全是手续费。

记住一句话:策略越短周期,对交易成本和执行精度的要求越高,越不适合新手练手。先把日线跑通,你会发现很多逻辑上的坑,再去玩短线也不迟。

3. 30分钟实战,从零写出可用的双均线回测

3.1 前5分钟:先明确你的策略规则

动手写代码之前,先把规则写清楚。这一段看起来“不产生代码”,但实际上最耗脑力,也最影响结果质量。

我的第一版双均线规则如下:

  • 标的:沪深300指数(000300.SH)
  • 时间窗口:2015年至今,跨越牛熊,有代表性
  • 快线:SMA 20日
  • 慢线:SMA 60日
  • 开仓条件:SMA20上穿SMA60,次日开盘买入(做多)
  • 平仓条件:SMA20下穿SMA60,次日开盘卖出
  • 仓位:全仓进出,不加杠杆
  • 成本:双边万三佣金 + 千一印花税(卖出时),大概按单边0.1%算

你可能注意到,我上面写的是“次日开盘买入”,而不是“当日收盘买入”。这里存在一个关键问题:你的信号是基于当日收盘价算出来的,理论上要到收盘后才知道今天金叉了,所以实际能成交的最早时间点是次日开盘。一旦你在金叉当日就按收盘价买入,你使用的是未来数据,这在回测里叫前视偏差,会让结果虚高。

我见过不少新手回测结果特别好,一上实盘就亏损,前视偏差是最常见的原因之一。

3.2 第6到15分钟:核心代码就这么几行

先明确一下,用pandas写向量化回测,本质上就是:算均线、生成信号、计算持仓收益、汇总绩效。我直接贴代码,每一段后面都有解释。

import pandas as pd import numpy as np import akshare as ak # 1. 数据准备 df = ak.stock_zh_index_daily(symbol="sh000300") df['date'] = pd.to_datetime(df['date']) df = df.set_index('date') df = df[df.index >= '2015-01-01'] df['close'] = df['close'].astype(float) # 2. 计算均线 df['sma_short'] = df['close'].rolling(window=20).mean() df['sma_long'] = df['close'].rolling(window=60).mean() # 3. 生成持仓信号 # signal=1表示持有,signal=0表示空仓 # 注意这行的操作逻辑:用shift(1)把信号往后挪一天,避免未来函数 df['signal'] = 0 df.loc[df['sma_short'] > df['sma_long'], 'signal'] = 1 df['position'] = df['signal'].shift(1).fillna(0) # 4. 计算策略每日收益 df['market_ret'] = df['close'].pct_change() df['strategy_ret'] = df['position'] * df['market_ret'] # 5. 计算累计净值 df['strategy_nav'] = (1 + df['strategy_ret']).cumprod() df['market_nav'] = (1 + df['market_ret']).cumprod()

这段代码里,最有含金量的一行是df['position'] = df['signal'].shift(1).fillna(0)。它保证了信号在“下一日”才生效,这就是我们通常所说的“信号延迟一天,规避未来函数”。你可以在代码里去掉shift,跑一遍看看,净值曲线会漂亮不少,但那不是真实可交易的结果。

另外,在生成signal的时候,直接判断df['sma_short'] > df['sma_long']本身就是逐日比较,在均线纠缠时可能频繁在0和1之间切换。对趋势策略来说,这会导致不必要的换手。后面可以加一个缓冲规则,但第一版先跑通再说,不要过早优化。

3.3 第16到20分钟:看一眼策略到底赚不赚钱

有了净值序列,接下来就要算绩效指标了。至少要算年化收益、最大回撤、夏普比率、胜率这四个维度,否则你根本判断不了策略好坏。

# 核心绩效指标计算 total_return = df['strategy_nav'].iloc[-1] - 1 years = len(df) / 252 annual_return = (1 + total_return) ** (1 / years) - 1 # 最大回撤 rolling_max = df['strategy_nav'].cummax() drawdown = df['strategy_nav'] / rolling_max - 1 max_drawdown = drawdown.min() # 夏普比率(年化) daily_vol = df['strategy_ret'].std() sharpe = (df['strategy_ret'].mean() / daily_vol) * (252 ** 0.5) print(f"累计收益率: {total_return:.2%}") print(f"年化收益率: {annual_return:.2%}") print(f"最大回撤: {max_drawdown:.2%}") print(f"夏普比率: {sharpe:.2f}")

我实测在2015年初到2023年末这段沪深300上,20/60双均线大概能跑出年化收益8%-10%左右,最大回撤在20%-30%之间。单看数字平平无奇,但你拿它和同期沪深300指数本身比较,就会发现策略在熊市阶段明显控制了回撤,这也解释了为什么双均线能长期存活在市场中。

这里有一个新手经常不理解的概念,叫最大回撤。简单说,就是你从净值最高点到最低点的最大亏损幅度。假设你100万进场,涨到150万,后来跌到100万,中间那个50万的亏损就是最大回撤的体现。它决定了你实盘时心理承受的极限,所以选策略不能只看收益,一定要看回撤。

3.4 第21到30分钟:快速调参跑第二轮

第一版跑通之后,你肯定会手痒想调参数。就以快线和慢线为例,常见组合有5/20、10/30、20/60、10/60等。与其凭感觉猜,不如用一个双层循环,把参数组合全部跑一遍:

def backtest_double_sma(df, short_win, long_win): df = df.copy() df['sma_short'] = df['close'].rolling(short_win).mean() df['sma_long'] = df['close'].rolling(long_win).mean() df['signal'] = 0 df.loc[df['sma_short'] > df['sma_long'], 'signal'] = 1 df['position'] = df['signal'].shift(1).fillna(0) df['strategy_ret'] = df['position'] * df['close'].pct_change() nav = (1 + df['strategy_ret']).cumprod() return nav.iloc[-1] - 1 results = [] for short_win in [5, 10, 20, 30]: for long_win in [20, 40, 60, 120]: if short_win >= long_win: continue ret = backtest_double_sma(df, short_win, long_win) results.append((short_win, long_win, ret)) results.sort(key=lambda x: x[2], reverse=True) for r in results[:10]: print(f"快线{r[0]} 慢线{r[1]} 累计收益{r[2]:.2%}")

跑完之后你会发现,不同的参数组合收益差异很大。有的组合在牛市里能翻倍,但在震荡市里反复打脸;有的组合虽然收益不高,但回撤控制得很好。所以调参的时候,我建议你同时看多个指标,别死盯着总收益一个数。收益高、回撤小的组合,才是真正值得留意的

如果你有精力,还可以把“信号延迟一天”改成“信号当日收盘成交”,对比一下结果差距。你会发现延迟一天的影响在某些参数下非常明显。这个对比过程对理解交易滑点和执行时机的意义,比任何理论讲解都管用。

4. 跑通之后,这些坑一定要提前避开

4.1 未来函数和前视偏差,是回测第一大杀手

前面提到过shift(1),这里再展开一次。我在给别人做代码审查时,最常见的问题就是未来函数,表现形式多种多样:

  • 用当日收盘价判断信号,却用当日收盘价成交(其实只有用次日开盘价才合理)
  • 对全量数据做归一化、zscore后,再切分训练集和测试集(相当于用了未来统计量)
  • 在rolling窗口里不小心包含了未来的数据(比如应差1天)

避免前视偏差有一个笨办法:你回测时假设自己只有截至“当天”的数据,凡是基于“未来”信息的操作都是作弊。这个原则贯穿所有回测,不只是双均线策略。

4.2 手续费和滑点,设多少才合理

很多新手跑回测时不设手续费,结果策略收益高得吓人,一上实盘就亏钱。以A股为例,佣金双边万三到万五,卖出印花税千一,再加上冲击成本,单边总体0.1%-0.2%是一个相对合理的估算。

双均线策略换手率不算高,20/60组合一年可能就交易十几次,手续费影响还小一些;但如果你把参数调成5/20,交易次数可能翻好几倍,手续费对最终收益的影响一下就会凸显出来。所以参数调优时一定要把成本加上

4.3 参数过拟合,别把回测做成玄学

调参的时候很容易掉进“优化陷阱”——你手动或者用循环把能跑的组合全跑一遍,挑出收益最高的一组,然后兴奋地在社区晒图。问题是,这组参数在历史上表现好,不代表未来也会好。参数越多、调得越精细,过拟合的风险越高。

怎么降低过拟合风险?我给你几个实操经验:

  • 在训练集(比如2015-2019年)和测试集(比如2020-2023年)上分别回测,看看收益是否都还行
  • 不要只看最优参数,看相邻参数组合的整体表现
  • 参数少一点、逻辑简单一点,往往比复杂模型更抗造

双均线只有快线和慢线两个参数,本身抗过拟合能力已经算不错了。你要做的就是别把参数范围划得过于奇葩,守住“快线比慢线短”这个符合直觉的前提。

4.4 震荡行情下反复止损是趋势策略的宿命

双均线策略最大的痛点是震荡行情。价格在一段区间里来回波动,均线频繁金叉死叉,每一次交叉都带来一次亏损。这时候你会怀疑策略是不是失效了,甚至会忍不住手动干预。

我的经验是:不要因为短期回撤就随便改参数或停掉策略。趋势策略的本质就是“用多次小亏换取一次大赚”,你要看的是长期的期望收益,而不是单月的表现。当然,如果策略在连续两年里持续跑输基准,那就应该回头检查逻辑,而不是盲目坚持。

5. 从跑通到进阶,双均线策略还能玩出什么花样

5.1 加一个趋势过滤器

一个非常简单但有效的改进,是在双均线策略之上加一个长周期的方向过滤:比如只有价格在200日均线上方才允许做多,价格在200日均线下方时保持空仓。这样做的好处是,减少震荡行情中的无效交易,把做多信号限制在更大的上升趋势里。

实现起来就多一行判断:df['filter'] = np.where(df['close'] > df['sma_200'], 1, 0),然后把signalfilter相乘。别小看这一行,它能过滤掉很多“大熊市里的抢反弹”,大幅降低最大回撤。

5.2 从全仓进出到分批加减仓

全仓进出是新手阶段最粗暴的仓位管理方式,它的问题在于:金叉出现时可能刚好是阶段高点,全仓买入后马上吃一波回调。进阶一点的做法是分批建仓,或者根据均线斜率和波动率来动态调整仓位。

比如你可以设计一个简单的仓位公式:position_size = base_size * (sma_short / sma_long - 1)。均线距离越远,说明趋势越强,仓位可以越重;均线黏合时仓位自动降低。这种思路叫“移动平均差值加权”,本质上是把信号的连续强度纳入仓位管理。

5.3 把双均线从主策略变成风控工具

很多人玩到最后会发现,双均线单一策略的上限就在那里了——它不可能抓住所有行情,也不可能完全规避所有回撤。但双均线作为风控工具非常实用。

比如你有一个选股策略,原本是每周调仓、买入一篮子股票。你可以额外设一条“市场级别均线”(比如指数站上20周均线才允许开新仓),这样在大盘趋势向下的阶段,选股策略会自动降仓或空仓。从整体组合的角度看,这比单独优化任何一个策略都更有效,也更贴近实际操作中基金经理的做法。

最后说点实在的

双均线策略在量化圈里常被调侃为“最老土”的策略,但我一直觉得,老土不等于没用。任何一个策略能长期在市场里被反复提及,一定是因为它捕获了某种真实的、可重复的市场行为特征。对新手来说,它是最好的第一课,我能看到从数据到信号、从回测到实盘验证,整个量化流程在这一个策略里全部过了一遍。

30分钟跑通只是第一步,后面更重要的是你对这个过程的熟悉、理解和怀疑。调一次参数,记录一次结果,复盘一次亏损,你会慢慢明白,量化交易里真正值钱的不是那几行代码,而是你对市场、对风险、对规则的理解。希望这篇分享能帮你的第一个策略跑得顺利,也少踩一些我当初踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询