在 MQL5 中寻找趋势的几种方法·综合运用
EA 生命周期与新柱信号触发骨架
这段 MT5 专家顾问骨架把指标句柄管理、新 K 线判定和信号下单串成了一条线。OnInit 里用 iCustom 挂了一个自定义均线趋势指标 Examples\MATrendDetector,句柄拿不到就直接返回 -1 终止初始化,避免后续空指针乱跑。 OnDeinit 只做一件事:句柄有效就 IndicatorRelease 释放掉,防止切换周期或移除 EA 时指标资源泄漏。 OnTick 的核心节奏是先 CheckNewBar 拦一道——它用 CopyRates 取最新 1 根 K 线,若 tick_volume 大于 1 就返回 0,意味着这根柱还在跑、不是刚开盘,EA 直接 return 不动作。只有新柱诞生才继续往下走。 新柱确认后,GetSignal 拿当前信号,首根 K 线用 is_first_signal 把 prev_signal 对齐,避免开局误判交叉。已有同品种仓位就先 CheckPositionClose 看是否反手平仓;没有则分别跑 CheckBuySignal / CheckSellSignal,命中就用 CTrade.PositionOpen 以市价ask/bid裸单进场,滑点和止损先填 0。 开 MT5 把这段贴进 EA,把 MATrendDetector 的 MAPeriod 从默认改到 50,能在欧美 15M 上直观看到新柱触发频率从每根都扫变成约每 15 分钟一次,外汇贵金属杠杆高,裸单 0 止损可能瞬间放大回撤,先开模拟盘验证。
class="type">int OnInit() { class=class="str">"cmt">// 创建外部指标句柄用于将来引用: ResetLastError(); indicator_handle = iCustom(Symbol(), PERIOD_CURRENT, "Examples\\MATrendDetector", MAPeriod); class=class="str">"cmt">// 如果初始化没有成功,返回非零值: if(indicator_handle == INVALID_HANDLE) { Print("MATrendDetector 初始化错误, 代码 = ", GetLastError()); class="kw">return(-class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 指标去初始化事件处理函数: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">void OnDeinit(const class="type">int _reason) { class=class="str">"cmt">// 删除指标句柄: if(indicator_handle != INVALID_HANDLE) { IndicatorRelease(indicator_handle); } } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 当前交易品种新订单事件处理函数: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int current_signal = class="num">0; class="type">int prev_signal = class="num">0; class="type">bool is_first_signal = true; class=class="str">"cmt">//--------------------------------------------------------------------- class="type">void OnTick() { class=class="str">"cmt">// 等待一个新柱的开始: if(CheckNewBar() != class="num">1) { class="kw">return; } class=class="str">"cmt">// 获得建仓/平仓信号: current_signal = GetSignal(); if(is_first_signal == true) { prev_signal = current_signal; is_first_signal = false; } class=class="str">"cmt">// 选择当前交易品种仓位: if(PositionSelect(Symbol()) == true) { class=class="str">"cmt">// 检查我们是否需要平掉一个反向仓位: if(CheckPositionClose(current_signal) == class="num">1) { class="kw">return; } } class=class="str">"cmt">// 检查是否有买入信号: if(CheckBuySignal(current_signal, prev_signal) == class="num">1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, Lots, SymbolInfoDouble(Symbol(), SYMBOL_ASK ), class="num">0, class="num">0); } class=class="str">"cmt">// 检查是否有卖出信号: if(CheckSellSignal(current_signal, prev_signal) == class="num">1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, Lots, SymbolInfoDouble(Symbol(), SYMBOL_BID ), class="num">0, class="num">0); } class=class="str">"cmt">// 保存当前信号: prev_signal = current_signal; } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 返回新柱的标志: class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// - 如果返回1,说明有一个新柱 class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int CheckNewBar() { class="type">MqlRates current_rates[class="num">1]; ResetLastError(); if(CopyRates(Symbol(), Period(), class="num">0, class="num">1, current_rates)!= class="num">1) { Print("CopyRates 复制错误, 代码 = ", GetLastError()); class="kw">return(class="num">0); } if(current_rates[class="num">0].tick_volume>class="num">1) { class="kw">return(class="num">0); } class="kw">return(class="num">1); }
「信号切换下的仓位翻转逻辑」
EA 在主循环里先用 CheckNewBar() 拦截,只有返回值为 1 的新柱才继续跑后续逻辑,否则直接 return,避免每 tick 重复计算。拿到 current_signal 后,如果是首根信号(is_first_signal==true),就把当前值存进 prev_signal 并关掉首信号标记,这一步是为了后面比对信号是否发生方向反转。 GetSignal() 只干一件事:用 CopyBuffer 从趋势指标句柄复制 0 号缓冲区的 1 个元素到 trend_direction 数组。复制失败(返回值不等于 1)就打印错误码并返回 0;成功则把趋势值强转成 int 返回,通常 1 代表多头、-1 代表空头、0 代表无倾向。 仓位处理上,PositionSelect 选中当前品种后进入 CheckPositionClose。该函数读 POSITION_TYPE:若信号为 1 且已是 BUY 仓、或信号为 -1 且已是 SELL 仓,直接返回 1 表示同方向仓已存在;否则调 trade.PositionClose(Symbol(),10) 平掉反向仓,偏离市价 10 点容差。 开仓判定分两个函数:CheckBuySignal / CheckSellSignal 比对 current 与 prev 信号,若发生向买或向卖的反转则返回 1,随后用 CTrade.PositionOpen 以 SYMBOL_ASK 或 SYMBOL_BID 市价下单,滑点参数填 0。外汇与贵金属杠杆高,这类翻转策略在震荡市可能频繁止损,上 MT5 用策略测试器跑 EURUSD 的 M15 能直接看信号翻转频率。
class=class="str">"cmt">// 等待一个新柱的开始: if(CheckNewBar()!= class="num">1) { class="kw">return; } class=class="str">"cmt">// 获得建仓/平仓信号: current_signal = GetSignal(); if(is_first_signal == true) { prev_signal = current_signal; is_first_signal = false; } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 取得建仓/平仓信号: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int GetSignal() { class="type">class="kw">double trend_direction[class="num">1]; class=class="str">"cmt">// 从趋势指标获得信号: ResetLastError(); if(CopyBuffer(indicator_handle, class="num">0, class="num">0, class="num">1, trend_direction) != class="num">1) { Print("CopyBuffer 复制错误, 代码 = ", GetLastError()); class="kw">return(class="num">0); } class="kw">return((class="type">int)trend_direction[class="num">0]); } class=class="str">"cmt">// 选择当前交易品种仓位: if(PositionSelect(Symbol()) == true) { class=class="str">"cmt">// 检查我们是否需要平掉一个反向仓位: if(CheckPositionClose(current_signal) == class="num">1) { class="kw">return; } } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 检查我们是否需要平仓: class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 返回值: class=class="str">"cmt">// class="num">0 - 没有开启的仓位 class=class="str">"cmt">// class="num">1 - 已经有了信号同方向上的仓位 class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int CheckPositionClose(class="type">int _signal) { class="type">long position_type = PositionGetInteger(POSITION_TYPE); if(_signal == class="num">1) { class=class="str">"cmt">// 如果已经有了买入仓位,则返回: if(position_type == (class="type">long)POSITION_TYPE_BUY) { class="kw">return(class="num">1); } } if(_signal==-class="num">1) { class=class="str">"cmt">// 如果已经有了卖出仓位,则返回: if( position_type == ( class="type">long )POSITION_TYPE_SELL ) { class="kw">return(class="num">1); } } class=class="str">"cmt">// 平仓: CTrade trade; trade.PositionClose(Symbol(), class="num">10); class="kw">return(class="num">0); } class=class="str">"cmt">// 检查是否有买入信号: if(CheckBuySignal(current_signal, prev_signal)==class="num">1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, Lots, SymbolInfoDouble(Symbol(), SYMBOL_ASK), class="num">0, class="num">0); } class=class="str">"cmt">// 检查是否有卖出信号: if(CheckSellSignal(current_signal, prev_signal) == class="num">1) { CTrade trade; trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, Lots, SymbolInfoDouble(Symbol(), SYMBOL_BID), class="num">0, class="num">0); } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 检查信号是否改为买入: class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 返回值: class=class="str">"cmt">// class="num">0 - 无信号 class=class="str">"cmt">// class="num">1 - 有买入信号 class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int CheckBuySignal(class="type">int _curr_signal, class="type">int _prev_signal) { class=class="str">"cmt">// 检查信号是否改为买入:
◍ 信号翻转的买卖判定函数
在 MT5 自定义指标或 EA 里,光有当前 K 线的信号值还不够,必须对比前一根的状态才能过滤掉连续同方向的噪音。下面这两个函数把「由中性或反向转为单向」作为触发条件,返回 1 表示出现新信号,否则返回 0。 买入逻辑只看当前信号变 1 且前一根是 0 或 -1,也就是空仓转多或空翻多才认;卖出逻辑对称,当前 -1 且前一根是 0 或 1 才确认。这样连续多头排列时函数恒返回 0,不会重复刷单。 外汇与贵金属杠杆高、滑点随机,这类信号函数只是入场触发器,实际成交概率仍受流动性影响,建议先在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍看信号频率。
if((_curr_signal==class="num">1 && _prev_signal==class="num">0) || (_curr_signal==class="num">1 && _prev_signal==-class="num">1)) { class="kw">return(class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 检查是否有卖出信号: class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// 返回值: class=class="str">"cmt">// class="num">0 - 无信号 class=class="str">"cmt">// class="num">1 - 有卖出信号 class=class="str">"cmt">//--------------------------------------------------------------------- class="type">int CheckSellSignal(class="type">int _curr_signal, class="type">int _prev_signal) { class=class="str">"cmt">// 检查信号是否改为卖出: if((_curr_signal==-class="num">1 && _prev_signal==class="num">0) || (_curr_signal==-class="num">1 && _prev_signal==class="num">1)) { class="kw">return(class="num">1); } class="kw">return(class="num">0); }
把五个趋势 EA 丢进 EURUSD 日线回测
用 1993—2010 年 EURUSD 的 D1 历史数据跑了五套趋势类 EA:MATrendExpert、FanTrendExpert、ADXTrendExpert(ADXTrendLevel 分别设 0 和 20)、RTRTrendExpert,以及 Heiken Ashi 版本。 最前面两个——移动均线和扇形均线——其实只用了上一根 K 线的平滑序列,却最贴近“跟价跟势”的本意。因为加载的是 200 日重均线,短期波动被磨平了,成交次数很低,但这不算缺陷:单笔仓位可能扛几个月,吃的是 200 天级别的趋势。MATrendExpert 在盈利、平移(浮盈不动作)、回撤三段之间交替,节奏很清楚。 ADX 那套也不差。把 PeriodADX 从默认改成 17,整段历史的一致性更好;ADXTrendLevel 的阈值过滤效应不明显,真要用的话建议随当前波动动态调,而不是写死 0 或 20。它有几个亏损周期,得配别的手段补平衡。 NRTR 在整个测试区间和随机抽的长线片段里,实际利润趋近于零,反而说明它趋势判定极稳。换参数或做一轮优化,才可能把这 EA 拉到正期望。 Heiken Ashi 版明显亏。历史图看着还行,大概率是实时重绘骗的;测试结果很难看。换平滑版(减少重绘倾向)或许能救回来。 外汇与贵金属属高风险品种,回测结论仅代表历史样本。若给这些 EA 加动态止损加目标位,再挂一套资金管理,长线利润空间和被击垮的概率都会改善,但都要在 MT5 策略测试器里自己复跑确认。
「最后一句大实话」
把趋势判断逻辑写进 EA 并不复杂,难的是你肯不肯亲手验证每条市场规则。规则理解越深,系统寿命越长,这是作者二十五年工程经验里的倾向,不是承诺。 附带的两个压缩包(experts.zip 11.42 KB、indicators__3.zip 9.59 KB)里是可直接拖进 MT5 的样例,打开编辑器编译跑一遍,比读十篇综述都实在。 外汇和贵金属杠杆高、回撤快,任何趋势信号都只是概率优势,别把样例当保本凭证。