- Генерация тиков в тестере
- Управление ходом времени в тестере: таймер, Sleep, GMT
- Визуализация тестирования: график, объекты, индикаторы
- Мультивалютное тестирование
- Критерии оптимизации
- Получение финансовых показателей теста: TesterStatistics
- Событие OnTester
- Авто-настройка: ParameterGetRange и ParameterSetRange
- Группа OnTester-событий для контроля оптимизации
- Отправка фреймов данных с агентов в терминал
- Получение фреймов данных в терминале
- Директивы препроцессора для тестера
- Управление видимостью индикаторов: TesterHideIndicators
- Эмуляция пополнения депозита и снятия средств
- Принудительная остановка тестирования: TesterStop
- Большой пример эксперта
- Математические вычисления
- Отладка и профилирование
- Ограничения работы функций в тестере
Большой пример эксперта
Для обобщения и закрепления знаний о возможностях тестера рассмотрим поэтапно большой пример эксперта, в котором сведем воедино:
- использование нескольких символов, включая синхронизацию баров;
- использование индикатора из эксперта;
- использование событий;
- самостоятельный подсчет основных статистических показателей торговли;
- расчет пользовательского критерия оптимизации R2 с поправкой на переменные лоты;
- отправку и обработку фреймов с прикладными данными (торговыми отчетами в разбивке по символам).
За техническую основу для эксперта возьмем MultiMartingale.mq5, но сделаем его менее рисковым за счет переключения на торговлю по мультивалютным сигналам перекупленности/перепроданности и увеличения лотов только в качестве опционального дополнения. Схема работы по торговым сигналам индикатора у нас уже обкатана на примере BandOsMA.mq5, но теперь в качестве сигнального индикатора выступит UseUnityPercentPro.mq5. Правда, сначала нам потребуется слегка его модифицировать. Новую версию назовем UnityPercentEvent.mq5, и суффикс "Event" здесь неспроста.
UnityPercentEvent.mq5
Напомним суть работы индикатора Unity. Он рассчитывает относительную силу валют или вообще тикеров, входящих в набор заданных инструментов (предполагается, что у всех инструментов есть общая валюта, через которую возможен пересчет). На каждом баре формируются отсчеты для всех валют: что-то окажется дороже, что-то дешевле, и два крайних элемента оказываются в пограничном состоянии. Далее для них можно рассматривать две противоположных по своей сути стратегии:
- дальнейший пробой (подтверждение и продолжение сильного движения в стороны);
- отскок (разворот движения к центру из-за перекупленности и перепроданности).
Для торговли любого из этих сигналов мы должны составить рабочий инструмент из двух валют (или вообще тикеров), если для данного сочетания есть что-то подходящее в Обзоре рынка. Например, если верхняя линия индикатора принадлежит EUR, а нижняя — USD, им соответствует пара EURUSD, и по стратегии на пробой мы должны её купить, а по стратегии на отскок — продать.
В более общем случае, например, когда в корзине рабочих инструментов индикатора указаны CFD или товары с общей валютой котирования, не всегда удастся составить реальный инструмент. Для подобных случаев потребовалось бы усложнить эксперт, введя торговлю синтетиками (составными позициями), но мы не станем здесь этого делать и ограничимся рынком Forex, где обычно в наличии практически все кросс-курсы.
Таким образом, эксперт должен не только прочитать все буфера индикатора, но и выяснить названия валют, которым соответствует максимальное и минимальное значения. И здесь нас ожидает маленькое препятствие.
Дело в том, что MQL5 не позволяет прочитать названия буферов стороннего индикатора, да и вообще любые свойства линий кроме целочисленных: для установки свойств имеется полная тройка функций — PlotIndexSetInteger, PlotIndexSetDouble, PlotIndexSetString, а вот для чтения — только одна PlotIndexGetInteger. Таково ограничение платформы, по крайней мере, сейчас.
В принципе, когда MQL-программы, составленные в единый торговый комплекс, имеют одного разработчика, это не большая проблема. В частности, мы могли бы выделить часть исходного кода индикатора в заголовочный файл и подключить его не только в индикатор, но и в эксперт. Тогда в эксперте можно было бы повторить разбор входных параметров индикатора и восстановить список валют, полностью аналогичный тому, что создает индикатор. Дублирование вычислений — не очень красиво, но оно сработало бы. Однако требуется и более универсальное решение, когда у индикатора другой разработчик, и он не желает раскрывать алгоритм или предполагает его смену в будущем (тогда откомпилированные версии индикатора и эксперта станут несовместимы). Подобная "стыковка" чужих индикаторов со своим или заказываемым в сервисе фриланс экспертом — весьма распространенная практика. И потому разработчику индикатора желательно сделать его максимально "дружелюбным" для интеграции с другими программами.
Одно из возможных решений — рассылка индикатором сообщений с номерами и названиями буферов после инициализации.
Вот как это сделано в обработчике OnInit индикатора UnityPercentEvent.mq5 (приведено с сокращениями, так как почти ничего не изменилось).
int OnInit()
|
По сравнению с исходной версией здесь добавлена всего одна строка с вызовом EventChartCustom. В качестве идентификатора копии индикатора (которых потенциально может быть несколько) используется входная переменная BarLimit. Поскольку индикатор будет вызываться из эксперта и не отобразится пользователю, в ней достаточно указать малое положительное число, как минимум 1, но, у нас будет, например, 10.
Теперь индикатор готов для того, чтобы использовать его сигналы в сторонних экспертах. Начнем разработку эксперта UnityMartingale.mq5. Чтобы упростить изложение, разобьем его на 4 этапа, постепенно добавляя новые блоки. У нас получится три предварительных версии и одна окончательная.
UnityMartingaleDraft1.mq5
На первом этапе, для версии UnityMartingaleDraft1.mq5, возьмем за основу MultiMartingale.mq5 и начнем его модифицировать.
Бывшую входную переменную StartType, которая определяла направление первой сделки в серии, переименуем в SignalType и будем использовать для выбора между рассмотренными стратегиями BREAKOUT и PULLBACK.
enum SIGNAL_TYPE
|
Для настройки индикатора потребуется отдельная группа входных переменных.
input group "U N I T Y S E T T I N G S"
|
Обратите внимание, что параметр UnitySymbols содержит перечень инструментов кластера для построения индикатора, и как правило отличается от перечня рабочих инструментов, которыми мы хотим торговать. Торгуемые инструменты по-прежнему задаются в параметре WorkSymbols.
Например, по умолчанию мы передаем в индикатор набор основных валютных пар Forex, и потому можем указывать в качестве торговых не только основные пары, но и любые кроссы. Обычно имеет смысл ограничить данный набор инструментами с лучшими торговыми условиями (в частности, малым или умеренным спредом). Кроме того, желательно не допускать перекосов, то есть соблюдать равное количество каждой валюты во всех парах, тем самым статистически нивелировать потенциальные риски выбора неудачного направления по одной из валют.
Управление индикатором обернем в класс UnityController. Помимо дескриптора самого индикатора (handle) в полях класса хранятся:
- количество буферов индикатора buffers, которое будет получено из сообщений от индикатора после его инициализации;
- номер бара bar, с которого считываются данные (обычно текущий незавершенный — 0 или последний завершенный — 1);
- массив data со значениями, прочитанными из буферов индикатора на указанном баре;
- время последнего чтения lastRead;
- признак работы по тикам или барам tickwise.
Кроме того, в классе используется объект MultiSymbolMonitor для синхронизации баров всех задействованных символов.
class UnityController
|
В конструкторе, принимающем через аргументы все параметры для индикатора, создаем сам индикатор и настраиваем объект sync.
public:
|
Количество буферов устанавливается методом attached. Мы его вызовем по получению сообщения от индикатора.
void attached(const int b)
|
Специальный метод isReady возвращает true, когда последние бары всех символов имеют одинаковое время. Только в состоянии такой синхронизации мы получим корректные значения индикатора. Следует обратить внимание, что здесь предполагается одинаковое расписание торговых сессий всех инструментов. Если это не так, анализ синхронизации нужно будет поменять.
bool isReady()
|
Текущее время мы определяем по-разному в зависимости от режима работы индикатора: при пересчете на каждом тике (tickwise равно true) берем серверное время, а при пересчете один раз за бар — время открытия последнего бара.
datetime lastTime() const
|
Наличие данного метода позволит исключить чтение индикатора, если текущее время не изменилось и, соответственно, последние прочитанные данные, хранящиеся в буфере data, еще актуальны. А вот, собственно, как организовано чтение индикаторных буферов в методе read. Нам достаточно одного значения каждого буфера для бара с индексом bar.
bool read()
|
В конце мы как раз сохраняем время чтения в переменную lastRead. Если она пуста или не равна новому текущему времени, обращение за данными контроллера в следующих методах вызовет чтение буферов индикатора с помощью read.
Основными внешними методами контроллера являются getOuterIndices для получения индексов максимального и минимального значений, а также оператор '[]' для чтения самих значений.
bool isNewTime() const
|
Напомним, что в эксперте BandOsMA.mq5 мы ввели концепцию интерфейса TradingSignal.
interface TradingSignal
|
На его основе опишем реализацию сигнала с использованием индикатора UnityPercentEvent. Объект контроллера UnityController передается в конструктор. Здесь же указываются индексы валют (буферов), сигналы по которым нас интересуют. Мы сможем создать произвольный набор разных сигналов для выбранных рабочих инструментов.
class UnitySignal: public TradingSignal
|
Метод signal возвращает 0 в неопределенной ситуации, либо +1 или -1 в состояниях перекупленности и перепроданности двух конкретных валют.
Для формализации торговых стратегий мы использовали интерфейс TradingStrategy.
interface TradingStrategy
|
В данном случае на его основе создан класс UnityMartingale, во многом совпадающий с SimpleMartingale из MultiMartingale.mq5. Мы покажем лишь отличия.
class UnityMartingale: public TradingStrategy
|
Торговая часть готова. Осталось рассмотреть инициализацию. На глобальном уровне описан автоуказатель на объект UnityController, а также массив с названиями валют. Пул торговых систем полностью аналогичен прежним наработкам.
AutoPtr<TradingStrategyPool> pool;
|
В обработчике OnInit создаем объект UnityController и ждем, когда индикатор пришлет распределение валют по индексам буферов.
int OnInit()
|
Если во входных параметрах индикатора выбран тип цены PRICE_CLOSE и единичный период, расчет в контроллере будет производиться один раз на баре. Во всех остальных случаях сигналы будут обновляться по тикам, но не чаще раза в секунду (вспоминаем реализацию метода lastTime в контроллере).
Вспомогательный метод StartUp в целом занимается тем же делом, что старый обработчик OnInit в эксперте MultiMartingale — заполнением структуры Settings с настройками, их проверкой на корректность и созданием пула торговых систем TradingStrategyPool, состоящего из объектов класса UnityMartingale для разных торгуемых символов WorkSymbols. Правда, теперь данный процесс разделен на 2 этапа из-за того, что нам нужно дождаться информации о распределении валют по буферам. Поэтому функция StartUp имеет входной параметр, обозначающий вызов из OnInit и — позднее — из OnChartEvent.
При разборе исходного кода StartUp важно помнить, что инициализация различается для случаев, когда мы торгуем только одним инструментом, совпадающим с текущим графиком, и когда задана корзина инструментов. Первый режим активен, когда в WorkSymbols пустая строка. Он удобен для оптимизации эксперта по конкретному инструменту. Найдя настройки для нескольких инструментов, мы можем объединить их в строке WorkSymbols.
bool StartUp(const bool init = false)
|
В OnInit функция StartUp вызывается с параметром true, что означает лишь проверку корректности настроек. Создание объекта торговой системы откладывается до получения сообщения от индикатора в OnChartEvent.
void OnChartEvent(const int id,
|
Здесь мы запоминаем количество валют в глобальной переменной currenciesCount и сохраняем их в массиве currencies, после чего вызываем StartUp уже с параметром false (значение по умолчанию, поэтому опущено). Сообщения поступают из очереди в том порядке, в котором они находятся в буферах индикатора. Таким образом, мы получаем соответствие между индексом и названием валюты.
При повторном вызове StartUp выполняется дополнительный код:
bool StartUp(const bool init = false)
|
Вспомогательная функция SplitSymbolToCurrencyIndices выделяет базовую валюту и валюту прибыли переданного символа и находит для них индексы в массиве currencies. Таким образом, мы получаем опорные данные для генерации сигналов в объектах UnitySignal — в каждом будет своя пара индексов валют.
bool SplitSymbolToCurrencyIndices(const string symbol, int &first, int &second)
|
В целом, эксперт готов.
Нетрудно заметить, что в последних примерах экспертов у нас фигурируют классы стратегий и классы торговых сигналов. Мы специально сделали их наследниками обобщенных интерфейсов TradingStrategy и TradingSignal, чтобы впоследствии иметь возможность собирать коллекции совместимых, но различных реализаций, которые можно комбинировать при разработке будущих экспертов. Подобные унифицированные конкретные классы обычно подлежат выделению в отдельные заголовочные файлы. В наших примерах мы не сделали это ради упрощения пошаговой модификации.
Но описанный подход является стандартным для ООП. В частности, как мы уже упоминали в разделе о создании заготовок экспертов, вместе с MetaTrader 5 поставляется "фреймворк" (framework) заголовочных файлов со стандартными классами торговых операций, сигнальных индикаторов и управления денежными средствами, которые используются в Мастере MQL. Другие похожие решения публикуются на сайте mql5.com в разделе статей и базы кодов.
В своих разработках вы можете взять за основу любую из готовых иерархий классов, которая удовлетворяет вас по своим возможностям и удобству пользования.
Для полноты картины мы хотели внедрить в эксперт собственный критерий оптимизации на базе R2. Чтобы избавиться от противоречия между линейной регрессией в формуле расчета R2 и переменными лотами, которые заложены в нашу стратегию, будем вычислять коэффициент не для обычной линии баланса, а для её кумулятивных приращений, нормированных по размерам лотов в каждой сделке.
Для этого в обработчике OnTester сделаем выборку сделок только по типам DEAL_TYPE_BUY и DEAL_TYPE_SELL и с направлением "выход" (OUT). Для сделок запросим все свойства, формирующие финансовый результат (прибыль/убыток), то есть DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, а также их объем DEAL_VOLUME.
#define STAT_PROPS 5 // количество запрашиваемых свойств сделки
double OnTester() { HistorySelect(0, LONG_MAX);
const ENUM_DEAL_PROPERTY_DOUBLE props[STAT_PROPS] = { DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, DEAL_VOLUME }; double expenses[][STAT_PROPS]; ulong tickets[]; // нужно из-за прототипа метода 'select', но удобно для отладки
DealFilter filter; filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE) .let(DEAL_ENTRY, (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY), IS::OR_BITWISE) .select(props, tickets, expenses); ... |
Далее в массиве balance накапливаем прибыли/убытки, нормированные торговыми объемами, и вычисляем для него критерий R2.
const int n = ArraySize(tickets);
|
На этом первая версия эксперта в принципе готова. Мы оставили "за скобками" проверку модели тиков с помощью TickModel.mqh. Предполагается, что эксперт будет тестироваться при генерации тиков в режиме OHLC M1 или лучше. При обнаружении модели "только цены открытия" эксперт пошлет в терминал специальный фрейм со статусом ошибки и выгрузит себя из тестера. К сожалению, это остановит только данный проход, но оптимизация продолжится. Поэтому копия эксперта, которая выполняется в терминале, выдает "алерт" для пользователя, чтобы он прервал оптимизацию вручную.
void OnTesterPass()
|
Вы можете провести оптимизацию параметров SYMBOL SETTINGS для любого символа, и повторить её для разных символов. При этом в группах COMMON SETTINGS и UNITY SETTINGS всегда должны быть одни и те же настройки, потому что они применяются ко всем символам и экземплярам торговых систем. Например, сопровождение (Trailing) должно быть либо включено, либо выключено для всех оптимизаций. Также следует помнить, что входные переменные для отдельного символа (т.е. группы SYMBOL SETTINGS) имеют эффект только пока в WorkSymbols находится пустая строка. Поэтому на стадии оптимизаций следует держать его пустым.
Например, для диверсификации рисков можно последовательно оптимизировать эксперт на полностью независимых парах: EURUSD, AUDJPY, GBPCHF, NZDCAD или в других сочетаниях. К исходному коду подключено 3 set-файла с примерами частных настроек.
#property tester_set "UnityMartingale-eurusd.set"
|
Для того чтобы торговать сразу по трем символам, следует "упаковать" эти настройки в общий параметр WorkSymbols:
EURUSD+0.01*1.6^5(200,200)[17,21];GBPCHF+0.01*1.2^8(600,800)[7,20];AUDJPY+0.01*1.2^8(600,800)[7,20] |
Такая настройка тоже приложена в отдельном файле.
#property tester_set "UnityMartingale-combo.set" |
Одна из проблем с текущей версией эксперта заключается в том, что отчет тестера сообщит нам общую статистику по всем символам (точнее по всем торговым стратегиям, так как мы можем включить в пул разные классы), в то время как нам было бы интересно контролировать и оценивать каждый компонент системы отдельно.
Чтобы это сделать, необходимо научиться самостоятельно подсчитывать основные финансовые показатели торговли, по аналогии с тем, как это делает для нас тестер. Займемся этим на втором этапе развития эксперта.
UnityMartingaleDraft2.mq5
Подсчет статистики — это часто возникающая задача, поэтому выделим её в отдельный заголовочный файл TradeReport.mqh, где организуем исходный код в соответствующие классы.
Основной класс так и назовем — TradeReport. Многие показатели торговли зависят от кривых баланса и свободных средств (эквити). Поэтому в классе имеются переменные для отслеживания текущего баланса и прибыли, а также постоянно дополняемый массив с историей баланса. Историю эквити мы хранить не будем, потому что она может меняться на каждом тике, и лучше считать её прямо на ходу. А для чего нам понадобится кривая баланса, мы расскажем чуть позже.
class TradeReport
|
Изменение и чтение полей класса производится с помощью методов, включая и конструктор, в котором баланс инициализируется свойством ACCOUNT_BALANCE.
TradeReport()
|
Эти методы потребуются для итеративного расчета просадки по эквити (на лету). Массив баланса data потребуется для одномоментного расчета просадки по балансу (это будем делать уже в конце теста).
На основе колебаний кривой (не важно, баланса или эквити) по одному и тому же алгоритму должна считаться абсолютная и относительная просадка. Поэтому данный алгоритм и необходимые для него внутренние переменные, хранящие промежуточные состояния, реализованы во вложенной структуре DrawDown: мы не станем её приводить полностью, а лишь представим главные методы и свойства.
struct DrawDown
|
Первый метод calcDrawdown рассчитывает просадки, когда нам известен весь массив и это будет использовано для баланса. Второй метод calcDrawdown рассчитывает просадку итеративно: при каждом вызове ему сообщается очередное значение ряда, и это будет использовано для эквити.
Помимо просадки, как мы знаем, существует большое количество стандартных статистических показателей для отчетов, но мы поддержим для начала лишь некоторые из них. Для этого опишем соответствующие поля в еще одной вложенной структуре GenericStats. Она унаследована от структуры DrawDown, потому что просадка нам все равно потребуется в отчете.
struct GenericStats: public DrawDown
|
По названиям переменных легко догадаться, каким стандартным метрикам они соответствуют. Некоторые показатели излишни и потому опущены. Например, имея общее количество трейдов (trades) и количество покупок среди них (buy_trades), легко найти количество продаж (trades - sell_trades). То же самое касается дополняющих друг друга статистик по выигрышам/проигрышам. Длительности выигрышных и проигрышных серий не подсчитываются. Желающие могут дополнить наш отчет этими показателями.
Для унификации с общей статистикой тестера имеется метод fillByTester, заполняющий все поля через функцию TesterStatistics. Позднее мы им воспользуется.
void fillByTester()
|
Но разумеется, нам нужно реализовать и свой собственный расчет для тех раздельных балансов и эквити торговых систем, которые тестер считать не умеет. Выше были представлены прототипы методов calcDrawdown: они в процессе свой работы как раз заполняют последнюю группу полей с префиксом "series_dd". Также в классе TradeReport есть метод для расчета коэффициента Шарпа. На вход он принимает ряд чисел и ставку безрискового финансирования. С полным исходным кодом можно ознакомиться в прилагаемом файле.
static double calcSharpe(const double &data[], const double riskFreeRate = 0); |
Как нетрудно догадаться, при вызове этого метода мы передадим в параметре data одноименный массив-член класса TradeReport с отсчетами баланса. Процесс же заполнения этого массива и вызов вышеупомянутых методов для конкретных показателей происходит в методе calcStatistics (см. ниже). Ему на вход передается объект-фильтр сделок (filter), начальные депозит (start) и время (origin). Предполагается, что вызывающий код настроит фильтр таким образом, чтобы под него попали только сделки интересующей нас торговой системы.
Метод возвращает заполненную структуру GenericStats, а кроме того заполняет два массива внутри объекта TradeReport: data и moments — значениями баланса и временными отсчетами изменений, соответственно. Нам это пригодится в окончательной версии эксперта.
GenericStats calcStatistics(DealFilter &filter,
|
Здесь видно, как мы вызываем calcSharpe и calcDrawdown для получения соответствующих показателей на массиве data. Остальные показатели считаются непосредственно в цикле внутри calcStatistics.
Вооружившись классом TradeReport, расширим функционал эксперта до версии UnityMartingaleDraft2.mq5.
Добавим в класс UnityMartingale новые члены.
class UnityMartingale: public TradingStrategy
|
Объект report нам нужен для вызова calcStatistics, куда будет включена просадка по балансу. Объект equity потребовался для независимого расчета просадки по эквити. Начальный баланс и дата, а также начало расчета просадки по эквити задаются в конструкторе.
public:
|
Продолжение расчета просадки по эквити делается на лету — при каждом вызове метода trade.
virtual bool trade() override
|
Но это далеко не все, что нужно для правильного расчета. Дело в том, что плавающую прибыль или убыток мы должны учитывать поверх баланса. В показанном фрагменте делается вызов только addFloatingPL, но в классе TradeReport есть еще и метод для модификации баланса — addBalance. Однако баланс изменяется только при закрытии позиции.
Благодаря концепции ООП, закрытие позиции у нас соответствует удалению объекта position класса PositionState. Так не можем ли мы перехватить его?
Непосредственно в классе PositionState для этого средств не предусмотрено, но мы можем объявить производный класс PositionStateWithEquity с особым конструктором и деструктором.
При создании объекта в конструктор передается не только идентификатор позиции, но и указатель на объект отчета, в который нужно будет отправить информацию.
class PositionStateWithEquity: public PositionState
|
В деструкторе мы находим все сделки по идентификатору закрытой позиции, подсчитываем общий финансовый результат (вместе с комиссиями и прочими отчислениями) и затем вызываем addBalance для связанного объекта report.
~PositionStateWithEquity()
|
Осталось прояснить один момент — как мы создадим для позиций объекты класса PositionStateWithEquity вместо PositionState. Для этого достаточно поменять оператор new в паре мест, где он вызывается в классе TradingStrategy.
position = MQLInfoInteger(MQL_TESTER) ? new PositionStateWithEquity(tickets[0], &report) : new PositionState(tickets[0]); |
Таким образом, мы разобрались со сбором данных, но осталось непосредственно сформировать отчет, то есть вызвать calcStatistics. Здесь не обойтись без расширения нашего интерфейса TradingStrategy: добавим в него метод statement.
interface TradingStrategy
|
Тогда в его реализации для нашей стратегии мы сможем довести работу до логического завершения.
class UnityMartingale: public TradingStrategy
|
Новый метод просто распечатает в журнале все рассчитанные показатели. Пробросив этот же метод через пул торговых систем TradingStrategyPool, запросим раздельные отчеты для всех символов из обработчика OnTester.
double OnTester()
|
Проверим корректность работы своего отчета. Для этого запустим эксперт в тестере по одному символу и сравним стандартный отчет с нашими расчетами. Например, для настройки UnityMartingale-eurusd.set, торгуя на EURUSD H1 получим за 2021 такие показатели.
Отчет тестера за 2021 год, EURUSD H1
В журнале наш вариант отображается как две структуры: DrawDown с показателями просадки по эквити и GenericStats с показателями просадки по балансу и прочей статистикой.
Separate trade report for EURUSD Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10022.48 10017.03 10000.00 9998.20 6.23 0.06 » » [series_dd_relative_percent] [series_dd_relative] » 0.06 6.23
Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10022.40 10017.63 10000.00 9998.51 5.73 0.06 » » [series_dd_relative_percent] [series_dd_relative] » » 0.06 5.73 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 194 97 43 42 19 23 57.97 -39.62 18.35 1.46 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.19 3.20 2.00 -2.01 0.15 |
Легко убедиться, что эти числа совпадают с отчетом тестера.
Теперь запустим на том же периоде торговлю сразу по трем символам (настройка UnityMartingale-combo.set).
В дополнение к записям по EURUSD в журнале появятся структуры для GBPCHF и AUDJPY.
Separate trade report for GBPCHF Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10029.50 10000.19 10000.00 9963.65 62.90 0.63 » » [series_dd_relative_percent] [series_dd_relative] » 0.63 62.90 Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10023.68 9964.28 10000.00 9964.28 59.40 0.59 » » [series_dd_relative_percent] [series_dd_relative] » » 0.59 59.40 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 600 300 154 141 63 78 394.53 -389.33 5.20 1.01 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.02 0.09 9.10 -6.73 0.01
Separate trade report for AUDJPY Equity DD: [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10047.14 10041.53 10000.00 9961.62 48.20 0.48 » » [series_dd_relative_percent] [series_dd_relative] » 0.48 48.20 Trade Statistics (with Balance DD): [maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] » [0] 10045.21 10042.75 10000.00 9963.62 44.21 0.44 » » [series_dd_relative_percent] [series_dd_relative] » » 0.44 44.21 » » [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] » » 332 166 91 89 54 35 214.79 -170.20 44.59 1.26 » » [average_trade] [recovery] [max_profit] [max_loss] [sharpe] » 0.27 1.01 7.58 -5.17 0.09 |
Отчет тестера в данном случае будет содержать обобщенные данные, так что благодаря своим классам мы получили недоступную ранее детализацию.
Однако смотреть на псевдо-отчет в журнале не очень удобно. Более того, хотелось бы увидеть и графическое представление хотя бы линии баланса — её внешний вид зачастую говорит больше о пригодности системы, чем сухая статистика.
Усовершенствуем эксперт, наделив его возможностью формировать наглядные отчеты в формате HTML: в конце концов, отчеты тестера также можно выгружать в HTML, сохранять и сравнивать по прошествии времени. Кроме того, подобные отчеты можно будет в перспективе передавать во фреймах в терминал прямо во время оптимизации, и пользователь получит возможность начать изучать отчеты конкретных проходов еще до завершения всего процесса.
Это будет предпоследняя версия примера UnityMartingaleDraft3.mq5.
UnityMartingaleDraft3.mq5
Визуализация торгового отчета включает линию баланса и таблицу со статистическими показателями. Мы не станем формировать полный отчет, аналогичный отчету тестера, а ограничимся избранными наиболее важными величинами — нам важно реализовать рабочий механизм, который можно затем кастомизировать в соответствии с личными требованиями.
Основу алгоритма оформим в виде класса TradeReportWriter (TradeReportWriter.mqh). Класс сможет хранить произвольное количество отчетов разных торговых систем: каждая в отдельном объекте DataHolder, который включает массивы значений и временных меток баланса (data и when, соответственно), структуру со статистикой stats, а также название, цвет и ширину линии для отображения.
class TradeReportWriter
|
Под объекты класса DataHolder выделен массив автоуказателей curves. Кроме того, нам понадобятся общие границы по суммам и срокам для совмещения линий всех торговых систем в картинке — это обеспечат переменные lower, upper, start и stop.
AutoPtr<DataHolder> curves[];
|
Добавить линию баланса позволяет метод addCurve.
virtual bool addCurve(double &data[], datetime &when[], const string name,
|
Второй вариант метода addCurve добавляет не только линию баланса, но и набор финансовых показателей в структуре GenericStats.
virtual bool addCurve(TradeReport::GenericStats &stats,
|
Наконец, самый главный метод класса — для визуализации отчета — сделан абстрактным.
virtual void render() = 0; |
Это дает возможность реализовать много способов отображения отчетов, например, как с записью в файлы разных форматов, так и с отрисовкой непосредственно на графике. Мы сейчас ограничимся формированием html-файлов, так как это наиболее технологичный и широко распространенный способ.
Новый класс HTMLReportWriter имеет конструктор, в параметрах которого указывается название файла, а также размеры картинки с кривыми балансов. Само изображение будем генерировать в известном формате векторной графики SVG: он идеально походит в данном случае, так как представляет собой подмножество XML-языка, коим является и сам HTML.
class HTMLReportWriter: public TradeReportWriter
|
Прежде чем обратиться к главному публичному методу render, потребуется познакомить читателя с одной технологией, которая будет подробно описана в заключительной 7-ой Части книги. Речь о ресурсах: файлах и массивах произвольных данных, подключаемых к MQL-программе для работы с мультимедиа (звук и изображения), встраивания откомпилированных индикаторов или просто как хранилище прикладной информации. Именно последним вариантом мы сейчас и воспользуемся.
Дело в том, что генерировать HTML-страницу лучше не целиком из MQL-кода, а на основе шаблона (заготовки страницы), в которую MQL-код лишь вставит значения некоторых переменных. Это известный прием в программировании, позволяющий разделить алгоритм и внешнее представление программы (или результата её работы). За счет этого мы можем раздельно экспериментировать с HTML-шаблоном и MQL-кодом, работая с каждой из составных частей в привычной среде. В частности, MetaEditor все же не очень приспособлен для редактирования веб-страниц и их просмотра, точно также как стандартный браузер ничего не "знает" о MQL5 (хотя это можно исправить).
HTML-шаблоны отчетов мы как раз и будем хранить в текстовых файлах, подключенных к исходному коду MQL5 как ресурсы. Подключение делается с помощью специальной директивы #resource. Например, вот какая строка есть в файле TradeReportWriter.mqh.
#resource "TradeReportPage.htm" as string ReportPageTemplate |
Она означает, что рядом с исходным кодом должен быть файл TradeReportPage.htm, который станет доступен в MQL-коде в виде строки ReportPageTemplate. По расширению вы можете понять, что файл представляет собой веб-страницу. Приведем содержимое этого файла с сокращениями (у нас нет задачи обучить читателя веб-разработке, хотя, как видно, знания в этой области могут пригодиться и для трейдера). Отступы добавлены для наглядного представления иерархии вложенности HTML-тегов, в файле отступов нет.
<!DOCTYPE html>
|
Принципы работы шаблонов выбирает разработчик. Существует большое количество уже готовых систем HTML-шаблонов, но они предоставляют много избыточных возможностей и потому слишком сложны для нашего примера. Мы разработаем свою концепцию.
Для начала обратим внимание, что в большинстве веб-страниц есть начальная часть (заголовок), есть завершающая часть ("подвал"), и между ними располагается полезная информация. Вышеприведенная заготовка отчета в этом смысле не исключение. Для обозначения полезного содержимого в ней использован символ тильды '~'. Вместо него MQL-код должен будет вставить изображение баланса и таблицу с показателями. Но наличие '~' необязательно, так как страница может представлять собой единое целое, то есть ту саму полезную среднюю часть: ведь MQL-код сможет при необходимости вставлять результат обработки одного шаблона в другой.
Чтобы завершить отступление касательно HTML-шаблонов, обратим внимание на еще один нюанс. В принципе, веб-страница состоит из тегов, выполняющих разные по сути функции. Стандартные теги HTML сообщают браузеру, что отображать. Кроме них имеются каскадные стили (CSS), которые описывают, как это отображать. Наконец, в странице может быть динамическая составляющая в виде скриптов JavaScript, которые интерактивно управляют и первым, и вторым.
Обычно эти три компонента подвергаются шаблонизации независимо, то есть, например, HTML-шаблон, строго говоря, должен содержать только HTML, но не CSS и JavaScript. Это позволяет, опять-таки, "развязать" содержимое, оформление и поведение веб-страницы, что облегчает разработку (рекомендуется придерживаться такого же подхода и в MQL5!).
Однако в нашем примере мы внесли в шаблон все компоненты. В частности, в приведенном шаблоне мы видим тег <style> со стилями CSS и тег <script> с некоторыми функциями JavaScript, которые опущены. Это сделано для упрощения примера, с акцентом на возможности MQL5, а не веб-разработки.
Имея шаблон веб-страницы в переменной ReportPageTemplate, подключенной как ресурс, мы можем написать метод render.
virtual void render() override
|
Он фактически разделяет страницу на верхнюю и нижнюю половины по символу '~', выводит их как есть, а между ними вызывает вспомогательный метод renderContent.
Мы уже описали, из чего будет состоять отчет: общая картинка с кривыми баланса и таблицы с показателями торговых систем, поэтому реализация renderContent закономерна.
private:
|
Генерация картинки внутри renderSVG основана на еще одном файле шаблона TradeReportSVG.htm, который связывается со строковой переменной SVGBoxTemplate:
#resource "TradeReportSVG.htm" as string SVGBoxTemplate |
Содержимое этого шаблона — последнее, которое мы здесь приведем. В исходные коды остальных шаблонов желающие могут заглянуть сами.
<span id="params" style="display:block;width:%WIDTH%px;text-align:center;"></span>
|
В коде метода renderSVG мы увидим знакомый прием с разделением содержимого на два блока "до" и "после" тильды, однако здесь встречается кое-что новое.
void renderSVG()
|
В начальной части страницы, в строке headerAndFooter[0] мы ищем подстроки особого вида "%WIDTH%" и "%HEIGHT%", и заменяем их на требуемые ширину и высоту картинки. Именно по такому принципу в наших шаблонах действует подстановка значений. Например, в данном шаблоне действительно встречаются данные подстроки в теге rect:
<rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%" style="fill:none; stroke-width:1; stroke: black;"/> |
Таким образом, если построение отчета заказано размером 600 на 400, строка преобразуется в следующую:
<rect x="0" y="0" width="600" height="400" style="fill:none; stroke-width:1; stroke: black;"/> |
Это выведет в браузере черную рамку указанных размеров толщиной 1 пиксель.
Генерацией тегов для рисования конкретных линий баланса занимается метод renderCurve, в который передаются все необходимые массивы и прочие настройки (название, цвет, толщина). Мы оставим данный метод и прочие узкоспециализированные методы (renderTables, renderTable) для самостоятельного изучения.
Вернемся к основному модулю эксперта UnityMartingaleDraft3.mq5. Зададим размеры изображения графиков балансов и подключим TradeReportWriter.mqh.
#define MINIWIDTH 400
|
Для того чтобы "подружить" стратегии с построителем отчетов потребуется модифицировать метод statement в интерфейсе TradingStrategy: передадим параметром указатель на объект TradeReportWriter, который вызывающий код сможет создать и настроить.
interface TradingStrategy
|
В конкретной реализации этого метода в классе нашей стратегии UnityMartingale добавим несколько строк.
class UnityMartingale: public TradingStrategy
|
Все сводится к тому, чтобы получить массив баланса и структуру с показателями из объекта report (класса TradeReport) и передать в объект TradeReportWriter, вызвав addCurve.
Разумеется, в пуле торговых стратегий обеспечена передача одного и того же объекта TradeReportWriter во все стратегии для генерации совмещенного отчета.
class TradingStrategyPool: public TradingStrategy
|
Наконец, наибольшей модификации подвергся обработчик OnTester. Для генерации HTML-отчета торговых стратегий было бы достаточно следующих строк.
double OnTester()
|
Однако, для наглядности и удобства пользователя было бы здорово добавить в отчет и общую кривую баланса, а также таблицу с общими показателями. Их имеет смысл выводить только когда в настройках эксперта указано несколько символов, потому что в противном случае отчет одной стратегии совпадает с общим в файле.
Это потребовало чуть больше кода.
double OnTester()
|
Посмотрим, что у нас получилось. Если запустить эксперт с настройками UnityMartingale-combo.set, мы получим в папке одного из агентов MQL5/Files файл temp.html. Вот как он выглядит в браузере.
HTML-отчет для эксперта с несколькими торговыми стратегиями/символами
Теперь, когда мы умеем формировать отчеты об одном тестовом проходе, мы можем отправлять их во время оптимизации в терминал, отбирать на лету лучшие и представлять их пользователю еще до завершения всего процесса. Все отчеты будем складывать в отдельную папку внутри MQL5/Files терминала. Папка получит имя, содержащее символ и таймфрейм из настроек тестера, а также название эксперта.
UnityMartingale.mq5
Как мы знаем, для отправки файла в терминал достаточно вызвать функцию FrameAdd. Файл мы уже сформировали в рамках предыдущей версии.
double OnTester()
|
В приемной копии эксперта выполним необходимую подготовку. Опишем структуру Pass с основными параметрами каждого прохода оптимизации.
struct Pass
|
В строке parameters пары "имя=значение" соединяются символом '&' — это пригодится для взаимодействия веб-страниц отчетов в дальнейшем (символ '&' — стандарт для объединения параметров в веб-адресах). Формат set-файлов мы не описывали, но исходный код далее для формирования строки preset позволяет изучить данный вопрос, так сказать, изнутри, на практике.
По мере поступления фреймов мы будем записывать улучшения по критерию оптимизации в массив TopPasses. Текущий лучший проход всегда будет в массиве последним и кроме того доступен в переменной BestPass.
Pass TopPasses[]; // стек постоянно улучшающихся проходов (последний - лучший)
|
В обработчике OnTesterInit сформируем имя папки.
void OnTesterInit()
|
В обработчике OnTesterPass будем последовательно отбирать только те фреймы, в которых показатель улучшился, выяснять для них значения оптимизируемых и прочих параметров и складывать всю эту информацию в массив структур Pass.
void OnTesterPass()
|
Полученные отчеты с улучшениями сохраняются в файлах с именами, включающими значение критерия оптимизации и номер прохода.
А теперь самое интересное. В обработчике OnTesterDeinit мы можем сформировать общий html-файл (overall.htm), позволяющий увидеть все отчеты сразу (или, скажем, 100 лучших). Здесь используется тот же принцип с шаблонами, что мы рассмотрели раньше.
#resource "OptReportPage.htm" as string OptReportPageTemplate
|
На следующем изображении показано, как выглядит обзорная веб-страница после выполнения оптимизации UnityMartingale.mq5 по параметру UnityPricePeriod в мультивалютном режиме.
Обзорная веб-страница с торговыми отчетами лучших проходов оптимизации
Для каждого отчета мы отображаем лишь верхнюю часть, куда попадает график балансов. Эта часть — наиболее удобная для оценки беглым взглядом.
Над каждым графиком выводятся списки оптимизируемых параметров ("имя=значение&имя=значение..."). По щелчку мыши на такой строке открывается блок с текстом для set-файла всех настроек данного прохода. Если щелкнуть мышью внутри блока, его содержимое будет скопировано в буфер обмена. Его можно сохранить в текстовом редакторе и тем самым получить готовый set-файл.
Щелчок мыши по графику вызовет переход на страницу конкретного отчета, вместе с таблицами показателей (приводилась выше).
В завершении раздела коснемся еще одного вопроса. Ранее мы обещали продемонстрировать эффект от функции TesterHideIndicators. В эксперте UnityMartingale.mq5 сейчас используется индикатор UnityPercentEvent.mq5, и после любого теста индикатор выводится на открывающийся график. Предположим, что мы хотим скрыть от пользователя механизм работы эксперта и откуда он берет сигналы. Тогда можно вызвать функцию TesterHideIndicators (с параметром true) в обработчике OnInit, перед созданием объекта UnityController, в котором и происходит получение дескриптора через iCustom.
int OnInit()
|
Такая версия эксперта уже не будет выводить индикатор на график. Однако скрыт он не очень хорошо. Если заглянуть в журнал тестера, мы там увидим среди множества полезной информации строки о загружаемых программах: сначала сообщение о загрузке самого эксперта, а чуть погодя — о загрузке индикатора.
...
|
Таким образом, дотошный пользователь может узнать имя индикатора. Исключить такую возможность позволяет механизм ресурсов, о котором мы уже вскользь говорили в контексте заготовок веб-страниц. Оказывается, что откомпилированный индикатор также можно встроить в MQL-программу (в эксперт или в другой индикатор) как ресурс. И подобные программы-ресурсы уже не упоминаются в журнале тестера. Подробно мы изучим ресурсы в 7-й Части книги, а сейчас покажем связанные с ними строки в окончательной версии нашего эксперта.
Прежде всего, опишем ресурс с индикатором директивой #resource. Фактически она просто содержит путь к откомпилированному файлу индикатора (очевидно, он уже должен быть откомпилирован заранее), причем здесь обязательно использовать удвоенные обратные косые черты в качестве разделителей — прямые одинарные черты в путях ресурсов, увы, не поддерживаются.
#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5" |
Затем в строках с вызовом iCustom заменим прежний оператор:
UnityController(const string symbolList, const int offset, const int limit,
|
на точно такой же, но со ссылкой на ресурс (обратите внимание на синтаксис с ведущей парой двоеточий '::' — это нужно для различения обычных путей в файловой системе и путей внутри ресурсов).
UnityController(const string symbolList, const int offset, const int limit,
|
Теперь откомпилированную версию советника можно поставлять пользователям саму по себе, без отдельного индикатора, так как тот спрятан внутри советника. На работе это никак не сказывается, но с учетом вызова TesterHideIndicators, внутреннее устройство скрыто. Следует помнить, что если затем индикатор будет обновлен, потребуется перекомпилировать и советник.