在 MQL5 中寻找趋势的几种方法·综合运用
📘

在 MQL5 中寻找趋势的几种方法·综合运用

第 3/3 篇

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 止损可能瞬间放大回撤,先开模拟盘验证。

MQL5 / C++
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 能直接看信号翻转频率。

MQL5 / C++
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 数据跑一遍看信号频率。

MQL5 / C++
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 的样例,打开编辑器编译跑一遍,比读十篇综述都实在。 外汇和贵金属杠杆高、回撤快,任何趋势信号都只是概率优势,别把样例当保本凭证。

常见问题

在 OnTick 里用静态变量记录上一根柱子的时间,当当前柱时间与之不同即视为新柱,再执行信号计算,避免同根柱重复触发。
倾向先平掉反向仓位再开新仓,用仓位翻转逻辑统一处理;具体可在信号切换函数里先 CheckClose 再 CheckOpen,防止双边敞口。
小布可以替你加载多个 EA 到同一品种周期做对照回测,并汇总胜率与回撤,你只需看结论调参数。
重点看净盈利、最大回撤和交易次数;日线级信号少,若回测年交易低于 20 次要怀疑样本太少。
给信号加最小确认阈值或冷却根数,只在穿越幅度大于 N 点才判定翻转,能显著降低外汇高波动下的假信号概率。