Tag Archives: программирование

MQL5 Урок 2 – Типы переменных

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

(далее…)
Уроки по MQL5 , ,

MQL5 Урок 1 – Редактор кода, События, Устройство mql-программ

Приветствую вас, друзья! Эта серия уроков направлена на новичков, которые вообще никогда не программировали. Для тех, кто пробовал, знает уже какой-то язык, особенно Си и mql4, многие из этих уроков будут простыми.

В первом уроке мы рассмотрим отличия MQL5 от 4 версии, устройство редактора кода MetaEditor 5 и основные функции устройства MQL-программ.

(далее…)
Уроки по MQL5 , ,

MQL4: Использование структур для повышения эффективности разработки программ

Здравствуйте, коллеги форекс-программисты!

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

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

MQL4 программирование - Структуры, улучшаем оптимизацию кода

Что такое структуры?

Структуры в MQL4 представляют собой наборы данных, которые называют элементами структуры или полями. В отличие от массивов, которые содержат элементы только одного типа, структуры могут состоять из элементов разных типов. Таким образом, первым и основным назначением структур является возможность группировать переменные по какому-то признаку. Структура – это пользовательский тип данных, а значит, можно объявлять переменные этого типа. Такую переменную-структуру можно копировать в другую того же типа с помощью оператора присваивания, передавать в функцию в качестве аргумента, возвращать значение из функции, объявлять массивы таких переменных. К каждому элементу структуры можно обращаться и изменять его напрямую, как и обычную переменную. Однако на все эти действия распространяются и определённые ограничения, которые мы рассмотрим далее.

Синтаксис

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

struct <имя_структуры>
  { 
   <тип> <элемент_1>;
   <тип> <элемент_2>;
   <тип> <элемент_3>;
    …
   <тип> <элемент_n>;
  };

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

  • цена открытия;
  • цена закрытия;
  • максимум;
  • минимум;
  • объём;
  • время открытия.

Другие характеристики, такие как тип свечи («медвежья» или «бычья»), высота тела свечи, размер верхней тени, размер нижней тени и прочие предполагается вычислять в реальном времени. Это не всегда удобно, особенно если надо часто обрабатывать такие параметры для целого массива свечей. Можно все расчёты делать в момент формирования новой свечи, а результаты хранить в переменных. Тогда для хранения указанных характеристик свечи нам понадобится 11 переменных разного типа:

double   open;                  // цена открытия
double   close;                 // цена закрытия
double   high;                  // максимум
double   low;                   // минимум
long     volume;                // тиковый объём
datetime time;                  // время открытия
uchar    type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
int      height_full;           // общая высота свечи
int      height_body;           // размер тела свечи
int      height_top_shadow;     // размер верхней тени
int      height_bottom_shadow;  // размер нижней тени

Если же мы захотим оперировать набором, скажем, в 100 свечей, нам придётся объявить 11 массивов по 100 элементов. Допустим, нам понадобилось передать все эти данные в функцию – получится весьма громоздкая конструкция, которая усложнит код и его восприятие программистом, что потенциально может привести к ошибкам. Однако все эти данные можно объединить в структуру:

struct Candle
{
 double   open;                  // цена открытия
 double   close;                 // цена закрытия
 double   high;                  // максимум
 double   low;                   // минимум
 long     volume;                // тиковый объём
 datetime time;                  // время открытия
 uchar    type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
 int      height_full;           // общая высота свечи
 int      height_body;           // размер тела свечи
 int      height_top_shadow;     // размер верхней тени
 int      height_bottom_shadow;  // размер нижней тени
};

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

Candle Bar;

Доступ к элементам структуры

Для доступа к элементам структуры применяется операция принадлежности «точка» (.).  Например:

Bar.volume=iVolume(_Symbol,_Period,1)

Здесь мы присваиваем элементу volume переменной-структуры Bar значение тикового объёма бара с индексом 1 на текущем графике.

Инициализация структур

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

Candle Bar = {0,0,0,0,100,D'2018.03.10 15:08:02',0,0,0,0,12};

Здесь мы объявили переменную Bar и проинициализировали все элементы структуры с первый по одиннадцатый некими значениями.
А так

Candle Bar = {0.1012,0.6321,0.2194,0.1784,100};

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

Candle Bar = {};

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

Candle Bar = {100};

Строго говоря, в нашем случае такая инициализация некорректна, так как элемент time имеет тип datetime, а значение 100 – это сто секунд с первого января 1970 года. Поэтому при подобной инициализации структур необходимо учитывать особенности неявного приведения типов.

Вложенные структуры

Элементы структуры могут быть не только простых типов, но и сложных: строками, статическими и динамическими массивами и структурами. Вложенные структуры определяются отдельно. Покажем на конкретном примере, как можно организовать данные таким образом. В нашей структуре Candle последние четыре элемента содержат размеры как всей свечи, так и её отдельных частей. Можно по этому признаку объединить эти элементы в отдельную структуру:

struct Height
  {
   int               full;           // общая высота свечи
   int               body;           // размер тела свечи
   int               top_shadow;     // размер верхней тени
   int               bottom_shadow;  // размер нижней тени
  };

Тогда в структуре Candle достаточно будет объявить одну переменную типа Height:

struct Candle
  {
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
   uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Height            height;                // высоты частей свечи
  };

Список инициализаторов также должен содержать вложенный список:

Candle Bar=
  {
   0.0,
   0.0,
   0.0,
   0.0,
   100,
   D'2018.03.10 15:08:02',
   1,
     {
      0,
      0,
      0,
      0
     }
  };

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

Bar.height.body = MathAbs(iOpen(_Symbol,_Period,1) - iClose(_Symbol,_Period,1));

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

Обмен данными между структурами

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

Heigh h1, h2={1,4,1,8};
h1=h2;

В результате значения всех полей переменной h2 скопируются в поля переменной h1.

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

   Candle Bar1, Bar2= {0.0,0.0,0.0,0.0,100,D'2018.03.10 15:08:02',1,{0,0,0,0}};
   Bar1.open                 = Bar2.open;                  // цена открытия
   Bar1.close                = Bar2.close;                 // цена закрытия
   Bar1.high                 = Bar2.high;                  // максимум
   Bar1.low                  = Bar2.low;                   // минимум
   Bar1.volume               = Bar2.volume;                // тиковый объём
   Bar1.time                 = Bar2.time;                  // время открытия
   Bar1.type                 = Bar2.type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Bar1.height.full          = Bar2.height.full;           // общая высота свечи
   Bar1.height.body          = Bar2.height.body;           // размер тела свечи
   Bar1.height.top_shadow    = Bar2.height.top_shadow;     // размер верхней тени
   Bar1.height.bottom_shadow = Bar2.height.bottom_shadow;  // размер нижней тени

Структуры и функции

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

// структура, содержащая координаты точки на плоскости
struct Coordinates
{
 int   x; 
 int   y;
};
// зададим точку с координатами (x=120,y=35)
Coordinates P={120,35};
//+------------------------------------------------------------------+
//| Смещает координаты точки на:                                     |
//| shift_x по горизонтали                                           |
//| shift_y по вертикали                                             |
//+------------------------------------------------------------------+
void MovePoint(Coordinates &point, int shift_x=0, int shift_y=0)
{
  point.x+=shift_x;
  point.y+=shift_y; 
}
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("x=",P.x,"; y=",P.y);
   MovePoint(P,2,15);
   Print("x=",P.x,"; y=",P.y);
   return(INIT_PARAMETERS_INCORRECT);
  }

Результат:

P.x=122; P.y=50
P.x=120; P.y=35

В приведённом примере мы передаём инициализированную переменную-структуру P и смещения координат по горизонтали и вертикали в функцию MovePoint(). В результате функция изменила оба элемента структуры, что можно увидеть, распечатав в журнал эти значения до и после вызова функции.

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

//+------------------------------------------------------------------+
//| Смещает координаты точки на:                                     |
//| shift_x по горизонтали                                           |
//| shift_y по вертикали                                             |
//| и возвращает структуру с новыми координатами                     |
//+------------------------------------------------------------------+
Coordinates MovePoint(Coordinates &point, int shift_x=0, int shift_y=0)
{
  Coordinates p;
  p.x=point.x+shift_x;
  p.y=point.y+shift_y; 
  return(p);
}
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("P.x=",P.x,"; P.y=",P.y);
   Coordinates MovedPoint=MovePoint(P,2,15);
   Print("MovedPoint.x=",MovedPoint.x,"; MovedPoint.y=",MovedPoint.y);
   Print("P.x=",P.x,"; P.y=",P.y);
 
  return(INIT_PARAMETERS_INCORRECT);
  }

Результат:

P.x=120; P.y=35
MovedPoint.x=122; MovedPoint.y=50
P.x=120; P.y=35

Мы видим, что функция возвратила в переменную MovedPoint рассчитанные значения, но при этом переменная P, переданная в функцию в качестве аргумента, не изменилась. Надо заметить, что возвращать можно только простые структуры, не содержащие массивы или вложенные структуры.

Методы

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

Давайте в нашей структуре Candle определим метод, который будет получать время открытия свечи, искать её в массиве-таймсерии и заполнять поля структуры данными о найденной свече.

struct Height
  {
   int               full;           // общая высота свечи
   int               body;           // размер тела свечи
   int               top_shadow;     // размер верхней тени
   int               bottom_shadow;  // размер нижней тени
  };
struct Candle
  {
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
   uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
   Height            height;                // высоты частей свечи

   void              GetCandleParam(datetime open_time)
     {
      // получаем индекс свечи по времени или индекс ближайшей свечи
      int index=iBarShift(_Symbol,_Period,open_time);
      
      // рассчитываем поля структуры
      open                 = iOpen(_Symbol,_Period,index);                  // цена открытия
      close                = iClose(_Symbol,_Period,index);                 // цена закрытия
      high                 = iHigh(_Symbol,_Period,index);                  // максимум
      low                  = iLow(_Symbol,_Period,index);                   // минимум
      volume               = iVolume(_Symbol,_Period,index);                // тиковый объём
      time                 = iTime(_Symbol,_Period,index);                  // время открытия
      height.full          = int((high-low)/_Point);                        // общая высота свечи
      height.body          = int((close-open)/_Point);                      // размер тела свечи
      type                 = height.body>0?1:(height.body<0?2:0);           // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
      height.top_shadow    = int((high-((type<2)?close:open))/_Point);      // размер верхней тени
      height.bottom_shadow = int((((type<2)?open:close)-low)/_Point);       // размер нижней тени
      height.body          = MathAbs(height.body);
     }
  } Bar; // сразу объявим переменную-структуру
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Bar.GetCandleParam(D'2020.10.02 10:00');
   Print("Цена открытия свечи: ",DoubleToStr(Bar.open,_Digits));
   Print("Цена закрытия свечи: ",DoubleToStr(Bar.close,_Digits));
   Print("Тип свечи: ",Bar.type==0?"дожи":(Bar.type==1?"бычья":"медвежья"));
   Print("Высота свечи: ", IntegerToString(Bar.height.full));
   Print("Время: ", Bar.time);   return(INIT_PARAMETERS_INCORRECT);
}

Спецификаторы доступа

Для того, чтобы ограничить доступ к полям и методам структуры, применяются спецификаторы доступа: private, protected и public. По умолчанию все поля и методы структуры имеют модификатор public, то есть к ним можно обращаться извне. Если не используется наследование, то спецификаторы protected и private равнозначны.

Все поля и методы, объявленные после private: и до public:, доступны только методам структуры.

struct Candle
  {
private:
   double            open;                  // цена открытия
   double            close;                 // цена закрытия
   double            high;                  // максимум
   double            low;                   // минимум
   long              volume;                // тиковый объём
   datetime          time;                  // время открытия
public:
  uchar             type;                  // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
  Height            height;                // высоты частей свечи
  void              GetCandleParam(datetime open_time)
     {
      // получаем индекс свечи по времени или индекс ближайшей свечи
      int index=iBarShift(_Symbol,_Period,open_time);
      
      // рассчитываем поля структуры
      open                 = iOpen(_Symbol,_Period,index);                  // цена открытия
      close                = iClose(_Symbol,_Period,index);                 // цена закрытия
      high                 = iHigh(_Symbol,_Period,index);                  // максимум
      low                  = iLow(_Symbol,_Period,index);                   // минимум
      volume               = iVolume(_Symbol,_Period,index);                // тиковый объём
      time                 = iTime(_Symbol,_Period,index);                  // время открытия
      height.full          = int((high-low)/_Point);                        // общая высота свечи
      height.body          = int((close-open)/_Point);                      // размер тела свечи
      type                 = height.body>0?1:(height.body<0?2:0);           // тип свечи (0-"дожи", 1-бычья, 2-медвежья)
      height.top_shadow    = int((high-((type<2)?close:open))/_Point);      // размер верхней тени
      height.bottom_shadow = int((((type<2)?open:close)-low)/_Point);       // размер нижней тени
      height.body          = MathAbs(height.body);
     }
  }

В этом примере поля структуры open, close, high, volume, time доступны только из метода GetCandleParam().

Пишем советник с использованием структур

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

Алгоритм советника

Алгоритм нашего советника в общем виде будет выглядеть так:

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

Входные параметры

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

input  double  Lots                = 0.01;     // Фиксированный лот
input  double  Multiplier          = 2.0;      // Множитель
input  int     MaxLegs             = 10;       // Максимальное количество колен
input  int     MinProfit           = 1.0;      // Минимальная прибыль
input  int     MinStep             = 100;      // Минимальное расстояние между ордерами(шаг)
input  int     Magic               = 1100;     // Magic Number (идентификатор ордеров)
input  int     Slippage            = 30;       // Максимально допустимое проскальзывание

Функции советника

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

//+------------------------------------------------------------------+
//| Новый бар на текущем таймфрейме                               |
//+------------------------------------------------------------------+
bool IsNewBar()
  {
   static datetime last_bar=0;
   datetime null_bar=iTime(_Symbol,_Period,0);
   if(last_bar!=null_bar)
     {
      last_bar=null_bar;
      return(true);
     }
   return(false);
  }

Далее напишем функцию, генерирующую торговый сигнал:

//+------------------------------------------------------------------+
//| Возвращает OP_BUY, если сформирован сигнал на покупку,           |
//|            OP_SELL, если сформирован сигнал на продажу и         |
//|            EMPTY, если сигнала на закрытом баре нет              |
//+------------------------------------------------------------------+
int GetSignal()
  {
   if(iClose(_Symbol,_Period,1)<iLow(_Symbol,_Period,2))
      return(OP_BUY);
   if(iHigh(_Symbol,_Period,2)<iClose(_Symbol,_Period,1))
      return(OP_SELL);
   return(EMPTY);
  }

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

//+------------------------------------------------------------------+
//| Возвращает тип первого найденного ордера, который был открыт     |
//| советником по текущему символу                                   |
//+------------------------------------------------------------------+
int GetOpenedOrdersType()
  {
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
            return(OrderType());
   return(EMPTY);
  }

Следующие две функции возвращают общее количество открытых советником ордеров и прибыль по этим ордерам соответственно:

//+------------------------------------------------------------------+
//| Возвращает общее количество открытых советником                  |
//| ордеров указанного типа по текущему символу                      |
//+------------------------------------------------------------------+
int GetOrdersCount(int type)
  {
   int count=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==type)
            count++;
   return(count);
  }

//+------------------------------------------------------------------+
//| Возвращает прибыль по всем ордерам, открытым советником          |
//+------------------------------------------------------------------+
double GetProfit()
  {
   double profit=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
            profit+=OrderProfit()+OrderCommission()+OrderSwap();
   return(profit);
  }

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

//+------------------------------------------------------------------+
//| Возвращает цену открытия самого верхнего из                      |
//| открытых ордеров Sell в сетке                                    |
//+------------------------------------------------------------------+
double GetTopSellPrice()
  {
   double price=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==OP_SELL)
            if(price<OrderOpenPrice())
               price=OrderOpenPrice();
   return(price);
  }

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

//+------------------------------------------------------------------+
//| Возвращает цену открытия самого нижнего  из                      |
//| открытых ордеров Buy в сетке                                     |
//+------------------------------------------------------------------+
double GetBottomBuyPrice()
  {
   double price=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic && OrderType()==OP_BUY)
            if(OrderOpenPrice()<price || price==0)
               price=OrderOpenPrice();
   return(price);
  }

Лот очередного ордера сетки будем вычислять с помощью следующей функции:

//+------------------------------------------------------------------+
//| Возвращает размер лота, рассчитанный как n-ый член               |
//| геометрической прогрессии                                        |
//+------------------------------------------------------------------+
double GetLot(int n)
  {
   return(NormalizeDouble(Lots*MathPow(MathMax(1,Multiplier),n),2));
  }

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

//+------------------------------------------------------------------+
//| Открывает сделку лотом lot                                       |
//| в направлении com                                                |
//+------------------------------------------------------------------+
bool SetOrder(double lot, int com)
  {
   MqlTick tick;
   if(SymbolInfoTick(_Symbol,tick))
     {
      for(int i=0; i<5; i++)
        {
         if(OrderSend(_Symbol,com,lot,((com==OP_BUY)?tick.ask:tick.bid),Slippage,0,0,NULL,Magic,0,((com==OP_BUY)?clrBlue:clrRed))>EMPTY)
            return(true);
         Sleep(1000);
        }
     }
   return(false);
  }

Вторая – закрывать все открытые советником ордера:

//+------------------------------------------------------------------+
//| Закрывает все рыночные ордера, открытые советником               |
//| по текущему символу                                              |
//+------------------------------------------------------------------+
void CloseOrders()
  {
   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
           {
            MqlTick tick;
            if(SymbolInfoTick(_Symbol,tick))
              {
               for(int j=0; j<5; j++)
                 {
                  if(OrderClose(OrderTicket(),OrderLots(),((OrderType()==OP_BUY)?tick.bid:tick.ask),Slippage,((OrderType()==OP_BUY)?clrBlue:clrRed)))
                     break;
                  Sleep(1000);
                 }
              }
           }
  }

Теперь, когда все необходимые функции созданы, осталось закодировать нехитрую торговую логику нашего советника и поместить целиком этот код в обработчик OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
// открываем новые ордера и фиксируем прибыль только на открытии нового бара
   if(IsNewBar())
     {
      // получаем новый сигнал
      int Signal=GetSignal();
      // сразу выходим из функции, если на закрытом баре не был сформирован сигнал
      if(Signal<0)
         return;

      // есть ли открытые советником ордера
      int OpenedOrderType=GetOpenedOrdersType();// (!!!)

      // есть открытые ордера и их не равен сигналу
      if(OpenedOrderType!=EMPTY && Signal!=OpenedOrderType)
        {
         // если есть прибыль закрываем ордера
         if(GetProfit()>MinProfit)// (!!!)
           {
            CloseOrders();
            OpenedOrderType=GetOpenedOrdersType();// (!!!)
           }
        }

      // нет открытых ордеров
      if(OpenedOrderType==EMPTY)
        {
         //открываем первый ордер сетки
         SetOrder(Lots,Signal);
         return;
        }

      // открываем усредняющие ордера по сигналу
      if(Signal==OpenedOrderType)
        {
         // рассчитываем количество открытых ордеров для расчёта следующего лота
         int Count=GetOrdersCount(OpenedOrderType);// (!!!)
         double Price;

         switch(OpenedOrderType)
           {
            case OP_BUY:
               Price=GetBottomBuyPrice();// (!!!)
               // проверка на минимальный шаг до самого верхнего ордера BUY
               if(int((Price-Ask)/_Point)>MinStep)
                 {
                  SetOrder(GetLot(Count),OP_BUY);
                 }
               break;

            // проверка на минимальный шаг до самого нижнего ордера SELL
            case OP_SELL:
               Price=GetTopSellPrice();// (!!!)

               if(int((Bid-Price)/_Point)>MinStep)
                 {
                  SetOrder(GetLot(Count),OP_SELL);
                 }
               break;
           }

        }
     }
  }

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

  • GetOpenedOrdersType();
  • GetOrdersCount();
  • GetProfit();
  • GetTopSellPrice();
  • GetBottomBuyPrice().

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

struct state
  {
   int               buy_count;         // количество ордеров buy
   int               sell_count;        // количество ордеров sell
   double            buy_bottom_price;  // цена открытия самого нижнего ордера buy
   double            sell_top_price;    // цена открытия самого верхнего ордера sell
   double            profit;            // прибыль всех ордеров

   // метод для сбора информации о состоянии счёта
   // и обновления элементов структуры
   void              Refresh();
  } State;

В структуре присутствует метод Refresh(), который рассчитывает и присваивает значения элементам структуры. Определим тело метода вне структуры. Для этого используем операцию разрешения контекста (::). Контекст – это дескриптор (имя) структуры:

//+------------------------------------------------------------------+
//| Метод собирает информацию о текущем состоянии счёта              |
//| и обновляет соответствующие поля структуры                       |
//+------------------------------------------------------------------+
void state::Refresh(void)
  {
// обнуляем числовые поля структуры
   ZeroMemory(this);

   for(int i=OrdersTotal()-1; i>=0; i--)
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
         if(OrderSymbol()==_Symbol && OrderMagicNumber()==Magic)
           {
            double OpenPrice=OrderOpenPrice();
            profit+=OrderProfit()+OrderCommission()+OrderSwap();
            switch(OrderType())
              {
               case OP_BUY:
                  buy_count++;
                  if(OpenPrice<buy_bottom_price || buy_bottom_price==0)
                     buy_bottom_price=OpenPrice;
                  break;
               case OP_SELL:
                  sell_count++;
                  if(OpenPrice>sell_top_price || sell_top_price==0)
                     sell_top_price=OpenPrice;
                  break;
              }
           }

  }

Обратите внимание, что в теле метода мы обращаемся к элементам структуры, не используя точку, так как мы использовали операцию разрешения контекста. Числовые поля перед их обновлением в самом начале тела метода обнуляются функцией ZeroMemory() с ключевым словом this, таким образом, структура передаёт ссылку на саму себя.

Основной код советника внутри обработчика OnTick() теперь будет выглядеть так:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   for(int i=0; i<2000; i++)
   {
    double b=MathSqrt(i);
   }
// открываем новые ордера и фиксируем прибыль только на открытии нового бара
   if(IsNewBar())
     {
      // получаем новый сигнал
      int Signal=GetSignal();
      // сразу выходим из функции, если на закрытом баре не был сформирован сигнал
      if(Signal<0)
         return;

      // обновляем структуру
      State.Refresh(); // (!!!)

      // если текущая прибыль больше минимальной и поступил обратный сигнал
      if(State.profit>MinProfit)
         if((Signal==OP_BUY && State.sell_count>0) ||
            (Signal==OP_SELL && State.buy_count>0))
           {
            // закрываем все ордера
            CloseOrders();
            // обновляем структуру
            State.Refresh(); // (!!!)
           }

      // открыты ордера Buy
      if(State.buy_count>0)
        {
         // усредняемся по сигналу, если расстояние до самого нижнего Buy больше минимального шага
         if(Signal==OP_BUY && int((State.buy_bottom_price-Ask)/_Point)>MinStep)
            SetOrder(GetLot(State.buy_count),Signal);
        }
      else
         // открыты ордера Sell
         if(State.sell_count>0)
           {
            // усредняемся по сигналу, если расстояние до самого верхнего Sell больше минимального шага
            if(Signal==OP_SELL && int((Bid-State.sell_top_price)/_Point)>MinStep)
               SetOrder(GetLot(State.sell_count),Signal);
           }
         else
            // нет ордеров, открываем новый по сигналу
            SetOrder(Lots,Signal);
     }
  }

Вызов метода State.Refresh() заменил вызовы пяти функций: GetOpenedOrdersType(), GetOrdersCount()GetProfit(), GetTopSellPrice(), GetBottomBuyPrice(). Следовательно, сократилось и число обращений к серверу, что не могло не сказаться на быстродействии. Вдобавок код стал компактнее.

Заключение

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

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , , ,

MQL4: Пишем советник по стратегии «Пирамидинг»

Здравствуйте, уважаемые форекс-программисты!

Сегодняшний урок по MQL4 мы посвятим созданию торгового эксперта, в котором реализуем построение пирамиды отложенных ордеров и последующее её сопровождение. Давайте сначала разберёмся, что представляет собой стратегия «Пирамидинг», насколько она эффективна и какие подводные камни таит в себе. А затем перейдем к написанию советника с пирамидингом в основе.

MQL4 - Пишем советник по стратегии Пирамидинга

Пирамидинг: основные понятия

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

На приведённом выше рисунке мы можем наблюдать работу пирамиды. Было открыто восемь позиций на покупку по тренду, в итоге все закрылись по общему Stop Loss: последняя сделка с убытком, предпоследняя – в безубытке, а шесть остальных – с прибылью. Может возникнуть вопрос: нельзя ли было открыть самую первую позицию сразу большим лотом, а закрыть по трейлинг-стопу? Дело в том, что мы не могли знать наверняка, пойдет ли цена в нужном нам направлении, и в таком случае рисковали бы всем объёмом. Пирамида же позволяет наращивать совокупный объём постепенно, так как очередная позиция открывается только после того, как предыдущая выйдет в прибыль.

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

Точки входа

Как уже отмечалось ранее, пирамидинг – это метод увеличения прибыли при торговле по тренду. Очевидно, что для диапазонной (флетовой) торговли такой подход не годится и может обернуться полной потерей депозита. Поэтому перед построением пирамиды трейдеру важно определить начало тренда. Решение этой задачи мы оставим за рамками сегодняшнего урока. Отметим лишь, что искать подобные точки входа лучше всего на старших таймфреймах от H1 и выше.

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

Наращивание объёма позиций

Наиболее консервативным и самым распространённым способом наращивания совокупного объёма является открытие каждой новой позиции фиксированным лотом.

При наличии сильного устойчивого тренда допустимо использовать и более агрессивную методику, когда объем каждой новой позиции будет больше предыдущего. Например, 1; 2; 3; 4 или 1; 2; 4; 8; 16. В этом случае важно вовремя зафиксировать прибыль, иначе если последняя позиция закроется по Stop Loss, образовавшийся убыток может перекрыть всю накопленную до этого прибыль.

Существует и обратный предыдущему способ, при котором объём каждой новой позиции меньше предыдущего. Например, 4; 3; 2; 1. Такой подход вполне обоснован, поскольку каждый тренд имеет тенденцию к затуханию, и с каждой новой ступенькой пирамиды вероятность разворота или сильного отката становится выше. Следовательно, убыток по последним позициям с меньшим лотом будет перекрыт прибылью первых. Недостатком такого способа является недополучение возможной прибыли, хотя риски, конечно же, становятся ниже. В нашем будущем советнике мы реализуем обе методики.

Фиксация прибыли

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

Другой распространённый способ – закрытие пирамиды при достижении определённого уровня прибыли. В этом случае трейдер сам определяет величину прибыли, достаточную для закрытия пирамиды.

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

Общий алгоритм советника

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

Здесь мы видим пирамиду на продажу из Sell Stop ордеров. Объём каждого последующего ордера возрастает в арифметической прогрессии с шагом 0.01.

Так выглядит эта же пирамида, когда первые четыре Sell Stop ордера были активированы при движении цены вниз. Теперь у нас открыты четыре рыночных ордера и шесть отложенных. Не хватает только общего Stop Loss у открытых позиций. Но об этом позже.

Алгоритм советника можно записать в общем виде с помощью следующей блок-схемы:

Определимся с полным функционалом будущего советника и опишем его входные параметры.

Money Management

Данная секция содержит параметры, относящиеся к управлению капиталом:

Lots – фиксированный/стартовый лот;

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

LotsMultiplicatorAr – разность арифметической прогрессии, каждый новый лот увеличивается на данное значение; если параметр равен 0 (нулю), то в качестве разности прогрессии выступает лот первого ордера серии. Используя отрицательные значения данного параметра, лот можно уменьшать;

LotsMultiplicatorGm – знаменатель геометрической прогрессии, каждый новый лот увеличивается в указанное число раз. Если значение меньше единицы, то лот будет уменьшаться. Например, при 1.5 каждый новый лот будет увеличиваться в полтора раза. А чтобы лот в полтора раза уменьшался, потребуется значение 1/1.5=0.67.

Сигнал

Как уже отмечалось, для успешной торговли нам необходимо построить пирамиду как можно ближе к возможному началу трендового движения. Воспользуемся следующей схемой: строить пирамиду из отложенных ордеров мы будем, когда линия стохастика войдёт в зону перекупленности (для пирамиды на продажу) или в зону перепроданности (для пирамиды на покупку). Если использовать классический сигнал не со входом, а с выходом линии стохастика из зон перекупленности/перепроданности, то из-за запаздывания индикатора ложных входов будет значительно больше. Мы же стараемся действовать на опережение. Если цена не развернётся в нашу сторону, нам нужно только удалить пирамиду (все отложенные ордера) и ждать нового сигнала.

StochasticPeriod – период стохастика;

StochasticSlowing – замедление;

StochasticLevelDn – уровень перепроданности/уровень перекупленности автоматически (например, для StochasticLevelDn = 20 верхний уровень 100 – StochasticLevelDn = 80).

Общие параметры торговли

DistanceToGrid – на каком расстоянии от цены строить сетку после получения сигнала от стохастика;

AfterSignaBarsCount – сколько баров ждать активации сетки. Так как мы строим сетку в момент входа стохастика в зону перекупленности или перепроданности, мы только предполагаем разворот, который может и не произойти. Поэтому если пирамида не была активирована, её надо удалить спустя какое-то время. С помощью данного параметра можно задать этот контрольный период в барах текущего таймфрейма. Очевидно, что удаление пирамиды также происходит и при поступлении обратного сигнала. Если параметр AfterSignaBarsCount равен нулю, то пирамида удаляется исключительно после формирования обратного сигнала;

Magic – «магический номер», идентификатор ордеров, открытых советником;

Slippage – максимально допустимое проскальзывание.

Параметры сетки

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

OrdersAmount – количество отложенных ордеров в сетке;

GridStep – шаг сетки, то есть расстояние между отложенными ордерами сетки. Это расстояние может быть как фиксированным, так и увеличивающимся (уменьшающимся) в зависимости от параметра StepMultMode. В случае динамического шага здесь задаётся его стартовое значение;

StepMultMode – режим расширения/уменьшения расстояния между ордерами в сетке по мере удаления от точки входа. Шаг по аналогии с объёмом позиции может изменяться в арифметической или геометрической прогрессии;

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

StepMultiplicatorGm – знаменатель геометрической прогрессии, шаг увеличивается в указанное число раз. Если значение меньше единицы, то шаг будет уменьшаться. Например, при 1.5 шаг будет увеличиваться в полтора раза. Чтобы шаг в полтора раза уменьшался, потребуется значение 1/1.5=0.67;

StopLossRatio – отношение Stop Loss к шагу. Классическая пирамида строится таким образом, что Stop Loss каждого следующего ордера равен цене открытия предыдущего. Однако с помощью этого параметра можно увязать размер Stop Loss с величиной шага;

TakeProfit – общий тейк-профит. Для фиксации прибыли в нашем советнике используется общий Stop Loss, но может случиться так, что будут активированы все ордера пирамиды и цена уйдёт дальше по тренду. Чтобы не терять в таких редких случаях прибыль, используется страховочный Take Profit, иначе нам придётся ждать возвращения цены и срабатывания общего Stop Loss. При этом лучше всё-таки использовать больше ордеров в пирамиде.

Функции советника

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

Функция RefreshState()

Начнём с функции, которая не представлена отдельным блоком в нашей схеме, так как относится к категории служебных, однако ей стоит уделить особое внимание. Функция RefreshState() собирает информацию об открытых советником ордерах и помещает её в переданную структуру state:

// структура для хранения информации об открытых советником ордерах
struct state
  {
   double            BuyTopPrice;         // цена открытия самого верхнего ордера Buy
   double            SellBottomPrice;     // цена открытия самого нижнего ордера Sell

   double            BuyTopSL;            // стоплосс самого верхнего ордера Buy
   double            BuyBottomSL;         // стоплосс самого нижнего ордера Buy
   double            SellTopSL;           // стоплосс самого верхнего ордера Sell
   double            SellBottomSL;        // стоплосс самого нижнего ордера Sell

   datetime          BuyStopStart;        // время установки пирамиды (открытия самого первого ордера Buy Stop в сетке)
   datetime          SellStopStart;       // время установки пирамиды (открытия самого первого ордера Sell Stop в сетке)

   int               BuyCount;            // количество ордеров Buy
   int               SellCount;           // количество ордеров Sell

   int               BuyStopCount;        // количество ордеров Buy Stop
   int               SellStopCount;       // количество ордеров Sell Stop
  };

Опираясь именно на собранную функцией RefreshState() статистику, советник отслеживает текущую обстановку на торговом счёте и определяет, на каком именно шаге алгоритма он находится.

Например, следующий код определяет, что все рыночные ордера пирамиды закрылись по Stop Loss и теперь требуется удалить несработавшие отложенные:

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }

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

В следующем участке кода решение о построении новой сетки мы принимаем только в случае, если нет ни рыночных, ни отложенных ордеров, открытых нашим советником:

      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

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

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }

Функция GetSignal()

Эта функция опрашивает индикатор и возвращает: OP_BUY (0 – сигнал на покупку), если стохастик входит в зону перепроданности (пересекает нижний сигнальный уровень сверху вниз); OP_SELL (1 – сигнал на продажу), если стохастик входит в зону перекупленности (пересекает верхний сигнальный уровень снизу вверх). Если сигнал не сформирован, функция возвратит EMPTY (-1):

//+------------------------------------------------------------------+
//| Функция возвращает:                                              |
//| OP_BUY (сигнал на покупку), если стохастик входит в зону         |
//| перепроданности (пересекает нижний сигнальный уровень            |
//| сверху вниз)                                                     |
//| OP_SELL (сигнал на продажу), если стохастик входит в зону        |
//| перекупленности (пересекает верхний сигнальный уровень           |
//| снизу вверх)                                                     |
//| EMPTY - сигнала нет                                              |
//+------------------------------------------------------------------+
int GetSignal()
  {
// получаем значение стохастика на баре с индексом 1
   double StochasticOnFirstBar=iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,1);

// стохастик пересекает нижний сигнальный уровень сверху вниз
   if(StochasticOnFirstBar<StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)>StochasticLevelDn)
         return(OP_BUY);
     }
// стохастик пересекает верхний сигнальный уровень снизу вверх
   if(StochasticOnFirstBar>100-StochasticLevelDn)
     {
      // для экономии ресурсов получаем значение стохастика на баре с индексом 2 только после того,
      // как убедились, что первая часть условия выполняется
      if(iStochastic(_Symbol,_Period,StochasticPeriod,1,StochasticSlowing,MODE_SMA,0,MODE_MAIN,2)<100-StochasticLevelDn)
         return(OP_SELL);
     }

// в любом другом случае возвращаем EMPTY
   return(EMPTY);
  }

Функция BuildPyramid()

Данная функция строит пирамиду отложенных ордеров Buy Stop, если переданный в неё параметр signal равен OP_BUY, и пирамиду ордеров Sell Stop, если signal имеет значение OP_SELL.

Сетка строится на расстоянии, равном значению входного параметра DistanceToGrid от цены Ask для ордеров Buy Stop и Bid для сетки Sell Stop. В случае, если это значение меньше минимально разрешённого, сетка строится на расстоянии Sell Stop для данного символа.

Если предполагается, что лот каждого нового ордера или шаг, на котором он выставляется от предыдущего, изменяется в прогрессии, происходит вызов служебной функции GetProgressionMember(). Эта функция возвращает n-ый член арифметической или геометрической прогрессии.

//+------------------------------------------------------------------+
//| Строит пирамиду из отложенных ордеров                            |
//| Buy Stop, если параметр signal равен OP_BUY                      |
//| Sell Stop, если параметр signal равен OP_SELL                    |
//| на расстоянии DistanceToGrid от текущей цены                     |
//+------------------------------------------------------------------+
void BuildPyramid(int signal)
  {
// определяем корректную величину Stop Level
   int StopLevel=GetTrueStopLevel();

// если расстояние, на котором предполагается строить пирамиду меньше Stop Level,
// то устанавливаем его значение, равным Stop Level
   int Distance=(DistanceToGrid<StopLevel)?StopLevel:DistanceToGrid;
   int Com;//тип отложенного ордера


// проверим корректность лотов перед установкой сетки
   if((Lots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
      (Lots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
     {
      Print("Некорректное значение стартового торгового объёма, пирамида не будет построена");
      return;
     }

   if(LotsMultMode>mlNone)
     {
      double LastOrderLots=NormalizeDouble(GetProgressionMember(LotsMultMode,OrdersAmount-1,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots),2);
      if((LastOrderLots<SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)) ||
         (LastOrderLots>SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX)))
        {
         Print("Обнаружены некорректные значения торгового объёма, пирамида не будет построена");
         return;
        }
     }

// проверим корректность шага перед выставлением последнего ордера пирамиды
   if((GridStep<StopLevel)  ||
      (StepMultMode>mlNone &&
       (int)GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep)<StopLevel))
     {
      Print("Обнаружены некорректные значения шага, пирамида не будет построена");
      return;
     }

// проверим корректность Stop Loss
   if((int(StopLossRatio*GridStep)<StopLevel) ||
      (StepMultMode>mlNone &&
       int(StopLossRatio*GetProgressionMember(StepMultMode,OrdersAmount-1,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep))<StopLevel))
     {
      Print("Обнаружены некорректные значения Stop Loss, пирамида не будет построена");
      return;
     }

// проверим корректность Take Profit
   if(TakeProfit>0 && TakeProfit<StopLevel)
     {
      Print("Общий Take Profit ",TakeProfit," меньше минимально допустимого значения ", StopLevel,", пирамида не будет построена");
      return;
     }

   double Price, Sign, Step=GridStep;

   MqlTick tick;
   if(!SymbolInfoTick(_Symbol,tick))
      return;

// определяем стартовую цену и тип отложенных ордеров
   if(signal==OP_BUY)
     {
      Price=tick.ask;
      Sign=1.0;
      Com=OP_BUYSTOP;
     }
   else
      if(signal==OP_SELL)
        {
         Price=tick.bid;
         Sign=-1.0;
         Com=OP_SELLSTOP;
        }
      else
         return;

// ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
   Price+=Sign*Distance*_Point;

   for(int i=0; i<OrdersAmount; i++)
     {
      // вычисляем лот для очередного ордера сетки
      double TradeLots;
      // увеличиваем или уменьшаем текущий лот в зависимости от типа прогрессии
      if(LotsMultMode>mlNone)
         TradeLots=GetProgressionMember(LotsMultMode,i,((LotsMultMode==mlArithmetical)?LotsMultiplicatorAr:LotsMultiplicatorGm),Lots);
      else
         TradeLots=Lots;

      double StopLoss,StopLossInPoints;
      // вычисляем StopLoss пропорционально шагу Step
      StopLossInPoints=StopLossRatio*Step;

      // Stop Loss для ордеров Buy Stop располагается ниже цены открытия, для Sell Stop - выше
      StopLoss=Price+EMPTY*Sign*StopLossInPoints*_Point;

      // пять попыток выставить очередной ордер пирамиды
      for(int j=0; j<5; j++)
        {
         if(OrderSend(_Symbol,Com,NormalizeDouble(TradeLots,2),NormalizeDouble(Price,_Digits),Slippage,NormalizeDouble(StopLoss,_Digits),0,NULL,Magic,0,OrderColor(Com))!=EMPTY)
            break;
         else
            Sleep(1000);
        }

      // увеличиваем или уменьшаем текущий шаг в зависимости от типа прогрессии
      if(StepMultMode>mlNone)
         Step=GetProgressionMember(StepMultMode,i,((StepMultMode==mlArithmetical)?StepMultiplicatorAr:StepMultiplicatorGm),GridStep);

      // ордера Buy Stop выставляются выше от цены, а Sell Stop - ниже
      Price+=Sign*Step*_Point;

     }
// устанавливаем общий тейкпрофит только что выставленным отложенным ордерам
   if(TakeProfit>0)
     {
      double tp=NormalizeDouble(Price+Sign*(TakeProfit<StopLevel?StopLevel:TakeProfit)*_Point,_Digits);

      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()>OP_SELL)
           {
            if(NormalizeDouble(OrderTakeProfit(),_Digits)!=tp)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),OrderStopLoss(),tp,0))
                     break;
                  else
                     Sleep(1000);
              }
           }
     }
  }

Функция CommonStopLoss()

Ключевая функция, которая отслеживает активацию отложенных ордеров и модифицирует Stop Loss рыночных ордеров. Каждый новый активированный рыночный ордер является самым дальним по отношению к стартовой цене пирамиды. Поэтому при его появлении Stop Loss всех предыдущих рыночных ордеров устанавливается на уровень Stop Loss нового ордера:

//+------------------------------------------------------------------+
//| Обновляет общий стоплосс рыночных ордеров пирамиды, то есть      |
//| устанавливает стоплоссы всех рыночных ордеров на один уровень    |
//+------------------------------------------------------------------+
void CommonStopLoss(state &Orders)
  {
   int type;
   bool refresh;
   double sl;

// из рыночных ордеров имеются только Buy, значит изначально была выставлена пирамида отложенных Buy Stop ордеров
   if(Orders.BuyCount>0 && Orders.SellCount==0)
     {
      type=OP_BUY;
      // стоплосс самого верхнего ордера Buy не совпадает со стоплосс самого нижнего, значит необходимо
      // установить всем ордерам стоплосс самого верхнего, иными словами, "подтянуть" в безубыток
      refresh=(NormalizeDouble(Orders.BuyTopSL,_Digits)!=NormalizeDouble(Orders.BuyBottomSL,_Digits));
      sl=NormalizeDouble(Orders.BuyTopSL,_Digits);
     }
   else
      // из рыночных ордеров имеются только Sell, значит изначально была выставлена пирамида отложенных Sell Stop ордеров
      if(Orders.BuyCount==0 && Orders.SellCount>0)
        {
         type=OP_SELL;
         // стоплосс самого верхнего ордера Sell не совпадает со стоплосс самого нижнего, значит необходимо
         // установить всем ордерам стоплосс самого нижнего, иными словами, "подтянуть" в безубыток
         refresh=(NormalizeDouble(Orders.SellTopSL,_Digits)!=NormalizeDouble(Orders.SellBottomSL,_Digits));
         sl=NormalizeDouble(Orders.SellBottomSL,_Digits);
        }
      else
         return;

   if(refresh)
      for(int i=OrdersTotal()-1; i>=0; i--)
         if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderMagicNumber()==Magic && OrderSymbol()==_Symbol && OrderType()==type)
           {
            if(NormalizeDouble(OrderStopLoss(),_Digits)!=sl)
              {
               for(int j=0; j<5; j++)
                  if(OrderModify(OrderTicket(),OrderOpenPrice(),sl,OrderTakeProfit(),0))
                     break;
                  else
                     Sleep(1000);
              }
           }
  }

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

Обработчик OnTick()

Завершая создание торгового эксперта, закодируем алгоритм вызова описанных блоков в соответствии с блок-схемой. Полученный код поместим внутрь обработчика OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   state State;

// получаем информацию об открытых советником ордерах и помещаем её в структуру State
   RefreshState(State);

// проверяем, сформировался ли новый бар
   if(IsNewBar())
     {
      // если сформирован новый бар, опрашиваем индикаторы с помощью вызова функции GetSignal()
      // и записываем результат в переменную TradeSignal
      int TradeSignal=GetSignal();

      // на открытии нового бара нет ни отложенных ни рыночных ордеров
      if(State.BuyCount==0 && State.SellCount==0 && State.BuyStopCount==0 && State.SellStopCount==0)
        {
         // если при этом сформирован торговый сигнал, строим пирамиду указанного типа
         if(TradeSignal!=EMPTY)
            BuildPyramid(TradeSignal);
         // выходим, так как дальнейшая обработка не имеет смысла, так как
         // либо никаких ордеров нет вовсе, либо мы только что построили пирамиду
         return;
        }

      // если с момента построения сетки было сформировано больше AfterSignaBarsCount баров,
      // или был сформирован противоположный сигнал, удаляем сетку
      if((State.BuyStopCount>0 && State.BuyCount==0 &&
          ((AfterSignaBarsCount>0 && State.BuyStopStart>0 && iTime(_Symbol,_Period,0)-State.BuyStopStart>=BarsInSeconds) || TradeSignal==OP_SELL))
         ||
         (State.SellStopStart>0 && State.SellCount==0 &&
          ((AfterSignaBarsCount>0 && State.SellStopStart>0 && iTime(_Symbol,_Period,0)-State.SellStopStart>=BarsInSeconds) || TradeSignal==OP_BUY)))
        {
         // удаляем отложенные ордера
         DeletePendingOrders();
         return;
        }
     }

// есть открытые ордера BUY или SELL, значит построена пирамида,
// и какие-то отложенные ордера сработали
   if(State.BuyCount>0 || State.SellCount>0)
     {
      // обновляем общий Stop Loss рыночных ордеров пирамиды, если требуется
      CommonStopLoss(State);
      return;
     }

// сетка ордеров закрылась по общему стоп-лоссу, удаляем оставшиеся отложки
   if((State.BuyStopCount>0 && State.BuyStopCount<OrdersAmount && State.BuyCount==0) ||
      (State.SellStopCount>0 && State.SellStopCount<OrdersAmount && State.SellCount==0))
     {
      // удаляем отложенные ордера
      DeletePendingOrders();
     }
  }

Тесты на истории

Чтобы проверить работоспособность нашего советника и дать оценку его торговому алгоритму, сделаем несколько тестов на исторических данных. Тестировать будем на паре EURUSD с качеством 99.9%. Период тестирования 01.01.2017 – 01.01.2020. Таймфрейм H1 (один час). Начальный депозит 10000 единиц.

Параметры по умолчанию: фиксированный лот 0.01, расстояние до сетки 100 п., шаг фиксированный 300 п., в сетке 15 ордеров, настройки стохастика стандартные, уровень перепроданности 20, отношение Stop Loss к шагу 1.0, ждать срабатывания сетки будем до появления обратного сигнала.

Cо значениями по умолчанию советник не блещет результатами, впрочем, и не сливает. Давайте немного «отодвинем» уровни перепроданности и перекупленности в настройках индикатора, установим параметр StochasticLevelDn на 10 вместо 20. Входы будут реже, но, возможно, точнее. Другие настройки трогать не будем.

На этот раз результаты заметно лучше, а ведь мы изменили только один параметр.

Посмотрим, как советник поведёт себя при увеличении лота в арифметической прогрессии. Для этого выберем в настройках режим «Арифметическая», параметру LotsMultiplicatorAr (разность арифметической прогрессии) присвоим значение 0.01. Можно оставить и ноль, в таком случае параметр автоматически приравнивается к начальному лоту, а он у нас равен 0.01. В итоге первый ордер пирамиды будет иметь объём 0.01, а последний откроется лотом 0.15. Получим следующую картину:

Теперь будем уменьшать лот в арифметической прогрессии от 0.15 до 0.01. Для этого параметру LotsMultiplicatorAr присвоим отрицательное значение -0.01, а размер стартового лота сделаем 0.15:

Исходя из особенностей трендовой торговли, вариант с уменьшением лота выглядит предпочтительнее, поскольку по мере затухания тренда мы уменьшаем и объём наших «доливок». Конечно, в этом случае прибыль ожидаемо больше, но пропорционально растёт и просадка.

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

Заключение

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

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

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

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

Скачать исходник советника из урока

Тема на форуме

С уважением, Юрий Лосев aka lsv107
Tlap.com

Уроки по MQL4 , , , , ,

Разворачиваем удобное окружение для программирования на MQL 4/5

Приветствую вас, MQL программисты!

Наверняка многим не очень нравится пользоваться стандартным metaeditor для программирования ваших советников, скриптов и индикаторов. Сегодня мы поговорим о том, как увеличить производительность и удобство при программировании.

Для работы мы установим phpstorm и специальный плагин для работы с mql, а также git консоль и пройдем регистрацию на bitbucket. Эти инструменты позволят нам существенно увеличить скорость и удобство разработки. Как это сделать быстро и эффективно – узнаем из нашего сегодняшнего урока!

Дополнительные инструменты для MQL4 программиста

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

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

Для установки phpstorm нужно перейти на сайт разработчика и скачать установочный файл для вашей операционной системы. Сама установка проста и занимает 5-10 минут, поэтому тут подробно описывать этот процесс нет смысла.

Установка плагина для phpstorm mql Idea

Открываем программу phpstorm. Жмем Новый проект и открываем папку mql4 вашего терминала.

Далее жмем Настройки -> Плагины:

В поиске ищем плагин mql idea, устанавливаем. Теперь синтаксис mql файлов будет пониматься программой phpstorm.

Регистрация на bitbucket

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

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

Далее придумайте имя репозиторию. При желании можно добавить описание:

Консоль git bash

Скачать консоль для git вы можете тут. Она доступна для любых операционных систем. Также рекомендую поставить ее и на ваши удаленные серверы. Установка проста и не вызывает никаких проблем. После установки у вас появятся такие значки:

В папке с терминалом вызываем консоль и пишем в ней git init. Затем уже в phpstorm вам нужно будет сделать первый коммит, а при создании репозитория у вас на экране должны появиться следующие команды для создания удаленного репозитория:

Заключение

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

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

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

Тема на форуме

С уважением, Дмитрий аkа Silentspec
Tlap.com

Уроки по MQL4, Уроки по MQL5 , , , , ,