//+------------------------------------------------------------------+ //| EA: ICT v2 | //| Mục đích: Setup ICT đồng thuận trend 3 timeframes | //| Người tạo: Bell CW | //+------------------------------------------------------------------+ #property copyright "Bell CW" #property version "2.00" #property strict // ------------------ Các input về định lượng ------------------ input int LS_LookbackBars = 5; // số nến nhìn lại input double LS_Wick_ATR_Ratio = 0.1; // wick ≥ 10% ATR input double MSS_Break_ATR_Ratio = 0.1; // phá ≥ 10% ATR input double MSS_Body_ATR_Ratio = 0.1; // body ≥ 10% ATR input double FVG_Min_ATR_Ratio = 0.25; input int ADX_Min = 10; // ------------------------------------------------------------- double DailyEquityStart = 0.0; datetime DailyEquityDate = 0; bool DailyLossBlocked = false; input double EntryFvgRetrace = 0.3; // 0.5 = 50%, 0.618 = aggressive input double MaxDailyLossPercent = 3.0; // --- Only Entry logs (user requested) --- bool PrintEntryLog = true; // nếu true -> in log chỉ liên quan tới entry input double RishPercent = 1.0; // % vốn rủi ro cho mỗi lệnh input double RiskRewardRatio = 6.0; // tỉ lệ R:R mặc định input double numberOfRToMoveToEntry = 1; // Nomal moving SL: số R cần đạt để dời SL về entry (BE) input int MaxLimitOrderTime = 180; input double trailingStopStartR = 2; // Advanced moving SL: bắt đầu kích hoạt trailing input double trailOffsetR = 1.5; // Advanced moving SL: khoảng cách SL so với giá hiện tại // Cấu hình Swing input int htfSwingRange = 1; // D1/H4: chắc trend input int mtfSwingRange = 2; // H1: BOS rõ input int ltfSwingRange = 2; // M5: giảm fake MSS input int MaxSwingKeep = 2; // Số đỉnh/đáy gần nhất cần lưu (bạn yêu cầu 2) // Struct pending entry (single slot) struct PendingEntry { bool active; // đang có pending entry int direction; // 1 = buy (bull), -1 = sell (bear) double price; // entry price (edge của LTF internal FVG) int fvgIndex; // index của MTF FVG (watched) hoặc -1 nếu none string compositeName; // tên object composite vẽ (line|label) datetime created_time; // thời điểm tạo int source_slot; // slot nơi phát hiện MSS (HTF/MTF/LTF) double sl_price; // giá SL (dựa trên swing gần nhất) double tp_price; // giá TP (dựa trên swing gần nhất) double lotSize; // kích thước lot tính toán ulong orderTicket; // ticket lệnh đã mở (0 = chưa mở) }; // global pending entry variable PendingEntry pendingEntry; // --- Watch MSS mode (user requested) --- bool watchingMSSMode = false; // mặc định false int watchingFVGIndex = -1; // index trong FVG_* arrays (khi watchingMSSMode==true) int watchingFVGDir = 0; // 1 = bullish FVG, -1 = bearish FVG // --- Mảng trạng thái: MTF FVG đã bị chạm bởi LTF hay chưa --- bool MTF_FVG_Touched[]; // true = MTF FVG k đã bị LTF chạm datetime MTF_FVG_TouchTime[]; // thời điểm bar LTF đầu tiên chạm zone (datetime) double MTF_FVG_TouchPrice[]; // giá chạm đại diện (low khi bullish, high khi bearish) // Cấu hình vẽ Swing trên chart input bool ShowSwingMarkers = true; // Hiển thị các marker swing trên chart input string SwingObjPrefix = "Swing_"; // Tiền tố tên object (dễ xóa/điều chỉnh) input int SwingMarkerFontSize = 10; // Kích thước font cho label input int FVGLookback = 200; // Số nến lookback khi tính FVG (dùng ở EnsureFVGUpToDate) input bool PrintToExperts = false; // không dùng — logs đã được chọn lại // --- lưu trend đã tính cho mỗi slot --- // slot 0 = HTF, 1 = MTF, 2 = LTF int TrendTF[3]; // 1 = up, -1 = down, 0 = sideway / unknown input ENUM_TIMEFRAMES HighTF = PERIOD_W1; // Khung thời gian cao input ENUM_TIMEFRAMES MiddleTF = PERIOD_H4; // Khung thời gian trung bình (dùng để tìm FVG) input ENUM_TIMEFRAMES LowTF = PERIOD_M15; // Khung thời gian thấp (Tìm điểm vào lệnh) // --- multi-TF swing storage (slots) --- // slot 0 = HighTF, slot 1 = MiddleTF, slot 2 = LowTF // dimension: [slot][index] (index 0 = nearest, 1 = older) double SwingHighPriceTF[3][2]; datetime SwingHighTimeTF[3][2]; int SwingHighCountTF[3]; double SwingLowPriceTF[3][2]; datetime SwingLowTimeTF[3][2]; int SwingLowCountTF[3]; // --- cấu hình cho trend provisional (phát hiện sớm) --- input int ProvisionalBreakPips = 5; // số pips để coi là phá sớm (change-as-you-like) input int ProvisionalConsecCloses = 1; // số nến đóng liên tiếp vượt level để xác nhận provisional (thường 1 hoặc 2) // --- FVG struct & storage (replace old separate arrays) --- struct FVG { int type; // 1 = bullish (gap below price), -1 = bearish (gap above price) double topPrice; // top edge (higher price) double bottomPrice; // bottom edge (lower price) datetime timebarA; // time of bar A (older) datetime timebarC; // time of bar C (newer) bool touched; // whether LTF touched this MTF FVG yet (first-touch) bool used; // whether this FVG has been used for entry already datetime touchTime; // time of first touch (on LowTF) double touchPrice; // representative touch price (edge) }; FVG MTF_FVGs[]; int MTF_FVG_count = 0; // number of FVGs found (kept for compatibility) // last closed bar time per slot (index 0=HighTF,1=MiddleTF,2=LowTF) datetime lastClosedBarTime[3] = {0,0,0}; // cache last FVG compute time (bar C or bar index 1 time used to detect change) datetime lastFVGBarTime = 0; struct MSS { bool found; int direction; // 1 = bearish → bullish | -1 = bullish → bearish datetime sweep_time; // time của nến quét liquidity double sweep_extreme_price; // GIÁ QUÉT THỰC TẾ datetime swept_swing_time; // time của swing bị quét double swept_swing_price; // GIÁ swing bị quét (liquidity pool) datetime break_time; // time nến BOS double bos_close_price; // GIÁ ĐÓNG nến BOS datetime broken_swing_time; // time swing bị phá double broken_swing_price; // GIÁ swing bị phá (key level) }; // Kết quả BOS struct BOSInfo { bool found; bool isReal; int direction; // 1 = up, -1 = down datetime break_time; // time của bar xác nhận (bar đóng) double break_price; // close của bar xác nhận double broken_sw_price; // giá swing bị phá (sh0 hoặc sl0) datetime broken_sw_time; // time của swing bị phá int confirm_bar_index; // index (iBarShift) của bar xác nhận int consec_closes; }; // Bật/tắt BOS detection cho từng TF input bool DetectBOS_HTF = false; input bool DetectBOS_MTF = false; input bool DetectBOS_LTF = true; // --- BOS storage: lưu tối đa 2 BOS cho mỗi slot (slot 0=HTF,1=MTF,2=LTF) // Reuse struct BOSInfo (đã khai báo trước đó) BOSInfo BOSStore[3][2]; // BOSStore[slot][index] : index 0 = older, 1 = newer string BOS_Names[3][2]; // tên objects (composite) cho mỗi store entry int BOS_Count[3] = {0,0,0}; // số BOS hiện có cho mỗi slot (0..2) bool BOS_HaveZone[3][2]; // flag có zone hay không input int NYStartHourVN = 19; input int NYEndHourVN = 22; input int LondonStartHourVN = 14; input int LondonEndHourVN = 18; // ===== NEWS FILTER ===== input bool EnableNewsFilter = true; // Block entry trước/sau tin (phút) input int NewsBlockBeforeMin = 30; input int NewsBlockAfterMin = 30; bool IsValidMSS( string sym, ENUM_TIMEFRAMES tf, int shift, double swingLevel, bool bullish ) { double atr = GetATR(sym, tf, 14, shift); double close = iClose(sym, tf, shift); double open = iOpen(sym, tf, shift); double body = MathAbs(close - open); if(bullish) { if(close <= swingLevel) return false; if(close - swingLevel < atr * MSS_Break_ATR_Ratio) return false; } else { if(close >= swingLevel) return false; if(swingLevel - close < atr * MSS_Break_ATR_Ratio) return false; } if(body < atr * MSS_Body_ATR_Ratio) return false; return true; } bool IsValidLiquiditySweep( string sym, ENUM_TIMEFRAMES tf, int shift, bool isHighSweep // true = sweep high, false = sweep low ) { double atr = GetATR(sym, tf, 14, shift); double wick; if(isHighSweep) wick = iHigh(sym, tf, shift) - MathMax(iOpen(sym, tf, shift), iClose(sym, tf, shift)); else wick = MathMin(iOpen(sym, tf, shift), iClose(sym, tf, shift)) - iLow(sym, tf, shift); if(wick < atr * LS_Wick_ATR_Ratio) return false; // check phá high/low N bars trước for(int i=shift+1; i<=shift+LS_LookbackBars; i++) { if(isHighSweep && iHigh(sym, tf, shift) <= iHigh(sym, tf, i)) return false; if(!isHighSweep && iLow(sym, tf, shift) >= iLow(sym, tf, i)) return false; } return true; } bool IsHighImpactNewsNear(string symbol, int minutesBefore, int minutesAfter) { // TODO: implement real news filter later return false; } void UpdateDailyEquity() { datetime now = TimeCurrent(); MqlDateTime t; TimeToStruct(now, t); // YYYYMMDD int todayKey = t.year * 10000 + t.mon * 100 + t.day; if(DailyEquityDate != todayKey) { DailyEquityDate = todayKey; DailyEquityStart = AccountInfoDouble(ACCOUNT_EQUITY); DailyLossBlocked = false; if(PrintEntryLog) PrintFormat("DAILY RESET | EquityStart=%.2f", DailyEquityStart); } } bool IsDailyLossExceeded() { if(DailyEquityStart <= 0.0) return false; double equity = AccountInfoDouble(ACCOUNT_EQUITY); double maxLoss = DailyEquityStart * (MaxDailyLossPercent / 100.0); double lossNow = DailyEquityStart - equity; if(lossNow >= maxLoss) { if(!DailyLossBlocked && PrintEntryLog) { PrintFormat( "MAX DAILY LOSS HIT | Loss=%.2f (%.2f%%) | Trading BLOCKED", lossNow, (lossNow / DailyEquityStart) * 100.0 ); } DailyLossBlocked = true; return true; } return false; } void UpdateMTFFVGTouched(string symbol) { if(MTF_FVG_count <= 0) return; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; // scan each stored FVG for(int fvgIndex = 0; fvgIndex < MTF_FVG_count; fvgIndex++) { // nếu đã touched trước đó -> bỏ qua luôn (không in gì cả) if(MTF_FVGs[fvgIndex].touched) continue; datetime mtfC = MTF_FVGs[fvgIndex].timebarC; if(mtfC == 0) continue; double top = MTF_FVGs[fvgIndex].topPrice; double bottom = MTF_FVGs[fvgIndex].bottomPrice; int dir = MTF_FVGs[fvgIndex].type; // 1=bull, -1=bear // find index of mtfC on LowTF int idxC_on_LTF = iBarShift(symbol, LowTF, mtfC, false); if(idxC_on_LTF == -1) idxC_on_LTF = 0; bool touchedNow = false; datetime touch_time = 0; double touch_price = 0.0; // scan forward on LowTF (bars newer than mtfC): idx = idxC_on_LTF-1 ... 0 int barsAvail = iBars(symbol, LowTF); for(int idx = idxC_on_LTF - 1; idx >= 0; idx--) { double hh = iHigh(symbol, LowTF, idx); double ll = iLow(symbol, LowTF, idx); if(hh == 0 || ll == 0) continue; if(dir == 1) // bullish FVG -> touch when LTF high >= top { if(hh >= top - tol) { touchedNow = true; touch_time = iTime(symbol, LowTF, idx); touch_price = top; // representative edge price break; } } else // dir == -1 -> bearish FVG -> touch when LTF low <= bottom { if(ll <= bottom + tol) { touchedNow = true; touch_time = iTime(symbol, LowTF, idx); touch_price = bottom; break; } } } if(touchedNow) { // chỉ set + print khi trước đó chưa touched (điều kiện đã đảm bảo vì ta continue ở trên nếu touched==true) MTF_FVGs[fvgIndex].touched = true; MTF_FVGs[fvgIndex].touchTime = touch_time; MTF_FVGs[fvgIndex].touchPrice = touch_price; // enable watchingMSSMode when any MTF FVG is first touched if(!watchingMSSMode && !MTF_FVGs[fvgIndex].used) { ToggleWatchingMSSMode(true, fvgIndex, dir); if(PrintEntryLog) PrintFormat("watchingMSSMode ENABLED for FVG idx=%d dir=%d", watchingFVGIndex, watchingFVGDir); } } } } void ToggleWatchingMSSMode(bool enable, int fvgIndex = -1, int fvgDir = 0) { watchingMSSMode = enable; watchingFVGIndex = fvgIndex; watchingFVGDir = fvgDir; } // Kiểm tra điều kiện vô hiệu hoá watchingMSSMode: // 1) last closed candle trên LowTF đóng dưới cạnh dưới của bullish FVG // 2) last closed candle trên LowTF đóng trên cạnh trên của bearish FVG void CheckWatchMSSInvalidation(string symbol) { if(!watchingMSSMode || watchingFVGIndex < 0 || watchingFVGIndex >= MTF_FVG_count) return; // Lấy last closed close trên LowTF (index 1) double lastClose = iClose(symbol, LowTF, 1); if(lastClose == 0.0) return; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; int idx = watchingFVGIndex; int dir = watchingFVGDir; // bảo đảm FVG arrays còn hợp lệ if(idx < 0 || idx >= MTF_FVG_count) return; double top = MTF_FVGs[idx].topPrice; double bottom = MTF_FVGs[idx].bottomPrice; // Nếu bullish FVG (dir == 1): invalid khi đóng dưới bottom if(dir == 1) { if(lastClose < bottom - tol) { if(PrintEntryLog) PrintFormat("watchingMSSMode DISABLED: bullish FVG idx=%d invalidated by close=%.5f < bottom=%.5f", idx, lastClose, bottom); ToggleWatchingMSSMode(false); } } else if(dir == -1) // bearish: invalid khi đóng trên top { if(lastClose > top + tol) { if(PrintEntryLog) PrintFormat("watchingMSSMode DISABLED: bearish FVG idx=%d invalidated by close=%.5f > top=%.5f", idx, lastClose, top); // clear ToggleWatchingMSSMode(false); } } } void StoreNewBOS(int slot, const BOSInfo &bos) { // Nếu chưa có BOS nào if(BOS_Count[slot] == 0) { BOSStore[slot][0] = bos; // Clear name => DrawBOS sẽ tạo object và lưu tên BOS_Names[slot][0] = ""; BOS_HaveZone[slot][0] = true; BOS_Count[slot] = 1; } else if(BOS_Count[slot] == 1) { // shift index0 -> index1 (data only), but DO NOT copy names BOSStore[slot][1] = BOSStore[slot][0]; BOS_HaveZone[slot][1] = false; // will be created by DrawBOS BOS_Names[slot][1] = ""; // store new into index0 BOSStore[slot][0] = bos; BOS_Names[slot][0] = ""; BOS_HaveZone[slot][0] = true; BOS_Count[slot] = 2; } else // BOS_Count == 2 { // xóa oldest (index1) trước nếu có object name lưu if(StringLen(BOS_Names[slot][1]) > 0) { DeleteCompositeBOS(BOS_Names[slot][1]); } // shift data index0 -> index1 (but clear names so Draw creates fresh objects) BOSStore[slot][1] = BOSStore[slot][0]; BOS_Names[slot][1] = ""; BOS_HaveZone[slot][1] = false; // store new into index0 BOSStore[slot][0] = bos; BOS_Names[slot][0] = ""; BOS_HaveZone[slot][0] = true; BOS_Count[slot] = 2; } // Vẽ lại tất cả BOS cho slot (DrawAllBOSForSlot sẽ tạo lại objects và ghi BOS_Names) // DrawAllBOSForSlot(slot); } // Vẽ tất cả BOS đã lưu cho 1 slot (0=HTF,1=MTF,2=LTF) // SỬA: không xóa theo prefix chung nữa, chỉ xóa các composite names đã lưu trong BOS_Names void DrawAllBOSForSlot(int slot) { if(!ShowSwingMarkers) return; int cnt = BOS_Count[slot]; if(cnt <= 0) return; string sym = Symbol(); // --- SAFE CLEANUP: xóa composite object cũ mà chúng ta từng lưu tên trong BOS_Names --- for(int j = 0; j < 2; j++) { if(StringLen(BOS_Names[slot][j]) > 0) { // DeleteCompositeBOS sẽ kiểm tra tồn tại từng object con trước khi xóa DeleteCompositeBOS(BOS_Names[slot][j]); BOS_Names[slot][j] = ""; // clear saved composite name so DrawBOS will recreate BOS_HaveZone[slot][j] = false; } } // draw older first (index1) then newer (index0) // Note: DrawBOS sẽ set BOS_Names[slot][storeIndex] after drawing, so we pass storeIndex to persist names if(cnt == 2) { if(BOSStore[slot][1].found) { DrawBOS(sym, slot, BOSStore[slot][1], 1); if(StringLen(BOS_Names[slot][1]) == 0) { // do nothing special } } if(BOSStore[slot][0].found) { DrawBOS(sym, slot, BOSStore[slot][0], 0); if(StringLen(BOS_Names[slot][0]) == 0) { // do nothing special } } } else // cnt == 1 { if(BOSStore[slot][0].found) { DrawBOS(sym, slot, BOSStore[slot][0], 0); } } } void HandleBOSDetections(string symbol, ENUM_TIMEFRAMES timeframe, int slot) { // Tôn trọng input bật/tắt cho từng slot (slot 0=HTF,1=MTF,2=LTF) bool enabled = false; if(slot == 0) enabled = DetectBOS_HTF; else if(slot == 1) enabled = DetectBOS_MTF; else if(slot == 2) enabled = DetectBOS_LTF; if(!enabled) return; // Phát hiện BOS trên slot (giữ nguyên tham số bạn muốn) BOSInfo bos = DetectBOSOnSlot(symbol, timeframe, slot, ProvisionalBreakPips, ProvisionalConsecCloses, true, 5); if(bos.found) StoreNewBOS(slot, bos); } // DetectBOSOnSlot: detect Break Of Structure based on nearest swing (index 0) // symbol, timeframe, slot: như EA của bạn // minBreakPips: số pips tối thiểu vượt swing để gọi là BOS (ví dụ 1..10 tuỳ TF) // consecRequired: số nến đóng liên tiếp xác nhận (1 = 1 bar close đủ) // useBodyCheck: nếu true thì yêu cầu bar xác nhận là nến thuận (close>open cho bullish, close= 1) ? SwingHighPriceTF[slot][0] : 0.0; double sl0 = (SwingLowCountTF[slot] >= 1) ? SwingLowPriceTF[slot][0] : 0.0; if(sh0 == 0.0 && sl0 == 0.0) return out; double pip = GetPipSize(symbol); double minBreakPoints = minBreakPips * pip; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; int barsAvailable = iBars(symbol, timeframe); if(barsAvailable < 2) return out; // --- BULLISH BOS: first try REAL (close > sh0 + threshold) --- if(sh0 != 0.0) { for(int idx = 1; idx <= MathMin(lookForwardBars, barsAvailable - 1); idx++) { double c = iClose(symbol, timeframe, idx); double o = iOpen(symbol, timeframe, idx); double h = iHigh(symbol, timeframe, idx); datetime t = iTime(symbol, timeframe, idx); if(c == 0 || h == 0) continue; // REAL: require close strictly greater than sh0 + minBreakPoints if(c > sh0 + minBreakPoints) { int consec = 0; for(int k = idx; k >= 1 && k >= idx - (consecRequired - 1); k--) { double ck = iClose(symbol, timeframe, k); double ok = iOpen(symbol, timeframe, k); if(ck == 0) break; if(useBodyCheck) { if(ck > ok && ck > sh0 + minBreakPoints) consec++; else break; } else { if(ck > sh0 + minBreakPoints) consec++; else break; } } if(consec >= consecRequired) { out.found = true; out.direction = 1; out.break_time = t; out.break_price = c; out.broken_sw_price = sh0; out.broken_sw_time = SwingHighTimeTF[slot][0]; out.confirm_bar_index = idx; out.consec_closes = consec; out.isReal = true; return out; } } } // If no REAL bullish BOS found, check for FAKE bullish BOS: for(int idx = 1; idx <= MathMin(lookForwardBars, barsAvailable - 1); idx++) { double c = iClose(symbol, timeframe, idx); double h = iHigh(symbol, timeframe, idx); datetime t = iTime(symbol, timeframe, idx); if(h == 0) continue; if(h > sh0 + minBreakPoints && c <= sh0 + minBreakPoints) { // classify as fake BOS out.found = true; out.direction = 1; out.break_time = t; out.break_price = c; out.broken_sw_price = sh0; out.broken_sw_time = SwingHighTimeTF[slot][0]; out.confirm_bar_index = idx; out.consec_closes = 0; out.isReal = false; return out; } } } // --- BEARISH BOS: first try REAL (close < sl0 - threshold) --- if(sl0 != 0.0) { for(int idx = 1; idx <= MathMin(lookForwardBars, barsAvailable - 1); idx++) { double c = iClose(symbol, timeframe, idx); double o = iOpen(symbol, timeframe, idx); double l = iLow(symbol, timeframe, idx); datetime t = iTime(symbol, timeframe, idx); if(c == 0 || l == 0) continue; if(c < sl0 - minBreakPoints) { int consec = 0; for(int k = idx; k >= 1 && k >= idx - (consecRequired - 1); k--) { double ck = iClose(symbol, timeframe, k); double ok = iOpen(symbol, timeframe, k); if(ck == 0) break; if(useBodyCheck) { if(ck < ok && ck < sl0 - minBreakPoints) consec++; else break; } else { if(ck < sl0 - minBreakPoints) consec++; else break; } } if(consec >= consecRequired) { out.found = true; out.direction = -1; out.break_time = t; out.break_price = c; out.broken_sw_price = sl0; out.broken_sw_time = SwingLowTimeTF[slot][0]; out.confirm_bar_index = idx; out.consec_closes = consec; out.isReal = true; return out; } } } // If no REAL bearish BOS found, check for FAKE bearish BOS: for(int idx = 1; idx <= MathMin(lookForwardBars, barsAvailable - 1); idx++) { double c = iClose(symbol, timeframe, idx); double l = iLow(symbol, timeframe, idx); datetime t = iTime(symbol, timeframe, idx); if(l == 0) continue; if(l < sl0 - minBreakPoints && c >= sl0 - minBreakPoints) { // classify as fake BOS out.found = true; out.direction = -1; out.break_time = t; out.break_price = c; out.broken_sw_price = sl0; out.broken_sw_time = SwingLowTimeTF[slot][0]; out.confirm_bar_index = idx; out.consec_closes = 0; out.isReal = false; return out; } } } return out; } // DrawBOS: vẽ 1 BOS (không shift/xóa store) — parameters: symbol, slot, bos info, storeIndex (0/1) // Nếu bạn gọi DrawBOS với storeIndex = -1 thì hàm sẽ chỉ tạo objects tạm (không lưu tên). void DrawBOS(string symbol, int slot, const BOSInfo &info, int storeIndex = -1) { if(!info.found) return; string basePrefix = SwingObjPrefix + "BOS_" + IntegerToString(slot) + "_"; ENUM_TIMEFRAMES tf = (slot==0 ? HighTF : (slot==1 ? MiddleTF : LowTF)); // get start time/price (swing broken) datetime t_start = info.broken_sw_time; double p_start = info.broken_sw_price; if(t_start == 0 || p_start == 0.0) { if(info.direction == 1) { t_start = SwingHighTimeTF[slot][0]; p_start = SwingHighPriceTF[slot][0]; } else { t_start = SwingLowTimeTF[slot][0]; p_start = SwingLowPriceTF[slot][0]; } } if(t_start == 0 || p_start == 0.0) { return; } datetime bar_secs = (datetime)PeriodSeconds(tf); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; datetime touch_time = FindTouchTime(symbol, tf, t_start, p_start, tol, bar_secs); if(touch_time <= t_start) { if(info.break_time != 0 && info.break_time > t_start) touch_time = info.break_time; else { datetime t0 = iTime(symbol, tf, 0); touch_time = (t0 != 0) ? (datetime)((long)t0 + (long)bar_secs) : (datetime)TimeCurrent(); } } datetime t_h_end = touch_time; double p_h = p_start; if(t_h_end == t_start) t_h_end = (datetime)((long)t_start + (long)bar_secs); // --- TẠO TÊN UNIQUE: bao gồm slot + storeIndex + thời điểm để tránh trùng --- int idxPart = (storeIndex >= 0) ? storeIndex : -1; int timeStampInt = (info.broken_sw_time != 0) ? (int)info.broken_sw_time : (int)t_start; string stamp = StringFormat("%d_%d_%d", slot, idxPart, timeStampInt); string nm_line = basePrefix + "HLINE_" + stamp; string nm_arrow = basePrefix + "ARW_" + stamp; string nm_txt = basePrefix + "LBL_" + stamp; // Nếu caller truyền storeIndex >=0 và đã có composite name lưu trước đó: if(storeIndex >= 0) { if(StringLen(BOS_Names[slot][storeIndex]) > 0) { string parts[]; int n = StringSplit(BOS_Names[slot][storeIndex], '|', parts); bool all_exist = true; for(int p = 0; p < n; p++) { if(StringLen(parts[p]) == 0) continue; if(ObjectFind(0, parts[p]) < 0) { all_exist = false; break; } } if(all_exist) { return; } else { // object cũ bị xóa một phần nào đó -> xóa compositeName cũ khỏi bộ nhớ trước khi vẽ mới DeleteCompositeBOS(BOS_Names[slot][storeIndex]); BOS_Names[slot][storeIndex] = ""; BOS_HaveZone[slot][storeIndex] = false; } } } // Create horizontal line (trend object used as line) if(!ObjectCreate(0, nm_line, OBJ_TREND, 0, t_start, p_h, t_h_end, p_h)) ; // skip silent else { int lineColor = info.isReal ? ((info.direction==1)?clrLime:clrRed) : clrYellow; ObjectSetInteger(0, nm_line, OBJPROP_COLOR, lineColor); ObjectSetInteger(0, nm_line, OBJPROP_WIDTH, 2); ObjectSetInteger(0, nm_line, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, nm_line, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, nm_line, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_line, OBJPROP_BACK, true); } // Arrow (or fallback text arrow) at t_h_end,p_h if(ObjectCreate(0, nm_arrow, OBJ_ARROW, 0, t_h_end, p_h)) { int code = (info.direction==1) ? 233 : 234; #ifdef __MQL5__ ObjectSetInteger(0, nm_arrow, OBJPROP_ARROWCODE, code); #endif int arrowColor = info.isReal ? ((info.direction==1)?clrLime:clrRed) : clrYellow; ObjectSetInteger(0, nm_arrow, OBJPROP_COLOR, arrowColor); ObjectSetInteger(0, nm_arrow, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_arrow, OBJPROP_BACK, true); } else { string nm_fallback = basePrefix + "ARW_T_" + stamp; if(ObjectCreate(0, nm_fallback, OBJ_TEXT, 0, t_h_end, p_h)) { string arrowTxt = (info.direction==1) ? "▲" : "▼"; ObjectSetString(0, nm_fallback, OBJPROP_TEXT, arrowTxt); int arrowColor = info.isReal ? ((info.direction==1)?clrLime:clrRed) : clrYellow; ObjectSetInteger(0, nm_fallback, OBJPROP_COLOR, arrowColor); ObjectSetInteger(0, nm_fallback, OBJPROP_FONTSIZE, SwingMarkerFontSize+2); ObjectSetInteger(0, nm_fallback, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_fallback, OBJPROP_BACK, true); nm_arrow = nm_fallback; } } double y_offset = SymbolInfoDouble(symbol, SYMBOL_POINT) * 4.0; double label_price = (info.direction==1) ? (p_h + y_offset) : (p_h - y_offset); // Label text and color depending on real/fake string labelText = info.isReal ? "BOS" : "FAKE"; int labelColor = info.isReal ? ((info.direction==1)?clrLime:clrRed) : clrYellow; if(!ObjectCreate(0, nm_txt, OBJ_TEXT, 0, t_h_end, label_price)) ; else { ObjectSetString(0, nm_txt, OBJPROP_TEXT, labelText); ObjectSetInteger(0, nm_txt, OBJPROP_COLOR, labelColor); ObjectSetInteger(0, nm_txt, OBJPROP_FONTSIZE, SwingMarkerFontSize + 1); ObjectSetInteger(0, nm_txt, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_txt, OBJPROP_BACK, true); } // Nếu caller truyền storeIndex >=0 thì lưu composite name để quản lý (xóa sau) string compositeName = nm_line + "|" + nm_arrow + "|" + nm_txt; if(storeIndex >= 0) { if(StringLen(BOS_Names[slot][storeIndex]) > 0) DeleteCompositeBOS(BOS_Names[slot][storeIndex]); BOS_Names[slot][storeIndex] = compositeName; BOS_HaveZone[slot][storeIndex] = true; } } // xóa compositeName dạng "lineName|arrowName|labelName" void DeleteCompositeBOS(string compositeName) { if(StringLen(compositeName) == 0) return; string parts[]; int n = StringSplit(compositeName, '|', parts); for(int i=0; i 0 && ObjectFind(0, nm) >= 0) ObjectDelete(0, nm); } } // ---------------------- Helper: pip size ---------------------- // Trả về 1 "pip" cho symbol (không phải point). double GetPipSize(string symbol) { int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(digits==5 || digits==3) return point * 10.0; return point; } // Tìm FVG nội bộ (3-candle gap: A = i+2, B = i+1, C = i) // // Điều kiện FVG: // • Bullish: low(C) > high(A) // • Bearish: high(C) < low(A) // // Trả về: // out_top[] – cạnh trên của FVG // out_bottom[] – cạnh dưới của FVG // out_timeA[] – thời gian bar A // out_timeC[] – thời gian bar C // out_type[] – 1 = bullish, -1 = bearish // // Lý do dùng total - 3: // Vì index A = i+2 → cần đảm bảo i+2 <= total-1 // → i <= total - 3 // // -------------------------------------------------------------- int FindInternalFVG(string symbol, ENUM_TIMEFRAMES timeframe, int lookback, double &out_top[], double &out_bottom[], datetime &out_timeA[], datetime &out_timeC[], int &out_type[]) { // clear output arrays ArrayFree(out_top); ArrayFree(out_bottom); ArrayFree(out_timeA); ArrayFree(out_timeC); ArrayFree(out_type); int found = 0; int total = iBars(symbol, timeframe); // cần tối thiểu 5 nến để tránh lỗi/truy cập thiếu (an toàn) if(total < 5) return 0; // maxScan = nhỏ nhất giữa lookback và total - 3 (đảm bảo idxA không vượt array) int maxScan = MathMin(lookback, total - 3); if(maxScan <= 0) return 0; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; // ---------------------------------------------------------- // Loop qua từng cấu trúc A–B–C (C = i, B = i+1, A = i+2) // ---------------------------------------------------------- for(int i = 1; i <= maxScan; i++) { int idxA = i + 2; int idxB = i + 1; int idxC = i; // kiểm tra A không vượt tổng số bar if(idxA > total - 1) break; // lấy giá 3 nến A và C double highA = iHigh(symbol, timeframe, idxA); double lowA = iLow(symbol, timeframe, idxA); double highC = iHigh(symbol, timeframe, idxC); double lowC = iLow(symbol, timeframe, idxC); if(highA == 0 || lowA == 0 || highC == 0 || lowC == 0) continue; // ------------------------------------------------------ // ❶ Kiểm tra Bullish Internal FVG // low(C) > high(A) // ------------------------------------------------------ if(lowC > highA + tol) { double top = lowC; double bottom = highA; // ===== FVG SIZE FILTER (ATR-based) ===== double atr = GetATR(symbol, timeframe, 14, idxC); if(MathAbs(top - bottom) < atr * FVG_Min_ATR_Ratio) continue; // kiểm tra tránh duplicate FVG bool dup = false; for(int k = 0; k < found; k++) { if(MathAbs(out_top[k] - top) <= tol && MathAbs(out_bottom[k] - bottom) <= tol) { dup = true; break; } } if(!dup) { ArrayResize(out_top, found+1); ArrayResize(out_bottom, found+1); ArrayResize(out_timeA, found+1); ArrayResize(out_timeC, found+1); ArrayResize(out_type, found+1); out_top[found] = top; out_bottom[found] = bottom; out_timeA[found] = iTime(symbol, timeframe, idxA); out_timeC[found] = iTime(symbol, timeframe, idxC); out_type[found] = 1; // bullish found++; } continue; } // ------------------------------------------------------ // ❷ Kiểm tra Bearish Internal FVG // high(C) < low(A) // ------------------------------------------------------ if(highC < lowA - tol) { double top = lowA; double bottom = highC; // ===== FVG SIZE FILTER (ATR-based) ===== double atr = GetATR(symbol, timeframe, 14, idxC); if(MathAbs(top - bottom) < atr * FVG_Min_ATR_Ratio) continue; bool dup = false; for(int k = 0; k < found; k++) { if(MathAbs(out_top[k] - top) <= tol && MathAbs(out_bottom[k] - bottom) <= tol) { dup = true; break; } } if(!dup) { ArrayResize(out_top, found+1); ArrayResize(out_bottom, found+1); ArrayResize(out_timeA, found+1); ArrayResize(out_timeC, found+1); ArrayResize(out_type, found+1); out_top[found] = top; out_bottom[found] = bottom; out_timeA[found] = iTime(symbol, timeframe, idxA); out_timeC[found] = iTime(symbol, timeframe, idxC); out_type[found] = -1; // bearish found++; } } } return found; } // ---------- HasBullFVGBetween & HasBearFVGBetween (gọi FindInternalFVG) ---------- bool HasBullFVGBetween(string symbol, ENUM_TIMEFRAMES timeframe, datetime t_start, datetime t_end, int fvgLookback) { if(t_start == 0 || t_end == 0 || t_end <= t_start) return false; double tops[]; double bottoms[]; datetime timeA[]; datetime timeC[]; int types[]; int cnt = FindInternalFVG(symbol, timeframe, fvgLookback, tops, bottoms, timeA, timeC, types); if(cnt <= 0) return false; for(int i = 0; i < cnt; i++) { if(types[i] == 1) { datetime tc = timeC[i]; if(tc > t_start && tc < t_end) return true; } } return false; } bool HasBearFVGBetween(string symbol, ENUM_TIMEFRAMES timeframe, datetime t_start, datetime t_end, int fvgLookback) { if(t_start == 0 || t_end == 0 || t_end <= t_start) return false; double tops[]; double bottoms[]; datetime timeA[]; datetime timeC[]; int types[]; int cnt = FindInternalFVG(symbol, timeframe, fvgLookback, tops, bottoms, timeA, timeC, types); if(cnt <= 0) return false; for(int i = 0; i < cnt; i++) { if(types[i] == -1) { datetime tc = timeC[i]; if(tc > t_start && tc < t_end) return true; } } return false; } // Kiểm tra 1 bar tại index i có phải SwingHigh không (xét râu - High) bool IsSwingHigh(string symbol, ENUM_TIMEFRAMES timeframe, int candleIndex, int swingRange) { double hi = iHigh(symbol, timeframe, candleIndex); if(hi == 0.0) return false; for(int i = 1; i <= swingRange; i++) { int idxPrev = candleIndex - i; if(idxPrev < 0) return false; double hprev = iHigh(symbol, timeframe, idxPrev); if(hprev == 0.0) return false; if(hprev >= hi) return false; } for(int k = 1; k <= swingRange; k++) { int idxNext = candleIndex + k; double hnext = iHigh(symbol, timeframe, idxNext); if(hnext == 0.0) return false; if(hnext >= hi) return false; } return true; } // Kiểm tra 1 bar tại index i có phải SwingLow không (xét râu - Low) bool IsSwingLow(string symbol, ENUM_TIMEFRAMES timeframe, int i, int X) { double lo = iLow(symbol, timeframe, i); if(lo == 0.0) return false; for(int j = 1; j <= X; j++) { int idxPrev = i - j; if(idxPrev < 0) return false; double lprev = iLow(symbol, timeframe, idxPrev); if(lprev == 0.0) return false; if(lprev <= lo) return false; } for(int k = 1; k <= X; k++) { int idxNext = i + k; double lnext = iLow(symbol, timeframe, idxNext); if(lnext == 0.0) return false; if(lnext <= lo) return false; } return true; } // Cập nhật mảng SwingHighPrice/Time và SwingLowPrice/Time (giữ MaxSwingKeep phần tử gần nhất) void UpdateSwings(string symbol, ENUM_TIMEFRAMES timeframe, int slot, int SwingRange) { SwingHighCountTF[slot] = 0; SwingLowCountTF[slot] = 0; int total = iBars(symbol, timeframe); if(total <= SwingRange + 2) return; int iStart = SwingRange + 1; int iEnd = total - SwingRange - 1; for(int i = iStart; i <= iEnd; i++) { if(SwingHighCountTF[slot] >= MaxSwingKeep && SwingLowCountTF[slot] >= MaxSwingKeep) break; if(SwingHighCountTF[slot] < MaxSwingKeep) { bool isH = IsSwingHigh(symbol, timeframe, i, SwingRange); if(isH) { double candleI = iHigh(symbol, timeframe, i); datetime ti = iTime(symbol, timeframe, i); if(SwingHighCountTF[slot] == 0) { SwingHighPriceTF[slot][0] = candleI; SwingHighTimeTF[slot][0] = ti; } else { SwingHighPriceTF[slot][1] = candleI; SwingHighTimeTF[slot][1] = ti; } SwingHighCountTF[slot]++; } } if(SwingLowCountTF[slot] < MaxSwingKeep) { bool isL = IsSwingLow(symbol, timeframe, i, SwingRange); if(isL) { double candleI = iLow(symbol, timeframe, i); datetime ti2 = iTime(symbol, timeframe, i); if(SwingLowCountTF[slot] == 0) { SwingLowPriceTF[slot][0] = candleI; SwingLowTimeTF[slot][0] = ti2; } else { SwingLowPriceTF[slot][1] = candleI; SwingLowTimeTF[slot][1] = ti2; } SwingLowCountTF[slot]++; } } } } //+------------------------------------------------------------------+ //| Vẽ SwingHigh & SwingLow lên chart | //+------------------------------------------------------------------+ void DrawSwingsOnChart(ENUM_TIMEFRAMES timeframe) { if(!ShowSwingMarkers) return; int slot = 0; string inChartSwingPrefix = "h"; if(timeframe == MiddleTF) { slot = 1; inChartSwingPrefix = "m"; } else if (timeframe == LowTF) { slot = 2; inChartSwingPrefix = "l"; } string prefix = SwingObjPrefix + inChartSwingPrefix; int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringLen(name) >= StringLen(prefix)) { if(StringSubstr(name, 0, StringLen(prefix)) == prefix) ObjectDelete(0, name); } } datetime bar_secs = (datetime)PeriodSeconds(timeframe); int maxH = (SwingHighCountTF[slot] < MaxSwingKeep) ? SwingHighCountTF[slot] : MaxSwingKeep; for(int h = 0; h < maxH; h++) { string obj = prefix + "H_" + IntegerToString(h); datetime t = SwingHighTimeTF[slot][h]; double price = SwingHighPriceTF[slot][h]; datetime display_time_h = (datetime)(t + (bar_secs/2)); if(t == 0 || price == 0.0) continue; if(!ObjectCreate(0, obj, OBJ_TEXT, 0, display_time_h, price)) { continue; } ObjectSetString(0, obj, OBJPROP_TEXT, "▲ " + inChartSwingPrefix + "H" + IntegerToString(h)); ObjectSetInteger(0, obj, OBJPROP_COLOR, clrRed); ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, SwingMarkerFontSize); ObjectSetInteger(0, obj, OBJPROP_ANCHOR, ANCHOR_BOTTOM); ObjectSetInteger(0, obj, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, obj, OBJPROP_HIDDEN, false); } int maxL = (SwingLowCountTF[slot] < MaxSwingKeep) ? SwingLowCountTF[slot] : MaxSwingKeep; for(int l = 0; l < maxL; l++) { string obj = prefix + "L_" + IntegerToString(l); datetime t = SwingLowTimeTF[slot][l]; double price = SwingLowPriceTF[slot][l]; datetime display_time_l = (datetime)(t + (bar_secs/2)); if(!ObjectCreate(0, obj, OBJ_TEXT, 0, display_time_l, price)) { continue; } ObjectSetString(0, obj, OBJPROP_TEXT, "▼ " + inChartSwingPrefix +"L" + IntegerToString(l)); ObjectSetInteger(0, obj, OBJPROP_COLOR, clrLime); ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, SwingMarkerFontSize); ObjectSetInteger(0, obj, OBJPROP_ANCHOR, ANCHOR_TOP); ObjectSetInteger(0, obj, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, obj, OBJPROP_HIDDEN, false); } } // --------------------------------------------------------------------- // FindTouchTime(): tìm thời điểm bar đầu tiên sau start_time mà bar chạm price // --------------------------------------------------------------------- datetime FindTouchTime(string symbol, ENUM_TIMEFRAMES timeframe, datetime start_time, double price, double tol, datetime bar_secs) { int idxStart = iBarShift(symbol, timeframe, start_time, false); if(idxStart == -1) idxStart = 0; for(int idx = idxStart - 1; idx >= 0; idx--) { double highM = iHigh(symbol, timeframe, idx); double lowM = iLow(symbol, timeframe, idx); if(highM == 0 || lowM == 0) continue; if(lowM <= price + tol && highM >= price - tol) return iTime(symbol, timeframe, idx); } datetime t0 = iTime(symbol, timeframe, 0); if(t0 != 0) return (datetime)((long)t0 + (long)bar_secs); return TimeCurrent(); } void DrawMss(string symbol, ENUM_TIMEFRAMES tf, const MSS &mss, int slot) { if(!ShowSwingMarkers || !mss.found) return; string basePrefix = SwingObjPrefix + "MSS_" + IntegerToString(slot) + "_"; // ===== clear old MSS objects for this slot ===== int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string nm = ObjectName(0, i); if(StringFind(nm, basePrefix, 0) == 0) ObjectDelete(0, nm); } int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); datetime bar_secs = (datetime)PeriodSeconds(tf); // ================================================= // 1️⃣ DRAW SWEEP LABEL (wick) // ================================================= if(mss.sweep_time != 0 && mss.sweep_extreme_price != 0.0) { string nmSweep = basePrefix + "SWEEP_" + IntegerToString((int)mss.sweep_time); if(ObjectCreate(0, nmSweep, OBJ_TEXT, 0, mss.sweep_time, mss.sweep_extreme_price)) { ObjectSetString(0, nmSweep, OBJPROP_TEXT, "SWEEP"); ObjectSetInteger(0, nmSweep, OBJPROP_COLOR, clrMagenta); ObjectSetInteger(0, nmSweep, OBJPROP_FONTSIZE, SwingMarkerFontSize + 2); ObjectSetInteger(0, nmSweep, OBJPROP_BACK, true); ObjectSetInteger(0, nmSweep, OBJPROP_SELECTABLE, false); } } // ================================================= // 2️⃣ DRAW MSS LABEL (BOS close) // ================================================= if(mss.break_time != 0 && mss.bos_close_price != 0.0) { string nmMSS = basePrefix + "MSS_" + IntegerToString((int)mss.break_time); if(ObjectCreate(0, nmMSS, OBJ_TEXT, 0, mss.break_time, mss.bos_close_price)) { ObjectSetString(0, nmMSS, OBJPROP_TEXT, "MSS"); ObjectSetInteger(0, nmMSS, OBJPROP_COLOR, clrOrange); ObjectSetInteger(0, nmMSS, OBJPROP_FONTSIZE, SwingMarkerFontSize + 2); ObjectSetInteger(0, nmMSS, OBJPROP_BACK, true); ObjectSetInteger(0, nmMSS, OBJPROP_SELECTABLE, false); } } // ================================================= // 3️⃣ DRAW LIQUIDITY SWEEP GUIDE LINE // ================================================= if(mss.swept_swing_time != 0 && mss.swept_swing_price != 0.0 && mss.sweep_time > mss.swept_swing_time) { string nmGuide = basePrefix + "GUIDE_" + IntegerToString((int)mss.sweep_time); if(ObjectCreate(0, nmGuide, OBJ_TREND, 0, mss.swept_swing_time, mss.swept_swing_price, mss.sweep_time, mss.swept_swing_price)) { ObjectSetInteger(0, nmGuide, OBJPROP_COLOR, clrMagenta); ObjectSetInteger(0, nmGuide, OBJPROP_WIDTH, 2); ObjectSetInteger(0, nmGuide, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, nmGuide, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, nmGuide, OBJPROP_BACK, true); ObjectSetInteger(0, nmGuide, OBJPROP_SELECTABLE, false); } } // ================================================= // 4️⃣ DRAW MSS BREAK (BOS) GUIDE LINE // ================================================= if(mss.broken_swing_time != 0 && mss.broken_swing_price != 0.0 && mss.break_time != 0) { datetime t_start = mss.broken_swing_time; datetime t_end = mss.break_time; // đảm bảo có độ dài để vẽ if(t_end == t_start) t_end = (datetime)((long)t_end + (long)bar_secs); string nmBreak = basePrefix + "BREAK_" + IntegerToString((int)mss.break_time); if(ObjectCreate(0, nmBreak, OBJ_TREND, 0, t_start, mss.broken_swing_price, t_end, mss.broken_swing_price)) { ObjectSetInteger(0, nmBreak, OBJPROP_COLOR, clrOrange); ObjectSetInteger(0, nmBreak, OBJPROP_WIDTH, 2); ObjectSetInteger(0, nmBreak, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, nmBreak, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, nmBreak, OBJPROP_BACK, true); ObjectSetInteger(0, nmBreak, OBJPROP_SELECTABLE, false); } } } // Hàm tiện ích chuyển giá trị xu hướng thành chuỗi string TrendToString(int trend) { if(trend==1) return "UPTREND"; if(trend==-1) return "DOWNTREND"; return "SIDEWAY"; } // Cập nhật TrendTF[slot] dựa trên 2 swing gần nhất (không gọi UpdateSwings bên trong) void UpdateTrendForSlot(int slot, ENUM_TIMEFRAMES timeframe, string symbol) { int oldTrend = TrendTF[slot]; // ===== LTF: KHÔNG TỰ TÍNH TREND ===== if(slot == 2) { // mirror MTF hoặc bỏ luôn TrendTF[slot] = TrendTF[1]; return; } double pip = GetPipSize(symbol); double provisionalThresh = (double)ProvisionalBreakPips * pip; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; bool haveTwoSwings = (SwingHighCountTF[slot] >= 2 && SwingLowCountTF[slot] >= 2); double sh0 = (SwingHighCountTF[slot] >= 1) ? SwingHighPriceTF[slot][0] : 0.0; double sh1 = (SwingHighCountTF[slot] >= 2) ? SwingHighPriceTF[slot][1] : 0.0; double sl0 = (SwingLowCountTF[slot] >= 1) ? SwingLowPriceTF[slot][0] : 0.0; double sl1 = (SwingLowCountTF[slot] >= 2) ? SwingLowPriceTF[slot][1] : 0.0; int newTrend = oldTrend; if(haveTwoSwings) { if(sh0 > sh1 && sl0 > sl1) newTrend = 1; else if(sh0 < sh1 && sl0 < sl1) newTrend = -1; else newTrend = 0; } else { newTrend = oldTrend; } double lastClosedClose = iClose(symbol, timeframe, 1); if(lastClosedClose == 0.0) lastClosedClose = iClose(symbol, timeframe, 0); int provisionalTrend = 0; if(sh0 != 0.0 && lastClosedClose > sh0 + provisionalThresh) provisionalTrend = 1; else if(sl0 != 0.0 && lastClosedClose < sl0 - provisionalThresh) provisionalTrend = -1; if(provisionalTrend != 0) { int countClosers = 0; for(int i = 1; i <= ProvisionalConsecCloses; i++) { double c = iClose(symbol, timeframe, i); if(c == 0.0) break; if(provisionalTrend == 1) { if(c > sh0 + provisionalThresh) countClosers++; else break; } else { if(c < sl0 - provisionalThresh) countClosers++; else break; } } if(countClosers >= ProvisionalConsecCloses) { if(!haveTwoSwings || newTrend == 0) { newTrend = provisionalTrend; } } } TrendTF[slot] = newTrend; } uint MakeARGB(int a, uint clr) { if(a < 0) a = 0; if(a > 255) a = 255; return ((uint)a << 24) | (clr & 0x00FFFFFF); } void DrawFVG(string symbol, ENUM_TIMEFRAMES timeframe, bool startFromCBar = true) { if(!ShowSwingMarkers) return; string prefix = SwingObjPrefix + "FVG_"; int tot = ObjectsTotal(0); for(int i = tot - 1; i >= 0; i--) { string nm = ObjectName(0, i); if(StringFind(nm, prefix, 0) == 0) ObjectDelete(0, nm); } if(MTF_FVG_count <= 0) return; datetime bar_secs = (datetime)PeriodSeconds(timeframe); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; for(int k = 0; k < MTF_FVG_count; k++) { double top = MTF_FVGs[k].topPrice; double bottom = MTF_FVGs[k].bottomPrice; datetime timeC = MTF_FVGs[k].timebarC; datetime timeA = MTF_FVGs[k].timebarA; datetime start_time = startFromCBar ? timeC : timeA; int idxC = iBarShift(symbol, timeframe, timeC, false); if(idxC == -1) idxC = 0; bool touched = false; datetime end_time = TimeCurrent(); for(int idx = idxC - 1; idx >= 0; idx--) { double highM = iHigh(symbol, timeframe, idx); double lowM = iLow(symbol, timeframe, idx); if(highM == 0 || lowM == 0) continue; if(lowM <= top + tol && highM >= bottom - tol) { touched = true; end_time = iTime(symbol, timeframe, idx); break; } } if(!touched) { datetime t0 = iTime(symbol, timeframe, 0); if(t0 != 0) end_time = t0 + bar_secs; else end_time = TimeCurrent(); } if(end_time <= start_time) end_time = (datetime)( (long)start_time + (long)bar_secs ); string obj = prefix + IntegerToString(k); if(!ObjectCreate(0, obj, OBJ_RECTANGLE, 0, start_time, top, end_time, bottom)) { continue; } ObjectSetInteger(0, obj, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, obj, OBJPROP_HIDDEN, false); ObjectSetInteger(0, obj, OBJPROP_BACK, true); uint fill_alpha = 80; uint border_alpha = 0; uint color_fill; if(MTF_FVGs[k].type == 1) color_fill = MakeARGB((int)fill_alpha, clrDodgerBlue); else color_fill = MakeARGB((int)fill_alpha, clrCrimson); uint col_border = MakeARGB((int)border_alpha, clrBlack); ObjectSetInteger(0, obj, OBJPROP_COLOR, (int)col_border); ObjectSetInteger(0, obj, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1); ObjectSetInteger(0, obj, OBJPROP_COLOR, (int)color_fill); } } // ------------------------------------------------------------ // UpdateLabel: vẽ label cho 1 timeframe bất kỳ // ------------------------------------------------------------ void UpdateLabel(string labelName, ENUM_TIMEFRAMES timeframe, int trend) { if(!ShowSwingMarkers) return; string txt = StringFormat("%s: %s", EnumToString(timeframe), TrendToString(trend)); int corner = CORNER_LEFT_UPPER; int xdist = 10; int ydist_default = 20; int ydist = ydist_default; int offset_down = 30; if (timeframe == MiddleTF) ydist = ydist_default + offset_down; else if(timeframe == LowTF) ydist = ydist_default + offset_down * 2; if(ObjectFind(0, labelName) >= 0) { ObjectSetString(0, labelName, OBJPROP_TEXT, txt); if(trend == 1) ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrLime); else if(trend == -1) ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrRed); else ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrGray); } else { if(!ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0)) { return; } ObjectSetInteger(0, labelName, OBJPROP_CORNER, corner); ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, xdist); ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, ydist); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, SwingMarkerFontSize); ObjectSetString(0, labelName, OBJPROP_TEXT, txt); if(trend == 1) ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrLime); else if(trend == -1) ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrRed); else ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrGray); } } // Trả true nếu bar đóng mới xuất hiện (dựa vào iTime(...,1)) bool IsNewClosedBar(string symbol, ENUM_TIMEFRAMES tf, int slot) { datetime t = iTime(symbol, tf, 1); if(t == -1 || t == 0) return false; if(t != lastClosedBarTime[slot]) { lastClosedBarTime[slot] = t; return true; } return false; } void EnsureFVGUpToDate(string symbol, ENUM_TIMEFRAMES tf, int lookback) { if(tf != MiddleTF) return; datetime closed = iTime(symbol, tf, 1); if(closed == 0) return; if(closed != lastFVGBarTime) { UpdateMTFFVGs(symbol, tf, lookback); lastFVGBarTime = closed; } } // VẼ các internal FVG trên timeframe tf nếu timeC nằm giữa sweep_time và break_time void DrawInternalFVGMatches(string sym, ENUM_TIMEFRAMES tf, datetime sweep_time, datetime break_time, double pmin, double pmax, int lookback) { // lấy tất cả internal FVG trên tf double tops[]; double bottoms[]; datetime timeA[]; datetime timeC[]; int types[]; int cnt = FindInternalFVG(sym, tf, lookback, tops, bottoms, timeA, timeC, types); if(cnt <= 0) return; double point = SymbolInfoDouble(sym, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; datetime bar_secs = (datetime)PeriodSeconds(tf); for(int i=0; i break_time) continue; // đảm bảo timeC strictly giữa sweep & break double top = tops[i]; double bottom = bottoms[i]; // kiểm tra overlap phần giá (nếu hoàn toàn ngoài cửa sổ pmin..pmax thì bỏ) if(bottom > pmax + tol || top < pmin - tol) continue; // tạo tên object duy nhất dựa trên timeC và index string nm = SwingObjPrefix + "INTFVG_" + IntegerToString(types[i]) + "_" + IntegerToString(i) + "_" + IntegerToString((int)tc); // Nếu đã tồn tại thì bỏ qua (tránh duplicate) if(ObjectFind(0, nm) >= 0) continue; // start từ bar A tới bar C + 1 bar để hiển thị rõ (tùy bạn có thể dùng timeA[i] hoặc timeC[i]) datetime start_time = timeA[i]; datetime end_time = (datetime)((long)tc + (long)bar_secs); // show zone tới C bar end // Tạo rectangle if(!ObjectCreate(0, nm, OBJ_RECTANGLE, 0, start_time, top, end_time, bottom)) continue; // set style similar DrawFVG uint fill_alpha = 80; uint color_fill = (types[i] == 1) ? MakeARGB((int)fill_alpha, C'229,255,30') : MakeARGB((int)fill_alpha, clrCrimson); uint col_border = MakeARGB(0, clrBlack); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); ObjectSetInteger(0, nm, OBJPROP_BACK, true); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_STYLE, STYLE_SOLID); // fill color ObjectSetInteger(0, nm, OBJPROP_COLOR, (int)color_fill); // MQL may reuse OBJPROP_COLOR for fill here like in DrawFVG } } void DetectMSSOnTimeframe( string sym, ENUM_TIMEFRAMES tf, int slot, bool requireFVG, double minBreakPips, int consecRequired, int lookForwardBars, int fvgLookback ) { // ===== REQUIRE WATCHING FVG BEFORE MSS ===== if(!watchingMSSMode) return; // chỉ xét MSS khi đang ở watching mode if(tf != LowTF) return; // chỉ xét MSS trên LTF if(BOS_Count[slot] < 2) return; // =============================== // 1️⃣ LẤY 2 BOS GẦN NHẤT & SẮP THỨ TỰ THỜI GIAN // =============================== BOSInfo b0 = BOSStore[slot][0]; BOSInfo b1 = BOSStore[slot][1]; BOSInfo olderBOS, newerBOS; if(b0.break_time < b1.break_time) { olderBOS = b0; newerBOS = b1; } else { olderBOS = b1; newerBOS = b0; } if(!olderBOS.found || !newerBOS.found) return; if(olderBOS.direction == newerBOS.direction) return; datetime sweep_time = olderBOS.break_time; datetime break_time = newerBOS.break_time; if(sweep_time == 0 || break_time == 0 || break_time <= sweep_time) return; // =============================== // 2️⃣ XÁC ĐỊNH GIÁ QUÉT THỰC (WICK) // =============================== int sweepIdx = iBarShift(sym, tf, sweep_time, false); if(sweepIdx < 0) return; double sweepExtreme = 0.0; if(newerBOS.direction == 1) sweepExtreme = iLow(sym, tf, sweepIdx); // bullish MSS → quét LOW else sweepExtreme = iHigh(sym, tf, sweepIdx); // bearish MSS → quét HIGH if(sweepExtreme == 0.0) return; // =============================== // 2️⃣.1 VALIDATE LIQUIDITY SWEEP (ATR-based) // =============================== bool isHighSweep = (newerBOS.direction == -1); // bearish MSS quét HIGH if(!IsValidLiquiditySweep(sym, tf, sweepIdx, isHighSweep)) { if(PrintEntryLog) Print("MSS rejected: invalid liquidity sweep (ATR filter)"); return; } // =============================== // 3️⃣ SWING BỊ QUÉT & SWING BỊ PHÁ // =============================== double sweptSwingPrice = olderBOS.broken_sw_price; datetime sweptSwingTime = olderBOS.broken_sw_time; double brokenSwingPrice = newerBOS.broken_sw_price; datetime brokenSwingTime = newerBOS.broken_sw_time; if(sweptSwingPrice == 0.0 || brokenSwingPrice == 0.0) return; // =============================== // 4️⃣ KIỂM TRA MSS HỢP LỆ (ICT CORE) // =============================== double tol = GetPipSize(sym) * 0.1; if(newerBOS.direction == 1) { // Bullish MSS: // - Wick quét sâu hơn swing low // - Sau đó phá swing high if(sweepExtreme >= sweptSwingPrice - tol) return; } else { // Bearish MSS if(sweepExtreme <= sweptSwingPrice + tol) return; } // =============================== // 4️⃣.1 MSS STRENGTH VALIDATION (ATR + BODY) // =============================== if(!IsValidMSS( sym, tf, newerBOS.confirm_bar_index, newerBOS.broken_sw_price, newerBOS.direction == 1 )) { if(PrintEntryLog) Print("MSS rejected: weak break (ATR/body filter)"); return; } // =============================== // 5️⃣ REQUIRE FVG (GIỮ LOGIC CŨ) // =============================== if(requireFVG) { bool hasFVG = false; if(newerBOS.direction == 1) hasFVG = HasBullFVGBetween(sym, tf, sweep_time, break_time, fvgLookback); else hasFVG = HasBearFVGBetween(sym, tf, sweep_time, break_time, fvgLookback); if(!hasFVG) return; } // =============================== // 6️⃣ TẠO MSS (STRUCT MỚI – CLEAN) // =============================== MSS mss; mss.found = true; mss.direction = newerBOS.direction; mss.sweep_time = sweep_time; mss.sweep_extreme_price = sweepExtreme; mss.swept_swing_time = sweptSwingTime; mss.swept_swing_price = sweptSwingPrice; mss.break_time = break_time; mss.bos_close_price = newerBOS.break_price; mss.broken_swing_time = brokenSwingTime; mss.broken_swing_price = brokenSwingPrice; // =============================== // 7️⃣ WATCHING MODE → ENTRY CHECK // =============================== if(watchingMSSMode && watchingFVGIndex >= 0) { if(mss.direction == watchingFVGDir) { bool valid = CheckValidEntry( mss.direction, TrendTF[0], // HTF TrendTF[1], // MTF watchingFVGIndex ); if(valid) { if(PrintEntryLog) Print(">>> ENTRY CONDITIONS PASSED → SETUP ENTRY. Disable watchingMSSMode."); SetUpPendingEntryForMSS(mss, slot); MTF_FVGs[watchingFVGIndex].used = true; ToggleWatchingMSSMode(false); } else { if(PrintEntryLog) Print(">>> ENTRY BLOCKED by CheckValidEntry()"); } } } // =============================== // 8️⃣ VẼ MSS // =============================== DrawMss(sym, tf, mss, slot); } bool CheckValidEntry( int mssDirection, int htfTrend, int mtfTrend, int fvgIndex ) { // ============================= // 0. Sanity check // ============================= if(mssDirection == 0) return false; // HTF phải có trend rõ ràng // TEST // if(htfTrend == 0) // { // if(PrintEntryLog) // Print("CheckValidEntry FAIL: HTF is SIDEWAY"); // return false; // } if(fvgIndex < 0 || fvgIndex >= MTF_FVG_count) { if(PrintEntryLog) Print("CheckValidEntry: invalid FVG index"); return false; } int fvgDir = MTF_FVGs[fvgIndex].type; // 1 bull, -1 bear // ============================= // 1. MSS direction == FVG direction // (thường đã đúng do watchingMSSMode, // nhưng giữ lại cho chắc) // ============================= if(mssDirection != fvgDir) { if(PrintEntryLog) PrintFormat( "CheckValidEntry FAIL: MSS dir=%d != FVG dir=%d", mssDirection, fvgDir ); return false; } // ============================= // 2. FVG direction thuận MTF trend // ============================= if(mtfTrend != 0 && fvgDir != mtfTrend) { if(PrintEntryLog) PrintFormat( "CheckValidEntry FAIL: FVG dir=%d not aligned with MTF trend=%d", fvgDir, mtfTrend ); return false; } // ============================= // 3. MTF trend thuận HTF trend // ============================= if(htfTrend != 0 && mtfTrend != 0 && htfTrend != mtfTrend) { if(PrintEntryLog) PrintFormat( "CheckValidEntry FAIL: MTF trend=%d not aligned with HTF trend=%d", mtfTrend, htfTrend ); return false; } // ============================= // PASS ALL CONDITIONS // ============================= if(PrintEntryLog) { PrintFormat( "CheckValidEntry PASS: dir=%d | FVG=%d | MTF trend=%d | HTF trend=%d", mssDirection, fvgDir, mtfTrend, htfTrend ); } return true; } void HandleLogicForTimeframe(string sym, ENUM_TIMEFRAMES tf, int slot, int swingRange, bool requireFVG, double minBreakPips, int minConsec, int lookbackBarsForSweep, int fvgLookback) { UpdateSwings(sym, tf, slot, swingRange); UpdateTrendForSlot(slot, tf, sym); HandleBOSDetections(sym, tf, slot); if(tf == MiddleTF) EnsureFVGUpToDate(sym, MiddleTF, fvgLookback); DetectMSSOnTimeframe(sym, tf, slot, requireFVG, minBreakPips, minConsec, lookbackBarsForSweep, fvgLookback); if(ShowSwingMarkers) { if (tf == LowTF) DrawSwingsOnChart(tf); if(tf == MiddleTF) DrawFVG(sym, MiddleTF, true); } UpdateLabel((tf == HighTF) ? "LBL_HTF" : (tf == MiddleTF) ? "LBL_MTF" : "LTF: TRIGGER", tf, TrendTF[slot]); } // Clear & reset pending entry (xóa visuals nếu có) void ClearPendingEntry() { if(pendingEntry.fvgIndex >= 0 && pendingEntry.fvgIndex < MTF_FVG_count) { MTF_FVGs[pendingEntry.fvgIndex].used = true; // FVG đã cho entry nhưng không khớp lệnh } if(!pendingEntry.active && StringLen(pendingEntry.compositeName)==0) { return; } if(StringLen(pendingEntry.compositeName) > 0) { string parts[]; int n = StringSplit(pendingEntry.compositeName, '|', parts); for(int i=0;i 0 && ObjectFind(0, nm) >= 0) ObjectDelete(0, nm); } } pendingEntry.active = false; pendingEntry.direction = 0; pendingEntry.price = 0.0; pendingEntry.fvgIndex = -1; pendingEntry.compositeName = ""; pendingEntry.created_time = 0; pendingEntry.source_slot = -1; if(PrintEntryLog) Print("PendingEntry CLEARED"); } // CalculateLotSizeForRisk: // - symbol: symbol // - entryPrice, slPrice: điểm entry và sl (giá thực tế) // - riskPercent: ví dụ 1.0 cho 1% equity // Trả về lotsize phù hợp (đã được clamp theo min/max/step) double CalculateLotSizeForRisk(string symbol, double entryPrice, double slPrice, double riskPercent) { double pipValue = GetPipSize(symbol); // 1 pip in price units double distance = MathAbs(entryPrice - slPrice); // in price units if(distance <= 0.0) return 0.0; // Use account equity as base double equity = AccountInfoDouble(ACCOUNT_EQUITY); if(equity <= 0.0) equity = AccountInfoDouble(ACCOUNT_BALANCE); if(equity <= 0.0) return 0.0; double riskAmount = equity * (riskPercent / 100.0); // Obtain tick size/value info from symbol double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); // Defensive fallback: if tick_size/tick_value unavailable, try point-based approximation double valuePerPointPerLot = 0.0; if(tick_size > 0.0 && tick_value > 0.0) { valuePerPointPerLot = tick_value / tick_size; } else { // approximate: use point and contract size double point = SymbolInfoDouble(symbol, SYMBOL_POINT); // assume valuePerPointPerLot ~ 10 (very rough) -> but better to abort // safer: abort by returning 0 if we don't have reliable tick info return 0.0; } // value risk per lot = distance (price units) * valuePerPointPerLot double valueRiskPerLot = distance * valuePerPointPerLot; if(valueRiskPerLot <= 0.0) return 0.0; double lots = riskAmount / valueRiskPerLot; // clamp to symbol lot limits and step double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double stepLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); if(minLot <= 0.0 || stepLot <= 0.0) { // fallback defaults if broker info missing minLot = 0.01; stepLot = 0.01; maxLot = 100.0; } // Normalize lots to nearest step double stepInv = MathRound(lots / stepLot); double lotsNorm = stepInv * stepLot; // ensure within min/max if(lotsNorm < minLot) lotsNorm = minLot; if(lotsNorm > maxLot) lotsNorm = maxLot; // final safety: round to allowed decimals (step determines decimals) int stepDigits = 0; { double tmp = stepLot; while(tmp < 1.0 && stepDigits < 8) { tmp *= 10.0; stepDigits++; } } lotsNorm = NormalizeDouble(lotsNorm, stepDigits); return lotsNorm; } // PlacePendingOrderFromPendingEntry: // - Gọi DrawPendingEntryVisuals để vẽ entry/sl/tp // - Gửi pending order (BUY_LIMIT nếu pendingEntry.direction==1, SELL_LIMIT nếu -1) // - Lưu ticket vào pendingEntry.order_ticket nếu thành công // - Trả về true nếu order đặt thành công, false nếu lỗi bool PlacePendingOrderFromPendingEntry() { string sym = Symbol(); if(!pendingEntry.active) { if(PrintEntryLog) Print("PlacePendingOrderFromPendingEntry: abort - pendingEntry.active == false"); return false; } if(pendingEntry.price <= 0.0 || pendingEntry.lotSize <= 0.0) { if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: abort - invalid price/lotsize (price=%.10f lots=%.4f)", pendingEntry.price, pendingEntry.lotSize); return false; } // Chuẩn bị trade request MqlTradeRequest request; MqlTradeResult result; ZeroMemory(request); ZeroMemory(result); request.action = TRADE_ACTION_PENDING; // đặt lệnh pending request.symbol = sym; request.volume = pendingEntry.lotSize; request.deviation= 10; // acceptable slippage in points (bạn chỉnh nếu muốn) request.magic = 123456; // chỉnh magic number nếu bạn dùng khác request.comment = "PEND_BY_MSS_OB"; // normalize prices int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); double price = NormalizeDouble(pendingEntry.price, digs); double sl = (pendingEntry.sl_price != 0.0) ? NormalizeDouble(pendingEntry.sl_price, digs) : 0.0; double tp = (pendingEntry.tp_price != 0.0) ? NormalizeDouble(pendingEntry.tp_price, digs) : 0.0; if(pendingEntry.direction == 1) { request.type = ORDER_TYPE_BUY_LIMIT; request.price = price; } else { request.type = ORDER_TYPE_SELL_LIMIT; request.price = price; } // set stoploss/takeprofit as absolute prices request.sl = sl; request.tp = tp; // optional: set expiration (0 = good till canceled) request.expiration = 0; // 3) Send request if(!OrderSend(request, result)) { // OrderSend failed to execute (interface error) if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: OrderSend() returned false. result.retcode=%d retcode_external=%d", result.retcode, result.retcode_external); return false; } // 4) Check result.retcode for success codes (10009 etc.) // Success for pending order is typically TRADE_RETCODE_DONE (10008) or TRADE_RETCODE_DONE_REMAINDER etc. if(result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED || result.retcode == 10006 || result.retcode == 10008 || result.retcode == 10009) { // store ticket pendingEntry.orderTicket = result.order; if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: SUCCESS ticket=%I64u (retcode=%d) entry=%.10f sl=%.10f tp=%.10f lots=%.4f", pendingEntry.orderTicket, result.retcode, price, sl, tp, pendingEntry.lotSize); return true; } else { // failure; log reason if(PrintEntryLog) { PrintFormat("PlacePendingOrderFromPendingEntry: FAILED retcode=%d retval=%d result_comment=%s", result.retcode, result.retcode, result.comment); } return false; } } void SetUpPendingEntryForMSS(const MSS &mss, int slot) { string symbol = Symbol(); int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); double pip = GetPipSize(symbol); // =============================== // 0️⃣ Sanity check // =============================== if(!mss.found || mss.sweep_time == 0 || mss.break_time == 0) return; // =============================== // 1️⃣ FIND ALL LTF INTERNAL FVGs // =============================== double fvgTopPrices[]; double fvgBottomPrices[]; datetime fvgTimeA[]; datetime fvgTimeC[]; int fvgDirections[]; int totalLtfFVGs = FindInternalFVG( symbol, LowTF, 100, // LTF lookback fvgTopPrices, fvgBottomPrices, fvgTimeA, fvgTimeC, fvgDirections ); if(totalLtfFVGs <= 0) return; // =============================== // 2️⃣ SELECT FVG: // - same direction as MSS // - timeC strictly between sweep & break // - closest to break_time // =============================== int selectedFvgIndex = -1; datetime selectedFvgTime = 0; for(int i = 0; i < totalLtfFVGs; i++) { // direction must match MSS if(fvgDirections[i] != mss.direction) continue; // must be between sweep & break if(fvgTimeC[i] <= mss.sweep_time || fvgTimeC[i] >= mss.break_time) continue; // choose the one closest to break_time if(selectedFvgIndex == -1 || fvgTimeC[i] > selectedFvgTime) { selectedFvgIndex = i; selectedFvgTime = fvgTimeC[i]; } } if(selectedFvgIndex == -1) { if(PrintEntryLog) Print("SetUpPendingEntryForMSS: no valid LTF FVG between sweep & break"); return; } // =============================== // 3️⃣ ENTRY = 50% OF SELECTED FVG // =============================== double fvgTop = fvgTopPrices[selectedFvgIndex]; double fvgBottom = fvgBottomPrices[selectedFvgIndex]; double retrace = EntryFvgRetrace; // trend mạnh → entry sớm hơn if(TrendTF[0] == TrendTF[1] && TrendTF[1] == mss.direction) retrace = 0.2; else retrace = EntryFvgRetrace; double entryPrice = NormalizeDouble( fvgBottom + (fvgTop - fvgBottom) * retrace, digits ); // =============================== // 4️⃣ STOP LOSS = LIQUIDITY SWEEP // =============================== double stopLoss = mss.sweep_extreme_price; double slBuffer = 1.0 * pip; // safety buffer if(mss.direction == 1) { stopLoss -= slBuffer; if(stopLoss >= entryPrice) return; } else { stopLoss += slBuffer; if(stopLoss <= entryPrice) return; } stopLoss = NormalizeDouble(stopLoss, digits); // =============================== // 5️⃣ TAKE PROFIT = RR // =============================== double riskDistance = MathAbs(entryPrice - stopLoss); if(riskDistance <= 0.0) return; double takeProfit; if(mss.direction == 1) takeProfit = entryPrice + RiskRewardRatio * riskDistance; else takeProfit = entryPrice - RiskRewardRatio * riskDistance; takeProfit = NormalizeDouble(takeProfit, digits); // =============================== // 6️⃣ LOT SIZE BY RISK // =============================== double lotSize = CalculateLotSizeForRisk( symbol, entryPrice, stopLoss, RishPercent ); if(lotSize <= 0.0) return; // =============================== // 7️⃣ STORE PENDING ENTRY // =============================== ClearPendingEntry(); pendingEntry.active = true; pendingEntry.direction = mss.direction; pendingEntry.price = entryPrice; pendingEntry.sl_price = stopLoss; pendingEntry.tp_price = takeProfit; pendingEntry.lotSize = lotSize; pendingEntry.created_time = TimeCurrent(); pendingEntry.source_slot = slot; pendingEntry.fvgIndex = -1; // LTF FVG, không phải MTF pendingEntry.orderTicket = 0; if(PrintEntryLog) { PrintFormat( "LTF FVG ENTRY SET | dir=%d | entry=%.5f | SL=%.5f | TP=%.5f | FVG time=%s", mss.direction, entryPrice, stopLoss, takeProfit, TimeToString(selectedFvgTime, TIME_DATE|TIME_MINUTES) ); } // =============================== // 8️⃣ PLACE LIMIT ORDER // =============================== PlacePendingOrderFromPendingEntry(); } void MoveStoploss() { string sym = Symbol(); // EA chỉ quản lý 1 position cho symbol hiện tại if(!PositionSelect(sym)) return; ulong ticket = (ulong)PositionGetInteger(POSITION_TICKET); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); if(entry <= 0.0 || sl <= 0.0) return; // Nếu SL đã >= entry (BUY) hoặc <= entry (SELL) → đã BE rồi if(type == POSITION_TYPE_BUY && sl >= entry) return; if(type == POSITION_TYPE_SELL && sl <= entry) return; double R = MathAbs(entry - sl); if(R <= 0.0) return; double bid = SymbolInfoDouble(sym, SYMBOL_BID); double ask = SymbolInfoDouble(sym, SYMBOL_ASK); bool shouldMove = false; // ======= ĐIỀU KIỆN BE DỰA TRÊN numberOfRToMoveToEntry ======= if(type == POSITION_TYPE_BUY) { if(bid >= entry + numberOfRToMoveToEntry * R) shouldMove = true; } else if(type == POSITION_TYPE_SELL) { if(ask <= entry - numberOfRToMoveToEntry * R) shouldMove = true; } if(!shouldMove) return; // ---- MODIFY POSITION SL -> ENTRY ---- MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_SLTP; req.position = ticket; req.symbol = sym; req.sl = NormalizeDouble(entry, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)); req.tp = tp; // giữ nguyên TP if(!OrderSend(req, res)) { if(PrintEntryLog) PrintFormat("MoveStoploss: OrderSend failed ticket=%I64u", ticket); return; } if(res.retcode == TRADE_RETCODE_DONE) { if(PrintEntryLog) PrintFormat("MoveStoploss: SL moved to BE at %.5f (%.2fR), ticket=%I64u", entry, numberOfRToMoveToEntry, ticket); } else { if(PrintEntryLog) PrintFormat("MoveStoploss: failed retcode=%d ticket=%I64u", res.retcode, ticket); } } void CancelExpiredLimitOrder() { if(!pendingEntry.active) return; if(pendingEntry.orderTicket == 0) return; if(pendingEntry.created_time == 0) return; ulong ticket = pendingEntry.orderTicket; // Nếu order không còn tồn tại → đã khớp hoặc bị xóa if(!OrderSelect(ticket)) { if(PrintEntryLog) Print("Pending LIMIT no longer exists → cleared"); ClearPendingEntry(); return; } ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); // Chỉ xử lý LIMIT if(type != ORDER_TYPE_BUY_LIMIT && type != ORDER_TYPE_SELL_LIMIT) return; datetime now = TimeCurrent(); int aliveSec = (int)(now - pendingEntry.created_time); int maxSec = MaxLimitOrderTime * 60; if(aliveSec < maxSec) return; // chưa quá hạn // ---- HỦY LỆNH LIMIT ---- MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_REMOVE; req.order = ticket; if(!OrderSend(req, res)) { if(PrintEntryLog) PrintFormat("Cancel LIMIT failed (OrderSend=false), ticket=%I64u", ticket); return; } if(res.retcode == TRADE_RETCODE_DONE) { if(PrintEntryLog) PrintFormat("LIMIT cancelled after %d minutes, ticket=%I64u", MaxLimitOrderTime, ticket); ClearPendingEntry(); } else { if(PrintEntryLog) PrintFormat("Cancel LIMIT failed retcode=%d ticket=%I64u", res.retcode, ticket); } } void MoveStoplossAdvanced() { string sym = Symbol(); // chỉ quản lý 1 position cho symbol if(!PositionSelect(sym)) return; ulong ticket = (ulong)PositionGetInteger(POSITION_TICKET); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); if(entry <= 0.0 || sl <= 0.0) return; // ===== R BAN ĐẦU ===== double R = MathAbs(entry - sl); if(R <= 0.0) return; double bid = SymbolInfoDouble(sym, SYMBOL_BID); double ask = SymbolInfoDouble(sym, SYMBOL_ASK); // ===== PROFIT HIỆN TẠI (theo R) ===== double profitR = 0.0; if(type == POSITION_TYPE_BUY) profitR = (bid - entry) / R; else profitR = (entry - ask) / R; // ===== CHƯA ĐỦ ĐIỀU KIỆN BE ===== if(profitR < trailingStopStartR) return; int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); double newSL = sl; // ========================================================= // 1️⃣ BE PHASE // ========================================================= if(profitR >= trailingStopStartR && profitR < (trailingStopStartR + trailOffsetR)) { newSL = entry; } else { // ========================================================= // 2️⃣ STEP TRAIL PHASE (theo bậc R) // ========================================================= int step = (int)MathFloor( (profitR - trailingStopStartR) / trailOffsetR ); double targetR = step * trailOffsetR; if(type == POSITION_TYPE_BUY) newSL = entry + targetR * R; else newSL = entry - targetR * R; } newSL = NormalizeDouble(newSL, digits); // ===== KHÔNG CHO SL ĐI LÙI ===== if(type == POSITION_TYPE_BUY && newSL <= sl) return; if(type == POSITION_TYPE_SELL && newSL >= sl) return; // ===== GỬI MODIFY ===== MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_SLTP; req.position = ticket; req.symbol = sym; req.sl = newSL; req.tp = tp; if(!OrderSend(req, res)) { if(PrintEntryLog) PrintFormat("MoveSL ICT-C: OrderSend failed ticket=%I64u", ticket); return; } if(res.retcode == TRADE_RETCODE_DONE && PrintEntryLog) { PrintFormat( "MoveSL ICT-C: SL -> %.5f | profitR=%.2fR | step=%d", newSL, profitR, (int)MathFloor((profitR - trailingStopStartR) / trailOffsetR) ); } } bool IsEntryTimeAllowed() { datetime now = TimeCurrent(); MqlDateTime t; TimeToStruct(now, t); int hour = t.hour; // ===== LONDON ===== if(hour >= LondonStartHourVN && hour < LondonEndHourVN) return true; // ===== NEW YORK ===== if(hour >= NYStartHourVN && hour < NYEndHourVN) return true; return false; } bool IsAllowedToTrade() { UpdateDailyEquity(); if(IsDailyLossExceeded()) return false; // =============================== // REGIME FILTER – TREND REQUIRED // =============================== double adx = GetADX(_Symbol, MiddleTF, 14, 1); // dùng bar đã đóng if(adx < ADX_Min) { if(PrintEntryLog) PrintFormat("Trade blocked: ADX %.2f < %d", adx, ADX_Min); return false; } if(!IsEntryTimeAllowed()) return false; if(EnableNewsFilter && IsHighImpactNewsNear(_Symbol, NewsBlockBeforeMin, NewsBlockAfterMin)) return false; return true; } void OnTick() { string sym = Symbol(); CancelExpiredLimitOrder(); if (numberOfRToMoveToEntry > 0) { // MoveStoploss(); MoveStoplossAdvanced(); } if(IsNewClosedBar(sym, LowTF, 2)) { // if(!IsAllowedToTrade()) //TEST // return; HandleLogicForTimeframe(sym, HighTF, 0, htfSwingRange, false, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, MiddleTF, 1, mtfSwingRange, true, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, LowTF, 2, ltfSwingRange, true, 10.0, 2, 50, FVGLookback); if (watchingMSSMode) { CheckWatchMSSInvalidation(sym); } } UpdateMTFFVGTouched(sym); } void MarkHistoricalTouchedFVGs(string symbol) { if(MTF_FVG_count <= 0) return; for(int i = 0; i < MTF_FVG_count; i++) { // reset mặc định MTF_FVGs[i].touched = false; MTF_FVGs[i].used = false; MTF_FVGs[i].touchTime = 0; MTF_FVGs[i].touchPrice = 0; } // Dùng logic đã có, nhưng scan TOÀN BỘ LTF history for(int i = 0; i < MTF_FVG_count; i++) { datetime mtfC = MTF_FVGs[i].timebarC; if(mtfC == 0) continue; int idxC = iBarShift(symbol, LowTF, mtfC, false); if(idxC <= 0) continue; double top = MTF_FVGs[i].topPrice; double bottom = MTF_FVGs[i].bottomPrice; int dir = MTF_FVGs[i].type; for(int idx = idxC - 1; idx >= 0; idx--) { double h = iHigh(symbol, LowTF, idx); double l = iLow(symbol, LowTF, idx); if(h == 0 || l == 0) continue; bool touched = (dir == 1 && h >= top) || (dir == -1 && l <= bottom); if(touched) { MTF_FVGs[i].touched = true; MTF_FVGs[i].used = true; // 🔥 QUAN TRỌNG MTF_FVGs[i].touchTime = iTime(symbol, LowTF, idx); MTF_FVGs[i].touchPrice= (dir == 1 ? top : bottom); break; } } } } void InitMTFFVGs(string symbol, ENUM_TIMEFRAMES tf, int lookback) { ArrayFree(MTF_FVGs); MTF_FVG_count = 0; int total = iBars(symbol, tf); if(total < 5) return; int maxScan = MathMin(lookback, total - 3); double tol = SymbolInfoDouble(symbol, SYMBOL_POINT) * 0.5; for(int i = 1; i <= maxScan; i++) { int idxA = i + 2; int idxC = i; if(idxA > total - 1) break; double highA = iHigh(symbol, tf, idxA); double lowA = iLow(symbol, tf, idxA); double highC = iHigh(symbol, tf, idxC); double lowC = iLow(symbol, tf, idxC); if(highA == 0 || lowA == 0 || highC == 0 || lowC == 0) continue; datetime timeC = iTime(symbol, tf, idxC); // Bullish FVG if(lowC > highA + tol) { int idx = MTF_FVG_count; ArrayResize(MTF_FVGs, MTF_FVG_count + 1); MTF_FVG_count++; MTF_FVGs[idx].type = 1; MTF_FVGs[idx].topPrice = lowC; MTF_FVGs[idx].bottomPrice = highA; MTF_FVGs[idx].timebarA = iTime(symbol, tf, idxA); MTF_FVGs[idx].timebarC = timeC; MTF_FVGs[idx].touched = false; MTF_FVGs[idx].used = false; MTF_FVGs[idx].touchTime = 0; MTF_FVGs[idx].touchPrice = 0; continue; } // Bearish FVG if(highC < lowA - tol) { int idx = MTF_FVG_count; ArrayResize(MTF_FVGs, MTF_FVG_count + 1); MTF_FVG_count++; MTF_FVGs[idx].type = -1; MTF_FVGs[idx].topPrice = lowA; MTF_FVGs[idx].bottomPrice = highC; MTF_FVGs[idx].timebarA = iTime(symbol, tf, idxA); MTF_FVGs[idx].timebarC = timeC; MTF_FVGs[idx].touched = false; MTF_FVGs[idx].used = false; MTF_FVGs[idx].touchTime = 0; MTF_FVGs[idx].touchPrice = 0; } } } void UpdateMTFFVGs(string symbol, ENUM_TIMEFRAMES tf, int lookback) { int total = iBars(symbol, tf); if(total < 5) return; int maxScan = MathMin(lookback, total - 3); double tol = SymbolInfoDouble(symbol, SYMBOL_POINT) * 0.5; for(int i = 1; i <= maxScan; i++) { int idxA = i + 2; int idxC = i; if(idxA > total - 1) break; datetime timeC = iTime(symbol, tf, idxC); if(timeC == 0) continue; // CHECK DUPLICATE BY timebarC bool exists = false; for(int k = 0; k < MTF_FVG_count; k++) { if(MTF_FVGs[k].timebarC == timeC) { exists = true; break; } } if(exists) continue; double highA = iHigh(symbol, tf, idxA); double lowA = iLow(symbol, tf, idxA); double highC = iHigh(symbol, tf, idxC); double lowC = iLow(symbol, tf, idxC); if(highA == 0 || lowA == 0 || highC == 0 || lowC == 0) continue; if(lowC > highA + tol || highC < lowA - tol) { int idx = MTF_FVG_count; ArrayResize(MTF_FVGs, MTF_FVG_count + 1); MTF_FVG_count++; MTF_FVGs[idx].type = (lowC > highA) ? 1 : -1; MTF_FVGs[idx].topPrice = (MTF_FVGs[idx].type == 1) ? lowC : lowA; MTF_FVGs[idx].bottomPrice = (MTF_FVGs[idx].type == 1) ? highA : highC; MTF_FVGs[idx].timebarA = iTime(symbol, tf, idxA); MTF_FVGs[idx].timebarC = timeC; MTF_FVGs[idx].touched = false; MTF_FVGs[idx].used = false; MTF_FVGs[idx].touchTime = 0; MTF_FVGs[idx].touchPrice = 0; } } } int OnInit() { string sym = Symbol(); InitMTFFVGs(sym, MiddleTF, FVGLookback); MarkHistoricalTouchedFVGs(sym); HandleLogicForTimeframe(sym, HighTF, 0, htfSwingRange, false, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, MiddleTF, 1, mtfSwingRange, true, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, LowTF, 2, ltfSwingRange, true, 10.0, 2, 50, FVGLookback); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { for(int s=0; s<3; s++) { for(int j=0; j<2; j++) { if(StringLen(BOS_Names[s][j]) > 0) DeleteCompositeBOS(BOS_Names[s][j]); BOS_Names[s][j] = ""; BOS_Count[s] = 0; BOSStore[s][j].found = false; BOS_HaveZone[s][j] = false; } } if(ObjectFind(0, "LBL_HTF") >= 0) ObjectDelete(0, "LBL_HTF"); if(ObjectFind(0, "LBL_MTF") >= 0) ObjectDelete(0, "LBL_MTF"); if(ObjectFind(0, "LTF: TRIGGER") >= 0) ObjectDelete(0, "LTF: TRIGGER"); int tot = ObjectsTotal(0); for(int i = tot - 1; i >= 0; i--) { string nm = ObjectName(0, i); if(StringFind(nm, SwingObjPrefix, 0) == 0) ObjectDelete(0, nm); } } double GetATR(string symbol, ENUM_TIMEFRAMES tf, int period, int shift) { static int atrHandle = INVALID_HANDLE; static ENUM_TIMEFRAMES lastTF = PERIOD_CURRENT; static int lastPeriod = 0; // recreate handle if TF or period changed if(atrHandle == INVALID_HANDLE || tf != lastTF || period != lastPeriod) { if(atrHandle != INVALID_HANDLE) IndicatorRelease(atrHandle); atrHandle = iATR(symbol, tf, period); lastTF = tf; lastPeriod = period; } if(atrHandle == INVALID_HANDLE) return 0.0; double buffer[]; if(CopyBuffer(atrHandle, 0, shift, 1, buffer) <= 0) return 0.0; return buffer[0]; } double GetADX(string symbol, ENUM_TIMEFRAMES tf, int period, int shift) { static int adxHandle = INVALID_HANDLE; static ENUM_TIMEFRAMES lastTF = PERIOD_CURRENT; static int lastPeriod = 0; // recreate handle if TF or period changed if(adxHandle == INVALID_HANDLE || tf != lastTF || period != lastPeriod) { if(adxHandle != INVALID_HANDLE) IndicatorRelease(adxHandle); adxHandle = iADX(symbol, tf, period); lastTF = tf; lastPeriod = period; } if(adxHandle == INVALID_HANDLE) return 0.0; double buffer[]; // ADX MAIN = buffer index 0 if(CopyBuffer(adxHandle, 0, shift, 1, buffer) <= 0) return 0.0; return buffer[0]; }