2.4 Пишем индикатор анализа закрытых ордеров. Графические объекты OBJ_LABEL

Доброго времени суток. 

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

Ну что ж, продолжим изучение языка программирования MQL4

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

Анализ и сохранение данных по ордерам

Начнем работу с создания нового индикатора. Внешних параметров у него не будет, вы сможете их добавить сами после прохождения урока при необходимости. Свойство программы #property должно отображать индикатор на графике (indicator_chart_window), а не в отдельном окне. Функции обработки событий OnInit и OnDeinit пока оставляем пустыми. Переходим к функции обработки событий OnCalculate.

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

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

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

Далее прописываем условие в OnCalculate

Идет сравнение сохраненного значения ордеров в память с фактическим количеством ордеров в истории. Если оно изменяется — мы перезаписываем значение переменной TotalHistoryOrders и выполняем все дальнейшие действия этого урока в фигурных скобках данного условия.

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

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

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

Слово struct обозначает объявление самой структуры, TradeEnv — ее имя. В фигурных скобках идет объявление переменных разного типа, которые нам пригодятся:

  • symbol — наименование валютной пары, которые мы найдем в истории.
  • countB, countS — переменные целого типа для подсчета количества ордеров отдельно для покупок и продаж.
  • profitB, profitS — суммарный прибыль в валюте депозита отдельно для покупок и продаж.
  • LotB, LotS — торговый лот всех ордеров отдельно для покупок и продаж.
  • win, loss — количество прибыльных и убыточных сделок.

Всю эту информацию мы будем хранить в массиве, потому что у нас, скорее всего в торговле участвует не одна, а несколько валютных пар. Поэтому назначаем динамический массив для этой структуры — tradeenv[], количество элементов которого будет изменяться по мере нахождения в истории новых символов. Вывести отдельно элемент структуры можно через точку, например: tradeenv[0].LotB будет содержать в себе информацию для первого элемента массива под номером ноль о торговом лоте для покупок. 

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

Мы добавили две строчки. Функция ArrayResize задает размер массиву tradeenv, это обязательное правило для динамических массивов. Так как мы не знаем, сколько в нем будет элементов, то размер задаем равный единице.

Функция ZeroMemory обнуляет значение массива tradeenv. Со значениями данного массива обнуляется и структура со всеми ее переменными. То есть этими действиями мы отчищаем данные с массива, чтобы начать в него сохранять новые значения после того, как количество ордеров на истории изменится.

Помимо подсчета значений для разных валютных пар мы будем также сохранять общее значение прибыли, ордеров и лота. Поэтому ниже вручную задаем самому первому элементу массива (нулевому) наименование символа «TOTAL».

То есть самый первый элемент массива у нас всегда будет хранить суммарное значение всех ордеров. Точка и приписка symbol символизирует о том, что мы использовали string элемент структуры для сохранения наименования валютной пары. Другие наименования символов будут сохраняться в последующих элементах массива: tradeenv[1].symbol, tradeenv[2].symbol и т.д.

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

Теперь приступаем непосредственно к анализу. Нам нужно запустить цикл for для всех ордеров, которые хранятся в истории.

Останавливаться подробно на цикле for не будет, его работы мы подробно разбирали в соответствующем уроке. Анализ идет с 0 позиции по значение количества ордеров в памяти, минус один, т.к. отсчет цикла идет с нуля, а ордера в истории учитываются с единицы.

Как и при анализе открытых ордеров, закрытый ордер вначале нужно выделить (OrderSelect). Выделение происходит по номеру позиции SELECT_BY_POS, источником данных тут является история (MODE_HISTORY). Если выделить не получилось — все дальнейшие действия бессмысленны, поэтому активируется оператор продолжения continue.

Окей, ордер был выделен, теперь нужно отсеять лишние типы ордеров. Нас интересуют только рыночные ордера, которые были закрыты с прибылью или убытком. Соответственно, если тип ордера OrderType больше единицы, значит это либо удаленный отложенный ордер, либо операция с балансом, что нас никак не интересует. Если же это так, активируем оператор continue.

Так как мы несколько раз будет использовать значение прибыли/убытка анализируемого ордера, то логичнее его сохранить в отдельную переменную

Как я уже упоминал, помимо торговой функции OrderProfit(), которая хранит в себе непосредственно информацию о прибыли или убытке ордера в валюте депозита, на счете есть еще две функции, связанные с деньгами: OrderSwap() — хранит в себе уплоченный своп при переносе позиции в ролловер и OrderCommission() — комиссия, которую мы оплатили брокеру при открытии сделки. Если вас эти дополнительные комиссии не интересуют, можете их не прибавлять к значению прибыли OrdProfit.

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

Так как в истории хранится много ордеров по одному и тому же символу, мы должны сохранять все их данные в общей ячейке массива tradeenv. Т.е. если по EURUSD у нас 20 ордеров на покупку, то нас интересует только суммарный лот и профит этих ордеров. Для этой пары должна быть выделена отдельная ячейка, для NZDCAD другая и т.д. Для этого нам необходимо внутри цикла по ордерам создать еще один цикл для анализа всех элементов имеющегося массива. В нем проверяется условие, если символ массива tradeenv[j].symbol совпадает с символом текущего ордера, тогда мы нашли искомый номер массива и сохраняем его порядковый номер (j) в переменную symbolnumber, которую необходимо предварительно объявить в самом начале функции OnCalculate до начала анализа. 

Далее для переменной bool found выставляется значение true и цикл прерывается. Мы нашли номер ячейки массива с текущим символом и будем его использовать в дальнейшем для сохранения данных по текущему ордеру. Но что, если данного символа еще нет в памяти массива, а анализ закрытых ордеров только начался?

Тогда логическое значение переменной found остается равным false и в работу включается условие if(!found). В фигурных скобках условия прописывается порядковый номер symbolnumber для массива tradeenv. Он будет равен размеру текущего массива ArraySize(tradeenv). То есть если размер массива был равен 1 и в нем был задействована только нулевая ячейка, то теперь текущему символу передается первая ячейка, а размер динамического массива нужно увеличить на единицу ArrayResize(tradeenv,symbolnumber+1). Массив стал больше на один элемент — новый символ.

Осталось сохранить обнаруженный символ как элемент структуры tradeenv[symbolnumber].symbol. Соответственно при следующей итерации цикла for наш массив будет уже больше и поиск нового символа будет происходить в уже увеличенном массиве.

Если более простым языком, то при начале анализе ордеров в истории массив tradeenv пока что пустой. В момент анализа первого выделенного ордера мы узнаем, что его символ USDJPY. Так как в массиве еще нет данных, то мы сохраняем этот символ в 1 ячейку массива и увеличиваем его размер. На следующей итерации у нас появляется пара AUDNZD. Ее не было до этого в массиве, поэтому она сохраняется во 2 ячейку и размер массива уже становится равным трем (включая нулевую ячейку). Следующая итерация — пара USDJPY, она уже есть в памяти массива, поэтому его размер не изменяется, мы просто запоминаем, что пара уже есть в 1 ячейке и с ней в дальнейшем продолжим работать во время данной итерации. 

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

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

Ранее я говорил, что суммарную информацию для всех валютных пар мы будем сохранять в самой первой ячейке массива, т.е. нулевой и имя символа будет «TOTAL». Соответственно для tradeenv[0] массива мы отдельно произведем сохранение в:

  • countB — счетчик количества ордеров на покупку. Каждую итерацию к элементу будет прибавляться единица, когда ордер будет на покупку.
  • LotB — суммарный торговый лот для всех закрытых ордеров. К нему прибавляем размер лота текущего ордера OrderLots().
  • profitB — прибыль всех ордеров на покупку. Прибавляем к этой переменной ранее объявленную переменную OrdProfit.

Этими тремя строками мы произвели действия для нулевой ячейки массива tradeenv, сохранив данные для раздела ИТОГО. Для того, чтобы сделать такое вычисление только для текущей валютной пары, нужно использовать уже определенный нами номер ячейки symbolnumber. Те же три строчки кода повторяются и для нее.

С продажами все аналогично:

Осталось выполнить последний анализ — определить прибыльным или убыточным является выделенный ордер и сохранить его в отдельную переменную.

Если переменная OrdProfit, содержащая в себе значение прибыли/убытка больше или равна нулю, то мы считаем, что данный ордер был «выигрышным» и прибавляем единицу к счетчику win как для total значения всех анализируемых ордеров, так и для текущего символа. Если же нет, то прибавление идет к счетчику «проигрышных» ордеров loss.

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

Ваша задача сейчас правильно перенести полученный код в редактор Meta Editor и скомпилировать код без ошибок.

Построение графических элементов на экране

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

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

Само тело функции добавляем после функции обработки событий OnCalculate. Она будет иметь вид:

Соответственно, если объект будет содержать в себе название DV (сокращенно от DaVinci), значит этот объект был создан нашим индикатором и его нужно удалить. Данная функция выполняет проход по всем графическим элементам на текущем графике через цикл for. Далее с помощью строковой функции StringFind в имени выделенного объекта ObjectName(i) ищется искомое слово search (в нашем случае это «DV«) и если оно найдено — объект удаляется благодаря функции ObjectDelete. и так для всех элементов цикла.

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

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

Чтобы показать, что мы не дилетанты, мы не будем создавать просто один невзрачный прямоугольник. Мы создадим пять!

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

Создание графических объектов каждого отдельного типа мы будем делать через пользовательские функции. В справочнике сайта MQL5 есть отдельный раздел «Типы объектов» где уже созданы и описаны эти функции.

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

Так как у нас будет использоваться пять прямоугольников, то мы пять раз вызываем функцию RectLabelCreate, но предварительно мы зададим изначальные координаты X и Y на графике для построения объектов.

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

Переменные Y0, Y_, X0 и X_ это начальные координаты, отступ от левой верхней границы графика. Они также подбираются вручную.

Вот что из себя представляет функция RectLabelCreate:

Тип данных функции bool, т.е. она возвращает true/false. Но нам это не важно для текущей задачи.

Весь этот код я описывать не буду, под каждым свойством объекта есть приписка из справочника, которая объясняет его действие. Если вкратце, то вначале создается объект (ObjectCreate). В нашем случае это OBJ_RECTANGLE_LABEL, т.е. прямоугольный лейбл. По условию, он создается только в том случае, если прямоугольника с таким именем еще нет на графике. Далее этому объекту задаются свойства: координаты X и Y, ширина и высота, цвет заливки, угол привязки к этому прямоугольнику, стиль рамки, ее толщина, выбор плана отображения (передний или задний), возможность выделять объект мышью, изначально выделять объект или нет, спрятать ли его имя в списке объектов и приоритет размещения на графике. Свойства для большинства других объектов аналогичны.

Соответственно из функции OnCalculate мы передаем следующие значения в эту функцию:

Где по порядку через запятую идет:

  • ID графика для построения графической фигуры. Текущий график обозначается как нулевой.
  • Наименование элемента. Оно может быть любым, я использовал «DV|Rect» с цифрами от 1 до 5 для пяти прямоугольников
  • Подокно. Нужно только, если вы используете подвальный индикатор. В нашем случае пишется ноль.
  • Координаты X и Y для начала построения прямоугольника. X=5, Y=16. Все подбирается на глаз.
  • Ширина в пикселях (910), высота в пикселях (30).
  • Цвет заливки прямоугольника. Я выбрал темно серый.
  • Тип границы обводки. Их три вида — плоский, выпуклый и вогнутый. В справочнике находятся как ENUM_BORDER_TYPE.
  • Угол привязки к графику. Мы строим в верхнем левом углу, его номер привязки 0. Для верхнего правого угла номер 1 и т.д.

Чтобы узнать заранее высоту для 2-5 прямоугольников, нужно заданный отступ Y0 умножить на количество элементов массива ArraySize(tradeenv)+1, т.к. оно уже определено и будет неизменным.

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

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

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

Тут создается (ObjectCreate) тип графического объекта OBJ_LABEL, если он не был найден на графике. Ему по порядку задается угол привязки к графику, возможность выделения мышью, якорь привязки к самому объекту (я сделал по центру), дистанция X и Y. С помощью свойства ObjectSetText созданному объекту передается текст, размер шрифта, тип шрифта и цвет.

Попробуем на наш фон нанести первые три надписи:

Три лейбла будут обозначать колонки для покупок, продаж и итого. В функцию мы передаем имя объекта («DV|Title»), тест для надписи, координаты для построения и цвет.

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

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

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

Объявили строковый массив titles, который хранит в себе слова Symbol, Trades, Lots и т.д. Зная, что у нас 12 элементов, запустили цикл for от 0 до 11 в котором создаем лейблы, задавая им имя равное DV|Title_ плюс номер итерации. Текстом является порядковый номер массива titles. Координата Y0 у нас остается неизменной, потому что перечень весь идет в одну строчку. В конце каждой итерации мы изменяем координату X_, чтобы объекты не наложились друг на друга и сдвигались вправо.

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

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

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

  1. tradeenv[j].symbol — текущее наименование символа для текста.
  2. tradeenv[j].countB — количество ордеров на покупку из памяти массива. Чтобы перевести целое число в текст мы используем функцию IntegerToString.
  3. tradeenv[j].LotB — суммарный лот всех ордеров на покупку для выбранного символа. Чтобы перевести дробное число в текст мы используем функцию DoubleToString и указываем количество знаков для округления.
  4. tradeenv[j].profitB — суммарная прибыль/убыток всех ордеров по текущему символу на покупку.
  5. tradeenv[j].countS — количество ордеров на продажу.
  6. tradeenv[j].LotS — лот всех ордеров на продажу.
  7. tradeenv[j].profitS — прибыль/убыток всех ордеров на продажу.
  8. tradeenv[j].countB+tradeenv[j].countS — складываем количество ордеров на покупки и продажу для определения суммарного значения для последней колонки таблицы Total.
  9. tradeenv[j].LotB+tradeenv[j].LotS — суммируем лоты для покупки и продажи для последней колонки таблицы Total
  10. tradeenv[j].profitB+tradeenv[j].profitS — суммируем прибыль/убыток для покупки и продажи для последней колонки таблицы Total

Чтобы указать не только количество прибыльных и убыточных ордеров отдельно, но и их процент от общего числа, мы создадим переменные win_prc и loss_prc с простой формулой расчета, описанной выше в коде.

    11. IntegerToString(tradeenv[j].win) + » (» + DoubleToString(win_prc,0) + «%)» — количество прибыльных ордеров и их процент от общего количества.

    12. IntegerToString(tradeenv[j].loss) + » (» + DoubleToString(loss_prc,0) + «%)» — количество убыточных ордеров и их процент от общего количества.

После каждого созданного лейбла необходимо сдвигаться по оси X вправо (X_ += 75). В конце каждой итерации необходимо восстанавливать значение переменной X, а также смещаться на определенную строчку вниз по координате Y.

Итоговая таблица будет иметь вот такой вид:

В таблице отображаются результаты торговли нашего советника Night Hawk. Сравним ее результаты с данными сайта MyFxBook:

Сортировка немного отличается, но количество ордеров и прибыль сходятся. А значит работа выполнена корректно.

Заключение

Мы разобрали пример анализа закрытых ордеров и отображения части информации о них. Все координаты X и Y были подобраны на глаз по диагонали монитора. Они введены только для примера, начинающий программист сможет их поменять по своему вкусу. Также мною не были добавлены в таблицу отдельно такие колонки, как: размер свопа, комиссии, средний Тейк Профит или Стоп Лосс, удаленные отложки, прибыль в пунктах и т.д. Тут я уже оставляю простор для творчества вам.

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

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

Если же вам понравилась статья или данный индикатор вам пригодится в коллекции, то поделитесь со знакомыми информацией о нашем сайте. 

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

0. Начало работы

Скачать код урока

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Заполните поле
Заполните поле
Пожалуйста, введите корректный адрес email.
Вы должны согласиться с условиями для продолжения

Меню