【Python量化策略实战 #07】回测参数随手填,上线就亏?网格搜索 + Walk-Forward 防过拟合实战
系列前情:#01~#06 我们完成了单因子/多因子选股、双均线/波动率择时、仓位管理、向量化回测框架。本篇进入策略上线前的关键一步——参数优化与过拟合防范。
一、为什么参数不能随便拍脑袋
很多初学者写策略时,均线周期随手填个 "5 日和 20 日",回测一看年化 30%,喜滋滋准备上线。结果实盘一跑,收益直接腰斩。
问题往往不是策略逻辑错了,而是参数在训练集上被过度优化了。你选的 "5/20" 可能只是恰好拟合了历史行情里的几段巧合,换段时间就完全失效。
量化圈有句老话:"回测是最好的骗子。" 本文给你两套工具——网格搜索找候选参数,Walk-Forward 滚动验证筛出真正稳健的参数。
二、本文你将得到什么
- 一个可复制的双均线参数网格搜索代码(3 个短周期 × 3 个长周期 = 9 组对比)
- Walk-Forward 滚动验证:用过去 30 天训练、未来 15 天测试,逐段推进
- 过拟合快速检测:训练集最优 vs 测试集平均,差距过大立即报警
- 真实数据跑通(演示数据 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 += stepwf_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~30 × 长周期 10~120 = 几百组组合,全样本最优几乎必然过拟合。建议参数空间 ≤20 组。
- 训练窗口太短:30 天日线样本太少,统计意义弱。正式跑建议 ≥6 个月训练 + ≥2 个月测试。
- 忽略交易成本:网格搜索时如果不计手续费,会高估高频策略。本文代码已内置 COST = 0.0015(单边千 1.5)。
- Walk-Forward fold 太少:演示数据仅 50 条,只能拆出 1 个 fold。正式证书可拉 2~3 年历史数据,拆出 10+ fold 才有说服力。
八、小结
本文用网格搜索 + Walk-Forward 给双均线策略做了一次完整的参数体检。关键结论:
- 全样本最优 ≠ 未来能赚钱
- Walk-Forward 是过滤"假最优"的最小可行方案
- 参数空间宁小勿大,训练窗口宁长勿短
下一篇(#08)我们将进入本系列收官篇——策略上线前的 checklist:滑点、手续费、再平衡频率、模拟盘记录,把回测到实盘的最小闭环跑通。
免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。

浙公网安备 33010602011771号