价格行为分析工具包开发(第三十部分):商品通道指数(CCI)零线的EA·进阶篇
📡

价格行为分析工具包开发(第三十部分):商品通道指数(CCI)零线的EA·进阶篇

(2/3)· 当快速与慢速CCI在零线交叠,EA如何借EMA与ATR滤掉锯齿噪声而非盲目追单

新手友好 第 2/3 篇

不少交易者把CCI上穿零线当成无条件做多指令,结果在震荡市被反复触发止损。零线交叉确实比±100突破更早,但裸信号里的假突破占比高得惊人。本篇接上篇基础,继续把双CCI配合均线波幅过滤的EA逻辑拆开给你看。

「信号触发与风控线的代码落地」

当 CCI 下穿零轴且快线位于零轴下方、价格跌破 EMA 时,系统登记空头信号并附带 ATR 计算的止损距离。这段逻辑把「方向判定」和「仓位边界」绑死在一次触发里,避免人工干预滞后。 dist 的算法很直接:开 ATR 开关就用 atrValue * ATR_Multiplier,否则退回到 SLBufferPoints * _Point 的固定点距。slPrice 与 tpPrice 按 isBuy 翻转加减,风险回报比由 RiskRewardRatio 控制,原文弹窗写死 R:R 1:1.5,意味着盈利目标距离是止损的 1.5 倍。 图表端用 ObjectCreate 画箭头和水平线:OBJ_ARROW 标买卖点,OBJ_HLINE 拉出 SL_Line / TP_Line。回测段用 iHigh / iLow 抓取历史 K 线高低,若 high >= signalTP 记一次 winSignals,low <= signalSL 仅标记 resolved,不重复计数——胜率统计因此可能偏低,因为扫损后反弹达标的不算赢。 EA 头部给了东京时段过滤器,UseTokyoSessionFilter 默认关,TokyoStartHour=0、TokyoEndHour=9 按服务器时间框定美日交易窗口。外汇与贵金属杠杆高,这类基于 CCI+EMA 的顺势信号在震荡市可能连续扫损,实盘前务必在 MT5 策略测试器用 2023—2024 年数据跑一遍。

MQL5 / C++
if(crossDown && cciS[class="num">0] < class="num">0 && price < emaVal[class="num">0])
    RegisterSignal(false, price, atrValue);
class="type">class="kw">double dist = UseATR ? atrValue * ATR_Multiplier : SLBufferPoints * _Point;
class="type">class="kw">double slPrice = isBuy ? price - dist : price + dist;
class="type">class="kw">double tpPrice = isBuy ? price + dist * RiskRewardRatio : price - dist * RiskRewardRatio;
ObjectCreate(class="num">0, name, OBJ_ARROW, class="num">0, barTime, arrowPrice);
ObjectSetInteger(class="num">0, name, OBJPROP_ARROWCODE, arrowCode);
ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, isBuy ? BuyArrowColor : SellArrowColor);
ObjectCreate(class="num">0, "SL_Line", OBJ_HLINE, class="num">0, class="num">0, slPrice);
ObjectCreate(class="num">0, "TP_Line", OBJ_HLINE, class="num">0, class="num">0, tpPrice);
Alert("CCI ZeroLine EMA + R:R class="num">1:class="num">1.5 " + (isBuy ? "BUY" : "SELL") +
      StringFormat(" @%.5f | SL: %.5f | TP: %.5f", price, slPrice, tpPrice));
signalSL[totalSignals] = slPrice;
signalTP[totalSignals] = tpPrice;
resolved[totalSignals] = false;
class="type">class="kw">double high = iHigh(_Symbol, _Period, idx);
class="type">class="kw">double low = iLow(_Symbol, _Period, idx);
if(high >= signalTP[i]) { winSignals++; resolved[i]=true; }
if(low <= signalSL[i]) { resolved[i]=true; }
IndicatorRelease(handleCCI_Long);
IndicatorRelease(handleCCI_Short);
ObjectDelete(class="num">0, "SL_Line");
ObjectDelete(class="num">0, "TP_Line");
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CCI Zero-Line EA|
class=class="str">"cmt">//|                                          Copyright class="num">2025, MetaQuotes Ltd.|
class=class="str">"cmt">//|                                     [MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.0"
class="macro">#class="kw">property strict
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Tools\Datetime.mqh>
CTrade trade;
class=class="str">"cmt">//--- Session filter for USDJPY(Tokyo session)
input class="type">bool UseTokyoSessionFilter = false;   class=class="str">"cmt">// enable Tokyo session filter
input class="type">int  TokyoStartHour       = class="num">0;        class=class="str">"cmt">// session start hour(server time)
input class="type">int  TokyoEndHour         = class="num">9;        class=class="str">"cmt">// session end hour(server time, exclusive)
class=class="str">"cmt">//--- SL/TP settings
input class="type">bool UseATR               = true;     class=class="str">"cmt">// true = ATR-based SL/TP, false = fixed points

参数与句柄的初始落地

这套 CCI+EMA 组合 EA 的可调入口集中在文件头部,先看清几个硬约束:ATR 回看 14 根、乘数 1.5,意味着止损距离 = 当前 ATR × 1.5;若关闭 UseATR,则退化为固定 10 点止损垫。风险回报比写死 1.5,即 TP 是 SL 的 1.5 倍,属于偏保守的 1:1.5 结构。 CCI 用了双周期:长周期 50 负责零轴穿越定方向,短周期 25 做确认;趋势过滤挂一条 34 期 EMA。箭头色直接给 clrLime / clrRed,跑起来买绿卖红一眼可辨。 OnInit 里先抓三个指标句柄,任一无效立即 INIT_FAILED,MT5 日志会直接报初始化失败,不用等 tick 来才暴露问题。开了 UseATR 才去建 ATR 句柄,省资源但也意味着关闭时别引用 handleATR。 最后一道防线是参数合法性:风险回报比、ATR 乘数必须为正;不用 ATR 时 SLBufferPoints 还不能 ≤0,否则同样 INIT_FAILED。开 MT5 把这几行 input 改一遍,编译看是否还过初始化,是最快验证参数边界的办法。外汇与贵金属杠杆高,参数跑通不等于策略能盈利,实盘前请用策略测试器做历史回测。

MQL5 / C++
input class="type">int      ATR_Period       = class="num">14;     class=class="str">"cmt">// ATR look-back
input class="type">class="kw">double ATR_Multiplier   = class="num">1.5;     class=class="str">"cmt">// SL/TP distance = ATR × this
input class="type">class="kw">double SLBufferPoints   = class="num">10.0;    class=class="str">"cmt">// fallback SL offset in pips if UseATR=false
input class="type">class="kw">double RiskRewardRatio  = class="num">1.5;     class=class="str">"cmt">// TP = SL × class="num">1.5 (class="num">1:class="num">1.5 RR)
class=class="str">"cmt">//--- Indicator periods
input class="type">int CCI_LongPeriod   = class="num">50;   class=class="str">"cmt">// CCI class="type">long period(zero-line cross)
input class="type">int CCI_ShortPeriod  = class="num">25;   class=class="str">"cmt">// CCI class="type">short period(confirmation)
input class="type">int EMAPeriod        = class="num">34;   class=class="str">"cmt">// EMA period for trend filter
class=class="str">"cmt">//--- Arrow colors
input class="type">class="kw">color BuyArrowColor  = clrLime;
input class="type">class="kw">color SellArrowColor = clrRed;
class=class="str">"cmt">//--- Indicator handles
class="type">int handleCCI_Long = INVALID_HANDLE;
class="type">int handleCCI_Short= INVALID_HANDLE;
class="type">int handleEMA      = INVALID_HANDLE;
class="type">int handleATR      = INVALID_HANDLE;
class=class="str">"cmt">//--- Signal-tracking arrays
class="type">int     totalSignals = class="num">0;
class="type">int     winSignals   = class="num">0;
class="type">int     signalBar[];
class="type">class="kw">double  signalSL[];
class="type">class="kw">double  signalTP[];
class="type">bool    resolved[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   handleCCI_Long  = iCCI(_Symbol, _Period, CCI_LongPeriod, PRICE_TYPICAL);
   handleCCI_Short = iCCI(_Symbol, _Period, CCI_ShortPeriod, PRICE_TYPICAL);
   handleEMA       = iMA(_Symbol, _Period, EMAPeriod, class="num">0, MODE_EMA, PRICE_CLOSE);
   if(handleCCI_Long == INVALID_HANDLE ||
      handleCCI_Short== INVALID_HANDLE ||
      handleEMA       == INVALID_HANDLE)
      class="kw">return(INIT_FAILED);
   if(UseATR)
     {
      handleATR = iATR(_Symbol, _Period, ATR_Period);
      if(handleATR == INVALID_HANDLE)
         class="kw">return(INIT_FAILED);
     }
   if(RiskRewardRatio <= class="num">0 ||
      ATR_Multiplier   <= class="num">0 ||
      (SLBufferPoints <= class="num">0 && !UseATR))
      class="kw">return(INIT_FAILED);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ EA 退出与逐笔信号过滤的写法

EA 在 OnDeinit 里必须手动释放指标句柄并删掉画图对象,否则 MT5 策略测试器反复加载会泄漏资源。上面那段对 CCI 长短周期、EMA、ATR 四个句柄分别做 IndicatorRelease,再 ObjectDelete 清掉 SL/TP 线,是标准收尾动作。 回测统计不靠外部脚本,直接在 OnTester 里算胜率:用 winSignals / totalSignals * 100 得出百分比,PrintFormat 打出 Signals / Wins / Win-Rate 三行。注意若 totalSignals 为 0 返回 0.0,避免除零崩溃——这是可立刻复制到自己的 EA 里验证的数据点。 OnTick 前半段是实盘过滤逻辑。USDJPY 若开了东京时段过滤,就用 TimeTradeServer 取服务器时间,转 MqlDateTime 拿 hour,不在 TokyoStartHour~TokyoEndHour 区间内直接 return,不交易。 只在新 K 线干活是老规矩:用 static datetime lastTime 存上根时间,iTime 取当前周期 0 号柱时间,相等就 return,不等才更新并继续。随后 CopyBuffer 拉 CCI 长周期 2 根、短周期 1 根、EMA 1 根,不足长度直接退出,防止数组越界。外汇与贵金属杠杆高,这类过滤失效时回撤可能快速扩大,参数区间需自行在 MT5 里压力测试。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
  {
   if(handleCCI_Long  != INVALID_HANDLE)
      IndicatorRelease(handleCCI_Long);
   if(handleCCI_Short != INVALID_HANDLE)
      IndicatorRelease(handleCCI_Short);
   if(handleEMA       != INVALID_HANDLE)
      IndicatorRelease(handleEMA);
   if(UseATR && handleATR != INVALID_HANDLE)
      IndicatorRelease(handleATR);
   ObjectDelete(class="num">0, "SL_Line");
   ObjectDelete(class="num">0, "TP_Line");
  }

class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double winRate = totalSignals > class="num">0
                     ? class="num">100.0 * winSignals / totalSignals
                     : class="num">0.0;
   PrintFormat("=== Backtest Win-Rate ===\nSignals: %d  Wins: %d  Win-Rate: %.2f%%",
               totalSignals, winSignals, winRate);
   class="kw">return(winRate);
  }

class="type">void OnTick()
  {
class=class="str">"cmt">// Tokyo session filter for USDJPY
   if(UseTokyoSessionFilter && _Symbol == "USDJPY")
     {
      class="type">class="kw">datetime now = TimeTradeServer();
      class="type">MqlDateTime tm;
      TimeToStruct(now, tm);
      class="type">int hr = tm.hour;
      if(hr < TokyoStartHour || hr >= TokyoEndHour)
         class="kw">return;
     }
class=class="str">"cmt">// Only act on new bar
   class="kw">static class="type">class="kw">datetime lastTime = class="num">0;
   class="type">class="kw">datetime t = iTime(_Symbol, _Period, class="num">0);
   if(t == lastTime)
      class="kw">return;
   lastTime = t;
class=class="str">"cmt">// Copy indicator buffers
   class="type">class="kw">double cciL[class="num">2], cciS[class="num">1], emaVal[class="num">1];
   if(CopyBuffer(handleCCI_Long,  class="num">0, class="num">0, class="num">2, cciL)  < class="num">2)
      class="kw">return;
   if(CopyBuffer(handleCCI_Short, class="num">0, class="num">0, class="num">1, cciS)  < class="num">1)
      class="kw">return;

「信号登记与止损止盈的距离算法」

这段逻辑紧接 CCI 零轴穿越判定之后,负责把通过的信号落库并计算具体的 SL/TP 距离。先取 EMA 缓冲,若 CopyBuffer 返回值小于 1 直接 return,说明当前 Bar 的均线值还没就绪,避免用空数据交易。 ATR 作为动态止损基准是可选的。当 UseATR 为 true 时,从 handleATR 拷 1 根缓冲到 atrBuf,失败同样 return;atrValue 即为当前 ATR 数值,后续乘 ATR_Multiplier 得出止损距离。 crossUp 与 crossDown 用前一根 cciL[1] 与当前 cciL[0] 的符号翻转定义。只有 crossUp 且短周期 cciS[0] > 0 且现价高于 EMA,才登记买入;反向对称登记卖出。这里价格用的是 SYMBOL_BID,做交叉过滤时需注意你是按买价判断的。 RegisterSignal 里 dist 的算法很直接:开 ATR 就取 atrVal * ATR_Multiplier,否则用 SLBufferPoints * _Point 的固定点数。SL 挂在 price ± dist,TP 则是同方向 dist * RiskRewardRatio,比如 R:R=2 时盈利空间是止损的两倍。 箭头画在 K 线外 5 点(offset = 5 * _Point,约 5 pip 偏移),买箭用 code 233、卖箭用 234,名字前缀 BUY_ / SELL_ 便于后续 ResolveSignals 回查挂单是否触碰 SL/TP。外汇与贵金属波动剧烈,这种基于 ATR 的动态距离在高波动时段可能瞬间放大止损,实盘前务必在 MT5 策略测试器用历史数据验证参数敏感性。

MQL5 / C++
if(CopyBuffer(handleEMA,        class="num">0, class="num">0, class="num">1, emaVal) < class="num">1)
      class="kw">return;
class=class="str">"cmt">// ATR if needed
   class="type">class="kw">double atrValue = class="num">0.0;
   if(UseATR)
     {
       class="type">class="kw">double atrBuf[class="num">1];
       if(CopyBuffer(handleATR, class="num">0, class="num">0, class="num">1, atrBuf) < class="num">1)
         class="kw">return;
       atrValue = atrBuf[class="num">0];
     }
   class="type">class="kw">double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
class=class="str">"cmt">// Detect zero-line cross
   class="type">bool crossUp   = (cciL[class="num">1] < class="num">0 && cciL[class="num">0] > class="num">0);
   class="type">bool crossDown = (cciL[class="num">1] > class="num">0 && cciL[class="num">0] < class="num">0);
class=class="str">"cmt">// Confirm with class="type">short CCI & EMA trend filter
   if(crossUp   && cciS[class="num">0] >  class="num">0 && price > emaVal[class="num">0])
       RegisterSignal(true,  price, atrValue);
   if(crossDown && cciS[class="num">0] <  class="num">0 && price < emaVal[class="num">0])
       RegisterSignal(false, price, atrValue);
class=class="str">"cmt">// Resolve pending signals for SL/TP hits
   ResolveSignals();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Register new buy/sell signal                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void RegisterSignal(class="type">bool isBuy, class="type">class="kw">double price, class="type">class="kw">double atrVal)
  {
   class="type">class="kw">double dist       = UseATR
                       ? atrVal * ATR_Multiplier
                       : SLBufferPoints * _Point;
   class="type">class="kw">double slPrice = isBuy ? price - dist : price + dist;
   class="type">class="kw">double tpPrice = isBuy
                       ? price + dist * RiskRewardRatio
                       : price - dist * RiskRewardRatio;
class=class="str">"cmt">// Arrow placement just outside the candle
   class="type">class="kw">datetime barTime = iTime(_Symbol, _Period, class="num">0);
   class="type">class="kw">double  barHigh = iHigh(_Symbol, _Period, class="num">0);
   class="type">class="kw">double  barLow  = iLow(_Symbol, _Period, class="num">0);
   class="type">class="kw">double  offset  = class="num">5 * _Point;   class=class="str">"cmt">// e.g. class="num">5-pip offset
   class="type">class="kw">double arrowPrice = isBuy
                       ? barLow  - offset
                       : barHigh + offset;
class=class="str">"cmt">// Use numeric arrow codes directly
   class="type">int arrowCode = isBuy ? class="num">233 : class="num">234;
   class="type">class="kw">string name = (isBuy ? "BUY_" : "SELL_")

用数组追踪信号并回测胜率

信号触发后不能只画个箭头就完事,得把每笔的进场 bar、SL、TP 存进动态数组,留给后续逐根 K 线回测。上面这段在信号生成末尾做了 totalSignals++ 并 ArrayResize 四个数组:signalBar 记相对偏移、signalSL/signalTP 存价格、resolved 标记是否已平仓,索引统一用 totalSignals-1。 ResolveSignals() 每 tick 跑一遍:对未 resolved 的信号,signalBar[i] 自增代表向前推一根历史 bar,用 iHigh/iLow 取该 bar 高低点。若 high >= signalTP[i] 计 winSignals++ 并标记 resolved;若 low <= signalSL[i] 也标记 resolved 但不计数——这意味着策略按 1:1.5 风险回报,触 TP 才算赢。 外汇与贵金属波动跳空频繁,实盘里 low <= SL 不一定真成交,回测胜率倾向高于实盘。把 winSignals / totalSignals 在 OnDeinit 打印,就能直接看样本内命中率,开 MT5 跑一周 EURUSD M15 验证最直观。 DrawLine() 只留了函数头,负责按名字和颜色画水平线;SL 传 clrRed、TP 传 clrLime,和箭头配色一致方便肉眼核对。

MQL5 / C++
  + TimeToString(TimeTradeServer(), TIME_SECONDS);
  ObjectCreate(class="num">0, name, OBJ_ARROW, class="num">0, barTime, arrowPrice);
  ObjectSetInteger(class="num">0, name, OBJPROP_ARROWCODE, arrowCode);
  ObjectSetInteger(class="num">0, name, OBJPROP_COLOR,    isBuy ? BuyArrowColor : SellArrowColor);
  ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH,    class="num">2);
class=class="str">"cmt">// Draw SL/TP lines
  DrawLine("SL_Line", slPrice, clrRed);
  DrawLine("TP_Line", tpPrice, clrLime);
class=class="str">"cmt">// Alert
  Alert("CCI ZeroLine EMA + R:R class="num">1:class="num">1.5 "
        + (isBuy ? "BUY" : "SELL")
        + StringFormat(" @%.5f | SL: %.5f | TP: %.5f",
                       price, slPrice, tpPrice));
class=class="str">"cmt">// Track for backtest metrics
  totalSignals++;
  ArrayResize(signalBar,  totalSignals);
  ArrayResize(signalSL,   totalSignals);
  ArrayResize(signalTP,   totalSignals);
  ArrayResize(resolved,   totalSignals);
  signalBar[totalSignals-class="num">1] = class="num">0;
  signalSL[totalSignals-class="num">1]  = slPrice;
  signalTP[totalSignals-class="num">1]  = tpPrice;
  resolved[totalSignals-class="num">1]  = false;
  }
class="type">void ResolveSignals()
  {
   class="type">int bars = Bars(_Symbol, _Period);
   for(class="type">int i = class="num">0; i < totalSignals; i++)
     {
      if(resolved[i])
        class="kw">continue;
      signalBar[i]++;
      class="type">int idx = signalBar[i];
      if(idx >= bars)
        class="kw">continue;
      class="type">class="kw">double high = iHigh(_Symbol, _Period, idx);
      class="type">class="kw">double low  = iLow(_Symbol, _Period, idx);
      if(high >= signalTP[i])
        {
         winSignals++;
         resolved[i] = true;
        }
      else
      if(low <= signalSL[i])
        {
         resolved[i] = true;
        }
     }
  }
class="type">void DrawLine(class="type">class="kw">string name, class="type">class="kw">double price, class="type">class="kw">color clr)
  {
让小布替你跑这套多指标巡检
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到双CCI与EMA、ATR的实时叠合状态,把重复劳动交给小布,你专注决策。

常见问题

快速CCI捕捉短期动能,慢速CCI评估整体强度,两者在零线附近的相对位置可区分趋势萌芽与噪声摆动,概率上更利于过滤假信号。
0.015把输出压到±100区间便于读图;调小会让极值更易触发、信号更敏感但也更噪,调大则钝化,需结合品种波动率测试。
ATR提供波动性度量,常用于动态止损与仓位缩放,使EA在高波动和低波动环境下不至于用固定点数硬扛。
小布内置的是指标叠合与预警视图,EA本体需在MT5自行编译部署;但小布可替你监控双CCI零线交叉与EMA位置关系,减少手动刷图。
零线负责核心预警,±100作辅助过滤并不冲突;只靠零线容易在外汇贵金属这类高风险品种上被锯齿洗出场。