- Генерация тиков в тестере
- Управление ходом времени в тестере: таймер, Sleep, GMT
- Визуализация тестирования: график, объекты, индикаторы
- Мультивалютное тестирование
- Критерии оптимизации
- Получение финансовых показателей теста: TesterStatistics
- Событие OnTester
- Авто-настройка: ParameterGetRange и ParameterSetRange
- Группа OnTester-событий для контроля оптимизации
- Отправка фреймов данных с агентов в терминал
- Получение фреймов данных в терминале
- Директивы препроцессора для тестера
- Управление видимостью индикаторов: TesterHideIndicators
- Эмуляция пополнения депозита и снятия средств
- Принудительная остановка тестирования: TesterStop
- Большой пример эксперта
- Математические вычисления
- Отладка и профилирование
- Ограничения работы функций в тестере
Получение фреймов данных в терминале
Фреймы, посланные с агентов тестирования функцией FrameAdd, попадают в терминал и записываются в порядке поступления в mqd-файл с именем эксперта в папку каталог_терминала/MQL5/Files/Tester. Поступление одного или сразу нескольких фреймов генерирует событие OnTesterPass.
Для анализа и чтения фреймов MQL5 API предоставляет 4 функции FrameFirst, FrameFilter, FrameNext, FrameInputs. Все функции возвращают логическое значение с признаком успеха (true) или ошибки (false).
Для доступа к имеющимся фреймам ядро поддерживает метафору внутреннего указателя на текущий фрейм. Указатель автоматически сдвигается вперед при чтении очередного фрейма функцией FrameNext, но его можно вернуть на начало всех фреймов с помощью FrameFirst или FrameFilter. Таким образом, MQL-программа может организовать перебор фреймов в цикле, пока не просмотрит все фреймы. И этот процесс можно при необходимости повторять, например, налагая разные фильтры в OnTesterDeinit.
bool FrameFirst()
Функция FrameFirst переводит внутренний указатель чтения фреймов на начало и сбрасывает фильтр (если он был ранее установлен с помощью функции FrameFilter).
В принципе, для однократного приема и обработки всех фреймов не требуется вызывать FrameFirst, так как указатель и так находится в начале при старте оптимизации.
bool FrameFilter(const string name, ulong id)
Устанавливает фильтр чтения фреймов и переводит внутренний указатель фреймов на начало. Фильтр будет влиять на то, какие фреймы попадут в последующие вызовы FrameNext.
Если в качестве первого параметра передана пустая строка, фильтр будет работать только по числовому параметру, то есть будут просматриваться все фреймы с указанным id. Если значение второго параметра равно ULONG_MAX, то работает только текстовый фильтр.
Вызов FrameFilter("", ULONG_MAX) эквивалентен вызову FrameFirst(), то есть равнозначен отсутствию фильтра.
Если вызываете FrameFirst или FrameFilter в OnTesterPass, проверьте, действительно это то, что нужно: вероятно код содержит логическую ошибку — возможно зацикливание, чтение одного и того же фрейма, или увеличение вычислительной нагрузки в геометрической прогрессии.
bool FrameNext(ulong &pass, string &name, ulong &id, double &value)
bool FrameNext(ulong &pass, string &name, ulong &id, double &value, void &data[])
Функция FrameNext читает один фрейм и перемещает указатель на следующий. В параметр pass будет записан номер прохода оптимизации. Параметры name, id и value получат значения, переданные в соответствующих параметрах функции FrameAdd.
Важно отметить, что функция может вернуть false вполне штатно, когда больше нет фреймов для чтения. В этом случае во встроенной переменной _LastError содержится значение 4000 (у него нет встроенного обозначения).
Вне зависимости от того, какая форма функции FrameAdd была использована для отправки данных, содержимое файла или массива будет помещено в приемный массив data. Тип приемного массива должен совпадать с типом отправляемого массива, а для случая отправки файла существуют нюансы.
Бинарный файл (FILE_BIN) желательно принимать в байтовый массив uchar, чтобы быть совместимым с любым размером (поскольку другие типы большего размера могут оказаться некратными размеру файла). Если размер файла (а фактически размер блока с данными в принятом фрейме) не будет кратен размеру типа приемного массива, функция FrameNext не прочитает данные и вернет ошибку INVALID_ARRAY (4006).
Текстовый файл Unicode (FILE_TXT или FILE_CSV без модификатора FILE_ANSI) необходимо принимать в массив типа ushort и затем конвертировать в строку с помощью вызова ShortArrayToString. Текстовый файл ANSI следует принимать в массив типа uchar и конвертировать с помощью CharArrayToString.
bool FrameInputs(ulong pass, string ¶meters[], uint &count)
Функция FrameInputs позволяет получить описания и значения input-параметров эксперта, на которых сформирован проход с указанным номером pass. Строковый массив parameters будет заполнен строками вида "ИмяПараметраN=значениеПараметраN". В параметр count записывается количество элементов в массиве parameters.
Вызовы всех 4-х функций разрешено делать только внутри обработчиков OnTesterPass и OnTesterDeinit.
Фреймы могут приходить в терминал пачками и для их доставки требуется время, поэтому не обязательно, что все из них успеют сгенерировать событие OnTesterPass и будут обработаны до окончания оптимизации. В связи с этим для гарантированного получения всех запоздавших фреймов необходимо поместить блок кода с их обработкой (с использованием функции FrameNext) в OnTesterDeinit.
Рассмотрим простой пример FrameTransfer.mq5.
В эксперте имеется 4 тестовых параметра. Все они, кроме последнего строкового, могут быть включены в оптимизацию.
input bool Parameter0;
|
Однако для упрощения примера количество шагов для параметров Parameter1 и Parameter2 ограничено 10-ю (для каждого). Таким образом, если не использовать Parameter0, максимальное количество проходов равно 121. Parameter3 служит примером параметра, который нельзя включить в оптимизацию.
Эксперт не торгует, а генерирует случайные данные, которые имитируют прикладные данные произвольного назначения. Не используйте в своих рабочих проектах такую рандомизацию, как здесь: она подходит только для демонстрации.
ulong startup; // засекаем время одного прогона (просто как демо-данные)
|
Данные отправляются фреймами двух типов: из файла и из массива. Для каждого типа выделен свой идентификатор.
#define MY_FILE_ID 100
|
Файл записывается как бинарный с простыми строками. Результатом (критерием) OnTester является простое арифметическое выражение с участием Parameter1 и Parameter2.
На принимающей стороне, в экземпляре эксперта, выполняющемся в сервисном режиме на графике терминала, мы собираем данные всех фреймов с файлами и складываем их в общий CSV-файл. Файл открывается в обработчике OnTesterInit.
int handle; // файл для сбора прикладных результатов
|
Как было сказано ранее, все фреймы могут не успеть попасть в обработчик OnTesterPass, и их нужно дополнительно проверить в OnTesterDeinit. Поэтому мы реализовали одну вспомогательную функцию ProcessFileFrames, которую будем вызывать и из OnTesterPass, и из OnTesterDeinit.
Внутри ProcessFileFrames мы ведем свой внутренний счетчик обработанных фреймов framecount. На его примере мы убедимся, что порядок прихода фреймов и нумерация тестовых проходов часто не совпадают.
void ProcessFileFrames()
|
Для приема фреймов в функции описаны переменные, необходимые согласно прототипу FrameNext. Приемный массив данных здесь описан типа uchar. Если бы мы записывали в свой двоичный файл некие структуры, то могли бы принимать их непосредственно в массив структур того же типа.
ulong pass;
|
Далее описаны переменные для получения входных переменных эксперта для текущего прохода, которому принадлежит фрейм.
string params[];
|
Затем мы в цикле читаем фреймы с помощью FrameNext. Напомним, что в обработчик может поступить сразу несколько фреймов, поэтому нужен цикл. Для каждого фрейма мы выводим в журнал (терминала) номер прохода, название фрейма и полученное значение double. Фреймы с идентификатором, отличным от MY_FILE_ID мы пропускаем, и будем их обрабатывать потом.
ResetLastError();
|
Для фреймов с MY_FILE_ID мы выполняем следующие действия: запрашиваем входные переменные, узнаем, какие из них включены в оптимизацию, и сохраняем их значения в общий CSV-файл вместе с информацией из фрейма. Когда счетчик фреймов равен 0, мы формируем заголовок CSV-файла в переменной header. Во всех фреймах текущая (новая) запись для CSV-файла формируется в переменной record.
void ProcessFileFrames()
|
Вызов ParameterGetRange можно было также сделать более эффективно — только при нулевом значении счетчика framecount. Это оставлено как самостоятельное упражнение.
В обработчике OnTesterPass просто вызываем ProcessFileFrames.
void OnTesterPass()
|
Дополнительно вызываем ту же функцию из OnTesterDeinit и закрываем CSV-файл.
void OnTesterDeinit()
|
Кроме того в OnTesterDeinit делаем обработку фреймов с MY_TIME_ID. В данных фреймах к нам приходят длительности тестовых проходов, и здесь рассчитывается средняя длительность одного прохода. В принципе, это имеет смысл делать только для анализа в своей программе, так как для пользователя длительности проходов и так выводятся тестером в журнал.
void OnTesterDeinit()
|
Эксперт готов. Включим для него оптимизацию полным перебором (потому что общее количество вариантов искусственно ограничено и мало для генетики), можно в режиме только по ценам открытия, так как эксперт не торгует. Из-за этого, кстати говоря, следует выбрать пользовательский критерий (все остальные критерии дадут 0). Например, установим диапазон изменения Parameter1 от 1 до 10 с единичным шагом, а Parameter2 от -0.5 до +0.5 с шагом 0.1.
Запустим оптимизацию. В журнале экспертов в терминале увидим записи о получаемых фреймах вида:
Pass: 0 Frame: binfile Value:5105.000000
|
В файле output.csv появятся соответствующие строки с номерами проходов, значениями параметров и содержимым фреймов:
Counter,Pass ID,Parameter1,Parameter2,Value,File Content
|
Очевидно, что наша внутренняя нумерация (колонка Count) идет по порядку, а номера проходов Pass ID могут быть перемешаны (это зависит от многих факторов параллельной обработки пакетов заданий агентами). В частности, пакет заданий может первым закончить тот агент, которому были присвоены задания с бОльшими порядковыми номерами: в таком случае нумерация в файле начнется со старших проходов.
В журнале тестера можно проверить служебную статистику по фреймам.
242 frames (42.78 Kb total, 181 bytes per frame) received
|
Важно отметить, что при генетической оптимизации номера проходов представляются в отчете оптимизации как пара (номер поколения, номер экземпляра), в то время как номер прохода, получаемый в функции FrameNext — по-прежнему число ulong — фактически номер прохода в пакетных заданиях в контексте текущего запуска оптимизации. MQL5 не предоставляет средств для сопоставления нумерации проходов с "генетическим" отчетом. Для этой цели следует рассчитывать контрольные суммы входных параметров каждого прохода. Opt-файлы с кэшем оптимизации уже содержат такое поле с MD5-хэшем.