English Deutsch 日本語
preview
MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统

MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统

MetaTrader 5交易 |
18 2
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

前一篇文章(第二十一部分)中,我们探讨了基于神经网络的交易策略,通过自适应学习率优化,提升了MetaQuotes Language 5(MQL5)中对市场走势的预测精度。在第二十二部分中,我们将重点转向构建一个与包络线趋势交易策略集成的区间补仓系统,该系统结合相对强弱指数(RSI)和包络线指标,实现交易自动化并有效管理亏损。我们将涵盖以下主题:

  1. 理解区域补仓包络线趋势架构
  2. 在MQL5中的实现
  3. 回测
  4. 结论

阅读完本文,您将获得一个专为动态市场环境设计的稳健的MQL5交易系统,可直接投入实盘测试——让我们开始深入探讨吧!


理解区域补仓包络线趋势架构

区间补仓是一种智能交易策略,旨在市场走势不利时通过开立反向仓位,将浮亏转化为盈利,最终实现扭亏为为盈或保本离场。假设您预期某货币对上涨而买入,但价格不涨反跌——区间补仓策略会设定一个价格区间(即“补仓区间”),一旦价格触及至该区间,系统就会自动创建反向仓位以抵消损失。我们计划在MQL5中开发一套自动化交易系统,将这一概念应用于外汇市场,在控制风险的同时最大化收益。

为实现这一目标,我们将利用两种技术指标精准捕捉最佳入场点。首先是市场动能指标,仅在价格出现明确的单边趋势时触发交易,从而过滤掉震荡行情中的无效信号。其次是包络线指标,通过在移动平均线上下绘制波动通道,识别价格的超买超卖区域,提示潜在的反弹入场点。两者相互配合,能有效捕捉趋势中价格即将反转的高概率交易机会。

策略整体逻辑如下:当指标发出反转信号时(例如价格以强劲动能触及包络线通道边缘),系统启动首笔交易。如果市场走势与预期相悖,系统将激活区间补仓机制,在预设的价格区间内开立反向仓位,仓位规模经过精密计算,以平衡风险与补仓需求。我们将限制交易次数,避免过度交易,严守系统准则。此配置既能抓住趋势机会,又能为不利行情提供安全保障,无论市场波动剧烈还是平稳运行,系统都能灵活应对。请跟随我们实现并验证这一规划!具体实现计划如下:

策略规划


在MQL5中的实现

要在MQL5中创建该程序,请打开MetaEditor,进入导航器,找到“指标”文件夹,点击“新建”选项卡,并按照提示创建文件。在编码环境中完成基础搭建后,我们首先会声明一些 输入参数,以便轻松控制程序的核心参数值。

//+------------------------------------------------------------------+
//|                 Envelopes Trend Bounce with Zone Recovery EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>                                             //--- Include trade library

enum TradingLotSizeOptions { FIXED_LOTSIZE, UNFIXED_LOTSIZE };         //--- Define lot size options

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    magicNumber = 123456789;                                  // Magic Number
input int    maxOrders = 1;                                            // Maximum Initial Positions
input double zoneTargetPoints = 600;                                   // Zone Target Points
input double zoneSizePoints = 300;                                     // Zone Size Points
input bool   restrictMaxOrders = true;                                 // Apply Maximum Orders Restriction

在此阶段,我们通过搭建核心组件和设置用户可配置参数,为MQL5中包络线趋势交易区间补仓系统奠定基础。首先引入<Trade/Trade.mqh>库文件,该库提供了 "CTrade"类,用于执行开仓、平仓等交易操作。此库的引入至关重要——它为我们的智能交易系统(EA)配备了与市场无缝交互所需的工具,尤其是订单初始化功能。文件引入方式如下:

MQL5交易操作文件

随后,我们定义了一个名为"TradingLotSizeOptions"的枚举,包含两个选项:固定手数"FIXED_LOTSIZE"和动态手数"UNFIXED_LOTSIZE"。这样使系统能够为用户提供两种仓位管理方式:一种是恒定手数交易,另一种是根据风险参数动态调整手数,从而灵活适配不同交易风格的需求。接下来,我们在“EA通用设置”分组下配置输入参数,用户可以在MetaTrader 5平台中直接修改这些值。

"lotOption"(默认值为"UNFIXED_LOTSIZE")——决定交易使用固定手数还是基于风险的动态手数。"initialLotSize"(默认值为0.01)——固定手数模式下的每笔交易手数;riskPercentage(默认值为1.0%)和riskPoints(默认值为300点)——动态手数模式下,分别定义账户余额的百分比风险和止损距离。这些参数共同控制每笔交易的风险敞口,确保EA与用户的风险承受能力保持一致。

我们为EA分配了一个唯一的"magicNumber"(123456789),用于标识该EA执行的所有交易,通过设置"maxOrders"(默认值为1)和"restrictMaxOrders"(默认值为true)这两个输入参数,我们限制了初始仓位的数量,防止EA一次性开仓过多。最后,"zoneTargetPoints"(默认值为600点)和"zoneSizePoints"(默认值为300点)分别设定了区间补仓策略的盈利目标和补仓区间大小(以点数为单位),明确了该策略的边界条件。编译后,我们会得到以下输出。

已加载的输入参数

在完成输入参数的加载后,我们即可开始构建整个系统的核心逻辑框架。由于我们计划采用面向对象编程( OOP)方法,首先需要声明一些后续会用到的结构体和类。

class MarketZoneTrader {
private:
   //--- Trade State Definition
   enum TradeState { INACTIVE, RUNNING, TERMINATING };                 //--- Define trade lifecycle states

   //--- Data Structures
   struct TradeMetrics {
      bool   operationSuccess;                                         //--- Track operation success
      double totalVolume;                                              //--- Sum closed trade volumes
      double netProfitLoss;                                            //--- Accumulate profit/loss
   };

   struct ZoneBoundaries {
      double zoneHigh;                                                 //--- Upper recovery zone boundary
      double zoneLow;                                                  //--- Lower recovery zone boundary
      double zoneTargetHigh;                                           //--- Upper profit target
      double zoneTargetLow;                                            //--- Lower profit target
   };

   struct TradeConfig {
      string         marketSymbol;                                     //--- Trading symbol
      double         openPrice;                                        //--- Position entry price
      double         initialVolume;                                    //--- Initial trade volume
      long           tradeIdentifier;                                  //--- Magic number
      string         tradeLabel;                                       //--- Trade comment
      ulong          activeTickets[];                                  //--- Active position tickets
      ENUM_ORDER_TYPE direction;                                       //--- Trade direction
      double         zoneProfitSpan;                                   //--- Profit target range
      double         zoneRecoverySpan;                                 //--- Recovery zone range
      double         accumulatedBuyVolume;                             //--- Total buy volume
      double         accumulatedSellVolume;                            //--- Total sell volume
      TradeState     currentState;                                     //--- Current trade state
   };

   struct LossTracker {
      double tradeLossTracker;                                         //--- Track cumulative profit/loss
   };
};

在此阶段,我们通过实现"MarketZoneTrader",为MQL5中包络线趋势交易系统定义核心架构,重点聚焦其私有部分(包含交易状态定义与数据结构)。该逻辑将系统化地组织管理交易、追踪补仓区间及监控绩效所需的关键组件。我们首先定义"MarketZoneTrader"类——作为EA的主干,封装 整个交易策略的逻辑实现。

在其私有部分中,我们引入了"TradeState"枚举 ,定义了三种状态:"INACTIVE"(闲置)、"RUNNING"(运行中)和"TERMINATING"(终止中)。这些状态使我们能够追踪交易操作的生命周期,确保实时掌握EA是处于空闲状态、积极管理持仓,还是正在平仓。这样对于维持交易流程控制至关重要,因为它能帮助我们协调诸如开启补仓交易或完成持仓清算等关键操作。

接下来,我们创建"TradeMetrics"结构体,用于存储交易的关键绩效数据。该结构体包含:"operationSuccess"——追踪交易操作(如平仓)是否成功;"totalVolume"——累计已平仓交易的总手数;"netProfitLoss"——累积这些交易的盈亏总额。此结构体帮助我们评估交易操作的结果,为补仓或平仓阶段的表现提供清晰的图示。

随后,我们定义"ZoneBoundaries"结构体,用于存储区间补仓策略的价格水平。"zoneHigh"和 "zoneLow"标记补仓区间的上下边界(在此区域执行反向交易以减少损失)。"zoneTargetHigh"和"zoneTargetLow"设定区间上方和下方的盈利目标(达到此获利水平时平仓)这些边界是策略的核心,因为它们决定了何时触发补仓操作或平仓。以下是可视化示意图,帮助您理解需要该结构体的原因:

区间样本

接下来,我们通过"TradeConfig"结构体存储交易的核心配置参数。其中包括:货币对的交易品种"marketSymbol",入场价格"openPrice",初始交易规模"initialVolume"。"tradeIdentifier"保存唯一标识magic数字, "tradeLabel"添加交易注释以便识别。"activeTickets"数组跟踪持仓订单号,"direction"指定交易方向是买入还是卖出。还包含以价格单位定义利润目标和补仓区间大小的"zoneProfitSpan"和"zoneRecoverySpan",以及监控每种交易类型总成交量的"accumulatedBuyVolume"和"accumulatedSellVolume" 。"currentState"变量使用"TradeState"枚举跟踪交易状态,将所有要素串联在一起。

最后,我们添加包含单个"tradeLossTracker"变量的"LossTracker"结构体,用于监控跨交易的累计盈亏。这样帮助我们评估补仓操作的资金影响,确保在亏损过大时能够调整策略。接下来,我们还可以定义一些辅助成员变量,用于存储其他次要但必要的交易信息。

//--- Member Variables
TradeConfig           m_tradeConfig;                                //--- Store trade configuration
ZoneBoundaries        m_zoneBounds;                                 //--- Store zone boundaries
LossTracker           m_lossTracker;                                //--- Track profit/loss
string                m_lastError;                                  //--- Store error message
int                   m_errorStatus;                                //--- Store error code
CTrade                m_tradeExecutor;                              //--- Manage trade execution
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
TradingLotSizeOptions m_lotOption;                                  //--- Lot size option
double                m_initialLotSize;                             //--- Fixed lot size
double                m_riskPercentage;                             //--- Risk percentage
int                   m_riskPoints;                                 //--- Risk points
int                   m_maxOrders;                                  //--- Maximum positions
bool                  m_restrictMaxOrders;                          //--- Position restriction flag
double                m_zoneTargetPoints;                           //--- Profit target points
double                m_zoneSizePoints;                             //--- Recovery zone points

我们在"MarketZoneTrader"类的private私有区域定义核心成员变量,用于管理交易配置、补仓区间和指标数据。我们使用"m_tradeConfig"("TradeConfig"结构体)存储交易细节(如品种、方向),通过"m_zoneBounds"("ZoneBoundaries"结构体)记录补仓区间和盈利目标价格,并使用"m_lossTracker"("LossTracker"结构体)跟踪盈亏状态。在错误处理方面,"m_lastError"(字符串)和"m_errorStatus" (整型)负责记录错误信息,而"m_tradeExecutor"("CTrade"类实例)负责处理订单操作。

指标句柄——"m_handleRsi"、"m_handleEnvUpper"、"m_handleEnvLower"——访问 RSI 和包络线数据,"m_rsiBuffer"、"m_envUpperBandBuffer"和 "m_envLowerBandBuffer"数组存储其数值。我们将输入设置存储于"m_lotOption"("TradingLotSizeOptions")、 "m_initialLotSize"、"m_riskPercentage"、"m_riskPoints"、"m_maxOrders"、"m_restrictMaxOrders"、"m_zoneTargetPoints"和"m_zoneSizePoints"中,以控制手数规模、持仓限制和区域大小。这些变量构成了管理交易和指标的主干,为后续交易逻辑做好准备。接下来,我们需要定义一些在程序中会频繁使用的辅助函数。

//--- Error Handling
void logError(string message, int code) {
   //--- Error Logging Start
   m_lastError = message;                                           //--- Store error message
   m_errorStatus = code;                                            //--- Store error code
   Print("Error: ", message);                                       //--- Log error to Experts tab
   //--- Error Logging End
}

//--- Market Data Access
double getMarketVolumeStep() {
   //--- Volume Step Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); //--- Retrieve broker's volume step
   //--- Volume Step Retrieval End
}

double getMarketAsk() {
   //--- Ask Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); //--- Retrieve ask price
   //--- Ask Price Retrieval End
}

double getMarketBid() {
   //--- Bid Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); //--- Retrieve bid price
   //--- Bid Price Retrieval End
}

在此阶段,我们添加了用于错误处理和市场数据访问的关键实用函数。“logError”函数将“message”存储在“m_lastError”中,将“code”存储在“m_errorStatus”中,并通过Print将消息记录到“专家”选项卡中,以便进行调试。“getMarketVolumeStep”函数使用SymbolInfoDouble函数并传入参数SYMBOL_VOLUME_STEP,来获取针对“m_tradeConfig.marketSymbol”的经纪商交易量增量,从而确保交易规模有效。“getMarketAsk”和“getMarketBid”函数分别使用“SymbolInfoDouble”函数并传入参数SYMBOL_ASK和“SYMBOL_BID”,来获取卖出价和买入价,以确保交易定价准确。

现在,我们定义用于执行交易操作的主要函数。让我们先从一些有助于初始化、存储交易票据(以便跟踪和监控交易操作)以及平仓的函数开始,因为这部分逻辑相对不那么复杂。

//--- Trade Initialization
bool configureTrade(ulong ticket) {
   //--- Trade Configuration Start
   if (!PositionSelectByTicket(ticket)) {                               //--- Select position by ticket
      logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); //--- Log selection failure
      return false;                                                     //--- Return failure
   }
   m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL);     //--- Set symbol
   m_tradeConfig.tradeLabel = __FILE__;                                 //--- Set trade comment
   m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC);  //--- Set magic number
   m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);   //--- Set direction
   m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN);    //--- Set entry price
   m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME);    //--- Set initial volume
   m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number for executor
   return true;                                                         //--- Return success
   //--- Trade Configuration End
}

//--- Trade Ticket Management
void storeTradeTicket(ulong ticket) {
   //--- Ticket Storage Start
   int ticketCount = ArraySize(m_tradeConfig.activeTickets);        //--- Get ticket count
   ArrayResize(m_tradeConfig.activeTickets, ticketCount + 1);       //--- Resize ticket array
   m_tradeConfig.activeTickets[ticketCount] = ticket;               //--- Store ticket
   //--- Ticket Storage End
}

//--- Trade Execution
ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, double tradeVolume, double price) {
   //--- Trade Opening Start
   ulong ticket = 0;                                                //--- Initialize ticket
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, 0, 0, m_tradeConfig.tradeLabel)) { //--- Open position
      ticket = m_tradeExecutor.ResultOrder();                       //--- Get ticket
   } else {
      Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); //--- Log failure
   }
   return ticket;                                                   //--- Return ticket
   //--- Trade Opening End
}

//--- Trade Closure
void closeActiveTrades(TradeMetrics &metrics) {
   //--- Trade Closure Start
   for (int i = ArraySize(m_tradeConfig.activeTickets) - 1; i >= 0; i--) {    //--- Iterate tickets in reverse
      if (m_tradeConfig.activeTickets[i] > 0) {                               //--- Check valid ticket
         if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { //--- Close position
            m_tradeConfig.activeTickets[i] = 0;                               //--- Clear ticket
            metrics.totalVolume += m_tradeExecutor.ResultVolume();            //--- Accumulate volume
            if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { //--- Check buy position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); //--- Calculate buy profit
            } else {                                                          //--- Handle sell position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); //--- Calculate sell profit
            }
         } else {
            metrics.operationSuccess = false;                                  //--- Mark failure
            Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); //--- Log failure
         }
      }
   }
   //--- Trade Closure End
}

//--- Bar Detection
bool isNewBar() {
   //--- New Bar Detection Start
   static datetime previousTime = 0;                                      //--- Store previous bar time
   datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), 0); //--- Get current bar time
   bool result = (currentTime != previousTime);                           //--- Check for new bar
   previousTime = currentTime;                                            //--- Update previous time
   return result;                                                         //--- Return new bar status
   //--- New Bar Detection End
}

在此阶段,我们深入探讨程序的核心逻辑,编写用于设置交易、跟踪持仓头寸、执行订单、平仓以及把握操作时机的函数。首先,我们创建“configureTrade”函数,为给定的订单编号"ticket"做好交易准备。首先,我们尝试通过PositionSelectByTicket函数选择该持仓头寸。如果操作失败,我们会使用“logError”函数记录问题,并返回false退出。如果操作成功,我们会用详细信息填充“m_tradeConfig”:使用PositionGetString函数获取市场交易品种“marketSymbol)”,将交易标签“tradeLabel”设置为__FILE__(即当前源文件名),并从PositionGetInteger函数中提取交易标识符"tradeIdentifier"和交易方向"direction",同时将“direction”转换为ENUM_ORDER_TYPE类型。接下来,我们使用PositionGetDouble函数设置开仓价格"openPrice"和初始交易量"initialVolume",并通过"SetExpertMagicNumber"为"m_tradeExecutor"打上标签,确保我们的交易准备就绪。

接下来,我们创建“storeTradeTicket”函数,以便对未平仓头寸进行有序管理。我们使用ArraySize函数检查“m_tradeConfig.activeTickets”数组的大小,然后使用ArrayResize函数将数组扩容一个位置,并将新的“ticket”存入该位置,这样就能随时掌握哪些交易处于活跃状态。紧接着,我们创建“openMarketTrade”函数,用于在市场中执行下单操作。我们调用“m_tradeExecutor.PositionOpen”方法,并传入"tradeDirection"、"tradeVolume"、"price"以及"m_tradeConfig"的详细信息如果下单成功,我们将“ResultOrder”返回的“ticket”进行赋值;如果下单失败,则使用“Print”函数记录错误信息,确保交易执行过程严谨无误。

接下来,我们通过“closeActiveTrades”函数处理平仓操作。我们逆向遍历“m_tradeConfig.activeTickets”数组,通过“m_tradeExecutor.PositionClose”方法逐个将有效订单平仓。当某笔订单被成功平仓时,我们清空该订单编号,将“ResultVolume”累加到“metrics.totalVolume”中,并利用“PositionGetInteger”和“PositionGetDouble”函数检查交易方向,进而计算净盈亏“metrics.netProfitLoss”。如果在平仓过程中出现任何问题,我们将“metrics.operationSuccess”设置为false,并使用Print函数记录错误信息,确保能追踪每个操作结果。

最后,我们添加“isNewBar”函数,以实现每根K线仅交易一次,从而降低资源占用。通过iTime函数获取“m_tradeConfig.marketSymbol”当前K线的开盘时间,将其与“previousTime”进行比较。如果时间不同,则更新“previousTime”,这样我们就能知道何时出现了新K线,进而检查是否有交易信号。最后,我们还需要一个用于计算交易量的函数,以及一个用于执行开仓操作的函数。

//--- Lot Size Calculation
double calculateLotSize(double riskPercent, int riskPips) {
   //--- Lot Size Calculation Start
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100;                //--- Calculate risk amount
   double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE);   //--- Get tick size
   double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value
   if (tickSize == 0 || tickValue == 0) {                           //--- Validate tick data
      Print("Invalid tick size or value");                          //--- Log invalid data
      return -1;                                                    //--- Return invalid lot
   }
   double lotValue = (riskPips * _Point) / tickSize * tickValue;    //--- Calculate lot value
   if (lotValue == 0) {                                             //--- Validate lot value
      Print("Invalid lot value");                                   //--- Log invalid lot
      return -1;                                                    //--- Return invalid lot
   }
   return NormalizeDouble(riskMoney / lotValue, 2);                 //--- Return normalized lot size
   //--- Lot Size Calculation End
}

//--- Order Execution
int openOrder(ENUM_ORDER_TYPE orderType, double stopLoss, double takeProfit) {
   //--- Order Opening Start
   int ticket;                                                      //--- Initialize ticket
   double openPrice;                                                //--- Initialize open price
   
   if (orderType == ORDER_TYPE_BUY) {                               //--- Check buy order
      openPrice = NormalizeDouble(getMarketAsk(), Digits());        //--- Set buy price
   } else if (orderType == ORDER_TYPE_SELL) {                       //--- Check sell order
      openPrice = NormalizeDouble(getMarketBid(), Digits());        //--- Set sell price
   } else {
      Print("Invalid order type");                                  //--- Log invalid type
      return -1;                                                    //--- Return invalid ticket
   }
   
   double lotSize = 0;                                              //--- Initialize lot size
   
   if (m_lotOption == FIXED_LOTSIZE) {                              //--- Check fixed lot
      lotSize = m_initialLotSize;                                   //--- Use fixed lot size
   } else if (m_lotOption == UNFIXED_LOTSIZE) {                     //--- Check dynamic lot
      lotSize = calculateLotSize(m_riskPercentage, m_riskPoints);   //--- Calculate risk-based lot
   }
   
   if (lotSize <= 0) {                                              //--- Validate lot size
      Print("Invalid lot size: ", lotSize);                         //--- Log invalid lot
      return -1;                                                    //--- Return invalid ticket
   }
   
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, __FILE__)) { //--- Open position
      ticket = (int)m_tradeExecutor.ResultOrder();                  //--- Get ticket
      Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log success
   } else {
      ticket = -1;                                                  //--- Set invalid ticket
      Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log failure
   }
   
   return ticket;                                                   //--- Return ticket
   //--- Order Opening End
}

我们首先从“calculateLotSize”函数入手,该函数根据风险参数确定交易手数。首先,我们使用AccountInfoDouble函数结合ACCOUNT_BALANCE参数,计算风险金额"riskMoney",即账户余额的某个百分比乘以风险百分比"riskPercent"。接下来,我们使用SymbolInfoDouble函数,分别传入“SYMBOL_TRADE_TICK_SIZE”和“SYMBOL_TRADE_TICK_VALUE”参数,获取“m_tradeConfig.marketSymbol”的"tickSize"和"tickValue"。如果其中任意一个值为0,我们使用“Print”函数记录错误信息,并返回-1,以避免无效计算。然后,我们利用"riskPips"、_Point,、"tickSize"和"tickValue"来计算每手价值"lotValue",如果计算结果为0,我们再次记录错误信息并返回-1。最后,我们使用NormalizeDouble将计算出的交易手数保留两位小数,并返回该值,确保其符合经纪商的要求。

接下来,我们创建“openOrder”函数以执行下单操作。首先,我们初始化“ticket”和“openPrice”,然后检查“orderType”。对于ORDER_TYPE_BUY,我们使用“getMarketAsk”函数获取当前卖出价,并结合Digits通过“NormalizeDouble”函数设置“openPrice”;而对于“ORDER_TYPE_SELL”,则使用“getMarketBid”函数获取当前买入价。如果“orderType”无效,我们使用“Print”函数记录错误信息并返回-1。我们根据“m_lotOption”确定“lotSize”:如果为“FIXED_LOTSIZE”,则使用“m_initialLotSize”;如果为“UNFIXED_LOTSIZE”,则调用“calculateLotSize”函数,传入“m_riskPercentage”和“m_riskPoints”进行计算。如果"lotSize"无效,我们使用“Print”函数记录错误信息并返回-1。接下来,我们通过“m_tradeExecutor.PositionOpen”方法开仓,传入"m_tradeConfig.marketSymbol"、"orderType"、"lotSize"、"openPrice",以及“FILE”作为交易备注。如果开仓成功,我们将“ResultOrder”返回的交易单号赋值给“ticket”,并使用“Print”函数记录成功信息;如果开仓失败,我们将“ticket”设为-1,并记录错误信息。最后,我们返回订单编号。

完成上述操作后,我们需要初始化系统值。虽然可以通过一个专用函数来实现,但为了保持代码简洁,我们将使用构造函数。建议将构造函数定义为公共访问修饰符,以便在程序的任何位置都能调用。同时,我们也一并定义析构函数

public:
   //--- Constructor
   MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, int maxOrds, bool restrictOrds, double targetPts, double sizePts) {
      //--- Constructor Start
      m_tradeConfig.currentState = INACTIVE;                           //--- Set initial state
      ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Initialize ticket array
      m_tradeConfig.zoneProfitSpan = targetPts * _Point;               //--- Set profit target
      m_tradeConfig.zoneRecoverySpan = sizePts * _Point;               //--- Set recovery zone
      m_lossTracker.tradeLossTracker = 0.0;                            //--- Initialize loss tracker
      m_lotOption = lotOpt;                                            //--- Set lot size option
      m_initialLotSize = initLot;                                      //--- Set initial lot
      m_riskPercentage = riskPct;                                      //--- Set risk percentage
      m_riskPoints = riskPts;                                          //--- Set risk points
      m_maxOrders = maxOrds;                                           //--- Set max positions
      m_restrictMaxOrders = restrictOrds;                              //--- Set restriction flag
      m_zoneTargetPoints = targetPts;                                  //--- Set target points
      m_zoneSizePoints = sizePts;                                      //--- Set zone points
      m_tradeConfig.marketSymbol = _Symbol;                            //--- Set symbol
      m_tradeConfig.tradeIdentifier = magicNumber;                     //--- Set magic number
      //--- Constructor End
   }

   //--- Destructor
   ~MarketZoneTrader() {
      //--- Destructor Start
      cleanup();                                                       //--- Release resources
      //--- Destructor End
   }

我们继续在“MarketZoneTrader”类的公共部分定义其构造函数和析构函数。首先定义“MarketZoneTrader”构造函数,其接收以下参数:“lotOpt”、“initLot”、“riskPct”、“riskPts”、“maxOrds”、“restrictOrds”、“targetPts”和“sizePts”。我们通过将“m_tradeConfig.currentState”设置为“INACTIVE”来初始化交易环境,表示当前没有活跃交易。接下来,我们使用ArrayResize函数将“m_tradeConfig.activeTickets”数组的大小调整为0,清空该数组,为存储新的订单编号做好准备。我们通过将“targetPts”和“sizePts”分别与“_Point”相乘,计算得出“m_tradeConfig.zoneProfitSpan”和“m_tradeConfig.zoneRecoverySpan”,从而根据价格单位设定盈利目标和补仓区间的大小。我们将“m_lossTracker.tradeLossTracker”重置为0.0,以便从头开始跟踪盈亏情况。

接下来,我们将输入参数赋值给对应的成员变量:将“lotOpt”赋值给“m_lotOption”,“initLot”赋值给“m_initialLotSize”,“riskPct”赋值给“m_riskPercentage”,“riskPts”赋值给“m_riskPoints”,“maxOrds”赋值给“m_maxOrders”,“restrictOrds”赋值给“m_restrictMaxOrders”,“targetPts”赋值给“m_zoneTargetPoints”,“sizePts”赋值给“m_zoneSizePoints”。我们将“m_tradeConfig.marketSymbol”设置为_Symbol,以便交易当前图表所对应的品种,并将“m_tradeConfig.tradeIdentifier”设置为“magicNumber”,作为唯一的订单标识。该设置确保EA能够反映用户的配置,并做好进行交易的准备。

接下来,我们定义“~MarketZoneTrader”析构函数,用于清理资源。我们调用“cleanup”函数来释放所有已分配的资源,例如指标句柄,确保EA能够彻底关闭,避免内存泄漏。需要注意的是,构造函数和析构函数的命名规则相同,只是析构函数名称前带有一个波浪号 (~)。仅此而已。当不再需要该类时,以下是用来销毁它的函数:

//--- Cleanup
void cleanup() {
   //--- Cleanup Start
   IndicatorRelease(m_handleRsi);                                   //--- Release RSI handle
   ArrayFree(m_rsiBuffer);                                          //--- Free RSI buffer
   IndicatorRelease(m_handleEnvUpper);                              //--- Release upper Envelopes handle
   ArrayFree(m_envUpperBandBuffer);                                 //--- Free upper Envelopes buffer
   IndicatorRelease(m_handleEnvLower);                              //--- Release lower Envelopes handle
   ArrayFree(m_envLowerBandBuffer);                                 //--- Free lower Envelopes buffer
   //--- Cleanup End
}

我们只需使用IndicatorRelease函数释放指标句柄,并使用ArrayFree函数释放存储数组占用的内存。既然已经涉及了指标操作,接下来我们定义一个初始化函数,该函数将在程序启动时被调用。

//--- Getters
TradeState getCurrentState() {
   //--- Get Current State Start
   return m_tradeConfig.currentState;                               //--- Return trade state
   //--- Get Current State End
}

double getZoneTargetHigh() {
   //--- Get Target High Start
   return m_zoneBounds.zoneTargetHigh;                              //--- Return profit target high
   //--- Get Target High End
}

double getZoneTargetLow() {
   //--- Get Target Low Start
   return m_zoneBounds.zoneTargetLow;                               //--- Return profit target low
   //--- Get Target Low End
}

double getZoneHigh() {
   //--- Get Zone High Start
   return m_zoneBounds.zoneHigh;                                    //--- Return recovery zone high
   //--- Get Zone High End
}

double getZoneLow() {
   //--- Get Zone Low Start
   return m_zoneBounds.zoneLow;                                     //--- Return recovery zone low
   //--- Get Zone Low End
}

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

在此阶段,我们首先创建一些简单的获取函数,以访问关键的交易数据。“getCurrentState”函数返回“m_tradeConfig.currentState”的值,使我们能够检查系统当前处于“INACTIVE”、“RUNNING”还是“TERMINATING”状态。接下来,我们构建“getZoneTargetHigh”和“getZoneTargetLow”函数,分别用于获取“m_zoneBounds.zoneTargetHigh”和“m_zoneBounds.zoneTargetLow”的值,从而提供订单的盈利目标价格。之后,我们添加“getZoneHigh”和“getZoneLow”函数,分别用于获取“m_zoneBounds.zoneHigh”和“m_zoneBounds.zoneLow”的值,为我们提供补仓区间的边界。

下面,我们编写“initialize”函数以设置EA。首先,我们通过调用“SetExpertMagicNumber”将“m_tradeConfig.tradeIdentifier”赋值给“m_tradeExecutor”,为我们的订单打上唯一标签。接下来,我们使用“PositionsTotal”检查当前是否存在持仓,并通过循环遍历所有持仓,使用“PositionGetTicket”获取每个持仓的"ticket"。如果通过PositionSelectByTicket成功选中某个持仓,并且该持仓的交易品种与“m_tradeConfig.marketSymbol”匹配,同时交易标识符与“m_tradeConfig.tradeIdentifier”一致(通过PositionGetString和“PositionGetInteger”验证),则调用“activateTrade”函数管理该持仓,并使用“Print”记录操作成功或失败的信息。

接下来,我们设置所需的指标。我们通过iRSI函数,在当前时间框架上设置8周期,并使用“PRICE_CLOSE”,为当前交易品种“m_tradeConfig.marketSymbol”构建RSI指标句柄。如果“m_handleRsi”返回INVALID_HANDLE,我们通过“Print”记录错误信息并返回“INIT_FAILED”。之后,我们初始化包络线指标:使用“iEnvelopes”函数创建上轨指标句柄“m_handleEnvUpper”,参数为150周期、简单移动平均线、0.1偏离值和“PRICE_CLOSE”;创建下轨指标句柄“m_handleEnvLower”,参数为95周期、1.4偏离值和收盘价。如果任一指标句柄为“INVALID_HANDLE”,我们记录失败信息并返回“INIT_FAILED”。最后,我们通过ArraySetAsSeries将“m_rsiBuffer”、“m_envUpperBandBuffer”和“m_envLowerBandBuffer”配置为时间序列数组,使用“Print”记录初始化成功信息,并返回INIT_SUCCEEDED。现在,我们可以在OnInit事件处理器中调用此函数,但首先需要创建该类的实例。

//--- Global Instance
MarketZoneTrader *trader = NULL;                                        //--- Declare trader instance

在此阶段,我们通过声明一个指向“MarketZoneTrader”类的指针来设置系统的全局实例。我们将“trader”变量定义为指向“MarketZoneTrader”的指针,并将其初始化为“NULL”。这一步骤确保我们在整个EA中拥有一个单一且全局可访问的交易系统实例,可用于管理所有交易操作,例如初始化交易、执行订单以及处理补仓区间。通过初始化为“NULL”,我们为后续正确实例化“trader”做好准备,避免在EA完全设置完成之前发生任何过早访问的情况。现在,我们可以继续调用相关函数。

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
}

OnInit事件处理器中,我们首先创建一个“MarketZoneTrader”类的新实例,并将其赋值给全局指针变量“trader”。我们将用户定义的输入参数——“lotOption”、“initialLotSize”、“riskPercentage”、“riskPoints”、“maxOrders”、“restrictMaxOrders”、“zoneTargetPoints”和“zoneSizePoints”——传递给构造函数,以便按照用户指定的设置配置交易系统。然后,我们调用“trader”的“initialize”函数来设置EA,包括交易订单标记、检查现有持仓以及初始化指标等操作,并返回其执行结果,以指示设置是否成功。此函数确保EA能够根据指定的配置完全准备好开始交易。编译后,我们得到以下输出。

初始化图

由图可见,程序已成功初始化。然而,当我们尝试卸载程序时,却出现了问题。具体如下:

对象内存泄漏

由图可见,存在未被释放的对象,导致了内存泄漏。要解决这一问题,我们需要进行对象清理操作。为实现这一目标,我们采用以下逻辑:

void OnDeinit(const int reason) {
   //--- EA Deinitialization Start
   if (trader != NULL) {                                                 //--- Check trader existence
      delete trader;                                                     //--- Delete trader
      trader = NULL;                                                     //--- Clear pointer
      Print("EA deinitialized");                                         //--- Log deinitialization
   }
   //--- EA Deinitialization End
}

为解决资源清理工作,在OnDeinit事件处理器中,我们首先检查“trader”指针是否为非“NULL”,以确保“MarketZoneTrader”实例已存在。如果存在,则使用delete操作符释放为“trader”分配的内存,防止内存泄漏。随后,将“trader”指针置于“NULL”,避免意外访问已释放的内存。最后,通过“Print”函数记录日志消息,确认EA已完成反初始化。该函数确保EA退出时资源释放干净,避免潜在问题。现在,我们可以继续定义核心逻辑,用于处理信号评估和管理已开仓位。因此,我们需要编写相应的辅助函数。

//--- Position Management
bool activateTrade(ulong ticket) {
   //--- Position Activation Start
   m_tradeConfig.currentState = INACTIVE;                           //--- Set state to inactive
   ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Clear tickets
   m_lossTracker.tradeLossTracker = 0.0;                            //--- Reset loss tracker
   if (!configureTrade(ticket)) {                                    //--- Configure trade
      return false;                                                 //--- Return failure
   }
   storeTradeTicket(ticket);                                        //--- Store ticket
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      m_zoneBounds.zoneHigh = m_tradeConfig.openPrice;              //--- Set zone high
      m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; //--- Set zone low
      m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; //--- Set buy volume
      m_tradeConfig.accumulatedSellVolume = 0.0;                    //--- Reset sell volume
   } else {                                                         //--- Handle sell position
      m_zoneBounds.zoneLow = m_tradeConfig.openPrice;               //--- Set zone low
      m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; //--- Set zone high
      m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; //--- Set sell volume
      m_tradeConfig.accumulatedBuyVolume = 0.0;                     //--- Reset buy volume
   }
   m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; //--- Set target high
   m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; //--- Set target low
   m_tradeConfig.currentState = RUNNING;                            //--- Set state to running
   return true;                                                     //--- Return success
   //--- Position Activation End
}

//--- Tick Processing
void processTick() {
   //--- Tick Processing Start
   double askPrice = NormalizeDouble(getMarketAsk(), Digits());     //--- Get ask price
   double bidPrice = NormalizeDouble(getMarketBid(), Digits());     //--- Get bid price
   
   if (!isNewBar()) return;                                         //--- Exit if not new bar
   
   if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) {            //--- Load RSI data
      Print("Error loading RSI data. Reverting.");                  //--- Log RSI failure
      return;                                                       //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { //--- Load upper Envelopes
      Print("Error loading upper envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { //--- Load lower Envelopes
      Print("Error loading lower envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   int ticket = 0;                                                     //--- Initialize ticket
   
   const int rsiOverbought = 70;                                       //--- Set RSI overbought level
   const int rsiOversold = 30;                                         //--- Set RSI oversold level
   
   if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { //--- Check buy signal
      if (askPrice > m_envUpperBandBuffer[0]) {                        //--- Confirm price above upper Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_BUY, 0, 0);                  //--- Open buy order
         }
      }
   } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { //--- Check sell signal
      if (bidPrice < m_envLowerBandBuffer[0]) {                        //--- Confirm price below lower Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_SELL, 0, 0);                 //--- Open sell order
         }
      }
   }
   
   if (ticket > 0) {                                                //--- Check if trade opened
      if (activateTrade(ticket)) {                                  //--- Activate position
         Print("New position activated: Ticket=", ticket);          //--- Log activation
      } else {
         Print("Failed to activate new position: Ticket=", ticket); //--- Log failure
      }
   }
   //--- Tick Processing End
}

在此阶段,我们继续开发程序,在“MarketZoneTrader”类中实现“activateTrade”和“processTick”函数,用于管理持仓并处理市场行情数据(tick)。我们从实现“activateTrade”函数开始,为指定的“ticket”激活交易。首先,将“m_tradeConfig.currentState”设置为“INACTIVE”,并使用ArrayResize函数清空“m_tradeConfig.activeTickets”数组,重置订单编号列表。将“m_lossTracker.tradeLossTracker”重置为0.0,然后调用“configureTrade”,并传入“ticket”。如果配置失败,直接返回false。通过“storeTradeTicket”保存“ticket”。对于买入订单(“m_tradeConfig.direction”为ORDER_TYPE_BUY):将“m_zoneBounds.zoneHigh”设置为开仓价格“m_tradeConfig.openPrice”,通过从开仓价格中减去“m_tradeConfig.zoneRecoverySpan”,计算“m_zoneBounds.zoneLow”,更新“m_tradeConfig.accumulatedBuyVolume”为初始手数“m_tradeConfig.initialVolume”,并重置“m_tradeConfig.accumulatedSellVolume”。

对于卖出订单,将“m_zoneBounds.zoneLow”设置为开仓价格“m_tradeConfig.openPrice”,通过在开仓价格基础上加上“m_tradeConfig.zoneRecoverySpan”,计算“m_zoneBounds.zoneHigh”,并相应地调整成交量。我们再根据“m_tradeConfig.zoneProfitSpan”设置“m_zoneBounds.zoneTargetHigh”和“m_zoneBounds.zoneTargetLow”,将“m_tradeConfig.currentState”更改为“RUNNING”,并返回true表示激活成功。

接下来,我们构建“processTick”函数以处理市场行情数据。使用“getMarketAsk”和“getMarketBid”函数分别获取当前市场的卖出价"askPrice"和买入价"bidPrice",通过NormalizeDouble结合当前交易品种的精度“Digits”对价格进行标准化处理。调用“isNewBar”函数判断是否形成新K线,如果返回false,则直接退出函数以节省系统资源。通过CopyBuffer函数从已初始化的指标句柄中复制数据到对应的缓冲区——从“m_handleRsi”复制数据到“m_rsiBuffer”,以及从“m_handleEnvLower”复制数据到“m_envLowerBandBuffer”,如果任一指标数据加载失败,通过“Print”函数记录错误日志并退出函数。对于交易信号,我们将超买阈值“rsiOverbought”设置为70,超卖阈值“rsiOversold”设置为30。

如果“m_rsiBuffer”显示市场处于超卖状态(RSI值 ≤ 30),且当前"askPrice"突破上轨包络线"m_envUpperBandBuffer",检查是否允许开仓:如果“m_restrictMaxOrders”为false或PositionsTotal小于预设的“m_maxOrders”,则调用“openOrder”函数开立买入订单。如果市场处于超买状态(RSI值 ≥ 70),且当前"bidPrice"跌破下轨包络线"m_envLowerBandBuffer",同样检查开仓条件,满足后调用“openOrder”开立卖出订单。如果返回有效的订单编号,则调用“activateTrade”函数激活该交易,并通过日志记录操作结果。我们可以在OnTick事件处理器中调用,以实现评估交易信号并管理持仓。

void OnTick() {
   //--- Tick Handling Start
   if (trader != NULL) {                                                 //--- Check trader existence
      trader.processTick();                                              //--- Process tick
   }
   //--- Tick Handling End
}

在"OnTick"事件处理器中,我们首先检查"trader"指针(即"MarketZoneTrader"类的实例)是否非"NULL",以确保交易系统已完成初始化。如果实例存在,则调用"trader"的"processTick"函数处理每个市场行情数据,包括评估持仓状态、检查指标信号,并在必要时执行交易。编译后,呈现如下效果:

初始持仓

由图可见,我们已识别到交易信号并完成评估,并成功开立买入订单。接下来我们需要做的是管理已开立的持仓。为实现代码的模块化,我们将通过独立的函数处理。

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
}

在此阶段,我们在"MarketZoneTrader"类中实现"evaluateMarketTick"函数,用于评估活跃交易的市场条件。我们首先检查"m_tradeConfig.currentState"是否为"INACTIVE"。如果是,则立即退出,避免在无活跃交易时进行不必要的处理。接下来,检查"m_tradeConfig.currentState"是否为"TERMINATING"。如果是,则调用"finalizePosition"函数将所有持仓平仓,结束交易周期,然后退出。平仓函数如下:

//--- Position Finalization
bool finalizePosition() {
   //--- Position Finalization Start
   m_tradeConfig.currentState = TERMINATING;                        //--- Set terminating state
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                       //--- Close all trades
   if (metrics.operationSuccess) {                                  //--- Check success
      ArrayResize(m_tradeConfig.activeTickets, 0);                  //--- Clear tickets
      m_tradeConfig.currentState = INACTIVE;                        //--- Set inactive state
      Print("Position closed successfully");                        //--- Log success
   } else {
      Print("Failed to close position");                            //--- Log failure
   }
   return metrics.operationSuccess;                                 //--- Return status
   //--- Position Finalization End
}

我们首先将"m_tradeConfig.currentState"设置为"TERMINATING",表示交易周期即将结束。这样有助于防止在平仓过程中触发管理周期。接下来,我们初始化名为"metrics"的"TradeMetrics" 结构体 ,将"operationSuccess"设置为true,"totalVolume"设为0.0,"netProfitLoss"设置为0.0,以跟踪平仓结果。我们调用"closeActiveTrades"并传入"metrics" ,将"m_tradeConfig.activeTickets"中列出的所有持仓平仓。如果"metrics.operationSuccess"仍为true,我们使用ArrayResize清空"m_tradeConfig.activeTickets"以重置订单列表,将"m_tradeConfig.currentState"设为"INACTIVE"标记系统为空闲状态,并使用"Print"记录成功信息。

如果平仓失败,我们则用"Print"记录失败信息。最后返回"metrics.operationSuccess",以指示整个平仓流程是否成功完成。如果此时未平仓,意味着当前不在平仓流程中,因此我们可以继续评估价格是否触及补仓区域或目标位。让我们先从买入实例开始。

double currentPrice;                                             //--- Initialize price
if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
   currentPrice = getMarketBid();                                //--- Get bid price
   if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
      Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
      finalizePosition();                                        //--- Close position
      return;                                                    //--- Exit
   } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
      Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
   }
}

我们继续在"MarketZoneTrader"类的"evaluateMarketTick"函数中实现处理买入持仓的逻辑。首先声明"currentPrice"以存储市场价格。如果"m_tradeConfig.direction"为ORDER_TYPE_BUY,我们使用"getMarketBid"函数获取买入价(即平仓买入持仓的价格),并赋值给"currentPrice"。之后,检查"currentPrice"是否超过"m_zoneBounds.zoneTargetHigh"。如果是,则使用"Print"记录平仓信息(显示买入价和目标位),然后调用"finalizePosition"平仓并返回退出。

如果"currentPrice"跌破"m_zoneBounds.zoneLow",我们使用"Print"记录补仓触发,并调用"triggerRecoveryTrade",传入ORDER_TYPE_SELL和"currentPrice"开立卖出订单以减少亏损。该逻辑确保我们及时止盈或启动亏损补仓,以保持策略的响应性。以下是负责开立补仓交易的函数逻辑:

//--- Recovery Trade Handling
void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, double price) {
   //--- Recovery Trade Start
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                      //--- Close existing trades
   for (int i = 0; i < 10 && !metrics.operationSuccess; i++) {      //--- Retry closure
      Sleep(1000);                                                  //--- Wait 1 second
      metrics.operationSuccess = true;                              //--- Reset success flag
      closeActiveTrades(metrics);                                   //--- Retry closure
   }
   m_lossTracker.tradeLossTracker += metrics.netProfitLoss;         //--- Update loss tracker
   if (m_lossTracker.tradeLossTracker > 0 && metrics.operationSuccess) { //--- Check positive profit
      Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); //--- Log closure
      finalizePosition();                                           //--- Close position
      m_lossTracker.tradeLossTracker = 0.0;                         //--- Reset loss tracker
      return;                                                       //--- Exit
   }
   double tradeSize = determineRecoverySize(tradeDirection);        //--- Calculate trade size
   ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); //--- Open recovery trade
   if (ticket > 0) {                                                //--- Check if trade opened
      storeTradeTicket(ticket);                                     //--- Store ticket
      m_tradeConfig.direction = tradeDirection;                     //--- Update direction
      if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; //--- Update buy volume
      else m_tradeConfig.accumulatedSellVolume += tradeSize;        //--- Update sell volume
      Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); //--- Log recovery trade
   }
   //--- Recovery Trade End
}

//--- Recovery Size Calculation
double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) {
   //--- Recovery Size Calculation Start
   double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; //--- Calculate lot size
   tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); //--- Round to volume step
   return tradeSize;                                                //--- Return trade size
   //--- Recovery Size Calculation End
}

为处理需要触发补仓实例的市场情况,我们从"triggerRecoveryTrade"函数开始,在持仓走势不利时处理补仓交易。首先,初始化名为"metrics"的"TradeMetrics" 结构体,将"operationSuccess"设置为true,"totalVolume"设置为0.0,"netProfitLoss"设置为0.0。调用"closeActiveTrades"并传入"metrics"将现有持仓平仓。如果"metrics.operationSuccess"为false,则最多重试10次,每次用Sleep等待一秒,并在每次尝试前重置"operationSuccess"。

我们将"metrics.netProfitLoss"的值累加到"m_lossTracker.tradeLossTracker"中,更新交易记录。如果"m_lossTracker.tradeLossTracker"为正值且"metrics.operationSuccess"为true,则使用"Print"记录平仓信息,调用"finalizePosition",将"m_lossTracker.tradeLossTracker"重置为0.0,并返回退出。否则,使用 "determineRecoverySize"并传入"tradeDirection"计算补仓交易规模"tradeSize",之后用"openMarketTrade"并传入"tradeDirection"、"tradeSize"和"price"开立新订单。

如果返回的"ticket"有效,我们通过"storeTradeTicket"保存,更新"m_tradeConfig.direction",根据"tradeDirection"调整"m_tradeConfig.accumulatedBuyVolume"或"m_tradeConfig.accumulatedSellVolume",并使用EnumToString通过"Print"记录交易。接下来,创建"determineRecoverySize"函数计算补仓交易的手数。我们通过将负值"m_lossTracker.tradeLossTracker"除以"m_tradeConfig.zoneProfitSpan"来计算"tradeSize",以确定交易规模足以覆盖亏损。接下来,使用MathCeil和"getMarketVolumeStep"将"tradeSize"按经纪商的最小步长向上取整,确保合规,并返回结果。至此,我们已处理补仓实例,接下来继续完成卖出区域的逻辑。该逻辑与买入相反,因此我们无需花费太多时间。最终完整函数如下:

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
   double currentPrice;                                             //--- Initialize price
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      currentPrice = getMarketBid();                                //--- Get bid price
      if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
         Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
         Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
      }
   } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {         //--- Handle sell position
      currentPrice = getMarketAsk();                                //--- Get ask price
      if (currentPrice < m_zoneBounds.zoneTargetLow) {              //--- Check profit target
         Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice > m_zoneBounds.zoneHigh) {            //--- Check recovery trigger
         Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);        //--- Open buy recovery
      }
   }
   //--- Tick Evaluation End
}

此函数现在可以应对所有补仓方向。编译后,呈现如下效果:

最终效果

由图可见,我们已成功处理因趋势反弹信号触发的持仓订单。接下来需完成的工作是程序回测,相关内容将在下一章节详细阐述。


回测

经过全面回测后,我们得到以下结果:

回测图:

图表

回测报告:

报告


结论

总体而言,我们已经成功构建了一个基于面向对象编程(OOP)的稳健MQL5程序,实现了包络线趋势交易的区间补仓系统,该系统结合相对强弱指数(RSI)与包络线指标识别交易机会,并通过结构化补仓区间管理亏损。借助"MarketZoneTrader"类等组件、"TradeConfig"与"ZoneBoundaries"等结构体,以及"processTick"和"triggerRecoveryTrade"等函数,我们创建了一个灵活的系统,您可以通过调整"zoneTargetPoints"或"riskPercentage"等参数来适应各种市场条件。

免责声明:本文仅用于教学目的。交易涉及重大财务风险,市场剧烈波动可能导致资金损失。在将本程序用于实盘交易前,请务必进行充分回测并制定严谨的风险管理策略。

基于本文构建的基础框架,您可以进一步优化区间补仓系统或改造其逻辑以开发全新交易策略,助力算法交易能力的持续提升。祝您交易顺利!

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

最近评论 | 前往讨论 (2)
Sabrina Hellal
Sabrina Hellal | 9 7月 2025 在 13:44
非常感谢 🙏
Allan Munene Mutiiria
Allan Munene Mutiiria | 9 7月 2025 在 16:46
Sabrina Hellal #:
非常感谢 🙏

非常欢迎。谢谢

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
从新手到专家:使用 MQL5 制作动画新闻标题(七)—— 新闻交易的后冲击策略 从新手到专家:使用 MQL5 制作动画新闻标题(七)—— 新闻交易的后冲击策略
在重大经济新闻发布后的第一分钟内,市场出现剧烈波动的风险极高。在那短暂的时间窗口内,价格走势可能不稳定且波动剧烈,经常会触发两个方向的挂单。在发布后不久 —— 通常在一分钟内 —— 市场趋于稳定,恢复或纠正更典型的波动性。在本节中,我们将探讨新闻交易的另一种方法,旨在评估其作为交易者工具包中有价值的补充的有效性。继续阅读,了解本讨论中的更多见解和细节。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
您应当知道的 MQL5 向导技术(第 63 部分):运用 DeMarker 和包络通道形态 您应当知道的 MQL5 向导技术(第 63 部分):运用 DeMarker 和包络通道形态
DeMarker 振荡器和包络指标是动量和支撑/阻力工具,能够在开发智能系统时配对。因此,我们逐一实证哪些形态能够实用,哪些潜在要回避。我们一如既往地使用由向导汇编的智能系统,伴同在信号类中内置的形态用法函数。