【Python量化策略实战 #07】回测参数随手填,上线就亏?网格搜索 + Walk-Forward 防过拟合实战

系列前情:#01~#06 我们完成了单因子/多因子选股、双均线/波动率择时、仓位管理、向量化回测框架。本篇进入策略上线前的关键一步——参数优化与过拟合防范。

一、为什么参数不能随便拍脑袋

很多初学者写策略时,均线周期随手填个 "5 日和 20 日",回测一看年化 30%,喜滋滋准备上线。结果实盘一跑,收益直接腰斩。

问题往往不是策略逻辑错了,而是参数在训练集上被过度优化了。你选的 "5/20" 可能只是恰好拟合了历史行情里的几段巧合,换段时间就完全失效。

量化圈有句老话:"回测是最好的骗子。" 本文给你两套工具——网格搜索找候选参数,Walk-Forward 滚动验证筛出真正稳健的参数。

二、本文你将得到什么

  1. 一个可复制的双均线参数网格搜索代码(3 个短周期 × 3 个长周期 = 9 组对比)
  2. Walk-Forward 滚动验证:用过去 30 天训练、未来 15 天测试,逐段推进
  3. 过拟合快速检测:训练集最优 vs 测试集平均,差距过大立即报警
  4. 真实数据跑通(演示数据 50 条,换正式证书后可扩展)

三、策略逻辑:双均线 + 参数空间

我们沿用 #03 的双均线金叉策略,但不再固定短/长期,而是让它们在合理范围内"自动竞争":

  • 短周期候选:3 日、5 日、8 日
  • 长周期候选:10 日、15 日、20 日
  • 约束:短周期 < 长周期

对每一组 (short, long),我们在训练集上跑回测,按 Sharpe 比率 排序取最优。

四、完整代码

from mairui import Client
import pandas as pd
import numpy as np
import itertools

演示证书(对外示例用;实际项目建议放环境变量)

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")

STOCK = "600519"
raw = client.stock_history(STOCK, "d", "f")
df = pd.DataFrame(raw)
df["date"] = pd.to_datetime(df["t"])
df = df.sort_values("date").reset_index(drop=True)

参数空间

SHORT_RANGE = [3, 5, 8]
LONG_RANGE = [10, 15, 20]
TRAIN_DAYS = 30
TEST_DAYS = 15
COST = 0.0015 # 手续费+滑点

def backtest(prices, short, long, cost=COST):
if long >= len(prices):
return None
ma_s = prices["c"].rolling(short).mean()
ma_l = prices["c"].rolling(long).mean()
signal = (ma_s > ma_l).astype(int).diff().fillna(0)

pos = 0
returns = []
trades = 0
for i in range(1, len(prices)):
if signal.iloc[i] == 1 and pos == 0:
pos = 1; trades += 1
elif signal.iloc[i] == -1 and pos == 1:
pos = 0; trades += 1

daily_ret = prices["c"].iloc[i] / prices["pc"].iloc[i] - 1.0
if pos == 1:
if signal.iloc[i] != 0:
daily_ret -= cost
returns.append(daily_ret)
else:
returns.append(0.0)

r = pd.Series(returns)
total = (1 + r).prod() - 1
sharpe = np.sqrt(252) * r.mean() / r.std() if r.std() > 1e-12 else 0.0
cum = (1 + r).cumprod()
dd = (cum / cum.cummax() - 1).min()
return {
"total_return": round(total * 100, 2),
"sharpe": round(sharpe, 3),
"max_drawdown": round(dd * 100, 2),
"trades": trades,
}

========== 网格搜索(全样本) ==========

grid_results = []
for s, l in itertools.product(SHORT_RANGE, LONG_RANGE):
if s >= l:
continue
res = backtest(df, s, l)
if res:
grid_results.append({"short": s, "long": l, **res})

grid_df = pd.DataFrame(grid_results).sort_values("sharpe", ascending=False)
print(grid_df.head(5).to_string(index=False))

运行后你会看到类似这样的输出(数据为演示结果):

short long total_return sharpe max_drawdown trades
3 10 2.68 1.500 -2.27 6
3 15 2.91 1.448 -2.97 4
5 15 1.86 1.006 -2.45 4

全样本最优是 (3, 10),Sharpe = 1.5。但先别急着高兴——这只是全样本拟合的结果。

五、Walk-Forward 滚动验证

核心思想:绝不把测试集当训练集用。我们用滚动窗口:

  • 第 1~30 天训练 → 选出最优参数 → 在第 31~45 天测试

  • 窗口向前滚动 15 天,重复

    Walk-Forward

    wf_results = []
    step = TEST_DAYS
    start = 0
    n = len(df)

    while start + TRAIN_DAYS + TEST_DAYS <= n:
    train = df.iloc[start : start + TRAIN_DAYS].copy()
    test = df.iloc[start + TRAIN_DAYS : start + TRAIN_DAYS + TEST_DAYS].copy()

    训练集选最优

    best_sharpe = -999
    best_params = None
    for s, l in itertools.product(SHORT_RANGE, LONG_RANGE):
    if s >= l:
    continue
    r = backtest(train, s, l, cost=0)
    if r and r["sharpe"] > best_sharpe:
    best_sharpe = r["sharpe"]
    best_params = (s, l)

    测试集验证

    if best_params:
    test_res = backtest(test, best_params[0], best_params[1])
    if test_res:
    wf_results.append({
    "fold": len(wf_results) + 1,
    "best_s": best_params[0],
    "best_l": best_params[1],
    **test_res,
    })
    start += step

    wf_df = pd.DataFrame(wf_results)
    print(wf_df.to_string(index=False))
    print(f"Walk-Forward 平均 Sharpe = {wf_df['sharpe'].mean():.3f}")

演示数据输出(50 条日线,仅 1 个 fold):

fold best_s best_l total_return sharpe max_drawdown trades
1 3 10 -1.47 -2.173 -2.18 1

六、过拟合检测:一眼看穿"假最优"

对比两组数字:

指标 全样本网格最优 (3,10) Walk-Forward 测试集平均
Sharpe 1.500 -2.173
总收益 +2.68% -1.47%

训练集最优参数在测试集上直接翻绿。这就是典型的过拟合——参数把历史噪音当成了信号。

实用判定规则:

  • 若训练集 Sharpe 比滚动测试集平均高 2 倍以上,且测试集 Sharpe < 0.5 → 过拟合警告
  • 对策:缩小参数空间、增加训练样本、降低交易频率、或放弃该策略

七、常见坑

  1. 参数空间太大:短周期 1~30 × 长周期 10~120 = 几百组组合,全样本最优几乎必然过拟合。建议参数空间 ≤20 组。
  2. 训练窗口太短:30 天日线样本太少,统计意义弱。正式跑建议 ≥6 个月训练 + ≥2 个月测试。
  3. 忽略交易成本:网格搜索时如果不计手续费,会高估高频策略。本文代码已内置 COST = 0.0015(单边千 1.5)。
  4. Walk-Forward fold 太少:演示数据仅 50 条,只能拆出 1 个 fold。正式证书可拉 2~3 年历史数据,拆出 10+ fold 才有说服力。

八、小结

本文用网格搜索 + Walk-Forward 给双均线策略做了一次完整的参数体检。关键结论:

  • 全样本最优 ≠ 未来能赚钱
  • Walk-Forward 是过滤"假最优"的最小可行方案
  • 参数空间宁小勿大,训练窗口宁长勿短

下一篇(#08)我们将进入本系列收官篇——策略上线前的 checklist:滑点、手续费、再平衡频率、模拟盘记录,把回测到实盘的最小闭环跑通。


免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。

代码与文档:https://github.com/MaiRuiApi

posted @ 2026-10-03 09:27  量化分析码农  阅读(9)  评论(0)    收藏  举报