//+------------------------------------------------------------------+ //| MssSetup.mqh — MSS pipeline (setup phase, không đặt order) | //+------------------------------------------------------------------+ //| Flow: | //| IDLE | //| │ IctMss_SelectNearestH1Poi() ⇒ chọn H1 FVG gần giá nhất | //| │ IctMss_HasFreshFvgTouch() ⇒ touch hợp lệ | //| ↓ | //| H1_TOUCH | //| │ IctMss_UpdateLiveM5Swings() ⇒ liveL0/H0/L1/H1 structural | //| │ (v1.171: keylv lock) | //| │ IctMss_TryLockMss() ⇒ break body M5 qua L0/H0 | //| │ veto MSS nếu InpMssRequireIntradayAligned && ngược bias | //| ↓ | //| CHOCH (locked) | //| │ IctConfirmFvg_ScanNew() ⇒ tìm M5 FVG sau chochTime | //| │ IctMss_ZonesOverlapH1() ⇒ M5 FVG gần H1 FVG | //| ↓ | //| M5_FVG → ENTRY_FILL → READY (chờ giá hồi vào M5 FVG) | //| | //| FVG fresh touch (v1.182) — cutoff bias-aware: | //| cutoff = max(g_ictMssAfterCloseGuard, | //| g_ictBiasInto{Bull,Bear}Time) | //| Touch hợp lệ phải xảy ra SAU cutoff. | //| | //| Structural keylv (v1.171) — không phải "pivot mới nhất": | //| BEAR (pullback UP): | //| liveH0 = đỉnh CAO NHẤT sau touch (extreme) | //| liveL0 = đáy gần nhất trước liveH0 (keylv MSS↓) | //| liveH1 = đỉnh trước liveL0 | //| BULL (pullback DOWN): | //| liveL0 = đáy THẤP NHẤT sau touch (extreme) | //| liveH0 = đỉnh gần nhất trước liveL0 (keylv MSS↑) | //| liveL1 = đáy trước liveH0 | //| ⇒ "lock" tự nhiên: pivot SAU extreme không đổi keylv | //| | //| MSS lock yêu cầu (v1.165): | //| - cấu trúc đủ 2 đỉnh/đáy SAU touch | //| - body M5 phá liveL0 (BEAR) / liveH0 (BULL) | //| | //| Globals owned: | //| g_ictMssAfterCloseGuard — TS sau khi 1 lệnh đóng (TP/SL/timeout/| //| stale limit): pipeline phải đợi touch fresh sau TS này | //| | //| Public API (chính): | //| void IctMss_Update(sym) | //| bool IctMss_HasFreshFvgTouch(sym, &h1Zone) | //| datetime IctMss_GetFvgTouchTime(sym, &h1Zone) | //| void IctMss_ResetState() | //| bool IctMss_TryLockMss(sym, tf, h1, bias) | //| int IctMss_CountH1PoiEligible() | //+------------------------------------------------------------------+ #ifndef ICT2026_MSSSETUP_MQH #define ICT2026_MSSSETUP_MQH #include #include #include #include #include #include // Guard: sau khi 1 lệnh đóng (TP/SL/manual), pipeline KHÔNG được pick H1 FVG // nào có firstTouch trước thời điểm này. Phải đợi 1 touch mới (FVG mới hoặc // FVG cũ được re-touched) thì mới tiếp tục setup MSS + entry. datetime g_ictMssAfterCloseGuard = 0; string IctMssPhaseText(const ENUM_ICT_MSS_PHASE ph) { switch(ph) { case ICT_MSS_H1_TOUCH: return "Retest FVG H1 OK"; case ICT_MSS_CHOCH: return "MSS OK"; case ICT_MSS_M5_FVG: return "M5 FVG"; case ICT_MSS_ENTRY_FILL: return "M5 fill entry"; case ICT_MSS_READY: return "READY entry"; default: return "Idle"; } } int IctMss_FindH1FvgById(const ulong id) { for(int i = 0; i < g_ictFvgCount; i++) if(g_ictFvgZones[i].id == id) return i; return -1; } // POI H1: FVG chưa Used, gần giá nhất (không phải Premium cao nhất / Discount thấp nhất) double IctMss_DistancePriceToFvg(const double price, const IctFvgZone &z) { if(price >= z.lower - _Point && price <= z.upper + _Point) return 0.0; if(price > z.upper) return price - z.upper; return z.lower - price; } bool IctMss_IsH1PoiEligible(const IctFvgZone &z) { if(!IctFvg_MatchesBias(z)) return false; if(!IctFvg_IsEntryRepPd(z)) return false; if(z.state == ICT_FVG_USED) return false; return true; } int IctMss_CountH1PoiEligible() { int n = 0; for(int i = 0; i < g_ictFvgCount; i++) if(IctMss_IsH1PoiEligible(g_ictFvgZones[i])) n++; return n; } int IctMss_SelectNearestH1Poi(const string sym) { const double px = SymbolInfoDouble(sym, SYMBOL_BID); int bestIdx = -1; double bestDist = DBL_MAX; for(int i = 0; i < g_ictFvgCount; i++) { if(!IctMss_IsH1PoiEligible(g_ictFvgZones[i])) continue; const double d = IctMss_DistancePriceToFvg(px, g_ictFvgZones[i]); if(d < bestDist - _Point) { bestDist = d; bestIdx = i; } else if(MathAbs(d - bestDist) <= _Point && bestIdx >= 0) { if(g_ictFvgZones[i].createdTime > g_ictFvgZones[bestIdx].createdTime) bestIdx = i; } } return bestIdx; } bool IctMss_PriceInsideFvg(const IctFvgZone &h1, const double price) { return (price >= h1.lower - _Point && price <= h1.upper + _Point); } bool IctMss_BarOverlapsFvg(const IctFvgZone &h1, const double barHi, const double barLo) { return (barHi >= h1.lower - _Point && barLo <= h1.upper + _Point); } bool IctMss_LivePriceTouchesFvg(const string sym, const IctFvgZone &h1) { const double bid = SymbolInfoDouble(sym, SYMBOL_BID); const double ask = SymbolInfoDouble(sym, SYMBOL_ASK); return IctMss_PriceInsideFvg(h1, bid) || IctMss_PriceInsideFvg(h1, ask); } // Touch M5 đầu tiên sau `since` (0 = bất kỳ) datetime IctMss_FirstM5TouchInFvg(const string sym, const IctFvgZone &h1, const datetime since = 0) { const ENUM_TIMEFRAMES tf = InpConfirmTf; const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2); const datetime minT = (since > h1.createdTime) ? since : h1.createdTime; datetime best = 0; for(int sh = lim; sh >= 1; sh--) { const datetime t = iTime(sym, tf, sh); if(t <= minT) continue; if(!IctMss_BarOverlapsFvg(h1, iHigh(sym, tf, sh), iLow(sym, tf, sh))) continue; if(best == 0 || t < best) best = t; } return best; } // Cutoff bias-aware: touch chỉ hợp lệ khi xảy ra SAU thời điểm Bias chuyển vào // side hiện tại VÀ sau thời điểm lệnh trước đóng. datetime IctMss_GetFvgTouchCutoff() { datetime cutoff = 0; if(g_ictDailyBias.bias == ICT_BIAS_BULL && g_ictBiasIntoBullTime > 0) cutoff = g_ictBiasIntoBullTime; else if(g_ictDailyBias.bias == ICT_BIAS_BEAR && g_ictBiasIntoBearTime > 0) cutoff = g_ictBiasIntoBearTime; if(g_ictMssAfterCloseGuard > cutoff) cutoff = g_ictMssAfterCloseGuard; return cutoff; } datetime IctMss_GetFvgTouchTime(const string sym, IctFvgZone &h1) { IctFvg_UpdateZoneState(sym, InpFvgTf, h1); const datetime cutoff = IctMss_GetFvgTouchCutoff(); // firstTouchTime chỉ hợp lệ nếu xảy ra sau cutoff datetime t = 0; if(h1.firstTouchTime > 0 && h1.firstTouchTime >= cutoff) t = h1.firstTouchTime; // Quét M5 bars để tìm touch fresh sau cutoff (bao gồm cả re-touch sau bias flip) const datetime tM5 = IctMss_FirstM5TouchInFvg(sym, h1, cutoff); if(tM5 > 0 && (t <= 0 || tM5 < t)) t = tM5; if(t <= 0 && IctMss_LivePriceTouchesFvg(sym, h1)) { const datetime curBar = iTime(sym, InpConfirmTf, 0); if(curBar >= cutoff) t = curBar; } return t; } // Retest = giá chạm FVG (H1/M5/bid-ask). Râu H1 xuyên FVG vẫn OK; chỉ hủy khi H1 body đóng xuyên. bool IctMss_HasFvgPriceTouch(const string sym, IctFvgZone &h1) { return (IctMss_GetFvgTouchTime(sym, h1) > 0); } // Touch hợp lệ cho POI mới — cutoff đã được apply trong `IctMss_GetFvgTouchTime`: // - touch xảy ra SAU lệnh trước đóng (g_ictMssAfterCloseGuard) // - touch xảy ra SAU Bias chuyển vào side hiện tại (g_ictBiasInto*Time) // - đạt FILL threshold theo PD position (Pure 25% / Mixed 50%) bool IctMss_HasFreshFvgTouch(const string sym, IctFvgZone &h1) { const datetime tTouch = IctMss_GetFvgTouchTime(sym, h1); if(tTouch <= 0) return false; const double requiredPct = IctFvg_RequiredTouchFillPct(h1); if(requiredPct >= 100.0 - 1e-6) return false; if(h1.maxFillRatio * 100.0 < requiredPct - 1e-9) return false; return true; } bool IctMss_HasH1FvgRetest(const string sym, IctFvgZone &h1) { return IctMss_HasFvgPriceTouch(sym, h1); } double IctMss_H1Atr(const string sym) { const int h = iATR(sym, InpFvgTf, InpFvgAtrPeriod); if(h == INVALID_HANDLE) return 0.0; double buf[]; ArraySetAsSeries(buf, true); if(CopyBuffer(h, 0, 1, 1, buf) != 1) { IndicatorRelease(h); return 0.0; } IndicatorRelease(h); return buf[0]; } double IctMss_H1ZoneBuffer(const IctFvgZone &h1, const string sym) { const double gapH = h1.upper - h1.lower; double buf = (gapH > _Point && InpMssMaxDistGapPct > 0.0) ? gapH * InpMssMaxDistGapPct / 100.0 : 0.0; const double atr = IctMss_H1Atr(sym); if(atr > 0.0 && InpMssMaxDistAtrMult > 0.0) buf = MathMax(buf, atr * InpMssMaxDistAtrMult); if(buf <= 0.0) buf = MathMax(gapH * 0.5, 20.0 * _Point); return buf; } void IctMss_GetH1ProximityBounds(const IctFvgZone &h1, const string sym, const ENUM_ICT_BIAS bias, double &outLo, double &outHi) { const double gapH = h1.upper - h1.lower; const double buf = IctMss_H1ZoneBuffer(h1, sym); outLo = h1.lower - buf; outHi = h1.upper + buf; if(gapH <= _Point) return; if(bias == ICT_BIAS_BEAR && InpMssExtraBelowGapPct > 0.0) outLo -= gapH * InpMssExtraBelowGapPct / 100.0; if(bias == ICT_BIAS_BULL && InpMssExtraAboveGapPct > 0.0) outHi += gapH * InpMssExtraAboveGapPct / 100.0; } bool IctMss_PriceNearH1Fvg(const IctFvgZone &h1, const string sym, const double price, const ENUM_ICT_BIAS bias) { if(price <= 0.0 || h1.upper <= h1.lower) return false; double lo = 0.0, hi = 0.0; IctMss_GetH1ProximityBounds(h1, sym, bias, lo, hi); return (price >= lo - _Point && price <= hi + _Point); } bool IctMss_ChochWithinTouchWindow(const IctFvgZone &h1, const datetime chochTime) { const datetime tTouch = (h1.firstTouchTime > 0) ? h1.firstTouchTime : h1.createdTime; if(chochTime > 0 && chochTime < tTouch) return false; if(InpMssMaxM5BarsAfterTouch > 0 && chochTime > 0) { const int sec = (int)PeriodSeconds(InpConfirmTf); const datetime tMax = tTouch + (datetime)(InpMssMaxM5BarsAfterTouch * sec); if(chochTime > tMax) return false; } return true; } int IctMss_MaxSwingShift(const IctSwingSet &sw) { int m = 1; if(sw.hasH0) m = MathMax(m, sw.h0.shift); if(sw.hasL0) m = MathMax(m, sw.l0.shift); if(sw.hasH1) m = MathMax(m, sw.h1.shift); if(sw.hasL1) m = MathMax(m, sw.l1.shift); return m + 2; } bool IctMss_BodyBrokeLevelSince(const string sym, const ENUM_TIMEFRAMES tf, const int maxShift, const double level, const bool wantBelow) { const int lim = MathMax(1, MathMin(maxShift, InpMssConfirmLookback)); for(int sh = 1; sh <= lim; sh++) { if(wantBelow && IctBodyBreakBelow(sym, tf, sh, level)) return true; if(!wantBelow && IctBodyBreakAbove(sym, tf, sh, level)) return true; } return false; } bool IctMss_HasM5BarInH1Fvg(const string sym, const IctFvgZone &h1, const datetime since) { const ENUM_TIMEFRAMES tf = InpConfirmTf; const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2); for(int sh = 1; sh <= lim; sh++) { if(since > 0 && iTime(sym, tf, sh) < since) break; if(IctMss_BarOverlapsFvg(h1, iHigh(sym, tf, sh), iLow(sym, tf, sh))) return true; } return false; } // Structural keylv — KHÔNG dùng "pivot mới nhất" (nhảy mỗi nến). // Định nghĩa: keylv = pivot GẦN NHẤT TRƯỚC extreme của pullback ⇒ // tự nhiên "lock" tới khi pullback mở rộng (BEAR: tạo high mới cao hơn; // BULL: tạo low mới thấp hơn). // // BEAR bias (pullback UP, mục tiêu MSS↓ phá L0): // liveH0 = ĐỈNH CAO NHẤT sau tTouch (extreme đỉnh pullback) // liveL0 = đáy gần nhất theo time TRƯỚC liveH0 (keylv — "đáy tạo ra đỉnh cao nhất") // liveH1 = đỉnh gần nhất TRƯỚC liveL0 (đỉnh cũ — đủ cấu trúc 2 đỉnh) // liveL1 = đáy gần nhất TRƯỚC liveH1 (đáy cũ — đủ cấu trúc 2 đáy) // SL = max(H0, H1) + buffer // // BULL bias (pullback DOWN, mục tiêu MSS↑ phá H0): // liveL0 = ĐÁY THẤP NHẤT sau tTouch (extreme đáy pullback) // liveH0 = đỉnh gần nhất theo time TRƯỚC liveL0 (keylv — "đỉnh tạo ra đáy thấp nhất") // liveL1 = đáy gần nhất TRƯỚC liveH0 (đáy cũ — đủ cấu trúc 2 đáy) // liveH1 = đỉnh gần nhất TRƯỚC liveL1 (đỉnh cũ — đủ cấu trúc 2 đỉnh) // SL = min(L0, L1) − buffer // // Nhờ tham chiếu "TRƯỚC extreme": // - Đỉnh mới tạo SAU extreme low (bullish bias) ⇒ KHÔNG ảnh hưởng keylv // - Đáy mới tạo SAU extreme high (bearish bias) ⇒ KHÔNG ảnh hưởng keylv // ⇒ keylv chỉ "nhảy" khi pullback thực sự mở rộng tạo extreme mới bool IctMss_UpdateLiveM5Swings(const string sym, const ENUM_ICT_BIAS bias, const datetime tTouch) { g_ictLowTf.mss.liveL0Price = 0.0; g_ictLowTf.mss.liveH0Price = 0.0; g_ictLowTf.mss.liveL0Time = 0; g_ictLowTf.mss.liveH0Time = 0; g_ictLowTf.mss.liveL1Price = 0.0; g_ictLowTf.mss.liveH1Price = 0.0; g_ictLowTf.mss.liveL1Time = 0; g_ictLowTf.mss.liveH1Time = 0; if(bias != ICT_BIAS_BEAR && bias != ICT_BIAS_BULL) return false; // BẮT BUỘC có tTouch hợp lệ — nếu không pipeline sẽ lấy cả pivot trước touch // (filter `time < tTouch` không có tác dụng khi tTouch = 0). if(tTouch <= 0) return false; IctSwingPoint highs[], lows[]; IctCollectSwings(sym, InpConfirmTf, InpConfirmSwingRange, InpConfirmSwingLookback, highs, lows); const int nH = ArraySize(highs); const int nL = ArraySize(lows); if(nH == 0 || nL == 0) return false; if(bias == ICT_BIAS_BEAR) { // 1) liveH0 = đỉnh cao nhất sau tTouch (extreme của pullback up) int idxH0 = -1; double maxHigh = -DBL_MAX; for(int i = 0; i < nH; i++) { if(highs[i].time < tTouch) continue; if(highs[i].price > maxHigh) { maxHigh = highs[i].price; idxH0 = i; } } if(idxH0 < 0) return false; g_ictLowTf.mss.liveH0Price = highs[idxH0].price; g_ictLowTf.mss.liveH0Time = highs[idxH0].time; const datetime tH0 = highs[idxH0].time; // 2) liveL0 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH0 (keylv MSS↓) int idxL0 = -1; datetime tL0Best = 0; for(int i = 0; i < nL; i++) { if(lows[i].time < tTouch) continue; if(lows[i].time >= tH0) continue; if(lows[i].time > tL0Best) { tL0Best = lows[i].time; idxL0 = i; } } if(idxL0 < 0) return false; g_ictLowTf.mss.liveL0Price = lows[idxL0].price; g_ictLowTf.mss.liveL0Time = lows[idxL0].time; const datetime tL0 = lows[idxL0].time; // 3) liveH1 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL0 int idxH1 = -1; datetime tH1Best = 0; for(int i = 0; i < nH; i++) { if(highs[i].time < tTouch) continue; if(highs[i].time >= tL0) continue; if(highs[i].time > tH1Best) { tH1Best = highs[i].time; idxH1 = i; } } if(idxH1 >= 0) { g_ictLowTf.mss.liveH1Price = highs[idxH1].price; g_ictLowTf.mss.liveH1Time = highs[idxH1].time; } // 4) liveL1 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH1 (nếu có) if(idxH1 >= 0) { int idxL1 = -1; datetime tL1Best = 0; for(int i = 0; i < nL; i++) { if(lows[i].time < tTouch) continue; if(lows[i].time >= tH1Best) continue; if(lows[i].time > tL1Best) { tL1Best = lows[i].time; idxL1 = i; } } if(idxL1 >= 0) { g_ictLowTf.mss.liveL1Price = lows[idxL1].price; g_ictLowTf.mss.liveL1Time = lows[idxL1].time; } } return true; } // BULL bias (pullback DOWN) // 1) liveL0 = đáy thấp nhất sau tTouch (extreme của pullback down) int idxL0 = -1; double minLow = DBL_MAX; for(int i = 0; i < nL; i++) { if(lows[i].time < tTouch) continue; if(lows[i].price < minLow) { minLow = lows[i].price; idxL0 = i; } } if(idxL0 < 0) return false; g_ictLowTf.mss.liveL0Price = lows[idxL0].price; g_ictLowTf.mss.liveL0Time = lows[idxL0].time; const datetime tL0 = lows[idxL0].time; // 2) liveH0 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL0 (keylv MSS↑) int idxH0 = -1; datetime tH0Best = 0; for(int i = 0; i < nH; i++) { if(highs[i].time < tTouch) continue; if(highs[i].time >= tL0) continue; if(highs[i].time > tH0Best) { tH0Best = highs[i].time; idxH0 = i; } } if(idxH0 < 0) return false; g_ictLowTf.mss.liveH0Price = highs[idxH0].price; g_ictLowTf.mss.liveH0Time = highs[idxH0].time; const datetime tH0 = highs[idxH0].time; // 3) liveL1 = đáy GẦN NHẤT THEO TIME TRƯỚC liveH0 int idxL1 = -1; datetime tL1Best = 0; for(int i = 0; i < nL; i++) { if(lows[i].time < tTouch) continue; if(lows[i].time >= tH0) continue; if(lows[i].time > tL1Best) { tL1Best = lows[i].time; idxL1 = i; } } if(idxL1 >= 0) { g_ictLowTf.mss.liveL1Price = lows[idxL1].price; g_ictLowTf.mss.liveL1Time = lows[idxL1].time; } // 4) liveH1 = đỉnh GẦN NHẤT THEO TIME TRƯỚC liveL1 (nếu có) if(idxL1 >= 0) { int idxH1 = -1; datetime tH1Best = 0; for(int i = 0; i < nH; i++) { if(highs[i].time < tTouch) continue; if(highs[i].time >= tL1Best) continue; if(highs[i].time > tH1Best) { tH1Best = highs[i].time; idxH1 = i; } } if(idxH1 >= 0) { g_ictLowTf.mss.liveH1Price = highs[idxH1].price; g_ictLowTf.mss.liveH1Time = highs[idxH1].time; } } return true; } // Hủy setup: H1 đóng (shift=1) — thân xuyên FVG (không giữ giá). Râu xuyên vẫn tiếp tục MSS. // v1.186: dùng IctBodyBreakBuffer (N × spread) thay vì _Point — siết để // một close-through "sát mép" do nhiễu không làm invalidate setup. bool IctMss_H1BodyInvalidatedSetup(const string sym, const IctFvgZone &h1, const ENUM_ICT_BIAS bias) { if(h1.upper <= h1.lower) return false; const double cls = iClose(sym, InpFvgTf, 1); const double buf = IctBodyBreakBuffer(sym); if(bias == ICT_BIAS_BEAR) return (cls > h1.upper + buf); if(bias == ICT_BIAS_BULL) return (cls < h1.lower - buf); return false; } bool IctMss_CheckH1InvalidationOnNewBar(const string sym, const IctFvgZone &h1, const ENUM_ICT_BIAS bias) { const datetime h1Bar1 = iTime(sym, InpFvgTf, 1); if(h1Bar1 <= 0) return false; if(h1Bar1 == g_ictLowTf.mss.h1LastInvalidBarTime) return false; g_ictLowTf.mss.h1LastInvalidBarTime = h1Bar1; return IctMss_H1BodyInvalidatedSetup(sym, h1, bias); } bool IctMss_FindMssBreakBar(const string sym, const ENUM_TIMEFRAMES tf, const double keyLevel, const datetime keyTime, const bool wantBreakBelow, datetime &breakTimeOut) { breakTimeOut = 0; if(keyLevel <= 0.0 || keyTime <= 0) return false; const int lim = MathMin(InpMssConfirmLookback, iBars(sym, tf) - 2); for(int sh = 1; sh <= lim; sh++) { if(iTime(sym, tf, sh) < keyTime) break; const bool broke = wantBreakBelow ? IctBodyBreakBelow(sym, tf, sh, keyLevel) : IctBodyBreakAbove(sym, tf, sh, keyLevel); if(!broke) continue; breakTimeOut = iTime(sym, tf, sh); return true; } return false; } bool IctMss_TryLockMss(const string sym, const ENUM_TIMEFRAMES tf, const IctFvgZone &h1, const ENUM_ICT_BIAS bias) { if(g_ictLowTf.mss.chochLocked && g_ictLowTf.mss.chochKeyLevel > 0.0) return true; const datetime tTouch = (g_ictLowTf.mss.h1TouchTime > 0) ? g_ictLowTf.mss.h1TouchTime : h1.firstTouchTime; if(tTouch <= 0) return false; if(!IctMss_UpdateLiveM5Swings(sym, bias, tTouch)) return false; datetime breakT = 0; if(bias == ICT_BIAS_BEAR) { // Pullback bullish phải có CẤU TRÚC ĐẦY ĐỦ: 2 đỉnh (H0 & H1) đều SAU touch // → tránh case mới có 1 đỉnh từ pullback mới, H1 còn dính sóng cũ if(g_ictLowTf.mss.liveH1Price <= 0.0 || g_ictLowTf.mss.liveH0Time < tTouch || g_ictLowTf.mss.liveH1Time < tTouch) return false; if(!IctMss_FindMssBreakBar(sym, tf, g_ictLowTf.mss.liveL0Price, g_ictLowTf.mss.liveL0Time, true, breakT)) return false; if(breakT < tTouch) return false; // SL trên cao nhất giữa H0 và H1 (cap toàn pullback bull) + buffer (cộng ở MssEntry) double slSwingHi = g_ictLowTf.mss.liveH0Price; if(g_ictLowTf.mss.liveH1Price > slSwingHi) slSwingHi = g_ictLowTf.mss.liveH1Price; g_ictLowTf.mss.chochKeyLevel = g_ictLowTf.mss.liveL0Price; g_ictLowTf.mss.chochKeyTime = g_ictLowTf.mss.liveL0Time; g_ictLowTf.mss.slSwingPrice = slSwingHi; } else if(bias == ICT_BIAS_BULL) { // Pullback bearish phải có CẤU TRÚC ĐẦY ĐỦ: 2 đáy (L0 & L1) đều SAU touch // → tránh case mới có 1 đáy từ pullback mới, L1 còn dính sóng cũ if(g_ictLowTf.mss.liveL1Price <= 0.0 || g_ictLowTf.mss.liveL0Time < tTouch || g_ictLowTf.mss.liveL1Time < tTouch) return false; if(!IctMss_FindMssBreakBar(sym, tf, g_ictLowTf.mss.liveH0Price, g_ictLowTf.mss.liveH0Time, false, breakT)) return false; if(breakT < tTouch) return false; // SL dưới thấp nhất giữa L0 và L1 (đáy toàn pullback bear) + buffer (trừ ở MssEntry) double slSwingLo = g_ictLowTf.mss.liveL0Price; if(g_ictLowTf.mss.liveL1Price > 0.0 && g_ictLowTf.mss.liveL1Price < slSwingLo) slSwingLo = g_ictLowTf.mss.liveL1Price; g_ictLowTf.mss.chochKeyLevel = g_ictLowTf.mss.liveH0Price; g_ictLowTf.mss.chochKeyTime = g_ictLowTf.mss.liveH0Time; g_ictLowTf.mss.slSwingPrice = slSwingLo; } else return false; g_ictLowTf.mss.chochTime = breakT; g_ictLowTf.mss.chochLocked = true; g_ictLowTf.mss.chochBias = bias; return true; } bool IctMss_ZonesOverlapH1(const IctFvgZone &h1, const string sym, const ENUM_ICT_BIAS bias, const double zoneLo, const double zoneHi) { if(zoneHi <= zoneLo) return false; double hLo = 0.0, hHi = 0.0; IctMss_GetH1ProximityBounds(h1, sym, bias, hLo, hHi); return !(zoneHi < hLo - _Point || zoneLo > hHi + _Point); } bool IctMss_IsChochNearH1Fvg(const string sym, const IctFvgZone &h1, const ENUM_ICT_BIAS bias, const double keyLevel, const double anchorPrice, const datetime chochTime) { const bool nearAnchor = (anchorPrice > 0.0 && IctMss_PriceNearH1Fvg(h1, sym, anchorPrice, bias)); const bool nearKey = (keyLevel > 0.0 && IctMss_PriceNearH1Fvg(h1, sym, keyLevel, bias)); if(!nearAnchor && !nearKey) return false; return IctMss_ChochWithinTouchWindow(h1, chochTime); } bool IctMss_GetConfirmMssSwing(const string sym, const ENUM_ICT_BIAS bias, double &swingOut) { swingOut = 0.0; IctSwingSet sw; ENUM_ICT_STRUCT structural = ICT_STRUCT_NONE; if(IctBuildConfirmSwingSet(sym, sw, structural)) { if(bias == ICT_BIAS_BEAR && sw.hasH0) { // BEAR SL: max(H0, H1) — cap toàn pullback bull double hi = sw.h0.price; if(sw.hasH1 && sw.h1.price > hi) hi = sw.h1.price; swingOut = hi; return true; } if(bias == ICT_BIAS_BULL && sw.hasL0) { // BULL SL: min(L0, L1) — đáy toàn pullback bear double lo = sw.l0.price; if(sw.hasL1 && sw.l1.price < lo) lo = sw.l1.price; swingOut = lo; return true; } } IctSwingPoint highs[], lows[]; IctCollectSwings(sym, InpConfirmTf, InpConfirmSwingRange, InpConfirmSwingLookback, highs, lows); const int nH = ArraySize(highs); const int nL = ArraySize(lows); if(bias == ICT_BIAS_BEAR && nH > 0) { // max của 2 swing high gần nhất double hi = highs[nH - 1].price; if(nH >= 2 && highs[nH - 2].price > hi) hi = highs[nH - 2].price; swingOut = hi; return true; } if(bias == ICT_BIAS_BULL && nL > 0) { // min của 2 swing low gần nhất double lo = lows[nL - 1].price; if(nL >= 2 && lows[nL - 2].price < lo) lo = lows[nL - 2].price; swingOut = lo; return true; } return false; } bool IctMss_FindExtremePivot(const IctSwingPoint &pts[], const int n, const int maxShift, const bool wantHighest, IctSwingPoint &out) { out.Clear(); int best = -1; for(int i = 0; i < n; i++) { if(pts[i].shift > maxShift || pts[i].shift < 3) continue; if(best < 0) best = i; else if(wantHighest) { if(pts[i].price > pts[best].price) best = i; } else { if(pts[i].price < pts[best].price) best = i; } } if(best < 0) return false; out = pts[best]; return true; } bool IctMss_DetectChoch(const string sym, const ENUM_TIMEFRAMES tf, const ENUM_ICT_BIAS bias, double &keyLevelOut, datetime &keyTimeOut, double &slSwingOut) { keyLevelOut = 0.0; keyTimeOut = 0; slSwingOut = 0.0; IctSwingPoint highs[], lows[]; IctCollectSwings(sym, tf, InpConfirmSwingRange, InpConfirmSwingLookback, highs, lows); const int nH = ArraySize(highs); const int nL = ArraySize(lows); const int maxSh = InpMssConfirmLookback; if(bias == ICT_BIAS_BULL) { IctSwingPoint extremeLo; if(!IctMss_FindExtremePivot(lows, nL, maxSh, false, extremeLo)) return false; IctSwingPoint keyHi = IctFindMostRecentOlder(highs, nH, extremeLo.shift); if(!keyHi.Valid()) return false; keyLevelOut = keyHi.price; keyTimeOut = keyHi.time; slSwingOut = extremeLo.price; return IctBodyBreakAbove(sym, tf, 1, keyLevelOut); } if(bias == ICT_BIAS_BEAR) { IctSwingPoint extremeHi; if(!IctMss_FindExtremePivot(highs, nH, maxSh, true, extremeHi)) return false; IctSwingPoint keyLo = IctFindMostRecentOlder(lows, nL, extremeHi.shift); if(!keyLo.Valid()) return false; keyLevelOut = keyLo.price; keyTimeOut = keyLo.time; slSwingOut = extremeHi.price; return IctBodyBreakBelow(sym, tf, 1, keyLevelOut); } return false; } bool IctMss_PriceInEntryZone(const string sym, const ENUM_TIMEFRAMES tf, const IctFvgZone &zone) { if(!IctFvg_HasFillAtLeast(zone, InpMssEntryFillPct)) return false; const double fillPx = IctFvg_PriceAtFillPct(zone, InpMssEntryFillPct); const double bid = SymbolInfoDouble(sym, SYMBOL_BID); const double ask = SymbolInfoDouble(sym, SYMBOL_ASK); const double mid = (bid + ask) * 0.5; if(zone.side == ICT_FVG_BULL) return (mid <= fillPx + _Point && mid >= zone.lower - _Point); if(zone.side == ICT_FVG_BEAR) return (mid >= fillPx - _Point && mid <= zone.upper + _Point); return false; } void IctMss_MarkFvgUsed(IctFvgZone &zone, const string sym) { if(zone.id == 0) return; zone.state = ICT_FVG_USED; if(zone.fvgUsedTime == 0) zone.fvgUsedTime = iTime(sym, InpFvgTf, 0); zone.timeEnd = IctFvg_GetFvgDrawTimeEnd(sym, InpFvgTf, zone); } void IctMss_MarkFvgUsedOnMssSuccess(const string sym, const ulong fvgId) { if(fvgId == 0) return; const int idx = IctMss_FindH1FvgById(fvgId); if(idx < 0) return; const bool fresh = (g_ictFvgZones[idx].state != ICT_FVG_USED); if(fresh) IctMss_MarkFvgUsed(g_ictFvgZones[idx], sym); if(g_ictLowTf.mss.h1WatchFvgId == fvgId) g_ictLowTf.mss.h1WatchFvgId = 0; if(fresh && InpMssLogJournal) PrintFormat("[ICT2026/MSS] FVG #%I64u → Used (giữ giá + MSS M5 OK)", fvgId); } void IctMss_FailFvgH1Body(const string sym, const ulong fvgId, const string reason) { if(fvgId > 0) { const int idx = IctMss_FindH1FvgById(fvgId); if(idx >= 0) IctMss_MarkFvgUsed(g_ictFvgZones[idx], sym); } IctMss_ResetState(); g_ictLowTf.mss.displayReason = reason; if(InpMssLogJournal) PrintFormat("[ICT2026/MSS] FVG #%I64u → Used (H1 body xuyên) | %s", fvgId, reason); g_ictMssJournalLast = ""; } void IctMss_ResetPipelineKeepWatch() { const ulong watch = g_ictLowTf.mss.h1WatchFvgId; const datetime inv = g_ictLowTf.mss.h1LastInvalidBarTime; g_ictLowTf.mss.Clear(); g_ictLowTf.mss.h1WatchFvgId = watch; g_ictLowTf.mss.h1LastInvalidBarTime = inv; IctMss_JournalReset(); } void IctMss_CheckH1WatchInvalidation(const string sym) { const ulong watchId = g_ictLowTf.mss.h1WatchFvgId; if(watchId == 0) return; const int idx = IctMss_FindH1FvgById(watchId); if(idx < 0) { g_ictLowTf.mss.h1WatchFvgId = 0; return; } if(g_ictFvgZones[idx].state == ICT_FVG_USED) { g_ictLowTf.mss.h1WatchFvgId = 0; return; } const ENUM_ICT_BIAS bias = g_ictDailyBias.bias; if(bias != ICT_BIAS_BEAR && bias != ICT_BIAS_BULL) return; if(!IctMss_CheckH1InvalidationOnNewBar(sym, g_ictFvgZones[idx], bias)) return; IctMss_FailFvgH1Body(sym, watchId, (bias == ICT_BIAS_BEAR) ? "FVG Used: H1 đóng thân trên Premium — không giữ giá" : "FVG Used: H1 đóng thân dưới Discount — không giữ giá"); } void IctMss_ResetState() { g_ictLowTf.mss.Clear(); IctMss_JournalReset(); } void IctMss_Update(const string sym) { g_ictLowTf.mss.displayReason = IctMssPhaseText(g_ictLowTf.mss.phase); IctMss_CheckH1WatchInvalidation(sym); // Pipeline gate = Bias rõ ràng (KHÔNG gate theo Intraday trend). // → POI scan + MSS detect chạy liên tục trong giai đoạn intraday transition // → Entry vẫn bị chặn bởi isAllowTrade trong IctMssEntry_Update const ENUM_TIMEFRAMES cTf = InpConfirmTf; const ENUM_ICT_FVG_SIDE wantSide = IctFvgSideFromBias(g_ictDailyBias.bias); if(wantSide == ICT_FVG_NONE) { if(g_ictLowTf.mss.phase != ICT_MSS_IDLE) { if(g_ictLowTf.mss.h1WatchFvgId == 0 && g_ictLowTf.mss.h1FvgId > 0) g_ictLowTf.mss.h1WatchFvgId = g_ictLowTf.mss.h1FvgId; IctMss_ResetPipelineKeepWatch(); } g_ictLowTf.mss.displayReason = "Bias none — MSS pipeline tạm dừng"; return; } if(g_ictLowTf.mss.phase == ICT_MSS_IDLE) { const int hIdx = IctMss_SelectNearestH1Poi(sym); if(hIdx < 0) { g_ictLowTf.mss.h1FvgId = 0; g_ictLowTf.mss.displayReason = StringFormat( "Chờ H1 %s FVG gần giá (%d POI / %d FVG)", (wantSide == ICT_FVG_BEAR) ? "Premium" : "Discount", IctMss_CountH1PoiEligible(), g_ictFvgCount); return; } g_ictLowTf.mss.h1FvgId = g_ictFvgZones[hIdx].id; if(!IctMss_HasFreshFvgTouch(sym, g_ictFvgZones[hIdx])) { const double px = SymbolInfoDouble(sym, SYMBOL_BID); const double dist = IctMss_DistancePriceToFvg(px, g_ictFvgZones[hIdx]); const datetime tTouch = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[hIdx]); const bool staleTouch = (tTouch > 0 && g_ictMssAfterCloseGuard > 0 && tTouch < g_ictMssAfterCloseGuard); const double reqPct = IctFvg_RequiredTouchFillPct(g_ictFvgZones[hIdx]); const double curPct = g_ictFvgZones[hIdx].maxFillRatio * 100.0; if(staleTouch) g_ictLowTf.mss.displayReason = StringFormat( "POI #%I64u [%.0f–%.0f] — chờ touch mới (sau lệnh trước)", g_ictFvgZones[hIdx].id, g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper); else if(tTouch > 0 && curPct < reqPct) g_ictLowTf.mss.displayReason = StringFormat( "POI #%I64u [%.0f–%.0f] fill %.0f%% / cần %.0f%% — chờ vào sâu hơn", g_ictFvgZones[hIdx].id, g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper, curPct, reqPct); else g_ictLowTf.mss.displayReason = StringFormat( "POI #%I64u [%.0f–%.0f] dist %.0f pts — chờ chạm", g_ictFvgZones[hIdx].id, g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper, dist / _Point); return; } const datetime tTouch = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[hIdx]); g_ictLowTf.mss.phase = ICT_MSS_H1_TOUCH; g_ictLowTf.mss.h1FvgId = g_ictFvgZones[hIdx].id; g_ictLowTf.mss.h1WatchFvgId = g_ictFvgZones[hIdx].id; g_ictLowTf.mss.h1TouchTime = tTouch; g_ictLowTf.mss.h1LastInvalidBarTime = 0; g_ictLowTf.mss.displayReason = StringFormat( "Giá đã chạm FVG — theo dõi MSS M5 [%.0f–%.0f]", g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper); } const int h1Idx = IctMss_FindH1FvgById(g_ictLowTf.mss.h1FvgId); if(h1Idx < 0) { IctMss_ResetState(); g_ictLowTf.mss.displayReason = "H1 FVG mất"; return; } if(g_ictFvgZones[h1Idx].state == ICT_FVG_USED && g_ictLowTf.mss.phase <= ICT_MSS_H1_TOUCH && !g_ictLowTf.mss.chochLocked) { IctMss_ResetState(); g_ictLowTf.mss.displayReason = "FVG POI Used — chọn FVG gần giá tiếp theo"; return; } if(g_ictLowTf.mss.phase >= ICT_MSS_H1_TOUCH && g_ictLowTf.mss.h1TouchTime <= 0) { const datetime t = IctMss_GetFvgTouchTime(sym, g_ictFvgZones[h1Idx]); if(t > 0) g_ictLowTf.mss.h1TouchTime = t; } if(g_ictLowTf.mss.phase == ICT_MSS_H1_TOUCH && !g_ictLowTf.mss.chochLocked) { const datetime tTouch = g_ictLowTf.mss.h1TouchTime; if(IctMss_UpdateLiveM5Swings(sym, g_ictDailyBias.bias, tTouch)) { // Check cấu trúc pullback đầy đủ (2 đỉnh/đáy đều sau touch) const bool fullStruct = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? (g_ictLowTf.mss.liveH1Price > 0.0 && g_ictLowTf.mss.liveH0Time >= tTouch && g_ictLowTf.mss.liveH1Time >= tTouch) : (g_ictLowTf.mss.liveL1Price > 0.0 && g_ictLowTf.mss.liveL0Time >= tTouch && g_ictLowTf.mss.liveL1Time >= tTouch); if(!fullStruct) { g_ictLowTf.mss.displayReason = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? "Chờ pullback đủ cấu trúc (2 đỉnh H0&H1 sau touch)" : "Chờ pullback đủ cấu trúc (2 đáy L0&L1 sau touch)"; } else if(g_ictDailyBias.bias == ICT_BIAS_BEAR) g_ictLowTf.mss.displayReason = StringFormat( "Chờ MSS↓ phá L0=%.2f | H0=%.2f H1=%.2f (M5 bull)", g_ictLowTf.mss.liveL0Price, g_ictLowTf.mss.liveH0Price, g_ictLowTf.mss.liveH1Price); else g_ictLowTf.mss.displayReason = StringFormat( "Chờ MSS↑ phá H0=%.2f | L0=%.2f L1=%.2f (M5 bear)", g_ictLowTf.mss.liveH0Price, g_ictLowTf.mss.liveL0Price, g_ictLowTf.mss.liveL1Price); } else g_ictLowTf.mss.displayReason = "M5 trong FVG — chờ pivot L0/H0"; if(!IctMss_TryLockMss(sym, cTf, g_ictFvgZones[h1Idx], g_ictDailyBias.bias)) return; // Veto MSS nếu Intraday NGƯỢC Bias — CHỈ áp dụng khi yêu cầu align (mặc định OFF). // Mặc định: cho phép MSS dù Intraday ngược ⇒ entry sớm hơn khi trend đổi muộn, // giảm trường hợp "giá chạy tới TP rồi mới khớp limit → quay lại SL". if(InpMssRequireIntradayAligned && !IctIntraday_TrendAlignsWithBias(g_ictDailyBias.bias, g_ictIntraday.trend)) { IctMss_MarkFvgUsedOnMssSuccess(sym, g_ictLowTf.mss.h1FvgId); const string sideLbl = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? "MSS↓" : "MSS↑"; const string trendLbl = IctTrendDisplayShort(g_ictIntraday.trend); IctMss_ResetState(); g_ictLowTf.mss.displayReason = StringFormat( "%s khớp khi Intraday=%s ngược Bias — bỏ POI, chờ POI mới", sideLbl, trendLbl); return; } IctMss_MarkFvgUsedOnMssSuccess(sym, g_ictLowTf.mss.h1FvgId); const double keyLv = g_ictLowTf.mss.chochKeyLevel; const double slSwing = g_ictLowTf.mss.slSwingPrice; g_ictLowTf.mss.phase = ICT_MSS_CHOCH; IctConfirmFvg_ScanNew(sym, cTf, wantSide, g_ictLowTf.mss.chochTime, true); g_ictLowTf.mss.displayReason = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? StringFormat("MSS↓ @ L0=%.2f | SL H0=%.2f — quét M5 FVG", keyLv, slSwing) : StringFormat("MSS↑ @ H0=%.2f | SL L0=%.2f — quét M5 FVG", keyLv, slSwing); } if(g_ictLowTf.mss.phase == ICT_MSS_CHOCH) { IctConfirmFvg_ScanNew(sym, cTf, wantSide, g_ictLowTf.mss.chochTime, false); const int mIdx = IctConfirmFvg_FindLatestAfter(g_ictLowTf.mss.chochTime, wantSide); if(mIdx < 0) { g_ictLowTf.mss.displayReason = "MSS OK — chờ M5 FVG"; return; } if(!IctMss_ZonesOverlapH1(g_ictFvgZones[h1Idx], sym, g_ictDailyBias.bias, g_ictConfirmFvgZones[mIdx].lower, g_ictConfirmFvgZones[mIdx].upper)) { g_ictLowTf.mss.displayReason = "M5 FVG xa H1 FVG — chờ FVG gần hơn"; return; } g_ictLowTf.mss.phase = ICT_MSS_M5_FVG; g_ictLowTf.mss.m5FvgId = g_ictConfirmFvgZones[mIdx].id; g_ictLowTf.mss.m5FvgTime = g_ictConfirmFvgZones[mIdx].createdTime; g_ictLowTf.mss.displayReason = StringFormat("M5 FVG #%I64u [%.2f–%.2f]", g_ictLowTf.mss.m5FvgId, g_ictConfirmFvgZones[mIdx].lower, g_ictConfirmFvgZones[mIdx].upper); } if(g_ictLowTf.mss.phase >= ICT_MSS_M5_FVG && g_ictLowTf.mss.m5FvgId > 0) { const int mIdx = IctConfirmFvg_FindById(g_ictLowTf.mss.m5FvgId); if(mIdx < 0) { g_ictLowTf.mss.phase = ICT_MSS_CHOCH; g_ictLowTf.mss.m5FvgId = 0; g_ictLowTf.mss.displayReason = "M5 FVG mất — quét lại"; return; } IctFvg_UpdateZoneState(sym, cTf, g_ictConfirmFvgZones[mIdx]); if(IctMss_PriceInEntryZone(sym, cTf, g_ictConfirmFvgZones[mIdx])) { g_ictLowTf.mss.phase = ICT_MSS_READY; g_ictLowTf.mss.displayReason = StringFormat("READY | M5 fill %.0f%% | %s %s", g_ictConfirmFvgZones[mIdx].maxFillRatio * 100.0, IctFvgSideText(wantSide), IctPdZoneText(IctFvg_GetRepPd(g_ictFvgZones[h1Idx]))); } else if(IctFvg_HasFillAtLeast(g_ictConfirmFvgZones[mIdx], InpMssEntryFillPct)) { g_ictLowTf.mss.phase = ICT_MSS_ENTRY_FILL; g_ictLowTf.mss.displayReason = StringFormat("M5 chạm %.0f%% — canh entry", InpMssEntryFillPct); } else { g_ictLowTf.mss.phase = ICT_MSS_M5_FVG; g_ictLowTf.mss.displayReason = StringFormat("Chờ hồi M5 FVG %.0f%% (fill %.0f%%)", InpMssEntryFillPct, g_ictConfirmFvgZones[mIdx].maxFillRatio * 100.0); } } IctMss_JournalPipeline(g_ictLowTf.mss.displayReason); } #endif