Основы программирования на MQL5 - Глобальные переменные терминала

Denis Kirichenko | 3 ноября, 2014

Введение

В среде MQL4/5 есть интересный инструмент – глобальные переменные клиентского терминала. Он позволяет создавать некоторую общую область хранения данных для всех программ терминала. Кроме того, жизнь этой области не прекращается при закрытии терминала. В данной статье я предлагаю воспользоваться средствами ООП, чтобы "разобраться" с глобальными переменными терминала.

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


1. Глобальные переменные, функции

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

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

В учебнике по MQL4 есть раздел "Переменные GlobalVariables", где рассматривается теория.

В следующих разделах будем использовать средства ООП для выполнения поставленных задач.


2. Класс CGlobalVar

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

//+------------------------------------------------------------------+
//| Class CGlobalVar                                                 |
//+------------------------------------------------------------------+
class CGlobalVar : public CObject
  {
   //--- === Data members === --- 
private:
   string            m_name;
   double            m_value;
   //---
   datetime          m_create_time;
   datetime          m_last_time;
   //--- flag for temporary var
   bool              m_is_temp;

   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CGlobalVar(void);
   void              CGlobalVar(const string _var_name,const double _var_val,
                                const datetime _create_time);
   void             ~CGlobalVar(void){};
   //--- create/delete
   bool              Create(const string _var_name,const double _var_val=0.0,
                            const bool _is_temp=false);
   bool              Delete(void);
   //--- exist
   bool              IsGlobalVar(const string _var_name,bool _to_print=false);

   //--- set methods
   bool              Value(const double _var_val);
   bool              ValueOnCondition(const double _var_new_val,const double _var_check_val);

   //--- get methods
   string            Name(void) const;
   datetime          CreateTime(void) const;
   datetime          LastTime(void);
   template<typename T>
   T                 GetValue(T _type) const;
   bool              IsTemporary(void) const;
   //---
private:
   string            FormName(const string _base_name,const bool _is_temp=false);
  };

Что должен в себя включать класс? Для минимального перечня атрибутов я бы выбрал такие свойства:

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

Несколько слов стоит сказать о методе CGlobalVar::GetValue. Это шаблонный метод. Он будет возвращать тот тип данных для значения переменной, который пользователь задает в качестве аргумента.

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

Создадим тестовый скрипт Globals_test1.mq5, в котором поработаем с объектами типа CGlobalVar.

#include "CGlobalVar.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CGlobalVar gVar1;
//--- create a temporary global var
   if(gVar1.Create("Gvar1",3.123456789101235,true))
     {
      Print("\n---=== A new global var ===---");
      PrintFormat("Name: \"%s\"",gVar1.Name());
      PrintFormat("Is temporary: %d",gVar1.IsTemporary());

      //--- Get the value 
      //--- double type
      double d=0.0;
      double dRes=gVar1.GetValue(d);
      PrintFormat("Double value: %0.15f",dRes);
      //--- float type
      float f=0.0;
      float fRes=gVar1.GetValue(f);
      PrintFormat("Float value: %0.7f",fRes);
      //--- string type
      string s=NULL;
      string sRes=gVar1.GetValue(s);
      PrintFormat("String value: %s",sRes);

      //--- Set a new value 
      double new_val=3.191;
      if(gVar1.Value(new_val))
         PrintFormat("New value is set: %f",new_val);

      //--- Set a new value on condition
      new_val=3.18;
      if(gVar1.ValueOnCondition(3.18,3.191))
         PrintFormat("New value on conditionis set: %f",new_val);
     }
  }

Создается глобальная переменная следующим образом:

gVar1.Create("Gvar1",3.123456789101235,true)

Первым аргументом выступает базовая часть имени будущей переменной ("Gvar1"), вторым - значение (3.123456789101235), третьим - признак того, что переменная будет временной (true).

Имя для переменной создается таким образом: к базовой части добавляются имя программы и тип программы.

В моем случае это:

  1. Gvar1 - базовая часть;
  2. prog_Globals_test1 - программа, где была создана переменная (по имени Globals_test1);
  3. тип программы - scr (скрипт).

В списке глобальных переменных, после нажатия клавиши F3 в окне терминала MetaTrader 5, должна появиться такая запись:

Рис.1 Значение переменной Test_temp_var1_prog_Globals_test1_scr равно 3.18

Рис.1. Значение переменной Test_temp_var1_prog_Globals_test1_scr равно 3.18

При его запуске и удачном выполнении в журнале "Эксперты" появятся следующие строки:

KP      0       10:20:20.736    Globals_test1 (AUDUSD.e,H1)     ---=== A new global var ===---
EH      0       10:20:21.095    Globals_test1 (AUDUSD.e,H1)     Name: "Gvar1_temp_prog_Globals_test1_scr"
LF      0       10:20:21.876    Globals_test1 (AUDUSD.e,H1)     Is temporary: 1
MO      0       10:20:31.470    Globals_test1 (AUDUSD.e,H1)     Double value: 3.123456789101235
KG      0       10:20:31.470    Globals_test1 (AUDUSD.e,H1)     Float value: 3.1234567
OP      0       10:20:31.470    Globals_test1 (AUDUSD.e,H1)     String value: 3.123456789101235
RH      0       10:20:31.470    Globals_test1 (AUDUSD.e,H1)     New value is set: 3.191000
DJ      0       10:20:31.470    Globals_test1 (AUDUSD.e,H1)     New value on conditionis set: 3.180000

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

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

В MQL4/5 при получении данных о глобальной переменной нельзя узнать, является ли переменная временной или нет. Наверное, самым простым способом идентификации временной переменной может стать какой-то ключ в имени самой переменной. Можно добавлять, например, суффикс "temp" в имя переменной. Недостаток такого подхода - необходимость контролировать имя глобальной переменной при создании. Особенно если такие переменные создаются другими программами, не использующими класс CGlobalVar.

Мне стало интересно, сколько и как быстро можно создать глобальные переменные?

Я изменил немного предыдущую версию скрипта и с разным числом проходов запустил его на выполнение, Globals_test2.mq5. Да, после каждой попытки перезапускал терминал, чтобы удалить переменные.

#property script_show_inputs
//---
#include "CGlobalVar.mqh"

input uint InpCnt=10000; // Число переменных
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- start value
   uint start=GetTickCount();
//---
   for(uint idx=0;idx<InpCnt;idx++)
     {
      CGlobalVar gVar;
      //--- Create a temporary global var
      if(!gVar.Create("Test_var"+IntegerToString(idx+1),idx+0.15,true))
         Alert("Ошибка создания глобальной переменной!");
     }
//--- finish value
   uint time=GetTickCount()-start;
//--- to print
   PrintFormat("Создание %d глобальных переменных заняло %d мсек",InpCnt,time);
  }

Вот что получилось (рис.2).

Рис.2 Временные затраты на создание временных глобальных переменных

Рис.2. Временные затраты на создание временных глобальных переменных

Результаты аналогичного теста для полноценных глобальных переменных представлены на рис.3. На их создание уходит немногим больше времени.

Так происходит потому, что эти переменные сохраняются на диск в файл gvariables.dat, располагающийся в папке Profiles.

Рис.3 Временные затраты на создание полноценных глобальных переменных

Рис.3. Временные затраты на создание полноценных глобальных переменных

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

В следующем разделе поработаем с совокупностью глобальных переменных.


3. Класс CGlobalVarList

Для того, чтобы упорядочить работу с глобальными переменными, создадим списочный класс глобальных переменных типа CGlobalVarList. Данный вид списка будет являться потомком стандартного списочного класса CList.

Определение класса может быть представлено так:

//+------------------------------------------------------------------+
//| Class CGlobalVarList                                             |
//+------------------------------------------------------------------+
class CGlobalVarList : public CList
  {
   //--- === Data members === --- 
private:
   ENUM_GVARS_TYPE   m_gvars_type;

   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CGlobalVarList(void);
   void             ~CGlobalVarList(void){};
   //--- load/unload
   bool              LoadCurrentGlobals(void);
   bool              KillCurrentGlobals(void);
   //--- working with files
   virtual bool      Save(const int _file_ha);
   virtual bool      Load(const int _file_ha);
   //--- service
   void              Print(const int _digs);
   void              SetGvarType(const ENUM_GVARS_TYPE _gvar_type);
   //---
private:
   bool              CheckGlobalVar(const string _var_name);
  };

Если нужно включить в список типа CGlobalVarList объекты, связанные с текущими глобальными переменными, то используется метод CGlobalVarList::LoadCurrentGlobals.

//+------------------------------------------------------------------+
//| Load current global vars                                         |
//+------------------------------------------------------------------+
bool CGlobalVarList::LoadCurrentGlobals(void)
  {
   ENUM_GVARS_TYPE curr_gvar_type=this.m_gvars_type;
   int gvars_cnt=GlobalVariablesTotal();
//---
   for(int idx=0;idx<gvars_cnt;idx++)
     {
      string gvar_name=GlobalVariableName(idx);
      if(this.CheckGlobalVar(gvar_name))
         continue;

      //--- gvar properties
      double gvar_val=GlobalVariableGet(gvar_name);
      datetime gvar_time=GlobalVariableTime(gvar_name);
      CGlobalVar *ptr_gvar=new CGlobalVar(gvar_name,gvar_val,gvar_time);
      //--- control gvar type 
      if(CheckPointer(ptr_gvar)==POINTER_DYNAMIC)
        {
         if(curr_gvar_type>GVARS_TYPE_ALL)
           {
            bool is_temp=ptr_gvar.IsTemporary();
            //--- only full-fledged
            if(curr_gvar_type==GVARS_TYPE_FULL)
              {if(is_temp)continue;}
            //--- only temporary
            else if(curr_gvar_type==GVARS_TYPE_TEMP)
              {if(!is_temp)continue;}
           }
         //--- try to add
         if(this.Add(ptr_gvar)>-1)
            continue;
        }
      //---
      return false;
     }
//---
   return true;
  }

Данный метод считывает все имеющиеся глобальные переменные и включает их в состав списка.

Атрибут m_gvars_type контролирует вид включаемой глобальной переменной. Он является перечислением типа ENUM_GVARS_TYPE:

//+------------------------------------------------------------------+
//| Enumeration for gvars type                                       |
//+------------------------------------------------------------------+
enum ENUM_GVARS_TYPE
  {
   GVARS_TYPE_ALL=-1,  // все глобальные
   GVARS_TYPE_FULL=0,  // только полноценные
   GVARS_TYPE_TEMP=1,  // только временные
  };

Допустим, что до инициализации списка CGlobalVarList имелся такой ряд глобальных переменных (рис.4)

Рис.4 Примерный перечень глобальных переменных

Рис.4. Примерный перечень глобальных переменных

Посмотрим, обработается ли этот перечень списком корректно. Для проверки создадим тестовый скрипт Globals_test3.mq5.

#include "CGlobalVarList.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CGlobalVarList gvarList;
   gvarList.LoadCurrentGlobals();   
   PrintFormat("Число переменных в списке: %d",gvarList.Total());
  }

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

Рис.5 Новый перечень глобальных переменных

Рис.5. Новый перечень глобальных переменных

И принт распечатал строку так:

2014.10.21 11:35:00.839       Globals_test3 (AUDUSD.e,H1)              Число переменных в списке: 10

Дело в том, что в определении метода CGlobalVarList::LoadCurrentGlobals есть обращение к методу CGlobalVar::Create.

Т.е. создается новая глобальная переменная на строке:

if(ptr_gvar.Create(gvar_name,gvar_val))

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

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

После изменения метод CGlobalVarList::LoadCurrentGlobals выглядит следующим образом:

//+------------------------------------------------------------------+
//| Load current global vars                                         |
//+------------------------------------------------------------------+
bool CGlobalVarList::LoadCurrentGlobals(void)
  {
   int gvars_cnt=GlobalVariablesTotal();
//---
   for(int idx=0;idx<gvars_cnt;idx++)
     {
      string gvar_name=GlobalVariableName(idx);
      double gvar_val=GlobalVariableGet(gvar_name);
      datetime gvar_time=GlobalVariableTime(gvar_name);
      CGlobalVar *ptr_gvar=new CGlobalVar(gvar_name,gvar_val,gvar_time);
      if(CheckPointer(ptr_gvar)==POINTER_DYNAMIC)
         if(this.Add(ptr_gvar)>-1)
            continue;
      //---
      return false;
     }
//---
   return true;
  }

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

2014.10.21 11:38:04.424      Globals_test3 (AUDUSD.e,H1)              Число переменных в списке: 6

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

Теперь скрипт Globals_test3.mq5 выглядит следующим образом:

//---
#include "CGlobalVarList.mqh"
//---
input ENUM_GVARS_TYPE InpGvarType=GVARS_TYPE_FULL; // Set gvar type
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CGlobalVarList gvarList;
//--- delete gvars
   gvarList.SetGvarType(InpGvarType);
//--- load current gvars  
   gvarList.LoadCurrentGlobals();
   Print("Печать списка до удаления.");
   gvarList.Print(10);
//--- delete gvars
   if(gvarList.KillCurrentGlobals())
     {
      Print("Печать списка после удаления.");
      gvarList.Print(10);
     }
  }

Немного усложним задачу. Создадим 10 разнотипных глобальных переменных (рис.6).

Рис.6 Разнотипные глобальные переменные

Рис.6. Разнотипные глобальные переменные

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

В журнале "Эксперты" получим такую картинку:

MG      0       11:05:01.113    Globals_test3 (AUDUSD.e,H1)     Печать списка до удаления.
KL      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     
OI      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     ---===Локальный список===---
QS      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Тип глобальных переменных: GVARS_TYPE_FULL
RI      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Всего глобальных переменных: 10
EG      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Глобальных переменных в текущем списке: 5
RN      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Gvar #1, имя - gVar10_prog_test1_scr, значение - 16.6400000000
KP      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Gvar #2, имя - gVar2_prog_test1_scr, значение - 4.6400000000
GR      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Gvar #3, имя - gVar4_prog_test1_scr, значение - 7.6400000000
RD      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Gvar #4, имя - gVar6_prog_test1_scr, значение - 10.6400000000
LJ      0       11:05:01.613    Globals_test3 (AUDUSD.e,H1)     Gvar #5, имя - gVar8_prog_test1_scr, значение - 13.6400000000
EH      0       11:06:18.675    Globals_test3 (AUDUSD.e,H1)     Печать списка после удаления.
FS      0       11:06:19.003    Globals_test3 (AUDUSD.e,H1)     
JJ      0       11:06:19.003    Globals_test3 (AUDUSD.e,H1)     ---===Локальный список===---
HN      0       11:06:19.003    Globals_test3 (AUDUSD.e,H1)     Тип глобальных переменных: GVARS_TYPE_FULL
KH      0       11:06:19.003    Globals_test3 (AUDUSD.e,H1)     Всего глобальных переменных: 5
QP      0       11:06:19.003    Globals_test3 (AUDUSD.e,H1)     Глобальных переменных в текущем списке: 0

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

Затем он был опустошен, и остались 5 временных переменных в терминале (рис.7).

Рис.7 Временные глобальные переменные

Рис.7. Временные глобальные переменные

Т.е. задуманное было выполнено.

В классе CGlobalVarList еще реализованы методы для сохранения данных в файл и загрузки данных из файла.


4. Практический смысл

Как известно, MQL4/5 – это специализированный язык программирования. Он создан для программирования торговых стратегий. Поэтому любой механизм в языке стоит рассматривать как средство для формализации какой-то конкретной трейдерской идеи.

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

Допустим, что есть следующий код торгового робота "Globals_test_EA", использующий модульный подход:

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

где главный модуль выглядит так:

//+------------------------------------------------------------------+
//| Main module                                                      |
//+------------------------------------------------------------------+
void Main(void)
  {
//--- установить флаги для всех модулей
   for(int idx=0;idx<GVARS_LIST_SIZE;idx++)
      SetFlag(idx,false);

//--- Проверка на возможность торговли и наличия связи
//--- разрешение на торговлю
   if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
      //--- подключение к торговому серверу
      if(TerminalInfoInteger(TERMINAL_CONNECTED))
         //--- разрешение на торговлю для запущенного советника
         if(MQLInfoInteger(MQL_TRADE_ALLOWED))
           {
            //--- 1) модуль открытия
            Open();
            //--- 2) модуль закрытия
            Close();
            //--- 3) модуль трала
            Trail();
           }
  }

Т.е. главный включает в себя 3 составляющих:

  1. модуль открытия;
  2. модуль закрытия;
  3. модуль трала.

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

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

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

В итоге нужно 6 переменных с именами:

//--- глобальные переменные: имена
string gVar_names[6]=
  {
   "gvarOpen_start","gvarOpen_finish",
   "gvarClose_start","gvarClose_finish",
   "gvarTrail_start","gvarTrail_finish"
  };

Флаги для всех модулей устанавливаются в начале функции Main(), а сбрасываются в каждом отдельном модуле. И естественно, что только "свои". Например, обратимся к модулю Open():

//+------------------------------------------------------------------+
//| Open module                                                      |
//+------------------------------------------------------------------+
void Open(void)
  {
   Comment(curr_module+__FUNCTION__);
//---
   if(!IsStopped())
     {
      //--- сбросить флаг старта модуля
      SetFlag(0,true);

      //--- допустим, модуль работает около 1.25 сек
        {
         Sleep(1250);
        }
      //--- сбросить флаг финиша модуля
      SetFlag(1,true);
     }
  }

При выполнении модуля в окне графика появится комментарий, что программа работает в блоке Open().

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

Схематично процесс отслеживания этапов работы модулей посредством глобальных переменных представлен на рис.8.

Рис.8 Диаграмма последовательностей для обработки флагов

Рис.8. Диаграмма последовательностей для обработки флагов

К примеру, советник "Globals_test_EA" находится на графике и работает в обычном режиме.

Я удалил его с графика и получил такую запись в журнале:

2014.10.22 20:14:29.575 Globals_test_EA (EURUSD.e,H1)   Программа принудительно завершена до выполнения: <<Open_finish>>

Таким образом, прерывание работы советника имело место в модуле Open().

Откроем глобальные переменные посредством клавиши F3 (рис.9).

Рис.9 Глобальные переменные для советника "Globals_test_EA"

Рис.9. Глобальные переменные для советника "Globals_test_EA"

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

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

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

RQ      0       20:28:25.135    Globals_test_EA (EURUSD.e,H1)   Ненулевое значение для: <<Open_finish>>
CL      0       20:28:25.135    Globals_test_EA (EURUSD.e,H1)   Ненулевое значение для: <<Close_start>>
DH      0       20:28:25.135    Globals_test_EA (EURUSD.e,H1)   Ненулевое значение для: <<Close_finish>>
ES      0       20:28:25.135    Globals_test_EA (EURUSD.e,H1)   Ненулевое значение для: <<Trail_start>>
RS      0       20:28:25.135    Globals_test_EA (EURUSD.e,H1)   Ненулевое значение для: <<Trail_finish>>

Таким образом, получим предупреждение о непрохождении этапов работы программы. Что делать, если они не были пройдены? Это уже вопрос второй.


Заключение

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

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

Как всегда приветствую замечания, предложения и конструктивную критику.