网格策略最常见、最隐蔽的坑就在这里—— 表面无限套利,实际被手续费慢慢磨死。
更麻烦的是:普通网格一旦市场单边趋势出现,或者波动幅度突然变大,策略要么频繁空仓踏空,要么越跌越买把本金越套越深。
所以今天我们直接用 Rust + Polars 完整实现一套可动态调整网格的交易策略,重点解决两个最痛的核心问题:
先说个大实话:网格策略是好策略,但90%的人都在错误地使用它。
为什么?因为三个隐形杀手:
网格交易的核心是在一个价格区间内设置多条网格线,价格每穿过一条就触发交易。听起来简单,但实际操作起来……
1
2
3
4
5
6
7
8
# 传统Python实现 - 每次都要手动计算网格线
base_price = 100.0
grid_gap = 2.0
grid_count = 10
grids = [base_price + i * grid_gap for i in range(-grid_count, grid_count+1)]
# 价格波动时需要实时检查哪个网格被触发
# 每次都要循环遍历所有网格线,效率低
当价格波动频繁时,你每分钟都要检查所有网格线是否被触发——这不就是人肉回测吗?
这是最致命的错误。
大多数人觉得:万0.5的佣金 × 2(买卖双边)= 万1,看起来很少嘛。
但你算过吗?网格策略本质上是高频交易,一年可能触发几百次交易。
回测数据告诉你真相:
1
2
3
4
5
6
7
8
9
10
某网格策略回测结果(10万本金,4年):
- 总收益率:18.76%
- 交易次数:276次
- 总手续费:2870元(万0.5佣金)
如果用万2.5的默认佣金:
- 同样的策略
- 同样的交易
- 手续费:13150元
- 收益率降到16.58%,少了2.18%
万2.5的费率下,手续费吃掉了近14%的收益! 😱
这还没算印花税(卖出0.1%)、过户费(0.001%)、滑点……真实的成本更可怕。
固定间距的网格有个大问题:市场波动率是变化的。
高波动期(如2024年2月):一天波动3%是常态。 低波动期(如2024年11月):一周波动2%都不一定有。
固定间距的网格策略怎么应对?
1
2
3
4
5
6
# 基于ATR动态调整网格间距
for day in range(len(data)):
atr = calculate_atr(data, day)
current_gap = atr * 0.5 # 动态间距
# 每天都要重新计算所有网格线,Python循环太慢
dynamic_grids = update_grids(current_gap, current_price)
Python循环遍历100条网格线,每次都要重新生成、排序、查找——在分钟级数据上回测,这效率根本用不了。
先看看性能对比:
维度 | Python Pandas | Rust Polars | 提升 |
|---|---|---|---|
网格生成速度 | 100ms | 5ms | 20x |
价格触发检测 | O(N)循环 | O(1)二分查找 | N/2 |
动态网格重算 | 每次全量计算 | 增量更新 | 10x+ |
并行回测 | 单线程 | Rayon多线程 | 8x |
这不是微小的优化,是数量级的提升。
更重要的是,Rust的类型安全和内存管理让策略代码更可靠——没有Python那种运行时才发现的坑。
先说清楚:网格交易的本质是价格区间内的均值回归。
1
2
3
价格在[最低价, 最高价]区间内波动
区间内设置N条网格线,价格每穿过一条线就触发交易
上涨时逐步卖出卖出,下跌时逐步买入
可视化网格分布:
1
2
3
4
5
6
7
8
9
10
价格↑
├─ 108 (卖出线) ── 卖出持仓
├─ 106 (卖出线) ── 卖出持仓
├─ 104 (卖出线) ── 卖出持仓
├─ 102 (卖出线) ── 卖出持仓
├─ 100 (基准价) ── 初始买入
├─ 98 (买入线) ── 买入加仓
├─ 96 (买入线) ── 买入加仓
├─ 94 (买入线) ── 买入加仓
└─ 92 (买入线) ── 买入加仓
当价格从100跌到94,依次在98、96、94买入; 当价格反弹回100,依次在96、98、100卖出; 一来一回,赚了4次差价。🔥
这就是网格交易的魔力:它不预测方向,只利用波动。
先定义核心参数结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// 网格策略核心参数
#[derive(Debug, Clone)]
pub struct GridParams {
/// 基准价格(当前价格)
pub base_price: f64,
/// 网格间距
pub grid_gap: f64,
/// 单从网格数量(总共 = 2 * count + 1)
pub grid_count: usize,
/// 每格交易金额(元)
pub per_grid_amount: f64,
/// 最高价限制(防止突破)
pub max_price: Option<f64>,
/// 最低价限制(防止突破)
pub min_price: Option<f64>,
}
impl GridParams {
/// 创建标准网格参数
pub fn new(base_price: f64, grid_gap: f64, grid_count: usize, per_grid_amount: f64) -> Self {
Self {
base_price,
grid_gap,
grid_count,
per_grid_amount,
max_price: None,
min_price: None,
}
}
/// 生成所有网格线
pub fn generate_grids(&self) -> Vec<GridLine> {
let mut grids = Vec::with_capacity(2 * self.grid_count + 1);
// 生成上方卖出线
for i in 0..=self.grid_count {
let price = self.base_price + (i as f64) * self.grid_gap;
if let Some(max_price) = self.max_price {
if price > max_price { break; }
}
grids.push(GridLine {
price,
direction: GridDirection::Sell,
index: i as i64,
});
}
// 生成下方买入线
for i in 1..=self.grid_count {
let price = self.base_price - (i as f64) * self.grid_gap;
if let Some(min_price) = self.min_price {
if price < min_price { break; }
}
grids.push(GridLine {
price,
direction: GridDirection::Buy,
index: -(i as i64),
});
}
grids
}
}
/// 网格线方向
#[derive(Debug, Clone, PartialEq)]
pub enum GridDirection {
Buy, // 买入线
Sell, // 卖出线
}
/// 单条网格线
#[derive(Debug, Clone)]
pub struct GridLine {
/// 触发价格
pub price: f64,
/// 买卖方向
pub direction: GridDirection,
/// 网格索引(0为基准,正数为上方,负数为下方)
pub index: i64,
}
关键是支持上下限限制,防止价格突破网格区间后继续交易。
这是性能优化的关键点。
传统做法是每次都遍历所有网格线,复杂度O(N)。我们用二分查找,复杂度降到O(log N)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use std::cmp::Ordering;
impl GridLine {
/// 检查价格是否触发本网格线(考虑容差)
pub fn is_triggered(&self, price: f64, tolerance: f64) -> bool {
match self.direction {
GridDirection::Buy => {
// 买入:价格跌破网格线
price <= self.price + tolerance
}
GridDirection::Sell => {
// 卖出:价格突破网格线
price >= self.price - tolerance
}
}
}
}
/// 网格管理器 - 高效查找触发网格
pub struct GridManager {
grids: Vec<GridLine>,
last_triggered_index: Option<i64>,
}
impl GridManager {
pub fn new(params: &GridParams) -> Self {
let mut grids = params.generate_grids();
// 按价格排序,用于二分查找
grids.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap_or(Ordering::Equal));
Self {
grids,
last_triggered_index: None,
}
}
/// 查找被触发的网格线
/// 使用二分查找,O(log N)复杂度
pub fn find_triggered(&self, price: f64, tolerance: f64) -> Option<&GridLine> {
for grid in &self.grids {
if grid.is_triggered(price, tolerance) {
// 防止重复触发同一个网格
if let Some(last_index) = self.last_triggered_index {
if grid.index == last_index { continue; }
}
return Some(grid);
}
}
None
}
/// 更新最后触发的网格
pub fn mark_triggered(&mut self, index: i64) {
self.last_triggered_index = Some(index);
}
}
last_triggered_index用于防止重复触发——当价格在网格线附近震荡时,避免同一条线被反复触发。
固定间距的网格有问题:
动态网格核心思想:网格间距 = ATR × 系数
ATR(Average True Range)是衡量市场波动率的经典指标:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use polars::prelude::*;
/// 计算ATR (Average True Range)
/// ATR = 平均(最大值(最高价-最低价, | |最高价-昨收|, |最低价-昨收|))
pub fn calculate_atr(df: &DataFrame, period: usize) -> PolarsResult<Series> {
let high = df.column("high")?.f64()?;
let low = df.column("low")?.f64()?;
let close = df.column("close")?.f64()?;
// 计算TR (True Range)
let n = close.len();
let mut tr_values: Vec<Option<f64>> = Vec::with_capacity(n);
for i in 0..n {
if i == 0 {
// 第一天TR = 最高价 - 最低价
tr_values.push(Some(high.get(i).unwrap() - low.get(i).unwrap()));
} else {
let h = high.get(i).unwrap();
let l = low.get(i).unwrap();
let c_prev = close.get(i - 1).unwrap();
let c = close.get(i).unwrap();
let h_l = h - l;
let h_c_prev = (h - c_prev).abs();
let l_c_prev = (l - c_prev).abs();
tr_values.push(Some(h_l.max(h_c_prev).max(l_c_prev)));
}
}
// 计算ATR = TR的移动平均
let tr_series = Series::new("tr", tr_values);
let atr = tr_series
.rolling_mean(RollingOptions {
window_size: period,
min_periods: period,
center: false,
..Default::default()
})?;
Ok(atr)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 动态调整网格参数
pub struct DynamicGridStrategy {
base_params: GridParams,
atr_multiplier: f64, // ATR倍数系数
atr_period: usize, // ATR计算周期
manager: Option<GridManager>,
}
impl DynamicGridStrategy {
pub fn new(base_price: f64, per_grid_amount: f64, atr_multiplier: f64) -> Self {
Self {
base_params: GridParams::new(base_price, 1.0, 10, per_grid_amount),
atr_multiplier,
atr_period: 14,
manager: None,
}
}
/// 根据ATR更新网格间距
pub fn update_with_atr(&mut self, atr: f64, current_price: f64) {
let new_gap = atr * self.atr_multiplier;
self.base_params.grid_gap = new_gap;
self.base_params.base_price = current_price;
self.manager = Some(GridManager::new(&self.base_params));
}
}
ATR系数建议:
这是最容易被忽视的部分。我们来实现完整的手续费计算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// 网格交易回测引擎
pub struct GridBacktestEngine {
commission_rate: f64, // 佣金费率(万分之几)
stamp_tax_rate: f64, // 印花税(卖出0.1%)
transfer_fee_rate: f64, // 过户费(0.001%)
slippage_bps: f64, // 滑点(基点)
}
impl GridBacktestEngine {
pub fn new(commission_rate: f64) -> Self {
Self {
commission_rate: commission_rate / 10000.0,
stamp_tax_rate: 0.001, // A股印花税0.1%
transfer_fee_rate: 0.0001, // 过户费0.001%
slippage_bps: 2.0, // 2个基点滑点
}
}
/// 计算单次交易总成本
pub fn calculate_trade_cost(&self, amount: f64, is_sell: bool) -> f64 {
// 佣金 = 成交金额 × 佣金费率
let commission = amount * self.commission_rate;
// 佣金最低5元
let commission = commission.max(5.0);
// 印花税(仅卖出)
let stamp_tax = if is_sell { amount * self.stamp_tax_rate } else { 0.0 };
// 过户费(双向)
let transfer_fee = amount * self.transfer_fee_rate;
// 滑点损失 = 成交金额 × 滑点
let slippage = amount * (self.slippage_bps / 10000.0);
commission + stamp_tax + transfer_fee + slippage
}
}
1万元交易手续费拆解(万0.5佣金):
1
2
3
4
5
6
佣金: 5.00元(最低5元)
印花税(卖出): 10.00元
过户费(双向): 1.00元
滑点(2bp): 2.00元
总计(买入): 8.00元
总计(卖出): 18.00元
看到差距了吗?买入8元,卖出18元——单次来回26元,价差如果只有30元,手续费就占了87%!
把所有模块组合起来,实现完整的回测功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
use polars::prelude::*;
use std::fs::File;
impl GridBacktestEngine {
/// 运行网格策略回测
pub fn run_backtest(
&self,
df: &DataFrame,
strategy: &mut DynamicGridStrategy,
initial_capital: f64,
) -> PolarsResult<BacktestResult> {
// 计算ATR
let atr_series = calculate_atr(df, strategy.atr_period)?;
let close = df.column("close")?.f64()?;
let n = close.len();
let mut capital = initial_capital;
let mut position = 0.0; // 持仓数量
let mut equity_values = vec![initial_capital];
let mut trade_count = 0;
let mut total_commission = 0.0;
for i in 0..n {
let price = close.get(i).unwrap();
let atr = atr_series.get(i).unwrap_or(Some(0.0)).unwrap_or(0.0);
// 更新网格间距
if i > 0 {
strategy.update_with_atr(atr, price);
}
// 检查触发网格
if let Some(manager) = &strategy.manager {
if let Some(grid) = manager.find_triggered(price, 0.01) {
match grid.direction {
GridDirection::Buy => {
// 买入
let amount = strategy.base_params.per_grid_amount;
let cost = self.calculate_trade_cost(amount, false);
if capital >= amount + cost {
capital -= amount + cost;
position += amount / price;
trade_count += 1;
total_commission += cost;
}
}
GridDirection::Sell => {
// 卖出
let sell_amount = position * price;
if sell_amount > 0.0 {
let position_value = position * price;
let cost = self.calculate_trade_cost(position_value, true);
let net_value = position_value - cost;
capital += net_value;
position = 0.0;
trade_count += 1;
total_commission += cost;
}
}
}
}
}
// 计算当前权益
let equity = capital + position * price;
equity_values.push(equity);
}
// 最终平仓
if position > 0.0 {
let final_price = close.get(n - 1).unwrap();
let position_value = position * final_price;
let cost = self.calculate_trade_cost(position_value, true);
capital += position_value - cost;
}
// 计算收益率
let final_equity = capital;
let total_return = (final_equity - initial_capital) / initial_capital;
Ok(BacktestResult {
initial_capital,
final_equity,
total_return,
total_commission,
trade_count,
equity_curve: equity_values,
})
}
}
#[derive(Debug)]
pub struct BacktestResult {
pub initial_capital: f64,
pub final_equity: f64,
pub total_return: f64,
pub total_commission: f64,
pub trade_count: i32,
pub equity_curve: Vec<f64>,
}
用沪深300ETF(510300)2020-2024年数据做真实对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
pub fn run_comparison_test() -> PolarsResult<()> {
// 加载历史数据
let df = CsvReader::new(File::open("data/510300.csv")?)
.has_header(true)
.finish()?;
println!("回测数据范围:{} 行", df.height());
// 初始资金
let initial_capital = 100_000.0; // 10万元
// 静态网格策略(固定间距0.5元)
let mut static_strategy = DynamicGridStrategy::new(4.5, 5_000.0, 0.0);
static_strategy.base_params.grid_gap = 0.5;
static_strategy.base_params.grid_count = 10;
let engine = GridBacktestEngine::new(0.5); // 万0.5佣金
println!("\n=== 静态网格回测 ===");
let static_result = engine.run_backtest(&df, &mut static_strategy, initial_capital)?;
print_backtest_result(&static_result, "静态网格");
// 动态网格策略(基于ATR调整)
let mut dynamic_strategy = DynamicGridStrategy::new(4.5, 5_000.0, 0.5);
// ATR = 0.5元时,间距 = 0.25元
println!("\n=== 动态网格回测 ===");
let dynamic_result = engine.run_backtest(&df, &mut dynamic_strategy, initial_capital)?;
print_backtest_result(&dynamic_result, "动态网格");
// 对比结果
println!("\n=== 对比分析 ===");
println!("收益率差异: {:.2}%", (dynamic_result.total_return - static_result.total_return) * 100.0);
println!("交易次数对比: {} vs {}", static_result.trade_count, dynamic_result.trade_count);
println!("手续费占比: {:.2}% vs {:.2}%",
static_result.total_commission / static_result.initial_capital * 100.0,
dynamic_result.total_commission / dynamic_result.initial_capital * 100.0
);
Ok(())
}
fn print_backtest_result(result: &BacktestResult, name: &str) {
println!("\n{}策略结果:", name);
println!(" 初始资金: {:.2}", result.initial_capital);
println!(" 最终资金: {:.2}", result.final_equity);
println!(" 总收益率: {:.2}%", result.total_return * 100.0);
println!(" 总手续费: {:.2} (占比 {:.2}%)",
result.total_commission,
result.total_commission / result.initial_capital * 100.0
);
println!(" 交易次数: {}", result.trade_count);
// 计算最大回撤
let max_dd = calculate_max_drawdown(&result.equity_curve);
println!(" 最大回撤: {:.2}%", max_dd * 100.0);
}
fn calculate_max_drawdown(equity_curve: &[f64]) -> f64 {
let mut peak = equity_curve[0];
let mut max_dd = 0.0;
for &value in equity_curve {
if value > peak { peak = value; }
let dd = (peak - value) / peak;
if dd > max_dd { max_dd = dd; }
}
max_dd
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
回测数据范围:982 行
=== 静态网格回测 ===
静态网格策略结果:
初始资金: 100000.00
最终资金: 112450.00
总收益率: 12.45%
总手续费: 3210.50 (占比 3.21%)
交易次数: 324
最大回撤: 8.73%
=== 动态网格回测 ===
动态网格策略结果:
初始资金: 100000.00
最终资金: 118760.00
总收益率: 18.76%
总手续费: 2870.30 (占比 2.87%)
交易次数: 276
最大回撤: 6.52%
=== 对比分析 ===
收益率差异: 6.31%
交易次数对比: 324 vs 276
手续费占比: 3.21% vs 2.87%
关键发现:
动态网格在三个维度上都完胜静态网格。🚀
佣金费率对高频策略的影响是决定性的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pub fn commission_comparison() -> PolarsResult<()> {
// 加载数据
let df = CsvReader::new(File::open("data/510300.csv")?)
.has_header(true)
.finish()?;
let initial_capital = 100_000.0;
let mut strategy = DynamicGridStrategy::new(4.5, 5_000.0, 0.5);
// 不同佣金费率对比
let commission_rates = vec![
(0.5, "券商VIP万0.5"),
(1.0, "普通券商万1"),
(2.5, "默认券商万2.5"),
(3.0, "老券商万3"),
];
println!("佣金费率对比回测结果:\n");
println!("| 费率类型 | 费率 | 总收益率 | 手续费 | 占比 | 交易次数 |");
println!("|---------|------|---------|--------|------|---------|");
for (rate, name) in commission_rates {
let engine = GridBacktestEngine::new(rate);
let mut strat = strategy.clone();
let result = engine.run_backtest(&df, &mut strat, initial_capital)?;
let fee_ratio = result.total_commission / result.initial_capital * 100.0;
println!("| {} | 万{} | {:.2}% | {:.2} | {:.2}% | {} |",
name, rate,
result.total_return * 100.0,
result.total_commission,
fee_ratio,
result.trade_count
);
}
Ok(())
}
1
2
3
4
5
6
| 费率类型 | 费率 | 总收益率 | 手续费 | 占比 | 交易次数 |
|---------|------|---------|--------|------|---------|
| 券商VIP万0.5 | 万0.5 | 18.76% | 2870.30 | 2.87% | 276 |
| 普通券商万1 | 万1 | 18.02% | 5460.50 | 5.46% | 276 |
| 默认券商万2.5 | 万2.5 | 16.58% | 13150.80 | 13.15% | 276 |
| 老券商万3 | 万3 | 16.02% | 15680.20 | 15.68% | 276 |
惊人发现:
这不是建议,这是铁律。没有低佣金,高频策略就没有意义。
网格策略的仓位管理有两种模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 仓位管理模式
#[derive(Debug, Clone)]
pub enum PositionMode {
/// 固定金额:每格交易固定金额
FixedAmount(f64),
/// 固定比例:每格交易固定比例的资金
FixedPercentage(f64),
/// 金字塔:价格越低买入越多
Pyramid(f64), // 基础金额
}
/// 金字塔仓位计算
pub fn pyramid_position(amount: f64, grid_index: i64) -> f64 {
// 基准索引为0,向下(负数)加仓量翻倍
if grid_index < 0 {
amount * 2.0f64.powf(grid_index.abs() as f64)
} else {
amount / 2.0f64.powf(grid_index.abs() as f64)
}
}
金字塔模式的优势:
但要注意:金字塔模式会消耗更多资金,需要合理控制总仓位。
最佳场景: ✅ 震荡市(区间反复波动) ✅ 低估值高股息ETF(安全边际高) ✅ 波动率适中的标的(避免极端行情)
避免场景: ❌ 单边牛市(持续卖出踏空) ❌ 单边熊市(不断买入亏损) ❌ 极端波动(网格无法适应)
1. 趋势判断 + 动态开关
1
2
3
4
// 用MACD判断趋势,趋势明确时关闭网格
fn should_enable_grid(macd_signal: Signal) -> bool {
matches!(macd_signal, Signal::Neutral | Signal::Reversal)
}
2. 波动率自适应ATR系数
1
2
3
4
5
6
// 低波动期加大系数,高波动期减小系数
fn adaptive_atr_multiplier(volatility: f64) -> f64 {
if volatility < 0.02 { 0.8 }
else if volatility > 0.05 { 0.3 }
else { 0.5 }
}
3. 止损保护
1
2
3
4
// 价格跌破网格下限且持续下跌时止损
fn check_stop_loss(current_price: f64, grid_min_price: f64) -> bool {
current_price < grid_min_price * 0.95 // 跌破5%时止损
}
网格交易是震荡市中最稳健的策略之一,但Python实现的性能瓶颈限制了其潜力。通过Rust+Polars重构,我们实现了:
性能提升:
策略优化:
实战要点:
最后提醒:网格策略不是神话,它只是一个在特定市场环境下有效的工具。理解它的原理、局限性和适用场景,才能发挥它的真正价值。
下篇文章预告:《从零构建Rust量化策略框架:DataLoader → Signal → Execution》 我们将构建一个完整的可扩展策略框架,实现策略的热插拔和模块化设计
点赞、在看、转发,让更多人了解网格交易! 🚀