ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python自适应网格交易策略:波动率与RSV动态调参及回测避坑

Python自适应网格交易策略:波动率与RSV动态调参及回测避坑

简介:这份文档聚焦网格交易中网格大小固定容易导致过早满仓或空仓的问题,面向具备一定Python基础、关注量化投资与股票分析的读者,提供一种动态调整网格间距的策略参考。内容以收盘价、最高价和最低价的标准差衡量市场波动,并据此放大或缩小网格;同时把买入网格与卖出网格分开设置,借助RSV值判断趋势强弱,使上升趋势中更易买、下降趋势中更易卖,文中还给出由初始网格计算买卖网格的具体示例。文档进一步说明如何结合波动率与趋势指标调整仓位节奏,减少极端行情下的被动满仓或空仓,并为后续用Python实现相关规则提供思路。资源包共1个docx文件,约165KB,便于集中阅读和检索。已有6377人学习,读者可从中梳理动态网格的计算逻辑、参数关系与策略落地要点,用于完善自己的网格交易规则或Python股票分析实践。

1. 网格交易的自适应网格方案:为什么固定网格大小一定会翻车

做量化策略的人多半都试过网格交易:设好价格区间,跌一格买、涨一格卖,靠波动赚钱。可实盘跑两个月就会发现问题——单边下跌时一路买到底,母仓加满、账户浮亏;单边上涨时早早卖光,后面那一大段行情跟你没关系。这两件事的根子是同一个:网格大小是写死的。价格波动天生有大小年,用一把固定刻度的尺子去量所有行情,翻车是迟早的事。这套方案要解决的就是这个问题:让网格大小跟着波动率和趋势位置自动变化,把「过早满仓」和「过早空仓」这两个老毛病压下去。适合正在做量化方向毕设或课程设计的学生,也适合手里有 Python 基础、想把网格策略真正跑起来的个人开发者。

2. 策略原理与总体设计:两层自适应机制怎么搭

2.1 先从固定网格的两个硬伤说起

固定网格的模型很好理解。假设某只股票长期在 7 元到 15 元之间震荡,以 0.5 元为一格,在 11 元建一定数量的母仓,之后每跌 0.5 元买一份、每涨 0.5 元卖一份,不追求母仓本身的价差,只在来回波动里赚格子钱。只要价格在区间内反复震荡,格数走得越多,累积的差价收益越多;即使十年后价格回到原点,靠波动也能攒出一笔收益。这也是网格策略对新手友好的原因——不需要预测方向。

但它的缺陷同样明显。第一是资金利用率,50 万资金里可能 20 万要压成母仓,剩下的留给下跌加仓,实际动用比例通常只有一半上下,收益率天然被摊薄。第二是极端行情下的失效:大跌时网格一份份被买穿,很快就满仓,后面再跌只能干看着;大涨时网格一份份被卖穿,很快空仓,后面的涨幅一点吃不到。前者是抄底抄在半山腰,后者是下车太早。

第二个问题本质上不是网格策略的错,而是网格大小没有随行情变化。波动剧烈的时候,固定的小格子会被大波浪瞬间穿透;波动平缓的时候,固定的大格子又完全触发不了,白白浪费小波动。所以改进方向很明确:让格子的大小变成一个随行情变动的量。

2.2 两层机制:波动率定总量,RSV 定买卖分配

这套方案的设计分为两层,职责不重叠。

第一层解决「格子该多大」。用近一段时期收盘价、最高价、最低价的标准差来衡量当前波动强度。标准差大,说明价格上下摆得厉害,网格要放大,让策略的容量能吃下大波浪;标准差小,说明行情温吞,网格要缩小,把小起伏也榨出利润。这个思路和布林带宽度随波动变化是同一套逻辑——布林带在剧烈行情里张口,在平静行情里收口,网格大小跟着这个宽度走,就不会在大行情里被一波带走。

第二层解决「买入和卖出该不该一样大」。答案是必须不一样。如果买卖格子相同,母仓数量就是恒定的,趋势一来照样满仓或空仓。方案把第一层算出的网格总量拆成买入格和卖出格两部分,拆分比例由 RSV 决定:

RSV =(收盘价 - 最低价)/(最高价 - 最低价)

RSV 是 KDJ 指标计算过程中的中间量,含义很直白:收盘价在当前价格区间里的相对位置。上涨趋势里收盘价贴着最高价,RSV 趋近 1;下跌趋势里收盘价贴着最低价,RSV 趋近 0。

于是分配规则就出来了。RSV 接近 1 时是上涨趋势,策略应该易买难卖——买入格调小,跌一点点就接;卖出格调大,涨很多才卖,母仓因此能跟着趋势往上漂。RSV 接近 0 时反过来,买入格拉大、卖出格收小,避免下跌途中被密集买穿。举个具体数字:第一层算出网格总量为 1 元(买卖格之和),当前 RSV 为 0.85,则买入格 = 1×(1−0.85) = 0.15 元,卖出格 = 1×0.85 = 0.85 元。落到操作上就是:股价回落一毛五就补一份,上涨八毛五才减一份。

下面这张表把三种行情下的参数取向往清楚里摆一下,方便对照理解:

行情状态波动率(标准差)RSV网格总量买入格卖出格策略意图
剧烈震荡高0.5 附近放大中等中等吃大波浪,防穿透
单边上行中趋近 1中等变小变大易买难卖,母仓上漂
单边下行中趋近 0中等变大变小缓买快卖,防满仓

提示:两层机制的顺序不能颠倒。必须先用波动率定出网格总量,再用 RSV 去拆分,而不是各自独立算一个买入格和卖出格。顺序反了,两个参数会互相打架,回测曲线会变得很难看。

3. 用 Python 把自适应网格算出来:核心模块与代码实现

3.1 数据准备与指标计算模块

整个策略的输入只有三列:收盘价、最高价、最低价。实际工程里通常还会带上日期,方便回测时按时间推进。指标计算模块负责产出两个东西——波动率刻度和 RSV 序列。

波动率用滚动窗口的标准差来算。窗口长度的取法有讲究:太短会让网格频繁跳变,交易成本吃掉利润;太长则反应迟钝,行情已经转向了网格还没调。我一般会取 20 到 30 个交易日这个量级,正好覆盖一个月的交易节奏,跟布林带默认参数的习惯一致。RSV 的窗口同理,用同一段窗口内的最高价和最低价,保证两个指标看到的是同一段行情。

import pandas as pd import numpy as np def calc_indicators(df, window=20): """ df 需包含 close / high / low 三列,按时间升序 window: 滚动窗口长度,默认 20 个交易日 """ # 滚动最高价与最低价,作为 RSV 的分母区间 df['hh'] = df['high'].rolling(window).max() df['ll'] = df['low'].rolling(window).min() # RSV:收盘价在区间中的相对位置,0~1 rng = (df['hh'] - df['ll']).replace(0, np.nan) df['rsv'] = (df['close'] - df['ll']) / rng df['rsv'] = df['rsv'].clip(0, 1) # 防止除零或异常值越界 # 波动率刻度:收盘价滚动标准差,再归一化到 0~1 df['vol_std'] = df['close'].rolling(window).std() vmin, vmax = df['vol_std'].min(), df['vol_std'].max() df['vol_ratio'] = (df['vol_std'] - vmin) / (vmax - vmin) return df.dropna().reset_index(drop=True)

这段代码有三个地方值得说明。rng那一行做了除零保护,行情横盘到最高价等于最低价时,RSV 会变成无穷大,不处理后面全乱。clip(0, 1)是给 RSV 加一道保险,虽然理论上它天然落在 0 到 1 之间,但滚动窗口边界处容易出现毛刺。vol_ratio的归一化是关键一步——标准差是个绝对值,不同价位的股票标准差差好几个量级,直接拿去做网格系数没法统一,归一化到 0 到 1 之后,网格大小的调节倍数才有可比性。

3.2 网格大小的自适应生成

有了vol_ratio和rsv,网格生成就是纯算术。设计上我用一个基准网格base_grid作为锚点,再乘上一个由波动率决定的放大系数。系数区间取 0.5 到 1.5 比较稳妥:波动最小时网格缩到基准的一半,最大时放到一倍半,既能跟上行情,又不至于把格子拉到完全无法触发。

def gen_grid(row, base_grid=0.5, lo=0.5, hi=1.5): """ base_grid: 基准网格大小(元),建仓时按标的价位设定 lo / hi: 网格放大系数的上下限 返回: (买入格, 卖出格, 网格总量) """ # 波动率决定网格总量:vol_ratio 越高,格子越大 scale = lo + (hi - lo) * row['vol_ratio'] total = base_grid * scale # RSV 决定买卖分配:上涨趋势买小卖大 buy_grid = total * (1 - row['rsv']) sell_grid = total * row['rsv'] # 给两侧格子设下限,避免出现 0.01 元这种无法成交的碎格 min_step = base_grid * 0.1 buy_grid = max(buy_grid, min_step) sell_grid = max(sell_grid, min_step) return buy_grid, sell_grid, total

拆解一下参数。base_grid是策略的锚,一般按标的当前价位的一个百分比来定,比如 11 元的股票取 0.5 元,大约是 4.5%,格子密度适中。scale的区间[0.5, 1.5]是经验值,行情特别极端时可以放宽到[0.4, 2.0],但不要更大,否则网格会变成半年才触发一次。min_step那两行看着不起眼,实际是防呆设计,RSV 极端接近 0 或 1 时,一侧格子会被压到极小,没有这个下限,回测里会出现大量金额小到无意义的成交,把手续费占比推高。

还有一个细节:total是买卖格之和,买入格加上卖出格恒等于total,这个恒等关系是整套分配逻辑自洽的前提。回测里可以加一条断言把它守住,一旦两边加起来不等于总量,说明分配代码被改坏了。

3.3 交易信号与持仓管理的落地

网格算出来只是参数,真正决定盈亏的是信号怎么触发、仓位怎么管。工程上的做法是维护一个「上一成交价」的状态,每次新价格进来,和上一成交价比较:跌超过买入格,买一份并把上一成交价更新为当前价;涨超过卖出格,卖一份并同样更新。注意是比较累计跌幅,不是每一根 K 线都重置,否则震荡行情里会反复触发同一格。

def run_grid(df, base_grid=0.5, init_shares=1000, lot=100): """ df: 含 date / close / buy_grid / sell_grid 的数据 init_shares: 初始母仓股数 lot: 单次买卖的最小股数(A 股按手,1 手 = 100 股) """ shares = init_shares last_price = df.iloc[0]['close'] # 上一成交价,初始化为首日收盘 records = [] for _, row in df.iterrows(): price = row['close'] # 价格下跌超过买入格 -> 买入一份 if last_price - price >= row['buy_grid']: shares += lot last_price = price records.append((row['date'], 'BUY', price, shares)) # 价格上涨超过卖出格 -> 卖出一份 elif price - last_price >= row['sell_grid']: shares = max(shares - lot, 0) # 不允许卖成负仓 last_price = price records.append((row['date'], 'SELL', price, shares)) return pd.DataFrame(records, columns=['date', 'action', 'price', 'shares'])

last_price这个状态变量是整套逻辑的核心,它承担了「上次成交锚点」的职责。用>=而不是>,是为了避免浮点误差导致刚好等于格子时漏触发。max(shares - lot, 0)的底线保护必须有,否则空仓状态下来一根大阳线,代码会把仓位卖成负数,回测结果直接失真。lot固定为 100 股是贴合 A 股交易规则的做法,如果换成其他市场,按最小交易单位调整即可。

到这里三个模块就闭环了:指标模块产出vol_ratio和rsv,网格模块把它转成买卖格,信号模块按格子推进持仓。整条链路没有未来函数,每个时点用到的都是当天及之前的数据,这一点在做毕设答辩时最容易被追问,务必在代码里显式体现rolling而非shift(-1)。

4. 回测、参数调优与避坑:几个最容易翻车的地方

4.1 用一套回测框架验证自适应到底有没有用

策略改完不能拿主观感觉判断好坏,得跑出对比数据。回测框架的最小结构是:加载数据、逐日推进、记录每笔成交、最后统计总收益、最大回撤、交易次数、手续费占比这几项。最关键的对比实验是「自适应网格 vs 固定网格」,用同一段行情、同一个初始仓位、同一套手续费规则各跑一遍,看自适应版本的回撤是否收窄、交易次数是否合理。

代码里已经埋好了逐日循环的骨架,补上收益统计就能用。这里的重点是统计口径要统一:初始母仓市值、每笔成交的手续费、期末持仓市值都要计入,只算已实现盈亏会把结论带偏。如果课程设计的篇幅允许,建议再拆成三段行情分别跑——单边上涨段、单边下跌段、横盘震荡段,三段的表现放在一张表里对比,比一条总收益曲线有说服力得多。

4.2 参数调优的正确姿势

可调参数其实只有三个:滚动窗口长度、基准网格base_grid、放大系数区间。调参的常见误区是拿全样本数据去找最优参数,回测漂亮但实盘照崩。更稳妥的做法是滚动调参:用前一段数据定参数,后一段数据做验证,窗口往前推,看参数在不同时间段是否稳定。如果某一组参数只在某一段行情里表现突出、别的段一塌糊涂,说明它拟合了那段行情,不能用。

base_grid的选取还有一个和固定网格不同的注意点:它会随波动率缩放,所以基准值不必追求极精确,大致定在标的价位的 3% 到 6% 之间都合理,真正的调节交给波动率系数。放大系数区间如果想让策略更激进,可以收窄上限,让网格在大行情里也不至于放得太宽;反之求稳就放宽。这类权衡没有标准答案,答辩时能把自己的取舍讲清楚,比给出一组「最优参数」更得分。

4.3 避坑清单:四个我踩过或者见人踩过的坑

现象一:回测收益曲线平滑得不像真的,实盘一跑就打回原形。原因多半是未来函数——用当天收盘价算了指标,又用同一个收盘价成交。解决办法是把信号和成交错开一根 K 线,指标用 T 日数据计算,成交按 T+1 的开盘价或收盘价执行,回测里显式写清这一点。

现象二:交易频率高得离谱,手续费吃掉大半利润。原因是 RSV 在 0.5 附近震荡时,买卖格被分得很小,价格稍微一动就触发。解决办法是给两侧格子加最小间隔,或者加一道冷却期——同一方向两次成交之间至少隔 N 个交易日。前面代码里的min_step就是干这个的。

现象三:单边下跌里照样满仓被套。原因是只做了网格大小的自适应,没有做总仓位上限。网格自适应能延缓满仓时点,但不能阻止满仓。通常的做法是再加一层硬约束:持仓数达到预设上限后停止买入,只保留卖出信号。这个上限按标的的历史最大回撤和自身资金承受力来定,没有一个普适数字。

现象四:横盘行情里策略几乎不动。原因是波动率归一化用了全样本的 min/max,早期数据把区间撑得太大,后期的vol_ratio一直贴地。解决办法是把归一化的参考区间改成滚动窗口,或者用分位数替代极值,让vol_ratio反映的是「相对近期」的波动水平,而不是相对整个历史。

注意:以上四条里,第一条的杀伤力远大于其他三条。回测出现不合理的平滑曲线时,先查未来函数,再查手续费,最后才怀疑策略本身。

5. 验收与答辩:怎么证明这套自适应机制真的有效

验收这件事,学生和从业者的标准其实是一回事:拿得出对比、讲得清取舍、经得起追问。具体到这套方案,我一般会按三步走。

第一步是基线对齐。固定网格作为对照组,参数取行业里最常见的设定——价格区间的固定百分比或者一个整数价位,母仓比例、手续费、回测区间全部与自适应版本保持一致。两组结果放同一张表,至少列出总收益率、最大回撤、成交笔数、手续费占比四项。只要自适应版本在回撤上有可见收窄、成交笔数没有失控,这套机制就算立住了。

验收项合格线说明
回撤收窄相对固定网格下降 15% 以上三段行情分别对比,不能只看总区间
成交笔数不高于固定网格的 2 倍过高说明格子碎、手续费会失控
未来函数零容忍指标与成交必须错开一根 K 线
参数稳定性滚动窗口下表现不发散换一段行情结论不能反转
极端行情有仓位上限兜底单边下跌不满仓、单边上涨不空仓

第二步是极端行情专项。单独截取一段连续下跌和一段连续上涨的行情,把每日持仓股数画出来,看自适应版本是不是比固定版本更晚满仓、更晚空仓。这张持仓曲线图是答辩时最有说服力的一页,因为它直接对应方案要解决的那个问题,评委一眼就能看懂。

第三步是把「为什么」讲在前面。答辩现场最常被问的两个问题是「RSV 为什么能代表趋势」和「波动率窗口为什么取 20」。前者的回答要落到定义上——收盘价在区间中的相对位置,趋势越强越贴边;后者要落到工程取舍上——太短网格跳变、太长反应迟钝,20 天大约一个月的交易节奏是个折中点。能答出「我试过 10 和 30,结果如何」,比背一个标准答案强得多。

源码本身也是一份可以对照的验收材料。这套方案的参考实现是开源可读的 Python 项目,代码结构清晰,指标计算、网格生成、信号触发各成一块,拿来做课程设计的技术底稿或者毕设的实现章节都很合适,对想学量化分析的读者来说,逐行读一遍比自己从零搭要快得多。

说个我自己的教训。早些年做策略验证,我习惯先跑完全样本再回头挑参数,曲线怎么调都好看,实盘一上就露馅。后来改成先切出验证段、回测时绝不碰它,等参数定死才允许跑,虽然麻烦,但心里踏实。从那以后我每次交方案前都强制走一遍「基线对比 + 极端行情 + 滚动验证」这三关,跳一步都不行。希望这套思路能帮到你。

本文还有配套的精品资源,点击获取

返回列表