价格行为分析工具包开发(第三十部分):商品通道指数(CCI)零线的EA·进阶篇
(2/3)· 当快速与慢速CCI在零线交叠,EA如何借EMA与ATR滤掉锯齿噪声而非盲目追单
不少交易者把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 年数据跑一遍。
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 改一遍,编译看是否还过初始化,是最快验证参数边界的办法。外汇与贵金属杠杆高,参数跑通不等于策略能盈利,实盘前请用策略测试器做历史回测。
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 里压力测试。
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 策略测试器用历史数据验证参数敏感性。
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,和箭头配色一致方便肉眼核对。
+ 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) {