如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)·进阶篇
📊

如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)·进阶篇

(2/3)· 多数交易者只把 CHV 当副图看看,却没把它接进自己的系统跑过回测

偏理论 第 2/3 篇

把波动率指标拖进图表不等于拥有波动率交易系统。蔡金波动率算出来的穿越信号,若没经过自定义编码和策略测试,只是视觉噪声。本文承接基础概念,直接进到怎么把它写成指标、怎么搭系统。

「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 差值在后续段落。外汇与贵金属波动剧烈,这类波动率指标仅反映历史摆动幅度,不代表未来方向,使用需自担高风险。

MQL5 / C++
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 之间,能直观对比小周期与日线级波动扩张节奏。

MQL5 / C++
    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 扫一遍,看净利润和回撤是否同步恶化,再决定跟不跟这套。

MQL5 / C++
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 的买入分支。

MQL5 / C++
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)
        {
把回测监控交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 CHV 穿越与波动率突变的实时标注,你只管判断要不要跟。

常见问题

CHV 本质衡量一定周期内高低价区间的变动率,区间快速扩张通常意味着波动放大,可能预示趋势加速或反转。
原版参数固定,自定义编码能改周期与平滑方式,让指标更贴合你的品种和周期,属于偏理论到实战的桥接。
概率上震荡市假信号偏多,需结合其他过滤条件,没有一种策略适合所有环境。
小布盯盘提供 CHV 相关信号标注与品种页诊断,但回测仍需你在 MT5 内用历史数据验证,外汇贵金属高风险须自担。
波动率结构随时段变化,同一套参数在趋势和盘整中结果可能迥异,自行跨品种跨周期测试才能知边界。