TradeObjects: Автоматизация торговли на основе графических объектов в MetaTrader

Stanislav Korotky | 27 июля, 2017

Геометрические построения на графиках котировок — одни из самых популярных инструментов трейдера уже на протяжении десятилетий. С развитием технологий становится все легче наносить линии поддержки или сопротивления, исторические уровни цен и целые фигуры — например, каналы и сетку Фибоначчи. ПО для алгоритмического трейдинга позволяет не только анализировать классические фигуры, но и торговать на их основе. Для MetaTrader тоже разработаны программы, автоматизирующие процесс в той или иной степени: достаточно добавить объект на график с запущенным экспертом или скриптом, и далее программа сама в нужный момент откроет позицию, будет её сопровождать и закроет в соответствии с настройками. С помощью такого ПО можно не только торговать онлайн, но и тренировать свои навыки в тестере в режиме визуализации. Подобные программы представлены и в Базе исходных кодов, и в Маркете сообщества трейдеров.

Но не все так гладко. Как правило, программы в Базе исходных кодов имеют упрощенный функционал, редко обновляются и потому устаревают (вплоть до потери совместимости с последними версиями языка MQL и терминала), а коммерческие продукты не всем по карману.

В этой статье мы попробуем разработать новый инструмент, представляющий собой золотую середину. Он будет максимально простым и при этом предоставит достаточно широкие возможности. Он будет совместим как с MetaTrader 4, так и с MetaTrader 5. А благодаря открытому исходному коду, его при необходимости легко можно расширить и модифицировать под свои нужды.

Поддержка MetaTrader 4 важна не только с точки зрения охвата большой части аудитории, которая по-прежнему пользуется этой версией, но и из-за некоторых ограничений тестера MetaTrader 5. В частности, визуальный тестер MetaTrader 5 не позволяет на текущий момент интерактивно работать с объектами (добавлять, удалять, редактировать свойства), что необходимо для данного инструмента. Поэтому оттачивать навыки по управлению объектами на истории можно только в тестере MetaTrader 4.


Выработка требований

Главное идеологическое требование для нашей системы автоматической торговли по графическим объектам — простота. Мы будем использовать стандартный интерфейс пользователя MetaTrader и свойства графических объектов, без специальных панелей и сложной логики настройки. Как показывает практика, все плюсы мощных и богатых на всевозможные опции систем часто нивелируются трудностями освоения и малой востребованностью каждого конкретного режима их эксплуатации. Мы будем стремиться использовать только общеизвестные приемы работы с объектами и интуитивно понятную интерпретацию их свойств.

Среди множества типов графических объектов для принятия торговых решений чаще всего используются:

  • трендовая линия;
  • горизонтальная линия;
  • вертикальная линия;
  • равноудаленный канал;
  • линии Фибоначчи.

Именно для них мы и обеспечим поддержку в первую очередь. Этот список можно было бы расширить новыми типами, доступными в MetaTrader 5, но это нарушит совместимость с MetaTrader 4. Вместе с тем, на основе базовых принципов обработки объектов, которые будут реализованы в данном проекте, пользователи легко могут добавить другие объекты, в соответствии со своими предпочтениями.

Каждый из вышеперечисленных объектов формирует в двумерном пространстве чарта некую логическую границу, пересечение или отскок от которой дает сигнал. Его интерпретация обычно соответствует одной из следующих ситуаций:

  • пробой или отскок от линии поддержки/сопротивления;
  • пробой или отскок от исторического ценового уровня;
  • достижение заданного уровня стоп-лосса или тейк-профита;
  • наступление заданного момента времени.

В зависимости от выбранной стратегии, трейдер может в результате события произвести покупку или продажу, установить отложенный ордер или закрыть имеющуюся позицию. Всё это должна уметь и наша система. Таким образом, перечень основных функций будет выглядеть так:

  • отсылка уведомлений различным способами (алерт, push-нотификация, e-mail);
  • открытие рыночных ордеров;
  • установка отложенных ордеров (buy stop, sell stop, buy limit, sell limit);
  • полное или частичное закрытие открытой позиции, в том числе в качестве стоп-лосса и тейк-профита.

Разработка пользовательского интерфейса

Во многих аналогичных продуктах пользовательскому интерфейсу уделяется очень много внимания. Панели, диалоги, кнопки, повсеместный drag'n'drop... Всего этого в нашем проекте не будет. Вместо специального графического интерфейса воспользуемся тем, что по умолчанию дает MetaTrader.

У каждого объекта есть стандартный набор свойств, который можно приспособить под текущую задачу.

Во-первых, нужно будет отличать объекты, предназначенные для автоторговли, от прочих объектов, которые пользователь может нанести на график. Для этого дадим нашим объектам названия с предопределенным префиксом. Если префикс не задавать, эксперт воспримет все объекты с подходящими свойствами как активные. Но такой режим не рекомендуется, так как терминал может создавать свои объекты (например, закрытые ордера), а они могут оказать побочные эффекты.

Во-вторых, для выполнения различных функций (список которых приведен выше) зарезервируем разные стили оформления. Например, в настройках эксперта зададим стиль STYLE_DASH для установки отложенных ордеров, а STYLE_SOLID — для немедленного входа в рынок при пересечении ценой соответствующей линии. Стили разных операций должны отличаться.

В-третьих, надо указать направление торговли. Например, для покупок можно использовать синий цвет, а для продаж — красный. Действия, не связанные со входом в рынок и выходом из него, будем помечать третьим цветом — например, серым. Это, например, уведомления или установка отложенных ордеров. Последний случай отнесен к "нейтральной" категории, потому что часто устанавливается сразу несколько — обычно пара — разнонаправленных отложенных ордеров.

В-четвертых, необходимо определение типа отложенных ордеров. Это можно однозначно сделать по взаимному расположению линии ордера относительно текущей рыночной цены и цвета линии. Например, синяя линия над ценой подразумевает buy stop, но синяя под ценой — buy limit.

Наконец, в-пятых, для большинства операций необходима некая дополнительная информация, атрибуты. В частности, если сработал алерт (которых может быть несколько), то пользователь, скорее всего, захочет получить осмысленное сообщение. Для этой цели хорошо подходит пока еще не задействованное поле Описание. Для ордеров в этом поле будем указывать размер лота и время истечения. Всё это опционально, поскольку для удобства и минимизации необходимых настроек объектов в эксперте будут предусмотрены входные параметры со значениями по умолчанию. Помимо настроек объектов, эти умолчания будут содержать и размеры стоп-лосса и тейк-профита.

Для случаев, когда по каждой линии устанавливаются специфические размеры стоп-лосса или тейк-профита, воспользуемся объектами, в которых объединено несколько линий. К примеру, равноудаленный канал имеет две линии. Первая из них, лежащая на двух точках, будет отвечать за формирование торгового сигнала, а параллельная — с третьей точкой — задаст расстояние до стоп-лосса или тейк-профита. С каким именно уровнем мы имеем дело, легко определить по взаимному расположению и цвету линий. Например, для красного канала дополнительная линия, расположенная выше основной, сформирует стоп-лосс. Если бы она была ниже, то трактовалась бы как тейк-профит.

Если хочется задать и стоп-лосс, и тейк-профит, объект должен состоять как минимум из трех линий. Для этого подходит, например, сетка уровней Фибоначчи. В проекте по умолчанию используются стандартные уровни 38.2% (стоп-лосс), 61.8% (точка входа в рынок) и 161.8% (тейк-профит). Более гибкую настройку этого и более сложных типов объектов, в данной статье мы рассматривать не будем.

При активации того или иного объекта в результате пересечения ценой необходимо пометить объект как отработавший. Это можно сделать, например, установив ему атрибут "фона" OBJPROP_BACK. Для визуальной обратной связи с пользователем мы приглушим исходный цвет таких объектов. Например, синяя линия станет темно-синей после её обработки.

Однако среди трейдерской разметки зачастую встречаются настолько "сильные" линии или уровни, что связанное с ними событие — например, отбой от линии поддержки на коррекциях восходящего тренда — может происходить много раз.

Предусмотрим такую ситуацию с помощью толщины линии. Как известно, стили MetaTrader позволяют задать толщину от 1 до 5. При активации линии будем смотреть на её толщину, и если она больше 1, то вместо выключения из дальнейшей работы уменьшим толщину на 1. Так мы можем обозначить на графике ожидаемые множественные события с числом повторений до 5.

У этой возможности есть нюанс: цены, как правило, флуктуируют вокруг некоторого значения, и любая линия может быть пересечена много раз в течение небольшого периода времени. Трейдер, торгующий вручную, анализирует динамику отклонений цены на глаз и фактически "фильтрует" весь ценовой шум. В эксперте необходимо реализовать механизм, делающий то же самое автоматически.

Для этого цели введем входные параметры, определяющие размер "горячей области" пересечения, т.е. минимальный диапазон движения цены и его длительность, в пределах которых сигналы не будут формироваться. Иными словами, событие "пересечение линии" состоится не сразу же, как цена пересечет её, а только когда она отступит на заданное расстояние в пунктах.

Аналогичным образом введем параметр, задающий минимальный интервал между двумя последовательными событиями с одной и той же линией (только для линий толщиной больше 1). Здесь возникает новая задача: надо где-то хранить время предыдущего события с линией. Используем для этого свойство объекта OBJPROP_ZORDER. Это число типа long, и значение datetime там прекрасно помещается. Изменение порядка отображения линий практически не сказывается на внешнем представлении графика.

В интерфейсе пользователя для настройки линии, предназначенной для работы с системой, достаточно:

  • открыть диалог свойств объекта;
  • добавить выбранный префикс к имени;
  • опционально указать параметры в описании:
    • лот для линий рыночных и отложенных ордеров, частичного закрытия позиции,
    • имена линий отложенных ордеров для линии активации отложенных ордеров,
    • срок истечения для линии отложенного ордера;
  • цвет как индикатор направления (по умолчанию, синий — покупка, красный — продажа, серый — нейтральный);
  • стиль как селектор операции (алерт, вход в рынок, установка отложенного ордера, закрытие позиции);
  • ширину как индикатор повторения события.

Настройка свойств горизонтальной линии для buy limit ордера (синяя штриховая) лотом 0.02 и сроком истечения 24 бара (часа)

Настройка свойств горизонтальной линии для buy limit ордера (синяя штриховая) лотом 0.02 и сроком истечения 24 бара (часа)

Перечень контролируемых системой ордеров (отвечающих описанным требованиям) будет выводиться в комментарии на чарте, вместе с детализацией — тип объекта, описание, статус.


Разработка механизма исполнения

В соответствии с требованиями и общими соображениями, изложенными выше, начнем реализацию эксперта с входных параметров, в которых передаются префикс имен объектов, обрабатываемые цвета и стили, значения по умолчанию, а также размеры областей, генерирующих события на графике.

input int Magic = 0;
input double Lot = 0.01 /*default lot*/;
input int Deviation = 10 /*tolerance to price changes during order execution*/;

input int DefaultTakeProfit = 0 /*points*/;
input int DefaultStopLoss = 0 /*points*/;
input int DefaultExpiration = 0 /*bars*/;

input string CommonPrefit = "exp" /*empty to handle all compatible objects*/;

input color BuyColor = clrBlue /*market and pending buy orders - open, close, sl, tp*/;
input color SellColor = clrRed /*market and pending sell orders - open, close, sl, tp*/;
input color ActivationColor = clrGray /*activation of pending orders placement, alert*/;

input ENUM_LINE_STYLE InstantType = STYLE_SOLID /*opens market trades*/;
input ENUM_LINE_STYLE PendingType = STYLE_DASH /*defines probable pending orders (requires activation)*/;
input ENUM_LINE_STYLE CloseStopLossTakeProfitType = STYLE_DOT /*applied to open positions*/;

input int EventHotSpot = 10 /*points*/;
input int EventTimeSpan = 10 /*seconds*/;
input int EventInterval = 10 /*bars*/;

Сам эксперт оформим в виде класса 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) // wait until we have history record of bid for 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). Мы создаем его экземпляр (e) в конструкторе и удаляем в деструкторе класса TradeObjects. Более подробно рассмотрим движок позднее, а пока отметим, что TradeObjects будет делегировать ему многие операции.

Все обработчики событий handleInit, handleTick, handleTimer, handleChart вызывают метод detectLines, который нам предстоит написать. В нем будут анализироваться имеющиеся объекты и отбираться только те, что удовлетворяют нашим требованиям, причем пользователь может создавать, удалять, редактировать объекты в процессе выполнения эксперта. Найденные объекты сохраняются во внутренний массив. Его наличие позволяет отслеживать изменение состояния графика и сообщать пользователю об обнаружении новых или удалении старых объектов.

Периодически по таймеру вызывается другой метод processLines, который должен в цикле по массиву проверять наступление событий и выполнять соответствующие действия.

Как можно заметить, у объектов проверяется префикс, а также их стили, цвета и статус с помощью метода 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-м баре соотносится с ценой Bid — в случае алерта и установки отложенных ордеров это делает функция 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) // close sell position, stoploss for sell, takeprofit for sell
          {
            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) // will trigger 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 похожи, они используют входные настройки эксперта, определяющие размер области срабатывания событий:

    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) // returns direction of price movement
    {
      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;
    }

Напомним, что цена PreviousBid сохраняется экспертом в обработчике handleTimer с периодичностью EventTimeSpan секунд. Результат работы функций — признак пересечения ценой Bid цены объекта на 0-м баре, причем checkActivation возвращает простой логический флаг, а checkMarket — направление движения цены: +1 — вверх, -1 — вниз.

Особо отметим, что контроль пересечения котировками объектов выполняется по цене bid. Это сделано потому, что весь график построен по цене bid, включая и нанесенные линии. Даже если трейдер сформировал разметку для ордеров покупки, они сработают по правильным сигналам: график ask неявно проходит над графиком bid на величину спреда, и потенциальные линии, которые могли бы быть построены по графику ask, пересекали бы цену синхронно с текущей разметкой по bid.

Для линий стиля PendingType и нейтрального ActivationColor поведение особенное: в момент их пересечения ценой выставляются отложенные ордера. Размещение самих ордеров задается другими линиями, имена которых перечислены через слэш ('/') в описании линии активации. Если описание линии активации пустое, система найдет все линии отложенных ордеров по стилю и установит их. Направление отложенных ордеров, как и для рыночных ордеров, соответствует их цвету — BuyColor или SellColor для покупки или продажи, а в описании можно указать лот и срок истечения (в барах).

Способы сочетания стилей и цветов объектов и их соответствующее значение приведены в таблице.

Цвет и Стиль BuyColor SellColor ActivationColor
InstantType покупка по рынку продажа по рынку алерт
PendingType потенциальный отложенный ордер на покупку потенциальный отложенный ордер на продажу инициирование выставления отложенных ордеров
CloseStopLossTakeProfitType закрытие, стоп-лосс, тейк-профит
для короткой позиции
закрытие, стоп-лосс, тейк-профит
для длинной позиции
закрыть всё

Вернемся к методу 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; // should not be a null, not used otherwise
      }
      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)
      {
        // level 61.8 is enter point at retracement (buy/sell limit),
        // 38.2 and 161.8 as stoploss/takeprofit
        
        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;
      }

TradeObjects позволяет выполнить частичное закрытие, если в описании объекта типа CloseStopLossTakeProfitType указан лот. Система открывает встречный ордер заданного объема, а потом вызывает OrderCloseBy. Для включения режима предусмотрен специальный флаг AllowOrderCloseBy во входных переменных. Если он включен, встречные позиции всегда будут "схлопываться" в одну. Напомню, что эта функция разрешена не на всех счетах (эксперт проверяет эту настройку и выводит в лог соответствующее сообщение, если данная возможность заблокирована). В случае MetaTrader 5 счет должен быть с хеджированием. Желающие могут усовершенствовать систему в плане альтернативной реализации частичного закрытия — без использования OrderCloseBy, с просмотром списка позиций и выбора конкретной, уменьшаемой с помощью каких-либо атрибутов.

Вернемся к классу Expert, выполняющему для TradeObjects все торговые операции. Он представляет собой простейший набор методов по открытию и закрытию ордеров, сопровождению стоп-лоссов, расчету лотов, исходя из заданного риска. В нем используется метафора ордеров MetaTrader 4, которая адаптируется для MetaTrader 5 с помощью библиотеки MT4Orders.

Класс не предоставляет функционала по изменению установленных отложенных ордеров. Перемещение их цены, уровней стоп-лосса и тейк-профита оставлено в ведении самого терминала: если включена опция "Показывать торговые уровни", он позволяет это делать с помощью Drag'n'Drop.

Класс Expert можно заменить на любой другой, которым вы пользуетесь.

Приложенный исходный код компилируется как в MetaTrader 4, так и в MetaTrader 5 (с дополнительными заголовочными файлами).

Следует отметить, что в текущей реализации TradeObjects есть отход от строгих практик ООП в угоду простоте. Например, нужно было бы иметь абстрактный торговый интерфейс, реализовать класс эксперта Expert как наследник этого интерфейса и затем передавать его в класс TradeObjects (например, через параметр конструктора). Это известный шаблон ООП внедрения зависимости (dependency injection). В данном проекте торговый движок жестким образом "зашит в код": создается и удаляется внутри объекта TradeObjects.

Кроме того, в нарушение принципов ООП, мы используем глобальные входные переменные непосредственно в коде класса TradeObjects. Лучший стиль программирования потребовал бы передавать их в класс как параметры конструктора или специальных методов. Это позволило бы, например, использовать TradeObjects как библиотеку внутри другого эксперта, дополняя его функциями ручной торговли по разметке.

В принципе, следовать базовым принципам ООП становится тем важнее, чем крупнее разрабатываемый проект. Поскольку здесь мы рассматриваем довольно простой и обособленный движок автоторговли по объектам, то его совершенствование (которое, как известно, не знает границ) оставлено для факультативного изучения.


Программа в действии

Ниже показано, как система выглядит в работе, при настройках стилей и цветов по умолчанию.

Допустим, мы увидели на графике формирование фигуры "голова-плечи" и размещаем горизонтальную красную линию для сделки продажи. Система выводит перечень обнаруженных и контролируемых ею объектов в комментарии.


Стоп-лосс будет установлен в соответствии с параметром эксперта DefaultStopLoss, а вместо тейк-профита ограничим время нахождения в рынке с помощью вертикальной синей пунктирной линии.


По достижении этой линии позиция закрывается (вне зависимости от прибыльности). Сработавшие линии помечаются как неактивные (их цвет приглушается, и сами они отправляются на задний план).


Через некоторое время котировки, похоже, собираются еще раз двинуться вниз, и мы ставим уровни Фибоначчи в надежде на отбой от уровня 61.8 (это всё, что умеет Фибоначчи в данном проекте по умолчанию, но вы можете реализовать и другие типы поведения). Обратите внимание, что цвет объекта Фибоначчи — это цвет диагональной линии, а не уровней: цвет уровней задается отдельной настройкой.


Когда цена доходит до уровня, открывается сделка, причем с заданными ценами стоп-лосс (38.2) и тейк-профит (161.8, не виден на скриншоте).


Какое-то время спустя мы видим формирование линии сопротивления сверху и размещаем синий канал в предположении, что цена все же пойдет вверх.


Отметим, что все линии до сих пор не содержали описания, и потому ордера открывались с лотом из параметра Lot (0.01, по умолчанию). В данном же случае стоит описание '-1', т.е. размер лота будет вычислен, как требующий 1% свободной маржи. Поскольку вспомогательная линия расположена ниже основной, канал задает расстояние до стоп-лосса (отличного от значения по умолчанию).


Канал действительно пробивается, и новая длинная позиция открывается. Как мы видим, объем был расчитан как 0.04 (при депозите 1000$). Синий сегмент на скриншоте — это сработавший канал, перемещенный в фон (так MetaTrader 4 изображает каналы на заднем плане).

Чтобы закрыть обе открытие позиции на покупку, разместим явную линию тейк-профита — красную пунктирную.


Цена доходит до данного уровня, и оба ордера закрываются.


Предположим, что после такого движения цена будет ходить в "коридоре". Чтобы поймать эту волатильность, установим две горизонтальные штриховые линии для лимитных ордеров сверху и снизу от цены, а также серую штриховую вертикальную линию для их активации. В принципе, они не обязаны быть горизонтальными или вертикальными.


Обратите внимание, что, например, для нижнего отложенного ордера в описании задан кастомизированный лот 0.02 и срок истечения через 24 бара (часа). По достижении линии активации устанавливаются отложенные ордера.


Через некоторое время срабатывает sell limit.


Buy limit истекает в понедельник.


Мы ставим вертикальную серую пунктирную линию, означающую закрытие всех позиций.


По её достижению закрывается короткая позиция, но если бы была открыта и длинная, то она была бы закрыта тоже.


В процессе работы эксперт выводит основные события в лог.

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 с линиями для демонстрационной торговли в тестере на графике EURUSD H1, начиная с 1 июля 2017 (вышеописанный период). Используются стандартные настройки эксперта за исключением параметра DefaultStopLoss, установленного в -1 (что соответствует потере 1% свободной маржи). Для наглядной иллюстрации расчета и сопровождения стоп-лосса предлагается начальный депозит 1000$, плечо 1:500. В случае MetaTrader 5 шаблон предварительно необходимо переименовать в tester.tpl (загрузка и редактирование шаблонов непосредственно в тестере пока не поддерживается платформой). 


Заключение

В статье представлен простой, но функциональный вариант организации полуавтоматической торговли с использованием стандартных объектов, размещенных на графике трейдером. С помощью трендовых, горизонтальных, вертикальных линий, каналов и сеток Фибоначчи система может исполнять рыночные ордера, выставлять отложенные ордера, уведомлять трейдера о формировании характерных фигур на рынке. Открытый исходный код позволяет расширить набор поддерживаемых типов объектов, а также усовершенствовать торговый функционал.