Ордерa, позиции и сделки в MetaTrader 5

MetaQuotes | 5 января, 2011


Торговые термины

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

Важно: действующие ордера и позиции всегда отображаются на вкладке "Торговля", а сделки и ордера из истории всегда отражаются в закладке "История". Не следует путать действующие ордера из закладки "Торговля" и исторические ордера из закладки "История".


Как терминал получает и хранит торговую информацию с сервера

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

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

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

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

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

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

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

Общая схема взаимодействия терминала и торгового сервера MetaTrader 5 представлена на рисунке.


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

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


Доступ к торговой истории из MQL5-программы

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

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

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


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

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

Возможные последствия при неправильном использовании кэша:


Функции для работы с кэшем

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

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

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

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


Торговые функции можно разделить на две категории: функции заполнения кэша и функции получения информации из кэша.


Функции заполнения кэша

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

Функция заполнения кэша торговли (действующие ордера и позиции):


Функция заполнения кэша истории:

Отдельно стоит рассмотреть две функции, которые затрагивают доступную в кэше торговую историю в целом:


OrderSelect и OrderGetTicket

Обе функции OrderSelect(ticket) и OrderGetTicket() действуют одинаково - они заполняют кэш действующих ордеров одним единственным ордером. OrderSelect(ticket) предназначена для случая, когда тикет ордера известен заранее. OrderGetTicket() в сочетании с OrdersTotal() позволяет осуществить перебор всех имеющихся в базе терминала ордеров.

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


PositionSelect и PositionGetSymbol

Как и для ордеров, эти две функции действуют также одинаково для позиций - они заполняют кэш позиций одной единственной позицией. PositionGetSymbol(index) требует в качестве параметра номер в списке позиций базы терминала, а PositionSelect(symbol)  заполняет кэш по имени символа, на котором открыта позиция. Имя символа, в свою очередь, можно получить функцией PositionGetSymbol(index).

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


HistoryOrderSelect

HistoryOrderSelect(ticket) выбирает в кэш исторический ордер из базы терминала по его тикету. Функция предназначена для использования в случае, когда заранее известен тикет нужного ордера.

При успешном выполнении кэш будет содержать один единственный ордер и функция HistoryOrdersTotal() вернет единицу. В противном случае кэш исторических ордеров будет пуст и функция HistoryOrdersTotal() вернет ноль.

HistoryDealSelect

HistoryDealSelect(ticket) выбирает в кэш сделку из базы терминала по ее тикету. Функция предназначена для использования в случае, когда заранее известен тикет сделки.

При успешном выполнении кэш будет содержать одну сделку и функция HistoryDealsTotal() вернет единицу. В противном случае кэш сделок будет пуст и функция HistoryDealsTotal() вернет ноль.


Функции получения информации из кэша

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

Функции получения тикета из кеша истории

HistoryOrderGetTicket(index) возвращает тикет исторического ордера по его номеру из кэша исторических ордеров (не из базы терминала!). Полученный тикет можно использовать в функции HistoryOrderSelect(ticket), которая очистит кеш и заново заполнит его только одним ордером в случае успеха. Напомним, что возвращаемое из  HistoryOrdersTotal() значение зависит от количества ордеров в кеше.

HistoryDealGetTicket(index) возвращает тикет сделки по ее номеру из кэша сделок. Тикет сделки можно использовать в функции HistoryDealSelect(ticket), которая очистит кеш и заново заполнит кеш только одной сделкой в случае успеха. Возвращаемое функцией HistoryDealsTotal() значение зависит от количества сделок в кеше.

Важно: перед вызовом функций HistoryOrderGetTicket(index) и HistoryDealGetTicket(index) заполните кеш истории историческими ордерами и сделками в достаточном объеме. Используйте для этого одну из функций: HistorySelect(start, end), HistorySelectByPosition(position_ID), HistoryOrderSelect(ticket) и HistoryDealSelect(ticket).


Получение информации по действующим ордерам

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

bool selected=OrderSelect(ticket);
if(selected)
  {
   double price_open=OrderGetDouble(ORDER_PRICE_OPEN);
   datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
   string symbol=OrderGetString(ORDER_SYMBOL);
   PrintFormat("Ордер #%d по %s был выставлен %s",ticket,symbol,TimeToString(time_setup));
  }
else
  {
   PrintFormat("Не удалось выбрать ордер с тикетом %d. Ошибка %d",ticket, GetLastError());
  }

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

Общий алгоритм работы с ордерами (со сделками и позициями то же самое) следующий:

  1. Получить общее количество ордеров с помощью функции OrdersTotal();
  2. Организовать цикл по перебору всех ордеров по их номерам в списке;
  3. Скопировать поочередно каждый ордер в кэш с помощью функции OrderGetTicket();
  4. Получить нужные данные ордера из кэша с помощью функций OrderGetDouble(), OrderGetInteger() и OrderGetString(). При необходимости проанализировать полученные данные и выполнить необходимые действия.
Вот краткий пример подобного алгоритма:
input long my_magic=555;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- получим общее количество ордеров
   int orders=OrdersTotal();
//--- пробежим по списку ордеров
   for(int i=0;i<orders;i++)
     {
      ResetLastError();
      //--- скопируем в кэш ордер по его номеру в списке
      ulong ticket=OrderGetTicket(i);
      if(ticket!=0)// если ордер успешно скопирован в кэш, работаем с ним
        {
         double price_open  =OrderGetDouble(ORDER_PRICE_OPEN);
         datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
         string symbol      =OrderGetString(ORDER_SYMBOL);
         long magic_number  =OrderGetInteger(ORDER_MAGIC);
         if(magic_number==my_magic)
           {
            //  обработаем ордер с заданным ORDER_MAGIC
           }
         PrintFormat("Ордер #%d по %s был выставлен %s, ORDER_MAGIC=%d",ticket,symbol,TimeToString(time_setup),magic_number);
        }
      else         // вызов OrderGetTicket() завершился неудачно
        {
         PrintFormat("Ошибка при получении ордера из списка в кэш. Код ошибки: %d",GetLastError());
        }

     }
  }


Получение информации по открытым позициям

Постоянное отслеживание открытых позиций является не только стандартной процедурой, но и безусловно должно быть реализовано в каждом эксперте. Для получения информации по конкретной позиции достаточно знать имя инструмента, по которому она открыта. Используйте для этого функцию PositionSelect(symbol). Для тех случаев, когда советник работает только на одном символе (на символе графика, к которому он прикреплен), имя символа можно получить функцией Symbol() или из предопределенной переменной _Symbol.

//--- будем искать позицию по символу графика, на котором работает эксперт
   string symbol=Symbol();
//--- попробуем получить позицию
   bool selected=PositionSelect(symbol);
   if(selected) // если позиция выбрана
     {
      long pos_id            =PositionGetInteger(POSITION_IDENTIFIER);
      double price           =PositionGetDouble(POSITION_PRICE_OPEN);
      ENUM_POSITION_TYPE type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      long pos_magic         =PositionGetInteger(POSITION_MAGIC);
      string comment         =PositionGetString(POSITION_COMMENT);
      PrintFormat("Позиция #%d по %s: POSITION_MAGIC=%d, цена=%G, тип=%s, комментарий=%s",
                  pos_id, symbol, pos_magic, price,EnumToString(type), comment);
     }
   else        // если выбрать позицию не удалось
     {
      PrintFormat("Не удалось выбрать позицию по символу %s. Ошибка",symbol,GetLastError());
     }
  }

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

Общий алгоритм работы с позициями :

  1. Получить общее количество позиций с помощью функции PositionsTotal();
  2. Организовать цикл по перебору всех позиций по их номерам в списке;
  3. Скопировать поочередно каждую позицию в кэш с помощью функции PositionGetSymbol();
  4. Получить нужные данные позиции из кэша с помощью функций PositionGetDouble(), PositionGetInteger() и PositionGetString(). При необходимости проанализировать полученные данные и выполнить необходимые действия.

Пример подобного алгоритма:

#property script_show_inputs

input long my_magic=555;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- получим общее количество позиций
   int positions=PositionsTotal();
//--- пробежим по списку ордеров
   for(int i=0;i<positions;i++)
     {
      ResetLastError();
      //--- скопируем в кэш позицию по ее номеру в списке
      string symbol=PositionGetSymbol(i); //  попутно получим имя символа, по которому открыта позиция
      if(symbol!="") // позицию скопировали в кэш, работаем с ней
        {
         long pos_id            =PositionGetInteger(POSITION_IDENTIFIER);
         double price           =PositionGetDouble(POSITION_PRICE_OPEN);
         ENUM_POSITION_TYPE type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         long pos_magic         =PositionGetInteger(POSITION_MAGIC);
         string comment         =PositionGetString(POSITION_COMMENT);
         if(pos_magic==my_magic)
           {
           //  обработаем позицию с заданным POSITION_MAGIC
           }
         PrintFormat("Позиция #%d по %s: POSITION_MAGIC=%d, цена=%G, тип=%s, комментарий=%s",
                     pos_id,symbol,pos_magic,price,EnumToString(type),comment);
        }
      else           // вызов PositionGetSymbol() завершился неудачно
        {
         PrintFormat("Ошибка при получении в кэш позиции c индексом %d."+
                     " Код ошибки: %d", i, GetLastError());
        }
     }
  }

Правила работы с кэшем истории

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

//---
   datetime start=0;           // начальная граница установлена на 1970 год
   datetime end=TimeCurrent();  // конечная граница установлена на текущее серверное время
//--- запросим в кэш программы всю торговую историю
   HistorySelect(start,end);
//--- получим количество всех ордеров в истории
   int history_orders=HistoryOrdersTotal();
//--- теперь пробежим по всем ордерам
   for(int i=0;i<history_orders;i++)
     {
     //  обработка каждого ордера в истории
     }   
    
    ...
         
//--- получим количество всех сделок в истории
   int deals=HistoryDealsTotal();
//--- теперь пробежим по всем сделкам
   for(int i=0;i<deals;i++)
     {
     //  обработка каждой сделки в истории
     }   

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

Важно: всегда внимательно относитесь ко всем случаям вызова функции HistorySelect()! Необдуманные и избыточные загрузки всей доступной торговой истории в кэш mql5-программы ухудшат ее производительность.

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

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

Правильный алгоритм работы с торговой историей:

  1. Определить необходимость запроса торговой истории в кеш. Если такой необходимости нет, то последующие пункты не выполнять;
  2. Определиться с конечной датой торговой истории (возможно, история по текущий момент и не нужна);
  3. Вычислить начальную дату торговой истории, отталкиваясь от конечной даты. Обычно в экспертах требуется торговая история не глубже одного дня или недели;
  4. Получить тикеты сделок и исторических ордеров для получения свойств по известным тикетам:
    • HistoryOrderGetDouble()
    • HistoryOrderGetInteger()
    • HistoryOrderGetString()
    • HistoryDealGetDouble()
    • HistoryDealGetInteger()
    • HistoryDealGetString()
  5. Если тикеты неизвестны, организовать при необходимости цикл по перебору;
  6. Получить в цикле тикет каждой сделки/ордера из кэша торговой истории по индексу (HistoryOrderGetTicket(index) и HistoryDealGetTicket(index));
  7. Получить необходимые свойства ордеров и сделок по известному тикету(смотри пункт 4).

Пример кода по такому алгоритму:

//--- переменная, которая устанаваливается в true только при изменени торговой истории
   bool TradeHistoryChanged=false;
//--- тут проверяем измение истории и выставляем TradeHistoryChanged=true если нужно
//... необходимый код

//--- проверяем - есть изменения в торговой истории или нет
   if(!TradeHistoryChanged) return;

//--- история изменилась, тогда есть смысл загружать ее в кеш 
//--- конечная граница установлена на текущее серверное время
   datetime end=TimeCurrent();
//--- начальную границу установим на 3 дня назад
   datetime start=end-3*PeriodSeconds(PERIOD_D1);
//--- запросим в кэш программы торговую историю за последние 3 дня
   HistorySelect(start,end);
//--- получим количество ордеров в кеше истории
   int history_orders=HistoryOrdersTotal();
//--- теперь пробежим по ордерам
   for(int i=0;i<history_orders;i++)
     {
      //--- получим тикет историческкого ордера
      ulong ticket=HistoryOrderGetTicket(i);
      //--- работаем с этим ордером - получаем его свойства
      long order_magic=HistoryOrderGetInteger(ticket,ORDER_MAGIC);
      // получить остальные свойства ордера по тикету
      // ...
     }

Основная идея представленного примера - сначала проверить факт изменения торговой истории. Один из вариантов - в функции OnTrade() устанавливать глобальной переменной TradeHistoryChanged значение true, так как событие Trade всегда поступает при любом торговом событии.

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

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

Примеры правильной и неправильной работы с торговой историей приложены к статье в файлах WrongWorkWithHistory.mq5 и RightWorkWithHistory.mq5.


Получение информации по ордерам из истории

Работа с историческими ордерами почти ничем не отличается от работы с действующими ордерами за одним только исключением. Если количество действующих ордеров в кэше mql5-программе не может быть больше одного, то результат HistoryOrdersTotal() и количество  исторических ордеров в кэше зависит от того, какой объем торговой истории был загружен функцией HistorySelect(start, end), HistorySelectByPosition() или HistoryOrderSelect().

Важно: если торговая история не была загружена в кэш mql5-программы одной из функций  HistorySelect(), HistorySelectByPosition() или HistoryOrderSelect(), то работать с историческими ордерами и сделками невозможно. Обязательно запрашивайте требуемую историю сделок и ордеров перед получением данных по торговой истории.

Для примера приведен скрипт, который ищет последний ордер за последний день и выводит по нему информацию. 

// --- определение границ требуемой торговой истории
   datetime end=TimeCurrent();                 // текущее серверное время
   datetime start=end-PeriodSeconds(PERIOD_D1);// установим начало на сутки назад
//--- запросим в кэш программы торговую историю за день
   HistorySelect(start,end);
//--- получим количество ордеров в истории
   int history_orders=HistoryOrdersTotal();
//--- получим тикет ордера из истории, имеющего последний индекс в списке
   ulong order_ticket=HistoryOrderGetTicket(history_orders-1);
   if(order_ticket>0) // получили в кэш исторический ордер, работаем с ним
     {
      //--- статус ордера
      ENUM_ORDER_STATE state=(ENUM_ORDER_STATE)HistoryOrderGetInteger(order_ticket,ORDER_STATE);
      long order_magic      =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
      long pos_ID           =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
      PrintFormat("Ордер #%d: ORDER_MAGIC=#%d, ORDER_STATE=%d, ORDER_POSITION_ID=%d",
                  order_ticket,order_magic,EnumToString(state),pos_ID);

     }
   else              // неудачная попытка получения ордера

     {
      PrintFormat("Всего в истории %d ордеров, не удалось выбрать ордер"+
                  " с индексом %d. Ошибка %d",history_orders,history_orders-1,GetLastError());
     }


В более общих случаях требуется перебрать в цикле ордера из кеша и произвести по ним анализ. Общий алгоритм будет такой:

  1. определить границы достаточной истории, если история загружается функцией HistorySelect() - нежелательно загружать всю историю торговли в кеш;
  2. загрузить в кэш программы торговую историю функций HistorySelect(), HistorySelectByPosition() или HistoryOrderSelect(ticket);
  3. получить общее количество ордеров в кэше с помощью функции HistoryOrdersTotal();
  4. организовать цикл по перебору всех ордеров по их номерам в списке;
  5. получить тикет ордера в кэше с помощью функции HistoryOrderGetTicket();
  6. получить данные ордера из кэша с помощью функций HistoryOrderGetDouble(), HistoryOrderGetInteger() и HistoryOrderGetString(). При необходимости проанализировать полученные данные и выполнить необходимые действия.

Пример подобного алгоритма:

#property script_show_inputs

input long my_magic=999;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
// --- определение границ требуемой торговой истории
   datetime end=TimeCurrent();                 // текущее серверное время
   datetime start=end-PeriodSeconds(PERIOD_D1);// установим начало на сутки назад
//--- запросим в кэш программы нужный интервал торговой истории
   HistorySelect(start,end);
//--- получим количество ордеров в истории
   int history_orders=HistoryOrdersTotal();
//--- теперь пробежим по всем ордерам
   for(int i=0;i<history_orders;i++)
     {
      //--- получим тикет ордера по его номеру в списке
      ulong order_ticket=HistoryOrderGetTicket(i);
      if(order_ticket>0) //  получили в кэш исторический ордер, работаем с ним
        {
         //--- время исполнения
         datetime time_done=HistoryOrderGetInteger(order_ticket,ORDER_TIME_DONE);
         long order_magic  =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
         long pos_ID       =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
         if(order_magic==my_magic)
           {
           //  обработаем позицию с заданным ORDER_MAGIC
           }
         PrintFormat("Ордер #%d: ORDER_MAGIC=#%d, time_done %s, ORDER_POSITION_ID=%d",
                     order_ticket,order_magic,TimeToString(time_done),pos_ID);
        }
      else               // неудачная попытка получения ордера из истории
        {
         PrintFormat("Не удалось выбрать ордер с индексом %d. Ошибка %d",
                     i,GetLastError());
        }
     }
  }
Важно: всегда внимательно относитесь ко всем случаям вызова функции HistorySelect()! Необдуманные и избыточные загрузки всей доступной торговой истории в кэш mql5-программы ухудшат ее производительность.


Получение информации по сделкам из истории

Обработка сделок имеет те же самые особенности, что и работа с историческими ордерами. Количество сделок в торговой истории и результат выполнения HistoryDealsTotal() зависит от того, какой объем торговой истории был загружен в кэш функцией HistorySelect(start, end) или HistorySelectByPosition().

Для заполнения кэша только одной сделкой по ее тикету служит функция HistoryDealSelect(ticket).

// --- определение границ требуемой торговой истории
   datetime end=TimeCurrent();                 // текущее серверное время
   datetime start=end-PeriodSeconds(PERIOD_D1);// установим начало на сутки назад
//--- запросим в кэш программы нужный интервал торговой истории
   HistorySelect(start,end);
//--- получим количество сделок в истории
   int deals=HistoryDealsTotal();
//--- получим тикет сделки, имеющей последний индекс в списке
   ulong deal_ticket=HistoryDealGetTicket(deals-1);
   if(deal_ticket>0) // получили в кэш сделку, работаем с ней
     {
      //--- тикет ордера, на основании которого была проведена сделка
      ulong order     =HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
      long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
      long pos_ID     =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
      PrintFormat("Сделка #%d по ордеру #%d с ORDER_MAGIC=%d участвовала в позиции %d",
                  deals-1,order,order_magic,pos_ID);

     }
   else              // неудачная попытка получения сделки
     {
      PrintFormat("Всего в истории %d сделок, не удалось выбрать сделку"+
                  " с индексом %d. Ошибка %d",deals,deals-1,GetLastError());
     }

В более общих случаях требуется перебрать в цикле сделки из кеша и произвести по ним анализ. Общий алгоритм будет такой:

  1. определить границы достаточной истории, если история загружается функцией HistorySelect(start, end) - нежелательно загружать всю историю торговли в кеш;
  2. загрузить в кэш программы торговую историю функций HistorySelect() или HistorySelectByPosition();
  3. получить общее количество сделок в истории с помощью функции HistoryDealsTotal();
  4. организовать цикл по перебору всех сделок по их номерам в списке;
  5. определить тикет очередной сделки в кэше с помощью функции HistoryDealGetTicket();
  6. получить информацию о сделке из кэша с помощью функций HistoryDealGetDouble(), HistoryDealGetInteger() и HistoryDealGetString(). При необходимости проанализировать полученные данные и выполнить необходимые действия.

Пример подобного алгоритма для подсчета прибылей и убытков:

input long my_magic=111;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
// --- определение границ требуемой торговой истории
   datetime end=TimeCurrent();                 // текущее серверное время
   datetime start=end-PeriodSeconds(PERIOD_D1);// установим начало на сутки назад
//--- запросим в кэш программы нужный интервал торговой истории
   HistorySelect(start,end);
//--- получим количество сделок в истории
   int deals=HistoryDealsTotal();

   int returns=0;
   double profit=0;
   double loss=0;
//--- пройдем по всем сделкам в истории
   for(int i=0;i<deals;i++)
     {
      //--- получим тикет сделки по ее индексу в списке
      ulong deal_ticket=HistoryDealGetTicket(i);
      if(deal_ticket>0) // получили в кэш сделку, работаем с ней
        {
         string symbol             =HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
         datetime time             =HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         ulong order               =HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
         long order_magic          =HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
         long pos_ID               =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
         ENUM_DEAL_ENTRY entry_type=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);

         //--- обрабатываем сделки с указанным DEAL_MAGIC
         if(order_magic==my_magic)
           {
            //... необходимые действия
           }

         //--- посчитаем убытки и прибыли с фиксаций результата
         if(entry_type==DEAL_ENTRY_OUT)
           {
            //--- увеличим количество сделок 
            returns++;
            //--- результат фиксации
            double result=HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
            //--- занесем положительный результат в суммарную прибыль
            if(result>0) profit+=result;
            //--- занесем отрицательный результат в суммарный убыток
            if(result<0) loss+=result;
           }
        }
      else // неудачная попытка получения сделки
        {
         PrintFormat("Не удалось выбрать сделку с индексом %d. Ошибка %d",
                     i,GetLastError());
        }
     }
   //--- выведем результаты подсчетов
   PrintFormat("Всего %d сделок с фиксаций результата. Прибыль=%.2f , Убыток= %.2f",
               returns,profit,loss);
  }
Важно: всегда внимательно относитесь ко всем случаям вызова функции HistorySelect()! Необдуманные и избыточные загрузки всей доступной торговой истории в кэш mql5-программы ухудшат ее производительность.


Получение в кэш истории по идентификатору позиции (POSITION_IDENTIFIER)

Функция HistorySelectByPosition(position_ID), как и функция HistorySelect(start, end), заполняет кэш сделками и ордерами из истории, но с одним условием - они должны иметь указанный идентификатор позиции (POSITION_IDENTIFIER). Идентификатор позиции - это уникальное число, которое автоматически присваивается каждой вновь открытой позиции и не изменяется в течение всей ее жизни. При этом нужно иметь ввиду, что переворот позиции (смена типа позиции с  POSITION_TYPE_BUY на POSITION_TYPE_SELL) не изменяет идентификатора позиции.

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

Функция HistorySelectByPosition(position_ID) служит для того, чтобы избавить программиста от необходимости самостоятельно писать код для перебора всей торговой истории в поисках такой информации. Типичный алгоритм работы с этой функцией:

  1. получить нужный идентификатор позиции;
  2. получить функцией HistorySelectByPosition() в кэш торговой истории все ордера и сделки, чей идентификатор позиции равен идентификатору текущей позиции;
  3. обработать торговую историю в соответствии с алгоритмом.


Заключение

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

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

Все приведенные примеры можно найти в прикрепленных к статье файлах.