4、网格参数调优:历史回测框架搭建、网格参数扫描、最优参数选择

网格做市策略,说白了就是「低买高卖」的机械化执行。但网格间距设多大?层数设多少?单层仓位多重?这些参数拍脑袋定的话,大概率是要交学费的。

我刚开始做网格策略时,就吃过这个亏。当时觉得比特币波动大,网格间距设了0.5%,结果行情一震荡,频繁触发,手续费把利润全吃掉了。后来我学乖了——参数必须用历史数据回测来验证

这一章,我们就来搭建一个完整的回测框架,然后做参数扫描,最后选出最优参数组合。

4.1 回测框架的核心逻辑

回测框架不复杂,核心就三件事:

  1. 加载历史数据——K线数据,最好是tick级或1分钟级
  2. 模拟交易逻辑——按网格规则挂单、成交、撤单
  3. 计算绩效指标——收益率、最大回撤、夏普比率等

我个人习惯用Python写回测框架,因为灵活。下面是一个最小可用的回测框架骨架:

import pandas as pd
import numpy as np

class GridBacktest:
    def __init__(self, data, grid_params):
        """
        data: DataFrame, 包含['time', 'price']列
        grid_params: dict, 包含grid_spacing, grid_layers, position_size等
        """
        self.data = data
        self.params = grid_params
        self.equity_curve = []
        
    def run(self):
        """执行回测"""
        # 初始化网格
        grid_levels = self._init_grid()
        # 逐笔模拟
        for i in range(1, len(self.data)):
            price = self.data.iloc[i]['price']
            # 检查是否触发网格
            self._check_grid(price, grid_levels)
            # 记录权益
            self.equity_curve.append(self._calc_equity(price))
        return self._calc_metrics()
    
    def _init_grid(self):
        """生成网格价格水平"""
        center = self.data.iloc[0]['price']
        spacing = self.params['grid_spacing']
        layers = self.params['grid_layers']
        levels = []
        for i in range(-layers, layers+1):
            levels.append(center * (1 + i * spacing))
        return sorted(levels)
    
    def _check_grid(self, price, levels):
        """检查是否触发网格交易"""
        # 实际实现需要维护挂单状态
        pass
    
    def _calc_equity(self, price):
        """计算当前权益"""
        # 包括现金+持仓市值
        pass
    
    def _calc_metrics(self):
        """计算绩效指标"""
        returns = pd.Series(self.equity_curve).pct_change().dropna()
        sharpe = np.sqrt(365) * returns.mean() / returns.std()
        max_dd = (pd.Series(self.equity_curve).cummax() - 
                  pd.Series(self.equity_curve)).max()
        return {
            'total_return': self.equity_curve[-1] / self.equity_curve[0] - 1,
            'sharpe_ratio': sharpe,
            'max_drawdown': max_dd
        }
💡 我的经验:回测时一定要考虑手续费和滑点。我曾经回测年化50%的策略,实盘跑起来只有20%,就是因为忽略了这两项。建议手续费按千分之一算,滑点按0.1%算。

4.2 网格参数扫描

参数扫描,就是让计算机替我们试错。我们定义好参数范围,然后让程序跑遍所有组合。

常用的扫描参数包括:

  • 网格间距:0.1% ~ 2%,步长0.1%
  • 网格层数:5 ~ 50层,步长5
  • 单层仓位:总资金的1% ~ 10%,步长1%
  • 基准价格:起始价 ± 5%

下面是一个参数扫描的实现:

def parameter_scan(data, param_grid):
    """
    param_grid: dict, 例如
    {
        'grid_spacing': [0.001, 0.002, 0.003],
        'grid_layers': [10, 20, 30],
        'position_size': [0.01, 0.02, 0.03]
    }
    """
    results = []
    from itertools import product
    
    keys = list(param_grid.keys())
    values = list(param_grid.values())
    
    for combo in product(*values):
        params = dict(zip(keys, combo))
        bt = GridBacktest(data, params)
        metrics = bt.run()
        metrics.update(params)
        results.append(metrics)
        
    return pd.DataFrame(results)
⚠️ 注意:参数扫描的计算量很大。假设每个参数有10个值,3个参数就是1000种组合。如果回测一次需要1秒,那就要跑16分钟。我建议先用粗粒度扫描,找到大致范围,再细粒度优化。

4.3 最优参数选择

扫描完参数后,怎么选最优?这里有个坑——不能只看收益率

我记得有一次扫描结果,收益率最高的参数组合回撤高达40%。这种策略你敢实盘吗?反正我不敢。

我一般用「综合评分法」来选参数:

指标 权重 说明
年化收益率 30% 越高越好
最大回撤 30% 越低越好(取倒数)
夏普比率 20% 越高越好
交易次数 10% 适中最好(太多手续费高,太少不活跃)
胜率 10% 越高越好

评分公式很简单:

def score_params(row):
    # 归一化处理
    norm_return = (row['total_return'] - min_return) / (max_return - min_return)
    norm_dd = 1 - (row['max_drawdown'] - min_dd) / (max_dd - min_dd)
    norm_sharpe = (row['sharpe_ratio'] - min_sharpe) / (max_sharpe - min_sharpe)
    
    score = (0.3 * norm_return + 
             0.3 * norm_dd + 
             0.2 * norm_sharpe +
             0.1 * norm_trades +
             0.1 * norm_winrate)
    return score
🎯 核心原则:最优参数不是「收益最高」的参数,而是「收益风险比最佳」的参数。我通常会在评分前10%的参数中,再人工筛选一遍,看看参数是否合理、是否过拟合。

4.4 过拟合检测

参数扫描最容易犯的错误就是过拟合。你想想看,参数调得刚好匹配历史数据,但未来行情一变,策略就失效了。

我常用的检测方法:

  • 样本外测试:把数据分成前70%和后30%,用前段选参数,后段验证
  • 参数稳定性检验:最优参数附近的小范围变动,绩效不应剧烈波动
  • 多周期验证:在牛市、熊市、震荡市分别测试

举个例子,如果网格间距从0.5%变成0.6%,收益率从30%掉到5%,那这个参数就太敏感了,不靠谱。

4.5 可视化分析

光看数字不够直观,我习惯把扫描结果画成热力图。下面是一个简单的可视化代码:

import matplotlib.pyplot as plt
import seaborn as sns

def plot_heatmap(results_df, x_param, y_param, metric='sharpe_ratio'):
    pivot = results_df.pivot_table(
        values=metric, 
        index=y_param, 
        columns=x_param
    )
    plt.figure(figsize=(10, 8))
    sns.heatmap(pivot, annot=True, fmt='.2f', cmap='RdYlGn')
    plt.title(f'{metric} Heatmap')
    plt.show()

热力图能让你一眼看出参数的最佳区域。比如网格间距在0.8%~1.2%、层数在15~25层时,夏普比率最高,那这就是我们要的参数区间。

💡 我的习惯:每次扫描完参数,我会把结果保存到CSV文件,方便后续复盘。文件名带上时间戳和参数范围,比如 scan_20240101_0.1_2.0_5_50.csv。这样半年后回来看,还能知道当时是怎么扫的。

4.6 本章知识体系

下面这张图总结了网格参数调优的完整流程:

网格参数调优流程 1. 数据准备 历史K线数据清洗 2. 参数定义 间距/层数/仓位 3. 回测引擎 逐笔模拟交易 4. 参数扫描 遍历所有参数组合 5. 绩效计算 收益率/回撤/夏普 6. 最优参数选择 综合评分 + 过拟合检测 关键指标 • 年化收益率 • 最大回撤 • 夏普比率

整个流程走下来,你会发现参数调优其实是个「科学试错」的过程。别指望一次就找到圣杯,多跑几轮,多分析结果,慢慢就能找到感觉。

嗯,这一章的内容就到这里。记住:回测是策略的照妖镜,参数扫描是找到好参数的捷径。动手写代码跑起来,比看十遍理论都有用。

专注资料整理