diff --git a/Experts/ICT_2026.mq5 b/Experts/ICT_2026.mq5 index 68cc3b8..05ad844 100644 --- a/Experts/ICT_2026.mq5 +++ b/Experts/ICT_2026.mq5 @@ -3,8 +3,8 @@ //| BOS = Continue | CHoCH = Reversal | //+------------------------------------------------------------------+ #property copyright "ICT 2026" -#property version "1.112" -#property description "Daily Bias + Intraday H1 + iTF FVG + IsAllowTrade" +#property version "1.126" +#property description "ICT2026 MSS limit entry + SL swing CHoCH + TP intraday" #include #include @@ -47,6 +47,7 @@ void OnDeinit(const int reason) IctPanel_Clear(); IctDraw_Clear(); IctFvgDraw_DeleteAll(); + IctMssDraw_DeleteAll(); ChartRedraw(); } @@ -80,6 +81,7 @@ void OnChartEvent(const int id, const long &lparam, const double &dparam, const IctPanel_Render(_Symbol); IctDraw_Render(_Symbol); IctFvgDraw_Render(_Symbol); + IctMssDraw_Render(_Symbol); } } @@ -92,4 +94,8 @@ bool ICT2026_IsAllowTrade() { return g_ictIntraday.isAllowTrade; } int ICT2026_GetFvgAvailableCount() { return g_ictLowTf.availableCount; } +ENUM_ICT_MSS_PHASE ICT2026_GetMssPhase() { return g_ictLowTf.mss.phase; } + +string ICT2026_GetMssReason() { return g_ictLowTf.mss.displayReason; } + //+------------------------------------------------------------------+ diff --git a/Include/ICT2026/Config.mqh b/Include/ICT2026/Config.mqh index aca6563..8aa7bf2 100644 --- a/Include/ICT2026/Config.mqh +++ b/Include/ICT2026/Config.mqh @@ -52,12 +52,35 @@ input double InpFvgUsedFillPct = 38.2; // % lấp FVG → input int InpFvgExpireDays = 3; // Xóa Available sau N ngày input int InpFvgMaxZones = 24; // Số FVG tối đa trên chart input bool InpDrawFvgZones = true; // Vẽ FVG + Premium/Discount -input color InpFvgBullColor = clrDodgerBlue; -input color InpFvgBearColor = clrOrangeRed; -input color InpFvgUsedColor = clrDimGray; +input color InpFvgBullColor = clrLimeGreen; // Bull FVG +input color InpFvgBearColor = clrRed; // Bear FVG +input color InpFvgUsedColor = clrDimGray; // Used FVG input color InpPdPremiumColor = clrMaroon; input color InpPdDiscountColor = clrDarkGreen; +input group "══ MSS / Entry (Confirm TF) ══" +input double InpMssH1MinFillPct = 38.2; // Retest FVG H1 (POI): % lấp tối thiểu trên H1 +input double InpMssEntryFillPct = 38.2; // M5 FVG: % lấp để sẵn sàng entry +input int InpMssConfirmLookback = 80; // Lookback pivot CHoCH trên Confirm TF +input int InpMssConfirmFvgBars = 40; // Quét M5 FVG sau CHoCH +input double InpMssMaxDistGapPct = 75.0; // MSS: buffer quanh H1 FVG (% chiều cao gap) +input double InpMssMaxDistAtrMult = 1.25; // MSS: thêm buffer (× ATR H1) +input double InpMssExtraBelowGapPct = 100.0; // Bear: thêm % gap dưới lower (MSS dưới FVG) +input double InpMssExtraAboveGapPct = 100.0; // Bull: thêm % gap trên upper +input int InpMssMaxM5BarsAfterTouch = 0; // MSS: max M5 bar sau chạm H1 (0=không giới hạn) +input bool InpDrawConfirmFvg = true; // Vẽ FVG Confirm TF (màu nhạt) +input bool InpDrawMssChoch = true; // Vẽ MSS sau H1 chạm FVG (CHoCH/H0) +input bool InpMssLogJournal = true; // Log lý do chặn vào Experts journal + +input group "══ MSS Trade ══" +input bool InpMssTradeEnabled = true; // Đặt lệnh limit MSS +input ulong InpMssMagic = 202604; // Magic number +input double InpMssRiskPct = 1.0; // R % balance mỗi lệnh +input double InpMssSlAtrMult = 0.5; // SL: buffer ngoài H0/L0 M5 (× ATR M5) +input double InpMssMinRR = 2.0; // TP tối thiểu (× risk entry→SL) +input int InpMssPendingExpireHours = 24; // Hết hạn pending (giờ, 0=không) +input bool InpMssOnePosition = true; // Một position/pending MSS + input group "══ Debug ══" input bool InpDebug = true; diff --git a/Include/ICT2026/ConfirmFvg.mqh b/Include/ICT2026/ConfirmFvg.mqh new file mode 100644 index 0000000..6dac29f --- /dev/null +++ b/Include/ICT2026/ConfirmFvg.mqh @@ -0,0 +1,128 @@ +//+------------------------------------------------------------------+ +//| ConfirmFvg.mqh — FVG trên Confirm TF (M5) cho MSS entry | +//+------------------------------------------------------------------+ +#ifndef ICT2026_CONFIRMFVG_MQH +#define ICT2026_CONFIRMFVG_MQH + +#include + +IctFvgZone g_ictConfirmFvgZones[]; +int g_ictConfirmFvgCount = 0; +static ulong g_ictConfirmFvgNextId = 100000; + +void IctConfirmFvg_Reset() +{ + g_ictConfirmFvgCount = 0; + ArrayResize(g_ictConfirmFvgZones, 0); +} + +bool IctConfirmFvg_IsDuplicate(const datetime createdTime, const double upper, const double lower) +{ + const double tol = _Point * 2.0; + for(int i = 0; i < g_ictConfirmFvgCount; i++) + { + if(g_ictConfirmFvgZones[i].createdTime != createdTime) + continue; + if(MathAbs(g_ictConfirmFvgZones[i].upper - upper) <= tol && + MathAbs(g_ictConfirmFvgZones[i].lower - lower) <= tol) + return true; + } + return false; +} + +void IctConfirmFvg_AddZone(const IctFvgZone &zone) +{ + if(g_ictConfirmFvgCount >= InpFvgMaxZones) + { + for(int i = 0; i < g_ictConfirmFvgCount - 1; i++) + g_ictConfirmFvgZones[i] = g_ictConfirmFvgZones[i + 1]; + g_ictConfirmFvgCount--; + } + ArrayResize(g_ictConfirmFvgZones, g_ictConfirmFvgCount + 1); + g_ictConfirmFvgZones[g_ictConfirmFvgCount] = zone; + g_ictConfirmFvgCount++; +} + +void IctConfirmFvg_UpdateAll(const string sym, const ENUM_TIMEFRAMES tf) +{ + for(int i = 0; i < g_ictConfirmFvgCount; i++) + IctFvg_UpdateZoneState(sym, tf, g_ictConfirmFvgZones[i]); +} + +void IctConfirmFvg_ScanNew(const string sym, const ENUM_TIMEFRAMES tf, + const ENUM_ICT_FVG_SIDE wantSide, + const datetime notBefore, + const bool fullLookback = false) +{ + if(wantSide == ICT_FVG_NONE) + return; + + const int maxShift = MathMin(InpMssConfirmFvgBars, Bars(sym, tf) - 3); + if(maxShift < 1) + return; + + int fromSh = fullLookback ? maxShift : MathMin(maxShift, MathMax(1, InpFvgScanBarsPerUpdate)); + + for(int sh = fromSh; sh >= 1; sh--) + { + ENUM_ICT_FVG_SIDE side = ICT_FVG_NONE; + double upper = 0.0, lower = 0.0; + datetime timeA = 0, timeC = 0; + + if(!IctFvg_TryDetectAtShift(sym, tf, sh, wantSide, side, upper, lower, timeA, timeC)) + continue; + if(notBefore > 0 && timeC < notBefore) + continue; + if(IctConfirmFvg_IsDuplicate(timeC, upper, lower)) + continue; + + IctFvgZone zone; + zone.Clear(); + zone.id = g_ictConfirmFvgNextId++; + zone.side = side; + zone.state = ICT_FVG_AVAILABLE; + zone.upper = upper; + zone.lower = lower; + zone.createdTime = timeC; + zone.timeStart = timeA; + zone.expireTime = timeC + (datetime)(InpFvgExpireDays * 86400); + zone.locked = true; + IctFvg_UpdateZoneState(sym, tf, zone); + IctConfirmFvg_AddZone(zone); + + if(InpDebug) + PrintFormat("[ICT2026/CFVG] New %s [%.2f – %.2f] @ %s", + IctFvgSideText(zone.side), zone.lower, zone.upper, + TimeToString(timeC, TIME_DATE | TIME_MINUTES)); + } +} + +int IctConfirmFvg_FindLatestAfter(const datetime notBefore, const ENUM_ICT_FVG_SIDE side) +{ + int bestIdx = -1; + datetime bestT = 0; + + for(int i = 0; i < g_ictConfirmFvgCount; i++) + { + if(g_ictConfirmFvgZones[i].side != side) + continue; + if(notBefore > 0 && g_ictConfirmFvgZones[i].createdTime < notBefore) + continue; + if(g_ictConfirmFvgZones[i].createdTime >= bestT) + { + bestT = g_ictConfirmFvgZones[i].createdTime; + bestIdx = i; + } + } + return bestIdx; +} + +int IctConfirmFvg_FindById(const ulong id) +{ + for(int i = 0; i < g_ictConfirmFvgCount; i++) + if(g_ictConfirmFvgZones[i].id == id) + return i; + return -1; +} + +#endif diff --git a/Include/ICT2026/Draw.mqh b/Include/ICT2026/Draw.mqh index 22aef33..984a489 100644 --- a/Include/ICT2026/Draw.mqh +++ b/Include/ICT2026/Draw.mqh @@ -90,13 +90,8 @@ void IctDraw_SwingSet(const string objPfx, const string tagPfx, bool IctDraw_BuildConfirmSwings(const string sym, IctSwingSet &sw) { - sw.Clear(); ENUM_ICT_STRUCT structural = ICT_STRUCT_NONE; - return IctBuildSwingSetRecentPivots(sym, InpConfirmTf, - InpConfirmSwingRange, - InpConfirmSwingLookback, - InpConfirmRecentBars, - sw, structural); + return IctBuildConfirmSwingSet(sym, sw, structural); } void IctDraw_Render(const string sym) diff --git a/Include/ICT2026/Fvg.mqh b/Include/ICT2026/Fvg.mqh index ea036ed..adfaea8 100644 --- a/Include/ICT2026/Fvg.mqh +++ b/Include/ICT2026/Fvg.mqh @@ -185,6 +185,42 @@ ENUM_ICT_PD_ZONE IctFvg_ClassifyPd(const ENUM_ICT_FVG_SIDE side, return ICT_PD_NONE; } +bool IctFvg_OverlapsPremium(const IctFvgZone &zone) +{ + if(zone.pdEq <= 0.0 || zone.pdHigh <= zone.pdLow + _Point) + return false; + return (zone.upper > zone.pdEq + _Point && zone.lower < zone.pdHigh + _Point); +} + +bool IctFvg_OverlapsDiscount(const IctFvgZone &zone) +{ + if(zone.pdEq <= 0.0 || zone.pdHigh <= zone.pdLow + _Point) + return false; + return (zone.lower < zone.pdEq - _Point && zone.upper > zone.pdLow - _Point); +} + +ENUM_ICT_PD_ZONE IctFvg_ComputeRepPd(const IctFvgZone &zone) +{ + if(zone.pdEq <= 0.0) + return ICT_PD_NONE; + + const bool inPrem = IctFvg_OverlapsPremium(zone); + const bool inDisc = IctFvg_OverlapsDiscount(zone); + + if(inPrem && !inDisc) + return ICT_PD_PREMIUM; + if(inDisc && !inPrem) + return ICT_PD_DISCOUNT; + if(inPrem && inDisc) + { + if(zone.side == ICT_FVG_BEAR) + return ICT_PD_PREMIUM; + if(zone.side == ICT_FVG_BULL) + return ICT_PD_DISCOUNT; + } + return ICT_PD_NONE; +} + double IctFvg_BarExtremeSince(const string sym, const ENUM_TIMEFRAMES tf, const datetime fromTime, const bool wantLow) { @@ -220,104 +256,52 @@ bool IctFvg_IsConfirmedPivot(const string sym, const ENUM_TIMEFRAMES tf, return IctIsSwingLow(sym, tf, pt.shift, str); } -// Đỉnh PD bear = swing high gần nhất (theo thời gian) trên FVG tại lúc tạo FVG — không dùng iH0/iH1 live +// Đỉnh PD bear = pivot swing high gần FVG nhất (theo bar shift), trên upper — không dùng wick nến bool IctFvg_ResolvePdHighBear(const string sym, const IctFvgZone &zone, double &outHigh) { - outHigh = 0.0; - datetime bestTime = 0; - bool found = false; + outHigh = 0.0; const ENUM_TIMEFRAMES pdTf = InpIntradayTf; - const int pdSec = (int)PeriodSeconds(pdTf); - const datetime tCutoff = zone.createdTime + (datetime)pdSec; - const datetime tMin = zone.createdTime - (datetime)(InpIntradayRecentBars * pdSec); + int fvgShift = iBarShift(sym, pdTf, zone.createdTime, true); + if(fvgShift < 0) + fvgShift = iBarShift(sym, pdTf, zone.createdTime, false); + if(fvgShift < 0) + return false; IctSwingPoint highs[], lows[]; IctCollectSwings(sym, pdTf, InpIntradaySwingRange, InpIntradaySwingLookback, highs, lows); const int nH = ArraySize(highs); - for(int i = 0; i < nH; i++) - { - if(highs[i].price <= zone.upper + _Point) - continue; - if(highs[i].time > tCutoff || highs[i].time < tMin) - continue; - if(!found || highs[i].time > bestTime) - { - bestTime = highs[i].time; - outHigh = highs[i].price; - found = true; - } - } + IctSwingPoint pt = IctFindNearestSwingHighBefore(highs, nH, fvgShift, zone.upper); + if(!pt.Valid()) + return false; - const int maxBars = MathMax(InpIntradayRecentBars, 30); - for(int sh = 0; sh < maxBars && sh < Bars(sym, pdTf); sh++) - { - const datetime t = iTime(sym, pdTf, sh); - if(t > tCutoff || t < tMin) - continue; - const double h = iHigh(sym, pdTf, sh); - if(h <= zone.upper + _Point) - continue; - if(!found || t > bestTime) - { - bestTime = t; - outHigh = h; - found = true; - } - } - - return found; + outHigh = pt.price; + return true; } -// Đáy PD bull = swing low gần nhất (theo thời gian) dưới FVG tại lúc tạo FVG +// Đáy PD bull = pivot swing low gần FVG nhất (theo bar shift), dưới lower bool IctFvg_ResolvePdLowBull(const string sym, const IctFvgZone &zone, double &outLow) { - outLow = 0.0; - datetime bestTime = 0; - bool found = false; + outLow = 0.0; const ENUM_TIMEFRAMES pdTf = InpIntradayTf; - const int pdSec = (int)PeriodSeconds(pdTf); - const datetime tCutoff = zone.createdTime + (datetime)pdSec; - const datetime tMin = zone.createdTime - (datetime)(InpIntradayRecentBars * pdSec); + int fvgShift = iBarShift(sym, pdTf, zone.createdTime, true); + if(fvgShift < 0) + fvgShift = iBarShift(sym, pdTf, zone.createdTime, false); + if(fvgShift < 0) + return false; IctSwingPoint highs[], lows[]; IctCollectSwings(sym, pdTf, InpIntradaySwingRange, InpIntradaySwingLookback, highs, lows); const int nL = ArraySize(lows); - for(int i = 0; i < nL; i++) - { - if(lows[i].price >= zone.lower - _Point) - continue; - if(lows[i].time > tCutoff || lows[i].time < tMin) - continue; - if(!found || lows[i].time > bestTime) - { - bestTime = lows[i].time; - outLow = lows[i].price; - found = true; - } - } + IctSwingPoint pt = IctFindNearestSwingLowBefore(lows, nL, fvgShift, zone.lower); + if(!pt.Valid()) + return false; - const int maxBars = MathMax(InpIntradayRecentBars, 30); - for(int sh = 0; sh < maxBars && sh < Bars(sym, pdTf); sh++) - { - const datetime t = iTime(sym, pdTf, sh); - if(t > tCutoff || t < tMin) - continue; - const double l = iLow(sym, pdTf, sh); - if(l >= zone.lower - _Point) - continue; - if(!found || t > bestTime) - { - bestTime = t; - outLow = l; - found = true; - } - } - - return found; + outLow = pt.price; + return true; } bool IctFvg_NearestLowBelow(const string sym, const double refLower, @@ -427,6 +411,45 @@ void IctFvg_FinalizePdMetrics(IctFvgZone &zone) zone.pdEq = (zone.pdHigh + zone.pdLow) * 0.5; const double mid = (zone.upper + zone.lower) * 0.5; zone.pdZone = IctFvg_ClassifyPd(zone.side, mid, zone.pdEq, zone.pdHigh, zone.pdLow); + zone.repPd = IctFvg_ComputeRepPd(zone); +} + +ENUM_ICT_PD_ZONE IctFvg_GetRepPd(const IctFvgZone &zone) +{ + if(zone.repPd != ICT_PD_NONE) + return zone.repPd; + return zone.pdZone; +} + +bool IctFvg_IsEntryRepPd(const IctFvgZone &zone) +{ + if(zone.pdEq <= 0.0) + return false; + if(g_ictDailyBias.bias == ICT_BIAS_BEAR) + return IctFvg_OverlapsPremium(zone); + if(g_ictDailyBias.bias == ICT_BIAS_BULL) + return IctFvg_OverlapsDiscount(zone); + return false; +} + +bool IctFvg_HasFillAtLeast(const IctFvgZone &zone, const double pct) +{ + if(pct <= 0.0) + return true; + return (zone.maxFillRatio >= pct / 100.0 - 1e-9); +} + +double IctFvg_PriceAtFillPct(const IctFvgZone &zone, const double pct) +{ + const double h = zone.upper - zone.lower; + if(h <= _Point) + return 0.0; + const double r = MathMax(0.0, MathMin(100.0, pct)) / 100.0; + if(zone.side == ICT_FVG_BULL) + return zone.upper - h * r; + if(zone.side == ICT_FVG_BEAR) + return zone.lower + h * r; + return 0.0; } void IctFvg_UpdatePdForZone(const string sym, IctFvgZone &zone) @@ -445,6 +468,10 @@ void IctFvg_UpdatePdForZone(const string sym, IctFvgZone &zone) const int nL = ArraySize(lows); const int nH = ArraySize(highs); + int fvgShift = iBarShift(sym, pdTf, zone.createdTime, true); + if(fvgShift < 0) + fvgShift = iBarShift(sym, pdTf, zone.createdTime, false); + if(zone.side == ICT_FVG_BEAR) { if(!zone.pdSwing1Set) @@ -455,7 +482,12 @@ void IctFvg_UpdatePdForZone(const string sym, IctFvgZone &zone) } IctSwingPoint swingLow; - if(IctFvg_FindFirstSwingLowAfter(lows, nL, zone.createdTime, swingLow) && + if(fvgShift >= 0) + swingLow = IctFindFirstSwingLowAfterShift(lows, nL, fvgShift); + else + IctFvg_FindFirstSwingLowAfter(lows, nL, zone.createdTime, swingLow); + + if(swingLow.Valid() && IctFvg_IsConfirmedPivot(sym, pdTf, swingLow, false)) { zone.pdLow = swingLow.price; @@ -481,7 +513,12 @@ void IctFvg_UpdatePdForZone(const string sym, IctFvgZone &zone) } IctSwingPoint swingHigh; - if(IctFvg_FindFirstSwingHighAfter(highs, nH, zone.createdTime, swingHigh) && + if(fvgShift >= 0) + swingHigh = IctFindFirstSwingHighAfterShift(highs, nH, fvgShift); + else + IctFvg_FindFirstSwingHighAfter(highs, nH, zone.createdTime, swingHigh); + + if(swingHigh.Valid() && IctFvg_IsConfirmedPivot(sym, pdTf, swingHigh, true)) { zone.pdHigh = swingHigh.price; diff --git a/Include/ICT2026/FvgDraw.mqh b/Include/ICT2026/FvgDraw.mqh index cf47f54..c2ed5b7 100644 --- a/Include/ICT2026/FvgDraw.mqh +++ b/Include/ICT2026/FvgDraw.mqh @@ -6,8 +6,11 @@ #include #include +#include +#include -const string ICT26_FVG_PFX = "ICT26_FVG_"; +const string ICT26_FVG_PFX = "ICT26_FVG_"; +const string ICT26_CFVG_PFX = "ICT26_CFVG_"; void IctFvgDraw_DeleteAll() { @@ -15,7 +18,7 @@ void IctFvgDraw_DeleteAll() for(int i = ObjectsTotal(ch, 0, -1) - 1; i >= 0; i--) { const string name = ObjectName(ch, i, 0, -1); - if(StringFind(name, ICT26_FVG_PFX) == 0) + if(StringFind(name, ICT26_FVG_PFX) == 0 || StringFind(name, ICT26_CFVG_PFX) == 0) ObjectDelete(ch, name); } } @@ -54,22 +57,6 @@ void IctFvgDraw_HLine(const string name, const datetime t1, const datetime t2, ObjectMove(ch, name, 1, t2, price); } -void IctFvgDraw_Label(const string name, const datetime t, const double price, - const string text, const color clr) -{ - const long ch = ChartID(); - if(ObjectFind(ch, name) < 0) - ObjectCreate(ch, name, OBJ_TEXT, 0, t, price); - - ObjectMove(ch, name, 0, t, price); - ObjectSetString(ch, name, OBJPROP_TEXT, text); - ObjectSetInteger(ch, name, OBJPROP_COLOR, clr); - ObjectSetInteger(ch, name, OBJPROP_FONTSIZE, InpChartLabelFontSize); - ObjectSetString(ch, name, OBJPROP_FONT, "Arial Bold"); - ObjectSetInteger(ch, name, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); - ObjectSetInteger(ch, name, OBJPROP_SELECTABLE, false); -} - void IctFvgDraw_Render(const string sym) { if(!InpDrawFvgZones) @@ -111,13 +98,29 @@ void IctFvgDraw_Render(const string sym) IctFvgDraw_HLine(pfx + "PD_EQ", pdStart, pdEnd, g_ictFvgZones[i].pdEq, clrWhite, STYLE_DOT); } + } - string tag = StringFormat("iFVG %s %s", IctFvgSideText(g_ictFvgZones[i].side), - IctFvgStateText(g_ictFvgZones[i].state)); - if(g_ictFvgZones[i].pdZone != ICT_PD_NONE) - tag += " " + IctPdZoneText(g_ictFvgZones[i].pdZone); + if(InpDrawConfirmFvg) + { + for(int j = 0; j < g_ictConfirmFvgCount; j++) + { + const string cid = IntegerToString((long)g_ictConfirmFvgZones[j].id); + const string cpfx = ICT26_CFVG_PFX + cid + "_"; + const datetime cStart = g_ictConfirmFvgZones[j].createdTime; + const datetime cEnd = IctFvg_GetFvgDrawTimeEnd(sym, InpConfirmTf, g_ictConfirmFvgZones[j]); - IctFvgDraw_Label(pfx + "LBL", fvgEnd, g_ictFvgZones[i].upper, tag, fvgClr); + color cClr = (g_ictConfirmFvgZones[j].side == ICT_FVG_BULL) ? + clrDarkGreen : clrFireBrick; + if(g_ictConfirmFvgZones[j].state == ICT_FVG_USED) + cClr = InpFvgUsedColor; + + if(IctLowTf_MssM5FvgId() == g_ictConfirmFvgZones[j].id && + IctLowTf_MssPhase() >= ICT_MSS_M5_FVG) + cClr = (g_ictConfirmFvgZones[j].side == ICT_FVG_BULL) ? clrAqua : clrOrange; + + IctFvgDraw_Rect(cpfx + "BOX", cStart, g_ictConfirmFvgZones[j].upper, + cEnd, g_ictConfirmFvgZones[j].lower, cClr, true); + } } ChartRedraw(); diff --git a/Include/ICT2026/Journal.mqh b/Include/ICT2026/Journal.mqh new file mode 100644 index 0000000..3ebda94 --- /dev/null +++ b/Include/ICT2026/Journal.mqh @@ -0,0 +1,53 @@ +//+------------------------------------------------------------------+ +//| Journal.mqh — log lý do chặn MSS/entry vào Experts journal | +//+------------------------------------------------------------------+ +#ifndef ICT2026_JOURNAL_MQH +#define ICT2026_JOURNAL_MQH + +#include + +string g_ictMssJournalLast = ""; +string g_ictMssEntryJournalLast = ""; + +bool IctMss_IsEntryReadyReason(const string reason) +{ + if(StringFind(reason, "READY") == 0) + return true; + if(StringFind(reason, "Limit ") >= 0) + return true; + if(StringFind(reason, "Sell/Buy limit") >= 0) + return true; + return false; +} + +void IctMss_JournalPipeline(const string reason) +{ + if(!InpMssLogJournal) + return; + if(IctMss_IsEntryReadyReason(reason)) + return; + if(reason == g_ictMssJournalLast) + return; + + g_ictMssJournalLast = reason; + PrintFormat("[ICT2026/MSS] %s", reason); +} + +void IctMss_JournalEntryBlock(const string reason) +{ + if(!InpMssLogJournal) + return; + if(reason == g_ictMssEntryJournalLast) + return; + + g_ictMssEntryJournalLast = reason; + PrintFormat("[ICT2026/Entry] Chặn lệnh: %s", reason); +} + +void IctMss_JournalReset() +{ + g_ictMssJournalLast = ""; + g_ictMssEntryJournalLast = ""; +} + +#endif diff --git a/Include/ICT2026/LowTfApi.mqh b/Include/ICT2026/LowTfApi.mqh new file mode 100644 index 0000000..6cbe646 --- /dev/null +++ b/Include/ICT2026/LowTfApi.mqh @@ -0,0 +1,15 @@ +//+------------------------------------------------------------------+ +//| LowTfApi.mqh — state Low TF / MSS (tránh include vòng) | +//+------------------------------------------------------------------+ +#ifndef ICT2026_LOWTFAPI_MQH +#define ICT2026_LOWTFAPI_MQH + +#include + +IctLowTfState g_ictLowTf; + +ulong IctLowTf_MssM5FvgId() { return g_ictLowTf.mss.m5FvgId; } + +ENUM_ICT_MSS_PHASE IctLowTf_MssPhase() { return g_ictLowTf.mss.phase; } + +#endif diff --git a/Include/ICT2026/LowTfTrend.mqh b/Include/ICT2026/LowTfTrend.mqh index 831c7de..aff0019 100644 --- a/Include/ICT2026/LowTfTrend.mqh +++ b/Include/ICT2026/LowTfTrend.mqh @@ -6,22 +6,29 @@ #include #include +#include +#include +#include #include - -IctLowTfState g_ictLowTf; +#include void IctLowTfTrend_Reset() { - g_ictLowTf.lastBarTime = 0; - g_ictLowTf.activeCount = 0; - g_ictLowTf.availableCount = 0; - g_ictLowTf.displayReason = ""; + g_ictLowTf.lastBarTime = 0; + g_ictLowTf.lastConfirmBarTime = 0; + g_ictLowTf.activeCount = 0; + g_ictLowTf.availableCount = 0; + g_ictLowTf.confirmFvgCount = 0; + g_ictLowTf.displayReason = ""; + g_ictLowTf.mss.Clear(); IctFvg_Reset(); + IctConfirmFvg_Reset(); } bool IctLowTfTrend_Init(const string sym) { IctLowTfTrend_Reset(); + IctMssEntry_Init(); if(Bars(sym, InpFvgTf) < 10) return false; IctLowTfTrend_Update(sym, true); @@ -62,6 +69,17 @@ bool IctLowTfTrend_Update(const string sym, const bool force = false) g_ictLowTf.activeCount = g_ictFvgCount; g_ictLowTf.availableCount = IctFvg_CountAvailable(); + const datetime cBar0 = iTime(sym, InpConfirmTf, 0); + const bool confirmNewBar = (force || cBar0 != g_ictLowTf.lastConfirmBarTime); + if(confirmNewBar) + { + g_ictLowTf.lastConfirmBarTime = cBar0; + IctConfirmFvg_UpdateAll(sym, InpConfirmTf); + IctMss_Update(sym); + IctMssEntry_Update(sym); + g_ictLowTf.confirmFvgCount = g_ictConfirmFvgCount; + } + if(InpDebug && (force || g_ictIntraday.isAllowTrade)) PrintFormat("[ICT2026/LowTF] %s | Allow=%s | FVG avail=%d total=%d | %s", sym, @@ -70,6 +88,7 @@ bool IctLowTfTrend_Update(const string sym, const bool force = false) g_ictLowTf.displayReason); IctFvgDraw_Render(sym); + IctMssDraw_Render(sym); return true; } @@ -103,8 +122,27 @@ void IctLowTfTrend_TickRefresh(const string sym) MathAbs(g_ictFvgZones[i].pdLow - oldPdLo[i]) > _Point) dirty = true; } + if(g_ictIntraday.isAllowTrade) + { + const ENUM_ICT_MSS_PHASE prevMss = g_ictLowTf.mss.phase; + IctMss_Update(sym); + IctMssEntry_Update(sym); + if(prevMss != g_ictLowTf.mss.phase) + dirty = true; + } + else if(g_ictLowTf.mss.pendingTicket > 0) + { + IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); + g_ictLowTf.mss.pendingTicket = 0; + } + if(dirty) IctFvgDraw_Render(sym); + + if(g_ictLowTf.mss.phase >= ICT_MSS_CHOCH) + IctMssDraw_Render(sym); + else if(!InpDrawMssChoch) + IctMssDraw_DeleteAll(); } void IctLowTfTrend_Get(IctLowTfState &out) { out = g_ictLowTf; } diff --git a/Include/ICT2026/MssDraw.mqh b/Include/ICT2026/MssDraw.mqh new file mode 100644 index 0000000..cc27298 --- /dev/null +++ b/Include/ICT2026/MssDraw.mqh @@ -0,0 +1,221 @@ +//+------------------------------------------------------------------+ +//| MssDraw.mqh — vẽ MSS sau H1 chạm FVG: touch, 38.2%, CHoCH, H0 | +//+------------------------------------------------------------------+ +#ifndef ICT2026_MSSDRAW_MQH +#define ICT2026_MSSDRAW_MQH + +#include +#include +#include + +const string ICT26_MSS_PFX = "ICT26_MSS_"; + +void IctMssDraw_DeleteAll() +{ + const long ch = ChartID(); + for(int i = ObjectsTotal(ch, 0, -1) - 1; i >= 0; i--) + { + const string name = ObjectName(ch, i, 0, -1); + if(StringFind(name, ICT26_MSS_PFX) == 0) + ObjectDelete(ch, name); + } +} + +void IctMssDraw_HLine(const string name, const datetime t1, const datetime t2, + const double price, const color clr, const ENUM_LINE_STYLE style, + const int width = 1) +{ + const long ch = ChartID(); + const string obj = ICT26_MSS_PFX + name; + if(ObjectFind(ch, obj) < 0) + ObjectCreate(ch, obj, OBJ_TREND, 0, t1, price, t2, price); + + ObjectSetInteger(ch, obj, OBJPROP_COLOR, clr); + ObjectSetInteger(ch, obj, OBJPROP_STYLE, style); + ObjectSetInteger(ch, obj, OBJPROP_WIDTH, width); + ObjectSetInteger(ch, obj, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(ch, obj, OBJPROP_BACK, true); + ObjectSetInteger(ch, obj, OBJPROP_SELECTABLE, false); + ObjectMove(ch, obj, 0, t1, price); + ObjectMove(ch, obj, 1, t2, price); +} + +void IctMssDraw_VLine(const string name, const datetime t, const color clr) +{ + const long ch = ChartID(); + const string obj = ICT26_MSS_PFX + name; + if(ObjectFind(ch, obj) < 0) + ObjectCreate(ch, obj, OBJ_VLINE, 0, t, 0.0); + + ObjectSetInteger(ch, obj, OBJPROP_COLOR, clr); + ObjectSetInteger(ch, obj, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(ch, obj, OBJPROP_WIDTH, 1); + ObjectSetInteger(ch, obj, OBJPROP_BACK, true); + ObjectSetInteger(ch, obj, OBJPROP_SELECTABLE, false); + ObjectMove(ch, obj, 0, t, 0.0); +} + +void IctMssDraw_Label(const string name, const string text, + const datetime t, const double price, + const bool above, const color clr) +{ + const long ch = ChartID(); + const string obj = ICT26_MSS_PFX + name; + if(ObjectFind(ch, obj) < 0) + ObjectCreate(ch, obj, OBJ_TEXT, 0, t, price); + + ObjectMove(ch, obj, 0, t, price); + ObjectSetString(ch, obj, OBJPROP_TEXT, text); + ObjectSetInteger(ch, obj, OBJPROP_COLOR, clr); + ObjectSetInteger(ch, obj, OBJPROP_FONTSIZE, InpChartLabelFontSize); + ObjectSetString(ch, obj, OBJPROP_FONT, "Arial Bold"); + ObjectSetInteger(ch, obj, OBJPROP_ANCHOR, above ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); + ObjectSetInteger(ch, obj, OBJPROP_SELECTABLE, false); +} + +datetime IctMssDraw_TimeEnd(const string sym) +{ + const ENUM_TIMEFRAMES cTf = InpConfirmTf; + return iTime(sym, cTf, 0) + (datetime)PeriodSeconds(cTf); +} + +void IctMssDraw_ResolveChoch(const string sym, const IctFvgZone &h1, + const IctMssState &mss, + double &keyLv, datetime &keyT, double &slSwing, datetime &chochT) +{ + keyLv = mss.chochKeyLevel; + keyT = mss.chochKeyTime; + slSwing = mss.slSwingPrice; + chochT = mss.chochTime; + + if(keyLv <= 0.0 && mss.phase >= ICT_MSS_H1_TOUCH) + { + datetime kt = 0; + double ss = 0.0; + if(IctMss_DetectChochAfterH1FvgRetest(sym, InpConfirmTf, h1, g_ictDailyBias.bias, + keyLv, kt, ss)) + { + keyT = kt; + slSwing = ss; + chochT = iTime(sym, InpConfirmTf, 1); + if(chochT == 0) + chochT = kt; + } + } + + if(chochT <= 0) + chochT = keyT; +} + +void IctMssDraw_Render(const string sym) +{ + if(!InpDrawMssChoch) + { + IctMssDraw_DeleteAll(); + return; + } + + const IctMssState mss = g_ictLowTf.mss; + if(mss.phase < ICT_MSS_H1_TOUCH || mss.h1FvgId == 0) + { + IctMssDraw_DeleteAll(); + return; + } + + const int h1Idx = IctMss_FindH1FvgById(mss.h1FvgId); + if(h1Idx < 0) + { + IctMssDraw_DeleteAll(); + return; + } + + IctMssDraw_DeleteAll(); + + const IctFvgZone h1 = g_ictFvgZones[h1Idx]; + const datetime tEnd = IctMssDraw_TimeEnd(sym); + const bool isBear = (g_ictDailyBias.bias == ICT_BIAS_BEAR); + + datetime tTouch = mss.h1TouchTime; + if(tTouch <= 0) + tTouch = h1.firstTouchTime; + if(tTouch <= 0) + tTouch = h1.createdTime; + + if(tTouch > 0) + { + IctMssDraw_VLine("H1_TOUCH", tTouch, clrDodgerBlue); + IctMssDraw_Label("H1_TOUCH_LBL", " Retest FVG H1 ", tTouch, h1.upper, true, clrDodgerBlue); + } + + const double fill382 = IctFvg_PriceAtFillPct(h1, InpMssH1MinFillPct); + if(fill382 > 0.0 && tTouch > 0) + { + IctMssDraw_HLine("H1_382", tTouch, tEnd, fill382, clrSilver, STYLE_DOT, 1); + IctMssDraw_Label("H1_382_LBL", + StringFormat(" H1 %.0f%% ", InpMssH1MinFillPct), + tTouch, fill382, !isBear, clrSilver); + } + + double keyLv = 0.0, slSwing = 0.0; + datetime keyT = 0, chochT = 0; + IctMssDraw_ResolveChoch(sym, h1, mss, keyLv, keyT, slSwing, chochT); + + if(keyLv > 0.0) + { + if(keyT <= 0) + keyT = tTouch; + if(chochT <= 0) + chochT = keyT; + + const color clrChoch = clrGold; + const string chochLbl = isBear ? " CHoCH↓ " : " CHoCH↑ "; + const bool chochAbove = !isBear; + const bool chochPending = (mss.phase == ICT_MSS_H1_TOUCH); + + IctMssDraw_HLine("CHOCH", keyT, tEnd, keyLv, clrChoch, + chochPending ? STYLE_DASH : STYLE_SOLID, 2); + IctMssDraw_Label("CHOCH_LBL", chochLbl, tEnd, keyLv, chochAbove, clrChoch); + } + + if(slSwing <= 0.0) + { + double mssSwing = 0.0; + if(IctMss_GetConfirmMssSwing(sym, g_ictDailyBias.bias, mssSwing)) + slSwing = mssSwing; + } + + if(slSwing > 0.0) + { + const string swTag = isBear ? " H0 " : " L0 "; + const color clrSw = clrYellow; + datetime tSw = (keyT > 0) ? keyT : tTouch; + + IctSwingSet sw; + ENUM_ICT_STRUCT structural = ICT_STRUCT_NONE; + if(IctBuildConfirmSwingSet(sym, sw, structural)) + { + if(isBear && sw.hasH0) + tSw = sw.h0.time; + else if(!isBear && sw.hasL0) + tSw = sw.l0.time; + } + + IctMssDraw_HLine("MSS_SW", tSw, tEnd, slSwing, clrSw, STYLE_DASH, 1); + IctMssDraw_Label("MSS_SW_LBL", swTag, tEnd, slSwing, true, clrSw); + } + + if(mss.pendingEntry > 0.0 && mss.phase >= ICT_MSS_M5_FVG) + { + datetime tSw = (keyT > 0) ? keyT : tTouch; + IctMssDraw_HLine("ENTRY", tEnd - (datetime)PeriodSeconds(InpConfirmTf) * 3, + tEnd, mss.pendingEntry, clrAqua, STYLE_DOT, 1); + if(mss.pendingSl > 0.0) + IctMssDraw_HLine("SL", tSw, tEnd, mss.pendingSl, clrOrangeRed, STYLE_DOT, 1); + if(mss.pendingTp > 0.0) + IctMssDraw_HLine("TP", tSw, tEnd, mss.pendingTp, clrLimeGreen, STYLE_DOT, 1); + } + + ChartRedraw(); +} + +#endif diff --git a/Include/ICT2026/MssEntry.mqh b/Include/ICT2026/MssEntry.mqh new file mode 100644 index 0000000..0c09c5f --- /dev/null +++ b/Include/ICT2026/MssEntry.mqh @@ -0,0 +1,320 @@ +//+------------------------------------------------------------------+ +//| MssEntry.mqh — Limit @ M5 FVG edge | SL H0/L0 M5 | TP min RR | +//+------------------------------------------------------------------+ +#ifndef ICT2026_MSSENTRY_MQH +#define ICT2026_MSSENTRY_MQH + +#include +#include +#include +#include + +CTrade g_ictMssTrade; + +double IctMssEntry_NormalizePrice(const string sym, const double price) +{ + const double tick = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); + if(tick <= 0.0) + return NormalizeDouble(price, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)); + return NormalizeDouble(MathRound(price / tick) * tick, + (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)); +} + +double IctMssEntry_NormalizeVolume(const string sym, double vol) +{ + const double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP); + const double vmin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN); + const double vmax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX); + if(step <= 0.0) + return 0.0; + vol = MathFloor(vol / step) * step; + if(vol < vmin - 1e-12) + return 0.0; + if(vol > vmax) + vol = vmax; + return NormalizeDouble(vol, 2); +} + +double IctMssEntry_VolumeForRisk(const string sym, const bool isBuy, + const double entry, const double sl) +{ + const double pt = SymbolInfoDouble(sym, SYMBOL_POINT); + if(MathAbs(entry - sl) < pt) + return 0.0; + + const double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (InpMssRiskPct / 100.0); + double profit = 0.0; + if(!OrderCalcProfit(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, + sym, 1.0, entry, sl, profit)) + return 0.0; + + const double lossPerLot = MathAbs(profit); + if(lossPerLot < DBL_EPSILON) + return 0.0; + + return riskMoney / lossPerLot; +} + +double IctMssEntry_Atr(const string sym, const ENUM_TIMEFRAMES tf, const int period) +{ + const int h = iATR(sym, tf, period); + 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 IctMssEntry_LimitPrice(const IctFvgZone &zone) +{ + if(zone.side == ICT_FVG_BULL) + return zone.upper; + if(zone.side == ICT_FVG_BEAR) + return zone.lower; + return 0.0; +} + +bool IctMssEntry_ComputeLevels(const string sym, const IctFvgZone &m5Zone, + double &entryOut, double &slOut, double &tpOut) +{ + entryOut = slOut = tpOut = 0.0; + + entryOut = IctMssEntry_LimitPrice(m5Zone); + if(entryOut <= 0.0) + return false; + + const bool isBuy = (m5Zone.side == ICT_FVG_BULL); + const ENUM_ICT_BIAS bias = isBuy ? ICT_BIAS_BULL : ICT_BIAS_BEAR; + + double swingSl = 0.0; + if(!IctMss_GetConfirmMssSwing(sym, bias, swingSl)) + { + if(g_ictLowTf.mss.slSwingPrice > 0.0) + swingSl = g_ictLowTf.mss.slSwingPrice; + else + return false; + } + + const double atrM5 = IctMssEntry_Atr(sym, InpConfirmTf, InpFvgAtrPeriod); + const double bufSl = (atrM5 > 0.0) ? atrM5 * InpMssSlAtrMult : 10.0 * _Point; + const double minRR = MathMax(1.0, InpMssMinRR); + + if(isBuy) + { + slOut = swingSl - bufSl; + const double risk = entryOut - slOut; + if(risk <= _Point) + return false; + tpOut = entryOut + risk * minRR; + } + else + { + slOut = swingSl + bufSl; + const double risk = slOut - entryOut; + if(risk <= _Point) + return false; + tpOut = entryOut - risk * minRR; + } + + entryOut = IctMssEntry_NormalizePrice(sym, entryOut); + slOut = IctMssEntry_NormalizePrice(sym, slOut); + tpOut = IctMssEntry_NormalizePrice(sym, tpOut); + + if(isBuy && (slOut >= entryOut - _Point || tpOut <= entryOut + _Point)) + return false; + if(!isBuy && (slOut <= entryOut + _Point || tpOut >= entryOut - _Point)) + return false; + + return true; +} + +bool IctMssEntry_HasOpenExposure(const string sym) +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != sym) + continue; + if((ulong)PositionGetInteger(POSITION_MAGIC) == InpMssMagic) + return true; + } + + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + const ulong ticket = OrderGetTicket(i); + if(ticket == 0 || !OrderSelect(ticket)) + continue; + if(OrderGetString(ORDER_SYMBOL) != sym) + continue; + if((ulong)OrderGetInteger(ORDER_MAGIC) != InpMssMagic) + continue; + const ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + if(t == ORDER_TYPE_BUY_LIMIT || t == ORDER_TYPE_SELL_LIMIT) + return true; + } + return false; +} + +bool IctMssEntry_CancelTicket(const ulong ticket) +{ + if(ticket == 0) + return true; + if(!OrderSelect(ticket)) + return true; + return g_ictMssTrade.OrderDelete(ticket); +} + +bool IctMssEntry_PlaceLimit(const string sym, const bool isBuy, + const double entry, const double sl, const double tp, + const double volume, ulong &ticketOut) +{ + ticketOut = 0; + g_ictMssTrade.SetExpertMagicNumber(InpMssMagic); + g_ictMssTrade.SetDeviationInPoints(20); + + const string cmt = StringFormat("ICT26_MSS %s", isBuy ? "BUY" : "SELL"); + bool ok = false; + if(InpMssPendingExpireHours > 0) + { + const datetime exp = TimeCurrent() + (datetime)(InpMssPendingExpireHours * 3600); + if(isBuy) + ok = g_ictMssTrade.BuyLimit(volume, entry, sym, sl, tp, ORDER_TIME_SPECIFIED, exp, cmt); + else + ok = g_ictMssTrade.SellLimit(volume, entry, sym, sl, tp, ORDER_TIME_SPECIFIED, exp, cmt); + } + else + { + if(isBuy) + ok = g_ictMssTrade.BuyLimit(volume, entry, sym, sl, tp, ORDER_TIME_GTC, 0, cmt); + else + ok = g_ictMssTrade.SellLimit(volume, entry, sym, sl, tp, ORDER_TIME_GTC, 0, cmt); + } + + if(ok) + ticketOut = g_ictMssTrade.ResultOrder(); + else if(InpDebug) + PrintFormat("[ICT2026/Entry] Order fail %d — %s", g_ictMssTrade.ResultRetcode(), + g_ictMssTrade.ResultRetcodeDescription()); + + return ok; +} + +void IctMssEntry_Init() +{ + g_ictMssTrade.SetExpertMagicNumber(InpMssMagic); +} + +void IctMssEntry_Update(const string sym) +{ + if(g_ictLowTf.mss.phase == ICT_MSS_IDLE && g_ictLowTf.mss.pendingTicket > 0) + { + IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); + g_ictLowTf.mss.pendingTicket = 0; + } + + if(!InpMssTradeEnabled) + { + if(g_ictLowTf.mss.phase >= ICT_MSS_M5_FVG) + IctMss_JournalEntryBlock("InpMssTradeEnabled=false"); + return; + } + + if(!g_ictIntraday.isAllowTrade || g_ictLowTf.mss.phase < ICT_MSS_M5_FVG || + g_ictLowTf.mss.m5FvgId == 0) + { + if(g_ictLowTf.mss.pendingTicket > 0) + { + IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); + g_ictLowTf.mss.pendingTicket = 0; + } + if(g_ictLowTf.mss.phase >= ICT_MSS_CHOCH) + { + if(!g_ictIntraday.isAllowTrade) + IctMss_JournalEntryBlock("AllowTrade=false"); + else if(g_ictLowTf.mss.phase < ICT_MSS_M5_FVG) + IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); + else if(g_ictLowTf.mss.m5FvgId == 0) + IctMss_JournalEntryBlock("Chưa có M5 FVG cho entry"); + } + return; + } + + if(InpMssOnePosition && IctMssEntry_HasOpenExposure(sym)) + { + if(g_ictLowTf.mss.pendingTicket > 0 && !OrderSelect(g_ictLowTf.mss.pendingTicket)) + g_ictLowTf.mss.pendingTicket = 0; + g_ictLowTf.mss.displayReason = StringFormat("%s | position/pending active", + IctMssPhaseText(g_ictLowTf.mss.phase)); + IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); + return; + } + + const int mIdx = IctConfirmFvg_FindById(g_ictLowTf.mss.m5FvgId); + if(mIdx < 0) + return; + + const IctFvgZone m5 = g_ictConfirmFvgZones[mIdx]; + double entry = 0.0, sl = 0.0, tp = 0.0; + if(!IctMssEntry_ComputeLevels(sym, m5, entry, sl, tp)) + { + g_ictLowTf.mss.displayReason = StringFormat("%s | chờ H0/L0 M5 cho SL", + IctMssPhaseText(g_ictLowTf.mss.phase)); + IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); + return; + } + + const bool isBuy = (m5.side == ICT_FVG_BULL); + const double vol = IctMssEntry_NormalizeVolume(sym, + IctMssEntry_VolumeForRisk(sym, isBuy, entry, sl)); + if(vol <= 0.0) + { + g_ictLowTf.mss.displayReason = "Lot=0 (SL quá gần hoặc risk)"; + IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); + return; + } + + const double risk = isBuy ? (entry - sl) : (sl - entry); + const double rr = (risk > _Point) ? (isBuy ? (tp - entry) : (entry - tp)) / risk : 0.0; + + const double pt = SymbolInfoDouble(sym, SYMBOL_POINT) * 2.0; + if(g_ictLowTf.mss.pendingTicket > 0 && OrderSelect(g_ictLowTf.mss.pendingTicket)) + { + if(MathAbs(g_ictLowTf.mss.pendingEntry - entry) < pt && + MathAbs(g_ictLowTf.mss.pendingSl - sl) < pt && + MathAbs(g_ictLowTf.mss.pendingTp - tp) < pt) + { + g_ictLowTf.mss.displayReason = StringFormat("Limit %.2f SL %.2f TP %.2f | RR %.1f", + entry, sl, tp, rr); + return; + } + IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); + g_ictLowTf.mss.pendingTicket = 0; + } + + ulong ticket = 0; + if(!IctMssEntry_PlaceLimit(sym, isBuy, entry, sl, tp, vol, ticket)) + { + g_ictLowTf.mss.displayReason = StringFormat("Đặt limit thất bại (%d %s)", + g_ictMssTrade.ResultRetcode(), + g_ictMssTrade.ResultRetcodeDescription()); + IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); + return; + } + + g_ictLowTf.mss.pendingTicket = ticket; + g_ictLowTf.mss.pendingEntry = entry; + g_ictLowTf.mss.pendingSl = sl; + g_ictLowTf.mss.pendingTp = tp; + g_ictLowTf.mss.displayReason = StringFormat("Sell/Buy limit %.2f | SL %.2f (H0/L0) | TP %.2f RR%.1f", + entry, sl, tp, rr); +} + +#endif diff --git a/Include/ICT2026/MssSetup.mqh b/Include/ICT2026/MssSetup.mqh new file mode 100644 index 0000000..7bcad16 --- /dev/null +++ b/Include/ICT2026/MssSetup.mqh @@ -0,0 +1,632 @@ +//+------------------------------------------------------------------+ +//| MssSetup.mqh — MSS: retest FVG H1 (POI) → CHoCH M5 → M5 FVG → entry | +//+------------------------------------------------------------------+ +#ifndef ICT2026_MSSSETUP_MQH +#define ICT2026_MSSSETUP_MQH + +#include +#include +#include + +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 "CHoCH OK"; + case ICT_MSS_M5_FVG: return "M5 FVG"; + case ICT_MSS_ENTRY_FILL: return "M5 fill 38.2%"; + 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; +} + +// H1 FVG mục tiêu: Bear → Premium cao nhất; Bull → Discount thấp nhất (chờ hồi chạm, chưa cần fill) +int IctMss_SelectTargetH1Fvg() +{ + int bestIdx = -1; + + for(int i = 0; i < g_ictFvgCount; i++) + { + if(!IctFvg_MatchesBias(g_ictFvgZones[i])) + continue; + if(!IctFvg_IsEntryRepPd(g_ictFvgZones[i])) + continue; + if(g_ictFvgZones[i].state == ICT_FVG_USED && g_ictFvgZones[i].maxFillRatio < 0.01) + continue; + + if(g_ictDailyBias.bias == ICT_BIAS_BEAR) + { + if(bestIdx < 0 || g_ictFvgZones[i].upper > g_ictFvgZones[bestIdx].upper) + bestIdx = i; + } + else if(g_ictDailyBias.bias == ICT_BIAS_BULL) + { + if(bestIdx < 0 || g_ictFvgZones[i].lower < g_ictFvgZones[bestIdx].lower) + bestIdx = i; + } + } + return bestIdx; +} + +// Retest FVG H1 (POI): giá chạm gap H1 + lấp ≥ InpMssH1MinFillPct (đo trên InpFvgTf, không phải M5) +bool IctMss_HasH1FvgRetest(const string sym, IctFvgZone &h1) +{ + IctFvg_UpdateZoneState(sym, InpFvgTf, h1); + if(h1.firstTouchTime <= 0) + return false; + if(!IctFvg_HasFillAtLeast(h1, InpMssH1MinFillPct)) + return false; + h1.mssH1Touch382 = true; + return true; +} + +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; +} + +// MSS confirm: cùng swing set M5 như chart (H0/H1/L0/L1) + body phá swing thuận bias +bool IctMss_DetectChochFromM5Swings(const string sym, const ENUM_TIMEFRAMES tf, + const datetime tTouch, const ENUM_ICT_BIAS bias, + double &keyLevelOut, datetime &keyTimeOut, + double &slSwingOut) +{ + keyLevelOut = 0.0; + keyTimeOut = 0; + slSwingOut = 0.0; + + IctSwingSet sw; + ENUM_ICT_STRUCT structural = ICT_STRUCT_NONE; + if(!IctBuildConfirmSwingSet(sym, sw, structural)) + return false; + + const int maxSh = IctMss_MaxSwingShift(sw); + + if(bias == ICT_BIAS_BEAR) + { + if(sw.hasH0 && sw.hasL0 && sw.h0.time >= tTouch && sw.l0.time >= sw.h0.time) + { + if(IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.l0.price, true)) + { + keyLevelOut = sw.l0.price; + keyTimeOut = sw.l0.time; + slSwingOut = sw.h0.price; + return true; + } + } + + if(sw.IsComplete() && structural == ICT_STRUCT_BULL && sw.l1.time >= tTouch) + { + const ENUM_ICT_MS_EVENT ev = + IctDetectStructureEvent(sym, tf, structural, sw.h1.price, sw.l1.price, 1); + if(ev == ICT_MS_CHOCH || + IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.l1.price, true)) + { + keyLevelOut = sw.l1.price; + keyTimeOut = sw.l1.time; + slSwingOut = sw.hasH0 ? sw.h0.price : sw.h1.price; + return true; + } + } + + if(sw.IsComplete() && structural == ICT_STRUCT_BEAR && sw.l1.time >= tTouch) + { + if(IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.l1.price, true)) + { + keyLevelOut = sw.l1.price; + keyTimeOut = sw.l1.time; + slSwingOut = sw.hasH0 ? sw.h0.price : sw.h1.price; + return true; + } + } + } + + if(bias == ICT_BIAS_BULL) + { + if(sw.hasL0 && sw.hasH0 && sw.l0.time >= tTouch && sw.h0.time >= sw.l0.time) + { + if(IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.h0.price, false)) + { + keyLevelOut = sw.h0.price; + keyTimeOut = sw.h0.time; + slSwingOut = sw.l0.price; + return true; + } + } + + if(sw.IsComplete() && structural == ICT_STRUCT_BEAR && sw.h1.time >= tTouch) + { + const ENUM_ICT_MS_EVENT ev = + IctDetectStructureEvent(sym, tf, structural, sw.h1.price, sw.l1.price, 1); + if(ev == ICT_MS_CHOCH || + IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.h1.price, false)) + { + keyLevelOut = sw.h1.price; + keyTimeOut = sw.h1.time; + slSwingOut = sw.hasL0 ? sw.l0.price : sw.l1.price; + return true; + } + } + + if(sw.IsComplete() && structural == ICT_STRUCT_BULL && sw.h1.time >= tTouch) + { + if(IctMss_BodyBrokeLevelSince(sym, tf, maxSh, sw.h1.price, false)) + { + keyLevelOut = sw.h1.price; + keyTimeOut = sw.h1.time; + slSwingOut = sw.hasL0 ? sw.l0.price : sw.l1.price; + return true; + } + } + } + + return false; +} + +// Sau retest FVG H1: MSS = phá swing M5 (confirm TF) +bool IctMss_DetectChochAfterH1FvgRetest(const string sym, const ENUM_TIMEFRAMES tf, + const IctFvgZone &h1, const ENUM_ICT_BIAS bias, + double &keyLevelOut, datetime &keyTimeOut, + double &slSwingOut) +{ + const datetime tTouch = h1.firstTouchTime; + if(tTouch <= 0) + return false; + + return IctMss_DetectChochFromM5Swings(sym, tf, tTouch, bias, + keyLevelOut, keyTimeOut, slSwingOut); +} + +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) + { + swingOut = sw.h0.price; + return true; + } + if(bias == ICT_BIAS_BULL && sw.hasL0) + { + swingOut = sw.l0.price; + 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) + { + swingOut = highs[nH - 1].price; + return true; + } + if(bias == ICT_BIAS_BULL && nL > 0) + { + swingOut = lows[nL - 1].price; + 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_ResetState() +{ + g_ictLowTf.mss.Clear(); + IctMss_JournalReset(); +} + +void IctMss_Update(const string sym) +{ + g_ictLowTf.mss.displayReason = IctMssPhaseText(g_ictLowTf.mss.phase); + + if(!g_ictIntraday.isAllowTrade) + { + if(g_ictLowTf.mss.phase != ICT_MSS_IDLE) + IctMss_ResetState(); + g_ictLowTf.mss.displayReason = "AllowTrade=false"; + return; + } + + const ENUM_TIMEFRAMES cTf = InpConfirmTf; + const ENUM_ICT_FVG_SIDE wantSide = IctFvgSideFromBias(g_ictDailyBias.bias); + if(wantSide == ICT_FVG_NONE) + { + IctMss_ResetState(); + g_ictLowTf.mss.displayReason = "Bias none"; + return; + } + + if(g_ictLowTf.mss.phase == ICT_MSS_IDLE || g_ictLowTf.mss.h1FvgId == 0) + { + const int hIdx = IctMss_SelectTargetH1Fvg(); + if(hIdx < 0) + { + g_ictLowTf.mss.displayReason = StringFormat("Chờ H1 %s FVG", + (wantSide == ICT_FVG_BEAR) ? "Premium" : "Discount"); + return; + } + + g_ictLowTf.mss.h1FvgId = g_ictFvgZones[hIdx].id; + + if(!IctMss_HasH1FvgRetest(sym, g_ictFvgZones[hIdx])) + { + g_ictLowTf.mss.displayReason = StringFormat( + "Chờ retest FVG H1 %s [%.0f–%.0f] (%.0f%% lấp trên H1)", + IctPdZoneText(IctFvg_GetRepPd(g_ictFvgZones[hIdx])), + g_ictFvgZones[hIdx].lower, g_ictFvgZones[hIdx].upper, + InpMssH1MinFillPct); + return; + } + + g_ictLowTf.mss.phase = ICT_MSS_H1_TOUCH; + g_ictLowTf.mss.h1TouchTime = g_ictFvgZones[hIdx].firstTouchTime; + g_ictLowTf.mss.displayReason = StringFormat("Retest FVG H1 OK | lấp %.0f%% (H1)", + g_ictFvgZones[hIdx].maxFillRatio * 100.0); + } + + 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_ictLowTf.mss.phase >= ICT_MSS_H1_TOUCH && + !IctMss_HasH1FvgRetest(sym, g_ictFvgZones[h1Idx])) + { + IctMss_ResetState(); + g_ictLowTf.mss.displayReason = "FVG H1 chưa retest đủ % — reset MSS"; + return; + } + + if(g_ictLowTf.mss.phase >= ICT_MSS_H1_TOUCH && g_ictLowTf.mss.h1TouchTime <= 0) + g_ictLowTf.mss.h1TouchTime = g_ictFvgZones[h1Idx].firstTouchTime; + + if(g_ictLowTf.mss.phase == ICT_MSS_H1_TOUCH) + { + double keyLv = 0.0; + datetime keyT = 0; + double slSwing = 0.0; + if(!IctMss_DetectChochAfterH1FvgRetest(sym, cTf, g_ictFvgZones[h1Idx], + g_ictDailyBias.bias, keyLv, keyT, slSwing)) + { + g_ictLowTf.mss.displayReason = (g_ictDailyBias.bias == ICT_BIAS_BEAR) ? + "Retest FVG H1 OK — chờ MSS↓ phá L0 M5" : + "Retest FVG H1 OK — chờ MSS↑ phá H0 M5"; + return; + } + + g_ictLowTf.mss.chochKeyLevel = keyLv; + g_ictLowTf.mss.chochKeyTime = keyT; + g_ictLowTf.mss.slSwingPrice = slSwing; + + datetime chochT = iTime(sym, cTf, 1); + if(chochT == 0) + chochT = keyT; + + if(!IctMss_IsChochNearH1Fvg(sym, g_ictFvgZones[h1Idx], g_ictDailyBias.bias, + keyLv, slSwing, chochT)) + { + g_ictLowTf.mss.displayReason = "CHoCH xa vùng gần H1 FVG — chờ MSS gần hơn"; + return; + } + + g_ictLowTf.mss.phase = ICT_MSS_CHOCH; + g_ictLowTf.mss.chochKeyLevel = keyLv; + g_ictLowTf.mss.chochKeyTime = keyT; + g_ictLowTf.mss.slSwingPrice = slSwing; + double mssH0 = 0.0; + if(IctMss_GetConfirmMssSwing(sym, g_ictDailyBias.bias, mssH0)) + g_ictLowTf.mss.slSwingPrice = mssH0; + g_ictLowTf.mss.chochTime = chochT; + + IctConfirmFvg_ScanNew(sym, cTf, wantSide, g_ictLowTf.mss.chochTime, true); + g_ictLowTf.mss.displayReason = StringFormat("CHoCH @ %.2f — quét M5 FVG", keyLv); + } + + 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 = "CHoCH 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 diff --git a/Include/ICT2026/Panel.mqh b/Include/ICT2026/Panel.mqh index f3ebb7f..8a2a8cf 100644 --- a/Include/ICT2026/Panel.mqh +++ b/Include/ICT2026/Panel.mqh @@ -12,7 +12,7 @@ const string ICT26_PANEL_PFX = "ICT26_PNL_"; const string ICT26_PANEL_LEGACY = "ICT26_BIAS_PANEL"; -const int ICT26_PANEL_MAXLINE = 12; +const int ICT26_PANEL_MAXLINE = 14; const color ICT_PANEL_CLR_UP = clrLime; const color ICT_PANEL_CLR_DOWN = clrOrangeRed; @@ -162,6 +162,12 @@ void IctPanel_Render(const string sym) lines[n] = StringFormat("iTF FVG: %s", g_ictLowTf.displayReason); colors[n++] = (g_ictLowTf.availableCount > 0) ? ICT_PANEL_CLR_ALLOW : ICT_PANEL_CLR_MUTED; + ArrayResize(lines, n + 1); + ArrayResize(colors, n + 1); + lines[n] = StringFormat("MSS: %s", g_ictLowTf.mss.displayReason); + colors[n++] = (g_ictLowTf.mss.phase >= ICT_MSS_READY) ? ICT_PANEL_CLR_ALLOW : + (g_ictLowTf.mss.phase > ICT_MSS_IDLE) ? clrGold : ICT_PANEL_CLR_MUTED; + for(int i = 0; i < n; i++) IctPanel_SetLine(ch, i, i * lh, lines[i], colors[i]); diff --git a/Include/ICT2026/README.md b/Include/ICT2026/README.md index df42a94..956ff19 100644 --- a/Include/ICT2026/README.md +++ b/Include/ICT2026/README.md @@ -193,7 +193,7 @@ Chỉ **quét / giữ** FVG thuận Daily Bias. **Vẽ FVG** (ngang): | Bear FVG | Bull FVG | |----------|----------| -| Swing 1: đỉnh **gần nhất theo thời gian** trên FVG (snapshot lúc tạo FVG) → `pdHigh` | Swing 1: đáy gần nhất dưới FVG → `pdLow` | +| Swing 1: pivot **gần FVG nhất** (bar shift trước nến C), giá > `upper` → `pdHigh` | Swing 1: pivot gần FVG, giá < `lower` → `pdLow` | | Swing 2: **đáy đầu tiên** sau FVG khi pivot xác nhận (`pdLow`) | Swing 2: **đỉnh đầu tiên** sau FVG (`pdHigh`) | **Vẽ P/D (ngang):** @@ -208,6 +208,44 @@ Swing 1 (`pdSwing1Set`) gán **một lần** khi FVG mới; swing 2 xác nhận FVG Used **không** kéo dài P/D thêm; P/D độc lập với Used (theo swing 2). +**`repPd`**: overlap Premium/Discount (không chỉ midpoint FVG). Bear thỏa MSS nếu **một phần FVG** nằm trên `pdEq` (Premium); fill 38.2% luôn tính từ **`lower`→`upper`** của FVG. + +### Retest FVG H1 (POI) — không nhầm với retest swing H1 + +**Retest FVG H1** = giá **hồi vào gap Fair Value Gap trên H1** (vùng POI đã chọn), đo trên **`InpFvgTf` (H1)**: + +| | | +|--|--| +| **Chạm** | `firstTouchTime` — nến H1 đầu tiên chạm vùng `[lower … upper]` | +| **Lấp %** | Từ cạnh FVG (`lower`→`upper` bear) — ≥ `InpMssH1MinFillPct` (mặc định 38.2%) | +| **Không phải** | Retest swing/pivot H1; retest trên M5; retest đường Premium/Discount riêng | + +Sau retest FVG H1 OK → mới arm MSS trên **M5** (phá swing confirm). + +### MSS entry (Confirm TF, v1.115) + +Khi `IsAllowTrade`, state machine `g_ictLowTf.mss`: + +| Phase | Điều kiện | +|-------|-----------| +| `H1_TOUCH` | **Retest FVG H1** OK: bias + `repPd` + lấp ≥ `InpMssH1MinFillPct` trên H1 | +| `CHOCH` | Sau retest FVG H1: **body phá swing M5** (H0/L0/L1/H1 — cùng build như label chart) | +| `M5_FVG` | FVG `InpConfirmTf` cùng hướng bias, sau thời điểm CHoCH | +| `ENTRY_FILL` / `READY` | Giá hồi lấp ≥ `InpMssEntryFillPct` vào M5 FVG đó | + +Sweep liquidity: **chưa** (phase sau). Module: `MssSetup.mqh`, pool `ConfirmFvg.mqh`. + +**Lệnh limit (v1.116)** — khi có M5 FVG (`MssEntry.mqh`): + +| | Bull | Bear | +|--|------|------| +| Limit | Buy @ `upper` M5 FVG | Sell @ `lower` M5 FVG | +| SL | Dưới `L0` M5 − buffer | Trên `H0` M5 + buffer | +| TP | Entry + `InpMssMinRR`×R | Entry − `InpMssMinRR`×R | +| Size | `InpMssRiskPct` % balance | | + +`InpMssTradeEnabled`, `InpMssMagic`, `InpMssOnePosition`. + ### IsAllowTrade Mục tiêu: **phát hiện sớm đảo chiều H1** khi CHoCH — không cần đợi HH-HL/LH-LL rõ. @@ -489,6 +527,79 @@ ENUM_ICT_BIAS ICT2026_GetDailyBias(); ## Changelog +### v1.126 — Thuật ngữ: Retest FVG H1 (POI), không ghi chung “H1 retest” + +- Panel/journal: “Retest FVG H1”, “Chờ retest FVG H1 … (lấp trên H1)” +- `IctMss_HasH1FvgRetest`, `IctMss_DetectChochAfterH1FvgRetest` + +### v1.125 — MSS = phá swing M5 (confirm), build structure như chart + +- `IctBuildConfirmSwingSet`: H0/H1/L0/L1 M5 dùng chung cho vẽ + MSS +- MSS/CHoCH: chỉ xét body phá L0/L1/H0/H1 sau H1 touch (bỏ pivot trong FVG) +- Bear: phá L0 sau H0, hoặc CHoCH phá L1 (bull→bear), hoặc BOS phá L1 (bear) + +### v1.124 — Sửa detect CHoCH↓ sau H1 retest (L0 M5) + +- CHoCH: ưu tiên `H0/L0` M5 (`BuildSwingSetRecentPivots`) + body phá trong lookback (không chỉ bar 1) +- Key level: swing **sau** đỉnh phản ứng (`IctFindMostRecentNewer`), fallback swing cũ hơn +- Bỏ `keyLo.time < tTouch` (gây miss khi L0 hình thành trước touch H1 trên timeline) + +### v1.123 — Vẽ MSS sau H1 touch + journal chặn lệnh + +- `MssDraw`: từ `H1_TOUCH` — vạch touch, mức 38.2% H1, CHoCH/H0 (kể cả khi pipeline chưa advance) +- `Journal.mqh` + `InpMssLogJournal`: `[ICT2026/MSS]` pipeline, `[ICT2026/Entry] Chặn lệnh:` khi không đặt được limit + +### v1.122 — MSS gần H1 FVG (không siết chặt) + +- Vùng gần: buffer % gap + ATR; bear thêm `InpMssExtraBelowGapPct` dưới `lower` (CHoCH/M5 FVG ngay dưới FVG vẫn hợp lệ) +- CHoCH OK nếu **key level** hoặc **H0** nằm trong vùng gần (không bắt buộc trong FVG) +- `InpMssMaxM5BarsAfterTouch=0` mặc định = không giới hạn thời gian + +### v1.121 — FVG qualify: overlap Premium/Discount; fill từ cạnh FVG + +- `IctFvg_OverlapsPremium` / `OverlapsDiscount`: bear MSS khi FVG chạm vùng Premium (kể cả nửa dưới trong Discount) +- `maxFillRatio`: % lấp theo chiều cao FVG (`lower`→`upper` bear), không theo pdEq + +### v1.120 — MSS đúng thứ tự: retest H1 Premium/Discount → CHoCH thuận bias + +- Target H1: Premium **cao nhất** (bear) / Discount **thấp nhất** (bull) — không nhảy CHoCH khi chưa retest +- CHoCH: đỉnh phản ứng **trong** H1 FVG sau retest → phá key **down** (bear) / **up** (bull) +- Vẽ: vàng **CHoCH↓** = đáy key; nét đứt **H0** = đỉnh MSS (SL) + +### v1.119 — Vẽ CHoCH + H0/L0 MSS trên chart (`MssDraw.mqh`) + +- Vàng: đường **CHoCH** (key level) | Vàng nét đứt: **H0/L0** swing MSS +- Aqua/đỏ/xanh: entry / SL / TP khi có pending +- `InpDrawMssChoch` + +### v1.118 — MSS phải gần / trong H1 FVG + +- CHoCH/M5 FVG **gần** H1 FVG: ± buffer + mở rộng phía dưới (bear) / trên (bull); key CHoCH hoặc H0 đều được tính +- Thời gian: ≤ `InpMssMaxM5BarsAfterTouch` M5 bar sau lần chạm H1 FVG +- M5 FVG phải overlap vùng H1 FVG mở rộng + +### v1.117 — MSS entry: SL = H0/L0 M5, TP min 2R + +- Sell limit @ **lower** M5 FVG | Buy limit @ **upper** +- SL: trên **H0** M5 (bear) / dưới **L0** M5 (bull) + `InpMssSlAtrMult`×ATR +- TP: tối thiểu `InpMssMinRR` (mặc định 2R), không phụ thuộc swing H1 + +### v1.116 — MSS limit entry: FVG edge, SL swing CHoCH, TP trước iH0/iL0 + +### v1.115 — MSS Confirm TF (H1 touch → CHoCH → M5 FVG → fill 38.2%) + +- `repPd` / MSS H1: overlap Premium (bear) hoặc Discount (bull); fill % theo biên FVG +- `MssSetup.mqh` + pool `ConfirmFvg` (M5); sweep tạm bỏ +- Panel dòng MSS; vẽ CFVG trên chart + +### v1.114 — FVG: bỏ label text, màu bull xanh / bear đỏ / used xám + +### v1.113 — PD swing 1: pivot gần FVG (shift), bỏ wick nến + +- `IctFindNearestSwingHighBefore` / `LowBefore`: cùng logic `IctFindMostRecentOlder` neo theo `iBarShift(createdTime)` +- Không fallback `iHigh`/`iLow` từng nến (gây đỉnh PD cao hơn swing vàng trên chart) +- Swing 2: `IctFindFirstSwing*AfterShift` — đáy/đỉnh đầu tiên sau nến FVG + ### v1.112 — PD lock: không cập nhật đỉnh sau pdComplete - `pdSwing1Set`: đỉnh/đáy swing 1 snapshot một lần diff --git a/Include/ICT2026/Swing.mqh b/Include/ICT2026/Swing.mqh index b249547..4ff1a2d 100644 --- a/Include/ICT2026/Swing.mqh +++ b/Include/ICT2026/Swing.mqh @@ -102,6 +102,104 @@ IctSwingPoint IctFindMostRecentOlder(IctSwingPoint &pts[], const int count, return pts[best]; } +// Pivot mới hơn anchor (shift nhỏ hơn = gần hiện tại hơn) +IctSwingPoint IctFindMostRecentNewer(IctSwingPoint &pts[], const int count, + const int anchorShift) +{ + IctSwingPoint empty; + empty.Clear(); + int best = -1; + for(int i = 0; i < count; i++) + { + if(pts[i].shift >= anchorShift) + continue; + if(best < 0 || pts[i].shift < pts[best].shift) + best = i; + } + if(best < 0) + return empty; + return pts[best]; +} + +// Pivot swing gần anchor nhất (shift nhỏ nhất trong các bar cũ hơn anchor) +IctSwingPoint IctFindNearestSwingHighBefore(IctSwingPoint &pts[], const int count, + const int anchorShift, const double refUpper) +{ + IctSwingPoint empty; + empty.Clear(); + int best = -1; + const double minP = refUpper + _Point; + for(int i = 0; i < count; i++) + { + if(pts[i].shift <= anchorShift) + continue; + if(pts[i].price <= minP) + continue; + if(best < 0 || pts[i].shift < pts[best].shift) + best = i; + } + if(best < 0) + return empty; + return pts[best]; +} + +IctSwingPoint IctFindNearestSwingLowBefore(IctSwingPoint &pts[], const int count, + const int anchorShift, const double refLower) +{ + IctSwingPoint empty; + empty.Clear(); + int best = -1; + const double maxP = refLower - _Point; + for(int i = 0; i < count; i++) + { + if(pts[i].shift <= anchorShift) + continue; + if(pts[i].price >= maxP) + continue; + if(best < 0 || pts[i].shift < pts[best].shift) + best = i; + } + if(best < 0) + return empty; + return pts[best]; +} + +IctSwingPoint IctFindFirstSwingLowAfterShift(IctSwingPoint &pts[], const int count, + const int anchorShift) +{ + IctSwingPoint empty; + empty.Clear(); + int best = -1; + for(int i = 0; i < count; i++) + { + if(pts[i].shift >= anchorShift) + continue; + if(best < 0 || pts[i].shift > pts[best].shift) + best = i; + } + if(best < 0) + return empty; + return pts[best]; +} + +IctSwingPoint IctFindFirstSwingHighAfterShift(IctSwingPoint &pts[], const int count, + const int anchorShift) +{ + IctSwingPoint empty; + empty.Clear(); + int best = -1; + for(int i = 0; i < count; i++) + { + if(pts[i].shift >= anchorShift) + continue; + if(best < 0 || pts[i].shift > pts[best].shift) + best = i; + } + if(best < 0) + return empty; + return pts[best]; +} + IctSwingSet IctPickStructuralBear(IctSwingPoint &highs[], const int nH, IctSwingPoint &lows[], const int nL) { @@ -308,6 +406,19 @@ bool IctBuildSwingSetRecentPivots(const string sym, const ENUM_TIMEFRAMES tf, return out.IsComplete(); } +// M5 confirm: cùng pivot/H0–L1 như label chart (InpConfirmTf) +bool IctBuildConfirmSwingSet(const string sym, IctSwingSet &sw, + ENUM_ICT_STRUCT &structuralOut) +{ + sw.Clear(); + structuralOut = ICT_STRUCT_NONE; + return IctBuildSwingSetRecentPivots(sym, InpConfirmTf, + InpConfirmSwingRange, + InpConfirmSwingLookback, + InpConfirmRecentBars, + sw, structuralOut); +} + void IctGetKeyLevels(const ENUM_ICT_STRUCT bias, IctSwingSet &sw, double &keyLv1, double &keyLv2) { diff --git a/Include/ICT2026/Types.mqh b/Include/ICT2026/Types.mqh index 26efbdd..fe254fc 100644 --- a/Include/ICT2026/Types.mqh +++ b/Include/ICT2026/Types.mqh @@ -148,12 +148,23 @@ enum ENUM_ICT_PD_ZONE ICT_PD_EQUILIBRIUM = 2 }; +enum ENUM_ICT_MSS_PHASE +{ + ICT_MSS_IDLE = 0, + ICT_MSS_H1_TOUCH = 1, // Đã retest FVG H1 (POI): chạm + lấp ≥ InpMssH1MinFillPct trên H1 + ICT_MSS_CHOCH = 2, + ICT_MSS_M5_FVG = 3, + ICT_MSS_ENTRY_FILL = 4, + ICT_MSS_READY = 5 +}; + struct IctFvgZone { ulong id; ENUM_ICT_FVG_SIDE side; ENUM_ICT_FVG_STATE state; ENUM_ICT_PD_ZONE pdZone; + ENUM_ICT_PD_ZONE repPd; double upper; double lower; double pdHigh; @@ -171,6 +182,7 @@ struct IctFvgZone datetime pdRangeEnd; bool pdComplete; bool pdSwing1Set; + bool mssH1Touch382; void Clear() { @@ -178,6 +190,7 @@ struct IctFvgZone side = ICT_FVG_NONE; state = ICT_FVG_AVAILABLE; pdZone = ICT_PD_NONE; + repPd = ICT_PD_NONE; upper = 0.0; lower = 0.0; pdHigh = 0.0; @@ -195,15 +208,55 @@ struct IctFvgZone pdRangeEnd = 0; pdComplete = false; pdSwing1Set = false; + mssH1Touch382 = false; + } +}; + +struct IctMssState +{ + ENUM_ICT_MSS_PHASE phase; + ulong h1FvgId; + datetime h1TouchTime; + ulong m5FvgId; + double chochKeyLevel; + datetime chochKeyTime; + datetime chochTime; + datetime m5FvgTime; + double slSwingPrice; + ulong pendingTicket; + double pendingEntry; + double pendingSl; + double pendingTp; + string displayReason; + + void Clear() + { + phase = ICT_MSS_IDLE; + h1FvgId = 0; + h1TouchTime = 0; + m5FvgId = 0; + chochKeyLevel = 0.0; + chochKeyTime = 0; + chochTime = 0; + m5FvgTime = 0; + slSwingPrice = 0.0; + pendingTicket = 0; + pendingEntry = 0.0; + pendingSl = 0.0; + pendingTp = 0.0; + displayReason = ""; } }; struct IctLowTfState { datetime lastBarTime; + datetime lastConfirmBarTime; int activeCount; int availableCount; + int confirmFvgCount; string displayReason; + IctMssState mss; }; #endif