网格交易

ZaynPei Lv6

网格交易法 (Grid Trading) 是一种在震荡行情中表现出色的“自动化高抛低吸”策略,其核心思想与追涨杀跌的趋势策略完全相反,非常考验交易者对市场波动区间的判断。

它的哲学非常直观:它不预测市场的未来方向,而是假设价格将在一个特定的区间内来回波动

  • 策略行为: 交易者预先设定一个价格区间(由压力位和支撑位定义),并在这个区间内密布多个预设的价格线(网格线)。

    • 当价格下跌并触及下方的某个节点时,就买入一份头寸。

    • 当价格上涨并触及上方的某个节点时,就卖出一份头寸。

  • 盈利模式: 策略的利润来自于在低价节点买入、在高价节点卖出这一过程的不断重复。每一次成功的“一买一卖”循环,就锁定了一份由网格间距决定的利润。

  • 左侧交易: 这是一种典型的左侧交易。它在价格下跌、看似“危险”时买入;在价格上涨、看似“乐观”时卖出。它总是在“着短期的小趋势操作,赌的是价格会回归到区间的中心

风险与适用场景

最大优势: 在横盘震荡市 (Ranging Market) 中,网格交易法如同永动机一般,能够持续不断地从价格的无序波动中榨取利润。

最大风险: 其“阿喀琉斯之踵”是强烈的单边趋势行情 (Trending Market)。

  • 在持续的单边下跌中,策略会不断地在更低的价格买入、买入、再买入,导致积累大量浮动亏损的多头头寸,俗称“接飞刀”。

  • 在持续的单边上涨中,策略会不断地卖出、卖出、再卖出,最终可能耗尽持仓并开始建立亏损的空头头寸,导致“踏空”并转为逆势做空。

因此,成功运用网格交易的关键在于正确判断当前市场处于震荡状态,并设定一个合理的波动区间。

策略逻辑

网格交易策略的实现可以分为以下几个步骤:

第一步:设定网格

  • 价格中枢: 策略以前一交易日的收盘价作为当天网格的中心基准。这个中枢价在当天是固定不变的。
    • 也可以用其他方法确定中枢价,比如前一日的最高价和最低价的均值,或者更复杂的技术指标。
  • 网格线: 在中枢价的基础上,按固定的百分比(如 ±1%, ±2%, ±3%)向上和向下扩展,形成一系列的网格线。这构成了一个等比例的网格系统。
    • 也可以用固定的点数间隔(如每隔10点)来设定网格线,具体取决于标的的价格水平和波动性, 甚至可以用ATR等波动率指标来动态调整网格间距。
  • 网格区域: 相邻的两条网格线之间形成一个“区域”或“格子”。

第二步:交易规则

  • 核心原则: 当价格从一个网格区域移动到另一个相邻的网格区域时,触发一次交易。

  • 向上移动 (例如,从区域3进入区域4): 卖出 m 手合约。如果原先持有多仓,则为平仓;如果原先为空仓或空头,则为开/加空仓。

  • 向下移动 (例如,从区域4进入区域3): 买入 m 手合约。如果原先持有空仓,则为平仓;如果原先为空仓或多头,则为开/加多仓。

举例分析

在行情震荡上涨时: alt text 假设格子之间的差为1元钱,每变化一个格子相应的买入或卖出1手,则通过网格交易当前账户的净收益为6元,持空仓4手,持仓均价为12.5元。

行情震荡下跌时: alt text 同理可知,净收益为8元,持4手多仓,平均成本为7.5元。

可以看到,无论行情上涨还是下跌,已平仓的部分均为正收益,未平仓的部分需要等下一个信号出现再触发交易。

即使网格交易能够获得较为稳定的收益,但也存在一定的风险。如果行情呈现大涨或大跌趋势,会导致不断开仓,增加风险敞口。这也是为什么网格交易更适用震荡行情,不合适趋势性行情。

代码示例

下面的代码示例展示了如何实现一个简单的网格交易策略:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# coding=utf-8
from __future__ import print_function, absolute_import, unicode_literals
import numpy as np
import pandas as pd
from gm.api import *
'''
本策略标的为:SHFE.rb1901
价格中枢设定为:前一交易日的收盘价
从阻力位到压力位分别为:1.03 * open、1.02 * open、1.01 * open、open、0.99 * open、0.98 * open、0.97 * open
每变动一个网格,交易量变化100个单位
回测数据为:SHFE.rb1901的1min数据
回测时间为:2017-07-01 08:00:00到2017-10-01 16:00:00
'''
def init(context):
# 策略标的为SHFE.rb1901
context.symbol = 'SHFE.rb1901'
# 订阅SHFE.rb1901, bar频率为1min
subscribe(symbols = context.symbol, frequency='60s')
# 设置每变动一格,增减的数量(下单的手数)
context.volume = 1
# 储存前一个网格所处区间,用来和最新网格所处区间作比较
context.last_grid = 0
# 以前一日的收盘价为中枢价格, history_n函数用于获取历史数据, 第[0]条返回形式为[{'open': 3500, 'high': 3520, 'low': 3480, 'close': 3510, 'volume': 1000}], 取close字段即为收盘价
context.center = history_n(symbol= context.symbol,frequency='1d',end_time=context.now,count = 1,fields = 'close')[0]['close']
# 记录上一次交易时网格范围的变化情况(例如从4区到5区,记为4,5)
context.grid_change_last = [0,0]

# bar 是一种 K 线数据,也就是把一段时间内的市场行情汇总成一条数据,而不是每笔成交的 tick。它反映了某个时间周期内的开盘、收盘、最高、最低价格和成交量。可以是 1分钟/5分钟/1小时/1天的 K 线
def on_bar(context, bars): # on_bar函数是策略的核心函数, 每当有新的bar数据到达时, 框架会自动调用这个函数, 并把最新的bar数据传入bars参数
# 获取最新的bar数据, bars是一个列表, 每个元素是一个Bar对象, 代表一个标的的bar数据. 这里我们只订阅了一个标的, 所以取第一个元素
bar = bars[0]
# 获取多仓仓位
position_long = context.account().position(symbol=context.symbol, side=PositionSide_Long)
# 获取空仓仓位
position_short = context.account().position(symbol=context.symbol, side=PositionSide_Short)

# 设置网格, 以开盘价为中枢价格, 计算网格线得到实际的网格线价格
context.band = np.array([0.97, 0.98, 0.99, 1, 1.01, 1.02, 1.03]) * context.center
# 计算当前价格所处的网格区间, pd.cut函数用于将数据分割成不同的区间(把连续数值分箱(bin)), labels参数用于给每个区间命名, 这里我们用1-6表示6个区间
grid = pd.cut([bar.close], context.band, labels=[1, 2, 3, 4, 5, 6])[0]
# pd.cut 的返回值是一个 Pandas Series 对象,长度和 x 一致. 由于 x=[bar.close] 只有一个元素,所以用 [0] 取出标签值

# 如果价格超出网格设置范围,则提示调节网格宽度和数量. 当价格低于最小网格或高于最大网格时,pd.cut 会返回 NaN
if np.isnan(grid):
print('价格波动超过网格范围,可适当调节网格宽度和数量')

# 如果新的价格所处网格区间和前一个价格所处的网格区间不同,说明触碰到了网格线,需要进行交易
# 如果新网格大于前一天的网格,做空或平多
if context.last_grid < grid:
# 记录新旧格子范围(按照大小排序)
grid_change_new = [context.last_grid,grid]
# 几种例外:
# 当last_grid = 0 时是初始阶段,不构成信号
# 如果此时grid = 3,说明当前价格仅在开盘价之下的3区域中,没有突破网格线
# 如果此时grid = 4,说明当前价格仅在开盘价之上的4区域中,没有突破网格线
if context.last_grid == 0:
context.last_grid = grid
return
if context.last_grid != 0:
# 如果前一次开仓是4-5,这一次是5-4,算是没有突破,不成交
if grid_change_new != context.grid_change_last:
# 更新前一次的数据
context.last_grid = grid
context.grid_change_last = grid_change_new
# 如果有多仓,平多
if position_long:
# 下一单, 平多
order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Sell, order_type=OrderType_Market,
position_effect=PositionEffect_Close)
print('以市价单平多仓{}手'.format(context.volume))
# 否则,做空
if not position_long:
order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Sell, order_type=OrderType_Market,
position_effect=PositionEffect_Open)
print('以市价单开空{}手'.format(context.volume))

# 如果新网格小于前一天的网格,做多或平空
if context.last_grid > grid:
# 记录新旧格子范围(按照大小排序)
grid_change_new = [grid,context.last_grid]
# 几种例外:
# 当last_grid = 0 时是初始阶段,不构成信号
# 如果此时grid = 3,说明当前价格仅在开盘价之下的3区域中,没有突破网格线
# 如果此时grid = 4,说明当前价格仅在开盘价之上的4区域中,没有突破网格线
if context.last_grid == 0:
context.last_grid = grid
return
if context.last_grid != 0:
# 如果前一次开仓是4-5,这一次是5-4,算是没有突破,不成交
if grid_change_new != context.grid_change_last:
# 更新前一次的数据
context.last_grid = grid
context.grid_change_last = grid_change_new
# 如果有空仓,平空
if position_short:
order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Buy,
order_type=OrderType_Market,
position_effect=PositionEffect_Close)
print('以市价单平空仓{}手'.format(context.volume))
# 否则,做多
if not position_short:
order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Buy,
order_type=OrderType_Market,
position_effect=PositionEffect_Open)
print('以市价单开多{}手'.format(context.volume))
# 设计一个止损条件:当持仓量达到10手,全部平仓
if position_short == 10 or position_long == 10:
order_close_all()
print('触发止损,全部平仓')
if __name__ == '__main__':
'''
strategy_id策略ID,由系统生成
filename文件名,请与本文件名保持一致
mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST
token绑定计算机的ID,可在系统设置-密钥管理中生成
backtest_start_time回测开始时间
backtest_end_time回测结束时间
backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
backtest_initial_cash回测初始资金
backtest_commission_ratio回测佣金比例
backtest_slippage_ratio回测滑点比例
'''
run(strategy_id='strategy_id',
filename='main.py',
mode=MODE_BACKTEST,
token='token_id',
backtest_start_time='2018-07-01 08:00:00',
backtest_end_time='2018-10-01 16:00:00',
backtest_adjust=ADJUST_PREV,
backtest_initial_cash=100000,
backtest_commission_ratio=0.0001,
backtest_slippage_ratio=0.0001)

获取数据的bar格式可能如下:

1
2
3
4
5
6
7
8
9
bar = {
'symbol': 'SHFE.rb1901',
'datetime': '2025-10-17 13:30:00',
'open': 3500,
'high': 3520,
'low': 3490,
'close': 3510,
'volume': 120
}

策略难点:

  • 怎样记录价格是否突破网格线?

    • 解决方法:有些人可能会想到用当前价格与网格线对应的价格进行比较,但这样操作比较麻烦,步骤繁琐。这里采用区域判断方式。根据网格线划分网格区域为1、2、3、4、5、6.利用pandas库提供的cut函数,将当前价格所处的网格区域表示出来。当网格区域发生变化,说明价格突破了一个网格线。
  • 如何避免出现4区-5区开仓一次,5区-4区又平仓一次这种“假突破”?

    • 解决方法:4-5开仓一次和5-4平仓一次实际上突破的是一根线,此时的形态是价格沿着这根线上下波动。只有第一次穿过这条线时才是真正的交易信号,其他的并没有形成突破。因此我们需要一个变量储存每一次交易时网格区域的变化形态(按照从大到小的顺序),比如5-4可以记为[4,5],4-5记为[4,5]。当新的记录=旧的记录时,信号失效。