
在 MQL5 中自动化交易策略(第三部分):用于动态交易管理的RSI区域反转系统
引言
在上一篇文章(本系列第二部分)中,我们演示了如何使用MQL5语言将云图突破策略转变为一个功能完备的智能交易系统(EA)。在本文(第三部分)中,我们重点关注RSI区域反转系统,这是一个旨在动态管理交易并从亏损中恢复的先进策略。该系统将用于触发入场信号的相对强弱指数(RSI)与一个区域反转机制相结合,当市场朝初始仓位的不利方向移动时,该机制会下达反向交易订单。其目标是通过适应市场状况来减轻回撤并提高整体盈利能力。
我们将逐步介绍编码反转逻辑的过程、使用动态手数管理仓位,以及利用 RSI 进行交易入场和反转信号。在本文结束时,您将清楚地了解如何实现区域反转 RSI 系统,使用 MQL5 策略测试器测试其性能,并对其进行优化以获得更好的风险管理和回报。文章结构如下,以便于理解。
策略设计与核心概念
区域反转 RSI 系统将用于交易入场的相对强弱指数(RSI)指标与一个用于管理不利价格变动的区域反转机制相结合。当 RSI 穿越关键阈值时——通常是 30(超卖,买入)和 70(超买,卖出)——会触发交易入场。然而,该系统的真正威力在于其通过一个结构良好的区域反转模型从亏损交易中恢复的能力。
区域反转系统为每笔交易建立了四个关键价格水平:区域上限、区域下限、目标上限和目标下限。当一笔交易开仓时,这些水平会相对于入场价格进行计算。对于一笔买入交易,区域下限设置在入场价格之下,而区域上限则位于入场价格。相反,对于一笔卖出交易,区域上限放置在入场价格之上,而区域下限则与其对齐。如果市场突破区域下限(针对买入)或区域上限(针对卖出),则会触发一个反向交易,其手数根据预定义的乘数增加。目标上限和目标下限定义了买入和卖出仓位的获利了结点,确保一旦市场朝有利方向移动,交易就能以盈利平仓。这种方法允许在通过系统化的仓位规模和水平调整来控制风险的同时,实现从亏损中回升。下面是一张用于总结整个模型的示意图。
在MQL5中的实现
在学习了所有关于区域反转交易策略的理论之后,让我们来将这些理论自动化,并为MetaTrader 5用MQL5编写一个EA。
要在MetaTrader 5终端中创建EA,请点击“工具”选项卡并选择“MetaQuotes语言编辑器”,或者简单地在键盘上按F4键。另外,您还可以点击工具栏上的IDE(集成开发环境)图标。这将打开MetaQuotes语言编辑器环境,允许您编写EA、技术指标、脚本和函数库。一旦MetaEditor被打开,在工具栏上,导航到“文件”选项卡并选择“新建文件”,或者简单地按CTRL + N,来创建一个新文档。另外,您也可以点击工具栏上的“新建”图标。这将弹出一个MQL向导(MQL Wizard)窗口。
在弹出的向导中,选择“Expert Advisor (template) ”并点击“下一步”。在EA的一般属性中,在名称部分,输入您的EA文件的名称。请注意,如果要指定或创建一个不存在的文件夹,您需要在EA名称前使用反斜杠。例如,这里我们默认有“Experts\”。这意味着我们的EA将被创建在Experts文件夹中,我们可以在那里找到它。其他部分都很容易理解,您也可以按照向导底部的链接去详细了解这一过程。
在提供了您想要的EA文件名后,点击“下一步”,再点击“下一步”,然后点击“完成”。 完成这些步骤后,我们现在就可以开始编写和规划我们的策略了。
首先我们从定义EA的基础数据开始。这包括EA的名称、版本信息和MetaQuotes的网站链接。我们还将指定EA的版本号,设置为“1.00”。
//+------------------------------------------------------------------+ //| 1. Zone Recovery RSI EA.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00"
当加载程序时,这会将程序基本信息显示出来。然后我们给程序添加一些后面将会用到的全局变量。首先,我们在源代码的开头使用#include包含一个交易实例。这使我们能够访问CTrade类,我们将使用该类来创建一个交易对象。这非常关键,因为我们需要用它来执行交易。
#include <Trade/Trade.mqh>
CTrade obj_Trade;
预处理器会用文件Trade.mqh的内容替换#include <Trade/Trade.mqh>这一行。尖括号表示Trade.mqh文件将从标准目录(通常是terminal_installation_directory\MQL5\Include)中获取。当前目录不会包含在搜索路径中。这行代码可以放在程序的任何位置,但通常,为了代码结构更好和引用更方便,所有的包含指令都放在源代码的开头。声明CTrade类的obj_Trade对象将使我们能够轻松访问该类中包含的方法,这得益于MQL5开发者的设计。
之后,我们需要声明几个将在交易系统中使用的重要全局变量。
// Global variables for RSI logic int rsiPeriod = 14; //--- The period used for calculating the RSI indicator. int rsiHandle; //--- Handle for the RSI indicator, used to retrieve RSI values. double rsiBuffer[]; //--- Array to store the RSI values retrieved from the indicator. datetime lastBarTime = 0; //--- Holds the time of the last processed bar to prevent duplicate signals.
为了处理信号生成,我们设置了管理RSI指标逻辑所需的全局变量。首先,我们将“rsiPeriod”定义为14,它决定了用于计算RSI的价格K线数量。这是技术分析中的一个标准设置,使我们能够衡量市场的超买或超卖状况。接下来,我们创建“rsiHandle”,作为RSI指标的一个引用(句柄)。该句柄将允许我们从MetaTrader平台请求并获取RSI值,使我们能够实时跟踪指标的动向。
为了存储这些RSI值,我们使用“rsiBuffer”,这是一个用于存放指标输出的数组。我们将分析这个缓冲区以检测关键的交叉点,例如当RSI移动至30以下(潜在的买入信号)或70以上(潜在的卖出信号)时。最后,我们引入“lastBarTime”,它存储了最近处理过的K线的时间。这个变量将确保我们每根K线只处理一个信号,从而防止在同一根K线内触发多次交易。之后,我们就可以定义一个用于处理反转机制的类了。
// Global ZoneRecovery object class ZoneRecovery { //--- };
在这里,我们使用一个名为“ZoneRecovery”的类来创建一个区域反转系统,该类作为一个容器,包含了管理反转过程所需的所有变量、函数和逻辑。通过使用类,我们可以将代码组织成一个独立的对象,从而管理交易、跟踪反转进度,并为每个交易周期计算关键水平。这种方法为同时处理多个交易头寸提供了更好的结构、可重用性和可扩展性。一个类可以包含三种成员封装形式,即private(私有)、protected(受保护)和public(公有)成员。让我们先定义私有成员。
private: CTrade trade; //--- Object to handle trading operations. double initialLotSize; //--- The initial lot size for the first trade. double currentLotSize; //--- The lot size for the current trade in the sequence. double zoneSize; //--- Distance in points defining the range of the recovery zone. double targetSize; //--- Distance in points defining the target profit range. double multiplier; //--- Multiplier to increase lot size in recovery trades. string symbol; //--- Symbol for trading (e.g., currency pair). ENUM_ORDER_TYPE lastOrderType; //--- Type of the last executed order (BUY or SELL). double lastOrderPrice; //--- Price at which the last order was executed. double zoneHigh; //--- Upper boundary of the recovery zone. double zoneLow; //--- Lower boundary of the recovery zone. double zoneTargetHigh; //--- Upper boundary for target profit range. double zoneTargetLow; //--- Lower boundary for target profit range. bool isRecovery; //--- Flag indicating whether the recovery process is active.
在这里,我们定义了“ZoneRecovery”类的私有成员变量,它们存储了管理区域反转过程所需的基本数据。这些变量使我们能够跟踪策略的状态,计算反转区域的关键水平,并管理交易执行逻辑。
我们使用“CTrade”对象来处理所有交易操作,例如下单、修改和平仓。“initialLotSize”代表第一笔交易的手数,而“currentLotSize”则跟踪后续反向交易的手数,该手数会根据“multiplier”(倍数)增加。“zoneSize”和“targetSize”定义了反转系统的关键边界。具体来说,反转区域由“zoneHigh”和“zoneLow”界定,而利润目标则由“zoneTargetHigh”和“zoneTargetLow”定义。
为了跟踪交易的流程,我们存储了“lastOrderType”(买入或卖出)以及上一笔交易的执行价格“lastOrderPrice”。这些信息帮助我们决定如何根据市场变动来安排未来的交易。“symbol”变量标识了正在使用的交易品种,而“isRecovery”标志则指示系统是否正处于反转过程中。通过将这些变量设为私有,我们确保只有类的内部逻辑可以修改它们,从而维护系统计算的完整性和准确性。之后,为了简单起见,我们可以直接定义类的函数,而不必先声明再定义。因此,我们不再先声明所需的函数然后再定义,而是一次性地声明并定义它们。让我们先定义负责计算反转区域的函数。
// Calculate dynamic zones and targets void CalculateZones() { if (lastOrderType == ORDER_TYPE_BUY) { zoneHigh = lastOrderPrice; //--- Upper boundary starts from the last BUY price. zoneLow = zoneHigh - zoneSize; //--- Lower boundary is calculated by subtracting zone size. zoneTargetHigh = zoneHigh + targetSize; //--- Profit target above the upper boundary. zoneTargetLow = zoneLow - targetSize; //--- Buffer below the lower boundary for recovery trades. } else if (lastOrderType == ORDER_TYPE_SELL) { zoneLow = lastOrderPrice; //--- Lower boundary starts from the last SELL price. zoneHigh = zoneLow + zoneSize; //--- Upper boundary is calculated by adding zone size. zoneTargetLow = zoneLow - targetSize; //--- Buffer below the lower boundary for profit range. zoneTargetHigh = zoneHigh + targetSize; //--- Profit target above the upper boundary. } Print("Zone recalculated: ZoneHigh=", zoneHigh, ", ZoneLow=", zoneLow, ", TargetHigh=", zoneTargetHigh, ", TargetLow=", zoneTargetLow); }
在这里,我们设计了“CalculateZones”函数,它在定义我们区域反转策略的关键水平方面发挥着至关重要的作用。该函数的主要目标是计算四个基本边界——“zoneHigh”、“zoneLow”、“zoneTargetHigh”和“zoneTargetLow”——这些边界指导着我们的入场、反转和盈利出场点。这些边界是动态的,会根据最后执行订单的类型和价格进行调整,确保我们能始终控制反转过程。
如果我们的最后一笔订单是买入(BUY),我们将“zoneHigh”设置为买入订单的执行价格。从这一点出发,我们通过从“zoneHigh”中减去“zoneSize”来计算“zoneLow”,从而在原始买入价格下方创建一个反转范围。为了建立我们的利润目标,我们通过将“targetSize”加到“zoneHigh”上来计算“zoneTargetHigh”,而“zoneTargetLow”则定位在“zoneLow”下方相同“targetSize”的距离。这种结构将使我们能够在原始买入入场下方安排反转交易,并定义我们利润范围的上下限。
如果我们的最后一笔订单是卖出(SELL),我们则翻转逻辑。在这里,我们将“zoneLow”设置为最后一笔卖出订单的价格。然后,我们通过将“zoneSize”加到“zoneLow”上来计算“zoneHigh”,形成反转范围的上边界。利润目标通过将“zoneTargetLow”计算为“zoneLow”下方的一个值来建立,而“zoneTargetHigh”则设置在“zoneHigh”上方,两者都相差“targetSize”的距离。这种设置同样允许我们在原始卖出入场上方启动反转交易,同时也定义了获利区域。
在此过程结束时,我们已经为买入和卖出交易建立了区域反转边界和利润目标。为了帮助调试和策略评估,我们使用Print函数在日志中显示“zoneHigh”、“zoneLow”、“zoneTargetHigh”和“zoneTargetLow”的值。这样,我们就可以定义另一个函数来处理交易执行逻辑了。
// Open a trade based on the given type bool OpenTrade(ENUM_ORDER_TYPE type) { if (type == ORDER_TYPE_BUY) { if (trade.Buy(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_BUY; //--- Mark the last trade as BUY. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isRecovery = true; //--- Set recovery state to true after the first trade. return true; } } else if (type == ORDER_TYPE_SELL) { if (trade.Sell(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_SELL; //--- Mark the last trade as SELL. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY SELL order placed" : "INITIAL SELL order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isRecovery = true; //--- Set recovery state to true after the first trade. return true; } } return false; //--- Return false if the trade fails. }
在这里,我们定义了一个名为“OpenTrade”的函数,它返回一个布尔值。此函数的目的是根据我们想要执行买入还是卖出订单来开仓。我们首先检查所请求的订单类型是否为买入。如果是,我们使用“trade.Buy”函数尝试以当前手数和指定的交易品种开一个买入仓位。如果交易成功开仓,我们将“lastOrderType”设置为BUY,然后使用SymbolInfoDouble函数获取买入价,并存储交易品种的当前价格。这个价格代表我们开仓的价格。接着,我们通过调用“CalculateZones”函数来重新计算反转区域,该函数会根据新仓位调整区域水平。
接下来,我们在日志中打印一条消息,指明这是一笔初始买入还是一笔反转买入。我们使用一个三元运算符来检查“isRecovery”标志是true还是false——如果为true,消息将说明这是一笔反转订单;否则,它将表明这是初始订单。之后,我们将“isRecovery”标志设置为true,表示后续的任何交易都将被视为反转过程的一部分。最后,函数返回true,确认交易已成功下单。
如果订单类型是卖出,我们遵循相同的步骤。我们通过调用“trade.Sell”函数(使用相同的参数)来尝试开一个卖出仓位,成功执行后,我们以同样的方式存储“lastOrderPrice”并调整反转区域。我们打印一条消息,指明这是一笔初始卖出还是一笔反转卖出,同样使用三元运算符来检查“isRecovery”标志。然后,“isRecovery”标志被设置为true,函数返回true以表示交易已成功下单。如果由于任何原因交易未能成功开仓,函数将返回false,表示交易尝试失败。这些是我们需要设为私有的关键函数。其他的我们可以设为公有,没有问题。
public: // Constructor ZoneRecovery(double initialLot, double zonePts, double targetPts, double lotMultiplier, string _symbol) { initialLotSize = initialLot; currentLotSize = initialLot; //--- Start with the initial lot size. zoneSize = zonePts * _Point; //--- Convert zone size to points. targetSize = targetPts * _Point; //--- Convert target size to points. multiplier = lotMultiplier; symbol = _symbol; //--- Initialize the trading symbol. lastOrderType = ORDER_TYPE_BUY; lastOrderPrice = 0.0; //--- No trades exist initially. isRecovery = false; //--- No recovery process active at initialization. }
在这里,我们声明“ZoneRecovery”类的公有部分,其中包含了构造函数。构造函数用于在创建“ZoneRecovery”类的对象时,使用特定参数对其进行初始化。该构造函数接收“initialLot”、“zonePts”、“targetPts”、“lotMultiplier”和“_symbol”作为输入。
我们首先将“initialLot”的值赋给“initialLotSize”和“currentLotSize”,确保两者都以相同的值(即第一笔交易的初始手数)开始。然后,我们通过将“zonePts”(以点数为单位的区域距离)乘以_Point来计算“zoneSize”,其中_Point是一个内置常量,代表该交易品种的最小价格变动单位。同样,“targetSize”也是通过使用相同的方法将“targetPts”(目标盈利距离)转换为实际价格值来计算的。“multiplier”被设置为“lotMultiplier”,它将用于后续调整反转交易的手数。
接下来,“symbol”被赋值给“symbol”变量,以指明将要使用的交易工具。“lastOrderType”最初被设置为ORDER_TYPE_BUY,假设第一笔交易将是买入订单。“lastOrderPrice”被设置为“0.0”,因为尚未执行任何交易。最后,“isRecovery”被设置为“false”,表示反转过程尚未激活。这个构造函数确保“ZoneRecovery”对象被正确初始化,并为管理交易和反转过程做好准备。接下来,我们定义一个函数,用于根据外部信号触发交易。
// Trigger trade based on external signals void HandleSignal(ENUM_ORDER_TYPE type) { if (lastOrderPrice == 0.0) //--- Open the first trade if no trades exist. OpenTrade(type); }
在这里,我们定义了一个名为“HandleSignal”的函数,它接收一个ENUM_ORDER_TYPE类型的参数,代表要执行的交易类型(买入或卖出)。首先,我们检查“lastOrderPrice”是否为“0.0”,这表明之前没有执行过任何交易。如果此条件为真,则意味着这是将要开立的第一笔交易,因此我们调用“OpenTrade”函数,并将“type”参数传递给它。然后,“OpenTrade”函数将根据接收到的信号,处理开立买入或卖出订单的逻辑。现在,我们可以通过下面的逻辑来开立反转交易,从而管理这些区域。
// Manage zone recovery positions void ManageZones() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price. // Open recovery trades based on zones if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { currentLotSize *= multiplier; //--- Increase lot size for recovery. OpenTrade(ORDER_TYPE_SELL); //--- Open a SELL order for recovery. } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { currentLotSize *= multiplier; //--- Increase lot size for recovery. OpenTrade(ORDER_TYPE_BUY); //--- Open a BUY order for recovery. } }
为了管理已开仓的交易,我们定义了一个名为“ManageZones”的void函数,该函数负责根据预定义的价格区域来管理反转交易。在此函数内部,我们首先使用带有SYMBOL_BID参数的SymbolInfoDouble函数来获取指定交易品种的当前卖出价。这为我们提供了资产当前交易的市场价格。
接下来,我们使用“lastOrderType”变量检查上一笔执行订单的交易类型。如果最后一笔交易是买入,并且当前市场价格已经下跌至或低于“zoneLow”(反转区域的下边界),我们通过将“currentLotSize”乘以“multiplier”来增加它,以便为反转交易分配更多资金。之后,我们使用ORDER_TYPE_SELL参数调用“OpenTrade”函数,表明我们需要开一个卖出仓位来管理之前买入交易的亏损。
类似地,如果最后一笔交易是卖出,并且当前市场价格已经上涨至或高于“zoneHigh”(反转区域的上边界),我们再次通过将“currentLotSize”乘以“multiplier”来增加它,从而为反转交易扩大规模。然后,我们使用ORDER_TYPE_BUY参数调用“OpenTrade”函数,开一个买入仓位以期从之前的卖出交易中反转。就这么简单。现在,在我们开立了初始交易和反转交易之后,我们需要一个逻辑在某个点位将它们平仓。那么,让我们在下面定义平仓或目标逻辑。
// Check and close trades at zone targets void CheckCloseAtTargets() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price. // Close BUY trades at target high if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions. if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol. ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number. int retries = 10; while (retries > 0) { if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("Closed BUY position with ticket: ", ticket); break; } else { Print("Failed to close BUY position with ticket: ", ticket, ". Retrying... Error: ", GetLastError()); retries--; Sleep(100); //--- Wait 100ms before retrying. } } if (retries == 0) Print("Gave up on closing BUY position with ticket: ", ticket); } } Reset(); //--- Reset the strategy after closing all positions. } // Close SELL trades at target low else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions. if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol. ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number. int retries = 10; while (retries > 0) { if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("Closed SELL position with ticket: ", ticket); break; } else { Print("Failed to close SELL position with ticket: ", ticket, ". Retrying... Error: ", GetLastError()); retries--; Sleep(100); //--- Wait 100ms before retrying. } } if (retries == 0) Print("Gave up on closing SELL position with ticket: ", ticket); } } Reset(); //--- Reset the strategy after closing all positions. } }
在这里,我们定义了一个名为“CheckCloseAtTargets”的函数,它负责检查是否有任何已开仓的交易达到了其预定义的目标价格,并相应地平仓。
首先,我们使用带有SYMBOL_BID参数的SymbolInfoDouble函数来获取给定交易品种的当前卖出价。这为我们提供了该交易品种的当前市场价格,我们将用它来与目标价格水平(无论是“zoneTargetHigh”还是“zoneTargetLow”)进行比较,以决定是否应该平仓。
接下来,我们检查最后一笔订单的类型是否为买入,以及当前价格是否已达到或超过了“zoneTargetHigh”(买入交易的目标价格水平)。如果满足这些条件,我们使用PositionsTotal函数从最后一个仓位开始,遍历所有已开仓的仓位。对于每个已开仓的仓位,我们使用PositionGetSymbol函数检查该仓位是否属于同一个交易品种。如果交易品种匹配,我们使用带有“POSITION_TICKET”参数的PositionGetInteger函数来获取该仓位的订单号。
之后,我们通过调用“trade.PositionClose”函数并传入获取到的订单号来尝试平仓。如果仓位成功平仓,我们会打印一条确认消息,说明买入仓位已平仓,并包含订单号。如果平仓失败,我们会重试最多10次,每次都打印一条错误消息,并使用Sleep函数等待100毫秒后再重试。如果在10次重试后我们仍然无法平仓,我们会打印一条失败消息,然后继续处理下一个已开仓的仓位。一旦所有仓位都已平仓或达到重试次数上限,我们就会调用“Reset”函数来重置策略,确保状态被清除,为未来的任何交易做好准备。
类似地,如果最后一笔订单的类型是卖出,并且当前价格已达到或跌至“zoneTargetLow”(卖出交易的目标价格水平)以下,那么将对所有卖出仓位重复此过程。该函数将以相同的方式尝试平掉卖出仓位,必要时进行重试,并在每一步打印状态消息。我们使用了一个外部函数来重置状态,但这里采用的是其逻辑。
// Reset the strategy after hitting targets void Reset() { currentLotSize = initialLotSize; //--- Reset lot size to the initial value. lastOrderType = -1; //--- Clear the last order type. lastOrderPrice = 0.0; //--- Clear the last order price. isRecovery = false; //--- Set recovery state to false. Print("Strategy reset after closing trades."); }
我们定义了一个名为“Reset”的函数,它负责重置策略的内部变量,并为下一次交易或重置场景准备系统。我们首先将“currentLotSize”重置为“initialLotSize”,这意味着在一系列反转交易或达到目标水平后,我们将手数恢复到其原始值。这确保了策略能以初始手数重新开始任何新的交易。
接下来,我们通过将“lastOrderType”设置为-1来清除它,这有效地表明没有之前的订单类型(既不是买入也不是卖出)。这有助于确保在未来的交易逻辑中不会对之前的订单类型产生混淆或依赖。同样,我们将“lastOrderPrice”重置为0.0,清除执行交易的最后价格。然后,我们将“isRecovery”标志设置为false,表示反转过程不再活跃。这一点尤为重要,因为它确保了任何未来的交易都被视为初始交易,而不是反转策略的一部分。
最后,我们使用Print函数打印一条消息,表明策略在平掉所有交易后已成功重置。这在终端中提供了反馈,帮助交易员追踪策略何时被重置,并确保为未来的操作设置了正确的状态。从本质上讲,该函数清除了所有跟踪交易条件、反转状态和交易规模的关键变量,将系统恢复到其默认设置以进行全新的操作。这就是该类处理所有传入信号所需的全部内容。我们现在可以通过传入默认参数来初始化类对象。
ZoneRecovery zoneRecovery(0.1, 200, 400, 2.0, _Symbol); //--- Initialize the ZoneRecovery object with specified parameters.
在这里,我们通过调用其构造函数并传入必要的参数,来创建“ZoneRecovery”类的一个实例。具体来说,我们使用以下值初始化对象“zoneRecovery”:
- 初始手数为“0.1”。这意味着第一笔交易量将为0.1手。
- 区域大小为“200”,这是定义反转区域范围的点数。然后,它乘以_Point,以将该值转换为指定交易品种的实际点值。
- 目标大小为“400”,定义了到目标盈利水平的距离(以点为单位)。与区域大小类似,这个值也使用_Point转换为点值。
- 倍数为“2.0”,它将用于在后续的反转交易中增加手数(如果需要)。
- “_Symbol”被用作此ZoneRecovery实例的交易品种,它对应于交易员正在使用的交易品种的代码。
通过使用这些参数初始化“zoneRecovery”,我们设置了该对象来处理此特定交易策略的交易逻辑,包括管理反转区域、手数调整以及任何将要开立或管理的交易的目标水平。一旦系统执行,该对象就准备好根据定义的反转策略来处理交易操作。我们现在可以进入事件处理程序,我们将专注于信号生成。我们从OnInit事件处理程序开始。在这里,我们只需要初始化指标句柄并将存储数组设置为时间序列。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize RSI indicator rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle. if (rsiHandle == INVALID_HANDLE) { //--- Check if RSI handle creation failed. Print("Failed to create RSI handle. Error: ", GetLastError()); return(INIT_FAILED); //--- Return failure status if RSI initialization fails. } ArraySetAsSeries(rsiBuffer, true); //--- Set the RSI buffer as a time series to align values. Print("Zone Recovery Strategy initialized."); //--- Log successful initialization. return(INIT_SUCCEEDED); //--- Return success status. }
在这里,我们在OnInit函数中初始化RSI指标,并通过执行一系列设置任务来为交易做准备。首先,我们通过调用iRSI函数创建一个RSI指标句柄,传入当前交易品种(_Symbol)、PERIOD_CURRENT时间框架、指定的“rsiPeriod”以及PRICE_CLOSE价格类型。这一步为策略中使用的RSI指标进行了设置。
然后,我们通过检查句柄是否不等于INVALID_HANDLE来验证句柄创建是否成功。如果创建失败,我们使用GetLastError函数打印一条带有具体错误代码的错误消息,并返回“INIT_FAILED”以表示初始化失败。如果句柄创建成功,我们接着使用ArraySetAsSeries将RSI缓冲区设置为时间序列,以使其与图表的时间序列对齐,确保最新的值位于索引0处。最后,我们打印一条成功消息,确认“区域反转策略”已初始化,并返回INIT_SUCCEEDED,表示设置成功,智能交易系统已准备好开始运行。图示如下:
然而,由于我们创建并初始化了一个指标,因此当不再需要该程序时,我们需要释放它以释放资源。以下是我们的逻辑代码。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (rsiHandle != INVALID_HANDLE) //--- Check if RSI handle is valid. IndicatorRelease(rsiHandle); //--- Release RSI indicator handle to free resources. Print("Zone Recovery Strategy deinitialized."); //--- Log deinitialization message. }
在这里,当智能交易系统被移除或停止时,我们对该策略进行反初始化,并释放RSI指标所使用的任何资源。在OnDeinit函数中,我们首先通过确认“rsiHandle”不等于INVALID_HANDLE来检查它是否有效。这确保了在尝试释放它之前,RSI指标句柄是存在的。
如果句柄有效,我们使用IndicatorRelease函数来释放与RSI指标关联的资源,从而确保在EA停止运行后,内存得到妥善管理,不会被继续占用。最后,我们打印一条“区域反转策略已反初始化”的消息,以记录反初始化过程已完成,确认系统已正确关闭。这确保了EA可以被安全地移除,而不会留下任何已分配的不必要资源。这是一个结果示例。
在处理完程序停止的实例后,我们就可以进入最后一个事件处理程序了,这也是最主要的一个,用于处理tick(价格变动)的OnTick事件处理程序。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Copy RSI values if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Attempt to copy RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Log failure if copying fails. return; //--- Exit the function on failure to avoid processing invalid data. } //--- }
在OnTick函数中,我们首先尝试使用CopyBuffer函数将RSI值复制到rsiBuffer”数组中。CopyBuffer函数的调用参数包括:RSI指标句柄rsiHandle”、缓冲区索引0(表示主要的RSI缓冲区)、起始位置1(从哪里开始复制数据)、要复制的值的数量2,以及用于存储复制数据的rsiBuffer”数组。此函数会检索最近的两个RSI值并将其存储在缓冲区中。
接下来,我们通过评估返回值是否大于0来检查复制操作是否成功。如果操作失败(即返回值小于或等于0),我们使用Print函数记录一条错误消息,表明“RSI缓冲区复制”失败,并显示GetLastError代码以提供有关失败的详细信息。记录错误后,我们立即使用return退出函数,以防止基于无效或缺失的RSI数据进行任何进一步的处理。这确保了EA不会尝试使用不完整或错误的数据做出交易决策,从而避免潜在的错误或损失。如果我们不终止该过程,则意味着我们已经获得了必要的请求数据,可以继续做出交易决策。
//--- Check RSI crossover signals datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Ensure processing happens only once per bar. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for RSI crossing below 30 (oversold signal). Print("BUY SIGNAL"); //--- Log a BUY signal. zoneRecovery.HandleSignal(ORDER_TYPE_BUY); //--- Trigger the Zone Recovery BUY logic. } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for RSI crossing above 70 (overbought signal). Print("SELL SIGNAL"); //--- Log a SELL signal. zoneRecovery.HandleSignal(ORDER_TYPE_SELL); //--- Trigger the Zone Recovery SELL logic. } }
在这里,我们在每个新的市场K线上检查RSI交叉信号,以触发潜在的交易。我们首先使用iTime函数获取当前K线的时间戳。该函数接收交易品种(_Symbol)、时间框架(PERIOD_CURRENT)和K线索引(0代表当前K线)。这提供了“currentBarTime”,它代表了最近一根已形成K线的时间戳。
接下来,我们通过比较“currentBarTime”和“lastBarTime”来确保交易逻辑在每个K线上只执行一次。如果时间不同,则意味着形成了新的K线,因此我们继续进行处理。然后,我们将“lastBarTime”更新为与“currentBarTime”相匹配,以跟踪最近处理过的K线,并防止在同一根K线期间重复执行。
下一步是检测RSI交叉信号。我们首先通过比较“rsiBuffer[1]”(来自前一根K线的RSI值)与“rsiBuffer[0]”(来自当前K线的RSI值)来检查RSI值是否已向下穿过30(超卖状况)。如果前一根K线的RSI高于30,而当前K线的RSI等于或低于30,这表明一个潜在的买入信号,因此我们打印一条“BUY SIGNAL”的消息,然后调用“zoneRecovery”对象的“HandleSignal”函数来触发买入订单的反转过程。
类似地,我们检查RSI是否已向上穿过70(超买状况)。如果前一根K线的RSI低于70,而当前K线的RSI等于或高于70,这表明一个潜在的卖出信号,我们打印“SELL SIGNAL”。然后,我们再次调用“HandleSignal”,但这次是针对卖出订单,触发相应的区域反转卖出逻辑。最后,我们只需调用相应的函数来管理已开仓的区域,并在达到目标时将其平仓。
//--- Manage zone recovery logic zoneRecovery.ManageZones(); //--- Perform zone recovery logic for active positions. //--- Check and close at zone targets zoneRecovery.CheckCloseAtTargets(); //--- Evaluate and close trades when target levels are reached.
在这里,我们使用点运算符(“.”)来调用属于“ZoneRecovery”类的函数。首先,我们使用“zoneRecovery.ManageZones()”来执行“ManageZones”方法,该方法处理基于当前价格和定义的反转区域来管理区间反转交易的逻辑。此方法会根据需要调整反转交易的手数并开立新仓位。
接下来,我们调用“zoneRecovery.CheckCloseAtTargets()”来触发“CheckCloseAtTargets”方法,该方法检查价格是否已达到平仓的目标水平。如果满足条件,它会尝试平掉已开仓的交易,确保策略符合其目标利润或亏损边界。通过使用点运算符,我们访问并执行“zoneRecovery”对象上的这些方法,以有效地管理反转过程。为确保这些方法在每个tick上都被成功调用,我们运行程序,结果如下。
从图片中,我们可以看到我们在第一个tick上成功调用了类方法来准备程序,这证实了我们的程序类已连接并准备就绪。为了进一步确认这一点,我们运行程序,以下是交易确认信息。
从图片中,我们可以看到我们确认了一个买入信号,据此开仓,将其添加到区域反转系统,重新计算区域水平,识别出这是一个初始仓位,当达到目标时,我们平掉该仓位并为下一笔交易重置系统。让我们尝试并看看进入区域反转系统的情况。
从图片中,我们可以看到,当市场对我们不利200个点时,我们假设趋势是看涨的,我们通过开一个更大手数(本例中为0.2)的买入仓位来跟随趋势。
我们可以再次看到,当市场达到目标水平时,我们平掉交易并为下一次交易重置。当系统处于反转模式时,我们会忽略任何传入的信号。这证实了我们已成功实现我们的目标,剩下要做的就是回测程序并分析其性能。这将在下一节中处理。
回测与分析
在本节中,我们专注于回测和分析我们的区域反转RSI系统性能的过程。回测使我们能够评估策略在历史数据上的有效性,识别潜在缺陷,并微调参数以在实盘交易中获得更好的结果。
我们首先在MetaTrader 5平台中设置策略测试器。策略测试器允许我们模拟历史市场条件,并像实时发生一样执行交易。要运行回测,我们选择相关的交易品种、时间框架和测试周期。如果我们想在图表上看到交易发生的过程,我们还要确保启用了“可视化模式”。
一旦回测环境准备就绪,我们就为程序配置输入参数。我们指定初始存款、手数以及与区域反转逻辑相关的特定参数。关键输入包括“initial lot size”(初始手数)、“zone size”(区域大小)、“target size”(目标大小)和“multiplier”(倍数)。通过改变这些输入,我们可以分析它们如何影响策略的整体盈利能力。以下是我们所做的更改。
ZoneRecovery zoneRecovery(0.1, 700, 1400, 2.0, _Symbol); //--- Initialize the ZoneRecovery object with specified parameters.
我们将系统配置为从2024年1月1日开始运行一整年,以下是结果。
策略测试器图表:
策略测试报告:
从获得的图表和报告结果来看,我们可以确定我们的策略正按预期工作。然而,我们还可以通过确保我们专注于利润最大化来提高程序的性能,方法是增加一个追踪止损逻辑,即不必等待完整的利润目标(这使我们大部分时间都暴露在反转实例中),而是可以通过应用追踪止损来保护我们已有的少量利润并将其最大化。由于我们只能追踪第一笔交易,我们可以有一个逻辑来确保我们只追踪第一笔交易,如果我们进入反转模式,我们就等待完全反转。因此,我们首先需要一个追踪止损函数。
//+------------------------------------------------------------------+ //| FUNCTION TO APPLY TRAILING STOP | //+------------------------------------------------------------------+ void applyTrailingStop(double slPoints, CTrade &trade_object, int magicNo=0, double minProfitPoints=0){ double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - slPoints*_Point, _Digits); //--- Calculate the stop loss price for BUY trades double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + slPoints*_Point, _Digits); //--- Calculate the stop loss price for SELL trades for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Loop through all open positions ulong ticket = PositionGetTicket(i); //--- Get the ticket number of the current position if (ticket > 0){ //--- Check if the ticket is valid if (PositionSelectByTicket(ticket)){ //--- Select the position by its ticket number if (PositionGetString(POSITION_SYMBOL) == _Symbol && //--- Check if the position belongs to the current symbol (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check if the position matches the given magic number or if no magic number is specified double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the opening price of the position double positionSl = PositionGetDouble(POSITION_SL); //--- Get the current stop loss of the position if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ //--- Check if the position is a BUY trade double minProfitPrice = NormalizeDouble(positionOpenPrice + minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked if (buySl > minProfitPrice && //--- Check if the calculated stop loss is above the minimum profit price buySl > positionOpenPrice && //--- Check if the calculated stop loss is above the opening price (buySl > positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is greater than the current stop loss or if no stop loss is set trade_object.PositionModify(ticket, buySl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss } } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ //--- Check if the position is a SELL trade double minProfitPrice = NormalizeDouble(positionOpenPrice - minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked if (sellSl < minProfitPrice && //--- Check if the calculated stop loss is below the minimum profit price sellSl < positionOpenPrice && //--- Check if the calculated stop loss is below the opening price (sellSl < positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is less than the current stop loss or if no stop loss is set trade_object.PositionModify(ticket, sellSl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss } } } } } } }
此处,我们创建一个名为 "applyTrailingStop"的函数,让我们能够对买单和卖单进行追踪止损。此追踪止损的目的是在市场朝着对我们有利的方向变动时,保护和锁定利润。我们使用CTrade”对象来自动修改交易的止损水平。为了确保追踪止损不会过早激活,我们包含了一个条件,要求在止损开始移动之前必须达到最低利润。这种方法可以防止过早调整止损,并确保我们在追踪前锁定一定数量的利润。
我们在此函数中定义了四个关键参数。slPoints”参数指定了从当前市场价格到新止损水平的距离(以点为单位)。trade_object”参数指的是CTrade”对象,它允许我们管理开仓位、修改止损和调整止盈。“magicNo”参数作为一个唯一标识符来筛选交易。如果magicNo”设置为0,我们将追踪止损应用于所有交易,无论其magic编号是多少。最后,“minProfitPoints”参数定义了在追踪止损激活前必须达到的最低利润(以点为单位)。这确保了我们只在仓位有足够盈利后才调整止损。
在这里,我们首先计算买入和卖出交易的追踪止损价格。对于买入交易,我们通过从当前卖出价中减去“slPoints”来计算新的止损价格。对于卖出交易,我们通过将“slPoints”加到当前买入价上来计算它。这些止损价格使用_Digits进行标准化,以确保基于交易品种价格精度的准确性。此标准化步骤确保价格符合特定交易品种报价的正确小数位数。
接下来,我们遍历所有开仓仓位,从最后一个仓位开始,移动到第一个。这种反向循环方法至关重要,因为在正向循环中修改仓位可能会导致仓位索引出错。对于每个仓位,我们获取其“ticket”(订单编号),即该仓位的唯一标识符。如果订单编号有效,我们使用PositionSelectByTicket函数来选择并访问仓位的详细信息。
一旦我们选中了仓位,我们就检查它是否匹配当前交易品种,以及它的magic编号是否与给定的 "magicNo”匹配。如果magicNo”设置为0,我们将追踪止损应用于所有交易,无论其magic编号是多少。在识别出匹配的仓位后,我们确定它是买入还是卖出交易。
如果该仓位是买入交易,我们计算在止损开始移动之前市场必须达到的最低价格。该值是通过将"minProfitPoints”加到仓位的开仓价上得出的。然后,我们检查计算出的追踪止损价格是否同时高于仓位的开仓价和当前止损。如果满足这些条件,我们使用“trade_object.PositionModify”修改仓位,更新买入交易的止损价格。
如果该仓位是卖出交易,我们遵循类似的过程。我们通过从仓位的开仓价中减去“minProfitPoints”来计算最低盈利价格。我们检查计算出的追踪止损价格是否同时低于仓位的开仓价和当前止损。如果满足这些条件,我们使用“trade_object.PositionModify”修改仓位,更新卖出交易的止损。
现在,有了这个函数,我们需要首先找到初始仓位,然后我们可以向这些函数添加追踪止损逻辑。为此,我们需要在区域反转类中定义一个布尔变量,但重要的一点是,通过将其设为public,使其可以在程序的任何地方访问。
public: bool isFirstPosition;
在这里,我们在“ZoneRecovery”类中有一个名为”isFirstPosition”的public变量。该变量是布尔(bool)类型,意味着它只能持有两个可能的值:true或false。该函数的目的是跟踪当前交易是否处于区域反转过程中的第一个仓位。当"isFirstPosition”为true时,表示之前没有开过任何交易,这是初始仓位。这个区别至关重要,因为处理第一笔交易的逻辑将会改变,因为我们想对其应用追踪止损逻辑。
由于我们将"isFirstPosition”声明为public,因此可以从“ZoneRecovery”类的外部访问和修改它。这使得程序的其他部分可以检查一个仓位是否是一系列交易中的第一个,或相应地更新其状态。现在,在负责开仓的函数内部,一旦仓位开立,我们需要为其分配一个布尔标志,以表明它是否是第一笔仓位。
if (trade.Buy(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_BUY; //--- Mark the last trade as BUY. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isFirstPosition = isRecovery ? false : true; isRecovery = true; //--- Set recovery state to true after the first trade. return true; }
在这里,如果该仓位被注册为反转仓位,我们将"isFirstPosition”变量设置为false;如果"isRecovery”变量为false,则将其设置为true。同样,在构造函数和重置函数中,我们将目标变量默认为false。由此,我们可以转到"OnTick”事件处理程序,并在我们有初始仓位时应用追踪止损。
if (zoneRecovery.isFirstPosition == true){ //--- Check if this is the first position in the Zone Recovery process applyTrailingStop(100, obj_Trade, 0, 100); //--- Apply a trailing stop with 100 points, passing the "obj_Trade" object, a magic number of 0, and a minimum profit of 100 points }
在这里,我们检查变量"zoneRecovery.isFirstPosition”是否为true,这表明这是区域反转过程中的第一个仓位。如果是,我们就调用"applyTrailingStop”函数。传入的参数是:移动止损距离为100点,交易对象为"obj_Trade”,用于识别交易的魔术编号为0,以及最低利润为100点。这确保了一旦交易达到100点的利润,就会应用追踪止损,通过在价格朝着对交易有利的方向变动时移动止损来保护收益。然而,当我们通过追踪止损平仓时,由于我们没有重置区域反转逻辑,因此仍然留有其残余部分。这导致即使我们没有未平仓头寸,系统仍然会开立反转交易订单。这就是我们所说的意思。
从可视化图中,您可以看到,一旦初始仓位被追踪损平仓,我们就必须重置系统。为此,我们需要采用以下代码逻辑。
if (zoneRecovery.isFirstPosition == true && PositionsTotal() == 0){ //--- Check if this is the first position and if there are no open positions zoneRecovery.Reset(); //--- Reset the Zone Recovery system, restoring initial settings and clearing previous trade data }
在这里,我们检查"isFirstPosition”变量是否为true,以及是否不存在任何仓位。如果两个条件都满足,这意味着我们曾有一个初始仓位,无论出于何种原因它已被平仓,而现在它已不复存在,因此我们调用"zoneRecovery.Reset()”函数。这会通过恢复其初始设置并清除任何先前的交易数据来重置区域反转系统,确保反转过程能从头开始。这些修改使系统变得完美。在运行最终测试后,我们得到以下结果。
策略测试器图表:
策略测试报告:
从图中我们可以看到,我们减少了反转仓位的数量,这显著提高了我们的命中率。这验证了我们已达成目标,即创建一个带有动态交易管理逻辑的区域反转系统。
结论
总之,我们演示了如何使用区域反转策略构建一个基于MQL5的EA。通过将RSI指标与“区域反转”逻辑相结合,我们创建了一个能够检测交易信号、管理反转仓位并通过追踪止损来确保利润的系统。关键要素包括信号识别、自动交易执行和动态仓位反转管理。
免责声明:本文旨在作为开发MQL5程序的培训指南。尽管“区域反转RSI”策略为交易管理提供了一种结构化的方法,但市场状况仍然是不可预测的。交易涉及金融风险,过往业绩并不保证未来结果。在实盘交易前,充分的测试和风险管理至关重要。
通过掌握本指南中概述的概念,您可以构建更具适应性的交易系统,并探索算法交易的新策略。祝编程愉快,交易成功!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16705
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。


我找不到 tradq mq 文件,它在哪里?
即使有图像,也能解释得很清楚。
说得好。该策略假定价格会突破最初的交易区间,以挽回损失。我们知道,至少在外汇市场上,价格区间比趋势更常见。减轻灾难性马丁格尔损失的方法是为初始交易添加波动过滤器。
说得好。该策略假设价格会突破最初的交易区间,以挽回损失。我们知道,至少在外汇市场上,价格区间比趋势更常见。减轻灾难性马丁格尔损失的方法是为初始交易添加波动过滤器。
当然可以。
我浏览了一下代码,想知道为什么在买入情况下不使用卖出价进行计算?如果我改变它,会有冲突吗?谢谢。