English Русский Español Deutsch 日本語 Português
preview

开发多币种 EA 交易(第 17 部分):为真实交易做进一步准备

MetaTrader 5测试者 | 15 五月 2025, 08:05
580 0
Yuriy Bykov
Yuriy Bykov

概述

在前面的一篇文章中,我们已经将注意力转向了处理真实账户所需的 EA 改进。到目前为止,我们的工作主要集中在在策略测试器中获得可接受的 EA 结果上。真实交易需要更多的准备。

除了在重启终端后恢复 EA 操作、使用略有不同的交易工具名称以及在达到指定指标时自动完成交易的能力外,我们还面临以下问题:为了形成初始化字符串,我们使用直接从数据库中获得的信息,该数据库存储了交易策略实例及其组的所有优化结果。

要运行 EA,我们必须在共享终端文件夹中有一个包含数据库的文件。数据库的大小已经达到几 GB,并且将来还会不断增长。因此,将数据库作为EA的一个组成部分是不合理的 — 只需要存储在那里的一小部分信息就可以启动。因此,有必要在 EA 中实现一种提取和使用此信息的机制。


绘制路径图 

让我们回顾一下,我们已经探讨并实现了两个阶段的测试自动化。第一阶段,优化交易策略单个实例的参数(第 11 部分)。所研究的模型交易策略仅使用一种交易工具(交易品种)和一个时段。因此,我们不断通过优化器运行它,改变交易品种和时段。对于每种交易品种和时段的组合,都会根据不同的优化标准依次进行优化。所有优化过程的结果都设置在我们数据库的“passes”表中。

在第二阶段,我们优化了第一阶段获得的一组参数集的选择,这些参数集一起使用时可产生最佳结果(第 6 部分第 13 部分)。与第一阶段一样,我们将使用相同交易品种-时段对的参数集归入一个组。我们的数据库中还保存了优化期间回顾的所有组的结果信息。

在第三阶段,我们不再使用标准策略测试优化器,因此我们还没有讨论它的自动化。第三阶段包括为每个可用的交易品种和时段组合选择第二阶段中找到的最佳组之一。我们对三个交易品种(EURGBP、EURUSD、GBPUSD)和三个时段(H1、M30、M15)进行了优化。这样,第三阶段的结果将是九个入选组。但为了简化和加速测试程序中的计算,我们在最后一篇文章中将自己限制为仅三个最佳组(具有三个不同的交易品种和 H1 时段)。

第三阶段的结果是来自 “passes” 表的一组行标识符,我们通过输入参数将其传递给最终的 SimpleVolumesExpert.mq5 EA:

input string     passes_ = "734469,"
                           "736121,"
                           "776928";    // - Comma-separated pass IDs

我们可以在启动 EA 测试之前更改此参数。因此,可以使用数据库中 “passes” 表中可用的组集合中的任何所需子组来运行最终 EA,或者更准确地说,使用长度不超过 247 个字符的子集。这是 MQL5 语言对输入字符串参数值的限制。根据文档,字符串参数值的最大长度可以是 191 到 253 个字符,具体取决于参数名称的长度。

因此,如果我们想把大约40多个组纳入到这项工作中,那么这种方式是行不通的。例如,我们可能必须通过从代码中删除单词 input,使 passes_ 变量成为一个简单的字符串型变量而不是输入字符串参数。在这种情况下,我们只能在源代码中指定所需的组集。然而,我们还不需要使用这么大的集合。此外,根据第 5 部分进行的实验,对我们来说,不从大量交易策略的单个副本或交易策略组中创建一个组获利更多。将初始数量的单个交易策略副本分成几个子组,从中可以组建数量较少的新组,这样更容易获利。这些新组可以组合成一个最终组,也可以通过划分为新的子组来重复分组过程。因此,在每一个统一层面上,我们都必须采取相对较少数量的策略或组作为单一组。

当 EA 可以访问包含所有优化过程结果的数据库时,只需通过输入传递所需优化通过的 ID 列表即可。EA 自行从数据库接收参与所列通过的交易策略组的初始化字符串。根据从数据库得到的初始化字符串,它将为 EA 对象构建一个初始化字符串,其中包含来自列出组的所有交易策略。该 EA 将使用其中包含的所有交易策略实例进行交易。

如果无法访问数据库,EA 仍然需要以某种方式为 EA 对象生成初始化字符串,其中包含交易策略的单个实例或交易策略组的所需组合。例如,我们可以将其保存到一个文件中,并将文件名传递给 EA,EA 将从中加载初始化字符串。或者,我们可以通过额外的 mqh 库文件将初始化字符串的内容插入到 EA 的源代码中。我们甚至可以通过将初始化字符串保存到文件,然后使用 MetaEditor 中的文件导入功能(编辑 → 插入 → 文件)将其导入,来组合这两种方法。

然而,如果我们想在一个 EA 中提供处理不同选定组的能力,并在输入参数中选择所需的组,那么这种方法很快就会显示出其可扩展性较弱。我们将需要做大量的手动、重复性工作。因此,让我们试着用不同的方式来表述这个问题:我们想形成一个良好的初始化字符串库,我们可以从中为当前的 EA 启动选择一个。库应该是 EA 的一个组成部分,这样我们就不必同时使用另一个单独的文件。

考虑到上述情况,即将开展的工作可分为以下阶段:

  • 选择和保存。在这个阶段,我们应该有一个工具,允许我们选择组并保存它们的初始化字符串以供以后使用。提供保存所选组的一些附加信息(名称、简要描述、大致组成、创建日期等)的功能可能是一个好主意。

  • 构建库。从上一阶段选择的组中,最终选择将在特定版本的 EA 库中使用的组,并形成一个包含所有必要信息的包含文件。

  • 创建最终的 EA 。通过修改上一部分的 EA,我们将使用创建的组库将其转换为新的最终 EA。此 EA 将不再需要访问我们的优化数据库,因为有关所使用的交易策略组的所有必要信息都将包含在其中。

让我们开始实现我们的计划吧。


回顾过去的成果

所提到的步骤是第 9 部分中描述的第 8 阶段实现的原型。让我们回顾一下,在那篇文章中,我们列出了一组阶段,完成这些阶段可以让我们获得一个具有良好交易性能的制作完成的 EA。第 8 阶段意味着我们将针对不同交易策略、交易品种、时段和其他参数找到的所有最佳组收集到一个最终的 EA 中。然而,我们还没有详细考虑“究竟应该如何选择最佳组?”这个问题。

一方面,这个问题的答案可能很简单。例如,我们可以根据一些参数(总利润、夏普比率、归一化平均年利润)从所有组中选择最佳结果。但另一方面,答案可能要复杂得多。例如,如果使用复杂的标准来选择最佳组,可以获得更好的测试结果,该怎么办?或者,如果一些最好的组根本不应该被纳入最终的 EA 中,因为它们的加入会使没有它们所取得的结果变得更糟?这个主题很可能需要自己进行详细的研究。

另一个需要单独研究的问题是如何将组最佳地划分为子组,并对子组进行归一化。在我们开始实现任何测试阶段的自动化之前,我在第 5 部分中已经谈到了这个问题。然后,我们手动选择了九个单独的交易策略实例,所使用的三个交易工具(交易品种)各有三个实例。

事实证明,如果你首先为每个交易品种制作三个标准化的三组,每组三种策略,然后将它们组合成一个最终的标准化组,那么与将九个单一的交易策略组合到一个最终标准化组相比,测试的结果会好一些。但我们不能确定这种分组方法是否是最佳的。与简单地将它们组合成一组相比,其他交易策略是否更可取?总的来说,这里也有进一步研究的空间。

幸运的是,我们可以把这两个问题推迟到以后。为了探索它们,我们需要尚未实现的辅助工具。没有它们,工作效率将大大降低,需要更多的时间。


选择并保存组

看起来我们已经拥有了我们需要的一切。只需从上一部分中获取 SimpleVolumesExpert.mq5 EA,在passes_ 输入参数中设置以逗号分隔的通过 ID,启动单个测试器通过,并将所需的初始化字符串保存到数据库中。看起来,唯一缺少的是一些额外的数据。但事实证明,有关通过的信息并没有进入数据库。

关键在于,我们只有优化过程的结果上传到数据库。单次通过的结果未上传。您可能还记得,上传是在 CTesterHandler::ProcessFrames() 方法中执行的,该方法从上层的 OnTesterPass() 处理程序调用:

//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Open the database
   DB::Connect();

// Variables for reading data from frames
   ...

// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);
      
      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                           s_idTask, pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Add it to the SQL query array
      APPEND(queries, query);
   }

// Execute all requests
   DB::ExecuteTransaction(queries);

// Close the database
   DB::Close();
}

当启动单次通过时,不会调用处理程序,因为单次通过事件模型不提供此功能。此处理程序仅在以数据帧收集模式运行的 EA 交易中调用。在此模式下,当优化开始时会自动启动 EA 的 EA 实例,但单次通过开始时不会启动。因此,事实证明,现有的实现不会将有关单次通过的信息保存到数据库中。

当然,我们可以保持一切不变,开发一个需要根据一些不必要的参数进行优化的 EA。这种优化的目标是获得第一次通过的结果,之后优化将停止。这样,通过的结果就会被输入到数据库中。但这似乎太丑陋了,所以我们将采取另一种方式。

在 EA 中运行单次通过时,OnTester() 处理程序将在完成时被调用。因此,我们必须将用于保存单次通过结果的代码直接插入到处理程序中或从处理程序调用的方法之一中。可能最适合插入该方法的位置是 CTesterHandler::Tester() 。然而,值得考虑的是,当 EA 完成优化过程时,也会调用此方法。该方法现在包含通过数据帧机制生成并发送优化结果的代码。

当启动单次通过时,仍会生成帧的数据,但数据帧本身即使创建了也不能使用。如果我们尝试使用 FrameNext() 函数来获取数据帧,则在以单次通过模式启动的 EA 中使用 FrameAdd() 函数创建数据帧后, FrameNext() 将不会读取创建的数据帧。它的行为就像没有创建任何数据帧一样。

因此,让我们执行以下操作。在 CTesterHandler::Tester() 处理函数中,我们将检查此通过是单独的还是作为优化的一部分执行的。根据结果,我们将立即将通过结果保存到数据库(用于单次通过),或者创建数据帧发送到主 EA(用于优化)。让我们添加一个名为“保存单个通过”的新方法和另一个生成 SQL 查询以将所需数据插入 passes 表的辅助方法。我们需要后者,因为现在这样的操作将在代码的两个地方执行,而不是在一个地方执行。因此,我们将其移至单独的方法。

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   
    ...

   static void       ProcessFrame(string values);  // Handle single pass data

   // Generate SQL query to insert pass results
   static string     GetInsertQuery(string values, string inputs, ulong pass = 0);
public:
   ...
};

我们已经有了 GetInsertQuery() 的实现。我们要做的就是将代码块从 ProcessFrames() 方法并在 ProcessFrames() 方法中的正确位置调用它

//+------------------------------------------------------------------+
//| Generate SQL query to insert pass results                        |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}


//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
   ...

// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);

      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = GetInsertQuery(values, inputs, pass);

      // Add it to the SQL query array
      APPEND(queries, query);
   }

   ...
}

为了保存单次通过的数据,我们将调用一个新方法 ProcessFrame() ,该方法接受一个字符串作为参数,它是 SQL 查询的一部分,包含有关该通过的数据,以便插入到 passes 表。在方法中,我们只需连接到数据库,生成最终的 SQL 查询并执行它:

//+------------------------------------------------------------------+
//| Handle single pass data                                          |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Open the database
   DB::Connect();

// Form an SQL query from the received data
   string query = GetInsertQuery(values, "", 0);

// Execute the request
   DB::Execute(query);

// Close the database
   DB::Close();
}

考虑到添加的方法,通过完成事件处理函数可以进行如下修改:

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {

    ... 

// Generate a string with pass data
   data = StringFormat("%s,'%s'", data, params);

// If this is a pass within the optimization,
   if(MQLInfoInteger(MQL_OPTIMIZATION)) {
      // Open a file to write a frame data
      int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

      // Write a description of the EA parameters
      FileWriteString(f, data);

      // Close the file
      FileClose(f);

      // Create a frame with data from the recorded file and send it to the main terminal
      if(!FrameAdd("", 0, 0, s_fileName)) {
         PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
      }
   } else {
      // Otherwise, it is a single pass, call the method to add its results to the database
      CTesterHandler::ProcessFrame(data);
   }
}

将所做的更改保存到当前文件夹中的 TesterHandler.mqh 文件。

现在,每次通过后,有关其结果的信息都会输入到我们的数据库中。就当前任务而言,我们对通过的各种统计参数不太感兴趣。对我们来说最重要的是保存在通过中使用的规范化策略组的初始化字符串。保存的字符串是我们在这里最需要的。

但是,在 passes 表列中存在所需的初始化字符串不足以保证其进一步的方便使用。我们还想在初始化字符串中附加一些信息。然而,扩展 passes 表列的集合并不值得,因为该表中的绝大多数行将存储有关优化通过结果的信息,而这些信息不需要额外的信息。 

因此,让我们创建一个新表,用于存储选定的结果。这已经属于库的构建阶段。


构建库

我们不要在新表中放入过多的冗余字段,这些字段可能包含从其他数据库表中获取的信息。例如,如果新表中的条目通过外部键与 passes 表中的条目( passes )有关系,那么就已经有一个创建日期。此外,使用通过 ID,我们可以建立一个连接链并确定此通过属于哪个项目,从而确定通过中使用的策略组。

考虑到这一点,让我们创建具有以下字段集的 strategy_groups 表:

  • id_passpasses 表中的通过 ID(外键)
  • name。将用于生成策略组选择输入枚举的策略组的名称。

创建所需表的 SQL 代码如下:

-- Table: strategy_groups
DROP TABLE IF EXISTS strategy_groups;

CREATE TABLE strategy_groups (
    id_pass INTEGER REFERENCES passes (id_pass) ON DELETE CASCADE
                                                ON UPDATE CASCADE
                    PRIMARY KEY,
    name    TEXT
);

让我们创建 CGroupsLibrary 辅助类来执行大部分的进一步操作。它的任务包括从数据库中插入和检索有关策略组的信息,并与最终 EA 将使用的良好组的实际库一起形成一个 mqh 文件。我们稍后会再讨论这个问题。现在,让我们制作一个用于构建库的 EA。

现有的 SimpleVolumesExpert.mq5 EA 几乎可以完成所有需要的功能,但仍需要一些改进。我们计划将其用作最终 EA 的最终版本。因此,让我们用新名称 SimpleVolumesStage3.mq5 保存它。现在,我们应该对新文件进行必要的添加。我们缺少两件事:指定当前选定通过所形成的组的名称的功能(在 passes_ 参数中)以及将该组的初始化字符串保存到新的 strategy_groups 表中。

前者实现起来相当简单。让我们添加一个新的 EA 输入参数,以便稍后用作组名。如果参数为空,则不会保存到库。

input group "::: Saving to library"
input string groupName_  = "";         // - Group name (if empty - no saving)

但如果是后者,我们就需要更多努力。要将数据插入到 strategy_groups 表中,我们需要知道插入到 passes 表中时分配给当前通过记录的 ID。由于它的值是由数据库本身自动分配的(在查询中,我们只传递NULL而不是它的值),因此它在代码中不作为任何变量的值存在。因此,我们目前无法在其他需要它的地方使用它。我们需要以某种方式定义这个值。

这可以通过不同的方式来实现。例如,知道分配给新行的标识符形成递增序列,您可以在插入后简单地选择当前最大 ID 的值。如果我们确信当前没有新的字符串传递到 passes 表,就可以做到这一点。但是,如果另一个第一或第二阶段的优化目前正在并行进行,其结果可能会最终出现在同一个数据库中。在这种情况下,我们无法再确定最后一个 ID 就是与我们启动以生成库的通过相对应的 ID。一般来说,只有当我们准备忍受一些限制并记住它们时,才能做到这一点。

以下是一种更可靠的方法,没有上述可能的错误。我们可以稍微修改一下插入数据的 SQL 查询,将其变成一个返回新表行生成的 ID 作为结果的查询。为此,只需将“RETURNING rowid”运算符添加到 SQL 查询的末尾。让我们在 GetInsertQuery() 方法中执行此操作,该方法生成一个 SQL 查询以将新行插入到 passes 表中。尽管 passes 表中的 ID 列名为 id_pass ,我们也可以将其命名为 rowid ,因为它具有适当的类型(INTEGER PRIMARY KEY AUTOINCREMENT)并替换 SQLite 表中自动存在的隐藏 rowid 列。

//+------------------------------------------------------------------+
//| Generate SQL query to insert pass results                        |
//+------------------------------------------------------------------+
string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s') RETURNING rowid;",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}

我们还需要修改发送此请求的 MQL5 代码。目前,我们对此使用 DB::Execute(query) 方法。这意味着传递给它的 query 不是返回任何数据的查询。

因此, CDatabase 类接收新方法 Insert() ,该方法将执行传递的插入查询并返回单个读取结果值。在内部,我们将使用 DatabasePrepare() 函数而不是 DatabaseExecute() 函数,该函数允许我们访问查询结果:

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   ...
public:
   ...
   // Execute a query to the database for insertion with return of the new entry ID
   static ulong      Insert(string query);
};

...

//+------------------------------------------------------------------+
//| Execute a query to the database for insertion returning the      |
//| new entry ID                                                     |
//+------------------------------------------------------------------+
ulong CDatabase::Insert(string query) {
   ulong res = 0;
   
// Execute the request
   int request = DatabasePrepare(s_db, query);

// If there is no error
   if(request != INVALID_HANDLE) {
      // Data structure for reading a single string of a query result 
      struct Row {
         int         rowid;
      } row;

      // Read data from the first result string
      if(DatabaseReadBind(request, row)) {
         res = row.rowid;
      } else {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d",
                     query, GetLastError());
      }
   } else {
      // Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d",
                  query, GetLastError());
   }
   return res;
}
//+------------------------------------------------------------------+

我决定不通过额外的检查来使此方法复杂化,即提交的查询确实是一个 INSERT 查询,它包含一个返回 ID 的命令,并且返回的值不是复合的。偏离这些条件将导致执行此代码时出错,但由于此方法将仅在项目中的一个位置使用,我们将尝试向其传递正确的请求。

将更改保存到当前文件夹的 Database.mqh 文件中。

在实现过程中出现的下一个问题是如何将 ID 值传递给更高级别的代码,因为在接收点处理它会导致需要为现有方法赋予外部功能和额外的传递参数。因此,我们决定采用以下方式: CTesterHandler 类接收 s_idPass 静态属性。当前通过的 ID 被写入其中。从这里,我们可以在程序的任意一点获取这个值:

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   ...
public:
   ...
   static ulong      s_idPass;
};

...
ulong CTesterHandler::s_idPass = 0;

...

//+------------------------------------------------------------------+
//| Handle single pass data                                          |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrame(string values) {
// Open the database
   DB::Connect();

// Form an SQL query from the received data
   string query = GetInsertQuery(values, "", 0);

// Execute the request
   s_idPass = DB::Insert(query);

// Close the database
   DB::Close();
}

将所做的更改保存到当前文件夹中的 TesterHandler.mqh 文件。

现在是时候返回到声明的 CGroupsLibrary 辅助类了。我们最终需要在其中声明两个公共方法、一个私有方法和一个静态数组:

//+------------------------------------------------------------------+
//| Class for working with a library of selected strategy groups     |
//+------------------------------------------------------------------+
class CGroupsLibrary {
private:
   // Exporting group names and initialization strings extracted from the database as MQL5 code
   static void       ExportParams(string &p_names[], string &p_params[]);

public:
   // Add the pass name and ID to the database
   static void       Add(ulong p_idPass, string p_name);

   // Export passes to mqh file
   static void       Export(string p_idPasses);

   // Array to fill with initialization strings from mqh file
   static string     s_params[];
};

在库生成 EA 中,只有 Add() 将会被使用。它将接收通过 ID 和组名,并保存到库中。方法代码本身非常简单:生成一个 SQL 查询,取出输入数据,用于将新条目插入到 strategy_groups 表中并执行。

//+------------------------------------------------------------------+
//| Add the pass name and ID to the database                         |
//+------------------------------------------------------------------+
void CGroupsLibrary::Add(ulong p_idPass, string p_name) {
   string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, '%s')",
                               p_idPass, p_name);

// Open the database
   if(DB::Connect()) {
      // Execute the request
      DB::Execute(query);

      // Close the database
      DB::Close();
   }
}

现在,为了完成库生成工具的开发,我们只需要在测试程序完成之后,SimpleVolumesStage3.mq5 EA 添加对 Add() 方法的调用:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   // Handle the completion of the pass in the EA object
   double res = expert.Tester();

   // If the group name is not empty, save the pass to the library
   if(groupName_ != "") {
      CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_);
   }
   return res;
}

让我们将对当前文件夹中的 SimpleVolumesStage3.mq5GroupsLibrary.mqh 文件所做的更改保存起来。如果我们为其余的 CGroupsLibrary 类方法添加替换方法,那么我们就可以使用已编译的 SimpleVolumesStage3.mq5 EA。 


填充库

让我们尝试从之前选择的九个好的通过 ID 中生成一个库。为此,在测试器中启动 SimpleVolumesStage3.ex5 EA,指定从 passes_ 输入参数中的九个 ID 中选择的各种组合。在 groupName_ 输入参数中,我们将设置一个明确的名称,该名称反映当前交易策略单个实例组合成一个组的组成情况。

经过几次运行后,让我们看看 strategy_groups 表中出现的结果,并为不同组进行的通过添加一些参数以供参考。例如,以下 SQL 查询将帮助我们实现这一点:

SELECT sg.id_pass,
       sg.name,
       p.custom_ontester,
       p.sharpe_ratio,
       p.profit,
       p.profit_factor,
       p.equity_dd_relative
  FROM strategy_groups sg
       JOIN
       passes p ON sg.id_pass = p.id_pass;

查询结果如下表:

图 1.组库的组成 

name 列中,我们可以看到组的名称,它反映了交易工具(交易品种)、时段以及该组中使用的交易策略实例的数量。例如,“EUR-GBP-USD”的存在意味着该组包括适用于三个交易品种的交易策略实例:EURGBP、EURUSD 和 GBPUSD。如果组名以“Only EURGBP”开头,则它仅包含针对 EURGBP 交易品种的策略实例。所用的时段以类似的方式表示。交易策略实例的数量在名称末尾指定。例如,“3x16 items”表示该组结合了三个标准化组,每组 16 个策略。

custom_ontester 列显示每个组的标准化年平均利润。需要注意的是,此参数的值范围超过了预期值,因此将来有必要了解这种现象的原因。例如,仅使用 GBPUSD 的组的结果明显高于具有多个交易品种的组的结果。最好的结果最后保存在第 20 行。在此组中,我们包含了可为每个交易品种和一个或多个时段产生最佳结果的子组。


导出库

下一步是将组库从数据库转移到可以连接到最终 EA 的 mqh 文件。为此,让我们实现 CGroupsLibrary 类中负责导出的方法,以及一个辅助 EA,用于运行这些方法。

Export() 方法中,我们将从数据库中获取库组的名称及其初始化字符串,并将其添加到相应的数组中。生成的数组将传递给下一个方法 ExportParams()

//+------------------------------------------------------------------+
//| Exporting passes to mqh file                                     |
//+------------------------------------------------------------------+
void CGroupsLibrary::Export(string p_idPasses) {
// Array of group names
   string names[];

// Array of group initialization strings
   string params[];

// If the connection to the main database is established,
   if(DB::Connect()) {
      // Form a request to receive passes with the specified IDs
      string query = "SELECT sg.id_pass,"
                     "       sg.name,"
                     "       p.params"
                     "  FROM strategy_groups sg"
                     "       JOIN"
                     "       passes p ON sg.id_pass = p.id_pass";

      query = StringFormat("%s "
                           "WHERE p.id_pass IN (%s);",
                           query, p_idPasses);

      // Prepare and execute the request
      int request = DatabasePrepare(DB::Id(), query);

      // If the request is successful
      if(request != INVALID_HANDLE) {
         // Structure for reading results
         struct Row {
            ulong          idPass;
            string         name;
            string         params;
         } row;

         // For all query results, add the name and initialization string to the arrays
         while(DatabaseReadBind(request, row)) {
            APPEND(names, row.name);
            APPEND(params, row.params);
         }
      }

      DB::Close();

      // Export to mqh file
      ExportParams(names, params);
   }
}

ExportParams() 方法中,使用 MQL5 代码形成一个字符串,它将创建一个具有给定名称 ENUM_GROUPS_LIBRARY 的枚举(enum)并用元素填充它。每个元素都会有一个包含组名的注释。接下来,代码将声明一个静态字符串数组 CGroupsLibrary::s_params[] ,其中将填充来自库的组的初始化字符串。每个初始化字符串都将被预处理:所有换行符将被替换为空格,并且在双引号前添加反斜杠。为了将初始化字符串放在生成的代码中的双引号内,这是必需的。

一旦代码在 data 变量中完全生成,我们就会创建名为 ExportedGroupsLibrary.mqh 的文件并将接收到的代码保存在其中。

//+------------------------------------------------------------------+
//| Export group names extracted from the database and               |
//| initialization strings in the form of MQL5 code                  |
//+------------------------------------------------------------------+
void CGroupsLibrary::ExportParams(string &p_names[], string &p_params[]) {
   // ENUM_GROUPS_LIBRARY enumeration header
   string data = "enum ENUM_GROUPS_LIBRARY {\n";

   // Fill the enumeration with group names
   FOREACH(p_names, { data += StringFormat("   GL_PARAMS_%d, // %s\n", i, p_names[i]); });

   // Close the enumeration
   data += "};\n\n";

   // Group initialization string array header and its opening bracket
   data += "string CGroupsLibrary::s_params[] = {";

   // Fill the array by replacing invalid characters in the initialization strings
   string param;
   FOREACH(p_names, {
      param = p_params[i];
      StringReplace(param, "\r", "");
      StringReplace(param, "\n", " ");
      StringReplace(param, "\"", "\\\"");
      data += StringFormat("\"%s\",\n", param);
   });

   // Close the array
   data += "};\n";

// Open the file to write data
   int f = FileOpen("ExportedGroupsLibrary.mqh", FILE_WRITE | FILE_TXT | FILE_ANSI);

// Write the generated code
   FileWriteString(f, data);

// Close the file
   FileClose(f);
}

接下来是非常重要的部分:

// Connecting the exported mqh file.
// It will initialize the CGroupsLibrary::s_params[] static variable
// and ENUM_GROUPS_LIBRARY enumeration
#include "ExportedGroupsLibrary.mqh"

我们将导出后将收到的文件直接包含在 GroupsLibrary.mqh 文件中。在这种情况下,最终的 EA 只需要包含此文件即可使用导出的库。这种方法带来了一点不便:为了能够编译处理库导出的 EA,导出后才会出现的 ExportedGroupsLibrary.mqh 文件应该已经存在。然而,只有这个文件的存在才是重要的,而不是它的内容。因此,我们只需在当前文件夹中创建一个具有该名称的空文件,编译就会顺利进行。

要运行 EA 方法,我们需要一个脚本或 EA,在其中实现这一点。它可能看起来像这样:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Exporting from library"
input string     passes_ = "802150,802151,802152,802153,802154,"
                           "802155,802156,802157,802158,802159,"
                           "802160,802161,802162,802164,802165,"
                           "802166,802167,802168,802169,802173";    // - Comma-separated IDs of the saved passes


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Call the group library export method
   CGroupsLibrary::Export(passes_);

// Successful initialization
   return(INIT_SUCCEEDED);
}

void OnTick() {
   ExpertRemove();
}

通过改变 passes_ 参数,我们可以选择将组从库导出到数据库的组成和顺序。在图表上运行 EA 一次后,ExportedGroupsLibrary.mqh 文件将出现在终端数据文件夹中。它应该被转移到包含项目代码的当前文件夹。


创建最终的 EA

我们终于进入了最后阶段。剩下的就是对 SimpleVolumesExpert.mq5 EA 进行一些小的修改。首先,我们需要包含 GroupsLibrary.mqh 文件:

#include "GroupsLibrary.mqh"

接下来,用一个新的输入参数替换 passes_ 输入参数,以便我们可以从库中选择一个组:

input group "::: Selection for the group"
input ENUM_GROUPS_LIBRARY       groupId_     = -1;    // - Group from the library

OnInit() 函数中,我们不再像以前一样通过 ID 从数据库中获取初始化字符串,而是直接从 CGroupsLibrary::s_params[] 数组中获取初始化字符串,其索引对应于 groupId_ 的选定值输入参数:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   ...

// Initialization string with strategy parameter sets
   string strategiesParams = NULL;

// If the selected strategy group index from the library is valid, then
   if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) {
      // Take the initialization string from the library for the selected group
      strategiesParams = CGroupsLibrary::s_params[groupId_];
   }

// If the strategy group from the library is not specified, then we interrupt the operation
   if(strategiesParams == NULL) {
      return INIT_FAILED;
   }

   ...

// Successful initialization
   return(INIT_SUCCEEDED);
}

将所做的更改保存到当前文件夹中的 SimpleVolumesExpert.mq5 文件。

由于我们已向 ENUM_GROUPS_LIBRARY 枚举元素添加了带有名称的注释,因此在选择 EA 参数的对话框中,我们将能够看到可理解的名称,而不仅仅是数字序列:


图 2.根据 EA 参数中的名称从库中选择一个组

让我们使用列表中的最后一组来运行 EA 并查看结果:

图 3.使用库中最具吸引力的组测试最终 EA 的结果

很明显,平均年度归一化利润指标的结果与数据库中存储的结果接近。微小的差异主要是由于最终的 EA 使用了标准化组(这可以通过查看最大相对回撤的值来验证,该值约为所用存款的 10%)。在 SimpleVolumesStage3.ex5 EA 中为该组生成初始化字符串时,该组在通过中尚未归一化,因此那里的回撤幅度约为 5.4%。 


结论

我们已经得到了最终的 EA,它可以独立于优化过程中填充的数据库运行。也许,我们会再次回到这个问题上,因为实践可以做出自己的调整,而本文提出的方法可能不如其他方法方便。但无论如何,实现既定目标是向前迈出的一步。

在编写本文的代码时,发现了需要进一步调查的新情况。例如,事实证明,测试此 EA 的结果不仅对报价服务器敏感,而且对策略测试器设置中选择为主要交易品种的交易品种也敏感。我们可能需要对第一阶段和第二阶段的优化自动化做一些调整。下次再详细讨论。

最后,我想提出一个以前隐含存在的警告。我在前面的部分中从未说过,遵循拟议的方向将使你获得有保证的利润。相反,我们在某些时候收到了令人失望的测试结果。此外,尽管为准备 EA 进行真实交易付出了努力,但我们不太可能在某个时候说,我们已经尽了一切可能和不可能的事情来确保 EA 在真实账户上的正确操作。这是一个可以而且应该努力的完美结果,但实现它似乎总是一个模糊的未来问题。然而,这并不妨碍我们接近它。

本文和本系列之前的所有文章中的所有结果仅基于历史测试数据,并不保证未来会有任何利润。该项目中的工作具有研究性质。所有已发表的结果都可以由任何人使用,风险自负。

感谢您的关注!期待很快与您见面!


存档内容

#
 名称
版本  描述   最近修改
 MQL5/专家/文章.15360
1 Advisor.mqh 1.04 EA 基类 第 10 部分
2 Database.mqh 1.04 处理数据库的类 第 17 部分
3 ExpertHistory.mqh 1.00 用于将交易历史记录导出到文件的类 第 16 部分
4 ExportedGroupsLibrary.mqh
生成的文件列出了策略组名称及其初始化字符串数组 第 17 部分
5 Factorable.mqh 1.01 从字符串创建的对象的基类 第 10 部分
6 GroupsLibrary.mqh 1.00 用于处理选定策略组库的类 第 17 部分
7 HistoryReceiverExpert.mq5 1.00 用于与风险管理器重现交易历史的 EA 第 16 部分  
8 HistoryStrategy.mqh  1.00 用于重现交易历史的交易策略类  第 16 部分
9 Interface.mqh 1.00 可视化各种对象的基类 第 4 部分
10 LibraryExport.mq5 1.00 EA 将库中选定通过的初始化字符串保存到 ExportedGroupsLibrary.mqh 文件 第 17 部分
11 Macros.mqh 1.02 用于数组操作的有用的宏 第 16 部分  
12 Money.mqh 1.01  资金管理基类 第 12 部分
13 NewBarEvent.mqh 1.00  用于定义特定交易品种的新柱形的类  第 8 部分
14 Receiver.mqh 1.04  将未平仓交易量转换为市场仓位的基类  第 12 部分
15 SimpleHistoryReceiverExpert.mq5 1.00 简化的EA,用于回放交易历史   第 16 部分
16 SimpleVolumesExpert.mq5 1.20 用于多组模型策略并行运行的 EA。参数将从内置组库中获取。 第 17 部分
17 SimpleVolumesStage3.mq5 1.00 将生成的标准化策略组保存到具有给定名称的组库中的 EA。 第 17 部分
18 SimpleVolumesStrategy.mqh 1.09  使用分时交易量的交易策略类 第 15 部分
19 Strategy.mqh 1.04  交易策略基类 第 10 部分
20 TesterHandler.mqh  1.03 优化事件处理类  第 17 部分 
21 VirtualAdvisor.mqh  1.06  处理虚拟仓位(订单)的 EA 类 第 15 部分
22 VirtualChartOrder.mqh  1.00  图形虚拟仓位类 第 4 部分  
23 VirtualFactory.mqh 1.04  对象工厂类  第 16 部分
24 VirtualHistoryAdvisor.mqh 1.00  交易历史回放 EA 类  第 16 部分
25 VirtualInterface.mqh  1.00  EA GUI 类  第 4 部分  
26 VirtualOrder.mqh 1.04  虚拟订单和仓位类  第 8 部分
27 VirtualReceiver.mqh 1.03  将未平仓交易量转换为市场仓位的类(接收方)  第 12 部分
28 VirtualRiskManager.mqh  1.02  风险管理类(风险管理器)  第 15 部分
29 VirtualStrategy.mqh 1.05  具有虚拟仓位的交易策略类  第 15 部分
30 VirtualStrategyGroup.mqh  1.00  交易策略组类 第 11 部分 
31 VirtualSymbolReceiver.mqh  1.00 交易品种接收器类  第 3 部分



本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15360

附加的文件 |
MQL5.zip (72.95 KB)
使用MQL5和Python构建自优化的EA(第四部分):模型堆叠 使用MQL5和Python构建自优化的EA(第四部分):模型堆叠
今天,我们将展示如何构建能够从自身错误中学习的AI驱动的交易应用程序。我们将展示一种称为堆叠(stacking)的技术,我们使用2个模型来做出1个预测。第一个模型通常是较弱的学习器,而第二个模型通常是更强大的模型,它学习较弱学习器的残差。我们的目标是创建一个模型集成,以期获得更高的准确性。
在任何市场中获得优势(第四部分):CBOE欧元和黄金波动率指数 在任何市场中获得优势(第四部分):CBOE欧元和黄金波动率指数
我们将分析芝加哥期权交易所(CBOE)整理的替代数据,以提高我们的深度神经网络在预测XAUEUR货币对时的准确性。
交易中的神经网络:统一轨迹生成模型(UniTraj) 交易中的神经网络:统一轨迹生成模型(UniTraj)
理解个体在众多不同领域的行为很重要,但大多数方法只专注其中一项任务(理解、噪声消除、或预测),这会降低它们在现实中的有效性。在本文中,我们将领略一个可以适配解决各种问题的模型。
您应当知道的 MQL5 向导技术(第 34 部分):采用非常规 RBM 进行价格嵌入 您应当知道的 MQL5 向导技术(第 34 部分):采用非常规 RBM 进行价格嵌入
受限玻尔兹曼(Boltzmann)机是一种神经网络形式,开发于 1980 年代中叶,当时的计算资源非常昂贵。在其初创时,它依赖于 Gibbs 采样,以及对比散度来降低维度,或捕获输入训练数据集上的隐藏概率/属性。我们验证当 RBM 为预测多层感知器“嵌入”价格时,反向传播如何执行类似的操作。