
开发多币种 EA 交易 (第 13 部分):自动化第二阶段 — 分组选择
概述
上一篇文章中风险管理稍微分散了我们的注意力,现在让我们回到正题 — 测试自动化。在之前的一篇文章中,我们概述了优化和搜索最终 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 参数传递给该函数。检索所需数据的查询连接了 passes、tasks、jobs 和 stages 表并返回满足以下条件的行:
- 该通过的阶段名称是“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

