回测策略参数网格搜索:MACD+SMA 参数空间的最优解在哪
本文最后更新于 2026年8月12日 凌晨
MACD 金叉买入、SMA 趋势确认,这是最经典的两个技术指标组合。教科书告诉你”参数用 12/26/9 和 5/20”,但从来没告诉你——这些数字是随便选的,换个市场、换个标的,它们大概率不是最优的。
我做了一件事:把 MACD 的快慢线周期和 SMA 的窗口长度展开成一个参数网格,每个格点跑一遍回测,看收益分布长什么样。结果很反直觉——最优参数不在教科书的默认值附近,收益最高的区域和收益最低的区域只差两个格点,中间隔着一道”收益悬崖”。
这篇文章拆的就是这次网格搜索的全过程:参数空间怎么设计、回测引擎怎么跑、结果怎么读、过拟合怎么防。所有数据来自真实代码和回测结果,不是编的。
前置条件
复现这套网格搜索,你需要的环境:
- AutoQuant:自建量化交易平台(FastAPI + React,端口 8100),提供策略调度和交易执行 API
- ReShare:A 股数据平台,
stock_data表存全量 K 线(OHLC),sqlite3直读 - Python 3.10+,核心依赖只有
sqlite3、pandas、numpy,不需要装回测框架
不装框架的原因后面讲。
网格搜索要跑几百到上千次回测,框架的每次调用开销会变成瓶颈。
整体数据流:
1 | |
参数空间怎么设计
网格搜索的第一步不是写代码,是想清楚”格子”怎么画。画太大跑不完,画太小没信息量。
MACD 的三个旋钮
MACD(Moving Average Convergence Divergence)有三个参数:
- fast_period(快线 EMA 周期):默认 12
- slow_period(慢线 EMA 周期):默认 26
- signal_period(信号线 EMA 周期):默认 9
金叉信号:MACD 线(快慢 EMA 之差)上穿信号线。
SMA 的一个旋钮
SMA(Simple Moving Average)只有一个参数——窗口长度 window,默认常取 5 或 20。趋势确认:收盘价在 SMA 之上才算多头。
网格范围的选择
我这样定的网格范围:
| 参数 | 下限 | 上限 | 步长 | 取值数 |
|---|---|---|---|---|
| fast_period | 5 | 20 | 1 | 16 |
| slow_period | 15 | 40 | 1 | 26 |
| signal_period | 5 | 15 | 1 | 11 |
| sma_window | 5 | 60 | 5 | 12 |
约束:slow_period > fast_period(否则 MACD 没意义)。去掉非法组合后,总格点数约 16 × 26 × 11 × 12 ≈ 54912,但约束砍掉一半,实际约 27000 个格点。
💡 步长不是越小越好。MACD 周期从 12 变到 13,结果几乎没差别;从 12 跳到 15,才有信息量。步长定 1 是为了画热力图好看,实操中步长 3-5 就够定位最优区。
回测引擎:为什么自己写
这 27000 个格点,每个都要跑一遍完整回测。如果用 BackTrader、VectorBT 这类框架,单次回测有初始化开销,27000 次累积下来要跑几小时。
我写了一个纯向量化回测引擎,核心思路是把每个格点的策略信号预先算成布尔数组,再用 numpy 做矩阵运算。单格点回测压到毫秒级,全网格 40 秒跑完。
核心逻辑(grid_search.py):
1 | |
信号去重那段用循环是因为状态依赖,无法纯向量化——这是整个引擎里唯一的循环,也是最踩坑的地方(下一节的”坑二”会讲)。
止损 -5% / 止盈 +10% / 最长持有 60 天,和 QFII 跟庄策略保持一致,便于横向对比。
结果:最优解不在教科书附近
跑完全网格,我把结果画成热力图(固定 signal=9,横轴 fast、纵轴 slow,颜色代表期望收益)。
收益分布的三个特征
第一,教科书默认值(12/26/9)不是最优。 在我测试的这只标的(A 股某消费股,2024-2026 日线)上,默认参数的期望收益是 +0.8%,而网格里最高点是 +3.2%,差了 4 倍。
第二,存在”收益悬崖”。 最高收益区(fast=8, slow=22)和相邻的低收益区(fast=8, slow=20)只差两个 slow 单位,期望收益从 +3.2% 掉到 +0.5%。这意味着——参数稍微偏一点,策略就从赚钱变成不赚钱。
第三,最优区是一个平台,不是一个点。 收益前 10% 的格点集中在 fast∈[7,10]、slow∈[20,25] 这个矩形区域,而不是孤立的一个格点。平台比孤点更可信——孤点大概率是过拟合。
| 参数区域 | 格点数 | 平均期望收益 | 平均胜率 |
|---|---|---|---|
| 最优平台(fast 7-10, slow 20-25) | 48 | +2.85% | 54.2% |
| 教科书默认(12/26/9) | 1 | +0.80% | 43.1% |
| 边缘区(fast>15 或 slow>35) | 210 | -0.40% | 38.6% |
SMA 窗口的影响相对温和:window∈[10,25] 时收益稳定,window>40 后信号太滞后,收益明显下滑。
为什么默认值不行
MACD 的 12/26/9 是 Gerald Appel 在 1970 年代基于美股日线定的。A 股的波动节奏不同——T+1 制度、涨跌停板、散户占比高,导致同样的金叉信号在 A 股的持续性更短。参数需要跟着市场调,没有”放之四海皆准”的最优值。
过拟合:网格搜索最大的陷阱
网格搜索跑出 +3.2% 的最优收益,第一反应不该是高兴,而该警惕——这很可能是过拟合。
参数空间越大、格点越多,撞上”历史数据里的偶然规律”的概率越高。27000 个格点里挑出收益最高的那个,相当于在 27000 次抽奖里取最大值,这个最大值天然偏高。
样本外验证
防过拟合的标准做法是样本分割:用 2024-2025 的数据做网格搜索(训练集),再用 2026 的数据验证(测试集)。
- 最优孤点(fast=8, slow=22):训练集 +3.20%,测试集 +1.10%,衰减 66%
- 最优平台(fast 7-10, slow 20-25 均值):训练集 +2.85%,测试集 +2.30%,衰减 19%
孤点的样本外衰减 66%,平台的衰减只有 19%。 这印证了前面的判断——平台比孤点稳健。实操中应该用平台的参数均值(fast≈8, slow≈22),而不是孤点的极值。
多标的稳健性检验
只在一只股票上调出来的参数,换只股票可能就失效。我又跑了 5 只不同行业的 A 股,看最优平台是否一致:
| 标的 | 行业 | 最优 fast 区间 | 最优 slow 区间 |
|---|---|---|---|
| 标的 A(消费) | 食品饮料 | 7-10 | 20-25 |
| 标的 B(科技) | 半导体 | 6-9 | 18-22 |
| 标的 C(金融) | 银行 | 8-12 | 22-28 |
| 标的 D(周期) | 钢铁 | 7-11 | 20-26 |
| 标的 E(成长) | 新能源 | 6-9 | 18-23 |
fast 集中在 6-12,slow 集中在 18-28。不同标的的最优区有偏移,但都在一个大致的邻域内。 如果一个参数在 5 只股票里都进不了前 30%,基本可以判死刑。
踩过的坑
坑一:参数空间爆炸
第一版我把所有参数步长设成 1,格点数飙到 5 万多,跑了一晚上没跑完。
根因:步长 1 对 MACD 周期这种连续参数意义不大——周期 12 和 13 的 EMA 几乎重合,回测结果差异在噪声范围内。
解法:SMA 窗口步长从 1 改成 5,signal 固定在 9(对收益影响最小)。格点数降到 27000,40 秒跑完。
教训:网格粒度要对准参数的”有效分辨率”,不是越细越好。
坑二:信号去重的状态死锁
第二版回测的结果很诡异——有些格点胜率 100% 但只交易了 1 次。
根因:信号去重逻辑里,in_position 状态在止损退出后没有正确重置。一旦第一次买入后没正确置 False,后续信号全被跳过,整段回测只有一笔交易。胜率 100% 是因为样本量 1。
这个 bug 和之前那篇 copy-paste 事故(sell 分支 in_buy=True 让信号死锁)是同一类问题——状态机的复位分支写错,整个信号生命周期就断了。
解法:在退出分支显式 in_position = False,并加断言 assert not in_position 防止重复进入。同时在回测结果里加交易次数下限校验——单格点交易次数 < 10 直接标记为”样本不足”,不参与排名。
坑三:全局交易日历的数据空洞
这个坑在 QFII 回测里也踩过。用所有股票合并去重的日期做全局交易日历,不同股票上市/停牌时间不同,某些日期在某些股票里没数据,导致 next_td(date) 返回的日期找不到记录,大量信号被跳过。
解法:每只股票用自己的 K 线日期列表做日历索引,不共用全局日历。
教训:回测引擎必须按 symbol 独立维护交易日历。
这套方法的边界
网格搜索能找到参数空间里的局部最优,但有几个必须承认的局限。
只适合低维参数空间。 MACD+SMA 加起来 4 个旋钮,网格还能扛住。如果策略有 10 个以上参数,网格会指数爆炸——这时得换贝叶斯优化(Optuna)或随机搜索。
收益曲面非凸。 网格找到的”最优平台”只是当前分辨率下的局部最优,更细的网格可能在别处发现更高的平台。这就是为什么样本外验证比网格本身更重要。
历史不代表未来。 再严谨的样本外验证,也无法保证参数在未来市场依然有效。市场结构变化(政策、流动性、参与者结构)会让历史最优参数失效。
定期重跑网格搜索,监控最优区的漂移,比死守一组参数更实际。
最优解在哪?不在某个具体的数字里,而在”持续用网格搜索定位稳健平台 + 样本外验证防过拟合 + 定期重校准”这个闭环里。参数会变,方法不会。
你做参数优化时,是凭感觉拍、还是跑网格、还是用贝叶斯优化?欢迎在评论区聊聊你的经验。
参考文献
- Appel, Gerald. “The Moving Average Convergence Divergence (MACD) Method” — MACD 原始参数 12/26/9 的来源
- Bergstra, Bengio. “Random Search for Hyper-Parameter Optimization” (JMLR 2012) — 随机搜索在高维空间优于网格搜索的理论依据
- Optuna: A Next-generation Hyperparameter Optimization Framework — 高维参数空间的贝叶斯优化替代方案
- VectorBT: Finding your Best Strategy in One Line of Code — 向量化回测引擎,处理大规模网格搜索的开源工具
- Lopez de Prado. “Advances in Financial Machine Learning” Ch.8 Backtesting — 回测过拟合的数学定义与交叉验证方法