第七节:网格搜索调优

网格搜索,说白了就是「穷举法」。

你把参数的可能取值列出来,然后让程序把所有组合都跑一遍。哪个组合效果最好,你就用哪个。简单粗暴,但非常有效。

我个人习惯在策略开发的初期阶段,先用网格搜索摸一遍参数空间。为什么?因为这时候你还不清楚参数之间的相互作用,网格搜索能给你一个全局的视野。

一、网格搜索原理

网格搜索的核心思想,就是把参数空间离散化。

比如你有一个参数 lookback,取值范围是 10 到 50。你不可能把每个整数都试一遍,那样太慢了。你按步长 5 来取,就得到 10、15、20、25、30、35、40、45、50 这 9 个值。

然后你把所有参数的取值组合成一个「网格」,每个网格点对应一组参数。程序遍历所有网格点,计算每个点的绩效指标,最后选出最优的那个。

核心公式:

假设有 k 个参数,每个参数有 n_i 个取值,那么总共有 N = n_1 × n_2 × ... × n_k 个组合。

每个组合跑一次回测,记录 Sharpe 比率、最大回撤、年化收益等指标。

嗯,这里要注意:网格搜索不保证找到全局最优,它只保证在你给定的离散点上找到最优。所以步长的选择很关键。

二、一维网格搜索

一维网格搜索最简单,就一个参数来回试。

我记得刚开始做基差交易时,第一个要调的就是「滚动窗口长度」。这个参数决定了你用多少天的数据来计算基差的均值和标准差。

import numpy as np
import pandas as pd

def grid_search_1d(data, param_name, param_values, strategy_func):
    """
    一维网格搜索
    param_name: 参数名,比如 'window'
    param_values: 参数取值列表,比如 [10, 20, 30, 40, 50]
    strategy_func: 策略函数,接收 data 和 param_value,返回绩效指标
    """
    results = []
    for val in param_values:
        # 跑回测
        perf = strategy_func(data, val)
        results.append({
            param_name: val,
            'sharpe': perf['sharpe'],
            'max_drawdown': perf['max_drawdown'],
            'annual_return': perf['annual_return']
        })
    
    # 转成 DataFrame 方便查看
    df = pd.DataFrame(results)
    return df

# 示例:调优滚动窗口
windows = [5, 10, 15, 20, 25, 30, 40, 50, 60]
result_df = grid_search_1d(data, 'window', windows, my_strategy)
print(result_df.sort_values('sharpe', ascending=False).head())

跑完之后,你会发现 Sharpe 比率随着窗口长度变化,通常是一个倒 U 型曲线。窗口太短,信号噪音大;窗口太长,信号反应迟钝。

我的经验:一维搜索时,先大步长粗搜,找到最优区域后,再小步长精搜。比如先按 10 的步长搜 10 到 100,找到 30 附近最好,再按 2 的步长搜 20 到 40。

三、二维网格搜索

两个参数一起调,情况就复杂了。

你想想看,参数之间可能有交互作用。单独调 A 参数时最优值是 20,单独调 B 参数时最优值是 0.5,但把 A=20 和 B=0.5 放一起,效果可能还不如 A=15、B=0.3。

这就是为什么需要二维网格搜索——它能捕捉参数之间的协同效应。

def grid_search_2d(data, param1_name, param1_values, 
                   param2_name, param2_values, strategy_func):
    """
    二维网格搜索
    返回一个 DataFrame,包含所有组合的绩效
    """
    results = []
    for v1 in param1_values:
        for v2 in param2_values:
            perf = strategy_func(data, v1, v2)
            results.append({
                param1_name: v1,
                param2_name: v2,
                'sharpe': perf['sharpe'],
                'max_drawdown': perf['max_drawdown']
            })
    
    df = pd.DataFrame(results)
    # 用透视表展示结果
    pivot = df.pivot_table(
        values='sharpe', 
        index=param1_name, 
        columns=param2_name
    )
    return df, pivot

# 示例:调优窗口和阈值
windows = [10, 20, 30, 40, 50]
thresholds = [0.5, 1.0, 1.5, 2.0, 2.5]
df, pivot = grid_search_2d(data, 'window', windows, 
                           'threshold', thresholds, my_strategy)

# 热力图可视化
import seaborn as sns
import matplotlib.pyplot as plt
sns.heatmap(pivot, annot=True, fmt='.2f', cmap='RdYlGn')
plt.title('二维网格搜索 - Sharpe 热力图')
plt.show()

热力图一看就明白了。颜色越绿,效果越好。你会发现最优区域往往不是角落,而是中间某个「高原」区域。这个区域里的参数组合,绩效都比较稳定。

避坑指南:我曾经在二维搜索时犯过一个错误——两个参数取值太多,导致组合数爆炸。比如 window 取 20 个值,threshold 取 20 个值,那就是 400 次回测。如果一次回测要 10 秒,那就是 4000 秒,一个多小时。所以参数取值要精打细算。

四、多维网格搜索

三维以上,事情就变得棘手了。

假设你有 5 个参数,每个参数取 10 个值,那就是 10^5 = 100,000 次回测。一次回测 1 秒,也要 28 个小时。这还不算数据加载和结果分析的时间。

所以多维网格搜索,必须讲究策略。

我的做法是这样的:

  1. 先做敏感性分析:用随机采样跑几百次,看哪些参数对绩效影响大。影响小的参数直接固定。
  2. 分层搜索:先粗搜所有参数,找到大致区域。然后固定不敏感的参数,对敏感参数做精细搜索。
  3. 使用并行计算:网格搜索天然可以并行。每个网格点的计算是独立的,用 multiprocessing 或者 joblib 可以大幅提速。
from joblib import Parallel, delayed
import itertools

def parallel_grid_search(data, param_grid, strategy_func):
    """
    并行多维网格搜索
    param_grid: 字典,key 是参数名,value 是取值列表
    """
    # 生成所有参数组合
    keys = param_grid.keys()
    values = param_grid.values()
    all_combos = [dict(zip(keys, combo)) 
                  for combo in itertools.product(*values)]
    
    # 并行计算
    def evaluate(combo):
        perf = strategy_func(data, **combo)
        combo['sharpe'] = perf['sharpe']
        combo['max_drawdown'] = perf['max_drawdown']
        return combo
    
    results = Parallel(n_jobs=-1)(
        delayed(evaluate)(combo) for combo in all_combos
    )
    
    return pd.DataFrame(results)

# 示例:4个参数的网格搜索
param_grid = {
    'window': [10, 20, 30],
    'threshold': [1.0, 1.5, 2.0],
    'stop_loss': [0.02, 0.03, 0.05],
    'position_size': [0.1, 0.2, 0.3]
}

result_df = parallel_grid_search(data, param_grid, my_strategy)
# 找出最优组合
best = result_df.loc[result_df['sharpe'].idxmax()]
print(best)

你看,4 个参数各取 3 个值,总共 81 个组合。用并行计算,几秒钟就跑完了。

五、网格搜索的优缺点

优点 缺点
实现简单,容易理解 计算量大,维度灾难
结果可复现,确定性高 只能搜索离散点,可能错过最优
适合参数少的情况(≤3个) 参数多时效率极低
可以可视化,直观 无法利用历史搜索信息
天然可并行 步长选择影响结果

网格搜索最大的问题,就是「维度灾难」。参数每增加一个,计算量就指数级增长。5 个参数各取 10 个值,就是 10 万次回测。10 个参数?想都别想。

那什么时候用网格搜索?我个人觉得,参数不超过 3 个的时候,网格搜索是最佳选择。超过 3 个,建议用随机搜索或者贝叶斯优化。

核心要点:

  • 网格搜索是「穷举法」,简单可靠
  • 一维搜索看趋势,二维搜索看交互
  • 多维搜索要配合敏感性分析和并行计算
  • 参数超过 3 个,考虑其他方法

嗯,网格搜索虽然基础,但它是理解参数调优的起点。你把这个搞明白了,后面学随机搜索、贝叶斯优化,就很容易上手。

网格搜索调优知识体系 网格搜索 原理:离散化 + 穷举 一维:单参数调优 二维:热力图分析 多维:维度灾难 优缺点分析 并行计算策略 敏感性分析 → 分层搜索 优点 vs 缺点 joblib / multiprocessing 适用场景:参数 ≤ 3 个

最后说一句:网格搜索不是银弹。但它是一个很好的起点。你先把网格搜索玩熟了,再去看那些花里胡哨的优化方法,心里就有底了。

避坑指南:我曾经在实盘前用网格搜索调出一组「完美参数」,回测 Sharpe 高达 3.5。结果实盘一个月就亏了 8%。后来发现,那组参数在样本内过拟合了。所以网格搜索的结果,一定要做样本外验证。

好了,网格搜索就讲到这里。记住:简单的方法,往往最可靠。


公众号:蓝海资料掘金营,微信deep3321