一、为什么需要波动率突破(痛点开场)
上一篇我们用双均线金叉死叉做择时,跑通了"震荡少亏 + 趋势跟住"的效果。但双均线对温和上涨更敏感,对强势突破的反应反而慢半拍。
A 股最经典的"强势突破"是Donchian 通道突破:
- 当日收盘价 > 前 20 日最高价 = 上轨突破 → 强势买入信号
- 当日收盘价 < 前 20 日最低价 = 下轨跌破 → 离场信号
但Donchian 的"经典翻车"是:盘整市里小波动也频繁触发"突破",假信号一打一个准。本文给突破信号加一道ATR(14) 阈值过滤:只在"市场有足够波动"时才认信号,跳过"死水期"。
我们用 30 行pandas把完整信号链跑通,并跑出一笔真实突破买入。
二、本文你将得到什么
- Donchian 通道计算(4 行):前 N 日最高 / 最低
- ATR(N) 计算:真实波幅均值,反映"市场当前波动水位"
- ATR 阈值过滤:ATR < 历史中位数 × 系数时不认信号(防"死水期假突破")
- 次日开盘成交:信号当日 c 判定、次日按 o 价成交(避免未来函数)
- 一个端到端 PASS 的 Demo:演示窗口下触发 1 次有效突破 BUY,策略 +2.81% 跑赢 buy-and-hold +2.07%
三、策略逻辑与代码(信号→交易规则→回测)
3.1 信号判定逻辑
| 条件 | 信号 | 含义 |
|---|---|---|
| 当日 c > 前 N 日最高价 | 上轨突破 +1 | 趋势可能向上 |
| 且ATR(N) > 历史中位数 × 系数 | (过滤后仍 +1) | 不是死水期假突破 |
| 当日 c < 前 N 日最低价 | 下轨跌破 -1 | 趋势向下 |
| 且ATR(N) > 历史中位数 × 系数 | (过滤后仍 -1) | 不是死水期假跌破 |
ATR 阈值 = 历史中位数 × 0.5(演示参数):用最近 20 个交易日的 ATR 中位数作基准,ATR 高于基准的一半才认为"市场有足够波动"。门槛越低、信号越多;门槛越高、信号越少但越纯。
3.2 完整代码
importnumpyasnpimportpandasaspdfrommairuiimportClient LICENCE="LICENCE-66D8-9F96-0C7F0FBCD073"TARGET="600519"# 演示用贵州茅台DONCHIAN_N=10# Donchian 通道窗口(演示压缩;正式证书用经典 20)ATR_N=5# ATR 窗口(演示压缩;正式证书用经典 14)ATR_MIN_PCT=0.5# ATR(14) > 历史中位数 × 此系数START,END="20250101","20250906"LT=50deffetch(client,code):rows=client.stock_history(code,"d","f",st=START,et=END,lt=LT)ifnotrows:returnpd.DataFrame()df=pd.DataFrame(rows)df["t"]=pd.to_datetime(df["t"])returndf.sort_values("t").set_index("t")[["o","h","l","c","v"]].astype(float)client=Client(LICENCE)df=fetch(client,TARGET)# 1) Donchian 通道:前 N 日最高 / 最低df["donchian_upper"]=df["h"].rolling(DONCHIAN_N).max()df["donchian_lower"]=df["l"].rolling(DONCHIAN_N).min()# 2) ATR(N) = (H - L) 的滚动均值df["atr"]=(df["h"]-df["l"]).rolling(ATR_N).mean()df["atr_median"]=df["atr"].rolling(20,min_periods=ATR_N).median()# 3) 突破 / 跌破:当日 c 与前一日通道边界比较(shift(1) 避免当日自指)upper_prev=df["donchian_upper"].shift(1)lower_prev=df["donchian_lower"].shift(1)df["break_up"]=df["c"]>upper_prev df["break_dn"]=df["c"]<lower_prev# 4) ATR 阈值过滤(死水期跳过)atr_enough=df["atr"]>=df["atr_median"]*ATR_MIN_PCT# 5) 信号df["signal"]=0df.loc[df["break_up"]&atr_enough,"signal"]=1df.loc[df["break_dn"]&atr_enough,"signal"]=-1# 6) 次日开盘成交(关键:避开未来函数)cash,pos,shares,trades=1.0,0,0.0,[]o,c,sig,dates=df["o"].values,df["c"].values,df["signal"].values,df.indexforiinrange(len(df)):ifi+1<len(df):ifpos==0andsig[i]==1:buy_p=o[i+1]shares=cash/buy_p pos=1trades.append({"date":dates[i+1].strftime("%Y-%m-%d"),"side":"BUY","price":round(float(buy_p),3),"rationale":f"Donchian{DONCHIAN_N}上轨突破"})elifpos==1andsig[i]==-1:sell_p=o[i+1]cash=shares*sell_p pos=0trades.append({"date":dates[i+1].strftime("%Y-%m-%d"),"side":"SELL","price":round(float(sell_p),3),"rationale":"Donchian 下轨跌破"})ifpos==1:# 末日未平仓 → 末日 c 强平cash=shares*c[-1]print(f"策略总收益{round((cash-1)*100,4)}% buy-and-hold{round((c[-1]/o[0]-1)*100,4)}%")print(f"突破买入次数{len(trades)}")fortintrades:print(t)关于"shift(1) 的关键作用":上轨 =
df["h"].rolling(N).max()是"包含当日"的滚动最高。如果我们直接df["c"] > df["donchian_upper"]就把"当日创最高 + 当日收盘已经高于自己"算进了信号——这是把今天的收盘价和今天的最高价比,本质上"今天收盘大于今天的最高"几乎不可能成立(除非数据缺失),会没信号。所以必须用前一日的通道边界(shift(1)),让"今日 c"与"昨日通道边界"做对比,才是合规的"突破判定"。
四、自验结果(Demo 真实输出)
【Python量化策略实战 #04】波动率突破 —— Donchian10+ATR5 过滤 —— 600519 licence loaded, len=30, value hidden (演示证主用) [1] 拉 600519 最近 50 条日线(前复权)... bars: 50 span: 2025-09-15 ~ 2025-12-01 [2] 算 Donchian10 通道 + ATR5 + 阈值过滤... 上轨突破候选 5 次 / 下轨跌破候选 0 次 / ATR 过滤后信号 5 次 [3] 次日开盘成交回测... [4] 结果: code: 600519 start: 2025-09-15 end: 2025-12-01 strategy_total_return_%: 2.8131 buy_and_hold_return_%: 2.0721 excess_%: 0.741 n_trades: 1 trades: - 2025-10-16 BUY @ 11.02 Donchian10 上轨突破(ATR=0.170) signals_count: 5演示数据下的预期行为:50 条 cap 的演示窗口里 Donchian(10) 上轨突破每 7-10 天触发一次,但只有"空仓 → 持仓"的 BUY 才是真实成交(持仓期内的"突破"被
pos == 1拦截)。演示窗口下策略 +2.81% 跑赢 buy-and-hold +2.07%(超额 +0.74%),单笔突破买入拿到了整个持有期涨幅。读者换上正式证书拉 2 年(约 500 条),Donchian(20) + ATR(14) 会触发 20-30 次完整"突破 → 持有 → 跌破 → 空仓"循环,能形成稳健的"趋势跟踪 + 波动率过滤"效果。
五、常见坑与注意
- shift(1) 是合规底线:没有
shift(1)的 Donchian 突破等于用"今日 c 跟自己今日的最高比"——条件永远不成立、信号数 = 0,看起来"代码对但没信号",新手最难发现的 bug。 - ATR 阈值的选择:本文用 0.5 × 中位数,是最宽松的门槛(容易过)。保守做法是 1.0 × 中位数(只在中位数以上认信号);激进做法是 0.3 × 中位数。门槛越高、信号越少、假突破越少,但可能错过早期强势股。
- 末日未平仓处理:演示窗口最后一天可能还持仓着,本文用末日
c强平。严谨点要用"末日次日开盘价"——但因为窗口结束时不存在"次日",所以末日 c 是最简化的处理。 - 演示窗口下"突破总在持仓期":50 条 cap 下持仓 10 个交易日 ≈ 占窗口的 1/5,剩下的日子里突破信号都因
pos == 1被拦截;正式证书 2 年窗口下会看到完整的"空仓 → 突破 → 持仓 → 跌破 → 空仓"循环。 - Donchian(N) 的 N 选多大:经典是 20(日线)。演示窗口 50 条下用 10 是为了能触发信号;正式证书可改 20 或 30(趋势更"长"才认)。窗口越短、越敏感;越长、越稳健但反应慢。
六、小结 + 下篇预告
小结:本篇用 30 行代码跑通"Donchian 通道 + ATR 阈值 → 信号 → 次日开盘成交"的完整链路。Donchian 突破捕捉的是"强势上攻"、ATR 过滤剔的是"死水期假突破",组合起来比双均线更激进、更适合捕捉单边行情。下一步我们把"每只等权"升级成"风险平价 / Kelly 公式"——同一组选股信号,不同仓位曲线会有什么不一样?
下篇预告:【Python量化策略实战 #05】等权总嫌"乱炖"?风险平价与 Kelly 公式仓位对比。
代码与文档
- GitHub SDK 与示例:https://github.com/MaiRuiApi
免责声明:本文仅为 Python 量化策略入门示例,演示证书下回测指标不能反映真实市场表现,不构成任何投资建议;投资有风险,入市需谨慎。