过拟合识别:参数平原与敏感度分析
判断一组参数是不是过拟合,最直接的办法是看它的邻居:好参数处在一片平缓的高原上,周围参数的表现都差不多;过拟合参数是一根孤零零的针尖,左右挪一格收益就腰斩。这个检查只需要一次网格扫描,比任何统计检验都直观。
本文要点
- 最优参数如果左右各挪一格收益就腰斩,那个峰值是噪声,不是规律。
- 参数数量与所需样本量正相关:经验法则是每个参数至少要有 100 笔独立交易来支撑。
- 样本外表现衰减到样本内 30% 以下,或直接转负,基本可以判定为过拟合。
- 交易次数太少(低于 30 笔)时任何绩效指标都不可靠,包括很高的夏普。
- 只在一两个币种上有效、换币种就失效的策略,通常是拟合了个别币种的特有噪声。
过拟合的本质:把噪声当成了规律
任何一段历史价格都由两部分组成:可能重复出现的结构(趋势的持续性、波动率聚集、流动性规律),和永不重复的随机噪声。过拟合就是你的参数把后者也一起记住了。
有个思想实验能说明问题的严重性。用随机数生成一条完全没有规律的价格序列,在上面扫 96 组均线参数,其中表现最好的那一组必然有正收益、看起来不错的夏普和像样的净值曲线。这条曲线里的信息量是零,但它长得跟真策略一模一样。
所以问题不是「我的回测有没有收益」,而是「这个收益能否与随机搜索的最优结果区分开」。区分办法只有两类:看参数邻域的形状(本文重点),和看未见过的数据上的表现(见Walk-Forward 验证)。两者应该都做。
参数平原:好参数长什么样
把一个参数的取值作为横轴、绩效指标作为纵轴画出来,你会看到三种典型形状,判读方式完全不同。
形状一:平缓高原。在一段较宽的取值区间内,绩效都维持在较高水平,边缘平滑下降。这是最理想的形态,说明策略捕捉的是一个真实存在、对参数不敏感的效应。取参数时应该选高原的中心,而不是高原上最高的那个点。
形状二:孤立针尖。某个特定取值收益极高,相邻取值断崖下跌。这几乎可以肯定是噪声。原因很好理解:真实的市场规律不会在均线周期 29 有效而 28 和 30 无效——这个差别只对应几根K线的偏移,不可能造成本质变化。
形状三:整体平坦或全负。所有参数表现都差不多且不佳,说明这个思路本身没有边缘。这其实是好消息,你可以干净地放弃它,不必再纠缠。
import numpy as np
import pandas as pd
from itertools import product
def param_sweep(df, run_fn, perf_fn, grid, metric="calmar"):
"""网格扫描, 返回长表便于后续做平原分析"""
keys = list(grid)
rows = []
for vals in product(*grid.values()):
p = dict(zip(keys, vals))
m = perf_fn(run_fn(df, **p))
rows.append({**p,
"score": m.get(metric),
"n_trades": m.get("n_trades"),
"mdd": m.get("max_drawdown")})
return pd.DataFrame(rows)
def plateau_score(res: pd.DataFrame, param: str,
k: int = 2, metric="score") -> pd.DataFrame:
"""邻域稳健性: 用相邻 ±k 格的均值替代原始得分。
真高原的 robust 分数接近原值; 针尖会被显著削平。
"""
r = res.sort_values(param).reset_index(drop=True)
w = 2 * k + 1
r["robust"] = (r[metric]
.rolling(w, center=True, min_periods=k + 1)
.mean())
r["peak_penalty"] = r[metric] - r["robust"] # 越大越像针尖
return r
def pick_plateau_center(r: pd.DataFrame, param: str):
"""选 robust 最高处的参数, 而不是原始 score 最高处"""
best_raw = r.loc[r["score"].idxmax()]
best_rob = r.loc[r["robust"].idxmax()]
print(f"原始最优 {param}={best_raw[param]} score={best_raw['score']:.2f}")
print(f"高原中心 {param}={best_rob[param]} score={best_rob['score']:.2f} "
f"robust={best_rob['robust']:.2f}")
return best_rob[param]
上面 plateau_score 的思路是核心:用邻域平均得分代替单点得分来做选择。这一步等价于假设未来的最优参数会在当前最优附近漂移,而这个假设几乎总是成立。选高原中心的代价是放弃一点样本内收益,换来的是样本外衰减明显变小。
python
两维扫描:看的是一片区域
单参数曲线只是入门。实际策略通常有两三个关键参数,这时应该做二维扫描并观察热力图形态。判读逻辑一样,只是从「线上的高原」变成「面上的高原」。
健康形态是连成一片的高分区域,且形状规整(近似椭圆或带状)。危险形态是高分格点零散分布,像撒了一把胡椒——这说明得分主要由噪声驱动,任意两个相邻格点之间没有连续性。
带状高原还有个额外好处:它常常揭示参数之间的真实关系。比如快线周期与慢线周期的高分区呈斜带状,说明真正起作用的是两者的比值而不是绝对值。这类发现可以让你把两个参数合并成一个,直接降低过拟合风险。
def sweep_2d_report(res, p1, p2, metric="score", top_q=0.9):
"""评估二维高分区是否连成一片"""
piv = res.pivot_table(index=p1, columns=p2, values=metric)
thr = res[metric].quantile(top_q)
mask = (piv >= thr).astype(int)
# 高分格点数, 以及其中有几个的四邻域也是高分
total = int(mask.values.sum())
a = mask.values
connected = 0
for i in range(a.shape[0]):
for j in range(a.shape[1]):
if a[i, j] == 0:
continue
nb = 0
for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
x, y = i + di, j + dj
if 0 <= x < a.shape[0] and 0 <= y < a.shape[1]:
nb += a[x, y]
if nb >= 2:
connected += 1
ratio = connected / total if total else 0.0
print(f"高分格点 {total} 个, 其中 {connected} 个有>=2个高分邻居")
print(f"连通率 {ratio:.0%} (>70% 视为成片, <40% 视为散点/过拟合)")
return piv, ratio
python
参数数量与样本量的关系
每增加一个可调参数,你的搜索空间就乘上该参数的取值个数,找到虚假高峰的概率随之上升。这是纯粹的组合数学,与策略思路好坏无关。
一个可操作的经验法则:每个自由参数至少需要 100 笔独立交易来支撑。三个参数就需要 300 笔以上。这个数字没有严格的理论依据,但和样本量与估计误差的一般关系一致,实践中相当有用——它能在你写代码之前就否掉很多方案。
举例:一条日线策略在两年数据上产生了 80 笔交易,而你有 4 个参数要调。80 笔支撑 4 个参数,平均每个参数只有 20 笔样本,这个结论无论多漂亮都不可信。解决办法有三个:延长数据区间、增加币种数量(注意相关性会削弱独立性)、或者把参数固定成常识值不再调。
第三个办法常被低估。把止损固定在 3%、把 ATR 周期固定成 14,理由是「这是通用惯例」而不是「回测里它最好」,这样它就不再是自由参数,不消耗你的样本预算。
| 自由参数个数 | 每参数 8 个取值时的组合数 | 建议最低交易笔数 | 过拟合风险 |
|---|---|---|---|
| 1 | 8 | 100+ | 低 |
| 2 | 64 | 200+ | 低到中 |
| 3 | 512 | 300+ | 中 |
| 4 | 4096 | 400+ | 高 |
| 5 及以上 | 32768+ | 500+(实际很难满足) | 极高,结论基本不可用 |
看这张表要注意:右侧的「建议最低交易笔数」在 4 个参数以上时通常无法满足。加密市场的可用历史本来就不长,中低频策略在几年数据上很难攒到 400 笔以上有效交易。这意味着参数超过三个的策略,多数情况下不具备被验证的条件,而不是「验证起来更难一点」。
过拟合征兆检查清单
把下面这些征兆做成清单,每条策略上实盘前逐项打勾。命中两条以上就该停下来重新设计,而不是继续微调。这份清单的价值在于它检查的是相互独立的角度,很难同时骗过全部。
| 征兆 | 怎么检查 | 警戒线 | 为什么是问题 |
|---|---|---|---|
| 针尖峰值 | 最优参数左右各挪一格看收益变化 | 变化 > 40% | 真实规律不会对一格偏移如此敏感 |
| 样本外崩塌 | 对比 WF 样本外与样本内得分 | 比值 < 0.3 或转负 | 参数没有泛化能力 |
| 交易次数过少 | 统计总成交笔数 | < 30 笔 | 统计量不足,任何指标都是噪声 |
| 单币种有效 | 同参数换 5 个以上币种跑 | 仅 1–2 个币为正 | 拟合了个别币种的特有噪声 |
| 单段贡献过高 | 计算最赚那段占总收益比例 | > 70% | 结果来自一次幸运而非可重复方法 |
| 参数过多 | 数一数有几个自由可调参数 | > 3 个 | 搜索空间大到必然出现虚假高峰 |
| 逻辑无法解释 | 口头说明为什么这个参数合理 | 说不出机制 | 没有先验支撑的参数几乎都是拟合 |
| 时段敏感 | 分年度/分季度拆开看收益 | 多数时段为负 | 策略只在特定行情窗口成立 |
| 规则打补丁 | 数一数有几个特例条件(如排除某月) | ≥ 1 条特例 | 特例条件是过拟合最直白的形式 |
最后一条值得单独强调。「除了 2024 年 8 月那次极端行情之外都有效」这种说法,等于承认策略在最需要它的时候失效了。回测里加一个「跳过某段时间」的条件,收益会立刻改善,而这个改善在未来完全不可复现。任何形式的特例条款都应该被视为红线。
第七条也常被忽略:参数值应该有先验解释。均线用 20 因为它接近一个月的交易日、ATR 用 14 因为这是通用惯例,这些都是可接受的理由。「因为回测里 37 最好」不是理由。能先验解释的参数,即使样本内表现略差,样本外通常更稳。
务实的抗过拟合流程
把上面这些整合成一套可重复的流程,顺序很重要,每一步都是在做减法。
- 先写机制:用一两句话说清这条策略赚的是什么钱(趋势延续、均值回归、结构性费率)。说不清就不要开始写代码。
- 限制参数:控制在 3 个以内,其余按惯例固定并写下理由。
- 含成本粗扫:每个参数取 5–8 个值做网格,成本按保守值设定。
- 看形状不看峰值:确认高分区成片(连通率 > 70%),取高原中心而非最高点。
- 跨币种复核:同一套参数在 5 个以上币种上跑,至少多数为正。
- Walk-Forward:做滚动前推验证,检查 WF 效率与参数漂移。
- 敏感度压力测试:成本乘 2、滑点乘 2、参数各挪一格,结论不应反转。
- 小额实盘:跑 4–8 周,用真实成交回填成本参数,比对实盘与回测偏差。
这套流程会淘汰掉你绝大多数想法,这是它正常工作的表现,不是它太严格。想法便宜、验证昂贵、亏损最贵,把淘汰环节尽量前移是唯一经济的做法。相关的完整学习路径可以参考量化学院里的教程顺序。
还有一个反直觉的建议:如果一条策略在几乎所有参数取值下都能小赚,但没有任何一组参数能大赚,这通常比找到一个高收益峰值更值得高兴。前者说明有真实但微弱的边缘存在,可以靠仓位管理和多策略叠加去放大;后者往往什么都不说明。
常见问题
参数取高原中心会不会损失太多收益?
样本内会损失,通常在 10%–30% 之间;样本外一般反而更好。这正是这个做法的意义:你放弃的是不可复现的样本内峰值,换取的是参数漂移时的容错空间。实盘中真实最优参数几乎不会正好落在你选的那一点上,高原中心给了你两侧的缓冲。
交易次数少但每次都赚,这也算过拟合吗?
样本量不足时无法区分策略有效与运气好。8 笔交易全赚,在随机情况下发生的概率约为 1/256——看起来很小,但如果你试过上百组参数,出现这种结果几乎是必然的。补救办法是延长回测区间或增加币种,把样本量提到 30 笔以上再谈结论。
同一策略在 BTC 有效在其他币无效,一定是过拟合吗?
不一定,但需要给出机制解释。如果策略依赖的是深度流动性或期现结构,那么只在头部品种有效是合理的,这个解释应该在做回测之前就存在。若你是先发现只有 BTC 能跑通、再去找理由,那基本可以判定为拟合了 BTC 的特有噪声。
机器学习模型怎么检查过拟合?
思路相同,检查对象变了。参数平原对应超参数的邻域稳健性,交易次数对应样本量与特征数之比,跨币种检验对应跨市场泛化。此外要特别注意特征泄漏——用到了未来信息的特征在 ML 流程里比传统策略更难发现,因为特征工程环节的时间对齐更容易出错。