
开发多币种 EA 交易(第 4 部分):虚拟挂单和保存状态
概述
在上一篇文章中,我们对代码架构进行了大幅修改,以构建一个具有多种并行工作策略的多币种 EA。为了做到简单明了,我们迄今为止只考虑了一些最基本的功能。即使考虑到我们任务的局限性,我们也对前几篇文章的代码做了很大改动。
现在,希望我们已经有了足够的基础,可以在不对已编写的代码进行彻底修改的情况下增加功能。只有在确有必要的情况下,我们才会进行尽量少的改动。
在进一步的开发中,我们将努力做到以下几点:
- 增加打开虚拟挂单(买入止损、卖出止损、买入限价、卖出限价)的功能,而不仅仅是虚拟仓位(买入、卖出);
- 添加一种可视化已下虚拟订单和仓位的简单方法,以便我们在测试所用交易策略中开仓/下单规则的正确执行情况时可以进行可视化控制;
- 实现由 EA 保存当前状态数据,这样当终端重新启动或 EA 转移到另一个终端时,它可以从工作中断时的状态继续工作。
让我们从最简单的事情开始 - 处理虚拟挂单。
虚拟挂单
我们已经创建了 CVirtualOrder 类来处理虚拟仓位。我们可以创建一个单独的类似的类来处理虚拟挂单。让我们来看看处理仓位和处理订单是否真的有那么大的区别。
它们的一系列属性在很大程度上是一致的。不过,挂单添加了一个属性,用于存储到期时间。正因为如此,它们增加了一个新的关闭原因 - 在到期时关闭。因此,我们需要在每个分时报价检查未关闭的虚拟挂单是否因此而关闭,当达到止损和止盈水平时,不应关闭。
当价格达到虚拟挂单的触发水平时,该挂单应转为打开的虚拟仓位。这里已经很清楚,当以不同类的形式实现时,我们将不得不做额外的工作来创建一个开启的虚拟仓位和删除一个已触发的虚拟挂单。如果我们将它们作为一个类来实现,那么我们只需更改类对象的一个属性 - 它的类型。因此,我们将实现这一方案。
订单与仓位还有什么不同?我们还需要指出虚拟挂单的开启价格。对于虚拟仓位,无需指定开启价格,因为它会根据当前市场数据自动确定。现在,我们将把它作为开启函数的一个必要参数。
让我们开始为类增加功能吧。我们将增加:
- m_expiration 属性,用于存储过期时间;
- m_isExpired 逻辑属性来指示挂单到期时间;
- 检查挂单是否被触发的 CheckTrigger() 方法,以及用于检查给定对象是否属于特定类型(挂单、限价挂单等)的多个方法;
- 在检查给定对象是买入还是卖出方向的方法中,添加了一个条件,规定如果是所需方向的挂单,则返回 "true",而不管它是限价单还是止损单。
- Open() 方法参数列表接收开启价格和到期时间。
//+------------------------------------------------------------------+ //| Class of virtual orders and positions | //+------------------------------------------------------------------+ class CVirtualOrder { private: ... //--- Order (position) properties ... datetime m_expiration; // Expiration time ... bool m_isExpired; // Expiration flag ... //--- Private methods ... bool CheckTrigger(); // Check for pending order trigger public: ... //--- Methods for checking the position (order) status ... bool IsPendingOrder() {// Is it a pending order? return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_LIMIT || m_type == ORDER_TYPE_SELL_STOP); } bool IsBuyOrder() { // Is it an open BUY position? return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_BUY_STOP); } bool IsSellOrder() { // Is it an open SELL position? return IsOpen() && (m_type == ORDER_TYPE_SELL || m_type == ORDER_TYPE_SELL_LIMIT || m_type == ORDER_TYPE_SELL_STOP); } bool IsStopOrder() { // Is it a pending STOP order? return IsOpen() && (m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_STOP); } bool IsLimitOrder() { // is it a pending LIMIT order? return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_SELL_LIMIT); } ... //--- Methods for handling positions (orders) bool CVirtualOrder::Open(string symbol, ENUM_ORDER_TYPE type, double lot, double price, double sl = 0, double tp = 0, string comment = "", datetime expiration = 0, bool inPoints = false); // Opening a position (order) ... };
在打开虚拟仓位的 Open() 方法中(现在也将打开虚拟挂单),添加将开仓价赋值给 m_openPrice 属性。如果结果显示这不是挂单,而是仓位,则为该属性添加当前市场开盘价:
bool CVirtualOrder::Open(string symbol, // Symbol ENUM_ORDER_TYPE type, // Type (BUY or SELL) double lot, // Volume double price = 0, // Open price double sl = 0, // StopLoss level (price or points) double tp = 0, // TakeProfit level (price or points) string comment = "", // Comment datetime expiration = 0, // Expiration time bool inPoints = false // Are the SL and TP levels set in points? ) { ... if(s_symbolInfo.Name(symbol)) { // Select the desired symbol s_symbolInfo.RefreshRates(); // Update information about current prices // Initialize position properties m_openPrice = price; ... m_expiration = expiration; // The position (order) being opened is not closed by SL, TP or expiration ... m_isExpired = false; ... // Depending on the direction, set the opening price, as well as the SL and TP levels. // If SL and TP are specified in points, then we first calculate their price levels // relative to the open price if(IsBuyOrder()) { if(type == ORDER_TYPE_BUY) { m_openPrice = s_symbolInfo.Ask(); } ... } else if(IsSellOrder()) { if(type == ORDER_TYPE_SELL) { m_openPrice = s_symbolInfo.Bid(); } ... } ... return true; } return false; }
在检查虚拟挂单是否被触发的 CheckTrigger() 方法中,我们会根据订单的方向获取当前市场的买入价或卖出价,并检查其是否已达到所需方的开盘价。如果是,则将当前对象的 m_type 属性替换为与所需方向仓位相对应的值,并通知接收者和策略对象新的虚拟仓位已经打开。
//+------------------------------------------------------------------+ //| Check whether a pending order is triggered | //+------------------------------------------------------------------+ bool CVirtualOrder::CheckTrigger() { if(IsPendingOrder()) { s_symbolInfo.Name(m_symbol); // Select the desired symbol s_symbolInfo.RefreshRates(); // Update information about current prices double price = (IsBuyOrder()) ? s_symbolInfo.Ask() : s_symbolInfo.Bid(); int spread = s_symbolInfo.Spread(); // If the price has reached the opening levels, turn the order into a position if(false || (m_type == ORDER_TYPE_BUY_LIMIT && price <= m_openPrice) || (m_type == ORDER_TYPE_BUY_STOP && price >= m_openPrice) ) { m_type = ORDER_TYPE_BUY; } else if(false || (m_type == ORDER_TYPE_SELL_LIMIT && price >= m_openPrice) || (m_type == ORDER_TYPE_SELL_STOP && price <= m_openPrice) ) { m_type = ORDER_TYPE_SELL; } // If the order turned into a position if(IsMarketOrder()) { m_openPrice = price; // Remember the open price // Notify the recipient and the strategy of the position opening m_receiver.OnOpen(GetPointer(this)); m_strategy.OnOpen(); return true; } } return false; }
如果是虚拟挂单,在 Tick() 方法中处理新分时报价时将调用此方法:
//+------------------------------------------------------------------+ //| Handle a tick of a single virtual order (position) | //+------------------------------------------------------------------+ void CVirtualOrder::Tick() { if(IsOpen()) { // If this is an open virtual position or order if(CheckClose()) { // Check if SL or TP levels have been reached Close(); // Close when reached } else if (IsPendingOrder()) { // If this is a pending order CheckTrigger(); // Check if it is triggered } } }
在 Tick() 方法中,调用 CheckClose() 方法,我们还需要在该方法中添加根据到期时间检查虚拟挂单关闭情况的代码:
//+------------------------------------------------------------------+ //| Check the need to close by SL, TP or EX | //+------------------------------------------------------------------+ bool CVirtualOrder::CheckClose() { if(IsMarketOrder()) { // If this is a market virtual position, ... // Check that the price has reached SL or TP ... } else if(IsPendingOrder()) { // If this is a pending order // Check if the expiration time has been reached, if one is specified if(m_expiration > 0 && m_expiration < TimeCurrent()) { m_isExpired = true; return true; } } return false; }
将更改保存到当前文件夹中的 VirtualOrder.mqh 文件。
现在让我们回到 CSimpleVolumesStrategy 交易策略类。在该策略中,我们为未来的修改留出了余地,以便能够添加对处理虚拟挂单的支持。OpenBuyOrder() 和 OpenSellOrder() 方法中就有这样的地方。让我们在此添加调用 Open() 方法的参数,以打开虚拟挂单。我们将首先从当前价格计算开启价格,然后按 m_openDistance 参数指定的点数从当前价格向所需方向后退。我们仅提供 OpenBuyOrder() 方法的代码。在另一个代码中,修改也会类似。
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenBuyOrder() { // Update symbol current price data ... // Retrieve the necessary symbol and price data ... // Let's make sure that the opening distance is not less than the spread int distance = MathMax(m_openDistance, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels ... // Expiration time datetime expiration = TimeCurrent() + m_ordersExpiration * 60; ... for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it if(m_openDistance > 0) { // Set SELL STOP pending order res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_STOP, m_fixedLot, NormalizeDouble(price, digits), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), "", expiration); } else if(m_openDistance < 0) { // Set SELL LIMIT pending order res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot, NormalizeDouble(price, digits), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), "", expiration); } else { // Open a virtual SELL position res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, 0, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); } break; // and exit } } ... }
将更改保存到当前文件夹的 SimpleVolumesStrategy.mqh 文件中。
这样就完成了必要的更改,以支持使用虚拟挂单的策略操作。我们只修改了两个文件,现在就可以编译 SimpleVolumesExpertSingle.mq5 EA 了。当设置 openDistance_ 参数不等于零时,EA 应打开虚拟挂单而不是虚拟仓位。不过,我们不会在图表上看到仓位的开启,我们只能在日志中看到相应的信息。只有当它们被转换成开启的虚拟仓位后,我们才能在图表上看到它们,而虚拟交易量的接收对象将把它们带入市场。
如果能以某种方式在图表上看到已下的虚拟挂单就更好了。我们稍后会再讨论这个问题,但现在让我们继续讨论一个更重要的问题 - 确保 EA 的状态在重启后得到保存和加载。
保存状态
我们基于开发的类创建了两个 EA。第一个 EA(SimpleVolumesExpertSingle.mq5)旨在优化交易策略单个实例的参数,第二个 EA(SimpleVolumesExpert.mq5)已经包含了多个交易策略副本,并使用第一个 EA 选定的最佳参数。今后,只有第二个 EA 有可能用于真实账户,第一个 EA 只能在策略测试器中使用。因此,我们只需要加载和保存第二个 EA 或其他 EA 的状态,其中还将包括许多交易策略实例。
其次,需要澄清的是,我们现在讨论的是保存和加载 EA 的状态,而不是 EA 中应使用的不同策略集合。换句话说,EA 中带有指定参数的策略实例集合是固定的,每次启动时都一样。首次启动后,EA 会打开虚拟和真实仓位,或许还会根据价格数据计算一些指标。正是这些信息构成了 EA 的整体状态。如果我们现在重新启动终端,EA 应该会将开启的仓位识别为自己的仓位,并恢复所有虚拟仓位和必要的计算值。虽然可以从终端获取开启仓位的信息,但 EA 应独立保存虚拟仓位和计算值的信息。
对于我们所考虑的简单交易策略,无需积累任何计算数据,因此 EA 的状态将完全由一组虚拟仓位和挂单决定。但是,仅将所有 CVirtualOrder 类对象的数组保存到文件中是不够的。
让我们设想一下,我们已经有了几个具有不同交易策略的 EA。但结果表明,每个 EA 创建的 CVirtualOrder 类对象的总数是相同的。例如,我们在每个交易中使用了 9 个交易策略实例,每个实例要求使用 3 个虚拟仓位对象。然后,每个 EA 将存储 27 个 CVirtualOrder 类对象的信息。在这种情况下,我们需要以某种方式确保自己免受这样一个事实的影响,即其中一个 EA 不会上传有关其 27 个虚拟仓位的信息,而是上传有关其他仓位的信息。
为此,我们可以在保存的文件中添加作为 EA 组成部分的策略参数信息,也可以添加 EA 本身的参数信息。
现在,让我们考虑一下应该在什么时间点保存状态。如果策略参数是固定的,那么它们在任何时候都是一样的。同时,虚拟仓位对象可能在打开和关闭操作过程中改变其属性值。这意味着在这些操作之后保存状态是有意义的。更频繁地保存(例如,每个分时或通过计时器保存)目前看来是多余的。
我们继续实现过程。因为我们有一个对象的分层结构,比如
- EA
- 策略
- 虚拟仓位
- 策略
将我们的保存工作委托给每一级,而不是将所有工作都集中在最高一级,尽管这也是可能的。
在每一级中,为分别负责保存和加载的 Save() 和 Load() 类添加两个方法。在最高级别,这些方法将打开一个文件,而在较低级别,它们将获取一个已打开文件的描述符作为参数。因此,我们将只在 EA 层面考虑选择保存文件名的问题。这个问题不会在策略和虚拟仓位层面出现。
EA 修改
CVirtualAdvisor 类获取 m_name 字段,用于存储 EA 名称。由于在操作过程中不会发生变化,我们将在构造函数中对其进行赋值。如果 EA 在可视化测试模式下启动,还可以立即扩展名称,添加幻数和可选的".test "后缀。
若要仅在虚拟仓位组成发生变化时执行保存,请添加 m_lastSaveTime 字段,该字段将存储上次保存的时间。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... string m_name; // EA name datetime m_lastSaveTime; // Last save time public: CVirtualAdvisor(ulong p_magic = 1, string p_name = ""); // Constructor ... virtual bool Save(); // Save status virtual bool Load(); // Load status };
创建 EA 交易时,我们将为以下两个新属性分配初始值:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") : ... m_lastSaveTime(0) { m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); };
将检查是否需要保存的逻辑放在 Save() 方法中。我们只需在对每个分时执行完其余操作后,在其上添加对该方法的调用即可:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); }
在保存方法中,我们需要做的第一件事就是检查是否需要执行它。为此,我们必须事先商定,我们将在接收者对象中添加一个新属性,该属性将存储虚拟开启的仓位最后一次变动的时间或实际开启交易量最后一次修正的时间。如果上一次保存的时间小于上一次更正的时间,那么就说明发生了变化,我们需要进行保存。
此外,如果目前正在进行优化或单次测试,而没有使用可视化模式,我们将不会保存更改。如果当前测试正在可视模式下进行,则将执行保存。这样,我们也可以在策略测试器中检查保存情况。
在 EA 层面,Save() 可能是这样的:我们检查是否需要保存,然后保存当前时间和策略数。之后,我们会循环调用所有策略的保存方法。
//+------------------------------------------------------------------+ //| 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 FileWrite(f, ArraySize(m_strategies)); // Number of strategies // All strategies FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).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() 状态加载方法应该做类似的工作,但不是写入,而是从文件中读取数据。换句话说,我们首先读取保存的时间和策略数量。在这里,我们可以检查读取的策略数是否与添加到 EA 中的策略数一致,以防万一。如果不是,那就没有必要再看下去了,这是一个错误的文件。如果是,那么一切都好,我们可以继续读取。然后,我们再次将后续工作委托给层次结构中下一级的对象:我们查看所有添加的策略,并调用它们从打开的文件中读取的方法。
在代码中看起来可能是这样的:
//+------------------------------------------------------------------+ //| 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)); // Number of strategies long f_strategiesCount = StringToInteger(FileReadString(f)); // Does the loaded number of strategies match the current one? res = (ArraySize(m_strategies) == f_strategiesCount); if(res) { // Load all strategies FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f)); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name); } } else { PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count (%d expected but %d found in file %s)", ArraySize(m_strategies), f_strategiesCount, m_name); } FileClose(f); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } return res; }
保存对当前文件夹中 VirtualAdvisor.mqh 文件所做的更改。
我们应该在启动 EA 时只调用一次状态加载方法,但我们不能在 EA 对象构造函数中这样做,因为此时策略尚未添加到 EA 中。因此,在将所有策略实例添加到 EA 对象后,让我们在 EA 文件的 OnInit() 函数中进行操作:
CVirtualAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Create and fill the array of strategy instances CStrategy *strategies[9]; strategies[0] = ... ... strategies[8] = ... // Create an EA handling virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes"); // Add strategies to the EA FOREACH(strategies, expert.Add(strategies[i])); // Load the previous state if available expert.Load(); return(INIT_SUCCEEDED); }
将更改保存到当前文件夹的 SimpleVolumesExpert.mq5 文件中。
基本策略修改
在交易策略基类中添加 Save() 和 Load() 方法,以及将当前策略对象转换为字符串的方法。为简洁起见,我们用重载的一元运算符 ~(波浪符)来实现这个方法。
//+------------------------------------------------------------------+ //| Class of a trading strategy with virtual positions | //+------------------------------------------------------------------+ class CVirtualStrategy : public CStrategy { ... public: ... virtual bool Load(const int f); // Load status virtual bool Save(const int f); // Save status string operator~(); // Convert object to string };
将对象转换为字符串的方法将返回一个字符串,其中包含当前类的名称和虚拟仓位数组的元素个数:
//+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CVirtualStrategy::operator~() { return StringFormat("%s(%d)", typename(this), ArraySize(m_orders)); }
为写入而打开的文件描述符会传递给 Save() 方法。转换为字符串时,从对象获取的字符串会首先写入文件。然后,每个虚拟仓位都将循环调用其保存方法。
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualStrategy::Save(const int f) { bool res = true; FileWrite(f, ~this); // Save parameters // Save virtual positions (orders) of the strategy FOREACH(m_orders, res &= m_orders[i].Save(f)); return res; }
Load() 方法只是按照写入的顺序读取数据,同时确保文件中的参数字符串和策略中的参数字符串相匹配:
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualStrategy::Load(const int f) { bool res = true; // Current parameters are equal to read parameters res = (~this == FileReadString(f)); // If yes, then load the virtual positions (orders) of the strategy if(res) { FOREACH(m_orders, res &= m_orders[i].Load(f)); } return res; }
将已执行的更改保存到当前文件夹中的 VirtualStrategy.mqh 文件。
修改交易策略
在具体交易策略的 CSimpleVolumesStrategy 类中,我们需要添加与基类相同的方法集:
//+------------------------------------------------------------------+ //| Trading strategy using tick volumes | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { ... public: ... virtual bool Load(const int f) override; // Load status virtual bool Save(const int f) override; // Save status string operator~(); // Convert object to string };
在将交易策略转换为字符串的方法中,我们将根据类的名称生成结果,并列出所有关键参数。目前,为了简单起见,我们将在这里添加所有参数的值,但将来我们可能会为了更方便而缩短这个列表。这样,我们就可以在参数略有变化的情况下重新启动 EA,而无需完全关闭所有之前打开的市场仓位。例如,如果我们增加 TakeProfit(获利)参数,那么就可以安全地留出 TakeProfit 水平较低的开启仓位。
//+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CSimpleVolumesStrategy::operator~() { return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)", // Strategy instance parameters typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot, m_signalPeriod, m_signalDeviation, m_signaAddlDeviation, m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration, m_maxCountOfOrders ); }
这也是我们需要再次在代码中写入所有策略参数的地方。我们会牢记这一点,直到我们开始使用输入参数。
Save() 方法非常简洁,因为基类将完成主要工作:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::Save(const int f) { bool res = true; FileWrite(f, ~this); // Save parameters res &= CVirtualStrategy::Save(f); // Save strategy return res; }
Load() 方法的篇幅会稍大一些,但这主要是为了提高代码的可读性:
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::Load(const int f) { bool res = true; string currentParams = ~this; // Current parameters string loadedParams = FileReadString(f); // Read parameters PrintFormat(__FUNCTION__" | %s", loadedParams); res = (currentParams == loadedParams); // Load if read parameters match the current ones if(res) { res &= CVirtualStrategy::Load(f); } return res; }
将更改保存到当前文件夹的 SimpleVolumesExpert.mqh 文件中。
修改虚拟仓位
我们需要为 CVirtualOrder 虚拟仓位类添加三个方法:
class CVirtualOrder { ... virtual bool Load(const int f); // Load status virtual bool Save(const int f); // Save status string operator~(); // Convert object to string };
在保存到文件时,我们不会使用转换为字符串的方法,但在记录加载的对象数据时,这种方法会很有用:
//+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CVirtualOrder::operator~() { if(IsOpen()) { return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f", m_id, TypeName(), m_symbol, m_lot, TimeToString(m_openTime), m_openPrice, m_stopLoss, m_takeProfit, TimeToString(m_closeTime), m_closePrice); } else { return StringFormat("#%d --- ", m_id); } }
不过,我们以后可能会通过添加从字符串读取对象属性的方法来改变这种情况。
最后,我们将在 Save() 方法中向文件写入一些更重要的信息:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualOrder::Save(const int f) { FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice, m_stopLoss, m_takeProfit, m_openTime, m_closePrice, m_closeTime, m_expiration, m_comment, m_point); return true; }
然后,Load() 不仅会读取写入的内容,用读取的信息填充必要的属性,还会通知相关的策略对象和接收者该虚拟仓位(订单)是打开还是关闭:
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualOrder::Load(const int f) { m_id = (ulong) FileReadNumber(f); m_symbol = FileReadString(f); m_lot = FileReadNumber(f); m_type = (ENUM_ORDER_TYPE) FileReadNumber(f); m_openPrice = FileReadNumber(f); m_stopLoss = FileReadNumber(f); m_takeProfit = FileReadNumber(f); m_openTime = FileReadDatetime(f); m_closePrice = FileReadNumber(f); m_closeTime = FileReadDatetime(f); m_expiration = FileReadDatetime(f); m_comment = FileReadString(f); m_point = FileReadNumber(f); PrintFormat(__FUNCTION__" | %s", ~this); // Notify the recipient and the strategy that the position (order) is open if(IsOpen()) { m_receiver.OnOpen(GetPointer(this)); m_strategy.OnOpen(); } else { m_receiver.OnClose(GetPointer(this)); m_strategy.OnClose(); } return true; }
将获得的代码保存到当前文件夹的 VirtualOrder.mqh 文件中。
测试保存功能
现在我们来测试保存和加载状态数据。为了避免等待开仓的有利时机,我们将临时更改交易策略,如果还没有未关闭的仓位/订单,则在启动时强制打开一个仓位或挂单。
由于我们添加了显示加载进度信息的功能,当我们重新启动 EA 时(最简单的方法就是重新编译),我们会在日志中看到类似下面的内容:
CVirtualAdvisor::Load | LAST SAVE at 2027.02.23 08:05:33 CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,13,0.30,1.00,0,10500.00,465.00,1000,3) CVirtualOrder::Load | Order#1 EURGBP 0.06 BUY in 2027.02.23 08:02 at 0.85494 (0.75007, 0.85985). 1970.01.01 00:00, 0.000000 CVirtualReceiver::OnOpen#EURGBP | OPEN VirtualOrder #1 CVirtualOrder::Load | Order#2 --- CVirtualOrder::Load | Order#3 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.11,17,1.70,0.50,210,16500.00,220.00,1000,3) CVirtualOrder::Load | Order#4 EURGBP 0.11 BUY STOP in 2027.02.23 08:02 at 0.85704 (0.69204, 0.85937). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#5 --- CVirtualOrder::Load | Order#6 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,51,0.50,1.10,500,19500.00,370.00,22000,3) CVirtualOrder::Load | Order#7 EURGBP 0.06 BUY STOP in 2027.02.23 08:02 at 0.85994 (0.66494, 0.86377). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#8 --- CVirtualOrder::Load | Order#9 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.04,80,1.10,0.20,0,6000.00,1190.00,1000,3) CVirtualOrder::Load | Order#10 GBPUSD 0.04 BUY in 2027.02.23 08:02 at 1.26632 (1.20638, 1.27834). 1970.01.01 00:00, 0.000000 CVirtualReceiver::OnOpen#GBPUSD | OPEN VirtualOrder #10 CVirtualOrder::Load | Order#11 --- CVirtualOrder::Load | Order#12 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.11,128,2.00,0.90,220,2000.00,1170.00,1000,3) CVirtualOrder::Load | Order#13 GBPUSD 0.11 BUY STOP in 2027.02.23 08:02 at 1.26852 (1.24852, 1.28028). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#14 --- CVirtualOrder::Load | Order#15 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.07,13,1.50,0.80,550,2500.00,1375.00,1000,3) CVirtualOrder::Load | Order#16 GBPUSD 0.07 BUY STOP in 2027.02.23 08:02 at 1.27182 (1.24682, 1.28563). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#17 --- CVirtualOrder::Load | Order#18 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.04,24,0.10,0.30,330,7500.00,2400.00,24000,3) CVirtualOrder::Load | Order#19 EURUSD 0.04 BUY STOP in 2027.02.23 08:02 at 1.08586 (1.01086, 1.10990). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#20 --- CVirtualOrder::Load | Order#21 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,18,0.20,0.40,220,19500.00,1480.00,6000,3) CVirtualOrder::Load | Order#22 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08476 (0.88976, 1.09960). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#23 --- CVirtualOrder::Load | Order#24 --- CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,128,0.70,0.30,550,3000.00,170.00,42000,3) CVirtualOrder::Load | Order#25 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08806 (1.05806, 1.08980). 1970.01.01 00:00, 0.000000 CVirtualOrder::Load | Order#26 --- CVirtualOrder::Load | Order#27 --- CVirtualAdvisor::Save | OK at 2027.02.23 08:19:48 to SimpleVolumes-27182.csv
我们可以看到,有关开启的虚拟仓位和挂单的数据已成功加载。如果是虚拟仓位,则调用开启事件处理程序,必要时开启真实市场仓位,例如手动关闭的仓位。
一般来说,EA 在重启期间与开启仓位有关的行为是一个相当复杂的问题。例如,如果我们想更改幻数,那么 EA 将不再把之前开启的交易视为自己的交易。是否应该强行关闭?如果我们想替换 EA 版本,则可能需要完全忽略已保存的虚拟仓位的存在,并强行关闭所有开启的仓位。我们每次都应该决定哪种情况最适合自己。这些问题还不是很紧迫,所以我们将推迟到以后再讨论。
可视化
现在是虚拟仓位和挂单可视化的时候了。乍一看,将其作为 CVirtualOrder 虚拟仓位类的扩展来实现似乎很自然。它已经掌握了可视化对象的所有必要信息。它比任何人都清楚自己什么时候需要重新绘制。实际上,第一次实现正是这样完成的。但随后,令人不快的问题开始出现。
其中一个问题是 "我们计划在什么图表上执行可视化?"最简单的答案就是当前图表。但它只适用于 EA 在一个交易品种上运行的情况,且该交易品种与启动 EA 的图表交易品种相匹配。一旦有多个交易品种,在交易品种图表上显示所有虚拟仓位就会变得非常不方便。图表变得一团糟。
换句话说,选择显示图表的问题需要一个解决方案,但它不再与 CVirtualOrder 类对象的主要功能相关。即使没有我们的可视化,它们也能很好地工作。
因此,让我们先不处理这个类,把目光放长远一些。如果我们稍微扩展一下目标,最好能够有选择地启用/禁用按策略或类型分组的虚拟仓位显示,以及实施更详细的数据,例如可见的未平仓价格、止损和止盈、触发时的计划损失和利润,甚至是手动修改这些水平的能力。不过,如果我们开发的是半自动交易面板,而不是严格基于算法策略的 EA,后者会更有用。一般来说,即使是开始一个简单的实现,我们也可以稍微超前一些,想象一下进一步的代码开发。这可以帮助我们选择一个方向,从而减少再次修改已编写代码的可能性。
因此,让我们为所有对象创建一个新的基类,这些对象都将以某种方式与图表的可视化相关联:
//+------------------------------------------------------------------+ //| Basic class for visualizing various objects | //+------------------------------------------------------------------+ class CInterface { protected: static ulong s_magic; // EA magic number bool m_isActive; // Is the interface active? bool m_isChanged; // Does the object have any changes? public: CInterface(); // Constructor virtual void Redraw() = 0; // Draw changed objects on the chart virtual void Changed() { // Set the flag for changes m_isChanged = true; } }; ulong CInterface::s_magic = 0; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CInterface::CInterface() : m_isActive(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)), m_isChanged(true) {}
将代码保存在当前文件夹的 Interface.mqh 中。
在该类的基础上创建两个新类:
- CVirtualChartOrder - 在终端图表上显示一个虚拟仓位或挂单的对象(图形虚拟仓位)。如果图表发生变化,它就能在图表上绘制虚拟仓位;如果在终端中没有打开,带有所需工具的图表就会自动打开。
- CVirtualInterface - EA 界面中所有图形对象的聚合器。目前,它只包含图形虚拟仓位数组。它将处理每次创建虚拟仓位对象时创建图形虚拟仓位对象的问题。它还会接收有关虚拟仓位组成变化的信息,并重新绘制相应的图形虚拟仓位。这样的聚合器将以单个实例的形式存在(实现单例设计模式),并在 CVirtualAdvisor 类中可用。
CVirtualChartOrder 类的外观如下:
//+------------------------------------------------------------------+ //| Graphic virtual position class | //+------------------------------------------------------------------+ class CVirtualChartOrder : public CInterface { CVirtualOrder* m_order; // Associated virtual position (order) CChart m_chart; // Chart object to be displayed // Objects on the chart to display the virtual position CChartObjectHLine m_openLine; // Open price line long FindChart(); // Search/open the desired chart public: CVirtualChartOrder(CVirtualOrder* p_order); // Constructor ~CVirtualChartOrder(); // Destructor bool operator==(const ulong id) { // Comparison operator by Id return m_order.Id() == id; } void Show(); // Show a virtual position (order) void Hide(); // Hide a virtual position (order) virtual void Redraw() override; // Redraw a virtual position (order) };
Redraw() 方法会检查是否需要执行,并在必要时调用显示或隐藏图表虚拟仓位的方法:
//+------------------------------------------------------------------+ //| Redraw a virtual position (order) | //+------------------------------------------------------------------+ void CVirtualChartOrder::Redraw() { if(m_isChanged) { if(m_order.IsOpen()) { Show(); } else { Hide(); } m_isChanged = false; } }
在 Show() 显示方法中,我们首先调用 FindChart() 方法来确定虚拟仓位应该显示在哪个图表上。在这种方法中,我们只需遍历所有开启的图表,直到找到具有匹配交易品种的图表。如果没有找到,我们将打开一个新图表。找到(或打开)的图表存储在 m_chart 属性中。
//+------------------------------------------------------------------+ //| Finding a chart to display | //+------------------------------------------------------------------+ long CVirtualChartOrder::FindChart() { if(m_chart.ChartId() == -1 || m_chart.Symbol() != m_order.Symbol()) { long currChart, prevChart = ChartFirst(); int i = 0, limit = 1000; currChart = prevChart; while(i < limit) { // we probably have no more than 1000 open charts if(ChartSymbol(currChart) == m_order.Symbol()) { return currChart; } currChart = ChartNext(prevChart); // get new chart on the basis of the previous one if(currChart < 0) break; // end of chart list is reached prevChart = currChart; // memorize identifier of the current chart for ChartNext() i++; } // If a suitable chart is not found, then open a new one if(currChart == -1) { m_chart.Open(m_order.Symbol(), PERIOD_CURRENT); } } return m_chart.ChartId(); }
Show() 方法只是绘制一条与开盘价相对应的水平线。其颜色和类型取决于仓位(订单)方向和类型。Hide() 方法将删除这条线。这些方法将进一步丰富类的内容。
将获得的代码保存到当前文件夹下的 VirtualChartOrder.mqh 文件中。
CVirtualInterface 类的实现过程如下:
//+------------------------------------------------------------------+ //| EA GUI class | //+------------------------------------------------------------------+ class CVirtualInterface : public CInterface { protected: // Static pointer to a single class instance static CVirtualInterface *s_instance; CVirtualChartOrder *m_chartOrders[]; // Array of graphical virtual positions //--- Private methods CVirtualInterface(); // Closed constructor public: ~CVirtualInterface(); // Destructor //--- Static methods static CVirtualInterface *Instance(ulong p_magic = 0); // Singleton - creating and getting a single instance //--- Public methods void Changed(CVirtualOrder *p_order); // Handle virtual position changes void Add(CVirtualOrder *p_order); // Add a virtual position virtual void Redraw() override; // Draw changed objects on the chart };
在创建单个 Instance() 的静态方法中,如果传入了非零的幻数值,则添加 s_magic 静态变量的初始化:
//+------------------------------------------------------------------+ //| Singleton - creating and getting a single instance | //+------------------------------------------------------------------+ CVirtualInterface* CVirtualInterface::Instance(ulong p_magic = 0) { if(!s_instance) { s_instance = new CVirtualInterface(); } if(s_magic == 0 && p_magic != 0) { s_magic = p_magic; } return s_instance; }
在处理虚拟仓位打开/关闭事件的方法中,我们将找到其对应的图形虚拟仓位对象,并标记该对象已发生变化:
//+------------------------------------------------------------------+ //| Handle virtual position changes | //+------------------------------------------------------------------+ void CVirtualInterface::Changed(CVirtualOrder *p_order) { // Remember that this position has changes int i; FIND(m_chartOrders, p_order.Id(), i); if(i != -1) { m_chartOrders[i].Changed(); m_isChanged = true; } }
最后,在 Redraw() 界面渲染方法中,循环调用绘制所有图形虚拟仓位的方法:
//+------------------------------------------------------------------+ //| Draw changed objects on a chart | //+------------------------------------------------------------------+ void CVirtualInterface::Redraw() { if(m_isActive && m_isChanged) { // If the interface is active and there are changes // Start redrawing graphical virtual positions FOREACH(m_chartOrders, m_chartOrders[i].Redraw()); m_isChanged = false; // Reset the changes flag } }
将获得的代码保存到当前文件夹的 VirtualInterface.mqh 文件中。
现在只剩下最后的编辑工作了,以便在图表上显示虚拟仓位的子系统能够正常工作。在 CVirtualAdvisor 类中,添加新的 m_interface 属性,该属性将存储显示界面对象的单个实例。我们需要在构造函数中对其进行初始化,并在析构函数中将其删除:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... CVirtualInterface *m_interface; // Interface object to show the status to the user ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") : ... // Initialize the interface with the static interface m_interface(CVirtualInterface::Instance(p_magic)), ... { ... }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { ... delete m_interface; // Remove the interface }
在 OnTick 事件处理程序中,在所有操作之后,添加调用重绘界面的方法,因为这是分时处理中最不重要的部分:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); // Render the interface m_interface.Redraw(); }
在 CVirtualReceiver 类中,还添加了新的 m_interface 属性,该属性将存储显示界面对象的单个实例。我们需要在构造函数中对其进行初始化:
//+------------------------------------------------------------------+ //| Class for converting open volumes to market positions (receiver) | //+------------------------------------------------------------------+ class CVirtualReceiver : public CReceiver { protected: ... CVirtualInterface *m_interface; // Interface object to show the status to the user ... }; //+------------------------------------------------------------------+ //| Closed constructor | //+------------------------------------------------------------------+ CVirtualReceiver::CVirtualReceiver() : m_interface(CVirtualInterface::Instance()), ... {}
在为策略分配所需虚拟仓位数量的方法中,我们将同时把它们添加到界面中:
//+------------------------------------------------------------------+ //| Allocate the necessary amount of virtual positions to strategy | //+------------------------------------------------------------------+ static void CVirtualReceiver::Get(CVirtualStrategy *strategy, // Strategy CVirtualOrder *&orders[], // Array of strategy positions int n // Required number ) { CVirtualReceiver *self = Instance(); // Receiver singleton CVirtualInterface *draw = CVirtualInterface::Instance(); ArrayResize(orders, n); // Expand the array of virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(strategy); // Fill the array with new objects APPEND(self.m_orders, orders[i]); draw.Add(orders[i])) // Register the created virtual position PrintFormat(__FUNCTION__ + " | OK, Strategy orders: %d from %d total", ArraySize(orders), ArraySize(self.m_orders)); }
最后,我们需要在该类中处理打开/关闭虚拟仓位的方法中添加接口提醒:
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) { m_interface.Changed(p_order); ... } //+------------------------------------------------------------------+ //| Handle closing a virtual position | //+------------------------------------------------------------------+ void CVirtualReceiver::OnClose(CVirtualOrder *p_order) { m_interface.Changed(p_order); ... } }
保存对当前文件夹中 VirtualAdvisor.mqh 和 VirtualReceiver.mqh 文件所做的更改。
如果我们现在编译并运行我们的 EA,那么如果有开启的虚拟仓位或挂单,我们就可以看到类似下面的内容:
图 1.在图表上显示虚拟订单和仓位
在这里,虚线显示虚拟挂单:橙色 - 止损卖出,蓝色 - 止损买入,实线显示虚拟仓位:蓝色 - 买入,红色 - 卖出。目前,只能看到开启水平,但将来可以显示得更丰富。
精美的图表
在论坛讨论中,有一种观点认为,文章的读者(至少是其中一部分)首先会看文章的结尾,他们希望看到一张漂亮的图表,显示在测试开发的 EA 时资金的增长情况。如果真的有这样一张图表,那么这将成为我们回到文章开头阅读它的额外动力。
在撰写本文时,我们没有对所使用的演示策略或演示 EA 中的策略实例集进行任何更改。一切与上一篇文章发表时的情况相同。因此,这里没有令人印象深刻的图表。
不过,在撰写这篇文章的同时,我还对其他策略进行了优化和训练,并将在以后发表。它们的测试结果让我们可以说,将大量策略实例整合到一个 EA 中是一种很有前途的方法。
下面是两个 EA 测试运行的示例,其中使用了约 170 个在不同交易品种和时间框架上运行的策略实例。测试时间:2023-01-01 至 2024-02-23。期间数据未用于优化和训练。在资本管理设置中,设定的参数假定一种情况下可接受的回撤率约为 10%,另一种情况下约为 40%。
图 2.测试结果可接受 10%的回撤
图 3.测试结果可接受的回撤率为 40
出现这些结果并不保证今后会再次发生。但我们可以努力使这一结果更有可能实现。我会努力不让这些结果恶化,也不会陷入过度训练的自欺欺人之中。
结论
在本文中,我们所做的工作在一定程度上偏离了主要方向。例如,最近发表的一篇文章 "使用优化算法即时配置 EA 参数 "讨论了自动优化的问题。
尽管如此,保存和加载状态的功能对于任何有可能在真实账户上启动的 EA 来说都是一个重要的组成部分。同样重要的是,它不仅能处理市场仓位,还能处理许多交易策略中使用的挂单。同时,环境评估工作的可视化有助于发现开发阶段的任何实现错误。
感谢您的关注,我们下期再见!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14246



