Визуализации сделок на графике (Часть 2): Графическая отрисовка информации
Введение
В этой статье мы завершим написание скрипта для визуализации сделок на графике, начатое в первой статье "Визуализации сделок на графике (Часть 1): Выбор периода для анализа". Мы напишем код для выбора данных по одной выбранной пользователем сделке и создадим код для отрисовки необходимых информационных объектов на графике, которые в дальнейшем будем сохранять в файл как принт-скрин соответствующих графиков. Данный скрипт позволит вам существенно сэкономить время на техническую работу, связанную с формированием графиков своих сделок и сохранением их в принт-скрины для ретроспективного анализа. Тот, кто не хочет тратить время на сборку проектов, может скачать уже готовую версию этого скрипта в Маркете.
Выбор данных по одной сделке
В отличие от выбора данных по сделкам за определённый период, выбор данных по одной сделке существенно упростит реализацию кейса выбора исторических ордеров. Основным отличием здесь будет то, что для запроса исторических данных, вместо предопределённой функции терминала HistorySelect(), мы будем использовать метод HistorySelectByPosition(). В параметры этого метода нужно передать идентификатор позиции POSITION_IDENTIFIER, который пользователь сможет найти в терминале Metatrader5 в разделе контекстного меню "Вид" -> "Инструменты" -> "История" -> "Столбец Тикет" и передать скрипту это значение через входную глобальную переменную inp_d_ticket.
В остальном логика кейса Select_one_deal будет полностью повторять реализацию логики предыдущего кейса, и в полном изложении представлена в коде ниже по тексту, с теми же информационными вставками для пользователя.
//---если нужна одна сделка case Select_one_deal: res = MessageBox("You have selected analysis of one deal. Continue?","",MB_OKCANCEL); // проинформировали в сообщение if(res == IDCANCEL) // если прервано пользователем { printf("%s - %d -> Scrypt was stoped by user.",__FUNCTION__,__LINE__); // проинформировали в журнал return; // прервали } MessageBox("Please press 'Ok' and wait for the next message until script will be done."); // проинформировали в сообщение //---выбираем по одной позиции if(HistorySelectByPosition(inp_d_ticket)) // выбираем позицию по id { int total = HistoryDealsTotal(); // общее кол-во сделок if(total <= 0) // если ничего не найдено { printf("%s - %d -> Deal was not found.",__FUNCTION__,__LINE__); // проинформировали MessageBox("Deal was not found with this tiket: "+IntegerToString(inp_d_ticket)+". Script is done."); // проинформировали в сообщение return; } for(int i=0; i<total; i++) // идём по количеству сделок { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // взяли номер сделки { //--- get deals properties position_id = HistoryDealGetInteger(ticket,DEAL_POSITION_ID); // взяли основной id entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);// вход или выход? if(entry == DEAL_ENTRY_IN) // если это вход { open = HistoryDealGetDouble(ticket,DEAL_PRICE); // взяли цену открытия time_open =(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); // взяли время открытия symbol=HistoryDealGetString(ticket,DEAL_SYMBOL); // взяли символ stop_loss = HistoryDealGetDouble(ticket,DEAL_SL); // взяли стоп-лосс take_profit = HistoryDealGetDouble(ticket,DEAL_TP); // взяли тейк-профит //--- magic = (int)HistoryDealGetInteger(ticket,DEAL_MAGIC); // взяли Magic comment=HistoryDealGetString(ticket,DEAL_COMMENT); // взяли комментарий externalID=HistoryDealGetString(ticket,DEAL_EXTERNAL_ID); // взяли внешний id volume = HistoryDealGetDouble(ticket,DEAL_VOLUME); // взяли объем commission = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // взяли размер комиссии } if(entry == DEAL_ENTRY_OUT) // если это выход { close = HistoryDealGetDouble(ticket,DEAL_PRICE); // взяли цену закрытия time_close =(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);// взяли время закрытия //--- reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); // взяли причину swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // взяли своп profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // взяли профит fee = HistoryDealGetDouble(ticket,DEAL_FEE); // взяли налог } //---вносим данные в основное хранилище //---проверяем есть ли такой id if(Find(PositionID,position_id)==-1) // если такого нет, { //---меняем размерности массивов ArrayResize(arr_time_open,ArraySize(arr_time_open)+1); // время открытия ArrayResize(arr_time_close,ArraySize(arr_time_close)+1); // время закрытия ArrayResize(arr_symbol,ArraySize(arr_symbol)+1); // символы ArrayResize(arr_stop_loss,ArraySize(arr_stop_loss)+1); // стопы ArrayResize(arr_take_profit,ArraySize(arr_take_profit)+1);// профиты ArrayResize(arr_open,ArraySize(arr_open)+1); // открытия ArrayResize(arr_close,ArraySize(arr_close)+1); // закрытия ArrayResize(PositionID,ArraySize(PositionID)+1); // id позиции //--- ArrayResize(arr_magic,ArraySize(arr_magic)+1); // Magic ArrayResize(arr_extermalID,ArraySize(arr_extermalID)+1); // внешний id ArrayResize(arr_comment,ArraySize(arr_comment)+1); // комментарий ArrayResize(arr_volume,ArraySize(arr_volume)+1); // объем ArrayResize(arr_commission,ArraySize(arr_commission)+1); // комиссия ArrayResize(arr_reason,ArraySize(arr_reason)+1); // причина ArrayResize(arr_swap,ArraySize(arr_swap)+1); // своп ArrayResize(arr_profit,ArraySize(arr_profit)+1); // профит ArrayResize(arr_fee,ArraySize(arr_fee)+1); // налог PositionID[ArraySize(arr_time_open)-1]=position_id; // id if(entry == DEAL_ENTRY_IN) // если это вход { arr_time_open[ ArraySize(arr_time_open)-1] = time_open; // время сделки arr_symbol[ ArraySize(arr_symbol)-1] = symbol; // символ инструмента arr_stop_loss[ ArraySize(arr_stop_loss)-1] = stop_loss; // стоп-лосс сделки arr_take_profit[ ArraySize(arr_take_profit)-1] = take_profit; // тейк-профит сделки arr_open[ ArraySize(arr_open)-1] = open; // цена открытия //--- arr_magic[ ArraySize(arr_magic)-1] = magic; // Magic arr_comment[ ArraySize(arr_comment)-1] = comment; // комментарий arr_extermalID[ ArraySize(arr_extermalID)-1] = externalID; // внешний id arr_volume[ ArraySize(arr_volume)-1] = volume; // объем arr_commission[ ArraySize(arr_commission)-1] = commission; // комиссия } if(entry == DEAL_ENTRY_OUT) // если это выход { arr_time_close[ ArraySize(arr_time_close)-1] = time_close; // время закрытия arr_close[ ArraySize(arr_close)-1] = close; // цены закрытия //--- arr_reason[ ArraySize(arr_reason)-1] = reason; // причина arr_swap[ ArraySize(arr_swap)-1] = swap; // своп arr_profit[ ArraySize(arr_profit)-1] = profit; // профит arr_fee[ ArraySize(arr_fee)-1] = fee; // налог } } else { int index = Find(PositionID,position_id); // если запись уже была, if(entry == DEAL_ENTRY_IN) // если это был вход { arr_time_open[index] = time_open; // время сделки arr_symbol[index] = symbol; // символ инструмента arr_stop_loss[index] = stop_loss; // стоп-лосс сделки arr_take_profit[index] = take_profit; // тейк-профит сделки arr_open[index] = open; // цена открытия //--- arr_magic[index] = magic; // Magic arr_comment[index] = comment; // комментарий arr_extermalID[index] = externalID; // внешний id arr_volume[index] = volume; // объем arr_commission[index] = commission; // комиссия } if(entry == DEAL_ENTRY_OUT) // если это выход { arr_time_close[index] = time_close; // время закрытия сделки arr_close[index] = close; // цена закрытия сделки //--- arr_reason[index] = reason; // причина arr_swap[index] = swap; // своп arr_profit[index] = profit; // профит arr_fee[index] = fee; // налог } } } } } else { printf("%s - %d -> Error of selecting history deals: %d",__FUNCTION__,__LINE__,GetLastError()); // проинформировали в журнал printf("%s - %d -> Deal was not found.",__FUNCTION__,__LINE__); // проинформировали в журнал MessageBox("Deal was not found with this tiket: "+IntegerToString(inp_d_ticket)+". Script is done."); // проинформировали в сообщение return; } break;
Теперь, когда оба варианта описаны, и все хранилища по ходу выполнения программы заполнены необходимыми данными, можем приступать к отрисовке этих данных на графиках терминала.
Отрисовка необходимых графиков
Для сохранения сделок на графике нам нужно будет предварительно на программном уровне открыть новое окно с нужным инструментом, сделать необходимые настройки оформления, в том числе индивидуальное смещение отступа справа, чтобы вся сделка была хорошо видна, и вызвать предопределенную функцию, которая сохранит нам принт-скрин в нужную папку.
В первую очередь объявим локальные переменные, необходимые нам для открытия окна нужного графика. В переменной bars будет храниться значение смещения для графика справа, в переменных chart_width и chart_height будут храниться соответствующие размеры для сохранения, а хендл нового графика, когда он будет открыт, будем хранить в переменной handle для обращения к графику в дальнейшем.
//---данные собраны, переходим на распечатку int bars = -1; // количество баров смещения int chart_width = -1; // ширина графика int chart_height =-1; // высота графика long handle =-1; // хендл графика
Перед началом запроса на открытие новых окон инструментов, нам обязательно нужно сделать запросы на валидность этих инструментов из истории. Данная проверка будет обязательно необходима, чтобы избежать ошибки открытия "несуществующего символа" по счёту. Думаю, здесь нужно отдельно пояснить, откуда же может взяться "несуществующий символ", если в торговой истории он зафиксирован, а значит, когда-то существовал.
В первую очередь это может быть связано с типами счетов брокера. На сегодняшний момент большинство брокеров предоставляет трейдерам несколько вариантов счетов, чтобы максимально сделать выгодным и удобным их использование с точки зрения применяемых торговых стратегий. На каких-то счетах взимается комиссия за открытие сделок, при этом очень низкий уровень спреда, а на каких-то типах счетов высокий спред, но при этом отсутствует плата за каждую сделку. Поэтому те трейдеры, что торгуют среднесрочно, могут не платить комиссию за сделку, при этом размер спреда при среднесрочной торговле не так уж и важен. А трейдеры, которые торгуют внутри дня небольшие импульсы, скорее предпочитают заплатить комиссию за открытие сделки, чем получить убыток по своей сделке только из-за того, что спред "вдруг" расширился. Как правило, брокеры упаковывают такие условия в типы счетов вроде Standart, Gold, Platinum, ESN и для каждого счёта вводят свое имя символа. Скажем, для пары EURUSD на стандартном счете символ на другом типе счета может выглядеть как EURUSDb, EURUSDz или EURUSD_i в зависимости от брокера.
Также имена символов могут меняться в зависимости от срока экспирации определённых инструментов, не связанных с торговлей валютных пар на форекс, но этот момент мы подробно рассматривать в этой статье не будем, так как основная часть всё же посвящена именно валютным парам.
И ещё одним условием необходимости проверки на валидность инструмента является чисто техническое отсутствие подписки на нужные инструменты в окне Обзор рынка терминала. Если даже имя символа существует на авторизованном счёте, но не выбрано в пункте контекстного меню терминала "Вид" -> "Обзор рынка", мы не сможем открыть данный график с последующей ошибкой вызывающей функции.
Реализацию проверки начнём с организации цикла перебора каждого инструмента в нашем хранилище, как показано ниже.
for(int i=0; i<ArraySize(arr_symbol); i++) // идём по всем символам сделок
Для проверки валидности инструмента, сохранённого в нашем контейнере, мы будем использовать предопределенную функцию терминала SymbolSelect(). Первым параметром мы будем передавать в неё имя инструмента в формате string, валидность которого мы хотим проверить, а вторым передадим логическое значение true. Передача вторым параметром значения true будет означать для терминала, что если данный инструмент валидный, но не выбран в "Обзоре рынка", его нужно туда обязательно выбрать автоматически. Полная логика проверки будет выглядеть, как показано ниже.
//---делаем проверку на доступность инструментов for(int i=0; i<ArraySize(arr_symbol); i++) // идём по всем символам сделок { if(!SymbolSelect(arr_symbol[i],true)) // проверяем есть ли символ в книге и добавляем если нет { printf("%s - %d -> Failed to add a symbol %s to the marketbook. Error: %d", __FUNCTION__,__LINE__,arr_symbol[i],GetLastError()); // проинформировали в журнал MessageBox("Failed to add a symbol to the marketbook: "+arr_symbol[i]+ ". Please select 'show all' in the your market book and try again. Script is done."); // проинформировали в сообщение return; // если не удалось, прерываем } }
Соответственно, если проверка на валидность инструмента не пройдена, мы прерываем выполнение программы с соответствующими уведомлениями для пользователя. Когда все проверки на валидность были пройдены, мы можем переходить к открытию необходимых графиков инструментов прямо в терминале.
В первую очередь предусмотрим вспомогательную переменную deal_close_date типа данных MqlDateTime, которая в дальнейшем поможет нам удобно отсортировать все сохраненные графики по соответствующим папкам временных периодов. Для явного приведения типа данных datetime в нашем хранилище в тип данных MqlDateTime, будем использовать предопределённую функцию терминала TimeToStruct(), как показано ниже.
MqlDateTime deal_close_date; // дата закрытия сделки в структуре TimeToStruct(arr_time_close[i],deal_close_date); // переводим дату в структуру
Прорисовку графиков будем делать в соответствии с введёнными пользователем данными в переменных main_graph, addition_graph, addition_graph_2 и addition_graph_3. Если в переменной находится значение перечисления PERIOD_CURRENT, мы не рисуем никакой график, если же в переменной введено конкретное значение, например PERIOD_D1, мы берём этот график на отрисовку. Данную проверку сделаем по всем введённым переменным в следующем виде, например, как по основному:
//---смотрим основной if(main_graph != PERIOD_CURRENT) // если выбран основной
Процедура отрисовки каждого графика будет начинаться с открытия нового графика нужного инструмента. Открывать новый график инструмента будем с помощью предопределенной функции терминала ChartOpen(), передав в её параметры символ нужного инструмента из хранилища и требуемый пользователем тайм-фрейм, как показано ниже.
//---открываем нужный график handle = ChartOpen(arr_symbol[i],main_graph); // открыли график нужного символа
Когда график открыт, применим к нему все стандартные настройки пользователя, о которых мы говорили выше. Для этого будем использовать предопределённую функцию терминала ChartApplyTemplate(), которая нам очень в этом поможет и избавит от самостоятельного написания кода. В параметры функции ChartApplyTemplate() передадим хендл графика, полученный от вызова функции ChartOpen(), и имя шаблона, указанного пользователем для тайм-фрейма сделки в формате "dailyHistorytemp". Код для вызова функции применения шаблона представлен далее.
ChartApplyTemplate(handle,main_template); // применяем шаблон
Сделаем небольшое отступление для тех, кто до настоящего момента не пользовался шаблонами в терминале Metatrader5, только из-за того, что если применить "некрасивый" шаблон, сохраненный принт-скрин сделки может оказаться как минимум "раздражающим", а возможно даже и просто "бесполезным". Чтобы создать самостоятельно шаблон "dailyHistorytemp" выполните следующие действия:
- Откройте любой график любого инструмента через контекстное меню "Файл" - "Новый график".
- Когда график откроется, нажмите F8 и у вас появится новое окно Свойства, например "СвойстваGBPAUD,Daily".
- Окно "Свойства" содержит несколько вкладок "Общие", "Показывать" и "Цвета", на каждом из них сделайте те настройки, которые будут вам более привычны – например, для дневного графика, и нажмите кнопку ОК. Подробнее про настройку можно посмотреть здесь - настройка графиков официальная справка по терминалу.
- После нажатия кнопки ОК, окно свойств будет закрыто, а график примет нужный вам вид.
- Теперь в контекстном меню выбираем "Графики" - "Шаблоны" - "Сохранить шаблон", и появляется окно сохранения шаблона, где в текстовое поле "Имя файла" вы вписываете "dailyHistorytemp.tpl" и нажимаете кнопку "Сохранить".
- После этого в папке терминала ..MQL5\Profiles\Templates появится файл "dailyHistorytemp.tpl", который вы можете использовать в скрипте. Главное обратите внимание, что в скрипт имя шаблона вводится без расширения .tpl, достаточно просто имени.
Теперь вернёмся к нашему коду. Когда нужный шаблон был применен, нам нужно сделать небольшую задержку в выполнении кода, чтобы дать время графику прогрузиться в нужном качестве. В противном случае, график может отображаться некорректно из-за необходимого времени загрузки нужных данных исторических цен в терминале. Например, если вы этот график в терминале давно не открывали, и терминалу требуется время для его корректного отображения. Задержку времени будем объявлять через предопределённую функцию терминала Sleep(), как показано ниже.
Sleep(2000); // ждём пока прогрузится график
В качестве задержки мы будем использовать значение 2000 миллисекунд или 2 секунды, взятые чисто из практики, чтобы график гарантированно успел прогрузиться, а исполнение скрипта не достигало долгих минут при большом количестве сделок. Для самостоятельной настройки этого значения можете самостоятельно вынести это значение в параметры настройки скрипта, чтобы ускорить или замедлить процесс, в зависимости от производительности вашего оборудования или интернет соединения. Как показывает практика, двух секунд будет достаточно для большинства кейсов.
Теперь нам нужно запретить прокручивание графиков на самые свежие значения баров, так как мы анализируем историю и нам не нужно, чтобы новые тики смещали наш график всё время вправо. Сделать это можно путем установки свойства CHART_AUTOSCROLL в значение false нужного графика через предопределенную функцию ChartSetInteger(), как показано далее.
ChartSetInteger(handle,CHART_AUTOSCROLL,false); // убрали автопрокрутку
Теперь, когда автопрокрутка запрещена, для смещения графика в сторону истории на период закрытия рассматриваемой сделки, в первую очередь нам нужно посчитать количество баров на графике соответствующего тайм-фрейма влево. Получить данное значение мы можем через предопределённую функцию терминала iBarShift(), передав в неё в качестве параметров символ нашего инструмента, тайм-фрейм графика, время закрытия сделки, так как мы же хотим видеть на принт-скрине всю сделку от начала и до конца. А также в параметр exact мы передаём значение false на всякий случай, если история будет уж очень глубокой. Но в данном случае это не настолько критично для нашей реализации. Полный вызов метода с параметрами показан ниже.
bars = iBarShift(arr_symbol[i],main_graph,arr_time_close[i],false); // получили смещение на время сделки
Когда мы узнали нужное нам смещение графика, мы можем отобразить именно тот период, который будет захватывать нужную нам сделку в истории. Сместить график в нужном направлении на нужное нам расстояние мы можем с помощью предопределённой переменной терминала ChartNavigate(), передав в неё следующие параметры, как показано далее.
ChartNavigate(handle,CHART_CURRENT_POS,-bars+bars_from_right_main); // сместили график с пользовательским запасом
Чтобы сместить график, мы передали хендл графика для смещения, значение текущей позиции CHART_CURRENT_POS перечисления ENUM_CHART_POSITION, а также полученное нами ранее смещение к сделке в переменной bars с отступом, введённым пользователем для оценки потенциала движения цены, после его выхода из позиции.
После описанных преобразований с графиком, на всякий случай, вызовем метод ChartRedraw() и можем приступать к отрисовке дополнительных данных на графике для анализа исторических сделок.
Для отрисовки пользовательских элементов панели информации и линий, указывающих на открытие, закрытие позиций и положения Stop Loss и Take Profit, будем использовать соответствующие пользовательские функции paintDeal() и paintPanel(). Мы сами будем их определять на основании стандартных шаблонов поведения работы с графиками в терминале, где paintDeal() будет рисовать линии цен открытия и закрытия сделки, а также Take Profit и Stop Loss , а метод paintPanel() будет содержать таблицу, содержащую полную информацию, полученную по сделке в углу экрана.
Подробное определение двух этих методов мы будем осуществлять в следующей отдельной главе "Отрисовка информационных объектов на графиках", а чтобы завершить эту главу, просто обозначим, что эти методы будут вызываться именно в этом месте нашего кода в следующем виде. Это сделано также и с той позиции, что вам необязательно использовать реализацию, указанную в текущей статье, для отрисовки этих двух групп элементов. Вы можете самостоятельно переопределить их у себя, сохранив нужную сигнатуру. Реализация этих методов в текущей статье является примером оптимального соотношения красоты и информативности графики ко времени её написания в коде. Главное сохранить положение вызова методов по основному коду в этой части.
//---прорисовываем сделку paintDeal(handle,PositionID[i],arr_stop_loss[i],arr_take_profit[i],arr_open[i],arr_close[i],arr_time_open[i],arr_time_close[i]); //---прорисовываем информационную панель paintPanel(handle,PositionID[i],arr_stop_loss[i],arr_take_profit[i],arr_open[i], arr_close[i],arr_time_open[i],arr_time_close[i],arr_magic[i],arr_comment[i], arr_extermalID[i],arr_volume[i],arr_commission[i],arr_reason[i],arr_swap[i], arr_profit[i],arr_fee[i],arr_symbol[i],(int)SymbolInfoInteger(arr_symbol[i],SYMBOL_DIGITS));
После того, как методы отрисовали линии сделки и панель информации по ней на графике, мы можем приступать к реализации сохранения принт-скрина всего, что получилось на текущем графике. Для этого сначала определим будущие размеры принт-скрина по ширине и высоте, просто запросив эти данные с открытого графика с помощью предопределённой функции терминала ChartSetInteger(), как показано ниже.
//---получаем данные по размеру экрана chart_width = (int) ChartGetInteger(handle,CHART_WIDTH_IN_PIXELS); // смотрим ширину графика chart_height = (int) ChartGetInteger(handle,CHART_HEIGHT_IN_PIXELS); // смотрим высоту графика
В качестве соответствующих параметров для отображения графика мы передавали значения перечисления ENUM_CHART_PROPERTY_INTEGER для ширины CHART_WIDTH_IN_PIXELS, а для высоты CHART_HEIGHT_IN_PIXELS соответственно.
Получив данные по размерам, нам нужно будет сформировать путь сохранения графика принт-скрина сделки в стандартной папке терминала. Чтобы советник не помещал все файлы в одну папку, а сортировал их для удобства пользователя, автоматизируем этот процесс через имя файла в следующей строке.
string name_main_screen = brok_name+"/"+ IntegerToString(account_num)+"/"+ IntegerToString(deal_close_date.year)+"-"+IntegerToString(deal_close_date.mon)+ "-"+IntegerToString(deal_close_date.day)+"/"+ IntegerToString(PositionID[i])+"/"+ EnumToString(main_graph)+IntegerToString(PositionID[i])+".png"; // дали имя
Графически структура сортировки файлов по папкам в стандартном каталоге будет такой, как показано на рисунке 1.
Рисунок 1. Структура адресов папок сохраненных принт-скринов по сделкам
Как видим, файлы графиков будут рассортированы по имени брокера, номеру счета, году, месяцу и дню проведения, чтобы пользователю не составило труда найти нужную сделку и не искать имя файла в одном общем списке. Разные тайм фреймы будут располагаться в папке соответствующего номера позиции из терминала.
Непосредственное сохранение информации произведём через вызов предопределённой функции терминала ChartScreenShot(), передав в неё в качестве параметров хендл нужного графика, полученные нами ранее размеры принт-скрина, которые соответствуют размерам графика, и также имя файла, содержащего всю структуру адресов папок, как показано на Рисунке 1 и в коде ниже.
ChartScreenShot(handle,name_main_screen,chart_width,chart_height,ALIGN_LEFT); // делаем скрин
Если указанных в иерархии папок не существует в стандартной папке терминала, терминал создаст их самостоятельно, без участия пользователя.
После завершения сохранения файла мы можем закрывать график, чтобы не засорять вид терминала, особенно если выгрузка насчитывает большое количество исторических сделок по счёту. Закрытие графика осуществим через предопределённую функцию терминала ChartClose(), передав в неё в параметр хендл нужного графика, чтобы не закрыть ничего лишнего. Вызов функции представлен далее.
ChartClose(handle); // закрыли график
Данную операцию мы будем повторять аналогично по всем тайм-фреймам, указанным пользователем во входных параметрах. Теперь, для завершения нашего скрипта, осталось лишь определить поведение методов paintDeal() и paintPanel() уже вне кода основной программы.
Отрисовка информационных объектов на графиках
Для удобного расположения информации на графике принт-скрина, нам остаётся переопределить всего два метода, от которых будет зависеть, как именно отобразится информация, необходимая пользователю для анализа своей торговли.
Начнём с описания метода paintDeal(), задачей которого будет являться отрисовка графики по позиции, связанной с расположением цен открытия, закрытия, положениям стопа и тейк-профита. Для этого вне тела основного кода объявим описание этого метода со следующей сигнатурой:
void paintDeal(long handlE, ulong tickeT, double stop_losS, double take_profiT, double opeN, double closE, datetime timE, datetime time_closE)
В параметрах метода указаны следующие значения handlE - хендл графика на котором будем рисовать, tickeT - тикет сделки, stop_losS - цена Stop Loss, если она была, take_profiT - Take Profit, если он был указан, цена открытия - opeN и цена закрытия - closE, время открытия сделки - timE и, соответственно, закрытия - time_closE.
Начнем отрисовку с наименования объекта, которое будет соответствовать уникальному имени, которое не должно повторяться. Поэтому в имени мы напишем признак, что этот объект соответствует стопу в виде "name_sl_" и для уникальности имени ещё и добавим номер тикета сделки, как показано ниже.
string name_sl = "name_sl_"+IntegerToString(tickeT); // дали имя
Теперь можем создавать сам графический объект с помощью предопределённой функции терминала ObjectCreate(), которая и отрисует нам уровень цены Stop Loss по исторической позиции на графике. В качестве параметров передадим в неё хендл графика, уникальное имя из переменной name_sl, укажем в качестве вида объекта значение OBJ_ARROW_LEFT_PRICE, что значит левая ценовая метка из перечисления ENUM_OBJECT, а также, собственно, значение цены и время установления метки на графике, как показано ниже.
ObjectCreate(handlE,name_sl,OBJ_ARROW_LEFT_PRICE,0,timE,stop_losS); // создали объект левый ярлык
Теперь, когда объект создан, установим значения его полей типа OBJPROP_COLOR и OBJPROP_TIMEFRAMES. Значение OBJPROP_COLOR установим равное clrRed, так как традиционно Stop Loss обозначается красным цветом, а значение OBJPROP_TIMEFRAMES установим равным OBJ_ALL_PERIODS для отображения на всех тайм фреймах. Хотя, второе условие не является в данной реализации критичным. В общем виде блок прорисовки Stop Loss будет выглядеть, как показано ниже.
//---рисуем стоп-лосс string name_sl = "name_sl_"+IntegerToString(tickeT); // дали имя ObjectCreate(handlE,name_sl,OBJ_ARROW_LEFT_PRICE,0,timE,stop_losS); // создали объект левый ярлык ObjectSetInteger(handlE,name_sl,OBJPROP_COLOR,clrRed); // добавили цвет ObjectSetInteger(handlE,name_sl,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // настроили видимость ChartRedraw(handlE); // перерисовали
После прорисовки каждого блока обязательно вызываем метод ChartRedraw().
Отрисовка блока Take Profit будет аналогична отрисовке Stop Loss за следующими исключениями. В первую очередь, в уникальное имя объекта добавим "name_tp_" плюс тикет сделки, а цвет установим из палитры зелёного, что соответствует традиционному обозначению полученного профита, через цвет clrLawnGreen. В остальном, логика аналогична блоку Stop Loss и полностью представлена здесь.
//---рисуем тейк-профит string name_tp = "name_tp_"+IntegerToString(tickeT); // дали имя ObjectCreate(handlE,name_tp,OBJ_ARROW_LEFT_PRICE,0,timE,take_profiT); // создали объект левый ярлык ObjectSetInteger(handlE,name_tp,OBJPROP_COLOR,clrLawnGreen); // добавили цвет ObjectSetInteger(handlE,name_tp,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // настроили видимость ChartRedraw(handlE); // перерисовали
Переходим к реализации отрисовки цены входа в сделку также через левую ценовую метку. Отличие от предыдущих блоков, в первую очередь, опять же в уникальном имени объекта, где мы добавим в начале "name_open_", а также цветом линии clrWhiteSmoke, чтобы она не сильно выделялась на графике, а в остальном всё так же, как и в других.
//---рисуем цену входа string name_open = "name_open_"+IntegerToString(tickeT); // дали имя ObjectCreate(handlE,name_open,OBJ_ARROW_LEFT_PRICE,0,timE,opeN); // создали объект левый ярлык ObjectSetInteger(handlE,name_open,OBJPROP_COLOR,clrWhiteSmoke); // добавили цвет ObjectSetInteger(handlE,name_open,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // настроили видимость ChartRedraw(handlE); // перерисовали
Тем же цветом мы нарисуем и линию, соединяющую ценовые метки открытия и закрытия сделки. Тип этой линии будет отличаться, и при создании объекта в параметре метода ObjectCreate(), в качестве третьего параметра мы передадим значение OBJ_TREND перечисления ENUM_OBJECT, чтобы создать трендовую линию. Для правильного расположения трендовой линии на графике нам понадобится указать дополнительные параметры положения двух точек, где каждая точка будет иметь два признака: цена и время. Для этого в последующие параметры передадим цены открытия и закрытия opeN и closE, а также время закрытия и открытия в переменных timE и time_closE, как показано ниже.
//---сама линия сделки string name_deal = "name_deal_"+IntegerToString(tickeT); // дали имя ObjectCreate(handlE,name_deal,OBJ_TREND,0,timE,opeN,time_closE,closE); // создали объект левый ярлык ObjectSetInteger(handlE,name_deal,OBJPROP_COLOR,clrWhiteSmoke); // добавили цвет ObjectSetInteger(handlE,name_deal,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // настроили видимость ChartRedraw(handlE); // перерисовали
Для полного отображения сделки на графике осталось прорисовать ценовую метку закрытия сделки. Для этого будем использовать не левую ценовую метку, как на открытии, а правую, чтобы информация отображалась на принт-скрине более эстетично. Чтобы нарисовать правую метку, передадим в метод ObjectCreate() третьим параметром значение OBJ_ARROW_RIGHT_PRICE, что значит правая ценовая метка из перечисления ENUM_OBJECT. Для остальной прорисовки нам потребуется только цена и время, которые мы передадим через соответствующие переменные time_closE,closE, как показано ниже.
//---рисуем цену выхода string name_close = "name_close"+IntegerToString(tickeT); // дали имя ObjectCreate(handlE,name_close,OBJ_ARROW_RIGHT_PRICE,0,time_closE,closE);// создали объект левый ярлык ObjectSetInteger(handlE,name_close,OBJPROP_COLOR,clrWhiteSmoke); // добавили цвет ObjectSetInteger(handlE,name_close,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS); // настроили видимость ChartRedraw(handlE); // перерисовали
На этом мы завершили описание нашего пользовательского метода paintDeal() для прорисовки линий входа и выхода из позиции, теперь можем приступать к описанию метода для прорисовки панели полной информации по сделке в методе paintPanel().
Описание метода прорисовки панели потребует от нас более сложной структуры методов, которые будут отвечать за прорисовку текстовых меток типа OBJ_LABEL, перечисления ENUM_OBJECT и объекта OBJ_RECTANGLE_LABEL "Прямоугольная метка" для создания и оформления пользовательского графического интерфейса. Объявим соответствующие пользовательские методы под названиями: LabelCreate() для создания текстовых меток и RectLabelCreate() для создания прямоугольной метки. Начнём описание со вспомогательных методов, а потом перейдём к описанию основного метода paintPanel(), в котором и будем использовать вспомогательные методы.
В общем виде структура методов нашего скрипта будет выглядеть как на Рисунке 2.
Рисунок 2. Структура пользовательских методов для отрисовки графики
Объявим метод LabelCreate() со следующей сигнатурой в качестве параметров:
bool LabelCreate(const long chart_ID=0, // ID графика const string name="Label", // имя метки const int sub_window=0, // номер подокна const long x=0, // координата по оси X const long y=0, // координата по оси Y const ENUM_BASE_CORNER corner=CORNER_LEFT_UPPER, // угол графика для привязки const string text="Label", // текст const string font="Arial", // шрифт const int font_size=10, // размер шрифта const color clr=clrRed, // цвет const double angle=0.0, // наклон текста const ENUM_ANCHOR_POINT anchor=ANCHOR_LEFT_UPPER, // способ привязки const bool back=false, // на заднем плане const bool selection=false, // выделить для перемещений const bool hidden=true, // скрыт в списке объектов const long z_order=0) // приоритет на нажатие мышью
В параметр chart_ID будем передавать хендл графика, на котором нужно отрисовать объект: name - это уникальное имя объекта, а значение 0 параметра sub_window будет означать, что мы хотим нарисовать объект в основном окне графика. Координаты левого верхнего угла объекта будут переданы для размещения через параметры Х и У соответственно. Привязку угла объекта к графику мы можем изменить со стандарта по левому углу, передав соответствующее значение в параметр corner, но оставим там значение по умолчанию ANCHOR_LEFT_UPPER. В параметр text мы передадим строковое значение информации для отображения, а вид отображения, такой как вид шрифта и его размер, цвет, угол наклона, передадим в соответствующих параметрах font, font_size, clr и angle. Также мы сделаем наш объект скрытым в списке объектов для пользователя и невыделяемым с помощью мыши, через параметры selection и hidden. Параметр z_order будет отвечать за порядок приоритета для нажатия мыши.
Описание метода начнём с обнуления переменной ошибки, чтобы было возможно корректно проконтролировать результат создания объекта в дальнейшем через предопределённую функцию терминала ResetLastError(). Обработку результата создания объекта типа OBJ_LABEL произведём через логический оператор if с вызовом в нём функции ObjectCreate(), как показано ниже. В случае, если объект не создался, мы проинформируем об этом пользователя в журнале эксперта и прервём выполнение метода через оператор возврата, как обычно.
//--- сбросим значение ошибки ResetLastError(); //--- создадим текстовую метку if(!ObjectCreate(chart_ID,name,OBJ_LABEL,sub_window,0,0)) { Print(__FUNCTION__, ": не удалось создать текстовую метку! Код ошибки = ",GetLastError()); return(false); }
Если объект был создан успешно, для придания ему необходимого вида, проинициализируем поля свойств этого объекта через предопределённые функции терминала ObjectSetInteger(), ObjectSetString() и ObjectSetDouble(). Через функцию ObjectSetInteger() установим значения соответствующих координат, угол привязки объекта, размер шрифта, способ привязки объекта, цвет, режим отображения, а также свойства, связанные с видимостью объекта для пользователя. С помощью функции ObjectSetDouble() выставим значения угла наклона шрифта, а с помощью функции ObjectSetString() определим содержание передаваемого текста и вида шрифта для отображения. Полная реализация тела метода представлена ниже.
//--- сбросим значение ошибки ResetLastError(); //--- создадим текстовую метку if(!ObjectCreate(chart_ID,name,OBJ_LABEL,sub_window,0,0)) { Print(__FUNCTION__, ": не удалось создать текстовую метку! Код ошибки = ",GetLastError()); return(false); } //--- установим координаты метки ObjectSetInteger(chart_ID,name,OBJPROP_XDISTANCE,x); ObjectSetInteger(chart_ID,name,OBJPROP_YDISTANCE,y); //--- установим угол графика, относительно которого будут определяться координаты точки ObjectSetInteger(chart_ID,name,OBJPROP_CORNER,corner); //--- установим текст ObjectSetString(chart_ID,name,OBJPROP_TEXT,text); //--- установим шрифт текста ObjectSetString(chart_ID,name,OBJPROP_FONT,font); //--- установим размер шрифта ObjectSetInteger(chart_ID,name,OBJPROP_FONTSIZE,font_size); //--- установим угол наклона текста ObjectSetDouble(chart_ID,name,OBJPROP_ANGLE,angle); //--- установим способ привязки ObjectSetInteger(chart_ID,name,OBJPROP_ANCHOR,anchor); //--- установим цвет ObjectSetInteger(chart_ID,name,OBJPROP_COLOR,clr); //--- отобразим на переднем (false) или заднем (true) плане ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back); //--- включим (true) или отключим (false) режим перемещения метки мышью ObjectSetInteger(chart_ID,name,OBJPROP_SELECTABLE,selection); ObjectSetInteger(chart_ID,name,OBJPROP_SELECTED,selection); //--- скроем (true) или отобразим (false) имя графического объекта в списке объектов ObjectSetInteger(chart_ID,name,OBJPROP_HIDDEN,hidden); //--- установим приоритет на получение события нажатия мыши на графике ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order); //--- успешное выполнение return(true);
Объявим метод RectLabelCreate() со следующей сигнатурой в качестве параметров создания объекта:
bool RectLabelCreate(const long chart_ID=0, // ID графика const string name="RectLabel", // имя метки const int sub_window=0, // номер подокна const int x=19, // координата по оси X const int y=19, // координата по оси Y const int width=150, // ширина const int height=20, // высота const color back_clr=C'236,233,216', // цвет фона const ENUM_BORDER_TYPE border=BORDER_SUNKEN, // тип границы const ENUM_BASE_CORNER corner=CORNER_LEFT_UPPER, // угол графика для привязки const color clr=clrRed, // цвет плоской границы (Flat) const ENUM_LINE_STYLE style=STYLE_SOLID, // стиль плоской границы const int line_width=1, // толщина плоской границы const bool back=true, // на заднем плане true const bool selection=false, // выделить для перемещений const bool hidden=true, // скрыт в списке объектов const long z_order=0) // приоритет на нажатие мышью
Параметры метода RectLabelCreate() во многом схожи с параметрами объявленного ранее метода LabelCreate(), за исключением дополнительных настроек границы прямоугольной метки, которая будет служить подложкой для отображения данных объекта предыдущего метода. Дополнительные параметры настройки границы объекта: border - тип границы, определённый перечислением ENUM_BORDER_TYPE со значением по умолчанию BORDER_SUNKEN, style - стиль границы, определённый перечислением ENUM_LINE_STYLE со значением по умолчанию STYLE_SOLID, и line_width - толщина линии границы в целочисленном измерении.
Определение тела метода будет выглядеть аналогично предыдущему и, также как и он, будет состоять из двух глобальных разделов: создание объекта и определение его свойств через соответствующие предопределённые методы терминала, как показано ниже.
//--- сбросим значение ошибки ResetLastError(); // сбросили ошибку //--- создадим прямоугольную метку if(ObjectCreate(chart_ID,name,OBJ_RECTANGLE_LABEL,sub_window,0,0)) // создали объект { //--- установим координаты метки ObjectSetInteger(chart_ID,name,OBJPROP_XDISTANCE,x); // дали координату х ObjectSetInteger(chart_ID,name,OBJPROP_YDISTANCE,y); // дали координату у //--- установим размеры метки ObjectSetInteger(chart_ID,name,OBJPROP_XSIZE,width); // ширина ObjectSetInteger(chart_ID,name,OBJPROP_YSIZE,height); // высота //--- установим цвет фона ObjectSetInteger(chart_ID,name,OBJPROP_BGCOLOR,back_clr); // цвет фона //--- установим тип границы ObjectSetInteger(chart_ID,name,OBJPROP_BORDER_TYPE,border); // тип границы //--- установим угол графика, относительно которого будут определяться координаты точки ObjectSetInteger(chart_ID,name,OBJPROP_CORNER,corner); // угол зацепа //--- установим цвет плоской рамки (в режиме Flat) ObjectSetInteger(chart_ID,name,OBJPROP_COLOR,clr); // рамка //--- установим стиль линии плоской рамки ObjectSetInteger(chart_ID,name,OBJPROP_STYLE,style); // стиль //--- установим толщину плоской границы ObjectSetInteger(chart_ID,name,OBJPROP_WIDTH,line_width); // ширина //--- отобразим на переднем (false) или заднем (true) плане ObjectSetInteger(chart_ID,name,OBJPROP_BACK,back); // по умолчанию на заднем //--- включим (true) или отключим (false) режим перемещения метки мышью ObjectSetInteger(chart_ID,name,OBJPROP_SELECTABLE,selection); // можно ли выбрать ObjectSetInteger(chart_ID,name,OBJPROP_SELECTED,selection); // //--- скроем (true) или отобразим (false) имя графического объекта в списке объектов ObjectSetInteger(chart_ID,name,OBJPROP_HIDDEN,hidden); // видно ли в списке //--- установим приоритет на получение события нажатия мыши на графике ObjectSetInteger(chart_ID,name,OBJPROP_ZORDER,z_order); // без событий //--- успешное выполнение } return(true);
Теперь, когда все вспомогательные методы описаны, определим тело основного метода, который будет рисовать панель полностью - paintPanel(). Входные параметры будут содержать поля, необходимые для отображения полной информации по сделкам для пользователя, как показано ниже.
void paintPanel(long handlE, ulong tickeT, double stop_losS, double take_profiT, double opeN, double closE, datetime timE, datetime time_closE, int magiC, string commenT, string externalIDD, double volumE, double commissioN, ENUM_DEAL_REASON reasoN, double swaP, double profiT, double feE, string symboL, int digitS )
Первый параметр, как и в предыдущих методах, будет отвечать за определение хендла графика, на котором будут созданы все объекты, связанные с панелью информации. Все остальные параметры будут повторять поля объекта исторической сделки.
Реализацию метода отрисовки информационной панели по сделке начнём с определения переменных для хранения размера панели, а также координат привязки столбцов с названием отображаемой информации и полученными ранее значениями, как показано ниже.
int height=20, max_height =0, max_width = 0; // высота столбца и макс значения для отступа int x_column[2] = {10, 130}; // координаты по оси Х столбцов int y_column[17]; // координаты по оси У
Переменная height будет хранить статическое значение 20, как величину высоты каждого столбца для равномерности рисования каждой строки, а в значениях max_height и max_width будут храниться максимальные значения каждого столбца для ровной прорисовки. Необходимые координаты по осям Х и У будут храниться в массивах x_column[] и y_column[] соответственно.
Теперь нужно объявить два массива, в которых будут храниться значения строк для отображения столбца заголовков и столбца значений. Столбец заголовков объявим через массив данных типа string, как показано в следующем коде.
string column_1[17] = { "Symbol", "Position ID", "External ID", "Magic", "Comment", "Reason", "Open", "Close", "Time open", "Time close", "Stop loss", "Take profit", "Volume", "Commission", "Swap", "Profit", "Fee" };
Все значения массива объявлены и проинициализированы статично, так как панель меняться не будет, и данные всегда будут отображаться в одной последовательности, это во многом будет удобно для пользователя с точки зрения привычности к просмотру информации по разным сделкам. Можно было бы реализовать функционал, позволяющий исключать из панели данные, не содержащие значений или равные нулю, но это было бы неудобно для глаз при быстром поиске информации. Всё-таки более привычно искать информацию на знакомом паттерне отображения, чем каждый раз вглядываться в значения столбцов.
В той же самой последовательности данных объявим второй массив, который уже будет содержать значения столбцов, объявленных в массиве выше. Объявление массива опишем следующим образом:
string column_2[17] = { symboL, IntegerToString(tickeT), externalIDD, IntegerToString(magiC), commenT, EnumToString(reasoN), DoubleToString(opeN,digitS), DoubleToString(closE,digitS), TimeToString(timE), TimeToString(time_closE), DoubleToString(stop_losS,digitS), DoubleToString(take_profiT,digitS), DoubleToString(volumE,2), DoubleToString(commissioN,2), DoubleToString(swaP,2), DoubleToString(profiT,2), DoubleToString(feE,2) };
Массив будет объявляться локально, на уровне метода, с одновременной инициализацией полей сразу из параметров метода с помощью соответствующих предопределённых функций терминала.
Теперь, когда мы объявили контейнеры с необходимыми данными, нам нужно рассчитать значения привязки координат каждой ячейки, с учётом поиска максимального значения в каждой из них. Это мы сможем реализовать с помощью следующего кода:
int count_rows = 1; for(int i=0; i<ArraySize(y_column); i++) { y_column[i] = height * count_rows; max_height = y_column[i]; count_rows++; int width_curr = StringLen(column_2[i]); if(width_curr>max_width) { max_width = width_curr; } } max_width = max_width*10; max_width += x_column[1]; max_width += x_column[0];
Здесь мы находим координаты привязки каждого объекта через перебор цикла по количеству строк, умножая их на фиксированное значение высоты каждого по оси У, а также проверяем каждое значение на максимальное значение ширины, чтобы получить координату по оси Х.
Когда у нас получены все значения с их координатами, мы можем приступить к отрисовке информации с помощью объявленного ранее пользовательского метода LabelCreate(), который мы будем вызывать циклически, в соответствии с количеством наших строк для отображения, как показано ниже.
color back_Color = clrWhiteSmoke; color font_Color = clrBlueViolet; for(int i=0; i<ArraySize(column_1); i++) { //---рисуем 1 string name_1 = column_1[i]+"_1_"+IntegerToString(tickeT); LabelCreate(handlE,name_1,0,x_column[0],y_column[i],CORNER_LEFT_UPPER,column_1[i],"Arial",10,font_Color,0,ANCHOR_LEFT_UPPER,false); //---рисуем 2 string name_2 = column_1[i]+"_2_"+IntegerToString(tickeT); LabelCreate(handlE,name_2,0,x_column[1],y_column[i],CORNER_LEFT_UPPER,column_2[i],"Arial",10,font_Color,0,ANCHOR_LEFT_UPPER,false); }
В завершение метода нам остаётся дорисовать фон объявленным и описанным ранее пользовательским методом RectLabelCreate() к этим значениям и обновить отображаемый график, как показано ниже.
//---рисуем фон RectLabelCreate(handlE,"RectLabel",0,1,height,max_width,max_height,back_Color); ChartRedraw(handlE);
На этом описание всех методов завершено, и проект готов к сборке и использованию.
В итоге, файл графика после использования скрипта будет выглядеть следующим образом, как показано на Рисунке 3.
Рисунок 3. Результат работы скрипта с представлением информации по сделке.
Как видим, вся информация по сделке представлена в обобщенном виде на одном графике, что делает более удобным процесс анализа и оценки проведённых торговых операций пользователем. Скрипт раскладывает такие файлы по соответствующим папкам, что также даёт возможность пользователю в любой момент найти необходимую ему информацию по любой торговой операции в истории счёта.
Заключение
Этой статьей мы завершили написание скрипта для автоматизированной визуализации сделок на графике. Используя данное программное решение вы сможете существенно улучшить свою торговлю за счёт исправления возможных ошибок при выборе точки входа, а также увеличить математическое ожидание всей вашей стратегии за счёт правильного выбора инструментов и ожидаемого места импульса цены. При этом использование данного скрипта позволит существенно сэкономить время на техническую работу по подготовке файлов графиков, которое вы сможете потратить на анализ и поиск новых идей для своей торговли. Главное помните, рынок постоянно меняется, и чтобы обеспечить стабильную работу, необходимо постоянно "держать руку на пульсе" и следить за изменениями, в этом и сможет вам помочь данный инструмент. Желаю вам успехов в работе и буду рад обратной связи в комментариях к этой статье.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования