
Пользовательский индикатор: Отображение сделок входа, выхода и разворота позиции на неттинговых счетах
Содержание
- Введение
- Что такое неттинговый счет?
- Работа с торговыми событиями
- Практический пример использования - введение
- Свойства индикаторов
- Описание алгоритма
- Еще один практический пример
- Интеграция с торговым советником
- Заключение
1. Введение
Когда мы говорим об индикаторах, можем представить себе различные функции: построение графиков (гистограммы, линии тренда, стрелки или бары); расчет данных на основе движения цены и объема; а также наблюдение за статистическими закономерностями в наших сделках. Однако в данной статье мы рассмотрим другой способ построения индикатора на MQL5, ориентированный на управление собственными позициями: входами, частичными выходами и т. д. Мы будем активно использовать динамические матрицы и некоторые торговые функции, связанные с историей сделок и открытых позиций.
2. Что такое неттинговый счет?
Как следует из названия статьи, этот индикатор имеет смысл использовать только на счете с неттинговой системой учета, также известной как взаимозачет. В этой системе допускается только одна позиция одного и того же символа. Если мы торгуем в одном направлении, размер позиции будет увеличиваться. С другой стороны, если сделка совершается в противоположном направлении, то у открытой позиции будет три возможных варианта:
- Объем меньше -> уменьшение позиции
- Объем одинаковый -> закрытие позиции
- Объем больше -> разворот позиций
Например, на хеджевом счете (или в системе хеджирования) мы сможем совершить две сделки покупки по одному лоту в 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования