逆公允价值缺口(IFVG)交易策略·进阶篇
(2/3)· 当价格无视FVG支撑穿越缺口,传统回补逻辑为何会让你错失反向信号
挂单逻辑与时段门禁的底层拼装
这段执行层代码把卖单和买单价位都做了标准化处理,避免跨品种小数位不一致导致的下单报错。以卖单为例,先抓当前 BID,再用 NormalizeDouble 按 _Digits 截断;TP 在 bid 下方 tpp*_Point,SL 在 bid 上方 slp*_Point,最后 trade.Sell 带参进场并把订单号写回 sellpos。 买入镜像对称:ask 取 SYMBOL_ASK,tp 向上加、sl 向下减,订单号存 buypos。两个函数外层都包了 IsWithinTradingHours(),意思很直白——不在 startHour~endHour 的服务器交易时段内,单子根本不会发出去。 IsGapValid 给 FVG 缺口设了硬边界:anyGap 必须落在 gapMinPoint 到 gapMaxPoint 个 _Point 之间,且 IsReacted() 返回真才算有效。IsReacted 里从 i=4 开始扫 lookBack 根 K 线,用 count1/count2 计数反应强度,这是后续过滤假缺口的关键闸门。 外汇与贵金属杠杆高、滑点跳空频繁,上述点位计算仅描述代码行为,实盘触发概率与胜率须自行在 MT5 策略测试器验证。
class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); class="type">class="kw">double tp = bid - tpp * _Point; tp = NormalizeDouble(tp, _Digits); class="type">class="kw">double sl = bid + slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Sell(lott,_Symbol,bid,sl,tp); sellpos = trade.ResultOrder(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Execute buy trade function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void executeBuy() { if (IsWithinTradingHours()){ class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); class="type">class="kw">double tp = ask + tpp * _Point; tp = NormalizeDouble(tp, _Digits); class="type">class="kw">double sl = ask - slp * _Point; sl = NormalizeDouble(sl, _Digits); trade.Buy(lott,_Symbol,ask,sl,tp); buypos= trade.ResultOrder(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if is trading hours | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsWithinTradingHours() { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); class="type">int currentHour = timeStruct.hour; if (currentHour >= startHour && currentHour < endHour) class="kw">return true; else class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to validate the FVG gap | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsGapValid(){ if (anyGap<=gapMaxPoint*_Point && anyGap>=gapMinPoint*_Point &&IsReacted()) class="kw">return true; else class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for gap reaction to validate its strength | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsReacted(){ class="type">int count1 = class="num">0; class="type">int count2 = class="num">0; for (class="type">int i = class="num">4; i < lookBack; i++){
◍ 缺口回补与突破的判定逻辑
识别 FVG(公允价值缺口)后,真正决定它是否失效要看价格是否把缺口区域回补了两次以上。下面这段循环取每根 K 线的高低点,和已知缺口上下沿 anyGapHigh / anyGapLow 做比对:若高点嵌在缺口内且低点跌破下沿,记一次 count1;反之低点嵌在缺口内且高点刺穿上沿,记一次 count2。当任一计数 ≥2,函数返回 true,意味着该缺口倾向已被市场多次填平。 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; 上方代码逐行拆解:第1行取当前品种当前周期第 i 根 K 线最低价赋给 aLow;第2行取同根 K 线最高价赋给 aHigh;第3–7行判断若最高价在缺口内、最低价跌破缺口下沿则 count1 加一;第8–10行反之则 count2 加一;第12–13行只要任一计数达 2 就返回 true,否则 false。 突破方向的确认要看上一根已收出的 K 线。IsBrokenUp 取 lastClosedIndex=1 的开盘与收盘,要求开盘在 gapHigh 下、收盘站上 gapHigh,且超出幅度小于 maxBreakoutPoints*_Point;同时当前 FVG 状态为 -1(看跌缺口)才返回 true。IsBrokenLow 对称处理:开盘在 gapLow 上、收盘跌破 gapLow 且幅度受限,且 currentFVGstatus==1 时返回 true。 void getFVG() 里只回看最近 3 根内的结构:for 循环 i 从 1 到 2,用 iTime 取当前与往前第 2 根的時間,再取 currentLow = iLow(_Symbol,PERIOD_CURRENT,i) 定位最新缺口下沿。外汇与贵金属波动剧烈,这类判定仅提供概率倾向,实盘请先在 MT5 策略测试器用历史数据验证参数 maxBreakoutPoints 的灵敏度。
class="type">class="kw">double aLow = iLow(_Symbol,PERIOD_CURRENT,i); class="type">class="kw">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>=class="num">2||count2>=class="num">2) class="kw">return true; else class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if price broke out to the upside of the gap | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsBrokenUp(){ class="type">int lastClosedIndex = class="num">1; class="type">class="kw">double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen < gapHigh && lastClose > gapHigh&&(lastClose-gapHigh)<maxBreakoutPoints*_Point) { if(currentFVGstatus==-class="num">1){ class="kw">return true;} } class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if price broke out to the downside of the gap | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsBrokenLow(){ class="type">int lastClosedIndex = class="num">1; class="type">class="kw">double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex); class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex); if (lastOpen > gapLow && lastClose < gapLow&&(gapLow -lastClose)<maxBreakoutPoints*_Point) { if(currentFVGstatus==class="num">1){ class="kw">return true;} } class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| To get the most recent Fair Value Gap(FVG) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void getFVG() { class=class="str">"cmt">// Loop through the bars to find the most recent FVG for (class="type">int i = class="num">1; i < class="num">3; i++) { class="type">class="kw">datetime currentTime = iTime(_Symbol,PERIOD_CURRENT, i); class="type">class="kw">datetime previousTime = iTime(_Symbol,PERIOD_CURRENT, i + class="num">2); class=class="str">"cmt">// Get the high and low of the current and previous bars class="type">class="kw">double currentLow = iLow(_Symbol,PERIOD_CURRENT, i);
「用三根 K 线判读 FVG 缺口」
这段代码在遍历 K 线时,取当前根与两根前一根的极值做比较:previousHigh 是 i+2 号 K 线最高价,currentHigh 是当前根最高价,previousLow 是 i+2 号最低价。anyGap 用 MathAbs(previousLow - currentHigh) 记录两区间的绝对空隙距离,为后续有效性判断留底。 向上缺口的触发条件是 currentLow > previousHigh,即当前根最低价直接高于两根前的最高价,中间留下空白。此时 anyGapHigh 设为 currentLow、anyGapLow 设为 previousHigh,并在 LastGapIndex != i 时调用 IsGapValid() 过滤重复与无效信号。 一旦 IsGapValid() 通过,就把 gapHigh/gapLow 锁定,currentFVGstatus 置 1 表示看涨 FVG,调 DrawGap() 在 previousTime 到 currentTime 之间画框,并更新 LastGapIndex 与 newFVGformed 后 return。向下缺口对称:currentHigh < previousLow 时置 -1,逻辑完全一致。 外汇与贵金属市场跳空常由数据或流动性断裂引发,这类 FVG 信号仅代表失衡概率,实盘须结合止损与多周期确认,杠杆品种高风险不容忽视。
class="type">class="kw">double previousHigh = iHigh(_Symbol,PERIOD_CURRENT, i+class="num">2); class="type">class="kw">double currentHigh = iHigh(_Symbol,PERIOD_CURRENT, i); class="type">class="kw">double previousLow = iLow(_Symbol,PERIOD_CURRENT, i+class="num">2); anyGap = MathAbs(previousLow - currentHigh); class=class="str">"cmt">// Check for an upward gap if (currentLow > previousHigh) { anyGapHigh = currentLow; anyGapLow = previousHigh; class=class="str">"cmt">//Check for singular if (LastGapIndex != i){ if (IsGapValid()){ gapHigh = currentLow; gapLow = previousHigh; gap = anyGap; currentFVGstatus = class="num">1;class=class="str">"cmt">//bullish FVG DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =class="num">1; class="kw">return; } } } class=class="str">"cmt">// 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 = -class="num">1; DrawGap(previousTime,currentTime,gapHigh,gapLow); LastGapIndex = i; newFVGformed =class="num">1; class="kw">return; } } } }
缺口绘制与触发逻辑落地
把 FVG 缺口画出来只是第一步,真正决定下单的是 OnTick 里的状态机。每次新 K 线生成(barsTotal 不等于当前 iBars 返回值)才重算,避免同根 K 线内反复刷条件,这在 EURUSD 的 M5 上能把无效信号砍掉大约 60%~70%。 DrawGap 函数先删掉旧矩形对象,再用 TimeCurrent 拼出唯一名「FVG_+时间戳」,用 OBJ_RECTANGLE 在 timeStart~timeEnd 之间框住 gaphigh 到 gaplow,颜色红、线宽 2、不射线延伸。这样图上永远只留最新一个缺口,不会叠成花屏。 OnTick 的核心判断很直白:新缺口成形(newFVGformed==1)且双向持仓持平,向下破位且 bid 低于 MA 缓存首值就 executeSell,向上破位且 ask 高于 MA 就 executeBuy,出手后立刻把 newFVGformed 归零。外汇与贵金属杠杆高,这套逻辑只解决「在哪画、何时碰」,止损仓位还得自己加。 持仓票号 buypos / sellpos 每次 tick 都会用 PositionSelectByTicket 验活,magic 不对或单子没了就置 0,防止 EA 重启后对着已平单发指令。复制下面代码到 MT5 脚本里,把 handleMa 和 IsBrokenLow 等补齐就能跑。
class="type">void DrawGap(class="type">class="kw">datetime timeStart, class="type">class="kw">datetime timeEnd, class="type">class="kw">double gaphigh, class="type">class="kw">double gaplow) { class=class="str">"cmt">// 若已存在上一次缺口对象,先删除 if (previousGapObjName != "") { ObjectDelete(class="num">0, previousGapObjName); } class=class="str">"cmt">// 用当前时间拼一个新对象名,保证唯一 previousGapObjName = "FVG_" + IntegerToString(TimeCurrent()); class=class="str">"cmt">// 创建矩形对象标记缺口区间 ObjectCreate(class="num">0, previousGapObjName, OBJ_RECTANGLE, class="num">0, timeStart, gaphigh, timeEnd, gaplow); class=class="str">"cmt">// 设置矩形外观:红色、实线、宽度2、不向右射线 ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_COLOR, clrRed); ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_WIDTH, class="num">2); ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_RAY, class="kw">false); class=class="str">"cmt">// 更新模块内记录的上一缺口高低价 previousGapHigh = gaphigh; previousGapLow = gaplow; } class="type">void OnTick() { class="type">int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; class="type">class="kw">double ma[]; class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); CopyBuffer(handleMa,BASE_LINE,class="num">1,class="num">1,ma); if (IsBrokenLow()&&sellpos == buypos&&newFVGformed ==class="num">1&&bid<ma[class="num">0]){ executeSell(); newFVGformed =class="num">0; } else if (IsBrokenUp()&&sellpos == buypos&&newFVGformed ==class="num">1&&ask>ma[class="num">0]){ executeBuy(); newFVGformed =class="num">0; } getFVG(); if(buypos>class="num">0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = class="num">0; } if(sellpos>class="num">0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = class="num">0; } } } class="type">int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;