English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Ордерa, позиции и сделки в MetaTrader 5

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

MetaTrader 5Трейдинг | 5 января 2011, 21:54
36 386 39
MetaQuotes
MetaQuotes

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

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

  • Ордера — это принятые торговым сервером запросы на совершение торговых операций, оформленные в соответствие с требованиями платформы MetaTrader 5. Если запрос составлен неправильно, то он не появится в торговой платформе в виде ордера. Ордера могут быть немедленного исполнения, например, купить или продать по текущей рыночной цене определенный объем на указанном финансовом инструменте. Другой вид ордеров - отложенные ордера, которые содержат приказ на совершение торговой операции по некоему условию. Отложенные ордера могут также содержать ограничение на время их действия - дату истечения (экспирации).



    Действующие (отложенные) ордера, которые находятся в ожидании условий их исполнения или отмены, показываются в терминале в закладке "Торговля". Эти ордера можно модифицировать или отменять. Постановка, отмена и модификация ордеров производится с помощью функции OrderSend(). Если ордер был отменен или истекло время его действия, или ордер был исполнен, то он перемещается в историю ордеров. Исполненные и отмененные ордера показываются в терминале в закладке "История". Ордера из истории недоступны для модификации.

  • Сделки — результат выполнения ордера (приказа на совершение торговой операции). Каждая сделка базируется на одном конкретном ордере, но один ордер может порождать множество сделок. Например, приказ на покупку 10 лотов может быть исполнен посредством нескольких последовательных сделок при частичном исполнении. Сделки всегда находятся в истории торговли и не могут модифицироваться. В терминале сделки отображаются в закладке "История".


  • Позиции  —  это наличие купленных или проданных контрактов по финансовому инструменту. Длинная позиция (Long) образуется в результате покупок в ожидании повышения цены, короткая позиция (Short) — результат продажи актива  в расчете на снижение цены в будущем. На одном счете по каждому финансовому инструменту может существовать только одна позиция. По каждому символу в любой момент времени может быть только одна открытая позиция - длинная или короткая.


    Объем позиции может увеличиваться в результате новой торговой операции в том же направлении. То есть объем длинной позиции будет увеличен после новой покупки (операции Buy) и уменьшится после продажи (операции Sell). Позиция считается закрытой, если в результате торговой операции объем обязательств стал равен нулю. Такая операция называется закрытием позиции.
Важно: действующие ордера и позиции всегда отображаются на вкладке "Торговля", а сделки и ордера из истории всегда отражаются в закладке "История". Не следует путать действующие ордера из закладки "Торговля" и исторические ордера из закладки "История".


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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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


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

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

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

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


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

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

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

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

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


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


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

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

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

  • OrderSelect(ticket) -  копирует из базы терминала действующий ордер по его тикету в кэш текущих ордеров для дальнейших обращений к его свойствам функциями OrderGetDouble(),  OrderGetInteger() и OrderGetString();
  • OrderGetTicket(index) - копирует из базы терминала действующий ордер по его номеру в списке ордеров базы терминала в кэш текущих ордеров для дальнейших обращений к его свойствам функциями OrderGetDouble(),  OrderGetInteger() и OrderGetString(). Общее количество ордеров в базе терминала можно получить функцией OrdersTotal();
  • PositionSelect(symbol) - копирует из базы терминала открытую позицию по имени символа в кэш для дальнейших обращений к ее свойствам функциями PositionGetDouble(),  PositionGetInteger() и PositionGetString();
  • PositionGetSymbol(index) - копирует из базы терминала открытую позицию  по ее номеру в списке позиций базы терминала в кэш для дальнейших обращений к ее свойствам функциями PositionGetDouble(),  PositionGetInteger() и PositionGetString(). Общее количество позиций в базе терминала можно получить функцией PositionsTotal().


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

  • HistoryOrderSelect(ticket) - копирует из базы терминала ордер из истории по его тикету в кэш исторических ордеров для дальнейших обращений к его свойствам функциями HistoryOrderGetDouble(),  HistoryOrderGetInteger() и HistoryOrderGetString();
  • HistoryDealSelect(ticket) - копирует из базы терминала сделку по ее тикету в кэш сделок для дальнейших обращений к ее свойствам функциями HistoryDealGetDouble(), HistoryDealGetInteger()  и HistoryDealGetString();

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

  • HistorySelect(start, end) - заполняет кэш истории сделками и ордерами за указанный интервал серверного времени. От результата выполнения этой функции зависят значения, которые возвращаются из HistoryDealsTotal() и HistoryOrdersTotal();
  • HistorySelectByPosition(position_ID) - заполняет кэш истории сделками и ордерами, имеющими указанный идентификатор позиции. Результат выполнения этой функции также влияет на HistoryDealsTotal() и HistoryOrdersTotal().


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-программы. Это вызвано тем, что запрашиваемая информация могла уже обновиться, а это значит, что хранящийся в кэше отпечаток уже устарел.
  • Ордера
    Для получения информации о действующем ордере его нужно предварительно скопировать в кэш действующих ордеров одной из двух функций: OrderGetTicket() или OrderSelect(). Именно для ордера, хранящегося в кэше, будут выдаваться значения свойств при вызове соответствующих функций:
    1. OrderGetDouble (тип_свойства)
    2. OrderGetInteger (тип_свойства)
    3. OrderGetString (тип_свойства)
    Эти функции получают все данные из кэша, поэтому для гарантированного получения свежих данных об ордере рекомендуется предварительно вызвать функцию заполнения кэша.

  • Позиции

    Для получения информации о позиции ее необходимо предварительно выбрать и скопировать в кэш с помощью одной из двух функций: PositionGetSymbol или PositionSelect. Именно из кэша будут выдаваться значения свойств позиции при вызове соответствующих функций:

    1. PositionGetDouble (тип_свойства)
    2. PositionGetInteger (тип_свойства)
    3. PositionGetString (тип_свойства)
    Так как данные функции получают все данные из кэша, то для гарантированного получения свежих данных о позиции рекомендуется вызывать функции заполнения кэша позиций.

  • Исторические ордера
    Для получения информации об ордере из истории нужно предварительно сформировать кэш исторических оредров с помощью одной из трех  функций: HistorySelect(start, end), HistorySelectByPosition() или HistoryOrderSelect(ticket). В результате успешного выполнения в кэше будут храниться ордера в количестве, возвращаемом функцией HistoryOrdersTotal(). Доступ к свойствам этих ордеров осуществляется поэлементно по тикету при помощи соответствующих функций:
    1. HistoryOrderGetDouble (тикет_ордера, тип_свойства)
    2. HistoryOrderGetInteger (тикет_ордера, тип_свойства)
    3. HistoryOrderGetString (тикет_ордера, тип_свойства)

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

  • Сделки
    Для получения информации о конкретной сделке в истории нужно предварительно сформировать кэш сделок с помощью одной из трех  функций: HistorySelect(start, end), HistorySelectByPosition() или HistoryDealSelect(ticket). В результате при успешном выполнении функции в кэше будут храниться сделки в количестве, возвращаемом функцией HistoryDealsTotal(). Доступ к свойствам этих сделок осуществляется по тикету при помощи соответствующих функций:
    1. HistoryDealGetDouble (тикет_сделки, тип_свойства)
    2. HistoryDealGetInteger (тикет_сделки, тип_свойства)
    3. HistoryDealGetString (тикет_сделки, тип_свойства)

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

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

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 хорошо продумана и проста для понимания. При этом обилие торговых функций позволяет решать каждую конкретную задачу наиболее эффективным образом.

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

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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (39)
mktr8591
mktr8591 | 31 мая 2021 в 10:18
fxsaber:

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

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


ЗЫ Как быстрее всего работать с историей - комментариев нет. На данный момент 100% быстрым способом является везде вызов только такого HistorySelect.

А почему не
HistorySelect(t, INT_MAX)

где t - произвольная не очень давняя и не меняющаяся от вызова к вызову дата (константа, единая для всей программы)?

fxsaber
fxsaber | 31 мая 2021 в 10:36
mktr8591:
А почему не

где t - произвольная не очень давняя и не меняющаяся от вызова к вызову дата (константа, единая для всей программы)?

Не уверен, что от этого кеш уменьшится.

fxsaber
fxsaber | 11 июн. 2021 в 10:53
fxsaber:

Не уверен, что от этого кеш уменьшится.

Потребление уменьшается. Прописывал такое в начале.

bool HistorySelect2( const datetime From, const datetime To )
{
  static const datetime NewFrom = ::TimeCurrent() - 24 * 3600 & 30; // -Month.
  
  return(::HistorySelect(From ? From : NewFrom, To));
}

#define HistorySelect HistorySelect2

Но пришлось отказаться из-за серьезных проблем.

fxsaber
fxsaber | 30 июн. 2021 в 18:11
#define PRINT(A) Print(#A + " = " + (string)(A))

void OnStart()
{
  PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED));
  
  if (HistorySelect(0, INT_MAX))
  {
    PRINT(HistoryDealsTotal());
    PRINT(HistoryOrdersTotal());
    
    PRINT(MQLInfoInteger(MQL_MEMORY_USED));
    PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED));
  }
}

Результат запуска на Терминале, где один M1 чарт, 5000 баров, один символ, нет ресурсов и графических объектов.

TerminalInfoInteger(TERMINAL_MEMORY_USED) = 426
HistoryDealsTotal() = 134502
HistoryOrdersTotal() = 218740
MQLInfoInteger(MQL_MEMORY_USED) = 1
TerminalInfoInteger(TERMINAL_MEMORY_USED) = 789

Многовато. 10 синхронных (OrderSend) советников съедает 4 гига. Два варианта:

  1. Открыть новый счет, перекинуть на него средства и продолжить торговлю уже на нем. К сожалению, не всегда возможно.
  2. Объединить всех ботов в один через асинхронность (OrderSendAsync). Очень тяжелый вариант отловли багов при супер-активной торговле.
Во втором пункте еще надо писать менеджер (GUI и прочее) ботов, вшитых в единый советник.
mktr8591
mktr8591 | 30 июн. 2021 в 18:43
fxsaber:



  1. Объединить всех ботов в один через асинхронность (OrderSendAsync). Очень тяжелый вариант отловли багов при супер-активной торговле.

По другому никак. (если конечно не отсекать старую историю и переделать полностью алгоритм работы с истрией, но это только если MQ не вернут старую сортировку).

Реализация мультивалютного режима в MetaTrader 5 Реализация мультивалютного режима в MetaTrader 5
Интерес к мультивалютному анализу и мультивалютной торговле существует давно. Но только с выпуском в свет терминала MetaTrader 5 и языка программирования MQL5 появилась возможность реализации полноценного мультивалютного режима. В данной статье предложен способ, позволяющий проводить анализ и обработку всех поступающих тиков по множеству финансовых инструментов. В качестве иллюстрации рассмотрен мультивалютный индикатор RSI для индекса доллара USDx.
Мастер MQL5: Как написать свой модуль торговых сигналов Мастер MQL5: Как написать свой модуль торговых сигналов
Генератор торговых стратегий Мастера MQL5 значительно упрощает проверку торговых идей. В статье рассказывается о том, как написать и подключить в Мастер MQL5 свой собственный класс торговых сигналов с реализацией сигналов по пересечению ценой скользящей средней, рассматривается структура и формат описания созданного класса для Мастера MQL5.
Мастер MQL5: Как написать свой модуль управления капиталом и рисками Мастер MQL5: Как написать свой модуль управления капиталом и рисками
Генератор торговых стратегий Мастера MQL5 значительно упрощает проверку торговых идей. В статье рассказывается о том, как написать и подключить в Мастер MQL5 свой собственный модуль управления капиталом и рисками. В качестве примера рассматривается создание алгоритма управления капиталом, в котором размер торгового объема определяется в зависимости от результатов предыдущей сделки. Рассматривается структура и формат описания созданного класса для Мастера MQL5.
Механическая Торговая Система "Вилка Чувашова" Механическая Торговая Система "Вилка Чувашова"
В данной статье вашему вниманию предлагается краткий обзор методики и программный код стратегии механической торговой системы по методике Станислава Чувашова. Рассматриваемый анализ состояния рынка перекликается с подходом Т. Демарка к построению линий тренда для последнего ближайшего отрезка времени, в качестве опорных точек для построения трендовых линий используются фракталы.