English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Разработка торгового советника с нуля

Разработка торгового советника с нуля

MetaTrader 5Трейдинг | 14 января 2022, 15:00
5 562 0
Daniel Jose
Daniel Jose

Введение

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

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

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


Планирование

Самая сложная часть при создании чего-либо — это придумать, как все должно работать. Для этого создадим минимальный код  — ведь чем сложнее код, тем больше вероятность ошибок и сбоев во время выполнения (RUN TIME). Именно поэтому я постарался сделать код максимально простым, но в то же время старался максимально использовать возможности MetaTrader 5. Ведь как ни крути, самая надежная часть во всем этом — это сама платформа, которая постоянно тестируется и в которой мы можем быть уверены.

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

Хотя рассматриваемый советник предназначен для торговли на B3 (Бразильская фондовая биржа), в частности для торговли фьючерсами (мини-индекс и мини-доллар), его можно расширить на все рынки с минимальным количеством редактирования, и это очень практично и просто. Чтобы упростить задачу и не повторять проверку того, каким инструментом торгуем, используем следующее перечисление:

enum eTypeSymbolFast {WIN, WDO, OTHER};


Если понадобится подключить новый инструмент, это можно сделать прямо здесь. При этом также потребуются небольшие изменения в коде, но использование этого перечисления значительно упрощает задачу и снижает вероятность ошибок. Очень любопытная часть кода — функция AdjustPrice…

   double AdjustPrice(const double arg)
     {
      double v0, v1;
      if(m_Infos.TypeSymbol == OTHER)
         return arg;
      v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg));
      v1 = fmod(round(v0), 5.0);
      v0 -= ((v1 != 0) || (v1 != 5) ? v1 : 0);
      return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0);
     };

Эта функция корректирует значение, используемое в цене, чтобы линии устанавливались в правильной точке графика. Почему нельзя просто установить линию на графике? Причина в том, что у инструментов может отличаться шаг изменения ценыю Например, шаг для WDO (Мини-доллар) составляет 0,5 пункта, для WIN (Мини-индекс) — 5 пунктов, а для акций — 0,01 пункта. Другими словами, каждый тик имеет разное значение для разных инструментов, и именно для этого нужна эта функция: она корректирует, вернее, подстраивает цену под нужное значение в соответствии с тиковым эквивалентном, что важно для создания ордера в нужной точке, иначе торговый сервер может отклонить ордер.

Без этой функции было бы трудно узнать, какие правильные значения использовать для совершения OCO-ордера (One Cancels the Other, взаимозаменяемы ордер). Далее показана функция, которая является сердцем советника — функция CreateOrderPendent, давайте посмотрим ее.

   ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true)
     {
      double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action        = TRADE_ACTION_PENDING;
      TradeRequest.symbol        = m_szSymbol;
      TradeRequest.volume        = Volume;
      TradeRequest.type          = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
      TradeRequest.price         = NormalizeDouble(Price, m_Infos.nDigits);
      TradeRequest.sl            = NormalizeDouble(Stop, m_Infos.nDigits);
      TradeRequest.tp            = NormalizeDouble(Take, m_Infos.nDigits);
      TradeRequest.type_time     = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
      TradeRequest.stoplimit     = 0;
      TradeRequest.expiration    = 0;
      TradeRequest.type_filling  = ORDER_FILLING_RETURN;
      TradeRequest.deviation     = 1000;
      TradeRequest.comment       = "Order Generated by Experts Advisor.";
      if(!OrderSend(TradeRequest, TradeResult))
        {
         MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
         return 0;
        };
      return TradeResult.order;
     };

Эта функция чрезвычайно проста и надежна. Здесь мы создаем OCO-ордер, который будет отправлен на торговый сервер. Обратите внимание, что мы используем ордера типа LIMIT или STOP. Это связано с тем, что этот тип ордера проще и его исполнение гарантировано даже в случае резких изменений цен.

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

TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));

Также в советнике е=есть возможность создавать кросс-ордера — для этого нужно указать инструмент для торговли в следующей строке:

TradeRequest.symbol = m_szSymbol;

Кроме того, надо дописать код, чтобы иметь возможность обрабатывать открытые или отложенные ордера через систему CROSS ORDER, так как у нас будет "неправильный" график. Давайте разберемся с этим на примере. Мы можем с графика полного индекса (IND) торговать мини-индексом (WIN), но MetaTrader 5 не отобразит открытую или отложенную позицию в WIN на графике IND. Поэтому надо добавить некоторый код, чтобы такие позиции отобразить. Мы будем считывать значения позиции и на основе их построим линии на текущем графике. Это позволяет наблюдать за историей актива. C помощью CROSS ORDER можно, например, торговать WIN (мини-индекс) с графика WIN$ (который является графиком истории мини-индекса), и это вполне возможно.

Далее посмотрим на следующие строки:

      TradeRequest.price         = NormalizeDouble(Price, m_Infos.nDigits);
      TradeRequest.sl            = NormalizeDouble(Stop, m_Infos.nDigits);
      TradeRequest.tp            = NormalizeDouble(Take, m_Infos.nDigits);

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

Тот же ордер в окне редактирования будет выглядеть так:

Когда эти данные заполнены, торговый сервер сам будет управлять ордером: как только он достигнет точки "Максимальный убыток" или "Максимальная прибыль", система закроет ордер. Но если убрать значение "Максимальный убыток" или "Максимальная прибыль", ордер останется открытым до тех пор, пока не произойдет какое-либо другое событие. При дневной торговле (Day Trade) ордер закроется в конце торгового дня; в противном случае она останется открытой до тех пор, пока мы не закроем или не закончатся средства для удержания позиции.

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

   void Initilize(int nContracts, int FinanceTake, int FinanceStop, color cp, color ct, color cs, bool b1)
     {
      string sz0 = StringSubstr(m_szSymbol = _Symbol, 0, 3);
      double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
      m_Infos.Id = ChartID();
      m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
      m_Infos.nDigits = (int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS);
      m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
      m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
      m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
      m_Infos.IsDayTrade = b1;
      CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp);
      CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct);
      CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs);
      ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice);
      ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop);
     };

Этот код отвечает за инициализацию данных советника в соответствии с заданными настройками пользователя и создает OCO-ордер. В него нужно внести небольшое изменение:

m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));

Здесь мы добавляем тип актива (к текущим), когда нам нужно предоставить что-то конкретное, что выходит за рамки имеющегося кода.

      m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
      m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
      m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);

Эти три строчки добавляют необходимые настройки для корректного создания ордера. nContracts — это фактор плеча, значения 1, 2, 3 и т. д. Нам не нужно знать минимальный объем для торговли инструментом. Вместо этого достаточно указать коэффициент плеча для этого минимального объема, например: если минимальный объем инструмента 5 контрактов, при значении плеча 3 система установит в ордере объем в 15 контрактов. Всё это значительно упрощает работу с активами, которые имеют разное значение минимального объема. Остальные 2 строчки относятся к Take Profit и Stop Loss — они устанавливают уровни на основе заданных настроек. Уровни настраиваются в соответствии с торгуемым объемом: при увеличении объема с сохранением допустимого риска эти уровни будут уменьшаться, а при увеличении риска с сохранением объема уровни будут увеличиваться. Так что советник будет автоматически делать все необходимые расчеты для размещения позиции, включая объем и риски, и сам будет размещать рассчитанные позиции.

   inline void MoveTo(int X, int Y, uint Key)
     {
      int w = 0;
      datetime dt;
      bool bEClick, bKeyBuy, bKeySell;
      double take = 0, stop = 0, price;
      bEClick  = (Key & 0x01) == 0x01;    //Клик левой кнопкой мыши
      bKeyBuy  = (Key & 0x04) == 0x04;    //Нажатый SHIFT
      bKeySell = (Key & 0x08) == 0x08;    //Нажатый CTRL
      ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price);
      ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : 0));
      ObjectMove(m_Infos.Id, m_Infos.szHLineTake, 0, 0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1)));
      ObjectMove(m_Infos.Id, m_Infos.szHLineStop, 0, 0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1)));
      if((bEClick) && (bKeyBuy != bKeySell))
         CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade);
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE));
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE));
      ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE));
     };

Код выше показывает, как создается ордер: уровень ордера показывается на основе движения мыши. Однако нам надо сообщить, нужен ли ордер на покупку (нажмем SHIFT) или продажу (нажмем CTRL). Далее, в момент клика кнопкой мыши будет создан отложенный ордер.

Также в этот код можно добавить дополнительные объекты, чтобы отобразить дополнительные параметры, например уровень безубытка.

К данному моменту мы создали советник, который может создавать OCO-ордера, но так как не всё идеально...


Проблема с OCO-ордерами

У OCO-ордеров есть одна проблема, которая не является ошибкой MetaTrader 5 или торгового сервера. Она связана с волатильностью, которая присутствует на рынке. Теоретически цена должна двигаться линейно, без отскоков. Но иногда у высокая волатильность создает гэпы внутри свечи, и если такие гэпы возникают в точке, где находится цена ордера Stop Loss или Take Profit, эти точки не сработают и, следовательно, ордер не будет закрыт. Также может случиться, что когда мы двигаем эти точки на графике, цена остается вне корридора, границы которого представлены уровнями Stop и Take, и в этом случае ордер также не будет закрыт. Это очень опасно, и трудно предсказать, когда это произойдет, но как программисты мы должны это предвидеть и создавать механизмы для минимизации ущерба.

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

   void UpdatePosition(void)
     {
      for(int i0 = PositionsTotal() - 1; i0 >= 0; i0--)
         if(PositionGetSymbol(i0) == m_szSymbol)
           {
            m_Take      = PositionGetDouble(POSITION_TP);
            m_Stop      = PositionGetDouble(POSITION_SL);
            m_IsBuy     = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
            m_Volume    = PositionGetDouble(POSITION_VOLUME);
            m_Ticket    = PositionGetInteger(POSITION_TICKET);
           }
     };

она будет вызываться OnTrade,  что является подпрограммой, которую MetaTrader 5 вызывает на каждом изменении, осуществляемое в позициях. Следующая подпрограмма, которую нужно использовать, вызывается OnTick. Она используется, чтобы проверить и убедиться, что цена остается в туннеле или в пределах границ OCO-ордера, давайте рассмотрим её:

   inline bool CheckPosition(const double price = 0, const int factor = 0)
     {
      double last;
      if(m_Ticket == 0)
         return false;
      last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      if(m_IsBuy)
        {
         if((last > m_Take) || (last < m_Stop))
            return ClosePosition();
         if((price > 0) && (price >= last))
            return ClosePosition(factor);
        }
      else
        {
         if((last < m_Take) || (last > m_Stop))
            return ClosePosition();
         if((price > 0) && (price <= last))
            return ClosePosition(factor);
        }
      return false;
     };

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

   bool ClosePosition(const int arg = 0)
     {
      double v1 = arg * m_VolMinimal;
      if(!PositionSelectByTicket(m_Ticket))
         return false;
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action     = TRADE_ACTION_DEAL;
      TradeRequest.type       = (m_IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
      TradeRequest.price      = SymbolInfoDouble(m_szSymbol, (m_IsBuy ? SYMBOL_BID : SYMBOL_ASK));
      TradeRequest.position   = m_Ticket;
      TradeRequest.symbol     = m_szSymbol;
      TradeRequest.volume     = ((v1 == 0) || (v1 > m_Volume) ? m_Volume : v1);
      TradeRequest.deviation  = 1000;
      if(!OrderSend(TradeRequest, TradeResult))
        {
         MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
         return false;
        }
      else
         m_Ticket = 0;
      return true;
     };

Эта функция закроет заданный объем, это наша защита. Но надо помнить, что для работы обязательно нужно подклбчение к серверу, так как она работает в клиентской платформе MetaTrader 5. Если случайно соединение с торговым сервером будет потеряно, функция станет совершенно бесполезной.

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


Работа с частичными ордерами


Частичные ордера нравятся многим, и многие трейдеры пользуются ими при торговле. Несмотря на то, что представленный советник позволяет закрывать часть ордера, я не буду здесь показывать, как реализовать этот код. Частичные ордера являются отдельной проблемой. Если всё-таки есть желание включить отложенные ордера, нужно вызвать подпрограмму CheckPosition и указать цену, по которой исполнится ордер и объем, в котором он исполнится — советник сделает всё остальное сам.

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

Одно можно точно сказать — надо по возможности избегать частичных закрытий с использованием ордеров на открытие позиции, ведь очень велик риск получить от этого проблемы. Давайте я объясню. Допустим, у нас есть позиция на покупку с плечом 3x, и мы хотим закрыть прибыль с плечом 2x, сохранив плечо 1x. Что мы делаем — продаем объем 2x. Но если советник отправляет ордер на продажу по рыночной цене, волатильность может привести к тому, что цена достигнет тейк-профита до того, как ордер на продажу будет фактически выполнен. В этом члучае получится, что мы откроем короткую позицию при неблагоприятном тренде. Как вариант, можно установить ордер Sell Limit или Sell Stop, чтобы попытаться сократить объем, закрыв 2x. Вроде бы решение кажется адекватным. Но давайте подумаем: если ордер будет отправлен до того, как цена достигнет точки частичного закрытия, нас может ожидать очень неприятный сюрприз, и наша открытая позиция будет закрыта, а немного позже отложенный ордер откроется и увеличит убыток. Если волатильность будет большой, может произойти то же самое, что и в случае входа по рыночной цене выше.

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


Заключение:

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

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

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





Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/10085

Прикрепленные файлы |
EA_Nano_rvl_1.1.mq5 (23.44 KB)
Как правильно выбирать советник в Маркете? Как правильно выбирать советник в Маркете?
В данной статье рассмотрим моменты, на которые следует обращать внимание при покупке советника в первую очередь. А также поищем способы повышения прибыли и, что самое, главное, как потратить деньги с умом и еще заработать на этом. Кроме того, после прочтения вы поймете, что заработать можно даже на простых и бесплатных продуктах.
Графика в библиотеке DoEasy (Часть 92): Класс памяти стандартных графических объектов. История изменения свойств объекта Графика в библиотеке DoEasy (Часть 92): Класс памяти стандартных графических объектов. История изменения свойств объекта
В статье создадим класс памяти стандартного графического объекта, позволяющий объекту сохранять свои состояния при модификации его свойств, что в свою очередь позволит в любое время вернуться к прошлым состояниям графического объекта.
Работаем со временем (Часть 2): Функции Работаем со временем (Часть 2): Функции
Научимся автоматически распознавать смещения времени у брокера и время по Гринвичу. Вместо того, чтобы обращаться к брокеру, который скорее всего даст недостаточно полный ответ (а кто захочет объяснять, куда пропал торговый час?), мы сами посмотрим, по какому времени приходят от них котировки в те недели, когда переводят часы. Но конечно же, это мы будем делать не вручную — пусть за нас работает программа.
Визуальная оценка результатов оптимизации Визуальная оценка результатов оптимизации
Разговор в этой статье пойдёт о том, как построить графики всех проходов оптимизации и подобрать оптимальный пользовательский критерий. А также о том, как, имея минимальные знания в MQL5 и большое желание, используя статьи сайта и комментарии на форуме, написать то, что хочется.