4、网格参数调优:历史回测框架搭建、网格参数扫描、最优参数选择
网格做市策略,说白了就是「低买高卖」的机械化执行。但网格间距设多大?层数设多少?单层仓位多重?这些参数拍脑袋定的话,大概率是要交学费的。
我刚开始做网格策略时,就吃过这个亏。当时觉得比特币波动大,网格间距设了0.5%,结果行情一震荡,频繁触发,手续费把利润全吃掉了。后来我学乖了——参数必须用历史数据回测来验证。
这一章,我们就来搭建一个完整的回测框架,然后做参数扫描,最后选出最优参数组合。
4.1 回测框架的核心逻辑
回测框架不复杂,核心就三件事:
- 加载历史数据——K线数据,最好是tick级或1分钟级
- 模拟交易逻辑——按网格规则挂单、成交、撤单
- 计算绩效指标——收益率、最大回撤、夏普比率等
我个人习惯用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
}
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)
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
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层时,夏普比率最高,那这就是我们要的参数区间。
scan_20240101_0.1_2.0_5_50.csv。这样半年后回来看,还能知道当时是怎么扫的。
4.6 本章知识体系
下面这张图总结了网格参数调优的完整流程:
整个流程走下来,你会发现参数调优其实是个「科学试错」的过程。别指望一次就找到圣杯,多跑几轮,多分析结果,慢慢就能找到感觉。
嗯,这一章的内容就到这里。记住:回测是策略的照妖镜,参数扫描是找到好参数的捷径。动手写代码跑起来,比看十遍理论都有用。