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 , , , ,