如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)·综合运用
📊

如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)·综合运用

(3/3)·把CHV指标、穿越策略与系统测试串成一条可跑的交易流水线

偏理论进阶 第 3/3 篇
很多交易者把CHV直接拖进图表就当信号用,却从没验证过它在自己品种上的失效边界。波动率工具一旦脱离具体系统与测试环境,很容易变成自我安慰的噪音源。

用 CHV 信号挂买卖单的止损止盈算法

这段逻辑依赖一个自定义指标 Custom_CHV 的输出值 chvVal,当它大于 0 时倾向做多,小于 0 时倾向做空。外汇与贵金属杠杆高,信号失效时回撤可能快速吃掉本金,实盘前务必在 MT5 策略测试器跑历史数据。 做多时先取当前卖价 SymbolInfoDouble(_Symbol,SYMBOL_ASK),止损放在 ask - slPips*_Point,止盈放在 ask + tpPips*_Point。默认 slPips=300、tpPips=600,也就是用 300 点止损博 600 点空间,风险回报比约 1:2。 做空则反过来取买价 SYMBOL_BID,止损在 bid + slPips*_Point,止盈在 bid - tpPips*_Point。注意多空止损都在现价外侧,避免点差瞬间扫损。 chvVal 与 chvValPrev 都做了 NormalizeDouble(...,1) 保留一位小数,复制缓冲时 ArraySetAsSeries(chvInd,true) 让索引 0 是最新柱。这样 chvInd[0] 与 chvInd[1] 直接对应今根与昨根信号。

MQL5 / C++
class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
class="type">class="kw">double slVal = ask - slPips*_Point;
class="type">class="kw">double tpVal = ask + tpPips*_Point;
trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
}
if(chvVal<class="num">0)
  {
  class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
  class="type">class="kw">double slVal = bid + slPips*_Point;
  class="type">class="kw">double tpVal = bid - tpPips*_Point;
  trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
  }

◍ 把 CHV 与均线叠进 EA 的初始化和触发

这段逻辑把 Chaikin Volatility(CHV)和一条移动均线塞进同一个 EA,用新 K 线事件驱动交易判定。参数层先暴露平滑周期 10、CHV 周期 10、均线周期 10 与 EMA 模式,止损 300 点、止盈 600 点,仓位写死 1 手——外汇与贵金属杠杆高,固定手数在极端波动下可能直接击穿账户净值。 OnInit 里用 iCustom 挂自编的 Custom_CHV,iMA 取收盘价 EMA,并记下当前 barsTotal。OnTick 只在 barsTotal 小于实时 bars 时跑,也就是每根新柱才处理一次,避免同根 K 线内重复发单。 触发条件是:前一根收盘低于前一根均线、最新收盘上穿均线、且 CHV 当前值大于 0。满足后按实时 ask 计算 sl/tp 并 trade.Buy。CHV>0 代表波动率扩张,和均线拐头共振时,价格向上突破的概率倾向更高,但是否成交仍取决于盘口流动性。 下面这段是可直接贴进 MT5 编辑器验证的源码骨架,注意 iCustom 的参数顺序必须和 Custom_CHV 指标内部 input 对齐,否则 CopyBuffer 取到的会是空序列。

MQL5 / C++
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 smoothMode smoothTypeInp=EMA;   class=class="str">"cmt">// Smoothing Mode
input class="type">int InpMAPeriod=class="num">10; class=class="str">"cmt">//MA Period
input ENUM_MA_METHOD InpMAMode=MODE_EMA; class=class="str">"cmt">// MA 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 ma;
class="type">int barsTotal;
CTrade trade;
class="type">int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   chv = iCustom(_Symbol,PERIOD_CURRENT,"Custom_CHV",InpSmoothPeriod,InpCHVPeriod, smoothTypeInp, lotSize, slPips, tpPips);
   ma=iMA(_Symbol,PERIOD_CURRENT, InpMAPeriod, class="num">0, InpMAMode, PRICE_CLOSE);
   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[];
      class="type">class="kw">double maInd[];
      CopyBuffer(chv,class="num">0,class="num">0,class="num">3,chvInd);
      ArraySetAsSeries(chvInd,true);
      CopyBuffer(ma,class="num">0,class="num">0,class="num">3,maInd);
      ArraySetAsSeries(maInd,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);
      class="type">class="kw">double maVal = NormalizeDouble(maInd[class="num">0], class="num">5);
      class="type">class="kw">double maValPrev = NormalizeDouble(maInd[class="num">1], class="num">5);
      class="type">class="kw">double lastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">1);
      class="type">class="kw">double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">2);
      if(prevLastClose<maValPrev && lastClose>maVal && chvVal>class="num">0)
        {
         class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         class="type">class="kw">double slVal = ask - slPips*_Point;
         class="type">class="kw">double tpVal = ask + tpPips*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }

「均线和收盘价交叉的双向触发写法」

这段逻辑把均线穿越和动量方向绑在一起做进出场:前一根收盘在均线上方、当前收盘掉到均线下方、且动量值 chvVal 小于 0,三个条件同时成立才允许 sell;反过来前收在均线下方、当前收站上均线、chvVal 大于 0 才 buy。这样能过滤掉单纯毛刺穿越带来的假信号,但外汇和贵金属杠杆高,信号失效时回撤可能很快扩大,实盘前务必在 MT5 策略测试器里跑一遍。 下方代码先取当前图表均线句柄,用 CopyBuffer 拉 3 根值并把数组设为时间序列,再拿最近两根收盘价做对比。注意 maInd[0] 是当前柱均线、maInd[1] 是上一根,iClose 的 shift 参数 1 是上一根收盘、2 是再前一根,别写反。 [CODE] int ma; ma=iMA(_Symbol,PERIOD_CURRENT, InpMAPeriod, 0, InpMAMode, PRICE_CLOSE); double maInd[]; CopyBuffer(ma,0,0,3,maInd); ArraySetAsSeries(maInd,true); double maVal = NormalizeDouble(maInd[0], 5); double maValPrev = NormalizeDouble(maInd[1], 5); double lastClose=iClose(_Symbol,PERIOD_CURRENT,1); double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,2); if(prevLastClose<maValPrev && lastClose>maVal && chvVal>0) { double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double slVal = ask - slPips*_Point; double tpVal = ask + tpPips*_Point; trade.Buy(lotSize,_Symbol,ask,slVal,tpVal); } if(prevLastClose>maValPrev && lastClose<maVal && chvVal<0) { double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); double slVal = bid + slPips*_Point; double tpVal = bid - tpPips*_Point; trade.Sell(lotSize,_Symbol,bid,slVal,tpVal); } [/CODE] 逐行拆一下:iMA 拿句柄时 PERIOD_CURRENT 表示用当前周期,InpMAPeriod 和 InpMAMode 是外部输入的周期数与平滑方式。CopyBuffer 第三个参数 0 是从当前柱开始、取 3 个值。ArraySetAsSeries 设 true 后索引 0 才是最新值。NormalizeDouble(...,5) 把均线四舍五入到 5 位小数,XAUUSD 这类报 3 位价的品种也得统一精度才好比大小。 开仓时 sell 用 SYMBOL_BID 做进场价,sl 放在 bid 上方 slPips*_Point、tp 在 bid 下方;buy 反之用 SYMBOL_ASK,sl 在 ask 下方。slPips / tpPips 单位都是点,乘以 _Point 自动适配品种小数位,黄金和欧美不会混。

MQL5 / C++
class="type">int ma;
ma=iMA(_Symbol,PERIOD_CURRENT, InpMAPeriod, class="num">0, InpMAMode, PRICE_CLOSE);
class="type">class="kw">double maInd[];
CopyBuffer(ma,class="num">0,class="num">0,class="num">3,maInd);
ArraySetAsSeries(maInd,true);
class="type">class="kw">double maVal = NormalizeDouble(maInd[class="num">0], class="num">5);
class="type">class="kw">double maValPrev = NormalizeDouble(maInd[class="num">1], class="num">5);
class="type">class="kw">double lastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">1);
class="type">class="kw">double prevLastClose=iClose(_Symbol,PERIOD_CURRENT,class="num">2);
if(prevLastClose<maValPrev && lastClose>maVal && chvVal>class="num">0)
  {
   class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   class="type">class="kw">double slVal = ask - slPips*_Point;
   class="type">class="kw">double tpVal = ask + tpPips*_Point;
   trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
  }
if(prevLastClose>maValPrev && lastClose<maVal && chvVal<class="num">0)
  {
   class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   class="type">class="kw">double slVal = bid + slPips*_Point;
   class="type">class="kw">double tpVal = bid - tpPips*_Point;
   trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
  }

把CHV接进你自己的EA里跑一遍

前面三篇把CHV指标的算法、MQL5自定义实现和两种穿越策略都拆完了,这一节只说落地。你已经能在MT5里挂上Custom_CHV.mq5,再用chvCrossover.mq5或chv_MA_Crossover.mq5跑策略测试器;ZIP里四个文件总大小不到10KB,下载解压直接编译即可。 回测时建议把CHV周期从默认14调到20,欧美盘波动率结构差异明显,参数偏移后信号频率可能下降但假突破概率倾向降低。外汇与贵金属杠杆高,任何基于历史波动率的系统都只代表过去,实盘前必须用策略测试器做样本外验证。 剩下能做的,是把移动平均线周期也一并优化,或者换RSI做过滤。改进没有终点,CHV只是给你一个量化波动的锚,真正边际优势来自你反复跑、反复调的那几轮。

让小布替你跑这套波动率回测
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到CHV区间与穿越提示,把重复劳动交给小布,你专注决策。

常见问题

CHV基于高低价区间的指数移动均值差,更敏感于区间扩张收缩;ATR取真实波幅均值,偏绝对幅度。两者可互补,但CHV对反转前压缩更敏锐。
会。缩短周期提升灵敏度却增加假穿越,拉长周期滤噪但滞后。建议在历史分段中按品种特性标定,而非统一套用。
贵金属跳空与时段波动结构特殊,MA平滑会吞掉CHV尖峰。概率上更倾向用CHV自身零轴穿越而非叠加慢线。
目前小布内置标准CHV与常用变体,自建mq5需经本地编译后挂品种页;AIGC层可读取输出值做预警,不替代你的策略逻辑。
不够。外汇贵金属高风险,不同杠杆与时段结构差异大,至少跨趋势与震荡两段样本,才可能看出CHV系统适用边界。