TradeObjects: 基于 MetaTrader 图形对象的自动化交易·进阶篇
📐

TradeObjects: 基于 MetaTrader 图形对象的自动化交易·进阶篇

(2/3)· 多数图形对象 EA 要么功能阉割要么价格劝退,本文给出可改写的开源中庸方案

实战向进阶 第 2/3 篇

接上篇,我们继续深挖如何把图表上的趋势线、通道和斐波那契真正变成下单指令。很多交易者以为拖一根线就能让 EA 自动盯盘,结果要么对象属性读错,要么测试器里根本没法交互调试。MT5 可视测试器目前不支持交互式对象操作,这套逻辑只能在 MT4 里闭环验证。

用事件驱动接住图表上的手画线

想在 MT5 上自动识别交易者手动画的趋势线,第一步不是去扫价格,而是先把图表事件通道打开。下面这段代码在 OnInit 里做了三件事:允许图表对象被删除时触发事件、挂上 1 秒定时器、再调一次初始化。 [CODE]void OnInit(){ ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); EventSetTimer(1); to.handleInit(); }[/CODE] OnChartEvent 把图表交互(含画线、删线)转交给一个叫 to 的实例处理;OnTimer 和 OnTick 则分别按秒和按报价驱动状态刷新。这样手画一条线,最快 1 秒内就可能被脚本感知,不需要等下一根 K 线。 内部用 LineObject 结构记录每条线的名字和状态,attachObject 负责去重挂载:若数组中已存在同名线就把 status 置 1,否则数组扩容一位再写入并返回 true。detectLines 则遍历当前图表全部对象(ObjectsTotal 取数量),对合规对象调用 attachObject,新线会触发 describe 并打印 "New lines: N"。外汇与贵金属波动剧烈,手画线被秒删秒画是常态,这种事件+定时双触发能降低漏检概率。

MQL5 / C++
class="type">void OnInit()
{
  ChartSetInteger(class="num">0, CHART_EVENT_OBJECT_DELETE, true);
  EventSetTimer(class="num">1);
  to.handleInit();
}
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
  to.handleChart(id, lparam, dparam, sparam);
}
class="type">void OnTimer()
{
  to.handleTimer();
}
class="type">void OnTick()
{
  to.handleTick();
}
  class="kw">private:
    class="kw">struct LineObject
    {
      class="type">class="kw">string name;
      class="type">int status;
      class="type">void class="kw">operator=(class="kw">const LineObject &o)
      {
        name = o.name;
        status = o.status;
      }
    };
    LineObject objects[];
  class="kw">protected:
    class="type">bool attachObject(class="kw">const class="type">class="kw">string name)
    {
      class="type">bool found = false;
      class="type">int n = ArraySize(objects);
      for(class="type">int i = class="num">0; i < n; i++)
      {
        if(objects[i].name == name)
        {
          objects[i].status = class="num">1;
          found = true;
          class="kw">break;
        }
      }
      if(!found)
      {
        ArrayResize(objects, n + class="num">1);
        objects[n].name = name;
        objects[n].status = class="num">1;
        class="kw">return true;
      }
      class="kw">return false;
    }
    class="type">bool detectLines()
    {
      startRefresh();
      class="type">int n = ObjectsTotal(ChartID(), class="num">0);
      class="type">int count = class="num">0;
      for(class="type">int i = class="num">0; i < n; i++)
      {
        class="type">class="kw">string obj = ObjectName(ChartID(), i, class="num">0);
        if(checkObjectCompliance(obj))
        {
          if(attachObject(obj))
          {
            describe(obj);
            count++;
          }
        }
      }
      if(count > class="num">0) Print("New lines: ", count);
      class="type">bool changes = stopRefresh() || (count > class="num">0);
      if(changes)
      {
        display();
      }
    }

「用对象属性过滤出可触发线」

想在 MT5 图表上自动识别「自己画的线」并触发提醒,第一步是给对象做合规性过滤。下面这段逻辑用前缀、类型、线型、颜色四个维度卡掉无关图形,只留你计划监控的那批。 checkObjectCompliance 先判断对象名是否带通用前缀(CommonPrefit 为空则不限),再查它的 OBJPROP_TYPE 是否落在 _ln 集合里——这里只收 OBJ_HLINE、OBJ_VLINE、OBJ_TREND、OBJ_CHANNEL、OBJ_FIBO 五种。线型必须匹配 _st 里的 InstantType、PendingType、CloseStopLossTakeProfitType,颜色必须命中 _cc 里的 BuyColor、SellColor、ActivationColor,三者同时为真才返回 true。 processLines 里遍历 objects 数组,用 ObjectGetInteger(ChartID(), name, OBJPROP_BACK) 跳过背景层对象;随后取出 style、clr、text、last 等属性。当 clr == ActivationColor 且 style == InstantType 时,若 checkActivation(price) 通过,就 disableLine(i) 并依 text 前缀发 Alert / Push / Mail——text 以 "Alert:" 开头截第 6 字符往后播报,"Push:" 与 "Mail:" 截第 5 字符往后推送。外汇与贵金属波动剧烈,这类自动提醒只降低漏看概率,不保证信号有效,实盘前请在模拟盘验证触发逻辑。 把 _ln / _st / _cc 三个 Set 的参数改成你模板里的实际枚举值,编译后拖到图表,画一条 ActivationColor 的瞬时线就能测通通知链路。

MQL5 / C++
class="type">bool checkObjectCompliance(class="kw">const class="type">class="kw">string obj)
{
   if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == class="num">0)
   {
      if(_ln[ObjectGetInteger(class="num">0, obj, OBJPROP_TYPE)]
      && _st[ObjectGetInteger(class="num">0, obj, OBJPROP_STYLE)]
      && _cc[(class="type">color)ObjectGetInteger(class="num">0, obj, OBJPROP_COLOR)])
      {
         class="kw">return true;
      }
   }
   class="kw">return false;
}
class="macro">#include <Set.mqh>
Set<ENUM_OBJECT> _ln(OBJ_HLINE, OBJ_VLINE, OBJ_TREND, OBJ_CHANNEL, OBJ_FIBO);
Set<ENUM_LINE_STYLE> _st(InstantType, PendingType, CloseStopLossTakeProfitType);
Set<class="type">color> _cc(BuyColor, SellColor, ActivationColor);
class="type">void processLines()
{
   class="type">int n = ArraySize(objects);
   for(class="type">int i = class="num">0; i < n; i++)
   {
      class="type">class="kw">string name = objects[i].name;
      if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) class="kw">continue;
      class="type">int style = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_STYLE);
      class="type">color clr = (class="type">color)ObjectGetInteger(class="num">0, name, OBJPROP_COLOR);
      class="type">class="kw">string text = ObjectGetString(class="num">0, name, OBJPROP_TEXT);
      class="type">class="kw">datetime last = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_ZORDER);
      class="type">class="kw">double aux = class="num">0, auxf = class="num">0;
      class="type">class="kw">double price = getCurrentPrice(name, aux, auxf);
      ...
      if(clr == ActivationColor)
      {
         if(style == InstantType)
         {
            if(checkActivation(price))
            {
               disableLine(i);
               if(StringFind(text, "Alert:") == class="num">0) Alert(StringSubstr(text, class="num">6));
               else if(StringFind(text, "Push:") == class="num">0) SendNotification(StringSubstr(text, class="num">5));
               else if(StringFind(text, "Mail:") == class="num">0) SendMail("TradeObjects", StringSubstr(text, class="num">5));

◍ 从图表对象解析买单的止损止盈

这段代码承接了上一节对图形标签的识别逻辑,专门处理当对象颜色被判定为 BuyColor 且样式为 InstantType 时的市价买单生成路径。它先调用 checkMarket 比较当前价与上一tick价格得出方向,若方向为0且 checkTime 通过,则按颜色强制赋予 +1(买)或 -1(卖)方向。 手数取自图表对象文本,用 StringToDouble 解析 OBJPROP_TEXT,若解析为0则回退到默认 Lot 变量;这一设计让你可以在对象上直接写「2」代表2%风险手数,不写就跑默认仓。 aux 参数决定 sl/tp 的计算方式:aux 大于 Ask 时视为止盈价,否则视为止损价;若 aux 为0,则统一用 DefaultStopLoss / DefaultTakeProfit 经 getPointsForLotAndRisk 换算点数再乘 _Point。所有 sl、tp 最后经 NormalizeDouble 按 _Digits 对齐报价精度,避免 MT5 拒单。 外汇与贵金属杠杆高,这类由图表对象触发的自动下单若 aux 写错方向,可能把止损当止盈挂,实盘前务必在策略测试器用历史数据跑一遍 checkMarket 与 aux 分支。

MQL5 / C++
      else Print(text);
        }
      }
      else if(clr == BuyColor)
      {
        if(style == InstantType)
        {
          class="type">int dir = checkMarket(price, last);
          if((dir == class="num">0) && checkTime(name))
          {
            if(clr == BuyColor) dir = +class="num">1;
            else if(clr == SellColor) dir = -class="num">1;
          }
          if(dir > class="num">0)
          {
            class="type">class="kw">double lot = StringToDouble(ObjectGetString(class="num">0, name, OBJPROP_TEXT)); class=class="str">"cmt">// lot[%]
            if(lot == class="num">0) lot = Lot;

            class="type">class="kw">double sl = class="num">0.0, tp = class="num">0.0;
            if(aux != class="num">0)
            {
              if(aux > Ask)
              {
                tp = aux;
                if(DefaultStopLoss != class="num">0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
              }
              else
              {
                sl = aux;
                if(DefaultTakeProfit != class="num">0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
              }
            }
            else
            {
              if(DefaultStopLoss != class="num">0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
              if(DefaultTakeProfit != class="num">0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
            }

            sl = NormalizeDouble(sl, _Digits);
            tp = NormalizeDouble(tp, _Digits);

            class="type">int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp);

用反手市价单平掉空头头寸

当图形对象被识别为 CloseStopLossTakeProfitType(空头平仓、空头止损或空头止盈线)且触发条件满足时,脚本走另一条分支:先由 checkMarket(price) 或 checkTime(name) 给出方向信号 dir,非 0 才继续。 若从对象文本里解析出的 lot 大于 0,就按该手数调用 e.placeMarketOrder(OP_BUY) 反手买入,由系统内部触发 OrderCloseBy() 对销空头;返回票号不等于 -1 即视为成功并 disableLine(i),否则弹「Partial sell close failed」提示。 lot 解析为 0 或负值时,改为 e.closeMarketOrders(e.mask(OP_SELL)) 一把平掉所有空头,返回值大于 0 才禁用该线,否则报「Complete sell close failed」。外汇与贵金属杠杆高,这类自动对销在滑点行情中可能部分失效,务必在 MT5 策略测试器里先跑一遍。 checkActivation() 用 EventHotSpot * _Point 构造一个双侧容差带:Bid 落进 [price - 热点, price + 热点] 直接返回 true;同时保留前一根 Bid 上穿 price 的判定,避免只在盘口粘连时误触。

MQL5 / C++
if(ticket != -class="num">1) class=class="str">"cmt">// success
{
  disableLine(i);
}
else
{
  showMessage("Market buy failed with &class="macro">#x27;" + name + "&class="macro">#x27;");
}
      }
    }
    else if(style == CloseStopLossTakeProfitType) class=class="str">"cmt">// 空头平仓, 空头止损, 空头止盈
    {
      class="type">int dir = checkMarket(price) || checkTime(name);
      if(dir != class="num">0)
      {
        class="type">class="kw">double lot = StringToDouble(ObjectGetString(class="num">0, name, OBJPROP_TEXT)); class=class="str">"cmt">// lot
        if(lot > class="num">0)
        {
          if(e.placeMarketOrder(OP_BUY, lot) != -class="num">1) class=class="str">"cmt">// 将触发 OrderCloseBy();
          {
            disableLine(i);
          }
          else
          {
            showMessage("Partial sell close failed with &class="macro">#x27;" + name + "&class="macro">#x27;");
          }
        }
        else
        {
          if(e.closeMarketOrders(e.mask(OP_SELL)) > class="num">0)
          {
            disableLine(i);
          }
          else
          {
            showMessage("Complete sell close failed with &class="macro">#x27;" + name + "&class="macro">#x27;");
          }
        }
      }
    }
class="type">bool checkActivation(class="kw">const class="type">class="kw">double price)
{
  if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point)
  {
    class="kw">return true;
  }
  
  if((PreviousBid < price && Bid >= price)

「用趋势线坐标反推实时价位」

上面那段 checkMarket 只是判断方向,真正要把画线对象变成可监控的价位,还得靠 getCurrentPrice 把趋势线的时间锚点换算成 BAR 索引。 它先读对象的 OBJPROP_TYPE,确认是 OBJ_TREND 才继续;随后取两个端点的 OBJPROP_TIME,用 iBarShift(NULL,0,dt,true) 找对应 K 线位置。若 i1<=i2 或任一返回 -1,说明线画反了或超出历史,直接 Print("Incorrect line") 并返 0,避免后续误判。 在 MT5 里手动画一条趋势线,把这段代码挂到 EA 的 OnChartEvent 里,就能实时拿到线对应的报价值,再喂给前面的 checkMarket 做穿越判定。外汇与贵金属杠杆高,这类自动监控只作辅助,信号失效可能随时发生。

MQL5 / C++
class="type">class="kw">double getCurrentPrice(class="kw">const class="type">class="kw">string name, class="type">class="kw">double &auxiliary, class="type">class="kw">double &auxiliaryFibo)
{
   class="type">int type = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_TYPE);
   if(type == OBJ_TREND)
   {
      class="type">class="kw">datetime dt1 = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_TIME, class="num">0);
      class="type">class="kw">datetime dt2 = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_TIME, class="num">1);
      class="type">int i1 = iBarShift(NULL, class="num">0, dt1, true);
      class="type">int i2 = iBarShift(NULL, class="num">0, dt2, true);
      if(i1 <= i2 || i1 == -class="num">1 || i2 == -class="num">1)
      {
         Print("Incorrect line: ", name);
         class="kw">return class="num">0;
      }
把对象信号交给小布盯盘
这些图形对象边界的突破与回滚诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时标注,你只需决定跟不跟。把重复劳动交给小布,你专注决策。

常见问题

当前 MT5 可视测试器不允许以交互方式添加、删除或编辑对象,而本工具依赖运行时对象操作,因此历史回测调试只能走 MT4 测试器。
核心坐标与边界属性在 MT4/MT5 中基本一致,但 MT5 新增对象类型会破坏兼容性,本篇仅覆盖趋势线、水平线、垂直线、等距通道和斐波那契回撤五种。
可以,小布盯盘的 AIGC 模块会解析常见图形对象的逻辑边界,在品种页标出突破或回滚概率区域,省去手动写 EA 的门槛。
外汇与贵金属属高风险品种,滑点在流动性稀薄时段可能放大,回测仅作概率参考,实盘倾向以边界触发结合点差过滤。
建议用条件编译隔离新对象处理分支,老分支维持原逻辑,这样扩展时 MT4 编译不受影响。