//+------------------------------------------------------------------+ //| ICT Early Trend Detection EA | //| Structure + Early Key-Level Break (MSS-style) | //| Clean Architecture – Meaningful Naming | //| Author: Bell CW | //+------------------------------------------------------------------+ #property strict //==================================================== // DEBUG CONFIG //==================================================== input bool Debug_Session = false; input bool Debug_HTFBias = false; input bool Debug_Trend = false; input bool Debug_OB = false; input bool Debug_TriggerTF = false; input bool Debug_Order = true; input bool Debug_DrawOnChart = true; input int TriggerMaxScanBars = 30; enum ENUM_OB_STATE { OB_NONE = 0, // chưa có OB nào OB_CANDIDATE, // tìm được OB hợp lệ, CHƯA CHẠM OB_TOUCHED, // đã chạm (mitigated), chờ trigger OB_USED, // đã trade HOẶC bị invalidate (KẾT THÚC) }; enum ENUM_BLOCK_REASON { BLOCK_NONE, BLOCK_NO_OB, BLOCK_OB_NOT_TOUCHED, BLOCK_BIAS_MISMATCH, BLOCK_SESSION, BLOCK_DAILY_LOSS }; ENUM_BLOCK_REASON g_BlockReason = BLOCK_NONE; enum ENUM_OB_RESET_REASON { OB_RESET_NONE, OB_RESET_BIAS_MISMATCH, OB_RESET_EXPIRED, OB_RESET_BAR_TIMEOUT, OB_RESET_CLOSE_BREAK, OB_RESET_INVALIDATED }; ENUM_OB_RESET_REASON g_LastOBResetReason; enum ENUM_EA_STATE { EA_STATE_IDLE, EA_STATE_WAIT_OB, EA_STATE_WAIT_TRIGGER, EA_STATE_ORDER_SENT }; ENUM_EA_STATE g_EAState = EA_STATE_IDLE; int g_OrderSentBarIndex = -1; //==================================================== // INPUT PARAMETERS //==================================================== input ENUM_TIMEFRAMES BiasTimeframe = PERIOD_D1; // TF bias cao hơn Trend TF input ENUM_TIMEFRAMES TrendTimeframe = PERIOD_H1; // Timeframe xác định trend input ENUM_TIMEFRAMES TriggerTimeframe = PERIOD_M5; // Timeframe kích hoạt logic input int SwingDetectionRange = 2; // Số nến trái/phải xác định swing input int StructureScanLookbackBars = 50; // Số nến quét structure input int OBMaxAliveMinutes = 180; // OB sống tối đa 180 phút //==================================================== // SESSION FILTER (SERVER TIME) //==================================================== // London Session input int LondonStartHour = 8; input int LondonEndHour = 17; input int LondonAvoidLastMin = 60; // tránh 60 phút cuối phiên // New York Session input int NewYorkStartHour = 13; input int NewYorkEndHour = 22; input int NewYorkAvoidLastMin = 60; // tránh 60 phút cuối phiên input double RiskPercent = 1.0; // % vốn rủi ro mỗi lệnh input double RiskReward = 2.0; // R:R = 1:2 input double MaxDailyLossPercent = 3.0; // Dừng lệnh nếu lỗ vượt 3% vốn trong ngày //==================================================== // DATA STRUCTURE //==================================================== struct TrendState { int trendDirection; // 1 = uptrend, -1 = downtrend, 0 = neutral double latestSwingHigh; // Swing High mới nhất double previousSwingHigh; // Swing High trước đó double latestSwingLow; // Swing Low mới nhất double previousSwingLow; // Swing Low trước đó int latestSwingHighIndex; int previousSwingHighIndex; int latestSwingLowIndex; int previousSwingLowIndex; double currentKeyLevel; // Key level theo ICT structure bool earlyFlipUsedThisBar; // Đã dùng early flip trong HTF bar này chưa datetime lastStructureUpdateTime; // Thời gian HTF bar cuối cùng đã xử lý }; //==================================================== // GLOBAL STATE //==================================================== TrendState g_TrendState; enum ENUM_HTF_BIAS { HTF_BIAS_NONE = 0, HTF_BIAS_UP, HTF_BIAS_DOWN, HTF_BIAS_SIDEWAY }; struct HTFBiasState { ENUM_HTF_BIAS bias; double rangeHigh; double rangeLow; datetime lastUpdateTime; }; HTFBiasState g_HTFBias; struct OBWatchState { int direction; // 1 = buy, -1 = sell ENUM_OB_STATE state; datetime touchTime; // 🔥 thời điểm chạm đầu tiên double obHigh; double obLow; datetime createdTime; int barsAlive; // số bar trigger TF đã trôi qua int triggerStartBar; }; OBWatchState g_OBWatch; struct OrderPlan { bool valid; int direction; // 1 = buy, -1 = sell double entry; double stopLoss; double takeProfit; double riskPoints; double lot; }; struct TriggerTFStructure { bool valid; int direction; // 1 = up, -1 = down double lastSwingHigh; double lastSwingLow; int lastSwingHighIndex; int lastSwingLowIndex; double breakLevel; // level sẽ bị phá double newKeyLevel; // 🔥 key level mới sau break }; TriggerTFStructure g_TriggerTF; struct DailyRiskState { double startBalance; datetime dayStartTime; bool lossLimitHit; }; DailyRiskState g_DailyRisk; void UpdateHTFBias( ENUM_TIMEFRAMES biasTf, HTFBiasState &state) { datetime tf0Time = iTime(_Symbol, biasTf, 0); if (tf0Time == state.lastUpdateTime) return; state.lastUpdateTime = tf0Time; // --- Bar 1 và Bar 2 đã đóng double b1Open = iOpen(_Symbol, biasTf, 1); double b1Close = iClose(_Symbol, biasTf, 1); double b1High = iHigh(_Symbol, biasTf, 1); double b1Low = iLow(_Symbol, biasTf, 1); double b2High = iHigh(_Symbol, biasTf, 2); double b2Low = iLow(_Symbol, biasTf, 2); state.rangeHigh = 0; state.rangeLow = 0; // ===== UP BIAS ===== if (b1Close > b2High) { state.bias = HTF_BIAS_UP; return; } // ===== DOWN BIAS ===== if (b1Close < b2Low) { state.bias = HTF_BIAS_DOWN; return; } // ===== SIDEWAY: inside bar ===== bool insideBar = b1High <= b2High && b1Low >= b2Low; if (insideBar) { state.bias = HTF_BIAS_SIDEWAY; state.rangeHigh = b2High; state.rangeLow = b2Low; return; } // ===== SIDEWAY: indecision ===== bool indecision = b1Close < b2High && b1Close > b2Low; if (indecision) { state.bias = HTF_BIAS_SIDEWAY; state.rangeHigh = b1High; state.rangeLow = b1Low; return; } state.bias = HTF_BIAS_NONE; } bool IsHTFBiasAligned(ENUM_HTF_BIAS bias, int trendDirection) { if (bias == HTF_BIAS_UP && trendDirection == 1) return true; if (bias == HTF_BIAS_DOWN && trendDirection == -1) return true; return false; } bool IsDailyLossExceeded( DailyRiskState &risk, double maxLossPercent) { if (risk.lossLimitHit) return true; double balance = AccountInfoDouble(ACCOUNT_BALANCE); double lossPct = (risk.startBalance - balance) / risk.startBalance * 100.0; if (lossPct >= maxLossPercent) { risk.lossLimitHit = true; PrintFormat( "⛔ MAX DAILY LOSS HIT | Loss=%.2f%% (Limit=%.2f%%)", lossPct, maxLossPercent); return true; } return false; } void UpdateDailyRiskState(DailyRiskState &risk) { // Thời gian mở nến D1 hiện tại datetime dailyBarTime = iTime(_Symbol, PERIOD_D1, 0); if (dailyBarTime != risk.dayStartTime) { risk.dayStartTime = dailyBarTime; risk.startBalance = AccountInfoDouble(ACCOUNT_BALANCE); risk.lossLimitHit = false; } } void BootstrapHTFBias( ENUM_TIMEFRAMES biasTf, HTFBiasState &state) { // ép reset state.lastUpdateTime = 0; // đảm bảo đủ data if (Bars(_Symbol, biasTf) < 5) return; UpdateHTFBias(biasTf, state); } void BootstrapMarketStructure( ENUM_TIMEFRAMES tf, TrendState &state) { ZeroMemory(state); state.trendDirection = 0; state.lastStructureUpdateTime = 0; int maxBar = MathMin(StructureScanLookbackBars, Bars(_Symbol, tf) - SwingDetectionRange - 2); double highs[2], lows[2]; int hiIdx[2], loIdx[2]; int hc = 0, lc = 0; for (int i = SwingDetectionRange + 1; i <= maxBar; i++) { if (hc < 2 && IsSwingHighAtBar(tf, i)) { highs[hc] = iHigh(_Symbol, tf, i); hiIdx[hc] = i; hc++; } if (lc < 2 && IsSwingLowAtBar(tf, i)) { lows[lc] = iLow(_Symbol, tf, i); loIdx[lc] = i; lc++; } if (hc == 2 && lc == 2) break; } if (hc < 2 || lc < 2) return; state.latestSwingHigh = highs[0]; state.previousSwingHigh = highs[1]; state.latestSwingLow = lows[0]; state.previousSwingLow = lows[1]; state.latestSwingHighIndex = hiIdx[0]; state.previousSwingHighIndex = hiIdx[1]; state.latestSwingLowIndex = loIdx[0]; state.previousSwingLowIndex = loIdx[1]; // interpret structure if (state.previousSwingHigh < state.latestSwingHigh && state.previousSwingLow < state.latestSwingLow) { state.trendDirection = 1; state.currentKeyLevel = state.latestSwingLow; } else if (state.previousSwingHigh > state.latestSwingHigh && state.previousSwingLow > state.latestSwingLow) { state.trendDirection = -1; state.currentKeyLevel = state.latestSwingHigh; } } bool BootstrapOBCandidate( ENUM_TIMEFRAMES tf, const TrendState &trend, OBWatchState &ob) { double h, l; int idx; if (!FindTrendOrderBlock(tf, trend, h, l, idx)) return false; ob.state = OB_CANDIDATE; // candidate ob.direction = trend.trendDirection; ob.obHigh = h; ob.obLow = l; ob.createdTime = iTime(_Symbol, tf, idx); ob.barsAlive = 0; ob.triggerStartBar = 1; return true; } void DrawInitialContext() { DrawMarketStructure(TrendTimeframe, g_TrendState); DrawTrendSummaryLabel(g_TrendState); DrawEAStateOverlay(); } //==================================================== // INITIALIZATION //==================================================== int OnInit() { // =============================== // 0️⃣ RESET TOÀN BỘ STATE // =============================== ZeroMemory(g_TrendState); ZeroMemory(g_HTFBias); ZeroMemory(g_OBWatch); ZeroMemory(g_TriggerTF); ZeroMemory(g_DailyRisk); g_EAState = EA_STATE_IDLE; // =============================== // 1️⃣ ENSURE HISTORY READY // =============================== if (Bars(_Symbol, TrendTimeframe) < StructureScanLookbackBars + 10 || Bars(_Symbol, BiasTimeframe) < 5) { Print("⏳ Not enough historical data to initialize EA"); return INIT_FAILED; } // =============================== // 2️⃣ BOOTSTRAP HTF BIAS // =============================== BootstrapHTFBias(BiasTimeframe, g_HTFBias); if (Debug_HTFBias) PrintFormat("INIT | HTF Bias = %d", g_HTFBias.bias); // =============================== // 3️⃣ BOOTSTRAP TREND STRUCTURE // =============================== BootstrapMarketStructure(TrendTimeframe, g_TrendState); if (!HasConfirmedMarketStructure(g_TrendState)) { Print("⚠ INIT | Market structure not confirmed yet"); return INIT_SUCCEEDED; // vẫn cho EA chạy, nhưng không trade } if (Debug_Trend) { PrintFormat( "INIT | Trend=%d | H0=%.2f | H1=%.2f | L0=%.2f | L1=%.2f", g_TrendState.trendDirection, g_TrendState.latestSwingHigh, g_TrendState.previousSwingHigh, g_TrendState.latestSwingLow, g_TrendState.previousSwingLow ); } // =============================== // 4️⃣ BOOTSTRAP OB CANDIDATE // =============================== if (IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection)) { bool obFound = BootstrapOBCandidate( TrendTimeframe, g_TrendState, g_OBWatch); if (obFound) { g_EAState = EA_STATE_WAIT_OB; if (Debug_OB) PrintFormat( "INIT | OB Candidate [%0.2f - %0.2f]", g_OBWatch.obLow, g_OBWatch.obHigh ); } else { if (Debug_OB) Print("INIT | No valid OB candidate found"); } } else { if (Debug_HTFBias) Print("INIT | Bias not aligned with trend → skip OB bootstrap"); } Print("✅ ICT Early Trend Detection EA initialized (BOOTSTRAPPED)"); DrawInitialContext(); return INIT_SUCCEEDED; } //==================================================== // BAR UTILITY //==================================================== bool IsNewBarFormed(ENUM_TIMEFRAMES timeframe, datetime &lastBarTime) { datetime currentBarTime = iTime(_Symbol, timeframe, 0); if (currentBarTime != lastBarTime) { lastBarTime = currentBarTime; return true; } return false; } //==================================================== // SWING DETECTION (GENERIC) //==================================================== bool IsSwingHighAtBar( ENUM_TIMEFRAMES timeframe, int barIndex) { double highPrice = iHigh(_Symbol, timeframe, barIndex); for (int offset = 1; offset <= SwingDetectionRange; offset++) { if (iHigh(_Symbol, timeframe, barIndex - offset) >= highPrice) return false; if (iHigh(_Symbol, timeframe, barIndex + offset) >= highPrice) return false; } return true; } bool IsSwingLowAtBar( ENUM_TIMEFRAMES timeframe, int barIndex) { double lowPrice = iLow(_Symbol, timeframe, barIndex); for (int offset = 1; offset <= SwingDetectionRange; offset++) { if (iLow(_Symbol, timeframe, barIndex - offset) <= lowPrice) return false; if (iLow(_Symbol, timeframe, barIndex + offset) <= lowPrice) return false; } return true; } //==================================================== // STRUCTURE VALIDATION //==================================================== bool HasConfirmedMarketStructure(const TrendState &state) { return ( state.latestSwingHigh > 0 && state.previousSwingHigh > 0 && state.latestSwingLow > 0 && state.previousSwingLow > 0); } //==================================================== // STRUCTURE UPDATE (TIMEFRAME AGNOSTIC) //==================================================== void UpdateMarketStructure( ENUM_TIMEFRAMES timeframe, TrendState &state) { datetime currentStructureBarTime = iTime(_Symbol, timeframe, 0); if (currentStructureBarTime == state.lastStructureUpdateTime) return; state.lastStructureUpdateTime = currentStructureBarTime; state.earlyFlipUsedThisBar = false; double detectedSwingHighs[2]; double detectedSwingLows[2]; int detectedSwingHighIndices[2]; int detectedSwingLowIndices[2]; int highCount = 0; int lowCount = 0; for (int barIndex = SwingDetectionRange + 1; barIndex < StructureScanLookbackBars; barIndex++) { if (highCount < 2 && IsSwingHighAtBar(timeframe, barIndex)) { detectedSwingHighs[highCount] = iHigh(_Symbol, timeframe, barIndex); detectedSwingHighIndices[highCount] = barIndex; highCount++; } if (lowCount < 2 && IsSwingLowAtBar(timeframe, barIndex)) { detectedSwingLows[lowCount] = iLow(_Symbol, timeframe, barIndex); detectedSwingLowIndices[lowCount] = barIndex; lowCount++; } if (highCount == 2 && lowCount == 2) break; } if (highCount < 2 || lowCount < 2) { if(Debug_Trend) Print("⏳ Waiting for enough swing points..."); return; } state.latestSwingHigh = detectedSwingHighs[0]; state.previousSwingHigh = detectedSwingHighs[1]; state.latestSwingLow = detectedSwingLows[0]; state.previousSwingLow = detectedSwingLows[1]; state.latestSwingHighIndex = detectedSwingHighIndices[0]; state.previousSwingHighIndex = detectedSwingHighIndices[1]; state.latestSwingLowIndex = detectedSwingLowIndices[0]; state.previousSwingLowIndex = detectedSwingLowIndices[1]; // ===== STRUCTURE INTERPRETATION ===== if (state.previousSwingHigh < state.latestSwingHigh && state.previousSwingLow < state.latestSwingLow) { state.trendDirection = 1; state.currentKeyLevel = state.latestSwingLow; } else if (state.previousSwingHigh > state.latestSwingHigh && state.previousSwingLow > state.latestSwingLow) { state.trendDirection = -1; state.currentKeyLevel = state.latestSwingHigh; } } //==================================================== // EARLY TREND FLIP (MSS-STYLE) //==================================================== void CheckForEarlyTrendFlip( TrendState &state, double currentPrice) { if (!HasConfirmedMarketStructure(state)) return; if (state.earlyFlipUsedThisBar) return; if (state.trendDirection == 1 && currentPrice < state.currentKeyLevel) { state.trendDirection = -1; state.currentKeyLevel = state.latestSwingHigh; state.earlyFlipUsedThisBar = true; } else if (state.trendDirection == -1 && currentPrice > state.currentKeyLevel) { state.trendDirection = 1; state.currentKeyLevel = state.latestSwingLow; state.earlyFlipUsedThisBar = true; } } //==================================================== // SESSION GUARD //==================================================== bool IsWithinSession( int currentMinutes, int sessionStartHour, int sessionEndHour, int avoidLastMinutes) { int sessionStartMin = sessionStartHour * 60; int sessionEndMin = sessionEndHour * 60; // Trong phiên if (currentMinutes < sessionStartMin || currentMinutes >= sessionEndMin) return false; // Tránh cuối phiên if (currentMinutes >= (sessionEndMin - avoidLastMinutes)) return false; return true; } bool IsTradingSessionAllowed() { datetime now = TimeCurrent(); MqlDateTime tm; TimeToStruct(now, tm); int currentMinutes = tm.hour * 60 + tm.min; if (IsWithinSession(currentMinutes, LondonStartHour, LondonEndHour, LondonAvoidLastMin)) return true; if (IsWithinSession(currentMinutes, NewYorkStartHour, NewYorkEndHour, NewYorkAvoidLastMin)) return true; return false; } bool HasPriceTouchedOB( int direction, double obHigh, double obLow, double price) { if (direction == 1) // buy return price <= obHigh && price >= obLow; if (direction == -1) // sell return price >= obLow && price <= obHigh; return false; } bool FindTrendOrderBlock( ENUM_TIMEFRAMES timeframe, const TrendState &trend, double &obHigh, double &obLow, int &obCandleIndex) { obHigh = 0; obLow = 0; obCandleIndex = -1; // ===== XÁC ĐỊNH VÙNG QUÉT ===== int scanFrom, scanTo; if (trend.trendDirection == 1) { // Uptrend: OB phải nằm giữa L1 -> L0 scanFrom = trend.latestSwingLowIndex - 1; scanTo = trend.previousSwingLowIndex + 1; } else if (trend.trendDirection == -1) { // Downtrend: OB phải nằm giữa H1 -> H0 scanFrom = trend.latestSwingHighIndex - 1; scanTo = trend.previousSwingHighIndex + 1; } else return false; scanFrom = MathMax(scanFrom, 1); scanTo = MathMax(scanTo, 1); // ===== QUÉT OB ===== for (int i = scanFrom; i >= scanTo; i--) { double open = iOpen(_Symbol, timeframe, i); double close = iClose(_Symbol, timeframe, i); // ===== NẾN NGƯỢC TREND ===== bool isBear = close < open; bool isBull = close > open; if ((trend.trendDirection == 1 && !isBear) || (trend.trendDirection == -1 && !isBull)) continue; double h = iHigh(_Symbol, timeframe, i); double l = iLow(_Symbol, timeframe, i); // ===== RANGE FILTER ===== if (MathAbs(h - l) < 10 * _Point) continue; // ===== FILTER 1: OB PHẢI NẰM TRONG CON SÓNG ===== if (trend.trendDirection == 1 && l < trend.latestSwingLow) continue; if (trend.trendDirection == -1 && h > trend.latestSwingHigh) continue; // ===== FILTER 2: CHƯA BỊ MITIGATION ===== bool mitigated = false; for (int j = i - 1; j >= 1; j--) { double price = iClose(_Symbol, timeframe, j); if (HasPriceTouchedOB(trend.trendDirection, h, l, price)) { mitigated = true; break; } } if (mitigated) continue; // ===== FILTER 3: CHƯA BỊ CLOSE BREAK (HTF) ===== if (IsOBCloseBreak(trend.trendDirection, h, l)) continue; // ===== OK → OB HỢP LỆ ===== obHigh = h; obLow = l; obCandleIndex = i; return true; } return false; } void ResetOBWithReason(ENUM_OB_RESET_REASON reason) { g_LastOBResetReason = reason; ResetOBWatch(); ResetTriggerTFStructure(); g_EAState = EA_STATE_IDLE; } void ResetOBWatch() { g_OBWatch.state = OB_NONE; g_OBWatch.touchTime = 0; g_OBWatch.direction = 0; g_OBWatch.obHigh = 0; g_OBWatch.obLow = 0; g_OBWatch.createdTime = 0; DebugLabel("EA_OB_RESET", StringFormat("OB Reset Reason=%d", g_LastOBResetReason), 130, clrRed ); } bool IsOBInvalidated( int direction, double obHigh, double obLow, double price) { if (direction == 1) // buy return price < obLow; if (direction == -1) // sell return price > obHigh; return false; } void ResetTriggerTFStructure() { ZeroMemory(g_TriggerTF); } bool IsTriggerTFStructureBreak( const TriggerTFStructure &ts, int trendDirection, ENUM_TIMEFRAMES tf) { if (!ts.valid) return false; double high = iHigh(_Symbol, tf, 1); double low = iLow(_Symbol, tf, 1); if (trendDirection == 1) return high > ts.breakLevel; if (trendDirection == -1) return low < ts.breakLevel; return false; } void UpdateTriggerTFStructure( ENUM_TIMEFRAMES tf, TriggerTFStructure &ts, int startBar) { double sh = 0.0; double sl = 0.0; int shi = -1; int sli = -1; bool foundHigh = false; bool foundLow = false; for (int i = startBar - SwingDetectionRange; i >= MathMax(1, startBar - TriggerMaxScanBars); i--) { if (!foundHigh && IsSwingHighAtBar(tf, i)) { sh = iHigh(_Symbol, tf, i); shi = i; foundHigh = true; } if (!foundLow && IsSwingLowAtBar(tf, i)) { sl = iLow(_Symbol, tf, i); sli = i; foundLow = true; } if (foundHigh && foundLow) break; } if (!foundHigh || !foundLow) return; ts.lastSwingHigh = sh; ts.lastSwingLow = sl; ts.lastSwingHighIndex = shi; ts.lastSwingLowIndex = sli; ts.direction = g_TrendState.trendDirection; ts.valid = true; if (ts.direction == 1) { ts.breakLevel = ts.lastSwingHigh; ts.newKeyLevel = ts.lastSwingLow; } else { ts.breakLevel = ts.lastSwingLow; ts.newKeyLevel = ts.lastSwingHigh; } } bool IsOBExpired(const OBWatchState &ob) { if (ob.createdTime <= 0) return false; int aliveSec = (int)(TimeCurrent() - ob.createdTime); return aliveSec > OBMaxAliveMinutes * 60; } void MarkOBAsUsed() { g_OBWatch.state = OB_USED; } void MarkOBAsTouched( OBWatchState &ob, ENUM_TIMEFRAMES triggerTf) { if (ob.state != OB_CANDIDATE) return; ob.state = OB_TOUCHED; // Thời điểm chạm đầu tiên (bar đã đóng) ob.touchTime = iTime(_Symbol, triggerTf, 1); // Reset trigger waiting ob.barsAlive = 0; int shift = iBarShift(_Symbol, triggerTf, ob.touchTime); ob.triggerStartBar = (shift > 0 ? shift : 1); // Reset trigger TF structure ResetTriggerTFStructure(); if (Debug_OB) { PrintFormat( "🟦 OB TOUCHED | dir=%d | OB[%.2f - %.2f]", ob.direction, ob.obLow, ob.obHigh ); } } bool IsOBCloseBreak( int direction, double obHigh, double obLow) { double close = iClose(_Symbol, TrendTimeframe, 1); if (direction == 1) // buy OB return close < obLow; if (direction == -1) // sell OB return close > obHigh; return false; } OrderPlan BuildOrderPlanFromTriggerTF( const TriggerTFStructure &ts, ENUM_TIMEFRAMES tf) { OrderPlan plan; ZeroMemory(plan); plan.valid = true; plan.direction = ts.direction; plan.entry = (ts.direction == 1) ? g_OBWatch.obHigh // buy tại đỉnh OB : g_OBWatch.obLow; // sell tại đáy OB double riskPoints; // ===== BUY ===== if (ts.direction == 1) { plan.stopLoss = MathMin(ts.newKeyLevel, g_OBWatch.obLow); riskPoints = plan.entry - plan.stopLoss; if (riskPoints <= 0) { plan.valid = false; return plan; } plan.takeProfit = plan.entry + riskPoints * RiskReward; } // ===== SELL ===== else { plan.stopLoss = MathMax(ts.newKeyLevel, g_OBWatch.obHigh); riskPoints = plan.stopLoss - plan.entry; if (riskPoints <= 0) { plan.valid = false; return plan; } plan.takeProfit = plan.entry - riskPoints * RiskReward; } return plan; } double CalculateRiskLot( double entryPrice, double stopLossPrice) { double equity = AccountInfoDouble(ACCOUNT_EQUITY); double riskMoney = equity * RiskPercent / 100.0; double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); if (contractSize <= 0) return 0; double slDistance = MathAbs(entryPrice - stopLossPrice); if (slDistance <= _Point) return 0; // Cost cho 1 lot nếu SL hit double costPerLot = slDistance * contractSize; if (costPerLot <= 0) return 0; double rawLot = riskMoney / costPerLot; // ===== Normalize theo broker ===== double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); rawLot = MathMax(minLot, MathMin(rawLot, maxLot)); rawLot = MathFloor(rawLot / lotStep) * lotStep; return NormalizeDouble(rawLot, 2); } bool ExecuteOrder(const OrderPlan &plan) { if (!plan.valid) return false; MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_PENDING; req.symbol = _Symbol; req.volume = plan.lot; req.type = plan.direction == 1 ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT; req.price = plan.entry; req.sl = plan.stopLoss; req.tp = plan.takeProfit; req.deviation = 5; req.magic = 202501; req.comment = "ICT_OB_Trigger"; bool sent = OrderSend(req, res); if (!sent || res.retcode != TRADE_RETCODE_DONE) { PrintFormat( "❌ OrderSend failed | sent=%d | retcode=%d", sent, res.retcode); return false; } else { PrintFormat( "✅ ORDER SENT | lot=%.2f | entry=%.2f | SL=%.2f | TP=%.2f", plan.lot, plan.entry, plan.stopLoss, plan.takeProfit); return true; } } string GetOBRectName(const OBWatchState &ob) { return StringFormat("OB_RECT_%I64d", ob.createdTime); } string GetOBLabelName(const OBWatchState &ob) { return StringFormat("OB_LABEL_%I64d", ob.createdTime); } bool IsReadyToTrade() { if (g_OBWatch.state != OB_TOUCHED) return false; if (!g_TriggerTF.valid) return false; if (!IsTriggerTFStructureBreak( g_TriggerTF, g_TrendState.trendDirection, TriggerTimeframe)) return false; return true; } bool TryExecuteTradeIfReady() { // ===================================== // 0️⃣ CHƯA SẴN SÀNG // ===================================== if (!IsReadyToTrade()) return false; // ===================================== // 1️⃣ BUILD ORDER PLAN // ===================================== OrderPlan plan = BuildOrderPlanFromTriggerTF( g_TriggerTF, TriggerTimeframe); if (!plan.valid) return false; plan.lot = CalculateRiskLot( plan.entry, plan.stopLoss); if (plan.lot <= 0) return false; // ===================================== // 2️⃣ EXECUTE ORDER (DUY NHẤT Ở ĐÂY) // ===================================== bool isExecuted = ExecuteOrder(plan); if (Debug_Order) { PrintFormat( "🚀 ORDER EXECUTED | dir=%d | entry=%.2f | SL=%.2f | TP=%.2f", plan.direction, plan.entry, plan.stopLoss, plan.takeProfit); } return isExecuted; } // Kiểm tra có đủ điều kiện tạo OB không void StateIdle() { if (!HasConfirmedMarketStructure(g_TrendState)) return; if (!IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection)) return; if (BootstrapOBCandidate(TrendTimeframe, g_TrendState, g_OBWatch)) { g_EAState = EA_STATE_WAIT_OB; } } // Theo dõi OB chờ chạm void StateWaitOB() { double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); // invalidate / expire if (IsOBCloseBreak(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow) || IsOBInvalidated(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow, price) || IsOBExpired(g_OBWatch)) { ResetOBWithReason(OB_RESET_INVALIDATED); return; } // touch if (HasPriceTouchedOB( g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow, price)) { MarkOBAsTouched(g_OBWatch, TriggerTimeframe); g_EAState = EA_STATE_WAIT_TRIGGER; } } // Theo dõi trigger timeframe chờ tín hiệu vào lệnh void StateWaitTrigger() { double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); // OB bị phá if (IsOBCloseBreak(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow) || IsOBInvalidated(g_OBWatch.direction, g_OBWatch.obHigh, g_OBWatch.obLow, price)) { ResetOBWithReason(OB_RESET_INVALIDATED); return; } static datetime lastTriggerBarForAlive = 0; datetime currentTriggerBar = iTime(_Symbol, TriggerTimeframe, 0); if (currentTriggerBar != lastTriggerBarForAlive) { lastTriggerBarForAlive = currentTriggerBar; if (g_OBWatch.state == OB_TOUCHED) g_OBWatch.barsAlive++; } if (g_OBWatch.barsAlive > TriggerMaxScanBars) { ResetOBWithReason(OB_RESET_EXPIRED); return; } static datetime lastTriggerBar = 0; datetime currentBar = iTime(_Symbol, TriggerTimeframe, 0); if (currentBar != lastTriggerBar) { lastTriggerBar = currentBar; UpdateTriggerTFStructure(TriggerTimeframe, g_TriggerTF, g_OBWatch.triggerStartBar); } if (IsReadyToTrade()) { bool isExecutedSuccess = TryExecuteTradeIfReady(); if (isExecutedSuccess) { MarkOBAsUsed(); g_EAState = EA_STATE_ORDER_SENT; g_OrderSentBarIndex = Bars(_Symbol, TriggerTimeframe); // lưu lại bar index khi lệnh được gửi } } } void StateOrderSent() { int currentBar = Bars(_Symbol, TriggerTimeframe); // ❌ Không cho reset trong cùng bar vừa gửi lệnh if (currentBar <= g_OrderSentBarIndex) return; if (!PositionSelect(_Symbol) && OrdersTotal() == 0) { g_EAState = EA_STATE_IDLE; } } void UpdateContext() { UpdateDailyRiskState(g_DailyRisk); UpdateHTFBias(BiasTimeframe, g_HTFBias); UpdateMarketStructure(TrendTimeframe, g_TrendState); static datetime lastTrendBarTime = 0; datetime currentTrendBarTime = iTime(_Symbol, TrendTimeframe, 0); if (currentTrendBarTime != lastTrendBarTime) { lastTrendBarTime = currentTrendBarTime; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); CheckForEarlyTrendFlip(g_TrendState, bid); } } bool EvaluateTradePermission() { g_BlockReason = BLOCK_NONE; if (!IsTradingSessionAllowed()) { g_BlockReason = BLOCK_SESSION; return false; } if (IsDailyLossExceeded(g_DailyRisk, MaxDailyLossPercent)) { g_BlockReason = BLOCK_DAILY_LOSS; return false; } if (!IsHTFBiasAligned(g_HTFBias.bias, g_TrendState.trendDirection)) { g_BlockReason = BLOCK_BIAS_MISMATCH; return false; } return true; } void RunStateMachine() { switch (g_EAState) { case EA_STATE_IDLE: StateIdle(); break; case EA_STATE_WAIT_OB: StateWaitOB(); break; case EA_STATE_WAIT_TRIGGER: StateWaitTrigger(); break; case EA_STATE_ORDER_SENT: StateOrderSent(); break; } } void ResetOBIfBlocked() { if (g_EAState == EA_STATE_WAIT_OB || g_EAState == EA_STATE_WAIT_TRIGGER) { ResetOBWithReason(OB_RESET_BIAS_MISMATCH); } } void OnTick() { UpdateContext(); bool allowStateMachine = EvaluateTradePermission(); if (allowStateMachine) RunStateMachine(); else ResetOBIfBlocked(); DrawVisuals(); } void DebugPrint(bool enabled, string message) { if (!enabled) return; Print(message); } //---------------------------------------------------- void DebugLabel(string name, string text, int y, color clr) { if (!Debug_DrawOnChart) return; ObjectDelete(0, name); ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_LOWER); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 8); ObjectSetString(0, name, OBJPROP_TEXT, text); } //==================================================== // DRAWING UTILITIES //==================================================== void DrawVisuals() { DrawMarketStructure(TrendTimeframe, g_TrendState); DrawTrendSummaryLabel(g_TrendState); DrawOrUpdateOBRectangle(g_OBWatch); DrawOrUpdateOBLabel(g_OBWatch); DrawEAStateOverlay(); DrawBlockReasonOverlay(); } void DrawEAStateOverlay() { string stepText; color stepColor = clrGray; switch (g_EAState) { case EA_STATE_IDLE: stepText = "STEP 0: IDLE / GUARD"; stepColor = clrSilver; break; case EA_STATE_WAIT_OB: stepText = "STEP 1: WAIT OB TOUCH"; stepColor = clrOrange; break; case EA_STATE_WAIT_TRIGGER: stepText = "STEP 2: WAIT TRIGGER"; stepColor = clrYellow; break; case EA_STATE_ORDER_SENT: stepText = "STEP 3: ORDER SENT"; stepColor = clrLime; break; } DebugLabel("EA_STATE", stepText, 40, stepColor); DebugLabel("EA_TREND", StringFormat("Trend=%d | Bias=%d", g_TrendState.trendDirection, g_HTFBias.bias), 70, clrWhite); if (g_OBWatch.state == OB_CANDIDATE) { DebugLabel("EA_OB", StringFormat("OB [%0.2f - %0.2f] alive=%d", g_OBWatch.obLow, g_OBWatch.obHigh, g_OBWatch.barsAlive), 100, clrOrange); } } void DrawSwingLabel( string objectName, string labelText, ENUM_TIMEFRAMES timeframe, int barIndex, double price, color labelColor) { if (barIndex < 0 || price <= 0) return; datetime barTime = iTime(_Symbol, timeframe, barIndex); ObjectDelete(0, objectName); ObjectCreate(0, objectName, OBJ_TEXT, 0, barTime, price); ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor); ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 9); ObjectSetString(0, objectName, OBJPROP_TEXT, labelText); // ===== PIXEL OFFSET ===== int SwingLabelYOffsetPx = 20; if (labelText == "H0" || labelText == "H1") { ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_BOTTOM); ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, SwingLabelYOffsetPx); } else if (labelText == "L0" || labelText == "L1") { ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_TOP); ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, SwingLabelYOffsetPx); } } void DrawMarketStructure( ENUM_TIMEFRAMES timeframe, const TrendState &state) { DrawSwingLabel("SWING_HIGH_LATEST", "H0", timeframe, state.latestSwingHighIndex, state.latestSwingHigh, clrLime); DrawSwingLabel("SWING_HIGH_PREVIOUS", "H1", timeframe, state.previousSwingHighIndex, state.previousSwingHigh, clrGreen); DrawSwingLabel("SWING_LOW_LATEST", "L0", timeframe, state.latestSwingLowIndex, state.latestSwingLow, clrRed); DrawSwingLabel("SWING_LOW_PREVIOUS", "L1", timeframe, state.previousSwingLowIndex, state.previousSwingLow, clrMaroon); } void DrawTrendSummaryLabel(const TrendState &state) { string labelName = "TREND_SUMMARY"; ObjectDelete(0, labelName); string labelText = "TREND: NEUTRAL"; color labelColor = clrGray; if (state.trendDirection == 1) { labelText = StringFormat( "UPTREND - Key Level: %.2f%s", state.currentKeyLevel, state.earlyFlipUsedThisBar ? " -> EARLY FLIP" : ""); labelColor = state.earlyFlipUsedThisBar ? clrOrange : clrLime; } else if (state.trendDirection == -1) { labelText = StringFormat( "DOWNTREND - Key Level: %.2f%s", state.currentKeyLevel, state.earlyFlipUsedThisBar ? " -> EARLY FLIP" : ""); labelColor = state.earlyFlipUsedThisBar ? clrOrange : clrRed; } ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, labelName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, 10); ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, 20); ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelColor); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9); ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); } void DrawOrUpdateOBRectangle(const OBWatchState &ob) { string rectName = GetOBRectName(ob); if (ob.state == OB_NONE) return; datetime endTime; if (ob.state == OB_CANDIDATE) endTime = iTime(_Symbol, TriggerTimeframe, 0); // kéo tới hiện tại else endTime = ob.touchTime; // cố định khi touched / used color clr; switch (ob.state) { case OB_CANDIDATE: clr = clrDodgerBlue; break; case OB_TOUCHED: clr = clrDeepSkyBlue; break; case OB_USED: clr = clrGray; break; default: return; } if (!ObjectFind(0, rectName)) { ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, ob.createdTime, ob.obHigh, endTime, ob.obLow); ObjectSetInteger(0, rectName, OBJPROP_BACK, true); ObjectSetInteger(0, rectName, OBJPROP_WIDTH, 1); ObjectSetInteger(0, rectName, OBJPROP_FILL, true); } ObjectSetInteger(0, rectName, OBJPROP_COLOR, clr); if (ob.state == OB_CANDIDATE) { ObjectMove(0, rectName, 1, endTime, ob.obLow); } } void DrawOrUpdateOBLabel(const OBWatchState &ob) { string labelName = GetOBLabelName(ob); if (ob.state == OB_NONE) return; string text; color clr; switch (ob.state) { case OB_CANDIDATE: text = "OB: CANDIDATE"; clr = clrDodgerBlue; break; case OB_TOUCHED: text = "OB: TOUCHED"; clr = clrDeepSkyBlue; break; case OB_USED: text = "OB: USED"; clr = clrGray; break; default: return; } datetime labelTime = ob.createdTime; double labelPrice = ob.obHigh; if (!ObjectFind(0, labelName)) { ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, labelPrice); } ObjectSetString(0, labelName, OBJPROP_TEXT, text); ObjectSetInteger(0, labelName, OBJPROP_COLOR, clr); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9); } string BlockReasonToText(ENUM_BLOCK_REASON reason) { switch (reason) { case BLOCK_SESSION: return "BLOCK: OUT OF SESSION"; case BLOCK_DAILY_LOSS: return "BLOCK: MAX DAILY LOSS"; case BLOCK_BIAS_MISMATCH: return "BLOCK: BIAS MISMATCH"; case BLOCK_NO_OB: return "BLOCK: NO VALID OB"; case BLOCK_OB_NOT_TOUCHED: return "BLOCK: OB NOT TOUCHED"; default: return ""; } } color BlockReasonToColor(ENUM_BLOCK_REASON reason) { switch (reason) { case BLOCK_SESSION: return clrOrangeRed; case BLOCK_DAILY_LOSS: return clrRed; case BLOCK_BIAS_MISMATCH: return clrOrange; case BLOCK_NO_OB: return clrGray; case BLOCK_OB_NOT_TOUCHED: return clrDodgerBlue; default: return clrNONE; } } void DrawBlockReasonOverlay() { if (!Debug_DrawOnChart) return; if (g_BlockReason == BLOCK_NONE) { ObjectDelete(0, "EA_BLOCK_REASON"); return; } string text = BlockReasonToText(g_BlockReason); color clr = BlockReasonToColor(g_BlockReason); ObjectDelete(0, "EA_BLOCK_REASON"); ObjectCreate(0, "EA_BLOCK_REASON", OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_CORNER, CORNER_LEFT_LOWER); ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_XDISTANCE, 10); ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_YDISTANCE, 160); ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_COLOR, clr); ObjectSetInteger(0, "EA_BLOCK_REASON", OBJPROP_FONTSIZE, 9); ObjectSetString(0, "EA_BLOCK_REASON", OBJPROP_TEXT, text); }