Python双均线策略回测实战:从零搭建量化交易入门模型 1. 先搞明白双均线策略为什么能当“第一个策略”很多人学量化最容易犯的错误就是一上来就整一堆机器学习、LSTM、多因子模型结果数据还没跑通就劝退了。我一直觉得双均线策略是普通人接触量化最适合的入门起点没有之一。原因很简单它够简单、够直观、逻辑完全透明你甚至不需要懂高深的数学就能在半小时内写出一份能跑、能看的回测结果。双均线策略的核心逻辑用一句话讲就是当短期均线从下方穿越长期均线时说明短期价格重心已经超过长期成本重心趋势大概率向上买入反过来短期均线从上方下穿长期均线说明动能转弱卖出或做空。均线本质上是“在某个时间窗口内收盘价的平均值”。5日均线代表过去5天的平均持仓成本20日均线代表过去20天的平均持仓成本。它们的交叉本质上是在比较“最近一小波人愿意花多少钱买”和“过去一大波人花了多少钱买”。短期均线高于长期均线意味着新进场的资金愿意出更高的价格市场情绪是偏乐观的。这个策略顺应了“动量效应”——资产价格在短期内往往会延续原有趋势。不是所有人都认可这一点但它确实是目前市场上大量趋势类策略的底层逻辑。即便你以后去学更复杂的策略你会发现很多模型里依然带着均线的影子甚至CTA领域很多经典策略就是均线体系的变种。我要强调一下说它适合当第一个策略还有一个重要原因它能让你的回测结果“看起来合理”。如果你一开始就搞高频或复杂的统计套利回测出来的权益曲线可能是灾难性的新手很容易被劝退而双均线在大多数指数或股票上至少能跑出一个还算平稳的净值曲线这对建立信心非常重要。2. 跑策略之前环境和数据怎么准备2.1 你需要的东西其实就三样在动手写代码之前先把工具链理清楚。其实跑一个简单的双均线回测并不需要多专业的环境你的电脑上只要有Python、pandas和一份历史数据就够了。具体来说我建议的环境配置如下组件方案说明Python3.9Anaconda安装Anaconda自带Jupyter适合逐段调试数据处理pandas、numpy计算均线、生成信号、算收益都靠它们数据源akshare或tushare都是免费开源库akshare无需注册即可用可视化matplotlib画净值曲线和信号图直观看出策略行为回测框架暂时不需要30分钟快速跑通用自写循环就够了不少教程一上来就推荐backtrader我不反对但第一遍跑通时真没必要碰框架。框架有学习成本你需要理解它的杠杆、订单、滑点、佣金模型等概念半小时根本不够用。用pandas做向量化回测几行代码就能算出结果先让自己看到“策略能跑出什么”再谈工程化。2.2 数据从哪里取取哪种数据数据这块我用的是akshare原因很现实免费、开放、不需要Tokenpip install akshare就能直接用。取沪深300指数日线数据的话一行代码就搞定import akshare as ak df ak.stock_zh_index_daily(symbolsh000300)这样拿到的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(symbolsh000300) 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(window20).mean() df[sma_long] df[close].rolling(window60).mean() # 3. 生成持仓信号 # signal1表示持有signal0表示空仓 # 注意这行的操作逻辑用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(keylambda x: x[2], reverseTrue) 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)然后把signal和filter相乘。别小看这一行它能过滤掉很多“大熊市里的抢反弹”大幅降低最大回撤。5.2 从全仓进出到分批加减仓全仓进出是新手阶段最粗暴的仓位管理方式它的问题在于金叉出现时可能刚好是阶段高点全仓买入后马上吃一波回调。进阶一点的做法是分批建仓或者根据均线斜率和波动率来动态调整仓位。比如你可以设计一个简单的仓位公式position_size base_size * (sma_short / sma_long - 1)。均线距离越远说明趋势越强仓位可以越重均线黏合时仓位自动降低。这种思路叫“移动平均差值加权”本质上是把信号的连续强度纳入仓位管理。5.3 把双均线从主策略变成风控工具很多人玩到最后会发现双均线单一策略的上限就在那里了——它不可能抓住所有行情也不可能完全规避所有回撤。但双均线作为风控工具非常实用。比如你有一个选股策略原本是每周调仓、买入一篮子股票。你可以额外设一条“市场级别均线”比如指数站上20周均线才允许开新仓这样在大盘趋势向下的阶段选股策略会自动降仓或空仓。从整体组合的角度看这比单独优化任何一个策略都更有效也更贴近实际操作中基金经理的做法。最后说点实在的双均线策略在量化圈里常被调侃为“最老土”的策略但我一直觉得老土不等于没用。任何一个策略能长期在市场里被反复提及一定是因为它捕获了某种真实的、可重复的市场行为特征。对新手来说它是最好的第一课我能看到从数据到信号、从回测到实盘验证整个量化流程在这一个策略里全部过了一遍。30分钟跑通只是第一步后面更重要的是你对这个过程的熟悉、理解和怀疑。调一次参数记录一次结果复盘一次亏损你会慢慢明白量化交易里真正值钱的不是那几行代码而是你对市场、对风险、对规则的理解。希望这篇分享能帮你的第一个策略跑得顺利也少踩一些我当初踩过的坑。