English Deutsch 日本語
preview
MQL5 交易策略自动化(第 23 部分):带追踪止损与篮子交易的区间补仓系统

MQL5 交易策略自动化(第 23 部分):带追踪止损与篮子交易的区间补仓系统

MetaTrader 5交易 |
42 1
Allan Munene Mutiiria
Allan Munene Mutiiria

引言

上一篇文章(第22部分)中,我们为包络线趋势交易开发了一个区间补仓系统,该系统在MetaQuotes Language 5(MQL5)中利用相对强弱指数(RSI)和包络线指标实现交易自动化,并通过结构化的区间补仓策略来控制亏损。在第23部分,我们对该策略进行了优化:加入了移动止损以动态锁定利润,并引入多篮子系统来高效处理多个交易信号,从而提升了系统在波动市场中的适应性。本文将包括几个方面:

  1. 理解增强型移动止损与多篮子(Multi-Basket)架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

读完本文,您将获得一个功能更完善、具备高级特性的MQL5交易系统,并为进一步测试和定制做好准备。让我们开始吧!


理解增强型移动止损与多篮子(Multi-Basket)架构

我们正在增强的区间补仓策略旨在通过在市场价格反向波动时于指定价格区间内下反向单,将潜在亏损转化为盈利。现在,我们通过两项关键改进来强化它:移动止损和多篮子交易。引入移动止损至关重要,它能在市场朝有利方向运行时锁定利润,既保护了收益又避免了过早平仓,这对于捕捉大幅趋势行情尤为关键。多篮子交易功能同样不可或缺。它能同时管理多个独立信号,在分散风险的同时,提高捕捉交易机会的能力。如下图所示 :

追踪止损架构

为实现上述优化,我们将整合追踪止损机制,根据市场波动动态调整止损位,从而在保留盈利空间的同时有效锁定利润。对于多篮子交易,我们将引入一个系统来处理多个交易实例,每个实例都有其唯一标识符,允许我们一次性追踪和管理多个区间补仓周期而不发生冲突。我们计划将这些特性与现有的相对强弱指标(RSI)和包络线指标相结合,以维持精准的入场点,同时让移动止损和篮子系统协同工作,优化利润保护和交易容量,使策略在各种市场条件下更稳健、更具适应性。请跟随我们,一起将这些改进变为现实 !


在MQL5中的实现

为了在MQL5中实现这些增强,我们为移动止损功能增加了一些额外的用户输入参数,并重命名了最大订单限制上限,因为现在我们要处理多个恢复实例。

input group "======= EA GENERAL SETTINGS ======="
input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE;               // Lot Size Option
input double initialLotSize = 0.01;                                    // Initial Lot Size
input double riskPercentage = 1.0;                                     // Risk Percentage (%)
input int    riskPoints = 300;                                         // Risk Points
input int    baseMagicNumber = 123456789;                              // Base Magic Number
input int    maxInitialPositions = 1;                                  // Maximum Initial Positions (Baskets/Signals)
input double zoneTargetPoints = 600;                                   // Zone Target Points
input double zoneSizePoints = 300;                                     // Zone Size Points
input bool   enableInitialTrailing = true;                             // Enable Trailing Stop for Initial Positions
input int    trailingStopPoints = 50;                                  // Trailing Stop Points
input int    minProfitPoints = 50;                                     // Minimum Profit Points to Start Trailing

我们首先更新MQL5中包络线趋势交易区间补仓系统的输入参数,在“EA通用设置”组下支持移动止损和多篮子交易。我们对输入参数进行了四项关键更改。首先,我们将“magicNumber”重命名为“baseMagicNumber”,设置为123456789,作为为多个交易篮子生成唯一幻数的起点,确保我们的多篮子系统能够分别追踪每个篮子。其次,我们用“maxInitialPositions”(设置为1)替换了“maxOrders”,用于限制初始交易篮子的数量,从而高效管理多个交易信号。

第三,我们添加了布尔变量“enableInitialTrailing”(设置为true),用于启用或禁用初始仓位的移动止损,从而控制这一新的利润锁定功能。第四,我们引入了“trailingStopPoints”(设置为50)和“minProfitPoints”(设置为50),分别定义移动止损距离和启动移动止损所需的最小利润,以实现动态利润保护。这些更改将使我们的系统能够处理多个交易篮子并有效保护利润,为后续增强奠定了基础。我们将高亮显示这些更改,以便更容易追踪和避免混淆。编译后,我们得到以下输入参数集。

NEW INPUTS SET

添加输入参数后,我们现在可以前向声明“MarketZoneTrader”类,以便基类可以访问它,因为我们现在想要处理多个交易实例。

//--- Forward Declaration of MarketZoneTrader
class MarketZoneTrader;

这里,我们引入了“MarketZoneTrader”的前向声明。我们将其放在“BasketManager”类定义之前(我们随后将定义该类),以便它可以引用“MarketZoneTrader”而无需其完整定义。此改动必不可少,因为新的多篮子系统需由“BasketManager”负责创建并管理多个“MarketZoneTrader”实例。通过首先声明“MarketZoneTrader”,我们确保编译器在新类中使用它时能识别出来,从而高效支持多个同时进行的交易周期。然后我们可以定义这个管理器类。

//--- Basket Manager Class to Handle Multiple Traders
class BasketManager {
private:
   MarketZoneTrader* m_traders[];                                        //--- Array of trader instances
   int               m_handleRsi;                                        //--- RSI indicator handle
   int               m_handleEnvUpper;                                   //--- Upper Envelopes handle
   int               m_handleEnvLower;                                   //--- Lower Envelopes handle
   double            m_rsiBuffer[];                                     //--- RSI data buffer
   double            m_envUpperBandBuffer[];                            //--- Upper Envelopes buffer
   double            m_envLowerBandBuffer[];                            //--- Lower Envelopes buffer
   string            m_symbol;                                          //--- Trading symbol
   int               m_baseMagicNumber;                                 //--- Base magic number
   int               m_maxInitialPositions;                             //--- Maximum baskets (signals)

   //--- Initialize Indicators
   bool initializeIndicators() {
      m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, 8, PRICE_CLOSE);
      if (m_handleRsi == INVALID_HANDLE) {
         Print("Failed to initialize RSI indicator");
         return false;
      }
      m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1);
      if (m_handleEnvUpper == INVALID_HANDLE) {
         Print("Failed to initialize upper Envelopes indicator");
         return false;
      }
      m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4);
      if (m_handleEnvLower == INVALID_HANDLE) {
         Print("Failed to initialize lower Envelopes indicator");
         return false;
      }
      ArraySetAsSeries(m_rsiBuffer, true);
      ArraySetAsSeries(m_envUpperBandBuffer, true);
      ArraySetAsSeries(m_envLowerBandBuffer, true);
      return true;
   }

}

为了帮助管理篮子交易,我们定义了“BasketManager”,其私有成员用于管理“MarketZoneTrader”类的多个实例和指标数据。我们创建了“m_traders”,一个“MarketZoneTrader”指针数组,用于存储单独的交易篮子,每个篮子代表一个独立的区间补仓周期。这一改动至关重要,解决了旧版单实例模式无法同时管理多个信号的痛点。我们还声明了“m_handleRsi”、“m_handleEnvUpper”和“m_handleEnvLower”来保存指标句柄,以及“m_rsiBuffer”、“m_envUpperBandBuffer”和“m_envLowerBandBuffer”数组来存储RSI和包络线数据,将指标管理从“MarketZoneTrader”转移到“BasketManager”,以便跨篮子进行集中控制。

此外,我们添加了“m_symbol”来存储交易品种,“m_baseMagicNumber”用于为每个篮子生成唯一编号,以及“m_maxInitialPositions”用于限制活动篮子的数量,这与新的“maxInitialPositions”输入保持一致。在“initializeIndicators”函数中,我们使用iRSI设置RSI指标(周期为8),并使用iEnvelopes设置包络线指标(偏差为0.1的150周期和偏差为1.4的95周期),检查“INVALID_HANDLE”并使用Print记录失败信息。我们使用ArraySetAsSeries将“m_rsiBuffer”、“m_envUpperBandBuffer”和“m_envLowerBandBuffer”配置为时间序列数组。这种新的类结构将使我们能够高效协调多个交易篮子,集中管理指标数据,从而为所有篮子生成一致的信号。接下来我们需要有一个方法来统计所有单独篮子的仓位以便于追踪,并清理篮子 。

//--- Count Active Baskets
int countActiveBaskets() {
   int count = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) {
         count++;
      }
   }
   return count;
}

//--- Cleanup Terminated Baskets
void cleanupTerminatedBaskets() {
   int newSize = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) {
         delete m_traders[i];
         m_traders[i] = NULL;
      }
      if (m_traders[i] != NULL) newSize++;
   }
   MarketZoneTrader* temp[];
   ArrayResize(temp, newSize);
   int index = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL) {
         temp[index] = m_traders[i];
         index++;
      }
   }
   ArrayFree(m_traders);
   ArrayResize(m_traders, newSize);
   for (int i = 0; i < newSize; i++) {
      m_traders[i] = temp[i];
   }
   ArrayFree(temp);
}

这里,我们向“BasketManager”类添加了两个新函数:“countActiveBaskets”和“cleanupTerminatedBaskets”。我们从“countActiveBaskets”函数开始,用于追踪活动交易篮子的数量。我们将“count”变量初始化为0,并使用ArraySize函数循环遍历“m_traders”数组。对于每个非空的“m_traders”条目,我们检查其通过“getCurrentState”获取的状态是否不为“MarketZoneTrader::INACTIVE”。如果处于活动状态,我们就将“count”加一。我们返回“count”以监控当前运行的篮子数量,这对于确保在开启新篮子时数量不超过“m_maxInitialPositions”的限制至关重要。

接下来,我们创建“cleanupTerminatedBaskets”函数以移除不活跃的篮子并优化内存。我们首先通过循环数组来统计“m_traders”中的非空项。如果不为空,并且其“getCurrentState”返回“MarketZoneTrader::INACTIVE”,我们就使用“delete”释放其内存并将该项设置为“NULL”。我们在“newSize”中跟踪剩余非空项的数量。然后,我们创建一个临时的“temp”数组,用ArrayResize将其大小调整为“newSize”,并使用“index”计数器将非空项从“m_traders”复制到“temp”。我们用“ArrayFree”清空“m_traders”,将其大小调整为“newSize”,然后将其从“temp”回传。最后,我们用ArrayFree释放“temp”。这确保我们移除了已终止的交易篮子,保持系统高效并为新的交易做好准备。然后我们构建公共的访问和修改方法,我们将在此更改构造函数析构函数处理类成员和元素初始化与销毁的方式。

public:
   BasketManager(string symbol, int baseMagic, int maxInitPos) {
      m_symbol = symbol;
      m_baseMagicNumber = baseMagic;
      m_maxInitialPositions = maxInitPos;
      ArrayResize(m_traders, 0);
      m_handleRsi = INVALID_HANDLE;
      m_handleEnvUpper = INVALID_HANDLE;
      m_handleEnvLower = INVALID_HANDLE;
   }

   ~BasketManager() {
      for (int i = 0; i < ArraySize(m_traders); i++) {
         if (m_traders[i] != NULL) delete m_traders[i];
      }
      ArrayFree(m_traders);
      cleanupIndicators();
   }

我们从“BasketManager”构造函数开始,该函数接受“symbol”、“baseMagic”和“maxInitPos”作为参数。我们将它们分别赋值给“m_symbol”、“m_baseMagicNumber”和“m_maxInitialPositions”,以设置交易品种、用于篮子唯一标识的基础幻数以及活动篮子的最大数量。我们使用ArrayResize函数将“m_traders”数组初始化为零大小,并将指标句柄——“m_handleRsi”、“m_handleEnvUpper”和“m_handleEnvLower”——设置为“INVALID_HANDLE”,为稍后的指标设置做准备。这个构造函数对于配置多篮子系统至关重要。

接下来,我们创建“~BasketManager”析构函数来清理资源。通常,析构函数以波浪号作为前缀。我们使用ArraySize循环遍历“m_traders”数组,并使用delete删除任何非空的“MarketZoneTrader”实例以释放其内存。然后我们使用ArrayFree清空“m_traders”数组,并调用“cleanupIndicators”释放指标句柄和缓冲区。这确保我们的系统能清爽地关闭,防止EA停止时发生内存泄漏。在旧版本中,我们因发现内存泄漏,不得不在OnDeinit事件中手动补充删除逻辑。而此次开发中,我们已预知该问题,故提前在析构函数中进行了处理。然后我们需要修改初始化代码,以便它能将现有仓位加载到相应的篮子中。以下是实现这一功能的代码。

bool initialize() {
   if (!initializeIndicators()) return false;
   //--- Load existing positions into baskets
   int totalPositions = PositionsTotal();
   for (int i = 0; i < totalPositions; i++) {
      ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket)) {
         if (PositionGetString(POSITION_SYMBOL) == m_symbol) {
            long magic = PositionGetInteger(POSITION_MAGIC);
            if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) {
               //--- Check if basket already exists for this magic
               bool exists = false;
               for (int j = 0; j < ArraySize(m_traders); j++) {
                  if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) {
                     exists = true;
                     break;
                  }
               }
               if (!exists && countActiveBaskets() < m_maxInitialPositions) {
                  createNewBasket(magic, ticket);
               }
            }
         }
      }
   }
   Print("BasketManager initialized with ", ArraySize(m_traders), " existing baskets");
   return true;
}

/*
//--- PREVIOUS INITIALIZATION
int initialize() {
   //--- Initialization Start
   m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number
   int totalPositions = PositionsTotal();                           //--- Get total positions
   
   for (int i = 0; i < totalPositions; i++) {                       //--- Iterate positions
      ulong ticket = PositionGetTicket(i);                          //--- Get ticket
      if (PositionSelectByTicket(ticket)) {                         //--- Select position
         if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic
            if (activateTrade(ticket)) {                            //--- Activate position
               Print("Existing position activated: Ticket=", ticket); //--- Log activation
            } else {
               Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure
            }
         }
      }
   }
   
   m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI
   if (m_handleRsi == INVALID_HANDLE) {                             //--- Check RSI
      Print("Failed to initialize RSI indicator");                  //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes
   if (m_handleEnvUpper == INVALID_HANDLE) {                        //--- Check upper Envelopes
      Print("Failed to initialize upper Envelopes indicator");      //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes
   if (m_handleEnvLower == INVALID_HANDLE) {                        //--- Check lower Envelopes
      Print("Failed to initialize lower Envelopes indicator");      //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   ArraySetAsSeries(m_rsiBuffer, true);                             //--- Set RSI buffer
   ArraySetAsSeries(m_envUpperBandBuffer, true);                    //--- Set upper Envelopes buffer
   ArraySetAsSeries(m_envLowerBandBuffer, true);                    //--- Set lower Envelopes buffer
   
   Print("EA initialized successfully");                            //--- Log success
   return INIT_SUCCEEDED;                                           //--- Return success
   //--- Initialization End
}
*/

这里,我们在“BasketManager”类中实现了更新的“initialize”函数,以支持我们的多篮子交易改进,具体是通过初始化指标并将现有仓位加载到单独的篮子中。我们首先调用“initializeIndicators”来设置RSI和包络线指标,如果失败则返回false,确保我们的系统拥有必要的市场数据。与之前的版本不同,我们之前是在“MarketZoneTrader”的“initialize”函数中直接处理指标设置,现在我们将其集中到“BasketManager”中,以便在多个篮子之间共享指标数据。接下来,我们使用 PositionsTotal 函数检查现有仓位,并循环遍历每个仓位,使用 PositionGetTicket 函数获取其“ticket”。

如果 PositionSelectByTicket 成功,并且仓位的品种(通过PositionGetString获取)与“m_symbol”匹配,我们会验证其通过“PositionGetInteger”获得的编码是否落在“m_baseMagicNumber”到“m_baseMagicNumber + m_maxInitialPositions”范围内。然后,我们通过循环“m_traders”并在非空项上调用“getMagicNumber”,来检查是否已存在该编码的篮子。如果不存在,且“countActiveBaskets”小于“m_maxInitialPositions”,那么我们使用“magic”编码和“ticket”调用“createNewBasket”,将仓位加载到一个新篮子中。最后,我们使用ArraySizea获取“m_traders”的大小,并用“Print”记录已初始化篮子的数量,然后返回true。运行程序时,我们得到以下结果。

BASKETS INITIALIZATION

现在我们可以继续处理报价(tick),在“processTick”函数中,我们需要在每个报价上处理现有篮子,并在确认新信号时创建新篮子,这与之前的版本不同,之前我们只需要根据确认的信号发起交易。

void processTick() {
   //--- Process existing baskets
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL) {
         m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer);
      }
   }
   cleanupTerminatedBaskets();

   //--- Check for new signals on new bar
   if (!isNewBar()) return;

   if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) {
      Print("Error loading RSI data. Reverting.");
      return;
   }
   if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) {
      Print("Error loading upper envelopes data. Reverting.");
      return;
   }
   if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) {
      Print("Error loading lower envelopes data. Reverting.");
      return;
   }

   const int rsiOverbought = 70;
   const int rsiOversold = 30;
   int ticket = -1;
   ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-1;

   double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits());
   double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits());

   if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) {
      if (askPrice > m_envUpperBandBuffer[0]) {
         if (countActiveBaskets() < m_maxInitialPositions) {
            signalType = ORDER_TYPE_BUY;
         }
      }
   } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) {
      if (bidPrice < m_envLowerBandBuffer[0]) {
         if (countActiveBaskets() < m_maxInitialPositions) {
            signalType = ORDER_TYPE_SELL;
         }
      }
   }

   if (signalType != (ENUM_ORDER_TYPE)-1) {
      //--- Create new basket with unique magic number
      int newMagic = m_baseMagicNumber + ArraySize(m_traders);
      if (newMagic < m_baseMagicNumber + m_maxInitialPositions) {
         MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic);
         ticket = newTrader.openInitialOrder(signalType); //--- Open INITIAL position
         if (ticket > 0 && newTrader.activateTrade(ticket)) {
            int size = ArraySize(m_traders);
            ArrayResize(m_traders, size + 1);
            m_traders[size] = newTrader;
            Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType));
         } else {
            delete newTrader;
            Print("Failed to create new basket: Ticket=", ticket);
         }
      } else {
         Print("Maximum initial positions (baskets) reached: ", m_maxInitialPositions);
      }
   }
}

在该函数中,我们首先使用 ArraySize 循环遍历“m_traders”数组,并对每个非空的“MarketZoneTrader”实例调用其“processTick”函数,传入“m_rsiBuffer”、“m_envUpperBandBuffer”和“m_envLowerBandBuffer”来处理每个篮子的逻辑。与之前的版本不同,当时“processTick”直接管理单个交易周期。然后我们调用“cleanupTerminatedBaskets”来移除不活跃的篮子,确保资源的高效利用。接下来,我们只在出现新柱线时使用“isNewBar”检查新的交易信号,如果为false则退出以节省资源。

我们使用 CopyBuffer 将“m_handleRsi”、“m_handleEnvUpper”和“m_handleEnvLower”的指标数据加载到各自的缓冲区中,如果失败则使用“Print”记录错误并退出,这与之前版本中在“MarketZoneTrader”中进行操作不同。我们将“rsiOverbought”设置为70,将“rsiOversold”设置为30,并初始化“ticket”和“signalType”。我们调用SymbolInfoDouble函数通过"SYMBOL_ASK" 和"SYMBOL_BID",获取“askPrice”和“bidPrice”,并使用NormalizeDouble函数进行标准化。

对于买入信号,如果“m_rsiBuffer”指示超卖状态,并且“askPrice”超过“m_envUpperBandBuffer”,则在“countActiveBaskets”低于“m_maxInitialPositions”时将“signalType”设置为ORDER_TYPE_BUY。对于卖出信号,如果“m_rsiBuffer”显示超买状态,并且“bidPrice”低于“m_envLowerBandBuffer”,则将“signalType”设置为ORDER_TYPE_SELL。如果存在有效的“signalType”,我们使用“m_baseMagicNumber”加上“ArraySize(m_traders)”创建一个唯一的magic编码,如果在“m_maxInitialPositions”范围内,我们使用输入参数和新的magic编码,来创建一个新的“MarketZoneTrader”对象。

我们使用“signalType”调用“openInitialOrder”,如果返回的“ticket”有效并且“activateTrade”成功,我们使用 ArrayResize 将新的交易者添加到“m_traders”中,并使用“Print”和 EnumToString 函数记录成功。否则,我们删除该交易者并记录失败,或者记录是否已达到篮子限制。一旦开启了新交易,我们就需要为它们创建新篮子。以下是实现该功能的代码。

private:
   void createNewBasket(long magic, ulong ticket) {
      MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic);
      if (newTrader.activateTrade(ticket)) {
         int size = ArraySize(m_traders);
         ArrayResize(m_traders, size + 1);
         m_traders[size] = newTrader;
         Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket);
      } else {
         delete newTrader;
         Print("Failed to load existing position into basket: Ticket=", ticket);
      }
   }

我们在“BasketManager”类的私有部分实现了“createNewBasket”函数,这是一个新增功能,用于支持我们的多篮子交易改进,通过为现有仓位创建和管理新的交易篮子。我们首先使用输入参数“lotOption”、“initialLotSize”、“riskPercentage”、“riskPoints”、“zoneTargetPoints”、“zoneSizePoints”和提供的“magic”数创建一个名为“newTrader”的新“MarketZoneTrader”实例,以配置一个唯一的交易篮子。回想一下,在之前的版本中,我们在初始化阶段有这个用户输入,因为我们只需要一个区域实例,所以它适用于所有新仓位,但在这种情况下,我们将它组织在新的类实例中。以下是用于快速比较的代码。

//--- PREVIOUS VERSION OF NEW CLASS INSTANCE
//--- Global Instance
MarketZoneTrader *trader = NULL;                                        //--- Declare trader instance

int OnInit() {
   //--- EA Initialization Start
   trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance
   return trader.initialize();                                           //--- Initialize EA
   //--- EA Initialization End
}

然后我们在“newTrader”上使用给定的“ticket”调用“activateTrade”以将现有仓位加载到篮子中。如果成功,我们使用ArraySize获取“m_traders”数组的当前大小,使用ArrayResize将其扩大1个单位,并将“newTrader”添加到其中。我们使用“Print”记录成功,包括“magic”和“ticket”值。如果“activateTrade”失败,我们删除“newTrader”以释放内存,并用“Print”记录失败信息。与旧版的单实例模式不同,该函数现可将现有仓位纳入独立篮子中管理,这正是多篮子系统的核心特性。该类现在使我们能够有效地管理交易篮子。然后让我们继续修改基类,使其能够包含新的多篮子和移动止损功能。让我们从其成员开始。

//--- Modified MarketZoneTrader Class
class MarketZoneTrader {
private:
   enum TradeState { INACTIVE, RUNNING, TERMINATING };

   struct TradeMetrics {
      bool   operationSuccess;
      double totalVolume;
      double netProfitLoss;
   };

   struct ZoneBoundaries {
      double zoneHigh;
      double zoneLow;
      double zoneTargetHigh;
      double zoneTargetLow;
   };

   struct TradeConfig {
      string         marketSymbol;
      double         openPrice;
      double         initialVolume;
      long           tradeIdentifier;
      string         initialTradeLabel;  //--- Label for initial positions
      string         recoveryTradeLabel; //--- Label for recovery positions
      ulong          activeTickets[];
      ENUM_ORDER_TYPE direction;
      double         zoneProfitSpan;
      double         zoneRecoverySpan;
      double         accumulatedBuyVolume;
      double         accumulatedSellVolume;
      TradeState     currentState;
      bool           hasRecoveryTrades;  //--- Flag to track recovery trades
      double         trailingStopLevel;  //--- Virtual trailing stop level
   };

   struct LossTracker {
      double tradeLossTracker;
   };

   TradeConfig           m_tradeConfig;
   ZoneBoundaries        m_zoneBounds;
   LossTracker           m_lossTracker;
   string                m_lastError;
   int                   m_errorStatus;
   CTrade                m_tradeExecutor;
   TradingLotSizeOptions m_lotOption;
   double                m_initialLotSize;
   double                m_riskPercentage;
   int                   m_riskPoints;
   double                m_zoneTargetPoints;
   double                m_zoneSizePoints;
}

这里,我们通过修改“MarketZoneTrader”类(特别是其私有部分)来增强我们的程序,以包含支持移动止损和改进交易标签的新特性。我们保留了核心结构,但对“TradeConfig”结构体进行了关键更改,以与我们的增强策略保持一致。我们保留了“TradeState”枚举,其中包含“INACTIVE”、“RUNNING”和“TERMINATING”状态,以及与之前版本相比未做更改的“TradeMetrics”、“ZoneBoundaries”和“LossTracker”结构体,因为它们继续管理交易状态、绩效指标、区域边界和损失追踪。

在“TradeConfig”结构体中,我们添加了两个新的字符串变量:“initialTradeLabel”和“recoveryTradeLabel”。这些标签使我们能够分别标记初始交易和恢复交易,从而改进每个篮子内的交易识别和追踪,这对于在我们的新系统中管理多个篮子尤为有用。我们还引入了“hasRecoveryTrades”,这是一个布尔值,用于追踪篮子是否包含恢复交易,这对于正确启用或禁用移动止损至关重要。此外,我们添加了“trailingStopLevel”,这是一个双精度数,用于存储每个篮子的虚拟移动止损位,从而为初始交易启用动态利润保护。

在成员变量中,我们保留了“m_tradeConfig”、“m_zoneBounds”、“m_lossTracker”、“m_lastError”、“m_errorStatus”、“m_tradeExecutor”、“m_lotOption”、“m_initialLotSize”、“m_riskPercentage”、“m_riskPoints”、“m_zoneTargetPoints”和“m_zoneSizePoints”的原样,但它们现在支持每个“MarketZoneTrader”实例中新的移动止损和多篮子功能。值得注意的是,我们从类中移除了指标相关变量(如“m_handleRsi”和“m_rsiBuffer”),因为它们现在由“BasketManager”类集中管理,从而简化了每个交易者对单独篮子操作的关注。在构造函数和析构函数中,我们需要稍微更改一些变量,以便它们能处理新特性。

public:
   MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, double targetPts, double sizePts, long magic) {
      m_tradeConfig.currentState = INACTIVE;
      ArrayResize(m_tradeConfig.activeTickets, 0);
      m_tradeConfig.zoneProfitSpan = targetPts * _Point;
      m_tradeConfig.zoneRecoverySpan = sizePts * _Point;
      m_lossTracker.tradeLossTracker = 0.0;
      m_lotOption = lotOpt;
      m_initialLotSize = initLot;
      m_riskPercentage = riskPct;
      m_riskPoints = riskPts;
      m_zoneTargetPoints = targetPts;
      m_zoneSizePoints = sizePts;
      m_tradeConfig.marketSymbol = _Symbol;
      m_tradeConfig.tradeIdentifier = magic;
      m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); //--- Label for initial positions
      m_tradeConfig.recoveryTradeLabel = "EA_RECOVERY_" + IntegerToString(magic); //--- Label for recovery positions
      m_tradeConfig.hasRecoveryTrades = false; //--- Initialize recovery flag
      m_tradeConfig.trailingStopLevel = 0.0; //--- Initialize trailing stop
      m_tradeExecutor.SetExpertMagicNumber(magic);
   }

   ~MarketZoneTrader() {
      ArrayFree(m_tradeConfig.activeTickets);
   }

“MarketZoneTrader”的构造函数增了“magic”参数,用于为每个交易篮子分配唯一编码,区别于旧版本使用的固定编码。为了支持改进的交易标签,我们将“m_tradeConfig.initialTradeLabel”添加为“EA_INITIAL”加上“magic”(通过IntegerToString),将“m_tradeConfig.recoveryTradeLabel”添加为“EA_RECOVERY”加上“magic”,从而能够清晰区分篮子内的初始交易和恢复交易。我们将“m_tradeConfig.hasRecoveryTrades”初始化为false以追踪恢复交易状态,并将“m_tradeConfig.trailingStopLevel”设置为0.0作为虚拟移动止损,这两个都是新增特性。最后,我们使用“magic”通过“SetExpertMagicNumber”配置“m_tradeExecutor”。我们已高亮显示主要更改以便快速识别。

接下来,我们简化了“~MarketZoneTrader”析构函数,相比之下,之前的版本中它被称为“cleanup”。如今我们仅通过ArrayFree清理m_tradeConfig.activeTickets数组,指标的销毁工作已交由BasketManager模块处理,这种优化将析构函数的职责范围收窄,使其仅聚焦于交易篮子专属的资源管理。然后我们可以更新负责激活交易的函数,以便它能初始化初始交易的移动止损位和恢复状态。

bool activateTrade(ulong ticket) {

   m_tradeConfig.hasRecoveryTrades = false;
   m_tradeConfig.trailingStopLevel = 0.0;
   
   //--- THE REST OF THE LOGIC REMAINS
   
   return true;
}

这里,我们只需添加逻辑将第一笔交易的移动止损位初始化为0,恢复状态初始化为false,以表明它是篮子中的第一个仓位。最后,我们可以添加一个函数来开启初始仓位。

int openInitialOrder(ENUM_ORDER_TYPE orderType) {
   //--- Open INITIAL position based on signal
   int ticket;
   double openPrice;
   if (orderType == ORDER_TYPE_BUY) {
      openPrice = NormalizeDouble(getMarketAsk(), Digits());
   } else if (orderType == ORDER_TYPE_SELL) {
      openPrice = NormalizeDouble(getMarketBid(), Digits());
   } else {
      Print("Invalid order type [Magic=", m_tradeConfig.tradeIdentifier, "]");
      return -1;
   }
   double lotSize = 0;
   if (m_lotOption == FIXED_LOTSIZE) {
      lotSize = m_initialLotSize;
   } else if (m_lotOption == UNFIXED_LOTSIZE) {
      lotSize = calculateLotSize(m_riskPercentage, m_riskPoints);
   }
   if (lotSize <= 0) {
      Print("Invalid lot size [Magic=", m_tradeConfig.tradeIdentifier, "]: ", lotSize);
      return -1;
   }
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, m_tradeConfig.initialTradeLabel)) {
      ticket = (int)m_tradeExecutor.ResultOrder();
      Print("INITIAL trade opened [Magic=", m_tradeConfig.tradeIdentifier, "]: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize);
   } else {
      ticket = -1;
      Print("Failed to open INITIAL order [Magic=", m_tradeConfig.tradeIdentifier, "]: Type=", EnumToString(orderType), ", Volume=", lotSize);
   }
   return ticket;
}

我们在“MarketZoneTrader”类的公共部分实现了一个新的“openInitialOrder”函数,以支持我们的多篮子和改进交易标签增强功能,通过为特定交易篮子开启具有清晰标识的初始仓位。我们首先初始化“ticket”和“openPrice”。对于设置为ORDER_TYPE_BUY的“orderType”,我们使用“getMarketAsk”设置“openPrice”,并使用NormalizeDouble和“Digits”对其进行标准化。对于“ORDER_TYPE_SELL”,我们使用“getMarketBid”。如果“orderType”无效,我们使用“Print”记录错误(包括“m_tradeConfig.tradeIdentifier”)并返回-1。

我们根据“m_lotOption”确定“lotSize”:对于“FIXED_LOTSIZE”,我们使用“m_initialLotSize”;对于“UNFIXED_LOTSIZE”,我们使用“m_riskPercentage”和“m_riskPoints”调用“calculateLotSize”。如果“lotSize”无效,我们使用“Print”记录错误并返回-1。然后我们使用“m_tradeExecutor.PositionOpen”开启仓位,参数包括“m_tradeConfig.marketSymbol”、“orderType”、“lotSize”、“openPrice”以及用于初始交易清晰标记的“m_tradeConfig.initialTradeLabel”。成功时,我们使用“ResultOrder”设置“ticket”,并使用“Print”记录交易(包括“m_tradeConfig.tradeIdentifier”和EnumToString函数)。失败时,我们将“ticket”设置为-1并记录错误。最后,我们返回“ticket”。与旧版“openOrder”函数不同,此函数使用全新的“initialTradeLabel”并专注于初始仓位,完美适配多篮子系统架构。编译后,我们得到以下结果。

INITIAL BASKET

从图中我们可以看到,我们可以开启初始交易并为其创建一个新的篮子实例。现在我们需要有移动止损逻辑,以便我们可以管理仓位的移动止损功能。

void evaluateMarketTick() {
   if (m_tradeConfig.currentState == INACTIVE) return;
   if (m_tradeConfig.currentState == TERMINATING) {
      finalizePosition();
      return;
   }
   double currentPrice;
   double profitPoints = 0.0;

   //--- Handle BUY initial position
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {
      currentPrice = getMarketBid();
      profitPoints = (currentPrice - m_tradeConfig.openPrice) / _Point;

      //--- Trailing Stop Logic for Initial Position
      if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) {
         //--- Calculate desired trailing stop level
         double newTrailingStop = currentPrice - trailingStopPoints * _Point;
         //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints
         if (profitPoints >= minProfitPoints + trailingStopPoints) {
            if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop > m_tradeConfig.trailingStopLevel) {
               m_tradeConfig.trailingStopLevel = newTrailingStop;
               Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points");
            }
         }
         //--- Check if price has hit trailing stop
         if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice <= m_tradeConfig.trailingStopLevel) {
            Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " <= TrailingStop=", m_tradeConfig.trailingStopLevel);
            finalizePosition();
            return;
         }
      }

      //--- Zone Recovery Logic
      if (currentPrice > m_zoneBounds.zoneTargetHigh) {
         Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh);
         finalizePosition();
         return;
      } else if (currentPrice < m_zoneBounds.zoneLow) {
         Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow);
         triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);
      }
   }
   //--- Handle SELL initial position
   else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {
      currentPrice = getMarketAsk();
      profitPoints = (m_tradeConfig.openPrice - currentPrice) / _Point;

      //--- Trailing Stop Logic for Initial Position
      if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) {
         //--- Calculate desired trailing stop level
         double newTrailingStop = currentPrice + trailingStopPoints * _Point;
         //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints
         if (profitPoints >= minProfitPoints + trailingStopPoints) {
            if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop < m_tradeConfig.trailingStopLevel) {
               m_tradeConfig.trailingStopLevel = newTrailingStop;
               Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points");
            }
         }
         //--- Check if price has hit trailing stop
         if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice >= m_tradeConfig.trailingStopLevel) {
            Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " >= TrailingStop=", m_tradeConfig.trailingStopLevel);
            finalizePosition();
            return;
         }
      }

      //--- Zone Recovery Logic
      if (currentPrice < m_zoneBounds.zoneTargetLow) {
         Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow);
         finalizePosition();
         return;
      } else if (currentPrice > m_zoneBounds.zoneHigh) {
         Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh);
         triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);
      }
   }
}

这里,我们通过更新“evaluateMarketTick”函数来增强程序,纳入移动止损逻辑,同时保留现有的区间补仓逻辑。我们首先检查“m_tradeConfig.currentState”是否为“INACTIVE”或“TERMINATING”,然后按原样退出或调用“finalizePosition”。对于买入仓位(“m_tradeConfig.direction”为ORDER_TYPE_BUY),我们使用“getMarketBid”获取“currentPrice”,并将“profitPoints”计算为“currentPrice”与“m_tradeConfig.openPrice”之差除以“_Point”。新的移动止损逻辑代码会检查“enableInitialTrailing”是否为true、“m_tradeConfig.hasRecoveryTrades”是否为false,以及“profitPoints”是否达到或超过“minProfitPoints”。如果是,我们通过从“currentPrice”减去“trailingStopPoints”乘以“_Point”来计算“newTrailingStop”。如果“profitPoints”也超过“minProfitPoints”加上“trailingStopPoints”,并且“m_tradeConfig.trailingStopLevel”为0.0或小于“newTrailingStop”,我们就更新“m_tradeConfig.trailingStopLevel”并用“Print”记录。

如果“m_tradeConfig.trailingStopLevel”已设置,并且“currentPrice”跌破该位,我们就记录触发并调用“finalizePosition”平仓。区间补仓逻辑保持不变:如果“currentPrice”超过“m_zoneBounds.zoneTargetHigh”,则平仓;如果跌破“m_zoneBounds.zoneLow”,则使用“triggerRecoveryTrade”触发卖出恢复交易。

对于卖出仓位(“m_tradeConfig.direction”为ORDER_TYPE_SELL),我们使用“getMarketAsk”获取“currentPrice”,并反向计算“profitPoints”。移动止损逻辑与买入情况类似,通过将“trailingStopPoints”乘以_Point加上“currentPrice”来设置“newTrailingStop”,在满足条件时更新“m_tradeConfig.trailingStopLevel”,并在“currentPrice”超过该位时平仓。区间补仓逻辑则在“currentPrice”低于“m_zoneBounds.zoneTargetLow”时平仓,或在高于“m_zoneBounds.zoneHigh”时触发买入恢复交易。我们不启用原生的物理移动止损机制,以保留对风控策略的完全自主控制权。这样,我们就能够保持所有实例受到监控和管理。以下是运行程序后移动止损功能的输出结果。

TRAILING STOP INSTANCE

从图中我们可以看到,我们可以移动仓位,并在价格回落至移动止损位时平仓。最后,我们只需创建一个篮子管理器实例,然后使用它进行全局管理。

//--- Global Instance
BasketManager *manager = NULL;

int OnInit() {
   manager = new BasketManager(_Symbol, baseMagicNumber, maxInitialPositions);
   if (!manager.initialize()) {
      delete manager;
      manager = NULL;
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) {
   if (manager != NULL) {
      delete manager;
      manager = NULL;
      Print("EA deinitialized");
   }
}

void OnTick() {
   if (manager != NULL) {
      manager.processTick();
   }
}

我们更新了全局实例和事件处理器,使用新的“BasketManager”类,替换了之前版本中对“MarketZoneTrader”的使用,以支持我们的多篮子交易改进,即通过集中管理多个交易篮子。我们首先声明一个指向“BasketManager”类的全局“manager”指针,并将其初始化为“NULL”,而不是之前指向“MarketZoneTrader”的“trader”指针。这一改动至关重要,它改变了旧版的单实例模式,允许通过单一管理器统筹管理多个交易篮子。

OnInitnit事件处理器中,我们为“manager”创建一个新的“BasketManager”实例,传入“_Symbol”、“baseMagicNumber”和“maxInitialPositions”,以配置当前图表、唯一篮子标识和最大篮子数。我们调用“manager.initialize”来设置指标并加载现有仓位,如果失败,则删除“manager”,将其设置为“NULL”,并返回INIT_FAILED。成功时,我们返回“INIT_SUCCEEDED”。

在“OnDeinit”事件处理器中,我们检查“manager”是否不为“NULL”,然后使用“delete”删除它,将其设置为“NULL”,并使用“Print”记录反初始化信息。在OnTick中,我们检查“manager”是否不为“NULL”,并调用“manager.processTick”来处理所有篮子的市场报价,替换之前对“trader.processTick”的调用。这集中了多个篮子的报价处理,增强了系统管理并发交易信号的能力。编译完成后,我们得到了以下结果。

FINAL TRADES

从图中我们可以看到,我们可以创建单独的信号篮子并管理它们,这些篮子具有由所提供的magic编码构建的不同标签。剩下的事情就是对该程序进行回测,这将在下一节中处理。


回测

经过彻底的回测后,我们得到以下结果。

回测结果图形:

图形

回测报告:

报告


结论

总之,我们通过引入移动止损和多篮子交易系统,增强了基于MQL5包络线趋势交易的区间补仓系统,在第22部分的基础上构建了“BasketManager”和更新的“MarketZoneTrader”函数等新组件。这些改进提供了一个更灵活、更稳健的交易框架,您可以通过调整“trailingStopPoints”或“maxInitialPositions”等参数进一步自定义 。

免责声明:本文仅用于教学目的。交易存在重大财务风险,市场波动可能导致损失。在将交易程序部署到实盘环境前,全面的历史回测与严谨的风控管理是必不可少的环节。

依托这些优化升级,你可以迭代完善这套交易系统,也可以借鉴其架构思路开发全新策略,持续进阶你的量化交易之路。祝您交易愉快!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18778

最近评论 | 前往讨论 (1)
Tyrone Chan
Tyrone Chan | 31 8月 2025 在 07:20

先生,没有打开初始卖出交易。

这与交易逻辑有关吗?

三角波与锯齿波:交易者的分析利器 三角波与锯齿波:交易者的分析利器
波浪分析是技术分析中常用的方法之一。本文聚焦两种非传统波浪形态:三角波与锯齿波。这些形态是众多专为市场价格分析设计的技术指标的基础。
从基础到中级:指标(一) 从基础到中级:指标(一)
在本文中,我们将创建第一个完全实用和功能齐全的指标。目标不是展示如何创建应用程序,而是帮助您了解如何开发自己的想法,并让您有机会以安全、简单和实用的方式应用它们。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流 外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流
您想利用利率差异获利吗?本文将探讨如何通过外汇掉期套利实现每晚稳定盈利,并构建抗市场波动的投资组合。