English Русский Español Deutsch 日本語 Português
preview
开发多币种 EA 交易系统(第 15 部分):为真实交易准备 EA

开发多币种 EA 交易系统(第 15 部分):为真实交易准备 EA

MetaTrader 5测试者 | 10 二月 2025, 10:01
445 0
Yuriy Bykov
Yuriy Bykov

概述

在前几篇文章中,我们已经取得了一定的成果,但仍有许多工作要做。我们希望看到的最终结果是一个多币种 EA,它可以设置为在一个真实账户或多个真实账户上使用不同的经纪商。到目前为止,我们的工作重点是在测试过程中取得良好的交易结果,因为如果做不到这一点,就不可能在真实账户上使用开发好的 EA 实现良好的交易。既然我们已经有了大致不错的测试结果,那么我们就可以稍微研究一下如何确保在真实账户上的操作正确。

我们已经部分地谈到了 EA 在这方面的开发。特别是,风险管理器的开发是确保遵守真实交易过程中可能出现的要求的一个步骤。测试交易思路并不需要风险管理器,因为它只是一个重要的辅助工具。

在本文的框架内,我们将尝试提供其他重要机制,没有这些机制,就不建议开始真实账户交易。由于这些将是在策略测试器中运行 EA 时不会发生的情况,因此我们很可能不得不开发额外的 EA 来调试它们并检查其操作的有效性。


绘制路径图 

在真实账户交易时,有很多细微差别需要考虑和注意。现在让我们关注其中的几个,如下所示:

  • 交易品种替换:我们进行了优化,并使用非常具体的交易工具名称(交易品种)形成了 EA 的初始化字符串。但在真实账户中,交易工具的名称可能与我们使用的不同。可能的差异包括名称中的后缀或前缀(EURGBP.x 或 xEURGBP,而不是 EURGBP),或使用不同的大小写(eurgbp,而不是 EURGBP)。今后,交易品种列表可能会扩大,以包括那些名称差异更大的交易品种。因此,有必要设定替换交易工具名称的规则,以便 EA 能够在特定经纪商使用的交易品种上运行。

  • 交易完成模式:由于我们计划定期更新 EA 内部同时运行的交易策略实例的组成和设置,因此最好能提供将已运行的 EA 切换到特殊模式的功能,在该模式下,EA 将 "仅为平仓" 而运行,也就是说,它将努力通过平仓(最好是获得总利润)所有未平仓位来完成交易。如果我们决定在未平仓位出现亏损时停止使用该 EA 进行交易,这可能需要一些时间。

  • 重启后恢复:这意味着 EA 在终端重启后继续工作的能力,重启的原因有很多。其中一些原因是不可能有保证的。然而,EA 不应该只是继续工作,而应该像 EA 在没有重启的情况下工作一样。因此,有必要确保 EA 在操作过程中保存所有必要的信息,这将使其在重启后恢复状态。

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


交易品种替换

让我们从最简单的事情开始- 在 EA 设置中设置替换交易品种名称的规则。通常情况下,差异在于是否存在额外的后缀和/或前缀。因此,乍一看,我们可以为输入参数中添加两个新参数,用于设置后缀和前缀。

不过,这种方法的灵活性较差,因为它意味着只能使用一种固定算法,从初始化字符串的原始名称中获取正确的交易品种名称。此外,转换为小写将需要一个额外的参数。因此,我们将采用另一种方法。

我们将在 EA 中添加一个参数,该参数将包含这样的字符串:

<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>

这里,<Symbol[i]> 代表初始化字符串中使用的交易品种的原始名称,而<TargetSymbol[i]> 代表将用于实际交易的交易品种的目标名称。例如:

EURGBP=EURGBPx;EURUSD=EURUSDx;GBPUSD=GBPUSDx

我们将把该参数的值传递给 EA 对象(CVirtualAdvisor 类)的一个特殊方法,该方法将执行所有必要的进一步操作。如果向该方法传递的是空字符串,则无需更改交易品种的名称。

让我们调用 SymbolsReplace 方法,并将调用该方法添加到 EA 初始化函数代码中:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input string   symbolsReplace_   = "";       // - Symbol replacement rules


datetime fromDate = TimeCurrent();


CVirtualAdvisor     *expert;             // EA object

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

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

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;

// Successful initialization
   return(INIT_SUCCEEDED);
}

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


让我们在 EA 类及其实现中添加对执行交易品种名称替换的 EA 方法的描述。在该方法中,我们将把传入的替换字符串解析为各个部分,先用分号 ";" 分隔,再用等号 "=" 分隔。我们将从所获得的部分中形成一个词汇表,将源交易品种的名称与目标交易品种的名称联系起来。然后,我们将依次把词汇表传递给每个交易策略实例,这样,如果它们的交易品种作为键出现在词汇表中,它们就可以执行必要的替换。

在可能发生错误的每一步,我们都会更新结果变量,以便上游函数知道交易品种名称替换是否失败。在这种情况下,EA 会报告初始化失败。

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

public:
   ...

   bool SymbolsReplace(const string p_symbolsReplace); // Replace symbol names
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) {
   // If the replacement string is empty, then do nothing
   if(p_symbolsReplace == "") {
      return true;
   }

   // Variable for the result
   bool res = true;

   string symbolKeyValuePairs[]; // Array for individual replacements
   string symbolPair[];          // Array for two names in one replacement

   // Split the replacement string into parts representing one separate replacement
   StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs);

   // Glossary for mapping target symbol to source symbol
   CHashMap<string, string> symbolsMap;

   // For all individual replacements
   FOREACH(symbolKeyValuePairs, {
      // Get the source and target symbols as two array elements 
      StringSplit(symbolKeyValuePairs[i], '=', symbolPair);

      // Check if the target symbol is in the list of available non-custom symbols
      bool custom = false;
      res &= SymbolExist(symbolPair[1], custom);

      // If the target symbol is not found, then report an error and exit
      if(!res) {
         PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]);
         return res;
      }
      
      // Add a new element to the glossary: key is the source symbol, while value is the target symbol
      res &= symbolsMap.Add(symbolPair[0], symbolPair[1]);
   });

   // If no errors occurred, then for all strategies we call the corresponding replacement method
   if(res) {
      FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap));
   }

   return res;
}
//+------------------------------------------------------------------+

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


我们将在交易策略类中添加一个同名方法,但该方法不再接受包含替换的字符串作为参数,而是接受替换词汇表。遗憾的是,我们无法在 CVirtualStrategy 类中添加其实现,因为我们对该类中使用的交易品种仍一无所知。因此,让我们把它虚拟化,把实现的责任转移到更低的层次 - 子类。

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...
public:
   ...
   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { 
      return true;
   }
};

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


到目前为止,我们只有一个子类,它的 m_symbol 属性存储了交易品种的名称。让我们添加 SymbolsReplace() 方法,该方法将简单地检查传入的词汇表是否包含与当前交易品种名称匹配的键,并在必要时更改交易工具:

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;         // Symbol (trading instrument)
   ...

public:
   ...

   // Replace symbol names
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap);
};

...

//+------------------------------------------------------------------+
//| Replace symbol names                                             |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) {
   // If there is a key in the glossary that matches the current symbol
   if(p_symbolsMap.ContainsKey(m_symbol)) {
      string targetSymbol; // Target symbol
      
      // If the target symbol for the current one is successfully retrieved from the glossary
      if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) {
         // Update the current symbol
         m_symbol = targetSymbol;
      }
   }
   
   return true;
}

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

至此,与本子任务相关的编辑工作完成。测试器的检查结果表明,根据替代规则,EA 成功地开始了新交易品种的交易。值得注意的是,由于我们使用 CHashMap::Add() 方法来填充替换词汇表,因此,如果尝试使用已经存在的键(源交易品种)来添加新元素(目标交易品种),则会导致错误。

这意味着,如果我们在替换字符串中为同一个交易品种指定两次替换规则,EA 将无法通过初始化。有必要调整替换字符串,将相同交易交易品种的重复替换规则排除在外。


交易完成模式

下一个计划任务是增加设置 EA 特殊运行模式的功能 交易完成。首先,我们需要就我们的意思达成一致。由于我们计划仅在我们想要启动具有不同参数的新 EA 而不是已经运行的 EA 时启用此模式,因此,一方面,我们希望尽快关闭旧 EA 开启的所有仓位。另一方面,如果目前的浮动利润为负数,我们并不想平仓。在这种情况下,最好等待一段时间,直到 EA 走出回撤。

因此,我们将问题表述如下:启用交易完成模式后,一旦浮动利润为非负,EA 就应关闭所有仓位,不再开新仓。如果在开启该模式时利润为非负值,则我们无需等待 EA 会立即关闭所有仓位。如果不是,那我们就只能等待了。

接下来的问题是,我们还要等多久。从历史测试的结果来看,我们可以观察到持续数月的回撤。因此,如果我们只是等待,那么如果交易完成模式启动的时机不成功,等待可能会拖很长时间。也许,在不等待盈利的情况下关闭旧版本的所有仓位会更好,即接受当前的亏损。这样,新版本就可以更快地投入运行,在等待旧版本盈利的同时,也有可能产生利润,弥补旧版本停止期间的损失。

然而,我们无法提前知道旧版本退出回撤的时间,也无法知道新版本在此期间的潜在利润,因为在做出决策的那一刻,这些结果都是未来的事情。 

在这种情况下,一种可能的折衷办法是引入一些最长等待时间,在这之后,旧版本的所有仓位在任何当前回撤时都会被强制平仓。我们可以提出更复杂的方案。例如,我们可以使用这个时间限制作为线性或非线性时间函数的参数,返回我们目前愿意平仓所有仓位的资金。在最简单的情况下,这将是一个阈值函数,在该时限之前返回 0。之后,它将返回一个小于当前账户资金的值,这将保证在指定时间后关闭所有仓位。

让我们继续实现。第一种方案是在 EA 文件中添加两个输入参数(启用关闭模式和以天为单位的限制时间),然后在初始化函数中使用它们:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input bool     useOnlyCloseMode_ = false;    // - Enable close mode
input double   onlyCloseDays_    = 0;        // - Maximum time of closing mode (days)

...

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

// 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,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            "    ,%d,%.2f\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_,
                            useOnlyCloseMode_, onlyCloseDays_
                         );

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

   ...

// Successful initialization
   return(INIT_SUCCEEDED);
}

然而,随着我们的进展,很明显,我们必须编写与我们最近所做的非常相似的代码。结果发现,关闭模式下所需的行为与 EA 的行为非常相似,后者的风险管理器设定的目标利润值等于关闭模式开始时的当前余额与基础余额之差。那么,为什么不对风险管理器稍作修改,以便只需在风险管理器中设置必要的参数就能实现关闭模式呢?

让我们想一想,我们的风险管理器在实现关闭模式工作时还缺少什么。在最简单的情况下,如果我们不处理最长时间,那么风险管理器就不需要做任何修改。在旧版本中,我们只需将目标利润参数设置为等于当前账户余额与基础余额之差的值,然后等待它达到这个值。我们甚至可以更进一步,随着时间的推移定期修改它。预计这一机制将很少使用。但最好在指定时间后自动关闭。因此,让我们为风险管理器增加一项功能,即不仅可以设定目标利润,还可以设定最大允许等待时间。它将发挥设置平仓最长时间的作用。

对我们来说,以特定日期和时间的形式传达这一时间更方便,从而消除了记住指定间隔应从开始计算的工作开始日期的需要。让我们把这个参数添加到与风险管理器有关的输入参数集合中。我们还将在初始化字符串中添加对其值的替换:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input group ":::  Risk manager"

...

input ENUM_RM_CALC_OVERALL_PROFIT
rmCalcOverallProfitLimit_                    = RM_CALC_OVERALL_PROFIT_MONEY_BB;  // - Method for calculating total profit
input double      rmMaxOverallProfitLimit_   = 1000000;                          // - Overall profit
input datetime    rmMaxOverallProfitDate_    = 0;                                // - Maximum time of waiting for the total profit (days)

...

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

// 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,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_
                         );
   
// Create an EA handling virtual positions
   expert = NEW(expertParams);

...

// Successful initialization
   return(INIT_SUCCEEDED);
}

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


在风险管理器类中,首先为等待给定利润的最长时间添加一个新属性,并在初始化字符串的构造函数中设置其值。此外,添加新方法 OverallProfit(),该方法将返回平仓时的预期利润值:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   ...
   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit
   double            m_maxOverallProfitLimit;            // Parameter for calculating the maximum overall profit
   datetime          m_maxOverallProfitDate;             // Maximum time of reaching the total profit

   ...

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss
   double            OverallProfit();        // Maximum profit

   ...
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_closeDailyPart = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);
   m_closeOverallPart = ReadDouble(p_params);
   m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params);
   m_maxOverallProfitLimit = ReadDouble(p_params);
   m_maxOverallProfitDate  = (datetime) ReadLong(p_params);
   m_maxRestoreTime = ReadDouble(p_params);
   m_lastVirtualProfitFactor = ReadDouble(p_params);

   ...
}


OverallProfit() 方法首先检查是否设置了实现预期利润的时间。如果时间已设定,而当前时间已超过设定时间,则该方法返回当前利润值,因为当前值已是期望值。这最终将导致所有仓位被关闭,交易停止。如果时间未到,则该方法返回根据输入参数计算出的预期利润值:

//+------------------------------------------------------------------+
//| Maximum total profit                                             |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallProfit() {
   // Current time
   datetime tc = TimeCurrent();
   
   // If the current time is greater than the specified maximum time,
   if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) {
      // Return the value that guarantees the positions are closed
      return m_overallProfit;
   } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallProfitLimit / 100;
   } else {
      // To get a fixed value, just return it 
      // RM_CALC_OVERALL_PROFIT_MONEY_BB
      return m_maxOverallProfitLimit;
   }
}


我们将在 CheckOverallProfitLimit() 方法中检查是否需要平仓时使用该方法:

//+------------------------------------------------------------------+
//| Check if the specified profit has been achieved                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit >= OverallProfit() && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart = 0;

      // Set the risk manager to the achieved overall profit state
      m_state = RM_STATE_OVERALL_PROFIT;

      // Set the value of the used part of the overall balance
      SetDepoPart();

      ...

      return true;
   }

   return false;
}

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

有关关闭模式的修改已基本完成。稍后我们将添加其余部分,以确保重新启动后可以恢复功能。


重启后恢复

从该系列的第一部分开始,就设想了提供这种可能性的必要性。我们创建的许多类都已经有了专门用于保存和载入对象状态的 Save()Load() 方法。在其中一些方法中,我们已经有了可用的代码,但后来我们忙于其他事情,没有让这些方法正常工作,因为这是不必要的。是时候关注它们,让它们恢复正常工作了。

也许,我们必须再次进行的主要更改是在风险管理器类中,因为这些方法在那里仍然是完全空白的。我们还需要确保在加载/保存 EA 时调用风险管理器保存和加载方法,因为风险管理器出现的时间较晚,没有添加到保存的信息中。

首先,让我们为 EA 添加一个输入参数,决定是否恢复之前的状态,默认为 True。如果我们想从头开始启动 EA,可以将其设置为 "False",然后重新启动 EA(在这种情况下,所有之前保存的信息都将被新信息覆盖),然后再将该参数设置为 "True"。在 EA 初始化函数中,检查是否需要加载之前的状态,如果需要,则加载它:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
...

input bool     usePrevState_     = true;     // - Load the previous state

...

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

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

// If the EA is not created, then return an error
   if(!expert) return INIT_FAILED;

// If an error occurred while replacing symbols, then return an error
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;


// If we need to restore the state,
   if(usePrevState_) {
      // Load the previous state if available
      expert.Load();
      expert.Tick();
   }

// Successful initialization
   return(INIT_SUCCEEDED);
}

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


在介绍保存/加载状态的方法之前,我们先来关注以下几个方面。在以前的版本中,当在可视化测试模式下运行时,我们根据 EA 的名称、幻数和 ".test" 一词来确定保存的文件名。EA 名称是一个常量值。它嵌入在源代码中,不会通过 EA 输入参数进行更改。幻数可通过输入参数进行更改。这意味着,如果我们更改了幻数,EA 将不会再加载用以前使用的幻数生成的文件。但这也意味着,如果我们改变交易策略单一实例的组成,但保留相同的幻数,那么 EA 将尝试使用之前的文件来加载状态。

这很可能会导致错误,因此我们需要防止出现这种情况。一种可能的方法是在文件名中包含一些取决于所用交易策略实例的部分。然后,如果它们的组成发生变化,那么文件名的这一部分也会发生变化,这意味着 EA 在更新策略组成后将不会使用旧文件。

通过从 EA 初始化字符串或其一部分计算一些哈希函数,可以形成文件名的这种变化部分。事实上,我们谈到了只有在更改交易策略的组成时才需要使用不同的文件。例如,如果我们更改风险管理器设置,这将更改初始化字符串,但不应导致用于保存状态的文件名称的更改。因此,我们将仅从初始化字符串中包含有关交易策略单个实例信息的部分计算哈希函数。

为此,添加新方法 HashParams(),并更改 EA 构造函数:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   virtual string    HashParams(string p_name);   // Hash value of EA parameters 

public:
   ...
};

...

//+------------------------------------------------------------------+
//| Hash value of EA parameters                                      |
//+------------------------------------------------------------------+
string CVirtualAdvisor::HashParams(string p_params) {
   uchar hash[], key[], data[];

   // Calculate the hash from the initialization string  
   StringToCharArray(p_params, data);
   CryptEncode(CRYPT_HASH_MD5, data, key, hash);

   // Convert it from the array of numbers to a string with hexadecimal notation
   string res = "";
   FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-");
   
   return res;
}

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {


       ... 

// If there are no read errors,
   if(IsValid()) {

      
    ... 

      // Form the name of the file for saving the state from the EA name and parameters
      m_name = StringFormat("%s-%d-%s%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            HashParams(groupParams),
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );;

      
    ... 
   }
}


在相应的 EA 方法中增加保存/加载风险管理器的功能: 

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

// Save status if:
   if(true
// later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// 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))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // If file is open, save
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Time of last changes

         // All strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         m_riskManager.Save(f);

         FileClose(f);

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

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

// Load status if:
   if(true
// file exists
         && FileIsExist(m_name)
// 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))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // If the file is open, then load
         m_lastSaveTime = FileReadDatetime(f);     // Last save time
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Load all strategies
         FOREACH(m_strategies, {
            res &= ((CVirtualStrategy*) m_strategies[i]).Load(f);
            if(!res) break;
         });

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
         }

         res &= m_riskManager.Load(f);
         
         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name);
         }

         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }

   return res;
}

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


现在我们要做的就是编写保存/加载风险管理器方法的实现。让我们来看看我们需要为风险管理器保存什么信息。风险管理器的输入参数无需保存它们始终取自 EA 输入参数。在下一次启动时,就可以替换已更改的值。也无需保存风险管理器本身更新的数值:余额、资产净值、每日利润等。唯一值得保留的每日基础水平,因为它每天只计算一次。

所有与当前状态和未平仓仓位大小管理有关的属性( 总体余额的已用部分除外)都应保留。

// Current state
   ENUM_RM_STATE     m_state;                // State
   double            m_lastVirtualProfit;    // Profit of open virtual positions at the moment of loss limit 
   datetime          m_startRestoreTime;     // Start time of restoring the size of open positions
   datetime          m_startTime;

// Updated values
   
    ... 

// Managing the size of open positions
   double            m_baseDepoPart;         // Used (original) part of the total balance
   double            m_dailyDepoPart;        // Multiplier of the used part of the total balance by daily loss
   double            m_overallDepoPart;      // Multiplier of the used part of the total balance by overall loss


考虑到上述情况,这些方法的实现过程可能是这样的:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Save(const int f) {
   FileWrite(f,
             m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime,
             m_dailyDepoPart, m_overallDepoPart);

   return true;
}

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Load(const int f) {
   m_state = (ENUM_RM_STATE) FileReadNumber(f);
   m_lastVirtualProfit = FileReadNumber(f);
   m_startRestoreTime = FileReadDatetime(f);
   m_startTime = FileReadDatetime(f);
   m_dailyDepoPart = FileReadNumber(f);
   m_overallDepoPart = FileReadNumber(f);

   return true;
}

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


测试

为了测试新增功能,我们将采取两种方法。首先,在图表上安装已编译好的 EA,并确保已创建状态数据文件。为了进一步验证,我们需要等待 EA 开仓。但我们可能为此等待相当长的时间,我们将不得不等待更长的时间来触发风险管理器,这样我们才能检查 EA 在干预后恢复工作的正确性。其次,我们将使用策略测试器,模拟 EA 停止工作后恢复工作的情况。

为此,我们将在现有 EA 的基础上创建一个新的 EA,并在其中添加两个新的输入参数:重启前的停止时间和重启的开始时间。处理方式如下:

  • 如果未指定重启前的停止时间(等于零或 1970-01-01 00:00:00)或该时间不在测试时间间隔内,则 EA 将与原始 EA 一样工作;
  • 如果在测试间隔内指定了特定的停止时间,那么当到达该时间时,EA 将停止执行 EA 对象的分时报价处理程序,直到第二个参数中指定的时间。

在代码中,这两个参数如下所示:

input datetime restartStopTime_  = 0;        // - Stop time before restart
input datetime restartStartTime_ = 0;        // - Restart launch time

让我们对 EA 中的分时报价处理函数进行修改。为了记住中断已发生,我们将添加全局布尔变量 isRestarting。如果为 True,则表示 EA 正在待命。一旦当前时间超过恢复时间,我们就会加载之前的 EA 状态并重置 isRestarting 标志:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
// If the stop time is specified,
   if(restartStopTime_ != 0) {
      // Define the current time
      datetime tc = TimeCurrent();

      // If we are in the interval between stopping and resuming,
      if(tc >= restartStopTime_ && tc <= restartStartTime_) {
         // Save the status and exit
         isRestarting = true;
         return;
      }

      // If we were in a state between stopping and resuming,
      // and it is time to resume work,
      if(isRestarting && tc > restartStartTime_) {
         // Load the EA status
         expert.Load();
         // Reset the status flag between stop and resume
         isRestarting = false;
      }
   }

// Perform tick handling
   expert.Tick();
}

将更改保存在当前文件夹中的 SimpleVolumesTestRestartExpert.mq5 文件中。


让我们看看 2021 到 2022 年期间不中断的结果。

图 1.测试结果无需中断交易


现在,让我们从 EA 的工作中暂时中断一小段时间。试运行后,结果与没有中断时完全一样。这表明在短暂中断后,EA 成功恢复状态并继续工作。

要想知道其中的差别,我们可以中断更长的时间,比如 4 个月。我们得到以下结果:

图 2.2021.07.27 至 2021.11.29 交易中断的测试结果

在图表上,带黄色边框的矩形显示了大致的中断位置。此时,EA 开设的仓位被放弃。但随后,EA 恢复了行动,拾起了未平仓位,并取得了普遍良好的结果。因此,可认为保存和加载 EA 状态的功能已经实现。


结论

让我们再来看看取得的成果。我们已经开始认真准备 EA 在真实账户上的工作。为此,我们考虑了在测试器中没有遇到过的,但大多是在实际交易中遇到的情况。

我们研究了如何确保 EA 在交易品种名称与优化时略有不同的账户上运行。为此,实现了替换交易品种名称的功能。如果需要使用其他输入参数启动 EA,我们还提供了平缓结束交易的功能。另一项重要发展是增加了保存 EA 状态的功能,以确保在重新启动终端后能正常恢复工作。

然而,这些并不是安装 EA 在真实交易账户上工作时应该做的所有准备。我们希望以某种方式更好地安排各种类型的辅助信息的显示,并在图表上显示 EA 当前状态的汇总数据。但更重要的是,修改允许 EA 在没有数据库的情况下工作,优化结果位于终端的工作文件夹中。目前,我们不能没有这个功能,因为正是从这个数据库中提取了用于形成 EA 初始化字符串的组件。我们将在以下文章中讨论这些改进。

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

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

您应当知道的 MQL5 向导技术(第 20 部分):符号回归 您应当知道的 MQL5 向导技术(第 20 部分):符号回归
符号回归是一种回归形式,它从最小、甚或没有假设开始,而底层模型看起来应当映射所研究数据集。尽管它可以通过贝叶斯(Bayesian)方法、或神经网络来实现,但我们看看如何使用遗传算法实现,从而有助于在 MQL5 向导中使用自定义的智能信号类。
您应当知道的 MQL5 向导技术(第 19 部分):贝叶斯(Bayesian)推理 您应当知道的 MQL5 向导技术(第 19 部分):贝叶斯(Bayesian)推理
贝叶斯(Bayesian)推理是运用贝叶斯定理,在获得新信息时更新概率假设。这在直观上倾向于时间序列分析中的适应性,那么我们来看看如何运用它来构建自定义类,不仅针对信号,还有资金管理、和尾随破位。
交易中的混沌理论(第一部分):简介、在金融市场中的应用和李亚普诺夫指数 交易中的混沌理论(第一部分):简介、在金融市场中的应用和李亚普诺夫指数
混沌理论可以应用于金融市场吗?在这篇文章中,我们将探讨传统混沌理论和混沌系统与比尔·威廉姆斯提出的概念有何不同。
神经网络实践:伪逆 (二) 神经网络实践:伪逆 (二)
由于这些文章本质上是教育性的,并不打算展示特定功能的实现,因此我们在本文中将做一些不同的事情。我们将重点介绍伪逆的因式分解,而不是展示如何应用因式分解来获得矩阵的逆。原因是,如果我们能以一种特殊的方式来获得一般系数,那么展示如何获得一般系数就没有意义了。更好的是,读者可以更深入地理解为什么事情会以这种方式发生。那么,现在让我们来弄清楚为什么随着时间的推移,硬件正在取代软件。