逆公允价值缺口(IFVG)交易策略
概述
当价格回到先前确定的公允价值缺口位置,且未表现出预期的支撑或阻力反应,而是无视该缺口时,便出现了逆公允价值缺口(IFVG)。这种“无视”现象可能预示着市场方向的潜在转变,并为反向交易提供优势。在本文中,我将介绍自己开发的量化方法,以及如何将IFVG作为一种策略,应用于MetaTrader 5智能交易系统(EA)中。
策略契机
首先理解公允价值缺口(FVGs)
要充分理解“逆公允价值缺口”背后的逻辑,需先从标准公允价值缺口的定义入手。公允价值缺口通常通过三根K线的价格形态来界定。

当K线B的实体(通常也包括影线)推动市场价格急剧向上或向下,留下一个“缺口”时,即形成FVG。具体而言,在强劲的上涨行情中,如果K线C的最低价高于K线A的最高价,则这两个价格点之间的空间就被视为FVG。这一缺口反映了市场中的低效或失衡区域——由于价格在某一方向上移动过快,导致交易未能充分实现双向参与。交易者通常认为,这种价格偏离是由机构订单流造成的,留下了大资金活动的“足迹”。
常见的逻辑是,价格迟早会回到这些缺口处进行“填补”。填补缺口可被视为市场对先前单向订单流的平衡行为。遵循这一原则的交易者通常会等待价格再次回到该缺口,寻找确认原方向延续或反转的反应信号。
什么是IFVG?
“IFVG”的概念基于此,但采用了一种逆向或反向推导的视角。与利用FVG作为确认原方向延续的区域不同,IFVG策略可能利用该缺口来预测市场可能无法延续原有方向并发生反转的位置。
例如,要识别一个看跌IFVG,可遵循以下步骤:
- 确定一个看涨FVG。
- 价格回到FVG区域。
- 观察价格行为,而非将其视为支撑。如果价格未能向上启动,反而穿过缺口,仿佛缺口未提供有意义的支撑,这种失败可能预示着动量转变。
- 做空,预期市场无法利用FVG作为更高价格的跳板,可能转而下跌。
IFVG的背景
- 机构足迹与失效点:FVG背后的基本假设是,大型、成熟的交易者制造了初始的不平衡。当价格回到这些区域时,往往是一种测试:如果大型交易者仍认为这些价格具有价值,他们的未平仓订单可能会支撑或阻止价格,引发反应。如果价格反而直接穿过FVG而没有强劲的反弹或延续,则表明这些大额订单可能已被成交、取消或不再捍卫该区域。这可能暗示市场意图的转变。
- 提前检测弱势或强势:通过关注价格回到缺口时未 发生的情况,交易者可以捕捉到潜在力量或变弱的微妙线索。如果一个看涨市场无法从已知的低效区域(看涨FVG)获得上涨动力,可能预示着看涨走势正在衰竭。
- 补充传统FVG策略:传统FVG策略依赖于重新平衡后原方向延续的假设。然而,市场是动态的,并非每个缺口填补都会导致先前趋势的恢复。逆FVG方法通过识别正常策略失效的情况,为交易者提供了额外的“优势”,因此反向操作可能具有更高的概率和更好的风险回报比。
IFVG的概念基于对市场不断测试和重新测试先前失衡区域而形成的认知。虽然传统FVG交易侧重于成功再平衡和趋势延续,但逆向方法通过识别再平衡过程未能产生预期结果的情况,从而获得高概率的反向交易机会。这种视角的转变将可能错过的机会——甚至可能是亏损的交易——转化为具有潜在高优势的反向交易设置。在市场环境中,预期意外往往能获得回报,IFVG概念为交易者的技术分析工具箱增添了一项附加的工具。
策略开发
与主观交易者利用FVG类似,IFVG也因识别有效形态所需的复杂标准而被运用于实战交易。不加甄别地交易每一个IFVG,很可能导致如随机漫步般的交易表现,因为大多数缺口并不符合我此前讨论的战略逻辑。为了量化主观交易者所考虑的特征设置,我进行了广泛的特征测试,并制定了以下规则:
-
与宏观趋势保持一致:价格应顺应总体宏观趋势,这一趋势通过价格相对于400周期移动平均线的位置来确定。
-
选择合适的时间框架:应使用较低的时间框架,如1至5分钟,因为“订单填补”的概念在短时间内发生。就本文而言,采用3分钟时间框架。
-
聚焦于最近的FVG:仅考虑最近的FVG,因为它在反映当前市场状况方面具有最高重要性。
-
FVG大小验证:与周围K线相比,FVG既不能过大也不能过小。过小的缺口缺乏作为可靠支撑或阻力位的显著性,而过大的缺口则可能是由新闻事件引起的,这可能会延迟反转信号。为确保FVG有意义,设定了特定阈值来验证每个缺口。
-
受控的突破K线大小:同样,突破K线不应过大,因为入场是基于K线收盘价。大型突破K线可能导致信号延迟,而该策略旨在避免这一点。
-
及时的价格反转与突破:在FVG形成后的指定时间内,价格必须反转回缺口,并以收盘K线从相反边缘突破。这通过仅在短回顾期内检查最近的FVG来实现。
-
突破强度确认:FVG应与先前的拒绝水平保持一致,确保FVG的突破代表相应方向上强度的增加。
现在,让我们逐步浏览代码。
首先,我们声明必要的全局变量。这些全局变量存储跟踪FVG、当前持仓交易以及系统状态的关键数据。像previousGapHigh、previousGapLow和lastGapIndex这样的变量有助于跟踪最近识别的缺口。handleMa将存储移动平均线的句柄。buypos和sellpos跟踪持仓交易的订单号,而currentFVGstatus和newFVGformed则跟踪最后识别的FVG的状态。
string previousGapObjName = ""; double previousGapHigh = 0.0; double previousGapLow = 0.0; int LastGapIndex = 0; double gapHigh = 0.0; double gapLow = 0.0; double gap = 0.0; double lott= 0.1; ulong buypos = 0, sellpos = 0; double anyGap = 0.0; double anyGapHigh = 0.0; double anyGapLow = 0.0; int barsTotal = 0; int newFVGformed = 0; int currentFVGstatus = 0; int handleMa; #include <Trade/Trade.mqh> CTrade trade;
接下来,我们声明以下函数,用于执行带有止盈和止损的交易,并跟踪每笔交易的订单编号。
//+------------------------------------------------------------------+ //| Store order ticket number into buypos/sellpos variables | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { if (IsWithinTradingHours()){ double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double tp = bid - tpp * _Point; tp = NormalizeDouble(tp, _Digits); double sl = bid + slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Sell(lott,_Symbol,bid,sl,tp); sellpos = trade.ResultOrder(); } } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { if (IsWithinTradingHours()){ double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double tp = ask + tpp * _Point; tp = NormalizeDouble(tp, _Digits); double sl = ask - slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Buy(lott,_Symbol,ask,sl,tp); buypos= trade.ResultOrder(); } } //+------------------------------------------------------------------+ //| Check if is trading hours | //+------------------------------------------------------------------+ bool IsWithinTradingHours() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; if (currentHour >= startHour && currentHour < endHour) return true; else return false; }
接着,我们利用这两个函数来验证FVG。IsReacted()函数用于检查在回顾期内,当前FVG价格范围内是否至少存在两根K线的影线,我们将此解读为之前对FVG的拒绝信号。随后,IsGapValid()函数会验证缺口大小是否处于我们期望的范围内,并返回true或false的结果。
//+------------------------------------------------------------------+ //| Function to validate the FVG gap | //+------------------------------------------------------------------+ bool IsGapValid(){ if (anyGap<=gapMaxPoint*_Point && anyGap>=gapMinPoint*_Point&&IsReacted()) return true; else return false; } //+------------------------------------------------------------------+ //| Check for gap reaction to validate its strength | //+------------------------------------------------------------------+ bool IsReacted(){ int count1 = 0; int count2 = 0; for (int i = 4; i < lookBack; i++){ double aLow = iLow(_Symbol,PERIOD_CURRENT,i); double aHigh = iHigh(_Symbol,PERIOD_CURRENT,i); if (aHigh<anyGapHigh&&aHigh>anyGapLow&&aLow<anyGapLow){ count1++; } else if (aLow<anyGapHigh&&aLow>anyGapLow&&aHigh>anyGapHigh){ count2++; } } if (count1>=2||count2>=2) return true; else return false; }
之后,我们利用这些函数来检查当前最后一个FVG是否出现了突破行情。
//+------------------------------------------------------------------+ //| Check if price broke out to the upside of the gap | //+------------------------------------------------------------------+ bool IsBrokenUp(){ int lastClosedIndex = 1; double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen < gapHigh && lastClose > gapHigh&&(lastClose-gapHigh)<maxBreakoutPoints*_Point) { if(currentFVGstatus==-1){ return true;} } return false; } //+------------------------------------------------------------------+ //| Check if price broke out to the downside of the gap | //+------------------------------------------------------------------+ bool IsBrokenLow(){ int lastClosedIndex = 1; double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen > gapLow && lastClose < gapLow&&(gapLow -lastClose)<maxBreakoutPoints*_Point) { if(currentFVGstatus==1){ return true;} } return false; }
最后,我们利用这两个函数,通过IsGapValid()检查缺口是否有效;若有效,则更新全局变量,将该FVG标记为新缺口,并在图表上绘制出来。getFVG()函数是编码整个策略的关键所在。我们在每个新K线柱上调用该函数,以检查是否存在有效的FVG。若该FVG有效,我们进一步验证其是否与上次保存的FVG不同,若不同,则将其保存至全局变量以更新状态。
//+------------------------------------------------------------------+ //| To get the most recent Fair Value Gap (FVG) | //+------------------------------------------------------------------+ void getFVG() { // Loop through the bars to find the most recent FVG for (int i = 1; i < 3; i++) { datetime currentTime = iTime(_Symbol,PERIOD_CURRENT, i); datetime previousTime = iTime(_Symbol,PERIOD_CURRENT, i + 2); // Get the high and low of the current and previous bars double currentLow = iLow(_Symbol,PERIOD_CURRENT, i); double previousHigh = iHigh(_Symbol,PERIOD_CURRENT, i+2); double currentHigh = iHigh(_Symbol,PERIOD_CURRENT, i); double previousLow = iLow(_Symbol,PERIOD_CURRENT, i+2); anyGap = MathAbs(previousLow - currentHigh); // Check for an upward gap if (currentLow > previousHigh) { anyGapHigh = currentLow; anyGapLow = previousHigh; //Check for singular if (LastGapIndex != i){ if (IsGapValid()){ gapHigh = currentLow; gapLow = previousHigh; gap = anyGap; currentFVGstatus = 1;//bullish FVG DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =1; return; } } } // Check for a downward gap else if (currentHigh < previousLow) { anyGapHigh = previousLow; anyGapLow = currentHigh; if (LastGapIndex != i){ if(IsGapValid()){ gapHigh = previousLow; gapLow = currentHigh; gap = anyGap; currentFVGstatus = -1; DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =1; return; } } } } } //+------------------------------------------------------------------+ //| Function to draw the FVG gap on the chart | //+------------------------------------------------------------------+ void DrawGap(datetime timeStart, datetime timeEnd, double gaphigh, double gaplow) { // Delete the previous gap object if it exists if (previousGapObjName != "") { ObjectDelete(0, previousGapObjName); } // Generate a new name for the gap object previousGapObjName = "FVG_" + IntegerToString(TimeCurrent()); // Create a rectangle object to highlight the gap ObjectCreate(0, previousGapObjName, OBJ_RECTANGLE, 0, timeStart, gaphigh, timeEnd, gaplow); // Set the properties of the rectangle ObjectSetInteger(0, previousGapObjName, OBJPROP_COLOR, clrRed); ObjectSetInteger(0, previousGapObjName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, previousGapObjName, OBJPROP_WIDTH, 2); ObjectSetInteger(0, previousGapObjName, OBJPROP_RAY, false); // Update the previous gap information previousGapHigh = gaphigh; previousGapLow = gaplow; }
然后,我们将所有策略规则像这样整合至OnTick()函数中,整个策略就完成了。
//+------------------------------------------------------------------+ //| OnTick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double ma[]; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); CopyBuffer(handleMa,BASE_LINE,1,1,ma); if (IsBrokenLow()&&sellpos == buypos&&newFVGformed ==1&&bid<ma[0]){ executeSell(); newFVGformed =0; } else if (IsBrokenUp()&&sellpos == buypos&&newFVGformed ==1&&ask>ma[0]){ executeBuy(); newFVGformed =0; } getFVG(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
补充说明一下,我们在OnTick()函数开头调用这段逻辑,目的是确保仅在形成新的K线柱(即新Bar)之后,才继续处理后续代码行。这一做法可有效节省计算资源。
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
此外,由于我们同一时间只想进行一笔交易,因此可以利用以下逻辑,仅当两个订单编号均设置为0时才入场交易,以此确保该特定EA当前没有任何开仓。
if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
快速总结:
- 全局声明与输入参数:设置环境、变量及用户可配置参数。
- 初始化(OnInit):准备移动平均线过滤器并设置magic编号(用于标识订单的唯一编号)。
- OnTick逻辑:主要工作流程——检查新K线柱、识别FVG、验证突破情况,并在条件满足时执行交易。
- FVG检测(getFVG、IsGapValid、IsReacted):识别并验证FVG及其市场反应。
- 突破验证(IsBrokenUp、IsBrokenLow):确认突破方向,以决定交易入场。
- 交易管理(OnTradeTransaction、executeBuy、executeSell):处理订单编号,确保交易正确执行。
- 图表绘制(DrawGap):可视化已识别的FVG。
- 时间过滤(IsWithinTradingHours):限制交易在特定时间段内进行。
策略测试
该策略在股票指数上表现最优,因其相对较低的点差和高波动性有利于零售性质的日内交易。我们将通过交易纳斯达克100指数(Nasdaq 100)来测试该策略,测试期间为2020年1月1日至2024年12月1日,采用3分钟(M3)时间框架。以下是我为该策略选择的参数。

以下是为该策略选择参数值的几点建议:
- 应选择市场波动率较高的时段进行交易,通常为股票市场开盘期间。具体时段需根据您所用经纪商的服务器时间调整。例如,以我的服务器时间(GMT+0)为准,股票市场交易时段约为14:00至19:00。
- 建议盈亏比大于1,因我们是在高波动市场中顺应宏观趋势进行交易。此外,需避免将止盈和止损(TPSL)水平设置得过高或过低。若TPSL幅度过大,则无法有效捕捉短期形态信号;若幅度过小,点差则可能对交易产生负面影响。
- 切勿过度调整缺口阈值、突破K线阈值及回顾期等参数值。应保持这些参数在交易品种价格范围的合理区间内,以避免过度拟合。
以下为回测结果:



我们可以看到,该策略在过去五年中表现非常稳定,显示出其潜在的盈利能力。
在策略测试器的可视化部分,一个典型的交易场景如下:

我鼓励读者基于此策略框架进行拓展,发挥自身创意来优化该策略。以下是我的一些建议:
- IFVG的强度可通过缺口区域周围被拒绝的K线数量来确定。您可将此类K线数量的差异作为评估规则。
- 本文仅关注最大突破点。然而,有时突破K线可能过小,表明突破力度较弱,可能对趋势延续产生负面影响。您可考虑同时设置最小突破点阈值。
- 当前退出规则基于止盈和止损设定。您也可根据特定回顾期内双向的关键价位来设定退出水平,或设定固定退出时间。
结论
在本文中,我介绍了自研发的一种量化并利用逆向公允价值缺口(IFVG)作为MetaTrader 5智能交易系统(EA)交易策略的方法,涵盖了策略的动机、开发过程及测试结果。该策略展现出极高的盈利能力潜力,在过去五年中表现稳定,累计交易超过400笔。可进一步修改该策略,使其适配不同的交易品种和时间框架。完整代码附于下方,欢迎您将其整合到自己的交易开发项目中。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16659
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:定义(二)
精通日志记录(第四部分):将日志保存到文件