Создаем кроссплатформенный советник-сеточник (гридер)

Roman Klymenko | 8 марта, 2019

Введение

Думаю, на данном сайте ни для кого не является секретом, что язык MQL5 является наилучшим вариантом для создания своих советников. Но вот беда — не все брокеры позволяют создавать счета, доступные в MetaTrader 5. И даже если сейчас вы работаете с брокером, который это позволяет, вполне возможна ситуация, когда в будущем вам придется перейти к брокеру, который имеет возможности работы только в MetaTrader 4. И что делать в этом случае со всеми теми советниками, которые вы создали на языке MQL5? Тратить кучу времени на их переработку под язык MQL4? Не лучше ли сразу сделать такой советник, который сможет работать и в MetaTrader 5 и в MetaTrader 4?

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

Несколько слов об условной компиляции

Создать советник, работающий и в MetaTrader 4, и в MetaTrader 5, нам поможет условная компиляция. Синтаксис, который мы будем использовать, следующий:

   #ifdef __MQL5__ 
      // код на языке MQL5
   #else 
      // код на языке MQL4
   #endif 

Условная компиляция позволяет указать, что определенный блок кода должен компилироваться только в том случае, если компиляция происходит в советник для языка MQL5. При компиляции в MQL4 и других версиях языка данный блок кода будет просто отбрасываться, а вместо него будет использоваться блок кода, идущий после оператора #else (если он задан).

Таким образом, если какой-то функционал по-разному реализуется в MQL4 и в MQL5, мы будем реализовывать его обоими способами, а условная компиляция позволит выбрать тот вариант, который нужен для конкретного языка.

В остальных случаях мы будем использовать синтаксис, одинаково работающий и в MQL4, и в MQL5.

О сеточных торговых системах

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

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

Лимитные ордера выставляются не по одной цене, а с определенным шагом. То есть на определенном расстоянии от текущей цены вверх выставляется первый лимитный ордер. Потом на том же расстоянии от выставленного ордера выставляется второй лимитный ордер. И так далее. Количество таких ордеров и шаг, на котором они выставляются, варьируются.

Выше текущей цены выставляются ордера в одном направлении, а ниже текущей цены — в противоположном. Считается, что:

Работать можно как со стоп лоссами и тейк-профитами, так и без них.

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

Пример открытой сетки представлен на данном скриншоте:

Пример выставленной сетки

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

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

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

Принцип работы нашего сеточника

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

Мы же в данной статье сначала попробуем реализовать самый простой вариант сеточка без стоп лоссов, так как идея, на которой он работает, весьма заманчива.

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

Кажется, единственный вариант, при котором данная торговая система может принести убыток — это когда цена сначала задевает один ордер, потом идет назад и задевает противоположный, потом опять меняет направление и задевает еще один ордер. И снова меняет направление. И так постоянно меняет направление, задевая все более дальние ордера. Но возможно ли вообще такое поведение цены в жизни?

Шаблон советника

Начнем разработку советника с шаблона, который будет использоваться. Так мы сразу увидим, какие стандартные функции MQL будут задействованы.

#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://www.mql5.com/ru/users/needtome"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  }

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
   {
   }

От стандартного шаблона, генерируемого при создании советника с помощью мастера MQL5, он отличается только строкой #property strict. Ее мы добавляем для того, чтобы наш советник работал и в MQL4.

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

Функция открытия позиций

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

Каждый ордер имеет свой тип:  ордер на покупку, ордер на продажу, лимитный ордер на покупку или на продажу. Переменная, в которой данный тип задается при выставлении ордера, также отличается в MQL5 и MQL4.

В MQL4 тип ордера задается переменной типа int. Тогда как в MQL5 используется специальное перечисление ENUM_ORDER_TYPE. И, более того, в MQL4 такого перечисления не существует. Поэтому, чтобы объединить оба метода, давайте создадим собственное перечисление, и именно его будем использовать для задания типа ордера. Благодаря этому функция, которую мы создадим в дальнейшем, не будет зависеть от версии MQL:

enum TypeOfPos{
   MY_BUY,
   MY_SELL,
   MY_BUYSTOP,
   MY_BUYLIMIT,
   MY_SELLSTOP,
   MY_SELLLIMIT,
}; 

Вот теперь можно создать собственную функцию для выставления ордера. Назовем ее pdxSendOrder(). А передавать в нее мы будем все, что нам необходимо для выставления ордера: тип ордера, цену открытия, стоп лосс (0, если не задан), тейк-профит (0, если не задан), объем, ticket открытой позиции (если нужно провести модификацию открытой позиции в MQL5), комментарий и символ (если нужно открыть ордер по символу, отличному от открытого в текущем графике):

// функция отправки ордера
bool pdxSendOrder(TypeOfPos mytype, double price, double sl, double tp, double volume, ulong position=0, string comment="", string sym=""){
   // проверяем переданные значения
   if( !StringLen(sym) ){
      sym=_Symbol;
   }
   int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS);
   if(sl>0){
      sl=NormalizeDouble(sl,curDigits);
   }
   if(tp>0){
      tp=NormalizeDouble(tp,curDigits);
   }
   if(price>0){
      price=NormalizeDouble(price,curDigits);
   }
   
   #ifdef __MQL5__ 
      ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL;
      ENUM_ORDER_TYPE type=ORDER_TYPE_BUY;
      switch(mytype){
         case MY_BUY:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_STOP;
            break;
         case MY_BUYLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_LIMIT;
            break;
         case MY_SELL:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_STOP;
            break;
         case MY_SELLLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_LIMIT;
            break;
      }
      
      MqlTradeRequest mrequest;
      MqlTradeResult mresult;
      ZeroMemory(mrequest);
      
      mrequest.action = action;
      mrequest.sl = sl;
      mrequest.tp = tp;
      mrequest.symbol = sym;
      if(position>0){
         mrequest.position = position;
      }
      if(StringLen(comment)){
         mrequest.comment=comment;
      }
      if(action!=TRADE_ACTION_SLTP){
         if(price>0){
            mrequest.price = price;
         }
         if(volume>0){
            mrequest.volume = volume;
         }
         mrequest.type = type;
         mrequest.magic = EA_Magic;
         switch(useORDER_FILLING_RETURN){
            case FOK:
               mrequest.type_filling = ORDER_FILLING_FOK;
               break;
            case RETURN:
               mrequest.type_filling = ORDER_FILLING_RETURN;
               break;
            case IOC:
               mrequest.type_filling = ORDER_FILLING_IOC;
               break;
         }
         mrequest.deviation=100;
      }
      if(OrderSend(mrequest,mresult)){
         if(mresult.retcode==10009 || mresult.retcode==10008){
            return true;
         }else{
            msgErr(GetLastError(), mresult.retcode);
         }
      }
   #else 
      int type=OP_BUY;
      switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            break;
         case MY_SELL:
            type=OP_SELL;
            break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            break;
      }
      if(OrderSend(sym, type, volume, price, 100, sl, tp, comment, EA_Magic, 0)<0){
         msgErr(GetLastError());
      }else{
         return true;
      }
   
   #endif 
   return false;
}

Сначала мы проверяем переданные в функцию значения и нормализуем цены.

Входящие параметры. Потом с помощью условной компиляции мы определяем текущую версию MQL и выставляем ордер по ее правилам. Для MQL5 при этом используется дополнительный входящий параметр useORDER_FILLING_RETURN. С его помощью мы настраиваем режим выполнения ордера в соответствии с тем, какие режимы поддерживаются нашим брокером. Поскольку входящий параметр useORDER_FILLING_RETURN необходим только для версии советника под MQL5, снова воспользуемся условной компиляцией, чтобы добавить его:

#ifdef __MQL5__ 
   enum TypeOfFilling //Filling Mode
     {
      FOK,//ORDER_FILLING_FOK
      RETURN,// ORDER_FILLING_RETURN
      IOC,//ORDER_FILLING_IOC
     }; 
   input TypeOfFilling  useORDER_FILLING_RETURN=FOK; //Filling Mode
#endif 

Также при выставлении ордера используется входящий параметр EA_Magic, содержащий в себе Magic-номер нашего советника.

Если в настройках советника данный параметр задан не будет, то любые позиции по символу, на котором он будет запущен, советник будет считать собственными. И сам будет решать, когда их закрывать и что вообще с ними делать.

Если же Magic-номер задан, то в своей работе советник будет учитывать только позиции с данным Magic-номером.

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

void msgErr(int err, int retcode=0){
   string curErr="";
   switch(err){
      case 1:
         curErr=langs.err1;
         break;
//      case N:
//         curErr=langs.errN;
//         break;
      default:
         curErr=langs.err0+": "+(string) err;
   }
   if(retcode>0){
      curErr+=" ";
      switch(retcode){
         case 10004:
            curErr+=langs.retcode10004;
            break;
//         case N:
//            curErr+=langs.retcodeN;
//            break;
      }
   }
   
   Alert(curErr);
}

Подробнее о локализации будет сказано в следующем разделе данной статьи.

Локализация советника

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

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

enum TypeOfLang{
   MY_ENG, // English
   MY_RUS, // Русский
}; 

input TypeOfLang  LANG=MY_RUS; // Язык

Далее создадим структуру, которая будет применяться для хранения всех используемых в советнике текстовых строк. После чего объявим переменную созданного нами типа:

struct translate{
   string err1;
   string err2;
//   ... другие строки
};
translate langs;

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

void init_lang(){
   switch(LANG){
      case MY_ENG:
         langs.err1="No error, but unknown result. (1)";
         langs.err2="General error (2)";
         langs.err3="Incorrect parameters (3)";
//         ... другие строки
         break;
      case MY_RUS:
         langs.err0="Во время выполнения запроса произошла ошибка";
         langs.err1="Нет ошибки, но результат неизвестен (1)";
         langs.err2="Общая ошибка (2)";
         langs.err3="Неправильные параметры (3)";
//         ... другие строки
         break;
   }
}

Единственное, что нам осталось сделать — это вызвать функцию init_lang(), чтобы строки заполнились нужными нам значениями. Идеальным местом для ее вызова является стандартная функция OnInit(), ведь она вызывается сразу при запуске советника. А именно это нам и нужно.

Основные входящие параметры

Пришла пора добавить в наш советник основные входящие параметры. Помимо уже рассмотренных EA_Magic и LANG это будут:

input double      Lot=0.01;     //Размер лота
input uint        maxLimits=7;  //Кол-во лимит. ордеров в сетке в одну сторону
input int         Step=10;      //Шаг сетки, пунктов
input double      takeProfit=1; //Закрывать сделки при прибыли, $

То есть мы будем открывать maxLimits ордеров в одну сторону и столько же в противоположную. Первый ордер будет находиться на Step пунктов от текущей цены. Второй — на Step пунктов от первого ордера. И так далее.

Прибыль мы будем фиксировать как только она достигнет takeProfit долларов. При этом будут закрыты все открытые позиции, а также отменены все выставленные ордера. После чего советник заново выставит свою сетку.

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

Заполняем функцию OnInit

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

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

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }

Таким образом, в самом советнике вместо входящего параметра Step мы будем использовать скорректированный параметр ST. Объявим его до вызова каких-либо функций, указав тип double.

И, поскольку в дальнейшем для формирования сетки нам нужно будет расстояние не в пунктах, а в цене инструмента, давайте сразу же выполним преобразование:

   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);

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

Проверку можно сделать с помощью данного небольшого кода:

   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }   

Если торговля запрещена, мы сообщаем об этом на выбранном пользователем языке, после чего завершаем работу советника.

В итоге, функция OnInit() примет следующий финальный вид:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   init_lang();
   
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }
   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   return(INIT_SUCCEEDED);
  }

Добавляем кнопку Закрыть все

Удобство работы с советником так же важно, как и его точное следование выбранной торговой стратегии.

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

И что так же важно — мы должны иметь возможность быстро закрыть все открытые ордера и выставленные позиции, если полученная прибыль нас уже устраивает или что-то идет не так.

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

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

string prefix_graph="grider_";

Подсчитываем позиции и прибыль. Теперь мы можем создать функцию, которая будет подсчитывать количество открытых позиций в Long и Short, а также общую прибыль по ним. После чего выводить кнопку с полученной информацией, или же обновлять текст на ней, если такая кнопка уже существует. Назовем  функцию getmeinfo_btn():

void getmeinfo_btn(string symname){
   double posPlus=0;
   double posMinus=0;
   double profit=0;
   double positionExist=false;

   // подсчитываем кол-во открытых позиций в Long и Short,
   // и общую прибыль по ним
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=symname) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         positionExist=true;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
         
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){
            posPlus+=PositionGetDouble(POSITION_VOLUME);
         }else{
            posMinus+=PositionGetDouble(POSITION_VOLUME);
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=symname) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            positionExist=true;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
            
            if(OrderType()==OP_BUY){
               posPlus+=OrderLots();
            }else{
               posMinus+=OrderLots();
            }
         }
      }
   #endif 
   
   // если есть открытые позиции,
   // тогда добавляем кнопку их закрытия
   if(positionExist){
      createObject(prefix_graph+"delall", 233, langs.closeAll+" ("+DoubleToString(profit, 2)+") L: "+(string) posPlus+" S: "+(string) posMinus);
   }else{
      // иначе удаляем кнопку закрытия позиций
      if(ObjectFind(0, prefix_graph+"delall")>0){
         ObjectDelete(0, prefix_graph+"delall");
      }
   }
   
   // обновляем текущий график, чтобы внести
   // сделанные изменения
   ChartRedraw(0);
}

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

Выводим кнопку. Также обратите внимание, что для вывода кнопки на график мы используем собственную функцию createObject(). Данная функция проверяет, присутствует ли на графике кнопка с именем, переданным в качестве первого аргумента функции.

Если кнопка уже создана, то мы просто обновляем текст на данной кнопке в соответствии с текстом, переданным в третьем аргументе функции.

Если же кнопки нет, то мы ее создаем в верхнем правом углу графика. При этом второй аргумент функции задает ширину создаваемой кнопки:

void createObject(string name, int weight, string title){
   // если кнопки с именем name нет на графике, тогда создать ее
   if(ObjectFind(0, name)<0){
      // определяем смещение относительно правого угла графика, где вывести кнопку
      long offset= ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
      long offsetY=0;
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset>offsetY){
            offsetY=tmpOffset;
         }
      }
      
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset!=offsetY ){
            continue;
         }
         
         tmpOffset=ObjectGetInteger(0, objName, OBJPROP_XDISTANCE);
         if( tmpOffset>0 && tmpOffset<offset){
            offset=tmpOffset;
         }
      }
      offset-=(weight+1);
      if(offset<0){
         offset=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
         offsetY+=25;
         offset-=(weight+1);
      }
  
     ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
     ObjectSetInteger(0,name,OBJPROP_XDISTANCE,offset); 
     ObjectSetInteger(0,name,OBJPROP_YDISTANCE,offsetY); 
     ObjectSetString(0,name,OBJPROP_TEXT, title); 
     ObjectSetInteger(0,name,OBJPROP_XSIZE,weight); 
     ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 8);
     ObjectSetInteger(0,name,OBJPROP_COLOR, clrBlack);
     ObjectSetInteger(0,name,OBJPROP_YSIZE,25); 
     ObjectSetInteger(0,name,OBJPROP_BGCOLOR, clrLightGray);
     ChartRedraw(0);
  }else{
     ObjectSetString(0,name,OBJPROP_TEXT, title);
  }
}

Реакция на нажатие кнопки. Теперь, если мы вызовем функцию getmeinfo_btn(), то на графике появится кнопка Закрыть все… (если у нас есть открытые позиции). Однако при нажатии на данную кнопку ничего происходить не будет.

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

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
{
   string text="";
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // если название нажатой кнопки равно prefix_graph+"delall", то
         if (sparam==prefix_graph+"delall"){
            closeAllPos();
         }
         break;
   }
}

Теперь при нажатии на кнопку закрытия позиций у нас будет вызываться функция closeAllPos(). Данная функция еще не создана. Но в следующем разделе статьи мы исправим данную оплошность.

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

Мы воспользуемся функцией getmeinfo_btn(), когда займемся кодом стандартной функции OnTick().

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

В результате тело функции OnDeInit() примет вид:

void OnDeinit(const int reason)
  {
      ObjectsDeleteAll(0, prefix_graph);
  }

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

Реализуем функцию закрытия всех позиций

Раз уж мы уже начали использовать функцию closeAllPos(), давайте реализуем ее код.

Функция closeAllPos() закрывает все открытые в данный момент позиции, а также удаляет все выставленные ордера.

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

Таким образом, первой строчкой функции closeAllPos() будет вызов еще одной функции: closeByPos().

Функция closeByPos() как раз и будет пытаться закрыть позиции при помощи открытых встречных. После того, как все встречные закрыты, функция closeAllPos() закрывает оставшиеся позиции обычным способом. После чего закрывает выставленные ордера.

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

#ifdef __MQL5__ 
   #include <Trade\Trade.mqh>
   CTrade Trade;
#endif 

Теперь можно приступить к написанию функции, закрывающей все позиции встречными:

void closeByPos(){
   bool repeatOpen=false;
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){
            long closefirst=PositionGetInteger(POSITION_TICKET);
            double closeLots=PositionGetDouble(POSITION_VOLUME);
            
            for(int ti2=cntMyPos-1; ti2>=0; ti2--){
               if(PositionGetSymbol(ti2)!=_Symbol) continue;
               if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
               if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue;
               if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue;
               
               MqlTradeRequest request;
               MqlTradeResult  result;
               ZeroMemory(request);
               ZeroMemory(result);
               request.action=TRADE_ACTION_CLOSE_BY;
               request.position=closefirst;
               request.position_by=PositionGetInteger(POSITION_TICKET);
               if(EA_Magic>0) request.magic=EA_Magic;
               if(OrderSend(request,result)){
                  repeatOpen=true;
                  break;
               }
            }
            if(repeatOpen){
               break;
            }
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               int closefirst=OrderTicket();
               double closeLots=OrderLots();
               
               for(int ti2=cntMyPos-1; ti2>=0; ti2--){
                  if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) continue; 
                  if( OrderSymbol()!=_Symbol ) continue;
                  if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
                  if( OrderType()!=OP_SELL ) continue;
                  if( OrderLots()<closeLots ) continue;
                  
                  if( OrderCloseBy(closefirst, OrderTicket()) ){
                     repeatOpen=true;
                     break;
                  }
               }
               if(repeatOpen){
                  break;
               }
            }
                        
         }
      }
   #endif 
   // если мы закрыли какую-нибудь позицию встречной,
   // то снова запускаем функцию closeByPos
   if(repeatOpen){
      closeByPos();
   }
}

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

После того, как были закрыты все встречные позиции, функция closeAllPos() выполняет закрытие оставшихся:

void closeAllPos(){
   closeByPos();
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;

         Trade.PositionClose(PositionGetInteger(POSITION_TICKET));
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         
         Trade.OrderDelete(orderTicket);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,100)){
               }
            }else if(OrderType()==OP_SELL){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,100)){
               }
            }else{
               if(!OrderDelete(OrderTicket())){
               }
            }
                        
         }
      }
   #endif 
   // удаляем кнопку закрытия позиций
   if(ObjectFind(0, prefix_graph+"delall")>0){
      ObjectDelete(0, prefix_graph+"delall");
   }

}

Реализуем функцию OnTick

Мы уже реализовали практически весь функционал советника. Осталось самое главное — выставление сетки ордеров. Давайте наконец займемся и этим.

Стандартная функция OnTick() вызывается при поступлении каждого тика по инструменту. Именно в ней мы и будем проверять, есть ли наша сетка из ордеров, и если нет, то создавать ее.

Проверка начала бара. Однако проверять это каждый тик избыточно. Было бы достаточным проверять наличие сетки, например, раз в 5 минут. Для этого давайте добавим в функцию OnTick() код, проверяющий начало бара. И если это не первый тик с начала бара, то будем завершать работу функции, ничего не делая:

   if( !pdxIsNewBar() ){
      return;
   }

Функция pdxIsNewBar() имеет вид:

bool pdxIsNewBar(){
   static datetime Old_Time;
   datetime New_Time[1];

   if(CopyTime(_Symbol,_Period,0,1,New_Time)>0){
      if(Old_Time!=New_Time[0]){
         Old_Time=New_Time[0];
         return true;
      }
   }
   return false;
}

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

Проверка тейк-профита. Но перед тем, как проверять наличие сетки, нам нужно проверить, не достигнут ли уже тейк-профит по всем открытым в данный момент позициям уже выставленной нами сетки. И если есть, то вызвать уже известную нам функцию closeAllPos().

   if(checkTakeProfit()){
      closeAllPos();
   }

Для проверки наличия тейк-профита мы вызываем функцию checkTakeProfit(). Она подсчитывает прибыль по всем открытым в данный момент позициям и сравнивает ее со значением входящего параметра takeProfit:

bool checkTakeProfit(){
   if( takeProfit<=0 ) return false;
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   if(profit>takeProfit){
      return true;
   }
   return false;
}

Вывод кнопки Закрыть все.... Но и это еще не все. Не будем забывать и про кнопку Закрыть все…, которую мы реализовали, но так и не вывели. Самое время добавить вызов ее функции:

getmeinfo_btn(_Symbol);

Выглядеть она будет примерно так:

Кнопка Закрыть все...

Выставление сетки. И наконец мы подходим к самой главной части нашего советника. Выглядит она весьма просто, так как весь код в очередной раз спрятан за функциями:

   // если по инструменту есть открытые позиции или выставленные ордера, то
   if( existLimits() ){
   }else{
   // иначе выставляем сетку
      initLimits();
   }

Функция existLimits() возвращает true, если по данному инструменту есть открытые позиции или выставлены какие-либо ордера:

bool existLimits(){
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         return true;
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         return true;
      }
   #else 
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            return true;
         }
      }
   #endif 
   
   return false;
}

Если данная функция вернула true, то мы ничего не делаем. Иначе выставляем новую сетку ордеров функцией initLimits():

void initLimits(){
   // цена для выставления ордеров сетки
   double curPrice;
   // текущая цена по инструменту
   MqlTick lastme;
   SymbolInfoTick(_Symbol, lastme);
   // если текущей цены получить не удалось, отменяем выставление сетки
   if( lastme.bid==0 ){
      return;
   }

   // минимальное расстояние от цены, на котором можно ставить стоп лоссы и,
   // скорее всего, выставлять отложенные ордера
   double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   
   // выставляем ордера в Long
   curPrice=lastme.bid;
   for(uint i=0; i<maxLimits; i++){
      curPrice+=ST;

      if( curPrice-lastme.ask < minStop ) continue;
      if(!pdxSendOrder(MY_BUYSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
   // выставляем ордера в Short
   curPrice=lastme.ask;
   for(uint i=0; i<maxLimits; i++){
      curPrice-=ST;
      if( lastme.bid-curPrice < minStop ) continue;
      if(!pdxSendOrder(MY_SELLSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
}

Тестирование работы советника

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

Поскольку наш советник работает и в MetaTrader 4, и в MetaTrader 5, мы можем выбирать, в какой версии терминала проводить тестирование. Хотя тут, наверное, выбор очевиден. Тестер стратегий MetaTrader 5 и наглядней, и, как говорят, качественнее.

Для начала проведем тестирование без какой-либо оптимизации. Наш советник особо не должен зависеть от значений входящих параметров, если использовать вменяемые значения. Возьмем:

Входящие параметры оставим по умолчанию (лот 0.01, шаг 10 пунктов, 7 ордеров в сетке, тейк-профит 1 доллар).

Результат представлен на рисунке:

График баланса при первом тесте советника

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

Результат визуализации

Все началось 13 сентября, чуть позже 16:15. Сначала был зацеплен ордер в Long, потом 2 ордера в Short, потом еще 2 ордера в Long, и дальше цена зацепила оставшиеся 5 ордеров в Short. Итого у нас открыты 3 ордера в Long и 7 ордеров в Short.

Далее на картинке не видно, но ниже цена не пошла, и к 20 сентября цена вернулась к верхней точке, и зацепила оставшиеся 4 ордера в Long.

Итого у нас открыты все 7 ордеров в Short и 7 ордеров в Long. Так что тейк-профита мы уже никогда не достигнем.

Если смотреть дальнейшее движение цены, то она пойдет вверх еще примерно на 80 пунктов. Так что если бы у нас в цепочке было не 7 ордеров, а, скажем, 13, то вполне возможно, что в данной ситуации мы смогли бы выйти в плюс.

Но даже если бы этого не хватило, то еще позже цена пошла бы вниз на 200 пунктов, так что при 30 ордерах в цепочке теоретически мы могли бы получить тейк-профит. Правда, заняло бы это у нас не один месяц. И просадка была бы не самая маленькая.

Тестируем новое количество ордеров в сетке. Проверим наши предположения. 13 ордеров в сетке ничего не изменило. А вот 20 ордеров действительно позволили выплыть сухими из воды:

Тестирование EURUSD, 20 ордеров в сетке

Но просадка у нас была в районе 300 долларов, тогда как общая прибыль чуть более 100 долларов. Так что наша торговая стратегия может быть и может работать, но ей требуются кардинальные улучшения.

По этой причине особого смысла в оптимизации сейчас нет. Но все же давайте попробуем ее провести.

Проводим оптимизацию. Оптимизация будет проводиться со следующими параметрами:

Лучше всего показал себя шаг в 13 пунктов, а количество ордеров в сетке равное 16:

Тестирование EURUSD, 16 ордеров в сетке, шаг 13 пунктов

Это результат тестирования в режиме Каждый тик на основе реальных данных. И несмотря на то, что результат положителен, прибыль за 5 месяцев в 119 долларов при просадке в 221 доллар, это не самый лучший результат. Так что наша торговая стратегия действительно требует доработки.

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

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

Давайте подумаем, что же можно сделать, чтобы решить обнаруженную проблему.

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

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

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

При обоих вариантах есть риск еще сильнее усугубить ситуацию, увеличив и без того большую просадку.

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

При этом вариантов, когда это можно сделать, будет больше:

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

input double      takeLoss=0; //Закрывать при убытке, $

Нам теперь придется переписать функцию checkTakeProfit(), чтобы она возвращала не true или false, а текущую прибыль по всем открытым позициям:

double checkTakeProfit(){
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   return profit;
}

Желтым выделено то, что мы изменили.

Вот теперь мы можем доработать функцию OnTick(), чтобы в ней проверялся не только тейк-профит, но и стоп лосс по всем позициям:

   if(takeProfit>0 && checkTakeProfit()>takeProfit){
      closeAllPos();
   }else if(takeLoss<0 && checkTakeProfit()<takeLoss){
      closeAllPos();
   }

Дополнительное тестирование

Посмотрим, получилось ли сделать лучше.

Оптимизировать будем только стоп-лосс в долларах. В диапазоне от -5 до -100 долларов. Остальные параметры оставим теми, что были выбраны при последнем тестировании (шаг 13 пунктов, количество ордеров в сетке 16).

В результате больше прибыли мы получим при стоп лоссе в -56 долларов. Прибыль за 5 месяцев составит 156 долларов при максимальной просадке в 83 доллара:

Тестируем EURUSD, стоп лосс в -56 долларов

Глядя на график, можно заметить, что стоп лосс за 5 месяцев сработал только один раз. И по соотношению прибыли к просадке результат, конечно, лучше.

Однако перед тем как делать окончательные выводы, давайте проверим, сможет ли наш советник с выбранными параметрами приносить хоть какую-то прибыль в долгосрочном плане. Хотя бы на протяжении последних 5 лет:

Тестирование EURUSD  со стоп лоссом, 5 лет

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

Добавляем стоп лоссы и тейк-профиты для ордеров

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

Оптимизация на истории в 5 лет показала результаты лучше представленных выше.

Лучше всего показал себя уровень стоп лосса в 140 пунктов и тейк-профита в 50 пунктов. При этом если по текущей сетке в течение 30 дней ни одной позиции открыто не было, то она закрывается и открывается новая сетка.

Итак, итоговый результат:

Используем стоп лосс и тейк-профит для ордеров

Прибыль 351 доллар при просадке в 226 долларов. Это, конечно, лучше, чем результаты торговли без стоп лоссов. Однако смущает, что все результаты при закрытии текущей сетки менее чем через 30 дней после совершения последней сделки убыточны. Да и количество дней более 30, в большинстве своем, также приводит к убыткам. Так что данный результат скорее случайность, чем закономерность.

Заключение

Основной целью данной статьи была попытка написать торговый советник, который будет работать и в MetaTrader 4, и в MetaTrader 5. И это нам действительно удалось.

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

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

Однако, также не стоит думать, что сеточные торговые стратегии не могут приносить прибыль. Например, посмотрите на данные сигналы:

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