//+------------------------------------------------------------------+ //| FVG Grid.mq5 | //| Copyright 2026, Shahrukh & AI | //| https://mql5.io | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Shahrukh & AI" #property link "https://mql5.io" #property version "5.20" #property description "FVG Grid: Уникальные короткие комментарии + Изолированный SL и Трейлинг" #include CTrade trade; input group "--- Риск и ММ ---" input double InpLotSize = 0.01; // Размер лота для каждого ордера input int InpStopLoss = 150; // Стоп-Лосс в пунктах от границы гэпа (0 - отключен) input int InpTakeProfit = 300; // Тейк-Профит (в пунктах, 0 - отключен) input ulong InpMagicNumber = 123456; // Мэджик номер советника input int InpMaxSpreadPoints = 35; // Максимальный спред в пунктах для входа (0 - отключен) input group "--- Детектор FVG ---" input int InpMaximumBars = 400; // Сколько бар сканировать для отрисовки input double InpMinimumGapPoints = 20.0; // Минимальный размер гэпа в пунктах input bool InpHideMitigated = true; // Скрывать протестированные гэпы input group "--- Настройки Сетки Ордеров ---" input double InpOrderOffsetPoints = 10.0; // Смещение уровней ордеров в пунктах input group "--- Визуализация (Пунктир) ---" input int InpExtensionBars = 80; // Длина прямоугольника в барах input color InpBullishGapColor = clrGreen; // Яркий цвет контура для Buy input color InpBearishGapColor = clrRed; // Яркий цвет контура для Sell input color InpMitigatedColor = clrSilver; // Цвет для смягченных зон input bool InpShowLabels = true; // Показывать текст input group "--- Индивидуальный Трейлинг-Стоп ---" input bool InpUseTrailing = true; // Включить трейлинг? input int InpTrailingStart = 150; // Активация трейлинга (в пунктах от безубытка гэпа) input int InpTrailingDist = 50; // Расстояние от текущей цены (в пунктах) input int InpTrailingStep = 10; // Шаг изменения стоп-лосса (в пунктах) // Глобальные переменные string g_prefix = "FVGSCAN_"; datetime g_last_bar = 0; bool g_first_run = true; string g_file_name; // База данных отторгованных FVG в ОЗУ string g_traded_gaps[]; // Предварительное объявление функций void BuildGapsAndTrade(const int rates_copied, const datetime &time[], const double &high[], const double &low[]); void CheckAndPlaceFvgGrid(string gap_id, double top, double bottom, bool is_bullish); bool IsGridAlreadyPlaced(string gap_id); bool WasGapTraded(string gap_id); void MarkGapAsTraded(string gap_id); void ApplyIsolatedTrailing(); void ModifyIsolatedGridSL(string target_comment, long position_type, double new_sl); void CancelLimitOrdersOnPositionClose(); bool IsBullishMitigated(const int formation_index, const double gap_bottom, const double gap_top, const double &low[]); bool IsBearishMitigated(const int formation_index, const double gap_bottom, const double gap_top, const double &high[]); void DrawGap(const string id, const datetime start_time, const datetime end_time, const double top, const double bottom, const bool bullish, const bool mitigated); void LoadGapsFromFile(); void SaveGapToFile(string gap_id); string GetShortUniqueId(datetime bar_time); //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(InpMaximumBars < 20 || InpMinimumGapPoints < 0.0 || InpExtensionBars < 1) { Print("Invalid scanner settings."); return(INIT_PARAMETERS_INCORRECT); } trade.SetExpertMagicNumber(InpMagicNumber); trade.SetMarginMode(); g_last_bar = 0; g_first_run = true; ArrayResize(g_traded_gaps, 0); g_file_name = "FVG_Grid_Storage_" + _Symbol + "_" + EnumToString(_Period) + ".txt"; LoadGapsFromFile(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(0, g_prefix); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { datetime current_bar_time = iTime(_Symbol, _Period, 0); if(current_bar_time == 0) return; double high[], low[]; datetime time[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(time, true); int copied_time = CopyTime(_Symbol, _Period, 0, InpMaximumBars, time); int copied_high = CopyHigh(_Symbol, _Period, 0, InpMaximumBars, high); int copied_low = CopyLow(_Symbol, _Period, 0, InpMaximumBars, low); if(copied_time < 10 || copied_high < 10 || copied_low < 10) return; int rates_total = MathMin(copied_time, MathMin(copied_high, copied_low)); // 1. Сканирование и выставление новых сеток на открытии бара if(current_bar_time != g_last_bar || g_first_run) { g_last_bar = current_bar_time; BuildGapsAndTrade(rates_total, time, high, low); } // 2. Контроль закрытия рыночных ордеров и очистка оставшихся лимиток текущего гэпа CancelLimitOrdersOnPositionClose(); // 3. Индивидуальный трейлинг-стоп отдельно для каждого гэпа if(InpUseTrailing) { ApplyIsolatedTrailing(); } } //+------------------------------------------------------------------+ //| ОБРАБОТКА ИЗМЕНЕНИЙ ГРАФИКА | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CLICK || id == CHARTEVENT_CHART_CHANGE) { double high[], low[]; datetime time[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(time, true); int copied_time = CopyTime(_Symbol, _Period, 0, InpMaximumBars, time); int copied_high = CopyHigh(_Symbol, _Period, 0, InpMaximumBars, high); int copied_low = CopyLow(_Symbol, _Period, 0, InpMaximumBars, low); if(copied_time >= 10 && copied_high >= 10 && copied_low >= 10) { int rates_total = MathMin(copied_time, MathMin(copied_high, copied_low)); BuildGapsAndTrade(rates_total, time, high, low); } } } //+------------------------------------------------------------------+ //| Генерация короткого ID для соблюдения лимита в 31 символ коммента| //+------------------------------------------------------------------+ string GetShortUniqueId(datetime bar_time) { // Используем последние 6 цифр таймштампа (этого достаточно для уникальности в рамках файла) ulong long_id = (ulong)bar_time; return IntegerToString((int)(long_id % 1000000)); } //+------------------------------------------------------------------+ //| Сканирование истории, построение объектов и выставление ордеров | //+------------------------------------------------------------------+ void BuildGapsAndTrade(const int rates_copied, const datetime &time[], const double &high[], const double &low[]) { ObjectsDeleteAll(0, g_prefix); const int seconds = PeriodSeconds(_Period); const int bar_seconds = (seconds > 0 ? seconds : 60); const int oldest = MathMin(InpMaximumBars, rates_copied - 3); if(oldest < 1) return; for(int index = oldest; index >= 1; index--) { const double bullish_size = low[index] - high[index + 2]; const double bearish_size = low[index + 2] - high[index]; const datetime end_time = time[index] + (datetime)(bar_seconds * InpExtensionBars); // Генерируем компактный ID, который гарантированно не обрежется в комментарии ордера const string short_id = GetShortUniqueId(time[index]); // 1. Проверяем Бычьи гэпы (Buy зоны) if(bullish_size >= InpMinimumGapPoints * _Point) { const double top = low[index]; const double bottom = high[index + 2]; const bool mitigated = IsBullishMitigated(index, bottom, top, low); if(!mitigated || !InpHideMitigated) DrawGap(short_id + "_B", time[index + 2], end_time, top, bottom, true, mitigated); if(!mitigated) CheckAndPlaceFvgGrid(short_id + "_B", top, bottom, true); } // 2. Проверяем Медвежьи гэпы (Sell зоны) if(bearish_size >= InpMinimumGapPoints * _Point) { const double top = low[index + 2]; const double bottom = high[index]; const bool mitigated = IsBearishMitigated(index, bottom, top, high); if(!mitigated || !InpHideMitigated) DrawGap(short_id + "_S", time[index + 2], end_time, top, bottom, false, mitigated); if(!mitigated) CheckAndPlaceFvgGrid(short_id + "_S", top, bottom, false); } } if(g_first_run) g_first_run = false; ChartRedraw(0); } //+------------------------------------------------------------------+ //| Функция выставления 3-х ордеров со СВОИМ индивидуальным Стоп-Лоссом| //+------------------------------------------------------------------+ void CheckAndPlaceFvgGrid(string gap_id, double top, double bottom, bool is_bullish) { if(InpMaxSpreadPoints > 0) { int current_spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); if(current_spread > InpMaxSpreadPoints) return; } if(IsGridAlreadyPlaced(gap_id) || WasGapTraded(gap_id)) return; double center = NormalizeDouble((top + bottom) * 0.5, _Digits); double offset = InpOrderOffsetPoints * _Point; double grid_levels[3]; if(is_bullish) { grid_levels[0] = NormalizeDouble(top + offset, _Digits); grid_levels[1] = NormalizeDouble(center + offset, _Digits); grid_levels[2] = NormalizeDouble(bottom + offset, _Digits); } else { grid_levels[0] = NormalizeDouble(top - offset, _Digits); grid_levels[1] = NormalizeDouble(center - offset, _Digits); grid_levels[2] = NormalizeDouble(bottom - offset, _Digits); } double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Комментарий теперь короткий и уникальный (Пример: FVG_123456_B) string comment = StringFormat("FVG_%s", gap_id); bool grid_placed = false; // Вычисляем Стоп-Лосс для каждого ордера от дальней границы гэпа double common_sl = 0; if(InpStopLoss > 0) { if(is_bullish) common_sl = NormalizeDouble(bottom - InpStopLoss * _Point, _Digits); else common_sl = NormalizeDouble(top + InpStopLoss * _Point, _Digits); } for(int i = 0; i < 3; i++) { if(is_bullish) { double tp = (InpTakeProfit > 0) ? NormalizeDouble(grid_levels[i] + InpTakeProfit * _Point, _Digits) : 0; if(ask <= grid_levels[i]) { if(trade.Buy(InpLotSize, _Symbol, ask, common_sl, tp, comment)) grid_placed = true; } else { if(trade.BuyLimit(InpLotSize, grid_levels[i], _Symbol, common_sl, tp, ORDER_TIME_GTC, 0, comment)) grid_placed = true; } } else // BEARISH FVG (SELL) { double tp = (InpTakeProfit > 0) ? NormalizeDouble(grid_levels[i] - InpTakeProfit * _Point, _Digits) : 0; if(bid >= grid_levels[i]) { if(trade.Sell(InpLotSize, _Symbol, bid, common_sl, tp, comment)) grid_placed = true; } else { if(trade.SellLimit(InpLotSize, grid_levels[i], _Symbol, common_sl, tp, ORDER_TIME_GTC, 0, comment)) grid_placed = true; } } } if(grid_placed) { MarkGapAsTraded(gap_id); } } //+------------------------------------------------------------------+ //| Функция отмены лимиток ТОЛЬКО при закрытии позиций этого гэпа | //+------------------------------------------------------------------+ void CancelLimitOrdersOnPositionClose() { int total_gaps = ArraySize(g_traded_gaps); if(total_gaps == 0) return; if(!HistorySelect(0, TimeCurrent())) return; int total_deals = HistoryDealsTotal(); for(int g = 0; g < total_gaps; g++) { string gap_id = g_traded_gaps[g]; string target_comment = "FVG_" + gap_id; bool has_open_positions = false; bool has_limit_orders = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { // Безопасное чтение свойств позиции в MQL5 по индексу if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { if(PositionGetString(POSITION_COMMENT) == target_comment) { has_open_positions = true; break; } } } for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket > 0 && OrderGetInteger(ORDER_MAGIC) == InpMagicNumber) { if(OrderGetString(ORDER_COMMENT) == target_comment) { long type = OrderGetInteger(ORDER_TYPE); if(type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT) { has_limit_orders = true; break; } } } } if(has_limit_orders && !has_open_positions) { bool was_executed_and_closed = false; for(int d = total_deals - 1; d >= 0; d--) { ulong deal_ticket = HistoryDealGetTicket(d); if(deal_ticket > 0 && HistoryDealGetInteger(deal_ticket, DEAL_MAGIC) == InpMagicNumber) { if(HistoryDealGetString(deal_ticket, DEAL_COMMENT) == target_comment) { was_executed_and_closed = true; break; } } } if(was_executed_and_closed) { for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket > 0 && OrderGetInteger(ORDER_MAGIC) == InpMagicNumber) { if(OrderGetString(ORDER_COMMENT) == target_comment) { long type = OrderGetInteger(ORDER_TYPE); if(type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT) { trade.OrderDelete(ticket); Print("Лимитный ордер удален. Причина: Исполненные позиции данного FVG закрылись."); } } } } } } } } //+------------------------------------------------------------------+ //| Индивидуальный трейлинг-стоп отдельно для каждого гэпа | //+------------------------------------------------------------------+ void ApplyIsolatedTrailing() { int total_gaps = ArraySize(g_traded_gaps); if(total_gaps == 0) return; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); for(int g = 0; g < total_gaps; g++) { string gap_id = g_traded_gaps[g]; string target_comment = "FVG_" + gap_id; double total_vol_buy = 0, total_vol_sell = 0; double weighted_price_buy = 0, weighted_price_sell = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { if(PositionGetString(POSITION_COMMENT) == target_comment) { double vol = PositionGetDouble(POSITION_VOLUME); double open = PositionGetDouble(POSITION_PRICE_OPEN); long type = PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_BUY) { weighted_price_buy += open * vol; total_vol_buy += vol; } if(type == POSITION_TYPE_SELL) { weighted_price_sell += open * vol; total_vol_sell += vol; } } } } if(total_vol_buy > 0) { double breakeven_buy = weighted_price_buy / total_vol_buy; if(bid - breakeven_buy >= InpTrailingStart * _Point) { double new_sl = NormalizeDouble(bid - InpTrailingDist * _Point, _Digits); if(new_sl > breakeven_buy) ModifyIsolatedGridSL(target_comment, POSITION_TYPE_BUY, new_sl); } } if(total_vol_sell > 0) { double breakeven_sell = weighted_price_sell / total_vol_sell; if(breakeven_sell - ask >= InpTrailingStart * _Point) { double new_sl = NormalizeDouble(ask + InpTrailingDist * _Point, _Digits); if(new_sl < breakeven_sell) ModifyIsolatedGridSL(target_comment, POSITION_TYPE_SELL, new_sl); } } } } //+------------------------------------------------------------------+ //| Модификация Стоп-Лосса внутри изолированной сетки конкретного FVG| //+------------------------------------------------------------------+ void ModifyIsolatedGridSL(string target_comment, long position_type, double new_sl) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket > 0 && PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { if(PositionGetString(POSITION_COMMENT) == target_comment && PositionGetInteger(POSITION_TYPE) == position_type) { double current_sl = PositionGetDouble(POSITION_SL); if(position_type == POSITION_TYPE_BUY) { if(current_sl == 0 || (new_sl - current_sl >= InpTrailingStep * _Point)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); } else if(position_type == POSITION_TYPE_SELL) { if(current_sl == 0 || (current_sl - new_sl >= InpTrailingStep * _Point)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); } } } } } //+------------------------------------------------------------------+ //| Исторический учет отторгованных зон | //+------------------------------------------------------------------+ bool WasGapTraded(string gap_id) { int size = ArraySize(g_traded_gaps); for(int i = 0; i < size; i++) if(g_traded_gaps[i] == gap_id) return(true); return(false); } void MarkGapAsTraded(string gap_id) { int size = ArraySize(g_traded_gaps); for(int i = 0; i < size; i++) if(g_traded_gaps[i] == gap_id) return; ArrayResize(g_traded_gaps, size + 1); g_traded_gaps[size] = gap_id; SaveGapToFile(gap_id); } //+------------------------------------------------------------------+ //| РАБОТА С ФАЙЛАМИ (Запись/Чтение с диска) | //+------------------------------------------------------------------+ void SaveGapToFile(string gap_id) { int file_handle = FileOpen(g_file_name, FILE_WRITE|FILE_READ|FILE_TXT|FILE_ANSI); if(file_handle != INVALID_HANDLE) { FileSeek(file_handle, 0, SEEK_END); FileWriteString(file_handle, gap_id + "\r\n"); FileClose(file_handle); } } void LoadGapsFromFile() { if(!FileIsExist(g_file_name)) return; int file_handle = FileOpen(g_file_name, FILE_READ|FILE_TXT|FILE_ANSI); if(file_handle != INVALID_HANDLE) { while(!FileIsEnding(file_handle)) { // Исправлено: Сдвиг указателя (параметр 0) предотвращает бесконечный цикл string line = FileReadString(file_handle, 0); StringTrimLeft(line); StringTrimRight(line); if(line != "") { int size = ArraySize(g_traded_gaps); ArrayResize(g_traded_gaps, size + 1); g_traded_gaps[size] = line; } } FileClose(file_handle); Print("Успешно загружено ", ArraySize(g_traded_gaps), " ID зон FVG из дискового файла хранилища."); } } //+------------------------------------------------------------------+ //| Проверка: были ли уже выставлены ордера по ID данного гэпа | //+------------------------------------------------------------------+ bool IsGridAlreadyPlaced(string gap_id) { string target_comment = "FVG_" + gap_id; for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket > 0 && OrderGetInteger(ORDER_MAGIC) == InpMagicNumber && OrderGetString(ORDER_COMMENT) == target_comment) return(true); } for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket > 0 && PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_COMMENT) == target_comment) return(true); } return(false); } //+------------------------------------------------------------------+ //| Проверка на смягчение бычьего гэпа | //+------------------------------------------------------------------+ bool IsBullishMitigated(const int formation_index, const double gap_bottom, const double gap_top, const double &low[]) { for(int index = formation_index - 1; index >= 0; index--) if(low[index] <= gap_bottom) return(true); return(false); } //+------------------------------------------------------------------+ //| Проверка на смягчение медвежьего гэпа | //+------------------------------------------------------------------+ bool IsBearishMitigated(const int formation_index, const double gap_bottom, const double gap_top, const double &high[]) { for(int index = formation_index - 1; index >= 0; index--) if(high[index] >= gap_top) return(true); return(false); } //+------------------------------------------------------------------+ //| Отрисовка пунктирных прямоугольников без заливки | //+------------------------------------------------------------------+ void DrawGap(const string id, const datetime start_time, const datetime end_time, const double top, const double bottom, const bool bullish, const bool mitigated) { const string rectangle_name = g_prefix + "BOX_" + id; color base_color = (mitigated ? InpMitigatedColor : (bullish ? InpBullishGapColor : InpBearishGapColor)); if(!ObjectCreate(0, rectangle_name, OBJ_RECTANGLE, 0, start_time, top, end_time, bottom)) return; ObjectSetInteger(0, rectangle_name, OBJPROP_COLOR, base_color); ObjectSetInteger(0, rectangle_name, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, rectangle_name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, rectangle_name, OBJPROP_FILL, false); ObjectSetInteger(0, rectangle_name, OBJPROP_BACK, true); ObjectSetInteger(0, rectangle_name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, rectangle_name, OBJPROP_HIDDEN, true); if(!InpShowLabels) return; const string label_name = g_prefix + "TEXT_" + id; const double label_price = (top + bottom) * 0.5; if(ObjectCreate(0, label_name, OBJ_TEXT, 0, start_time, label_price)) { string label = (bullish ? "Bullish FVG" : "Bearish FVG"); if(mitigated) label += " filled"; ObjectSetString(0, label_name, OBJPROP_TEXT, label); ObjectSetString(0, label_name, OBJPROP_FONT, "Arial"); ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, label_name, OBJPROP_COLOR, mitigated ? clrGray : (bullish ? clrForestGreen : clrFireBrick)); ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, ANCHOR_LEFT); ObjectSetInteger(0, label_name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, label_name, OBJPROP_HIDDEN, true); } }