滚动标准差与RSV自适应网格交易策略及回测
简介《一种自动调整网格交易策略中网格大小的方法》是一份面向网格交易初学者与Python量化分析爱好者的方法文档重点解决固定网格在极端行情中容易过早满仓或空仓的问题。文档围绕波动率自适应展开以收盘价、最高价和最低价的标准差衡量市场波动波动剧烈时放大网格、波动平缓时缩小网格并进一步将买入与卖出网格差异化使网格尺寸与KDJ中的RSV值挂钩上升趋势偏买、下降趋势偏卖。文中还用初始网格0.5元、RSV为0.85时买入网格0.15元、卖出网格0.85元的例子说明参数如何影响交易节奏与仓位管理。资源包仅含1个docx文件约165KB内容集中、便于通读也便于对照自身策略做笔记。已有6377人学习适合希望把统计学思路、技术指标和网格交易结合到股票分析软件并降低回撤风险的读者参考。1. 网格大小写死是原罪一份把「标准差 RSV」塞进网格的文档网格交易写成 python 代码不到三十行跑起来却在第三个月翻车——网格大小是写死的。价格在 7 到 15 元之间震荡时5 毛一格很舒服一旦波动放大价格两天走完三个格子母仓早早满上或早早清空剩下的行情只能干看。这份《一种自动调整网格交易策略中网格大小的方法》文档做的就是把这个写死的数字变成每天自己会动的变量用收盘价的滚动标准差定当期的网格基准宽度再用 KDJ 里的 RSV 把这一格拆成不对称的买入格和卖出格。它不解决网格交易资金利用率只有一半的老毛病专门解决「大涨早卖光、大跌早满仓」这个更要命的问题。适合已经会 pandas、想在自己账户上把网格跑起来的人也适合拿它当量化分析的练手项目。2. 用滚动标准差定基准宽度把布林带的「带宽」借过来网格交易的本质是「赚价格来回走的距离」那么网格宽度就应该等于「这段时间价格大概能来回走多远」。固定 5 毛的问题不在于 5 毛这个数不对而在于它假设了「来回距离」永远不变。文档给出的第一个改法是把布林带指标背后的那个量——收盘价的滚动标准差——拿过来当尺子。2.1 为什么标准差比固定百分比和 ATR 更贴合网格先把几种常见的「网格尺子」摆在一起比选型理由就清楚了网格尺子计算方式优点翻车点固定百分比grid 股价 × 3%参数少好解释新手友好波动率切换时整套参数失效ATR真实波幅的移动均值含跳空止损场景常用把跳空当成波动网格被撑大收盘价标准差σ(close, n)直接对应「来回走多远」对单边趋势不敏感只度量离散度高低价差标准差σ(high-low, n)贴近日内振幅隔夜跳空信息丢失噪声大选收盘价标准差有两个具体理由。第一网格成交看的是收盘价附近的挂单区间用收盘价的离散程度去度量「价格能晃多远」量纲和语义都对得上ATR 里的跳空对网格来说是「一次性跨过好几格」把它算进波动里会让网格偏大反而更容易错过中间的成交。第二布林带的上下轨就是均值 ± 2σ带宽 4σ 本身就是一个现成的波动率可视化工具文档把这个带宽直接借来当网格尺子逻辑上不需要额外发明指标。提示标准差度量的是一段时间内的离散程度不是方向。当标的走出单边趋势时标准差会同步放大网格跟着变宽这正好是想要的效果——趋势行情里本来就该少折腾。2.2 两条实现路径绝对值缩放与相对缩放把标准差变成网格宽度有两条路。第一条是绝对值路线grid c × σ(close, n)c 是一个手调系数。问题是 c 的量纲不干净——同样的 c1.0用在 8 元的标的和 80 元的标的上网格宽度差十倍换一个标的就得重新调参。第二条是相对缩放路线我更倾向这条先给一个交易者手调的初始网格base_grid比如 0.5 元再用「当前标准差 / 长期平均标准差」这个无量纲比值去缩放它。grid_base_t base_grid × clip(σ_now / σ_mean, 0.5, 2.0)这样做的价值在于交易者对「5 毛一格」有直觉base_grid保留了这个直觉而缩放系数只负责回答「现在的波动是常态的几倍」。历史回测里即使标的从 10 元涨到 30 元缩放系数仍然在 0.5 到 2.0 之间不会因为价格水平漂移而失效。窗口 n 的取法有讲究。n20 约等于一个月的交易日是起点n 小于 10单根异常 K 线就能把标准差顶上去网格会跟着抽风n 大于 60波动率已经切换完了标准差还在慢慢爬反应迟钝。我一般先用 20再在 15 到 30 之间扫一遍看成交次数的稳定性。缩放的截断区间[0.5, 2.0]是防止极端行情把网格拉到没有意义的大小一路缩到 0.1 倍一格只有几分钱A 股最小变动单位就把策略卡死了一路放到 5 倍一格比一个月振幅还大等于停止交易。2.3 Python 落地从 K 线算到每日网格宽度import numpy as np import pandas as pd def add_grid_base(df, base_grid, n20, lookback120, lower_ratio0.5, upper_ratio2.0, floor_pct0.008, cap_pct0.12): df: 至少含 open/high/low/close索引为交易日务必用前复权数据 base_grid: 交易者手调的初始网格宽度元例如 0.50 n: 标准差窗口20 约等于一个月交易日 lookback: 计算「常态波动」的历史长度120 约半年 lower_ratio/upper_ratio: 缩放系数的截断区间 floor_pct/cap_pct: 网格宽度占股价比例的硬上下限 out df.copy() # 1) 收盘价滚动标准差代表近 n 日价格的离散程度 out[std_close] out[close].rolling(n, min_periodsn).std() # 2) 与自身长期均值相比得到无量纲的波动缩放系数 std_mean out[std_close].rolling(lookback, min_periodsn).mean() out[vol_scale] (out[std_close] / std_mean).clip(lower_ratio, upper_ratio) # 3) 缩放后得到当期网格基准宽度 out[grid_base] base_grid * out[vol_scale] # 4) 用股价比例做第二层硬约束挡住低价股网格过密、高价股网格过疏 out[grid_base] out[grid_base].clip( lowerout[close] * floor_pct, upperout[close] * cap_pct ) return out这段代码的关键在两层约束的先后顺序。第一层是相对缩放回答「现在比平时波动大还是小」第二层是绝对比例限制回答「这个宽度在物理上合不合理」。floor_pct0.008意味着网格不小于股价的 0.8%对于一只 12 元的标的下限约 0.096 元刚好是 0.01 元最小变动单位的近十倍还能正常成交cap_pct0.12意味着网格不超过股价的 12%再宽就失去网格的意义了。min_periodsn不能省。默认的min_periods1会让序列开头的几行用不足 n 个样本算出标准差数值偏小回测第一周的网格宽度全是错的这种错误还特别隐蔽——dropna()之后看不出任何异常。lookback120的均值窗口和n20的标准差窗口不冲突前者是分母的平滑长度后者是分子的度量长度两者分开调一个控制敏感度一个控制基准的稳定性。跑完之后先别急着接回测把grid_base和close画在同一张图上扫一眼如果grid_base是一条几乎水平的线说明缩放系数被lookback均值稀释掉了把lookback降到 60 试试如果它天天大幅上下跳说明n太小或者没有截断先加截断再看。3. RSV 拆买卖网格同一个 0.5 元为什么买是 0.15、卖是 0.85网格基准宽度解决了「一格多大」但网格还有一个隐含假设向上走一格和向下走一格距离相等。文档的第二个改法就是打破这个假设——卖出格和买入格不等长两者的长度由 KDJ 指标计算中用到的 RSV 值决定。3.1 RSV 到底在描述什么以及它和网格的错位RSV 的定义很朴素RSV (收盘价 - N 日最低价) / (N 日最高价 - N 日最低价)它的取值天然落在 0 到 1 之间描述的是一件很具体的事当前收盘价在过去 N 天的价格区间里站在什么位置。收盘价贴着区间上沿RSV 趋近 1贴着下沿RSV 趋近 0。文档里用 N9也就是 KDJ 的默认参数。这里有个容易被忽略的错位网格交易本质上是逆势的——跌了买、涨了卖赚的是均值回归的钱而 RSV 携带的是趋势信息。把 RSV 塞进网格不是要把网格改成趋势策略而是让网格的「两把尺子」不等长从而让持仓重心随趋势缓慢漂移。RSV 区间趋势含义买入格占总宽比例卖出格占总宽比例策略实际行为0.85 ~ 1.00强势上行0.150.85跌一点点就加涨很多才减0.60 ~ 0.85温和上行0.15 ~ 0.400.60 ~ 0.85偏买减仓节奏放慢0.40 ~ 0.60中性震荡0.40 ~ 0.600.40 ~ 0.60接近对称网格0.15 ~ 0.40温和下行0.60 ~ 0.850.15 ~ 0.40偏卖加仓节奏放慢0.00 ~ 0.15单边下跌0.850.15涨一点点就减跌很多才加看第一行和最后一行会发现这个设计其实很反直觉强势上行时买入格变小意思是「稍微回调就买」持仓越涨越多单边下跌时卖出格变小意思是「稍微反抽就卖」持仓越跌越少。它对冲的正是文档开头点出的那个痛点——大涨时早早卖光大跌时早早满仓。3.2 拆分公式与母仓重心的漂移拆分的约束是「买卖格之和恒定」买入格 2 × grid_base × (1 - RSV) 卖出格 2 × grid_base × RSV把文档里的例子代进去验算一遍。grid_base 0.5总宽2 × 0.5 1.0当前 RSV 0.85买入格 1.0 × (1 − 0.85) 0.15 元卖出格 1.0 × 0.85 0.85 元也就是股价从当前锚点回落 0.15 元就加一笔上涨 0.85 元才减一笔。RSV 从 0.85 掉到 0.3同样的 0.5 元基准宽度买入格变成 0.70 元、卖出格变成 0.30 元节奏立刻反过来。这个公式有一个好性质总和恒定意味着「网格的疏密程度」没有被 RSV 改变改变的只是重心的偏移方向。波动率管的是网格多宽RSV 管的是网格往哪边偏两个变量正交调参时不会互相打架。代价也必须说清楚。单边下跌里 RSV 会长时间贴在 0.05 附近卖出格只有 0.05 元理论上「稍微反抽就能减仓」但真实下跌中反抽幅度经常不足以覆盖手续费于是仓位僵在高位网格变成了钝刀子。grid_base越大的标的这个问题越明显。所以这个策略必须配一条仓位上限不能指望 RSV 自己在极端行情里救命。注意RSV 在窄幅横盘里会在 0.5 上下反复穿越买入格和卖出格天天换长度。不做平滑的话网格宽度会出现一日三变回测里表现为成交记录杂乱、单次利润被手续费吃光。3.3 代码RSV 计算、平滑与死区def add_rsv_split(df, n9, smooth3, dead_zone0.05, max_step0.30): n: RSV 的极值窗口9 对应 KDJ 默认参数 smooth: RSV 的 EMA 平滑跨度压掉单根异常 K 线造成的尖刺 dead_zone: 死区宽度|RSV-0.5| 小于它时按中性处理 max_step: RSV 单日变化上限防止网格宽度一夜翻倍 out df.copy() llv out[low].rolling(n, min_periodsn).min() hhv out[high].rolling(n, min_periodsn).max() span (hhv - llv).replace(0, np.nan) # 一字板/停牌会让分母为 0 rsv_raw (out[close] - llv) / span # 1) EMA 平滑 rsv_s rsv_raw.ewm(spansmooth, adjustFalse).mean().fillna(0.5) # 2) 死区RSV 在 0.5 附近摆动时视为中性买卖格等长 rsv_eff rsv_s.where((rsv_s - 0.5).abs() dead_zone, 0.5) # 3) 变化率限幅先取前一日值再夹住今日值 prev rsv_eff.ffill().shift(1).fillna(0.5) rsv_eff rsv_eff.clip(lowerprev - max_step, upperprev max_step) out[rsv] rsv_eff total 2 * out[grid_base] out[buy_grid] total * (1 - out[rsv]) out[sell_grid] total * out[rsv] return out三个防护措施的动机各不一样。EMA 平滑针对的是极值法本身的脆弱性9 日窗口里只要出现一根长下影线llv就会被压到很低RSV 随之中枢上移第二天这根 K 线滑出窗口RSV 又掉回来。死区针对的是横盘震荡把 |RSV−0.5| 0.05 的区间压平成 0.5等价于在中间地带退回标准对称网格少做无用功。变化率限幅针对的是跳变单日 RSV 从 0.2 跳到 0.9网格宽度瞬间反转这种跳变在真实行情里几乎都是数据或极值窗口造成的假信号max_step0.30相当于强制它分三天走完。span. replace(0, np.nan)这一行是防御性的。一字涨停或长期停牌会让 N 日最高价等于最低价分母为 0除出来是 inf后面所有计算全部污染。换成 NaN 之后fillna(0.5)会把它当中性处理不会炸掉整列。4. 把自适应网格跑一遍回测成交次数、单次利润、空仓天数参数算出来只是纸面数字能不能用必须在事件循环里跑一遍。这一段给一个最小可用的回测骨架重点不在收益率而在把「自适应到底改了什么」变成可观察的指标。4.1 最小可回测事件循环def run_adaptive_grid(df, cash200_000, lot100, fee_rate0.0003, stamp_rate0.001, warmup140): df: 已包含 grid_base / buy_grid / sell_grid 列 cash: 初始资金 lot: 最小交易单位A 股为 100 股 warmup: 预热天数要大于 n lookback 里的较大者 pos 0 anchor None # 上一次成交价只在成交时更新 trades [] equity [] for i in range(warmup, len(df)): row df.iloc[i] px row[close] bg, sg row[buy_grid], row[sell_grid] if anchor is None: anchor px # 首个有效日只建锚点不交易 elif px anchor - bg: qty int(cash * 0.1 / px // lot) * lot # 每格投入总资金 10% if qty lot and qty * px * (1 fee_rate) cash: cash - qty * px * (1 fee_rate) pos qty anchor px trades.append((df.index[i], B, px, qty, bg, sg)) elif px anchor sg and pos lot: qty int(pos * 0.1 // lot) * lot # 每次卖出持仓 10% if qty lot: cash qty * px * (1 - fee_rate - stamp_rate) pos - qty anchor px trades.append((df.index[i], S, px, qty, bg, sg)) equity.append(cash pos * px) return (pd.DataFrame(trades, columns[date, side, price, qty, buy_grid, sell_grid]), pd.Series(equity, indexdf.index[warmup:]))anchor是这个循环里最重要的变量也是最容易写错的地方。它代表「上一次成交的价格」只在成交时更新。如果偷懒写成每根 K 线都用px重算触发价价格会追着价格跑——今天跌到anchor - bg买入明天锚点跟着最新价下移新的买入门槛又变成「当前价再跌 bg」于是永远等不到。回测跑完几百个交易日一笔不成交八成是这个原因。qty用int(... // lot) * lot而不是直接round是为了向下取整到整手。//之后乘lot这两步不能省否则会算出 137 股这种在 A 股下不出去的委托。每次买入用cash * 0.1卖出用pos * 0.1是让每笔交易的大小随账户自动缩放避免早期买满后期没钱加仓。4.2 回测里最容易搞错的三个设置第一个是复权。所有价格必须用前复权序列标准差和 RSV 都建立在连续价格上。用不复权数据除权日会出现一个凭空跳空标准差瞬间拉大、RSV 直接打到 0 或 1网格宽度跟着错乱回测里表现为除权日附近一连串匪夷所思的成交。第二个是成本。fee_rate0.0003只覆盖佣金stamp_rate0.001是卖出印花税两者都按成交金额算。网格策略的成交次数远高于买入持有成本对结果的侵蚀非常明显同一段样本成交 40 次和成交 200 次扣掉成本后的差距可能比策略本身的边际改进还大。第三个是warmup。n20加lookback120前 140 个交易日里grid_base是 NaN 或者由不足样本算出必须跳过。把warmup设小了回测第一周的交易记录全是假的。4.3 评价指标与参数敏感性网格策略不能只看总收益要看四个指标成交次数策略是否真的在工作、单次平均利润每次交易扣完成本还剩多少、最大回撤极端行情里的实际损失、空仓天数占比仓位是否长期偏离目标。下面这张表是同一段震荡样本上的相对趋势不是绝对数值换标的必须重跑标准差窗口 n缩放上限成交次数单次均利最大回撤空仓天数占比102.0高低偏大偏低202.0中中中中201.5中中偏小偏高402.0低高偏小偏高403.0低高大低读这张表的方法n 越小网格越贴波动成交越频繁单次利润被手续费压得越薄缩放上限越大极端行情里网格放得越开回撤压得住但空仓或满仓的时间也会拉长。我一般选 n20、上限 2.0 作为基线再用 n40、上限 1.5 做一次对照两个配置的成交次数差不超过 50%说明参数不敏感可以放心上差出两三倍说明这段样本的波动率结构不稳定换标的或者换区间再验。5. 避坑与排查五条把自适应网格跑崩的血泪经验自适应逻辑最大的问题是它会「安静地出错」——参数算出来了回测跑完了曲线看着也还行但某个环节错了你从收益曲线上完全看不出来。所以排查的第一原则是先定位现象再怀疑参数。5.1 先看现象再改参数一份排查顺序表现象第一嫌疑先查什么回测几百个交易日零成交锚点每根 K 线都被重算打印 anchor 序列看是否恒等于当日收盘网格宽度日间跳三倍以上RSV 未平滑或未限幅统计 RSV 单日变化量的分布除权日附近连续异常成交用了不复权数据对比前复权与原始价格序列算出来的买入手数为 0网格小于最小变动单位或资金不足一手打印 grid_base/close 与单手金额单边下跌中仓位不减反增RSV 贴 0卖出格极小但反抽不触发看 pos 与 close 的相关性5.2 五条踩坑记录第一条回测零成交锚点漂了。现象是跑完一年一个信号都没有日志里anchor每天等于当日收盘价。原因是触发判断写在了anchor px之后或者每根 K 线都无条件更新锚点。解决方式是把anchor的更新严格锁在成交分支里并在循环外加一句断言非交易日的anchor必须与前一日相同。加了这个断言之后同类问题再没出现过。第二条除权日买卖格突然翻倍。某只标的一次分红除权不复权序列上出现 6% 的向下跳空std_close一夜放大三倍vol_scale顶到上限网格宽度直接翻倍同时 RSV 被打到 0卖出格缩到最小。两个效应叠加回测里在该日连续买入。原因是数据源默认给不复权价。解决办法是在数据入口统一做前复权并在加载函数里加一条校验单日涨跌幅绝对值超过 11% 的打印告警。第三条横盘时网格宽度一日三变。现象是buy_grid在 0.18 和 0.82 之间反复横跳成交记录里密集出现小额买卖扣完成本几乎不赚钱。原因是 9 日极值窗口对单根长影线过于敏感RSV 在 0.5 附近高频穿越。解决是加上 EMA 平滑、死区和变化率限幅三层防护。参数上smooth3和dead_zone0.05是我试出来比较稳的一组smooth1等于没平滑smooth10会让 RSV 滞后到趋势结束才反应。第四条买入手数算成 0。低价股加上网格下限floor_pct0.008一只 5 元的标的网格下限只有 0.04 元一手 500 元而每格投入的资金是cash * 0.1。如果初始资金只有 3 万每格 3000 元一手都买不起// lot之后得 0策略静默地不执行任何买入也不会报错。解决办法是在回测入口加一次启动校验如果cash * 0.1 close * lot直接抛异常别让它跑完一年再告诉你没交易。第五条单边下跌里仓位僵在高位。现象是标的从高位跌了 40%pos几乎没降。原因是 RSV 长期贴在 0.05 附近卖出格被压到 0.02 元理论上「稍微反抽就卖」但反抽幅度经常连手续费都盖不住实际成交几笔就停了。这是这套方法的结构性边界不是 bug。解决办法是加一条独立于 RSV 的仓位上限当pos超过初始母仓的 1.5 倍时暂停买入分支只允许卖出。这条规则写在网格逻辑之外不要试图用调 RSV 参数来解决。6. 进阶用波动率分位替代绝对系数让网格宽度自己找尺度base_grid手调最大的问题是它会随标的和时段失效。同一只标的去年给它 0.5 元很合适今年波动率整体下了一个台阶0.5 元就显得太宽成交次数掉一半。与其反复手调不如让当前波动率在全历史里的分位来决定缩放系数——这就是把「当前标准差是长期均值的几倍」换成「当前标准差处在历史的什么位置」。def quantile_scale(df, n20, window250, knots(0.1, 0.5, 0.9), mults(0.6, 1.0, 1.8)): 按波动率分位映射缩放系数替代固定的放大缩小 knots: 分位点0.1 表示历史 10% 分位 mults: 对应的缩放倍数必须与 knots 等长 out df.copy() out[std_pct] out[close].rolling(n, min_periodsn).std() / out[close] # 用扩张窗口算历史分位避免未来函数 q_lo out[std_pct].expanding(window).quantile(knots[0]) q_mid out[std_pct].expanding(window).quantile(knots[1]) q_hi out[std_pct].expanding(window).quantile(knots[2]) scale np.select( [out[std_pct] q_lo, out[std_pct] q_mid, out[std_pct] q_hi], [mults[0], mults[1], mults[2]], defaultmults[2] * 1.4 # 超过 90% 分位再放大一档 ) out[vol_scale_q] scale out[grid_base] (out[close] * out[std_pct] * 12) * scale return out这里有两处刻意的设计。第一处是expanding(...).quantile(...)用扩张窗口而不是滚动窗口算分位避免引入未来信息——滚动窗口在计算第 t 行的分位时会用到 t 之后的数据回测结果会虚高实盘直接打回原形。第二处是映射曲线做成阶梯状而不是线性的mults(0.6, 1.0, 1.8)加上超过 90% 分位后再乘 1.4是为了让网格宽度在波动率切换时有明确的档位感而不是连续微调连续微调会让grid_base每天都在变成交价被不断挪动实盘里的滑点会明显放大。验证这套映射有没有用不看收益看三个数网格宽度的日变化率分布|Δgrid|/grid超过 20% 的交易日占比应该低于 5%、成交次数的月度标准差应该比手调base_grid时小、以及仓位重心与价格趋势的相关系数自适应版本应该显著为正说明它确实在跟着趋势调整重仓方向。第三个指标最能说明问题如果相关系数接近 0说明 RSV 的效果被别的环节吃掉了回去检查拆分那一步是不是用了未平滑的原始 RSV。我自己的习惯是每次改完参数先把这三个数打出来贴在回测报告最前面再去看收益曲线。有段时间我跳过了这一步直接看收益率结果一个expanding写成rolling的 bug 让回测好看了整整 18%实盘上线的第一个月就被打回原形。从那以后我每次改波动率相关的代码都强制把分位计算的窗口类型和一行断言跑一遍确认它没有偷看未来。希望帮到你。本文还有配套的精品资源点击获取