
开发多币种 EA 交易(第 9 部分):收集单一交易策略实例的优化结果
概述
在前面的文章中,我们已经实现了很多有趣的功能。我们可以在 EA 中实现一个或多个交易策略。此外,我们还开发了在单个 EA 中连接多个交易策略实例的结构,增加了管理最大允许回撤的工具,研究了自动选择策略参数集的可行方法,以确保其在组中发挥最佳作用,学习了如何从策略实例组,甚至从不同的策略实例组中组建一个 EA。但是,如果我们能够将这些成果结合起来,已经取得的成果的价值还能大大提高。
让我们试着在文章框架内勾勒出一个总体结构:输入是单个的交易策略,而输出则是一个完成好的 EA,它使用的是经过挑选和分组的原始交易策略副本,能提供最佳的交易结果。
在绘制了粗略的路线图之后,让我们仔细观察其中的某些部分,分析我们需要什么来实现选定的阶段,并着手实际实现。
主要阶段
让我们列出开发 EA 时必须经历的主要阶段:
- 实现交易策略。我们开发的类派生于 CVirtualStrategy,它实现了开启、维护和关闭虚拟仓位和订单的交易逻辑。我们在本系列文章的前四部分中已经做到了这一点。
- 优化交易策略。我们为交易策略挑选出一套好的输入参数,并展示出值得注意的结果。如果没有找到,则返回第 1 点。
通常情况下,我们在一个交易品种和时间框架上进行优化更为方便。对于遗传优化,我们很可能需要使用不同的优化标准(包括我们自己的一些标准)运行多次。只有在参数数量非常少的策略中,才有可能使用暴力优化。即使在我们的模型策略中,穷举搜索也过于昂贵。因此,在谈到优化时,我将进一步讨论 MetaTrader 5 策略测试器中的遗传优化。文章中没有详细描述优化过程,因为这是一个非常标准的过程。 - 参数集聚类。这一步不是强制性的,但它将可以为下一步节省一些时间。在此,我们将大幅减少交易策略实例参数集的数量,并从中选择合适的组。在第六部分中对此进行了描述。
- 选择参数集组。根据前一阶段的结果,进行优化选择
能产生最佳结果的交易策略实例的最适合的参数集。这也主要在第六和第七部分中有所描述。 - 从参数集组中选择组。现在,使用与合并单个实例参数集时相同的原则,将上一阶段的结果合并成组。
- 在交易品种和时间框架中进行迭代。对所有需要的交易品种和时间框架中重复步骤 2 到 5。或许,除了交易品种和时间框架外,还可以对某些交易策略的某些其他输入类别进行单独优化。
- 其他策略。如果您还有其他交易策略,那么对每个策略都重复步骤 1 到 6。
- 组装 EA。我们会将针对不同交易策略、交易品种、时间框架和其他参数找到的所有最佳组群收集到一个最终的 EA 中。
每个阶段完成后都会生成一些数据,这些数据需要保存并用于下一阶段。到目前为止,我们使用的都是临时凑合的手段,用一两次足够方便,但反复使用就不太方便了。
例如,我们将第二阶段后的优化结果保存在 Excel 文件中,然后手动添加缺失的列,并将其保存为 CSV 文件,以用于第三阶段。
我们或者直接使用策略测试器界面第三阶段的结果,或者将其保存到 Excel 文件中,在其中进行一些处理,然后再次使用测试器界面获得的结果。
我们并没有实际开展第五阶段的工作,只是注意到开展这一工作的可能性。因此,它从未实现。
对于所有这些接收到的数据,我们希望采用单一的存储和使用结构。
实现选项
从本质上讲,我们需要存储和使用的主要类型的数据是多个 EA 的优化结果。如您所知,策略测试器会将所有优化结果记录在以 *.opt 为扩展名的独立缓存文件中,然后可以在测试器中重新打开,甚至可以在另一个 MetaTrader 5 终端的测试器中打开。文件名是根据优化后的 EA 名称和优化参数计算出的哈希值来决定的。这样,在优化提前中断或改变优化标准后继续优化时,就不会丢失已完成的通过的信息。
因此,正在考虑的方案之一是使用优化缓存文件来存储中间结果。fxsaber提供了一个很好的库,允许我们访问 MQL5 程序中保存的所有信息。
但是,随着优化次数的增加,包含优化结果的文件数量也会增加。为了避免混淆,我们将需要为这些缓存文件的存储和处理提供一些额外的结构。如果优化不是在一台服务器上进行的,那么就有必要实现同步或将所有缓存文件存储在一个地方。此外,在下一阶段,我们还需要进行一些处理,以便在下一阶段将获得的优化结果导出到 EA 中。
然后,我们来看看如何在数据库中处理存储所有结果。乍一看,这需要大量时间来实现。但是,这项工作可以分成几个较小的阶段,我们将能够立即使用其成果,而无需等待全面实现。这种方法还允许更自由地选择最方便的方式对存储结果进行中间处理。例如,我们可以将一些处理分配给简单的 SQL 查询,将一些处理分配给 MQL5 中的计算,将一些处理分配给 Python 或 R 程序。我们可以尝试不同的处理选项,并选择最合适的选项。
MQL5 提供了用于处理 SQLite 数据库的内置函数。此外,还有第三方库的实现,例如可以与 MySQL 协同工作。目前还不清楚 SQLite 的功能是否足以满足我们的需要,但该数据库很可能足以满足我们的需要。如果还不够,我们就会考虑迁移到另一个 DBMS。
开始设计数据库
首先,我们需要确定要存储其信息的实体。当然,一次测试运行就是其中之一。该实体的字段包括测试输入数据字段和测试结果字段。一般来说,它们可以被区分为独立的实体。输入数据本质上可以细分为更小的实体:EA、优化设置和 EA 单程参数。但是,让我们继续以最少行动原则为指导。首先,我们只需使用一个表,其中包含我们在以前的文章中使用过的通过结果字段,以及一到两个用于放置有关通过输入的必要信息的文本字段即可。
可以通过以下 SQL 查询创建这样一个表:
CREATE TABLE passes ( id INTEGER PRIMARY KEY AUTOINCREMENT, pass INT, -- pass index inputs TEXT, -- pass input values params TEXT, -- additional pass data initial_deposit REAL, -- pass results... withdrawal REAL, profit REAL, gross_profit REAL, gross_loss REAL, max_profittrade REAL, max_losstrade REAL, conprofitmax REAL, conprofitmax_trades REAL, max_conwins REAL, max_conprofit_trades REAL, conlossmax REAL, conlossmax_trades REAL, max_conlosses REAL, max_conloss_trades REAL, balancemin REAL, balance_dd REAL, balancedd_percent REAL, balance_ddrel_percent REAL, balance_dd_relative REAL, equitymin REAL, equity_dd REAL, equitydd_percent REAL, equity_ddrel_percent REAL, equity_dd_relative REAL, expected_payoff REAL, profit_factor REAL, recovery_factor REAL, sharpe_ratio REAL, min_marginlevel REAL, deals REAL, trades REAL, profit_trades REAL, loss_trades REAL, short_trades REAL, long_trades REAL, profit_shorttrades REAL, profit_longtrades REAL, profittrades_avgcon REAL, losstrades_avgcon REAL, complex_criterion REAL, custom_ontester REAL, pass_date DATETIME DEFAULT (datetime('now') ) NOT NULL );
让我们创建辅助 CDatabase 类,它将包含用于处理数据库的方法。我们可以将其设置为静态,因为在一个程序中不需要很多实例,一个就足够了。由于我们目前计划将所有信息集中到一个数据库中,因此我们可以在源代码中固定指定数据库文件名。
该类将包含 s_db 字段,用于存储打开的数据库句柄。Open() 数据库打开方法将设置其值。如果数据库在打开时尚未创建,则将通过调用 Create() 方法来创建。打开后,我们可以使用 Execute() 方法向数据库执行单个 SQL 查询,或使用 ExecuteTransaction() 方法在单个事务中执行批量 SQL 查询。最后,我们将使用 Close() 方法关闭数据库。
我们还可以声明一个短宏,用较短的 DB 代替较长的 CDatabase 类名。
#define DB CDatabase //+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { static int s_db; // DB connection handle static string s_fileName; // DB file name public: static bool IsOpen(); // Is the DB open? static void Create(); // Create an empty DB static void Open(); // Opening DB static void Close(); // Closing DB // Execute one query to the DB static bool Execute(string &query); // Execute multiple DB queries in one transaction static bool ExecuteTransaction(string &queries[]); }; int CDatabase::s_db = INVALID_HANDLE; string CDatabase::s_fileName = "database.sqlite";
在数据库创建方法中,我们只需创建一个包含用于创建表的 SQL 查询的数组,并在一个事务中执行这些查询:
//+------------------------------------------------------------------+ //| Create an empty DB | //+------------------------------------------------------------------+ void CDatabase::Create() { // Array of DB creation requests string queries[] = { "DROP TABLE IF EXISTS passes;", "CREATE TABLE passes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "pass INT," "inputs TEXT," "params TEXT," "initial_deposit REAL," "withdrawal REAL," "profit REAL," "gross_profit REAL," "gross_loss REAL," ... "pass_date DATETIME DEFAULT (datetime('now') ) NOT NULL" ");" , }; // Execute all requests ExecuteTransaction(queries); }
在打开数据库方法中,我们将首先尝试打开一个现有的数据库文件。如果它不存在,我们就创建再打开它,然后调用 Create() 方法创建数据库结构:
//+------------------------------------------------------------------+ //| Is the DB open? | //+------------------------------------------------------------------+ bool CDatabase::IsOpen() { return (s_db != INVALID_HANDLE); } ... //+------------------------------------------------------------------+ //| Open DB | //+------------------------------------------------------------------+ void CDatabase::Open() { // Try to open an existing DB file s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_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 | DATABASE_OPEN_COMMON); // Report an error in case of failure if(!IsOpen()) { PrintFormat(__FUNCTION__" | ERROR: %s open failed with code %d", s_fileName, GetLastError()); return; } // Create the database structure Create(); } PrintFormat(__FUNCTION__" | Database %s opened successfully", s_fileName); }
在执行多个 ExecuteTransaction() 查询的方法中,我们创建一个事务,然后开始在一个循环中逐个执行所有 SQL 查询。如果在执行下一个请求时出现错误,我们会中断循环,报告错误,并取消该事务中之前的所有请求。如果没有错误发生,则确认事务:
//+------------------------------------------------------------------+ //| Execute multiple DB queries in one transaction | //+------------------------------------------------------------------+ bool CDatabase::ExecuteTransaction(string &queries[]) { // Open a transaction DatabaseTransactionBegin(s_db); bool res = true; // Send all execution requests FOREACH(queries, { res &= Execute(queries[i]); if(!res) break; }); // If an error occurred in any request, then if(!res) { // Report it PrintFormat(__FUNCTION__" | ERROR: Transaction failed, error code=%d", GetLastError()); // Cancel transaction DatabaseTransactionRollback(s_db); } else { // Otherwise, confirm transaction DatabaseTransactionCommit(s_db); PrintFormat(__FUNCTION__" | Transaction done successfully"); } return res; }
将更改保存到当前文件夹的 Database.mqh 文件中。
修改 EA 以收集优化数据
在优化过程中只使用本地计算机上的代理时,我们可以安排在 OnTester() 或 OnDeinit() 处理函数中将传递结果保存到数据库。在本地网络或 MQL5 云网络中使用代理时,即使可能,也很难实现保存结果。幸运的是,MQL5 通过创建、发送和接收数据帧,提供了从测试代理(无论它们在哪里)获取任何信息的绝佳标准方法。
参考文献和 AlgoBook 中对这一机制有足够详细的描述。为了使用它,我们需要在优化后的程序中添加三个额外的事件处理函数:OnTesterInit()、OnTesterPass() 和 OnTesterDeinit()。
优化总是从某个 MetaTrader 5 终端启动,此后我们有条件地将其称为主终端。从主终端启动带有此类处理程序的 EA 进行优化时,会在主终端打开一个新图表,并在该图表上启动另一个 EA 实例,然后再将 EA 实例分配给测试代理,以便使用不同的参数集执行正常的优化过程。
该实例以特殊模式启动:不执行标准的 OnInit()、OnTick() 和 OnDeinit() 处理函数。只执行这三个新的处理函数。这种模式甚至有自己的名字 - 优化结果帧收集模式。如有必要,我们可以在 EA 功能中通过调用 MQLInfoInteger() 函数来检查 EA 是否以这种模式运行:
// Check if the EA is running in data frame collection mode bool isFrameMode = MQLInfoInteger(MQL_FRAME_MODE);
顾名思义,在帧收集模式下,OnTesterInit() 处理函数会在优化前运行一次,OnTesterPass() 处理函数会在每个测试代理完成其测试通过后运行一次,而OnTesterDeinit() 处理函数则会在所有预定的优化测试通过完成后或优化中断时运行一次。
在主终端图上以帧收集模式启动的 EA 实例将负责收集所有测试代理的数据帧。"数据帧" 只是在描述测试代理与主终端中的 EA 之间的数据交换时使用的一个方便的名称。它表示测试代理创建的数据集,带有名称和数字 ID,并在完成一次优化后发送到主终端。
需要注意的是,只在测试代理上以正常模式运行的 EA 实例中创建数据帧,而只在主终端上以帧收集模式运行的 EA 实例中收集和处理数据帧是合理的。那么,让我们从创建帧开始吧。
我们可以将在 EA 中创建帧的操作放在 OnTester() 处理函数或从 OnTester() 调用的任何函数或方法中。处理函数在通过完成后启动,我们可以在其中获取已完成通过的所有统计特征值,并在必要时计算用户标准值,以评估通过结果。
我们目前的代码可以计算自定义标准,显示在最大可实现回撤 10% 的情况下可获得的预测利润:
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = fixedBalance_ * 0.1 / balanceDrawdown; // Recalculate the profit double fittedProfit = profit * coeff; return fittedProfit; }
让我们把这段代码从 SimpleVolumesExpertSingle.mq5 EA 文件移到新的 CVirtualAdvisor 方法类中,而 EA 只负责返回方法调用结果:
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { return expert.Tester(); }
在移动时,我们应该考虑到,我们不能再在方法内部使用 fixedBalance_ 变量,因为它可能不存在于另一个 EA 中。但它的值可以通过调用 CMoney::FixedBalance() 方法从 CMoney 静态类中获取。在此过程中,我们将对用户标准的计算方法再做一次修改。在确定预计利润后,我们将重新计算每单位时间的利润,例如每年的利润。这样,我们就可以大致比较不同时间段的通过结果。
为此,我们需要记住 EA 中的测试开始日期。让我们添加新属性 m_fromDate,用于在 EA 对象构造函数中存储当前时间。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... datetime m_fromDate; public: ... virtual double Tester() override; // OnTester event handler ... }; //+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown; // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ; // Perform data frame generation on the test agent CTesterHandler::Tester(fittedProfit, ~((CVirtualStrategy *) m_strategies[0])); return fittedProfit; }
之后,我们可能会制定几个自定义优化标准,然后将这些代码再次移动到新的位置。但现在,我们还是不要分心去研究优化 EA 的各种适配函数这一广泛的话题,让代码保持原样吧。
SimpleVolumesExpertSingle.mq5 EA 文件现在有了新的处理函数 OnTesterInit()、OnTesterPass() 和 OnTesterDeinit()。根据我们的计划,这些功能的逻辑应该对所有 EA 都是一样的,因此我们将首先把它们的实现降低到 EA 级别(CVirtualAdvisor 类对象)。
需要注意的是,当 EA 以帧收集模式在主终端启动时,创建 EA 实例的 OnInit() 函数将不会被执行。因此,为了避免将 EA 实例的创建/删除添加到新的处理程序中,应在 CVirtualAdvisor 类中设置静态方法来处理这些事件。然后,我们需要在 EA 中添加以下代码:
//+------------------------------------------------------------------+ //| Initialization before starting optimization | //+------------------------------------------------------------------+ int OnTesterInit(void) { return CVirtualAdvisor::TesterInit(); } //+------------------------------------------------------------------+ //| Actions after completing the next optimization pass | //+------------------------------------------------------------------+ void OnTesterPass() { CVirtualAdvisor::TesterPass(); } //+------------------------------------------------------------------+ //| Actions after optimization is complete | //+------------------------------------------------------------------+ void OnTesterDeinit(void) { CVirtualAdvisor::TesterDeinit(); }
我们将来可以做的另一个改动是,在 EA 创建后,不再单独调用 CVirtualAdvisor::Add() 方法为其添加交易策略。相反,我们会立即将有关策略的信息传输给 EA 的构造函数,而它自己会调用 Add() 方法。然后就可以从公共部分删除该方法。
采用这种方法后,OnInit() EA 初始化函数将如下所示:
int OnInit() { CMoney::FixedBalance(fixedBalance_); // Create an EA handling virtual positions expert = new CVirtualAdvisor( new CSimpleVolumesStrategy( symbol_, timeframe_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_, 0), // One strategy instance magic_, "SimpleVolumesSingle", true); return(INIT_SUCCEEDED); }
将更改保存到当前文件夹的 SimpleVolumesExpertSingle.mq5 文件中。
修改 EA 类
为了避免重载 CVirtualAdvisor EA 类,我们将把 TesterInit、TesterPass 和 OnTesterDeinit 事件处理函数的代码移到单独的 CTesterHandler 类中,并在其中创建静态方法来处理这些事件。在这种情况下,我们需要在 CVirtualAdvisor 类中添加与主 EA 文件中大致相同的代码:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { ... public: ... static int TesterInit(); // OnTesterInit event handler static void TesterPass(); // OnTesterDeinit event handler static void TesterDeinit(); // OnTesterDeinit event handler }; //+------------------------------------------------------------------+ //| Initialization before starting optimization | //+------------------------------------------------------------------+ int CVirtualAdvisor::TesterInit() { return CTesterHandler::TesterInit(); } //+------------------------------------------------------------------+ //| Actions after completing the next optimization pass | //+------------------------------------------------------------------+ void CVirtualAdvisor::TesterPass() { CTesterHandler::TesterPass(); } //+------------------------------------------------------------------+ //| Actions after optimization is complete | //+------------------------------------------------------------------+ void CVirtualAdvisor::TesterDeinit() { CTesterHandler::TesterDeinit(); }
我们还要对 EA 对象构造函数代码做一些补充。考虑到未来的改进,将构造函数中的所有操作移至新的 Init() 初始化方法中。这样,我们就可以添加多个带有不同参数集的构造函数,在对参数稍作预处理后,这些构造函数都将使用相同的初始化方法。
让我们添加构造函数,其第一个参数要么是策略对象,要么是策略组对象。然后,我们可以直接在构造函数中为 EA 添加策略。在这种情况下,我们不再需要在 OnInit() EA 函数中调用 Add() 方法。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... datetime m_fromDate; public: CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ); ... }; ... //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { CVirtualStrategy *strategies[] = {p_strategy}; Init(new CVirtualStrategyGroup(strategies), p_magic, p_name, p_useOnlyNewBar); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { Init(p_group, p_magic, p_name, p_useOnlyNewBar); }; //+------------------------------------------------------------------+ //| EA initialization method | //+------------------------------------------------------------------+ void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false ) { // Initialize the receiver with a static receiver m_receiver = CVirtualReceiver::Instance(p_magic); // Initialize the interface with the static interface m_interface = CVirtualInterface::Instance(p_magic); m_lastSaveTime = 0; m_useOnlyNewBar = p_useOnlyNewBar; m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); m_fromDate = TimeCurrent(); Add(p_group); delete p_group; };
将更改保存到当前文件夹的 VirtualExpert.mqh 中。
优化事件处理类
现在,让我们直接关注开始前、通过完成后和优化完成后所执行操作的执行情况。我们将创建 CTesterHandler 类,并为其添加用于处理必要事件的方法,以及放在类的封闭部分的几个辅助方法:
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { static string s_fileName; // File name for writing frame data static void ProcessFrames(); // Handle incoming frames static string GetFrameInputs(ulong pass); // Get pass inputs public: static int TesterInit(); // Handle the optimization start in the main terminal static void TesterDeinit(); // Handle the optimization completion in the main terminal static void TesterPass(); // Handle the completion of a pass on an agent in the main terminal static void Tester(const double OnTesterValue, const string params); // Handle completion of tester pass for agent }; string CTesterHandler::s_fileName = "data.bin"; // File name for writing frame data
主终端的事件处理程序看起来非常简单,因为我们将把主要代码移到辅助函数中:
//+------------------------------------------------------------------+ //| Handling the optimization start in the main terminal | //+------------------------------------------------------------------+ int CTesterHandler::TesterInit(void) { // Open / create a database DB::Open(); // If failed to open it, we do not start optimization if(!DB::IsOpen()) { return INIT_FAILED; } // Close a successfully opened database DB::Close(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Handling the optimization completion in the main terminal | //+------------------------------------------------------------------+ void CTesterHandler::TesterDeinit(void) { // Handle the latest data frames received from agents ProcessFrames(); // Close the chart with the EA running in frame collection mode ChartClose(); } //+--------------------------------------------------------------------+ //| Handling the completion of a pass on an agent in the main terminal | //+--------------------------------------------------------------------+ void CTesterHandler::TesterPass(void) { // Handle data frames received from the agent ProcessFrames(); }
完成一次通过后执行的操作将有两个版本:
- 用于试验代理。通过后,将在那里收集必要的信息,并创建一个数据帧发送到主终端。这些操作将被收集到 Tester() 事件处理函数中。
- 用于主终端。在这里,我们可以接收来自测试代理的数据帧,解析帧中接收到的信息并将其输入数据库。这些操作将在 TesterPass() 处理函数中收集。
为测试代理生成数据帧应在 EA 中执行,即在 OnTester 处理函数中执行。由于我们将其代码移到了 EA 对象级别(CVirtualAdvisor 类),因此需要在此处添加 CTesterHandler::Tester() 方法。我们将把新计算出的自定义优化标准值和描述优化 EA 中使用的策略参数的字符串作为方法参数传递。为了形成这样一个字符串,我们将使用已经创建的 ~(波浪号)来表示 CVirtualStrategy 类对象。
//+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown; // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ; // Perform data frame generation on the test agent CTesterHandler::Tester(fittedProfit, ~((CVirtualStrategy *) m_strategies[0])); return fittedProfit; }
在 CTesterHandler::Tester() 方法中,遍历可用统计特征的所有可能名称,获取它们的值,将其转换为字符串,并将这些字符串添加到 stats 数组中。为什么需要将实数特征转换为字符串?只有这样,它们才能与策略参数的字符串描述一起在一个帧中传递。在一帧中,我们可以传递一个简单类型的数值数组(字符串不适用),或者一个包含任何数据的预创建文件。因此,为了避免发送两个不同帧(一个包含数字,另一个包含来自文件的字符串)的麻烦,我们将把所有数据转换成字符串,写入一个文件,并在一个帧中发送其内容:
//+------------------------------------------------------------------+ //| Handling completion of tester pass for agent | //+------------------------------------------------------------------+ void CTesterHandler::Tester(double custom, // Custom criteria string params // Description of EA parameters in the current pass ) { // Array of names of saved statistical characteristics of the pass ENUM_STATISTICS statNames[] = { STAT_INITIAL_DEPOSIT, STAT_WITHDRAWAL, STAT_PROFIT, ... }; // Array for values of statistical characteristics of the pass as strings string stats[]; ArrayResize(stats, ArraySize(statNames)); // Fill the array of values of statistical characteristics of the pass FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2)); // Add the custom criterion value to it APPEND(stats, DoubleToString(custom, 2)); // Screen the quotes in the description of parameters just in case StringReplace(params, "'", "\\'"); // Open the file to write data for the frame int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI); // Write statistical characteristics FOREACH(stats, FileWriteString(f, stats[i] + ",")); // Write a description of the EA parameters FileWriteString(f, StringFormat("'%s'", params)); // 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()); } }
最后,让我们考虑一种辅助方法,它将接受数据帧并将其中的信息保存到数据库中。在这种方法中,我们会循环接收当前尚未处理的所有传入帧。我们从每个帧中获取字符数组形式的数据,并将其转换为字符串。接下来,我们用给定索引的传递参数的名称和值组成一个字符串。我们使用获得的值形成 SQL 查询,在数据库的 passes 表中插入新行。将创建的 SQL 查询添加到 SQL 查询数组中。
以这种方式处理完当前接收到的所有数据帧后,我们将在一个事务中执行数组中的所有 SQL 查询。
//+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { // Open the database DB::Open(); // Variables for reading data from frames string name; // Frame name (not used) ulong pass; // Frame pass index long id; // Frame type ID (not used) double value; // Single frame value (not used) uchar data[]; // Frame data array as a character array string values; // Frame data as a string string inputs; // String with names and values of pass parameters string query; // A single SQL query string string queries[]; // SQL queries for adding records to the database // 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, %s,\n'%s',\n'%s');", 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(); }
GetFrameInputs() 辅助方法用于生成包含输入变量名称和值的字符串,该方法取自 AlgoBook,并根据我们的需要稍作补充。
将获得的代码保存到当前文件夹下的 TesterHandler.mqh 文件中。
检查运行
为了测试其功能,让我们使用少量参数在相对较短的时间内进行优化。优化过程完成后,我们可以在策略测试器和创建的数据库中查看结果。
图 1.策略测试器中的优化结果
图 2.数据库中的优化结果
我们可以看到,数据库中的结果与测试器中的结果相吻合:在按照用户标准进行排序的情况下,我们在两种情况下都观察到了相同的利润值序列。最佳通行证报告称,初始存款为 10,000 美元,最大可能回撤为初始存款的 10%(1,000 美元),一年内预期利润可能超过 5,000 美元。不过,目前我们对优化结果的定量特征并不感兴趣,我们感兴趣的是这些结果现在可以存储在数据库中。
结论
这样,我们离目标就又近了一步。我们设法将 EA 参数的优化结果保存到我们的数据库中。这样,我们就为进一步自动实现 EA 开发的第二阶段奠定了基础。
还有很多隐藏的问题,许多事情不得不推迟到将来,因为实现这些计划需要大量精力。但是,在取得目前的成果之后,我们可以更加明确地制定项目进一步发展的方向。
目前,所实现的保存功能仅适用于一个优化过程,即我们保存了通过的相关信息,但仍难以从中提取与一个优化过程相关的字符串组。为此,我们需要对数据库结构进行修改,而现在这一切都变得异常简单。今后,我们将尝试自动启动多个连续的优化过程,并为待优化的参数初步分配不同的选项。
感谢您的关注!期待很快与您见面!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14680



