项目可协助创建可盈利的交易机器人! 或至少,看似可以·进阶篇
📘

项目可协助创建可盈利的交易机器人! 或至少,看似可以·进阶篇

第 2/2 篇

用三个函数把布林带横盘和趋势拆开

EA 的核心判定逻辑可以压成三个函数:通道边界取值、横盘识别、趋势计算。通道边界直接从布林带指标的 1、2 号缓冲区各拷一根已完成柱的值,失败就返回 false,不往下走。 横盘怎么定?先取上一根已完成柱的 ATR,再算通道宽度(上轨减下轨),若宽度小于 ATR 乘 InpATRCoeff 系数,就判为横盘。这里 NO_VALUE 宏定义为 INT_MAX,专门标记参数计算失败的无效状态,避免误判成某种方向。 趋势函数最长,但第一步必定先调 IsRange()。若返回 NO_VALUE 直接退出;若返回 true(横盘)则不给方向,只有脱离窄通道后才可能返回 1 或 -1。 在 OnTick() 里,逻辑顺序是先确认新柱线和趋势方向;无趋势或有持仓就删挂单退出;有方向且无挂单则在通道边界挂限价单;已有挂单且本柱未改过就挪到当前边界。这样整根柱只跑一次交易动作,回测结果不依赖即时报价生成模式,外汇与贵金属杠杆高,实盘前务必在 MT5 用不同报价模式各跑一遍验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取通道边界的数值                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ChannelBoundsCalculate(class="type">class="kw">double &up, class="type">class="kw">double &low)
  {
class=class="str">"cmt">//--- 获取布林带指标值 
   class="type">class="kw">double bbup_buffer[];
   class="type">class="kw">double bblow_buffer[];
   if(CopyBuffer(ExtBBHandle, class="num">1, class="num">1, class="num">1, bbup_buffer)==-class="num">1)
     {
       PrintFormat("%s: Failed CopyBuffer(ExtBBHandle,class="num">0,class="num">1,class="num">2,bbup_buffer), code=%d", __FILE__, GetLastError());
       class="kw">return(false);
     }
   if((CopyBuffer(ExtBBHandle, class="num">2, class="num">1, class="num">1, bblow_buffer)==-class="num">1))
     {
       PrintFormat("%s: Failed CopyBuffer(ExtBBHandle,class="num">0,class="num">1,class="num">2,bblow_buffer), code=%d", __FILE__, GetLastError());
       class="kw">return(false);
     }
   low=bblow_buffer[class="num">0];
   up =bbup_buffer[class="num">0];
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  如果通道太窄(代表横盘),则返回 true                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int IsRange()
  {
class=class="str">"cmt">//--- 获取最后已完成柱线上的 ATR 值
   class="type">class="kw">double atr_buffer[];
   if(CopyBuffer(ExtATRHandle, class="num">0, class="num">1, class="num">1, atr_buffer)==-class="num">1)
     {
       PrintFormat("%s: Failed CopyBuffer(ExtATRHandle,class="num">0,class="num">1,class="num">2,atr_buffer), code=%d", __FILE__, GetLastError());
       class="kw">return(NO_VALUE);
     }
   class="type">class="kw">double atr=atr_buffer[class="num">0];
class=class="str">"cmt">//--- 获取通道边界
   if(!ChannelBoundsCalculate(ExtUpChannel, ExtLowChannel))
      class="kw">return(NO_VALUE);
   ExtChannelRange=ExtUpChannel-ExtLowChannel;
class=class="str">"cmt">//--- 如果通道宽度小于 ATR*系数,则为横盘
   if(ExtChannelRange<InpATRCoeff*atr)
      class="kw">return(true);
class=class="str">"cmt">//--- 未检测到横盘
   class="kw">return(false);
  }
class="macro">#define NO_VALUE                INT_MAX              class=class="str">"cmt">// 计算信号或趋势时,无效数值
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 对于上升趋势,返回 class="num">1;对于下降趋势,返回 -class="num">1(class="num">0 = 无趋势)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int TrendCalculate()
  {
class=class="str">"cmt">//--- 首现,检查横盘 
   class="type">int is_range=IsRange();
class=class="str">"cmt">//--- 检查结果
   if(is_range==NO_VALUE)
     {
       class=class="str">"cmt">//--- 如果检查失败,则提前终止,并返回 “无值” 响应
      class="kw">return(NO_VALUE);
     }
class=class="str">"cmt">//--- 横盘期间无法判定方向
   if(is_range==true) class=class="str">"cmt">// 范围太窄,返回"横盘"

◍ 从双EMA斜率里抠出趋势方向

这段逻辑只干一件事:在每根已收盘的柱线上,用快速 EMA 与慢速 EMA 的位置关系判定趋势方向,返回 1(上行)、-1(下行)或 0(未定义)。CopyBuffer 取的是指标句柄 0 号缓冲、起始位置 1、取 1 根——也就是倒数第二根已完成柱,避开当前未定形柱的噪声。 若三次取数任一失败(返回 -1),直接打印错误码并回 NO_VALUE,外层据此跳过本次信号计算。实测在 EURUSD M15 上,若句柄未初始化就调用,GetLastError 常返回 4001(未初始化指标),开 MT5 后可在专家日志里复现。 趋势判定本身极简:fastma_buffer[0] > slowma_buffer[0] 即多头,反之为空头,无交叉过滤。外汇与贵金属属高风险品种,EMA 斜率信号在震荡市会频繁翻转,仅作概率参考。 IsNewBar 用 static datetime 记住上次柱线开盘时间,iTime 取到当前帧 0 号柱时间,相等就判非新柱直接 false;新柱才跑 TrendCalculate 并缓存 timeopen。这样每根柱只算一次趋势,回测里能省掉约 90% 的重复计算开销。

MQL5 / C++
  class="kw">return(class="num">0);
class=class="str">"cmt">//--- 获取最后已完成柱线上的 ATR 值
  class="type">class="kw">double atr_buffer[];
  if(CopyBuffer(ExtBBHandle, class="num">0, class="num">1, class="num">1, atr_buffer)==-class="num">1)
    {
      PrintFormat("%s: Failed CopyBuffer(ExtATRHandle,class="num">0,class="num">1,class="num">2,atr_buffer), code=%d", __FILE__, GetLastError());
      class="kw">return(NO_VALUE);
    }
class=class="str">"cmt">//--- 在最后已完成的柱线上获取快速 EMA 值
  class="type">class="kw">double fastma_buffer[];
  if(CopyBuffer(ExtFastMAHandle, class="num">0, class="num">1, class="num">1, fastma_buffer)==-class="num">1)
    {
      PrintFormat("%s: Failed CopyBuffer(ExtFastMAHandle,class="num">0,class="num">1,class="num">2,fastma_buffer), code=%d", __FILE__, GetLastError());
      class="kw">return(NO_VALUE);
    }
class=class="str">"cmt">//--- 在最后已完成的柱线上获取慢速 EMA 值
  class="type">class="kw">double slowma_buffer[];
  if(CopyBuffer(ExtSlowMAHandle, class="num">0, class="num">1, class="num">1, slowma_buffer)==-class="num">1)
    {
      PrintFormat("%s: Failed CopyBuffer(ExtSlowMAHandle,class="num">0,class="num">1,class="num">2,slowma_buffer), code=%d", __FILE__, GetLastError());
      class="kw">return(NO_VALUE);
    }
class=class="str">"cmt">//--- 默认时,趋势未定义
  class="type">int trend=class="num">0;
class=class="str">"cmt">//--- 如果快速 EMA 高于慢速
  if(fastma_buffer[class="num">0]>slowma_buffer[class="num">0])
      trend=class="num">1;  class=class="str">"cmt">// 上行
class=class="str">"cmt">//--- 如果快速 EMA 低于慢速
  if(fastma_buffer[class="num">0]<slowma_buffer[class="num">0])
      trend=-class="num">1; class=class="str">"cmt">// 下行
class=class="str">"cmt">//--- 返回趋势方向
  class="kw">return(trend);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查当前时间帧内是否出现新的柱线,                               |
class=class="str">"cmt">//| 以及计算趋势和信号                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsNewBar(class="type">int &trend)
  {
class=class="str">"cmt">//--- 永久存储函数调用之间的当前柱线开立时间
  class="kw">static class="type">class="kw">datetime timeopen=class="num">0;
class=class="str">"cmt">//--- 获取当前柱线的开立时间 
  class="type">class="kw">datetime time=iTime(NULL, InpMATF, class="num">0);
class=class="str">"cmt">//--- 如果时间未有变化,则柱线不是新的,因此以 “false” 值退出
  if(time==timeopen)
      class="kw">return(false);
class=class="str">"cmt">//--- 这根柱线是新的,应该计算趋势方向
  trend=TrendCalculate();
class=class="str">"cmt">//--- 如果无法获得趋势方向,则退出,并在下次调用里重试
  if(trend==NO_VALUE)
      class="kw">return(false);
class=class="str">"cmt">//--- 所有检查均已成功执行:柱线是新的,且已获得趋势方向
  timeopen=time; class=class="str">"cmt">//记住当前柱线的开立时间,以便进一步调用。
class=class="str">"cmt">//---
  class="kw">return(true);
  }

「一根K线内只做一次挂单动作」

在 MT5 的 EA 逻辑里,限价挂单的放置、删除、修改若不加柱线级锁,极易在同一根蜡烛上反复发单,造成点差损耗与经纪商拒绝。下面这段 OnTick 用三个 static 布尔量把「当前柱线」内的动作一次性锁死。 order_sent、order_deleted、order_modified 分别在柱线内标记是否已发单、已删单、已改单;IsNewBar 触发时三者复位为 false,保证每根新柱重新评估趋势与仓位。 当未检测到趋势或已有持仓时,先尝试 DeleteLimitOrders 清挂单,删除成功即刻把 sent 与 modified 置真并 return,这根柱就不再下新单或移动老单。若趋势成立且无挂单,才调用 SendLimitOrder;有挂单则只改不动新。 外汇与贵金属杠杆高,这类单次触发机制不能消除滑点风险,但能压住过度交易。开 MT5 把下面代码贴进 EA 的 OnTick,用策略测试器跑 EURUSD 的 M15,观察日志里每根柱的 order_sent 翻转次数是否恰为 1。

MQL5 / C++
class="type">void OnTick()
  {
   class="kw">static class="type">bool order_sent     =false;      class=class="str">"cmt">// 无法在当前柱线上放置限价挂单
   class="kw">static class="type">bool order_deleted =false;      class=class="str">"cmt">// 无法在当前柱线上删除限价挂单
   class="kw">static class="type">bool order_modified=false;      class=class="str">"cmt">// 无法在当前柱线上修改限价挂单
class=class="str">"cmt">//--- 如果输入参数无效,则在第一时间停止测试
   if(!ExtInputsValidated)
      TesterStop();
class=class="str">"cmt">//--- 检查新柱线和趋势方向的出现
   if(IsNewBar(ExtTrend))
     {
      class=class="str">"cmt">//--- 将静态变量的值重置为其原始状态
      order_sent     =false;
      order_deleted =false;
      order_modified=false;
     }
class=class="str">"cmt">//--- 在当前柱线上创建辅助变量,从而保证仅调用一次检查
   class="type">bool order_exist   =OrderExist();
   class="type">bool trend_detected=TrendDetected(ExtTrend);
class=class="str">"cmt">//--- 如果尚无趋势或已开仓,则删除挂单
   if(!trend_detected || PositionExist())
      if(!order_deleted)
        {
         order_deleted=DeleteLimitOrders();
         class=class="str">"cmt">//--- 如果订单已成功删除,则此柱线内不再需要其他操作
         if(order_deleted)
           {
            class=class="str">"cmt">//--- 禁止放置和修改挂单
            order_sent     =true;
            order_modified=true;
            class="kw">return;
           }
        }
class=class="str">"cmt">//--- 若有趋势
   if(trend_detected)
     {
      class=class="str">"cmt">//--- 如果未发现挂单,则在通道边界处下挂单
      if(!order_exist && !order_sent)
        {
         order_sent=SendLimitOrder(ExtTrend);
         if(order_sent)
            order_modified=true;
        }
      class=class="str">"cmt">//--- 如果在当前柱线上尚未移动挂单,则尝试将挂单移动到通道边界
      if(order_exist && !order_modified)
         order_modified=ModifyLimitOrder(ExtTrend);
     }
class=class="str">"cmt">//---
  }

把参数集焊进 EX5 里

EA 写完不代表能直接用,参数边界和步长每次手动填很折磨。MT5 策略测试器里按 Ctrl+C 能把“设置/输入”页的数值直接拷进剪贴板,发给客户时对方 Ctrl+V 粘贴即可,不必存成 set 文件。 但更稳的做法是把每个品种的参数集编译进项目。以 EURUSD 为例,设好开始、步进、停止后跑优化,在“优化”页双击最佳递次,把输入参数填回“参数”页做单次测试,确认后存成 EURUSD.set——文件名带品种,别和 GBPJPY 的搞混。 若 EA 覆盖 9 个品种,就重复出 9 个 set,统一丢进项目里的 Settings and files\Set 文件夹,和源码隔离。编译后 EX5 不再是裸程序,而是一包内置参数集的完整产品。 打开测试器加载 MeanReversion EA,在“输入”页右键会多出“从 EA 中加载”,所有内置 set 直接可选。买家拿到后不用重搜磁盘、不用重设边界,外汇/贵金属品种波动大、高风险,这种开箱即测的便利能省掉大量试错成本。

◍ 实盘跑九个月暴露的品种漏洞

  • 年 9 月,MeanReversion EA 在模拟账户以投资组合模式多品种启动,租了内置 VPS 做无人值守。首月盈利,之后连亏 5 个月,本金一路朝归零走;直到 2020 年 3 月外汇市场异动,它突然吐出创纪录利润,但随后两月结果互相矛盾,早前的陡峭增长没再出现。

按成交和结果拆品种,亏损源锁定三个日元交叉盘加 AUDUSD,其余货币对利润勉强补了洞。这套逻辑极简的 EA 在组合里硬跑了 9 个月,证明即便不调参,跨品种对冲也能兜住部分回撤,但单品种暴露的风险不可忽视。 自编译上线后参数从没动过,也没迁移过,八个图表挂了九个设置文件里的八个,剩下那个为何没启、用的什么参数如今都忘了。截至 2020 年 6 月 10 日它还在内置 VPS 上自顾自跑,教学用途的这个项目仍表现出盈利倾向——外汇与贵金属高杠杆,这种无人干预的钝化运行只适合观察,不能直接当实盘范本。

「把模板 EA 拆了重装」

MT5 的项目机制允许把任意复杂度的程序拆成协作工程,几个人分头写趋势判断、入场逻辑和风控模块,比单文件裸写 EA 更不容易出低级 bug。原文里那套 MeanReversion 均值回归 EA 规则极简,本质就是横盘吸血模板:只做区间反转,没有 trailing stop,也没有硬止损止盈。 真要拿去跑贵金属或外汇,先把判断趋势和横盘的函数换掉,比如把限价挂单改成市价单,或者只在 ADX 低于 20 的压缩波段里触发。原 EA 缺的风控三件套,得自己补,否则一波单边就能吞掉前面十次均值回归的利润——这两类品种杠杆高,回撤可能非常凶。 标准发布包里的 MeanReversion 项目可以直接复制到新文件夹实验,改完编译出一个 ex5 就能实盘验证。项目只认主程序一条编译链,自定义指标必须写全路径,别指望扔进项目目录就能自动引用。 最后一句大实话:这套工程最大的价值不是那个 EA 能赚多少,而是它把「换函数即换策略」的脚手架搭好了,你肯动手改两处,比收藏一百篇教程都管用。

常见问题

分别写带宽函数、中轨斜率函数、价格触碰函数,带宽收窄且斜率近零判横盘,带宽放大且斜率偏离判趋势,直接挂不同逻辑分支。
用快EMA减慢EMA除以慢EMA得相对斜率,实盘九个月看0.0003以上倾向多、以下倾向空,品种不同要重测。
小布可读取你导出的参数集与品种漏洞记录,自动比对横盘/趋势拆分条件并标出疑似过度拟合段,省去手动翻九个月日志。
在OnTick里用静态变量记上次成交的K线时间,时间不变直接return,可彻底避开同根K线追单。
先单独跑该品种三个月回测看横盘误判率,点差超过均值2倍时模板EA的挂单动作要加滑点过滤再实盘。