English Русский Español Deutsch 日本語 Português
preview
一步步学习如何利用公允价值缺口(FVG)或市场不平衡性来交易的策略:一种“聪明资金”的交易方法

一步步学习如何利用公允价值缺口(FVG)或市场不平衡性来交易的策略:一种“聪明资金”的交易方法

MetaTrader 5交易 | 18 十一月 2024, 12:19
1 798 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

在这篇文章中,我们将讨论基于公允价值缺口(FVG)或市场不平衡性策略以及聪明资金的概念方法,来创建EA的基础步骤。从本质上说,基于公允价值缺口策略创建EA的过程是艺术与科学的结合,通常要求交易者不仅能够分析K线图,还能够绘制图表以便可视化这一概念。请随我们一起揭开聪明资金的神秘面纱,并踏上利用其在算法交易领域中的变革力量的征途。我们将通过以下主题来打造基于公允价值缺口的EA:

  1. 不平衡定义
  2. 交易策略描述
  3. 交易策略蓝图
  4. MQL5交易系统
  5. 策略测试结果
  6. 结论


在接下来的分享中,我们将广泛使用MetaQuotes Language 5(MQL5)作为我们的基础集成开发环境(IDE)编码环境,并在MetaTrader 5(MT5)交易平台终端上执行文件。因此,安装好上述版本至关重要。那么,让我们开始吧。


公允价值缺口(FVG)/不平衡的定义

公允价值缺口描述了由买卖压力造成的市场不均衡之间的差异。通常,当市场出现高波动性,完全将空头或多头驱逐出市场时,就会产生这种不平衡的市场压力,从而形成公允价值缺口。这通常表现为市场中出现大规模的单向运动。在图表上,你通常会注意到长长的K线图。具备了这些知识,我们可以采用各种方法来利用公允价值缺口,并可以制定出一种交易策略。

公允价值缺口(FVG)交易策略描述

公允价值缺口交易策略结合了公允价值评估的概念与K线图不平衡性,以识别潜在的交易机会。

以下是结合K线图不平衡性的公允价值缺口交易策略的详细描述:

  • 交易者需进行详尽的基本面分析,以确定公允价值缺口。这是通过分析市场价格行为并使用K线图模式来识别买卖压力之间的不平衡来实现的。K线图模式,如看涨吞没、看跌吞没和十字星,提供了对市场情绪和潜在变化方向的判断。交易者寻找市场价格显著偏离估计公允价值的情况,同时观察K线图明显的不平衡形态。前一交易日收盘价与当前交易日开盘价之间的巨大差距,加上强烈的看涨或看跌K线图模式,可能表明存在潜在的公允价值缺口。与其他图表模式一样,公允价值缺口方法最难的部分是在价格图表上识别这种特殊形态。公允价值缺口要求出现特定的具有指向性的三根K线图模式。当这种情况发生时,公允价值缺口就是第一根和第三根k线图引线之间的距离或区域。

以下是如何在图表上识别公允价值缺口的方法:

  • 寻找长K线图:在尝试确定公允价值缺口时,首先要做的是在价格图表上寻找一个长K线图。这个长K线图的实体与引线的比例应该很明显,理想情况下为70%
  • 检查相邻K线图:在找到这个大K线图后,检查它前面和后面的K线图。这个重要的K线图不应该与附近的K线图完全重叠。不过,在这个K线图的上部和下部可能有轻微的重叠。然后,相邻K线图引线之间的空间就形成了公允价值缺口。
  • 确定公允价值差异:最后一步,必须在价格图表上定义并标出公允价值缺口。在下跌趋势之下,前一根K线图的高点和低点之间的价格范围被称为公允价值缺口。这显现了市场的失衡之处,并可能带来交易机会。同样,上涨趋势也适用这一概念,但条件刚好相反。



  • 入场与出场信号:当识别出公允价值缺口并伴有显著的K线图不平衡时,交易者会据此执行交易。例如,如果市场价格低于估算的公允价值,并且出现看涨吞没形态,交易者可能会考虑入场做多,预期价格会得到调整。相反,如果市场价格高于公允价值,并且出现看跌吞没形态,交易者可能会启动做空头寸。
  • 风险管理:交易者实施风险管理技术,如设置止损订单和调整头寸规模,以减轻交易未按预期展开时可能产生的潜在损失。
  • 监控与调整:交易者持续监控交易,根据不断变化的市场条件和重新评估的公允价值估算来调整其头寸。

如您所见,有两种不同类型的公允价值缺口,每种都对交易者产生不同的影响。详细描述如下:

  • 看跌FVG或低估FVG

这种FVG表明,某一货币对或任何其他金融资产的价格目前低于其公允价值。简而言之,交易者可以预期市场会出现回调,以纠正这种效率低下的情况。如果您在图表上看到一根较大的看跌K线图,这可能表明存在一个低估的FVG。

  • 看涨FVG或高估FVG

另一方面,高估的FVG表明,某一金融资产或货币对目前的交易价格高于其公允价值。此时市场过热,即将出现回调。在上涨之前,交易者应该预期价格会出现回调,因为市场正在进行调整。


FVG交易策略蓝图

在定义了公允价值缺口策略并对其进行描述之后,现在让我们逐步明确执行该策略时需要考虑的特定条件。请记住,市场上可能形成两种类型的FVG。

  • 看涨FVG:根据策略,我们需要找到一个具有显著价格波动的看涨K线,然后评估其相邻的左侧和右侧K线。如果确实如此,我们接下来将按时间序列方式计算第三根K线图与第一根K线图之间的差异。如果差异不在预先设定的限制范围内,我们就认为存在一个看涨FVG,并据此进行交易。FVG成为我们的关注点,并在算法中记录下来。同时,我们在图表上以绿色/青柠色绘制FVG,并为其设置预定义长度以便于观察,这表示我们已经发现了一个看涨FVG形态,并准备进行交易。因此,当价格回调并触及FVG的下部区域时,我们立即发送市价买入订单。止盈点将设置在FVG的上部区域,而止损点则设置在订单开盘价下方,风险与回报的比例为1:3。然而,如果价格在预定义长度内没有回到FVG形态,我们就不再关注它。


同样的过程也适用于所有其他看涨的FVG设置。

  • 看跌FVG分析:在这里,我们再次需要找到一个具有主要价格变动的看跌K线,并评估其左右相邻的K线。如果确实如此,那么我们将继续以时间序列的方式计算第三个K线图与第一个K线图之间的差异。如果这个差异不在预先设定的限制范围内,那么我们就得到了一个看跌的FVG信号,我们将据此进行交易。FVG成为我们的关注点,并在算法中记录下来。我们同时在图表上绘制出预定长度的FVG(这里用红色/番茄色标记),以便进行可视化,这表明我们已经找到了一个看跌的FVG形态,并且我们准备根据这一形态进行交易。现在,如果价格回调并触及到FVG形态的上部区域,我们会立即发送市价卖出订单。止盈点将设置在FVG形态的下方,而止损点则设置在订单开盘价的上方,风险报酬比设定为1:10。然而,如果价格在预定义长度内没有回到FVG形态,我们就不再关注它。


对于所有其他的看跌FVG形态,我们都遵循相同的分析过程。


MQL5中的FVG交易系统

在了解了FVG交易策略的所有理论之后,让我们将这些理论应用于实践,在MQL5中为MetaTrader 5平台编写一个EA。 

要在MetaTrader 5终端中创建EA,请点击“工具”选项卡并选择“MetaQuotes语言编辑器”,或者简单地在键盘上按F4键。这样就会打开MetaQuotes语言编辑器环境,该环境允许用户编写自动交易、技术指标、脚本和函数库。


现在点击新建,检查EA(模板),然后点击下一步。


打开新文件

然后,输入您想要的EA文件名,分两次点击“下一步”,最后点击“完成”。完成这些步骤后,我们现在就可以开始编写我们的FVG策略程序。

首先,我们在源代码的开头使用#include指令来包含一个交易实例。获得了CTrade类的访问权限后,我们将使用该类来创建一个交易对象。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

同样,我们使用#define定义一个变量,将其作为前缀分配给所有创建的FVG矩形及其颜色。

#define FVG_Prefix "FVG REC "
#define CLR_UP clrLime
#define CLR_DOWN clrRed

接下来说明上述参数的重要性。


我们需要预先定义一些其他变量,以进一步简化EA编码。以下是判断不平衡柱形图范围是否可行的最低标准,在这种情况下,绘制出的矩形的长度是从中心不平衡柱形图向外延伸的柱形图数量。我们将这些定义为全局变量。

int minPts = 100;
int FVG_Rec_Ext_Bars = 10;

以下是插图。我们将最小点定义为minPts,将矩形长度描绘的条形范围定义为FVG_Rec_Bars。


最后,我们定义了四个数组,分别用于存储字符串、整数、日期时间和布尔数据类型变量,这些数组将用于存储我们在创建EA时所使用的数据。这些仍然是全局变量。

string totalFVGs[];
int barINDICES[];
datetime barTIMEs[];
bool signalFVGs[];

OnInit 部分,我们只搜索图表上可见柱形图的FVG设置。这将使我们的EA具有更多类似指标主导的特性。因此,如果用户启动图表,他们仍然能够看到先前的FVG设置,并且等待在前面的K线图上创建更多设置。 

首先,我们获取图表上所有可见的柱形图,并通知它们的数量。

   int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
   Print("Total visible bars on chart = ",visibleBars);

如果图表上没有矩形对象,我们将释放存储数组并将其大小重置为0,以便为新数据做好准备。这是一个非常重要的步骤,因为用户可能在再次初始化EA之前删除了图表上的所有对象。

   if (ObjectsTotal(0,0,OBJ_RECTANGLE)==0){
      Print("No FVGs Found, Resizing storage arrays to 0 now!!!");
      ArrayResize(totalFVGs,0);
      ArrayResize(barINDICES,0);
      ArrayResize(signalFVGs,0);
   }

为了避免对象重叠,我们再次移除所有先前的FVG设置,并根据当前应用的图表环境属性创建新的设置。这是通过给我们的FVG设置添加一个前缀来实现的,这个前缀确保我们只删除由我们的EA创建的FVG矩形对象,从而使其能够与其他EA兼容。

   ObjectsDeleteAll(0,FVG_Prefix);

接下来,我们将遍历图表上所有可见的柱形图,获取它们的属性,然后检查它们是否满足成为有效FVG设置的条件。

下面开始逐步说明:

为了搜索看涨FVG设置,我们获取第一根柱形图(索引为i)的最低价,如果我们假设已经有一个仅包含3根柱形图的设置,那么这实际上是我们的第0根柱形图。然后,我们获取第三根柱形图(索引为i+2)的最高价,并计算它们之间的点数差距(即高低价差)。

      double low0 = iLow(_Symbol,_Period,i);
      double high2 = iHigh(_Symbol,_Period,i+2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);

在搜索看跌设置时,应使用相同的逻辑。为了搜索看跌FVG设置,我们获取第一根柱形图(索引为i)的最高价,如果我们假设已经有一个仅包含3根柱形图的设置,那么这实际上是我们的第0根柱形图。然后,我们获取第三根柱形图(索引为i+2)的最低价,并计算它们之间的点数差距(即高低价差)。

      double high0 = iHigh(_Symbol,_Period,i);
      double low2 = iLow(_Symbol,_Period,i+2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);

在获取了点数差距之后,我们现在可以使用返回的值来检查是否存在FVG设置。为了实现这一点,我们使用了两个布尔变量: 

  1. FVG_UP - 检查柱形图索引i的最低价是否大于柱形图索引i+2的最高价,并且同时,计算出的点数差距大于允许的最小矩形点数。

  2. FVG_DOWN - 检查柱形图索引i的最高价是否小于柱形图索引i+2的最低价,并且同时,计算出的点数差距大于允许的最小矩形点数。

显然,检查最小点数确保我们只拥有有效且有意义的FVG设置,这些设置是由可能的价格自发波动引起的,并且不会使我们的图表变得杂乱无章。

      bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
      bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;

一旦我们确认了任何FVG设置,我们就会继续创建相应的FVG设置,并在存储数组中记录其数据。

我们定义了一个名为time1的datetime类型变量,用于存储中间或第二根柱形图的时间,索引为i+1,即矩形开始的位置。

同样,我们定义了一个名为price1的双精度类型变量,在这里我们使用三元运算符:如果是看涨FVG设置,则返回第三根柱形图的最高价;如果是看跌FVG设置,则返回第一根柱形图的最高价。

从技术上讲,这构成了我们即将绘制的矩形对象的第一个点的坐标。

         datetime time1 = iTime(_Symbol,_Period,i+1);
         double price1 = FVG_UP ? high2 : high0;

在定义了第一个坐标之后,我们接着定义第二个坐标。 

我们定义了一个名为time2的datetime类型变量,用于存储结束柱形图的时间,即绘制矩形结束的位置。这可以通过简单地向起始时间time1(即矩形开始的时间)添加要扩展矩形的柱形图数量来实现。

同样,我们定义了一个名为price2的双精度类型变量,在这里我们使用三元运算符:如果是看涨FVG设置,则返回第一根柱形图的最低价;如果是看跌FVG设置,则返回第三根柱形图的最低价。

         datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
         double price2 = FVG_UP ? low0 : low2;

下面是确保平滑创建矩形所需的可视化坐标。


在获取了FVG设置的坐标之后,我们需要为该FVG矩形对象命名。

我们使用预定义的前缀作为FVG的前缀,并在其后添加创建时间。这确保了一旦我们在那个柱形图时间创建了FVG对象,我们就无法再次创建它,因为在那个特定的柱形图时间,已经存在一个具有相同标题的对象。同时也确保了唯一性,因为肯定不会有其他相同的柱形图时间。

         string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";

我们还需要为FVG设置分配不同的颜色,以便区分看涨和看跌设置。看涨设置被赋予预定义的看涨颜色,即CLR_UP,看跌设置被赋予预定的看跌颜色,即CLE_DOWN。

         color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN;

到目前为止,我们已经具备了绘制相应FVG设置所需的要素。 

为了轻松完成这项任务,我们在全局作用域中创建了一个空函数。我们将该函数定义为CreateRec,并传入对象名称、第一个点和第二个点的坐标,以及颜色等变量,这些变量在创建FVG设置时是必需的。

void CreateRec(string objName,datetime time1,double price1,
               datetime time2, double price2,color clr){
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2);
      
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_FILL,true);
      ObjectSetInteger(0,objName,OBJPROP_BACK,false);
      
      ChartRedraw(0);
   }
}

在创建函数之后,我们可以用它在图表上绘制相应的FVG矩形,并传入预先准备好的参数。

         CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr);

在创建了FVG设置之后,我们需要将数据保存至存储数组中。但是,由于我们创建的是动态数组,因此首先需要使用ArrayResize函数来调整它们的大小,以便能够包含附加的数据值。

         ArrayResize(totalFVGs,ArraySize(totalFVGs)+1);
         ArrayResize(barINDICES,ArraySize(barINDICES)+1);

在调整好数据存储数组的大小之后,我们可以继续将新数据添加到数组中。

         totalFVGs[ArraySize(totalFVGs)-1] = fvgNAME;
         barINDICES[ArraySize(barINDICES)-1] = i+1;

到目前为止,我们已经成功地为图表上所有可见的柱形图创建了FVG设置。以下结果是一个里程碑。


用户的需求可能是,一旦确认FVG设置后,就需要将这些已经确认的设置截断,以表示已经成功完成了一个FVG设置。所以,一旦价格得到确认,我们就需要截断这些设置中多余的部分。为了实现这一点,我们需要遍历所有已创建和确认的FVG设置,获取它们的详细信息,根据控制逻辑检查这些细节,并最后更新这些设置。

以下是实现这一目标的分步流程:

使用for循环功能遍历所有创建的FVG设置。

   for (int i=ArraySize(totalFVGs)-1; i>=0; i--){// ... }

获取/检索设置数据。

      string objName = totalFVGs[i];
      string fvgNAME = ObjectGetString(0,objName,OBJPROP_NAME);
      int barIndex = barINDICES[i];
      datetime timeSTART = (datetime)ObjectGetInteger(0,fvgNAME,OBJPROP_TIME,0);
      datetime timeEND = (datetime)ObjectGetInteger(0,fvgNAME,OBJPROP_TIME,1);
      double fvgLOW = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,0);
      double fvgHIGH = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,1);
      color fvgColor = (color)ObjectGetInteger(0,fvgNAME,OBJPROP_COLOR);

遍历所有的矩形扩展柱状图,即所选矩形的长度。

      for (int k=barIndex-1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){//... }

获取柱状图数据。

         datetime barTime = iTime(_Symbol,_Period,k);
         double barLow = iLow(_Symbol,_Period,k);
         double barHigh = iHigh(_Symbol,_Period,k);

如果矩形在时间序列中超出了第一个可见柱形图的范围,则简单地将其截断到第一个柱形图的时间,即索引为0的柱状图,简言之,就是当前柱形图的时间。在检测到溢出时,更新FVG以匹配当前柱形图的时间,并中断操作循环。

         if (k==0){
            Print("OverFlow Detected @ fvg ",fvgNAME);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }

为了方便更新设置,我们创建了一个名为 UpdateRec 的空函数,并向该函数传递要更新的矩形对象名称以及该矩形对象两个关键点坐标。

void UpdateRec(string objName,datetime time1,double price1,
               datetime time2, double price2){
   if (ObjectFind(0,objName) >= 0){
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);
      
      ChartRedraw(0);
   }
}

在这样的情况下,如果FVG设置是看跌FVG(通过颜色来识别),并且所选柱形图的最高价高于FVG对象的上边界坐标,这表示FVG是有效的,因此应该相应地对其进行截断,即更新第二个点的坐标到FVG被突破的K形图上。同样地,对于看涨FVG设置也适用上述逻辑。

         if ((fvgColor == CLR_DOWN && barHigh > fvgHIGH) ||
            (fvgColor == CLR_UP && barLow < fvgLOW)
         ){
            Print("Cut Off @ bar no: ",k," of Time: ",barTime);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }


一切就绪后,我们将存储阵列的大小调整为0,为OnTick部分做好准备。

   ArrayResize(totalFVGs,0);
   ArrayResize(barINDICES,0);

负责在图表的可见柱形图上创建FVG设置,并根据需要进行更新的完整OnInit代码如下所示:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
   Print("Total visible bars on chart = ",visibleBars);
   
   if (ObjectsTotal(0,0,OBJ_RECTANGLE)==0){
      Print("No FVGs Found, Resizing storage arrays to 0 now!!!");
      ArrayResize(totalFVGs,0);
      ArrayResize(barINDICES,0);
      ArrayResize(signalFVGs,0);
   }
   
   ObjectsDeleteAll(0,FVG_Prefix);
   
   for (int i=0; i<=visibleBars; i++){
      //Print("Bar Index = ",i);
      double low0 = iLow(_Symbol,_Period,i);
      double high2 = iHigh(_Symbol,_Period,i+2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);
      
      double high0 = iHigh(_Symbol,_Period,i);
      double low2 = iLow(_Symbol,_Period,i+2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);
      
      bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
      bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;
      
      if (FVG_UP || FVG_DOWN){
         Print("Bar Index with FVG = ",i+1);
         datetime time1 = iTime(_Symbol,_Period,i+1);
         double price1 = FVG_UP ? high2 : high0;
         datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
         double price2 = FVG_UP ? low0 : low2;
         string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";
         color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN;
         CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr);
         Print("Old ArraySize = ",ArraySize(totalFVGs));
         ArrayResize(totalFVGs,ArraySize(totalFVGs)+1);
         ArrayResize(barINDICES,ArraySize(barINDICES)+1);
         Print("New ArraySize = ",ArraySize(totalFVGs));
         totalFVGs[ArraySize(totalFVGs)-1] = fvgNAME;
         barINDICES[ArraySize(barINDICES)-1] = i+1;
         ArrayPrint(totalFVGs);
         ArrayPrint(barINDICES);
      }
   }
   
   for (int i=ArraySize(totalFVGs)-1; i>=0; i--){
      string objName = totalFVGs[i];
      string fvgNAME = ObjectGetString(0,objName,OBJPROP_NAME);
      int barIndex = barINDICES[i];
      datetime timeSTART = (datetime)ObjectGetInteger(0,fvgNAME,OBJPROP_TIME,0);
      datetime timeEND = (datetime)ObjectGetInteger(0,fvgNAME,OBJPROP_TIME,1);
      double fvgLOW = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,0);
      double fvgHIGH = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,1);
      color fvgColor = (color)ObjectGetInteger(0,fvgNAME,OBJPROP_COLOR);
      
      Print("FVG NAME = ",fvgNAME," >No: ",barIndex," TS: ",timeSTART," TE: ",
            timeEND," LOW: ",fvgLOW," HIGH: ",fvgHIGH," CLR = ",fvgColor);
      for (int k=barIndex-1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){
         datetime barTime = iTime(_Symbol,_Period,k);
         double barLow = iLow(_Symbol,_Period,k);
         double barHigh = iHigh(_Symbol,_Period,k);
         //Print("Bar No: ",k," >Time: ",barTime," >H: ",barHigh," >L: ",barLow);
         
         if (k==0){
            Print("OverFlow Detected @ fvg ",fvgNAME);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }
         
         if ((fvgColor == CLR_DOWN && barHigh > fvgHIGH) ||
            (fvgColor == CLR_UP && barLow < fvgLOW)
         ){
            Print("Cut Off @ bar no: ",k," of Time: ",barTime);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }
      }
      
   }
   
   ArrayResize(totalFVGs,0);
   ArrayResize(barINDICES,0);

   return(INIT_SUCCEEDED);
}

OnTick部分,通常会使用与OnInit相似的函数和逻辑。在OnTick处理中,我们会遍历所有预定义的先前扩展柱形图,或者更具体地说,是遍历FVG设置在当前柱形图之前的长度范围内的所有柱形图。我们的目标是搜索潜在的FVG设置,在找到并确认后创建它们。 

以下是在OnTick中与OnInit中创建有关FVG设置循环的不同之处:

在OnInit FVG设置循环中,我们考虑图表上所有可见的柱形图。相反,在OnTick中,我们只考虑最后一个预定义的扩展长度。

   for (int i=0; i<=FVG_Rec_Ext_Bars; i++){//... }

在获取柱状图信息时,我们从当前柱状图索引之前的柱状图开始,因为柱状图仍处于形成过程中。因此,我们将所选索引+1。

      double low0 = iLow(_Symbol,_Period,i+1);
      double high2 = iHigh(_Symbol,_Period,i+2+1);

在OnInit FVG截断循环中,我们会截断那些已经确认并测试过的FVG设置。在OnTick部分,我们不会截断这些设置,因为我们想要查看它们。相反,我们会分别发送即时市价单。再次强调一下,一旦价格超出了柱形图的长度范围,就意味着我们无法对该设置进行交易,因此我们会清除存储在数组中的相关数据。下面是二者代码的差异:

我们添加了一个新的布尔变量fvgExist,并将其初始化为false,以便它能够标记在扫描的柱形图中是否存在有效的设置。

      bool fvgExist = false;

我们从当前柱形图之前的一柱(即0 + 1 = 1)开始,再次通过预定义的柱形图进行循环,获取所选柱形图的最高价和最低价。如果其中任何价格与设定中第二点的坐标相匹配,那么它仍然在范围内,允许进行交易。因此,我们将变量fvgExist设置为true。

      for (int k=1; k<=FVG_Rec_Ext_Bars; k++){
         double barLow = iLow(_Symbol,_Period,k);
         double barHigh = iHigh(_Symbol,_Period,k);
         
         if (barHigh == fvgLow || barLow == fvgLow){
            //Print("Found: ",fvgNAME," @ bar ",k);
            fvgExist = true;
            break;
         }
      }

如果我们想要入场,就需要知道入场价格水平,因此我们提前定义了当前交易品类的报价。

      double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
      double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);

接下来,我们使用if语句来检查所选的FVG是否为看跌设定,当前买价是否高于设定的上限,并且这是否是该设定的第一个交易信号。当所有这些条件都满足时,我们以0.01的交易量开立一个卖单。卖单的开仓价格是当前的市场买价,盈利目标(takeprofit)设定在FVG设定的下限,止损(stoploss)则根据风险报酬比1:10设置在开仓价格之上。一旦仓位建立,我们将该特定索引处的信号数据设置为true,以便在下一个价格变动时,我们不会基于该设定开立任何其他仓位。

      if (fvgColor == CLR_DOWN && Bid > fvgHigh && !signalFVGs[j]){
         Print("SELL SIGNAL For (",fvgNAME,") Now @ ",Bid);
         double SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point,_Digits);
         double trade_lots = Check1_ValidateVolume_Lots(0.01);
         
         if (Check2_Margin(ORDER_TYPE_SELL,trade_lots) &&
             Check3_VolumeLimit(trade_lots) &&
             Check4_TradeLevels(POSITION_TYPE_SELL,SL_sell,fvgLow)){
            obj_Trade.Sell(trade_lots,_Symbol,Bid,SL_sell,fvgLow);
            signalFVGs[j] = true;
         }
         ArrayPrint(totalFVGs,_Digits," [< >] ");
         ArrayPrint(signalFVGs,_Digits," [< >] ");
      }


同样地,看涨确认也成立,但条件却是相反的。

      else if (fvgColor == CLR_UP && Ask < fvgLow && !signalFVGs[j]){
         Print("BUY SIGNAL For (",fvgNAME,") Now @ ",Ask);
         double SL_buy = Bid - NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point,_Digits);
         double trade_lots = Check1_ValidateVolume_Lots(0.01);

         if (Check2_Margin(ORDER_TYPE_BUY,trade_lots) &&
             Check3_VolumeLimit(trade_lots) &&
             Check4_TradeLevels(POSITION_TYPE_BUY,SL_buy,fvgHigh)){
            obj_Trade.Buy(trade_lots,_Symbol,Ask,SL_buy,fvgHigh);
            signalFVGs[j] = true;
         }
         ArrayPrint(totalFVGs,_Digits," [< >] ");
         ArrayPrint(signalFVGs,_Digits," [< >] ");
      }


最后,一旦FVG设置不再存在,就意味着我们无法再基于它进行交易,因此我们不再关心FVG设置。为了释放存储空间,我们需要删除相应的数据。这可以通过使用ArrayRemove函数来实现,该函数需要传入数组、起始位置和要删除的元素总数作为参数。在这个情况下,起始位置为0(表示第一个数据),因为我们想要删除的是当前选中的那个单一的FVG设置的数据,所以要删除的元素总数为1。

      if (fvgExist == false){
         bool removeName = ArrayRemove(totalFVGs,0,1);
         bool removeTime = ArrayRemove(barTIMEs,0,1);
         bool removeSignal = ArrayRemove(signalFVGs,0,1);
         if (removeName && removeTime && removeSignal){
            Print("Success removing the FVG DATA from the arrays. New Data as Below:");
            Print("FVGs: ",ArraySize(totalFVGs)," TIMEs: ",ArraySize(barTIMEs),
                     " SIGNALs: ",ArraySize(signalFVGs));
            ArrayPrint(totalFVGs);
            ArrayPrint(barTIMEs);
            ArrayPrint(signalFVGs);
         }
      }


以下是创建FVG设置、交易确认设置以及从存储数据组中删除这些超出范围的数据,其OnTick代码如下:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   
   for (int i=0; i<=FVG_Rec_Ext_Bars; i++){
      double low0 = iLow(_Symbol,_Period,i+1);
      double high2 = iHigh(_Symbol,_Period,i+2+1);
      double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);
      
      double high0 = iHigh(_Symbol,_Period,i+1);
      double low2 = iLow(_Symbol,_Period,i+2+1);
      double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);
      
      bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
      bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;
      
      if (FVG_UP || FVG_DOWN){
         datetime time1 = iTime(_Symbol,_Period,i+1+1);
         double price1 = FVG_UP ? high2 : high0;
         datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
         double price2 = FVG_UP ? low0 : low2;
         string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";
         color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN;
         
         if (ObjectFind(0,fvgNAME) < 0){
            CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr);
            Print("Old ArraySize = ",ArraySize(totalFVGs));
            ArrayResize(totalFVGs,ArraySize(totalFVGs)+1);
            ArrayResize(barTIMEs,ArraySize(barTIMEs)+1);
            ArrayResize(signalFVGs,ArraySize(signalFVGs)+1);
            Print("New ArraySize = ",ArraySize(totalFVGs));
            totalFVGs[ArraySize(totalFVGs)-1] = fvgNAME;
            barTIMEs[ArraySize(barTIMEs)-1] = time1;
            signalFVGs[ArraySize(signalFVGs)-1] = false;
            ArrayPrint(totalFVGs);
            ArrayPrint(barTIMEs);
            ArrayPrint(signalFVGs);
         }
      }
   }
   
   for (int j=ArraySize(totalFVGs)-1; j>=0; j--){
      bool fvgExist = false;
      string objName = totalFVGs[j];
      string fvgNAME = ObjectGetString(0,objName,OBJPROP_NAME);
      double fvgLow = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,0);
      double fvgHigh = ObjectGetDouble(0,fvgNAME,OBJPROP_PRICE,1);
      color fvgColor = (color)ObjectGetInteger(0,fvgNAME,OBJPROP_COLOR);
      
      for (int k=1; k<=FVG_Rec_Ext_Bars; k++){
         double barLow = iLow(_Symbol,_Period,k);
         double barHigh = iHigh(_Symbol,_Period,k);
         
         if (barHigh == fvgLow || barLow == fvgLow){
            //Print("Found: ",fvgNAME," @ bar ",k);
            fvgExist = true;
            break;
         }
      }
      
      //Print("Existence of ",fvgNAME," = ",fvgExist);
      
      double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
      double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
      
      if (fvgColor == CLR_DOWN && Bid > fvgHigh && !signalFVGs[j]){
         Print("SELL SIGNAL For (",fvgNAME,") Now @ ",Bid);
         double SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point,_Digits);
         double trade_lots = Check1_ValidateVolume_Lots(0.01);
         
         if (Check2_Margin(ORDER_TYPE_SELL,trade_lots) &&
             Check3_VolumeLimit(trade_lots) &&
             Check4_TradeLevels(POSITION_TYPE_SELL,SL_sell,fvgLow)){
            obj_Trade.Sell(trade_lots,_Symbol,Bid,SL_sell,fvgLow);
            signalFVGs[j] = true;
         }
         ArrayPrint(totalFVGs,_Digits," [< >] ");
         ArrayPrint(signalFVGs,_Digits," [< >] ");
      }
      else if (fvgColor == CLR_UP && Ask < fvgLow && !signalFVGs[j]){
         Print("BUY SIGNAL For (",fvgNAME,") Now @ ",Ask);
         double SL_buy = Bid - NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point,_Digits);
         double trade_lots = Check1_ValidateVolume_Lots(0.01);

         if (Check2_Margin(ORDER_TYPE_BUY,trade_lots) &&
             Check3_VolumeLimit(trade_lots) &&
             Check4_TradeLevels(POSITION_TYPE_BUY,SL_buy,fvgHigh)){
            obj_Trade.Buy(trade_lots,_Symbol,Ask,SL_buy,fvgHigh);
            signalFVGs[j] = true;
         }
         ArrayPrint(totalFVGs,_Digits," [< >] ");
         ArrayPrint(signalFVGs,_Digits," [< >] ");
      }
      
      if (fvgExist == false){
         bool removeName = ArrayRemove(totalFVGs,0,1);
         bool removeTime = ArrayRemove(barTIMEs,0,1);
         bool removeSignal = ArrayRemove(signalFVGs,0,1);
         if (removeName && removeTime && removeSignal){
            Print("Success removing the FVG DATA from the arrays. New Data as Below:");
            Print("FVGs: ",ArraySize(totalFVGs)," TIMEs: ",ArraySize(barTIMEs),
                     " SIGNALs: ",ArraySize(signalFVGs));
            ArrayPrint(totalFVGs);
            ArrayPrint(barTIMEs);
            ArrayPrint(signalFVGs);
         }
      }      
   }
   
}

恭喜!现在,我们确实已经基于FVG/不平衡策略创建了一个智能交易系统,用于生成交易信号。


FVG策略测试结果

在策略测试器上进行测试后,我们得到了以下结果:

  • 余额/资产净值图表:


  • 回测结果



结论

总而言之,通过对如前所述的各种主题进行公允价值缺口或不平衡策略的编码实践,为量化交易方法提供了宝贵的见解。通过本文,我们深入探讨了实施此类策略的具体细节,涉及到数据分析、统计建模和算法交易技术等关键组成部分。

首先,理解公允价值缺口或不平衡的概念至关重要,因为它是该策略的基础。这一概念涉及到识别资产的市场价格与其内在价值之间的差异,并利用统计方法来准确衡量这些差异。

此外,文章还强调了应用强大的数据分析技术,从金融数据中提取有意义的见解的重要性。时间序列分析、K线图分析和情绪分析等技术,在识别用于指导交易决策的模式和趋势方面发挥着至关重要的作用。

此外,该策略的编码方面强调了熟练掌握MQL5编程语言以及熟悉MQL5库和函数的重要性。高效的编码实践能够实现交易过程的自动化,从而加快执行速度并提升策略的可扩展性。

免责声明:本文中的信息仅用于教学目的。本文旨在展示如何基于“聪明资金概念”(Smart Money Concept)方法创建一个公允价值缺口EA。因此,这些信息应被视为创建更优化、更完善的EA的基础,其中需要更多考虑优化和数据提取等方面。所展示的信息并不能保证任何交易结果。 

综上所述,本文强调了量化交易、统计学以及K线分析之间的跨学科领域,这对于制定有效策略以应对动态金融市场至关重要。通过将理论概念与实际编码实践相结合,本文为读者提供了成功从事量化交易所需的关键工具,特别是“聪明资金概念”(Smart Money Concept, SMC)方法。


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

附加的文件 |
FVG_SMC_EA.mq5 (21.23 KB)
基于预测的统计套利 基于预测的统计套利
我们将探讨统计套利,使用Python搜索具有相关性和协整性的交易品种,为皮尔逊(Pearson)系数制作一个指标,并编制一个用于交易统计套利的EA,该系统将使用Python和ONNX模型进行预测。
一种采用纯MQL5语言实现的基于能量学习的特征选择算法 一种采用纯MQL5语言实现的基于能量学习的特征选择算法
本文介绍了一种在学术论文《FREL:一种稳定的特征选择算法》中描述的特征选择算法的实现,该算法被称为基于正则化能量的特征加权学习。
Python中的虚假回归(伪回归) Python中的虚假回归(伪回归)
虚假回归通常发生在两个时间序列之间仅因偶然因素而展现出高度相关性时,这会导致回归分析产生误导性的结果。在这种情况下,尽管变量之间可能看似存在关联,但这种关联仅仅是巧合,模型可能并不可靠。
MQL5 简介(第 6 部分):MQL5 中的数组函数新手指南 (二) MQL5 简介(第 6 部分):MQL5 中的数组函数新手指南 (二)
开始我们 MQL5 旅程的下一阶段。在这篇深入浅出、适合初学者的文章中,我们将探讨其余的数组函数,揭开复杂概念的神秘面纱,让您能够制定高效的交易策略。我们将讨论 ArrayPrint、ArrayInsert、ArraySize、ArrayRange、ArrarRemove、ArraySwap、ArrayReverse 和 ArraySort。利用这些基本的数组函数,提升您的算法交易专业知识。加入我们的精通 MQL5 之路吧!