English Русский Español Deutsch 日本語
preview
在MQL5中自动化交易策略(第5部分):开发自适应交叉RSI交易套件策略

在MQL5中自动化交易策略(第5部分):开发自适应交叉RSI交易套件策略

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

引言

上一篇文章(本系列第4部分)中,我们介绍了多级区域回补系统,展示了如何将区域回补原则扩展到在MetaQuotes Language 5 (MQL5) 中同时管理多个独立的交易设置。在本文(第5部分)中,我们将采用一个全新的方向,介绍自适应交叉RSI交易套件策略。这是一个旨在识别并把握高概率交易机会的综合系统。该策略结合了两个关键的技术分析工具——以自适应移动平均线交叉(周期14和50)作为核心信号生成器,并以一个周期14的相对强弱指标 (RSI) 作为确认过滤器。

此外,它还采用了一个交易日过滤器,以排除低概率的交易时段,从而确保更高的准确性和性能。为了增强可用性,该系统通过在图表上直接绘制箭头并附上清晰的信号描述来可视化已确认的交易信号。还包含一个仪表盘,用于提供策略状态、关键指标和信号活动的实时摘要,让交易者一目了然地掌握全面情况。本文将逐步指导您完成开发此策略的全过程,从规划蓝图、在MQL5中实现、回测其性能,到分析结果。我们将通过以下主题来构建本文内容:

  1. 策略的具体实现
  2. 在MQL5中的实现
  3. 回测
  4. 结论

到文末,您将对如何创建一个基于过滤器的自适应交易系统,并针对不同市场条件对其进行优化以实现稳健性能,有一个切实的理解。让我们开始吧。


策略的具体实现

自适应交叉RSI交易套件策略建立在移动平均线交叉和动量确认的基础上,创造了一种均衡的交易方法。核心信号将源自14期快速移动平均线和50期慢速移动平均线之间的相互作用。当快速移动平均线向上穿越慢速移动平均线时,将产生买入信号,暗示着看涨趋势;而当快速移动平均线向下跌破慢速移动平均线时,则会生成卖出信号,表明看跌趋势。

为了提高这些信号的准确性,将采用一个14期的相对强弱指标 (RSI) 作为确认过滤器。RSI将确保交易与当前的市场动量保持一致,从而减少在超买或超卖条件下入市的可能性。例如,买入信号只有在RSI高于50的阈值时才会被确认,而卖出信号则要求RSI低于其相应的阈值。该策略还将包含一个交易日过滤器,通过避开历史上波动性较低或表现不佳的交易日来优化性能。此过滤器将确保系统只专注于高概率的交易机会。简而言之,策略的总体方案如下。

卖出交易确认方案:

卖出交易策略

买入交易确认方案:

买入交易策略

此外,一旦交易得到确认,系统将在图表上用信号箭头和注释进行标记,清晰地识别入场点。一个仪表盘将提供实时更新,展示信号活动、关键指标以及系统整体状态的快照。这种结构化且自适应的方法将确保策略的稳健性和用户友好性。最终的方案将如下图所示。

FINAL BLUEPRINT


在MQL5中的实现

在了解了所有关于自适应交叉RSI交易套件策略的理论之后,让我们将这些理论自动化,并为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的基础数据开始。这包括EA的名称、版本信息和MetaQuotes的网站链接。我们还将指定EA的版本号,设置为“1.00”。

//+------------------------------------------------------------------+
//|                         Adaptive Crossover RSI Trading Suite.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "EA that trades based on MA Crossover, RSI + Day Filter"
#property strict

当加载程序时,这会将程序基本信息显示出来。然后我们给程序添加一些后面将会用到的全局变量。首先,我们在源代码的开头使用#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开发者的设计。

CTrade 类

之后,我们需要声明几个重要的输入变量,这样用户就可以在不修改代码本身的情况下,将交易值更改为所需值。为了实现这一点,为了清晰起见,我们将输入变量分为几个组,即:常规设置、指标设置和过滤设置。

sinput group "GENERAL SETTINGS"
sinput double inpLots = 0.01; // LotSize
input int inpSLPts = 300; // Stoploss Points
input double inpR2R = 1.0; // Risk to Reward Ratio
sinput ulong inpMagicNo = 1234567; // Magic Number
input bool inpisAllowTrailingStop = true; // Apply Trailing Stop?
input int inpTrailPts = 50; // Trailing Stop Points
input int inpMinTrailPts = 50; // Minimum Trailing Stop Points

sinput group "INDICATOR SETTINGS"
input int inpMA_Fast_Period = 14; // Fast MA Period
input ENUM_MA_METHOD inpMA_Fast_Method = MODE_EMA; // Fast MA Method
input int inpMA_Slow_Period = 50; // Slow MA Period
input ENUM_MA_METHOD inpMA_Slow_Method = MODE_EMA; // Slow MA Method

sinput group "FILTER SETTINGS"
input ENUM_TIMEFRAMES inpRSI_Tf = PERIOD_CURRENT; // RSI Timeframe
input int inpRSI_Period = 14; // RSI Period
input ENUM_APPLIED_PRICE inpRSI_Applied_Price = PRICE_CLOSE; // RSI Application Price
input double inpRsiBUYThreshold = 50; // BUY Signal Threshold
input double inpRsiSELLThreshold = 50; // SELL Signal Threshold

input bool Sunday = false; // Trade on Sunday?
input bool Monday = false; // Trade on Monday?
input bool Tuesday = true; // Trade on Tuesday?
input bool Wednesday = true; // Trade on Wednesday?
input bool Thursday = true; // Trade on Thursday?
input bool Friday = false; // Trade on Friday?
input bool Saturday = false; // Trade on Saturday?

在这里,我们为自适应交叉RSI交易套件程序定义了核心参数和配置,从而能够精确控制其行为。我们将这些设置分为三个主要组:“常规设置 (GENERAL SETTINGS)”、“指标设置 (INDICATOR SETTINGS)”和“过滤器设置 (FILTER SETTINGS)”,以及针对交易日的特定控件。变量类型和枚举的使用增强了系统设计的灵活性和清晰度。

在“常规设置 (GENERAL SETTINGS)”组中,我们定义了交易管理参数。我们使用关键字input来定义可优化参数,使用 sinput 来定义字符串或不可优化的参数。变量“inpLots”指定了交易的手数,而“inpSLPts”则以点为单位设置止损水平,确保每笔交易的风险都得到控制。“inpR2R”变量建立了期望的风险回报比,在风险和潜在回报之间保持有利的平衡。使用“inpMagicNo”分配了一个唯一的交易标识符,程序用它来区分自己的订单。移动止损功能由“inpisAllowTrailingStop”管理,允许用户激活或停用它。“inpTrailPts”和“inpMinTrailPts”变量分别指定了移动止损的距离和最小激活阈值,确保移动止损与市场状况保持一致。

在“指标设置 (INDICATOR SETTINGS)”中,我们配置了移动平均线的参数,它们构成了信号生成的骨干。快速移动平均线的周期由“inpMA_Fast_Period”定义,其计算方法则使用枚举ENUM_MA_METHOD和变量“inpMA_Fast_Method”来选择,该枚举支持 MODE_SMA、MODE_EMA、MODE_SMMA 和 MODE_LWMA 等选项。同样,慢速移动平均线通过“inpMA_Slow_Period”进行设置,其方法则由“inpMA_Slow_Method”决定。这些枚举确保用户可以根据不同市场条件,用他们偏好的移动平均线类型来自定义策略。

“过滤器设置 (FILTER SETTINGS)”组主要关注RSI指标,它充当动量过滤器。变量“inpRSI_Tf”使用 ENUM_TIMEFRAMES 枚举定义,允许用户选择RSI的时间周期,例如 PERIOD_M1、“PERIOD_H1”或“PERIOD_D1”。RSI周期由“inpRSI_Period”指定,而“inpRSI_Applied_Price”(一个ENUM_APPLIED_PRICE枚举)则决定了用于计算的价格数据(例如,“PRICE_CLOSE”、“PRICE_OPEN”或“PRICE_MEDIAN”)。用于验证买入和卖出信号的阈值通过“inpRsiBUYThreshold”和“inpRsiSELLThreshold”设置,确保RSI在执行交易前与市场动量保持一致。

最后,我们使用布尔变量(如“Sunday”、“Monday”等)实现了一个交易日过滤器,从而可以控制EA在特定日期的活动。通过在不太有利的日期禁用交易,系统可以避免在不盈利的潜在条件下进行不必要的风险敞口。之后,我们需要定义将要使用的指标句柄。

int handleMAFast = INVALID_HANDLE;
int handleMASlow = INVALID_HANDLE;
int handleRSIFilter = INVALID_HANDLE;

我们初始化了三个关键变量——“handleMAFast”、“handleMASlow”和“handleRSIFilter”——并将它们设置为 INVALID_HANDLE。通过这样做,我们确保EA以一个干净且受控的状态启动,避免了因未初始化或无效的指标句柄而可能产生的问题。我们使用“handleMAFast”来管理快速移动平均线指标,我们将其配置为根据定义的参数捕捉短期价格趋势。

同样,“handleMASlow”被指定用于处理慢速移动平均线指标,使我们能够跟踪长期价格趋势。这些句柄对于动态检索和处理我们策略所需的移动平均线值至关重要。通过“handleRSIFilter”,我们准备连接到RSI指标,我们用它作为动量过滤器来确认我们的信号。接下来,我们需要定义存储数组,用于存放从指标中检索到的数据。这也需要三个数组。

double bufferMAFast[];
double bufferMASlow[];
double bufferRSIFilter[];

在这里,我们声明了三个动态数组:“bufferMAFast[]”、“bufferMASlow[]”和“bufferRSIFilter[]”。这些数组将作为存储容器,用于收集和管理我们策略中所用指标的计算值。通过这种方式组织数据,我们确保EA在运行期间能够直接、高效地访问指标结果。从这里开始,我们需要进入初始化函数并创建指标句柄。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
//---
   
   handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE);
   handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE);
   
   handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price);

//----

}

在这里,我们初始化策略中将要使用的指标的句柄:快速移动平均线、慢速移动平均线和RSI过滤器。我们首先使用函数iMA来初始化快速移动平均线。此函数需要几个参数。第一个,_Symbol,告诉函数为当前交易品种计算移动平均线。第二个,_Period,指定图表的时间周期(如1分钟、1小时)。

我们还传入快速移动平均线周期(“inpMA_Fast_Period”),它决定了用于计算移动平均线的K线数量。"0"参数是移动平均线的“偏移量”,其中"0"表示无偏移。移动平均线方法(“inpMA_Fast_Method”)指定了它是指数移动平均线还是简单移动平均线,而"PRICE_CLOSE"表示我们使用每根K线的收盘价来计算平均值。

此函数的结果被赋值给"handleMAFast",使我们能够在未来的计算中访问快速移动平均线的值。

接下来,我们以同样的方式,通过调用 iMA 函数来初始化慢速移动平均线。在这里,我们使用相同的 _Symbol_Period 以及慢速移动平均线周期(“inpMA_Slow_Period”)。同样,我们指定了用于计算此移动平均线的方法和价格(“PRICE_CLOSE”)。该值存储在"handleMASlow"中以备将来使用。最后,我们使用函数 iRSI 来初始化RSI过滤器。我们提供 _Symbol 来指定交易品种,RSI的时间周期(“inpRSI_Tf”),RSI周期(“inpRSI_Period”)和应用价格类型(“inpRSI_Applied_Price”)。函数的结果存储在"handleRSIFilter"中,这将使我们能够在策略中使用RSI值来确认交易信号。

由于这些句柄是我们策略的支柱,我们需要确保它们被正确初始化,如果没有,那么我们继续运行程序显然就没有任何意义了。

if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){
   Print("ERROR!无法创建指标句柄。Reveting Now!");
   return (INIT_FAILED);
}

在这里,我们检查指标句柄的初始化是否成功。我们评估是否有任何句柄(“handleMAFast”、“handleMASlow"或"handleRSIFilter”)等于 INVALID_HANDLE,这将表明创建相应指标失败。如果任何句柄失败,我们使用"Print"函数在终端中显示一条错误消息,以提醒我们注意该问题。最后,我们返回 INIT_FAILED,如果任何指标句柄无效,这将停止EA的执行,确保EA不会在错误条件下继续运行。

另一个错误也可能发生在用户提供了不切实际的周期值,即技术上小于或等于零。因此,我们需要检查用户为快速移动平均线、慢速移动平均线和RSI定义的输入周期值,以确保这些周期(“inpMA_Fast_Period”、“inpMA_Slow_Period”、“inpRSI_Period”)大于零。

if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){
   Print("ERROR! Periods cannot be <= 0. Reverting Now!");
   return (INIT_PARAMETERS_INCORRECT);
}

在这里,如果用户输入值不大于零,我们通过返回 INIT_PARAMETERS_INCORRECT 来终止程序。如果我们通过了这里的检查,那么我们就准备好了指标句柄,并且可以将存储数组设置为时间序列。 

ArraySetAsSeries(bufferMAFast,true);
ArraySetAsSeries(bufferMASlow,true);
ArraySetAsSeries(bufferRSIFilter,true);

obj_Trade.SetExpertMagicNumber(inpMagicNo);

Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode());

最后,我们执行几个关键操作来完成初始化过程。首先,我们使用函数 ArraySetAsSeries 将数组(“bufferMAFast”、“bufferMASlow"和"bufferRSIFilter”)设置为时间序列。这很重要,因为它确保了这些数组中的数据以与MetaTrader处理时间序列数据兼容的方式存储——将最新的数据存储在索引0处。通过将每个数组都设置为序列,我们确保在交易期间能以正确的顺序访问指标。

接下来,我们在对象"obj_Trade"上调用方法"SetExpertMagicNumber",并将"inpMagicNo"值作为魔术数字传入。magic数字是EA交易的唯一标识符,确保它们可以区分手动执行或由其他EA执行的交易。最后,我们使用函数 Print 在终端中输出一条成功消息,确认初始化过程已完成。该消息包括账户类型,该类型通过函数"trading_Account_Mode"获取——指明账户是模拟账户还是真实账户。负责此功能的函数如下。

string trading_Account_Mode(){
   string account_mode;
   switch ((ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE)){
      case ACCOUNT_TRADE_MODE_DEMO:
         account_mode = "DEMO";
         break;
      case ACCOUNT_TRADE_MODE_CONTEST:
         account_mode = "COMPETITION";
         break;
      case ACCOUNT_TRADE_MODE_REAL:
         account_mode = "REAL";
         break;
   }
   return account_mode;
}

在这里,我们定义一个 string 类型的函数 “trading_Account_Mode”,用于根据 ACCOUNT_TRADE_MODE 参数的值来确定交易账户类型(无论是模拟账户、竞赛账户还是真实账户)。我们首先声明一个变量 “account_mode”,用于将账户类型以字符串形式存储。然后,我们使用 “switch” 语句来评估账户交易模式,该模式是通过调用函数 “AccountInfoInteger” 并传入参数 ACCOUNT_TRADE_MODE 获取的。此函数将账户的交易模式作为一个整数值返回。switch 语句检查这个整数的值,并将其与可能的账户模式进行比较:

  1. 如果账户模式是 ACCOUNT_TRADE_MODE_DEMO,我们将 “account_mode” 设置为 “DEMO”。
  2. 如果账户模式是 ACCOUNT_TRADE_MODE_CONTEST,我们将 “account_mode” 设置为 “COMPETITION”。
  3. 如果账户模式是 ACCOUNT_TRADE_MODE_REAL,我们将 “account_mode” 设置为 “REAL”。

最后,该函数将 “account_mode” 作为字符串返回,该字符串指明了EA所连接的账户类型。因此,最终的初始化函数如下:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
//---
   
   handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE);
   handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE);
   
   handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price);
   
   if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){
      Print("ERROR!无法创建指标句柄。Reveting Now!");
      return (INIT_FAILED);
   }
   
   if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){
      Print("ERROR!Periods cannot be <= 0. Reverting Now!");
      return (INIT_PARAMETERS_INCORRECT);
   }
   
   ArraySetAsSeries(bufferMAFast,true);
   ArraySetAsSeries(bufferMASlow,true);
   ArraySetAsSeries(bufferRSIFilter,true);
   
   obj_Trade.SetExpertMagicNumber(inpMagicNo);
   
   Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode());
   
//---
   return(INIT_SUCCEEDED);
}

现在我们进入到 OnDeinit事件处理器,在这里我们需要释放指标句柄,因为我们不再需要它们了。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
//---
   IndicatorRelease(handleMAFast);
   IndicatorRelease(handleMASlow);
   IndicatorRelease(handleRSIFilter);
}

为了释放分配给指标句柄的资源,我们首先为每个指标句柄:“handleMAFast”、“handleMASlow” 和 “handleRSIFilter” 调用函数 IndicatorRelease。该函数的目的是释放与EA执行期间初始化的指标句柄相关联的内存和资源。这确保了平台的资源不会被不再使用的指标不必要地占用。接下来,我们进入到 OnTick 事件处理器,我们大部分的交易逻辑将在这里处理。我们首先需要从句柄中检索指标数据。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
//--- Check if data can be retrieved for the fast moving average (MA)
   if (CopyBuffer(handleMAFast,0,0,3,bufferMAFast) < 3){
   //--- Print error message for fast MA data retrieval failure
      Print("ERROR! Failed to retrieve the requested FAST MA data. Reverting.");
   //--- Exit the function if data retrieval fails
      return;
   }
//--- Check if data can be retrieved for the slow moving average (MA)
   if (CopyBuffer(handleMASlow,0,0,3,bufferMASlow) < 3){
   //--- Print error message for slow MA data retrieval failure
      Print("ERROR! Failed to retrieve the requested SLOW MA data. Reverting.");
   //--- Exit the function if data retrieval fails
      return;
   }
//--- Check if data can be retrieved for the RSI filter
   if (CopyBuffer(handleRSIFilter,0,0,3,bufferRSIFilter) < 3){
   //--- Print error message for RSI data retrieval failure
      Print("ERROR! Failed to retrieve the requested RSI data. Reverting.");
   //--- Exit the function if data retrieval fails
      return;
   }

   //---   

}

在这里,我们专注于检索快速移动平均线、慢速移动平均线和RSI过滤器的最新指标数据,以确保EA拥有做出交易决策所需的信息。首先,我们对快速移动平均线句柄(“handleMAFast”)使用函数 CopyBuffer。该函数将指标值提取到相应的缓冲区(“bufferMAFast”)中以进行处理。具体来说,我们从索引0(代表图表上最新的数据)开始请求3个数据点。如果检索到的值数量少于3个,则表示访问所需数据失败。在这种情况下,我们使用 Print 函数打印一条错误消息,并使用 return 运算符提前终止函数。

接下来,我们对慢速移动平均线句柄(“handleMASlow”)及其缓冲区(“bufferMASlow”)重复类似的过程。同样,如果 CopyBuffer 函数未能检索到至少3个数据点,我们会打印一条错误消息并退出函数,以防止进一步执行。最后,我们对RSI过滤器句柄(“handleRSIFilter”)及其缓冲区(“bufferRSIFilter”)使用相同的函数。和之前一样,我们确保请求的数据点被成功检索;否则,将打印一条错误消息,并且函数被终止。如果执行到此处函数尚未返回,说明我们已经获得了必要的数据,可以继续生成信号了。但是,我们希望在每个K线上生成信号,而不是在每个tick上生成。因此,我们需要一个函数来检测新K线的生成。

//+------------------------------------------------------------------+
//|     Function to detect if a new bar is formed                    |
//+------------------------------------------------------------------+
bool isNewBar(){
//--- Static variable to store the last bar count
   static int lastBarCount = 0;
//--- Get the current bar count
   int currentBarCount = iBars(_Symbol,_Period);
//--- Check if the bar count has increased
   if (currentBarCount > lastBarCount){
   //--- Update the last bar count
      lastBarCount = currentBarCount;
   //--- Return true if a new bar is detected
      return true;
   }
//--- Return false if no new bar is detected
   return false;
}

在这里,我们定义 “isNewBar” 函数,该函数旨在检测图表上是否出现了新K线。此函数至关重要,它能确保我们的操作在每个K线上只执行一次,而不是在每个tick上重复执行。我们首先声明一个静态变量 “lastBarCount” 并将其初始化为0。静态变量在函数调用之间会保留其值,这使我们能够将当前状态与之前的状态进行比较。然后,我们使用 iBars 函数获取图表上的K线总数,传入 _Symbol(当前交易品种)和 _Period(当前时间周期)。结果存储在 “currentBarCount” 中。

接下来,我们将 “currentBarCount” 与 “lastBarCount” 进行比较。如果 “currentBarCount” 更大,则表明图表上已形成一根新K线。在这种情况下,我们将 “lastBarCount” 更新为与 “currentBarCount” 相等,并返回 true,以表示出现了新K线。如果未检测到新K线,则函数返回 false。现在我们可以在tick事件处理程序中使用此函数。

//--- Check if a new bar has formed
if (isNewBar()){
//--- Print debug message for a new tick
   //Print("THIS IS A NEW TICK");
   
//--- Identify if a buy crossover has occurred
   bool isMACrossOverBuy = bufferMAFast[1] > bufferMASlow[1] && bufferMAFast[2] <= bufferMASlow[2];
//--- Identify if a sell crossover has occurred
   bool isMACrossOverSell = bufferMAFast[1] < bufferMASlow[1] && bufferMAFast[2] >= bufferMASlow[2];
   
//--- Check if the RSI confirms a buy signal
   bool isRSIConfirmBuy = bufferRSIFilter[1] >= inpRsiBUYThreshold;
//--- Check if the RSI confirms a sell signal
   bool isRSIConfirmSell = bufferRSIFilter[1] <= inpRsiSELLThreshold;

   //---
}

在这里,我们实现了核心逻辑,用于根据移动平均线之间的关系和RSI确认来检测特定的交易信号。该过程首先使用 “isNewBar” 函数检查是否形成了新K线。这确保了后续逻辑在每个K线上只执行一次,避免了在同一K线内重复评估。

如果检测到新K线,我们首先通过评估快速和慢速移动平均线之间的关系来准备识别买入交叉。具体来说,我们检查前一根K线(“bufferMAFast[1]”)的快速移动平均线值是否大于同一根K线(“bufferMASlow[1]”)的慢速移动平均线值,同时,两根K线前(“bufferMAFast[2]”)的快速移动平均线值小于或等于该K线(“bufferMASlow[2]”)的慢速移动平均线值。如果两个条件都为真,我们将布尔变量 “isMACrossOverBuy” 设置为 true,表示发生了买入交叉。

类似地,我们通过检查前一根K线(“bufferMAFast[1]”)的快速移动平均线值是否小于同一根K线(“bufferMASlow[1]”)的慢速移动平均线值,同时两根K线前(“bufferMAFast[2]”)的快速移动平均线值大于或等于该K线(“bufferMASlow[2]”)的慢速移动平均线值,来识别卖出交叉。如果满足这些条件,我们将布尔变量 “isMACrossOverSell” 设置为 true,表示发生了卖出交叉。

接下来,我们将RSI作为检测到的交叉信号的确认过滤器。对于买入确认,我们验证前一根K线(“bufferRSIFilter[1]”)的RSI值是否大于或等于买入阈值(“inpRsiBUYThreshold”)。如果为真,我们将布尔变量 “isRSIConfirmBuy” 设置为 true。类似地,对于卖出确认,我们检查前一根K线(“bufferRSIFilter[1]”)的RSI值是否小于或等于卖出阈值(“inpRsiSELLThreshold”)。如果为真,我们将布尔变量 “isRSIConfirmSell” 设置为 true。现在我们可以使用这些变量来做出交易决策。

//--- Handle buy signal conditions
if (isMACrossOverBuy){
   if (isRSIConfirmBuy){
   //--- Print buy signal message
      Print("BUY SIGNAL");

   //---
}

在这里,我们检查移动平均线是否发生交叉,以及RSI是否确认了该信号。如果所有条件都为真,我们就打印一个买入信号。然而,在开立买入仓位之前,我们需要检查是否遵守了交易日过滤器。因此,我们需要一个函数来保持所有内容的模块化。

//+------------------------------------------------------------------+
//|     Function to check trading days filter                        |
//+------------------------------------------------------------------+
bool isCheckTradingDaysFilter(){
//--- Structure to store the current date and time
   MqlDateTime dateTIME;
//--- Convert the current time into structured format
   TimeToStruct(TimeCurrent(),dateTIME);
//--- Variable to store the day of the week
   string today = "DAY OF WEEK";
   
//--- Assign the day of the week based on the numeric value
   if (dateTIME.day_of_week == 0){today = "SUNDAY";}
   if (dateTIME.day_of_week == 1){today = "MONDAY";}
   if (dateTIME.day_of_week == 2){today = "TUESDAY";}
   if (dateTIME.day_of_week == 3){today = "WEDNESDAY";}
   if (dateTIME.day_of_week == 4){today = "THURSDAY";}
   if (dateTIME.day_of_week == 5){today = "FRIDAY";}
   if (dateTIME.day_of_week == 6){today = "SATURDAY";}
   
//--- Check if trading is allowed based on the input parameters
   if (
      (dateTIME.day_of_week == 0 && Sunday == true) ||
      (dateTIME.day_of_week == 1 && Monday == true) ||
      (dateTIME.day_of_week == 2 && Tuesday == true) ||
      (dateTIME.day_of_week == 3 && Wednesday == true) ||
      (dateTIME.day_of_week == 4 && Thursday == true) ||
      (dateTIME.day_of_week == 5 && Friday == true) ||
      (dateTIME.day_of_week == 6 && Saturday == true)
   ){
   //--- Print acceptance message for trading
      Print("Today is on ",today,". Trade ACCEPTED.");
   //--- Return true if trading is allowed
      return true;
   }
   else {
   //--- Print rejection message for trading
      Print("Today is on ",today,". Trade REJECTED.");
   //--- Return false if trading is not allowed
      return false;
   }
}

在这里,我们创建一个函数 “isCheckTradingDaysFilter”,用于根据用户的输入设置来确定当前日期是否允许交易。这能确保交易只在允许的交易日执行,从而提高精确度,并避免在受限日期进行意外操作。首先,我们定义一个结构化对象 “MqlDateTime dateTIME” 来存储当前的日期和时间。使用 TimeToStruct 函数,我们将当前服务器时间(TimeCurrent)转换到 “dateTIME” 结构中,使我们能够轻松访问星期几等组成部分。

接下来,我们定义一个变量 “today” 并为其分配一个占位符字符串 “DAY OF WEEK”。稍后,该变量将以人类可读的格式存储当前日期的名称。我们使用一系列 if 条件语句,将数字形式的 “day_of_week” 值(0代表周日,6代表周六)映射到其对应的日期名称,并用正确的日期更新 “today” 变量。

在此之后,我们通过将 “dateTIME.day_of_week” 与相应的布尔输入变量(“Sunday”、“Monday” 等)进行比较,来检查当前日期是否允许交易。如果当前日期与某个启用的交易日匹配,则使用 “Print” 打印一条消息,表明允许交易,并包含日期名称,然后函数返回 true。反之,如果不允许交易,则打印一条消息表明交易被拒绝,函数返回 false。从技术上讲,此函数充当一个守门员的角色,确保交易操作符合用户对特定日期的偏好设置。我们可以用它来构建交易日过滤器。

//--- Verify trading days filter before placing a trade
if (isCheckTradingDaysFilter()){
//--- Retrieve the current ask price
   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
//--- Retrieve the current bid price
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
//--- Set the open price to the ask price
   double openPrice = Ask;
//--- Calculate the stop-loss price
   double stoploss = Bid - inpSLPts*_Point;
//--- Calculate the take-profit price
   double takeprofit = Bid + (inpSLPts*inpR2R)*_Point;
//--- Define the trade comment
   string comment = "BUY TRADE";
   
//--- Execute a buy trade
   obj_Trade.Buy(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment);
//--- Initialize the ticket variable
   ulong ticket = 0;
//--- Retrieve the order result ticket
   ticket = obj_Trade.ResultOrder();
//--- Print success message if the trade is opened
   if (ticket > 0){
      Print("SUCCESS. Opened the BUY position with ticket # ",ticket);
   }
//--- Print error message if the trade fails to open
   else {Print("ERROR! Failed to open the BUY position.");}
}

在这里,我们通过调用 “isCheckTradingDaysFilter” 函数来检查当前日期是否允许交易。如果该函数返回 true,我们就会继续收集市场数据并执行交易,以确保交易遵守用户定义的日期过滤器。首先,我们使用 SymbolInfoDouble 函数获取当前市场价格。SYMBOL_ASKSYMBOL_BID 参数分别用于获取当前交易品种(_Symbol)的卖价和买价。这些值分别存储在变量 “Ask” 和 “Bid” 中,为后续计算提供了基础。

接下来,我们计算交易所需的价格水平。“Ask” 价格被设置为 “openPrice”,代表买入仓位的入场价格。我们通过从 “Bid” 价格中减去 “inpSLPts”(输入的止损点数)与 _Point 的乘积,来计算止损价格。同样地,止盈价格是通过将 “inpSLPts”、风险回报比(“inpR2R”)和 _Point 三者的乘积加到 “Bid” 价格上来确定的。这些计算定义了交易的风险和回报边界。

然后,我们定义一个交易注释(“BUY TRADE”),以便为交易打上标签,方便日后参考。之后,我们使用 “obj_Trade.Buy” 方法执行买入交易,将手数(“inpLots”)、交易品种、入场价格、止损价格、止盈价格和注释作为参数传入。此函数将交易订单发送到市场。交易执行后,我们将 “ticket” 变量初始化为 0,并使用 “obj_Trade.ResultOrder” 方法返回的订单号为其赋值。如果订单号大于 0,则表示交易已成功开仓,并打印一条包含该订单号的成功消息。如果订单号仍为 0,则表示交易失败,并显示一条错误消息。对于卖出仓位,我们遵循相同的程序,但条件相反。其代码片段如下:

//--- Handle sell signal conditions
else if (isMACrossOverSell){
   if (isRSIConfirmSell){
   //--- Print sell signal message
      Print("SELL SIGNAL");
   //--- Verify trading days filter before placing a trade
      if (isCheckTradingDaysFilter()){
      //--- Retrieve the current ask price
         double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      //--- Retrieve the current bid price
         double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         
      //--- Set the open price to the bid price
         double openPrice = Bid;
      //--- Calculate the stop-loss price
         double stoploss = Ask + inpSLPts*_Point;
      //--- Calculate the take-profit price
         double takeprofit = Ask - (inpSLPts*inpR2R)*_Point;
      //--- Define the trade comment
         string comment = "SELL TRADE";
         
      //--- Execute a sell trade
         obj_Trade.Sell(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment);
      //--- Initialize the ticket variable
         ulong ticket = 0;
      //--- Retrieve the order result ticket
         ticket = obj_Trade.ResultOrder();
      //--- Print success message if the trade is opened
         if (ticket > 0){
            Print("SUCCESS. Opened the SELL position with ticket # ",ticket);
         }
      //--- Print error message if the trade fails to open
         else {Print("ERROR! Failed to open the SELL position.");}
      }
   }
}

运行程序后,我们得到以下结果。

信号与交易确认

从图片中,我们可以看到我们对交易进行了确认。然而,为了更清晰,最好能在图表上将信号可视化地显示出来。因此,我们需要一个函数来绘制带注释的箭头。

//+------------------------------------------------------------------+
//|    Create signal text function                                   |
//+------------------------------------------------------------------+
void createSignalText(datetime time,double price,int arrowcode,
            int direction,color clr,double angle,string txt
){
//--- Generate a unique name for the signal object
   string objName = " ";
   StringConcatenate(objName, "Signal @ ",time," at Price ",DoubleToString(price,_Digits));
//--- Create the arrow object at the specified time and price
   if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)){
   //--- Set arrow properties
      ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP);
      if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
   }
   
//--- Generate a unique name for the description text object
   string objNameDesc = objName+txt;
//--- Create the text object at the specified time and price
   if (ObjectCreate(0,objNameDesc,OBJ_TEXT,0,time,price)){
   //--- Set text properties
      ObjectSetInteger(0,objNameDesc,OBJPROP_COLOR,clr);
      ObjectSetDouble(0,objNameDesc,OBJPROP_ANGLE,angle);
      if (direction > 0){
         ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_LEFT);
         ObjectSetString(0,objNameDesc,OBJPROP_TEXT,"    "+txt);
      }
      if (direction < 0){
         ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
         ObjectSetString(0,objNameDesc,OBJPROP_TEXT,"    "+txt);
      }
      
   }
   
}

在这里,我们定义了 “createSignalText” 函数,以便在图表上用箭头和描述性文字来直观地表示交易信号。此函数通过标记买入或卖出信号等重要事件,增强了图表的清晰度。首先,我们使用 StringConcatenate 函数为箭头对象生成一个唯一的名称。该名称包含 “Signal” 这个词、指定的时间以及信号的价格。这种唯一的命名方式确保了它不会与图表上的其他对象重叠。

接下来,我们使用 ObjectCreate 函数在图表上指定的时间和价格位置创建一个箭头对象。如果创建成功,我们接着自定义其属性。“arrowcode” 参数决定了要显示的箭头类型,而 “clr” 参数则指定了箭头的颜色。根据信号的方向,对于向上信号,箭头的锚点被设置为顶部(ANCHOR_TOP);对于向下信号,则设置为底部(ANCHOR_BOTTOM)。这确保了箭头的位置与信号的上下文正确对齐。

然后,我们创建一个描述性文本对象来配合箭头。通过在箭头名称后附加 “txt” 描述,为文本对象生成一个唯一的名称。文本对象被放置在与箭头相同的时间和价格坐标上。文本对象的属性被设置以改善其外观和对齐方式。“clr” 参数定义了文本颜色,角度参数决定了其旋转角度。对于向上信号,锚点向左对齐(ANCHOR_LEFT),并在文本前添加空格以调整间距。同样地,对于向下信号,锚点向底部对齐(ANCHOR_BOTTOM),并进行相同的间距调整。

现在我们可以使用这个函数来创建带有相应注释的箭头了。

//--- FOR A BUY SIGNAL
//--- Retrieve the time of the signal
datetime textTime = iTime(_Symbol,_Period,1);
//--- Retrieve the price of the signal
double textPrice = iLow(_Symbol,_Period,1);
//--- Create a visual signal on the chart for a buy
createSignalText(textTime,textPrice,221,1,clrBlue,-90,"Buy Signal");

//...

//--- FOR A SELL SIGNAL
//--- Retrieve the time of the signal
datetime textTime = iTime(_Symbol,_Period,1);
//--- Retrieve the price of the signal
double textPrice = iHigh(_Symbol,_Period,1);
//--- Create a visual signal on the chart for a sell
createSignalText(textTime,textPrice,222,-1,clrRed,90,"Sell Signal");

在这里,我们在图表上创建视觉标记来表示买入和卖出信号。这些标记由箭头和附带的描述性文本组成,以增强图表的清晰度并辅助决策。

对于买入信号:

  • 获取信号的时间:

使用 iTime 函数,我们获取当前交易品种和时间周期(_Symbol_Period)图表上倒数第二根已形成K线(索引为1)的 datetime 时间。这确保了信号对应于一根已确认的K线。

  • 获取信号的价格:

我们使用 iLow 函数来获取同一根K线(索引1)的最低价。这作为我们想要放置标记的位置。

  • 创建视觉信号:

调用 “createSignalText” 函数,并传入获取到的 “textTime” 和 “textPrice” 值,以及其他参数:

  1. “221”:代表买入信号的特定箭头类型的箭头代码。
  2. “1”:信号方向,表示向上移动。
  3. “clrBlue”:箭头和文本的颜色,代表一个积极信号。
  4. “-90”:用于正确对齐的文本角度。
  5. “Buy Signal”:显示在箭头附近的描述性文本。这在图表上直观地标记了买入信号。

对于卖出信号:</s0>

  • 获取信号的时间:

与买入信号一样,我们使用 iTime 来获取索引为1的K线的时间。

  • 获取信号的价格:

使用 iHigh 函数来获取同一根K线的最高价。这代表了卖出信号标记的放置位置。

  • 创建视觉信号:

调用 “createSignalText” 函数,并传入以下参数:

  1. “222”:代表卖出信号的箭头代码。
  2. “-1”:信号方向,表示向下移动。
  3. “clrRed”:箭头和文本的颜色,表示一个消极信号。
  4. “90”:用于对齐的文本角度。
  5. “Sell Signal”:显示在箭头附近的描述性文本。这在图表上为卖出信号添加了一个清晰的标记。

运行程序,我们得到如下输出。

带注释的箭头

从图片中我们可以看到,一旦我们有了确认的信号,图表上就会出现箭头及其相应的注释,以确保清晰度。这为图表增添了专业感,通过清晰的视觉提示,使交易信号更易于解读。现在我们可以进一步为代码添加移动止损功能,以便在达到某些预定义水平时锁定部分利润。为简单起见,我们将使用一个函数来实现。

//+------------------------------------------------------------------+
//|         Trailing stop function                                   |
//+------------------------------------------------------------------+
void applyTrailingStop(int slpoints, CTrade &trade_object,ulong magicno=0,int minProfitPts=0){
//--- Calculate the stop loss price for buy positions
   double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID) - slpoints*_Point,_Digits);
//--- Calculate the stop loss price for sell positions
   double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK) + slpoints*_Point,_Digits);
   
//--- Loop through all positions in the account
   for (int i=PositionsTotal()-1; i>=0; i--){
   //--- Get the ticket of the position
      ulong ticket = PositionGetTicket(i);
   //--- Ensure the ticket is valid
      if (ticket > 0){
      //--- Select the position by ticket
         if (PositionSelectByTicket(ticket)){
         //--- Check if the position matches the symbol and magic number (if provided)
            if (PositionGetSymbol(POSITION_SYMBOL) == _Symbol &&
               (magicno == 0 || PositionGetInteger(POSITION_MAGIC) == magicno)
            ){
            //--- Retrieve the open price and current stop loss of the position
               double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN);
               double positionSl = PositionGetDouble(POSITION_SL);
               
            //--- Handle trailing stop for buy positions
               if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
               //--- Calculate the minimum profit price for the trailing stop
                  double minProfitPrice = NormalizeDouble((positionOpenPrice+minProfitPts*_Point),_Digits);
               //--- Apply trailing stop only if conditions are met
                  if (buySl > minProfitPrice &&
                     buySl > positionOpenPrice &&
                     (positionSl == 0 || buySl > positionSl)
                  ){
                  //--- Modify the position's stop loss
                     trade_object.PositionModify(ticket,buySl,PositionGetDouble(POSITION_TP));
                  }
               }
               //--- Handle trailing stop for sell positions
               else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
               //--- Calculate the minimum profit price for the trailing stop
                  double minProfitPrice = NormalizeDouble((positionOpenPrice-minProfitPts*_Point),_Digits);
               //--- Apply trailing stop only if conditions are met
                  if (sellSl < minProfitPrice &&
                     sellSl < positionOpenPrice &&
                     (positionSl == 0 || sellSl < positionSl)
                  ){
                  //--- Modify the position's stop loss
                     trade_object.PositionModify(ticket,sellSl,PositionGetDouble(POSITION_TP));
                  }
               }
               
            }
         }
      }
   }
   
}

此处,我们在 “applyTrailingStop” 函数中实现了一个移动止损机制,用于动态调整活跃交易仓位的止损水平。这确保了当市场朝有利方向变动时,我们能够锁定利润,同时最小化风险。该函数按以下逻辑运行。首先,我们计算买入和卖出仓位的止损水平。使用 SymbolInfoDouble 函数,我们获取 SYMBOL_BID(买入价)来确定 “buySl” 水平,即从该价格中减去指定的 “slpoints”(以点为单位的止损距离),并使用 NormalizeDouble 函数将其标准化为正确的小数位数。同样地,我们通过将 “slpoints” 加到 SYMBOL_ASK(卖出价)上并对其进行标准化,来计算 “sellSl” 水平。

接下来,我们使用一个反向 for 循环 (“for (int i=PositionsTotal()-1; i>=0; i–)”) 遍历交易账户中的所有活跃仓位。对于每个仓位,我们使用 PositionGetTicket 函数获取其 “ticket”(仓单号)。如果 “ticket” 有效,我们使用 PositionSelectByTicket 函数选择相应的仓位。在循环内部,我们检查该仓位是否与当前的 “symbol”(交易品种)和提供的 “magicno”(magic数字)相匹配。如果 “magicno” 为 0,则我们包含所有仓位,无论其魔术数字是什么。对于符合条件的仓位,我们获取它们的 POSITION_PRICE_OPEN(开仓价格)和 POSITION_SL(当前止损水平)。

对于买入仓位,我们通过将 “minProfitPts”(以点为单位的最小利润)加到开仓价格上并对其进行标准化,来计算 “minProfitPrice”。我们仅在 “buySl” 水平满足所有条件时才应用移动止损:

  • “buySl” 高于 “minProfitPrice”。
  • “buySl” 高于开仓价格。
  • “buySl” 大于当前止损,或者根本没有设置止损(“positionSl == 0”)。

如果这些条件得到满足,我们使用 “CTrade” 对象中的 “PositionModify” 方法来修改仓位的止损。对于卖出仓位,我们通过从开仓价格中减去 “minProfitPts” 并对其进行标准化,来计算 “minProfitPrice”。同样地,如果 “sellSl” 水平满足以下条件,我们就应用移动止损:

  • “sellSl” 低于 “minProfitPrice”。
  • “sellSl” 低于开仓价格。
  • “sellSl” 低于当前止损,或者根本没有设置止损。

如果这些条件得到满足,我们也使用 “PositionModify” 方法来修改仓位的止损。然后,我们可以在每个tick上调用这些函数,为开仓仓位应用移动止损逻辑,如下所示。

//--- Apply trailing stop if allowed in the input parameters
if (inpisAllowTrailingStop){
   applyTrailingStop(inpTrailPts,obj_Trade,inpMagicNo,inpMinTrailPts);
}

在这里,我们调用了移动止损函数,运行程序后,我们得到以下结果。

追踪止损

从图片中我们可以看到,移动止损的目标已成功实现。现在我们需要在图表上将数据可视化。为此,我们需要一个带有主底座和标签的仪表盘。对于底座,我们将需要一个矩形标签。这是该函数的实现。

//+------------------------------------------------------------------+
//|     Create Rectangle label function                              |
//+------------------------------------------------------------------+
bool createRecLabel(string objNAME,int xD,int yD,int xS,int yS,
                  color clrBg,int widthBorder,color clrBorder = clrNONE,
                  ENUM_BORDER_TYPE borderType = BORDER_FLAT,ENUM_LINE_STYLE borderStyle = STYLE_SOLID
){
//--- Reset the last error code
   ResetLastError();
//--- Attempt to create the rectangle label object
   if (!ObjectCreate(0,objNAME,OBJ_RECTANGLE_LABEL,0,0,0)){
   //--- Log the error if creation fails
      Print(__FUNCTION__,": Failed to create the REC LABEL. Error Code = ",_LastError);
      return (false);
   }
   
//--- Set rectangle label properties
   ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD);
   ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD);
   ObjectSetInteger(0, objNAME,OBJPROP_XSIZE, xS);
   ObjectSetInteger(0, objNAME,OBJPROP_YSIZE, yS);
   ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, objNAME,OBJPROP_BGCOLOR, clrBg);
   ObjectSetInteger(0, objNAME,OBJPROP_BORDER_TYPE, borderType);
   ObjectSetInteger(0, objNAME,OBJPROP_STYLE, borderStyle);
   ObjectSetInteger(0, objNAME,OBJPROP_WIDTH, widthBorder);
   ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrBorder);
   ObjectSetInteger(0, objNAME,OBJPROP_BACK, false);
   ObjectSetInteger(0, objNAME,OBJPROP_STATE, false);
   ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false);
   
//--- Redraw the chart to reflect changes
   ChartRedraw(0);
   
   return (true);
}

在我们定义的 “createRecLabel” 布尔函数中,我们通过一系列步骤在图表上创建一个可自定义的矩形标签。首先,我们使用 ResetLastError 函数重置所有之前的错误代码。然后,我们尝试使用 ObjectCreate 函数创建矩形标签对象。如果创建失败,我们会打印一条包含失败原因的错误消息,并返回 “false”。如果创建成功,我们接着使用 ObjectSetInteger 函数为矩形标签设置各种属性。

这些属性允许我们定义矩形的位置、大小、背景颜色、边框样式以及其他视觉方面。我们通过 OBJPROP_XDISTANCE、“OBJPROP_YDISTANCE”、“OBJPROP_XSIZE” 和 “OBJPROP_YSIZE” 来分配 “xD”、“yD”、“xS” 和 “yS” 参数,以确定矩形标签的位置和大小。此外,我们还通过 OBJPROP_BGCOLOR、“OBJPROP_BORDER_TYPE” 和 “OBJPROP_STYLE” 分别设置背景颜色、边框类型和边框样式。

最后,为确保标签的图形得到更新,我们调用 ChartRedraw 函数来刷新图表。如果矩形标签成功创建且所有属性都设置正确,则函数返回 “true”。通过这种方式,我们可以根据提供的参数,用自定义的矩形标签在图表上进行可视化标注。我们对标签函数也执行同样的操作。

//+------------------------------------------------------------------+
//|    Create label function                                         |
//+------------------------------------------------------------------+
bool createLabel(string objNAME,int xD,int yD,string txt,
                  color clrTxt = clrBlack,int fontSize = 12,
                  string font = "Arial Rounded MT Bold"
){
//--- Reset the last error code
   ResetLastError();
//--- Attempt to create the label object
   if (!ObjectCreate(0,objNAME,OBJ_LABEL,0,0,0)){
   //--- Log the error if creation fails
      Print(__FUNCTION__,": Failed to create the LABEL. Error Code = ",_LastError);
      return (false);
   }
   
//--- Set label properties
   ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD);
   ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD);
   ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetString(0, objNAME,OBJPROP_TEXT, txt);
   ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrTxt);
   ObjectSetString(0, objNAME,OBJPROP_FONT, font);
   ObjectSetInteger(0, objNAME,OBJPROP_FONTSIZE, fontSize);
   ObjectSetInteger(0, objNAME,OBJPROP_BACK, false);
   ObjectSetInteger(0, objNAME,OBJPROP_STATE, false);
   ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false);
   
//--- Redraw the chart to reflect changes
   ChartRedraw(0);
   
   return (true);
}

有了这些函数,我们现在可以创建一个函数,以便在需要时处理信息面板的创建。

//+------------------------------------------------------------------+
//|    Create dashboard function                                     |
//+------------------------------------------------------------------+
void createDashboard(){

   //---
}

在这里,我们创建一个名为 “createDashboard” 的 void 类型函数,可以用它来存放创建信息面板的逻辑。为了有效地跟踪变化,我们可以在定义其函数体之前,先在 OnInit 事件处理程序中调用该函数,如下所示。

//---

createDashboard();

//---

调用该函数后,我们就可以定义函数的主体了。我们首先要做的是定义信息面板的主体,为此需要将其名称定义为一个 全局常量

//+------------------------------------------------------------------+
//|    Global constants for dashboard object names                   |
//+------------------------------------------------------------------+
const string DASH_MAIN = "MAIN";

在这里,我们定义了一个常量字符串,const 关键字意味着它在整个程序中都不会被改变。现在,我们使用这个常量来创建标签,如下所示。

//+------------------------------------------------------------------+
//|    Create dashboard function                                     |
//+------------------------------------------------------------------+
void createDashboard(){
//--- Create the main dashboard rectangle
   createRecLabel(DASH_MAIN,10,50+30,200,120,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID);
   
   //---

}

在 “createDashboard” 函数中,我们启动了在图表上创建可视化信息面板的过程。为实现这一目标,我们调用了 “createRecLabel” 函数,该函数负责在图表上绘制一个矩形,作为信息面板的基础。该函数接收了特定的参数,用于定义这个矩形的外观和位置。首先,我们将矩形的名称指定为 “DASH_MAIN”,这将使我们稍后能够识别这个对象。然后,我们通过使用 “xD” 和 “yD” 参数,将其左上角设置在图表的 (10, 50+30) 坐标处,从而定义了矩形的位置。矩形的宽度和高度分别通过 “xS” 和 “yS” 参数设置为 200 和 120 像素,但之后可以进行调整。

接下来,我们定义矩形的视觉外观。矩形的背景颜色被设置为 “clrBlack”(黑色),我们为边框选择了蓝色(“clrBlue”)。边框的宽度为 2 像素,线条样式为实线(STYLE_SOLID),边框类型设置为平面(BORDER_FLAT)。这些设置确保了矩形具有清晰且独特的外观。这个矩形作为信息面板的基础元素,在后续步骤中可以向其添加文本或交互组件等附加元素。不过,让我们先运行当前的里程碑并查看结果。

信息面板

从图片中我们可以看到,信息面板的基础正如我们所预期的那样。然后,我们可以使用标签函数并遵循相同的程序为信息面板创建其他元素。因此,我们按如下方式定义其余的对象。

//+------------------------------------------------------------------+
//|    Global constants for dashboard object names                   |
//+------------------------------------------------------------------+
const string DASH_MAIN = "MAIN";
const string DASH_HEAD = "HEAD";
const string DASH_ICON1 = "ICON 1";
const string DASH_ICON2 = "ICON 2";
const string DASH_NAME = "NAME";
const string DASH_OS = "OS";
const string DASH_COMPANY = "COMPANY";
const string DASH_PERIOD = "PERIOD";
const string DASH_POSITIONS = "POSITIONS";
const string DASH_PROFIT = "PROFIT";

在这里,我们只是定义了其余的对象。同样,我们使用标签函数来创建标题标签,如下所示。

//+------------------------------------------------------------------+
//|    Create dashboard function                                     |
//+------------------------------------------------------------------+
void createDashboard(){
//--- Create the main dashboard rectangle
   createRecLabel(DASH_MAIN,10,50+30,200,120+30,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID);
   
//--- Add icons and text labels to the dashboard
   createLabel(DASH_ICON1,13,53+30,CharToString(40),clrRed,17,"Wingdings");
   createLabel(DASH_ICON2,180,53+30,"@",clrWhite,17,"Webdings");
   createLabel(DASH_HEAD,65,53+30,"Dashboard",clrWhite,14,"Impact");
}

在这里,我们通过使用 “createLabel” 函数添加图标和标题来增强信息面板。该函数被多次调用,以将基于文本的元素放置在图表上的特定位置,从而让我们能够构建一个视觉上吸引人且信息丰富的界面。首先,我们创建一个标记为 “DASH_ICON1” 的图标,它相对于图表放置在 (13, 53+30) 坐标处。该图标由字符代码 40 表示,使用 “CharToString(40)” 函数转换为字符串。此图标以红色(“clrRed”)显示,字体大小为 17,字体样式设置为 “Wingdings”,以将字符渲染为图形符号。

接下来,我们添加另一个标记为 “DASH_ICON2” 的图标,放置在 (180, 53+30) 坐标处。此图标使用 “@” 字符,以白色(“clrWhite”)显示,字体大小为 17。字体样式是 “Webdings”,确保 “@” 字符以装饰性和风格化的方式出现。这是它的表示形式。

WEBDINGS 字体

最后,我们在 (65, 53+30) 位置包含一个标记为 “DASH_HEAD” 的文本标题。标题以白色(“clrWhite”)显示文本 “Dashboard”,字体大小为 14。字体样式设置为 “Impact”,这使标题具有粗体和独特的外观。然后,我们就可以定义其余的标签了。

createLabel(DASH_NAME,20,90+30,"EA Name: Crossover RSI Suite",clrWhite,10,"Calibri");
createLabel(DASH_COMPANY,20,90+30+15,"LTD: "+AccountInfoString(ACCOUNT_COMPANY),clrWhite,10,"Calibri");
createLabel(DASH_OS,20,90+30+15+15,"OS: "+TerminalInfoString(TERMINAL_OS_VERSION),clrWhite,10,"Calibri");
createLabel(DASH_PERIOD,20,90+30+15+15+15,"Period: "+EnumToString(Period()),clrWhite,10,"Calibri");

createLabel(DASH_POSITIONS,20,90+30+15+15+15+30,"Positions: "+IntegerToString(PositionsTotal()),clrWhite,10,"Calibri");
createLabel(DASH_PROFIT,20,90+30+15+15+15+30+15,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY),clrWhite,10,"Calibri");

在这里,我们使用 “createLabel” 函数,用重要的信息标签来填充信息面板。首先,我们创建一个标签 “DASH_NAME”,位置在 (20, 90+30)。该标签以白色(“clrWhite”)显示文本 “EA Name: Crossover RSI Suite”,字体大小为 10,字体样式为 “Calibri”。此标签用作EA的名称,为用户提供清晰的标识。

接下来,我们在 (20, 90+30+15) 位置添加 “DASH_COMPANY” 标签。它显示文本 "LTD: ",其后是账户的公司信息,该信息通过使用参数 ACCOUNT_COMPANYAccountInfoString 函数获取。该标签样式为白色,字体大小为 10,并使用 “Calibri” 字体。随后,“DASH_OS” 标签被放置在 (20, 90+30+15+15)。它显示操作系统版本,文本为 "OS: ",与使用参数 TERMINAL_OS_VERSIONTerminalInfoString 函数的结果相结合。此标签帮助用户了解终端的操作系统,同样采用白色、10号字体和 “Calibri” 字体的样式。

然后,我们在 (20, 90+30+15+15+15) 位置包含 “DASH_PERIOD” 标签。此标签显示图表的当前时间周期,文本为 "Period: ",后跟通过 EnumToString 函数转换周期后得到的结果。白色文本、小号字体和 “Calibri” 字体与信息面板的整体设计保持一致。此外,我们在 (20, 90+30+15+15+15+30) 位置添加 “DASH_POSITIONS” 标签。此标签显示账户当前开仓的总数,文本为 "Positions: ",后跟总持仓数量。此信息对于跟踪活跃交易至关重要。

最后,“DASH_PROFIT” 标签被放置在 (20, 90+30+15+15+15+30+15)。它显示账户的当前利润,文本为 "Profit: ",后跟账户利润函数的结果(利润保留两位小数),以及通过 AccountInfoString 函数获取的账户货币。

最后,一旦程序被移除,我们需要在结束时删除信息面板。因此,我们需要一个函数来删除信息面板。

//+------------------------------------------------------------------+
//|     Delete dashboard function                                    |
//+------------------------------------------------------------------+
void deleteDashboard(){
//--- Delete all objects related to the dashboard
   ObjectDelete(0,DASH_MAIN);
   ObjectDelete(0,DASH_ICON1);
   ObjectDelete(0,DASH_ICON2);
   ObjectDelete(0,DASH_HEAD);
   ObjectDelete(0,DASH_NAME);
   ObjectDelete(0,DASH_COMPANY);
   ObjectDelete(0,DASH_OS);
   ObjectDelete(0,DASH_PERIOD);
   ObjectDelete(0,DASH_POSITIONS);
   ObjectDelete(0,DASH_PROFIT);
   
//--- Redraw the chart to reflect changes
   ChartRedraw();
} 

在这里,我们创建一个 void 函数 “deleteDashboard”,用所有对象名称调用 ObjectDelete 函数,最后使用 ChartRedraw 函数重绘图表以使更改生效。然后,我们在反初始化函数中调用此函数。同样,只要我们有持仓,就需要更新信息面板以显示正确的持仓和利润。以下是我们采用的逻辑。

if (PositionsTotal() > 0){
   ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal()));
   ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
}

在这里,我们检查如果持仓数量大于 0,表示我们有持仓,然后我们就可以更新其属性。结果如下。

利润未重置

从可视化效果中,我们可以看到,一旦持仓被关闭,信息面板并不会更新。因此,我们需要跟踪持仓何时被关闭,并且当没有持仓时,我们将信息面板的值重置为默认值。为此,我们将需要 OnTradeTransaction 事件处理程序。

//+------------------------------------------------------------------+
//|    OnTradeTransaction function                                   |
//+------------------------------------------------------------------+
void  OnTradeTransaction(
   const MqlTradeTransaction&    trans,     // trade transaction structure 
   const MqlTradeRequest&        request,   // request structure 
   const MqlTradeResult&         result     // response structure 
){
   if (trans.type == TRADE_TRANSACTION_DEAL_ADD){
      Print("A deal was added. Make updates.");
      if (PositionsTotal() <= 0){
         ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal()));
         ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
      }
   }
}

在这里,我们设置了 OnTradeTransaction 函数,每当发生与交易相关的交易时,该函数就会被触发。此函数处理交易事件,并根据特定操作更新信息面板上的相关信息。我们首先检查交易交易的 “type”(类型),该类型由 MqlTradeTransaction 类型的 “trans” 参数提供,是否等于 TRADE_TRANSACTION_DEAL_ADD。此条件用于确定是否已将新的交易添加到账户中。当检测到此类交易时,我们向日志打印一条消息 “A deal was added. Make updates.”(“已添加一笔交易。进行更新。”),用于调试或提供信息。

接下来,我们检查通过 PositionsTotal 函数获取的开仓总数是否小于或等于 0。这确保了仅在账户中没有活跃持仓时才执行对信息面板的更新。如果满足条件,我们使用 ObjectSetString 函数来更新信息面板上的两个标签。结果如下。

利润重置

从图像中,我们可以看到,更新确实在每笔交易发生时生效,实现了我们的目标,剩下要做的就是回测程序并分析其性能。这将在下一节中处理。


回测

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

回测结果图形:

图形

回测报告:

报告

这里还有一个视频,展示了在2024年一年期间整个策略的回测情况。


结论

总之,我们展示了如何开发一个强大的 MQL5 EA,该系统集成了技术指标、自动化交易管理和一个交互式信息面板。通过结合移动平均线交叉、相对强弱指数(RSI)和移动止损等工具,以及动态交易更新、移动止损和用户友好界面等功能,我们创建了一个能够生成信号、管理交易并提供实时洞察以进行有效决策的 EA。

免责声明:本文仅用于教学目的。交易涉及重大的财务风险,市场状况可能不可预测。虽然所讨论的策略提供了一个结构化的框架,但过往表现并不保证未来结果。在实盘部署之前,彻底的测试和适当的风险管理是必不可少的。

通过应用这些概念,您可以构建更具适应性的交易系统,并增强您的算法交易策略。祝编程愉快,交易成功!

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

最近评论 | 前往讨论 (2)
1149190
1149190 | 17 2月 2025 在 21:33
我似乎无法复制澳元兑美元在 2024 年期间的回溯测试结果。我得到的结果要糟糕得多。我检查了一下,我的输入参数似乎与教程视频中使用的参数相同。您知道为什么我的结果与文章中的结果不一致吗?
Allan Munene Mutiiria
Allan Munene Mutiiria | 18 2月 2025 在 13:01
1149190 测试结果。我得到的结果要糟糕得多。我检查了一下,我的输入参数似乎与教程视频中使用的参数相同。您知道为什么我的结果与您文章中的结果不一致吗?

您好。视频中显示了所有内容,包括编译、测试时间和使用的输入参数。

MQL5 简介(第 10 部分):MQL5 中使用内置指标的初学者指南 MQL5 简介(第 10 部分):MQL5 中使用内置指标的初学者指南
本文介绍如何使用 MQL5 中的内置指标,重点介绍如何使用基于项目的方法创建基于 RSI 的 EA 交易。您将学习获取和利用 RSI 值、处理流动性清扫以及使用图表对象增强交易可视化。此外,本文强调了有效的风险管理,包括设定基于百分比的风险、实施风险回报率以及应用风险修改来确保利润。
以 MQL5 实现强化分类任务的融汇方法 以 MQL5 实现强化分类任务的融汇方法
在本文中,我们讲述以 MQL5 实现若干融汇分类器,并讨论了它们在不同状况下的功效。
基于LSTM的趋势预测在趋势跟踪策略中的应用 基于LSTM的趋势预测在趋势跟踪策略中的应用
长短期记忆网络(LSTM)是一种特殊的循环神经网络(RNN),其设计初衷是通过有效捕捉数据中的长期依赖关系,并解决传统RNN存在的梯度消失问题,从而实现对时序数据的高效建模。本文将系统阐述如何利用LSTM进行未来趋势预测,进而提升趋势跟踪策略的实战表现。具体内容涵盖这些模块:LSTM关键概念介绍与发展契机、从MetaTrader 5平台提取数据、在Python中构建并训练模型、将机器学习模型嵌入MQL5中、基于统计回测的结果分析与改进方向。
MQL5自动化交易策略(第四部分):构建多层级区域恢复系统 MQL5自动化交易策略(第四部分):构建多层级区域恢复系统
本文将介绍如何在MQL5中开发一个基于相对强弱指数(RSI)生成交易信号的多层级区域恢复(反转)系统(Multi-Level Zone Recovery System)。该系统通过动态数组结构管理多个信号实例,使区域恢复逻辑能够同时处理多重交易信号。通过这种设计,我们展示了如何在保持代码可扩展性和健壮性的前提下,有效应对复杂的交易管理场景。