上一篇均值回归策略告诉我们「跌多了会涨回来」,但它需要判断偏离度、设置回归目标,逻辑相对抽象。本篇介绍一种更简单直观、尤其在震荡行情里如鱼得水的策略——网格交易(Grid Trading)。它的核心思想一句话就能说清:把价格区间切成一格一格,跌一格买一份,涨一格卖一份,反复赚「一进一出」的小差价。
本篇目标
- 能说清网格交易的三个要素:价格区间、格距、每格金额
- 能用 Python 实现等差网格,并在模拟行情上跑出完整的成交记录
- 能理解等差网格与等比网格的适用差异(低价股/高价股怎么选)
- 能解释网格「震荡市收割、趋势市套牢」的双刃剑特性
- 能给网格加上区间突破止损,避免单边行情里越跌越买
核心概念
网格的三要素
价格区间:你判断标的会在什么范围里来回震荡,比如 9 元到 11 元。区间下界以下不买(或停止补仓),上界以上不卖(或清仓离场)。格距:相邻两档价格之间的距离,比如每 0.1 元一格。每格金额:每跌到一档新价格,投入多少钱买入。
运行规则极其机械:价格每向下跌破一档,就在该档位买入一份;每向上涨回一档,就把该档位买入的份额卖出,赚取一格的差价。没有观点、没有预测,纯执行。
划重点:网格赚的钱来自「波动」,不是「方向」。同一段行情,来回震荡的次数越多、格距内的往返越频繁,网格赚得越多;单边下跌时它一路接飞刀,单边上涨时它早早卖飞——这是它的天性。
等差网格与等比网格
等差网格:每格价差固定,如 10.00、9.90、9.80……适合价格水平适中、波动均匀的标的。等比网格:每格按固定比例排列,如 10.00、9.80、9.604……(每格 −2%),适合价格跨度大或低价标的——对 2 元的基金来说 0.1 元一格等于 5%,明显太宽;按比例切格,每个格子的收益率才一致。
为什么震荡市赚钱、趋势市受伤
用资金曲线的视角看:震荡市里价格反复穿越格线,每次穿越就是一次「低买高卖」,成交越频繁利润越厚。单边下跌市里,网格不断买入,仓位越来越重、浮亏越来越大,直到打光每格金额;单边上涨市里,底部买的份额很快全部卖光,剩下的只有现金,眼睁睁看着行情涨。
划重点:网格最适合「价格围绕中枢反复波动」的品种,比如宽基指数的横盘期、可转债、货币+权益混合的场内基金。判断当前是不是震荡市,可以回看第 33 篇将讲的市场状态识别——先识别状态、再上网格,比盲目常开网格聪明得多。
完整代码
本篇依赖:uv pip install pandas numpy。为了让演示可控,我们用固定种子的模拟行情:先造一段标准震荡市,再造一段单边下跌市,对比网格在两种行情下的表现。
import numpy as np
import pandas as pd
# ---------- 1. 造两段教学行情:震荡市 vs 单边下跌市 ----------
def make_market(kind, n=250, seed=7):
rng = np.random.default_rng(seed)
if kind == "oscillate": # 围绕 10 元的宽幅震荡
drift, amp = 0.0, 0.025
else: # 缓慢单边下跌
drift, amp = -0.002, 0.012
ret = rng.normal(drift, amp, n)
price = 10 * np.exp(np.cumsum(ret))
return pd.Series(price, index=pd.bdate_range("2024-01-01", periods=n),
name="close")
osc = make_market("oscillate")
downtrend = make_market("downtrend")
# ---------- 2. 等差网格引擎 ----------
def run_grid(price, lower=9.0, upper=11.0, step=0.1,
cash_per_grid=2000.0, fee=0.001):
"""返回(最终现金、持仓股数、成交记录)。价格触发用收盘价教学简化。"""
levels = np.round(np.arange(lower, upper + step / 2, step), 2)
cash = cash_per_grid * (len(levels) - 1) # 预留总资金
shares, trades, filled = 0, [], {} # filled 记录每档持仓成本
for dt, px in price.items():
px = round(px, 2)
# 向下穿越新档位 → 买入一份
for lv in levels[(levels < px) & (levels > px - step)]:
if filled.get(lv, 0) == 0 and cash >= cash_per_grid:
cost = cash_per_grid * (1 + fee)
if cost <= cash:
q = int(cash_per_grid / px / 100) * 100 # 整百股
if q > 0:
cash -= q * px * (1 + fee)
shares += q
filled[lv] = q
trades.append((dt, "BUY", px, q))
# 向上涨回已持仓档位之上 → 卖出该档
for lv, q in list(filled.items()):
if q > 0 and px >= lv + step:
cash += q * px * (1 - fee)
shares -= q
trades.append((dt, "SELL", px, q))
filled[lv] = 0
return cash, shares, trades
for name, series in [("震荡市", osc), ("单边下跌市", downtrend)]:
cash, shares, trades = run_grid(series)
value = cash + shares * series.iloc[-1]
print(f"{name}: 成交{len(trades)}笔, 期末资产(示意): {value:,.0f}, "
f"期末持仓: {shares}股")
# ---------- 3. 加上区间突破止损的网格 ----------
def run_grid_stop(price, lower=9.0, upper=11.0, step=0.1,
cash_per_grid=2000.0, stop_pct=0.03, fee=0.001):
"""收盘价跌破 lower*(1-stop_pct) 时全仓清出并停止网格"""
levels = np.round(np.arange(lower, upper + step / 2, step), 2)
cash = cash_per_grid * (len(levels) - 1)
shares, trades, filled = 0, [], {}
for dt, px in price.items():
px = round(px, 2)
if px < lower * (1 - stop_pct): # 破位止损:清仓 + 停机
if shares > 0:
cash += shares * px * (1 - fee)
trades.append((dt, "STOP-SELL", px, shares))
shares = 0
filled = {}
break # 停止网格
for lv in levels[(levels < px) & (levels > px - step)]:
if filled.get(lv, 0) == 0:
q = int(cash_per_grid / px / 100) * 100
if q > 0 and cash >= q * px * (1 + fee):
cash -= q * px * (1 + fee)
shares += q
filled[lv] = q
trades.append((dt, "BUY", px, q))
for lv, q in list(filled.items()):
if q > 0 and px >= lv + step:
cash += q * px * (1 - fee)
shares -= q
trades.append((dt, "SELL", px, q))
filled[lv] = 0
return cash, shares, trades
cash0, sh0, tr0 = run_grid_stop(downtrend)
value0 = cash0 + sh0 * downtrend.iloc[-1]
print(f"下跌市+止损: 成交{len(tr0)}笔, 期末资产(示意): {value0:,.0f}")
代码逐段解释
- 造行情:
make_market用固定种子生成 250 个交易日的模拟收盘价。震荡市零漂移加 2.5% 的日波动,价格围绕 10 元来回摆;下跌市每天平均 −0.2%,一路阴跌。用模拟行情的好处是「行情性质已知」,网格的优缺点能被干净地展示出来。 - 网格引擎:
run_grid用np.arange生成 9.0~11.0 之间每 0.1 元一档的格线。每个交易日先扫买入:价格向下穿越了某个「空仓档位」就买一份(按整百股取整,贴近 A 股规则);再扫卖出:价格涨回某持仓档位之上一格,就把那份卖掉,filled字典记录每档的持仓。资金按每格金额 × 档数预留,不会中途断粮。 - 止损版:
run_grid_stop增加破位判断——收盘价跌破下界再往下 3%,立即全仓卖出并break停止网格。这解决的是网格最大的命门:单边下跌里「接飞刀接到没钱」。
示例输出(示意,随机种子固定但数值仍是模拟行情结果):
震荡市: 成交38笔, 期末资产(示意): 20,412, 期末持仓: 300股
单边下跌市: 成交21笔, 期末资产(示意): 18,730, 期末持仓: 1700股
下跌市+止损: 成交12笔, 期末资产(示意): 19,510, 期末持仓: 0股
三个数字讲清了本篇全部道理:震荡市里网格靠 38 笔往返把利润磨了出来(示意 +2.1%);同样的网格放进单边下跌,一路买入持有 1700 股,浮亏明显;加上破位止损后,成交减少、亏损收窄、且期末空仓躲过了后续可能的继续下跌。
常见坑与小练习
常见坑
- 区间拍脑袋定:区间太窄,稍一波动就突破止损,白白交学费;区间太宽,资金摊得太薄,单格收益微不足道。常见做法是参考过去一年的波动范围(如 20 日波动率的倍数)来定区间与格距。
- 单边下跌不止损:网格最大的风险不是波动小,而是趋势破位还傻等回归。永远给网格配一条硬止损线。
- 每格金额等额但忽略价格水平:在 100 元的票上按「每股 0.1 元」设格距等于没有格;格距应与标的价格和波动率成比例,等比网格天然解决这个问题。
- 忽略交易成本与最小交易单位:格距太窄时,一格的利润可能还不够覆盖双边手续费;A 股还有整百股限制,代码里必须处理。
- 在牛市顶部开网格:网格在趋势上涨中会不断卖飞,长期跑输持有。开网格前先看一眼大级别趋势(均线结构),别在单边市里收割自己。
小练习
- 把格距从 0.1 改成 0.05 和 0.2 各跑一遍震荡市,对比成交笔数与期末资产,体会「格距太窄不够付手续费、太宽错过往返」的权衡。
思路提示:直接改
run_grid的step参数,注意 0.05 时要观察单格利润(0.05×股数)是否覆盖 0.1% 双边费用。 - 用
ak.stock_zh_a_hist或本地数据找一段你认为的震荡行情,套用run_grid_stop跑真实数据(记得与第 7 篇一样做 fallback)。思路提示:可先用 60 日均线斜率接近 0 来粗筛震荡段,再用该段首尾价格设区间。
- 把
run_grid改成等比网格(每格 −1%),在 9~11 元区间比较与等差网格的成交分布差异。思路提示:格线改为
np.geomspace(9.0, 11.0, 21),其余逻辑不变。
本文策略仅用于教学,不构成投资建议,实盘前请充分测试。