双均线策略从回测到实战:30分钟跑通你的第一个量化交易模型
2026/9/16 6:18:35 网站建设 项目流程

不管你是学金融的还是学计算机的,只要聊起量化交易,第一个被提到的策略大概率就是双均线。它的思路简单到我第一次看的时候有点怀疑:就这?可偏偏是这么个“就这”,能让我在一个小时内把完整回测、绩效指标一条龙跑出来,也让我第一次真正理解什么叫趋势跟踪、什么叫回撤、什么叫参数过拟合。这篇文章就把我踩过的坑和完整流程都摊开说,照着做,30分钟足够跑通你的第一个策略。

1. 双均线策略:为什么值得作为你的第一个策略

1.1 双均线到底在交易什么逻辑

双均线策略的核心逻辑一句话就能讲清楚:当短期均线从下往上穿过长期均线时买入,当短期均线从上往下跌破长期均线时卖出。前者叫“金叉”,后者叫“死叉”。快线代表近期持仓成本,慢线代表长期持仓成本,两者交叉意味着短期内市场买方的力量对比发生了方向性变化。

但别因为它简单就小看它。双均线本质上是趋势跟踪策略的元祖,它背后博弈的其实是市场惯性。价格一旦形成趋势,在情绪和资金的推动下,短期均线和长期均线的位置关系会保持一段时间,你顺着这个关系站队,吃到的就是趋势延续的利润。它不预测顶和底,只回答“现在该不该在这个方向上待着”这个问题。

我第一次实盘跑这个策略的时候,最吃惊的一点是:策略的盈利来源并不是每次都做对,而是做错的时候亏得少,做对的时候拿得住。均线本身的过滤作用会把一些杂乱的短期波动过滤掉,让你别天天手痒乱动。这一点放到今天的A股、期货、加密货币市场也一样成立。

1.2 选参数的本质是在权衡趋势和震荡

双均线的参数组合五花八门,最常见的像5日对20日、10日对30日、20日对60日。参数并不是随便拍的,它直接决定了策略对市场节奏的敏感度。

快慢线参数拉得越近,比如5日和10日,信号越灵敏,稍微有一点上涨苗头就能进场,但问题是震荡行情里你会被反复打脸。价格一反弹金叉进场,一回落死叉止损,来回几次,手续费和止损的亏损就能吃掉一大块本金。反过来,参数拉得越远,比如20日和60日,信号越迟钝,但一旦出现大趋势,你拿到的仓位质量更高,不会因为中途的洗盘被震下车。

所以选参数这件事,本质上是在做趋势和震荡之间的权衡。你要先想清楚自己要在什么样的市场环境里赚什么钱。如果你交易的标的一年到头都在窄幅箱体里晃,那短周期双均线很可能是给券商打工;如果标的长期有清晰的牛熊波动,慢一点的参数反而更稳。

1.3 双均线最大的短板

我必须把丑话说在前面:双均线策略最大的短板是它在震荡市里几乎必然连续亏损。趋势策略的宿命就是如此,用震荡期的小额亏损换取趋势期的超额收益,整体实现正期望。

很多人第一次跑回测,看到某个年份收益曲线一路向下就开始怀疑策略有问题,其实不是策略坏了,而是那一整年市场都在横盘。你要接受一个事实:双均线不是全天候策略,它只在有趋势的市场里赚钱。理解了这个,你才能客观看待回测结果,而不是一看到连续亏损就急着调参数。

这也是我强烈建议新手把双均线作为第一个策略的原因——它足够简单,简单到你能够彻底理解每一笔盈亏的来源,而不是把结果归因给运气。

2. 动手前5分钟:环境、数据与参数

2.1 30分钟内跑完的最小环境

我默认你电脑上已经装了Python 3,这个条件基本是量化交易的最低门槛了。如果你连Python都没装,那可以直接装一个Anaconda,装完自带Jupyter Notebook和pandas,省心很多。

需要安装的库就三个:pandas、numpy,还有用于数据获取的akshare。后两个库如果版本冲突,建议先升级一下pip再安装:

pip install pandas numpy akshare -U

这步正常情况下两三分钟搞定。数据获取我用的是akshare,它对国内行情支持很友好,不需要注册token,直接调接口就能拿到A股、指数、ETF的日线数据。如果你在香港或海外交易,也可以换成yfinance或者自己准备CSV文件,逻辑完全一样。

参数我先给一个默认组合:快线5日、慢线20日。这个组合在日线级别上属于中短周期,一年到头交易次数不至于太多,趋势响应速度也还算及时,适合拿来当基线跑通全流程。标的选择我建议用宽基指数或者流动性好的大票,别一上来就选一只日均成交额几千万的小票,后边你会被跳空和停牌问题折磨疯。

2.2 数据获取与清洗的三个关键细节

数据是整个回测的地基,地基歪了,后面全白搭。我分享三个会被新手忽略、却直接影响结果的细节。

第一个是复权。股票分红送股都会导致价格出现“断层”,如果直接拿不复权的收盘价算均线,金叉死叉的位置会被这些除权缺口干扰,出现大量虚假信号。我个人的习惯是统一用前复权数据,它能保证历史价格连续,贴近真实的可交易价格。

第二个是数据排序。很多接口返回的数据是按日期倒序的,你直接 rolling 算均线,等于把未来数据当成过去数据用了,出来的信号完全颠倒。所以拿到数据第一件事就是按日期升序排序,然后重置索引,这点代码里必须做好。

第三个是缺失值和停牌。停牌期间的成交量为0,价格可能维持在前一日的收盘价,这种假数据会让均线计算出现横线,进而导致信号失真。最简单的做法是把成交量为0或者收盘价为空的行直接删除,回测时默认这些天无法交易。

import pandas as pd import numpy as np import akshare as ak # 拉取沪深300指数日线数据,这里以510300 ETF为例,前复权 df = ak.fund_etf_hist_em(symbol="510300", period="daily", start_date="20180101", end_date="20241231", adjust="qfq") # 统一列名并清洗 df.rename(columns={"日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume"}, inplace=True) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) df = df[df["volume"] > 0].reset_index(drop=True)

这段代码里最关键的是adjust="qfq"参数,它直接帮你完成了前复权处理。数据清洗之后,你可以顺手打印一下头尾几行,确认时间范围和价格连续性,没问题再往下走。

3. 核心实现:信号生成、回测与绩效

3.1 用 pandas 向量化生成双均线信号

终于到了写策略代码的环节。双均线策略用pandas实现非常干净,核心就四步:算均线、定信号、映射持仓、计算收益。

# 计算快慢均线 df["ma_fast"] = df["close"].rolling(window=5).mean() df["ma_slow"] = df["close"].rolling(window=20).mean() # 生成原始信号:快线在慢线上方=1,否则=0 df["signal"] = np.where(df["ma_fast"] > df["ma_slow"], 1, 0)

这一段生成的signal是逐日的状态,1代表该持有,0代表该空仓。但这里有个关键点:signal是用当日收盘价计算的,而收盘价是收盘后才确定的,真实交易中你不可能在收盘的那一瞬间以收盘价成交。所以信号必须往后推一天,也就是第二天开盘再执行。

这就是我能告诉你的最影响回测真实性的一个细节:信号延迟。不少新手直接把signal当持仓直接算收益,等于每个交易日收盘那一刻精准穿越到第二天开盘买入,这在现实中根本做不到,回测收益会被显著高估。

# 持仓状态:信号后移一天,模拟次日开盘成交 df["position"] = df["signal"].shift(1).fillna(0)

完成这一步,position里就记录了每一日实际应该持有的仓位状态。今天收盘信号变成1,明天position才变成1,也就是明天整个交易日都处于持仓状态,后天拿到明天收益,逻辑闭环。

3.2 回测引擎搭建与仓位管理逻辑

有了持仓状态,回测就顺理成章了。策略的每日收益等于持仓状态乘以当日标的收益率,注意收益要用百分比收益率而不是绝对价差,否则净值曲线会失真。

# 计算每日收益率 df["ret"] = df["close"].pct_change().fillna(0) # 策略每日收益 = 持仓状态 * 当日标的收益 df["strategy_ret"] = df["position"] * df["ret"] # 计算策略净值曲线 df["nav"] = (1 + df["strategy_ret"]).cumprod()

那么交易成本怎么加?我见过很多人在这个环节偷懒,直接忽略手续费和滑点。实际上,手续费对双均线这种中低频策略的影响可能没那么致命,但滑点和印花税会真实地吃掉利润,尤其在你频繁开平仓的时候。

A股的交易成本大致可以这样估算:买入时佣金按万2.5,卖出时佣金万2.5加上印花税千1,另外还有少量过户费。我们简化处理,按每次换手(仓位从0变1或从1变0)扣除0.15%的成本,再把滑点按0.05%计入,总共每笔换手扣0.2%。

# 计算换手:仓位每次变化就代表一次买入或卖出 df["turnover"] = df["position"].diff().abs().fillna(0) # 单边成本+滑点,粗略按0.002计 cost = 0.002 df["strategy_ret_net"] = df["position"] * df["ret"] - df["turnover"] * cost # 扣费后的净值 df["nav_net"] = (1 + df["strategy_ret_net"]).cumprod()

这个写法的含义是:平时持仓的日子只承担市场波动,不扣额外费用;只有在仓位改变的那一天,也就是真实买卖发生的那一天,才扣除一笔交易成本。三个月不开仓就没有多余成本,跟现实交易的情况是吻合的。

3.3 绩效指标计算:不能只看总收益率

跑完回测之后,很多人第一眼就去看总收益率,发现策略涨了80%就打算直接上实盘。别急,这个判断方式太粗糙了。真正衡量一个策略好坏的指标至少看四个:年化收益率、最大回撤、夏普比率、胜率或盈亏比。

最大回撤指的是净值从最高点跌到之后最低点的幅度,它直接反映你实盘时可能要承受的心理压力。一个年化50%但最大回撤40%的策略,实操中大部分人拿不住,因为没等它创新高你就已经割肉了。

夏普比率衡量的是每承受一单位风险能换来多少超额收益,粗略算法就是策略年化收益率除以收益波动率。对散户来说,夏普大于1就挺不错,超过1.5已经算优秀。

我写了个简化版的计算代码:

# 总收益率 total_return = df["nav_net"].iloc[-1] - 1 # 年化收益率:按252个交易日计算 days = len(df) annual_return = (df["nav_net"].iloc[-1]) ** (252 / days) - 1 # 最大回撤 drawdown = df["nav_net"] / df["nav_net"].cummax() - 1 max_drawdown = drawdown.min() # 夏普比率(简化:无风险利率假设为0) daily_ret = df["strategy_ret_net"].dropna() sharpe = daily_ret.mean() / daily_ret.std() * (252 ** 0.5) # 交易次数和胜率 trade_count = df["turnover"].sum() profit_days = daily_ret[daily_ret > 0].sum() loss_days = daily_ret[daily_ret < 0].sum()

打印这几个数字,你对策略的“健康度”就有概念了。我特意强调这一点,是想说:没有回撤信息的收益率都是耍流氓。很多新手第一版策略看着收益亮眼,一算最大回撤直接崩溃,这时候反而应该庆幸,因为花在回测上的时间帮你避免了真金白银的教训。

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

4.1 未来函数:看起来赚钱的元凶

双均线策略跑出来的收益如果异常高,我第一个怀疑的就是未来函数。未来函数的意思是,你的回测代码里偷偷用了未来才可能出现的信息,导致“未卜先知”。这是回测中最高频、也最隐蔽的错误。

最常见的错误写法是这样的:用当天的收盘价计算均线交叉信号,然后当天的收益也按当天的信号来算。等于你认为自己能在收盘那一刻完美买卖,这根本不现实。我们前面用shift(1)做的信号延迟就是为了解决这个问题。另外还有一种更隐蔽的情况:你在计算某一个指标时,不小心用了整个数据集的信息,比如用全样本的均值去做标准化,这等于让模型偷看了未来。

排查未来函数我有一个土办法:把策略的每日收益曲线拎出来,看看有没有某一天涨幅特别夸张,比如单日超过10%甚至20%。正常来说,日线级别的双均线策略每天能赚一个涨跌幅就已经很了不起了,如果出现单日暴涨,几乎可以断定是信号和成交使用了同一天的收盘价。把那一天找出来,往前倒推,基本就能定位到代码里的问题。

4.2 复权与涨跌停:数据细节决定策略命运

如果说未来函数是策略逻辑的硬伤,那么数据细节就是回测的软刀子。复权和涨跌停处理不好,回测结果会跟实盘差出一大截。

先说复权问题。如果你用不复权数据回测一只长期高分红的股票,那么每次除权除息,价格都会向下跳空,均线会被莫名其妙地拉下来,产生一个虚假的“死叉”,策略可能就会因此卖出。用后复权数据会把历史价格调整得非常大,收益率曲线倒是准确,但显示价格时很别扭。前复权是最符合直观认知的选择,它让当前价格保持真实,历史价格向前调整,虽然历史区间长的时候会有轻微失真,但做日线级别的均线策略完全够用。

再说涨跌停的问题。双均线的金叉往往会出现在股价已经有一定涨幅之后,这时候如果标的直接一字涨停,你根本买不进去;同样,死叉后如果连续跌停,你也卖不掉。回测代码里如果不加约束,系统会默认你每天都能按收盘价成交,这等于在回测里偷偷加了杠杆。主流回测框架里可以加涨跌停过滤,比如收盘价触及涨停且封死的情况下,不执行买入信号;触及跌停且封死的情况下,不执行卖出信号。A股主板和ETF的涨跌幅限制不同,回测脚本里要按标的类型区分。

4.3 交易成本与滑点:回测利润消失术

我见过太多人在这一步翻车。同一个双均线策略,忽略交易成本时年化收益可能做到20%,加入0.2%的单边成本后直接变成12%,如果再考虑实际滑点,可能只剩8%。这就是为什么我强烈建议,从一开始就在回测里把成本算进去。

滑点这件事尤其容易被低估。你在回测里假设以收盘价买入,但真实环境下,当你看到均线交叉信号时,市场里和你一样动作的人有很多,冲击成本会推动成交价偏离你预期的价格。尤其在小盘股上,滑点可能比佣金还贵。保守一点,单边滑点按0.05%到0.1%估算,如果你做的还是分钟级别的高频策略,这个比例还得往上调。

我想再提醒一点:双均线策略的换手频率并不高,按5日对20日的参数,一年可能也就交易二十几次,交易成本对整个收益的侵蚀不算特别大。但如果你的参数改成5日对10日,交易次数翻倍,成本的影响就会显著放大。这也是为什么参数选择会间接影响净收益,因为交易成本对高频信号的惩罚是成倍增加的。

4.4 参数敏感性:过拟合悬崖

跑通基线策略之后,大部分人的第一反应是优化参数。把5日改成6日,20日改成22日,观察收益率是不是变得更好,这种思路本身没问题,但千万别滑入过拟合的陷阱。

过拟合最典型的症状是:某个参数组合的回测结果异常好,但稍微改一个参数,收益就断崖式下跌。比如20日对60日年化收益15%,改成19日对60日就变成负的5%,这说明你的“最佳参数”是在捕捉噪音而不是市场规律。真正有效的策略应该在参数附近的邻域内表现平滑,比如20日到25日之间的慢线参数,回测结果应该差异不大,而不是呈锯齿状剧烈波动。

排查过拟合我建议画出参数热力图。把快线参数从5变到30,慢线参数从20变到60,每个组合都跑一遍回测,记录年化收益或夏普比率。如果地图上是一整片连续抬升或连续下降的区域,那说明策略有稳定的逻辑基础;如果只有某一个孤立的小格子特别亮,其他全是黑暗,那这个亮点大概率是噪声。这类排查虽然要花几分钟去循环跑回测,但比起将来用真金白银在实盘里试错,这点时间成本低得可以忽略。

5. 跑通之后,还能往哪里走

先给你一个提醒:回测效果不错,不代表直接上实盘也能赚钱,但你已经比绝大多数没跑过回测的人强了。接下来如果想让这套双均线策略真正可用,我的建议是沿着三个方向去折腾。

第一个方向是加过滤器。裸的双均线策略在震荡市会反复亏损,既然知道它的痛点,那就想办法避开震荡市。比如加一条更长周期的均线作为大趋势过滤器,只在价格位于长周期均线上方时才允许做多,这样能过滤掉一部分空头趋势里的假金叉。再比如加一个波动率过滤,当市场波动太小的时候不交易,因为低波动往往意味着趋势还没启动。

第二个方向是改离场逻辑。双均线的交叉出场虽然简单,但滞后性很强,往往把利润回吐大半才发出离场信号。你可以试试用移动止损替代交叉出场,比如从最高点回撤8%就离场,这样在趋势反转早期就能锁定利润,代价是可能会被短暂的洗盘震出去。每种离场方式都有取舍,你把它们组合起来反复测试才能找到适合自己风险偏好的版本。

第三个方向是换标的和周期。双均线策略在日线上的表现和在小周期上完全不同,因为小周期上的噪音更多,信号更难稳定。同一套参数在沪深300上有效,不代表在全市场所有股票上都有效,这就是所谓的“策略失效边界”。我在跑完基线之后会习惯性地把同一套代码往三五个不同标的上跑一遍,看看策略的收益分布是不是靠运气。如果只有一只票赚钱,其他全亏,那这策略离实盘还很远;如果在多个标的上都有稳定表现,你对它的信心会完全不同。

我个人做了这么多年回测的一个深刻体会是:任何一个策略,在实盘之前都只是假说。双均线的价值不光是让你赚到钱,更是让你以最快速度理解量化交易中最重要的几个概念——信号、延迟、成本、回撤、过拟合。想清楚这些,你再去看那些更复杂的策略,会发现它们不过是把双均线的某个环节做得更精细而已。先把这条线跑通,后面的大楼都有地基。

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

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

立即咨询