English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами

Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами

MetaTrader 5Трейдинг |
45 14
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 23) мы усовершенствовали систему Zone Recovery для торговли по тренду с использованием индикатора Envelopes, написанную на MQL5, добавив трейлинг-стопы и несколько корзин для лучшей защиты прибыли и обработки сигналов. В части 24 мы разрабатываем систему на пробое лондонской сессии, которая определяет диапазоны до открытия сессии, размещает отложенные ордера и содержит инструменты управления рисками — соотношение риск/прибыль, ограничения просадки и панель управления для мониторинга в реальном времени. В статье рассмотрим следующие темы:

  1. Понимание стратегии пробоя лондонской сессии
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

В итоге вы получите полноценную программу на MQL5 с продвинутыми механизмами контроля рисков, готовую к тестированию и доработке. Приступим!


Понимание стратегии пробоя лондонской сессии

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

Для такой защиты мы будем рассчитывать максимум и минимум до лондонской сессии для установки ордеров buy stop и sell stop с отступами, добавляя соотношение риск/прибыль для тейк-профитов, трейлинг-стопы для фиксации прибыли и ограничения на количество открытых сделок и дневную просадку для защиты капитала. Также будем использовать панель управления для мониторинга в реальном времени и проверок сессии, чтобы сделки совершались только в заданных диапазонах, а сама система при этом будет адаптивной к различным рыночным условиям. Ниже представлена концепция системы, которую мы будем реализовывать.

СТРУКТУРА СТРАТЕГИИ


Реализация средствами MQL5

Чтобы создать программу в MQL5, откройте MetaEditor, перейдите в Navigator, найдите папку Indicators, нажмите вкладку New и следуйте инструкциям для создания файла. После этого в среде разработки начнем с объявления входных параметров и структур.

//+------------------------------------------------------------------+
//|                                        London Breakout EA.mq5    |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh> //--- Include Trade library for trading operations

//--- Enumerations
enum ENUM_TRADE_TYPE {     //--- Enumeration for trade types
   TRADE_ALL,              // All Trades (Buy and Sell)
   TRADE_BUY_ONLY,         // Buy Trades Only
   TRADE_SELL_ONLY         // Sell Trades Only
};

//--- Input parameters
sinput group "General EA Settings"
input double inpTradeLotsize = 0.01; // Lotsize
input ENUM_TRADE_TYPE TradeType = TRADE_ALL; // Trade Type Selection
sinput int MagicNumber = 12345;     // Magic Number
input double RRRatio = 1.0;        // Risk to Reward Ratio
input int StopLossPoints = 500;    // Stop loss in points
input int OrderOffsetPoints = 10;   // Points offset for Orders
input bool DeleteOppositeOrder = true; // Delete opposite order when one is activated?
input bool UseTrailing = false;    // Use Trailing Stop?
input int TrailingPoints = 50;     // Trailing Points (distance)
input int MinProfitPoints = 100;   // Minimum Profit Points to start trailing

sinput group "London Session Settings"
input int LondonStartHour = 9;        // London Start Hour
input int LondonStartMinute = 0;      // London Start Minute
input int LondonEndHour = 8;          // London End Hour
input int LondonEndMinute = 0;        // London End Minute
input int MinRangePoints = 100;       // Min Pre-London Range in points
input int MaxRangePoints = 300;       // Max Pre-London Range in points

sinput group "Risk Management"
input int MaxOpenTrades = 2;       // Maximum simultaneous open trades
input double MaxDailyDrawdownPercent = 5.0; // Max daily drawdown % to stop trading

//--- Structures
struct PositionInfo {      //--- Structure for position information
   ulong ticket;           // Position ticket
   double openPrice;      // Entry price
   double londonRange;    // Pre-London range in points for this position
   datetime sessionID;    // Session identifier (day)
   bool trailingActive;   // Trailing active flag
};

Мы начинаем реализацию системы пробоя лондонской сессии с подключения библиотеки <Trade\Trade.mqh> и определения перечислений, входных параметров и структуры для отслеживания позиций. Библиотека <Trade\Trade.mqh> используется для доступа к классу CTrade, который позволяет выполнять торговые операции — выставлять ордера и изменять позиции. Определим перечисление ENUM_TRADE_TYPE с вариантами TRADE_ALL для торговли в обоих направлениях, TRADE_BUY_ONLY только для покупок и TRADE_SELL_ONLY только для продаж, что позволяет ограничивать направления торговли.

Затем задаем входные параметры по группам: в разделе общих настроек параметр inpTradeLotsize установлен на 0.01 для размера лота, TradeType использует перечисление со значением по умолчанию TRADE_ALL, MagicNumber равен 12345 для идентификации сделок советника, соотношения риск/прибыль RRRatio установлено на 1.0, расстояние стоп-лосса StopLossPoints равно 500, смещение входа OrderOffsetPoints = 10, DeleteOppositeOrder установлен в true для удаления противоположных отложенных ордеров, для использования трейлинг-стопа параметр UseTrailing установлен в false, расстояние TrailingPoints равно 50 и начало трейлинга MinProfitPoints равно 100.

В разделе London Session Settings задается начало сессии через параметры LondonStartHour = 9 и LondonStartMinute = 0, окончание сессии через LondonEndHour = 8 и LondonEndMinute = 0. Также для проверки диапазона используются параметры MinRangePoints = 100 и MaxRangePoints = 300. В разделе Risk Management параметр MaxOpenTrades установлен в значение 2 для ограничения количества одновременных позиций, а MaxDailyDrawdownPercent равен 5.0 для остановки торговли при чрезмерной просадке. Определяем структуру PositionInfo для отслеживания открытых сделок, где ticket — это тикет позиции, openPrice — цена открытия, londonRange — диапазон до лондонской сессии, sessionID — идентификатор дня, а trailingActive — логическое значение состояния трейлинга. После компиляции получаем следующий результат.

Набор входных параметров

Теперь можно определить дополнительные глобальные переменные, которые будут использоваться в программе.

//--- Global variables
CTrade obj_Trade;                 //--- Trade object
double PreLondonHigh = 0.0;       //--- Pre-London session high
double PreLondonLow = 0.0;        //--- Pre-London session low
datetime PreLondonHighTime = 0;   //--- Time of Pre-London high
datetime PreLondonLowTime = 0;    //--- Time of Pre-London low
ulong buyOrderTicket = 0;         //--- Buy stop order ticket
ulong sellOrderTicket = 0;        //--- Sell stop order ticket
bool panelVisible = true;         //--- Panel visibility flag
double LondonRangePoints = 0.0;   //--- Current session's Pre-London range
PositionInfo positionList[];      //--- Array to store position info
datetime lastCheckedDay = 0;      //--- Last checked day
bool noTradeToday = false;        //--- Flag to prevent trading today
bool sessionChecksDone = false;   //--- Flag for session checks completion
datetime analysisTime = 0;        //--- Time for London analysis
double dailyDrawdown = 0.0;       //--- Current daily drawdown
bool isTrailing = false;          //--- Global flag for any trailing active
const int PreLondonStartHour = 3; //--- Fixed Pre-London Start Hour
const int PreLondonStartMinute = 0; //--- Fixed Pre-London Start Minute

Здесь мы объявляем глобальные переменные программы: obj_Trade как объект CTrade для торговли, PreLondonHigh и PreLondonLow типа double для диапазонов, PreLondonHighTime и PreLondonLowTime типа datetime для хранения времени формирования этих экстремумов, buyOrderTicket и sellOrderTicket типа ulong для ордеров, panelVisible со значением true для отображения панели, LondonRangePoints равный 0.0 для текущего диапазона, positionList как массив PositionInfo для позиций, lastCheckedDay равный 0 для ежедневного отслеживания, noTradeToday и sessionChecksDone со значением false как флаги торговли, analysisTime равный 0 для времени анализа, dailyDrawdown равный 0.0 для риска, isTrailing со значением false для трейлинга, а также константы PreLondonStartHour = 3 и PreLondonStartMinute - 0.

После этого приступим к созданию панели, что является самым простым этапом, а затем перейдем к более сложной торговой логике. Начнем с необходимых функций для создания панели.

//+------------------------------------------------------------------+
//| Create a rectangle label for the panel background                |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xD, int yD, int xS, int yS,
                    color clrBg, int widthBorder, color clrBorder = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT, ENUM_LINE_STYLE borderStyle = STYLE_SOLID) {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
        Print(__FUNCTION__, ": failed to create rec label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS); //--- Set x-size
    ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS); //--- Set y-size
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg); //--- Set background color
    ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
    ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle); //--- Set border style
    ObjectSetInteger(0, objName, OBJPROP_WIDTH, widthBorder); //--- Set border width
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBorder); //--- Set border color
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create a text label for panel elements                           |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xD, int yD,
                 string txt, color clrTxt = clrBlack, int fontSize = 10,
                 string font = "Arial") {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label
        Print(__FUNCTION__, ": failed to create the label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set color
    ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
    ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

Здесь мы реализуем вспомогательные функции для создания элементов пользовательского интерфейса панели управления с использованием прямоугольных меток для фона и текстовых меток для отображения. Сначала создаем функцию createRecLabel для создания прямоугольных меток фона панели, в которую передаем необходимые параметры. Сбрасываем ошибки с помощью функции ResetLastError и создаем с помощью ObjectCreate объект типа OBJ_RECTANGLE_LABEL, выводя ошибки через Print и возвращая false в случае неудачи. Свойства задаем с помощью ObjectSetInteger для OBJPROP_XDISTANCE и аналогично для остальных целочисленных свойств, затем выполняется перерисовка графика через ChartRedraw и возвращается true.

Далее создаем функцию createLabel для текстовых меток панели с параметрами objName, xD, yD, txt, clrTxt, fontSize и font. Сбрасываем ошибки с помощью ResetLastError и создаем через ObjectCreate объект типа OBJ_LABEL, при этом выводим ошибки через Print и возвращаем false в случае неудачи. Свойства задаются с помощью ObjectSetInteger аналогично функции прямоугольной метки, но дополнительно используется ObjectSetString для свойств OBJPROP_TEXT и OBJPROP_FONT, после чего выполняется перерисовка и возвращается true. Эти функции позволят создать динамическую панель управления для мониторинга данных сессии и состояния программы. Теперь можно использовать их для создания и обновления панели.

string panelPrefix = "LondonPanel_"; //--- Prefix for panel objects

//+------------------------------------------------------------------+
//| Create the information panel                                     |
//+------------------------------------------------------------------+
void CreatePanel() {
   createRecLabel(panelPrefix + "Background", 10, 10, 270, 200, clrMidnightBlue, 1, clrSilver); //--- Create background
   createLabel(panelPrefix + "Title", 20, 15, "London Breakout Control Center", clrGold, 12); //--- Create title
   createLabel(panelPrefix + "RangePoints", 20, 40, "Range (points): ", clrWhite, 10); //--- Create range label
   createLabel(panelPrefix + "HighPrice", 20, 60, "High Price: ", clrWhite); //--- Create high price label
   createLabel(panelPrefix + "LowPrice", 20, 80, "Low Price: ", clrWhite); //--- Create low price label
   createLabel(panelPrefix + "BuyLevel", 20, 100, "Buy Level: ", clrWhite); //--- Create buy level label
   createLabel(panelPrefix + "SellLevel", 20, 120, "Sell Level: ", clrWhite); //--- Create sell level label
   createLabel(panelPrefix + "AccountBalance", 20, 140, "Balance: ", clrWhite); //--- Create balance label
   createLabel(panelPrefix + "AccountEquity", 20, 160, "Equity: ", clrWhite); //--- Create equity label
   createLabel(panelPrefix + "CurrentDrawdown", 20, 180, "Drawdown (%): ", clrWhite); //--- Create drawdown label
   createRecLabel(panelPrefix + "Hide", 250, 10, 30, 22, clrCrimson, 1, clrNONE); //--- Create hide button
   createLabel(panelPrefix + "HideText", 258, 12, CharToString(251), clrWhite, 13, "Wingdings"); //--- Create hide text
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_SELECTABLE, true); //--- Make hide selectable
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_STATE, true); //--- Set hide state
}

//+------------------------------------------------------------------+
//| Update panel with current data                                   |
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Format range text
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Update range text
   
   string highText = "High Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh, _Digits) : "N/A"); //--- Format high text
   ObjectSetString(0, panelPrefix + "HighPrice", OBJPROP_TEXT, highText); //--- Update high text
   
   string lowText = "Low Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow, _Digits) : "N/A"); //--- Format low text
   ObjectSetString(0, panelPrefix + "LowPrice", OBJPROP_TEXT, lowText); //--- Update low text
   
   string buyText = "Buy Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh + OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format buy text
   ObjectSetString(0, panelPrefix + "BuyLevel", OBJPROP_TEXT, buyText); //--- Update buy text
   
   string sellText = "Sell Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow - OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format sell text
   ObjectSetString(0, panelPrefix + "SellLevel", OBJPROP_TEXT, sellText); //--- Update sell text
   
   string balanceText = "Balance: " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2); //--- Format balance text
   ObjectSetString(0, panelPrefix + "AccountBalance", OBJPROP_TEXT, balanceText); //--- Update balance text
   
   string equityText = "Equity: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); //--- Format equity text
   ObjectSetString(0, panelPrefix + "AccountEquity", OBJPROP_TEXT, equityText); //--- Update equity text
   
   string ddText = "Drawdown (%): " + DoubleToString(dailyDrawdown, 2); //--- Format drawdown text
   ObjectSetString(0, panelPrefix + "CurrentDrawdown", OBJPROP_TEXT, ddText); //--- Update drawdown text
   ObjectSetInteger(0, panelPrefix + "CurrentDrawdown", OBJPROP_COLOR, dailyDrawdown > MaxDailyDrawdownPercent / 2 ? clrYellow : clrWhite); //--- Set drawdown color
}

Здесь мы определяем строку panelPrefix со значением LondonPanel_ для добавления префикса ко всем именам объектов панели, обеспечивая удобную идентификацию элементов панели управления. Создаем функцию CreatePanel для построения пользовательского интерфейса информационной панели. Вызываем createRecLabel для panelPrefix + Background, чтобы создать фон панели в позиции 10,10 размером 270x200 с цветом clrMidnightBlue, шириной 1 и серебряной рамкой. Затем используем createLabel для добавления заголовка London Breakout Control Center в позиции 20,15 золотым цветом и размером 12, а также меток для диапазона, максимальной цены, минимальной цены, уровня покупки, уровня продажи, баланса, средств (equity) и просадки в соответствующих позициях белым цветом и размером 10.

Для кнопки скрытия вызываем createRecLabel для panelPrefix + Hide в позиции 250,10 размером 30x22 с фоном clrCrimson и createLabel для panelPrefix + HideText с использованием CharToString(251) из набора Wingdings в позиции 258,12 с цветом clrWhite и размером 13. Мы устанавливаем свойства OBJPROP_SELECTABLE и OBJPROP_STATE в true с помощью ObjectSetInteger, чтобы сделать кнопку интерактивной. Выбор кода Wingdings зависит от ваших эстетических предпочтений. Ниже приведен список доступных кодов.

КОДЫ WINGDINGS

Далее реализуем функцию UpdatePanel для обновления панели текущими данными. Форматируем rangeText с использованиемLondonRangePoints через DoubleToString или выводим Calculating..., если значение равно нулю, и обновляем текст panelPrefix + RangePoints с помощью ObjectSetString. Аналогично форматируем и обновляем значения для максимальной цены, минимальной цены, уровня покупки (добавляя OrderOffsetPoints * _Point к PreLondonHigh), уровня продажи (вычитая OrderOffsetPoints * _Point из PreLondonLow), баланса через AccountInfoDouble(ACCOUNT_BALANCE), средств через AccountInfoDouble(ACCOUNT_EQUITY) и просадки через dailyDrawdown.

Цвет просадки задается через ObjectSetInteger: желтый, если дневная просадка превышает MaxDailyDrawdownPercent / 2, иначе белый. Чтобы сделать функции рабочими, мы вызываем их в функции инициализации следующим образом.

//+------------------------------------------------------------------+
//| Initialize EA                                                    |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Set magic number
   ArrayFree(positionList);           //--- Free position list
   CreatePanel();                     //--- Create panel
   panelVisible = true;               //--- Set panel visible
   return(INIT_SUCCEEDED);            //--- Return success
}

//+------------------------------------------------------------------+
//| Deinitialize EA                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
   ArrayFree(positionList);           //--- Free position list
}

В обработчике события OnInit мы устанавливаем magic number через торговый объект, используем функцию ArrayFree для очистки списка позиций, вызываем функцию CreatePanel для создания панели и устанавливаем флаг видимости панели в true после ее создания. Затем в обработчике события OnDeinit мы используем функцию ObjectsDeleteAll для удаления всех объектов с указанным префиксом и освобождаем массив списка позиций, так как он больше не нужен. После компиляции получаем следующий результат.

НАЧАЛЬНАЯ ПАНЕЛЬ

Поскольку панель уже создана, добавим ей интерактивность, сделав кнопку закрытия активной — при нажатии панель будет удаляться. Это реализуем в обработчике события OnChartEvent.

//+------------------------------------------------------------------+
//| Handle chart events (e.g., panel close)                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == panelPrefix + "Hide") { //--- Check hide click
      panelVisible = false;           //--- Set panel hidden
      ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
      ChartRedraw(0);                 //--- Redraw chart
   }
}

В обработчике OnChartEvent мы проверяем, что идентификатор события соответствует клику по объекту и что объект является кнопкой скрытия или закрытия, после чего устанавливаем флаг видимости в false. Затем удаляем объекты панели и перерисовываем график, чтобы изменения вступили в силу. После компиляции получаем следующий результат.

Закрытие панели

На анимации видно, что панель полностью готова. Теперь обновим ее, чтобы она инициализировала все элементы. Для этого нам понадобятся функции для задания дневных диапазонов и расчета просадки.

//+------------------------------------------------------------------+
//| Check if it's a new trading day                                  |
//+------------------------------------------------------------------+
bool IsNewDay(datetime currentBarTime) {
   MqlDateTime barTime;               //--- Bar time structure
   TimeToStruct(currentBarTime, barTime); //--- Convert time
   datetime currentDay = StringToTime(StringFormat("%04d.%02d.%02d", barTime.year, barTime.mon, barTime.day)); //--- Get current day
   if (currentDay != lastCheckedDay) { //--- Check new day
      lastCheckedDay = currentDay;    //--- Update last day
      sessionChecksDone = false;      //--- Reset checks
      noTradeToday = false;           //--- Reset no trade
      buyOrderTicket = 0;             //--- Reset buy ticket
      sellOrderTicket = 0;            //--- Reset sell ticket
      LondonRangePoints = 0.0;        //--- Reset range
      return true;                    //--- Return new day
   }
   return false;                      //--- Return not new day
}

//+------------------------------------------------------------------+
//| Update daily drawdown                                            |
//+------------------------------------------------------------------+
void UpdateDailyDrawdown() {
   static double maxEquity = 0.0;     //--- Max equity tracker
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
   if (equity > maxEquity) maxEquity = equity; //--- Update max equity
   dailyDrawdown = (maxEquity - equity) / maxEquity * 100; //--- Calculate drawdown
   if (dailyDrawdown >= MaxDailyDrawdownPercent) noTradeToday = true; //--- Set no trade if exceeded
}

Здесь мы реализуем функцию IsNewDay для проверки наступления нового торгового дня. Создаем структуру MqlDateTime barTime и преобразуем в нее currentBarTime с помощью функции TimeToStruct. Вычисляем currentDay с использованием StringToTime и StringFormat на основе barTime.year, barTime.mon и barTime.day. Если currentDay отличается от lastCheckedDay, мы обновляем lastCheckedDay, сбрасываем sessionChecksDone и noTradeToday в false, обнуляем buyOrderTicket и sellOrderTicket, устанавливаем LondonRangePoints в 0.0 и возвращаем true; в противном случае возвращаем false. Эта функция обеспечивает ежедневный сброс параметров анализа сессии и торговых флагов.

Далее реализуем функцию UpdateDailyDrawdown для мониторинга дневного риска. Используем статическую переменную maxEquity, инициализированную значением 0.0, для отслеживания максимального значения средств (equity). Получаем текущее значение с помощью функции AccountInfoDouble с параметром ACCOUNT_EQUITY, обновляем maxEquity, если значение выше, и рассчитываем dailyDrawdown как процентное снижение от maxEquity. Если dailyDrawdown достигает или превышает MaxDailyDrawdownPercent, устанавливаем noTradeToday в true — это остановит торговлю, чтобы защитить от просадки. Чтобы использовать эти функции, вызываем их в обработчике OnTick для обновления данных панели.

//+------------------------------------------------------------------+
//| Main tick handler                                                |
//+------------------------------------------------------------------+
void OnTick() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   IsNewDay(currentBarTime);          //--- Check new day
   
   UpdatePanel();                     //--- Update panel
   UpdateDailyDrawdown();             //--- Update drawdown
}

Здесь мы просто вызываем функцию нового дня для установки параметров диапазона, обновления панели и уровня дневной просадки соответственно. При запуске программы получаем следующий результат.

ИНИЦИАЛИЗИРОВАННАЯ ПАНЕЛЬ

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

//+------------------------------------------------------------------+
//| Fixed lot size                                                   |
//+------------------------------------------------------------------+
double CalculateLotSize(double entryPrice, double stopLossPrice) {
   return NormalizeDouble(inpTradeLotsize, 2); //--- Normalize lot size
}

//+------------------------------------------------------------------+
//| Calculate session range (high-low) in points                     |
//+------------------------------------------------------------------+
double GetRange(datetime startTime, datetime endTime, double &highVal, double &lowVal, datetime &highTime, datetime &lowTime) {
   int startBar = iBarShift(_Symbol, _Period, startTime, true); //--- Get start bar
   int endBar = iBarShift(_Symbol, _Period, endTime, true); //--- Get end bar
   if (startBar == -1 || endBar == -1 || startBar < endBar) return -1; //--- Invalid bars

   int highestBar = iHighest(_Symbol, _Period, MODE_HIGH, startBar - endBar + 1, endBar); //--- Get highest bar
   int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, startBar - endBar + 1, endBar); //--- Get lowest bar
   highVal = iHigh(_Symbol, _Period, highestBar); //--- Set high value
   lowVal = iLow(_Symbol, _Period, lowestBar); //--- Set low value
   highTime = iTime(_Symbol, _Period, highestBar); //--- Set high time
   lowTime = iTime(_Symbol, _Period, lowestBar); //--- Set low time
   return (highVal - lowVal) / _Point; //--- Return range in points
}

//+------------------------------------------------------------------+
//| Place pending buy/sell stop orders                               |
//+------------------------------------------------------------------+
void PlacePendingOrders(double preLondonHigh, double preLondonLow, datetime sessionID) {
   double buyPrice = preLondonHigh + OrderOffsetPoints * _Point; //--- Calculate buy price
   double sellPrice = preLondonLow - OrderOffsetPoints * _Point; //--- Calculate sell price
   double slPoints = StopLossPoints; //--- Set SL points
   double buySL = buyPrice - slPoints * _Point; //--- Calculate buy SL
   double sellSL = sellPrice + slPoints * _Point; //--- Calculate sell SL
   double tpPoints = slPoints * RRRatio; //--- Calculate TP points
   double buyTP = buyPrice + tpPoints * _Point; //--- Calculate buy TP
   double sellTP = sellPrice - tpPoints * _Point; //--- Calculate sell TP
   double lotSizeBuy = CalculateLotSize(buyPrice, buySL); //--- Calculate buy lot
   double lotSizeSell = CalculateLotSize(sellPrice, sellSL); //--- Calculate sell lot

   if (TradeType == TRADE_ALL || TradeType == TRADE_BUY_ONLY) { //--- Check buy trade
      obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, 0, 0, "Buy Stop - London"); //--- Place buy stop
      buyOrderTicket = obj_Trade.ResultOrder(); //--- Get buy ticket
   }

   if (TradeType == TRADE_ALL || TradeType == TRADE_SELL_ONLY) { //--- Check sell trade
      obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, 0, 0, "Sell Stop - London"); //--- Place sell stop
      sellOrderTicket = obj_Trade.ResultOrder(); //--- Get sell ticket
   }
}

//+------------------------------------------------------------------+
//| Draw session ranges on the chart                                 |
//+------------------------------------------------------------------+
void DrawSessionRanges(datetime preLondonStart, datetime londonEnd) {
   string sessionID = "Sess_" + IntegerToString(lastCheckedDay); //--- Session ID

   string preRectName = "PreRect_" + sessionID; //--- Rectangle name
   ObjectCreate(0, preRectName, OBJ_RECTANGLE, 0, PreLondonHighTime, PreLondonHigh, PreLondonLowTime, PreLondonLow); //--- Create rectangle
   ObjectSetInteger(0, preRectName, OBJPROP_COLOR, clrTeal); //--- Set color
   ObjectSetInteger(0, preRectName, OBJPROP_FILL, true); //--- Enable fill
   ObjectSetInteger(0, preRectName, OBJPROP_BACK, true); //--- Set background

   string preTopLineName = "PreTopLine_" + sessionID; //--- Top line name
   ObjectCreate(0, preTopLineName, OBJ_TREND, 0, preLondonStart, PreLondonHigh, londonEnd, PreLondonHigh); //--- Create top line
   ObjectSetInteger(0, preTopLineName, OBJPROP_COLOR, clrBlack); //--- Set color
   ObjectSetInteger(0, preTopLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preTopLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preTopLineName, OBJPROP_BACK, true); //--- Set background

   string preBotLineName = "PreBottomLine_" + sessionID; //--- Bottom line name
   ObjectCreate(0, preBotLineName, OBJ_TREND, 0, preLondonStart, PreLondonLow, londonEnd, PreLondonLow); //--- Create bottom line
   ObjectSetInteger(0, preBotLineName, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(0, preBotLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preBotLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preBotLineName, OBJPROP_BACK, true); //--- Set background
}

Чтобы обеспечить расчет диапазона, установку ордеров и отрисовку на графике, начнем с функции CalculateLotSize, которая вычисляет фиксированный размер лота. Передаем ей параметры entryPrice и stopLossPrice (не используются при фиксированном размере). Функция возвращает значение inpTradeLotsize, нормализованное с помощью NormalizeDouble до 2 знаков, чтобы размер лота был одинаковый для всех сделок. Вы можете использовать другое количество знаков в зависимости от типа счета.

Далее создаем функцию GetRange для расчета диапазона до лондонской сессии. Получаем бары startBar и endBar с помощью функции iBarShift, используя startTime и endTime, и возвращаем -1, если значения некорректны или startBar < endBar. Определяем в заданном диапазоне баров максимальный бар highestBar с помощью функции iHighest по MODE_HIGH и минимальный бар lowestBar с помощью iLowest по MODE_LOW. Устанавливаем значения highVal через iHigh для highestBar, lowVal через iLow для lowestBar, highTime через iTime для highestBar и lowTime через iTime для lowestBar. Возвращаем диапазон как значение (highVal-lowVal) /_Point.

Затем определяем функцию PlacePendingOrders для установки отложенных ордеров buy stop и sell stop. Рассчитываем цену покупки buyPrice как preLondonHigh + OrderOffsetPoints * _Point, а цену продажи sellPrice — как preLondonLow - OrderOffsetPoints * _Point. Устанавливаем slPoints равным StopLossPoints, buySL как buyPrice - slPoints * _Point, sellSL как sellPrice + slPoints * _Point, tpPoints как slPoints * RRRatio, buyTP как buyPrice + tpPoints * _Point и sellTP как sellPrice - tpPoints * _Point. Вычисляем размеры лотов lotSizeBuy и lotSizeSell с помощью CalculateLotSize.

Если тип торговли TradeType равен TRADE_ALL или TRADE_BUY_ONLY, размещаем buy stop через функцию obj_Trade.BuyStop с параметрами lotSizeBuy, buyPrice, buySL, buyTP и меткой "Buy Stop - London", сохраняя тикет в buyOrderTicket через ResultOrder. Аналогично размещаем sell stop, если TradeType равен TRADE_ALL или TRADE_SELL_ONLY.

В завершение реализуем функцию DrawSessionRanges для визуализации сессии на графике. Создаем sessionID как Sess_ плюс lastCheckedDay с помощью функции IntegerToString. Для прямоугольника с названием preRectName = PreRect_ + sessionID используем ObjectCreate типа OBJ_RECTANGLE от PreLondonHighTime, PreLondonHigh до PreLondonLowTime, PreLondonLow, задаваем цвет OBJPROP_COLOR равным clrTeal, заливку OBJPROP_FILL равным true и размещение фоном OBJPROP_BACK равным true.

Для верхней линии с названием в preTopLineName, образованным как PreTopLine_ плюс sessionID, создаем объект OBJ_TREND от preLondonStart, PreLondonHigh до londonEnd, PreLondonHigh, устанавливая OBJPROP_COLOR в clrBlack, OBJPROP_WIDTH в 1, OBJPROP_RAY_RIGHT в false и OBJPROP_BACK в true. Аналогично создаем нижнюю линию preBotLineName как PreBottomLine_ плюс sessionID от preLondonStart, PreLondonLow до londonEnd, PreLondonLow с красным цветом. Теперь можно определить функцию проверки торговых условий с использованием этих функций.

//+------------------------------------------------------------------+
//| Check trading conditions and place orders                        |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Time structure
   TimeToStruct(currentTime, timeStruct); //--- Convert time
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Get today

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Pre-London start
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- London start
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- London end
   analysisTime = londonStart;        //--- Set analysis time

   if (currentTime < analysisTime) return; //--- Exit if before analysis

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Get range
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Check range limits
      noTradeToday = true;            //--- Set no trade
      sessionChecksDone = true;       //--- Set checks done
      DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
      return;                         //--- Exit
   }

   LondonRangePoints = preLondonRange; //--- Set range points
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Place orders
   noTradeToday = true;               //--- Set no trade
   sessionChecksDone = true;          //--- Set checks done
   DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
}

Реализуем функцию CheckTradingConditions для оценки условий сессии и выставления ордеров в нашей системе пробоя лондонской сессии. Создаем структуру MqlDateTime timeStruct и преобразуем текущее время с помощью TimeToStruct. Вычисляем today через StringToTime и StringFormat на основе timeStruct.year, timeStruct.mon и timeStruct.day. Устанавливаем preLondonStart как today плюс PreLondonStartHour и PreLondonStartMinute в секундах, londonStart как today плюс LondonStartHour и LondonStartMinute, и londonEnd как today плюс LondonEndHour и LondonEndMinute. Присваиваем analysisTime значение londonStart и выходим, если текущее время меньше этого значения.

Получаем preLondonRange с помощью функции GetRange, передавая preLondonStart, currentTime и ссылки на PreLondonHigh, PreLondonLow, PreLondonHighTime и PreLondonLowTime. Если preLondonRange меньше MinRangePoints или больше MaxRangePoints, устанавливаем noTradeToday и sessionChecksDone в true, вызываем DrawSessionRanges с параметрами preLondonStart и londonEnd и выходим. В противном случае присваиваем LondonRangePoints значение preLondonRange, вызываем PlacePendingOrders с параметрами PreLondonHigh, PreLondonLow и today, устанавливаем noTradeToday и sessionChecksDone в true и вызываем DrawSessionRanges, обеспечивая выполнение сделок только при корректных диапазонах. Теперь можно вызвать эту функцию в обработчике OnTick для генерации сигналов.

if (!noTradeToday && !sessionChecksDone) { //--- Check trading conditions
   CheckTradingConditions(TimeCurrent()); //--- Check conditions
}

Если на текущий день еще не было сделок и проверка сессии не выполнялась, вызываем функцию проверки условий для текущего времени. При запуске программы получаем следующий результат.

УСТАНОВЛЕННЫЕ ДИАПАЗОНЫ

Мы видим, что диапазоны заданы и отложенные ордера установлены. Диапазон составляет 100 пунктов, что соответствует нашим торговым условиям. Теперь можно перейти к управлению сделками, но сначала добавим проверку и удаление противоположных отложенных ордеров при активации одного из них, а также добавим позиции в список для последующего управления.

//+------------------------------------------------------------------+
//| Delete opposite pending order when one is filled                 |
//+------------------------------------------------------------------+
void CheckAndDeleteOppositeOrder() {
   if (!DeleteOppositeOrder || TradeType != TRADE_ALL) return; //--- Exit if not applicable

   bool buyOrderExists = false;       //--- Buy exists flag
   bool sellOrderExists = false;      //--- Sell exists flag

   for (int i = OrdersTotal() - 1; i >= 0; i--) { //--- Iterate through orders
      ulong orderTicket = OrderGetTicket(i); //--- Get ticket
      if (OrderSelect(orderTicket)) { //--- Select order
         if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { //--- Check symbol and magic
            if (orderTicket == buyOrderTicket) buyOrderExists = true; //--- Set buy exists
            if (orderTicket == sellOrderTicket) sellOrderExists = true; //--- Set sell exists
         }
      }
   }

   if (!buyOrderExists && sellOrderExists && sellOrderTicket != 0) { //--- Check delete sell
      obj_Trade.OrderDelete(sellOrderTicket); //--- Delete sell order
   } else if (!sellOrderExists && buyOrderExists && buyOrderTicket != 0) { //--- Check delete buy
      obj_Trade.OrderDelete(buyOrderTicket); //--- Delete buy order
   }
}

//+------------------------------------------------------------------+
//| Add position to tracking list when opened                        |
//+------------------------------------------------------------------+
void AddPositionToList(ulong ticket, double openPrice, double londonRange, datetime sessionID) {
   if (londonRange <= 0) return;      //--- Exit if invalid range
   int index = ArraySize(positionList); //--- Get current size
   ArrayResize(positionList, index + 1); //--- Resize array
   positionList[index].ticket = ticket; //--- Set ticket
   positionList[index].openPrice = openPrice; //--- Set open price
   positionList[index].londonRange = londonRange; //--- Set range
   positionList[index].sessionID = sessionID; //--- Set session ID
   positionList[index].trailingActive = false; //--- Set trailing inactive
}

Начнем с функции CheckAndDeleteOppositeOrder для удаления противоположного отложенного ордера при активации одного из них. Сразу выходим, если DeleteOppositeOrder равен false или TradeType не равен TRADE_ALL. Инициализируем переменные buyOrderExists и sellOrderExists значением false. Перебираем ордера в обратном порядке с помощью OrdersTotal и OrderGetTicket, выбирая каждый через OrderSelect. Если ордер соответствует _Symbol и MagicNumber через OrderGetString и OrderGetInteger, устанавливаем buyOrderExists или sellOrderExists, если тикет совпадает с buyOrderTicket или sellOrderTicket.

Если buy-ордер отсутствует, а sell-ордер существует, удаляем sellOrderTicket через obj_Trade.OrderDelete; аналогично, если отсутствует sell-ордер, а buy-ордер существует. Эта функция гарантирует, что после активации останется только одно направление сделки.

Далее создаем функцию AddPositionToList для отслеживания открытых позиций. Выходим, если londonRange <= 0, так как диапазон еще не задан. Получаем текущий индекс через ArraySize массива positionList, увеличиваем размер массива с помощью ArrayResize, добавив новый элемент, и заполняем positionList[index]. ticket, openPrice, londonRange, sessionID и устанавливаем trailingActive в false. Это позволяет поддерживать список позиций для управления трейлинг-стопами и данными сессии. Теперь можно реализовать эту логику в обработчике события OnTick.

CheckAndDeleteOppositeOrder();     //--- Delete opposite order

// Add untracked positions
for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions
   ulong ticket = PositionGetTicket(i); //--- Get ticket
   if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check position
      bool tracked = false;        //--- Tracked flag
      for (int j = 0; j < ArraySize(positionList); j++) { //--- Check list
         if (positionList[j].ticket == ticket) tracked = true; //--- Set tracked
      }
      if (!tracked) {              //--- If not tracked
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
         AddPositionToList(ticket, openPrice, LondonRangePoints, lastCheckedDay); //--- Add to list
      }
   }
}

Здесь мы вызываем функцию CheckAndDeleteOppositeOrder для управления отложенными ордерами, чтобы при исполнении ордера в одном направлении противоположный ордер удалялся в соответствии с параметром DeleteOppositeOrder и чтобы в итоге у нас не было конфликтующих сделок.

Далее мы добавляем неотслеживаемые позиции в массив positionList, чтобы контролировать все нужные открытые сделки для трейлинг-стопов. Мы перебираем все позиции с помощью PositionsTotal и PositionGetTicket, получая каждый ticket. Если PositionSelectByTicket выполняется успешно и позиция соответствует символу _Symbol и магическому числу MagicNumber через PositionGetString и PositionGetInteger, мы устанавливаем флаг tracked в false и проверяем массив positionList с помощью ArraySize и внутреннего цикла, чтобы определить, существует ли нужный тикет в positionList[j].ticket. Если позиция не отслеживается, получаем openPrice через PositionGetDouble с параметром POSITION_PRICE_OPEN и вызываем AddPositionToList с параметрами ticket, openPrice, LondonRangePoints и lastCheckedDay. Это гарантирует, что каждая подходящая позиция добавляется в список без дубликатов. Ниже представлен результат.

УДАЛЕНИЕ ОТЛОЖЕННОГО ОРДЕРА

Поскольку на данном этапе всё работает корректно, можем определить функцию для управления позициями.

//+------------------------------------------------------------------+
//| Remove position from tracking list when closed                   |
//+------------------------------------------------------------------+
void RemovePositionFromList(ulong ticket) {
   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through list
      if (positionList[i].ticket == ticket) { //--- Match ticket
         for (int j = i; j < ArraySize(positionList) - 1; j++) { //--- Shift elements
            positionList[j] = positionList[j + 1]; //--- Copy next
         }
         ArrayResize(positionList, ArraySize(positionList) - 1); //--- Resize array
         break;                   //--- Exit loop
      }
   }
}

//+------------------------------------------------------------------+
//| Manage trailing stops                                            |
//+------------------------------------------------------------------+
void ManagePositions() {
   if (PositionsTotal() == 0 || !UseTrailing) return; //--- Exit if no positions or no trailing
   isTrailing = false;                //--- Reset trailing flag

   double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get bid
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get ask
   double point = _Point;             //--- Get point value

   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through positions
      ulong ticket = positionList[i].ticket; //--- Get ticket
      if (!PositionSelectByTicket(ticket)) { //--- Select position
         RemovePositionFromList(ticket); //--- Remove if not selected
         continue;                    //--- Skip
      }

      if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip if not matching

      double openPrice = positionList[i].openPrice; //--- Get open price
      long positionType = PositionGetInteger(POSITION_TYPE); //--- Get type
      double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; //--- Get current price
      double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; //--- Calculate profit points

      if (profitPoints >= MinProfitPoints + TrailingPoints) { //--- Check for trailing
         double newSL = 0.0;          //--- New SL variable
         if (positionType == POSITION_TYPE_BUY) { //--- Buy position
            newSL = currentPrice - TrailingPoints * point; //--- Calculate new SL
         } else {                     //--- Sell position
            newSL = currentPrice + TrailingPoints * point; //--- Calculate new SL
         }
         double currentSL = PositionGetDouble(POSITION_SL); //--- Get current SL
         if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { //--- Check move condition
            if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { //--- Modify position
               positionList[i].trailingActive = true; //--- Set trailing active
               isTrailing = true;        //--- Set global trailing
            }
         }
      }
   }
}

Здесь мы реализуем функции для удаления закрытых позиций из списка отслеживания и управления трейлинг-стопами. Начнем с функции RemovePositionFromList для очистки массива positionList при закрытии позиции, принимающей параметр ticket. Мы перебираем positionList с помощью ArraySize, и если positionList[i].ticket совпадает с ticket, сдвигаем последующие элементы с помощью внутреннего цикла, копируя positionList[j + 1] в positionList[j], затем уменьшаем размер массива с помощью ArrayResize и выходим из цикла. Эта функция обеспечивает актуальность списка и исключает лишние проверки закрытых позиций, особенно при использовании трейлинга и их закрытии.

Далее создаем функцию ManagePositions для обработки трейлинг-стопов открытых сделок. Сразу выходим, если PositionsTotal равно 0 или UseTrailing равен false. Сбрасываем isTrailing в false, получаем currentBid и currentAsk через SymbolInfoDouble с параметрами SYMBOL_BID и SYMBOL_ASK, а также значение point как _Point. Перебираем positionList с помощью ArraySize, получаем ticket и выбираем позицию через PositionSelectByTicket. Если выбрать не удалось, вызываем RemovePositionFromList и продолжаем цикл. Если позиция не соответствует символу _Symbol или магическому числу MagicNumber через PositionGetString и PositionGetInteger, пропускаем ее. Получаем openPrice из positionList[i], тип позиции positionType через PositionGetInteger, текущую цену currentPrice в зависимости от типа позиции и рассчитываем profitPoints как разницу, деленную на point.

Если profitPoints больше или равен MinProfitPoints + TrailingPoints, вычисляем новый уровень стоп-лосса newSL как currentPrice - TrailingPoints * point для покупок или currentPrice + TrailingPoints * point для продаж. Получаем текущий стоп-лосс currentSL через PositionGetDouble, и если новый стоп-лосс newSL лучше текущего значения как минимум на point, модифицируем позицию через obj_Trade.PositionModify, используя нормализованное значение newSL и текущий тейк-профит из PositionGetDouble. При успешном выполнении устанавливаем positionList[i].trailingActive и isTrailing в true, что позволяет динамически подтягивать стоп-лосс, фиксируя прибыль и одновременно давая прибыльным сделкам развиваться. Теперь остается лишь вызывать эту функцию на каждом тике для управления позициями. После компиляции получаем следующий результат.

УПРАВЛЕНИЕ ПОЗИЦИЯМИ

На рисунке видно, что мы проверяем торговые условия, размещаем ордера и управляем ими в соответствии с торговой стратегией. Остается только протестировать программу. Для этого перейдем в новый раздел.


Тестирование на истории

После тщательного тестирования на истории получены следующие результаты.

График тестирования:

График

Отчет тестирования:

ОТЧЕТ


Заключение

Итак, мы разработали систему для торговли на пробое лондонской сессии на MQL5. Она анализирует диапазоны до открытия сессии и устанавливает отложенные ордера с настраиваемым соотношением риск/прибыль, трейлинг-стопами и ограничениями на количество сделок. Также в ней мы реализовали панель управления для мониторинга диапазонов, уровней и просадки в реальном времени. Благодаря модульным компонентам, включая структуру PositionInfo, данная программа предлагает эффективный подход к торговле на пробой, который можно дополнительно оптимизировать, изменяя параметры сессии или риска.

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

Вы можете свободно использовать представленные концепции и реализацию, чтобы адаптировать систему под свой стиль торговли или усилить собственные алгоритмические стратегии. Удачной торговли! 

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18867

Прикрепленные файлы |

Другие статьи автора

Последние комментарии | Перейти к обсуждению на форуме трейдеров (14)
Allan Munene Mutiiria
Allan Munene Mutiiria | 10 авг. 2025 в 22:20
Kyle Young Sangster тестер стратегий. Он находит диапазон каждый день и рисует рамку на графике. Однако он не совершает сделок каждый день (предполагается, что он должен совершать сделки). За 1,5 месяца он совершил всего 3 сделки.



Вторая проблема заключается в том, что уровни максимума/минимума и уровни покупки/продажи в панели управления не обновляются.


Уровни высокого / низкого диапазона четко отображаются на графике, поэтому я предполагаю, что уровни покупки / продажи также должны отображаться на графике и обновляться в панели управления, так как они являются производными непосредственно от уровней высокого / низкого диапазона.

Какие у вас есть предложения, чтобы заставить это работать правильно?

Заранее спасибо.

Что касается второй проблемы, то в статье это объясняется, но если предположить, что ваша проблема связана с плохими тестовыми данными и дать подсказку, когда диапазон находится в расчете, вы всегда будете видеть статус "Calculating...", пока не будет достаточно данных для установки сессии диапазона Лондона или любой сессии, которую вы определите во входных данных. Предполагая, что вы используете настройки по умолчанию, с предлондонским временем 3, и ваше время из общего скриншота 13 февраля, 2 бара после 22:00, что составляет 2*15 минут = 30, следовательно, давая 22:30, находится вне времени расчета диапазона, поэтому данные на панели должны быть видны, так как предыдущий установленный диапазон все еще в игре, если первая сессия еще не найдена, и будет очищен, когда расчет диапазона будет достигнут с полуночи. См. ниже:

const int PreLondonStartHour = 3; //--- Фиксированный час начала работы до Лондона
const int PreLondonStartMinute = 0; //--- Фиксированная минута начала до Лондона

Вам может понадобиться увидеть приведенную ниже логику поиска диапазона

//+------------------------------------------------------------------+
//| Проверьте торговые условия и разместите ордера |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Структура времени
   TimeToStruct(currentTime, timeStruct); //--- Конвертировать время
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Получить сегодня

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Старт перед Лондоном
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- Начало в Лондоне
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- London end
   analysisTime = londonStart;        //--- Установите время анализа

   if (currentTime < analysisTime) return; //--- Выход, если не успели провести анализ

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Получить диапазон
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Проверьте границы диапазона
      noTradeToday = true;            //--- Установить отсутствие торговли
      sessionChecksDone = true;       //--- Проверки набора выполнены
      DrawSessionRanges(preLondonStart, londonEnd); //--- Нарисуйте диапазоны
      return;                         //--- Выход
   }

   LondonRangePoints = preLondonRange; //--- Установите точки диапазона
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Разместить заказы
   noTradeToday = true;               //--- Установить отсутствие торговли
   sessionChecksDone = true;          //--- Проверки набора выполнены
   DrawSessionRanges(preLondonStart, londonEnd); //--- Нарисуйте диапазоны
}

И как он устанавливается.

//+------------------------------------------------------------------+
//| Обновление панели с текущими данными|
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Форматирование текста диапазона
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Обновите текст диапазона

   //---

}

На изображении ниже, хотя мы не знаем год вашего тестирования, мы возьмем 2025, если это 2020, как в вашем случае, у нас нет качественных данных для этого, поэтому в любом случае мы используем 2025 и, таким образом, расчет диапазона должен начаться в полночь.

23:55

Из изображения видно, что данные в 23:55 все еще нетронуты. Однако, когда наступит полночь, мы должны сбросить данные. Смотрите ниже.

ПОЛНОЧНЫЕ ДАННЫЕ 00:00

Видно, что мы сбросили данные в полночь для другого расчета диапазона. На самом деле, когда расчет диапазона завершен, визуализация может помочь вам понять, что произошло на самом деле. Например, в вашем случае при использовании настроек по умолчанию мы увидим столбики raneg с 03:00 до 08:00, потому что именно это мы и определили. См. ниже:

ЧАСЫ ДИАПАЗОНА

Надеюсь, это еще раз прояснит ситуацию. Вы можете настроить все в соответствии с вашим стилем торговли. Чтобы избежать проблем, с которыми вы столкнулись, рекомендуется использовать надежные тестовые данные. Спасибо.

Kyle Young Sangster
Kyle Young Sangster | 10 авг. 2025 в 22:34
Где в коде вы собирались использовать переменную "MaxOpenTrades"? Она определена, но на нее никогда не ссылаются.
Kyle Young Sangster
Kyle Young Sangster | 10 авг. 2025 в 22:52
Allan Munene Mutiiria #:

Что касается второй проблемы, то в статье это объясняется, но если предположить, что ваша проблема связана с плохими тестовыми данными и дать подсказку, то когда диапазон находится в процессе расчета, вы всегда будете видеть статус "Calculating...", пока не будет достаточно данных для установки сессии лондонского диапазона или любой другой сессии, которую вы определите во входных данных. Предполагая, что вы используете настройки по умолчанию, с предлондонским временем 3, и ваше время из общего скриншота 13 февраля, 2 бара после 22:00, что составляет 2*15 минут = 30, следовательно, давая 22:30, находится вне времени расчета диапазона, поэтому данные на панели должны быть видны, так как предыдущий установленный диапазон все еще в игре, если первая сессия еще не найдена, и будет очищен, когда расчет диапазона будет достигнут с полуночи. См. ниже:

Вам может понадобиться следующая логика для определения диапазона

И как он устанавливается.

Смотрите изображение ниже, хотя мы не знаем год вашего тестирования, мы возьмем 2025, если это 2020, как в вашем случае, у нас нет качественных данных для этого, так что в любом случае мы используем 2025 и, таким образом, расчет диапазона должен начинаться в полночь.


Из изображения видно, что данные в 23:55 все еще нетронуты. Однако, когда наступит полночь, мы должны сбросить данные. Смотрите ниже.

Вы можете видеть, что мы сбросили данные в полночь для другого расчета диапазона. На самом деле, когда расчет диапазона завершен, визуализация может помочь вам понять, что произошло на самом деле. Например, в вашем случае при использовании настроек по умолчанию мы увидим столбики raneg с 03:00 до 08:00, потому что именно это мы и определили. См. ниже:

Надеюсь, это еще раз прояснит ситуацию. Вы можете настроить все в соответствии с вашим стилем торговли. Чтобы избежать проблем, с которыми вы столкнулись, рекомендуется использовать надежные тестовые данные. Спасибо.

Большое спасибо за исчерпывающий ответ.

Да, я действительно прочитал статью, следил за кодированием своей собственной копии, пока не столкнулся с проблемами, которые я описал. Я увидел, что панель не обновляется, даже в стандартное время. Мой скриншот должен был показать, что, хотя коробка была нарисована на графике, данные были собраны, но панель не была обновлена. Кроме того, в логах не было сообщений об ошибках, связанных с недействительными ценами или уровнями.

Я добавил сообщения в логах в свою версию; из них видно, что панель не обновляется, когда диапазон слишком большой или слишком маленький, так что это может быть частью причины.

Я еще раз проверю качество тестовых данных. И спасибо, что указали, на какой паре вы тестировали; я обязательно внесу коррективы для выбранных мной пар.

Большое спасибо за помощь.

Allan Munene Mutiiria
Allan Munene Mutiiria | 10 авг. 2025 в 23:49
Kyle Young Sangster #:

Большое спасибо за исчерпывающий ответ.

Да, я прочитал статью, следил за кодированием своей собственной копии, пока не столкнулся с проблемами, которые я описал. Я увидел, что панель не обновляется, даже в стандартное время. Мой скриншот должен был показать, что, хотя коробка была нарисована на графике, данные были собраны, но панель не была обновлена. Кроме того, в логах не было сообщений об ошибках, связанных с недействительными ценами или уровнями.

Я добавил сообщения в логах в свою версию; из них видно, что панель не обновляется, когда диапазон слишком большой или слишком маленький, так что это может быть частью причины.

Я еще раз проверю качество тестовых данных. И спасибо, что указали, на какой паре вы тестировали; я обязательно внесу коррективы для выбранных мной пар.

Большое спасибо за помощь.

Конечно. Добро пожаловать.

Torsten Busch
Torsten Busch | 11 нояб. 2025 в 21:01

Спасибо, что поделились с нами своим кодом.

Поскольку я сам писал сессионно-зависимых советников, могу сказать, что код работает только в том случае, если ваш брокер всегда находится в часовом поясе GMT+1, а также использует британское летнее время.

Во всех остальных случаях время начала сессии не будет работать. Почему? Потому что лондонская сессия начинается в 8:00 утра по британскому времени. Зимой это 8:00 по Гринвичу, а летом - 7:00 по Гринвичу.

TimeCurrent() возвращает не ваше местное время, а всегда время с торгового сервера.

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики
В этой статье мы разрабатываем ценовую панель на основе холста (canvas) в MQL5 с использованием класса CCanvas для создания интерактивных панелей для визуализации последних графиков цен и статистики счетов с поддержкой фоновых изображений, эффектов тумана и градиентной заливки. Система включает в себя функции перетаскивания и изменения размера с помощью обработки событий мыши, переключение тем оформления между темным и светлым режимами с динамической настройкой цветов, а также элементы управления сворачиванием/разворачиванием для эффективного управления пространством графика.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: Адаптивное масштабирование представлений (Окончание) Нейросети в трейдинге: Адаптивное масштабирование представлений (Окончание)
В статье представлена интеграция ранее реализованных компонентов фреймворка ADS в прикладную торговую модель и их проверка на исторических данных. Показано, как построение объекта верхнего уровня позволяет встроить сложную архитектуру в существующие решения, сохранив управляемость и прозрачность модели. Проведенное тестирование раскрывает как потенциал подхода в генерации прибыли, так и его ограничения, формируя основу для дальнейшей оптимизации риск-менеджмента и повышения устойчивости системы.