Плеер торговли на основе истории сделок

Mykola Demko | 2 мая, 2011


Лучше один раз увидеть

Визуальный анализ истории торговли - важная часть аналитической работы трейдера. Если бы это было не так, то не было бы технического анализа, который переводит мир цифр в мир рисунков. Да оно и понятно, ведь человек на 80% воспринимает мир глазами. Статистика, являя собой обобщение данных, не может передать многие нюансы. И лишь визуализация с ее интуитивным восприятием мира цифр может расставить все точки над i. Как говорится, лучше один раз увидеть, чем сто раз услышать.

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

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


Прогон сделок в тестере MetaTrader 5

Работа плеера основывается на HTML-отчете MetaTrader 5. Поэтому, историю Чемпионата 2010 года можно получить, залогинившись на нужный cчет ATC-2010 и сохранив историю торговли в виде HTML-отчета.

Поскольку сервер Чемпионата 2008 года остановлен, то с этой историей так сделать не получится. На сайте хранится общий отчет всех участников, сжатый в zip-архиве. Automated_Trading_Championship_2008_All_Trades.zip

Архив "Automated Trading Championship 2008 All Trades.zip" нужно распаковать в каталог \Files установленного MetaTrader 5.

Для того чтобы проанализировать историю Automated Trading Championship 2008, потребуется запустить скрипт Report Parser MT4, который распарсит историю, сделает выборку по указанному логину и сохранит результат в двоичный файл. Этот двоичный файл читается советником Player Report.

Советник Player Report нужно запустить в тестере, указав требуемый логин. После тестирования нужно сохранить отчет в формате HTML. Указанный логин не влияет на результат тестирования, но будет отображен в отчете в виде входной переменной login. Это даст возможность в последующем различать отчеты. Поскольку отчеты создает один и тот же советник, то желательно при сохранении давать отчету имя, отличное от имени по умолчанию.

Скрипт Report Parser MT4 также имеет входную переменную login, в которой требуется указать логин участника, историю которого вы желаете просмотреть. Если вы не знаете логина участника, а знаете лишь его ник, то следует запустить скрипт с нулевым (по умолчанию) значением логина. В этом случае скрипт не будет производить выборку по логину, а лишь создаст csv-файл с перечислением всех логинов в алфавитном порядке. Имя файла "Automated Trading Championship 2008 All Trades_plus". Найдя нужного участника в этом файле, нужно будет запустить скрипт повторно, уже с указанием логина.

Таким образом, скрипт Report Parser MT4 в тандеме с советником Player Report создает из истории торговли формата MetaTrader 4 стандартный html-отчет Тестера MetaTrader 5.

Советник Player Report не исполняет сделки в точности, как это было в реальности, а лишь приближено. Тому виной разница в котировках, округление времени до минут в отчете, проскальзывания при исполнении. Разница чаще всего выражается в несколько пунктов, и то лишь в ~10% трейдов. Но этого достаточно, чтобы прибыль торговли, например, с ~170 тысяч упала в тестере до ~160 тысяч. Все зависит от объемов сделок, на которых было проскальзывание.


Как работает плеер

Как уже говорилось выше, плеер можно использовать как для просмотра истории Automated Trading Championship 2008, используя дополнительные программы, так и для просмотра Automated Trading Championship 2010, но уже напрямую.

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

Параметры советника Player History Trades exp v5:

Отчет тестера MetaTrader 5 является входным файлом для плеера истории сделок. Именно название файла отчета требуется указать во входном параметре советника Player History Trades exp v5 "имя html-файла отчета тестера". При запуске плеера пользователь может, но не обязан, указать период проигрывания во входных переменных "начало истории" и "окончание истории".

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

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

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

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

Я намеренно пропустил описание параметра "Удалить чарты при удалении советника". Он относится не к управлению советником, а к поведению советника при удалении. Дело в том, что советник для своей работы анализирует множество данных. Я счел, что некоторая информация, которой владеет советник, будет полезной для анализа в виде файлов. Советник создает csv-файлы с выборками торговли по каждому инструменту, а также файл, в котором будет сведены в синхронном виде балансы всех символов, что может быть полезно для выявления символа в мультивалютной корзине.

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

Далее перечисляются не столь важные параметры.

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

Почему бы не генерировать все с периода М1? Зачем нужно изменять период генератора? Дело в том, что на больших ТФ в одном баре умещается довольно много минутных баров, и может понабиться ускорить процесс генерации. Для этого и предусмотрена возможность менять период генератора. В самом генераторе предусмотрены не все ТФ, а лишь выборочно. Как это изменить в коде будет рассмотрено ниже.

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

История торговли рисуется отдельными сделками, но цвет линий отрисовки зависит от типа позиции.

Параметрами "цвет операций buy", "цвет операций sell" можно задать нужные цвета.


Отображение истории торговли

Из рисунка видно, что позиция часто отличается по уровню от сделки.

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

[объем сделки|объем позиции]

Если тип сделки не равен типу позиции (например, частичное закрытие), то отображение объемов будет иметь дополнительные значки

[<объем сделки>|объем позиции]

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

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

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

Теперь перейдем к элементам управления плеера.

Управление плеером

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

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

В состоянии "All" ,будут выведены данные общего баланса счета, а в состоянии "Sym" отображается выборка баланса по символу графика, на котором запущен индикатор. Управление индикатором не синхронно, а это значит, что на одном графике может быть запущен индикатор, а на другом нет. 

Индикатор баланса

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

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

Объект линейка прогресса состоит из 100 кнопок управления, построенных по принципу триггера: если одна кнопка нажимается, то остальные отжимаются. Поскольку кнопок 100, то весь период проигрывания делится на 100 участков. Если количество баров не делится нацело на 100, то остаток будет приплюсован к последнему участку. Именно поэтому в настройках выведены параметры "начало истории" и "окончание истории". Управляя этими параметрами, можно более детально осуществлять навигацию нужного периода истории.

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

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

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

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

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



На видео показано воспроизведение торговли участника Manov с чемпионата  ATC 2010. Для этого из клиентского терминала было сделано подключение к его счету с параметрами login=630165 и пароль=MetaTrader. Отчет о торговле был сохранен под именем ReportHistory-630165.html в папке каталог_данных_терминала\MQL5\Files. Вы можете просто скачать этот файл в заархивированном виде и распаковать в указанную папку.


Подготовка к старту

  1. Для того чтобы все работало, нужно скачать архив player_history_trades.zip и распаковать его в каталог каталог_данных_терминала/MQL5/Indicators.
  2. Открыть скопированный каталог Player History Trades и скомпилировать в MetaEditor четыре файла, находящихся в корне этого каталога. Последовательность файлов при компиляции не имеет значения.
  3. Убедитесь что требуемый участок истории по всем инструментам, участвовавшим в торговом отчёте, доступен на  таймфрейме М1.  Для чего вручную откройте нужный чарт ТФ М1, установите вертикальную линию и с помощью команды Ctrl+B или из контекстного меню Список объектов, через свойства измените дату вертикальной линии на дату начала торговли.
  4. Далее нажмите кнопку «Показать». Если котировок нет, то возможны две причины. Либо котировки не закачаны, либо установлен слишком маленький параметр «Макс. Баров в окне» в контекстном меню Сервис->Настройки->Графики.

Теперь все будет работать.


Начало разработки

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

  1. Мультивалютность.
  2. Автоматическое открытие нужных графиков.
  3. Удобный интерфейс навигации, возможность промотать историю в обоих направлениях.
  4. Синхронность показа по всем графикам.
  5. Старт/Пауза проигрывания.
  6. Выбор (и режим по умолчанию) количества и символов графиков участвующих в отображении.
  7. Выбор периода (и режим по умолчанию), на котором плеер будет работать.
  8. Отображение истории сделок на графике.
  9. Отображение истории баланса и эквити.
  10. Раздельное отображение баланса (эквити) символа или общего баланса (эквити) счета.

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

Общий план работы проигрывателя:

  1. Загрузить HTML-отчет;
  2. Распарсить его на сделки и восстановить историю позиций;
  3. Подготовить сделки в виде очереди приказов на открытие/закрытие;
  4. Запустить по команде пользователя показ сделок в динамике на истории с расчетом необходимых показателей в виде индикаторов (графики эквити, просадок и т.д.);
  5. Организовать вывод информационного табло остальных показателей на графике.

Также нужен специальный эксперт, который будет торговать в тестере по данным отчета MetaTrader 4:

  1. Распарсенные сделки записать в виде двоичного файла данных для эксперта;
  2. Создать отчет тестера MetaTrader 5.

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

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


Ордера и сделки

На сегодняшний день одновременно существует две торговые концепции. Старая концепция, используемая в MetaTrader 4 и на которой в настоящий момент проводятся реальные торги, и новая, так называемая "неттинговая" концепция, которая принята за основу в MetaTrader 5. Подробно о различиях описано в статье Ордерa, позиции и сделки в MetaTrader 5.

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

В MetaTrader 5 роль такого контейнера берет на себя позиция. Но существенным различием есть то, что истории позиций нет. Есть лишь общая история ордеров и сделок. И хотя в истории имеется вся необходимая информация для восстановления истории позиции, но все же, перестройка мышления требует некоторых усилий.

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


Парсер HTML

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

В парсере используется два больших класса CTable и CHTML. Применение класса CTable было подробно описано в статье Электронные таблицы на MQL5 , поэтому повторятся не буду.

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

Общую концепцию класса можно уложить в понятие тег. Тег можно представить как функцию с вложениями. Например, вот так Teg(header,casket); где header - это заголовок тега (в котором обычно указываются переменные тега, управляющие отображением страницы), casket - это содержимое контейнера тега. И вот из таких тегов состоит весь язык HTML.

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

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

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

h.td.base.casket

Данная запись означает, что объект h через вложенный объект td (который является объектом тега <td header >casket</td>)  через вложенный объект base (который является объектом класса CTegs) вызывает значение переменной casket.

В классе также доступны методы поиска тега, скрыто объединенные в публичном методе

h.td.Search(text,start);

возвращающего поисковую точку окончания тега и заполняющего переменные header и casket тега.

Остальные заготовленные в классе функции не используются, и останавливаться на них я не стану, впереди еще много интересного.

В завершении описания работы с HTML -документами хочу упомянуть, что в статье используется два вида парсера, отличающихся лишь типом сохранения полученных из файла данных. Первый тип использует сохранение всего документа в одну переменную типа string и используется в плеере. Второй тип использует построчный парсинг отчета. Он применяется в скрипте подготовки истории Чемпионата 2008 года.

Почему я применил два подхода? Все дело в том, что для правильной работы функций класса CTegs, тег должен быть полностью помещен в анализируемую строку. А это не всегда возможно. Например, в случае с такими тегами как table, html, body (они многострочные). Переменная типа string позволяет сохранять в ней (по моим подсчетам) 32750 символов без символа табуляции. А с \r (после каждого 32748-го символа) мне удалось сохранить 2 000 000, после чего я прекратил свои попытки. Наверняка можно и больше.

Почему же применено два подхода? Дело в том, что для универсальности парсера плейера нужно сначала найти требуемую таблицу. Для отчета тестера и отчета торговой истории сделок требуемая таблица находится на разных местах. Для сохранения универсальности (чтоб парсер понимал и тот и другой отчет) я применил схему поиска таблицы, у которой есть тег td, с содержимым "сделки" или "deals".

Структура отчета Чемпионата 2008 года заранее известна и нет необходимости вести поиск требуемой таблицы. Зато документ отчета очень большой (35 Мб) и помещение всего отчета в одну переменную займет много времени. Этим обусловлен второй подход к парсингу.


Проигрыватель

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

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

Общая схема плеера истории сделок

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

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

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


Для дальнейшего повествования нужно прояснить три понятия ООП: Ассоциация, Агрегация, Композиция.

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

class Base
  {
public:
                     Base(void){};
                    ~Base(void){};
   int               a;
  };
//+------------------------------------------------------------------+

class A_Association
  {
public:
                     A_Association(void){};
                    ~A_Association(void){};
   void              Association(Base *a_){};
   // При ассоциации данные связываемого объекта 
   // будут доступны через указатель объекта только в методе, 
   // в который передан указатель.
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class A_Aggregation
  {
   Base             *a;
public:
                     A_Aggregation(void){};
                    ~A_Aggregation(void){};
   void              Aggregation(Base *a_){a=a_;};
   // При агрегации данные связываемого объекта 
   // будут доступны через указатель объекта в любом методе класса.
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class A_Composition
  {
   Base             *a;
public:
                     A_Composition(void){ a=new Base;};
                    ~A_Composition(void){delete a;};
   // При композиции объект становится частью класса.
  };

Для передачи указателя через параметр в MQL5 существует функция 

GetPointer(указатель)

аргументом которой выступает указатель объекта.

Например:

void OnStart()
  {
   Base a; 
   A_Association b;
   b.Association(GetPointer(a));
  }


Функции, вызываемые в OnInit(), в моем коде часто используют ассоциацию. Композиция применяется в классе CHTML. А агрегацию наряду с композицией я применяю для связывания объектов внутри класса CPlayer. Конкретно объекты классов CChartData и SBase, используя агрегацию, создают общее поле данных для всех объектов внутри плеера созданных композиционно.

Графически это можно представить так:

Связывание данных

Классы, объекты которых композиционно создаются в классе CPlayer, имеют шаблонную структуру с последующим наращиванием функционала. Применение шаблонов описано в статье Применение псевдошаблонов как альтернатива шаблонов С++, поэтому я не стану подробно останавливаться на этом вопросе.

Шаблон, создающий заготовку класса выглядит так:

//это_стартовая_точка
//+******************************************************************+
class _XXX_
  {
private:
   long              chart_id;
   string            name;
   SBase            *s;
   CChartData       *d;
public:
   bool              state;
                     _XXX_(){state=0;};
                    ~_XXX_(){};
   void              Create(long Chart_id, SBase *base, CChartData *data);
   void              Update();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void _XXX_::Create(long Chart_id, SBase *base, CChartData *data)
  {
   chart_id=Chart_id;
   s=base; // связывание данных со структурой плеера
   d=data; // связывание данных со структурой чарта
   name=" "+ChartSymbol(chart_id);

   if(ObjectFind(chart_id,name)<0)// если объекта еще нет
     {//--- попробуем создать объект         
      if(ObjectCreate(chart_id,name,OBJ_TREND,0,0,0,0,0))
        {//---
        }
      else
        {//--- объект создать не удалось, сообщим об этом
         Print("Не удалось создать объект "+name+". Код ошибки ",GetLastError());
         ResetLastError();
        }
     }       
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void _XXX_::Update()
  {
  };
//+******************************************************************+
//это_конечная_точка

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

Теперь, когда общие принципы построения прояснены, можно переходить к конкретике.

Сначала рассмотрим работу функций, объявленных в советнике Player History Trades exp v5 .

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

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

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

Результатом работы функции Balance_Process() есть двоичные файлы истории баланса и эквити на М1, из которых впоследствии индикатор баланса нарезает данные на нужный период. Собственно, в этом месте я забежал вперед, работа индикатора баланса будет рассматриваться ниже.

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

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

ChartOpen(symbol,period)

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

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

ChartTotal(arrayID);   // получим список чартов до открытия дополнительных
CurrentChart(arrayID); // получим список чартов для работы

которые запускаются одна до, а вторая после цикла открытия чартов. Функция ChartTotal() получает список чартов, которые были открыты до запуска советника (включая и чарт, на котором запущен советник) и сохраняет их ID во входном массиве.

Функция CurrentChart() получает эти данные, создает новый список уже с учетом открытия новых чартов, и по разнице списков сохраняет на выходе в параметрический массив ID тех чартов, которые открыл советник. Такая схема вполне надежна, так как работает по факту открытия чарта.

Теперь имея ID нужных нам графиков, можно брать их под управление. Для чего перебираем в цикле все чарты, рисуем с помощью объекта класса CParser_Tester (если вы помните, выше я говорил, что он еще понадобится) историю сделок и создаем объекты управления чартом.

И последнее действие в OnInit() - создаем таймер, и вызываем его для работы. Все остальное действие будет разворачиваться в OnTimer().

Первая проблема при создании плеера появилась уже на начальной стадии разработки. Проблема в создании таймера. Функция EventSetTimer(timer) позволяет создавать таймер с частотой не меньше 1 секунды. То есть, тики в таком варианте генерировались бы раз в секунду. При всей ограниченности восприятия человеческого зрения, один тик в секунду это невообразимо долго. Мне требовалось, по меньшей мере, 100 миллисекунд.

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

Заменой события активации чарта стал композиционный класс CClick, объекты которого, обрабатываясь в цикле функции Click(n), создают сигнал изменения активного чарта. Функция Click() являет собой триггер, который ищет новые изменения кнопки активации чарта, и, найдя нажатую кнопку, переводит все остальные объекты в пассивное состояние. Кнопка активации чарта все время находится у пользователя перед глазами, но он ее не видит, из-за того что кнопка во-первых, занимает все поле чарта, во вторых, имеет цвет фона, в третьих, располагается на заднем плане. При активации чарта кнопка убирается за видимые границы чарта, что дает возможность наблюдать графические обьекты управления плеером, которые в пассивном положении просто закрыты кнопкой активации чарта.

Далее, выяснив с помощью функции Click() какой из чартов старший, переходим к расчетам по движению времени, вызываем функцию Progress(Time) активного плеера.  Эта функция производит следующие расчеты: проверка действий пользователя по навигации - если таковых нет, то проверка, не пора ли переходить на следующий бар; и если пора, то не пора ли менять прогресс на новый участок.

В целом выйдя из функции Progress(Time), цикл имеет данные о текущем времени, на котором далее производятся расчеты. Дальше происходит копирование настроек активного графика ведомыми графиками. Происходит это опять же циклом в функции CopyPlayer(n). После этого в функции Play(Time) переходим к исполнению всех изменений, которые требуется внести на чарт для создания у пользователя иллюзии, что время движется, котировки поступают, торговля ведется.


Композиционные классы плеера.

  1. CArrayRuler* -  производит хранение и поиск данных для быстрого перемещения по барам текущего ТФ.
  2. CRuler*         -  производит хранение и поиск данных минутной истории для генерации тиков.
  3. CSpeed          -  ведает настройками скорости и периодом генератора тиков.
  4. CProgress      -  объединяет все кнопки прогресса в один объект, следит, чтоб только одна кнопка была нажата, изменяет цвета кнопок.
  5. CPlay             -  отвечает за запуск/остановку плеера, а также управляет индикатором баланса.
  6. CClick            -  отвечает за сигнал активации чарта.
  7. CBackGround  -  объект закрывает от пользователя нулевой бар, а также будущие бары при включенном состоянии отступа графика от правого края.
  8. CBarDraw      -  рисует нулевой бар в зависимости от масштаба и типа графика (бары, свечи, линия).
  9. CHistoryDraw -  создает иллюзию для пользователя, что последняя сделка изменяется в реал-тайме.

* - классы не имеют в своем составе графических объектов.

Как я уже писал выше, объекты классов CChartData и SBase, используя агрегацию, создают общее поле данных для всех объектов внутри плеера. Объект класса CChartData отвечает за хранение и обновление данных о чарте, а также управления чартом. Под управлением чартом имеется в виду изменение настроек чарта путем копирования настроек активного чарта. Таким образом, происходит синхронизация всех чартов. Пользователь лишь дает начальный сигнал, изменяя настройки активного чарта, а ряд функций плеера делают всю остальную работу по синхронизации.

Делается это так:

Функция CopyPlayer(n), описаная в советнике, в цикле вызывает функции CPlayer::Copy(CPlayer *base), передавая при этом ассоциативно указатель на плеер активного чарта. Внутри CPlayer::Copy(CPlayer *base) из указателя плеера, также ассоциативно передается указатель объекта CChartData активного плеера. Таким образом, данные о состоянии активного чарта попадают для копирования внутрь объекта класса CChartData ведомого чарта. После чего происходит обновление данных в функции CPlayer::Update(), где производятся все необходимые проверки и приведение всех объектов к нужным состояниям.

Выше я обещал рассказать о том, как добавить периоды в список доступных периодов генератора. Для этого нужно открыть подключаемый файл "Player5_1.mqh". В начале файла статично объявлен массив TFarray[]. Требуемый период нужно добавить на свое место в заполняющее массив перечисление, и не забыть изменить размер массива и переменную CountTF. После чего откомпилировать советник Player History Trades exp v5


Графики баланса и просадки

Управление индикатором баланса происходит в объекте класса CPlay. Тут расположены кнопки и методы управления.

К методам управления индикатора относятся:

   Ind_Balance_Create();                 // добавить индикатор
   IndicatorDelete(int ind_total);     // удалить индикатор
   EventIndicators(bool &prev_state);   // отправить индикатору событие
   StateIndicators();                  // статус индикатора, проверки состояния

Методы добавления/удаления работают в зависимости от состояния кнопки name_on_balance и используют в решении своих задач стандартные функции MQL5 IndicatorCreate() и ChartIndicatorDelete().

Индикатор получает событие и производит расчет, расположенный в OnChartEvent() индикатора в зависимости от кода события. События разделяются на три вида. 

Это события "обновить индикатор", "рассчитать общий баланс", "рассчитать баланс символа". Таким образом, отправляя события, в зависимости от состояния кнопки name_all_balance, пользователь получает контроль над типом расчета. Но в самом коде индикатора нет никакого парсинга истории торговли, расчета позиций, пересчета прибыли. Это и не нужно индикатору.

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

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

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

Кроме этого нужно знать что было раньше - максимум или минимум. Для восстановления этой информации функция  Balance_Process()  использует тот же принцип, что и тестер в режиме "по контрольным точкам", а именно: если бар закрылся с понижением, то вторая точка будет максимум, иначе вторая точка - это минимум.

По такой же схеме ищется третья точка. В результате получаем формат данных (открытие, 2-ая точка, 3-я точка, закрытие) в котором все последовательно и однозначно. На этот формат нарезается история котировок М1. А уже из нее, в зависимости от распарсеной истории торговли (в такой же формат), рассчитывается история баланса и эквити.


Заключение

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

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

Удачи.