← К каталогу
Исходный код · ID 4628

Lizard MT5

lizard mt5 2.1.mq5
Вы автор или правообладатель?

Сообщите нам, если считаете, что материал размещён без разрешения.

Краткий обзор

О программе

Lizard MT5 2.1 — торговый советник для MetaTrader 5, предназначенный для работы на инструментах золота (XAUUSD/GOLD). Советник обрабатывает тиковые события, события сделок и таймерные вызовы, использует торговые функции MetaTrader 5 для выставления отложенных ордеров, изменения защитных уровней и закрытия позиций.

Логика построена на нескольких независимых стратегических модулях (зон A1–A3 и B1–B3), каждый из которых имеет собственный магический номер и настройки сопровождения. Предусмотрены фильтры по времени, торговым сессиям, волатильности и новостям, а также ограничение дневной просадки и управление объёмом позиции (фиксированный лот, процент риска или сумма на 0.01 лота).

Советник содержит информационную панель с отображением риска, защитных уровней, результатов по зонам, новостного статуса и кривой прибыли. В тестере доступен пользовательский критерий оценки. Поддерживается виртуальный стоп-лосс и несколько режимов трейлинга/безубытка.

Настройки программы

НастройкаОписание
Notice Информационное поле с предупреждением о рисках торговли.
SimpleForexTeam__ Информационное поле со ссылкой на канал и бесплатные инструменты.
InpNewsSetup1 Информационное поле с указанием включить WebRequest в настройках терминала для работы новостного фильтра.
InpWarn1 Информационное предупреждение о нежелательности использования MQL VPS.
InpWarn2 Информационное предупреждение о необходимости быстрого и стабильного VPS.
InpInfoBacktest Информационное поле с напоминанием выполнить тестирование перед использованием.
InpTimeFilter Выбор варианта фильтра по часам торговли: старый или новый.
InpFakeout Выбор режима фильтра ложных пробоев: выключен, мягкий, обычный или агрессивный.
InpFakeoutMinutes Таймер в минутах для обычного режима фильтра ложных пробоев, после которого убыточная позиция может быть закрыта.
Zone1_ID Базовый магический номер, от которого отсчитываются магические номера стратегических модулей.
InpTrailMode Выбор режима трейлинга и безубытка: динамический высокий, динамический низкий или фиксированный.
InpFixedBE Уровень активации безубытка в пунктах для фиксированного режима трейлинга.
InpFixedTrail Дистанция трейлинга в пунктах для фиксированного режима.
InpSLInfo Справочное информационное поле с примерами значений стоп-лосса.
InpStopLossPts Базовый размер стоп-лосса в пунктах.
InpVSL Режим виртуального стоп-лосса: срабатывание на уровне заморозки, всегда активен или отключён.
InpCloseWindow Количество минут до закрытия рынка, в течение которых советник удаляет отложенные ордера и может закрывать позиции.
InpOpenDelay Задержка в минутах после открытия рынка, в течение которой новые входы не выполняются.
InpCloseWeekend Включает закрытие позиций советника перед выходными при попадании в окно закрытия рынка.
InpNewsSetup3 Информационное поле с напоминанием, что новостному фильтру требуется разрешение WebRequest.
InpNFP_Enable Включает дополнительную защиту вокруг времени публикации NFP.
InpNFPWeek Режим недели NFP: выключен, блокировка торговли с понедельника по пятницу или уменьшение лота вдвое.
InpNews_High Включает блокировку торговли вокруг новостей высокой важности.
InpNews_Holidays Включает запрет торговли в дни праздников США.
Inp_GMT_Winter Зимнее смещение GMT брокера, используемое в тестере и при ручной настройке.
Inp_GMT_Summer Летнее смещение GMT брокера, используемое в тестере и при ручной настройке.
InpGMT_AutoDetect Включает автоматическое определение смещения GMT брокера в реальной торговле; при выключении используются заданные зимнее и летнее смещения.
InpNFP_CloseOpen Включает закрытие открытых позиций советника при активации новостного фильтра.
InpNFP_ClosePending Включает удаление отложенных ордеров советника при активации новостного фильтра.
InpNFP_Before Количество минут до новости, в течение которых торговля блокируется.
InpNFP_After Количество минут после новости, в течение которых торговля блокируется.
InpRiskInfo Информационное поле, поясняющее, что настройки лота не ограничивают убыток, для этого служит дневная просадка.
InpLotMode Выбор способа расчёта объёма: фиксированный лот, процент риска или сумма на 0.01 лота.
InpFixLot Фиксированный размер лота, используемый в режиме фиксированного объёма.
InpRiskPct Процент риска на сделку, используемый в режиме расчёта по риску.
InpDollarPer001 Сумма в долларах, соответствующая объёму 0.01 лота, для режима расчёта объёма по деньгам.
InpUseEquity Определяет, использовать эквити вместо баланса при расчёте объёма.
InpCheckMargin Включает проверку достаточности маржи перед выставлением новых ордеров.
InpMaxDailyDD Порог максимальной дневной просадки; при нуле ограничение отключено.
InpMaxDDUnit Единица измерения максимальной дневной просадки: процент от пиковой эквити или денежная сумма.
InpZoneA1 Включает стратегический модуль A1.
InpZoneA2 Включает стратегический модуль A2.
InpZoneA3 Включает стратегический модуль A3.
InpZoneB1 Включает стратегический модуль B1.
InpZoneB2 Включает стратегический модуль B2.
InpZoneB3 Включает стратегический модуль B3.
InpSess_Asia Включает торговлю в азиатскую сессию.
InpSess_London Включает торговлю в лондонскую сессию.
InpSess_Overlap Включает торговлю в период пересечения лондонской и нью-йоркской сессий.
InpSess_NY Включает торговлю в нью-йоркскую сессию.
InpPanelMode Выбор размера информационной панели: малый или большой с дополнительными метриками.
InpShowPanel Включает отображение информационной панели в реальной торговле.
InpPanelTest Включает отображение информационной панели в тестере стратегий.
InpPanelSize Масштаб информационной панели.
InpFontScale Масштаб шрифта информационной панели.
Получить полный код

Поиск по коду файла

#property strict
#property version "2.10"
#property description "Lizard MT5 2.1"

enum LotMode
{
   FIXED_VOLUME = 0, // Fixlot
   RISK_PERCENT = 1, // Risk %
   BALANCE_PER_001 = 2 // $ per 0.01 Lot
};

struct VolumeLimits
{
   double minimum;
   double maximum;
   double step;
   double strategyMaximum;
   double tickValue;
   double tickSize;
   double point;
   double priceUnit;
};

struct SizingSettings
{
   LotMode mode;
   double fixedVolume;
   double riskPercent;
   double dollarsPer001;
};

double CalculateStrategyVolume(const SizingSettings &settings,
                               const VolumeLimits &limits,
                               const double accountBasis,
                               const double roundedUsdBasis,
                               const double stopDistanceUnits,
                               const double allocationPercent,
                               const bool reduceForNfpWeek,
                               const double previousVolume)
{
   int initialPrecision = 2;
   if(limits.step == 0.1)
      initialPrecision = 1;

   if(settings.mode == FIXED_VOLUME)
   {
      double volume = NormalizeDouble(allocationPercent * 0.01 * settings.fixedVolume,
                                      initialPrecision);
      if(reduceForNfpWeek)
         volume = NormalizeDouble(limits.step * MathFloor(volume * 0.5 / limits.step), 8);
      volume = MathMax(volume, limits.minimum);
      if(limits.maximum > 0.0)
         volume = MathMin(volume, limits.maximum);
      return volume;
   }

   double volume = previousVolume;
   if(settings.mode == BALANCE_PER_001)
   {
      if(settings.dollarsPer001 <= 0.0)
         return limits.minimum;
      volume = (roundedUsdBasis / settings.dollarsPer001) * 0.01 * allocationPercent * 0.01;
      volume = NormalizeDouble(volume, initialPrecision);
   }
   else if(settings.mode == RISK_PERCENT)
   {
      double tickSize = limits.tickSize;
      if(tickSize <= 0.0)
         tickSize = limits.point;
      double lossPerLot = limits.tickValue * (stopDistanceUnits * limits.priceUnit) / tickSize;
      if(lossPerLot <= 0.0)
         lossPerLot = 1.0;
      volume = (accountBasis * settings.riskPercent / 100.0) / lossPerLot;
   }

   volume = MathMax(volume, limits.step);
   volume = MathMin(volume, limits.strategyMaximum);
   volume = MathMax(volume, limits.minimum);
   if(limits.maximum > 0.0)
      volume = MathMin(volume, limits.maximum);
   if(reduceForNfpWeek)
      volume *= 0.5;
   volume = NormalizeDouble(limits.step * MathFloor(volume / limits.step), 8);
   return MathMax(volume, limits.minimum);
}

enum TimeFilter
{
   TIME_OLD = 0, // Old
   TIME_NEW = 1 // New
};
enum FakeoutMode
{
   FAKEOUT_OFF = 0, // OFF
   FAKEOUT_SOFT = 1, // Soft
   FAKEOUT_NORMAL = 2, // Normal
   FAKEOUT_AGGRESSIVE = 3 // Aggressive
};
enum TrailMode
{
   TRAIL_DYNAMIC_HIGH = 0, // Dynamic High (high risk / high profit)
   TRAIL_DYNAMIC_LOW = 1, // Dynamic Low (low risk / low profit)
   TRAIL_FIXED = 2 // Fixed (custom BE + trail points)
};
enum VirtualStopMode
{
   VIRTUAL_ON_FREEZE = 0, // Trigger at Freeze Level (default)
   VIRTUAL_ALWAYS = 1, // On (always active)
   VIRTUAL_OFF = 2 // Off
};
enum OpeningDelay
{
   OPEN_AFTER_5 = 5, // 5 Min after open
   OPEN_AFTER_10 = 10, // 10 Min after open
   OPEN_AFTER_15 = 15, // 15 Min after open
   OPEN_AFTER_30 = 30, // 30 Min after open
   OPEN_AFTER_60 = 60 // 1 Hour after open
};
enum PayrollWeekMode
{
   PAYROLL_WEEK_OFF = 0, // Off
   PAYROLL_WEEK_BLOCK = 1, // Block (no trading Mon-Fri)
   PAYROLL_WEEK_HALF = 2 // Reduce Lot 50%
};
enum DailyLossUnit
{
   DAILY_LOSS_PERCENT = 0, // Percent %
   DAILY_LOSS_MONEY = 1 // Money $
};
enum PanelMode
{
   PANEL_SMALL = 0, // Small
   PANEL_LARGE = 1 // Large
};

input string Notice = "Trading are risky! Trade only if you can loss your money";
input string SimpleForexTeam__ = "free tools for forex! Join our channel for more free eas";
input string The_our_telegram = "";
input string VIP_group = "";
input group "======= News Filter Setup - READ FIRST ======="
input string InpNewsSetup1 = "Tools > Options > Expert > Allow WebRequest"; // NEWS FILTER: enable this
input string InpNewsSetup2 = ""; // ...and add this URL (copy)

input group "======= Community & Support ======="
input string InpWarn1 = "!! WARNING: Do NOT use MQL VPS !!"; // READ THIS
input string InpWarn2 = "Use a fast and stable VPS! Disconnects lose trade management"; // Risk Notice
input string InpInfoBacktest = ""; // Perform a Backtest before use!

input group "======= Main Settings ======="
input TimeFilter InpTimeFilter = TIME_NEW; // Time Filter
input FakeoutMode InpFakeout = FAKEOUT_NORMAL; // Fakeout Filter
input int InpFakeoutMinutes = 8; // Fakeout Normal: close-in-loss timer (minutes)
input int Zone1_ID = 1337; // Magic Number
input string Zone1_Tag = ""; // Trade Comment

input group "============ Trailing/SL ============"
input TrailMode InpTrailMode = TRAIL_DYNAMIC_HIGH; // Trailing/BE Mode
input int InpFixedBE = 0; // Fixed: BE Trigger (points, 0=must set)
input int InpFixedTrail = 0; // Fixed: Trail Distance (points, 0=must set)
input string InpSLInfo = "Normal=3000 / High=1000 / Armageddon=500"; // SL Presets (reference)
input int InpStopLossPts = 3000; // Stop Loss (points)
input VirtualStopMode InpVSL = VIRTUAL_ON_FREEZE; // Virtual SL (Freeze=automatic, On=always, Off=disabled)

input group "========== Market Window =========="
input int InpCloseWindow = 120; // Close Window (minutes before broker close)
input OpeningDelay InpOpenDelay = OPEN_AFTER_15; // Resume Entries After Open
input bool InpCloseWeekend = false; // Close Before Weekend

input group "======= News Filter ======="
input string InpNewsSetup3 = "Tools > Options > Expert > Allow WebRequest"; // REQUIRED for the news filter
input string InpNewsSetup4 = ""; // Add this URL (copy)
input bool InpNFP_Enable = true; // NFP (extra guard, High Impact already covers NFP)
input PayrollWeekMode InpNFPWeek = PAYROLL_WEEK_OFF; // NFP Week (Block = no trades Mon-Fri, Reduce = half lot)
input bool InpNews_High = true; // High Impact (CPI, FOMC, PPI, NFP etc.)
input bool InpNews_Holidays = true; // USD Holidays (no trading entire day)
input int Inp_GMT_Winter = 2; // GMT Offset Winter (Backtest + Manual Live)
input int Inp_GMT_Summer = 3; // GMT Offset Summer (Backtest + Manual Live)
input bool InpGMT_AutoDetect = true; // GMT Auto-Detect Live (Off = use Winter/Summer above)
input bool InpNFP_CloseOpen = true; // Close Open Positions (News)
input bool InpNFP_ClosePending = true; // Close Pending Orders (News)
input int InpNFP_Before = 30; // Minutes Before News
input int InpNFP_After = 30; // Minutes After News

input group "======= Lot Settings ======="
input string InpRiskInfo = ""; // Lot sizing only! Not a loss cap - use Max Daily DD for that
input LotMode InpLotMode = RISK_PERCENT; // Lot Mode
input double InpFixLot = 0.01; // Fix Lot Size
input double InpRiskPct = 1.0; // Risk %
input double InpDollarPer001 = 2500.0; // $ per 0.01 Lot
input bool InpUseEquity = false; // Use Equity
input bool InpCheckMargin = true; // Check Margin
input double InpMaxDailyDD = 0.0; // Max Daily DD value (0=off; % or $ per Unit)
input DailyLossUnit InpMaxDDUnit = DAILY_LOSS_PERCENT; // Max Daily DD Unit (% of peak equity, or $)

input group "======= Zone Selection (Custom) ======="
input bool InpZoneA1 = true; // A1 H1 Breakout (Base)
input bool InpZoneA2 = true; // A2 H1 Breakout (Base)
input bool InpZoneA3 = true; // A3 H1 Breakout (Base)
input bool InpZoneB1 = true; // B1 H1 Breakout (Mid)
input bool InpZoneB2 = true; // B2 H1 Breakout (Mid)
input bool InpZoneB3 = true; // B3 H1 Breakout (Mid)

input group "======= Session Filter ======="
input bool InpSess_Asia = true; // Asia: Sydney+Tokyo (NY close -> London open)
input bool InpSess_London = true; // London (London open -> NY open)
input bool InpSess_Overlap = true; // London+NY Overlap (NY open -> London close)
input bool InpSess_NY = true; // New York (London close -> NY close)

input group "======= Info Panel ======="
input PanelMode InpPanelMode = PANEL_SMALL; // Panel Mode (Large adds execution, cost, curve)
input bool InpShowPanel = true; // Show Panel (Live)
input bool InpPanelTest = false; // Show Panel (Backtest)
input double InpPanelSize = 1.0; // Panel Scale (0.5-2.0)
input double InpFontScale = 0.9; // Font Scale (0.5-2.0)

enum StrategyId
{
   CORE_A1,
   CORE_A2,
   CORE_A3,
   CORE_B1,
   CORE_B2,
   CORE_B3
};

bool IsStrategyHourBlocked(const StrategyId strategy, const int filterMode,
                           const datetime serverTime, const int brokerOffsetHours)
{
   MqlDateTime clock;
   TimeToStruct(serverTime, clock);
   int hour = (clock.hour - brokerOffsetHours + 24) % 24;
   if(filterMode == TIME_NEW)
   {
      switch(strategy)
      {
         case CORE_A1: return hour == 16 || hour == 19;
         case CORE_A2: return hour == 19;
         case CORE_A3: return hour == 5;
         case CORE_B1: return hour == 4 || hour == 7 || hour == 16 || hour == 17;
         default: return false;
      }
   }
   switch(strategy)
   {
      case CORE_A1: return hour == 8 || hour == 13 || hour == 16;
      case CORE_A2: return hour == 8 || hour == 16 || hour == 18 || hour == 19;
      case CORE_A3: return hour == 5 || hour == 8 || hour == 10 || hour == 14;
      case CORE_B1:
         return clock.day_of_week == 3 || hour == 1 || hour == 5 || hour == 8 ||
                hour == 10 || hour == 11 || hour == 12 || hour == 13 ||
                hour == 14 || hour == 19;
      default: return false;
   }
}

datetime CalendarTime(const int year, const int month, const int day,
                      const int hour = 0, const int minute = 0)
{
   MqlDateTime date = {};
   date.year = year;
   date.mon = month;
   date.day = day;
   date.hour = hour;
   date.min = minute;
   return StructToTime(date);
}

bool UsesSummerBrokerOffset(const datetime serverTime)
{
   MqlDateTime now, start, finish;
   TimeToStruct(serverTime, now);
   if(now.year < 1987)
      return false;
   if(now.year < 2007)
   {
      int startDay = 1 + ((6 * now.year - now.year / 4 + 2) % 7);
      int endDay = 31 - ((5 * now.year / 4 + 1) % 7);
      TimeToStruct(CalendarTime(now.year, 4, startDay, 2), start);
      TimeToStruct(CalendarTime(now.year, 10, endDay, 2), finish);
   }
   else
   {
      int weekdayOffset = (5 * now.year / 4 + 1) % 7;
      TimeToStruct(CalendarTime(now.year, 3, 14 - weekdayOffset, 2), start);
      TimeToStruct(CalendarTime(now.year, 11, 7 - weekdayOffset, 2), finish);
   }
   return now.day_of_year > start.day_of_year && now.day_of_year < finish.day_of_year;
}

int BrokerUtcOffset(const datetime serverTime, const datetime utcTime,
                    const bool autoDetect, const bool testing,
                    const int winterHours, const int summerHours)
{
   if(autoDetect && !testing)
   {
      MqlDateTime serverClock, utcClock;
      TimeToStruct(serverTime, serverClock);
      TimeToStruct(utcTime, utcClock);
      int offset = serverClock.hour - utcClock.hour;
      if(offset < -12) offset += 24;
      if(offset > 12) offset -= 24;
      return offset;
   }
   return UsesSummerBrokerOffset(serverTime) ? summerHours : winterHours;
}

int SundayOfMonth(const int year, const int month, const int occurrence)
{
   MqlDateTime first;
   TimeToStruct(CalendarTime(year, month, 1), first);
   return 1 + (7 - first.day_of_week) % 7 + (occurrence - 1) * 7;
}

int LastSundayOfMonth(const int year, const int month)
{
   int nextMonth = month + 1;
   int nextYear = year;
   if(nextMonth == 13) { nextMonth = 1; ++nextYear; }
   MqlDateTime last;
   TimeToStruct(CalendarTime(nextYear, nextMonth, 1) - 86400, last);
   return last.day - last.day_of_week;
}

bool IsTradingSessionBlocked(const datetime utcTime, const bool asia,
                             const bool london, const bool overlap, const bool newYork)
{
   if(asia && london && overlap && newYork)
      return false;
   MqlDateTime now;
   TimeToStruct(utcTime, now);
   datetime londonSummerStart = CalendarTime(now.year, 3, LastSundayOfMonth(now.year, 3), 1);
   datetime londonSummerEnd = CalendarTime(now.year, 10, LastSundayOfMonth(now.year, 10), 1);
   datetime newYorkSummerStart = CalendarTime(now.year, 3, SundayOfMonth(now.year, 3, 2), 7);
   datetime newYorkSummerEnd = CalendarTime(now.year, 11, SundayOfMonth(now.year, 11, 1), 6);
   bool londonSummer = utcTime >= londonSummerStart && utcTime < londonSummerEnd;
   bool newYorkSummer = utcTime >= newYorkSummerStart && utcTime < newYorkSummerEnd;
   int londonStart = londonSummer ? 7 : 8;
   int londonEnd = londonSummer ? 16 : 17;
   int newYorkStart = newYorkSummer ? 12 : 13;
   int newYorkEnd = newYorkSummer ? 21 : 22;
   if(now.hour >= newYorkEnd || now.hour < londonStart) return !asia;
   if(now.hour < newYorkStart) return !london;
   if(now.hour < londonEnd) return !overlap;
   return !newYork;
}

const datetime PAYROLL_DATES[] =
{
   D'2026.12.04 12:30:00',
   D'2026.11.06 12:30:00',
   D'2026.10.02 12:30:00',
   D'2026.09.04 12:30:00',
   D'2026.08.07 12:30:00',
   D'2026.07.02 12:30:00',
   D'2026.06.05 12:30:00',
   D'2026.05.08 12:30:00',
   D'2026.04.03 12:30:00',
   D'2026.03.06 12:30:00',
   D'2026.02.11 12:30:00',
   D'2026.01.09 12:30:00',
   D'2025.12.16 12:30:00',
   D'2025.11.07 12:30:00',
   D'2025.10.03 12:30:00',
   D'2025.09.05 12:30:00',
   D'2025.08.01 12:30:00',
   D'2025.07.03 12:30:00',
   D'2025.06.06 12:30:00',
   D'2025.05.02 12:30:00',
   D'2025.04.04 12:30:00',
   D'2025.03.07 12:30:00',
   D'2025.02.07 12:30:00',
   D'2025.01.10 12:30:00',
   D'2024.12.06 12:30:00',
   D'2024.11.01 12:30:00',
   D'2024.10.04 12:30:00',
   D'2024.09.06 12:30:00',
   D'2024.08.02 12:30:00',
   D'2024.07.05 12:30:00',
   D'2024.06.07 12:30:00',
   D'2024.05.03 12:30:00',
   D'2024.04.05 12:30:00',
   D'2024.03.08 12:30:00',
   D'2024.02.02 12:30:00',
   D'2024.01.05 12:30:00',
   D'2023.12.08 12:30:00',
   D'2023.11.03 12:30:00',
   D'2023.10.06 12:30:00',
   D'2023.09.01 12:30:00',
   D'2023.08.04 12:30:00',
   D'2023.07.07 12:30:00',
   D'2023.06.02 12:30:00',
   D'2023.05.05 12:30:00',
   D'2023.04.07 12:30:00',
   D'2023.03.10 12:30:00',
   D'2023.02.03 12:30:00',
   D'2023.01.06 12:30:00'
};

datetime PayrollDateForMonth(const int year, const int month)
{
   for(int i = 0; i < ArraySize(PAYROLL_DATES); ++i)
   {
      MqlDateTime event;
      TimeToStruct(PAYROLL_DATES[i], event);
      if(event.year == year && event.mon == month)
         return PAYROLL_DATES[i];
   }
   if(year < 2027)
      return 0;
   MqlDateTime first;
   TimeToStruct(CalendarTime(year, month, 1), first);
   int firstFriday = 1 + (5 - first.day_of_week + 7) % 7;
   return CalendarTime(year, month, firstFriday, 12, 30);
}

bool IsPayrollWindow(const datetime utcLikeTime, const datetime serverTime,
                     const int minutesBefore, const int minutesAfter)
{
   MqlDateTime now;
   TimeToStruct(serverTime, now);
   datetime event = PayrollDateForMonth(now.year, now.mon);
   if(event == 0)
      return false;
   if(!UsesSummerBrokerOffset(serverTime))
      event += 3600;
   return utcLikeTime >= event - minutesBefore * 60 &&
          utcLikeTime <= event + minutesAfter * 60;
}

enum SwingSide
{
   SWING_HIGH,
   SWING_LOW
};

struct SwingConfig
{
   ENUM_TIMEFRAMES timeframe;
   int newerBars;
   int olderBars;
   int lastCandidateShift;
   double minimumQuoteDistance;
   double buyEntryOffset;
   double sellEntryOffset;
   double pendingSpacing;
};

double ReadSwingBarPrice(const string symbol, const ENUM_TIMEFRAMES timeframe,
                        const SwingSide side, const int shift)
{
   if(side == SWING_HIGH)
      return iHigh(symbol, timeframe, shift);
   return iLow(symbol, timeframe, shift);
}

bool IsMoreExtremePrice(const double price, const double candidate,
                        const SwingSide side)
{
   if(side == SWING_HIGH)
      return price > candidate;
   return price < candidate;
}

bool HasNearbyStopOrder(const string symbol, const long magic,
                       const SwingSide side, const double entryPrice,
                       const double spacing)
{
   ENUM_ORDER_TYPE expectedType = ORDER_TYPE_BUY_STOP;
   if(side == SWING_LOW)
      expectedType = ORDER_TYPE_SELL_STOP;

   for(int index = OrdersTotal() - 1; index >= 0; --index)
   {
      if(OrderGetTicket(index) == 0)
         continue;
      if(OrderGetInteger(ORDER_MAGIC) != magic || OrderGetString(ORDER_SYMBOL) != symbol)
         continue;
      if((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE) != expectedType)
         continue;
      if(MathAbs(OrderGetDouble(ORDER_PRICE_OPEN) - entryPrice) < spacing)
         return true;
   }
   return false;
}

double FindSwingPrice(const string symbol, const long magic,
                      const SwingConfig &config, const SwingSide side,
                      const double priceUnit, const int priceDigits)
{
   for(int candidateShift = config.newerBars + 1;
       candidateShift <= config.lastCandidateShift; ++candidateShift)
   {
      bool newerSideValid = true;
      for(int shift = candidateShift; shift >= candidateShift - config.newerBars; --shift)
      {
         double neighbor = ReadSwingBarPrice(symbol, config.timeframe, side, shift);
         double candidate = ReadSwingBarPrice(symbol, config.timeframe, side, candidateShift);
         if(IsMoreExtremePrice(neighbor, candidate, side))
         {
            newerSideValid = false;
            break;
         }
      }

      bool olderSideValid = true;
      for(int shift = candidateShift; shift <= candidateShift + config.olderBars; ++shift)
      {
         double neighbor = ReadSwingBarPrice(symbol, config.timeframe, side, shift);
         double candidate = ReadSwingBarPrice(symbol, config.timeframe, side, candidateShift);
         if(IsMoreExtremePrice(neighbor, candidate, side))
         {
            olderSideValid = false;
            break;
         }
      }

      double candidate = ReadSwingBarPrice(symbol, config.timeframe, side, candidateShift);
      bool farEnough;
      if(side == SWING_HIGH)
      {
         double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
         farEnough = candidate > ask + config.minimumQuoteDistance * priceUnit;
      }
      else
      {
         double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
         farEnough = candidate < bid - config.minimumQuoteDistance * priceUnit;
      }
      if(!newerSideValid || !olderSideValid || !farEnough)
         continue;

      double recentExtreme = ReadSwingBarPrice(symbol, config.timeframe, side, 0);
      for(int shift = 1; shift <= candidateShift; ++shift)
      {
         double price = ReadSwingBarPrice(symbol, config.timeframe, side, shift);
         if(IsMoreExtremePrice(price, recentExtreme, side))
            recentExtreme = price;
      }
      if(IsMoreExtremePrice(recentExtreme, candidate, side))
         continue;

      candidate = NormalizeDouble(ReadSwingBarPrice(symbol, config.timeframe, side,
                                                    candidateShift), priceDigits);
      double offset = config.buyEntryOffset;
      if(side == SWING_LOW)
         offset = -config.sellEntryOffset;
      if(HasNearbyStopOrder(symbol, magic, side, candidate + offset * priceUnit,
                           config.pendingSpacing * priceUnit))
         continue;
      return candidate;
   }
   return 0.0;
}

struct StrategyConfig
{
   long magicOffset;
   string commentSuffix;
   double referenceDailyOpen;
   double takeProfitExtraFraction;
   ENUM_TIMEFRAMES swingTimeframe;
   ENUM_TIMEFRAMES entryRefreshTimeframe;
   int swingOlderBars;
   int swingNewerBars;
   int lastSwingShift;
   double minimumSwingDistance;
   double buyEntryOffset;
   double sellEntryOffset;
   int pendingSearchPassLimit;
   double pendingSpacing;
   int pendingLifetimeHours;
   bool throttleManagementToM1;
   int maxPositionsPerSide;
   double stopDistance;
   double takeProfitBaseDistance;
   double trailingDistance;
   double trailingActivation;
   double trailingStep;
   double takeProfitFollowDistance;
   double takeProfitFollowAdverseThreshold;
   double breakEvenActivation;
   double breakEvenLockedProfit;
   bool auxiliarySwingUseH1;
   bool auxiliarySwingSearchTo50;
   int auxiliarySwingNewerBars;
   int auxiliarySwingOlderBars;
};

const StrategyConfig strategyDefinitions[6] =
{
   {9, "_A1", 2400.0, 0.03, PERIOD_H1, PERIOD_M5, 26, 24, 140, 120.0, -105.0, -130.0, 5, 55.0, 20, false, 5, 3636.0, 800.0, 145.0, 145.0, 0.1, 350.0, 1950.0, 145.0, 36.0, true, true, 0, 0},
   {14, "_A2", 2600.0, 0.05, PERIOD_H1, PERIOD_M15, 25, 23, 145, 10.0, -55.0, -140.0, 5, 90.0, 60, false, 5, 2150.0, 1450.0, 158.0, 158.0, 0.1, 350.0, 2800.0, 158.0, 39.0, true, true, 12, 17},
   {15, "_A3", 2800.0, 0.07, PERIOD_H1, PERIOD_M15, 26, 20, 235, 80.0, -140.0, -170.0, 5, 5.0, 55, false, 20, 1800.0, 1200.0, 170.0, 170.0, 0.1, 250.0, 1950.0, 170.0, 42.0, true, true, 15, 3},
   {13, "_B1", 1900.0, 0.1, PERIOD_H1, PERIOD_M15, 7, 5, 200, 40.0, -130.0, -125.0, 3, 5.0, 15, false, 99, 2878.0, 1350.0, 115.0, 115.0, 0.1, 250.0, 1850.0, 115.0, 29.0, true, true, 1, 9},
   {12, "_B2", 2600.0, 0.03, PERIOD_H1, PERIOD_M15, 30, 19, 110, 160.0, -115.0, -105.0, 3, 55.0, 30, false, 99, 3940.0, 900.0, 158.0, 158.0, 0.1, 250.0, 1900.0, 158.0, 39.0, true, true, 0, 0},
   {8, "_B3", 2000.0, 0.1, PERIOD_D1, PERIOD_H1, 7, 2, 20, 250.0, -112.0, -104.0, 1, 980.0, 432, false, 99, 730.0, 3300.0, 121.0, 121.0, 0.1, 2000.0, 1000.0, 121.0, 30.0, true, true, 7, 4}
};

void BuildEffectiveStrategyConfig(const StrategyId id,
                                  const double previousDailyOpen,
                                  const TrailMode trailingMode,
                                  const double fixedBreakEven,
                                  const double fixedTrail,
                                  const double configuredStop,
                                  StrategyConfig &config)
{
   config = strategyDefinitions[(int)id];
   double scale = 1.0;
   if(config.referenceDailyOpen > 0.0)
      scale = previousDailyOpen / config.referenceDailyOpen;
   if(scale <= 0.0)
      return;

   config.minimumSwingDistance *= scale;
   config.buyEntryOffset = NormalizeDouble(config.buyEntryOffset * scale, 0);
   config.sellEntryOffset = NormalizeDouble(config.sellEntryOffset * scale, 0);
   config.takeProfitBaseDistance *= scale;
   if(trailingMode == TRAIL_DYNAMIC_HIGH || trailingMode == TRAIL_DYNAMIC_LOW)
   {
      double trailingScale = scale;
      if(trailingMode == TRAIL_DYNAMIC_LOW)
         trailingScale = MathMin(trailingScale, 1.2);
      config.trailingDistance *= trailingScale;
      config.trailingActivation *= trailingScale;
      config.takeProfitFollowDistance *= trailingScale;
      config.takeProfitFollowAdverseThreshold *= trailingScale;
      config.breakEvenActivation *= trailingScale;
      config.breakEvenLockedProfit *= trailingScale;
   }
   else
   {
      config.trailingActivation = fixedBreakEven;
      config.trailingDistance = fixedTrail;
      config.breakEvenActivation = fixedBreakEven;
      config.breakEvenLockedProfit = 0.0;
      config.takeProfitFollowDistance *= scale;
      config.takeProfitFollowAdverseThreshold *= scale;
   }
   config.stopDistance = configuredStop;
}

void AdjustActivationForSpread(StrategyConfig &config, const double spread,
                               const double priceUnit)
{
   double spreadUnits = spread / priceUnit;
   if(spreadUnits > 0.0)
   {
      config.breakEvenActivation = MathMax(1.0, config.breakEvenActivation - spreadUnits);
      config.trailingActivation = MathMax(1.0, config.trailingActivation - spreadUnits);
   }
}

void GetSwingConfig(const StrategyConfig &config, SwingConfig &swing)
{
   swing.timeframe = config.swingTimeframe;
   swing.newerBars = config.swingNewerBars;
   swing.olderBars = config.swingOlderBars;
   swing.lastCandidateShift = config.lastSwingShift;
   swing.minimumQuoteDistance = config.minimumSwingDistance;
   swing.buyEntryOffset = config.buyEntryOffset;
   swing.sellEntryOffset = config.sellEntryOffset;
   swing.pendingSpacing = config.pendingSpacing;
}

struct EntryReference
{
   ulong ticket;
   double price;
};
struct VirtualTarget
{
   ulong ticket;
   double price;
   bool armed;
};
struct VirtualStop
{
   ulong ticket;
   double price;
   ENUM_POSITION_TYPE side;
};
EntryReference entryReferences[100];
VirtualTarget virtualTargets[50];
VirtualStop virtualStops[50];
int virtualStopCount = 0;

void RememberEntryPrice(const ulong ticket, const double price)
{
   for(int i = 0; i < ArraySize(entryReferences); ++i)
   {
      if(entryReferences[i].ticket == ticket)
      {
         entryReferences[i].price = price;
         return;
      }
   }
   for(int i = 0; i < ArraySize(entryReferences); ++i)
   {
      if(entryReferences[i].ticket == 0)
      {
         entryReferences[i].ticket = ticket;
         entryReferences[i].price = price;
         return;
      }
   }
}

double EntryReferencePrice(const ulong ticket, const double actualEntry)
{
   for(int i = 0; i < ArraySize(entryReferences); ++i)
      if(entryReferences[i].ticket == ticket && entryReferences[i].price > 0.0)
         return entryReferences[i].price;
   RememberEntryPrice(ticket, actualEntry);
   return actualEntry;
}

void ForgetEntryPrice(const ulong ticket)
{
   for(int i = 0; i < ArraySize(entryReferences); ++i)
      if(entryReferences[i].ticket == ticket)
         ZeroMemory(entryReferences[i]);
}

void PruneEntryReferences()
{
   for(int i = 0; i < ArraySize(entryReferences); ++i)
   {
      ulong ticket = entryReferences[i].ticket;
      if(ticket != 0 && !PositionSelectByTicket(ticket) && !OrderSelect(ticket))
         ZeroMemory(entryReferences[i]);
   }
}

int FindVirtualTarget(const ulong ticket)
{
   for(int i = 0; i < ArraySize(virtualTargets); ++i)
      if(virtualTargets[i].ticket == ticket)
         return i;
   return -1;
}

int RegisterVirtualTarget(const ulong ticket, const double price)
{
   int existing = FindVirtualTarget(ticket);
   if(existing >= 0)
      return existing;
   for(int i = 0; i < ArraySize(virtualTargets); ++i)
   {
      if(virtualTargets[i].ticket != 0)
         continue;
      virtualTargets[i].ticket = ticket;
      virtualTargets[i].price = price;
      virtualTargets[i].armed = false;
      string name = "vtp_" + (string)ticket;
      ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
      ObjectSetInteger(0, name, OBJPROP_COLOR, clrYellow);
      ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, name, OBJPROP_BACK, true);
      return i;
   }
   return -1;
}

void ClearVirtualTarget(const ulong ticket)
{
   int index = FindVirtualTarget(ticket);
   if(index >= 0)
      ZeroMemory(virtualTargets[index]);
   ObjectDelete(0, "vtp_" + (string)ticket);
}

int FindVirtualStop(const ulong ticket)
{
   for(int i = 0; i < virtualStopCount; ++i)
      if(virtualStops[i].ticket == ticket)
         return i;
   return -1;
}

void TightenVirtualStop(const ulong ticket, const ENUM_POSITION_TYPE side, const double price)
{
   int index = FindVirtualStop(ticket);
   if(index < 0)
   {
      if(virtualStopCount >= ArraySize(virtualStops))
         return;
      index = virtualStopCount++;
      virtualStops[index].ticket = ticket;
      virtualStops[index].side = side;
      virtualStops[index].price = 0.0;
   }
   else if(virtualStops[index].price > 0.0)
   {
      if(side == POSITION_TYPE_BUY && price <= virtualStops[index].price) return;
      if(side == POSITION_TYPE_SELL && price >= virtualStops[index].price) return;
   }
   virtualStops[index].price = price;
}

void RemoveVirtualStop(const ulong ticket)
{
   int index = FindVirtualStop(ticket);
   if(index < 0)
      return;
   --virtualStopCount;
   virtualStops[index] = virtualStops[virtualStopCount];
   ZeroMemory(virtualStops[virtualStopCount]);
}

double CalculateVirtualTarget(const string symbol, const double entry,
                              const ENUM_POSITION_TYPE side, const double point, const int digits)
{
   double prices[];
   int copied;
   if(side == POSITION_TYPE_BUY)
      copied = CopyLow(symbol, PERIOD_H1, 1, 24, prices);
   else
      copied = CopyHigh(symbol, PERIOD_H1, 1, 24, prices);
   double direction = side == POSITION_TYPE_BUY ? 1.0 : -1.0;
   double distance = 100.0 * point;
   if(copied == 24)
   {
      double extreme = prices[0];
      for(int bar = 1; bar < 24; ++bar)
      {
         if(side == POSITION_TYPE_BUY) extreme = MathMin(extreme, prices[bar]);
         else extreme = MathMax(extreme, prices[bar]);
      }
      if(extreme > 0.0)
         distance = 0.25 * MathMax(distance, direction * (entry - extreme));
   }
   return NormalizeDouble(entry + direction * distance, digits);
}

#include <Trade/Trade.mqh>

void RecordExecutionDuration(const int action, const ulong started, const bool accepted);

CTrade trade;

struct SymbolSnapshot
{
   string symbol;
   int digits;
   double point;
   double priceUnit;
   double bid;
   double ask;
   double spread;
   double stopsDistance;
   double freezeDistance;
};

void RefreshSymbolSnapshot(const string symbol, SymbolSnapshot &market)
{
   market.symbol = symbol;
   market.point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   market.digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   market.priceUnit = market.point;
   if(market.digits == 3 || market.digits == 5)
      market.priceUnit *= 10.0;
   if((ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE) == SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE)
      market.priceUnit = market.point / 10.0;
   market.ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
   market.bid = SymbolInfoDouble(symbol, SYMBOL_BID);
   market.spread = market.ask - market.bid;
   market.stopsDistance = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * market.point;
   market.freezeDistance = SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL) * market.point;
}

bool IsOwnedMagic(const long magic)
{
   return magic > Zone1_ID && magic <= (long)Zone1_ID + 15;
}

bool IsSelectedOwnedPosition(const string symbol)
{
   return PositionGetString(POSITION_SYMBOL) == symbol && IsOwnedMagic(PositionGetInteger(POSITION_MAGIC));
}

bool IsSelectedOwnedOrder(const string symbol)
{
   return OrderGetString(ORDER_SYMBOL) == symbol && IsOwnedMagic(OrderGetInteger(ORDER_MAGIC));
}

void DeleteOwnedPendingOrders(const string symbol)
{
   for(int i = OrdersTotal() - 1; i >= 0; --i)
   {
      ulong ticket = OrderGetTicket(i);
      if(ticket != 0 && IsSelectedOwnedOrder(symbol))
         trade.OrderDelete(ticket);
   }
}

void CloseOwnedPositions(const string symbol)
{
   for(int i = PositionsTotal() - 1; i >= 0; --i)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0 && IsSelectedOwnedPosition(symbol))
         CloseManagedPosition(ticket);
   }
}

int CountStrategyPositions(const string symbol, const long magic, const ENUM_POSITION_TYPE side)
{
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; --i)
   {
      if(PositionGetTicket(i) == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic &&
         (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == side)
         ++count;
   }
   return count;
}

bool ModifyProtection(const ulong ticket, const double stopLoss, const double takeProfit)
{
   ulong started = GetMicrosecondCount();
   bool accepted = trade.PositionModify(ticket, stopLoss, takeProfit);
   RecordExecutionDuration(1, started, accepted);
   return accepted;
}

bool CloseManagedPosition(const ulong ticket)
{
   ulong started = GetMicrosecondCount();
   bool accepted = trade.PositionClose(ticket);
   RecordExecutionDuration(2, started, accepted);
   return accepted;
}

struct TrapState
{
   datetime lastHour;
   double positiveDirection;
   double negativeDirection;
   double stochastic;
};

void RefreshTrapIndicators(const string symbol, TrapState &state)
{
   datetime currentHour = iTime(symbol, PERIOD_H1, 0);
   if(currentHour == state.lastHour)
      return;
   state.lastHour = currentHour;

   double highs[], lows[], closes[];
   if(CopyHigh(symbol, PERIOD_H1, 0, 43, highs) == 43 &&
      CopyLow(symbol, PERIOD_H1, 0, 43, lows) == 43 &&
      CopyClose(symbol, PERIOD_H1, 0, 43, closes) == 43)
   {
      double positive = 0.0, negative = 0.0, range = 0.0;
      for(int bar = 1; bar < 43; ++bar)
      {
         double rise = highs[bar] - highs[bar - 1];
         double fall = lows[bar - 1] - lows[bar];
         double positiveMove = 0.0, negativeMove = 0.0;
         if(rise > fall && rise > 0.0) positiveMove = rise;
         if(fall > rise && fall > 0.0) negativeMove = fall;
         double trueRange = MathMax(highs[bar] - lows[bar],
                                   MathMax(MathAbs(highs[bar] - closes[bar - 1]),
                                           MathAbs(lows[bar] - closes[bar - 1])));
         if(bar <= 14)
         {
            positive += positiveMove;
            negative += negativeMove;
            range += trueRange;
         }
         else
         {
            positive = positive + positive / -14.0 + positiveMove;
            negative = negative - negative / 14.0 + negativeMove;
            range = range - range / 14.0 + trueRange;
         }
      }
      if(range > 0.0)
      {
         state.positiveDirection = positive / range * 100.0;
         state.negativeDirection = negative / range * 100.0;
      }
   }

   if(CopyHigh(symbol, PERIOD_H1, 1, 17, highs) != 17 ||
      CopyLow(symbol, PERIOD_H1, 1, 17, lows) != 17 ||
      CopyClose(symbol, PERIOD_H1, 1, 17, closes) != 17)
      return;
   double total = 0.0;
   for(int bar = 14; bar < 17; ++bar)
   {
      double highest = highs[bar - 13];
      double lowest = lows[bar - 13];
      for(int prior = bar - 12; prior <= bar; ++prior)
      {
         highest = MathMax(highest, highs[prior]);
         lowest = MathMin(lowest, lows[prior]);
      }
      double oscillator = 50.0;
      if(highest > lowest)
         oscillator = ((closes[bar] - lowest) / (highest - lowest)) * 100.0;
      total += oscillator;
   }
   state.stochastic = total / 3.0;
}

bool IsPendingTrap(const ENUM_ORDER_TYPE type, const double orderPrice,
                   const double bid, const double ask, const double stopsDistance,
                   const TrapState &state)
{
   if(type == ORDER_TYPE_BUY_STOP)
      return ask >= orderPrice - stopsDistance &&
             (state.positiveDirection <= state.negativeDirection || state.stochastic <= 65.0);
   if(type == ORDER_TYPE_SELL_STOP)
      return bid <= orderPrice + stopsDistance &&
             (state.negativeDirection <= state.positiveDirection || state.stochastic >= 35.0);
   return false;
}

bool CheckFallbackStop(const ulong ticket, const double bid, const double ask)
{
   int index = FindVirtualStop(ticket);
   if(index < 0 || virtualStops[index].price <= 0.0)
      return false;
   bool crossed = virtualStops[index].side == POSITION_TYPE_BUY
                  ? bid <= virtualStops[index].price : ask >= virtualStops[index].price;
   if(!crossed || !CloseManagedPosition(ticket))
      return false;
   ClearVirtualTarget(ticket);
   RemoveVirtualStop(ticket);
   return true;
}

bool CloseFakeoutPosition(const ulong ticket)
{
   if(!CloseManagedPosition(ticket))
      return false;
   ClearVirtualTarget(ticket);
   return true;
}

bool IsCandleFakeout(const string symbol, const datetime opened, const double entry,
                     const ENUM_POSITION_TYPE side)
{
   datetime previousBar = iTime(symbol, PERIOD_M1, 1);
   datetime currentBar = iTime(symbol, PERIOD_M1, 0);
   if(opened < previousBar || opened >= currentBar)
      return false;
   double open = iOpen(symbol, PERIOD_M1, 1);
   double close = iClose(symbol, PERIOD_M1, 1);
   if(side == POSITION_TYPE_BUY)
      return close < open && close < entry;
   return close > open && close > entry;
}

bool CheckFakeoutExits(const ulong ticket, const datetime opened, const double entry,
                       const ENUM_POSITION_TYPE side, const SymbolSnapshot &market)
{
   if(InpFakeout >= FAKEOUT_SOFT && IsCandleFakeout(market.symbol, opened, entry, side) &&
      CloseFakeoutPosition(ticket))
      return true;
   bool losing = side == POSITION_TYPE_BUY ? market.bid < entry : market.ask > entry;
   if(InpFakeout >= FAKEOUT_NORMAL && TimeCurrent() >= opened + MathMax(1, InpFakeoutMinutes) * 60 &&
      losing && CloseFakeoutPosition(ticket))
      return true;
   if(InpFakeout >= FAKEOUT_AGGRESSIVE && opened >= iTime(market.symbol, PERIOD_M1, 2) &&
      opened < iTime(market.symbol, PERIOD_M1, 0))
   {
      double close = iClose(market.symbol, PERIOD_M1, 1);
      bool adverse = side == POSITION_TYPE_BUY ? close < entry : close > entry;
      if(adverse && CloseFakeoutPosition(ticket))
         return true;
   }
   return false;
}

bool CheckVirtualTarget(const ulong ticket, const double entry, const ENUM_POSITION_TYPE side,
                        const SymbolSnapshot &market)
{
   int index = FindVirtualTarget(ticket);
   if(index < 0)
   {
      double initialTarget = CalculateVirtualTarget(market.symbol, entry, side, market.point, market.digits);
      if(side == POSITION_TYPE_BUY && initialTarget <= entry) return false;
      if(side == POSITION_TYPE_SELL && initialTarget >= entry) return false;
      index = RegisterVirtualTarget(ticket, initialTarget);
   }
   if(index < 0) return false;
   double target = virtualTargets[index].price;
   if(side == POSITION_TYPE_BUY && target <= entry) return false;
   if(side == POSITION_TYPE_SELL && target >= entry) return false;
   double quote = side == POSITION_TYPE_BUY ? market.bid : market.ask;
   if(!virtualTargets[index].armed)
   {
      bool reached = side == POSITION_TYPE_BUY ? quote >= target : quote <= target;
      if(reached) virtualTargets[index].armed = true;
      return false;
   }
   bool pulledBack = side == POSITION_TYPE_BUY ? quote <= target : quote >= target;
   if(!pulledBack) return false;
   ClearVirtualTarget(ticket);
   return CloseManagedPosition(ticket);
}

bool ApplyBuyProtection(const ulong ticket, const double entry, const double reference,
                        double &stopLoss, double &takeProfit,
                        const StrategyConfig &config, const SymbolSnapshot &market)
{
   bool changed = false;
   double candidate;
   if(config.breakEvenActivation > 0.0 &&
      market.bid > NormalizeDouble(entry + config.breakEvenActivation * market.priceUnit, market.digits))
   {
      candidate = NormalizeDouble(entry + config.breakEvenLockedProfit * market.priceUnit, market.digits);
      if(candidate > stopLoss + market.point && candidate + market.stopsDistance < market.bid &&
         market.bid < takeProfit - market.freezeDistance && candidate < market.bid - market.stopsDistance)
      {
         stopLoss = candidate;
         if(ModifyProtection(ticket, candidate, takeProfit)) changed = true;
      }
   }
   if(config.trailingDistance > 0.0 &&
      market.bid > NormalizeDouble(stopLoss + market.point +
                                   (config.trailingStep + config.trailingDistance) * market.priceUnit, market.digits) &&
      market.bid > NormalizeDouble(reference + config.trailingActivation * market.priceUnit, market.digits) &&
      market.bid < takeProfit - market.freezeDistance)
   {
      candidate = NormalizeDouble(market.bid - config.trailingDistance * market.priceUnit, market.digits);
      if(candidate < market.bid - market.stopsDistance && ModifyProtection(ticket, candidate, takeProfit))
      {
         stopLoss = candidate;
         changed = true;
      }
   }
   bool slipped = reference > 0.0 && entry - reference > 5000.0 * market.priceUnit;
   if(slipped && market.bid > stopLoss + 100.0 * market.priceUnit + market.point + market.stopsDistance &&
      market.bid > reference + 400.0 * market.priceUnit && market.bid < takeProfit - market.freezeDistance)
   {
      candidate = NormalizeDouble(market.bid - 100.0 * market.priceUnit, market.digits);
      if(candidate < market.bid - market.stopsDistance && ModifyProtection(ticket, candidate, takeProfit))
      {
         stopLoss = candidate;
         changed = true;
      }
   }
   if(config.takeProfitFollowDistance > 0.0 &&
      market.ask < NormalizeDouble(takeProfit - market.point - config.takeProfitFollowDistance * market.priceUnit, market.digits) &&
      market.ask < NormalizeDouble(reference - config.takeProfitFollowAdverseThreshold * market.priceUnit, market.digits) &&
      market.bid < takeProfit - market.freezeDistance)
   {
      candidate = NormalizeDouble(market.bid + config.takeProfitFollowDistance * market.priceUnit, market.digits);
      if(market.ask + market.stopsDistance < candidate && ModifyProtection(ticket, stopLoss, candidate))
      {
         takeProfit = candidate;
         changed = true;
      }
   }
   return changed;
}

bool ApplySellProtection(const ulong ticket, const double entry, const double reference,
                         double &stopLoss, double &takeProfit,
                         const StrategyConfig &config, const SymbolSnapshot &market)
{
   bool changed = false;
   double candidate;
   if(config.breakEvenActivation > 0.0 && market.ask < entry - config.breakEvenActivation * market.priceUnit)
   {
      candidate = entry - config.breakEvenLockedProfit * market.priceUnit;
      if(candidate < stopLoss - market.point && market.ask < candidate - market.stopsDistance &&
         takeProfit + market.freezeDistance < market.ask)
      {
         candidate = NormalizeDouble(candidate, market.digits);
         if(candidate > market.ask + market.stopsDistance && ModifyProtection(ticket, candidate, takeProfit))
         {
            stopLoss = candidate;
            changed = true;
         }
      }
   }
   if(config.trailingDistance > 0.0 &&
      market.ask < (stopLoss - market.point) - (config.trailingStep + config.trailingDistance) * market.priceUnit &&
      market.ask < reference - config.trailingActivation * market.priceUnit &&
      takeProfit + market.freezeDistance < market.ask)
   {
      candidate = NormalizeDouble(market.ask + config.trailingDistance * market.priceUnit, market.digits);
      if(candidate > market.ask + market.stopsDistance && ModifyProtection(ticket, candidate, takeProfit))
      {
         stopLoss = candidate;
         changed = true;
      }
   }
   bool slipped = reference > 0.0 && reference - entry > 5000.0 * market.priceUnit;
   if(slipped && market.ask < (stopLoss - market.point) - 100.0 * market.priceUnit &&
      market.ask < reference - 400.0 * market.priceUnit && takeProfit + market.freezeDistance < market.ask)
   {
      candidate = NormalizeDouble(market.ask + 100.0 * market.priceUnit, market.digits);
      if(candidate > market.ask + market.stopsDistance && ModifyProtection(ticket, candidate, takeProfit))
      {
         stopLoss = candidate;
         changed = true;
      }
   }
   if(config.takeProfitFollowDistance > 0.0 &&
      market.bid > NormalizeDouble(market.point + takeProfit + config.takeProfitFollowDistance * market.priceUnit, market.digits) &&
      market.bid > NormalizeDouble(reference + config.takeProfitFollowAdverseThreshold * market.priceUnit, market.digits) &&
      market.bid > takeProfit + market.freezeDistance)
   {
      candidate = NormalizeDouble(market.bid - config.takeProfitFollowDistance * market.priceUnit, market.digits);
      if(candidate < market.bid - market.stopsDistance && ModifyProtection(ticket, stopLoss, candidate))
      {
         takeProfit = candidate;
         changed = true;
      }
   }
   return changed;
}

double VirtualProtectionCandidate(const ulong ticket, const ENUM_POSITION_TYPE side,
                                  const double entry, const double reference, const double stopLoss,
                                  const StrategyConfig &config, const SymbolSnapshot &market,
                                  const bool includeExistingVirtual)
{
   double previous = stopLoss;
   int index = FindVirtualStop(ticket);
   if(includeExistingVirtual && index >= 0 && virtualStops[index].price > 0.0)
   {
      if(side == POSITION_TYPE_BUY) previous = MathMax(previous, virtualStops[index].price);
      else if(previous == 0.0 || virtualStops[index].price < previous) previous = virtualStops[index].price;
   }
   double candidate = 0.0;
   if(side == POSITION_TYPE_BUY)
   {
      if(config.breakEvenActivation > 0.0 && market.bid > NormalizeDouble(entry + config.breakEvenActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(entry + config.breakEvenLockedProfit * market.priceUnit, market.digits);
         if(level > 0.0 && level > previous + market.point) candidate = level;
      }
      if(config.trailingDistance > 0.0 &&
         market.bid > NormalizeDouble(previous + market.point + (config.trailingStep + config.trailingDistance) * market.priceUnit, market.digits) &&
         market.bid > NormalizeDouble(reference + config.trailingActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(market.bid - config.trailingDistance * market.priceUnit, market.digits);
         if(level > previous + market.point && level > candidate) candidate = level;
      }
   }
   else
   {
      if(config.breakEvenActivation > 0.0 && market.ask < entry - config.breakEvenActivation * market.priceUnit)
      {
         double level = NormalizeDouble(entry - config.breakEvenLockedProfit * market.priceUnit, market.digits);
         if(level > 0.0 && level < previous - market.point) candidate = level;
      }
      if(config.trailingDistance > 0.0 &&
         market.ask < (previous - market.point) - (config.trailingStep + config.trailingDistance) * market.priceUnit &&
         market.ask < reference - config.trailingActivation * market.priceUnit)
      {
         double level = NormalizeDouble(market.ask + config.trailingDistance * market.priceUnit, market.digits);
         if(level < previous - market.point && (candidate == 0.0 || level < candidate)) candidate = level;
      }
   }
   return candidate;
}

double BrokerFallbackCandidate(const ENUM_POSITION_TYPE side, const double entry,
                               const double reference, const double stopLoss,
                               const StrategyConfig &config, const SymbolSnapshot &market)
{
   double candidate = 0.0;
   if(side == POSITION_TYPE_BUY)
   {
      if(config.breakEvenActivation > 0.0 &&
         market.bid > NormalizeDouble(entry + config.breakEvenActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(entry + config.breakEvenLockedProfit * market.priceUnit, market.digits);
         if(level > stopLoss + market.point) candidate = level;
      }
      if(config.trailingDistance > 0.0 &&
         market.bid > NormalizeDouble(reference + config.trailingActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(market.bid - config.trailingDistance * market.priceUnit, market.digits);
         if(level > stopLoss + market.point && level > candidate) candidate = level;
      }
   }
   else
   {
      if(config.breakEvenActivation > 0.0 &&
         market.ask < NormalizeDouble(entry - config.breakEvenActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(entry - config.breakEvenLockedProfit * market.priceUnit, market.digits);
         if(level > 0.0 && level < stopLoss - market.point) candidate = level;
      }
      if(config.trailingDistance > 0.0 &&
         market.ask < NormalizeDouble(reference - config.trailingActivation * market.priceUnit, market.digits))
      {
         double level = NormalizeDouble(market.ask + config.trailingDistance * market.priceUnit, market.digits);
         if(level > 0.0 && level < stopLoss - market.point && (candidate == 0.0 || level < candidate)) candidate = level;
      }
   }
   return candidate;
}

void ApplyFallbackProtection(const ulong ticket, const ENUM_POSITION_TYPE side,
                             const double entry, const double reference, const double stopLoss,
                             const double takeProfit, const bool modified,
                             const StrategyConfig &config, const SymbolSnapshot &market)
{
   bool testing = MQLInfoInteger(MQL_TESTER) != 0;
   if(InpVSL == VIRTUAL_OFF)
   {
      if(testing) return;
      double buffer = MathMax(market.stopsDistance, market.freezeDistance) + 3.0 * market.point;
      if(buffer <= 10.0 * market.point) return;
      double candidate = BrokerFallbackCandidate(side, entry, reference, stopLoss, config, market);
      if(candidate <= 0.0) return;
      if(side == POSITION_TYPE_BUY)
      {
         candidate = MathMin(candidate, NormalizeDouble(market.bid - buffer, market.digits));
         if(candidate > stopLoss + market.point) ModifyProtection(ticket, candidate, takeProfit);
      }
      else
      {
         candidate = MathMax(candidate, NormalizeDouble(market.ask + buffer, market.digits));
         if(candidate < stopLoss - market.point) ModifyProtection(ticket, candidate, takeProfit);
      }
      return;
   }
   if(InpVSL < VIRTUAL_ON_FREEZE || InpVSL > VIRTUAL_OFF) return;
   bool forceVirtual = InpVSL == VIRTUAL_ALWAYS && !testing;
   bool brokerRestricted = market.stopsDistance > 10.0 * market.point || market.freezeDistance > 10.0 * market.point;
   if(!forceVirtual && (modified || !brokerRestricted)) return;
   double candidate = VirtualProtectionCandidate(ticket, side, entry, reference, stopLoss, config, market, true);
   if(candidate > 0.0) TightenVirtualStop(ticket, side, candidate);
}

datetime lastDeferredCloseMessage = 0;

void CloseCrossedStop(const ulong ticket)
{
   if(CloseManagedPosition(ticket))
   {
      Print("Closing with virtual SL | #", ticket);
      ClearVirtualTarget(ticket);
      return;
   }
   uint code = trade.ResultRetcode();
   bool retryable = code == TRADE_RETCODE_REQUOTE || code == TRADE_RETCODE_PRICE_CHANGED ||
                    code == TRADE_RETCODE_PRICE_OFF || code == TRADE_RETCODE_TOO_MANY_REQUESTS ||
                    code == TRADE_RETCODE_FROZEN;
   if(retryable)
   {
      datetime now = TimeCurrent();
      if(now == lastDeferredCloseMessage) return;
      lastDeferredCloseMessage = now;
      Print("Virtual SL close deferred (", trade.ResultRetcodeDescription(), ") #", ticket,
            " - price inside freeze band, will retry");
   }
   else Print("Virtual SL close failed: ", code, " (", trade.ResultRetcodeDescription(), ") #", ticket);
}

bool ManageStrategyPositions(const long magic, const ENUM_POSITION_TYPE side,
                             const StrategyConfig &config, const SymbolSnapshot &market)
{
   bool activity = false;
   for(int index = PositionsTotal() - 1; index >= 0; --index)
   {
      ulong ticket = PositionGetTicket(index);
      if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != market.symbol ||
         PositionGetInteger(POSITION_MAGIC) != magic ||
         (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != side)
         continue;
      double entry = NormalizeDouble(PositionGetDouble(POSITION_PRICE_OPEN), market.digits);
      double stopLoss = NormalizeDouble(PositionGetDouble(POSITION_SL), market.digits);
      double takeProfit = NormalizeDouble(PositionGetDouble(POSITION_TP), market.digits);
      datetime opened = (datetime)PositionGetInteger(POSITION_TIME);
      double direction = side == POSITION_TYPE_BUY ? 1.0 : -1.0;
      if(takeProfit == 0.0)
         takeProfit = NormalizeDouble(entry + direction * (1.0 + config.takeProfitExtraFraction) *
                                      config.takeProfitBaseDistance * market.priceUnit, market.digits);
      if(CheckFallbackStop(ticket, market.bid, market.ask)) return true;

      double quickThreshold = InpTrailMode == TRAIL_FIXED ? (double)InpFixedBE : 250.0;
      double quickLock = InpTrailMode == TRAIL_FIXED ? 0.0 : 10.0;
      double quickStop = NormalizeDouble(entry + direction * quickLock * market.point, market.digits);
      bool needsQuickStop;
      if(side == POSITION_TYPE_BUY)
         needsQuickStop = stopLoss < entry && market.bid >= entry + quickThreshold * market.point &&
                          quickStop > stopLoss + market.point && MathAbs(market.bid - quickStop) > market.freezeDistance;
      else
         needsQuickStop = (stopLoss > entry || stopLoss == 0.0) && market.ask <= entry - quickThreshold * market.point &&
                          (quickStop < stopLoss - market.point || stopLoss == 0.0) && MathAbs(market.ask - quickStop) > market.freezeDistance;
      if(needsQuickStop)
      {
         ModifyProtection(ticket, quickStop, takeProfit);
         continue;
      }

      bool modified = false;
      if(stopLoss == 0.0)
      {
         stopLoss = NormalizeDouble(entry - direction * config.stopDistance * market.priceUnit, market.digits);
         modified = ModifyProtection(ticket, stopLoss, takeProfit);
      }
      if(CheckFakeoutExits(ticket, opened, entry, side, market)) return true;
      double reference = EntryReferencePrice(ticket, entry);
      double hardStop = entry - config.stopDistance * market.priceUnit - market.spread;
      if(side == POSITION_TYPE_SELL)
         hardStop = entry + config.stopDistance * market.priceUnit + market.spread;
      double roundedHardStop = NormalizeDouble(hardStop, market.digits);
      bool improveHardStop = side == POSITION_TYPE_BUY ? stopLoss < roundedHardStop : roundedHardStop < stopLoss;
      if(improveHardStop)
      {
         stopLoss = roundedHardStop;
         ModifyProtection(ticket, stopLoss, takeProfit);
      }
      bool hardStopCrossed = side == POSITION_TYPE_BUY ? market.bid < hardStop : market.ask > hardStop;
      if(hardStopCrossed)
      {
         CloseManagedPosition(ticket);
         ClearVirtualTarget(ticket);
         return true;
      }
      if(CheckVirtualTarget(ticket, entry, side, market)) return true;
      if(side == POSITION_TYPE_BUY)
      {
         if(ApplyBuyProtection(ticket, entry, reference, stopLoss, takeProfit, config, market)) modified = true;
      }
      else
      {
         if(ApplySellProtection(ticket, entry, reference, stopLoss, takeProfit, config, market)) modified = true;
      }
      ApplyFallbackProtection(ticket, side, entry, reference, stopLoss, takeProfit, modified, config, market);
      bool stopCrossed = side == POSITION_TYPE_BUY ? stopLoss > market.bid : stopLoss < market.ask;
      if(stopCrossed)
      {
         CloseCrossedStop(ticket);
         return true;
      }
      if(modified) activity = true;
   }
   return activity;
}

struct DailyRiskState
{
   int dailyBars;
   double openingBalance;
   double peakProfit;
   bool paused;
};
DailyRiskState dailyRisk;

void CloseDailyRiskPositions(const string symbol)
{
   long fillingFlags = 0;
   bool fillingKnown = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE, fillingFlags);
   ENUM_ORDER_TYPE_FILLING filling = ORDER_FILLING_FOK;
   bool retryWithIoc = true;
   if(fillingKnown)
   {
      if((fillingFlags & SYMBOL_FILLING_IOC) != 0)
      {
         filling = ORDER_FILLING_IOC;
         retryWithIoc = false;
      }
      else if((fillingFlags & SYMBOL_FILLING_FOK) != 0) filling = ORDER_FILLING_FOK;
      else
      {
         filling = ORDER_FILLING_RETURN;
         retryWithIoc = false;
      }
   }
   for(int i = PositionsTotal() - 1; i >= 0; --i)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !IsSelectedOwnedPosition(symbol)) continue;
      ENUM_POSITION_TYPE side = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      MqlTradeRequest request = {};
      MqlTradeResult result = {};
      request.action = TRADE_ACTION_DEAL;
      request.position = ticket;
      request.symbol = symbol;
      request.volume = PositionGetDouble(POSITION_VOLUME);
      request.deviation = 5000;
      request.type_filling = filling;
      request.type = side == POSITION_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
      request.price = SymbolInfoDouble(symbol, side == POSITION_TYPE_BUY ? SYMBOL_BID : SYMBOL_ASK);
      if(OrderSend(request, result))
      {
         Print("DD KILL | Position closed #", ticket, " | ", EnumToString(side), " ",
               DoubleToString(request.volume, 2), " lots");
         continue;
      }
      Print("DD KILL | PositionClose FAILED #", ticket, " | retcode: ", result.retcode,
            " | error: ", GetLastError(), " | fill: ", EnumToString(filling));
      if(!retryWithIoc) continue;
      request.type_filling = ORDER_FILLING_IOC;
      request.price = SymbolInfoDouble(symbol, side == POSITION_TYPE_BUY ? SYMBOL_BID : SYMBOL_ASK);
      if(OrderSend(request, result)) Print("DD KILL | PositionClose RETRY OK #", ticket);
      else Print("DD KILL | PositionClose RETRY FAILED #", ticket, " | retcode: ", result.retcode,
                 " | error: ", GetLastError());
   }
   for(int i = OrdersTotal() - 1; i >= 0; --i)
   {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0 || !IsSelectedOwnedOrder(symbol)) continue;
      ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if(type != ORDER_TYPE_BUY_LIMIT && type != ORDER_TYPE_SELL_LIMIT &&
         type != ORDER_TYPE_BUY_STOP && type != ORDER_TYPE_SELL_STOP) continue;
      MqlTradeRequest request = {};
      MqlTradeResult result = {};
      request.action = TRADE_ACTION_REMOVE;
      request.order = ticket;
      if(OrderSend(request, result)) Print("DD KILL | Pending deleted #", ticket);
      else Print("DD KILL | OrderDelete FAILED #", ticket, " | retcode: ", result.retcode,
                 " | error: ", GetLastError());
   }
}

bool UpdateDailyRisk(const string symbol)
{
   int bars = Bars(symbol, PERIOD_D1);
   if(dailyRisk.dailyBars != bars)
   {
      dailyRisk.dailyBars = bars;
      dailyRisk.openingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      dailyRisk.peakProfit = 0.0;
      dailyRisk.paused = false;
   }
   if(dailyRisk.openingBalance <= 0.0)
      dailyRisk.openingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   if(InpMaxDailyDD <= 0.0)
      return dailyRisk.paused;

   double profit = 0.0;
   if(HistorySelect(iTime(symbol, PERIOD_D1, 0), TimeCurrent()))
   {
      for(int i = 0; i < HistoryDealsTotal(); ++i)
      {
         ulong deal = HistoryDealGetTicket(i);
         if(deal == 0 || HistoryDealGetString(deal, DEAL_SYMBOL) != symbol ||
            !IsOwnedMagic(HistoryDealGetInteger(deal, DEAL_MAGIC)))
            continue;
         ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal, DEAL_ENTRY);
         if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT)
            profit += HistoryDealGetDouble(deal, DEAL_PROFIT) +
                      HistoryDealGetDouble(deal, DEAL_SWAP) + HistoryDealGetDouble(deal, DEAL_COMMISSION);
      }
   }
   for(int i = PositionsTotal() - 1; i >= 0; --i)
   {
      if(PositionGetTicket(i) != 0 && IsSelectedOwnedPosition(symbol))
         profit += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
   }
   dailyRisk.peakProfit = MathMax(dailyRisk.peakProfit, profit);
   double threshold = InpMaxDailyDD;
   if(InpMaxDDUnit != DAILY_LOSS_MONEY)
      threshold = dailyRisk.openingBalance * InpMaxDailyDD / 100.0;
   if(profit < 0.0 && -profit > threshold)
   {
      if(!dailyRisk.paused)
         Print("DD KILL | Max Daily DD reached (", DoubleToString(InpMaxDailyDD, 1),
               InpMaxDDUnit == DAILY_LOSS_MONEY ? "$" : "%", ") | Daily PnL: ", DoubleToString(profit, 2),
               " | Closing EA positions + pendings only");
      CloseDailyRiskPositions(symbol);
      dailyRisk.paused = true;
      dailyRisk.peakProfit = 0.0;
   }
   return dailyRisk.paused;
}

struct NewsSettings
{
   int brokerUtcOffsetHours;
   int minutesAfter;
   bool blockHighImpact;
   bool blockHolidays;
};

struct NewsEvent
{
   datetime time;
   string title;
   int impact;
   bool blocksTrading;
};

class NewsCalendar
{
private:
   string m_text;
   datetime m_downloadedAt;
   datetime m_nextAttempt;
   datetime m_lastParse;
   datetime m_lastWarning;
   NewsEvent m_events[64];
   int m_count;
   bool m_loaded;
   bool m_holiday;
   bool m_webRequestForbidden;
   int m_successfulFetches;

   string Trim(string value)
   {
      StringTrimLeft(value);
      StringTrimRight(value);
      return value;
   }

   string Field(const string xml,const string name)
   {
      const string opening="<"+name+">";
      const string closing="</"+name+">";
      int first=StringFind(xml,opening);
      if(first<0) return "";
      first+=StringLen(opening);
      const int last=StringFind(xml,closing,first);
      if(last<0) return "";
      string value=StringSubstr(xml,first,last-first);
      StringReplace(value,"<![CDATA[","");
      StringReplace(value,"]]>","");
      StringReplace(value,"&amp;","&");
      return Trim(value);
   }

   bool ReadTime(string text,int &hour,int &minute)
   {
      StringToLower(text);
      const bool pm=StringFind(text,"pm")>=0;
      const bool am=StringFind(text,"am")>=0;
      if(!pm && !am) return false;
      StringReplace(text,"am","");
      StringReplace(text,"pm","");
      text=Trim(text);
      const int colon=StringFind(text,":");
      if(colon<0) return false;
      hour=(int)StringToInteger(StringSubstr(text,0,colon));
      minute=(int)StringToInteger(StringSubstr(text,colon+1));
      if(pm && hour<12) hour+=12;
      if(am && hour==12) hour=0;
      return hour>=0 && hour<24 && minute>=0 && minute<60;
   }

   datetime ReadDateTime(const string date,const int hour,const int minute,
                         const int offsetHours)
   {
      if(StringLen(date)<10) return 0;
      const int month=(int)StringToInteger(StringSubstr(date,0,2));
      const int day=(int)StringToInteger(StringSubstr(date,3,2));
      const int year=(int)StringToInteger(StringSubstr(date,6,4));
      if(month<1 || month>12 || day<1 || day>31 || year<2000) return 0;
      const datetime utc=StringToTime(StringFormat("%04d.%02d.%02d %02d:%02d",
                                                 year,month,day,hour,minute));
      if(utc<=0) return 0;
      return (datetime)((long)utc+(long)offsetHours*3600);
   }

   bool Fetch(const string url,string &text,int &httpStatus)
   {
      char request[],response[];
      string responseHeaders;
      ResetLastError();
      httpStatus=WebRequest("GET",url,"User-Agent: Mozilla/5.0\r\n",8000,
                            request,response,responseHeaders);
      if(httpStatus==-1)
      {
         m_webRequestForbidden=GetLastError()==4014;
         return false;
      }
      m_webRequestForbidden=false;
      if(httpStatus!=200) return false;
      text=CharArrayToString(response,0,-1,CP_UTF8);
      if(StringFind(text,"<event>")<0) return false;
      m_successfulFetches++;
      return true;
   }

   void ReadCache()
   {
      const string filename="LZ_FFCal.xml";
      if(!FileIsExist(filename,FILE_COMMON)) return;
      const int file=FileOpen(filename,FILE_READ|FILE_BIN|FILE_COMMON);
      if(file==INVALID_HANDLE) return;
      const int size=(int)FileSize(file);
      if(size<1)
      {
         FileClose(file);
         return;
      }
      uchar bytes[];
      ArrayResize(bytes,size);
      FileReadArray(file,bytes,0,size);
      FileClose(file);
      const string text=CharArrayToString(bytes,0,size,CP_UTF8);
      if(StringLen(text)<=20 || StringFind(text,"LZFF|")!=0) return;
      const int newline=StringFind(text,"\n");
      if(newline<0) return;
      const datetime timestamp=(datetime)StringToInteger(StringSubstr(text,5,newline-5));
      if(timestamp>m_downloadedAt)
      {
         m_text=text;
         m_downloadedAt=timestamp;
      }
   }

   bool WriteCache(const string text)
   {
      const int file=FileOpen("LZ_FFCal.xml",FILE_WRITE|FILE_BIN|FILE_COMMON);
      if(file==INVALID_HANDLE) return false;
      uchar bytes[];
      int size=StringToCharArray(text,bytes,0,-1,CP_UTF8);
      if(size>0) size--;
      if(size>0) FileWriteArray(file,bytes,0,size);
      FileClose(file);
      return size>0;
   }

   void Refresh(const datetime now,const int offsetHours)
   {
      MqlDateTime date;
      TimeToStruct(now,date);
      date.hour=1;
      date.min=0;
      date.sec=0;
      datetime boundary=StructToTime(date);
      if(now<boundary) boundary-=86400;
      boundary=(datetime)((long)boundary-(long)offsetHours*3600);
      if(m_downloadedAt>=boundary && StringLen(m_text)>20) return;
      ReadCache();
      if(m_downloadedAt>=boundary || now<m_nextAttempt) return;
      m_nextAttempt=now+1800;
      const string lockName="LZ_FFCAL_LOCK";
      const double lockTime=GlobalVariableGet(lockName);
      if(lockTime>0.0 && (long)TimeGMT()-(long)lockTime<=119) return;
      GlobalVariableSet(lockName,(double)TimeGMT());
      string thisWeek="",nextWeek="";
      int status=0;
      if(!Fetch("",thisWeek,status))
      {
         GlobalVariableSet(lockName,0.0);
         Print("ForexFactory download failed (HTTP ",status,", error ",
               GetLastError(),"). Keeping the previous calendar, next attempt in 30 min.");
         return;
      }
      Sleep(1000);
      const bool nextWeekAvailable=Fetch(
         "",nextWeek,status);
      GlobalVariableSet(lockName,0.0);
      string combined="LZFF|"+IntegerToString((long)TimeGMT())+"\n"+thisWeek;
      if(nextWeekAvailable) combined+=nextWeek;
      if(!WriteCache(combined)) return;
      m_text=combined;
      m_downloadedAt=TimeGMT();
      Print("ForexFactory calendar updated (thisweek",
            nextWeekAvailable ? " + nextweek" : " only, nextweek failed",").");
   }

   void InsertEvent(const datetime time,const string title,const int impact,
                    const bool blocksTrading)
   {
      int index=m_count;
      while(index>0 && m_events[index-1].time>time)
      {
         m_events[index]=m_events[index-1];
         index--;
      }
      m_events[index].time=time;
      m_events[index].title=title;
      m_events[index].impact=impact;
      m_events[index].blocksTrading=blocksTrading;
      m_count++;
   }

   void Parse(const datetime now,const NewsSettings &settings)
   {
      m_count=0;
      m_holiday=false;
      m_loaded=false;
      i

Показано первые 50% кода (символов: 104837)

Открыть по тарифу

Купите тариф с пакетом открытий или войдите, если тариф уже есть. Открытый файл останется в вашем кабинете навсегда.

Разовая покупка файла

На этой странице откроется полный исходный код файла вместо ознакомительного фрагмента.

Чтобы оформить заказ, введите email и перейдите к оплате.

Стоимость полного кода
10 483,70₽ ≈ $129.16