如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)·进阶篇
(2/3)· 多数交易者只把 CHV 当副图看看,却没把它接进自己的系统跑过回测
把波动率指标拖进图表不等于拥有波动率交易系统。蔡金波动率算出来的穿越信号,若没经过自定义编码和策略测试,只是视觉噪声。本文承接基础概念,直接进到怎么把它写成指标、怎么搭系统。
「Chaikin 波动率指标的初始化与缓冲绑定」
下面这段 MT5 自定义指标代码,干的是 Chaikin Volatility 的底层装配:先把平滑周期、Chaikin 周期从输入参数接进来,遇到非法值(≤0)就回退到 10 并打日志,这是实盘里最容易踩的坑——外部加载时参数填错不会崩,但曲线会悄悄用默认值重画。 代码里用 SetIndexBuffer 把三个数组绑到指标槽:0 号是输出曲线 chvBuffer,1 号和 2 号(hlBuffer、shlBuffer)标成 INDICATOR_CALCULATIONS,只参与中间计算不在副图绘制。PlotIndexSetInteger 里 PLOT_DRAW_BEGIN 设为 smoothPeriod+chvPeriod-1,意味着前 19 根 K 线(默认双 10)不画点,新手常误以为指标“延迟”其实是起点被砍了。 OnCalculate 头部先算 posCHV = chvPeriod+smoothPeriod-2,若 rates_total 小于这个数直接 return(0),说明数据不够就别算。prev_calculated<1 时 pos 从 0 重跑,否则只补算最后一根,这是 MT5 指标避免重复计算的标配写法,改周期时 CPU 占用差异能差出数倍。 hlBuffer[i]=High[i]-Low[i] 这一行在循环里只算了真实波幅(最高减最低),还没做平滑,真正的 Chaikin 差值在后续段落。外汇与贵金属波动剧烈,这类波动率指标仅反映历史摆动幅度,不代表未来方向,使用需自担高风险。
class="type">class="kw">double shlBuffer[]; class="type">int smoothPeriod,chvPeriod; class="type">void OnInit() { class="type">class="kw">string maName; if(InpSmoothType==SMA) maName="SMA"; else maName="EMA"; if(smoothPeriodInp<=class="num">0) { smoothPeriod=class="num">10; printf("Incorrect value for Smoothing Period input = %d. Default value = %d.",smoothPeriodInp,smoothPeriod); } else smoothPeriod=smoothPeriodInp; if(chvPeriodInp<=class="num">0) { chvPeriod=class="num">10; printf("Incorrect value for Chaikin Volatility Period input = %d. Default value = %d.",chvPeriodInp,chvPeriod); } else chvPeriod=chvPeriodInp; SetIndexBuffer(class="num">0,chvBuffer); SetIndexBuffer(class="num">1,hlBuffer,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">2,shlBuffer,INDICATOR_CALCULATIONS); PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,smoothPeriod+chvPeriod-class="num">1); PlotIndexSetString(class="num">0,PLOT_LABEL,"CHV("+class="type">class="kw">string(smoothPeriod)+","+maName+")"); IndicatorSetString(INDICATOR_SHORTNAME,"Chaikin Volatility("+class="type">class="kw">string(smoothPeriod)+","+maName+")"); IndicatorSetInteger(INDICATOR_DIGITS,class="num">1); } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &Time[], const class="type">class="kw">double &Open[], const class="type">class="kw">double &High[], const class="type">class="kw">double &Low[], const class="type">class="kw">double &Close[], const class="type">long &TickVolume[], const class="type">long &Volume[], const class="type">int &Spread[]) { class="type">int i,pos,posCHV; posCHV=chvPeriod+smoothPeriod-class="num">2; if(rates_total<posCHV) class="kw">return(class="num">0); if(prev_calculated<class="num">1) pos=class="num">0; else pos=prev_calculated-class="num">1; for(i=pos;i<rates_total && !IsStopped();i++) { hlBuffer[i]=High[i]-Low[i]; } if(pos<smoothPeriod-class="num">1) { pos=smoothPeriod-class="num">1;
◍ 波动幅度变化率的缓冲区填充逻辑
这段计算核心在 OnCalculate 尾部:先按平滑类型把高低区间均值写入 shlBuffer,SMA 走 SimpleMAOnBuffer,其余走 ExponentialMAOnBuffer,平滑周期由 smoothPeriod 控制。 随后把计算起点对齐到 posCHV,避免通道未成型时算出失真值。若 pos 小于 posCHV 则强制拉齐,保证后面变化率序列连续。 真正的变化率写在最后那个 for 循环里:chvBuffer[i] = 100.0*(shlBuffer[i]-shlBuffer[i-chvPeriod])/shlBuffer[i-chvPeriod],即当前平滑区间高度相对 chvPeriod 根之前的百分比变异。分母为零时直接置 0.0,防止除零崩指标。外汇与贵金属波动跳变频繁,该序列仅描述历史幅度变化,不预示方向,实盘须自担高风险。 循环带 !IsStopped() 中断判断,MT5 用户把 chvPeriod 从默认改到 20~30 之间,能直观对比小周期与日线级波动扩张节奏。
for(i=class="num">0;i<pos;i++) { shlBuffer[i]=class="num">0.0; } } if(InpSmoothType==SMA) { SimpleMAOnBuffer(rates_total,prev_calculated,class="num">0,smoothPeriod,hlBuffer,shlBuffer); } else ExponentialMAOnBuffer(rates_total,prev_calculated,class="num">0,smoothPeriod,hlBuffer,shlBuffer); if(pos<posCHV) { pos=posCHV; } for(i=pos;i<rates_total && !IsStopped();i++) { if(shlBuffer[i-chvPeriod]!=class="num">0.0) chvBuffer[i]=class="num">100.0*(shlBuffer[i]-shlBuffer[i-chvPeriod])/shlBuffer[i-chvPeriod]; else chvBuffer[i]=class="num">0.0; } class="kw">return(rates_total); }
用蔡金波动率给方向加一道滤网
蔡金波动率(CHV)本身只刻画近期高低价区间的扩张与收缩,把它的数值过零当作多空分界,是最朴素的用法:CHV>0 直接买,CHV<0 直接卖。这种纯穿越逻辑在 MT5 里可以写进 EA 自动跑,但裸信号在震荡市容易被来回扫。 加一条移动平均线做方向过滤,能砍掉一部分逆势单。规则很具体:CHV 上穿 0 且收盘价站在 MA 上方才买入;CHV 下穿 0 且收盘价落在 MA 下方才卖出。相当于用均线确认波动放大发生在顺势一侧。 外汇与贵金属杠杆高、跳空频繁,CHV 基于历史区间,对突发消息滞后,任何信号都只是概率倾向,实盘前务必用策略测试器跑十年以上 Tick 数据看回撤。 两个版本的伪规则对照: CHV>0 => 买入 CHV<0 => 卖出 CHV>0 且收盘价>MA => 买入 CHV<0 且收盘价<MA => 卖出 打开 MT5 把 CHV 周期设成默认 10,MA 用 20 周期收盘价,先跑 2020—2024 的 XAUUSD H1,看加滤网后交易次数和最大回撤的变化再决定要不要用。
「蔡金波动率EA的两种实战写法」
先把蔡金波动率(CHV)做成可视化底座:用 iCustom 抓自定义指标句柄,OnTick 里 CopyBuffer 取 3 根 K 线的缓存,ArraySetAsSeries 反序后把 chvInd[0] 归一化到 1 位小数,直接 Comment 打印到图表。和原指标叠图对比,打印值若与 CHV 曲线读数一致,说明句柄和缓冲区没接错,再往下写交易逻辑才稳。 纯 CHV 穿越的第一版 EA 很直白:CHV 当前值大于 0 就买、小于 0 就卖。2023 年全年 EURUSD 一小时周期回测,净利润是 -35936.34 USD,最大回撤 48.12%,夏普 -1.22,六个核心指标全绿不起来,属于典型裸信号过拟合外样本就崩。外汇与贵金属杠杆高,这种回撤幅度实盘可能直接洗掉账户。 加一条移动平均线做方向过滤后画风变了。买条件改成:前收低于前 MA、现收高于现 MA 且 CHV>0;卖条件镜像处理。同周期同品种回测净利润翻正到 20817.39 USD,回撤压到 9.62%,盈利系数 1.15,夏普 1.71。MA 过滤把逆势噪音砍掉大半,概率上更站得住。 别把正态当圣经 回测盈利不等于实盘能复制,EURUSD 一小时这一年样本只有 251 个交易日,参数窗口薄。开 MT5 把 smoothPeriodInp 和 chvPeriodInp 从 10 调到 5~20 扫一遍,看净利润和回撤是否同步恶化,再决定跟不跟这套。
enum SmoothMethod { SMA=class="num">0,class=class="str">"cmt">// Simple MA EMA=class="num">1 class=class="str">"cmt">// Exponential MA }; input class="type">int InpSmoothPeriod=class="num">10; class=class="str">"cmt">// Smoothing period input class="type">int InpCHVPeriod=class="num">10; class=class="str">"cmt">// Chaikin Volatility period input SmoothMethod InpSmoothType=EMA; class=class="str">"cmt">// Smoothing method class="type">int chv; chv = iCustom(_Symbol,PERIOD_CURRENT,"Custom_CHV",smoothPeriodInp,chvPeriodInp, smoothTypeInp); Print("EA is removed"); class="type">class="kw">double chvInd[]; CopyBuffer(chv,class="num">0,class="num">0,class="num">3,chvInd); ArraySetAsSeries(chvInd,true); class="type">class="kw">double chvVal = NormalizeDouble(chvInd[class="num">0], class="num">1); Comment("CHV value = ",chvVal); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| chvVal.mq5 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum SmoothMethod { SMA=class="num">0,class=class="str">"cmt">// Simple MA EMA=class="num">1 class=class="str">"cmt">// Exponential MA }; input class="type">int smoothPeriodInp=class="num">10; class=class="str">"cmt">// Smoothing period input class="type">int chvPeriodInp=class="num">10; class=class="str">"cmt">// Chaikin Volatility period
◍ 把 CHV 读数接进 EA 下单逻辑
上面这段把 Chaikin Volatility 自定义指标直接挂进 EA 生命周期。OnInit 里用 iCustom 绑定 "Custom_CHV",传入平滑周期 10、CHV 周期 10 与 EMA 模式,句柄存到 chv 变量,MT5 终端编译后能在导航树里看到该 EA 依赖这个指标文件。 OnTick 只在新 K 线出现时跑计算:用 CopyBuffer 取指标缓冲前 3 根,ArraySetAsSeries 置为时间倒序,chvVal 取最新值、chvValPrev 取前一根,均保留 1 位小数。当 chvVal 大于 0 时进入分支,这里就是后续挂单的触发口。 输入参数里 lotSize=1、slPips=300、tpPips=600 已写死进 iCustom 调用,意味着指标内部可能直接处理交易。外汇与贵金属杠杆高,300 点止损在黄金上可能瞬间被扫,实盘前务必把 lotSize 改小到 0.01 验证。 让小布替你跑这套:开 MT5 把 Custom_CHV 编译好,新建 EA 粘贴下面代码,切到 XAUUSD 的 M15 观察 Comment 输出的 CHV value 是否随波动率跳变,再决定是否补完大于 0 的买入分支。
input SmoothMethod smoothTypeInp=EMA; class=class="str">"cmt">// Smoothing method class="type">int chv; class="type">int OnInit() { chv = iCustom(_Symbol,PERIOD_CURRENT,"Custom_CHV",smoothPeriodInp,chvPeriodInp, smoothTypeInp); class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { Print("EA is removed"); } class="type">void OnTick() { class="type">class="kw">double chvInd[]; CopyBuffer(chv,class="num">0,class="num">0,class="num">3,chvInd); ArraySetAsSeries(chvInd,true); class="type">class="kw">double chvVal = NormalizeDouble(chvInd[class="num">0], class="num">1); Comment("CHV value = ",chvVal); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| chvCrossover.mq5 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <trade/trade.mqh> enum smoothMode { SMA=class="num">0,class=class="str">"cmt">// Simple MA EMA=class="num">1 class=class="str">"cmt">// Exponential MA }; input class="type">int smoothPeriodInp=class="num">10; class=class="str">"cmt">// Smoothing period input class="type">int chvPeriodInp=class="num">10; class=class="str">"cmt">// Chaikin Volatility period input smoothMode smoothTypeInp=EMA; class=class="str">"cmt">// Smoothing Mode input class="type">class="kw">double lotSize=class="num">1; input class="type">class="kw">double slPips = class="num">300; input class="type">class="kw">double tpPips = class="num">600; class="type">int chv; class="type">int barsTotal; CTrade trade; class="type">int OnInit() { barsTotal=iBars(_Symbol,PERIOD_CURRENT); chv = iCustom(_Symbol,PERIOD_CURRENT,"Custom_CHV",smoothPeriodInp,chvPeriodInp, smoothTypeInp, lotSize, slPips, tpPips); class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { Print("EA is removed"); } class="type">void OnTick() { class="type">int bars=iBars(_Symbol,PERIOD_CURRENT); if(barsTotal < bars) { barsTotal=bars; class="type">class="kw">double chvInd[]; CopyBuffer(chv,class="num">0,class="num">0,class="num">3,chvInd); ArraySetAsSeries(chvInd,true); class="type">class="kw">double chvVal = NormalizeDouble(chvInd[class="num">0], class="num">1); class="type">class="kw">double chvValPrev = NormalizeDouble(chvInd[class="num">1], class="num">1); if(chvVal>class="num">0) {