English 中文 Español Deutsch 日本語 Português
preview
Пользовательский индикатор: Отображение сделок входа, выхода и разворота позиции на неттинговых счетах

Пользовательский индикатор: Отображение сделок входа, выхода и разворота позиции на неттинговых счетах

MetaTrader 5Примеры |
469 0
Daniel Santos
Daniel Santos

Содержание

  1. Введение
  2. Что такое неттинговый счет?
  3. Работа с торговыми событиями
  4. Практический пример использования - введение
  5. Свойства индикаторов
  6. Описание алгоритма
  7. Еще один практический пример
  8. Интеграция с торговым советником
  9. Заключение


1. Введение

Когда мы говорим об индикаторах, можем представить себе различные функции: построение графиков (гистограммы, линии тренда, стрелки или бары); расчет данных на основе движения цены и объема; а также наблюдение за статистическими закономерностями в наших сделках. Однако в данной статье мы рассмотрим другой способ построения индикатора на MQL5, ориентированный на управление собственными позициями: входами, частичными выходами и т. д. Мы будем активно использовать динамические матрицы и некоторые торговые функции, связанные с историей сделок и открытых позиций. 


2. Что такое неттинговый счет?

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

  1. Объем меньше -> уменьшение позиции
  2. Объем одинаковый -> закрытие позиции
  3. Объем больше -> разворот позиций


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

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

Расчет неттинга

У нас есть средняя цена (P), взвешенная по объему (N) лотов в каждой сделке.

Для получения более подробной информации о разнице между системами мы рекомендуем прочитать статью, написанную MetaQuotes: "В MetaTrader 5 добавлена хеджинговая система учета позиций". С этого момента в данной статье мы рассмотрим все операции, выполняемые на неттинговом счете. Если у вас еще нет такого счета, можно открыть бесплатный демо-счет в MetaQuotes, как показано ниже.

В MetaTrader 5 нажмите Файл > Открыть счет:

Выбор компании для открытия счета

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

Завершаем открытие демо-счета


3. Работа с торговыми событиями

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

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


4. Практический пример использования - введение

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

График частичных входов и объемов


5. Свойства индикаторов

Свойства индикаторов в обязательном порядке задаются в начале описания и настроены следующим образом:

#property indicator_chart_window               // Indicator displayed in the main window
#property indicator_buffers 0                  // Zero buffers
#property indicator_plots   0                  // No plotting
//--- plot Label1
#property indicator_label1  "Line properties"
#property indicator_type1   DRAW_LINE          // Line type for the first plot
#property indicator_color1  clrRoyalBlue       // Line color for the first plot
#property indicator_style1  STYLE_SOLID        // Line style for the first plot
#property indicator_width1  1                  // Line width for the first plot 

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

Индикатор загрузки


6. Описание алгоритма

Функция OnInit(), загружаемая в начале программы, отвечает за инициализацию массива типа double под названием "Element", который выступает как настоящий буфер индикатора. Данный массив состоит из трех столбцов, и каждый индекс хранит цену (0), объем (1) и номер тикета (2). Каждая строка этого массива соответствует какой-то сделке в истории. Если инициализация прошла успешно, т.е. подтвердилось, что счет не является хеджевым, запускается функция OnTrade(). Если при инициализации произошла ошибка, индикатор закрывается и удаляется с графика.

Посмотрите:

int OnInit()
  {
   ArrayResize(Element,0,0);
   int res=INIT_SUCCEEDED;
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      res=INIT_FAILED;
   else
      OnTrade();
   return(res);
  }

После инициализации функция OnTrade() активируется функцией OnCalculate() нативным образом при возникновении торговых событий. Для того, чтобы она активировалась только один раз и только при формировании новой свечи, реализован фильтр с помощью функции isNewBar и булевой переменной isOldBar. Таким образом, функция OnTrade активируется в трех случаях: при инициализации, при смене свечи и при каждом торговом событии. Данные процессы обеспечивают считывание, обработку и сохранение событий в массиве Element, что отражается с точностью в графических объектах на экране в виде линий и текста.

Функция OnTrade() обновляет ключевые переменные торгового алгоритма. Она начинается с переменной типа datetime под названием "date", которая хранит время начала выбора из истории ордеров. Если в начале программы нет открытой позиции, переменная "date" обновляется временем открытия текущей свечи.

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

Если появляется вторая позиция с другим идентификатором, необходимо будет проверять наличие удаляемых графических элементов с помощью функции ClearRectangles (), чтобы убедиться, что всё актуально. Затем устанавливаем размер массива Element равный нулю, что удаляет данные, которые содержатся в нем. Если открытых позиций нет, функция также активирует ClearRectangles() и сбрасывает массив Element. В переменной "date" хранится значение последнего известного времени сервера, т.е. текущее время. Наконец, оставшееся значение переменной "date" передается в функцию ListOrdersPositions().

void
 OnTrade()
  {
//---
   static datetime date=0;
   if(date==0)
      date=lastTime;
   long positionId=-1,numberOfPositions=0;
   for(int i=PositionsTotal()-1; i>=0; i--)
      if(m_position.SelectByIndex(i))
         if(m_position.Symbol()==ativo000)
           {
            numberOfPositions++;
            positionId=m_position.Identifier();
            oldPositionId=positionId;
           }
   if(numberOfPositions!=0)
     {
      //Print("PositionId: "+positionId);
      HistorySelectByPosition(positionId);
      date=TimeCurrent();
      for(int j=0; j<HistoryDealsTotal(); j++)
        {
         ulong ticket = HistoryDealGetTicket(j);
         if(ticket > 0)
            if(HistoryDealGetInteger(ticket,DEAL_TIME)<date)
               date=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
        }
      if(HistoryDealsTotal()==1 && (ArraySize(Element)/3)>1)
         if(ClearRectangles())
            ArrayResize(Element,0,0);
     }
   else
     {
      bool isClean=ClearRectangles();
      ArrayResize(Element,0,0);
      if(isClean)
        date=TimeCurrent();        // Do not use the array until there is new open position
      ArrayPrint(Element);         // If there are no errors, this function will not be called here: the array with zero size
     }
   ListOrdersPositions(date);
  }

Функция ListOrdersPositions() играет важную роль, поскольку отвечает за активацию функций добавления или удаления записей из массива Element с помощью функций AddValue() и RemoveValue(). При получении параметра dateStart, имеющего тип int, будут возможны два варианта. Если в течение периода, указанного для функции HistorySelect(start, end), нет истории сделок, то она перейдет непосредственно к ее концу, вызывая функцию PlotRectangles(), которая обновляет объекты на экране в соответствии с содержимым массива Element. С другой стороны, если в истории есть сделка, то функция HistoryDealsTotal() должна возвращать ненулевое значение. В этом случае проводится новая проверка, целью которой является изучение каждой найденной сделки, её классификация по типу входа, сбор информации о цене, объеме и номере тикета. Данные сделки могут быть: DEAL_ENTRY_IN, DEAL_ENTRY_OUT или DEAL_ENTRY_INOUT.

Если сделка является сделкой входа, то активируется функция AddValue; если является сделкой выхода, то активируется RemoveValue со следующими параметрами: цена, объем и номера тикета, полученные ранее. Если у нас разворот, то также срабатывает функция AddVolume(), если номер тикета не был ранее введен в массив. Кроме того, передаются параметры цены и объема, причем последний рассчитывается как разница между собранным объемом и объемом предыдущих сделок, всё еще присутствующих в массиве.

Данный процесс имитирует реконструкцию исторической позиции, так что при достижении сделки разворота позиция разворачивается и включается в массив, как если бы это была новая запись, корректирующая количество лотов. Кроме того, строки, которые были на экране до этого момента, удаляются. Функция Sort() сортирует массив Element в возрастающем порядке по столбцу цены и удаляет с графика объекты, значения которых в столбце 1 (объем) массива равны нулю. Наконец, данная функция проверяет на несоответствие и удаляет из массива строки, индексы 0 и 1 которых (цена и объем) равны нулю.

void ListOrdersPositions(datetime dateInicio)
  {
//Analyze the history
   datetime inicio=dateInicio,fim=TimeCurrent();
   if(inicio==0)
      return;
   HistorySelect(inicio, fim);
   double deal_price=0, volume=0,newVolume;
   bool encontrouTicket;
   uint tamanhoElement=0;
   for(int j=0; j<HistoryDealsTotal(); j++)
     {
      ulong ticket = HistoryDealGetTicket(j);
      if(ticket <= 0)
         return;
      if(HistoryDealGetString(ticket, DEAL_SYMBOL)==_Symbol)
        {
         encontrouTicket=false;
         newVolume=0;            // Need to reset each 'for' loop
         volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
         deal_price=HistoryDealGetDouble(ticket,DEAL_PRICE);
         double auxArray[1][3] = {deal_price,volume,(double)ticket};
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_IN)
            AddValue(deal_price,volume,(double)ticket);
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
            RemoveValue(deal_price,volume,(double)ticket);
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
           {
            tamanhoElement = ArraySize(Element)/3; //Always check the array size, it can vary with the Add/RemoveValue() functions
            for(uint i=0; i<tamanhoElement; i++)
               if(Element[i][2]==ticket)
                 {
                  encontrouTicket=true;
                  break;
                 }
            if(!encontrouTicket) // If after the previous scanning we don't find mentioning of the ticket in the array
              {
               for(uint i=0; i<tamanhoElement; i++)
                 {
                  newVolume+=Element[i][1];
                  Element[i][1]=0;
                 }
               newVolume=volume-newVolume;
               AddValue(deal_price,newVolume,double(ticket));
              }
           }
        }
     }
   PlotRectangles();
  }

7. Еще один пример из практики

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

 Time Symbol
 Deal Type
Direction
Volume
Price
2023.05.04  09:42:05
winm23
1352975
buy
in
1
104035
2023.05.04  09:43:16
winm23
1356370
sell
in/out
2
103900
2023.05.04 16:34:51
winm23
2193299
buy
out
1
103700
2023.05.04 16:35:05
winm23
2193395
buy
in
1
103690
2023.05.04 16:35:24
winm23
2193543
buy
in
1
103720
2023.05.04 16:55:00
winm23
2206914
sell
out
1
103470
2023.05.04 17:27:26
winm23
2214188
sell
in/out
2
103620
2023.05.04 17:30:21
winm23
2215738
buy
in/out
4
103675
2023.05.05 09:03:28
winm23
2229482
buy
in
1
104175
2023.05.05 09:12:27
winm23
2236503
sell
out
1
104005
2023.05.05 09:19:18
winm23
2246014
sell
out
1
103970
2023.05.05 09:22:45
winm23
2250253
buy
in
1
103950
2023.05.05 16:00:10
winm23
2854029
sell
out
1
106375
2023.05.05 16:15:40
winm23
2864767
sell
out
1
106275
2023.05.05 16:59:41
winm23
2884590
sell
out
1
106555

Независимо от предыдущих операций, в данный момент у массива Element размер будет нулевой, и в нем также не будет открытых позиций. В 09:42:05 04 мая 2023 года происходит сделка входа Sell (которая уже зафиксирована в истории платформы), на 1 лот, что немедленно вызывает функцию OnTrade(). Учитывая, что MetaTrader 5 был запущен на компьютере на несколько минут раньше (09:15 ч), времени хватило, чтобы переменная даты обновилась до 2023.05.04 09:15:00, и с тех пор данное значение сохраняется. В OnTrade() пройдемся по списку открытых позиций; в используемом нами типе счета будет разрешена только одна позиция на символ: в данном случае WINM23. Переменная numberOfPositions принимает значение 1, а переменная positionID - значение 1352975, что совпадает с тикетом первой сделки, который, в свою очередь, является номером того ордера, который ее создал. Теперь переменная date обновляется временем сделки, и последующие сделки, вплоть до сделки с номером 2193299, будут получать то же время из функции Identifier().

Вызывается функция ListOrdersPositions(date), которая выбирает период с 09:42:05 по TimeCurrent() для запроса истории. В цикле повторения, при обнаружении входа типа "IN", активируется функция AddValue() со значениями price=104 035, volume=1 и ticket=1352975. AddValue() не найдет этот тикет в пустом массиве, поэтому фунцкия вставит строку, содержащую три переданных значения. Функция ArrayPrint(Element) выводит массив с данной строкой на терминал.

Далее вызывается функция PlotRectangles(), которая сохраняет время текущей свечи и предыдущей 15-й свечи, что и будет параметрами для определения размера рисуемых линий. Функция GetDigits() определяет количество знаков размера тика символа (который в данном случае равен нулю), и это значение используется для составления строки, которая будет формировать названия объектов вместе с ценами, полученными из массива Element. Прямоугольные и текстовые объекты создаются до тех пор, пока объем, соответствующий цене массива, не равен нулю и пока данные объекты еще не существуют на графике. Если объект уже существует, то обновляются такие атрибуты, как цвет, текст и положение. Прямоугольники работают как линии на экране, так как их высота равна нулю. На начальных этапах проекта выбрали OBJ_RECTANGLE, поскольку предполагалось реализовать функцию, которая удаляла бы все объекты этого типа из графика. Хотя этот общий механизм устранения не реализован, использование прямоугольников с нулевой высотой было сохранено. Таким образом, обрабатывается строка, соответствующая сделке покупки в 104035. Так как связанный объем не является нулевым, а объект с именем "104035text" еще не существует, создаются связанные с ним объекты текста и прямоугольника.

Через минуту происходит сделка Sell двумя лотами. Поскольку уже была позиция на покупку размером в один лот, это приводит к развороту, поэтому теперь есть позиция на продажу размером в один лот. MetaTrader также немедленно добавляет данную сделку в историю.  Аналогичным образом повторяется процесс обработки предыдущей сделки, пока не будет достигнут цикл истории ордеров. Снова возвращается сделка с тикетом = 1352975, так как она находится в пределах выбранного периода, и передается в функцию AddValue(). Данная функция находит значение тикета в единственном элементе, присутствующем пока в массиве, и поэтому выходит из функции без добавления нового элемента. Следующий элемент имеет тип INOUT, а единственной предыдущей сделкой, уже имеющейся в массиве, является элемент Element[0][1], хранящийся в newVolume, который затем устанавливается равным нулю.

Объем, зафиксированный в сделке, рассчитывается как (HistoryDealGetDouble(ticket, DEAL_PRICE) - newVolume), т.е. newVolume = 2 - 1 = 1. Затем функция вызывает AddValue(103900, 1, 135370). Я полагаю, что к этому моменту вы уже понимаете механизм. Аналогично, функция PlotRectangles() снова выполняет свой код, и первой ценой массива будет 103900, поскольку сортировка по возрастанию была выполнена с помощью функции Sort(). Объекты, связанные с данной ценой, не существуют на графике, поэтому они создаются. Второй элемент массива (с ценой 104035) уже имеет свои объекты, нарисованные на экране, поэтому его атрибуты обновляются. Содержимое массива Element на данный момент такое: {{103900, 1, 1356370}}, {104035, 0, 1352975}}.

На третьей строке всё обрабатывается аналогичным образом, пока не будет найдена сделка выхода с ценой 103,700, объемом 1 и тикетом 219,3299. Сделка выхода вызывает функцию RemoveValue() с новыми найденными параметрами. Функция завершается, если находит нулевой объем или строку в массиве с таким же тикетом. Поскольку данные условия не выполняются, RemoveValue() продолжает поиск цены для удаления с помощью функции ArrayBsearch(). Это бинарный поиск, и для его выполнения массив нужно сортировать (условие, которое ранее выполнялось методом Sort()). Индекс, ближайший к цене 103700, является первым в массиве. Объем этой первой строки также равен 1 и уменьшается до нуля, поэтому активируется соответствующая функция RemoveRectangle. Это действие удаляет с экрана объекты, связанные с ценой 103900. После этого функция AddValue() вставляет в массив строку {103700, 0, 219299}, которая не будет изменена функцией Sort(). Позиция закрывается.  Содержимое массива Element на данный момент: {{103700, 0, 219299}}, {103900, 0, 1356370}}, {104035, 0, 1352975}}.

Когда позиция полностью закрыта, переменная numberOfPositions устанавливается в ноль, а при успешном выполнении ClearRectangles() переменная isClean устанавливается в true. Массив уменьшаем до нуля элементов, а переменная date получает текущее время. Таким образом, нет никаких возвращаемых ордеров внутри вновь установленного периода. Система ожидает новой сделки, чтобы продолжить передачу массиву и обработать последующие действия.  Содержимое Element становится: { }. Мы вернулись к ситуации, аналогичной той, которую описали в начале этого примера, и те же самые рассуждения можно применить для понимания эффективности индикатора в отношении следующих сделок. Операция указывается в пункте "5. Практический пример использования" начинается с цены 103690 в текущем примере. Если вы будете выполнять операции шаг за шагом, то сможете понять вопрос в этом первом примере. Однако я предполагаю, что объяснение связано с ценами сделок на выход и тем, как алгоритм последовательно отсеивает строки, чьи цены ближе к ценам сделок типа DEAL_ENTRY_OUT.


8. Интеграция с торговым советником 

Существует два способа использования таких пользовательских индикаторов в тестере стратегий. Во-первых, нужно скомпилировать советник или другой индикатор, который запускает наш пользовательский индикатор. Просто убедитесь в наличии скомпилированного файла "Plotagem de Entradas Parciais.ex5" в папке "Indicators" и вставьте следующие строки кода в функцию OnInit() программы. Она выступит в качестве триггера. И не забудем предварительно объявить глобальную переменную handlePlotagemEntradasParciais типа int:

   iCustom(_Symbol,PERIOD_CURRENT,"Plotagem de Entradas Parciais");
//--- if the handle is not created
   if(handlePlotagemEntradasParciais ==INVALID_HANDLE)
     {
      //--- Print an error message and exit with an error code
      PrintFormat("Failed to create indicator handle for symbol %s/%s, error code %d",
                  _Symbol,
                  EnumToString(_Period),
                  GetLastError());
      //--- The indicator is terminated prematurely
      return(INIT_FAILED);
     }

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


9. Заключение

Индикатор представления частичных входов был создан для изучения новых возможностей разработки и использования индикаторов на языке MQL5 на одной из самых современных и продвинутых торговых платформ: MetaTrader 5. 


Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/12576

Прикрепленные файлы |
Функции активации нейронов при обучении: ключ к быстрой сходимости? Функции активации нейронов при обучении: ключ к быстрой сходимости?
В данной работе представлено исследование взаимодействия различных функций активации с алгоритмами оптимизации в контексте обучения нейронных сетей. Особое внимание уделяется сравнению классического ADAM и его популяционной версии при работе с широким спектром функций активации, включая осциллирующие функции ACON и Snake. Используя минималистичную архитектуру MLP (1-1-1) и единичный обучающий пример, производится изоляция влияния функций активации на процесс оптимизации от других факторов. Предложен подход к контролю весов сети через границы функций активации и механизма отражения весов, что позволяет избежать проблем с насыщением и застоем в обучении.
От начального до среднего уровня: Переменные (II) От начального до среднего уровня: Переменные (II)
Сегодня мы рассмотрим, как работать со статическими переменными. Этот вопрос часто ставит в тупик многих программистов, как начинающих, так и имеющих некоторый опыт, и это связано с тем, что существует несколько советов и рекомендаций, которые необходимо соблюдать при использовании данного механизма. Представленные здесь материалы предназначены исключительно для дидактических целей. Ни в коем случае нельзя рассматривать это как приложение, чьей целью будет что-то иное помимо изучения и освоения представленных концепций.
Введение в MQL5 (Часть 8): Руководство для начинающих по созданию советников (II) Введение в MQL5 (Часть 8): Руководство для начинающих по созданию советников (II)
В этой статье рассматриваются частые вопросы, которые начинающие программисты задают на форуме MQL5. Также демонстрируются практические решения. Мы научимся совершать основные действия: покупку и продажу, получение цен свечей, а также управление торговыми аспектами, включая торговые лимиты, периоды и пороговые значения прибыли/убытка. В статье представлены пошаговые инструкции, которые помогут вам лучше понять и реализовать обсуждаемые концепции на MQL5.
Создание советника Daily Drawdown Limiter на языке MQL5 Создание советника Daily Drawdown Limiter на языке MQL5
В статье подробно рассматриваются возможности реализации советника на основе торгового алгоритма. Это поможет автоматизировать систему на MQL5 и взять под контроль дневную просадку.