Получение фреймов данных в терминале

Фреймы, посланные с агентов тестирования функцией 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 &parameters[], uint &count)

Функция FrameInputs позволяет получить описания и значения input-параметров эксперта, на которых сформирован проход с указанным номером pass. Строковый массив parameters будет заполнен строками вида "ИмяПараметраN=значениеПараметраN". В параметр count записывается количество элементов в массиве parameters.

Вызовы всех 4-х функций разрешено делать только внутри обработчиков OnTesterPass и OnTesterDeinit.

Фреймы могут приходить в терминал пачками и для их доставки требуется время, поэтому не обязательно, что все из них успеют сгенерировать событие OnTesterPass и будут обработаны до окончания оптимизации. В связи с этим для гарантированного получения всех запоздавших фреймов необходимо поместить блок кода с их обработкой (с использованием функции FrameNext) в OnTesterDeinit.

Рассмотрим простой пример FrameTransfer.mq5.

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

input bool Parameter0;
input long Parameter1;
input double Parameter2;
input string Parameter3;

Однако для упрощения примера количество шагов для параметров Parameter1 и Parameter2 ограничено 10-ю (для каждого). Таким образом, если не использовать Parameter0, максимальное количество проходов равно 121. Parameter3 служит примером параметра, который нельзя включить в оптимизацию.

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

ulong startup// засекаем время одного прогона (просто как демо-данные)
   
int OnInit()
{
   startup = GetMicrosecondCount();
   MathSrand((int)startup);
   return INIT_SUCCEEDED;
}

Данные отправляются фреймами двух типов: из файла и из массива. Для каждого типа выделен свой идентификатор.

#define MY_FILE_ID 100
#define MY_TIME_ID 101
   
double OnTester()
{
   // посылаем файл в одном фрейме
   const static string filename = "binfile";
   int h = FileOpen(filenameFILE_WRITE | FILE_BIN | FILE_ANSI);
   FileWriteString(hStringFormat("Random: %d"MathRand()));
   FileClose(h);
   FrameAdd(filenameMY_FILE_IDMathRand(), filename);
   
   // посылаем массив в другом фрейме
   ulong dummy[1];
   dummy[0] = GetMicrosecondCount() - startup;
   FrameAdd("timing"MY_TIME_ID0dummy);
   
   return (Parameter2 + 1) * (Parameter1 + 2);
}

Файл записывается как бинарный с простыми строками. Результатом (критерием) OnTester является простое арифметическое выражение с участием Parameter1 и Parameter2.

На принимающей стороне, в экземпляре эксперта, выполняющемся в сервисном режиме на графике терминала, мы собираем данные всех фреймов с файлами и складываем их в общий CSV-файл. Файл открывается в обработчике OnTesterInit.

int handle// файл для сбора прикладных результатов
void OnTesterInit()
{
   handle = FileOpen("output.csv"FILE_WRITE | FILE_CSV | FILE_ANSI",");
}

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

Внутри ProcessFileFrames мы ведем свой внутренний счетчик обработанных фреймов framecount. На его примере мы убедимся, что порядок прихода фреймов и нумерация тестовых проходов часто не совпадают.

void ProcessFileFrames()
{
   static ulong framecount = 0;
   ...

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

   ulong   pass;
   string  name;
   long    id;
   double  value;
   uchar   data[];
   ...

Далее описаны переменные для получения входных переменных эксперта для текущего прохода, которому принадлежит фрейм.

   string  params[];
   uint    count;
   ...

Затем мы в цикле читаем фреймы с помощью FrameNext. Напомним, что в обработчик может поступить сразу несколько фреймов, поэтому нужен цикл. Для каждого фрейма мы выводим в журнал (терминала) номер прохода, название фрейма и полученное значение double. Фреймы с идентификатором, отличным от MY_FILE_ID мы пропускаем, и будем их обрабатывать потом.

   ResetLastError();
   
   while(FrameNext(passnameidvaluedata))
   {
      PrintFormat("Pass: %lld Frame: %s Value:%f"passnamevalue);
      if(id != MY_FILE_IDcontinue;
      ...
   }
   
   if(_LastError != 4000 && _LastError != 0)
   {
      Print("Error: "E2S(_LastError));
   }
}

Для фреймов с MY_FILE_ID мы выполняем следующие действия: запрашиваем входные переменные, узнаем, какие из них включены в оптимизацию, и сохраняем их значения в общий CSV-файл вместе с информацией из фрейма. Когда счетчик фреймов равен 0, мы формируем заголовок CSV-файла в переменной header. Во всех фреймах текущая (новая) запись для CSV-файла формируется в переменной record.

void ProcessFileFrames()
{
   ...
      if(FrameInputs(passparamscount))
      {
         string headerrecord;
         if(framecount == 0// готовим CSV заголовок
         {
            header = "Counter,Pass ID,";
         }
         record = (string)framecount + "," + (string)pass + ",";
         // собираем оптимизируемые параметры и их значения
         for(uint i = 0i < counti++)
         {
            string name2value[];
            int n = StringSplit(params[i], '=', name2value);
            if(n == 2)
            {
               long pvaluepstartpsteppstop;
               bool enabled = false;
               if(ParameterGetRange(name2value[0],
                  enabledpvaluepstartpsteppstop))
               {
                  if(enabled)
                  {
                     if(framecount == 0// готовим CSV заголовок
                     {
                        header += name2value[0] + ",";
                     }
                     record += name2value[1] + ","// поле данных
                  }
               }
            }
         }
         if(framecount == 0// готовим CSV заголовок
         {
            FileWriteString(handleheader + "Value,File Content\n");
         }
         // записываем данные в CSV
         FileWriteString(handlerecord + DoubleToString(value) + ","
            + CharArrayToString(data) + "\n");
      }
      framecount++;
   ...
}

Вызов ParameterGetRange можно было также сделать более эффективно — только при нулевом значении счетчика framecount. Это оставлено как самостоятельное упражнение.

В обработчике OnTesterPass просто вызываем ProcessFileFrames.

void OnTesterPass()
{
   ProcessFileFrames(); // стандартная обработка фреймов на лету
}

Дополнительно вызываем ту же функцию из OnTesterDeinit и закрываем CSV-файл.

void OnTesterDeinit()
{
   ProcessFileFrames(); // подбираем припозднившиеся фреймы
   FileClose(handle);   // закрываем CSV-файл
   ..
}

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

void OnTesterDeinit()
{
   ...
   ulong   pass;
   string  name;
   long    id;
   double  value;
   ulong   data[]; // тот же тип массива, что и при отправке
   
   FrameFilter("timing"MY_TIME_ID); // перемотка на первый фрейм
   
   ulong count = 0;
   ulong total = 0;
   // цикл только по фреймам 'timing'
   while(FrameNext(passnameidvaluedata))
   {
      if(ArraySize(data) == 1)
      {
         total += data[0];
      }
      else
      {
         total += (ulong)value;
      }
      ++count;
   }
   if(count > 0)
   {
      PrintFormat("Average timing: %lld"total / count);
   }
}

Эксперт готов. Включим для него оптимизацию полным перебором (потому что общее количество вариантов искусственно ограничено и мало для генетики), можно в режиме только по ценам открытия, так как эксперт не торгует. Из-за этого, кстати говоря, следует выбрать пользовательский критерий (все остальные критерии дадут 0). Например, установим диапазон изменения Parameter1 от 1 до 10 с единичным шагом, а Parameter2 от -0.5 до +0.5 с шагом 0.1.

Запустим оптимизацию. В журнале экспертов в терминале увидим записи о получаемых фреймах вида:

Pass: 0 Frame: binfile Value:5105.000000
Pass: 0 Frame: timing Value:0.000000
Pass: 1 Frame: binfile Value:28170.000000
Pass: 1 Frame: timing Value:0.000000
Pass: 2 Frame: binfile Value:17422.000000
Pass: 2 Frame: timing Value:0.000000
...
Average timing: 1811

В файле output.csv появятся соответствующие строки с номерами проходов, значениями параметров и содержимым фреймов:

Counter,Pass ID,Parameter1,Parameter2,Value,File Content
0,0,0,-0.5,5105.00000000,Random: 87
1,1,1,-0.5,28170.00000000,Random: 64
2,2,2,-0.5,17422.00000000,Random: 61
...
37,35,2,-0.2,6151.00000000,Random: 68
38,62,7,0.0,17422.00000000,Random: 61
39,36,3,-0.2,16899.00000000,Random: 71
40,63,8,0.0,17422.00000000,Random: 61
...
117,116,6,0.5,27648.00000000,Random: 74
118,117,7,0.5,16899.00000000,Random: 71
119,118,8,0.5,17422.00000000,Random: 61
120,119,9,0.5,28170.00000000,Random: 64

Очевидно, что наша внутренняя нумерация (колонка Count) идет по порядку, а номера проходов Pass ID могут быть перемешаны (это зависит от многих факторов параллельной обработки пакетов заданий агентами). В частности, пакет заданий может первым закончить тот агент, которому были присвоены задания с бОльшими порядковыми номерами: в таком случае нумерация в файле начнется со старших проходов.

В журнале тестера можно проверить служебную статистику по фреймам.

242 frames (42.78 Kb total, 181 bytes per frame) received
local 121 tasks (100%), remote 0 tasks (0%), cloud 0 tasks (0%)
121 new records saved to cache file 'tester\cache\FrameTransfer.EURUSD.H1. »
  » 20220101.20220201.20.9E2DE099D4744A064644F6BB39711DE8.opt'

Важно отметить, что при генетической оптимизации номера проходов представляются в отчете оптимизации как пара (номер поколения, номер экземпляра), в то время как номер прохода, получаемый в функции FrameNext — по-прежнему число ulong — фактически номер прохода в пакетных заданиях в контексте текущего запуска оптимизации. MQL5 не предоставляет средств для сопоставления нумерации проходов с "генетическим" отчетом. Для этой цели следует рассчитывать контрольные суммы входных параметров каждого прохода. Opt-файлы с кэшем оптимизации уже содержат такое поле с MD5-хэшем.