//+------------------------------------------------------------------+ //| 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 // Bật/tắt MSS detection cho từng TF input bool DetectMSS_HTF = true; input bool DetectMSS_MTF = true; input bool DetectMSS_LTF = false; // Cấu hình Swing input int htfSwingRange = 1; // X: số nến trước và sau để xác định 1 đỉnh/đáy input int mtfSwingRange = 3; // X: số nến trước và sau để xác định 1 đỉnh/đáy input int ltfSwingRange = 3; // X: số nến trước và sau để xác định 1 đỉnh/đáy input int MaxSwingKeep = 2; // Số đỉnh/đáy gần nhất cần lưu (bạn yêu cầu 2) // 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 = true; // In log ra Expert // --- 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_D1; // Khung thời gian cao input ENUM_TIMEFRAMES MiddleTF = PERIOD_H1; // Khung thời gian trung bình (dùng để tìm FVG) input ENUM_TIMEFRAMES LowTF = PERIOD_M5; // 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 results (global) double FVG_top[]; // giá trên của zone (lớn hơn) double FVG_bottom[]; // giá dưới của zone (nhỏ hơn) datetime FVG_timeA[]; // time của bar A (older) datetime FVG_timeC[]; // time của bar C (newer) int FVG_type[]; // 1 = bullish, -1 = bearish int FVG_count = 0; // 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 dùng để trả thông tin khi phát hiện MSS struct MSSInfo { bool found; // true nếu tìm thấy MSS int direction; // 1 = down->up (bull MSS), -1 = up->down (bear MSS) datetime sweep_time; // thời điểm bar "sweep" (bar quét thanh khoản) double sweep_price; // giá (râu) bị sweep (low cho bull, high cho bear) // --- thêm: swing gốc đã bị swept (time/price) --- datetime swept_swing_time; // time của swing bar trước khi bị sweep (ví dụ l0/h0 tại thời điểm detect) double swept_swing_price; // giá swing gốc double key_level; // mức key level (swing cần phá) datetime break_time; // thời điểm bar phá key level (thỏa điều kiện break) // --- thêm: swing gốc bị broken (time/price) --- datetime broken_swing_time; // time của swing bar bị phá (ví dụ sh0/sl0 tại thời điểm detect) double broken_swing_price; // giá swing gốc bị phá }; // ---------------------- Helper: pip size ---------------------- // Trả về 1 "pip" cho symbol (không phải point). // Lý do: nhiều broker dùng 5 chữ số (0.00001) => pip = point*10; 4 chữ số => pip = point. double GetPipSize(string symbol) { // Lấy số chữ số (digits) và point của symbol int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); // Quy ước phổ biến: // - digits 5 hoặc 3 => broker dùng extra digit -> 1 pip = point * 10 // - digits 4 hoặc 2 => 1 pip = point // Việc dùng pip (không phải point) thuận tiện để người dùng nhập ngưỡng theo pips (ví dụ 30 pips). if(digits==5 || digits==3) return point * 10.0; return point; } bool HasBullFVGAfter(string symbol, ENUM_TIMEFRAMES timeframe, datetime after_time, int fvgLookback) { FindFVG(symbol, timeframe, fvgLookback); // cập nhật FVG_count và FVG_* arrays for(int k = 0; k < FVG_count; k++) { if(FVG_type[k] == 1 && FVG_timeC[k] > after_time) return true; } return false; } bool HasBearFVGAfter(string symbol, ENUM_TIMEFRAMES timeframe, datetime after_time, int fvgLookback) { FindFVG(symbol, timeframe, fvgLookback); for(int k = 0; k < FVG_count; k++) { if(FVG_type[k] == -1 && FVG_timeC[k] > after_time) return true; } return false; } // ---------------------- Hàm chính: DetectMSSOnSlot ---------------------- // Mục tiêu: xác định Market Structure Shift (MSS) trên một timeframe (slot). // - Dùng các swing đã tính sẵn: SwingHighPriceTF[slot][*], SwingLowPriceTF[slot][*] // - Có thể yêu cầu FVG xuất hiện sau sweep (requireFVG = true) // Tham số: // symbol : ký hiệu (Symbol()) // timeframe : timeframe để kiểm tra (ví dụ MiddleTF) // slot : index trong mảng swings (0=HTF,1=MTF,2=LTF trong EA của bạn) // minBreakPips : số pips tối thiểu để coi là "phá" key level (ví dụ 30 pips) // minConsec : số nến liên tiếp cùng màu sau sweep để chứng minh momentum // lookbackBarsForSweep : số bar tối đa quét tìm bar sweep sau swing (giảm chi phí tính toán) // fvgLookback : nếu dùng FVG, đưa vào FindFVG(..., fvgLookback) // requireFVG : nếu true, bắt buộc phải có FVG loại phù hợp xuất hiện sau sweep MSSInfo DetectMSSOnSlot( string symbol, ENUM_TIMEFRAMES timeframe, int slot, double minBreakPips = 5.0, int minConsec = 2, int lookbackBarsForSweep = 50, int fvgLookback = 200, bool requireFVG = true) { MSSInfo result; result.found = false; result.direction = 0; result.sweep_time = 0; result.sweep_price = 0.0; result.key_level = 0.0; result.break_time = 0; if(PrintToExperts) PrintFormat("DetectMSSOnSlot START: sym=%s tf=%s slot=%d minBreakPips=%.1f minConsec=%d lookback=%d requireFVG=%s", symbol, EnumToString(timeframe), slot, minBreakPips, minConsec, lookbackBarsForSweep, (requireFVG ? "true":"false")); // cần ít nhất 2 swing high + 2 swing low if(SwingHighCountTF[slot] < 2 || SwingLowCountTF[slot] < 2) { if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d: not enough swings H=%d L=%d -> exit", slot, SwingHighCountTF[slot], SwingLowCountTF[slot]); return result; } // lấy swing (index 0 = nearest, 1 = older) double sh0 = SwingHighPriceTF[slot][0]; double sh1 = SwingHighPriceTF[slot][1]; double sl0 = SwingLowPriceTF[slot][0]; double sl1 = SwingLowPriceTF[slot][1]; if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d swings: sh0=%.5f sh1=%.5f sl0=%.5f sl1=%.5f", slot, sh0, sh1, sl0, sl1); double pip = GetPipSize(symbol); double minBreakPoints = minBreakPips * pip; double tol = SymbolInfoDouble(symbol, SYMBOL_POINT) * 0.5; int barsAvailable = iBars(symbol, timeframe); if(barsAvailable <= 3) { if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d: not enough bars (%d) -> exit", slot, barsAvailable); return result; } int maxScan = MathMin(lookbackBarsForSweep, barsAvailable - 1); // trend đã tính trước và lưu vào TrendTF[] int currentTrend = TrendTF[slot]; if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d currentTrend=%d maxScan=%d minBreakPoints=%.8f tol=%.8f", slot, currentTrend, maxScan, minBreakPoints, tol); // ----------------------- BULLISH PATH ----------------------- // Logic (updated): swept = sl0 (nearest low), key_level = sh0 (nearest high) if(currentTrend == -1 || currentTrend == 0) { double sweptLow = sl0; // đáy gần nhất phải bị sweep double key_level = sh0; // đỉnh gần nhất cần bị phá để xác nhận MSS (breaker) if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d BULL check: sweptLow=%.5f key_level=%.5f", slot, sweptLow, key_level); for(int idx = 1; idx <= maxScan; idx++) { double low_i = iLow(symbol, timeframe, idx); double close_i = iClose(symbol, timeframe, idx); datetime t_i = iTime(symbol, timeframe, idx); if(low_i == 0 || close_i == 0) continue; // sweep xuống dưới sweptLow và đóng trở lại trên sweptLow if(low_i < sweptLow - tol && close_i > sweptLow + tol) { if(PrintToExperts) PrintFormat("slot=%d idx=%d: potential BULL sweep at low=%.5f close=%.5f sweptLow=%.5f t=%s", slot, idx, low_i, close_i, sweptLow, TimeToString(t_i, TIME_DATE|TIME_MINUTES)); // check momentum (nến xanh liên tiếp ngay sau sweep) int consec = 0; int j = idx - 1; while(j >= 0 && consec < minConsec) { double c_j = iClose(symbol, timeframe, j); double o_j = iOpen(symbol, timeframe, j); if(c_j == 0 || o_j == 0) break; if(c_j > o_j) consec++; else break; j--; } if(PrintToExperts) PrintFormat("slot=%d idx=%d: consec green=%d required=%d", slot, idx, consec, minConsec); if(consec >= minConsec) { bool fvg_ok = true; if(requireFVG) fvg_ok = HasBullFVGAfter(symbol, timeframe, t_i, fvgLookback); if(PrintToExperts && requireFVG) PrintFormat("slot=%d idx=%d: fvg_ok=%s", slot, idx, (fvg_ok?"true":"false")); if(fvg_ok) { // tìm breaker: nến đóng >= key_level + threshold bool broke = false; datetime break_time = 0; for(int k = idx - 1; k >= 0; k--) { double close_k = iClose(symbol, timeframe, k); if(close_k == 0) continue; if(close_k >= key_level + minBreakPoints) { broke = true; break_time = iTime(symbol, timeframe, k); if(PrintToExperts) PrintFormat("slot=%d idx=%d: breaker at k=%d close=%.5f (>= %.5f) t=%s", slot, idx, k, close_k, key_level + minBreakPoints, TimeToString(break_time, TIME_DATE|TIME_MINUTES)); break; } } if(broke) { result.found = true; result.direction = 1; result.sweep_time = t_i; result.sweep_price = low_i; result.key_level = key_level; result.break_time = break_time; // --- lưu swing gốc tại thời điểm detect --- // swept swing = nearest low at detect time => sl0 (index 0) result.swept_swing_time = SwingLowTimeTF[slot][0]; result.swept_swing_price = SwingLowPriceTF[slot][0]; // broken swing (key) = nearest high sh0 (index 0) result.broken_swing_time = SwingHighTimeTF[slot][0]; result.broken_swing_price = SwingHighPriceTF[slot][0]; if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d -> BULL MSS FOUND sweep=%.5f key=%.5f break=%s", slot, result.sweep_price, result.key_level, TimeToString(result.break_time, TIME_DATE|TIME_MINUTES)); return result; } else { if(PrintToExperts) PrintFormat("slot=%d idx=%d: no breaker after sweep (need >= %.5f)", slot, idx, key_level + minBreakPoints); } } } } // end if sweep } // end for idx if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d: finished BULL checks, not found", slot); } // ----------------------- BEARISH PATH ----------------------- // Logic (updated): swept = sh0 (nearest high), key_level = sl0 (nearest low) if(currentTrend == 1 || currentTrend == 0) { double sweptHigh = sh0; // đỉnh gần nhất phải bị sweep double key_level = sl0; // đáy gần nhất cần bị phá để xác nhận MSS (breaker) if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d BEAR check: sweptHigh=%.5f key_level=%.5f", slot, sweptHigh, key_level); for(int idx = 1; idx <= maxScan; idx++) { double high_i = iHigh(symbol, timeframe, idx); double close_i = iClose(symbol, timeframe, idx); datetime t_i = iTime(symbol, timeframe, idx); if(high_i == 0 || close_i == 0) continue; // sweep lên trên sweptHigh và đóng trở lại dưới sweptHigh if(high_i > sweptHigh + tol && close_i < sweptHigh - tol) { if(PrintToExperts) PrintFormat("slot=%d idx=%d: potential BEAR sweep at high=%.5f close=%.5f sweptHigh=%.5f t=%s", slot, idx, high_i, close_i, sweptHigh, TimeToString(t_i, TIME_DATE|TIME_MINUTES)); // check momentum (nến đỏ liên tiếp) int consec = 0; int j = idx - 1; while(j >= 0 && consec < minConsec) { double c_j = iClose(symbol, timeframe, j); double o_j = iOpen(symbol, timeframe, j); if(c_j == 0 || o_j == 0) break; if(c_j < o_j) consec++; else break; j--; } if(PrintToExperts) PrintFormat("slot=%d idx=%d: consec red=%d required=%d", slot, idx, consec, minConsec); if(consec >= minConsec) { bool fvg_ok = true; if(requireFVG) fvg_ok = HasBearFVGAfter(symbol, timeframe, t_i, fvgLookback); if(PrintToExperts && requireFVG) PrintFormat("slot=%d idx=%d: fvg_ok=%s", slot, idx, (fvg_ok?"true":"false")); if(fvg_ok) { // tìm breaker: nến đóng <= key_level - threshold bool broke = false; datetime break_time = 0; for(int k = idx - 1; k >= 0; k--) { double close_k = iClose(symbol, timeframe, k); if(close_k == 0) continue; if(close_k <= key_level - minBreakPoints) { broke = true; break_time = iTime(symbol, timeframe, k); if(PrintToExperts) PrintFormat("slot=%d idx=%d: breaker at k=%d close=%.5f (<= %.5f) t=%s", slot, idx, k, close_k, key_level - minBreakPoints, TimeToString(break_time, TIME_DATE|TIME_MINUTES)); break; } } if(broke) { result.found = true; result.direction = -1; result.sweep_time = t_i; result.sweep_price = high_i; result.key_level = key_level; result.break_time = break_time; // lưu swing gốc result.swept_swing_time = SwingHighTimeTF[slot][0]; result.swept_swing_price = SwingHighPriceTF[slot][0]; // broken swing (key) = nearest low sl0 result.broken_swing_time = SwingLowTimeTF[slot][0]; result.broken_swing_price = SwingLowPriceTF[slot][0]; if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d -> BEAR MSS FOUND sweep=%.5f key=%.5f break=%s", slot, result.sweep_price, result.key_level, TimeToString(result.break_time, TIME_DATE|TIME_MINUTES)); return result; } else { if(PrintToExperts) PrintFormat("slot=%d idx=%d: no breaker after sweep (need <= %.5f)", slot, idx, key_level - minBreakPoints); } } } } // end if sweep up } // end for idx if(PrintToExperts) PrintFormat("DetectMSSOnSlot slot=%d: finished BEAR checks, not found", slot); } if(PrintToExperts) PrintFormat("DetectMSSOnSlot END: slot=%d not found any MSS", slot); return result; } // 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; // So sánh với X nến trước (index giảm) for(int i = 1; i <= swingRange; i++) { int idxPrev = candleIndex - i; if(idxPrev < 0) return false; // không đủ dữ liệu double hprev = iHigh(symbol, timeframe, idxPrev); if(hprev == 0.0) return false; // dữ liệu thiếu if(hprev >= hi) // nếu có râu high trước >= hi -> không phải swing high return false; } // So sánh với X nến sau (index tăng) for(int k = 1; k <= swingRange; k++) { int idxNext = candleIndex + k; // nếu idxNext vượt quá bars available -> không có dữ liệu đủ -> return false double hnext = iHigh(symbol, timeframe, idxNext); if(hnext == 0.0) return false; if(hnext >= hi) // nếu có râu high sau >= hi -> không phải swing high 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; // So sánh với X nến trước (index giảm) for(int j = 1; j <= X; j++) { int idxPrev = i - j; if(idxPrev < 0) return false; // không đủ dữ liệu double lprev = iLow(symbol, timeframe, idxPrev); if(lprev == 0.0) return false; // dữ liệu thiếu if(lprev <= lo) // nếu râu low trước <= lo -> không phải swing low return false; } // So sánh với X nến sau (index tăng) 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) // nếu râu low sau <= lo -> không phải swing low return false; } return true; } // Cập nhật mảng SwingHighPrice/Time và SwingLowPrice/Time (giữ MaxSwingKeep phần tử gần nhất) // UpdateSwings vào slot (0 = HighTF, 1 = MiddleTF) void UpdateSwings(string symbol, ENUM_TIMEFRAMES timeframe, int slot, int SwingRange) { // Reset slot SwingHighCountTF[slot] = 0; SwingLowCountTF[slot] = 0; int total = iBars(symbol, timeframe); if(total <= SwingRange + 2) return; // dữ liệu không đủ int iStart = SwingRange + 1; int iEnd = total - SwingRange - 1; // Duyệt từ index nhỏ (gần nhất) tới xa để tìm Swing gần nhất trước for(int i = iStart; i <= iEnd; i++) { // nếu đã đủ cả 2 loại thì dừng if(SwingHighCountTF[slot] >= MaxSwingKeep && SwingLowCountTF[slot] >= MaxSwingKeep) break; // Kiểm tra SwingHigh bằng hàm tiện ích 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]++; } } // Kiểm tra SwingLow bằng hàm tiện ích 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 | //+------------------------------------------------------------------+ // Draw swings for the given timeframe (uses stored slot arrays) // Draw swings for the given timeframe (uses stored slot arrays) // Updated: use explicit prefixes "htf_" and "MTF_" and delete only matching objects void DrawSwingsOnChart(ENUM_TIMEFRAMES timeframe) { if(!ShowSwingMarkers) return; // mapping chung: 0=HTF, 1=MTF, 2=LTF int slot = 0; // default 0=HTF string inChartSwingPrefix = "h"; if(timeframe == MiddleTF) { slot = 1; inChartSwingPrefix = "m"; } else if (timeframe == LowTF) { slot = 2; inChartSwingPrefix = "l"; } string prefix = SwingObjPrefix + inChartSwingPrefix; // Xóa các marker cũ chỉ theo prefix này (không xóa chung tất cả SwingObjPrefix) 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); } } // Thời lượng bar (giây) datetime bar_secs = (datetime)PeriodSeconds(timeframe); // Vẽ Swing High int maxH = (SwingHighCountTF[slot] < MaxSwingKeep) ? SwingHighCountTF[slot] : MaxSwingKeep; for(int h = 0; h < maxH; h++) { string obj = prefix + "H_" + IntegerToString(h); // e.g. "HTF_SW_mid_H_0" 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)) { Print("Cannot create object: ", obj); 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); } // Vẽ Swing Low 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)) { Print("Cannot create object: ", obj); 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) { // Lấy index bar tại thời điểm start_time int idxStart = iBarShift(symbol, timeframe, start_time, false); if(idxStart == -1) idxStart = 0; // Quét các bar mới hơn (index nhỏ hơn) 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; // Nếu bar chạm price if(lowM <= price + tol && highM >= price - tol) return iTime(symbol, timeframe, idx); } // Không tìm thấy → kéo đến bar hiện tại + 1 bar datetime t0 = iTime(symbol, timeframe, 0); if(t0 != 0) return (datetime)((long)t0 + (long)bar_secs); return TimeCurrent(); } // DrawMss: vẽ MSS + liquidity sweep cho 1 slot/timeframe // symbol : symbol (Symbol()) // timeframe : timeframe (MiddleTF / HighTF / LowTF ...) // info : MSSInfo struct (result của DetectMSSOnSlot) - truyền bằng tham chiếu // slot : 0=HTF,1=MTF,2=LTF (dùng để phân biệt object name và xoá cũ) // DrawMss: vẽ MSS + liquidity sweep cho 1 slot/timeframe // symbol : symbol (Symbol()) // timeframe : timeframe (MiddleTF / HighTF / LowTF ...) // info : MSSInfo struct (result của DetectMSSOnSlot) - truyền bằng tham chiếu // slot : 0=HTF,1=MTF,2=LTF (dùng để phân biệt object name và xoá cũ) void DrawMss(string symbol, ENUM_TIMEFRAMES timeframe, const MSSInfo &info, int slot) { if(!ShowSwingMarkers) return; // tiền tố timeframe cho tên object string tfPrefix = "ltf_"; if(slot == 0) tfPrefix = "htf_"; else if(slot == 1) tfPrefix = "mtf_"; string basePrefix = SwingObjPrefix + "MSS_" + IntegerToString(slot) + "_"; // Xóa object MSS cũ cho slot này int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string nm = ObjectName(0, i); if(StringLen(nm) >= StringLen(basePrefix)) if(StringSubstr(nm, 0, StringLen(basePrefix)) == basePrefix) ObjectDelete(0, nm); } if(!info.found) { PrintFormat("Error: DrawMss: no MSS found for slot=%d, nothing to draw", slot); return; } if(info.sweep_time == 0 || info.break_time == 0) { PrintFormat("Error: DrawMss: invalid MSSInfo for slot=%d (sweep_time=%d break_time=%d)", slot, info.sweep_time, info.break_time); return; } // object names (có tiền tố tfPrefix) string nm_sweep_ray = basePrefix + tfPrefix + "SWEEP_RAY"; string nm_key_ray = basePrefix + tfPrefix + "KEY_RAY"; string nm_sweep_txt = basePrefix + tfPrefix + "SWEEP_TXT"; string nm_key_txt = basePrefix + tfPrefix + "KEY_TXT"; // style int col_sweep = clrMagenta; // màu sweep (tím) int col_key = clrOrange; // màu key/break int width_line = 2; int font_sweep = SwingMarkerFontSize + 2; int font_key = SwingMarkerFontSize; datetime bar_secs = (datetime)PeriodSeconds(timeframe); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; // --- SWEEP RAY --- // End of sweep ray is the sweep bar (info.sweep_time, info.sweep_price) datetime end_sweep_time = info.sweep_time; double end_sweep_price = info.sweep_price; if(end_sweep_time == 0) end_sweep_time = iTime(symbol, timeframe, 0); if(end_sweep_price == 0.0) end_sweep_price = (info.direction == 1 ? SwingLowPriceTF[slot][0] : SwingHighPriceTF[slot][0]); // Start of sweep ray must be the swing bar that was swept: // - bullish (direction==1): start from SwingLow index 0 (l0) // - bearish (direction==-1): start from SwingHigh index 0 (h0) datetime start_sweep_time = end_sweep_time; double start_sweep_price = end_sweep_price; // ưu tiên dùng swing gốc đã lưu trong info (nếu có) if(info.swept_swing_time != 0 && info.swept_swing_price != 0.0) { start_sweep_time = info.swept_swing_time; start_sweep_price = info.swept_swing_price; } else { // fallback: dùng current Swing arrays nếu info không có if(info.direction == 1) { if(SwingLowTimeTF[slot][0] != 0 && SwingLowPriceTF[slot][0] != 0.0) { start_sweep_time = SwingLowTimeTF[slot][0]; start_sweep_price = SwingLowPriceTF[slot][0]; } } else if(info.direction == -1) { if(SwingHighTimeTF[slot][0] != 0 && SwingHighPriceTF[slot][0] != 0.0) { start_sweep_time = SwingHighTimeTF[slot][0]; start_sweep_price = SwingHighPriceTF[slot][0]; } } } // Draw sweep ray from start_sweep -> sweep bar (end_sweep) if(!ObjectCreate(0, nm_sweep_ray, OBJ_TREND, 0, start_sweep_time, start_sweep_price, end_sweep_time, end_sweep_price)) PrintFormat("DrawMss: Cannot create %s", nm_sweep_ray); ObjectSetInteger(0, nm_sweep_ray, OBJPROP_COLOR, col_sweep); ObjectSetInteger(0, nm_sweep_ray, OBJPROP_WIDTH, width_line); ObjectSetInteger(0, nm_sweep_ray, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, nm_sweep_ray, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_sweep_ray, OBJPROP_BACK, true); // sweep label (ở giữa đoạn) datetime mid_sweep = (datetime)(((long)start_sweep_time + (long)end_sweep_time)/2); if(!ObjectCreate(0, nm_sweep_txt, OBJ_TEXT, 0, mid_sweep, end_sweep_price)) PrintFormat("DrawMss: Cannot create %s", nm_sweep_txt); else { ObjectSetString(0, nm_sweep_txt, OBJPROP_TEXT, StringFormat("%sSweep: %s", tfPrefix, DoubleToString(end_sweep_price, _Digits))); ObjectSetInteger(0, nm_sweep_txt, OBJPROP_COLOR, col_sweep); ObjectSetInteger(0, nm_sweep_txt, OBJPROP_FONTSIZE, font_sweep); ObjectSetInteger(0, nm_sweep_txt, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_sweep_txt, OBJPROP_BACK, true); } // --- KEY / BREAKER RAY --- // Start of key ray should be the broken swing bar (sh0 for bullish breaker, sl0 for bearish breaker) datetime start_key_time = info.break_time; double start_key_price = info.key_level; // ưu tiên dùng broken swing gốc đã lưu trong info if(info.broken_swing_time != 0 && info.broken_swing_price != 0.0) { start_key_time = info.broken_swing_time; start_key_price = info.broken_swing_price; } else { // fallback: dùng current Swing arrays if(info.direction == 1) { if(SwingHighTimeTF[slot][0] != 0 && SwingHighPriceTF[slot][0] != 0.0) { start_key_time = SwingHighTimeTF[slot][0]; start_key_price = SwingHighPriceTF[slot][0]; } } else if(info.direction == -1) { if(SwingLowTimeTF[slot][0] != 0 && SwingLowPriceTF[slot][0] != 0.0) { start_key_time = SwingLowTimeTF[slot][0]; start_key_price = SwingLowPriceTF[slot][0]; } } } // End of key ray: use close price of breaker bar (info.break_time). If not available fallback to key_level double end_key_price = info.key_level; datetime end_key_time = info.break_time; if(end_key_time != 0) { int idxBreak = iBarShift(symbol, timeframe, end_key_time, false); if(idxBreak >= 0) { double closeBreak = iClose(symbol, timeframe, idxBreak); if(closeBreak != 0.0) end_key_price = closeBreak; // set end_key_time to the exact time of that bar (safe) end_key_time = iTime(symbol, timeframe, idxBreak); } else { // fallback to current bar time + 1 datetime t0 = iTime(symbol, timeframe, 0); end_key_time = (t0 != 0) ? (datetime)((long)t0 + (long)bar_secs) : TimeCurrent(); } } else { // if no break_time, extend to current + 1 bar datetime t0 = iTime(symbol, timeframe, 0); end_key_time = (t0 != 0) ? (datetime)((long)t0 + (long)bar_secs) : TimeCurrent(); } // Draw key ray from start_key -> breaker close (end_key) if(!ObjectCreate(0, nm_key_ray, OBJ_TREND, 0, start_key_time, start_key_price, end_key_time, end_key_price)) PrintFormat("DrawMss: Cannot create %s", nm_key_ray); ObjectSetInteger(0, nm_key_ray, OBJPROP_COLOR, col_key); ObjectSetInteger(0, nm_key_ray, OBJPROP_WIDTH, width_line); ObjectSetInteger(0, nm_key_ray, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, nm_key_ray, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_key_ray, OBJPROP_BACK, true); // key label (ở giữa đoạn) datetime mid_key = (datetime)(((long)start_key_time + (long)end_key_time)/2); if(!ObjectCreate(0, nm_key_txt, OBJ_TEXT, 0, mid_key, end_key_price)) PrintFormat("DrawMss: Cannot create %s", nm_key_txt); else { ObjectSetString(0, nm_key_txt, OBJPROP_TEXT, StringFormat("%sBreak: %s", tfPrefix, DoubleToString(info.key_level, _Digits))); ObjectSetInteger(0, nm_key_txt, OBJPROP_COLOR, col_key); ObjectSetInteger(0, nm_key_txt, OBJPROP_FONTSIZE, font_key); ObjectSetInteger(0, nm_key_txt, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm_key_txt, OBJPROP_BACK, true); } if(PrintToExperts) { PrintFormat("DrawMss: [%s] SWEEP start=%s price=%.5f -> sweepAt=%s price=%.5f | KEY start=%s price=%.5f -> breakerAt=%s price=%.5f", tfPrefix, TimeToString(start_sweep_time, TIME_DATE|TIME_MINUTES), start_sweep_price, TimeToString(end_sweep_time, TIME_DATE|TIME_MINUTES), end_sweep_price, TimeToString(start_key_time, TIME_DATE|TIME_MINUTES), start_key_price, TimeToString(end_key_time, TIME_DATE|TIME_MINUTES), end_key_price); } } // 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) // Bổ sung: phát hiện sớm (provisional) dựa trên các nến đóng gần nhất nếu swing chưa hoàn chỉnh void UpdateTrendForSlot(int slot, ENUM_TIMEFRAMES timeframe, string symbol) { // mặc định giữ trend hiện tại (không reset về 0 ngay lập tức để tránh flicker) int oldTrend = TrendTF[slot]; // Tolerances / thresholds 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; // Nếu không có đủ dữ liệu swing, ta vẫn sẽ thử provisional detection bool haveTwoSwings = (SwingHighCountTF[slot] >= 2 && SwingLowCountTF[slot] >= 2); // Lấy giá swing (index 0 = nearest, 1 = older). Có thể = 0 nếu chưa có. 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; // default giữ trend cũ nếu không xác định được // 1) Rule "cứng" nếu có đủ 2 swings (như trước) if(haveTwoSwings) { if(sh0 > sh1 && sl0 > sl1) newTrend = 1; // uptrend else if(sh0 < sh1 && sl0 < sl1) newTrend = -1; // downtrend else newTrend = 0; // không rõ } else { // nếu chưa có đủ swings, tạm giữ oldTrend (sẽ có chance bị override bởi provisional) newTrend = oldTrend; } // 2) Provisional / early detection: // Nếu swing "cứng" không xác định được (newTrend == 0) hoặc chúng ta thiếu swings, // thử kiểm tra xem last closed candle(s) đã đóng vượt sh0/sl0 chưa => áp provisional. // Lấy last closed candle close (index 1) double lastClosedClose = iClose(symbol, timeframe, 1); if(lastClosedClose == 0.0) lastClosedClose = iClose(symbol, timeframe, 0); // fallback 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) { // Nếu chúng ta có provisionalTrend, xác nhận bằng số nến đóng liên tiếp (ProvisionalConsecCloses) 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 // provisionalTrend == -1 { if(c < sl0 - provisionalThresh) countClosers++; else break; } } if(countClosers >= ProvisionalConsecCloses) { // Áp provisional nếu: (a) không có trend cứng (newTrend==0) hoặc (b) thiếu swings (haveTwoSwings==false) // hoặc bạn muốn provisional override cả khi newTrend khác (cẩn trọng) - ở đây ta chỉ override khi newTrend == 0 hoặc không đủ swings if(!haveTwoSwings || newTrend == 0) { newTrend = provisionalTrend; if(PrintToExperts) PrintFormat("UpdateTrendForSlot slot=%d tf=%s: provisionalTrend applied=%d (sh0=%.5f sl0=%.5f lastClose=%.5f thresh=%.5f count=%d)", slot, EnumToString(timeframe), newTrend, sh0, sl0, lastClosedClose, provisionalThresh, countClosers); } else { // Nếu bạn muốn provisional override ngay cả khi haveTwoSwings==true && newTrend != 0, // uncomment dòng dưới đây (cẩn thận: có thể gây false positives) // newTrend = provisionalTrend; if(PrintToExperts) PrintFormat("UpdateTrendForSlot slot=%d tf=%s: provisional candidate=%d but two-swing trend exists=%d -> not applied", slot, EnumToString(timeframe), provisionalTrend, newTrend); } } else { if(PrintToExperts) PrintFormat("UpdateTrendForSlot slot=%d tf=%s: provisional candidate=%d NOT confirmed, consecutive closes=%d (required %d)", slot, EnumToString(timeframe), provisionalTrend, countClosers, ProvisionalConsecCloses); } } // Ghi kết quả vào biến global TrendTF TrendTF[slot] = newTrend; if(PrintToExperts) PrintFormat("UpdateTrendForSlot slot=%d tf=%s -> TrendTF=%d (haveTwoSwings=%s sh0=%.5f sh1=%.5f sl0=%.5f sl1=%.5f lastClose=%.5f)", slot, EnumToString(timeframe), TrendTF[slot], (haveTwoSwings ? "true":"false"), sh0, sh1, sl0, sl1, lastClosedClose); } //+------------------------------------------------------------------+ //| FindFVG - tìm Fair Value Gaps trên timeframe cho symbol | //| symbol: chuỗi symbol (Symbol()) | //| timeframe: ENUM_TIMEFRAMES (ví dụ PERIOD_H1) | //| lookback: số nến đóng (ví dụ 200) | //| returns: số FVG tìm được (và ghi vào mảng global FVG_*) | //+------------------------------------------------------------------+ int FindFVG(string symbol, ENUM_TIMEFRAMES timeframe, int lookback) { // reset kết quả cũ ArrayFree(FVG_top); ArrayFree(FVG_bottom); ArrayFree(FVG_timeA); ArrayFree(FVG_timeC); ArrayFree(FVG_type); FVG_count = 0; // total closed bars available int total = iBars(symbol, timeframe); if(total < 5) return 0; // lấy số nến tối đa quét (chỉ nến đóng: index >=1) int maxScan = MathMin(lookback, total - 3); // cần ít nhất 3 bar cho một triple if(maxScan <= 0) return 0; // tolerance nhỏ theo pip để tránh noise double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; // quét: i = index của bar C (mới nhất trong triple), i từ 1..maxScan // triple: A = i+2 (older), B = i+1 (middle), C = i (newer) --- tất cả là nến đã đóng for(int i = 1; i <= maxScan; i++) { int idxA = i + 2; int idxB = i + 1; int idxC = i; if(idxA > total - 1) break; // không đủ bars // lấy giá High/Low (dùng râu để FVG) 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; // skip nếu dữ liệu thiếu // --- Bullish FVG: low(C) > high(A) --- if(lowC > highA + tol) { double top = lowC; double bottom = highA; // tránh trùng lặp: nếu đã có zone với top/bottom tương tự -> skip bool dup = false; for(int k = 0; k < FVG_count; k++) { if(MathAbs(FVG_top[k] - top) <= tol && MathAbs(FVG_bottom[k] - bottom) <= tol) { dup = true; break; } } if(!dup) { // append ArrayResize(FVG_top, FVG_count+1); ArrayResize(FVG_bottom, FVG_count+1); ArrayResize(FVG_timeA, FVG_count+1); ArrayResize(FVG_timeC, FVG_count+1); ArrayResize(FVG_type, FVG_count+1); FVG_top[FVG_count] = top; FVG_bottom[FVG_count] = bottom; FVG_timeA[FVG_count] = iTime(symbol, timeframe, idxA); FVG_timeC[FVG_count] = iTime(symbol, timeframe, idxC); FVG_type[FVG_count] = 1; FVG_count++; } continue; // không cần kiểm tra bearish nếu đã bullish cho triple này } // --- Bearish FVG: high(C) < low(A) --- if(highC < lowA - tol) { double top = lowA; // top > bottom double bottom = highC; bool dup = false; for(int k = 0; k < FVG_count; k++) { if(MathAbs(FVG_top[k] - top) <= tol && MathAbs(FVG_bottom[k] - bottom) <= tol) { dup = true; break; } } if(!dup) { ArrayResize(FVG_top, FVG_count+1); ArrayResize(FVG_bottom, FVG_count+1); ArrayResize(FVG_timeA, FVG_count+1); ArrayResize(FVG_timeC, FVG_count+1); ArrayResize(FVG_type, FVG_count+1); FVG_top[FVG_count] = top; FVG_bottom[FVG_count] = bottom; FVG_timeA[FVG_count] = iTime(symbol, timeframe, idxA); FVG_timeC[FVG_count] = iTime(symbol, timeframe, idxC); FVG_type[FVG_count] = -1; FVG_count++; } } } return FVG_count; } //+------------------------------------------------------------------+ //| Make ARGB color with alpha (0..255) | //+------------------------------------------------------------------+ uint MakeARGB(int a, uint clr) { if(a < 0) a = 0; if(a > 255) a = 255; return ((uint)a << 24) | (clr & 0x00FFFFFF); } //+------------------------------------------------------------------+ //| DrawFVG: vẽ tất cả FVG đã tìm được cho symbol & timeframe | //| symbol: chuỗi symbol (ví dụ Symbol()) | //| timeframe: ENUM_TIMEFRAMES (ví dụ MiddleTF) | //| startFromCBar: nếu true thì vẽ start time = FVG_timeC (mặc định) | //+------------------------------------------------------------------+ void DrawFVG(string symbol, ENUM_TIMEFRAMES timeframe, bool startFromCBar = true) { if(!ShowSwingMarkers) return; // prefix object để dễ xóa string prefix = SwingObjPrefix + "FVG_"; // Xóa các FVG cũ (theo prefix) 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); } // Nếu không có FVG nào thì return if(FVG_count <= 0) return; // Thời gian bar (giây) — dùng kiểu datetime để không gây cảnh báo cast datetime bar_secs = (datetime)PeriodSeconds(timeframe); // Tolerance cho việc detect touch double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tol = (point > 0.0) ? point * 0.5 : 0.0; // Mỗi FVG: xác định thời điểm kết thúc (time2) khi price chạm zone, hoặc thời điểm hiện tại for(int k = 0; k < FVG_count; k++) { double top = FVG_top[k]; double bottom = FVG_bottom[k]; datetime timeC = FVG_timeC[k]; // bar C (mới hơn) - dùng làm start datetime timeA = FVG_timeA[k]; // start time: chọn timeC (bar C) hoặc timeA tùy flag datetime start_time = startFromCBar ? timeC : timeA; // tìm bar sau timeC (index nhỏ hơn) mà chạm zone (quét forward) // xác định index của bar C int idxC = iBarShift(symbol, timeframe, timeC, false); if(idxC == -1) idxC = 0; // fallback bool touched = false; datetime end_time = TimeCurrent(); // fallback -> hiện tại // quét các bar mới hơn (idx = idxC-1, idxC-2, ..., 0) 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; // Nếu bar này *chạm* zone: nghĩa là phạm vi bar overlap zone if(lowM <= top + tol && highM >= bottom - tol) { touched = true; end_time = iTime(symbol, timeframe, idx); break; } } // Nếu không tìm thấy bar chạm, set end_time = thời điểm đóng bar hiện tại của timeframe if(!touched) { // lấy time của bar index 0 (nến đóng gần nhất) datetime t0 = iTime(symbol, timeframe, 0); if(t0 != 0) end_time = t0 + bar_secs; // kéo tới đầu nến hiện tại + 1 bar để dễ nhìn else end_time = TimeCurrent(); } // chuẩn hoá: nếu end_time <= start_time thì kéo end_time = start_time + 1 bar if(end_time <= start_time) end_time = (datetime)( (long)start_time + (long)bar_secs ); // Tạo object rectangle name string obj = prefix + IntegerToString(k); // MQL5 rectangle: ObjectCreate(..., OBJ_RECTANGLE, 0, time1, price1, time2, price2) // Tôi đặt (time1= start_time, price1= top) và (time2 = end_time, price2 = bottom) // (hàm sẽ tự vẽ vùng giữa top & bottom và thời gian start->end) if(!ObjectCreate(0, obj, OBJ_RECTANGLE, 0, start_time, top, end_time, bottom)) { PrintFormat("DrawFVG: Không thể tạo rectangle %s", obj); continue; } // Set properties: không selectable, show in background ObjectSetInteger(0, obj, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, obj, OBJPROP_HIDDEN, false); ObjectSetInteger(0, obj, OBJPROP_BACK, true); // Màu: bullish = xanh mờ, bearish = đỏ mờ uint fill_alpha = 80; // 0..255 (0 trong suốt, 255 đặc) uint border_alpha = 0; // border trong suốt uint color_fill; if(FVG_type[k] == 1) // bullish { color_fill = MakeARGB((int)fill_alpha, clrDodgerBlue); } else // bearish { color_fill = MakeARGB((int)fill_alpha, clrCrimson); } uint col_border = MakeARGB((int)border_alpha, clrBlack); // Gán màu (một số terminal dùng OBJPROP_COLOR cho fill khi màu có alpha) ObjectSetInteger(0, obj, OBJPROP_COLOR, (int)col_border); // Một số terminal/phiên bản chấp nhận OBJPROP_FILL - nếu không có, property sẽ bị bỏ qua // Cố gắng set cả những thuộc tính khả dĩ: ObjectSetInteger(0, obj, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1); // OBJPROP_COLOR đã set cho border; set màu fill bằng property OBJPROP_BACKGROUND_COLOR nếu có // Một số build MQL5 hỗ trợ OBJPROP_BACKCOLOR (không chuẩn), ta thử set tên sau (nếu không hợp lệ compiler sẽ báo) // Dùng ObjectSetInteger/Set to attempt; nếu compiler báo lỗi bạn có thể xóa những dòng thử nghiệm này. #ifdef __MQL5__ // try to set fill via OBJPROP_COLOR with alpha (already set), also attempt OBJPROP_COLOR by ObjectSetInteger above #endif // Set fill by using ObjectSetInteger for OBJPROP_COLOR (again) - terminals usually honor alpha channel ObjectSetInteger(0, obj, OBJPROP_COLOR, (int)color_fill); // Nếu terminal hỗ trợ OBJPROP_FILL (some do), bật nó: // Note: if your compiler complains about OBJPROP_FILL, remove the following line. // ObjectSetInteger(0, obj, OBJPROP_FILL, 1); // Done for this zone } // end for FVG } // ------------------------------------------------------------ // UpdateLabel: vẽ label cho 1 timeframe bất kỳ // labelName : tên object label muốn hiển thị // timeframe : timeframe cần hiển thị (HighTF / MiddleTF / LowTF ...) // trend : giá trị trend 1 / 0 / -1 // ------------------------------------------------------------ // ------------------------------------------------------------ // UpdateLabel: vẽ label cho 1 timeframe bất kỳ (không còn hardcode) // labelName : tên object label muốn hiển thị (ví dụ "LBL_HTF", "LBL_MTF") // timeframe : timeframe cần hiển thị (HighTF / MiddleTF / ...) // trend : giá trị trend 1 / 0 / -1 // ------------------------------------------------------------ void UpdateLabel(string labelName, ENUM_TIMEFRAMES timeframe, int trend) { if(!ShowSwingMarkers) return; string txt = StringFormat("%s: %s", EnumToString(timeframe), TrendToString(trend)); // vị trí chung (corner + distance mặc định) int corner = CORNER_LEFT_UPPER; int xdist = 10; int ydist_default = 20; int ydist = ydist_default; // nếu timeframe giống MiddleTF (khung nhỏ hơn) -> dịch xuống thêm 1 hàng // bạn có thể tinh chỉnh offset_down tuỳ ý (ví dụ 20 px) int offset_down = 30; if (timeframe == MiddleTF) ydist = ydist_default + offset_down; else if(timeframe == LowTF) ydist = ydist_default + offset_down * 2; // Nếu object đã tồn tại -> cập nhật text và giữ vị trí if(ObjectFind(0, labelName) >= 0) { ObjectSetString(0, labelName, OBJPROP_TEXT, txt); // cập nhật màu (giữ vị trí) 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 { // Tạo label mới với vị trí xác định if(!ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0)) { Print("UpdateLabel: Không thể tạo object label: ", labelName); 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); // Set màu theo trend 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); // time of last closed bar if(t == -1 || t == 0) return false; if(t != lastClosedBarTime[slot]) { lastClosedBarTime[slot] = t; return true; } return false; } // Tính FVG cho MiddleTF chỉ khi bar C (hoặc bar 1) thay đổi void EnsureFVGUpToDate(string symbol, ENUM_TIMEFRAMES tf, int lookback) { if(tf != MiddleTF) return; // chỉ cache cho MiddleTF (theo EA bạn) datetime currentClosed = iTime(symbol, tf, 1); if(currentClosed == -1 || currentClosed == 0) return; if(currentClosed != lastFVGBarTime) { // bar vừa thay -> recompute FVG FindFVG(symbol, tf, lookback); lastFVGBarTime = currentClosed; if(PrintToExperts) PrintFormat("EnsureFVGUpToDate: recomputed FVG at %s", TimeToString(currentClosed, TIME_DATE|TIME_MINUTES)); } } // requireFVG : có cần FVG (true/false) truyền vào DetectMSSOnSlot // minBreakPips.. : các tham số truyền xuống DetectMSSOnSlot (mặc định hợp lý) void ProcessMSSForSlot(string sym, ENUM_TIMEFRAMES tf, int slot, bool enabled, bool requireFVG = false, double minBreakPips = 10.0, int minConsec = 2, int lookbackBarsForSweep = 50, int fvgLookback = 200) { if(!enabled) return; // Gọi DetectMSSOnSlot với tham số truyền vào MSSInfo info = DetectMSSOnSlot(sym, tf, slot, minBreakPips, minConsec, lookbackBarsForSweep, fvgLookback, requireFVG); if(info.found) { // Vẽ MSS & sweep DrawMss(sym, tf, info, slot); // TODO: thêm logic order/alert nếu bạn muốn // Ví dụ: SendNotification / Alert / PlaceOrder... } } void HandleLogicForTimeframe(string sym, ENUM_TIMEFRAMES tf, int slot, bool detectMSS, int swingRange, bool requireFVG, double minBreakPips, int minConsec, int lookbackBarsForSweep, int fvgLookback) { UpdateSwings(sym, tf, slot, swingRange); UpdateTrendForSlot(slot, tf, sym); if(tf == MiddleTF) EnsureFVGUpToDate(sym, MiddleTF, fvgLookback); ProcessMSSForSlot(sym, tf, slot, detectMSS, requireFVG, minBreakPips, minConsec, lookbackBarsForSweep, fvgLookback); if(ShowSwingMarkers) { DrawSwingsOnChart(tf); if(tf == MiddleTF) DrawFVG(sym, MiddleTF, true); } UpdateLabel((tf == HighTF) ? "LBL_HTF" : (tf == MiddleTF) ? "LBL_MTF" : "LBL_LTF", tf, TrendTF[slot]); } void OnTick() { string sym = Symbol(); if(IsNewClosedBar(sym, HighTF, 0)) { HandleLogicForTimeframe(sym, HighTF, 0, DetectMSS_HTF, htfSwingRange, false, 10.0, 2, 50, FVGLookback); } if(IsNewClosedBar(sym, MiddleTF, 1)) { HandleLogicForTimeframe(sym, MiddleTF, 1, DetectMSS_MTF, mtfSwingRange, true, 10.0, 2, 50, FVGLookback); } if(IsNewClosedBar(sym, LowTF, 2)) { HandleLogicForTimeframe(sym, LowTF, 2, DetectMSS_LTF, ltfSwingRange, false, 10.0, 2, 50, FVGLookback); } } int OnInit() { string sym = Symbol(); HandleLogicForTimeframe(sym, HighTF, 0, false, htfSwingRange, false, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, MiddleTF, 1, false, mtfSwingRange, true, 10.0, 2, 50, FVGLookback); HandleLogicForTimeframe(sym, LowTF, 2, false, ltfSwingRange, false, 10.0, 2, 50, FVGLookback); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { // Xoá 3 label if(ObjectFind(0, "LBL_HTF") >= 0) ObjectDelete(0, "LBL_HTF"); if(ObjectFind(0, "LBL_MTF") >= 0) ObjectDelete(0, "LBL_MTF"); if(ObjectFind(0, "LBL_LTF") >= 0) ObjectDelete(0, "LBL_LTF"); // Xóa tất cả object dùng tiền tố SwingObjPrefix (bao gồm htf_/mtf_/ltf_ và FVG) 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); } }