
使用MQL5开发基于震荡区间突破策略的EA
引言
在本文中,我们将探讨如何使用MetaQuotes Language 5(MQL5,即 MetaTrader 5的编程语言)开发一个基于整单区间突破策略的智能交易系统(EA)。在金融交易的快节奏世界中,利用市场模式和行为的策略对于成功至关重要,而震荡区间突破策略就是其中之一,该策略侧重于识别市场震荡期并在随后的突破时进行交易。该策略在捕捉低波动期之后出现的大幅价格波动方面特别有效。我们将通过以下几步来构建一个采用震荡区间突破策略的智能交易系统:
- 策略概览
- 策略的具体实现
- 在MetaQuotes Language 5 (MQL5)中的实现
- 回测结果
- 结论
到本文结束时,您将全面了解如何在MQL5中开发和实施一个基于震荡区间突破策略的稳健EA,这将使您能够增强自己的交易工具箱。我们将大量使用MetaQuotes Language 5(MQL5)作为我们的基础集成开发环境(IDE)编码环境,并在MetaTrader 5(MT5)交易平台执行文件。因此,拥有上述版本将至关重要。让我们开始吧。
策略概览
为了更容易理解震荡区间突破策略,让我们将其分解为几个部分。
- 震荡区间解释
振荡区间,也称为交易区间,是指金融工具的价格在一个确定的范围内水平振荡,而没有表现出强烈的上涨或下跌趋势的时期。这个时期以低波动性为特征,价格在一个明确的支撑水平(下边界)和阻力水平(上边界)之间振荡。交易者经常利用这个阶段来预测潜在的突破点,即震荡期结束后价格可能在一个方向上大幅移动的点。
- 该策略如何运作
震荡区间突破策略利用震荡期间价格的可预测行为来识别和交易突破。以下是它如何运作的:
识别震荡区间:第一步是通过观察最近的价格走势来检测震荡区间。这包括确定特定数量K线图(蜡烛图/柱形图)中的最高和最低价格,以定义区间的上边界和下边界,这些边界通常作为区间的阻力水平和支撑水平。这里的时间框架选择不是固定的,因为任何图表都可以用于识别该过程,因此,您需要选择一个适合您交易风格的图表时间框架。
监控突破:一旦确定了震荡区间,该策略就会监控价格走势是否突破区间的上边界或下边界。当价格收盘高于阻力水平或低于支撑水平时,就发生了突破。其他交易者考虑对突破同一区间的K线图进行剥头皮交易,即一旦价格跌破或涨破区间极值,他们就认为已经发生了突破。
交易突破:在检测到突破后,该策略将按照突破的方向发起交易。如果价格突破阻力水平,则发出买入订单。相反,如果价格跌破支撑水平,则发出卖出订单。同样,一些交易者考虑等待回调,即在突破后,为了进一步确认,他们等待价格重新测试区间,并且一旦再次以与初始突破相同的方向突破,他们便进入市场。在本例中,我们将不考虑回调选项。
- 策略实现
实现震荡区间突破策略将涉及以下几个步骤:
定义区间参数:确定用于识别震荡区间的柱线数量,并设定构成突破的标准。确定并设置K线范围和价格目标范围。例如,要使震荡区间有效,需要在700点或70个点的价格范围内至少有10根K线。
开发检测逻辑:编写代码以扫描历史价格数据,在指定范围内识别最高点和最低点,并在图表上绘制这些水平。代码应清晰明确,包含震荡区间被视为有效的条件,并清楚概述所做的假设,以避免歧义。
监控实时价格数据:持续监控传入的价格数据,以便在突破发生时立即检测到。必须在每个价格变动时进行监控,如果不必要,则可在每个新K线图生成时进行。
执行交易:实施交易执行逻辑,在检测到突破时发出买入或卖出订单,包括设置适当的止损和止盈水平以管理风险。
优化和测试:使用历史数据对策略进行回测,以优化参数并确保其在实时交易前的有效性。这将帮助您确定最佳参数,并准确指出需要改进或过滤的关键特征,以增强和改进系统。
通过遵循这些步骤,我们可以创建一个强大的工具,利用震荡区间突破策略来利用市场条件。简而言之,以下是创建策略所需的一些参数以及通常所需内容的概述。
策略的具体实现
为了更容易地理解我们传达的概念,让我们通过设计图来直观展示。
- 震荡区间上沿突破
- 震荡区间下沿突破
在MetaQuotes Language 5 (MQL5)中的实现
在掌握了构建震荡区间突破策略所需的基本步骤和方法后,接下来让我们将这一理论自动化,并在MetaTrader 5(MT5)平台上使用MetaQuotes Language 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文件名后,点击“下一步”,再点击“下一步”,然后点击“完成”。 完成这些步骤后,我们现在就可以开始编写和规划我们的策略了。
首先,我们在源代码的开头使用#include包含一个交易实例。这使我们能够访问CTrade类,我们将使用该类来创建一个交易对象。这非常关键,因为我们需要它来开设交易。
#include <Trade/Trade.mqh> // Include the trade library CTrade obj_Trade; // Create an instance of the CTrade class
预处理器会用文件Trade.mqh的内容替换#include <Trade/Trade.mqh>这一行。尖括号表示Trade.mqh文件将从标准目录(通常是terminal_installation_directory\MQL5\Include)中获取。当前目录不会包含在搜索路径中。这行代码可以放在程序的任何位置,但通常,为了代码结构更好和引用更方便,所有的包含指令都放在源代码的开头。声明CTrade类的obj_Trade对象将使我们能够轻松访问该类中包含的方法,这得益于MQL5开发者的设计。
由于我们需要在图表上以图形方式绘制一个区间,因此我们需要给这个区间命名。我们只需要并使用同一个矩形区间对象,因此,将使用单个对象进行可视化。一旦绘制完成,我们只需更新其设置,而无需重新绘制。为了实现一个可以轻易回忆并即时重用的静态名称,我们定义如下:
#define rangeNAME "CONSOLIDATION RANGE" // Define the name of the consolidation range
我们使用#define关键字来定义一个名为“rangeNAME”的宏,其值为“CONSOLIDATION RANGE”,以便轻松存储我们的巩固区间名称,而无需在每次创建区间时都重复键入名称,这大大节省了我们的时间,并降低了提供错误名称的可能性。因此,基本上,宏在编译期间用于文本替换。
再次强调,我们需要存储将要绘制的矩形的坐标。这些坐标是格式为(x, y)的二维坐标,用于明确标识分别记录为x1,y1和x2,y2的第一和第二位置。在价格图表上,x轴代表日期和时间刻度,而y轴代表价格刻度。为了更易于理解和引用,让我们来看一个视觉图示。
从所呈现的图像中,我们现在可以清楚地看到为什么我们需要矩形对象的坐标来进行绘制。以下是我们用来确保存储区间坐标的逻辑,这样我们就不需要在每次更新坐标时都重新声明它们了。
datetime TIME1_X1, TIME2_Y2; // Declare datetime variables to hold range start and end times double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices
在这里,我们声明了两个数据类型为datetime的变量。这里使用的方法称为单一数据类型声明,它用于声明多个相同数据类型的变量。这是一种简写方式,可以在一条语句中声明多个相同类型的变量。它很有用,因为它保持了代码的简洁性,减少了代码行数,并将相关变量组合在一起,使得更容易理解它们属于同一类型且可能用于相似目的,同时保持了代码的一致性。你也可以将它们写成如下形式:
datetime TIME1_X1; datetime TIME2_Y2;
“TIME1_X1”变量存储沿x轴的第一个坐标的时间值,"TIME2_Y2"变量存储沿x轴的第二个坐标的时间值。类似地,我们声明价格坐标如下:
double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices
我们需要不断扫描市场上的每一根新K线,以评估是否因波动率较低而形成了震荡区间。因此,将需要两个变量来存储标志,一个表示区间确实存在,另一个表示价格处于该区间内。
bool isRangeExist = false; // Flag to check if the range exists bool isInRange = false; // Flag to check if we are currently within the range
在这里,我们定义了两个布尔型变量,分别命名为“isRangeExist”和“isInRange”,并对它们进行了初始化。“isRangeExist”变量将作为一个标志,用于指示是否已在图表上识别并绘制了震荡区间。我们将其初始化为false,因为在开始时还没有确定任何区间。同样,“isInRange”变量也被初始化为false,用于确定当前市场价格是否处于已识别的震荡区间内。这些标志对于智能交易系统的逻辑至关重要,因为它们有助于管理区间检测和突破监控过程的状态,确保仅在满足适当条件时才采取行动。
在全局作用域中,我们还需要定义被视为区间的最小K线数量以及以点数为单位的区间大小。正如我们在理论部分已经做过的那样,我们指出这些参数对于保持震荡区间的有效性以及确保我们拥有有意义的震荡区间至关重要。
int rangeBars = 10; // Number of bars to consider for the range int rangeSizePoints = 400; // Maximum range size in points
我们再次声明并初始化两个整型变量:“rangeBars”和“rangeSizePoints”。变量“rangeBars”被设置为10,以指定我们将分析多少个柱形图(或蜡烛图)来确定震荡区间。这意味着我们将回顾过去的10个柱形图,找到最高价和最低价,以此来定义我们的区间。变量“rangeSizePoints”被设置为400,这定义了震荡区间在点数上的最大允许大小。如果在这10个柱形图内的最高价和最低价之间的范围超过了400点,那么它就不被视为一个有效的震荡区间。这些参数对于设定区间的标准以及确保我们在价格数据中识别出有意义的巩固期至关重要。
最后,由于我们将开立头寸,因此我们需要定义止损点和止盈点。
double sl_points = 500.0; // Stop loss points double tp_points = 500.0; // Take profit points
以上就是在全局范围我们所需定义的全部变量。你可能在想,什么是全局作用域呢?全局作用域简单来说,就是程序中一个特定的区域,在这个区域内声明的变量、函数和其他元素在整个代码范围内都是可访问的,不受任何函数或代码块的限制。当变量或函数在全局作用域中被声明时,程序的任何部分都可以访问和修改它们。
我们的所有操作都将在OnTick事件处理器中执行。这将是纯粹的价格行为分析,并且我们将严重依赖这个事件处理器。因此,让我们来看一下除了该函数本身之外,它还需要哪些参数,因为它是这段代码的核心。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- }
如我们所见,这是一个简单但至关重要的函数,它不接受任何参数也不返回任何内容。它只是一个void函数,意味着它不需要返回任何内容。这个函数在EA中使用,当特定商品的价格报价发生新的变动(即新的tick)时执行。
既然我们已经了解到OnTick函数是在价格每次变化时生成的,我们需要定义一些控制逻辑,以便我们能够仅在每个柱状图(bar)上执行一次代码,而不是在每个tick上执行,从而至少避免不必要的代码运行,进而节省设备内存。在寻找震荡区间形态时,这一点是必要的。我们并不需要在每个价格变动(tick)时都去搜索这些形态,因为只要我们还处于同一根蜡烛图上,得到的结果将会是相同的。以下是其逻辑:
int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars
首先,我们声明一个整型变量“currBars”,用于存储指定交易品种和周期(或你可能听说过的时间框架)在图表上的当前柱形图数量。这是通过使用iBars函数实现的,该函数仅接受两个参数,即品种和周期。其次,我们声明一个静态整型变量“prevBars”,并将其初始化为当前的柱形图数量。static关键字确保“prevBars”变量在函数调用之间保持其值,从而有效地记住了上一个价格变动时的柱形图数量。第三,我们声明一个静态布尔型变量“isNewBar”,并将其初始化为false。这个变量将帮助我们跟踪是否有新的柱形图出现。接下来,我们使用条件语句来检查当前柱形图数量是否等于前一个柱形图数量。如果它们相等,则意味着没有新的柱形图形成,因此我们将新柱形图生成的标志设置为false。否则,如果前一个柱形图数量不等于当前柱形图数量,则意味着柱形图数量增加了,这表明出现了新的柱形图。因此,我们将新柱形图生成的标志设置为true,并更新前一个柱形图数量的值为当前柱形图数量。
现在,对于生成的每个柱形图,并且我们还没有确定震荡区间时,我们需要扫描预定义的柱形图数量,以寻找潜在的低波动期。
if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared ... }
我们检查“isRangeExist”是否为false,这意味着尚未确定任何区间,同时“isNewBar”是否为true,这意味着出现了新的柱形图。这确保了只有在尚未识别出震荡区间且已形成新的柱形图时,我们才会继续执行后续操作。
为了确定要在图表上绘制的矩形对象第一个点的坐标,我们需要极值阻力位,即在我们预定义的柱形图扫描范围内的最后一根柱形图的时间和该范围内最高柱形图的价格。
TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range
首先,我们使用iTime函数来设置区间的开始时间,该函数返回给定品种和周期下特定柱形图的开盘时间。该函数接受三个输入参数,其中_Symbol是交易品种(例如,"AUDUSD"),_Period是时间框架(例如,PERIOD_M1表示1分钟柱形图),而"rangeBars"是指定周期前柱形图的索引。结果存储在"TIME1_X1"中,标记了我们震荡区间的开始时间。
接下来,我们使用iHighest函数找到指定范围内最高高价的柱形图,该函数返回具有指定柱形图数量内最高高价柱形图的索引。该函数接受五个参数。我们无需再次解释前两个参数的作用,因为前面已经介绍过。第三个参数"MODE_HIGH"用于指示我们正在寻找最高高价。第四个参数"rangeBars"指定了在扫描分析中要考虑的柱形图数量,最后一个参数1意味着我们从当前正在形成的柱形图之前的柱形图开始查找。从技术上讲,当前正在形成的柱形图索引为0,而它之前的柱形图索引为1。得到的索引存储在整型变量"highestHigh_BarIndex"中。
最后,我们使用iHigh函数从该柱形图中获取最高高价,该函数返回特定柱形图的高价。该函数接受三个输入参数,其中前两个参数非常直接。第三个参数"highestHigh_BarIndex"是在前一步中确定的柱形图索引。高价存储在"PRICE1_Y1"中。这些变量使我们能够定义震荡区间的起点和最高点,这对于绘制区间以及后续检测突破至关重要。
为了获取第二个坐标点,我们采用了与确定第一个点坐标类似的方法。
TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range
代码中的不同之处在于,首先,我们的时间与当前柱形图相关联,当前柱形图的索引为0。其次,为了获取预定义柱形图范围内最低价柱形图的索引,我们使用iLowest函数,并使用"MODE_LOW"来指示我们正在寻找最低低价。最后,使用iLow函数来获取最低价柱形图的价格。简而言之,如果我们采用任意图表,以下是所需坐标的可视化概述:
既然我们已经有了震荡区间的点,我们需要在将它们视为有效点位并在图表上绘制之前,检查其有效性,以确保满足之前在引言部分提到的有效区间的条件。
isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points
我们计算区间内最高高价(PRICE1_Y1)与最低低价(PRICE2_Y2)之间的差值,然后将该差值除以一个点的大小_Point,将其转换为点数。例如,我们可能有最高价为0.66777,最低价为0.66773。它们的数学差值为0.66777 - 0.66773 = 0.00004。假设交易品种的点值为0.00001。现在,将结果除以点值得出0.00004 / 0.00001 = 4点。然后,将这个值与“rangeSizePoints”进行比较,“rangeSizePoints”是以点数定义的最大允许区间大小。
最后,我们检查是否已识别出有效区间,如果是,我们将其绘制到图表上,并通知已成功创建。
if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted }
在这里,我们通过评估“isInRange”变量来检查已识别的震荡区间是否有效。如果变量标志为true,表示区间大小在可接受范围内,我们继续将震荡区间绘制到图表上。为了进行绘制,我们调用“plotConsolidationRange”函数,并传入参数“rangeNAME”、“TIME1_X1”、“PRICE1_Y1”、“TIME2(此处应为TIME2_X2,假设为笔误)”和“PRICE2_Y2”,这些参数共同创建了区间的可视化表示。成功绘制区间后,我们将“isRangeExist”标志设置为true,以表示已识别并绘制了有效区间。此外,我们在终端上打印消息“RANGE PLOTTED”以进行日志记录,确认震荡区间已成功可视化。
负责绘制或更新整固区间的函数代码片段如下:
//+------------------------------------------------------------------+ //| Function to plot the consolidation range | //| rangeName - name of the range object | //| time1_x1 - start time of the range | //| price1_y1 - high price of the range | //| time2_x2 - end time of the range | //| price2_y2 - low price of the range | //+------------------------------------------------------------------+ void plotConsolidationRange(string rangeName,datetime time1_x1,double price1_y1, datetime time2_x2,double price2_y2){ if (ObjectFind(0,rangeName) < 0){ // If the range object does not exist ObjectCreate(0,rangeName,OBJ_RECTANGLE,0,time1_x1,price1_y1,time2_x2,price2_y2); // Create the range object ObjectSetInteger(0,rangeName,OBJPROP_COLOR,clrBlue); // Set the color of the range ObjectSetInteger(0,rangeName,OBJPROP_FILL,true); // Enable fill for the range ObjectSetInteger(0,rangeName,OBJPROP_WIDTH,5); // Set the width of the range } else { // If the range object exists ObjectSetInteger(0,rangeName,OBJPROP_TIME,0,time1_x1); // Update the start time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,0,price1_y1); // Update the high price of the range ObjectSetInteger(0,rangeName,OBJPROP_TIME,1,time2_x2); // Update the end time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,1,price2_y2); // Update the low price of the range } ChartRedraw(0); // Redraw the chart to reflect changes }
首先,我们定义一个void类型的函数,命名为“plotConsolidationRange”,并向它传递5个参数或参数值,即区间名称、第一个点的二维坐标和第二个点的二维坐标。接下来,我们使用条件语句结合ObjectFind函数来检查对象是否存在。如果对象不存在,ObjectFind函数会返回一个负整数。如果对象不存在,我们继续创建一个标识为OBJ_RECTANGLE的对象,该对象根据当前时间和指定的价格,确定第一个和第二个坐标的位置。接着,我们设置该对象的颜色、填充区域和宽度。如果对象已存在,我们只需更新其时间和价格到指定的值,并重绘图表以使当前更改生效。修饰符值0用于指向第一个坐标,而修饰符值1用于指向第二个坐标。
这就是我们需要在图表上绘制已识别整固区间的全部操作。负责此操作的完整源代码如下:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted } } }
编译后的结果如下:
您可以看到,我们在预定义的点之间绘制了区间,并在日志中记录了绘制实例。如果您不需要填充区间,只需将填充属性标志设置为false即可。这样将只绘制矩形的线条属性,并且会启用并应用所定义的线条宽度。下面是相关逻辑:
ObjectSetInteger(0,rangeName,OBJPROP_FILL,false); // Disable fill for the range
这些将产生以下区间:
对于本文,我们将使用一个填充的区间。既然我们已经确定可以设定一个区间,接下来就需要继续开发一个逻辑,该逻辑将分别监控区间的突破以开立头寸,或监控区间的扩展并更新区间的坐标至新值。
接下来,我们只需根据理论部分描述的那样识别突破的情况,如果存在突破的情况,我们就相应地开立市场头寸。这需要在每一个价格变动(tick)时都进行检查,因此我们不受新K线出现的限制。我们首先声明我们将用于开立头寸的买入价(Ask)和卖出价(Bid)价格,一旦满足相应条件。请注意,这也需要在每一个价格变动时进行,以便我们获得最新的价格报价。
double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); // Get and normalize the current Ask price double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); // Get and normalize the current Bid price
在这里,我们声明了双精度浮点型变量来存储最新价格,并通过四舍五入浮点数到符号货币的位数来对其进行规范化,以保持准确性。
由于我们在每一个价格变动时都扫描突破情况,因此我们需要一个逻辑来确保只有当区间存在且我们处于区间内时,程序才会受到压力。大多数情况下,市场将处于中等到高波动性状态,因此震荡区间出现的频率将受到限制。如果区间已被识别且位于图表附近,价格最终将突破该区间。当上述任一条件成立时,就没有必要再检查突破情况或进行进一步的区间更新。我们只需放松等待,直到另一个区间被识别出来。
if (isRangeExist && isInRange){ // If the range exists and we are in range ... }
在这里,我们检查震荡区间是否存在(“isRangeExist”)以及当前价格是否在这个区间内(“isInRange”)。如果这两个条件都为真,我们将继续计算潜在的突破价格。
double R_HighBreak_Prc = (PRICE2_Y2+rangeSizePoints*_Point); // Calculate the high breakout price double R_LowBreak_Prc = (PRICE1_Y1-rangeSizePoints*_Point); // Calculate the low breakout price
为了找到高位突破价格,我们将允许的最大区间大小(以点为单位)加到最低低价上。这个计算是通过将区间大小(以点为单位)乘以点值,然后将结果加到变量“PRICE2_Y2”上,并将最终值存储在一个名为“R_HighBreak_Prc”的双精度浮点型变量中完成的。例如,仍然假设我们的最低低价是0.66773,区间大小(以点为单位)是400,点值是0.00001。将400乘以0.00001,我们得到(400 * 0.00001)0.00400。将这个值加到价格上,我们得到(0.66773 + 0.00400)0.67173。这个最终计算结果是存储在高位突破价格中的值,我们将使用这个值与市场价格进行比较,如果买入价超过这个价格,就定义为突破。同样地,为了确定低位突破价格,我们从最高高价中减去允许的最大区间大小(以点为单位)。这是通过将区间大小(以点为单位)乘以点值,然后从最高高价中减去结果,并将最终值存储在“R_LowBreak_Prc”变量中完成的。
然后,我们检查是否发生突破,如果发生,我们就开立相应的头寸。首先,让我们处理市场价格突破定义的高位突破价格的情况,这表示出现了买入机会。
if (Ask > R_HighBreak_Prc){ // If the Ask price breaks the high breakout price Print("BUY NOW, ASK = ",Ask,", L = ",PRICE2_Y2,", H BREAK = ",R_HighBreak_Prc); // Print a message to buy isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Buy(0.01,_Symbol,Ask,Bid-sl_points*_Point,Bid+tp_points*_Point); return; // Exit the function }
首先,我们检查当前的买入价是否高于高位突破价格。如果这个条件为真,则表示市场价格已经突破了震荡区间的上限。作为响应,我们在终端打印一条消息,记录该事件,并通过包含当前买入价、区间的最低低价和高位突破价格,为买入信号提供上下文。然后,我们将“isInRange”和“isRangeExist”标志重置为假,表示当前的震荡区间不再有效,并防止基于该区间做出进一步决策。接下来,我们使用PositionsTotal函数检查是否存在任何现有头寸,如果存在,则提前退出函数,以避免同时开立多个头寸。如果不存在现有头寸,我们将使用CTrade对象“obj_Trade”的Buy方法开立买入订单,指定交易数量、交易品种、以买入价作为开盘价、止损价格和止盈价格。最后,我们退出函数以完成交易发起过程,确保在当前价格变动(tick)内不再执行其他代码。
最后,我们退出函数以完成交易发起过程,确保在当前价格变动(tick)内不再执行其他代码。
else if (Bid < R_LowBreak_Prc){ // If the Bid price breaks the low breakout price Print("SELL NOW"); // Print a message to sell isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Sell(0.01,_Symbol,Bid,Ask+sl_points*_Point,Ask-tp_points*_Point); return; // Exit the function }
编译后我们得到如下结果。
从图片中可以看出,一旦我们突破了这个区间(在这个例子中是高价),我们就会开立一个买入头寸,并分别设置止损和止盈水平。入场条件和入场价格是完全动态的,你可以使用你认为合适或符合你交易风格的水平。例如,你可以在极端值范围内或根据风险收益比设定入场价格。为了确认,你可以看到买入价是0.68313,而低价是0.67911,这使得高位突破价格为(0.67911 + 0.00400)0.68311。从数学上讲,当前的买价0.68313高于计算出的高位价格0.68311,这满足了我们的高位突破条件,因此会在当前的买入价处开立买入头寸。
目前,这个区间是静态的,不会移动。也就是说,这个矩形区间是固定的。你可以看到,即使我们正确地设定了区间,区间对象也不会更新。因此,如果价格超过了定义的区间价格,我们就需要更新这个区间。为了让这个矩形区间“活”起来,让我们考虑一种逻辑,这种逻辑将始终根据已经生成的K线来更新区间的扩展。首先,让我们考虑当前价格超过了震荡区间内之前记录的最高价的场景。当这个条件满足时,就表示整固区间的上边界需要更新,以反映新的最高价格。这是通过下面的代码实现的。
if (Ask > PRICE1_Y1){ // If the Ask price is higher than the current high price PRICE1_Y1 = Ask; // Update the high price to the Ask price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE1_Y1 TO ASK, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range }
如果价格高于之前记录的最高价格,我们将“PRICE1_Y1”设置为当前的买入价。同时,我们使用iTime函数获取当前时间(将目标柱形索引设为当前柱形,即0),并更新范围“TIME2_Y2”的结束时间为该当前时间。为了记录这些调整并确保清晰性,我们在终端打印一条消息,指示该范围已更新并需要重新绘制。随后,我们调用“plotConsolidationRange”函数,并传入更新后的参数(包括新的最高价格和当前时间),以便在图表上直观地反映这些变化。
为了处理当前出价价格低于震荡区间内之前记录的最低价格的情况(这表明震荡区间的下边界需要更新以反映新的最低价格),我们采用了类似的方法。
else if (Bid < PRICE2_Y2){ // If the Bid price is lower than the current low price PRICE2_Y2 = Bid; // Update the low price to the Bid price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE2_Y2 TO BID, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range }
为了追踪这些变化,我们可以举一个例子,分别展示没有更新和已有更新的两种情况,并且这两种情况都以图形交换格式(GIF)呈现,以便于我们进行比较。
更新前:
更新后:
最后,还有一种情况,即买入价没有超过最高价格,出价价格也没有低于最低价格。如果是这种情况,我们仍然需要将震荡区间扩展到包括最新的完整柱形。以下是一个处理这种情况的代码片段。
else{ if (isNewBar){ // If a new bar has appeared TIME2_Y2 = iTime(_Symbol,_Period,1); // Update the end time to the previous bar time Print("EXTEND THE RANGE TO PREV BAR TIME"); // Print a message indicating the range is extended plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } }
我们使用“isNewBar”标志来检查是否出现了新的柱形。如果确实出现了新的柱形,我们将震荡区间的结束时间“TIME2_Y2”更新为前一柱形的时间。我们使用iTime函数来获取这个时间,将目标柱形索引设为1(即当前柱形的前一个柱形)。为了确保清晰性并记录这一调整,我们在终端打印一条消息,指示区间的结束时间已扩展到前一柱形的时间。然后,我们调用“plotConsolidationRange”函数,并传入更新后的参数(包括新的结束时间),以便在图表上直观地反映这些变化。
以下是区间内更新里程碑的示意图。
以下是基于MQL5语言编写的用于创建震荡区间突破策略EA的完整源代码:
//+------------------------------------------------------------------+ //| CONSOLIDATION RANGE BREAKOUT.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 <Trade/Trade.mqh> // Include the trade library CTrade obj_Trade; // Create an instance of the CTrade class #define rangeNAME "CONSOLIDATION RANGE" // Define the name of the consolidation range datetime TIME1_X1, TIME2_Y2; // Declare datetime variables to hold range start and end times double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices bool isRangeExist = false; // Flag to check if the range exists bool isInRange = false; // Flag to check if we are currently within the range int rangeBars = 10; // Number of bars to consider for the range int rangeSizePoints = 400; // Maximum range size in points double sl_points = 500.0; // Stop loss points double tp_points = 500.0; // Take profit points //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- // Initialization code here (we don't initialize anything) //--- return(INIT_SUCCEEDED); // Return initialization success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- // Deinitialization code here (we don't deinitialize anything) } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted } } double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); // Get and normalize the current Ask price double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); // Get and normalize the current Bid price if (isRangeExist && isInRange){ // If the range exists and we are in range double R_HighBreak_Prc = (PRICE2_Y2+rangeSizePoints*_Point); // Calculate the high breakout price double R_LowBreak_Prc = (PRICE1_Y1-rangeSizePoints*_Point); // Calculate the low breakout price if (Ask > R_HighBreak_Prc){ // If the Ask price breaks the high breakout price Print("BUY NOW, ASK = ",Ask,", L = ",PRICE2_Y2,", H BREAK = ",R_HighBreak_Prc); // Print a message to buy isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Buy(0.01,_Symbol,Ask,Bid-sl_points*_Point,Bid+tp_points*_Point); return; // Exit the function } else if (Bid < R_LowBreak_Prc){ // If the Bid price breaks the low breakout price Print("SELL NOW"); // Print a message to sell isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Sell(0.01,_Symbol,Bid,Ask+sl_points*_Point,Ask-tp_points*_Point); return; // Exit the function } if (Ask > PRICE1_Y1){ // If the Ask price is higher than the current high price PRICE1_Y1 = Ask; // Update the high price to the Ask price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE1_Y1 TO ASK, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } else if (Bid < PRICE2_Y2){ // If the Bid price is lower than the current low price PRICE2_Y2 = Bid; // Update the low price to the Bid price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE2_Y2 TO BID, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } else{ if (isNewBar){ // If a new bar has appeared TIME2_Y2 = iTime(_Symbol,_Period,1); // Update the end time to the previous bar time Print("EXTEND THE RANGE TO PREV BAR TIME"); // Print a message indicating the range is extended plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } } } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Function to plot the consolidation range | //| rangeName - name of the range object | //| time1_x1 - start time of the range | //| price1_y1 - high price of the range | //| time2_x2 - end time of the range | //| price2_y2 - low price of the range | //+------------------------------------------------------------------+ void plotConsolidationRange(string rangeName,datetime time1_x1,double price1_y1, datetime time2_x2,double price2_y2){ if (ObjectFind(0,rangeName) < 0){ // If the range object does not exist ObjectCreate(0,rangeName,OBJ_RECTANGLE,0,time1_x1,price1_y1,time2_x2,price2_y2); // Create the range object ObjectSetInteger(0,rangeName,OBJPROP_COLOR,clrBlue); // Set the color of the range ObjectSetInteger(0,rangeName,OBJPROP_FILL,true); // Enable fill for the range ObjectSetInteger(0,rangeName,OBJPROP_WIDTH,5); // Set the width of the range } else { // If the range object exists ObjectSetInteger(0,rangeName,OBJPROP_TIME,0,time1_x1); // Update the start time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,0,price1_y1); // Update the high price of the range ObjectSetInteger(0,rangeName,OBJPROP_TIME,1,time2_x2); // Update the end time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,1,price2_y2); // Update the low price of the range } ChartRedraw(0); // Redraw the chart to reflect changes }
回测结果
在策略测试器上进行测试后,我们得到了以下结果:
- 余额/净值图:
- 回测结果:
- 按周期进行交易入场:
结论
总结来说,我们可以自信地说,一旦给予必要的思考,震荡区间突破策略的自动化并不像人们想象的那么复杂。从技术层面来看,你可以看到,创建这一策略只需要对策略及其实际需求,或者更准确地说,是创建有效策略设置必须达到的目标有清晰的理解。
总体而言,本文强调了创建震荡区间突破外汇交易策略时必须考虑并清晰理解的理论部分。这包括其定义、概述以及设计方案。此外,策略的编码方面突出了分析蜡烛图、识别低波动期、确定这些时期的支撑和阻力水平、跟踪其突破、可视化输出以及根据生成的信号开立交易头寸等步骤。从长远来看,这实现了震荡区间突破策略的自动化,促进了更快的执行速度和策略的可扩展性。
我们希望你觉得这篇文章有帮助、有趣且易于理解,以便你能够利用所呈现的知识来开发未来的智能交易系统。从技术上讲,这简化了你基于价格行为方法,特别是震荡区间突破策略来分析市场的方式。祝您顺利。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15311



