TradeObjects: 基于 MetaTrader 图形对象的自动化交易

16 十月 2017, 08:04
Stanislav Korotky
0
3 463
品种图表上的几何结构早已是数十年来最受欢迎的交易工具之一。先进的技术令其轻松地应用支撑或阻力线、历史价位和整体形态, 例如通道和菲波纳奇网格。算法交易软件能够令我们分析标准形态并在交易中使用它们。MetaTrader 还拥有众多应用程序能够在一定程度上将流程自动化: 在大多数情况下, 只需在已启动 EA 或脚本的图表上添加对象即可。应用程序将根据设置在适当的时间点建仓, 跟踪并将之平仓。这样的应用程序令我们不仅可以在网上进行交易, 还可以在测试器的可视化模式下磨练我们的技能。我们可以在 代码库市场 中找到这样的应用程序。

然而, 一切并非那么简单。通常, 代码库里的应用程序只有简化的功能。此外, 它们含有更新且迅速变得过时 (经常与最新的 MQL 语言和平台版本失去兼容性), 而商用产品常常太贵了。

在本文中, 我们将开发一款体现中庸之道的新工具。它将尽可能地简洁, 同时提供充足的机会。它将兼容 MetaTrader 4 和 MetaTrader 5。感谢开源代码, 可以轻松地扩展和修改它以便满足您的需求。

对 MetaTrader 4 的支持很重要, 不仅在于受众广泛方面, 而且还因为 MetaTrader 5 测试器的一些限制。特别地, MetaTrader 5 可视测试器当前不允许以交互方式处理对象 (添加, 删除, 编辑属性), 而这一条对此工具是必需的。只有 MetaTrader 4 测试器能够提高我们管理历史对象的技能。

设置需求

涉及图形对象的自动交易系统其主要意识形态方面的需求很简单。我们将会使用标准的 MetaTrader 界面和图形对象的属性, 而无需特殊的面板和复杂的配置逻辑。实践表明, 富含各种选项的强劲系统, 其所有优点往往由于开发困难和每个特定操作模式的相关性较低而被损失殆尽。我们尽力只用那些众所周知的对象处理方法及其属性的直观解释。

用于制定交易决策的多种类型图形对象中, 最常用的是以下这些:

  • 趋势线;
  • 水平线;
  • 垂直线;
  • 等距通道;
  • 菲波纳奇回撤。

我们将在首要位置为它们提供支持。列表可能已被 MetaTrader 5 中可用的新类型扩展, 但这将破坏与 MetaTrader 4 的兼容性。用户将能够根据自己的喜好轻松添加其它对象, 并运用在本项目中已实现的对象处理基本原理。

上述每个对象在图表的二维空间中形成逻辑边界。自边界交叉或回滚会产生信号。其解释通常对应于以下情况之一:

  • 从支撑/阻力线突破或回滚;
  • 从历史价位突破或回落;
  • 达到指定的止损/止盈价位;
  • 达到指定的时间。

取决于所选策略, 交易者可以买入或卖出, 设置挂单或响应事件而平仓。我们的系统应该支持所有这些动作。因此,基本功能列表如下:

  • 发送各种通知 (警报, 推送通知, 电子邮件);
  • 进场开单;
  • 放置挂单 (buy stop, sell stop, buy limit, sell limit);
  • 全面或部分平仓, 包括止损和止盈。

开发用户界面

在众多相似的产品中, 大多关注与用户界面元素, 如面板, 对话框, 按钮以及广泛运用的拖放。我们的项目与此无关。我们仅使用 MetaTrader 提供的默认元素来替代特殊的图形界面。

每个对象都有一组标准的属性可以契合当前任务。

首先, 我们需要将自动交易使用的对象与我们可以在图表上绘制的其它对象区分开来。为此, 我们需要为我们的对象名称提供一个预定义的前缀。如果未指定前缀, 则 EA 将假定具有相应属性的所有对象都处于活动状态。不过, 不建议使用这种模式, 因为终端可能创建自己的对象 (例如, 平仓), 这可能会产生副作用。

第二, 为了执行各种功能 (上面提供的功能列表), 我们应该保留不同的执行样式。例如, 在 EA 设置中, 设置 STYLE_DASH 样式用以放置挂单, 而 STYLE_SOLID — 当价格穿越相应指示线时立即入场。不同操作的样式应该不同。

第三, 我们应该指定交易方向。例如, 您可以使用蓝色作为买入, 红色作为卖出。与入场离场无关的操作将以第三种颜色标记 - 例如, 灰色。举例来说, 这些可能是通知或下订单。后一种情况被归类为 "中性 类别, 因为它通常设置一对不同指向的挂单。

第四, 我们应该定义挂单的类型。这可依据订单指示线相对于当前市价的相互排列和线条颜色来完成。例如, 高于价格的蓝线意味着 buy stop, 而低于价格的蓝线意味着 buy limit。

第五, 对于大多数操作, 我们需要一些额外的数据和属性。特别地, 如果已经触发了一个警报 (或多个), 则用户更希望接收到有意义的消息。我们尚未使用的 Description 字段非常适合于此。我们将在此字段中设置订单的手数和有效时间。所有这些都是可选的, 因为出于便利我们要为输入参数提供默认值, 且在 EA 中将必要的对象设置最小化。除了对象设置之外, 这些默认值将包含止损和止盈值。

如果为每条特定指示线设置止损和止盈, 请使用多线组合对象。例如, 等距通道有两条线。第一次穿越两条线负责形成一个交易信号, 而平行线 (第三点) 设定止停和止盈的距离。我们很容易通过相互排列和线颜色的定义来处理价位。例如, 位于主体上方的附加线形成红色通道的止损。如果低于它. 那么它将被视为止盈。

如果您要设定止损和止盈, 则该对象至少应由三条线组成。例如, 菲波纳奇等级的网格恰合于此。默认情况下, 项目采用标准等级 38.2% (止损), 61.8% (入场点) 和 161.8% (止盈)。在本文中, 我们不会介绍如何更灵活亦或更复杂地配置对象类型。

当激活对象之一表示价格交叉时, 对象应被标记为已激活。这可通过将 OBJPROP_BACK "背景" 属性分配给它来完成。我们将这些对象的原始颜色亮度降低向用户直观反馈。例如, 处理后蓝色线变成深蓝色。

然而, 交易者的标记经常包括与之相关事件的 "强势" 线段和价位 — 像是在上升趋势调整期间自支撑线回滚 — 也许会多次发生。

我们在粗线的帮助下研究这种情况。我们知道, MetaTrader 的样式允许将宽度设置为 1 到 5。当激活指示线时, 我们将看它的粗细度, 如果超过 1, 我们将把粗细度减 1, 而不是将其排除在后续处理之外。例如, 我们可以在图表上指定预期的多个事件, 最多重复 5 次。

这可能有一点细微差别: 价格倾向于围绕某一价位波动, 任何指示线均可在短时间内交叉多次。手工交易者通过视觉分析价格偏差的动态, 并排除所有的价格噪音。EA 应该实现一个自动执行的机制。

为此目的, 我们将引入输入来判断交汇处的 "热点区域" 大小, 即价格走势的最小范围及其持续时间, 此间不会形成信号。换言之, "线交叉" 事件不会在价格穿越之后立即发生, 只有当它退缩到指定的点数距离时才会。

类似地, 我们将引入一个参数, 该参数指定同一条指示线 (仅对于线宽大于 1) 的两个连续事件之间的最小间隔。这里出现了一个新的任务: 我们需要保存某条指示线前一事件的时间。我们使用 OBJPROP_ZORDER 对象属性。这是一个 long 类型数字, datetime 数值可完美地保存于内。指示线显示顺序的变化几乎对图表的视觉呈现没有影响。

配置与系统操作所用的指示线, 执行以下操作足矣:

  • 打开对象属性对话框;
  • 将选定的前缀添加到名称;
  • 在描述中设置参数:
    • 市价和挂单指示线, 以及部分平仓的手数,
    • 挂单激活指示线的名称,
    • 挂单指示线的有效期指示线;
  • 颜色作为方向指示器 (默认是蓝色 — 买入, 红色 — 卖出, 灰色 — 中性);
  • 样式作为操作选择器 (警报, 入场, 挂单, 平仓);
  • 宽度作为事件重复指示器。

配置 buy limit 订单 (蓝色短划线) 的水平线属性, 手数 0.02, 且有效期为 24 根柱线 (小时)

配置 buy limit 订单 (蓝色短划线) 的水平线属性, 手数 0.02, 且有效期为 24 根柱线 (小时)

由系统管理的订单列表 (符合需求描述) 显示在图表中, 注释内是合并的详情 — 对象类型, 描述和状态。


开发执行机制

我们开始实现 EA, 起初是传递对象名称的前缀, 过程颜色和样式的输入, 默认值以及生成图表事件的区域大小, 同时考虑到上述需求和一般注意事项。

input int Magic = 0;
input double Lot = 0.01 /*默认手数*/;
input int Deviation = 10 /*订单执行期间能够容忍的价格变化*/;

input int DefaultTakeProfit = 0 /*点数*/;
input int DefaultStopLoss = 0 /*点数*/;
input int DefaultExpiration = 0 /*柱线数*/;

input string CommonPrefit = "exp" /*清空以便处理所有兼容的对象*/;

input color BuyColor = clrBlue /*用于买入的市价和挂单 - 开单价, 收盘价, 止损, 止盈*/;
input color SellColor = clrRed /*用于卖出的市价和挂单 - 开单价, 收盘价, 止损, 止盈*/;
input color ActivationColor = clrGray /*放置挂单的激活警报*/;

input ENUM_LINE_STYLE InstantType = STYLE_SOLID /*开市价单*/;
input ENUM_LINE_STYLE PendingType = STYLE_DASH /*定义可能的挂单 (需要激活)*/;
input ENUM_LINE_STYLE CloseStopLossTakeProfitType = STYLE_DOT /*适用于持仓*/;

input int EventHotSpot = 10 /*点数*/;
input int EventTimeSpan = 10 /*秒数*/;
input int EventInterval = 10 /*柱线数*/;

EA 本身作为 TradeObjects 类 (TradeObjects.mq4 和 .mq5) 实现。其唯一的公开元素是处理标准事件的构造函数, 析构函数和方法。

class TradeObjects
{
  private:
    Expert *e;

  public:
    void handleInit()
    {
      detectLines();
    }
    
    void handleTick()
    {
      #ifdef __MQL4__  
      if(MQLInfoInteger(MQL_TESTER))
      {
        static datetime lastTick = 0;
        if(TimeCurrent() != lastTick)
        {
          handleTimer();
          lastTick = TimeCurrent();
        }
      }
      #endif
    
      e.trailStops();
    }
    
    void handleTimer()
    {
      static int counter = 0;
      
      detectLines();
      
      counter++;
      if(counter == EventTimeSpan) // 等待 EventTimeSpan, 直到我们有的价格的历史纪录
      {
        counter = 0;
        if(PreviousBid > 0) processLines();
        if(PreviousBid != Bid) PreviousBid = Bid;
      }
    }
    
    void handleChart(const int id, const long &lparam, const double &dparam, const string &sparam)
    {
      if(id == CHARTEVENT_OBJECT_CREATE || id == CHARTEVENT_OBJECT_CHANGE)
      {
        if(checkObjectCompliance(sparam))
        {
          if(attachObject(sparam))
          {
            display();
            describe(sparam);
          }
        }
        else
        {
          detectLines();
        }
      }
      else if(id == CHARTEVENT_OBJECT_DELETE)
      {
        if(removeObject(sparam))
        {
          display();
          Print("Line deleted: ", sparam);
        }
      }
    }
    
    TradeObjects()
    {
      e = new Expert(Magic, Lot, Deviation);
    }
    
    ~TradeObjects()
    {
      delete e;
    }
};

静态创建类的实例, 后将其事件处理程序绑定到相应的全局函数。

TradeObjects to;

void OnInit()
{
  ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
  EventSetTimer(1);
  to.handleInit();
}

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
  to.handleChart(id, lparam, dparam, sparam);
}
                 
void OnTimer()
{
  to.handleTimer();
}

void OnTick()
{
  to.handleTick();
}

所有交易操作都分配给隐藏在外部 Expert 类 (Expert01.mqh) 中的单独引擎。我们在构造函数中创建其实例, 并在 TradeObjects 类的析构函数中将其删除。稍后会详细研究引擎。TradeObjects 会将许多操作委托给它。

所有 handleInit, handleTick, handleTimer, handleChart 事件处理程序应该调用我们编写的 detectLines 方法。在此方法中, 分析对象并选择满足我们需求的对象。用户可以在运行 EA 时创建, 删除和修改对象。找到的对象被保存到内部数组。它的存在可令您监视图表状态变化, 并通知用户发现新对象并删除旧对象。

processLine 方法由定时器周期性地调用。它循环检查数组中的事件是否发生并执行适当的动作。

正如我们所见, 该方法使用 checkObjectCompliance 方法检查对象的前缀以及它们的样式, 颜色和状态 (见下文)。使用 attachObject 函数将合适的对象添加到内部数组, 而从图表中删除的对象将使用 removeObject 函数从数组中删除。使用 "display" 方法将对象列表作为图表注释显示。

该数组由包含对象名称和状态的简单结构组成:

  private:
    struct LineObject
    {
      string name;
      int status;
      void operator=(const LineObject &o)
      {
        name = o.name;
        status = o.status;
      }
    };
    
    LineObject objects[];

该状态主要用于标记存在的对象 — 例如, 使用 attachObject 函数向数组添加新对象之后:

  protected:
    bool attachObject(const string name)
    {
      bool found = false;
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        if(objects[i].name == name)
        {
          objects[i].status = 1;
          found = true;
          break;
        }
      }
      
      if(!found)
      {
        ArrayResize(objects, n + 1);
        objects[n].name = name;
        objects[n].status = 1;
        return true;
      }
      
      return false;
    }

detectLines 方法在随后的时刻检查每个对象的存在:

    bool detectLines()
    {
      startRefresh();
      int n = ObjectsTotal(ChartID(), 0);
      int count = 0;
      for(int i = 0; i < n; i++)
      {
        string obj = ObjectName(ChartID(), i, 0);
        if(checkObjectCompliance(obj))
        {
          if(attachObject(obj))
          {
            describe(obj);
            count++;
          }
        }
      }
      if(count > 0) Print("New lines: ", count);
      bool changes = stopRefresh() || (count > 0);
      
      if(changes)
      {
        display();
      }
      
      return changes;
    }

在此, startRefresh 辅助函数在一开始就被调用。其目的是将所有数组对象的状态标志重置为 0。之后, 工作对象使用 attachObject 在循环内再次收到状态 1。在末尾执行 stopRefresh 的调用。它通过零状态在内部数组中找到未使用的对象, 并通知用户。

在 checkObjectCompliance 方法中检查每个对象是否符合要求:

    bool checkObjectCompliance(const string obj)
    {
      if(CommonPrefit == "" || StringFind(obj, CommonPrefit) == 0)
      {
        if(_ln[ObjectGetInteger(0, obj, OBJPROP_TYPE)]
        && _st[ObjectGetInteger(0, obj, OBJPROP_STYLE)]
        && _cc[(color)ObjectGetInteger(0, obj, OBJPROP_COLOR)])
        {
          return true;
        }
      }
      return false;
    }

除了名称前缀之外, 还会检查类型、样式和对象颜色的标志设置。Set 辅助类即用于此:

#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<color> _cc(BuyColor, SellColor, ActivationColor);

现在, 是时候描述主要方法了 — processLines。该方法的作用影响其大小。整体代码在附件中呈现。在此, 我们只揭示关键的片段。

    void processLines()
    {
      int n = ArraySize(objects);
      for(int i = 0; i < n; i++)
      {
        string name = objects[i].name;
        if(ObjectGetInteger(ChartID(), name, OBJPROP_BACK)) continue;
        
        int style = (int)ObjectGetInteger(0, name, OBJPROP_STYLE);
        color clr = (color)ObjectGetInteger(0, name, OBJPROP_COLOR);
        string text = ObjectGetString(0, name, OBJPROP_TEXT);
        datetime last = (datetime)ObjectGetInteger(0, name, OBJPROP_ZORDER);
    
        double aux = 0, auxf = 0;
        double price = getCurrentPrice(name, aux, auxf);
        ...

在循环中, 我们要排除已使用的那些 (具有 OBJPROP_BACK 标志) 对象, 并传递其余所有对象。下面显示的 getCurrentPrice 函数允许我们找出当前的对象价格。由于某些对象类型由若干条线组成, 所以我们应该使用 2 个参数传递额外的价位。

        if(clr == ActivationColor)
        {
          if(style == InstantType)
          {
            if(checkActivation(price))
            {
              disableLine(i);
              if(StringFind(text, "Alert:") == 0) Alert(StringSubstr(text, 6));
              else if(StringFind(text, "Push:") == 0) SendNotification(StringSubstr(text, 5));
              else if(StringFind(text, "Mail:") == 0) SendMail("TradeObjects", StringSubstr(text, 5));
              else Print(text);
            }
          }

接着, 我们应该检查对象样式以便判断事件类型, 及其价格如何与第 0 根柱线上的供给价相关联 — checkActivation 函数会在发生警报和放置挂单时执行。如果发生激活, 请执行相应的动作 (在发生警报的情况下, 显示消息或将其发送给用户), 并使用 disableLine 将对象标记为禁用。

当然, 交易操作的激活代码会更加复杂。以下是市价买入并将空头平仓的简单示例:

        else if(clr == BuyColor)
        {
          if(style == InstantType)
          {
            int dir = checkMarket(price, last);
            if((dir == 0) && checkTime(name))
            {
              if(clr == BuyColor) dir = +1;
              else if(clr == SellColor) dir = -1;
            }
            if(dir > 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot[%]
              if(lot == 0) lot = Lot;
    
              double sl = 0.0, tp = 0.0;
              if(aux != 0)
              {
                if(aux > Ask)
                {
                  tp = aux;
                  if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                }
                else
                {
                  sl = aux;
                  if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
                }
              }
              else
              {
                if(DefaultStopLoss != 0) sl = Bid - e.getPointsForLotAndRisk(DefaultStopLoss, lot) * _Point;
                if(DefaultTakeProfit != 0) tp = Bid + e.getPointsForLotAndRisk(DefaultTakeProfit, lot) * _Point;
              }
              
              sl = NormalizeDouble(sl, _Digits);
              tp = NormalizeDouble(tp, _Digits);
            
              int ticket = e.placeMarketOrder(OP_BUY, lot, sl, tp);
              if(ticket != -1) // success
              {
                disableLine(i);
              }
              else
              {
                showMessage("Market buy failed with '" + name + "'");
              }
            }
          }
          else if(style == CloseStopLossTakeProfitType) // 空头平仓, 空头止损, 空头止盈
          {
            int dir = checkMarket(price) || checkTime(name);
            if(dir != 0)
            {
              double lot = StringToDouble(ObjectGetString(0, name, OBJPROP_TEXT)); // lot
              if(lot > 0)
              {
                if(e.placeMarketOrder(OP_BUY, lot) != -1) // 将触发 OrderCloseBy();
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Partial sell close failed with '" + name + "'");
                }
              }
              else
              {
                if(e.closeMarketOrders(e.mask(OP_SELL)) > 0)
                {
                  disableLine(i);
                }
                else
                {
                  showMessage("Complete sell close failed with '" + name + "'");
                }
              }
            }
          }

checkMarket 函数 (更复杂的 checkActivation 版本) 检查事件发生 (两者都在下面描述)。当事件被触发时, 我们从对象属性中收到止损/止盈及手数, 然后开单。

合约中的手数大小在对象描述中指定, 或作为可用保证金的百分比指定 - 在后一种情况下, 该值被写为负数。这种符号的含义很容易记住, 如果您想象一下, 您实际上在指明资金的一部分可用于新订单。

checkActivation和checkMarket 函数是相似的。它们都使用 EA 的输入定义事件激活区域大小:

    bool checkActivation(const double price)
    {
      if(Bid >= price - EventHotSpot * _Point && Bid <= price + EventHotSpot * _Point)
      {
        return true;
      }
      
      if((PreviousBid < price && Bid >= price)
      || (PreviousBid > price && Bid <= price))
      {
        return true;
      }
      return false;
    }
    
    int checkMarket(const double price, const datetime last = 0) // 返回价格走势的方向
    {
      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }
    
      if(PreviousBid >= price - EventHotSpot * _Point && PreviousBid <= price + EventHotSpot * _Point)
      {
        if(Bid > price + EventHotSpot * _Point)
        {
          return +1; // up
        }
        else if(Bid < price - EventHotSpot * _Point)
        {
          return -1; // down
        }
      }
    
      if(PreviousBid < price && Bid >= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return +1;
      }
      else if(PreviousBid > price && Bid <= price && MathAbs(Bid - PreviousBid) >= EventHotSpot * _Point)
      {
        return -1;
      }
      
      return 0;
    }

您可能还记得, 在周期为 EventTimeSpan 秒的 handleTimer 处理程序中, 由 EA 保存 PreviousBid 的价格。函数操作的结果是在第 0 根柱线上价格穿越对象供给价的标志, 因此 checkActivation 返回一个简单的逻辑标志, 而 checkMarket 是价格走势方向: +1 — 上行, -1 — 下行。

由于整个图表建立在包括应用价格在内的供给价上, 所以通过供给价来检测报价穿越对象。即使一位交易者形成买单的标记, 它们也是由正确的信号触发的: 采购价图表隐性地在供给价图表上方间距点差值经过, 而基于采购价图表的潜在指示线将同步地穿越当前标记的供给价。

对于 PendingType 样式和中性 ActivationColor 的指示线, 其行为是特殊的: 在它们被价格穿越的时刻, 放置挂单。订单放置使用其它指示线设置。它们的名称在激活指示线的描述中用斜杠 ('/') 分开。如果描述为空, 系统将按照样式查找所有挂单指示线并放置它们。就像市价单一样, 挂单的方向对应于其颜色 — BuyColor 或 SellColor 用于买或卖, 而在说明中, 您可以指定手数和有效日期 (以柱线为单位)。

表格中给出了对象的组合样式和颜色的方法及其对应值。

颜色和样式 买入颜色 卖出颜色 激活颜色
即时类型 市价买入 市价卖出 警报
挂单类型 潜在买入挂单 潜在卖出挂单 初始放置挂单
平仓止损止盈类型 平仓, 止损, 止盈
空头仓位
平仓, 止损, 止盈
多头仓位
全部平仓

我们回到 getCurrentPrice 方法, 这可能是 processLines 之后最重要的一个方法。

    double getCurrentPrice(const string name, double &auxiliary, double &auxiliaryFibo)
    {
      int type = (int)ObjectGetInteger(0, name, OBJPROP_TYPE);
      if(type == OBJ_TREND)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1)
        {
          Print("Incorrect line: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        return b;
      }
      else if(type == OBJ_HLINE)
      {
        return ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
      }
      else if(type == OBJ_VLINE)
      {
        return EMPTY_VALUE; // 不应是 null, 否则不使用
      }
      else if(type == OBJ_CHANNEL)
      {
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        datetime dt3 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 2);
        int i1 = iBarShift(NULL, 0, dt1, true);
        int i2 = iBarShift(NULL, 0, dt2, true);
        int i3 = iBarShift(NULL, 0, dt3, true);
        if(i1 <= i2 || i1 == -1 || i2 == -1 || i3 == -1)
        {
          Print("Incorrect channel: ", name);
          return 0;
        }
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        double p3 = ObjectGetDouble(0, name, OBJPROP_PRICE, 2);
        
        double k = -(p1 - p2)/(i2 - i1);
        double b = -(i1 * p2 - i2 * p1)/(i2 - i1);
        
        double dy = i3 * k + b - p3;
        
        auxiliary = p3 - i3 * k;
        
        return b;
      }
      else if(type == OBJ_FIBO)
      {
        // 回撤等级 61.8 是入场点 (buy/sell limit),
        // 38.2 和 161.8 作为止损/止盈
        
        double p1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
        double p2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);
        datetime dt1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
        datetime dt2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
        
        if(dt2 < dt1)
        {
          swap(p1, p2);
        }
        
        double price = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 4) + p1;
        auxiliary = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 2) + p1;
        auxiliaryFibo = (p2 - p1) * ObjectGetDouble(0, name, OBJPROP_LEVELVALUE, 6) + p1;
        return price;
      }
      return 0;
    }

这个思路很简单 — 根据对象类型, 我们应该在第 0 根柱线 (主要和附加指示线) 上计算其价格。将在图表上放置对象时, 对于所有对象最重要的一点就是它们位于过去 — 即一个有效的柱线编号。否则, 该对象被视为无效, 因为无法明确地计算其价格。

在垂直线的情况下, 我们返回 EMPTY_VALUE — 这不是零, 也不是特定的价格 (因为这样的指示线满足任何价格)。所以, 对于垂直线, 您应该使用额外的检查来匹配当前时间。这是由 checkTime 函数执行的。它已经在 processLines 片段中被调用。

    bool checkTime(const string name)
    {
      return (ObjectGetInteger(0, name, OBJPROP_TYPE) == OBJ_VLINE
        && (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0) == Time[0]);
    }

最后, 我们来描述一下 disableLine 函数的实现, 这在我们的代码中已多次遇到。

    void disableLine(const string name)
    {
      int width = (int)ObjectGetInteger(0, name, OBJPROP_WIDTH);
      if(width > 1)
      {
        ObjectSetInteger(0, name, OBJPROP_WIDTH, width - 1);
        ObjectSetInteger(0, name, OBJPROP_ZORDER, TimeCurrent());
      }
      else
      {
        ObjectSetInteger(0, name, OBJPROP_BACK, true);
        ObjectSetInteger(0, name, OBJPROP_COLOR, darken((color)ObjectGetInteger(0, name, OBJPROP_COLOR)));
      }
      display();
    }

如果线宽超过 1, 则将其增加 1, 并将当前事件时间保存在 OBJPROP_ZORDER 属性中。在普通线指示线的情况下, 我们将它们转移到背景并降低颜色亮度。背景中的对象被视为禁用。

对于 OBJPROP_ZORDER 属性, 它将在 processLines 方法中读入 'datetime last' 变量 (如上所示), 反过来, 它作为参数传递给 checkMarket (price, last) 方法。在内里, 我们确保从上一次激活以来的时间超过输入变量中设置的时间间隔 (以柱线为单位):

      if(last != 0 && (TimeCurrent() - last) / PeriodSeconds() < EventInterval)
      {
        return 0;
      }

如果在 CloseStopLossTakeProfitType 类型的对象描述中指定了手数, TradeObjects 允许您执行部分平仓。系统按照指定交易量逆向开仓, 然后调用 OrderCloseBy。要启用该模式, 输入变量中有一个特殊的 AllowOrderCloseBy 标志。如果它是开, 逆向仓位总是 "塌缩" 成一个。您也许知道, 此功能并非在所有帐户上都可用 (EA 检查设置, 如果该选项被阻止, 则发送相应的消息)。在 MetaTrader 5 的情况下, 该帐户应支持对冲。那些感兴趣的人可以通过实现部分平仓的替代方案来改进系统 — 无需使用 OrderCloseBy, 且有能力查看仓位列表, 并依据各种属性选择特定的一个减持。

我们返回 Expert 类执行 TradeObjects 的所有交易操作。这是一套简单的打仓和平仓方法, 支持止损以及根据指定的风险计算手数。它应用了 MetaTrader 4 订单隐喻, 该隐喻适用于使用 MT4Orders 函数库的 MetaTrader 5。

该类不提供修改已放置挂单的功能。移动它们的价格, 以及止损和止盈价位由终端管理: 如果启用 "显示交易价位" 选项, 它允许使用拖放进行操作。

Expert 类可以替换为您熟悉的任何其它代码。

附加的源代码在 MetaTrader 4 和 MetaTrader 5 中都可编译 (带有附加的头文件)。

在当前的 TradeObjects 实现中, 为了简化起见, 与严格的 OOP 实施存在略微偏差。否则, 您将不得不应用抽象交易界面, 实现 Expert 类作为接口的继承者, 然后将其传递给 TradeObjects 类 (例如, 通过 constructor 参数)。这是一个众所周知的 依赖注入 OOP 模板。在这个项目中, 交易引擎是硬性嵌入代码: 它在 TradeObjects 对象内创建和删除。

此外, 我们在 TradeObjects 类的代码里直接使用全局变量已违背了 OOP 原则。最佳的编程风格要求将它们传递到类构造器或指定方法作为参数。这将允许我们使用在另一个 EA 中补充功能的 TradeObjects 作为一个函数库, 通过标记进行手工交易。

项目规模越大, 基本的 OOP 原则越重要。由于我们研究的是一个相当简单和孤立的引擎, 通过对象进行自动交易, 因此其改进 (已知无限制) 可供选择性研究。

程序的行动

下面, 我们将展示系统如何使用默认样式和颜色进行操作。

假设我们已检测到了头和肩形态, 且将放置卖单的水平红线。系统会在注释中显示已检测到并由其控制的对象列表。


根据 EA 的 DefaultStopLoss 参数设置止损。行情生存期受蓝色垂直虚线限制, 而非止盈。


到达这条线时, 则平仓 (无关盈利)。已激活指示线标记为失效 (它们的颜色亮度降低, 且被移至背景)。


片刻之后, 报价好似再次向下移动, 我们设置的菲波纳奇等级有望自 61.8 突破 (默认情况下, 这是该项目中菲波纳奇所能做到的一切, 但您可以实现其它类型的行为)。请注意, 菲波纳奇对象的颜色是对角线的颜色, 而不是等级: 等级颜色由单独的设置设定。


当价格抵达等级时, 指定了止损 (38.2) 和止盈价格的一笔交易开仓 (在屏幕截图上 161.8 的位置不可见)。


一段时间之后, 我们看到上方形成阻力线, 放置蓝色通道, 假设价格仍会上涨。


请注意, 到目前为止, 所有指示线都没有包含说明, 所以已开单的手数均来自 Lot 参数 (默认为 0.01)。在此情况下, 有关 '-1' 的描述, 即手数大小将按照保证金的 1% 计算。由于辅助线位于主线之下, 通道指定距止损的距离 (与默认值不同)。


通道突破, 一笔新的多头开仓。我们可以看到, 交易量计算为 0.04 (存款 1000 美元)。屏幕截图中的蓝色区段是将已激活的通道移到背景 (因此 MetaTrader 4 在背景上显示通道)。

为了将两笔多头仓位平仓, 放置一条明显的红色虚线作为止盈。


价格达到这个价位, 两笔订单都将平仓。


假设在这样的走势之后, 价格在 "走廊" 内移动。为了捕捉这种波动性, 我们在价格上方和下方建立两条水平虚线用于限价订单, 以及一条灰色垂直虚线用来激活它们。事实上, 它们不一定是水平或垂直。


请注意, 在描述中更低的挂订单指定了 0.02 手和有效日期 24 根柱线 (小时)。一旦抵达激活线后, 则设置挂单。


一段时间后, sell limit 被触发。


Buy limit 与周一失效。


我们放一条垂直的灰色虚线, 意味着所有持仓将平仓。


当其到达后, 空头平仓, 但即便它是多头持仓, 也将平仓。


在工作期间, EA 会在日志中显示主要事件。

2017.07.06 02:00:00  TradeObjects EURUSD,H1: New line added: 'exp Channel 42597 break up' OBJ_CHANNEL buy -1
2017.07.06 02:00:00  TradeObjects EURUSD,H1: New lines: 1
2017.07.06 10:05:27  TradeObjects EURUSD,H1: Activated: exp Channel 42597 break up
2017.07.06 10:05:27  TradeObjects EURUSD,H1: open #3 buy 0.04 EURUSD at 1.13478 sl: 1.12908 ok
...
2017.07.06 19:02:18  TradeObjects EURUSD,H1: Activated: exp Horizontal Line 43116 takeprofit
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #3 buy 0.04 EURUSD at 1.13478 sl: 1.13514 at price 1.14093
2017.07.06 19:02:18  TradeObjects EURUSD,H1: close #2 buy 0.01 EURUSD at 1.13414 sl: 1.13514 tp: 1.16143 at price 1.14093
...
2017.07.07 05:00:09  TradeObjects EURUSD,H1: Activated: exp Vertical Line 42648
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #4 sell limit 0.01 EURUSD at 1.14361 sl: 1.15395 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #4 2017.07.07 05:00:09 sell limit 0.01 EURUSD 1.14361 1.15395 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00
2017.07.07 05:00:09  TradeObjects EURUSD,H1: open #5 buy limit 0.02 EURUSD at 1.13731 sl: 1.13214 ok
2017.07.07 05:00:09  TradeObjects EURUSD,H1: #5 2017.07.07 05:00:09 buy limit 0.02 EURUSD 1.13731 1.13214 0.00000 0.00000 0.00 0.00 0.00  0 expiration 2017.07.08 05:00

当然, 过时的对象可能被删除, 以便图表洁净。在示例中, 它们被保留, 作为执行操作的凭证。

文后附带了 MetaTrader 4 和 MetaTrader 5 的模板, 其中包含自 2017 年 7 月 1 日 (区间如上所述) 起 EURUSD H1 在测试器上的演示交易指示线。我们应用标准 EA 设置, 但 DefaultStopLoss 参数设置为 -1 (对应的亏损为可用保证金的 1%)。初始存款 1000 美元, 1:500 的杠杆用于描绘止损计算和跟踪。在 MetaTrader 5 的情况下, 应该首先将模板重命名为 tester.tpl (平台不支持在测试器中直接加载和编辑模板)。

结束语

本文提供了一种简单但有效的方式来安置一款半自动交易系统, 可通过交易者在图表上放置标准对象进行交易。在趋势线, 水平线和垂线以及菲波纳奇通道和网格的帮助下, 系统可以执行市价订单, 放置挂单, 并通知交易者特定的市场形态。开源代码允许用户扩充支持的对象类型, 并改进交易功能。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/3442

附加的文件 |
MT4.zip (11.97 KB)
MT5.zip (29.13 KB)
跨平台智能交易系统: 停止位 跨平台智能交易系统: 停止位

本文讨论智能交易系统中停止价位的实现, 以便在两个平台 Metatrader 4 和 Metatrader 5 之间兼容。

深度神经网络 (第 IV 部)。创建, 训练和测试神经网络模型 深度神经网络 (第 IV 部)。创建, 训练和测试神经网络模型

本文将研究 darch 软件包的新功能 (v.0.12.0)。它包含具有不同数据类型, 不同结构和训练顺序的深度神经网络训练的描述。培训结果也包括在内。

单一资产交易顺序中的风险评估 单一资产交易顺序中的风险评估

本文介绍在交易系统分析中使用概率论方法和数学统计。

图形界面 XI: 集成标准图形库 (统合构建 16) 图形界面 XI: 集成标准图形库 (统合构建 16)

能够创建科学图表 (CGraphic 类) 的新版本图形库已于最近发布。创建图形界面的开发中函数库在本次更新中将引入创建图表的新版本控件。不同类型数据的可视化现在更加容易了。