English Русский Español Deutsch 日本語 Português
preview
开发多币种 EA 交易 (第 13 部分):自动化第二阶段 — 分组选择

开发多币种 EA 交易 (第 13 部分):自动化第二阶段 — 分组选择

MetaTrader 5测试者 | 20 一月 2025, 10:01
273 0
Yuriy Bykov
Yuriy Bykov

概述

上一篇文章中风险管理稍微分散了我们的注意力,现在让我们回到正题 测试自动化。在之前的一篇文章中,我们概述了优化和搜索最终 EA 的最佳参数时应完成的几个阶段。我们已经实现了第一阶段,其中我们优化了单个交易策略实例的参数,其结果保存在数据库中。

下一阶段是选择一组优秀的单个交易策略实例,当它们协同工作时,将改善交易参数 减少回撤、增加余额曲线增长的线性等等。我们已经在本系列文章的第六部分中研究了如何手动执行此阶段。首先,我们从优化单个交易策略实例参数的结果中选择了值得关注的。这可以使用各种标准来完成,但当时我们只限于简单地删除负利润的结果。然后,我们使用不同的方法,尝试将八个交易策略实例的不同组合,将它们组合在一个 EA 中,并在测试器中运行它们,以评估它们共同工作的参数。

从手动选择开始,我们还实现了从存储在 CSV 文件中的参数列表中选择的单一交易策略实例的输入参数组合的自动选择。事实证明,即使在最简单的情况下,当我们简单地运行选择八种组合的遗传优化时,也能实现预期的结果。

现在,让我们修改执行组选择优化的 EA,以便它可以使用数据库中第一阶段的结果。它还应将结果保存在数据库中。我们还将考虑通过向数据库添加必要的条目来创建进行第二阶段优化的任务。


向测试代理传输数据

对于之前用于选择合适组的 EA,我们不得不稍作修改,以确保可以使用远程测试代理进行优化。问题是优化后的 EA 必须从 CSV 文件中读取数据。在本地计算机上进行优化时,这不会造成任何问题 只需将数据文件放置在终端共享文件夹中,所有本地测试代理都可以访问它。

但是远程测试代理无法访问此类包含数据的文件。这就是我们使用 #property tester_file 指令的原因,它允许我们将任何指定的文件传递给其数据文件夹中的所有测试代理。当开始优化时,数据文件从共享文件夹复制到启动优化过程的本地代理的数据文件夹。然后,本地代理数据文件夹中的数据文件会自动发送到所有其他测试代理的数据文件夹中。

由于我们现在有 SQLite 数据库中测试单个交易策略实例的结果数据,我的第一个冲动就是做同样的事情。由于 SQLite 数据库是媒体上的单个文件,因此可以使用上述指令以相同的方式将其复制到远程测试代理。但这里有一个小细节 — 传输的 CSV 文件的大小约为 2 MB,而数据库文件的大小超过 300 MB。

这种差异是由于这样一个事实,首先,我们试图在数据库中保存关于每次通过的尽可能多的统计信息,CSV 文件只存储了一些统计参数和策略实例输入参数值的数据。其次,在我们的数据库中,我们已经拥有关于三个不同交易品种以及每个交易品种的三个不同时间框架的策略优化结果的信息。换句话说,通过的数量增加了约九倍。

考虑到每个测试代理都会收到自己的传输文件副本,我们需要在 32 核服务器上放置超过 9 GB 的数据才能对其进行测试。如果我们在第一阶段处理更多的交易品种和时间框架,那么包含数据库的文件的大小将会增加好几倍。这可能导致代理服务器上可用磁盘空间耗尽,更不用说需要通过网络传输大量数据。

然而,我们要么不需要第二阶段已完成测试通过结果的大部分存储信息,要么不需要同时使用所有信息。换句话说,从一个通过的整个存储值集合中,我们只需要提取此通过中使用的 EA 初始化字符串。我们还计划收集几组交易策略的单份副本 每个交易品种和时间框架的组合各一份。例如,如果我们搜索 EURGBP H1 组,我们不需要除 EURGBP 之外的其他交易品种和除 H1 之外的其他时间框架的通过数据。

因此,让我们执行以下操作:当我们开始每次优化时,我们将创建一个具有预定义名称的新数据库,并用给定优化任务所需的最少信息填充它。 我们将调用现有的数据库 主数据库,而新创建的数据库 将被称为优化问题数据库 或简称为 任务数据库。 

由于我们将在 #property tester_file 指令中指定其名称,因此数据库文件将被传递给测试代理。在测试代理上运行时,优化的 EA 将与主数据库中的摘要一起工作。当在数据帧收集模式下,在本地计算机上运行时,优化的 EA 仍会将从测试代理接收到的数据保存到主数据库中。

要实现这样的工作流程,首先需要修改用于处理数据库的 CDatabase 类。


CDatabase 修改

在开发这个类时,我没有预见到我们需要从单个 EA 的代码中处理多个数据库。相反,我们似乎应该确保只使用一个数据库,以免以后对存储的内容和位置感到困惑。但现实会做出自己的调整,我们必须改变我们的方法。

为了尽量减少编辑,我决定暂时将 CDatabase 类保留为静态。换句话说,我们不会创建类对象,而是将其公有方法简单地用作给定名称空间中的一组函数。同时,我们仍然可以使用此类中的私有属性和方法。 

为了能够连接到不同的数据库,我们修改了 Open() 方法并将其重命名为 Connect()。重命名是因为首先添加了新的 Connect() 方法,后来发现它实际上执行与 Open() 相同的工作。因此,我们决定放弃后者。

新方法与之前方法的主要区别在于能够将数据库名称作为参数传递。Open() 方法始终只打开具有 s_fileName 属性中指定的名称的数据库,该属性是一个常量。如果不向新方法传递数据库名称,则新方法也会保留此行为。如果我们向 Connect() 方法传递一个非空名称,那么它不仅会使用传递的名称打开数据库,而且还会将其保存在 s_fileName 属性中。因此,重复调用 Connect() 而不指定名称将会打开最后打开的数据库。

除了将文件名传递给 Connect() 方法之外,我们还将传递使用共享文件夹的标志。这是必要的,因为将主数据库存储在通用终端数据文件夹中更方便,而任务数据库存储在测试代理数据文件夹中。因此,在某些情况下,我们需要在数据库打开函数中指定 DATABASE_OPEN_COMMON 标志。让我们添加一个新的静态类 s_common 来存储标志。默认情况下,我们假设我们要从共享文件夹打开数据库文件。主要基础名称仍设置为 s_fileName 静态属性的初始值。

那么类描述将会是这样的:

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   static int        s_db;          // DB connection handle
   static string     s_fileName;    // DB file name
   static int        s_common;      // Flag for using shared data folder
   
public:
   static int        Id();          // Database connection handle

   static bool       IsOpen();      // Is the DB open?
   static void       Create();      // Create an empty DB

   // Connect to the database with a given name and location
   static bool       Connect(string p_fileName = NULL,
                             int p_common = DATABASE_OPEN_COMMON
                            );

   static void       Close();       // Closing DB
   ...
};

int    CDatabase::s_db       =  INVALID_HANDLE;
string CDatabase::s_fileName = "database892.sqlite";
int    CDatabase::s_common   =  DATABASE_OPEN_COMMON;


Connect() 方法本身中,我们将首先检查当前是否有任何数据库处于打开状态。如果有,我们将关闭它。接下来,我们将检查是否已指定新的数据库文件名。如果是,则设置一个新名称和访问共享文件夹的标志。之后,我们执行打开数据库的步骤,并在必要时创建一个空的数据库文件。 

此时,我们已经通过调用 Create() 方法删除了强制创建表和数据填充新创建的数据库的功能,就像之前所做的那样。由于我们主要使用现有数据库,因此这将更加方便。如果我们仍然需要重新创建并再次用初始信息填充数据库,我们可以使用辅助的 CleanDatabase 脚本。

//+------------------------------------------------------------------+
//| Check connection to the database with the given name             |
//+------------------------------------------------------------------+
bool CDatabase::Connect(string p_fileName, int p_common) {
   // If the database is open, close it 
   if(IsOpen()) {
      Close();
   }

   // If a file name is specified, save it together with the shared folder flag
   if(p_fileName != NULL) {
      s_fileName = p_fileName;
      s_common = p_common;
   }

   // Open the database
   // Try to open an existing DB file
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common);

   // If the DB file is not found, try to create it when opening
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common);

      // Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d",
                     s_fileName, GetLastError());
         return false;
      }
   }

   return true;
}

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


第一阶段的 EA 交易

在本文中,我们不会使用第一阶段的 EA 交易,但为了保持一致性,我们将对其进行一些细微的修改。首先,我们将删除上一篇文章中添加的风险管理器输入参数。我们在这个 EA 中不需要它们,因为在第一阶段我们肯定不会选择风险管理器参数。我们会将它们添加到后面优化阶段之一的 EA 中。我们将立即从非活动状态下的初始化字符串创建风险管理器对象本身。

此外,在优化的第一阶段,我们不需要改变诸如幻数、固定交易余额和缩放因子等输入参数。因此我们在声明时将 input 关键词从它们那里去掉。我们得到以下代码:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int         idTask_              = 0;
input group "===  Opening signal parameters"
input int         signalPeriod_        = 130;  // Number of candles for volume averaging
input double      signalDeviation_     = 0.9;  // Relative deviation from the average to open the first order 
input double      signaAddlDeviation_  = 1.4;  // Relative deviation from the average for opening the second and subsequent orders

input group "===  Pending order parameters"
input int         openDistance_        = 231;  // Distance from price to pending order
input double      stopLevel_           = 3750; // Stop Loss (in points)
input double      takeLevel_           = 50;   // Take Profit (in points)
input int         ordersExpiration_    = 600;  // Pending order expiration time (in minutes)

input group "===  Money management parameters"
input int         maxCountOfOrders_    = 3;     // Maximum number of simultaneously open orders

ulong             magic_               = 27181; // Magic
double            fixedBalance_        = 10000;
double            scale_               = 1;

datetime fromDate = TimeCurrent();

CAdvisor     *expert;         // Pointer to the EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(1.0);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                              Symbol(), Period(),
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_
                           );

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 0,0,0,0,0,0
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

将获取的代码以新名称 SimpleVolumesStage1.mq5 保存在当前文件夹中。


第二阶段的 EA 交易

现在是时候进入本文的重点了 第二阶段优化 EA。正如前面提到的,它将致力于优化第一阶段获得的一组交易策略单个实例的选择。让我们以第六 部分中的 OptGroupExpert.mq5 EA 作为基础,并对其进行必要的更改。

首先,在 #property tester_file 指令中设置测试任务数据库的名称。 选择特定名称并不重要,因为它仅用于执行一次优化运行,并且仅在此 EA 内使用。

#define PARAMS_FILE "database892.stage2.sqlite"
#property tester_file PARAMS_FILE


我们现在将指定主数据库的名称,而不是输入参数中指定的 CSV 文件名:

input group "::: Selection for the group"
sinput string  fileName_      = "database892.sqlite"; // - File with the main database


由于我们想要选择在同一交易品种和时间框架内工作的单个交易策略实例组,而这些实例又在主数据库的“job”(作业)表中定义,因此我们将在输入中添加指定作业 ID 的功能,该作业的任务构成了当前组中可供选择的单个交易策略实例集:

input int      idParentJob_   = 1;                    // - Parent job ID


以前,我们使用 8 个副本作为一组的选择,但现在我们将其数量增加到 16 个。为此,为其他策略实例索引添加八个输入参数,并增加 count_ 参数的默认值:

input int      count_         = 16;                   // - Number of strategies in the group (1 .. 16)

input int   i1_ = 1;       // - Strategy index #1
input int   i2_ = 2;       // - Strategy index #2
input int   i3_ = 3;       // - Strategy index #3
input int   i4_ = 4;       // - Strategy index #4
input int   i5_ = 5;       // - Strategy index #5
input int   i6_ = 6;       // - Strategy index #6
input int   i7_ = 7;       // - Strategy index #7
input int   i8_ = 8;       // - Strategy index #8
input int   i9_ = 9;       // - Strategy index #9
input int   i10_ = 10;     // - Strategy index #10
input int   i12_ = 11;     // - Strategy index #11
input int   i11_ = 12;     // - Strategy index #12
input int   i13_ = 13;     // - Strategy index #13
input int   i14_ = 14;     // - Strategy index #14
input int   i15_ = 15;     // - Strategy index #15
input int   i16_ = 16;     // - Strategy index #16


让我们创建一个单独的函数来负责当前优化任务的数据库的创建。在函数中,我们将通过调用 DB::Connect() 方法连接到任务数据库。我们将仅向数据库添加一个包含两个字段的表:

  • id_pass 第一阶段的测试器通过 ID
  • params 第一阶段测试器通过的 EA 初始化字符串

如果该表之前已经添加(这不是第二阶段优化的第一次运行),那么我们将删除并重新创建它,因为我们需要第一阶段的其他通过来进行新的优化。

然后我们连接到主数据库并从中提取我们现在将选择一组的测试通过的数据。主数据库文件的名称作为 fileName 参数传递给该函数。检索所需数据的查询连接了 passestasksjobsstages 表并返回满足以下条件的行:

  • 该通过的阶段名称是“First”。这就是我们所说的第一阶段,通过这个名称我们可以仅对属于第一阶段的通过进行排序;
  • job ID 等于 idParentJob 函数参数传递的ID;
  • 通过的规范化利润超过 2500;
  • 交易数量超过 20;
  • 夏普比率大于 2。

最后三个条件是可选的。它们的参数是根据第一阶段特定传递的结果选择的,这样,一方面,我们会在查询结果中包含相当多的通过,另一方面,这些通过的质量会很好。

在检索查询结果时,我们立即创建一个 SQL 查询数组以将数据插入任务数据库。一旦检索到所有结果,我们就从主数据库切换到任务数据库,并在一个事务中执行所有生成的数据插入查询。此后,我们再切换回主数据库。

//+------------------------------------------------------------------+
//| Creating a database for a separate stage task                    |
//+------------------------------------------------------------------+
void CreateTaskDB(const string fileName, const int idParentJob) {
// Create a new database for the current optimization task
   DB::Connect(PARAMS_FILE, 0);
   DB::Execute("DROP TABLE IF EXISTS passes;");
   DB::Execute("CREATE TABLE passes (id_pass INTEGER PRIMARY KEY AUTOINCREMENT, params TEXT);");
   DB::Close();

// Connect to the main database
   DB::Connect(fileName);

// Request to obtain the required information from the main database
   string query = StringFormat(
                     "SELECT DISTINCT  p.params"
                     "  FROM passes p"
                     "       JOIN"
                     "       tasks t ON p.id_task = t.id_task"
                     "       JOIN"
                     "       jobs j ON t.id_job = j.id_job"
                     "       JOIN"
                     "       stages s ON j.id_stage = s.id_stage"
                     " WHERE (s.name='First' AND "
                     "       j.id_job = %d AND"
                     "       p.custom_ontester > 2500 AND "
                     "       trades > 20 AND "
                     "       p.sharpe_ratio > 2)"
                     " ORDER BY s.id_stage ASC,"
                     "          j.id_job ASC,"
                     "          p.custom_ontester DESC;", idParentJob);

// Execute the request
   int request = DatabasePrepare(DB::Id(), query);
   if(request == INVALID_HANDLE) {
      PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
      DB::Close();
      return;
   }

// Structure for query results
   struct Row {
      string         params;
   } row;

// Array for requests to insert data into a new database
   string queries[];

// Fill the request array: we will only save the initialization strings
   while(DatabaseReadBind(request, row)) {
      APPEND(queries, StringFormat("INSERT INTO passes VALUES(NULL, '%s');", row.params));
   }

// Reconnect to the new database and fill it
   DB::Connect(PARAMS_FILE, 0);
   DB::ExecuteTransaction(queries);

// Reconnect to the main database
   DB::Connect(fileName);
   DB::Close();
}


该函数将在两个地方被调用。其主要调用位置 OnTesterInit() 处理函数,该处理函数在单独的终端图表上开始优化之前启动。其任务创建和填充优化任务数据库,检查创建的任务数据库中是否存在交易策略单个实例的参数集,并设置枚举单个实例索引的正确范围:

//+------------------------------------------------------------------+
//| Initialization before optimization                               |
//+------------------------------------------------------------------+
int OnTesterInit(void) {
// Create a database for a separate stage task
   CreateTaskDB(fileName_, idParentJob_);

// Get the number of strategy parameter sets
   int totalParams = GetParamsTotal();

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_FAILED);
   }

// Set scale_ to 1
   ParameterSetRange("scale_", false, 1, 1, 1, 2);

// Set the ranges of change for the parameters of the set index iteration
   for(int i = 1; i <= 16; i++) {
      if(i <= count_) {
         ParameterSetRange("i" + (string) i + "_", true, 0, 1, 1, totalParams);
      } else {
         // Disable the enumeration for extra indices
         ParameterSetRange("i" + (string) i + "_", false, 0, 1, 1, totalParams);
      }
   }

   return CVirtualAdvisor::TesterInit(idTask_);
}


独立的 GetParamsTotal() 函数负责获取单个实例的参数集数量的任务。它的目标非常简单:连接到任务数据库,执行一个 SQL 查询以获取所需数量并返回其结果:

//+------------------------------------------------------------------+
//| Number of strategy parameter sets in the task database           |
//+------------------------------------------------------------------+
int GetParamsTotal() {
   int paramsTotal = 0;

// If the task database is open,
   if(DB::Connect(PARAMS_FILE, 0)) {
      // Create a request to get the number of passes for this task
      string query = "SELECT COUNT(*) FROM passes p";
      int request = DatabasePrepare(DB::Id(), query);
      
      if(request != INVALID_HANDLE) {
         // Data structure for query result
         struct Row {
            int      total;
         } row;
         
         // Get the query result from the first string
         if (DatabaseReadBind(request, row)) {
            paramsTotal = row.total;
         }
      } else {
         PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
      }
      DB::Close();
   }

   return paramsTotal;
}


接下来,我们将重写 LoadParams() 函数,它用于加载交易策略单个实例的参数集。与之前的实现不同,当我们读取整个文件,创建一个包含所有参数集的数组,然后从这个数组中选择几个必要的参数集时,现在我们将以不同的方式执行。我们将向该函数传递所需集合索引的列表,并形成一个 SQL 查询,该查询将从任务数据库仅提取具有这些索引的集合。我们将把从数据库获取的参数集(以初始化字符串的形式)组合成一个以逗号分隔的初始化字符串,该字符串将由此函数返回:

//+------------------------------------------------------------------+
//| Loading strategy parameter sets                                  |
//+------------------------------------------------------------------+
string LoadParams(int &indexes[]) {
   string params = NULL;
// Get the number of sets
   int totalParams = GetParamsTotal();

// If they exist, then
   if(totalParams > 0) {
      if(DB::Connect(PARAMS_FILE, 0)) {
         // Form a string from the indices of the comma-separated sets taken from the EA inputs
         // for further substitution into the SQL query
         string strIndexes = "";
         FOREACH(indexes, strIndexes += IntegerToString(indexes[i]) + ",");
         strIndexes += "0"; // Add a non-existent index so as not to remove the last comma

         // Form a request to obtain sets of parameters with the required indices
         string query = StringFormat("SELECT params FROM passes p WHERE id_pass IN(%s)", strIndexes);
         int request = DatabasePrepare(DB::Id(), query);

         if(request != INVALID_HANDLE) {
            // Data structure for query results
            struct Row {
               string   params;
            } row;

            // Read the query results and join them with a comma
            while(DatabaseReadBind(request, row)) {
               params += row.params + ",";
            }
         } else {
            PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d",
                        query, GetLastError());
         }
         DB::Close();
      }
   }

   return params;
}


最后,到了 EA 初始化函数的时间了。除了设置资金管理参数外,我们首先组装一个所需数量的单个交易策略实例参数集索引数组。所需数量在 count_ EA 输入参数中指定,而索引本身在名为 i{N}_ 的输入参数中设置 ,其中 {N} 取值范围为 1 至 16。

然后,我们将所有索引放入集合类型容器(CHashSet)中,并确保集合具有与数组相同数量的索引,以检查生成的索引数组中是否存在重复项。如果是这样,那么所有索引都是唯一的。如果集合的索引少于数组的索引,则报告输入参数错误并且不要运行此过程。

如果索引一切正常,则检查当前 EA 的模式。如果该通过是优化过程的一部分,那么任务数据库肯定是在优化开始之前创建的,现在可用。如果这是一次常规的单独测试运行,那么我们无法保证任务数据库的存在,因此我们将通过调用 CreateTaskDB() 函数简单地重新创建它。

之后,以单个初始化字符串 (或者更确切地说,是它的一部分,我们将其替换为 EA 对象的最终初始化字符串) 的形式从任务数据库加载具有所需索引的参数集。剩下的就是形成最终的初始化字符串并从中创建一个 EA 对象。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Array of all indices from the EA inputs
   int indexes_[] = {i1_, i2_, i3_, i4_,
                     i5_, i6_, i7_, i8_,
                     i9_, i10_, i11_, i12_,
                     i13_, i14_, i15_, i16_
                    };
                    
// Array for indices to be involved in optimization
   int indexes[];
   ArrayResize(indexes, count_);

// Copy the indices from the inputs into it
   FORI(count_, indexes[i] = indexes_[i]);

// Multiplicity for parameter set indices
   CHashSet<int> setIndexes;

// Add all indices to the multiplicity
   FOREACH(indexes, setIndexes.Add(indexes[i]));

// Report an error if
   if(count_ < 1 || count_ > 16           // number of instances not in the range 1 .. 16
         || setIndexes.Count() != count_  // not all indexes are unique
     ) {
      return INIT_PARAMETERS_INCORRECT;
   }

// If this is not an optimization, then you need to recreate the task database
   if(!MQLInfoInteger(MQL_OPTIMIZATION)) {
      CreateTaskDB(fileName_, idParentJob_);
   }

// Load strategy parameter sets
   string strategiesParams = LoadParams(indexes);

// If nothing is loaded, report an error
   if(strategiesParams == NULL) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  "database892.sqlite");
      return(INIT_PARAMETERS_INCORRECT);
   }

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%d,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategiesParams, scale_,
                            0, 0, 0, 0, 0, 0,
                            magic_, "SimpleVolumes", useOnlyNewBars_
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}
将所做的更改保存到当前文件夹中的 SimpleVolumesStage2.mq5 文件。第二阶段要优化的 EA 已经准备好了,现在我们开始在主数据库中创建第二阶段优化的任务。


创建第二阶段任务

首先,让我们创建优化本身的第二阶段。为此,向 stages 表中添加新行并按如下方式填充其值:

图 1. 'stages' 表的第二阶段行

目前我们需要第二阶段的 id_stage 值为 2,第二阶段的 name 值我们设为 Second。要创建第二阶段的 jobs ,我们需要获取第一阶段的所有工作,并创建具有相同交易品种和时间框架的第二阶段的相应工作。tester_inputs 字段的值形成一个字符串,其中对应的第一阶段作业的 ID 设置为 idParentJob_ EA 输入参数。

为此,在主数据库中执行以下 SQL 查询:

INSERT INTO jobs 
SELECT NULL,
       2 AS id_stage,
       j.symbol,
       j.period,
       'idParentJob_=' || j.id_job || '||0||1||10||N' AS tester_inputs,
       'Queued' AS status
  FROM jobs j
  JOIN stages s ON j.id_stage = s.id_stage
  WHERE s.name='First';


我们只需要执行一次,就会为所有现有的第一阶段作业创建第二阶段的作业:

图 2.为第二阶段作业添加了条目 (id_job = 10 ..18)

您可能注意到了,虽然我说了我们已经完成了第一阶段的优化,但是不管是第一阶段,还是主库中第一阶段的任务,都是Queued (在队列中)状态的。这似乎是一个矛盾。是的,确实如此。至少现在是这样。事实上,我们还没有注意到在完成工作中包含的所有优化任务后更新作业的状态,以及在完成阶段中包含的全部作业后更新阶段的状态。我们可以通过两种方式修复此问题:

  • 通过向我们的优化 EA 添加额外的代码,以便在每个优化任务完成时,进行检查以查看是否不仅需要更新任务的状态,而且需要更新作业和阶段的状态;
  • 通过向数据库添加跟踪任务改变事件的触发器,当该事件发生时,触发代码将需要检查是否需要更新作业和阶段的状态,并更新它们。
两种方法都是可行的,但我认为第二种方法更美观。但其实现尚需时日,目前尚无迫切实现的必要。我们尚未达到可以启动整个项目、分几个阶段进行优化并仅等待结果的阶段。目前,我们将手动运行每个阶段,并将其与前面的阶段分开运行。因此,就目前而言,我们只需更新我们已经实现的任务的状态就足够了。

剩下的就是为每个作业创建任务,然后就可以启动第二阶段了。与第一阶段不同,在这里我们不会在一项作业中使用具有不同优化标准的多个任务。我们只使用一个标准 平均规范化年利润。为了设置此标准,我们需要在优化标准字段中选择索引 6。

我们可以使用以下 SQL 查询为所有符合优化标准 6 的作业创建第二阶段任务:

INSERT INTO tasks 
SELECT NULL,
       j.id_job AS id_job,
       6 AS optimization,
       NULL AS start_date,
       NULL AS finish_date,
       'Queued' AS status
  FROM jobs j
  JOIN stages s ON j.id_stage = s.id_stage
  WHERE s.name='Second';


让我们运行一次并在 tasks 表中获取与第二阶段执行的任务相对应的新条目。之后,将 Optimization.ex5 EA 添加到任意终端图表并等待终端完成所有优化任务。执行时间可能因 EA 本身、测试间隔的长度、交易品种和时间框架的数量以及所涉及的代理的数量 而有很大差异。

对于本项目使用的 EA,所有第二阶段优化任务均在两年间隔内(2021 年和 2022 年)在大约 5 小时内完成,并在 32 个代理上针对三个交易品种和三个时间框架进行了优化。让我们看看结果。


指定通过的 EA

为了简化我们的任务,我们对现有的 EA 做一些小的改动。我们将实现 passes_ 输入参数,其中我们将指示逗号分隔的 ID,以及我们希望在该 EA 中合并为一个组的策略集。

然后,在EA初始化方法中,我们只需要从主数据库中获取这些传递的参数(策略组的初始化字符串),并将其替换为 EA 中 EA 对象的初始化字符串:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Money management"
sinput double  expectedDrawdown_ = 10;    // - Maximum risk (%)
sinput double  fixedBalance_     = 10000; // - Used deposit (0 - use all) in the account currency
input double   scale_            = 1.00;  // - Group scaling multiplier

input group "::: Selection for the group"
input string     passes_ = "734469,735755,736046,736121,761710,776928,786413,795381"; // - Comma-separated pass IDs

ulong  magic_            = 27183;   // - Magic
bool   useOnlyNewBars_   = true;    // - Work only at bar opening

datetime fromDate = TimeCurrent();


CVirtualAdvisor     *expert;             // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

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

// If the connection to the main database is established,
   if(DB::Connect()) {
      // Form a request to receive passes with the specified IDs
      string query = StringFormat(
                        "SELECT DISTINCT  p.params"
                        "  FROM passes p"
                        " WHERE id_pass IN (%s);"
                        , passes_);
      int request = DatabasePrepare(DB::Id(), query);

      if(request != INVALID_HANDLE) {
         // Structure for reading results
         struct Row {
            string         params;
         } row;

         // For all query result strings, concatenate initialization rows 
         while(DatabaseReadBind(request, row)) {
            strategiesParams += row.params + ",";
         }
      }
      DB::Close();
   }
// If no parameter sets are found, abort the test
   if(strategiesParams == NULL) {
      return INIT_FAILED;
   }

// Prepare the initialization string for an EA with a group of several strategies
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%d,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategiesParams, scale_,
                            0, 0, 0, 0, 0, 0,
                            magic_, "SimpleVolumes", useOnlyNewBars_
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

将生成的组合 EA 保存在当前文件夹的 SimpleVolumesExpert.mq5 文件中。

我们可以获取第二阶段最佳通过的 ID,例如,使用以下 SQL 查询:

SELECT p.id_pass,
       j.symbol,
       j.period,
       p.custom_ontester,
       p.profit,
       p.profit_factor,
       p.sharpe_ratio,
       p.equity_dd,
       p.params
  FROM (
           SELECT p0.*,
                  ROW_NUMBER() OVER (PARTITION BY id_task ORDER BY custom_ontester DESC) AS rn
             FROM passes p0
       )
       AS p
       JOIN
       tasks t ON t.id_task = p.id_task
       JOIN
       jobs j ON j.id_job = t.id_job
       JOIN
       stages s ON s.id_stage = j.id_stage
 WHERE rn = 1 AND 
       s.name = 'Second';


在这个查询中,我们再次组合主数据库中的表,以便我们可以选择属于名为“Second”的阶段的那些过程。我们还将 passes 表与其副本进行合并,将其分为具有相同任务 ID 的部分。在每个部分中,行都按我们的优化标准值(custom_ontester)的降序编号和排序。各部分中的行索引位于 rn 列内。在最终结果中,我们只保留每个部分的第一行 — 具有最高优化标准值的行。 

图 3。第二阶段各作业最好结果的通过 ID列表

我们将第一列 id_pass 中的 ID 替换为组合 EA 的 passes_ 输入参数。运行测试,并得到以下结果:

图 4.三种交易品种和三种时间框架的组合 EA 测试结果

在此测试间隔内,净值图看起来相当不错:增长率在整个间隔内保持大致相同,回撤在可接受的预期限度内。但我更感兴趣的是,我们现在可以几乎自动生成一个 EA 初始化字符串,该字符串结合了针对不同交易品种和时间框架的几组最佳交易策略单一实例。


结论

所以,我们计划的优化程序的第二阶段也是以草案的形式实现的。为了进一步方便,最好创建一个单独的 Web 界面来创建和管理项目以优化交易策略。在我们开始实现各种质量改善措施之前,合理的做法是先完成整个计划路径,不要被暂时可以不做的事情分散注意力。而且,在制定实现方案的过程中,我们常常会因为推进过程中出现的新情况而被迫对原计划做出一些调整。

我们现在仅在相对较短的时间间隔内进行了第一阶段和第二阶段的优化。当然,最好延长测试间隔并再次优化一切。我们还没有尝试在第二阶段连接聚类,我们在该系列文章的第六 部分进行了尝试,以加速优化过程。但这需要更多的开发工作,因为我们必须开发一种机制来自动执行在 MQL5 中难以实现但在 Python 或 R 中很容易添加的操作。

很难决定下一步我们该怎么做,所以,让我们休息一下,这样今天不清楚的事情明天就会变得清楚。

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



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

附加的文件 |
Database.mqh (13.72 KB)
Optimization.mq5 (19.13 KB)
TesterHandler.mqh (17.85 KB)
MQL5 向导技巧须知(第27部分):移动平均线与攻击角度 MQL5 向导技巧须知(第27部分):移动平均线与攻击角度
攻击角度是一个经常被引用的指标,其陡峭程度被认为与当前趋势的强度密切相关。让我们来看一下通常如何使用和理解该指标,并探讨在测量时是否可以做出一些改变,以优化那些将其纳入交易系统的应用效果。
神经网络变得简单(第 87 部分):时间序列补片化 神经网络变得简单(第 87 部分):时间序列补片化
预测在时间序列分析中扮演重要角色。在新文章中,我们将谈谈时间序列补片化的益处。
利用季节性因素进行外汇价差交易 利用季节性因素进行外汇价差交易
本文探讨了在外汇价差交易中利用季节性因素生成并提供报告数据的可能性。
开发回放系统(第 53 部分):事情变得复杂(五) 开发回放系统(第 53 部分):事情变得复杂(五)
在本文中,我们将介绍一个很少有人了解的重要话题:定制事件。危险。这些要素的优缺点。对于希望成为 MQL5 或其他语言专业程序员的人来说,本主题至关重要。在此,我们将重点介绍 MQL5 和 MetaTrader 5。