一步步学习如何利用公允价值缺口(FVG)或市场不平衡性来交易的策略:一种聪明资金的交易方法·进阶篇
FVG 缺口判定与矩形标记的实现
在 MT5 的 K 线序列里,FVG(公允价格缺口)靠三根连续棒线识别:取第 i 根低点 low0、第 i+2 根高点 high2,若 low0 大于 high2 且两者价差换算成点数后超过 minPts,就倾向构成向上缺口;反之 low2 大于 high0 则可能是向下缺口。 代码里用 iLow / iHigh 取对应价位,再用 NormalizeDouble 除以 _Point 得到以最小报价单位计的差值 gap_L0_H2 与 gap_H0_L2,这一步直接决定你画面上的缺口阈值灵敏度,外汇与贵金属波动属性不同,minPts 不能照抄别人参数。 判定成立后,以中间棒 i+1 的时间 time1 为起点,按 PeriodSeconds(_Period)*FVG_Rec_Ext_Bars 向右延展出 time2,调用 CreateRec 画 OBJ_RECTANGLE。该函数先 ObjectFind 防重名,再写时间/价格锚点与填充色,最后 ChartRedraw 刷新。 每次新建矩形都会把名字塞进 totalFVGs 数组、把中间棒索引存进 barINDICES,两个数组同步 ArrayResize +1。实操时开 MT5 把这段拼进 EA,调 minPts 从 20 点到 50 点观察 XAUUSD 的 15 分钟图,缺口标记密度会明显变化,高风险品种请先开模拟盘验证。
ArrayResize(signalFVGs,class="num">0); } ObjectsDeleteAll(class="num">0,FVG_Prefix); class="type">class="kw">double low0 = iLow(_Symbol,_Period,i); class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2); class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits); class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i); class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2); class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits); class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts; class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts; class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1); class="type">class="kw">double price1 = FVG_UP ? high2 : high0; class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars; class="type">class="kw">double price2 = FVG_UP ? low0 : low2; class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")"; class="type">color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN; class="type">void CreateRec(class="type">class="kw">string objName,class="type">class="kw">datetime time1,class="type">class="kw">double price1, class="type">class="kw">datetime time2, class="type">class="kw">double price2,class="type">color clr){ if (ObjectFind(class="num">0,objName) < class="num">0){ ObjectCreate(class="num">0,objName,OBJ_RECTANGLE,class="num">0,time1,price1,time2,price2); ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">0,time1); ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">0,price1); ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">1,time2); ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">1,price2); ObjectSetInteger(class="num">0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(class="num">0,objName,OBJPROP_FILL,true); ObjectSetInteger(class="num">0,objName,OBJPROP_BACK,false); ChartRedraw(class="num">0); } } CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr); ArrayResize(totalFVGs,ArraySize(totalFVGs)+class="num">1); ArrayResize(barINDICES,ArraySize(barINDICES)+class="num">1); totalFVGs[ArraySize(totalFVGs)-class="num">1] = fvgNAME; barINDICES[ArraySize(barINDICES)-class="num">1] = i+class="num">1;
◍ FVG 区域被反向刺穿时的回收逻辑
在 MT5 里跑 FVG(公平价值缺口)标记脚本时,最容易被忽略的是缺口被后续 K 线反向刺穿后的边界重算。上面这段代码从已记录的缺口数组尾部倒序遍历,把每个图形对象的名称、起止时间、高低价和颜色全部读出来,再拿去和近期 K 线比。 内层循环从缺口所在 bar 的前一根开始,往回数 FVG_Rec_Ext_Bars 根。只要向下缺口(CLR_DOWN)被某根 bar 的高点越过、或向上缺口(CLR_UP)被某根 bar 的低点跌破,就判定该缺口在此处被「Cut Off」,调用 UpdateRec 把终止时间和价格改写掉并跳出。 若回数到 k==0 还没触发刺穿条件,脚本会打印 OverFlow Detected 并以当前 bar 时间强制收口,避免数组越界死循环。外汇与贵金属市场跳空频繁,这类边界回收直接决定你看到的 FVG 是不是「还活着」,实盘前建议把 FVG_Rec_Ext_Bars 从默认值调到 50~100 之间观察重绘频率。 UpdateRec 本身只做四件小事:用 ObjectSetInteger / ObjectSetDouble 重写对象的起止时间与双边价格,最后 ChartRedraw(0) 强制重绘。循环结束处 ArrayResize 清零两个全局数组,保证下一轮扫描不残留旧引用。
for (class="type">int i=ArraySize(totalFVGs)-class="num">1; i>=class="num">0; i--){ class="type">class="kw">string objName = totalFVGs[i]; class="type">class="kw">string fvgNAME = ObjectGetString(class="num">0,objName,OBJPROP_NAME); class="type">int barIndex = barINDICES[i]; class="type">class="kw">datetime timeSTART = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">0); class="type">class="kw">datetime timeEND = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">1); class="type">class="kw">double fvgLOW = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">0); class="type">class="kw">double fvgHIGH = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">1); class="type">color fvgColor = (class="type">color)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_COLOR); for (class="type">int k=barIndex-class="num">1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){ class="type">class="kw">datetime barTime = iTime(_Symbol,_Period,k); class="type">class="kw">double barLow = iLow(_Symbol,_Period,k); class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k); if (k==class="num">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; } } } class="type">void UpdateRec(class="type">class="kw">string objName,class="type">class="kw">datetime time1,class="type">class="kw">double price1, class="type">class="kw">datetime time2, class="type">class="kw">double price2){ if (ObjectFind(class="num">0,objName) >= class="num">0){ ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">0,time1); ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">0,price1); ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">1,time2); ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">1,price2); ChartRedraw(class="num">0); } } ArrayResize(totalFVGs,class="num">0); ArrayResize(barINDICES,class="num">0);
「初始化时扫描可见 K 线的 FVG 缺口」
EA 启动时先抓当前图表可见 K 线数:用 ChartGetInteger(0,CHART_VISIBLE_BARS) 拿到数值并打印,这一步决定后面循环扫几根。若图表上没有任何 OBJ_RECTANGLE 类型的 FVG 对象,直接把 totalFVGs、barINDICES、signalFVGs 三个数组 Resize 到 0,避免旧数据干扰;随后 ObjectsDeleteAll 按前缀清掉上次画的矩形。 核心扫描在 for(i=0;i<=visibleBars;i++) 里:取第 i 根低点 low0 与第 i+2 根高点 high2,算出 gap_L0_H2=(low0-high2)/_Point 并 NormalizeDouble 到当前品种精度;反向再算 gap_H0_L2。当 low0>high2 且 gap 超过 minPts 判为上方 FVG,low2>high0 且超阈值为下方 FVG——这两个布尔量就是缺口识别的硬条件。 命中后按 i+1 根时间画矩形:time1 取 i+1 的 K 线时间,price1 上方缺口用 high2、下方用 high0;time2 向后推 PeriodSeconds()*FVG_Rec_Ext_Bars 根周期,price2 对应 low0 或 low2。名字拼成 前缀+(时间),颜色按方向取 CLR_UP/CLR_DOWN,调 CreateRec 落框,并把名字塞进 totalFVGs 数组且每次 ArrayResize +1,日志会打出 Old/New ArraySize 方便你核对内存增长。 把下面代码直接贴进 MT5 的 OnInit,把 minPts、FVG_Rec_Ext_Bars、FVG_Prefix、CLR_UP、CLR_DOWN 在外部定义好,挂 EURUSD 15 分钟这类高波动品种,可能扫出 5~20 个缺口矩形;外汇和贵金属杠杆高,实盘前务必先在策略测试器跑历史验证。
class="type">int OnInit(){ class="type">int visibleBars = (class="type">int)ChartGetInteger(class="num">0,CHART_VISIBLE_BARS); Print("Total visible bars on chart = ",visibleBars); if (ObjectsTotal(class="num">0,class="num">0,OBJ_RECTANGLE)==class="num">0){ Print("No FVGs Found, Resizing storage arrays to class="num">0 now!!!"); ArrayResize(totalFVGs,class="num">0); ArrayResize(barINDICES,class="num">0); ArrayResize(signalFVGs,class="num">0); } ObjectsDeleteAll(class="num">0,FVG_Prefix); for (class="type">int i=class="num">0; i<=visibleBars; i++){ class=class="str">"cmt">//Print("Bar Index = ",i); class="type">class="kw">double low0 = iLow(_Symbol,_Period,i); class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2); class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits); class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i); class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2); class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits); class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts; class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts; if (FVG_UP || FVG_DOWN){ Print("Bar Index with FVG = ",i+class="num">1); class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1); class="type">class="kw">double price1 = FVG_UP ? high2 : high0; class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars; class="type">class="kw">double price2 = FVG_UP ? low0 : low2; class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")"; class="type">color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN; CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr); Print("Old ArraySize = ",ArraySize(totalFVGs)); ArrayResize(totalFVGs,ArraySize(totalFVGs)+class="num">1); ArrayResize(barINDICES,ArraySize(barINDICES)+class="num">1); Print("New ArraySize = ",ArraySize(totalFVGs)); totalFVGs[ArraySize(totalFVGs)-class="num">1] = fvgNAME;
FVG 缺口被回填的判定与截断逻辑
这段循环干的事很直接:倒序遍历已识别的 FVG 对象,把每个缺口的名称、起止时间、高低价和颜色从图形对象里读出来,再去回看它左侧若干根 K 线,判断缺口是否已被价格回填。 核心判定写在内部 k 循环里:若是下跌缺口(fvgColor == CLR_DOWN),只要某根左侧 K 线的最高价 barHigh 超过了 fvgHIGH,就视为被截断;上涨缺口则看最低价 barLow 是否跌破 fvgLOW。命中即调用 UpdateRec 重写记录并 break,不再往前找。 还有一个边界情况——当 k 递减到 0 仍未触发截断,说明可回看范围溢出,代码会打印 OverFlow Detected 并以当前 bar 时间补全记录。外汇与贵金属市场跳空频繁,这类溢出在极端行情中可能出现概率偏高,实盘前应在 MT5 策略测试器里用真实品种跑一遍看触发频率。 循环结束会把 totalFVGs 和 barINDICES 两个数组 ArrayResize 清 0,函数最终 return INIT_SUCCEEDED。你可以直接把这段贴进 EA 的初始化尾部,把 FVG_Rec_Ext_Bars 调成 20~50 之间,观察日志里 Cut Off 与 OverFlow 的输出分布。
barINDICES[ArraySize(barINDICES)-class="num">1] = i+class="num">1; ArrayPrint(totalFVGs); ArrayPrint(barINDICES); } } for (class="type">int i=ArraySize(totalFVGs)-class="num">1; i>=class="num">0; i--){ class="type">class="kw">string objName = totalFVGs[i]; class="type">class="kw">string fvgNAME = ObjectGetString(class="num">0,objName,OBJPROP_NAME); class="type">int barIndex = barINDICES[i]; class="type">class="kw">datetime timeSTART = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">0); class="type">class="kw">datetime timeEND = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">1); class="type">class="kw">double fvgLOW = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">0); class="type">class="kw">double fvgHIGH = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">1); class="type">color fvgColor = (class="type">color)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_COLOR); Print("FVG NAME = ",fvgNAME," >No: ",barIndex," TS: ",timeSTART," TE: ", timeEND," LOW: ",fvgLOW," HIGH: ",fvgHIGH," CLR = ",fvgColor); for (class="type">int k=barIndex-class="num">1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){ class="type">class="kw">datetime barTime = iTime(_Symbol,_Period,k); class="type">class="kw">double barLow = iLow(_Symbol,_Period,k); class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k); class=class="str">"cmt">//Print("Bar No: ",k," >Time: ",barTime," >H: ",barHigh," >L: ",barLow); if (k==class="num">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,class="num">0); ArrayResize(barINDICES,class="num">0); class="kw">return(INIT_SUCCEEDED); } for (class="type">int i=class="num">0; i<=FVG_Rec_Ext_Bars; i++){class=class="str">"cmt">//... }
◍ FVG 成交缺口的回测与下单触发
这段逻辑先抓前一根低点与前第三根高点,用来界定一个已识别的 FVG(公允价值缺口)边界。low0 取 i+1 根低点,high2 取 i+3 根高点,两者构成缺口的上下沿参照。 随后用 for 循环在 FVG_Rec_Ext_Bars 根内扫描,只要某根 K 线的高或低触碰 fvgLow,就把 fvgExist 置真并跳出。这一步是在确认缺口是否已被后续价格部分回填,属于实盘前必跑的过滤。 真正下单发生在 Bid 上穿 fvgHigh(下行缺口)或 Ask 下破 fvgLow(上行缺口)时。以卖单为例,止损被设为 Ask 加上「缺口高度折成点数再乘 10 个 point」的缓冲,即 SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point, _Digits),这种算法让止损距离随缺口大小浮动,而非固定点数。 下单前连过四道闸:Check1 校验手数、Check2 查保证金、Check3 限交易量、Check4 核交易层级,全过才由 obj_Trade.Sell 或 Buy 发单,并把 signalFVGs[j] 锁真防止重复触发。外汇与贵金属杠杆高,这类自动缺口策略回测胜率可能随品种波动,实盘前务必在 MT5 策略测试器用 0.01 手跑一遍 EURUSD 的 H1 数据。
class="type">class="kw">double low0 = iLow(_Symbol,_Period,i+class="num">1); class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2+class="num">1); class="type">bool fvgExist = false; for (class="type">int k=class="num">1; k<=FVG_Rec_Ext_Bars; k++){ class="type">class="kw">double barLow = iLow(_Symbol,_Period,k); class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k); if (barHigh == fvgLow || barLow == fvgLow){ class=class="str">"cmt">//Print("Found: ",fvgNAME," @ bar ",k); fvgExist = true; break; } } class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); if (fvgColor == CLR_DOWN && Bid > fvgHigh && !signalFVGs[j]){ Print("SELL SIGNAL For(",fvgNAME,") Now @ ",Bid); class="type">class="kw">double SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*class="num">10)*_Point,_Digits); class="type">class="kw">double trade_lots = Check1_ValidateVolume_Lots(class="num">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); class="type">class="kw">double SL_buy = Bid - NormalizeDouble((((fvgHigh-fvgLow)/_Point)*class="num">10)*_Point,_Digits); class="type">class="kw">double trade_lots = Check1_ValidateVolume_Lots(class="num">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;
「失效 FVG 的数组清理与 OnTick 实时扫描」
当某根 K 线不再满足 FVG 条件(fvgExist == false),脚本会从 totalFVGs、barTIMEs、signalFVGs 三个同步数组里各移除索引 0 的一条记录。ArrayRemove 返回 bool,只有三个删除都成功才打印剩余数组大小,方便你直接在 MT5 Experts 日志里核对数据有没有错位。 删除成功后用 ArraySize 打印的新长度,应当等于删除前长度减 1;若日志里 FVGs、TIMEs、SIGNALs 三者不一致,说明前面写入逻辑有偏移,这是调试 FVG 指标最常见的坑。 OnTick 里用 for 循环从 i=0 扫到 FVG_Rec_Ext_Bars,拿 i+1 和 i+3 号 K 线算缺口。gap_L0_H2 是 (low0-high2)/_Point 并按 _Digits 规范化,向上 FVG 要求 low0>high2 且 gap 大于 minPts;向下反之。 命中后取 time1 为 i+2 根时间,price1 向上取 high2、向下取 high0,time2 再往后推 FVG_Rec_Ext_Bars 根周期秒数,price2 取对应另一侧价格,最后拼出带前缀和时间的 fvgNAME。外汇与贵金属波动大,FVG 失效概率不低,实盘前务必用策略测试器跑一遍多周期验证。
} ArrayPrint(totalFVGs,_Digits," [< >] "); ArrayPrint(signalFVGs,_Digits," [< >] "); } if (fvgExist == false){ class="type">bool removeName = ArrayRemove(totalFVGs,class="num">0,class="num">1); class="type">bool removeTime = ArrayRemove(barTIMEs,class="num">0,class="num">1); class="type">bool removeSignal = ArrayRemove(signalFVGs,class="num">0,class="num">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); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(){ for (class="type">int i=class="num">0; i<=FVG_Rec_Ext_Bars; i++){ class="type">class="kw">double low0 = iLow(_Symbol,_Period,i+class="num">1); class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2+class="num">1); class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits); class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i+class="num">1); class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2+class="num">1); class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits); class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts; class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts; if (FVG_UP || FVG_DOWN){ class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1+class="num">1); class="type">class="kw">double price1 = FVG_UP ? high2 : high0; class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars; class="type">class="kw">double price2 = FVG_UP ? low0 : low2; class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";