English Русский Español Português
preview
开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二)

开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二)

MetaTrader 5测试者 |
56 0
Yuriy Bykov
Yuriy Bykov

概述

本系列的前几部分已经专门讨论过这个问题。这使我们能够为项目的进一步发展选择更正确的方向。现在,我们不再需要手动在优化数据库中创建单个项目中执行的所有任务,而是有了一个更方便的工具 —— 优化项目创建脚本。更准确地说,它更像是一个模板,可以轻松进行调整,以创建用于优化各种交易策略的项目。

这篇文章提供了一个功能齐全的解决方案,允许我们将选定的新交易策略组直接导出到新数据库,从而启动创建的优化项目。该数据库被命名为 EA 数据库,以便与之前使用的优化数据库(完整版和缩写版)区分开来。任何在交易账户上运行的最终 EA 都可以使用 EA 数据库,只需更新所用交易系统的设置,无需重新编译。我们尚未验证该机制的正确性。但是,可以肯定的是,这种方法简化了整个输送机的操作。在此之前,我们计划在现有的三个输送机平台的基础上增加几个平台:

  • 导出库,在数据文件夹中获取 ExportedGroupsLibrary.mqh(Stage4)。
  • 将文件复制到工作文件夹(Stage5、Python 或 DLL)或修改上一阶段以直接导出到工作文件夹。
  • 编译最终 EA(Stage6,Python)。
  • 使用新版本的最终 EA 启动终端。

现在这些步骤已不再必要。与此同时,我们也消除了一个主要缺点:在策略测试器中无法检查这种自动更新机制的正确运行情况。重新编译阶段的存在与以下事实不符:在一次测试过程中,EA 的编译代码不能更改。

但最重要的是,我们将努力朝着简化所有书面代码的使用迈出重要一步,以优化任意策略,并将尝试描述一个循序渐进的操作算法。


规划路径

让我们从实现早就应该对项目文件结构进行的更改开始。目前,它们位于一个文件夹中,这一方面简化了新项目中所有代码的传输和使用,但另一方面,在不断开发的过程中,我们最终会得到几个几乎相同的工作项目文件夹,用于不同的交易策略,每个文件夹都需要单独更新。因此,我们将把所有代码分为一个库部分和一个项目部分,库部分对所有项目都是相同的,项目部分包含特定于不同项目的代码。

接下来,我们实施了一项检查,以确保如果在最终 EA 操作期间出现新的策略组,它将能够正确加载更新的参数并继续工作。让我们像往常一样,首先在策略测试器中运行的 EA 中对所需行为进行建模。如果测试结果令人满意,那么就可以将其用于在测试器中不再有效的最终 EA 中。

为此我们需要什么呢?在上一节中,我们还没有实现将优化间隔的结束日期和优化输送机执行完成日期等信息保存到 EA 数据库中。现在我们需要这个信息,否则,在运行测试程序时,最终 EA 将无法确定这组策略是否已在特定的模拟日期形成。

最终 EA 还需要进行修改,以便在 EA 数据库中出现新的策略组时能够执行自身的重新初始化。目前,它还不具备这种功能。在这里,至少有一些关于当前交易策略组的信息是有用的,这样人们就可以清楚地看到从一个组到另一个组的成功过渡。直接在运行 EA 的图表上查看此信息会更方便,但当然,您可以为此目的使用终端日志的常规输出。

最后,我们将介绍使用迄今为止开发的工具的通用算法。

让我们开始吧!


过渡到不同的文件结构

在前面的所有部分中,开发都是在一个工作项目文件夹中进行的。其中的现有文件已被修改,并不时添加新文件。有时,与项目不再相关的文件会被删除或“遗忘”。当只使用一种可能的交易策略时(例如,文章中用作示例的 SimpleVolumes 策略),这种方法是合理的。但是,当将自动优化机制扩展到其他交易策略时,有必要创建项目工作文件夹的完整副本,然后只更改其中的一小部分文件。

随着以这种方式连接的交易策略数量的增长(以及不同工作文件夹数量的增长),保持所有这些策略中的代码更新变得越来越耗费人力。因此,我们将把所有工作文件夹都相同的代码部分移到位于 MQL5/Include 中的一个单独的库文件夹中。库文件的共享文件夹名称已设置为 Advisor ,为了防止它与可能存在的同名文件夹冲突,已向其添加了一个唯一组件。现在库文件将位于 MQL5/Include/antekov/Advisor/ 中。

把所有的文件都转移到里面后,我们开始进一步系统化。决定将所有文件分发到子文件夹中,以反映其中文件的某些共同目的。这需要对将一些文件包含到其他文件中的指令进行一些处理,因为它们的相对位置已经发生了变化。但最终,我们成功地分别编译了 EA 和所有库文件。

修改后的文件结构如下所示:

图 1.EA 库文件结构

如您所见,我们选择了几组文件,并将其放置在以下子文件夹中:

  • Base. 其他项目类都继承自基类。
  • Database.用于处理项目 EA 使用的所有类型数据库的文件。
  • Experts.包含不同类型的已用 EA 的公共部分的文件。
  • Optimization.负责自动优化的类。
  • Strategies.用于演示项目如何运作的交易策略示例。
  • Utils.辅助工具,用于简化代码的宏。
  • Virtual.通过使用虚拟交易订单和头寸系统来创建各种对象的类。

在上面列出的所有子文件夹中,只有一个子文件夹脱颖而出,值得特别提及。这就是 Experts 文件夹。如果我们将图1中的文件组成与上一部分的文件组成进行比较,我们可以注意到,只有这个文件夹包含以前不存在的文件。起初,您可能会认为我们只是部分重命名了所用 EA 的文件并将其移动到这里,但请注意,它们的扩展名不是 *.mq5,而是 *.mqh。但在我们更详细地研究它们之前,让我们先看看某个交易策略的单独项目文件夹中会保留什么。

我们将在 MQL5/Experts/ 目录下创建一个单独的文件夹,并随意命名。它将包含所有已使用 EA 的文件:

图 2.使用 EA 库的项目的文件结构

这些文件的用途如下:

  • CreateProject.mq5 — 用于在优化数据库中创建自动优化项目的 EA。数据库中的每个项目都分为三个阶段,每个阶段包含一个或多个任务。每项作业由阶段 EA 执行的一个或多个优化任务组成。

  • HistoryReceiverExpert.mq5 — 用于重现先前保存的交易历史记录的 EA。我们已经很久没有使用它了,因为它最初只是为了检查更换经纪商时结果的可重复性而创建的。自动优化不需要它,所以如果你愿意,你可以安全地删除它。

  • Optimization.mq5 — 用于运行来自自动优化项目任务的 EA。我们将按顺序执行此类任务的过程称为自动优化输送机。

  • SimpleVolumes.mq5 — 一个最终 EA,它结合了许多 SimpleVolumes 类型交易策略的单个实例。它将从 EA 数据库中获取有关这些样本成分的信息。这些信息随后将由自动优化流程的第三阶段 EA 放入 EA 数据库中。

  • Stage1.mq5 — 自动优化输送机第一阶段的 EA。它优化交易策略的单个实例。

  • Stage2.mq5 — 自动优化输送机第二阶段的 EA。在优化过程中,它从第一阶段获得的许多优秀单例中选择出一小部分实例(通常是 8 或 16 个),这些实例协同工作时,在标准化利润方面表现出最佳结果。

  • Stage3.mq5 — 自动优化输送机第三阶段的 EA。 它将第二阶段获得的所有组合并,对仓位大小进行归一化,并将生成的组以设置中指定的缩放因子保存到 EA 数据库中。

在列出的 EA 文件中,只有 CreateProject.mq5 保存了其内容。所有其他 EA 基本上只包含一个命令,用于从位于 MQL5/Include/antekov/Advisor/ExpertsAdvisor 库中包含相应的 mqh 文件。值得注意的是, SimpleVolumes.mq5HistoryReceiverExpert.mq5 使用了相同的 Expert.mqh 包含文件。实践证明,对于不同的交易策略,我们不需要为第二阶段和第三阶段的 EA 编写不同的代码。对于第一阶段的 EA 来说,唯一的区别在于不同的输入参数以及根据它们的值创建所需的初始化字符串。其他一切都将保持不变。

因此,当切换到具有不同交易策略的项目时,只有 CreateProject.mq5 需要进行更重大的修改。在未来,我们也将尝试从中提取共同部分。

现在让我们来看看需要对库文件进行哪些更改才能实现最终 EA 的自动更新。


完成日期

让我们回顾一下,在上一部分中,我们启动了四个几乎相同的优化项目。它们之间的区别在于交易策略单实例优化区间的结束日期。交易工具的构成、时间周期和其他参数均无差异。因此,EA 数据库的 strategy_groups 表中出现了以下条目:

由于我们在组名称中添加了优化间隔的结束日期,因此该信息使我们能够了解哪个组对应于哪个结束日期。但 EA 也应该能够理解这一点。我们在这个表中专门创建了两个字段来存储这些日期,在创建记录时需要填写这些日期,甚至在代码中准备了一个需要这样做的地方:

//+------------------------------------------------------------------+
//| Export an array of strategies to the specified EA database       |
//| as a new group of strategies                                     |
//+------------------------------------------------------------------+
void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) {
// Connect to the required EA database
   if(DB::Connect(p_advFileName, DB_TYPE_ADV)) {
      string fromDate = "";   // Start date of the optimization interval
      string toDate = "";     // End date of the optimization interval

      // Create an entry for a new strategy group
      string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL)"
                                  " RETURNING rowid;",
                                  p_groupName, fromDate, toDate);
      ulong groupId = DB::Insert(query);

      // ...
   }
}

优化区间的开始日期和结束日期存储在数据库的 stages 表中。因此,我们可以通过在代码中的这一点执行相应的 SQL 查询来从那里获取它们。但事实证明,这种方法并不理想,因为我们已经实现了执行 SQL 查询以获取这些日期等信息的代码。这发生在自动优化 EA 中。它应该从数据库中接收有关下一个优化任务的信息。这些信息必须包括我们需要的日期。让我们好好利用这一点。 

我们需要通过将优化数据库的名称传递给其构造函数来创建 COptimizerTask 类的对象。它存在于 CTesterHandler::s_fileName 静态类字段中。另一个静态字段 CTesterHandler::s_idTask 包含当前优化任务 ID。我们将把它传递给加载优化问题数据的方法。之后,可以从任务对象的 m_params 结构的相应字段中获取所需的日期。

//+------------------------------------------------------------------+
//| Export an array of strategies to the specified EA database       |
//| as a new group of strategies                                     |
//+------------------------------------------------------------------+
void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) {
// Create an optimization task object
   COptimizerTask task(s_fileName);
// Load the data of the current optimization task into it
   task.Load(CTesterHandler::s_idTask);

// Connect to the required EA database
   if(DB::Connect(p_advFileName, DB_TYPE_ADV)) {
      string fromDate = task.m_params.from_date; // Start date of the optimization interval
      string toDate = task.m_params.to_date;     // End date of the optimization interval

      // Create an entry for a new strategy group
      string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL)"
                                  " RETURNING rowid;",
                                  p_groupName, fromDate, toDate);
      ulong groupId = DB::Insert(query);

      // ...
   }
}

让我们保存对库的 Virtual 子文件夹中的 TesterHandler.mqh 文件所做的更改。

让我们使用 CreateProject.ex5 EA 重新创建几个项目。为了加快进程,我们将优化间隔缩短(4 个月)。我们将把每个后续项目的优化间隔的开始和结束日期提前一个月。因此,我们得到以下结果:

如您所见,EA 数据库中的每个组现在都包含优化间隔的结束日期。请注意,此日期取自第三阶段任务的间隔。为了使一切正确,所有三个阶段的间隔日期应该相同。这是在项目创建 EA 中提供的。


修改最终 EA

在开始对最终 EA 中使用的策略组实施自动更新之前,让我们先来看看过渡到新的项目文件结构所带来的变化。如前所述,最终的 EA 现在以两个文件的形式呈现。主文件位于项目文件夹中,名为 SimpleVolumes.mq5 。以下是它的完整代码:

//+------------------------------------------------------------------+
//|                                                SimpleVolumes.mq5 |
//|                                 Copyright 2024-2025, Yuriy Bykov |
//|                            https://www.mql5.com/en/users/antekov |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024-2025, Yuriy Bykov"
#property link      "https://www.mql5.com/en/articles/16913"
#property description "The final EA, combining multiple instances of trading strategies:"
#property description " "
#property description "Strategies open a market or pending order when,"
#property description "the candle tick volume exceeds the average volume in the direction of the current candle."
#property description "If orders have not yet turned into positions, they are deleted at expiration time."
#property description "Open positions are closed only by SL or TP."
#property version "1.22"

#include <antekov/Advisor/Experts/Expert.mqh>

//+------------------------------------------------------------------+

这段代码实际上只有一个命令,用于导入最终 EA 的库文件。这正是“不存在的”比“存在”更重要的现象。让我们将其与第二个 HistoryReceiverExpert.mq5 EA 的代码进行比较:

//+------------------------------------------------------------------+
//|                                        HistoryReceiverExpert.mq5 |
//|                                 Copyright 2024-2025, Yuriy Bykov |
//|                            https://www.mql5.com/en/users/antekov |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024-2025, Yuriy Bykov"
#property link      "https://www.mql5.com/en/articles/16913"
#property description "The EA opens a market or pending order when,"
#property description "the candle tick volume exceeds the average volume in the direction of the current candle."
#property description "If orders have not yet turned into positions, they are deleted at expiration time."
#property description "Open positions are closed only by SL or TP."
#property version "1.01"

//+------------------------------------------------------------------+
//| Declare the name of the final EA.                                |
//| During the compilation, the function of generating               |
//| the initialization string from the current file will be used     |
//+------------------------------------------------------------------+
#define  __NAME__ MQLInfoString(MQL_PROGRAM_NAME)

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Testing the deal history"
input string historyFileName_    = "SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30]"
                                   " [10000, 34518, 1294, 3.75].history.csv";    // File with history

//+------------------------------------------------------------------+
//| Function for generating the strategy initialization string       |
//| from the inputs                                                  |
//+------------------------------------------------------------------+
string GetStrategyParams() {
   return StringFormat("class CHistoryStrategy(\"%s\")\n", historyFileName_);
}

#include <antekov/Advisor/Experts/Expert.mqh>

//+------------------------------------------------------------------+

SimpleVolumes.mq5 文件中不存在的三个块以颜色突出显示。在最终 EA 的 Experts/Expert.mqh 库文件中会考虑它们的存在:如果没有指定最终 EA 的名称常量,则会声明一个用于生成初始化字符串的函数,该函数将从 EA 的数据库中接收初始化字符串。如果指定了名称,则必须在包含库文件的父文件中声明此类函数。

// If the constant with the name of the final EA is not specified, then
#ifndef __NAME__
// Set it equal to the name of the EA file 
#define  __NAME__ MQLInfoString(MQL_PROGRAM_NAME)

//+------------------------------------------------------------------+
//| Function for generating the strategy initialization string       |
//| from the default inputs (if no name was specified).              | 
//| Import the initialization string from the EA database            |
//| by the strategy group ID                                         |
//+------------------------------------------------------------------+
string GetStrategyParams() {
// Take the initialization string from the new library for the selected group
// (from the EA database)
   string strategiesParams = CVirtualAdvisor::Import(
                                CVirtualAdvisor::FileName(__NAME__, magic_),
                                groupId_
                             );

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

   return strategiesParams;
}
#endif

接下来,在 Experts/Expert.mqh 库文件的 EA 初始化函数中,使用了初始化字符串生成函数的一个可能选项:

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

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

// Take the initialization string from the new library for the selected group
// (from the EA database)
   strategiesParams = GetStrategyParams();

// 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);
}

因此,如果需要,我们可以创建一个最终 EA,该 EA 不会从 EA 数据库加载初始化字符串。为此,请在 *.mq5 文件中声明 __NAME__ 常量和签名函数。 

string GetStrategyParams()

现在我们可以进行自动更新了。


自动更新

实现自动更新的第一个选项可能不是最漂亮的,但它可以作为一个开始。最重要的是它有效。对最终 EA 库文件所需的更改包括两个部分。

首先,我们稍微改变了输入参数的组成,从旧库中删除了带有组号的枚举,将其替换为 EA 数据库中的组 ID,并添加了一个启用自动更新的逻辑参数:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "::: Use a strategy group"
sinput int        groupId_       = 0;     // - ID of the group from the new library (0 - last)
sinput bool       useAutoUpdate_ = true;  // - Use auto update?

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

// ...

其次,我们在新代码块处理函数中,在突出显示的字符串之后添加了以下代码:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   expert.Tick();

// If both are executed at the same time:
   if(groupId_ == 0                       // - no specific group ID specified
         && useAutoUpdate_                // - auto update enabled
         && IsNewBar(Symbol(), PERIOD_D1) // - a new day has arrived
         && expert.CheckUpdate()          // - a new group of strategies discovered
     ) {
      // Save the current EA state
      expert.Save();

      // Delete the EA object
      delete expert;

      // Call the EA initialization function to load a new strategy group 
      OnInit();
   }
}

因此,只有当 groupId_=0useAutoUpdate_=true 时,自动更新才会生效。如果我们指定一个非零的组 ID,那么在整个测试期间都将使用该组。在这种情况下,最终 EA 何时可以进行交易没有限制。

启用自动更新后,生成的 EA 只会执行 EA 数据库中最早组的优化间隔结束日期之后的交易。该机制将在 CVirtualAdvisor::CheckUpdate() 类的新方法中实现:

//+------------------------------------------------------------------+
//| Check the presence of a new strategy group in the EA database    |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::CheckUpdate() {
// Request to get strategies of a given group or the last group
   string query = StringFormat("SELECT MAX(id_group) FROM strategy_groups"
                               " WHERE to_date <= '%s'",
                               TimeToString(TimeCurrent(), TIME_DATE));

// Open the EA database
   if(DB::Connect(m_fileName, DB_TYPE_ADV)) {
// Execute the request
      int request = DatabasePrepare(DB::Id(), query);

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

         // Read data from the first result string
         while(DatabaseReadBind(request, row)) {
            // Remember the strategy group ID
            // in the static property of the EA class
            return s_groupId < row.groupId;
         }
      } else {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
      }

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

   return false;
}

在本方法中,我们从 EA 数据库中获取最大的组 ID,使得优化间隔结束日期不大于当前日期。因此,即使数据库中已经存在一条记录,但其出现时间(>= 优化间隔的结束时间)相对于策略测试器的当前模拟时间而言是未来的,那么通过所使用的 SQL 查询将无法获得该记录。

初始化时,EA 会将加载的策略组的 ID 存储在 CVirtualAdvisor::s_groupId 类的静态字段中。因此,我们可以通过将刚从 EA 数据库中获得的 ID 与先前加载的组的 ID 进行比较来检测新组的出现。如果第一个更大,则说明出现了一个新的组。 

在从 EA 数据库获取初始化字符串的方法中(该方法已直接与数据库交互),我们对启用自动更新的组测试间隔的结束日期使用了相同的条件:

//+------------------------------------------------------------------+
//| Get the strategy group initialization string                     |
//| from the EA database with the given ID                           |
//+------------------------------------------------------------------+
string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) {
   string params[];   // Array for strategy initialization strings

// Request to get strategies of a given group or the last group
   string query = StringFormat("SELECT id_group, params "
                               "  FROM strategies"
                               " WHERE id_group = %s;",
                               (p_groupId > 0 ? (string) p_groupId 
                                : "(SELECT MAX(id_group) FROM strategy_groups WHERE to_date <= '"
                                + TimeToString(TimeCurrent(), TIME_DATE) +
                                "')"));

// Open the EA database
   if(DB::Connect(p_fileName, DB_TYPE_ADV)) {
      // ...
   }

// Strategy group initialization string
   string groupParams = NULL;

// Total number of strategies in the group
   int totalStrategies = ArraySize(params);

// If there are strategies, then
   if(totalStrategies > 0) {
      // Concatenate their initialization strings with commas
      JOIN(params, groupParams, ",");

      // Create a strategy group initialization string
      groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)",
                                 groupParams,
                                 totalStrategies);
   }

// Return the strategy group initialization string
   return groupParams;
}

最后值得一提的是,新增了一个在切换到新的策略组后加载 EA 状态的方法。关键在于,来自新组的新策略将无法在 EA 数据库中找到它们的设置,因为还没有为它们调用 Save() 方法,因此会报告下载错误。但这个错误应该忽略。

另一个新增方法与加载新策略后需要立即关闭旧策略的虚拟仓位有关。为此,只需为旧策略使用的所有交易品种创建交易品种接收器对象即可。这些对象将在下一个价格变动周期修正未平仓头寸的交易量。如果不这样做,那么只有当采用新策略开设虚拟仓位时,成交量调整才会发生。如果新策略不再使用之前使用过的某个交易品种,那么在该交易品种上的仓位将继续保持开放。

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;
   ulong groupId = 0;

// Load status if:
   if(true
// file exists
         && FileIsExist(m_fileName, FILE_COMMON)
// currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      // If the connection to the EA database is established
      if(CStorage::Connect(m_fileName)) {
         // If the last modified time is loaded and less than the current time
         if(CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime)
               && m_lastSaveTime <= TimeCurrent()) {

            PrintFormat(__FUNCTION__" | LAST SAVE at %s",
                        TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

            // If the saved strategy group ID is loaded
            if(CStorage::Get("CVirtualAdvisor::s_groupId", groupId)) {
               // Load all strategies ignoring possible errors
               FOREACH(m_strategies, {
                  res &= ((CVirtualStrategy*) m_strategies[i]).Load();
               });

               if(groupId != s_groupId) {
                  // Actions when launching an EA with a new group of strategies.
                  PrintFormat(__FUNCTION__" | UPDATE Group ID: %I64u -> %I64u", s_groupId, groupId);
                  
                  // Reset a possible error flag when loading strategies
                  res = true;
                  
                  string symbols[]; // Array for symbol names
                  
                  // Get the list of all symbols used by the previous group
                  CStorage::GetSymbols(symbols);
                  
                  // For all symbols, create a symbolic receiver.
                  // This is necessary for the correct closing of virtual positions 
                  // of the old strategy group immediately after loading the new one
                  FOREACH(symbols, m_receiver[symbols[i]]);
               }

               // ...
            }
         } else {
            // If the last modified time is not found or is in the future,
            // then start work from scratch
            PrintFormat(__FUNCTION__" | NO LAST SAVE [%s] - Clear Storage",
                        TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));
            CStorage::Clear();
            m_lastSaveTime = 0;
         }

         // Close the connection
         CStorage::Close();
      }
   }

   return res;
}

之后,您可以开始检查自动加载新组的功能。不幸的是,没有立即取得成功,因为有必要纠正出现的错误。例如,如果 EA 数据库突然变为空,EA 就会陷入无限循环。如果测试开始日期设定在第一批策略出现日期的前一天,则测试不会开始。最终,所有发现的错误都得到了纠正。

现在让我们看看使用创建的库作为一个整体的算法和自动更新检查的结果。


使用 Advisor 库的算法

这次我们将介绍使用 Advisor 库自动优化 SimpleVolumes 模型策略的算法,该策略是库的一部分,并在最终 EA 的测试器中启动。

  1. 将库设置到 Include(图 1)。

  2. 创建项目文件夹并将 EA 文件传输到其中(图 2)。

  3. 我们对第一阶段 EA 文件和项目创建 EA 文件进行了修改。使用模型策略时,无需进行任何更改,因为它们是最新的。编译项目文件夹中的所有 EA 文件。

  4. 启动项目创建 EA,设置所需的参数值(您可以保留默认值)。
    输出应该是一个优化数据库,其中填充了共享终端数据文件夹中项目的任务。您可以在项目描述中指定任何内容,例如优化间隔的开始和结束日期。目前,这只是创建一个启动输送机的任务。启动将由另一个 EA 执行。

  5. 如果需要,您可以重复上一步任意次数,并更改参数。例如,您可以同时创建多个项目,以便在不同的时间间隔内进行自动优化。

  6. 启动优化 EA 并等待。完成添加到优化数据库的所有项目所需的时间取决于项目的数量,以及项目中的交易品种和时间周期的数量、测试/优化时间间隔的持续时间和所实施交易策略的复杂性。这段时间也取决于参与优化的测试代理的数量。
    输出是一个共享文件夹中包含 EA 数据库的文件。它的名称取自设置。
    EA 数据库将包含已保存的策略组。

  7. 让我们发布最终版 EA。重要的是,它的名称和幻数必须与优化过程中指定的名称和幻数一致。否则,它将创建一个空的 EA 数据库,并等待其中出现内容。如果最终的 EA 找到了它的数据库,它会尝试加载具有指定 ID 的策略组,如果 ID 为 0,则加载最后添加的策略组。如果启用了自动更新选项,EA 将每天检查一次 EA 数据库中是否出现了按日期划分的新策略组。如果出现,它将替换先前使用的组。


测试自动更新

因此,在对数据库中所有不同完成日期的项目进行优化之后,我们将拥有一个包含多个不同组合策略组的 EA 数据库。它们在优化周期的结束日期上也存在差异。我们最终得到了一个 EA,随着测试的进行,当模拟的当前时间超过该新策略组的优化区间结束时间时,它可以从数据库中获取一组新的策略。

请注意,只有当 EA 在图表上启动或在可视化测试模式下运行时,保存和加载 EA 参数才有效。因此,要检查测试器中的自动更新,必须使用可视化模式。

让我们运行最终 EA,指定某个组 groupId_=1 。在这种情况下,无论 useAutoUpdate_ 参数值如何,都只会使用此组。它针对 2022 年 9 月 1 日至 2023 年 1 月 1 日的时间段进行了优化,因此我们将从 2022 年 9 月 1 日(主要期间)开始启动测试程序,并从 2023 年 1 月 1 日开始向前推演至 2024 年 1 月 1 日。

主要时期:2022年9月1日 至 2023年1月1日

前测期为 2023 年 1 月 1 日至 2024 年 1 月 1 日

图 3.最终 EA 模型的结果,参数 groupId_=1,时间区间为 2022.09.01 至 2024.01.01

从我们可以看到,EA 在主要时期(与优化区间重合)表现良好,但在前侧期,情况则完全不同。下跌幅度更大,净值曲线没有明显增长。当然,我希望看到更美的景象,但这个结果也在意料之中。毕竟,我们在优化过程中使用了非常小的时间间隔、很少的交易品种和很少的时间周期。因此,我们发现,在已知截面中,参数选择得对于这个特定的短截面来说太理想了。EA 在这样一个未知的领域无法以这种方式证明自己。

出于兴趣,我们来看看另一组交易策略是否也呈现出类似的模式。 让我们运行生成的 EA,并指定 groupId_=3 。该组针对 2022 年 11 月 1 日至 2023 年 3 月 1 日期间进行了优化,因此我们将从 2022 年 11 月 1 日(主要期间)开始启动测试程序,并从 2023 年 3 月 1 日开始,向前推演至 2024 年 1 月 1 日。

主要时期:2022年11月1日 至 2023年3月1日

前测期为 2023 年 3 月 1 日至 2024 年 1 月 1 日

图 4.最终 EA 模型的结果,参数 groupId_=3,时间区间为2022.11.01 至 2024.01.01

是的,结果与第一组相同。两个组在五六月份均出现大幅回撤。你可能会认为,对于该策略而言,这是一个不祥的时期。但是,如果我们选取一个在此范围内优化的组,我们会发现,该组中策略的相同参数也成功被选中。它展现了图表同样流畅优美的增长趋势。 

如果我们从 2023.01.01 开始运行最终 EA,参数为 groupId_=0useAutoUpdate=false ,那么我们将得到与第一组在远期期间相同的结果,因为在这种情况下,第一组将被加载(从通过开始日期起,它已经“存在”)。但是,由于自动更新功能已禁用,因此不会被稍后出现的群组所替换。

最后,让我们在 2023.01.01 至 2024.01.01 区间运行最终的 EA,启用自动更新,指定 groupId_=0useAutoUpdate=true

图 5.最终 EA 模型的结果,参数为 groupId_=0, useAutoUpdate=true,时间间隔为 2023.01.01 至 2024.01.01

交易结果本身并不重要,因为为了减少自动优化所需的时间,优化间隔非常短(仅 4 个月)。现在我们只想演示一下自动更新所用策略组的机制的功能。从日志记录和每月初自动平仓的情况来看,这确实符合预期: 

SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualReceiver::Get | OK, Strategy orders: 3 from 144 total
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualStrategyGroup::CVirtualStrategyGroup | Scale = 2.44, total strategies = 1
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualStrategyGroup::CVirtualStrategyGroup | Scale = 48.00, total groups = 48
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualStrategyGroup::CVirtualStrategyGroup | Scale = 1.00, total groups = 1
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualRiskManager::UpdateBaseLevels | DAILY UPDATE: Balance = 0.00 | Equity = 0.00 | Level = 0.00 | depoPart = 0.10 = 0.10 * 1.00 * 1.00
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualAdvisor::Load | LAST SAVE at 2023.01.31 20:32:00
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:00   CVirtualAdvisor::Load | UPDATE Group ID: 1 -> 2
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:00:59   CSymbolNewBarEvent::IsNewBar | Register new event handler for GBPUSD PERIOD_D1
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:01:00   CSymbolNewBarEvent::IsNewBar | Register new event handler for EURUSD PERIOD_D1
SimpleVolumes (GBPUSD,H1)       2023.02.01 00:01:00   CSymbolNewBarEvent::IsNewBar | Register new event handler for EURGBP PERIOD_D1


结论

让我们总结一下结果。我们最终将所有可重用代码以 Advisor 库的形式放在了 Include 文件夹中。现在,它将有可能连接到使用不同交易策略的项目。后续的库更新将自动分发到使用它的所有项目。

创建和启动自动优化项目变得越来越容易。我们现在已经简化了将优化结果应用到最终 EA 中的机制。只需在第三阶段优化的设置中指定所需的 EA 数据库名称,结果就会存储在最终 EA 可以检索到的位置。

然而,仍有相当多的问题需要注意。其中之一是开发一种算法,用于添加一种新型的交易策略,并在最终的 EA 中包含不同类型交易策略的组。下次再详细讨论。

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


重要警告

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


存档内容

#
 名称
版本  描述  最近修改
  MQL5/Experts/Article.16913   项目工作文件夹  
1 CreateProject.mq5 1.01
用于创建具有阶段、作业和优化任务的项目的 EA 脚本。
第 23 部分
2 HistoryReceiverExpert.mq5 1.01
用于与风险管理器回放交易历史的 EA
第 23 部分
3 Optimization.mq5
1.00 用于项目自动优化的 EA  第 23 部分
4 SimpleVolumesExpert.mq5
1.22 最终 EA 算法,用于并行运行多组模型策略。参数将从内置组库中获取。
第 23 部分
5 Stage1.mq5 1.22  交易策略单实例优化 EA(第一阶段)
第 23 部分
6 Stage2.mq5
1.00 交易策略实例组优化 EA(第二阶段)
第 23 部分
Stage3.mq5
1.00 EA 将生成的标准化策略组保存到具有给定名称的 EA 数据库中。
第 23 部分
  MQL5/Include/antekov/Advisor/Base
  其他项目类所继承的基类  
8 Advisor.mqh 1.04 EA 基类 第 10 部分
9 Factorable.mqh
1.05
从字符串创建的对象的基类
第 22 部分
10 Interface.mqh 1.01
可视化各种对象的基类
第 4 部分
11 Receiver.mqh
1.04  将未平仓交易量转换为市场仓位的基类
第 12 部分
12 Strategy.mqh
1.04
交易策略基类
第 10 部分
  MQL5/Include/antekov/Advisor/Database
  用于处理项目 EA 使用的所有类型数据库的文件  
13 Database.mqh 1.10 处理数据库的类 第 22 部分
14 db.adv.schema.sql 1.00
最终 EA 的数据库结构 第 22 部分
15 db.cut.schema.sql
1.00 截断优化数据库的结构
第 22 部分
16 db.opt.schema.sql
1.05  优化数据库结构
第 22 部分
17 Storage.mqh   1.01
用于处理 EA 数据库中最终 EA 的键值存储的类
第 23 部分
  MQL5/Include/antekov/Advisor/Experts
  包含不同类型已使用 EA 的公共部分的文件
 
18 Expert.mqh  1.22 最终 EA 的库文件。组参数可以从 EA 数据库中获取。
第 23 部分
19 Optimization.mqh  1.04 用于管理优化任务启动 EA 的库文件
第 23 部分
20 Stage1.mqh
1.19 单实例交易策略优化 EA(第一阶段)的库文件
第 23 部分
21 Stage2.mqh 1.04 用于优化一组交易策略实例的 EA 的库文件(第二阶段)   第 23 部分
22 Stage3.mqh
1.04 EA 库文件,用于将生成的标准化策略组保存到具有给定名称的 EA 数据库中。 第 23 部分
  MQL5/Include/antekov/Advisor/Optimization
  负责自动优化的类  
23 Optimizer.mqh
1.03  项目自动优化管理器类
第 22 部分
24 OptimizerTask.mqh
1.03
优化任务类
第 22 部分
  MQL5/Include/antekov/Advisor/Strategies    用于演示项目如何运作的交易策略示例
 
25 HistoryStrategy.mqh 
1.00 用于回放交易历史的交易策略类
第 16 部分
26 SimpleVolumesStrategy.mqh
1.11
使用分时交易量的交易策略类
第 22 部分
  MQL5/Include/antekov/Advisor/Utils
  辅助工具、用于代码简化的宏
 
27 ExpertHistory.mqh 1.00 用于将交易历史导出到文件的类 第 16 部分
28 Macros.mqh 1.05 用于数组操作的有用的宏 第 22 部分
29 NewBarEvent.mqh 1.00  用于定义特定交易品种的新柱形的类  第 8 部分
30 SymbolsMonitor.mqh  1.00 用于获取交易工具(交易品种)信息的类 第 21 部分
  MQL5/Include/antekov/Advisor/Virtual
  通过使用虚拟交易订单和头寸系统创建各种对象的类
 
31 Money.mqh 1.01  资金管理基类
第 12 部分
32 TesterHandler.mqh  1.07 优化事件处理类  第 23 部分
33 VirtualAdvisor.mqh  1.10  处理虚拟仓位(订单)的 EA 类 第 23 部分
34 VirtualChartOrder.mqh  1.01  图形虚拟仓位类 第 18 部分
35 VirtualFactory.mqh 1.04  对象工厂类  第 16 部分
36 VirtualHistoryAdvisor.mqh 1.00  交易历史回放 EA 类  第 16 部分
37 VirtualInterface.mqh  1.00  EA GUI 类  第 4 部分
38 VirtualOrder.mqh 1.09  虚拟订单和仓位类  第 22 部分
39 VirtualReceiver.mqh 1.04 将未平仓交易量转换为市场仓位的类(接收方)  第 23 部分
40 VirtualRiskManager.mqh  1.04 风险管理类(风险管理器)  第 23 部分
41 VirtualStrategy.mqh 1.09  具有虚拟仓位的交易策略类  第 23 部分
42 VirtualStrategyGroup.mqh  1.02  交易策略组类 第 23 部分
43 VirtualSymbolReceiver.mqh  1.00 交易品种接收器类  第 3 部分
  MQL5/Common/Files   共享终端文件夹   
44 SimpleVolumes-27183.test.db.sqlite 添加了策略组的 EA 数据库  

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

附加的文件 |
MQL5.zip (421.44 KB)
MQL5中表格模型的实现:应用MVC概念 MQL5中表格模型的实现:应用MVC概念
在本文中,我们将探讨如何使用MVC(模型-视图-控制器)架构模式在MQL5中开发表格模型,该模式可将数据逻辑、展示和控制进行分离,从而实现结构化、灵活且可扩展的代码。我们将考虑实现用于构建表格模型的各类,包括使用链表来存储数据。
开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡 开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡
如果要自动进行周期性优化,我们需要考虑自动更新交易账户上已经运行的 EA 设置。这样一来,我们就可以在策略测试器中运行 EA,并在单次运行中更改其设置。
从新手到专家:对K线进行编程 从新手到专家:对K线进行编程
在本文中,我们将迈出 MQL5 编程的第一步,即使是完全零基础的初学者也能上手。我们将向您展示,如何将熟悉的 K线形态 转换为一个功能完备的自定义指标。K线形态之所以有价值,是因为它们反映了真实的价格行为,并预示着市场的转变。与其手动扫描图表——这种方法容易出错且效率低下——我们将讨论如何通过一个指标来自动化这个过程,该指标会自动识别并标记出这些形态。在此过程中,我们将探讨一些关键概念,例如索引、时间序列、平均真实波幅(用于在多变的市场波动性中提高准确性),以及如何开发一个可自定义、可复用的 K线形态库,以便在未来的项目中使用。
交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(终篇) 交易中的神经网络:针对加密货币市场的记忆扩充上下文感知学习(终篇)
针对加密货币交易的 MacroHFT 框架采用上下文感知强化学习和记忆,以便适应动态市场条件。在本文末尾,我们将在真实历史数据上测试所实现的方式,从而评估其有效性。