English 中文 Español Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 9): Создаем советник для стратегии прорыва азиатской сессии

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

MetaTrader 5Трейдинг |
413 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 8) мы исследовали стратегию торговли с разворотом, создав советник на языке MetaQuotes Language 5 (MQL5) на основе гармонического паттерна Butterfly с использованием точных соотношений Фибоначчи. Теперь, в Части 9, сосредоточимся на Стратегии прорыва азиатской сессии — методе, определяющем ключевые сессионные максимумы и минимумы для формирования зон прорыва, использует скользящую среднюю для фильтрации трендов и интегрирует динамическое управление рисками.

В настоящей статье мы рассмотрим следующее:

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

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


    План стратегии

    Для создания программы мы разработаем подход, при котором используется ключевой ценовой диапазон, сформированный во время Азиатской торговой сессии. Первым шагом будет определение времени сессии путем определения наивысшего максимума и самого низкого минимума в течение определенного временного интервала — обычно между 23:00 и 03:00 по Гринвичу (GMT). Однако эти временные промежутки можно полностью настроить в соответствии с вашими потребностями. Этот определенный диапазон представляет собой область консолидации, из которой мы ожидаем прорыва.

    Далее установим уровни прорыва на границах этого диапазона. Разместим отложенный стоп-ордер на покупку чуть выше верхней границы поля, если рыночные условия подтвердят бычий тренд, используя скользящую среднюю (например, 50—периодную скользящую среднюю) для подтверждения тренда. И наоборот, если тренд медвежий, разместим стоп-ордер на продажу чуть ниже нижней части поля. Эта двойная настройка гарантирует, что наш советник будет готов зафиксировать значительные движения в любом направлении, как только цена совершит пробой.

    Управление рисками является важнейшим компонентом нашей стратегии. Мы будем устанавливать ордера стоп-лосс непосредственно за пределами поля для защиты от ложных пробоев или разворотов, в то время как уровни тейк-профита будут определяться на основе заранее определенного соотношения риска и прибыли. Кроме того, реализуем стратегию выхода, основанную на времени, которая автоматически закрывает все открытые сделки, если они остаются активными после установленного времени выхода, например, 13:00 по Гринвичу. В целом, наша стратегия сочетает в себе точное определение диапазона на основе сессий, фильтрацию трендов и надежное управление рисками для создания советника, способного фиксировать значительные прорывные движения на рынке. В двух словах, вот визуализация всей стратегии, которую мы хотим реализовать.

    ПЛАН СТРАТЕГИИ


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

    Чтобы создать программу на MQL5, откройте  MetaEditor, перейдите в Навигатор, найдите папку «Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нам нужно будет объявить некоторые  глобальные переменные, которые будем использовать во всей программе.

    //+------------------------------------------------------------------+
    //|                        Copyright 2025, Forex Algo-Trader, Allan. |
    //|                                 "https://t.me/Forex_Algo_Trader" |
    //+------------------------------------------------------------------+
    #property copyright "Forex Algo-Trader, Allan"
    #property link      "https://t.me/Forex_Algo_Trader"
    #property version   "1.00"
    #property description "This EA trades based on ASIAN BREAKOUT Strategy"
    #property strict
    
    #include <Trade\Trade.mqh>                                //--- Include trade library
    CTrade obj_Trade;                                         //--- Create global trade object
    
    //--- Global indicator handle for the moving average
    int maHandle = INVALID_HANDLE;                            //--- Global MA handle
    
    //==== Input parameters
    //--- Trade and indicator settings
    input double         LotSize              = 0.1;          //--- Trade lot size
    input double         BreakoutOffsetPips   = 10;           //--- Offset in pips for pending orders
    input ENUM_TIMEFRAMES BoxTimeframe         = PERIOD_M15;  //--- Timeframe for box calculation (15 or 30 minutes)
    input int            MA_Period            = 50;           //--- Moving average period for trend filter
    input ENUM_MA_METHOD MA_Method            = MODE_SMA;     //--- MA method (Simple Moving Average)
    input ENUM_APPLIED_PRICE MA_AppliedPrice   = PRICE_CLOSE; //--- Applied price for MA (Close price)
    input double         RiskToReward         = 1.3;          //--- Reward-to-risk multiplier (1:1.3)
    input int            MagicNumber          = 12345;        //--- Magic number (used for order identification)
    
    //--- Session timing settings (GMT) with minutes
    input int            SessionStartHour     = 23;           //--- Session start hour
    input int            SessionStartMinute   = 00;           //--- Session start minute
    input int            SessionEndHour       = 03;           //--- Session end hour
    input int            SessionEndMinute     = 00;           //--- Session end minute
    input int            TradeExitHour        = 13;           //--- Trade exit hour
    input int            TradeExitMinute      = 00;           //--- Trade exit minute
    
    //--- Global variables for storing session box data
    datetime lastBoxSessionEnd = 0;                           //--- Stores the session end time of the last computed box
    bool     boxCalculated     = false;                       //--- Flag: true if session box has been calculated
    bool     ordersPlaced      = false;                       //--- Flag: true if orders have been placed for the session
    double   BoxHigh           = 0.0;                         //--- Highest price during the session
    double   BoxLow            = 0.0;                         //--- Lowest price during the session
    //--- Variables to store the exact times when the session's high and low occurred
    datetime BoxHighTime       = 0;                           //--- Time when the highest price occurred
    datetime BoxLowTime        = 0;                           //--- Time when the lowest price occurred
    

    Здесь мы включаем торговую библиотеку, используя "#include <Trade\Trade.mqh>", чтобы получить доступ к встроенным торговым функциям и создать глобальный торговый объект с именем "obj_Trade". Определяем глобальный хэндл индикатора "maHandle", инициализируем его значением INVALID_HANDLE и настраиваем пользовательские входные параметры для торговли и настройки индикатора, такие как "LotSize", "BreakoutOffsetPips" и "BoxTimeframe" (который использует тип ENUM_TIMEFRAMES ), а также параметры для скользящей средней ("MA_Period", "MA_Method", "MA_AppliedPrice") и управление рисками ("RiskToReward", "MagicNumber").

    Кроме того, разрешаем пользователям указывать тайминг сессии в часах и минутах (используя такие входные данные, как "SessionStartHour", "SessionStartMinute", "SessionEndHour", "SessionEndMinute", "TradeExitHour" и "TradeExitMinute") и объявлять глобальные переменные для хранения данных в поле во время сессии ("BoxHigh", «BoxLow»), а также точное время возникновения этих экстремумов ("BoxHighTime", "BoxLowTime"), вместе с флагами ("boxCalculated" и "ordersPlaced") для управления логикой программы. Далее переходим к обработчику событий OnInit и инициализируем хэндл.

    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit(){
       //--- Set the magic number for all trade operations
       obj_Trade.SetExpertMagicNumber(MagicNumber);           //--- Set magic number globally for trades
       //--- Create the Moving Average handle with user-defined parameters
       maHandle = iMA(_Symbol, 0, MA_Period, 0, MA_Method, MA_AppliedPrice); //--- Create MA handle
       if(maHandle == INVALID_HANDLE){                        //--- Check if MA handle creation failed
          Print("Failed to create MA handle.");               //--- Print error message
          return(INIT_FAILED);                                //--- Terminate initialization if error occurs
       }
       return(INIT_SUCCEEDED);                                //--- Return successful initialization
    }

    В обработчике событий OnInit устанавливаем магическое число торгового объекта, вызывая метод "obj_Trade.SetExpertMagicNumber(MagicNumber)", обеспечивая, что все сделки будут уникально идентифицированы. Затем создаем хэндл Скользящая средняя, используя функцию iMA с нашими пользовательскими параметрами ("MA_Period", "MA_Method" и "MA_AppliedPrice"). Затем проверяем, успешно ли создан хэндл путем проверки того, равен ли "maHandle" значению INVALID_HANDLE. Если равен, выводим сообщение об ошибке и возвращаем INIT_FAILED, в противном случае, возвращаем INIT_SUCCEEDED для сигнализирования об успешной инициализации. Далее необходимо освободить созданный хэндл, чтобы сэкономить ресурсы, когда программа не используется.

    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason){
       //--- Release the MA handle if valid
       if(maHandle != INVALID_HANDLE)                     //--- Check if MA handle exists
          IndicatorRelease(maHandle);                     //--- Release the MA handle
       //--- Drawn objects remain on the chart for historical reference
    }

    В функции OnDeinit проверяем, является ли хэндл скользящей средней "maHandle" допустимым (т.е. не равным INVALID_HANDLE). Если он является допустимым, освобождаем хэндл, вызывая функцию IndicatorRelease, чтобы освободить ресурсы. Теперь можно перейти к основному обработчику событий OnTick, на котором мы будем основывать всю нашу логику управления.

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick(){
       //--- Get the current server time (assumed GMT)
       datetime currentTime = TimeCurrent();              //--- Retrieve current time
       MqlDateTime dt;                                    //--- Declare a structure for time components
       TimeToStruct(currentTime, dt);                     //--- Convert current time to structure
       
       //--- Check if the current time is at or past the session end (using hour and minute)
       if(dt.hour > SessionEndHour || (dt.hour == SessionEndHour && dt.min >= SessionEndMinute)){
          //--- Build the session end time using today's date and user-defined session end time
          MqlDateTime sesEnd;                             //--- Declare a structure for session end time
          sesEnd.year = dt.year;                          //--- Set year
          sesEnd.mon  = dt.mon;                           //--- Set month
          sesEnd.day  = dt.day;                           //--- Set day
          sesEnd.hour = SessionEndHour;                   //--- Set session end hour
          sesEnd.min  = SessionEndMinute;                 //--- Set session end minute
          sesEnd.sec  = 0;                                //--- Set seconds to 0
          datetime sessionEnd = StructToTime(sesEnd);     //--- Convert structure to datetime
          
          //--- Determine the session start time
          datetime sessionStart;                          //--- Declare variable for session start time
          //--- If session start is later than or equal to session end, assume overnight session
          if(SessionStartHour > SessionEndHour || (SessionStartHour == SessionEndHour && SessionStartMinute >= SessionEndMinute)){
             datetime prevDay = sessionEnd - 86400;       //--- Subtract 24 hours to get previous day
             MqlDateTime dtPrev;                          //--- Declare structure for previous day time
             TimeToStruct(prevDay, dtPrev);               //--- Convert previous day time to structure
             dtPrev.hour = SessionStartHour;              //--- Set session start hour for previous day
             dtPrev.min  = SessionStartMinute;            //--- Set session start minute for previous day
             dtPrev.sec  = 0;                             //--- Set seconds to 0
             sessionStart = StructToTime(dtPrev);         //--- Convert structure back to datetime
          }
          else{
             //--- Otherwise, use today's date for session start
             MqlDateTime temp;                            //--- Declare temporary structure
             temp.year = sesEnd.year;                     //--- Set year from session end structure
             temp.mon  = sesEnd.mon;                      //--- Set month from session end structure
             temp.day  = sesEnd.day;                      //--- Set day from session end structure
             temp.hour = SessionStartHour;                //--- Set session start hour
             temp.min  = SessionStartMinute;              //--- Set session start minute
             temp.sec  = 0;                               //--- Set seconds to 0
             sessionStart = StructToTime(temp);           //--- Convert structure to datetime
          }
          
          //--- Recalculate the session box only if this session hasn't been processed before
          if(sessionEnd != lastBoxSessionEnd){
             ComputeBox(sessionStart, sessionEnd);        //--- Compute session box using start and end times
             lastBoxSessionEnd = sessionEnd;              //--- Update last processed session end time
             boxCalculated   = true;                      //--- Set flag indicating the box has been calculated
             ordersPlaced    = false;                     //--- Reset flag for order placement for the new session
          }
       }
    }

    В тиковой функции эксперта OnTick в первую очередь вызываем TimeCurrent для получения текущего времени сервера, а затем преобразовываем её в структуру MqlDateTime с помощью функции TimeToStruct так, чтобы получить доступ к её компонентам. Сравниваем текущие часы и минуты с заданными пользователем "SessionEndHour" и «SessionEndMinute». Если текущее время совпадает с окончанием сессии или превышает его, создаем структуру "sesEnd" и преобразуем ее в datetime с помощью StructToTime.

    В зависимости от того, начнется ли сессия до или после окончания сессии, мы определяем подходящее время "sessionStart" (используя сегодняшнюю дату или корректируя на ночную сессию), и если эта "sessionEnd" отличается от "lastBoxSessionEnd", вызываем функцию "ComputeBox" для пересчета поля сессии с обновлением "lastBoxSessionEnd" и сбросом наших флагов "boxCalculated" и "ordersPlaced". Мы используем пользовательскую функцию для вычисления свойств поля. Ниже приведён фрагмент ее кода.

    //+------------------------------------------------------------------+
    //| Function: ComputeBox                                             |
    //| Purpose: Calculate the session's highest high and lowest low, and|
    //|          record the times these extremes occurred, using the     |
    //|          specified session start and end times.                  |
    //+------------------------------------------------------------------+
    void ComputeBox(datetime sessionStart, datetime sessionEnd){
       int totalBars = Bars(_Symbol, BoxTimeframe);       //--- Get total number of bars on the specified timeframe
       if(totalBars <= 0){
          Print("No bars available on timeframe ", EnumToString(BoxTimeframe)); //--- Print error if no bars available
          return;                                        //--- Exit if no bars are found
       }
         
       MqlRates rates[];                                 //--- Declare an array to hold bar data
       ArraySetAsSeries(rates, false);                   //--- Set array to non-series order (oldest first)
       int copied = CopyRates(_Symbol, BoxTimeframe, 0, totalBars, rates); //--- Copy bar data into array
       if(copied <= 0){
          Print("Failed to copy rates for box calculation."); //--- Print error if copying fails
          return;                                        //--- Exit if error occurs
       }
         
       double highVal = -DBL_MAX;                        //--- Initialize high value to the lowest possible
       double lowVal  = DBL_MAX;                         //--- Initialize low value to the highest possible
       //--- Reset the times for the session extremes
       BoxHighTime = 0;                                  //--- Reset stored high time
       BoxLowTime  = 0;                                  //--- Reset stored low time
       
       //--- Loop through each bar within the session period to find the extremes
       for(int i = 0; i < copied; i++){
          if(rates[i].time >= sessionStart && rates[i].time <= sessionEnd){
             if(rates[i].high > highVal){
                highVal = rates[i].high;                //--- Update highest price
                BoxHighTime = rates[i].time;            //--- Record time of highest price
             }
             if(rates[i].low < lowVal){
                lowVal = rates[i].low;                  //--- Update lowest price
                BoxLowTime = rates[i].time;             //--- Record time of lowest price
             }
          }
       }
       if(highVal == -DBL_MAX || lowVal == DBL_MAX){
          Print("No valid bars found within the session time range."); //--- Print error if no valid bars found
          return;                                        //--- Exit if invalid data
       }
       BoxHigh = highVal;                                //--- Store final highest price
       BoxLow  = lowVal;                                 //--- Store final lowest price
       Print("Session box computed: High = ", BoxHigh, " at ", TimeToString(BoxHighTime),
             ", Low = ", BoxLow, " at ", TimeToString(BoxLowTime)); //--- Output computed session box data
       
       //--- Draw all session objects (rectangle, horizontal lines, and price labels)
       DrawSessionObjects(sessionStart, sessionEnd);    //--- Call function to draw objects using computed values
    }

    Здесь мы определяем функцию типа void "ComputeBox" для вычисления экстремумов сессии. Начинаем с получения общего количества баров на указанном таймфрейме с использованием функци Bars и затем копируем данные баров в массив MqlRates воспользовавшись функцией CopyRates. Инициализируем переменную "highVal" значением -DBL_MAX, а "lowVal" - значением DBL_MAX, чтобы гарантировать, что любая действующая цена обновит эти экстремумы. При прохождении циклом по каждому бару, попадающему в период сессии, если "максимум" бара превышает "highVal", обновляем "highVal" и записываем время этого бара в «BoxHighTime». Аналогично, если "минимум" бара ниже "lowVal", обновляем "lowVal" и фиксируем время в "BoxLowTime".

    Если после обработки данных "highVal" остается "-DBL_MAX" или "lowVal" остается DBL_MAX, выводим сообщение об ошибке, указывающее на то, что не найдено допустимых баров. В противном случае присваиваем "BoxHigh" и "BoxLow" вычисленные значения и используем функцию TimeToString для вывода записанного времени в удобочитаемом формате. Наконец, вызываем функцию "DrawSessionObjects" с указанием времени начала и окончания сессии, чтобы визуально отобразить поле сессии и связанные с ним объекты на графике. Реализация функции выглядит так.

    //+----------------------------------------------------------------------+
    //| Function: DrawSessionObjects                                         |
    //| Purpose: Draw a filled rectangle spanning from the session's high    |
    //|          point to its low point (using exact times), then draw       |
    //|          horizontal lines at the high and low (from sessionStart to  |
    //|          sessionEnd) with price labels at the right. Dynamic styling |
    //|          for font size and line width is based on the current chart  |
    //|          scale.                                                      |
    //+----------------------------------------------------------------------+
    void DrawSessionObjects(datetime sessionStart, datetime sessionEnd){
       int chartScale = (int)ChartGetInteger(0, CHART_SCALE, 0); //--- Retrieve the chart scale (0 to 5)
       int dynamicFontSize = 7 + chartScale * 1;        //--- Base 7, increase by 2 per scale level
       int dynamicLineWidth = (int)MathRound(1 + (chartScale * 2.0 / 5)); //--- Linear interpolation
       
       //--- Create a unique session identifier using the session end time
       string sessionID = "Sess_" + IntegerToString(lastBoxSessionEnd);
       
       //--- Draw the filled rectangle (box) using the recorded high/low times and prices
       string rectName = "SessionRect_" + sessionID;       //--- Unique name for the rectangle
       if(!ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, BoxHighTime, BoxHigh, BoxLowTime, BoxLow))
          Print("Failed to create rectangle: ", rectName); //--- Print error if creation fails
       ObjectSetInteger(0, rectName, OBJPROP_COLOR, clrThistle); //--- Set rectangle color to blue
       ObjectSetInteger(0, rectName, OBJPROP_FILL, true);       //--- Enable filling of the rectangle
       ObjectSetInteger(0, rectName, OBJPROP_BACK, true);       //--- Draw rectangle in background
       
       //--- Draw the top horizontal line spanning from sessionStart to sessionEnd at the session high
       string topLineName = "SessionTopLine_" + sessionID; //--- Unique name for the top line
       if(!ObjectCreate(0, topLineName, OBJ_TREND, 0, sessionStart, BoxHigh, sessionEnd, BoxHigh))
          Print("Failed to create top line: ", topLineName); //--- Print error if creation fails
       ObjectSetInteger(0, topLineName, OBJPROP_COLOR, clrBlue); //--- Set line color to blue
       ObjectSetInteger(0, topLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, topLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Draw the bottom horizontal line spanning from sessionStart to sessionEnd at the session low
       string bottomLineName = "SessionBottomLine_" + sessionID; //--- Unique name for the bottom line
       if(!ObjectCreate(0, bottomLineName, OBJ_TREND, 0, sessionStart, BoxLow, sessionEnd, BoxLow))
          Print("Failed to create bottom line: ", bottomLineName); //--- Print error if creation fails
       ObjectSetInteger(0, bottomLineName, OBJPROP_COLOR, clrRed); //--- Set line color to blue
       ObjectSetInteger(0, bottomLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, bottomLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Create the top price label at the right edge of the top horizontal line
       string topLabelName = "SessionTopLabel_" + sessionID; //--- Unique name for the top label
       if(!ObjectCreate(0, topLabelName, OBJ_TEXT, 0, sessionEnd, BoxHigh))
          Print("Failed to create top label: ", topLabelName); //--- Print error if creation fails
       ObjectSetString(0, topLabelName, OBJPROP_TEXT," "+DoubleToString(BoxHigh, _Digits)); //--- Set label text to session high price
       ObjectSetInteger(0, topLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, topLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, topLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
       
       //--- Create the bottom price label at the right edge of the bottom horizontal line
       string bottomLabelName = "SessionBottomLabel_" + sessionID; //--- Unique name for the bottom label
       if(!ObjectCreate(0, bottomLabelName, OBJ_TEXT, 0, sessionEnd, BoxLow))
          Print("Failed to create bottom label: ", bottomLabelName); //--- Print error if creation fails
       ObjectSetString(0, bottomLabelName, OBJPROP_TEXT," "+DoubleToString(BoxLow, _Digits)); //--- Set label text to session low price
       ObjectSetInteger(0, bottomLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, bottomLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, bottomLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
    }

    В функции "DrawSessionObjects" начинаем с получения масштаба текущего графика, используя функцию ChartGetInteger с помощью CHART_SCALE (которая возвращает значение от 0 до 5), а затем вычисляем параметры динамического стиля: динамический размер шрифта, рассчитываемый как "7 + chartScale * 1" (с базовым размером 7, который увеличивается на 1 для каждого уровня масштаба) и динамическую ширину строки с использованием MathRound для линейной интерполяции, так что при масштабе графика равном 5, ширина становится равной 3. Далее создаем уникальный идентификатор сессии, преобразуя "lastBoxSessionEnd" в строку с префиксом "Sess_", что гарантирует, что объекты каждой сессии будут иметь разные имена. Затем рисуем заполненный прямоугольник, используя ObjectCreate, передавая тип OBJ_RECTANGLE с точными временными указателями и ценами максимума сессии ("BoxHighTime", "BoxHigh") и минимума ("BoxLowTime", "BoxLow"), устанавливая цвет на "clrThistle", заполнить его с помощью OBJPROP_FILL и поместив его в фоновом режиме с помощью OBJPROP_BACK.

    После этого рисуем две горизонтальные линии тренда — одну на максимуме сессии и одну на минимуме сессии — от "sessionStart" до «sessionEnd». Устанавливаем цвет верхней линии на "clrBlue", а цвет нижней линии на "clrRed", и обе линии используют динамическую ширину линии и не расширяются бесконечно ("OBJPROP_RAY_RIGHT" имеет значение false). Наконец, создаем текстовые объекты для верхней и нижней ценовых меток на правом краю (в "SessionEnd"), устанавливая их текст в значения максимума и минимума сессии (отформатированные с помощью DoubleToString с использованием точности символа, _Digits), а их цвет установлен как "clrBlack" и применен динамический размер шрифта. Привязываем их слева, чтобы текст появлялся справа от привязки. После компиляции получаем следующий результат.

    ASIAN BOX IDENTIFIED

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

    //--- Build the trade exit time using user-defined hour and minute for today
    MqlDateTime exitTimeStruct;                        //--- Declare a structure for exit time
    TimeToStruct(currentTime, exitTimeStruct);         //--- Use current time's date components
    exitTimeStruct.hour = TradeExitHour;               //--- Set trade exit hour
    exitTimeStruct.min  = TradeExitMinute;             //--- Set trade exit minute
    exitTimeStruct.sec  = 0;                           //--- Set seconds to 0
    datetime tradeExitTime = StructToTime(exitTimeStruct); //--- Convert exit time structure to datetime
    
    //--- If the session box is calculated, orders are not placed yet, and current time is before trade exit time, place orders
    if(boxCalculated && !ordersPlaced && currentTime < tradeExitTime){
       double maBuffer[];                           //--- Declare array to hold MA values
       ArraySetAsSeries(maBuffer, true);            //--- Set the array as series (newest first)
       if(CopyBuffer(maHandle, 0, 0, 1, maBuffer) <= 0){  //--- Copy 1 value from the MA buffer
          Print("Failed to copy MA buffer.");       //--- Print error if buffer copy fails
          return;                                   //--- Exit the function if error occurs
       }
       double maValue = maBuffer[0];                 //--- Retrieve the current MA value
       
       double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current bid price
       bool bullish = (currentPrice > maValue);      //--- Determine bullish condition
       bool bearish = (currentPrice < maValue);       //--- Determine bearish condition
       
       double offsetPrice = BreakoutOffsetPips * _Point; //--- Convert pips to price units
       
       //--- If bullish, place a Buy Stop order
       if(bullish){
          double entryPrice = BoxHigh + offsetPrice; //--- Set entry price just above the session high
          double stopLoss   = BoxLow - offsetPrice;    //--- Set stop loss below the session low
          double risk       = entryPrice - stopLoss;     //--- Calculate risk per unit
          double takeProfit = entryPrice + risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.BuyStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Buy Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                        //--- Set flag indicating an order has been placed
          }
          else{
             Print("Buy Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
       //--- If bearish, place a Sell Stop order
       else if(bearish){
          double entryPrice = BoxLow - offsetPrice;  //--- Set entry price just below the session low
          double stopLoss   = BoxHigh + offsetPrice;   //--- Set stop loss above the session high
          double risk       = stopLoss - entryPrice;    //--- Calculate risk per unit
          double takeProfit = entryPrice - risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.SellStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Sell Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                       //--- Set flag indicating an order has been placed
          }
          else{
             Print("Sell Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
    }

    Здесь мы определяем время выхода из сделки, объявляя структуру MqlDateTime с именем "exitTimeStruct". Затем используем функцию TimeToStruct, чтобы разложить текущее время на части и присвоить функции "exitTimeStruct" пользовательские значения "TradeExitHour" и "TradeExitMinute" (с секундами, равными 0). Затем преобразуем эту структуру обратно в значение datetime, вызывая функцию StructToTime, в результате чего получаем "tradeExitTime". После этого, если поле сессии было рассчитано, ордеров размещено не было, а текущее время предшествует "tradeExitTime", переходим к размещению ордеров.

    Объявляем массив "maBuffer" для хранения значений скользящей средней и вызываем функцию ArraySetAsSeries, чтобы гарантировать, что массив сначала будет проиндексирован самыми свежими данными. Затем используем функцию CopyBuffer для получения последнего значения от индикатора «скользящая средняя» (с помощью "maHandle") в "maBuffer". Сравниваем это значение скользящей средней с текущей ценой bid (полученной с помощью функции SymbolInfoDouble), чтобы определить, является ли рынок бычьим или медвежьим. Исходя из этого условия рассчитываем соответствующую цену входа, стоп-лосс и тейк-профит, используя параметр «BreakoutOffsetPips». Затем размещаем либо стоп-ордер на покупку, используя метод "obj_Trade.BuyStop", либо стоп-ордер на продажу с использованием метода "obj_Trade.SellStop".

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

    PENDING ORDER CONFIRMED

    Из этой функции видно, что сразу по достижении прорыва мы выставляем отложенный ордер относительно направления фильтра скользящей средней, так же со стоп-ордерами. Единственное, что остается, - это выйти из позиций или удалить отложенные ордера, как только время торговли выходит за рамки установленного времени торговли.

    //--- If current time is at or past trade exit time, close positions and cancel pending orders
    if(currentTime >= tradeExitTime){
       CloseOpenPositions();                          //--- Close all open positions for this EA
       CancelPendingOrders();                         //--- Cancel all pending orders for this EA
       boxCalculated = false;                         //--- Reset session box calculated flag
       ordersPlaced  = false;                         //--- Reset order placed flag
    }
    

    Здесь мы проверяем, достигло ли текущее время времени выхода из сделки или превысило его. Если это так, вызываем функцию "CloseOpenPositions", чтобы закрыть все открытые позиции, связанные с советником, а затем вызываем функцию "CancelPendingOrders", чтобы отменить все отложенные ордера. После выполнения этих функций сбрасываем флаги "boxCalculated" и "ordersPlaced" на значение false, подготавливая программу к новой сессии. Мы используем следующие пользовательские функции.

    //+------------------------------------------------------------------+
    //| Function: CloseOpenPositions                                     |
    //| Purpose: Close all open positions with the set magic number      |
    //+------------------------------------------------------------------+
    void CloseOpenPositions(){
       int totalPositions = PositionsTotal();           //--- Get total number of open positions
       for(int i = totalPositions - 1; i >= 0; i--){      //--- Loop through positions in reverse order
          ulong ticket = PositionGetTicket(i);           //--- Get ticket number for each position
          if(PositionSelectByTicket(ticket)){            //--- Select position by ticket
             if(PositionGetInteger(POSITION_MAGIC) == MagicNumber){ //--- Check if position belongs to this EA
                if(!obj_Trade.PositionClose(ticket))        //--- Attempt to close position
                  Print("Failed to close position ", ticket, ": ", obj_Trade.ResultRetcodeDescription()); //--- Print error if closing fails
                else
                  Print("Closed position ", ticket);    //--- Confirm position closed
             }
          }
       }
    }
      
    //+------------------------------------------------------------------+
    //| Function: CancelPendingOrders                                    |
    //| Purpose: Cancel all pending orders with the set magic number     |
    //+------------------------------------------------------------------+
    void CancelPendingOrders(){
       int totalOrders = OrdersTotal();                 //--- Get total number of pending orders
       for(int i = totalOrders - 1; i >= 0; i--){         //--- Loop through orders in reverse order
          ulong ticket = OrderGetTicket(i);              //--- Get ticket number for each order
          if(OrderSelect(ticket)){                       //--- Select order by ticket
             int type = (int)OrderGetInteger(ORDER_TYPE); //--- Retrieve order type
             if(OrderGetInteger(ORDER_MAGIC) == MagicNumber && //--- Check if order belongs to this EA
                (type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_SELL_STOP)){
                if(!obj_Trade.OrderDelete(ticket))         //--- Attempt to delete pending order
                  Print("Failed to cancel pending order ", ticket); //--- Print error if deletion fails
                else
                  Print("Canceled pending order ", ticket); //--- Confirm pending order canceled
             }
          }
       }
    }
    

    Здесь, в функции "CloseOpenPositions", мы сначала получаем общее количество открытых позиций, используя функцию PositionsTotal, а затем перебираем каждую позицию в обратном порядке. Для каждой позиции мы получаем соответствующий номер тикета, используя PositionGetTicket, и выбираем позицию с помощью PositionSelectByTicket. Затем проверяем, соответствует ли значение позиции POSITION_MAGIC нашему пользовательскому "MagicNumber", чтобы убедиться, что оно принадлежит нашему советнику. Если это так, пытаемся закрыть позицию, используя функцию "obj_Trade.PositionClose" и вывести сообщение с подтверждением или сообщение об ошибке (используя "obj_Trade.ResultRetcodeDescription") в зависимости от результата.

    В функции "CancelPendingOrders" сначала извлекаем общее количество отложенных ордеров с помощью функции OrdersTotal и перебираем их в обратном порядке. Для каждого ордера получаем соответствующий тикет с помощью OrderGetTicket и выбираем его с помощью OrderSelect. Затем проверяем, соответствует ли значение ORDER_MAGIC нашему "MagicNumber" и является ли его тип "ORDER_TYPE_BUY_STOP" или ORDER_TYPE_SELL_STOP. Если оба условия выполнены, пытаемся отменить ордер, используя функцию "obj_Trade.OrderDelete", выводя либо сообщение об успешном завершении, либо сообщение об ошибке в зависимости от того, завершилась ли отмена успешно. Запустив программу, получаем следующие результаты.

    STRATEGY GIF

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


    Тестирование на истории и оптимизация

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

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

    GRAPH 1

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

    //+------------------------------------------------------------------+
    //|        FUNCTION TO APPLY TRAILING STOP                           |
    //+------------------------------------------------------------------+
    void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){
       double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions
       double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions
    
       for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions
          ulong ticket = PositionGetTicket(i);          //--- Get position ticket
          if (ticket > 0){                              //--- If ticket is valid
             if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY &&
                   buySL > PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (buySL > PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met
                   trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP));
                }
                else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL &&
                   sellSL < PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (sellSL < PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met
                   trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP));
                }
             }
          }
       }
    }
    
    //---- CALL THE FUNCTION IN THE TICK EVENT HANDLER
    
    if (PositionsTotal() > 0){                       //--- If there are open positions
       applyTrailingSTOP(30*_Point,obj_Trade,0);  //--- Apply a trailing stop
    }
    

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

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

    ГРАФИК БЭК-ТЕСТИРОВАНИЯ

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

    РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ НА ИСТОРИИ


    Заключение

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

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

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

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

    Прикрепленные файлы |
    От начального до среднего уровня: События (II) От начального до среднего уровня: События (II)
    В этой статье мы увидим, что не всегда нужно реализовывать всё каким-то определенным образом. Существуют альтернативные способы решения проблем. Для правильного понимания этой статьи необходимо понять концепции, описанные в предыдущих статьях. Представленные здесь материалы предназначены исключительно для образовательных целей. Не надо рассматривать его как окончательное приложение, целью которого не является изучение представленных здесь концепций.
    Создание вероятностного рыночно-нейтрального робота на основе распределения доходностей Создание вероятностного рыночно-нейтрального робота на основе распределения доходностей
    Рыночно-нейтральная торговая стратегия на основе эмпирического распределения доходностей представляет альтернативу классическим методам технического анализа, заменяя прогнозирование направления цены статистическим размещением ордеров в точках вероятного достижения. Статья подробно разбирает математический аппарат расчета перцентилей, алгоритмы взвешивания объемов позиций по вероятности срабатывания и механизмы адаптации к изменению рыночных условий через экспирацию сетки. Приводится полная реализация на MQL5.
    Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Интеграция спайков) Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Интеграция спайков)
    В статье представлена практическая реализация ключевых компонентов фреймворка SEW-ResNet средствами MQL5. Использование динамических массивов и спайковых механизмов позволяет гибко строить архитектуру модели и эффективно обрабатывать финансовые временные ряды. Предложенные решения показывают, как SEW-ResNet может оптимизировать вычисления и улучшить выделение значимых признаков.
    От начального до среднего уровня: События (I) От начального до среднего уровня: События (I)
    Учитывая всё, что ,было показано до настоящего момента, я думаю, что теперь мы можем начать реализовывать некое приложение для запуска какого-либо символа непосредственно на графике. Однако сначала нам нужно поговорить о довольно запутанном понятии для новичков, а именно о том, что приложения, разработанные на MQL5 и предназначенные для отображения на графике, создаются не так, как мы видели до сих пор. В этой статье мы начнем разбираться в этом немного лучше.