在终端获取数据帧

通过 FrameAdd 函数从测试代理发送的帧被传送到终端,并按照接收顺序写入 mqd 文件,该文件以 EA 交易的名称命名,位于文件夹 terminal_directory/MQL5/Files/Tester 中。一个或多个帧同时到达会生成 OnTesterPass 事件。

MQL5 API 提供了下列 4 个用于分析和读取帧的函数:FrameFirstFrameFilterFrameNextFrameInputs。所有函数都会返回一个布尔值,指示成功 (true) 或错误 (false)。

为了访问现有的帧,内核采用了指向当前帧的内部指针这一隐喻机制。当 FrameNext 函数读取下一帧时,指针会自动向前移动,但可以使用 FrameFirstFrameFilter 返回到所有帧的开头。因此,MQL 程序可以在一个循环中组织帧的迭代,直到检查完所有的帧。如果需要,可以重复这个过程,例如,通过在 OnTesterDeinit 中应用不同的筛选器。

bool FrameFirst()

FrameFirst 函数可将内部帧读取指针设置为开始,并重置筛选器(如果之前使用 FrameFilter 函数设置了筛选器)。

理论上,对于所有帧的单次接收和处理,没有必要调用 FrameFirst,因为当优化开始时,指针已经在开头位置。

bool FrameFilter(const string name, ulong id)

该函数可设置帧读取筛选器并将内部帧指针设置为开头位置。筛选器将影响 FrameNext 的后续调用中包含哪些帧。

如果一个空字符串作为第一个参数传递,筛选器将只通过一个数字参数工作,即所有具有指定 id 的帧。如果第二个参数的值等于 ULONG_MAX,那么只有文本筛选器起作用。

调用 FrameFilter("", ULONG_MAX) 相当于调用 FrameFirst(),相当于没有筛选器。

如果你在 OnTesterPass 中调用l FrameFirstFrameFilter ,请确保确实是你所需要的:其代码可能包含逻辑错误,因为它可能会循环、读取相同帧或以指数方式增加计算负载。

bool FrameNext(ulong &pass, string &name, ulong &id, double &value)

bool FrameNext(ulong &pass, string &name, ulong &id, double &value, void &data[])

FrameNext 函数可读取一帧并将指针移动到下一帧。pass 参数中可记录优化测试轮次编号。nameidvalue 参数可接收在 FrameAdd 函数的相应参数中传递的值。

值得注意的是,当没有更多帧需要读取时,该函数在正常运行时可能会返回 false。在这种情况下,内置变量 _LastError 包含值 4000(没有内置符号)。

无论使用哪种形式的 FrameAdd 函数来发送数据,文件或数组的内容都会放在接收data 数组中。接收数组的类型必须与发送数组的类型相匹配,在发送文件的情况下有一些细微差别。

二进制文件 (FILE_BIN) 最好在字节数组 uchar 中接受,以确保与任何大小兼容(因为其他更大的类型可能不是文件大小的倍数)。如果文件大小(实际上为接收帧中数据块的大小)不是接收数组类型大小的倍数,FrameNext 函数将不会读取数据,并将返回 INVALID_ARRAY (4006) 错误。

Unicode 文本文件(没有 FILE_ANSI 修饰符的 FILE_TXT 或 FILE_CSV)应被 ushort 类型的数组接受,然后通过调用 ShortArrayToString 转换为字符串。ANSI 文本文件应以 uchar 数组的形式接收,并使用 CharArrayToString 进行转换。

bool FrameInputs(ulong pass, string &parameters[], uint &count)

FrameInputs 函数允许你获取 EA 交易 input 参数的说明和值,形成具有指定传递编号的测试轮次。parameters 字符串数组用类似 "ParameterNameN=ValueParameterN" 的行填充。count 参数用 parameters 数组中的元素数填充。

这四个函数只允许在 OnTesterPass OnTesterDeinit 处理程序内部进行调用。

帧可以成批到达终端,在这种情况下,其需要时间传送。因此,没有必要让它们都有时间生成 OnTesterPass 事件,并一直处理到优化结束。在这方面,为了保证接收到所有迟到的帧,有必要在 OnTesterDeinit 中放置一个代码块对其进行处理(使用 FrameNext 函数)。

考虑一个简单的示例 FrameTransfer.mq5

EA 交易有四个测试参数。除了最后一个字符串,所有这些参数都可以包含在优化中。

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

但是,为了简化该示例,参数 Parameter1Parameter2 的步骤数被限制为 10 步(对于每个参数)。因此,如果不使用 Parameter0,最大传递次数为 121 次。 Parameter3 是一个不能包含在优化中的参数示例。

EA 交易不进行交易,而是生成模拟任意应用数据的随机数据。切勿在你的工作项目中像这样使用随机化:其仅适用于演示。

ulong startup// track the time of one run (just like demo data)
   
int OnInit()
{
   startup = GetMicrosecondCount();
   MathSrand((int)startup);
   return INIT_SUCCEEDED;
}

数据以两种类型的帧发送:文件和数组。每种类型都有自己的标识符。

#define MY_FILE_ID 100
#define MY_TIME_ID 101
   
double OnTester()
{
 // send file in one frame
   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);
   
 // send array in another frame
   ulong dummy[1];
   dummy[0] = GetMicrosecondCount() - startup;
   FrameAdd("timing"MY_TIME_ID0dummy);
   
   return (Parameter2 + 1) * (Parameter1 + 2);
}

该文件以二进制格式编写,包含简单的字符串。OnTester 的结果(准则)是一个简单的算术表达式,包含 Parameter1Parameter2

在接收端,在终端图表上以服务模式运行的 EA 交易实例中,我们可从所有带文件的帧中收集数据,并将它们放入一个公共 CSV 文件中。该文件可在 OnTesterInit 处理程序中打开。

int handle// file for collecting applied results
void OnTesterInit()
{
   handle = FileOpen("output.csv"FILE_WRITE | FILE_CSV | FILE_ANSI",");
}

如前所述,不一定所有帧都有时间进入 OnTesterPass 处理程序,需要在 OnTesterDeinit 中进行额外检查。因此,我们实现了一个辅助函数 ProcessFileFrames,我们将通过 OnTesterPassOnTesterDeinit调用该函数。

ProcessFileFrames 中,我们保存了已处理帧的内部计数器 framecount。以此为例,我们将确保帧的到达顺序和测试轮次编号通常不会匹配。

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

为了在函数中接收帧,说明了根据原型 FrameNext 所需的变量。此处,接收数据数组描述为 uchar。如果我们要写一些结构体到我们的二进制文件中,我们可以把它们直接放入一个相同类型的结构体数组中。

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

下面介绍了几个变量,用于获得帧所属的当前测试轮次的 EA 交易输入。

   string  params[];
   uint    count;
   ...

然后我们用 FrameNext 循环读取帧。请注意,几个帧可以同时进入该处理程序,所以需要一个循环。对于每个帧,我们向终端日志输出轮次编号、帧名和生成的 double 值。我们可跳过 ID 不同于 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 时,我们可在 header 变量中形成 CSV 文件的标题。在所有帧中,CSV 文件的当前(新)记录是在 record 变量中形成的。

void ProcessFileFrames()
{
   ...
      if(FrameInputs(passparamscount))
      {
         string headerrecord;
         if(framecount == 0// prepare CSV header
         {
            header = "Counter,Pass ID,";
         }
         record = (string)framecount + "," + (string)pass + ",";
         // collect optimized parameters and their values
         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// prepare CSV header
                     {
                        header += name2value[0] + ",";
                     }
                     record += name2value[1] + ","// data field
                  }
               }
            }
         }
         if(framecount == 0// prepare CSV header
         {
            FileWriteString(handleheader + "Value,File Content\n");
         }
         // write data to CSV
         FileWriteString(handlerecord + DoubleToString(value) + ","
            + CharArrayToString(data) + "\n");
      }
      framecount++;
   ...
}

调用 ParameterGetRange 也可以更有效地完成,只是 framecount 的值为零。你可以尝试这样做。

OnTesterPass 处理程序中,我们只调用 ProcessFileFrames

void OnTesterPass()
{
   ProcessFileFrames(); // standard processing of frames on the go
}

此外,我们从 OnTesterDeinit 调用相同的函数并关闭 CSV 文件。

void OnTesterDeinit()
{
   ProcessFileFrames(); // pick up late frames
   FileClose(handle);   // close the CSV file
   ..
}

OnTesterDeinit 中,我们用 MY_TIME_ID 处理帧。测试轮次的持续时间在这些帧中提供,并且一个轮次的平均持续时间在此处计算。理论上,只在程序中进行分析时这样做是有意义的,因为对于用户而言,测试程序已经在日志中显示了测试轮次的持续时间。

void OnTesterDeinit()
{
   ...
   ulong   pass;
   string  name;
   long    id;
   double  value;
   ulong   data[]; // same array type as sent
   
   FrameFilter("timing"MY_TIME_ID); // rewind to the first frame
   
   ulong count = 0;
   ulong total = 0;
   // cycle through 'timing' frames only
   while(FrameNext(passnameidvaluedata))
   {
      if(ArraySize(data) == 1)
      {
         total += data[0];
      }
      else
      {
         total += (ulong)value;
      }
      ++count;
   }
   if(count > 0)
   {
      PrintFormat("Average timing: %lld"total / count);
   }
}

EA 交易准备就绪。我们为其启用完全优化(因为选项的总数是人为限制的,对于遗传算法而言太小了)。我们只能选择开盘价,因为 EA 交易并不执行交易。因此,你应选择一个自定义准则(所有其他准则将给出 0)。例如,我们以单步长从 1 到 10 设置范围 Parameter1,以 0.1 步长从 -0.5 到 +0.5 设置 Parameter2

我们运行优化。在终端的专家日志中,我们将看到关于接收帧的条目,格式如下:

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'

需要注意的是,在遗传优化过程中,运行编号在优化报告中是成对 (generation number, copy number) 出现的,而在 FrameNext 函数中获得的传递编号为 ulong。事实上,其为当前优化运行环境中批次作业的轮次编号。MQL5 没有提供将轮次编号与遗传报告相匹配的方法。为此,应计算每个轮次输入参数的校验和。具有优化缓存的 Opt 文件已经包含这样一个具有 MD5 散列的字段。