//+------------------------------------------------------------------+ //| ICT EA – FVG Edition (MQL5) v4.0 | //| Architecture : BiasTF(D1) + MiddleTF(H1) + TriggerTF(M5) | //| State Machine: IDLE → WAIT_TOUCH → WAIT_TRIGGER → IN_TRADE | //| | //| Flow tổng thể: | //| 1. D1 bias (UP/DOWN) đồng thuận H1 trend | //| 2. H1 FVG thuận xu hướng → chờ price retrace vào FVG | //| 3. Giá touch H1 FVG → chờ M5 MSS xác nhận đảo chiều | //| 4. M5 MSS = keyLevel sóng hồi bị phá: | //| - H1 UP: M5 đang downtrend (hồi), tH0 bị phá (close>tH0) | //| - H1 DOWN: M5 đang uptrend (hồi), tL0 bị phá (close tH0) = bull MSS // - H1 DOWN → M5 đang UP (hồi) → tL0 bị phá (close < tL0) = bear MSS // // Entry = giá keyLevel vừa bị phá (đặt limit chờ giá hồi) // SL = swing extreme sóng hồi (tL0 cho buy, tH0 cho sell) // Cả 2 được capture TRƯỚC khi re-scan swing. //---------------------------------------------------------------------- struct TFTrendContext { // ── Swing structure ──────────────────────────────────────────── MarketDir trend; // DIR_UP / DIR_DOWN / DIR_NONE double h0, h1, l0, l1; // 2 swing highs + 2 swing lows gần nhất int idxH0, idxH1, idxL0, idxL1; // Bar indices tương ứng double keyLevel; // L0 (uptrend) hoặc H0 (downtrend) datetime lastBarTime; // Guard: chỉ recalc trên bar mới // ── MSS tracking ─────────────────────────────────────────────── datetime lastMssTime; // Open time cây nến phá keyLevel double lastMssLevel; // KeyLevel bị phá (= giá entry) MarketDir lastMssBreak; // DIR_UP = bull MSS, DIR_DOWN = bear MSS double mssSLSwing; // v4: swing SL tại thời điểm MSS // (L0 cho bull, H0 cho bear) }; struct FVGRecord { int id; // Unique FVG ID, auto-increment FVGStatus status; // PENDING → TOUCHED → USED lifecycle int usedCase; // 0=expired 1=broken(H1 close phá) 2=triggered(MSS) MarketDir direction; // DIR_UP=bullish gap, DIR_DOWN=bearish gap double high, low, mid; // Biên trên, biên dưới, midpoint của gap datetime createdTime; // Thời gian cây nến bên phải gap (i-1) datetime touchTime; // Thời gian bid đầu tiên vào vùng gap datetime usedTime; // Thời gian FVG bị consumed (broken/expired/triggered) MarketDir triggerTrendAtTouch; // M5 trend tại thời điểm touch (dùng cho case2 check) }; struct OrderPlan { bool valid; // true khi đã build thành công, sẵn sàng gửi int direction; // +1 = BUY LIMIT, -1 = SELL LIMIT double entry, stopLoss, takeProfit, lot; // Giá entry, SL, TP, lot size int parentFVGId; // ID FVG gốc → ghi vào comment lệnh }; struct DailyRiskContext { double startBalance, currentBalance; // Balance đầu ngày vs hiện tại datetime dayStartTime; // Open time D1 bar hiện tại bool limitHit; // true → dừng trade hết ngày }; //==================================================== // GLOBALS //==================================================== EAState g_State = EA_IDLE; // State machine hiện tại BlockReason g_BlockReason = BLOCK_NONE; // Lý do bị block (nếu có) BiasContext g_Bias; // D1 bias context TFTrendContext g_MiddleTrend; // H1 trend + swing + MSS TFTrendContext g_TriggerTrend; // M5 trend + swing + MSS (entry trigger) DailyRiskContext g_DailyRisk; // Daily drawdown tracking FVGRecord g_FVGPool[MAX_FVG_POOL]; // Pool chứa tối đa 30 FVG records int g_FVGCount = 0; // Số FVG hiện có trong pool int g_NextFVGId = 0; // ID tiếp theo cho FVG mới int g_ActiveFVGIdx = -1; // Index FVG đang theo dõi (-1 = không có) int g_TradeBarIndex = -1; // Bar index lúc đặt lệnh (dùng cho timeout) OrderPlan g_OrderPlan; // Kế hoạch lệnh hiện tại ulong g_PendingTicket = 0; // Ticket pending order (0 = không có) // Chart object prefixes const string SW_PREFIX = "SW_"; // MiddleTF swing arrows/labels const string TS_PREFIX = "TS_"; // TriggerTF swing arrows/labels const string MSS_PREFIX = "MSS_"; // MSS markers const string FVGP_PREFIX = "FVGP_"; // MiddleTF FVG rectangles const string ORD_PREFIX = "ORD_"; // Order visualization (TradingView-style) const string DBG_PREFIX = "DBG_"; // Debug panel labels //+------------------------------------------------------------------+ //| MQL5 HELPER: iBarShift replacement | //+------------------------------------------------------------------+ int MyBarShift(string symbol, ENUM_TIMEFRAMES tf, datetime time, bool exact = false) { datetime arr[]; // Mảng chứa open time tất cả bars int maxCopy = MathMin(Bars(symbol, tf), 5000); // Giới hạn 5000 bars tránh quá tải bộ nhớ int copied = CopyTime(symbol, tf, 0, maxCopy, arr); // Copy toàn bộ bar times vào arr[] if (copied <= 0) return -1; // Không có data → trả -1 for (int i = copied - 1; i >= 0; i--) // Duyệt từ mới nhất → cũ nhất { if (arr[i] <= time) // Tìm bar đầu tiên có time <= target return copied - 1 - i; // Convert array index → bar shift (0=newest) } return exact ? -1 : copied - 1; // exact=true: ko tìm thấy→-1, false→bar cũ nhất } //+------------------------------------------------------------------+ //| SECTION 1 – SWING HELPERS | //+------------------------------------------------------------------+ bool IsSwingHighAt(ENUM_TIMEFRAMES tf, int i) { double p = iHigh(_Symbol, tf, i); // Giá high tại bar i (ứng viên swing) for (int k = 1; k <= InpSwingRange; k++) // Kiểm tra InpSwingRange bars 2 bên if (iHigh(_Symbol, tf, i-k) >= p || iHigh(_Symbol, tf, i+k) >= p) return false; // Có bar cao hơn → không phải swing return true; // Bar i là đỉnh cao nhất trong vùng } bool IsSwingLowAt(ENUM_TIMEFRAMES tf, int i) { double p = iLow(_Symbol, tf, i); // Giá low tại bar i (ứng viên swing) for (int k = 1; k <= InpSwingRange; k++) // Kiểm tra InpSwingRange bars 2 bên if (iLow(_Symbol, tf, i-k) <= p || iLow(_Symbol, tf, i+k) <= p) return false; // Có bar thấp hơn → không phải swing return true; // Bar i là đáy thấp nhất trong vùng } bool ScanSwingStructure( ENUM_TIMEFRAMES tf, int lookback, double &h0, double &h1, int &idxH0, int &idxH1, double &l0, double &l1, int &idxL0, int &idxL1) { int maxBar = MathMin(lookback, Bars(_Symbol, tf) - InpSwingRange - 2); // Giới hạn scan range double highs[2]; int hiIdx[2]; int hc = 0; // 2 swing highs gần nhất (h0=mới, h1=cũ) double lows [2]; int loIdx[2]; int lc = 0; // 2 swing lows gần nhất (l0=mới, l1=cũ) for (int i = InpSwingRange + 1; i <= maxBar; i++) // Bắt đầu từ bar đủ xa để có range 2 bên { if (hc < 2 && IsSwingHighAt(tf, i)) { highs[hc] = iHigh(_Symbol, tf, i); hiIdx[hc] = i; hc++; } // Thu thập swing high if (lc < 2 && IsSwingLowAt (tf, i)) { lows [lc] = iLow (_Symbol, tf, i); loIdx[lc] = i; lc++; } // Thu thập swing low if (hc == 2 && lc == 2) break; // Đủ 2 cặp → dừng scan } if (hc < 2 || lc < 2) return false; // Thiếu swing → không xác định được trend h0 = highs[0]; idxH0 = hiIdx[0]; // Swing high gần nhất (most recent) h1 = highs[1]; idxH1 = hiIdx[1]; // Swing high trước đó (để so sánh HH/LH) l0 = lows [0]; idxL0 = loIdx[0]; // Swing low gần nhất l1 = lows [1]; idxL1 = loIdx[1]; // Swing low trước đó (để so sánh HL/LL) return true; } void ResolveTrendFromSwings( ENUM_TIMEFRAMES tf, double h0, double h1, double l0, double l1, MarketDir &trend, double &keyLevel) { double c1 = iClose(_Symbol, tf, 1); if (h0 > h1 && l0 > l1 && c1 > l0) { trend = DIR_UP; keyLevel = l0; } // HH+HL → uptrend, KL=L0 else if (h0 < h1 && l0 < l1 && c1 < h0) { trend = DIR_DOWN; keyLevel = h0; } // LH+LL → downtrend, KL=H0 else { trend = DIR_NONE; keyLevel = 0; } } //+------------------------------------------------------------------+ //| SECTION 2 – CONTEXT UPDATERS | //+------------------------------------------------------------------+ HTFBias ResolveBias(double b1H, double b1L, double b1C, double b2H, double b2L) { if (b1C > b2H) return BIAS_UP; // Close trên high hôm qua if (b1C < b2L) return BIAS_DOWN; // Close dưới low hôm qua if (b1H > b2H && b1C < b2H) return BIAS_DOWN; // False break up → bearish if (b1L < b2L && b1C > b2L) return BIAS_UP; // False break down → bullish return BIAS_SIDEWAY; } void UpdateBiasContext() { datetime t0 = iTime(_Symbol, InpBiasTF, 0); // Open time bar D1 hiện tại if (t0 == g_Bias.lastBarTime) return; // Đã tính rồi → skip (chỉ tính 1 lần/ngày) g_Bias.lastBarTime = t0; // Đánh dấu đã xử lý bar này if (Bars(_Symbol, InpBiasTF) < 4) { g_Bias.bias = BIAS_NONE; return; } // Chưa đủ data double b1H = iHigh (_Symbol, InpBiasTF, 1); // D1 bar hôm qua: high double b1L = iLow (_Symbol, InpBiasTF, 1); // low double b1C = iClose(_Symbol, InpBiasTF, 1); // close double b2H = iHigh (_Symbol, InpBiasTF, 2); // D1 bar hôm kia: high (reference range) double b2L = iLow (_Symbol, InpBiasTF, 2); // low (reference range) HTFBias prev = g_Bias.bias; // Lưu bias cũ để log khi thay đổi g_Bias.bias = ResolveBias(b1H, b1L, b1C, b2H, b2L); // So sánh b1 close vs b2 range g_Bias.rangeHigh = (g_Bias.bias == BIAS_SIDEWAY) ? b2H : 0; // SIDEWAY: lưu biên trên g_Bias.rangeLow = (g_Bias.bias == BIAS_SIDEWAY) ? b2L : 0; // SIDEWAY: lưu biên dưới if (InpDebugLog && g_Bias.bias != prev) PrintFormat("[BIAS] %s → %s | b1[H=%.5f L=%.5f C=%.5f] b2[H=%.5f L=%.5f]", EnumToString(prev), EnumToString(g_Bias.bias), b1H, b1L, b1C, b2H, b2L); } //---------------------------------------------------------------------- // UpdateTFTrendContext // // Gọi mỗi bar mới. Thực hiện TRƯỚC khi re-scan swing: // // v4 MSS check (TriggerTF only): // - M5 đang downtrend (hồi khi H1 UP): keyLevel = tH0 // → close > tH0 = bull MSS → entry BUY LIMIT tại tH0 // - M5 đang uptrend (hồi khi H1 DOWN): keyLevel = tL0 // → close < tL0 = bear MSS → entry SELL LIMIT tại tL0 // // Chỉ nhận MSS thuận chiều MiddleTF (filter duy nhất). // Entry price = keyLevel bị phá (tH0 hoặc tL0). // SL = swing extreme sóng hồi (tL0 cho buy, tH0 cho sell). // Cả entry + SL capture TRƯỚC re-scan vì sau đó swing thay đổi. //---------------------------------------------------------------------- void UpdateTFTrendContext(ENUM_TIMEFRAMES tf, int lookback, TFTrendContext &ctx) { datetime t0 = iTime(_Symbol, tf, 0); if (t0 == ctx.lastBarTime) return; // Chỉ chạy 1 lần/bar ctx.lastBarTime = t0; double bar1C = iClose(_Symbol, tf, 1); // Bar vừa đóng: close double bar1H = iHigh (_Symbol, tf, 1); // high double bar1L = iLow (_Symbol, tf, 1); // low datetime bar1T = iTime (_Symbol, tf, 1); // time // ══════════════════════════════════════════════════════════════════ // MSS check (TRƯỚC re-scan, dùng OLD keyLevel + swing) // // Logic: // trend hiện tại có keyLevel → nếu close phá keyLevel: // breakDir = hướng phá (UP nếu phá lên, DOWN nếu phá xuống) // Chỉ nhận nếu breakDir == MiddleTF trend (TriggerTF only) // // Capture tại thời điểm MSS: // lastMssLevel = keyLevel bị phá = ENTRY PRICE cho limit order // mssSLSwing = swing extreme phía đối diện = SL level // Bull MSS: SL = L0 (đáy sóng hồi) // Bear MSS: SL = H0 (đỉnh sóng hồi) // ══════════════════════════════════════════════════════════════════ if (ctx.trend != DIR_NONE && ctx.keyLevel > 0) { bool mssHit = (ctx.trend == DIR_UP && bar1C < ctx.keyLevel) || // Uptrend KL=L0 → break below (ctx.trend == DIR_DOWN && bar1C > ctx.keyLevel); // Downtrend KL=H0 → break above if (mssHit && bar1T != ctx.lastMssTime) { MarketDir breakDir = (ctx.trend == DIR_UP) ? DIR_DOWN : DIR_UP; // TriggerTF: chỉ nhận MSS thuận chiều MiddleTF bool accept = true; if (tf == InpTriggerTF && g_MiddleTrend.trend != DIR_NONE) { if (breakDir != (MarketDir)g_MiddleTrend.trend) accept = false; // Ngược chiều → bỏ qua } if (accept) { ctx.lastMssTime = bar1T; // Thời gian cây nến MSS ctx.lastMssLevel = ctx.keyLevel; // KeyLevel bị phá = ENTRY ctx.lastMssBreak = breakDir; // Hướng break // v4: Capture SL swing TRƯỚC re-scan if (breakDir == DIR_UP) ctx.mssSLSwing = ctx.l0; // Bull MSS → SL dưới L0 (đáy hồi) else ctx.mssSLSwing = ctx.h0; // Bear MSS → SL trên H0 (đỉnh hồi) if (InpDebugLog) PrintFormat("[%s MSS] %s | entry=%.5f SL_swing=%.5f | close=%.5f | %s", EnumToString(tf), (breakDir == DIR_UP) ? "▲ Bull" : "▼ Bear", ctx.lastMssLevel, ctx.mssSLSwing, bar1C, TimeToString(bar1T)); } else if (InpDebugLog) { PrintFormat("[%s MSS skip] %s ngược H1=%s | KL=%.5f", EnumToString(tf), (breakDir == DIR_UP) ? "▲" : "▼", EnumToString(g_MiddleTrend.trend), ctx.keyLevel); } } } // ══════════════════════════════════════════════════════════════════ // Re-scan swing structure // ══════════════════════════════════════════════════════════════════ double h0, h1, l0, l1; int idxH0, idxH1, idxL0, idxL1; if (!ScanSwingStructure(tf, lookback, h0, h1, idxH0, idxH1, l0, l1, idxL0, idxL1)) { ctx.trend = DIR_NONE; return; } ctx.h0 = h0; ctx.idxH0 = idxH0; ctx.h1 = h1; ctx.idxH1 = idxH1; ctx.l0 = l0; ctx.idxL0 = idxL0; ctx.l1 = l1; ctx.idxL1 = idxL1; MarketDir prev = ctx.trend; ResolveTrendFromSwings(tf, h0, h1, l0, l1, ctx.trend, ctx.keyLevel); if (InpDebugLog && ctx.trend != prev) PrintFormat("[%s TREND] %s → %s | H0=%.5f H1=%.5f L0=%.5f L1=%.5f | KL=%.5f", EnumToString(tf), EnumToString(prev), EnumToString(ctx.trend), h0, h1, l0, l1, ctx.keyLevel); } void UpdateDailyRiskContext() { datetime today = iTime(_Symbol, PERIOD_D1, 0); // Open time ngày hiện tại if (today != g_DailyRisk.dayStartTime) // Ngày mới → reset tracking { g_DailyRisk.dayStartTime = today; // Đánh dấu ngày mới g_DailyRisk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Snapshot balance đầu ngày g_DailyRisk.limitHit = false; // Reset flag daily loss if (InpDebugLog) PrintFormat("[DAILY RISK] New day | start=%.2f", g_DailyRisk.startBalance); } if (g_DailyRisk.limitHit) return; // Đã hit limit → không cần check nữa g_DailyRisk.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); // Balance hiện tại double lostPct = (g_DailyRisk.startBalance - g_DailyRisk.currentBalance) // Tính % loss trong ngày / g_DailyRisk.startBalance * 100.0; if (lostPct >= InpMaxDailyLossPct) // Vượt ngưỡng → dừng trade { g_DailyRisk.limitHit = true; // Set flag → EvaluateGuards() sẽ block PrintFormat("[DAILY RISK] ⛔ Limit hit | lost=%.2f%% | bal=%.2f", lostPct, g_DailyRisk.currentBalance); } } void UpdateAllContexts() { UpdateDailyRiskContext(); // 1. Check daily drawdown trước tiên UpdateBiasContext(); // 2. D1 bias (UP/DOWN/SIDEWAY) UpdateTFTrendContext(InpMiddleTF, InpSwingLookback, g_MiddleTrend); // 3. H1 swing+trend+MSS UpdateTFTrendContext(InpTriggerTF, InpTriggerSwingLookback, g_TriggerTrend); // 4. M5 swing+trend+MSS (entry signal) } //+------------------------------------------------------------------+ //| SECTION 3 – GUARDS | //+------------------------------------------------------------------+ bool IsSessionAllowed() { return true; /* TODO: London/NY session filter dựa trên InpLondon/NYHour */ } bool IsDailyLossOK() { return !g_DailyRisk.limitHit; } // false nếu đã vượt InpMaxDailyLossPct bool IsBiasValid() { return g_Bias.bias == BIAS_UP || g_Bias.bias == BIAS_DOWN; } // Cần direction rõ ràng bool IsMiddleTrendAligned() // H1 trend phải cùng hướng D1 bias { if (g_MiddleTrend.trend == DIR_NONE) return false; // H1 chưa xác định → block return (g_Bias.bias == BIAS_UP && g_MiddleTrend.trend == DIR_UP) || // D1 UP + H1 UP ✓ (g_Bias.bias == BIAS_DOWN && g_MiddleTrend.trend == DIR_DOWN); // D1 DOWN + H1 DOWN ✓ } bool EvaluateGuards() // Kiểm tra tất cả điều kiện trước khi trade { g_BlockReason = BLOCK_NONE; // Reset reason if (!IsSessionAllowed()) { g_BlockReason = BLOCK_SESSION; return false; } // Ngoài giờ if (!IsDailyLossOK()) { g_BlockReason = BLOCK_DAILY_LOSS; return false; } // Vượt loss/ngày if (!IsBiasValid()) { g_BlockReason = BLOCK_NO_BIAS; return false; } // D1 sideway/none if (!IsMiddleTrendAligned()) { g_BlockReason = BLOCK_BIAS_MISMATCH; return false; } // H1 ngược D1 return true; // Tất cả OK → cho phép trade } //+------------------------------------------------------------------+ //| SECTION 4 – STATE MACHINE HELPERS | //+------------------------------------------------------------------+ void TransitionTo(EAState next) { if (InpDebugLog) PrintFormat("[STATE] %s → %s", EnumToString(g_State), EnumToString(next)); g_State = next; } void ResetToIdle(string reason = "") { if (InpDebugLog && reason != "") PrintFormat("[RESET→IDLE] %s", reason); g_ActiveFVGIdx = -1; // Bỏ FVG đang theo dõi g_TradeBarIndex = -1; // Reset bar index g_PendingTicket = 0; // Không còn pending order ZeroMemory(g_OrderPlan); // Xóa order plan TransitionTo(EA_IDLE); // Quay về IDLE tìm FVG mới } //+------------------------------------------------------------------+ //| SECTION 5 – FVG HELPERS | //+------------------------------------------------------------------+ bool IsCandleStrong(ENUM_TIMEFRAMES tf, int i) { double h = iHigh(_Symbol, tf, i), l = iLow(_Symbol, tf, i); // Range = high - low double o = iOpen(_Symbol, tf, i), c = iClose(_Symbol, tf, i); // Body = |close - open| double range = h - l; // Tổng chiều dài nến if (range < _Point) return false; // Nến quá nhỏ → bỏ qua return (MathAbs(c - o) / range * 100.0) >= InpFVGMinBodyPct; // Body >= 60% range → strong } bool IsFVGInPool(datetime created) { for (int j = 0; j < g_FVGCount; j++) if (g_FVGPool[j].createdTime == created) return true; return false; } //+------------------------------------------------------------------+ //| SECTION 6 – FVG POOL: SCAN & REGISTER | //+------------------------------------------------------------------+ void ScanAndRegisterFVGs() { static datetime s_lastScan = 0; // Guard: chỉ scan 1 lần mỗi bar H1 datetime t0 = iTime(_Symbol, InpMiddleTF, 0); // Open time bar H1 hiện tại if (t0 == s_lastScan) return; // Đã scan bar này rồi → skip s_lastScan = t0; // Đánh dấu đã scan MarketDir dir = g_MiddleTrend.trend; // Chỉ scan FVG thuận chiều H1 trend if (dir == DIR_NONE) return; // Không có trend → không scan int maxBar = MathMin(InpFVGScanBars, Bars(_Symbol, InpMiddleTF) - 2); // Giới hạn scan range for (int i = 2; i <= maxBar; i++) { double leftH = iHigh (_Symbol, InpMiddleTF, i + 1); // Cây bên trái gap double leftL = iLow (_Symbol, InpMiddleTF, i + 1); double rightH = iHigh (_Symbol, InpMiddleTF, i - 1); // Cây bên phải gap double rightL = iLow (_Symbol, InpMiddleTF, i - 1); double midO = iOpen (_Symbol, InpMiddleTF, i); // Cây giữa (tạo gap) double midC = iClose(_Symbol, InpMiddleTF, i); double gH = 0, gL = 0; if (dir == DIR_UP) // Bullish FVG { if (leftH >= rightL) continue; // Không có gap if (midC <= midO) continue; // Cây giữa phải bullish if (!IsCandleStrong(InpMiddleTF, i)) continue; // Body đủ lớn gL = leftH; gH = rightL; // Gap = leftH → rightL } else // Bearish FVG { if (leftL <= rightH) continue; if (midC >= midO) continue; // Cây giữa phải bearish if (!IsCandleStrong(InpMiddleTF, i)) continue; gH = leftL; gL = rightH; // Gap = rightH → leftL } datetime created = iTime(_Symbol, InpMiddleTF, i - 1); // FVG created = open time cây phải if (IsFVGInPool(created)) continue; // Đã có trong pool → skip trùng // ── Evict oldest USED if pool full ────────────────────────────── if (g_FVGCount >= MAX_FVG_POOL) // Pool đầy 30 slot { int evict = -1; datetime oldest = TimeCurrent(); // Tìm USED cũ nhất để xóa for (int j = 0; j < g_FVGCount; j++) if (g_FVGPool[j].status == FVG_USED && g_FVGPool[j].createdTime < oldest) { oldest = g_FVGPool[j].createdTime; evict = j; } // Ghi nhớ slot cũ nhất if (evict < 0) { if (InpDebugLog) Print("[FVG POOL] Full"); break; } // Không có USED → stop for (int j = evict; j < g_FVGCount - 1; j++) g_FVGPool[j] = g_FVGPool[j + 1]; // Shift array g_FVGCount--; // Giảm count if (g_ActiveFVGIdx > evict) g_ActiveFVGIdx--; // Fix active index sau shift else if (g_ActiveFVGIdx == evict) g_ActiveFVGIdx = -1; // Active bị evict → clear } // ── Build record ───────────────────────────────────────────────── FVGRecord rec; ZeroMemory(rec); // Init tất cả field = 0 rec.id = g_NextFVGId++; rec.direction = dir; // Assign ID tự tăng + hướng rec.high = gH; rec.low = gL; rec.mid = (gH + gL) / 2.0; // Biên gap + midpoint rec.createdTime = created; // Thời gian tạo int rightBar = i - 1; // Cây bên phải gap (dùng cho scan status) // P1: case1 – Kiểm tra đã bị phá xuyên chưa (H1 close qua gap) bool c1Hit = false; datetime c1T = 0; // Flag + thời gian bị phá for (int j = rightBar - 1; j >= 1; j--) // Scan từ cây phải → hiện tại { double cl = iClose(_Symbol, InpMiddleTF, j); // Close mỗi bar H1 if ((rec.direction == DIR_UP && cl < rec.low) || // Bull FVG: close dưới biên dưới = phá (rec.direction == DIR_DOWN && cl > rec.high)) // Bear FVG: close trên biên trên = phá { c1Hit = true; c1T = iTime(_Symbol, InpMiddleTF, j); break; } // Ghi nhận thời gian phá } if (c1Hit) { rec.status = FVG_USED; rec.usedCase = 1; rec.usedTime = c1T; } // Case1 = broken else { // P2: touch – Kiểm tra giá đã wick vào gap chưa bool tdHit = false; datetime tdT = 0; // Flag + thời gian touch for (int j = rightBar - 1; j >= 1; j--) // Scan từ cây phải → hiện tại { bool inGap = (rec.direction == DIR_UP && iLow (_Symbol, InpMiddleTF, j) <= rec.high) || // Bull: low ≤ gap top (rec.direction == DIR_DOWN && iHigh(_Symbol, InpMiddleTF, j) >= rec.low); // Bear: high ≥ gap bottom if (inGap) { tdHit = true; tdT = iTime(_Symbol, InpMiddleTF, j); break; } // Đã touch } if (tdHit) { rec.status = FVG_TOUCHED; rec.touchTime = tdT; rec.triggerTrendAtTouch = g_TriggerTrend.trend; } // Ghi nhận M5 trend lúc touch else { rec.status = FVG_PENDING; // Chưa touch, chưa bị phá → chờ if ((int)(TimeCurrent() - rec.createdTime) > InpFVGMaxAliveMin * 60) // Quá 72h { rec.status = FVG_USED; rec.usedCase = 0; rec.usedTime = TimeCurrent(); } // Case0 = expired } } g_FVGPool[g_FVGCount] = rec; // Thêm vào pool g_FVGCount++; // Tăng count if (InpDebugLog) PrintFormat("[FVG +] #%d %s [%.5f–%.5f] %s | %s", rec.id, EnumToString(rec.direction), rec.low, rec.high, EnumToString(rec.status), TimeToString(rec.createdTime)); } } //+------------------------------------------------------------------+ //| SECTION 7 – FVG POOL: UPDATE STATUSES (every tick) | //+------------------------------------------------------------------+ void UpdateFVGStatuses() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Giá bid hiện tại (dùng cho touch check) double midC1 = iClose(_Symbol, InpMiddleTF, 1); // H1 bar vừa đóng: close (dùng cho case1) datetime midT1 = iTime (_Symbol, InpMiddleTF, 1); // H1 bar vừa đóng: time for (int i = 0; i < g_FVGCount; i++) // Duyệt toàn bộ pool { if (g_FVGPool[i].status == FVG_USED) continue; // Đã consumed → skip // ── 1. Case1: H1 close phá xuyên FVG → broken ────────────────── bool c1 = (g_FVGPool[i].direction == DIR_UP && midC1 < g_FVGPool[i].low) || // Bull: close < bottom (g_FVGPool[i].direction == DIR_DOWN && midC1 > g_FVGPool[i].high); // Bear: close > top if (c1) { g_FVGPool[i].status = FVG_USED; g_FVGPool[i].usedCase = 1; g_FVGPool[i].usedTime = midT1; if (InpDebugLog) PrintFormat("[FVG #%d] BROKEN | close=%.5f", g_FVGPool[i].id, midC1); continue; } if (g_FVGPool[i].status == FVG_PENDING) { // ── 2. Expire ──────────────────────────────────────────────────── int age = (int)(TimeCurrent() - g_FVGPool[i].createdTime); if (age > InpFVGMaxAliveMin * 60) { g_FVGPool[i].status = FVG_USED; g_FVGPool[i].usedCase = 0; g_FVGPool[i].usedTime = TimeCurrent(); continue; } // ── 3. Touch: bid vào vùng gap ─────────────────────────────────── bool touched = (g_FVGPool[i].direction == DIR_UP && bid <= g_FVGPool[i].high) || // Bull: bid retrace xuống ≤ gap top (g_FVGPool[i].direction == DIR_DOWN && bid >= g_FVGPool[i].low); // Bear: bid retrace lên ≥ gap bottom if (touched) { g_FVGPool[i].status = FVG_TOUCHED; // Chuyển sang TOUCHED g_FVGPool[i].touchTime = TimeCurrent(); // Ghi nhận thời gian touch g_FVGPool[i].triggerTrendAtTouch = g_TriggerTrend.trend; // Snapshot M5 trend lúc touch if (InpDebugLog) PrintFormat("[FVG #%d] TOUCHED | bid=%.5f [%.5f–%.5f]", g_FVGPool[i].id, bid, g_FVGPool[i].low, g_FVGPool[i].high); } } else if (g_FVGPool[i].status == FVG_TOUCHED) { // ── 4. Case2: M5 MSS đã xảy ra SAU touch → triggered ─────────── // // v4: Kiểm tra g_TriggerTrend.lastMssTime > touchTime // VÀ hướng MSS phù hợp FVG direction // bool hasMSS = g_TriggerTrend.lastMssTime > g_FVGPool[i].touchTime && // MSS sau touch g_TriggerTrend.lastMssBreak == g_FVGPool[i].direction; // Cùng hướng if (hasMSS) { g_FVGPool[i].status = FVG_USED; g_FVGPool[i].usedCase = 2; // Case2 = triggered g_FVGPool[i].usedTime = TimeCurrent(); if (InpDebugLog) PrintFormat("[FVG #%d] TRIGGERED | MSS %s @ %s", g_FVGPool[i].id, (g_TriggerTrend.lastMssBreak == DIR_UP) ? "▲" : "▼", TimeToString(g_TriggerTrend.lastMssTime, TIME_MINUTES)); continue; } // ── 5. Trigger timeout ──────────────────────────────────────────── int bars = (int)((TimeCurrent() - g_FVGPool[i].touchTime) / PeriodSeconds(InpTriggerTF)); if (bars > InpTriggerMaxBars && g_ActiveFVGIdx >= 0 && g_FVGPool[g_ActiveFVGIdx].id == g_FVGPool[i].id) { if (InpDebugLog) PrintFormat("[FVG #%d] Timeout (%d bars)", g_FVGPool[i].id, bars); g_ActiveFVGIdx = -1; } } } } //+------------------------------------------------------------------+ //| SECTION 7B – ORDER PLAN & EXECUTION | //| | //| v4: Entry = MSS broken keyLevel (tH0 hoặc tL0) | //| SL = swing extreme sóng hồi (capture lúc MSS) | //| TP = Entry ± InpRiskReward * |Entry - SL| | //+------------------------------------------------------------------+ double CalcLotFromRisk(double entry, double sl) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); // Balance hiện tại double riskMoney = balance * InpRiskPercent / 100.0; // Số tiền chấp nhận mất (VD: 1% of 10000 = 100) double slPips = MathAbs(entry - sl) / _Point; // SL tính bằng points (VD: 50 points) if (slPips < 1) return 0; // SL quá nhỏ → invalid double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); // Giá trị 1 tick/lot double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); // Kích thước 1 tick if (tickValue <= 0 || tickSize <= 0) return 0; // Broker data invalid double pipValue = tickValue * (_Point / tickSize); // Giá trị 1 point cho 1 lot double rawLot = riskMoney / (slPips * pipValue); // Lot = riskMoney / (points × value/point) double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); // VD: 0.01 double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); // VD: 100.0 double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); // VD: 0.01 if (lotStep <= 0) lotStep = 0.01; // Fallback rawLot = MathFloor(rawLot / lotStep) * lotStep; // Làm tròn xuống theo lotStep rawLot = MathMax(minLot, MathMin(maxLot, rawLot)); // Clamp trong [min, max] return NormalizeDouble(rawLot, 2); // Trả về lot 2 chữ số thập phân } bool BuildOrderPlan(int fvgId, MarketDir dir) { ZeroMemory(g_OrderPlan); // Reset plan cũ // v4: Entry = keyLevel vừa bị phá (đã capture trong MSS context) double entry = NormalizeDouble(g_TriggerTrend.lastMssLevel, _Digits); // tH0 (buy) hoặc tL0 (sell) // v4: SL = swing extreme sóng hồi (đã capture trong MSS context) double sl = g_TriggerTrend.mssSLSwing; // tL0 (buy) hoặc tH0 (sell), captured TRƯỚC re-scan if (entry <= 0 || sl <= 0) return false; // Data invalid → không đặt lệnh double tp; if (dir == DIR_UP) // ── BUY LIMIT setup ── { if (sl >= entry) return false; // SL phải dưới entry cho buy sl = NormalizeDouble(sl - 2 * _Point, _Digits); // Buffer 2 points dưới swing low double riskDist = entry - sl; // Khoảng cách risk (points) tp = NormalizeDouble(entry + InpRiskReward * riskDist, _Digits); // TP = entry + 2R (mặc định) } else // ── SELL LIMIT setup ── { if (sl <= entry) return false; // SL phải trên entry cho sell sl = NormalizeDouble(sl + 2 * _Point, _Digits); // Buffer 2 points trên swing high double riskDist = sl - entry; // Khoảng cách risk (points) tp = NormalizeDouble(entry - InpRiskReward * riskDist, _Digits); // TP = entry - 2R } double lot = CalcLotFromRisk(entry, sl); // Tính lot từ risk% và SL distance if (lot <= 0) return false; // Lot invalid → skip g_OrderPlan.valid = true; // Plan sẵn sàng g_OrderPlan.direction = (dir == DIR_UP) ? 1 : -1; // +1=buy, -1=sell g_OrderPlan.entry = entry; // Giá đặt limit order g_OrderPlan.stopLoss = sl; // SL đã có buffer g_OrderPlan.takeProfit = tp; // TP theo R:R ratio g_OrderPlan.lot = lot; // Lot size theo risk management g_OrderPlan.parentFVGId = fvgId; // Link về FVG gốc if (InpDebugLog) PrintFormat("[ORDER PLAN] %s | entry=%.5f SL=%.5f TP=%.5f lot=%.2f | FVG#%d", (dir == DIR_UP) ? "BUY LIMIT" : "SELL LIMIT", entry, sl, tp, lot, fvgId); return true; } ulong ExecuteLimitOrder() { if (!g_OrderPlan.valid) return 0; ENUM_ORDER_TYPE cmd = (g_OrderPlan.direction > 0) ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT; double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Validate: BUY LIMIT < Ask, SELL LIMIT > Bid if (cmd == ORDER_TYPE_BUY_LIMIT && g_OrderPlan.entry >= ask) { if (InpDebugLog) PrintFormat("[ORDER] BUY LIMIT entry %.5f >= ask %.5f → skip", g_OrderPlan.entry, ask); return 0; } if (cmd == ORDER_TYPE_SELL_LIMIT && g_OrderPlan.entry <= bid) { if (InpDebugLog) PrintFormat("[ORDER] SELL LIMIT entry %.5f <= bid %.5f → skip", g_OrderPlan.entry, bid); return 0; } MqlTradeRequest request; MqlTradeResult result; ZeroMemory(request); // Init tất cả field = 0 ZeroMemory(result); request.action = TRADE_ACTION_PENDING; // Đặt lệnh chờ (không market order) request.symbol = _Symbol; // Symbol hiện tại trên chart request.volume = g_OrderPlan.lot; // Lot size đã tính từ risk% request.type = cmd; // BUY_LIMIT hoặc SELL_LIMIT request.price = g_OrderPlan.entry; // Giá đặt lệnh = MSS keyLevel request.sl = g_OrderPlan.stopLoss; // SL = swing hồi + buffer request.tp = g_OrderPlan.takeProfit; // TP = entry ± R:R ratio request.deviation = (ulong)InpSlippage; // Slippage tối đa (points) request.magic = InpMagicNumber; // Magic number để identify EA's orders request.comment = StringFormat("ICT#%d", g_OrderPlan.parentFVGId); // Comment link FVG request.type_filling = ORDER_FILLING_RETURN; // Return unfilled volume (broker-safe) request.type_time = ORDER_TIME_GTC; // Good Till Cancel (không hết hạn) if (!OrderSend(request, result)) { PrintFormat("[ORDER] ❌ retcode=%u", result.retcode); return 0; } if (result.retcode != TRADE_RETCODE_DONE && result.retcode != TRADE_RETCODE_PLACED) { PrintFormat("[ORDER] ❌ rejected retcode=%u: %s", result.retcode, result.comment); return 0; } PrintFormat("[ORDER] ✅ %s #%llu | %.2f @ %.5f SL=%.5f TP=%.5f", (cmd == ORDER_TYPE_BUY_LIMIT) ? "BUY_LIM" : "SELL_LIM", result.order, g_OrderPlan.lot, g_OrderPlan.entry, g_OrderPlan.stopLoss, g_OrderPlan.takeProfit); return result.order; } //+------------------------------------------------------------------+ //| SECTION 8 – BEST FVG SELECTOR | //+------------------------------------------------------------------+ int GetBestActiveFVGIdx() { int bestIdx = -1; datetime bestTime = 0; bool foundTouch = false; // Track best candidate for (int i = 0; i < g_FVGCount; i++) { if (g_FVGPool[i].status == FVG_USED) continue; // Skip FVG đã consumed if (g_FVGPool[i].status == FVG_TOUCHED) // TOUCHED được ưu tiên hơn PENDING { if (!foundTouch || g_FVGPool[i].createdTime > bestTime) // Mới nhất trong TOUCHED { foundTouch = true; bestIdx = i; bestTime = g_FVGPool[i].createdTime; } } else if (!foundTouch && g_FVGPool[i].createdTime > bestTime) // PENDING: chỉ khi chưa có TOUCHED { bestIdx = i; bestTime = g_FVGPool[i].createdTime; } } return bestIdx; // -1 nếu không có FVG nào khả dụng } //+------------------------------------------------------------------+ //| SECTION 9 – STATE HANDLERS | //+------------------------------------------------------------------+ void OnStateIdle() { int idx = GetBestActiveFVGIdx(); if (idx < 0) return; g_ActiveFVGIdx = idx; if (InpDebugLog) PrintFormat("[ACTIVE FVG] #%d %s [%.5f–%.5f] %s", g_FVGPool[idx].id, EnumToString(g_FVGPool[idx].direction), g_FVGPool[idx].low, g_FVGPool[idx].high, EnumToString(g_FVGPool[idx].status)); TransitionTo(g_FVGPool[idx].status == FVG_TOUCHED ? EA_WAIT_TRIGGER : EA_WAIT_TOUCH); } void OnStateWaitTouch() { if (g_ActiveFVGIdx < 0) { ResetToIdle("active lost"); return; } int ai = g_ActiveFVGIdx; if (g_FVGPool[ai].status == FVG_USED) { ResetToIdle(StringFormat("FVG #%d broken/expired", g_FVGPool[ai].id)); return; } if (g_FVGPool[ai].status == FVG_TOUCHED) { TransitionTo(EA_WAIT_TRIGGER); return; } // Kiểm tra FVG mới tốt hơn int better = GetBestActiveFVGIdx(); if (better >= 0 && better != ai) { bool bTouch = (g_FVGPool[better].status == FVG_TOUCHED); bool bNewer = (g_FVGPool[better].createdTime > g_FVGPool[ai].createdTime); if (bTouch || bNewer) { if (InpDebugLog) PrintFormat("[SWITCH] FVG #%d → #%d", g_FVGPool[ai].id, g_FVGPool[better].id); g_ActiveFVGIdx = better; if (bTouch) TransitionTo(EA_WAIT_TRIGGER); } } } void OnStateWaitTrigger() { if (g_ActiveFVGIdx < 0) { ResetToIdle("active lost"); return; } int ai = g_ActiveFVGIdx; if (g_FVGPool[ai].status == FVG_USED) { if (g_FVGPool[ai].usedCase == 2) // Case2 = MSS triggered { if (InpDebugLog) PrintFormat("[ENTRY SIGNAL] FVG #%d %s [%.5f–%.5f] | MSS entry=%.5f", g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction), g_FVGPool[ai].low, g_FVGPool[ai].high, g_TriggerTrend.lastMssLevel); // v4: Build order plan dùng MSS data (entry=keyLevel, SL=swing hồi) if (BuildOrderPlan(g_FVGPool[ai].id, g_FVGPool[ai].direction)) { ulong ticket = ExecuteLimitOrder(); if (ticket > 0) { g_PendingTicket = ticket; g_TradeBarIndex = Bars(_Symbol, InpTriggerTF); TransitionTo(EA_IN_TRADE); } else ResetToIdle(StringFormat("FVG #%d order failed", g_FVGPool[ai].id)); } else ResetToIdle(StringFormat("FVG #%d invalid plan", g_FVGPool[ai].id)); } else ResetToIdle(StringFormat("FVG #%d case%d", g_FVGPool[ai].id, g_FVGPool[ai].usedCase)); return; } if (g_FVGPool[ai].status == FVG_PENDING) { TransitionTo(EA_WAIT_TOUCH); return; } } void OnStateInTrade() { // ── 1. Pending order còn active? ─────────────────────────────── if (g_PendingTicket > 0) { bool foundPending = false; for (int i = OrdersTotal() - 1; i >= 0; i--) { if (OrderGetTicket(i) == g_PendingTicket) { foundPending = true; break; } } if (!foundPending) { // Kiểm tra đã fill → position bool posFound = false; for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong pt = PositionGetTicket(i); if (pt > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { posFound = true; g_PendingTicket = 0; // Filled if (InpDebugLog) PrintFormat("[TRADE] Filled → position #%llu", pt); break; } } if (!posFound) { HistorySelect(TimeCurrent() - 86400, TimeCurrent()); // Cancelled? bool cancelled = false; for (int i = HistoryOrdersTotal() - 1; i >= 0; i--) { ulong ht = HistoryOrderGetTicket(i); if (ht == g_PendingTicket) { long st = HistoryOrderGetInteger(ht, ORDER_STATE); if (st == ORDER_STATE_CANCELED || st == ORDER_STATE_EXPIRED || st == ORDER_STATE_REJECTED) cancelled = true; break; } } if (cancelled) { ResetToIdle("order cancelled"); return; } // Position closed? bool closed = false; for (int i = HistoryDealsTotal() - 1; i >= 0; i--) { ulong dt = HistoryDealGetTicket(i); if (HistoryDealGetInteger(dt, DEAL_MAGIC) == InpMagicNumber && HistoryDealGetString(dt, DEAL_SYMBOL) == _Symbol && HistoryDealGetInteger(dt, DEAL_ENTRY) == DEAL_ENTRY_OUT) { double profit = HistoryDealGetDouble(dt, DEAL_PROFIT); if (InpDebugLog) PrintFormat("[TRADE] Closed | profit=%.2f", profit); closed = true; break; } } ResetToIdle(closed ? "trade closed" : "order lost"); return; } } return; // Pending alive → wait } // ── 2. Track position ────────────────────────────────────────── bool hasPos = false; for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong pt = PositionGetTicket(i); if (pt > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol) { hasPos = true; break; } } if (!hasPos) ResetToIdle("position closed"); } //+------------------------------------------------------------------+ //| SECTION 10 – STATE MACHINE RUNNER | //+------------------------------------------------------------------+ void RunStateMachine() { UpdateFVGStatuses(); // Cập nhật trạng thái FVG pool (mỗi tick) ScanAndRegisterFVGs(); // Scan H1 bars cho FVG mới (mỗi bar H1) switch (g_State) // Dispatch theo state hiện tại { case EA_IDLE: OnStateIdle(); break; // Tìm FVG tốt nhất → WAIT_TOUCH/TRIGGER case EA_WAIT_TOUCH: OnStateWaitTouch(); break; // Chờ giá retrace vào FVG case EA_WAIT_TRIGGER: OnStateWaitTrigger(); break; // Chờ M5 MSS → đặt limit order case EA_IN_TRADE: OnStateInTrade(); break; // Track pending/position → close } } //+------------------------------------------------------------------+ //| SECTION 11 – DRAWING | //+------------------------------------------------------------------+ void DrawOneSwingPoint( string prefix, ENUM_TIMEFRAMES tf, string tag, bool isHigh, int barIdx, double price, color clr, bool isKL, int arrowSz = 2, int fontSize = 8) { string arrN = prefix + "ARR_" + tag; string txtN = prefix + "TXT_" + tag; string klN = prefix + "KL_" + tag; datetime t = iTime(_Symbol, tf, barIdx); if (ObjectFind(0, arrN) < 0) ObjectCreate(0, arrN, OBJ_ARROW, 0, t, price); ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isHigh ? 234 : 233); ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr); ObjectSetInteger(0, arrN, OBJPROP_WIDTH, arrowSz); ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isHigh ? ANCHOR_BOTTOM : ANCHOR_TOP); ObjectMove(0, arrN, 0, t, price); double rng = iHigh(_Symbol, tf, barIdx) - iLow(_Symbol, tf, barIdx); double txtY = isHigh ? price + rng * 0.3 : price - rng * 0.3; if (ObjectFind(0, txtN) < 0) ObjectCreate(0, txtN, OBJ_TEXT, 0, t, txtY); ObjectMove(0, txtN, 0, t, txtY); ObjectSetString (0, txtN, OBJPROP_TEXT, tag); ObjectSetInteger(0, txtN, OBJPROP_COLOR, clr); ObjectSetInteger(0, txtN, OBJPROP_FONTSIZE, fontSize); ObjectSetInteger(0, txtN, OBJPROP_ANCHOR, isHigh ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); if (isKL) { datetime tEnd = iTime(_Symbol, tf, 0); if (ObjectFind(0, klN) < 0) ObjectCreate(0, klN, OBJ_TREND, 0, t, price, tEnd, price); ObjectSetInteger(0, klN, OBJPROP_COLOR, clr); ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false); ObjectMove(0, klN, 0, t, price); ObjectMove(0, klN, 1, tEnd, price); } else ObjectDelete(0, klN); } void DrawMiddleSwingPoints() { if (!InpDebugDraw) return; if (g_MiddleTrend.idxH0 <= 0 || g_MiddleTrend.idxH1 <= 0 || g_MiddleTrend.idxL0 <= 0 || g_MiddleTrend.idxL1 <= 0) { ObjectsDeleteAll(0, SW_PREFIX); return; } bool isUp = (g_MiddleTrend.trend == DIR_UP); bool isDown = (g_MiddleTrend.trend == DIR_DOWN); DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H0", true, g_MiddleTrend.idxH0, g_MiddleTrend.h0, clrAqua, isDown, 2, 8); DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "H1", true, g_MiddleTrend.idxH1, g_MiddleTrend.h1, C'0,140,160', false, 2, 8); DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L0", false, g_MiddleTrend.idxL0, g_MiddleTrend.l0, clrYellow, isUp, 2, 8); DrawOneSwingPoint(SW_PREFIX, InpMiddleTF, "L1", false, g_MiddleTrend.idxL1, g_MiddleTrend.l1, C'160,140,0', false, 2, 8); } void DrawTriggerSwingPoints() { if (!InpDebugDraw) return; if (g_TriggerTrend.idxH0 <= 0 || g_TriggerTrend.idxH1 <= 0 || g_TriggerTrend.idxL0 <= 0 || g_TriggerTrend.idxL1 <= 0) { ObjectsDeleteAll(0, TS_PREFIX); return; } bool isUp = (g_TriggerTrend.trend == DIR_UP); bool isDown = (g_TriggerTrend.trend == DIR_DOWN); DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH0", true, g_TriggerTrend.idxH0, g_TriggerTrend.h0, C'180,100,255', isDown, 1, 7); DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tH1", true, g_TriggerTrend.idxH1, g_TriggerTrend.h1, C'100,60,160', false, 1, 7); DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL0", false, g_TriggerTrend.idxL0, g_TriggerTrend.l0, C'255,160,40', isUp, 1, 7); DrawOneSwingPoint(TS_PREFIX, InpTriggerTF, "tL1", false, g_TriggerTrend.idxL1, g_TriggerTrend.l1, C'160,100,20', false, 1, 7); } //---------------------------------------------------------------------- // DrawMSSMarker – MSS arrow + label + keyLevel line //---------------------------------------------------------------------- void DrawMSSMarker( string mssId, ENUM_TIMEFRAMES tf, datetime mssTime, double mssLevel, MarketDir mssBreak) { if (!InpDebugDraw || mssTime == 0) return; string arrN = MSS_PREFIX + mssId + "_ARR"; string lblN = MSS_PREFIX + mssId + "_LBL"; string klN = MSS_PREFIX + mssId + "_KL"; bool isBull = (mssBreak == DIR_UP); color clr = isBull ? clrLime : clrTomato; int shift = MyBarShift(_Symbol, tf, mssTime); if (shift < 0) return; double closeAtMss = iClose(_Symbol, tf, shift); // Arrow tại close cây nến MSS if (ObjectFind(0, arrN) < 0) ObjectCreate(0, arrN, OBJ_ARROW, 0, mssTime, closeAtMss); ObjectSetInteger(0, arrN, OBJPROP_ARROWCODE, isBull ? 233 : 234); ObjectSetInteger(0, arrN, OBJPROP_COLOR, clr); ObjectSetInteger(0, arrN, OBJPROP_WIDTH, 2); ObjectSetInteger(0, arrN, OBJPROP_ANCHOR, isBull ? ANCHOR_TOP : ANCHOR_BOTTOM); ObjectMove(0, arrN, 0, mssTime, closeAtMss); // Label "▲MSS" / "▼MSS" double rng = iHigh(_Symbol, tf, shift) - iLow(_Symbol, tf, shift); double lblY = isBull ? closeAtMss - rng * 0.4 : closeAtMss + rng * 0.4; if (ObjectFind(0, lblN) < 0) ObjectCreate(0, lblN, OBJ_TEXT, 0, mssTime, lblY); ObjectMove(0, lblN, 0, mssTime, lblY); ObjectSetString (0, lblN, OBJPROP_TEXT, isBull ? "▲MSS" : "▼MSS"); ObjectSetInteger(0, lblN, OBJPROP_COLOR, clr); ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, isBull ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER); // Dotted line tại broken keyLevel datetime tEnd = iTime(_Symbol, tf, 0); if (ObjectFind(0, klN) < 0) ObjectCreate(0, klN, OBJ_TREND, 0, mssTime, mssLevel, tEnd, mssLevel); ObjectSetInteger(0, klN, OBJPROP_COLOR, clr); ObjectSetInteger(0, klN, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, klN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, klN, OBJPROP_RAY_RIGHT, false); ObjectMove(0, klN, 0, mssTime, mssLevel); ObjectMove(0, klN, 1, tEnd, mssLevel); } void DrawMSSMarkers() { if (!InpDebugDraw) return; if (g_MiddleTrend.lastMssTime > 0) { string mid = "M_" + IntegerToString((long)g_MiddleTrend.lastMssTime); DrawMSSMarker(mid, InpMiddleTF, g_MiddleTrend.lastMssTime, g_MiddleTrend.lastMssLevel, g_MiddleTrend.lastMssBreak); } if (g_TriggerTrend.lastMssTime > 0) { string tid = "T_" + IntegerToString((long)g_TriggerTrend.lastMssTime); DrawMSSMarker(tid, InpTriggerTF, g_TriggerTrend.lastMssTime, g_TriggerTrend.lastMssLevel, g_TriggerTrend.lastMssBreak); } } //---------------------------------------------------------------------- // DrawOrderVisualization – TradingView-style Entry/SL/TP //---------------------------------------------------------------------- void DrawOrderVisualization() { if (!InpDebugDraw) return; string tpZoneN = ORD_PREFIX + "TP_ZONE"; string slZoneN = ORD_PREFIX + "SL_ZONE"; string entLineN = ORD_PREFIX + "ENTRY_LINE"; string slLineN = ORD_PREFIX + "SL_LINE"; string tpLineN = ORD_PREFIX + "TP_LINE"; string entLblN = ORD_PREFIX + "ENTRY_LBL"; string slLblN = ORD_PREFIX + "SL_LBL"; string tpLblN = ORD_PREFIX + "TP_LBL"; string infoLblN = ORD_PREFIX + "INFO_LBL"; if (!g_OrderPlan.valid || g_State == EA_IDLE) { ObjectDelete(0, tpZoneN); ObjectDelete(0, slZoneN); ObjectDelete(0, entLineN); ObjectDelete(0, slLineN); ObjectDelete(0, tpLineN); ObjectDelete(0, entLblN); ObjectDelete(0, slLblN); ObjectDelete(0, tpLblN); ObjectDelete(0, infoLblN); return; } bool isBuy = (g_OrderPlan.direction > 0); double entry = g_OrderPlan.entry; double sl = g_OrderPlan.stopLoss; double tp = g_OrderPlan.takeProfit; datetime tStart = (g_TriggerTrend.lastMssTime > 0) ? g_TriggerTrend.lastMssTime : iTime(_Symbol, InpTriggerTF, 20); datetime tEnd = iTime(_Symbol, InpTriggerTF, 0) + PeriodSeconds(InpTriggerTF) * 25; double slPips = MathAbs(entry - sl) / _Point; double tpPips = MathAbs(tp - entry) / _Point; double rr = (slPips > 0) ? tpPips / slPips : 0; color entryClr = isBuy ? C'33,150,243' : C'255,152,0'; color tpFill = C'15,65,35'; color slFill = C'85,15,15'; color tpClr = C'38,166,91'; color slClr = C'229,57,53'; // TP zone double tpTop = isBuy ? tp : entry, tpBot = isBuy ? entry : tp; if (ObjectFind(0, tpZoneN) < 0) ObjectCreate(0, tpZoneN, OBJ_RECTANGLE, 0, tStart, tpTop, tEnd, tpBot); ObjectSetInteger(0, tpZoneN, OBJPROP_COLOR, tpFill); ObjectSetInteger(0, tpZoneN, OBJPROP_FILL, true); ObjectSetInteger(0, tpZoneN, OBJPROP_BACK, true); ObjectMove(0, tpZoneN, 0, tStart, tpTop); ObjectMove(0, tpZoneN, 1, tEnd, tpBot); // SL zone double slTop = isBuy ? entry : sl, slBot = isBuy ? sl : entry; if (ObjectFind(0, slZoneN) < 0) ObjectCreate(0, slZoneN, OBJ_RECTANGLE, 0, tStart, slTop, tEnd, slBot); ObjectSetInteger(0, slZoneN, OBJPROP_COLOR, slFill); ObjectSetInteger(0, slZoneN, OBJPROP_FILL, true); ObjectSetInteger(0, slZoneN, OBJPROP_BACK, true); ObjectMove(0, slZoneN, 0, tStart, slTop); ObjectMove(0, slZoneN, 1, tEnd, slBot); // Entry line if (ObjectFind(0, entLineN) < 0) ObjectCreate(0, entLineN, OBJ_TREND, 0, tStart, entry, tEnd, entry); ObjectSetInteger(0, entLineN, OBJPROP_COLOR, entryClr); ObjectSetInteger(0, entLineN, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, entLineN, OBJPROP_WIDTH, 2); ObjectSetInteger(0, entLineN, OBJPROP_RAY_RIGHT, true); ObjectMove(0, entLineN, 0, tStart, entry); ObjectMove(0, entLineN, 1, tEnd, entry); // TP line if (ObjectFind(0, tpLineN) < 0) ObjectCreate(0, tpLineN, OBJ_TREND, 0, tStart, tp, tEnd, tp); ObjectSetInteger(0, tpLineN, OBJPROP_COLOR, tpClr); ObjectSetInteger(0, tpLineN, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, tpLineN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, tpLineN, OBJPROP_RAY_RIGHT, true); ObjectMove(0, tpLineN, 0, tStart, tp); ObjectMove(0, tpLineN, 1, tEnd, tp); // SL line if (ObjectFind(0, slLineN) < 0) ObjectCreate(0, slLineN, OBJ_TREND, 0, tStart, sl, tEnd, sl); ObjectSetInteger(0, slLineN, OBJPROP_COLOR, slClr); ObjectSetInteger(0, slLineN, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, slLineN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, slLineN, OBJPROP_RAY_RIGHT, true); ObjectMove(0, slLineN, 0, tStart, sl); ObjectMove(0, slLineN, 1, tEnd, sl); // Labels datetime lblT = tEnd; string eTxt = StringFormat("%s %s | %.2f lot", isBuy?"▶ BUY LIM":"▶ SELL LIM", DoubleToString(entry,_Digits), g_OrderPlan.lot); if (ObjectFind(0, entLblN) < 0) ObjectCreate(0, entLblN, OBJ_TEXT, 0, lblT, entry); ObjectMove(0, entLblN, 0, lblT, entry); ObjectSetString(0, entLblN, OBJPROP_TEXT, eTxt); ObjectSetInteger(0, entLblN, OBJPROP_COLOR, entryClr); ObjectSetInteger(0, entLblN, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, entLblN, OBJPROP_ANCHOR, ANCHOR_LEFT); string tTxt = StringFormat("◎ TP %s +%.0fp (%.1fR)", DoubleToString(tp,_Digits), tpPips, rr); if (ObjectFind(0, tpLblN) < 0) ObjectCreate(0, tpLblN, OBJ_TEXT, 0, lblT, tp); ObjectMove(0, tpLblN, 0, lblT, tp); ObjectSetString(0, tpLblN, OBJPROP_TEXT, tTxt); ObjectSetInteger(0, tpLblN, OBJPROP_COLOR, tpClr); ObjectSetInteger(0, tpLblN, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, tpLblN, OBJPROP_ANCHOR, isBuy?ANCHOR_LEFT_LOWER:ANCHOR_LEFT_UPPER); string sTxt = StringFormat("✕ SL %s -%.0fp", DoubleToString(sl,_Digits), slPips); if (ObjectFind(0, slLblN) < 0) ObjectCreate(0, slLblN, OBJ_TEXT, 0, lblT, sl); ObjectMove(0, slLblN, 0, lblT, sl); ObjectSetString(0, slLblN, OBJPROP_TEXT, sTxt); ObjectSetInteger(0, slLblN, OBJPROP_COLOR, slClr); ObjectSetInteger(0, slLblN, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, slLblN, OBJPROP_ANCHOR, isBuy?ANCHOR_LEFT_UPPER:ANCHOR_LEFT_LOWER); string iTxt = StringFormat("Risk %.1f%% | %.0f:%.0f pips | %.1fR", InpRiskPercent, slPips, tpPips, rr); if (ObjectFind(0, infoLblN) < 0) ObjectCreate(0, infoLblN, OBJ_TEXT, 0, lblT, (entry+sl)/2.0); ObjectMove(0, infoLblN, 0, lblT, (entry+sl)/2.0); ObjectSetString(0, infoLblN, OBJPROP_TEXT, iTxt); ObjectSetInteger(0, infoLblN, OBJPROP_COLOR, C'160,160,160'); ObjectSetInteger(0, infoLblN, OBJPROP_FONTSIZE, 7); ObjectSetInteger(0, infoLblN, OBJPROP_ANCHOR, ANCHOR_LEFT); } //---------------------------------------------------------------------- // DrawOneFVGRecord – MiddleTF FVG rectangle + midline + label //---------------------------------------------------------------------- void DrawOneFVGRecord(int idx) { if (!InpDebugDraw || idx < 0 || idx >= g_FVGCount) return; string sid = IntegerToString(g_FVGPool[idx].id); string rectN = FVGP_PREFIX + "RECT_" + sid; string midN = FVGP_PREFIX + "MID_" + sid; string lblN = FVGP_PREFIX + "LBL_" + sid; datetime rectEnd; if (g_FVGPool[idx].status == FVG_PENDING) rectEnd = iTime(_Symbol, InpMiddleTF, 0); else if (g_FVGPool[idx].touchTime > 0) { int shift = MyBarShift(_Symbol, InpTriggerTF, g_FVGPool[idx].touchTime); rectEnd = (shift >= 0) ? iTime(_Symbol, InpTriggerTF, shift) : iTime(_Symbol, InpMiddleTF, 0); } else { int shift = MyBarShift(_Symbol, InpMiddleTF, g_FVGPool[idx].usedTime); rectEnd = (shift >= 0) ? iTime(_Symbol, InpMiddleTF, shift) : iTime(_Symbol, InpMiddleTF, 0); } if (rectEnd <= g_FVGPool[idx].createdTime) rectEnd = iTime(_Symbol, InpMiddleTF, 0); color fillColor; if (g_FVGPool[idx].status == FVG_PENDING) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,50,110' : C'90,25,0'; else if (g_FVGPool[idx].status == FVG_TOUCHED) fillColor = (g_FVGPool[idx].direction == DIR_UP) ? C'0,120,220' : C'220,75,0'; else if (g_FVGPool[idx].usedCase == 2) fillColor = C'0,100,0'; else if (g_FVGPool[idx].usedCase == 1) fillColor = C'70,0,0'; else fillColor = C'50,50,50'; if (ObjectFind(0, rectN) < 0) ObjectCreate(0, rectN, OBJ_RECTANGLE, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high, rectEnd, g_FVGPool[idx].low); ObjectSetInteger(0, rectN, OBJPROP_COLOR, fillColor); ObjectSetInteger(0, rectN, OBJPROP_FILL, true); ObjectSetInteger(0, rectN, OBJPROP_BACK, true); ObjectMove(0, rectN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high); ObjectMove(0, rectN, 1, rectEnd, g_FVGPool[idx].low); color midColor = (g_FVGPool[idx].status == FVG_USED) ? C'60,60,60' : clrSilver; if (ObjectFind(0, midN) < 0) ObjectCreate(0, midN, OBJ_TREND, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].mid, rectEnd, g_FVGPool[idx].mid); ObjectSetInteger(0, midN, OBJPROP_COLOR, midColor); ObjectSetInteger(0, midN, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, midN, OBJPROP_WIDTH, 1); ObjectSetInteger(0, midN, OBJPROP_RAY_RIGHT, false); ObjectMove(0, midN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].mid); ObjectMove(0, midN, 1, rectEnd, g_FVGPool[idx].mid); string sym = (g_FVGPool[idx].direction == DIR_UP) ? "▲" : "▼"; string stTxt = ""; if (g_FVGPool[idx].status == FVG_TOUCHED) stTxt = " [T]"; else if (g_FVGPool[idx].usedCase == 2) stTxt = " [TRIG]"; else if (g_FVGPool[idx].usedCase == 1) stTxt = " [BRK]"; else if (g_FVGPool[idx].status == FVG_USED) stTxt = " [EXP]"; if (ObjectFind(0, lblN) < 0) ObjectCreate(0, lblN, OBJ_TEXT, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high); ObjectMove(0, lblN, 0, g_FVGPool[idx].createdTime, g_FVGPool[idx].high); ObjectSetString(0, lblN, OBJPROP_TEXT, StringFormat("FVG#%d %s%s", g_FVGPool[idx].id, sym, stTxt)); ObjectSetInteger(0, lblN, OBJPROP_COLOR, fillColor); ObjectSetInteger(0, lblN, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, lblN, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); } void DrawFVGPool() { if (!InpDebugDraw) return; for (int i = 0; i < g_FVGCount; i++) DrawOneFVGRecord(i); } //---------------------------------------------------------------------- // DrawContextDebug – top-left info panel //---------------------------------------------------------------------- void DrawContextDebug() { if (!InpDebugDraw) return; #define LBL(name,txt,y,clr) \ if(ObjectFind(0,name)<0) ObjectCreate(0,name,OBJ_LABEL,0,0,0); \ ObjectSetInteger(0,name,OBJPROP_CORNER, CORNER_LEFT_UPPER); \ ObjectSetInteger(0,name,OBJPROP_XDISTANCE, 10); \ ObjectSetInteger(0,name,OBJPROP_YDISTANCE, y); \ ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 9); \ ObjectSetInteger(0,name,OBJPROP_COLOR, clr); \ ObjectSetString (0,name,OBJPROP_TEXT, txt); LBL("DBG_HDR", "── ICT EA v4 ──", 10, clrSilver) color cB = (g_Bias.bias==BIAS_UP)?clrLime:(g_Bias.bias==BIAS_DOWN)?clrTomato:(g_Bias.bias==BIAS_SIDEWAY)?clrOrange:clrGray; LBL("DBG_BIAS", StringFormat("Bias : %s", EnumToString(g_Bias.bias)), 34, cB) color cMT = (g_MiddleTrend.trend==DIR_UP)?clrLime:(g_MiddleTrend.trend==DIR_DOWN)?clrTomato:clrGray; LBL("DBG_MT", StringFormat("H1 : %s KL=%.5f", EnumToString(g_MiddleTrend.trend), g_MiddleTrend.keyLevel), 58, cMT) color cTT = (g_TriggerTrend.trend==DIR_UP)?clrLime:(g_TriggerTrend.trend==DIR_DOWN)?clrTomato:clrGray; LBL("DBG_TT", StringFormat("M5 : %s KL=%.5f", EnumToString(g_TriggerTrend.trend), g_TriggerTrend.keyLevel), 82, cTT) if (g_TriggerTrend.lastMssTime > 0) { color cM = (g_TriggerTrend.lastMssBreak==DIR_UP)?clrLime:clrTomato; LBL("DBG_MSS", StringFormat("M5MSS: %s entry=%.5f SL=%.5f @ %s", (g_TriggerTrend.lastMssBreak==DIR_UP)?"▲":"▼", g_TriggerTrend.lastMssLevel, g_TriggerTrend.mssSLSwing, TimeToString(g_TriggerTrend.lastMssTime, TIME_MINUTES)), 106, cM) } else ObjectDelete(0,"DBG_MSS"); double lostPct = g_DailyRisk.startBalance > 0 ? (g_DailyRisk.startBalance - g_DailyRisk.currentBalance) / g_DailyRisk.startBalance * 100.0 : 0.0; color cR = g_DailyRisk.limitHit?clrRed:(lostPct>InpMaxDailyLossPct*0.7?clrOrange:clrLime); LBL("DBG_RISK", StringFormat("Risk : %.2f%% / %.2f%%", lostPct, InpMaxDailyLossPct), 130, cR) color cS = (g_State==EA_IDLE)?clrSilver:(g_State==EA_WAIT_TOUCH)?clrOrange:(g_State==EA_WAIT_TRIGGER)?clrYellow:clrLime; LBL("DBG_ST", StringFormat("State: %s", EnumToString(g_State)), 154, cS) if (g_BlockReason != BLOCK_NONE) { LBL("DBG_BLK", StringFormat("Block: %s", EnumToString(g_BlockReason)), 178, clrTomato) } else ObjectDelete(0,"DBG_BLK"); int nP=0,nT=0,nU=0; for(int i=0;i= 0 && g_ActiveFVGIdx < g_FVGCount) { int ai = g_ActiveFVGIdx; LBL("DBG_ACT", StringFormat("Act : #%d %s [%.5f–%.5f] %s", g_FVGPool[ai].id, EnumToString(g_FVGPool[ai].direction), g_FVGPool[ai].low, g_FVGPool[ai].high, EnumToString(g_FVGPool[ai].status)), 226, clrDeepSkyBlue) } else ObjectDelete(0,"DBG_ACT"); if (g_PendingTicket > 0 && g_OrderPlan.valid) { LBL("DBG_ORD", StringFormat("Order: %s #%llu @ %.5f SL=%.5f TP=%.5f", (g_OrderPlan.direction>0)?"BUY":"SELL", g_PendingTicket, g_OrderPlan.entry, g_OrderPlan.stopLoss, g_OrderPlan.takeProfit), 250, clrGold) } else ObjectDelete(0,"DBG_ORD"); #undef LBL ChartRedraw(0); } //---------------------------------------------------------------------- // DrawVisuals – master draw function //---------------------------------------------------------------------- void DrawVisuals() { DrawMiddleSwingPoints(); // H1 swing H0/H1/L0/L1 + keyLevel dashed line DrawTriggerSwingPoints(); // M5 swing tH0/tH1/tL0/tL1 + keyLevel dashed line DrawMSSMarkers(); // MSS arrow + label + broken keyLevel line (cả H1 và M5) DrawFVGPool(); // MiddleTF FVG rectangles (PENDING=dark, TOUCHED=bright, USED=status color) DrawOrderVisualization(); // TradingView-style: TP zone (green) + SL zone (red) + entry/SL/TP lines DrawContextDebug(); // Top-left info panel: bias, trend, MSS, risk, state, pool, order } //+------------------------------------------------------------------+ //| SECTION 12 – EA LIFECYCLE | //+------------------------------------------------------------------+ int OnInit() { ZeroMemory(g_Bias); ZeroMemory(g_MiddleTrend); ZeroMemory(g_TriggerTrend); ZeroMemory(g_DailyRisk); ZeroMemory(g_OrderPlan); for (int i = 0; i < MAX_FVG_POOL; i++) ZeroMemory(g_FVGPool[i]); g_FVGCount = 0; g_NextFVGId = 0; g_ActiveFVGIdx = -1; g_TradeBarIndex = -1; g_PendingTicket = 0; g_State = EA_IDLE; g_BlockReason = BLOCK_NONE; UpdateAllContexts(); ScanAndRegisterFVGs(); DrawVisuals(); PrintFormat("✅ ICT EA v4 | Bias=%s | H1=%s | M5=%s | Pool=%d", EnumToString(g_Bias.bias), EnumToString(g_MiddleTrend.trend), EnumToString(g_TriggerTrend.trend), g_FVGCount); return INIT_SUCCEEDED; } void OnTick() { UpdateAllContexts(); // 1. Cập nhật D1 bias, H1/M5 swing+MSS if (EvaluateGuards()) // 2. Check guards (session, risk, alignment) RunStateMachine(); // Pass → chạy state machine else if (g_State != EA_IDLE) // Fail + đang active → reset ResetToIdle(EnumToString(g_BlockReason)); // Log lý do block rồi về IDLE if (InpDebugDraw) DrawVisuals(); // 3. Vẽ chart objects (nếu enabled) } void OnDeinit(const int reason) { ObjectsDeleteAll(0, SW_PREFIX); // Xóa H1 swing objects (tạm thời) ObjectsDeleteAll(0, TS_PREFIX); // Xóa M5 swing objects (tạm thời) ObjectsDeleteAll(0, DBG_PREFIX); // Xóa debug panel (giữ lại: FVGP_, MSS_, ORD_) ChartRedraw(0); // Force redraw chart PrintFormat("ICT EA v4 deinit | reason=%d | pool=%d", reason, g_FVGCount); }