Четыре способа пометить ордер или позицию в МТ4

Автор: | 08.08.2018

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

В данной статье будет рассмотрено четыре способа маркировки ордеров и позиций:

  1. Комментарий ордера.
  2. Глобальные переменные.
  3. Файлы.
  4. Кодирование магика.

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

Комментарий ордера

При вызове функции OrderSend(), открывающей позицию, в нее можно передать строковый параметр, определяющий комментарий ордера. Текст комментария формируется перед вызовом функции. Следовательно, открытие позиции и её маркировка выполняется одним действием. Если позиция оказалась открытой, имеется абсолютная гарантия, что она имеет комментарий, то есть нужную нам маркировку.

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

31 символ – это незначительное количество, но более чем достаточное при создании экспертов. Обычно необходимо всего лишь одно поле для индексации позиции в последовательности, обычно это число от 0 до примерно 10-ти, по крайней мере, не более 100, то есть всего два знака. Иногда требуется еще одно поле для маркировки группы позиций, для значения этого поля удобно пользоваться числовым выражением времени, а это не более 11-ти знаков. В итоге получается 16 знаков: 2 знака одно поле, 11 – второе и разделители: в начале, между полями и в конце. Разделители в начале и конце будут полезны для контроля целостности комментария.

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

Из всех комментариев добавляемых сервером, было замечено сообщение о закрытии позиции по столпосс – комментарий «[sl]» или тейкпрфит – комментарий «[tp]», то есть всего 4-ре знака. Значит, если, как было написано выше, наш комментарий занимает не более 16-ти знаков, комментарий добавляемый сервером не повредит имеющийся комментарий ордера.

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

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

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

Использование комментариев является и самым простым способом с точки зрения программной реализации. Формирование комментария выполняется налету прямо при вызове функции OrderSend(). Допустим, индекс ордера находится в переменной Index имеющий тип int, а идентификатор группы ордеров в переменной ID имеющей тип datetime, в качестве разделителя используется знак «=». Формируем комментарий:

“=”+(string)Index+”=”+(string)(long)ID+”=”

Обратите внимание, переменная ID сначала приводится к типу long, а затем к типу sring, в этом случае значение переменной преобразуется в число, а не в строковое выражение даты-времени, занимающее больший размер, чем число.

Для извлечения данных из комментария может использоваться следующая функция (если в комментарии имеется одно после данных, или же два, но нужно только первое):

bool OrderIndex1(string comment,double & value1,string separator="="){
   if(StringFind(comment,separator,0)!=0)return(false);
   int pe=StringFind(comment,separator,1);
   if(pe<=1)return(false);
   value1=(double)StringSubstr(comment,1,pe-1);
   return(true);
} 

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

Если необходимо извлечь сразу два поля, можно применять следующую функцию:

bool OrderIndex2(string comment,double & value1,double & value2,string separator="="){
   if(StringFind(comment,separator,0)!=0)return(false);
   int pe=StringFind(comment,separator,1);
   if(pe<=1)return(false);
   value1=(double)StringSubstr(comment,1,pe-1);
   int pe2=StringFind(comment,separator,pe+1);
   if(pe2<=pe)return(false); 
   value2=(double)StringSubstr(comment,pe+1,pe2-pe-1);   
   return(true);
} 

Глобальные переменные

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

  1. Глобальные переменные сохраняются терминалом на диске, следовательно, работающий эксперт не может быть перенесен на другой компьютер. Для переноса эксперта необходимо дождаться завершения им цикла своей работы (что бы в рынке не было открытых позиций и ордеров).
  2. Данные присваиваются позиции после ее открытия. То есть, имеется, хоть и мизерная, но вероятность того, что позиция, в случае программного сбоя, останется без маркировки.
  3. Имеется вероятность, так же очень незначительная, что при аварийном завершении работы терминала, он не сохранит глобальные переменные. В данном случае, имеется ввиду внезапное прекращение электропитания компьютера, что в наше время практически нереально. Терминалы работают на ноутбуках, которые, в случае разрядки аккумулятора выполняют корректное завершение работы системы, или же работают на выделенных серверах, где внезапное отключение электроэнергии вообще событие нереальное. Кроме того, в языке имеется функция для форсированного сохранения глобальных переменных на диске, при ее применении сохранение переменных не зависит от терминала.

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

class CGVOrderInfo{
   private:
      string m_pref;
   public:
      void CGVOrderInfo(){   
         m_pref="";
      }
      void ~CGVOrderInfo(){
         if(IsTesting() && m_pref!=""){
            GlobalVariablesDeleteAll(m_pref);
         }      
      }
      void Init(string symbol,int magic){
         m_pref=WindowExpertName()+"_"+symbol+"_"+(string)magic;
         if(IsDemo())m_pref=m_pref+"_d";
         if(IsTesting()){
            m_pref=m_pref+"_t";
            GlobalVariablesDeleteAll(m_pref);
         }
      }
      void SetField(int ticket,int field,double value){
GlobalVariableSet(m_pref+"/"+(string)ticket+"_"+(string)field,value);
      }
      void GetField(int ticket,int field,double & value){
value=GlobalVariableGet(m_pref+"/"+(string)ticket+"_"+(string)field);
      }       
};

Перед использованием класса необходимо создать объект:

CGVOrderInfo gv;

В функции OnInit() выполнить инициализацию объекта передав ему символ и магик эксперта:

gv.Init(Symbol(),123);

Сразу после открытия позиции или установки ордера становится известен его тикет, используя метод SetField() привязываем к позиции необходимые данные:

gv.SetField(12345,0,1);

Первый параметр – тикет, второй – индекс поля (любое целое число ), третий – значение типа double.

Затем, располагая тикетом позиции, используя метод GetField(), извлекаем данные:

double v;
gv.GetField(12345,0,v);

Первый параметр – тикет, второй – индекс поля (любое целое число ), третий – значение типа double возвращаемое по ссылке.

Для форсированного сброса глобальных переменных на диск применяется стандартная функция GlobalVariablesFlush().

Файлы

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

  1. Работающий эксперт не может быть перенесен на другой компьютер. Для переноса эксперта необходимо дождаться завершения им цикла своей работы (что бы в рынке не было открытых позиций и ордеров).
  2. Данные присваиваются позиции после ее открытия. То есть, имеется, хоть и мизерная, но вероятность того, что позиция, в случае программного сбоя, останется без маркировки.
  3. В отличие от глобальных переменных, файл сразу сохраняется на диск, что с одной стороны может повышать надежность применения файлов, но с другой стороны – снижать быстродействие эксперта. При тестировании эксперта в тестере, глобальные переменные сохраняются и считываются из виртуальной памяти, а вот при использовании файлов, они будут реально сохраняться на диск и читаться с него, что, конечно, снизит скорость тестирования.
  4. Файлы с данными хранятся открыто на диске, что повышает опасность их потери по неосторожности.

Использование файлов для маркировки ордеров реализовано в виде класса:

class CFileOrderInfo{
   private:
      bool m_init;
      string m_path;
   public:
      void CFileOrderInfo(){
         m_init=false;
      }
      void ~CFileOrderInfo(){
         if(IsTesting() && m_init){
            FolderClean(m_path);
         }      
      }
      bool Init(string symbol,int magic){
         string m_base="FilePosInfo";
         if(!FolderCreate(m_base)){
            return(false);
         }      
         m_path=m_base+"/"+WindowExpertName()+"_"+
         symbol+"_"+(string)magic;
         if(IsDemo())m_path=m_path+"_d";
         if(IsTesting())m_path=m_path+"_t";
         if(!FolderCreate(m_path)){
            return(false);
         }                   
         if(IsTesting()){
            if(!FolderClean(m_path)){
               return(false);
            }
         }
         m_init=true;
         return(true);
      }
      bool SetField(int ticket,int field,string value){
         int h=FileOpen(m_path+"/"+(string)ticket+"_"+
         (string)field+".txt",FILE_TXT|FILE_WRITE);
         if(h==-1)return(false);
         FileWriteString(h,value);
         FileClose(h);
         return(true);
      }
      bool GetField(int ticket,int field,string & value){
         int h=FileOpen(m_path+"/"+(string)ticket+"_"+
         (string)field+".txt",FILE_TXT|FILE_READ);
         if(h==-1)return(false);
         value=FileReadString(h);
         FileClose(h);
         return(true);
      }
};

Для использования класса создается объект:

CFileOrderInfo foi;

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

if(!foi.Init(Symbol(),123)){
   return(INIT_FAILED);
}

При инициализации выполняется создание общей папки (имя: FilePosInfo), а в ней папки соответствующей данному эксперту (формируется из имени эксперта, символа и магика). Если папку создать не удалось, метод Init() возвращает false и при этом завершается работа эксперта.

После открытия позиции используя метод SetField() сохраняем данные:

bool check=foi.SetField(123,1,(string)25));

Параметры метода SetField(): первый – тикет, второй – индекс поля (целое число), третий поле – значение. Для расширения возможностей класса, третий параметр имеет строковый тип, поэтому, если нужно сохранить числовое значение, он преобразуется в стрококу.
В зависимости от успешности/неуспешности сохранения данных функция возвращает true или false.

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

Получение данных:

string v;   
if(!foi.GetField(123,1,v)){
   return;
}

Параметры метода GetField(): первый параметр – тикет, в торой – индекс поля, третий – значение возвращаемое по ссылке. При удачном чтении файла функция возвращает true, при ошибке чтения – false. Кстати, это можно отнести к существенным недостаткам использования файлов в отличие от глобальных переменных. Если при чтении данных еще можно завершить функцию OnTick() и повторить попытку на следующем тике, то при сохранении такой возможности нет, надо входить в цикл или как-то по-другому обеспечивать повторение попытки сохранения.

Кодирование магика

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

  1. Отсутствие наглядности. В окне свойств советника вводится одно значение магика, но после добавления к нему дополнительных данных, его видимое значение меняется, что делает непонятным к какому эксперту относится данный магик. Если в терминале работают эксперты от разных разработчиков, есть вероятность совпадения их магиков. Конечно, в предлагаемом далее программном решении приняты соответствующие меры, но все равно, начинать применение экспертов разных разработчиков нужно внимательно и с пониманием того, как формируются их магики.
  2. Ограниченный диапазон свободных значений магика. В предлагаем ниже решении допустимый диапазон магиков (вводимых в окно свойств) от 1 до 9999. В таком же диапазоне должны быть магики и у других советников, не использующих кодирование магика. Если же параллельно используется советник с кодированием магика от другого разработчика, нужно разбираться с принципом его кодирования, что бы быть уверенным что магики не совпадут ни в каком случае.
  3. Сохраняемые значения могут быть только целочисленными и их количество и величина очень ограничены.

Для магика в терминале MmetaTrader4 используется переменная типа int, максимальное число состоящее из цифр 9, которое можно вместить в эту переменную, это 999999999 – девять знаков. Если для магика вводимого в окно свойств выделить четыре знака, у нас останется еще пять знаков, их можно разделить на два поля: из трех знаков и из двух знаков. Приводимый ниже класс универсален, он позволяет разбивать магик на произвольное количество полей различного размера, вплоть до девяти полей размеров в один знак. Наиболее оптимальным кажется разбиение на три поля размером 4, 3 и 2 знака.

Код класса:

class CMagicEncodingDecoding{
   private:
      int m_len[];
      int m_sz;
      int m_d[];  
      int m_r[];  
      int m_f[];        
      int m_m[];      
   public:
      void CMagicEncodingDecoding(string map="432"){
         m_sz=StringLen(map);
         ArrayResize(m_len,m_sz);
         ArrayResize(m_d,m_sz);         
         ArrayResize(m_r,m_sz); 
         ArrayResize(m_f,m_sz);   
         ArrayResize(m_m,m_sz);                          
         int m_ch=0;
         for(int i=0;i<m_sz;i++){ 
            m_len[i]=(int)StringSubstr(map,i,1); 
            m_ch+=m_len[i]; 
            m_d[i]=(int)MathPow(10,m_len[i]); 
            if(i==1){ 
               m_m[i]=m_d[0]; 
            }else if(i>1){
               m_m[i]=m_m[i-1]*m_d[i-1];  
            }
         }
         if(m_ch>9){
            Print("Wrong parameter for CMagicCript(). Sum of digits must not be more 9");
         }
      }
      int ExtractMagicOnly(int magic){
         return(magic%m_d[0]);
      }
      void SetEncodedMagic(int magic){
         for(int i=0;i<m_sz;i++){
            m_r[i]=magic%m_d[i]; 
            magic/=m_d[i];  
         }
      }
      int GetField(int index){
         return(m_r[index]);
      }
      void SetField(int index,int value){
         m_f[index]=value;
      }
      int EncodedFields(){
         int m_rm=m_f[0];
         for(int i=1;i<m_sz;i++){
            m_rm+=m_f[i]*m_m[i];
         }
         return(m_rm);
      }
};

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

Значит создание объекта:

CMagicEncodingDecoding mag;

Или так:

CMagicEncodingDecoding mag("432");

Важно следить, что бы сумма цифр передаваемых в конструктор не превышала 9.
Перед открытие позиции кодируем магик. Для этого, используя метод SetField(), устанавливаем полям нужные значения. Первый параметр метода – индекс поля, второй – значение. Поле 0 – для магика вводимого через окно свойств, остальные поля – по вашему усмотрению:

mag.SetField(0,Magic);
mag.SetField(1,567);
mag.SetField(2,89);  

После установки полей получаем значение магика, передаваемого в функцию OrderSend():

int magicVal=mag.EncodedFields();

Метод ExtractMagicOnly() используется только для извлечения поля 0 из закодированного значения. Это метод пригодиться для общих функция советника, например, для функции закрытия всех позиций по суммарной прибыли.

Для извлечения всех полей используется два метода. Сначала объекту передается закодированной магик, для этого используется метод SetEncodedMagic(). В этом методе происходит полное раскодирование всех полей. После этого, для получения значения какого-то конкретного поля, используется метод GetField(). В метод передается индекс поля:

int x;   
x=999999999;
mag.SetEncodedMagic(x);
Alert("1: ",mag.GetField(0)," ",mag.GetField(1)," ",mag.GetField(1));

Таблица сравнительного анализа

Комментарии Глобальные переменные Файлы Магик
Открытие и маркировка за одно действие + +
Независимость от вмешательства брокера + + +
Гарантированное считывание без ошибки + + +
Наглядность * ++
Переносимость эксперта + +
Простота кода +
Отсутствие ограничений на тип и размер данных +
Простота в эксплуатации + + +
Сохраняемость данных ** + +

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

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

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

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

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

Скачать:

  • CommentInfo – скрипт с функциями извлечения данных из комментарий и примером их применения.
  • CGVOrderInfo – скрипт с классом для работы с глобальными переменным и примером его применения.
  • CFileOrderInfo – скрипт с классом для работы с файлами и примером его применения.
  • CMagicEncodingDecoding – скрипт с классом для кодирования/раскодирования значений магика и пример его использования.