Español
preview
Реализация частичного закрытия позиций в MQL5

Реализация частичного закрытия позиций в MQL5

MetaTrader 5Торговые системы |
61 0
Niquel Mendoza
Niquel Mendoza


Введение в частичное закрытие позиций: преимущества и недостатки

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

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

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

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

Возможные недостатки

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

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

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


Интегрируем частичное закрытие позиций в MQL5

Для интеграции частичного закрытия позиций в MQL5 можно использовать простой способ: задать уровни Take Profit, по достижении которых будет закрываться определенный процент текущего объема позиции. Например, закрытие 30% объема при достижении ценой установленного уровня.

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

Определение уровней Take Profit
Будем называть "уровнем Take Profit" цену, при достижении которой выполняется частичное закрытие позиции.

  • В позиции на покупку уровень должен быть выше цены открытия, но ниже начального уровня Take Profit.
  • В позиции на продажу уровень должен быть ниже цены открытия, но выше начального уровня Take Profit.

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

Figure1_Partials_Example

 

Рисунок 1. Пример уровней частичного закрытия

Расчет уровней
Для расчета уровней, таких как tp1, tp2 и tp3, можно использовать процентное соотношение расстояния между ценой входа и начальным уровнем Take Profit. Например:

  • tp1 — 30% этого расстояния
  • tp2 — 60% этого расстояния
  • tp3  90% этого расстояния 

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

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

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

Новые изменения в управлении рисками
Я внес ряд важных изменений в архитектуру управления рисками. Ниже я объясню наиболее важные моменты.

1. Использование CLoggerBase

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

2. Библиотека Utils

Была создана библиотека с названием Utils. Ее назначение — объединить утилиты, которые можно использовать в советниках, библиотеках и проектах в целом. Она состоит из нескольких файлов .mqh, которые содержат:

  • классы (контроль открытия бара, ATR, расчет минимальной разницы и другие),
  • функции (время, простые математические вычисления, обработка строк, преобразования, преобразование массивов в строки, сортировка, операции с массивами и т. д.).

3. Реструктуризация управления рисками

Ранее управление рисками было сосредоточено в одном файле. Теперь оно разделено на несколько модулей для улучшения структуры и расширяемости:

  • AccountStatus.mqh  модуль, отвечающий за вызов функций объектов CAccountGestor,
  • LossProfit.mqh  реализация класса CLossProfit, предназначенного для расчета и проверки превышения «максимальной прибыли или убытка»,
  • LoteSizeCalc.mqh  модуль для расчета размера лота,
  • Modificators.mqh  модуль, предназначенный для модификаторов "максимальной прибыли или убытка",
  • OcoOrder.mqh  базовая реализация ордеров OCO,
  • OrdersGestor.mqh  реализация класса COrderGestor, который управляет отложенными ордерами,
  • RiskManagement.mqh  основной модуль, в котором реализован класс CRiskManagement,
  • RiskManagementBases.mqh  содержит базовый класс CRiskManagementBases,
  • RM_Defines.mqh  определения структур, макросов и перечислений, используемых другими модулями,
  • RM_Functions.mqh  набор вспомогательных функций, используемых указанными выше модулями.

Единственными модулями, которые не участвуют напрямую в CRiskManagement, являются OrdersGestor и OcoOrder.

4. AccountStatus и CAccountGestor

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

class CAccountGestor : public CSpecializedManager
 {
public:
  virtual void       OnOpenClosePosition(const ROnOpenClosePosition &pos) = 0; //Function that will be executed each time a position is opened or closed
  virtual void       OnOrderDelete(const ROnOrderDelete& order) { } //Function that will be executed each time an order is closed, by deletion, expiration, etc.

  //-- Function that is executed only once, where only the account profit fields are filled, such as account_gross_profit
  //daily, weekly, etc.
  virtual void       OnNewProfit(const ROnOpenClosePosition &profit) { }

  //--- Function that is executed each time TesterDeposit or TesterWithdrawal is called... or capital is added to the account
  virtual void       OnWithdrawalDeposit(double value) { }  //If the value is positive it means a deposit, otherwise a withdrawal

  //-- Function that is executed every new day
  virtual void       OnNewDay(datetime init_time) { }

  //-- Function that will be executed every new week
  virtual void       OnNewWeek(datetime init_time) { }

  //-- Function that is executed every new month
  virtual void       OnNewMonth(datetime init_time) { }

  //--- Function that is executed only once, only if there are previously open trades, only the position structure
  virtual void       OnInitNewPos(const ROnOpenClosePosition &pos) { }
 };

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

5. Modificators.mqh и CExtraModifications

Новым изменением является добавление модуля Modificators.mqh. В предыдущих статьях по управлению рисками он не был реализован. Класс CExtraModifications позволяет создавать модификаторы для "максимального убытка или прибыли".

Например, можно установить, что каждый раз при получении прибыли максимально допустимое значение прибыли или убытка (GMLPO) удваивается. В случае же убытка, это значение сбрасывается до исходного состояния.

//+--------------------------------------------------------------------+
//| Class to integrate external modifications to risk management       |
//+--------------------------------------------------------------------+
class CExtraModifications : public CLoggerBase
 {
protected:
  CLossProfit*       m_modifier;
  ENUM_TYPE_LOSS_PROFIT m_property_to_modify;
  string             m_modifier_name;

public:
                     CExtraModifications(ENUM_TYPE_LOSS_PROFIT _property_to_modify = WRONG_VALUE) { m_property_to_modify = _property_to_modify; }

  //--- Non-modifiable functions
  // Function that returns the type of "maximum loss or profit" that this class is modifying
  inline ENUM_TYPE_LOSS_PROFIT MaximumProfitOrLossAModify() const { return m_property_to_modify; };

  // Function that will be used in CRiskManagement to assign the "maximum loss or profit" based on the type of "maximum loss or profit" chosen
  // in the constructor.
  void               SetPointer(CLossProfit* _modifier);

  // Name of the custom modifier (to differentiate from others)
  inline string      Name() const { return m_modifier_name; }

  //--- Virtual functions
  // Function that will be executed each time a position is closed
  virtual void       OnClosePosition(ModifierOnOpenCloseStruct &on_open_close_struct) {  }

  // Function that will be executed each time an operation is opened
  virtual void       OnOpenPosition(ModifierOnOpenCloseStruct &on_open_close_struct) {  }

  // Function that will be executed only once (at the moment of creating the m_modifier)
  virtual void       OnInitModifier(ModfierInitInfo &on_init) {  }

  // Function that will be executed each new day
  virtual void       OnNewDay() { }
 };

На этой основе можно реализовать пользовательские классы, такие как CDynamicRisk. Этот класс динамически регулирует риск, используя различные методы: умножение, экспоненциальный расчет или суммирование.

//+------------------------------------------------------------------+
//| Clase para aumentar el riesgo                                    |
//+------------------------------------------------------------------+
enum ENUM_MULTIPLIER_METHOD_DR
 {
  DR_MULTIPLIER = 0,  // Multiplier
  DR_EXPONECIAL = 1,  // Exponential
  DR_SUMATORIO = 2    // Additive (Summation)
 };

//--- Clase
class CDynamicRisk : public CExtraModifications
 {
private:
  double             paso;
  double             val;
  double             percentage_a_empezar_modfiicacioneS;
  ENUM_MULTIPLIER_METHOD_DR metod;
  void               Aumentar();
  bool               is_set;

public:
                     CDynamicRisk(double step, double percentage_to_empezar, ENUM_TYPE_LOSS_PROFIT property_to_modify, ENUM_MULTIPLIER_METHOD_DR multiplier_);
  void               OnClosePosition(ModifierOnOpenCloseStruct &on_open_close_struct) override;
  void               OnInitModifier(ModfierInitInfo &on_init) override;
 };
//+------------------------------------------------------------------+
CDynamicRisk::CDynamicRisk(double step, double percentage_to_empezar, ENUM_TYPE_LOSS_PROFIT property_to_modify, ENUM_MULTIPLIER_METHOD_DR multiplier_)
  : CExtraModifications(property_to_modify)
 {
  m_modifier_name = "Risk enhancer by default";
  paso = step;
  percentage_a_empezar_modfiicacioneS = percentage_to_empezar;
  metod = multiplier_;
  is_set = false;

  if((int)metod > 2 || metod < 0)
   {
    LogFatalError(StringFormat("The method %s is invalid", EnumToString(metod)), FUNCION_ACTUAL);
    Remover();
   }

 }
//+------------------------------------------------------------------+
void CDynamicRisk::OnInitModifier(ModfierInitInfo & on_init)
 {
  percentage_a_empezar_modfiicacioneS /= 100.0;
  percentage_a_empezar_modfiicacioneS *= on_init.balance;
  Print("Balance to increase risk: ", percentage_a_empezar_modfiicacioneS);
  val = m_modifier.GetPercentage();
//Print("Valor del porcentaje: ", val);
 }
//+------------------------------------------------------------------+
void CDynamicRisk::OnClosePosition(ModifierOnOpenCloseStruct & on_open_close_struct)
 {
  if(on_open_close_struct.position.profit > 0 && on_open_close_struct.profit_total > percentage_a_empezar_modfiicacioneS)
   {
    LogInfo(StringFormat("By increasing the risk, a profit of %.2f has been obtained. The initial balance has been exceeded by %.2f.",
                         on_open_close_struct.position.profit, percentage_a_empezar_modfiicacioneS), FUNCION_ACTUAL);
    Aumentar();
    m_modifier.SetPercentage(val);
   }
  else
   {
    m_modifier.SetInitialPercentageOrMoney();
    val = m_modifier.GetPercentage();
   }
 }
//+------------------------------------------------------------------+
void CDynamicRisk::Aumentar(void)
 {
  switch(metod)
   {
    case DR_MULTIPLIER:
      val *= paso;
      break;

    case DR_EXPONECIAL:
      val *= MathExp(paso);
      break;

    case DR_SUMATORIO:
      val += paso;
      break;

    default:
      Remover();
      break;
   }
 }

В суммарном методе, например, к текущему проценту «максимального убытка или прибыли» добавляется значение step.


Создание класса CPartials

Перед написанием кода класса включим модуль управления рисками.

#include  "..\\RM\\RiskManagement.mqh"

Для применения частичного закрытия позиций необходимо определять, когда открываются и закрываются сделки. Для этого класс CPartials будет наследоваться от CAccountGestor, что позволит нам использовать уже определенные события, которые затем будут вызываться из AccountStatus.

//+------------------------------------------------------------------+
//| Class that implements partial closures                           |
//+------------------------------------------------------------------+
class CPartials : public CAccountGestor

Приватные переменные

Общие переменные

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

Кроме того, нам потребуется экземпляр CTrade, который обеспечит доступ к функции PositionClosePartial. Также добавим логическую переменную (bool) в качестве флага состояния, указывающего, может ли класс выполняться (true) или он отключен (false).

  //--- General variables
  ulong              m_magic_number;          // Magic number used to apply partials only to certain operations
  CTrade             m_trade_executor;        // CTrade type instance to apply partials
  bool               m_disable_partials_flag; // Boolean that indicates if partials can be executed

Переменные символа

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

  //--- Symbol variables
  string             m_trading_symbol;     // Symbol from which minimum volume and volume step data will be obtained
  double             m_volume_step;        // Volume step
  double             m_minimum_volume;     // Minimum volume
  int                m_price_digits;       // Symbol digits
  MqlTick            m_latest_tick;        // MqlTick type structure

Структуры для частичного закрытия позиций
Сначала мы определяем структуру PartialTakeProfit, которая представляет уровень частичного закрытия позиции:

struct PartialTakeProfit
 {
  double             partial_price;             // Price where the partial will be executed
  double             tp_level_percentage;        // Percentage of TP level
  double             volume_percentage_to_close; // Percentage of volume to close

  inline void Reset()
   {
    ZeroMemory(this);
   }
 };

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

struct TrackedPosition
 {
  PartialTakeProfit  tp_levels[];             // Array of partial levels
  ulong              ticket;                  // Position ticket
  ENUM_POSITION_TYPE type;                    // Position type
  int                current_partial_index;   // Next partial to be applied
 };

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

  //--- Variables for partials
  int                m_max_partial_levels;  // Number of partials to take from each operation
  TrackedPosition    m_tracked_positions[]; // Array of operations to which partials will be applied
  int                m_indices_to_remove[]; // Indices to remove from the m_tracked_positions array

Кроме того, для применения частичного закрытия позиций необходимо хранить два массива:

  1. процент объема, подлежащего закрытию,
  2. проценты уровней Take Profit, на которых будет выполняться частичное закрытие.
      //--- Arrays to take, both arrays must be the same size.
      double             m_volume_percentages_to_close[];
      double             m_tp_level_percentages[];

    Временный HashMap
    Наконец, мы объявляем CHashMap. Его функция заключается в упорядочивании уровней Take Profit и облегчении поиска следующего уровня для проверки. Идея заключается в том, чтобы этот "внутренний указатель" передвигался вперед каждый раз при выполнении частичного закрытия, подобно указателю чтения в файле.

      //--- Temporary hashmap
      CHashMap<double, double> m_temporary_hashmap;
    

    В следующем разделе будет объяснено, как этот CHashMap будет использоваться в логике выполнения.

    Конструктор и деструктор
    Начнем с определения конструктора, в котором инициализируем переменные-члены класса значениями по умолчанию.

    CPartials::CPartials()
      : m_magic_number(NOT_MAGIC_NUMBER), m_disable_partials_flag(true), m_trading_symbol(NULL), m_volume_step(0.00),
        m_minimum_volume(0.00), m_price_digits(0), m_max_partial_levels(0)
     {
    
     }

    Деструктор оставляем пустым.

    //+------------------------------------------------------------------+
    CPartials::~CPartials()
     {
    // Empty destructor
     }


    Объявление и определение функций класса CPartials

    В этом разделе мы продолжим реализацию класса CPartials, объявляя и определяя его функции.

    Первая функция, которая нам нужна, — Init, отвечающая за инициализацию класса и установку основных параметров.

    Функция Init
    В качестве параметров она будет принимать:

    • magic_number_ — магический номер для фильтрации сделок,
    • symbol_ — символ, к которому будет применяться частичное закрытие позиций,
    • volume_percentages_string — строка с указанием процентного объема закрытия на каждом уровне,
    • tp_percentages_string — строка с уровнями Take Profit в процентах от расстояния между ценой открытия и уровнем Take Profit.

    Например:

    • Процентные уровни Take Profit: "20,40,60" — уровни на 20%, 40% и 60% расстояния между ценой открытия и Take Profit.
    • Объем, подлежащий закрытию: "30,30,30" на каждом уровне будет закрыто 30% от текущего объема.

    Если начальный объем составлял 0,90 лота, то при достижении первого уровня Take Profit (20%) будет закрыто 0,27 лота. После выполнения этого действия индекс current_partial_index перейдет к следующему уровню Take Profit.

    Объявление функции выглядит следующим образом:

      //--- Initialize function
      bool               Init(ulong magic_number_, string symbol_, string volume_percentages_string, string tp_percentages_string);
        
    

    Начальные определения
    Определяем константу, указывающую, что частичное закрытие применяться не будет (значение "0").

    #define PARTIAL_NO_APPLIED "0"
    

    Полная функция начинается с проверки строк:

    bool CPartials::Init(ulong magic_number_, string symbol_, string volume_percentages_string, string tp_percentages_string)
     {
    //--- Validate if the strings are valid
      if(volume_percentages_string == PARTIAL_NO_APPLIED  || tp_percentages_string == PARTIAL_NO_APPLIED) // "0" as an invalid value
       {
        m_disable_partials_flag = true;
        return false;
       }

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

    //--- Set the variables
      m_disable_partials_flag = false;
      m_magic_number = magic_number_;
      m_trading_symbol = symbol_;
      m_volume_step = SymbolInfoDouble(m_trading_symbol, SYMBOL_VOLUME_STEP);
      m_minimum_volume = SymbolInfoDouble(m_trading_symbol, SYMBOL_VOLUME_MIN);
      m_price_digits = (int)SymbolInfoInteger(m_trading_symbol, SYMBOL_DIGITS);
      m_trade_executor.SetExpertMagicNumber(m_magic_number);
        
    

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

    #define CPARCIAL_RESERVE_ARR 5
    #define CPARCIAL_RESERVE_TO_DELETE 5

    Код.

    //--- Initial resize
      ArrayResize(m_tracked_positions, 0, CPARCIAL_RESERVE_ARR);
      ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE);
    

    Преобразование строк в массивы
    Используем пространство имен StrTo из нашей библиотеки Utils\FA\StringToArray.mqh, которое содержит функции для преобразования текстовых строк в массивы требуемого типа (функции перегружены, поэтому нужная функция определяется во время компиляции).

    //--- Convert strings to double arrays
      StrTo::CstArray(m_volume_percentages_to_close, volume_percentages_string, ',');
      StrTo::CstArray(m_tp_level_percentages, tp_percentages_string, ',');
    

    Проверка массивов
    Сначала проверяем, что массивы не пустые, а затем — что они одинакового размера:

    //--- 2nd validation, we validate that the array size is valid
      if(m_volume_percentages_to_close.Size() < 1 || m_tp_level_percentages.Size() < 1)
       {
        LogCriticalError(StringFormat("The size of the partial arrays to take %u or percentages to take %u are invalid", m_volume_percentages_to_close.Size(), m_tp_level_percentages.Size()), FUNCION_ACTUAL);
        m_disable_partials_flag = true;
        return false;
       }
    
    //--- 3rd validation, we validate that both arrays are the same size
      if(m_volume_percentages_to_close.Size() != m_tp_level_percentages.Size())
       {
        LogCriticalError(StringFormat("The size of the partial arrays to take %u and percentages to take %u are not equal", m_volume_percentages_to_close.Size(), m_tp_level_percentages.Size()), FUNCION_ACTUAL);
        m_disable_partials_flag = true;
        return false;
       }
    
    

    Если включен вывод сообщений типа info, выводим в журнал начальное состояние массивов.

    //--- Initial log only if "info" type log is enabled
      if(IsInfoLogEnabled())
       {
        FastLog(FUNCION_ACTUAL, INFO_TEXT, "Arrays before revision");
        PrintArrayAsTable(m_tp_level_percentages, "Percentage of position where partials will be taken", "percentages simplified");
        PrintArrayAsTable(m_volume_percentages_to_close, "Percentage to take", "percentages simplified");
       }

    Очистка массивов и добавление в HashMap
    На этом этапе вступает в игру HashMap, который функционирует как временный контейнер пар "ключ-значение" для хранения только допустимых данных. В дальнейшем мы будем использовать значения ключей (key) для получения значений (value).

    Использование HashMap обусловлено следующей проблемой: если уровни Take Profit отсортировать, это приведет к потере соответствия с массивом процентов объема, подлежащего закрытию.

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

    • Ключ (key)  процент уровня Take Profit
    • Значение (value) — процент объема, подлежащего частичному закрытию

    Если в процессе обнаруживаются недопустимые или дублирующиеся значения (в массиве "процентных уровней Take Profit"), их индексы сохраняются во временном массиве indices_to_remove_temp. Затем эти элементы удаляются из массива значений "процентных уровней Take Profit", после чего массив сортируется в порядке возрастания.

    //--- Declaration of indices to remove and hashmap cleanup
      m_temporary_hashmap.Clear();
      int indices_to_remove_temp[];
    
    //--- Iteration and array cleanup
      for(int i = 0; i < ArraySize(m_tp_level_percentages); i++)
       {
        if(m_tp_level_percentages[i] <= 0.00)
         {
          LogWarning(StringFormat("The percentage where partials will be taken %f with index %d is invalid, therefore it will not be considered in partials", m_tp_level_percentages[i], i), FUNCION_ACTUAL);
          AddArrayNoVerification(indices_to_remove_temp, i, 0);
          continue;
         }
    
        if(m_volume_percentages_to_close[i] <= 0.00)
         {
          LogWarning(StringFormat("The percentage to take %f with index %d is invalid, therefore it will not be considered in partials", m_volume_percentages_to_close[i], i), FUNCION_ACTUAL);
          AddArrayNoVerification(indices_to_remove_temp, i, 0);
          continue;
         }
    
        if(m_temporary_hashmap.ContainsKey(m_tp_level_percentages[i]))
          AddArrayNoVerification(indices_to_remove_temp, i, 0);
        else
          m_temporary_hashmap.Add(m_tp_level_percentages[i], m_volume_percentages_to_close[i]);
       }
    
    //--- Sort and removal
      RemoveMultipleIndexes(m_tp_level_percentages, indices_to_remove_temp, 0);
      ArraySort(m_tp_level_percentages);
      ArrayResize(m_volume_percentages_to_close, ArraySize(m_tp_level_percentages));

    Преобразование и нормализация значений
    На этом этапе:

    • преобразуем проценты в доли (делением на 100),
    • перезаписываем массивы, используя допустимые значения, полученные из HashMap.

    //--- Final iteration
      for(int i = 0; i < ArraySize(m_tp_level_percentages); i++)
       {
        double val;
        m_temporary_hashmap.TryGetValue(m_tp_level_percentages[i], val);
    
        m_volume_percentages_to_close[i] = val / 100.0;
        m_tp_level_percentages[i] /= 100.0;
       }

    Финальный вывод в журнал и максимальное количество частичных закрытий
    В завершение выводим в журнал скорректированные массивы и задаем максимальное количество частичных закрытий для одной позиции:

    //--- Final log
      if(IsInfoLogEnabled())
       {
        FastLog(FUNCION_ACTUAL, INFO_TEXT, "Arrays after revision");
        PrintArrayAsTable(m_tp_level_percentages, "Percentage of position where partials will be taken", "percentages simplified");
        PrintArrayAsTable(m_volume_percentages_to_close, "Percentage to take", "percentages simplified");
       }
    
      m_max_partial_levels = ArraySize(m_volume_percentages_to_close);
      return true;
     }
    

    Функции для добавления позиций в отслеживание в классе CPartials
    Для применения частичного закрытия к открытым позициям, необходимо сначала добавить эти позиции во внутренний массив m_tracked_positions.

    Определим две функции.

    1. AddPositionToTrack (публичная):

    • позволяет добавлять позиции в отслеживание;
    • Используется как автоматически, при открытии сделок (при выполнении условий), так и вручную, например, из панели, где пользователь выбирает, для каких позиций будет доступно частичное закрытие.  

         2. AddToTrackedPositions (приватная):

    • вспомогательная функция, которая напрямую добавляет объект TrackedPosition во внутренний массив;
    • вызывается из AddPositionToTrack.

    Публичная функция AddPositionToTrack
    Объявление в публичной секции класса:

      //--- Function to add a position to the internal array manually (this is automatically invoked in OnOpen..... for all positions
      // opened with the magic number and with a valid tp).
      void               AddPositionToTrack(ulong position_ticket, double position_tp, double entry_price, ENUM_POSITION_TYPE position_type);
        
    

    Тело функции:

    //+------------------------------------------------------------------+
    //| Adds a position with a set of variables to the internal array    |
    //| Inputs:                                                          |
    //| - position_ticket: position ticket.                              |
    //| - position_tp: position takeprofit.                              |
    //| - entry_price: position entry price.                             |
    //| - position_type: position type (buy or sell)                     |
    //|                                                                  |
    //| Outputs:                                                         |
    //| - The function returns nothing.                                  |
    //|                                                                  |
    //| Notes:                                                           |
    //| - The mentioned properties can be consulted with the native      |
    //|   function: PositionGetDouble(...);                              |
    //| - The position must have a valid tp.                             |
    //| - The function does not check if the ticket is valid, so if      |
    //|   you manually introduce an operation, its ticket must be valid. |
    //+------------------------------------------------------------------+
    void CPartials::AddPositionToTrack(ulong position_ticket, double position_tp, double entry_price, ENUM_POSITION_TYPE position_type)
     {
      if(m_disable_partials_flag)
        return;
    
    //--- Initial check, we verify that the tp is valid
      if(position_tp <= 0.00000000000001)
       {
        LogError(StringFormat("The take profit with a value of %f from position %I64u is less than or equal to 0", position_tp, position_ticket), FUNCION_ACTUAL);
        return;
       }
    
    //---
      TrackedPosition new_tracked_position;
      new_tracked_position.ticket = position_ticket;
      new_tracked_position.type = position_type;
      new_tracked_position.current_partial_index = 0;
      ArrayResize(new_tracked_position.tp_levels, ArraySize(m_volume_percentages_to_close));
    
      for(int i = 0; i < ArraySize(new_tracked_position.tp_levels); i++)
       {
        new_tracked_position.tp_levels[i].Reset();
        new_tracked_position.tp_levels[i].tp_level_percentage = m_tp_level_percentages[i];
        new_tracked_position.tp_levels[i].volume_percentage_to_close = m_volume_percentages_to_close[i];
    
        if(position_type == POSITION_TYPE_BUY)
         {
          double calculated_partial_price = entry_price + ((position_tp - entry_price) * m_tp_level_percentages[i]);
          new_tracked_position.tp_levels[i].partial_price = calculated_partial_price;
         }
        else
         {
          double calculated_partial_price = entry_price - ((entry_price - position_tp) * m_tp_level_percentages[i]);
          new_tracked_position.tp_levels[i].partial_price = calculated_partial_price;
         }
       }
    
    //--- Log about the "tps" where partials will be taken
      if(IsCautionLogEnabled())
       {
        FastLog("TPS: ", CAUTION_TEXT, FUNCION_ACTUAL);
        ArrayPrint(new_tracked_position.tp_levels, m_price_digits, " | ");
       }
    
    //--- Add to internal array
      AddToTrackedPositions(new_tracked_position);
     }
        
    

    Объяснение логики работы

    1. Начальная проверка: если частичное закрытие отключено (m_disable_partials_flag), никаких действий не выполняется.
    2. Проверка Take Profit: уровень Take Profit позиции должен быть больше нуля.
    3. Создание объекта TrackedPosition: инициализируются данные о позиции, и резервируются уровни частичного закрытия (tp_levels).
    4. Расчет цен для частичного закрытия: для каждого уровня определяется цена, по достижению которой будет выполнено частичное закрытие.
    5. В позициях на покупку (POSITION_TYPE_BUY) цена частичного закрытия рассчитывается путем сложения процента расстояния между entry_price и Take Profit.
    6. В позициях на продажу (POSITION_TYPE_SELL)  путем вычитания.
    7. Вывод сообщений в журнал: необязательно, выводятся настроенные уровни Take Profit.
    8. Добавление во внутренний массив: вызывается приватная функция AddToTrackedPositions.

    Приватная функция AddToTrackedPositions
    Объявление в приватной секции класса:

      //--- Function to add a new partial to the internal array
      void               AddToTrackedPositions(TrackedPosition & new_tracked_position);
    

    Определение:

    void CPartials::AddToTrackedPositions(TrackedPosition &new_tracked_position)
     {
      AddArrayNoVerification2(m_tracked_positions, new_tracked_position, CPARCIAL_RESERVE_ARR)
     }

    Макрос AddArrayNoVerification2 (определенный в Utils\FA\FuncionesBases.mqh) отвечает за добавление нового элемента new_tracked_position в конец динамического массива без дополнительных проверок и с резервированием при необходимости.

    Обнаружение открытия и закрытия позиций в CPartials
    Как было показано при определении класса CAccountGestor, у нас есть несколько функций, которые можно переопределить для обработки событий счета (открытие, закрытие, удаление ордеров и т. д.).

    В нашем случае нам нужно реализовать только функцию OnOpenClosePosition, поскольку именно она автоматически выполняется каждый раз при открытии или закрытии сделки.

    Объявление в классе
    В публичной секции класса CPartials объявляем:

      //--- Function that will be automatically invoked by account status each time a position is opened or closed
      void               OnOpenClosePosition(const ROnOpenClosePosition &pos) override;
    
    

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

    Если сделка закрывается, мы вызываем функцию RemoveIndexFromAnArrayOfPositions, которая удалит позицию, если та существует, из массива m_tracked_positions.

    //+--------------------------------------------------------------------+
    //| Function that runs every time a position is opened or closed       |
    //+--------------------------------------------------------------------+
    void CPartials::OnOpenClosePosition(const ROnOpenClosePosition &pos)
     {
      if(m_disable_partials_flag)
        return;
    
    //---
      if(pos.deal_entry_type == DEAL_ENTRY_OUT)
       {
        // Once the position is closed we verify if that ticket is in the internal array, if so the function
        // will remove it.
        RemoveIndexFromAnArrayOfPositions(m_tracked_positions, pos.position.ticket, CPARCIAL_RESERVE_ARR);
       }
      else
        if(pos.deal_entry_type  == DEAL_ENTRY_IN && (pos.position.magic == m_magic_number || m_magic_number == NOT_MAGIC_NUMBER))
         {
          LogInfo(StringFormat("A trade has just been opened with ticket %I64u", pos.position.ticket), FUNCION_ACTUAL);
          AddPositionToTrack(pos.position.ticket, pos.position.first_tp, pos.position.open_price, pos.position.type);
         }
     }

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

    Разработка функции для частичного закрытия позиций
    В публичной секции класса объявляем основную функцию проверки позиций.

    Эта функция не принимает параметров и не возвращает значений.

      //--- Main function for position review
      void               CheckTrackedPositions();
    
    

    Теперь определим тело функции. Первым делом реализуем две проверки для досрочного выхода из функции.

    Первая проверяет, активен ли флаг m_disable_partials_flag. Если да, выполняем return и предотвращаем выполнение остальной части кода.

    Вторая проверяет, нет ли открытых позиций. В этом случае также выполняем return, чтобы не выполнять обход массива m_tracked_positions.

    //--- Initial check to verify not to execute the function if partials are not allowed
      if(m_disable_partials_flag)
        return;
    
    //--- If the internal array m_partials is empty we do an early exit.
      if(m_tracked_positions.Size() < 1)
        return;

    Далее используем функцию SymbolInfoTick для получения последнего тика и сохраняем его в приватную переменную m_latest_tick.

    Затем подготовим массив, который будет использоваться для хранения индексов позиций, которые необходимо удалить. Для этого мы резервируем пять элементов и инициализируем массив нулями.

    //---
      SymbolInfoTick(m_trading_symbol, m_latest_tick);
      ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE);
    
    

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

    Если это условие верно, выполняем частичное закрытие позиции и смещаем внутренний указатель (index++) к следующему уровню Take Profit. Этот процесс представлен на иллюстрациях ниже.

    Для позиций на покупку

    Алгоритм частичного закрытия длинной позиции

    Рисунок 2. Алгоритм частичного закрытия для позиций на покупку

    Для позиций на продажу

    Алгоритм частичного закрытия короткой позиции

    Рисунок 3. Алгоритм частичного закрытия для позиций на продажу

    Первым шагом будет определение того, является ли позиция покупкой или продажей. Для этого используем член type структуры TrackedPosition и сравниваем его со значениями POSITION_TYPE_BUY или POSITION_TYPE_SELL.

    //--- Main iteration
      for(int i = 0; i < ArraySize(m_tracked_positions); i++)
       {
        const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket
    
        if(m_tracked_positions[i].type == POSITION_TYPE_BUY)
         {
      
         }
        else
          if(m_tracked_positions[i].type == POSITION_TYPE_SELL)
           {
       
           }
       }

    Внутри этих блоков (тела операторов if) добавляется соответствующая логика. Первым шагом в этом блоке является проверка, превысила ли цена Bid или Ask значение partial_price. Если условие выполняется, выбираем тикет позиции.

      for(int i = 0; i < ArraySize(m_tracked_positions); i++)
       {
        const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket
    
        if(m_tracked_positions[i].type == POSITION_TYPE_BUY)
         {
          const int current_level_index = m_tracked_positions[i].current_partial_index;
          if(m_latest_tick.ask > m_tracked_positions[i].tp_levels[current_level_index].partial_price)
           {
            // Failed to select the ticket
            if(!PositionSelectByTicket(current_ticket))
             {
              LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL);
              continue;
             }
           }
         }
        else
          if(m_tracked_positions[i].type == POSITION_TYPE_SELL)
           {
            const int current_level_index = m_tracked_positions[i].current_partial_index;
            if(m_latest_tick.bid < m_tracked_positions[i].tp_levels[current_level_index].partial_price)
             {
              if(!PositionSelectByTicket(current_ticket))
               {
                LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL);
                continue;
               }
             }
           }
       }

    Если удалось правильно выбрать тикет позиции, рассчитываем объем, который должен быть закрыт.

    const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current operation volume
    double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume that will be "removed" from the position
    

    Если рассчитанный объем меньше m_minimum_volume, частичное закрытие игнорируется, внутренний указатель смещается, и если уровней больше не осталось, позиция удаляется из m_tracked_positions (она добавляется во временный массив m_indices_to_remove для последующего удаления).

    if(volume_to_close < m_minimum_volume)
     {
      m_tracked_positions[i].current_partial_index += 1;
      LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL);
      if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached
       {
        // Remove the partial
        AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
       }
     continue;
     }
    

    Если объем допустимый, выполняем частичное закрытие с этим значением, внутренний указатель увеличивается, и, если уровней больше не осталось, позиция также удаляется.

    volume_to_close = RoundToStep(volume_to_close, m_volume_step);
    m_trade_executor.PositionClosePartial(current_ticket, volume_to_close);
    m_tracked_positions[i].current_partial_index += 1;
    
    if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached
     {
      // Remove the partial
      AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
     }

    В завершение индексы, хранящиеся во временном массиве, удаляются из основного массива.

    //---
      RemoveMultipleIndexes(m_tracked_positions, m_indices_to_remove, CPARCIAL_RESERVE_ARR);

    Теперь полный код функции выглядит следующим образом:

    //+------------------------------------------------------------------+
    //| Function that will iterate over the m_partials array             |
    //| and if necessary will partially close the operation              |
    //+------------------------------------------------------------------+
    void CPartials::CheckTrackedPositions(void)
     {
    //--- Initial check to verify not to execute the function if partials are not allowed
      if(m_disable_partials_flag)
        return;
    
    //--- If the internal array m_partials is empty we do an early exit.
      if(m_tracked_positions.Size() < 1)
        return;
    
    //---
      SymbolInfoTick(m_trading_symbol, m_latest_tick);
      ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE);
    
    //--- Main iteration
      for(int i = 0; i < ArraySize(m_tracked_positions); i++)
       {
        const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket
    
        if(m_tracked_positions[i].type == POSITION_TYPE_BUY)
         {
          const int current_level_index = m_tracked_positions[i].current_partial_index;
          if(m_latest_tick.ask > m_tracked_positions[i].tp_levels[current_level_index].partial_price)
           {
            // Failed to select the ticket
            if(!PositionSelectByTicket(current_ticket))
             {
              LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL);
              continue;
             }
    
            const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current operation volume
            double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume that will be "removed" from the position
    
            if(volume_to_close < m_minimum_volume)
             {
              m_tracked_positions[i].current_partial_index += 1;
              LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL);
              if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached
               {
                // Remove the partial
                AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
               }
              continue;
             }
    
            volume_to_close = RoundToStep(volume_to_close, m_volume_step);
            m_trade_executor.PositionClosePartial(current_ticket, volume_to_close);
            m_tracked_positions[i].current_partial_index += 1;
    
            if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached
             {
              // Remove the partial
              AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
             }
           }
         }
        else
          if(m_tracked_positions[i].type == POSITION_TYPE_SELL)
           {
            const int current_level_index = m_tracked_positions[i].current_partial_index;
            if(m_latest_tick.bid < m_tracked_positions[i].tp_levels[current_level_index].partial_price)
             {
              if(!PositionSelectByTicket(current_ticket))
               {
                LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL);
                continue;
               }
    
              const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current trade volume
              double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume to close
    
              if(volume_to_close < m_minimum_volume) // Volume too small
               {
                m_tracked_positions[i].current_partial_index += 1; // Continue with the next one
                LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL);
                if(m_tracked_positions[i].current_partial_index == m_max_partial_levels)
                 {
                  AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
                 }
                continue;
               }
    
              //---
              volume_to_close = RoundToStep(volume_to_close, m_volume_step);
              m_trade_executor.PositionClosePartial(current_ticket, volume_to_close); // Partial closure
              m_tracked_positions[i].current_partial_index += 1; // Move to next index
              if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Limit reached, remove this index
               {
                AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE)
               }
             }
           }
       }
    
    //---
      RemoveMultipleIndexes(m_tracked_positions, m_indices_to_remove, CPARCIAL_RESERVE_ARR);
     }


    Реализация класса CPartials в советнике

    Для тестирования класса CPartials мы будем использовать советник на основе Order Block, который уже был представлен в предыдущих статьях. Файл советника будет находиться в папке OrderBlock.

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

    папка Order Block EA

    Рисунок 4. Расположение файла "Order Block EA MetaTrader 5.mq5" в репозитории

    #include  "..\\PosManagement\\Breakeven.mqh"
    #include  "..\\PosManagement\\Partials.mqh"

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

    Первый блок: настройка советника на основе Order Blocks
    В этой части располагаются параметры, относящиеся к индикатору Order Blocks.

    sinput group "-------| Order Block EA settings |-------"
    input ulong InpMagic = 545244; //Magic number
    input ENUM_TIMEFRAMES InpTimeframeOrderBlock = PERIOD_M5; //Order block timeframe
    
    sinput group "-- Order Block --"
    input int  InpRangoUniversalBusqueda = 500; //search range of order blocks
    input int  InpWidthOrderBlock = 1; //Width order block
    input bool InpBackOrderBlock = true; //Back order block?
    input bool InpFillOrderBlock = true; //Fill order block?
    input color InpColorOrderBlockBajista = clrRed; //Bearish order block color
    input color InpColorOrderBlockAlcista = clrGreen; //Bullish order block color
    
    input double InpTransparency = 0.5; // Transparency from 0.0 (invisible) to 1.0 (opaque)
    
    sinput group ""
    sinput group "-------| Strategy |-------"
    input ENUM_TP_SL_STYLE InpTpSlStyle = ATR;//Tp and sl style:
    
    sinput group "- TP SL by ATR "
    input double InpAtrMultiplier1 = 9.3;//Atr multiplier 1 (SL)
    input double InpAtrMultiplier2 = 24.4;//Atr multiplier 2 (TP)
    
    sinput group "- TP SL by POINT "
    input int InpTpPoint = 1000; //TP in Points
    input int InpSlPoint = 1000; //SL in Points
    

    Также добавляются общие параметры стратегии, включая режимы Take Profit и Stop Loss.

    Второй блок: параметры AccountManager
    Здесь определяется только уровень логирования.

    sinput group ""
    sinput group "-------| Account Status |-------"
    input ENUM_VERBOSE_LOG_LEVEL InpLogLevelAccountStatus = VERBOSE_LOG_LEVEL_ERROR_ONLY; //(Account Status|Ticket Mangement) log level:
    

    Третий блок: управление рисками
    В этой части определяются основные параметры управления рисками.  

    sinput group ""
    sinput group "-------| Risk Management |-------"
    input ENUM_LOTE_TYPE InpLoteType = Dinamico; //Lote Type:
    input double InpLote = 0.1; //Lot size (only for fixed lot)
    input ENUM_MODE_RISK_MANAGEMENT InpRiskMode = risk_mode_personal_account; //type of risk management mode
    input bool InpUpdateDailyLossRiskModeProp = true; //Update the MDL, if the risk-management type is propfirm?
    input ENUM_GET_LOT InpGetMode = GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION; //How to get the lot
    input double InpPropFirmBalance = 0; //If risk mode is Prop Firm FTMO, then put your ftmo account balance
    input ENUM_VERBOSE_LOG_LEVEL InpLogLevelRiskManagement = VERBOSE_LOG_LEVEL_ERROR_ONLY; //Risk Management log level:
    
    sinput group "- ML/Maximum loss/Maximum loss -"
    input double InpPercentageOrMoneyMlInput = 0; //percentage or money (0 => not used ML)
    input ENUM_RISK_CALCULATION_MODE InpModeCalculationMl = percentage; //Mode calculation Max Loss
    input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMl = Balance; //ML percentage applies to:
    
    sinput group "- MWL/Maximum weekly loss/Maximum weekly loss -"
    input double InpPercentageOrMoneyMwlInput  = 0; //percentage or money (0 => not used MWL)
    input ENUM_RISK_CALCULATION_MODE InpModeCalculationMwl = percentage; //Mode calculation Max weekly Loss
    input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMwl = Balance;//MWL percentage applies to:
    
    sinput group "- MDL/Maximum  daily loss/Maximum daily loss -"
    input double InpPercentageOrMoneyMdlInput  = 3.0; //percentage or money (0 => not used MDL)
    input ENUM_RISK_CALCULATION_MODE InpModeCalculationMdl = percentage; //Mode calculation Max daily loss
    input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMdl = Balance;//MDL percentage applies to:
    
    sinput group "- GMLPO/Gross maximum loss per operation/Percentage to risk per operation -"
    input ENUM_OF_DYNAMIC_MODES_OF_GMLPO InpModeGmlpo = NO_DYNAMIC_GMLPO; //Select GMLPO mode:
    input double InpPercentageOrMoneyGmlpoInput  = 2.0; //percentage or money (0 => not used GMLPO)
    input ENUM_RISK_CALCULATION_MODE InpModeCalculationGmlpo = percentage; //Mode calculation Max Loss per operation
    input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesGmlpo = Balance;//GMPLO percentage applies to:

    По сравнению с предыдущей версией, блок GMLPO был упрощен за счет удаления одной части. Кроме того, был добавлен новый параметр InpUpdateDailyLossRiskModeProp. Это значение указывает классу, следует ли автоматически обновлять Maximum Daily Loss на счетах типа PropFirm.

    Четвертый блок: динамические параметры GMLPO и MDP
    Эта часть включает в себя дополнительные настройки динамического риска (GMLPO) и параметры, связанные с Maximum Daily Profit.

    sinput group "-- Optional GMLPO settings, Dynamic GMLPO"
    sinput group "--- Full customizable dynamic GMLPO"
    input string InpNote1 = "subtracted from your total balance to establish a threshold.";  //This parameter determines a specific percentage that will be
    input string InpStrPercentagesToBeReviewed = "15,30,50"; //percentages separated by commas.
    input string InpNote2 = "a new risk level will be triggered on your future trades: "; //When the current balance (equity) falls below this threshold
    input string InpStrPercentagesToApply = "10,20,25"; //percentages separated by commas.
    input string InpNote3 = "0 in both parameters => do not use dynamic risk in gmlpo"; //Note:
    
    sinput group "--- Fixed dynamic GMLPO with parameters"
    sinput group "- 1 -"
    input string InpNote11 = "subtracted from your total balance to establish a threshold.";  //This parameter determines a specific percentage that will be
    input double InpBalancePercentageToActivateTheRisk1 = 2.0; //percentage 1 that will be exceeded to modify the risk separated by commas
    input string InpNote21 = "a new risk level will be triggered on your future trades: "; //When the current balance (equity) falls below this threshold
    input double InpPercentageToBeModified1 = 1.0;//new percentage 1 to which the gmlpo is modified
    sinput group "- 2 -"
    input double InpBalancePercentageToActivateTheRisk2 = 5.0;//percentage 2 that will be exceeded to modify the risk separated by commas
    input double InpPercentageToBeModified2 = 0.7;//new percentage 2 to which the gmlpo is modified
    sinput group "- 3 -"
    input double InpBalancePercentageToActivateTheRisk3 = 7.0;//percentage 3 that will be exceeded to modify the risk separated by commas
    input double InpPercentageToBeModified3 = 0.5;//new percentage 3 to which the gmlpo is modified
    sinput group "- 4 -"
    input double InpBalancePercentageToActivateTheRisk4 = 9.0;//percentage 4 that will be exceeded to modify the risk separated by commas
    input double InpPercentageToBeModified4 = 0.33;//new percentage 4  1 to which the gmlpo is modified
    
    sinput group "-- MDP/Maximum daily profit/Maximum daily profit --"
    input bool InpMdpIsStrict = true; //MDP is strict?
    input double InpPercentageOrMoneyMdpInput = 11.0; //percentage or money (0 => not used MDP)
    input ENUM_RISK_CALCULATION_MODE InpModeCalculationMdp = percentage; //Mode calculation Max Daily Profit
    input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMdp = Balance;//MDP percentage applies to:

    Пятый блок: модификаторы риска
    В этой части объявляются три модификатора риска.

    • Risk Modifier 1  соответствует модификатору CDynamicRisk (тип booster), представленному в начале статьи.
    • Risk Modifier 2  регулирует риск на основе списка процентов. При последовательности убытков применяются заданные уровни. При достижении Take Profit риск возвращается к начальному значению.
    • Risk Modifier 3  изменяет риск на сделку GMLPO только в том случае, если результат счета отрицательный. Для расчета используется функция FunctionKevin.

    inline double FunctionKevin(double defect_risk, double loss_percentage, double constante)
     {
      return (defect_risk * MathExp(loss_percentage / constante));
     }

    Код 5-го блока.

    sinput group ""
    sinput group "-------| Risk Modifier |-------"
    input bool InpActivarModificadorDeRiesgo = false; // Enables dynamic risk adjustment (Booster)
    input double InpStepMod = 2.0;                 // Increment applied to risk each time the condition is met
    input double InpStart = 2.0;                    // Profit percentage from which risk adjustment begins
    input ENUM_MULTIPLIER_METHOD_DR InpMethodDr = DR_EXPONECIAL; // Type of progression used to increase risk
    
    sinput group ""
    sinput group "-------| Risk modifier 2 |-------"
    input bool InpActivarModificadorDeRiesgo2 = false; //Activate risk modifier 2
    input string InpStrPercentagesToApplyRiesgoCts = "6, 8, 10, 20, 25"; //Percentages to apply
    
    sinput group ""
    sinput group "-------| Risk modifier 3 by Kevin |-------"
    input bool InpActivarModificadorDeRiesgo3 = false; //Activate risk modifier 3
    input double InpConstante = 8.0; //Function constant

    Шестой блок: торговая сессия
    В этом блоке располагаются параметры торговой сессии.

    sinput group ""
    sinput group "-------| Session |-------"
    input char InpPaSessionStartHour = 1;   // Start hour to operate (0-23)
    input char InpPaSessionStartMinute = 0; // Start minute to operate (0-59)
    input char InpPaSessionEndHour = 23;    // End hour to operate (1-23)
    input char InpPaSessionEndMinute = 0;  // End minute to operate (0-59)

    Седьмой блок: параметры перевода в безубыток
    В этой части будут указаны параметры для активации механизма безубыточности.

    sinput group ""
    sinput group "-------| Breakeven |-------"
    input         bool InpUseBe                       = true;               // Enable Breakeven logic
    input ENUM_BREAKEVEN_TYPE InpTypeBreakEven       = BREAKEVEN_TYPE_RR;  // Calculation method (RR, ATR, or fixed points)
    input ENUM_VERBOSE_LOG_LEVEL InpLogLevelBe      = VERBOSE_LOG_LEVEL_ERROR_ONLY; // Break Even log level:
    
    sinput group "--- Breakeven based on Risk/Reward (RR) ---"
    input         string InpBeRrAdv                  = "Requires a configured Stop Loss.";  // Warning: Stop Loss is required to calculate RR
    input         double InpBeRrDbl                  = 1.0;                                 // Risk/Reward ratio to activate Breakeven (e.g. 1.0)
    input ENUM_TYPE_EXTRA_BE_BY_RRR InpBeTypeExtraRr = EXTRA_BE_RRR_BY_ATR;                // Method to adjust Breakeven price (ATR or points)
    input         double InpBeExtraPointsRrOrAtrMultiplier = 1.0;                       // Adjustment value: ATR multiplier (atr 14 period) if method = ATR, or fixed points if method = Points
    
    sinput group "--- Breakeven based solely on ATR ---"
    input         double InpBeAtrMultiplier          = 2.0;    // ATR multiplier to trigger Breakeven (atr 14 period)
    input         double InpBeAtrMultiplierExtra    = 1.0;    // Additional multiplier for precise Breakeven adjustment (atr 14 period)
    
    sinput group "--- Breakeven based on fixed points ---"
    input         int InpBeFixedPointsToPutBe     = 200;   // Minimum distance (in points) to trigger Breakeven
    input         int InpBeFixedPointsExtra         = 100;   // Extra points added when Breakeven is triggered

    Восьмой блок: частичное закрытие позиций
    В этом завершающем блоке задаются параметры класса CPartials.

    sinput group ""
    sinput group "-------| Partial Closures |-------"
    input ENUM_VERBOSE_LOG_LEVEL InpLogLevelPartials  = VERBOSE_LOG_LEVEL_ALL; // Partial Closures log level
    input string InpComentVolumen4 = "Use '0' in both to disable. Order from lowest to highest.";
    input string InpPartesDelTpDondeSeTomaraParciales = "0";    // Percentage of TP distance where partials trigger
    input string InpComentParciales1 = "List of TP levels (%) to trigger partials."; //->
    input string InpComentParciales2 = "Ex: \"25,50,75\" triggers at 25%, 50%, 75%."; //->
    input string InpComentParciales3 = "String allows multiple dynamic levels."; //->
    input string InpSeparatorPar = ""; // -------
    input string InpVolumenQueSeQuitaraDeLaPosicionEnPorcentaje = "0";  // Volume to close at each defined level
    input string InpComentVolumen1 = "Percentage of total volume closed at each level."; //->
    input string InpComentVolumen2 = "Must match count of TP level entries."; //->
    input string InpComentVolumen3 = "Ex: \"30,40,30\" closes 30%, 40%, then 30%."; //
    

    Глобальная область советника
    После завершения части с входными параметрами переходим к глобальной области.

    В этом блоке объявляются переменные и экземпляры классов, которые будут использоваться во всем советнике.

    Сначала определяется указатель типа CRiskManagement для управления рисками, а также создаются экземпляры CBreakEven и CPartials.

    CRiskManagemet *risk;
    CBreakEven break_even(InpMagic, _Symbol);
    CPartials g_partials;

    Также создаются два дескриптора: один для индикатора блоков ордеров (Order Block) и другой — для индикатора скользящих средних.

    //--- Handles
    int order_block_indicator_handle;
    int hanlde_ma;
    

    Объявляются четыре массива типа double, которые будут хранить данные, полученные из буферов индикатора блоков ордеров.

    //--- Global buffers
    double tp1[];
    double tp2[];
    double sl1[];
    double sl2[];

    Далее объявляются две переменные типа datetime для хранения начала и конца сессии.

    //--- Session
    datetime start_sesion;
    datetime end_sesion;

    Затем создаются несколько экземпляров CBarControler для контроля открытия свечей на различных таймфреймах: текущем, дневном, недельном и месячном.

    //--- General
    CBarControler bar_d1(PERIOD_D1, _Symbol);
    CBarControler bar_w1(PERIOD_W1, _Symbol);
    CBarControler bar_mn1(PERIOD_MN1, _Symbol);
    CBarControler bar_curr(PERIOD_CURRENT, _Symbol);

    Далее создаются экземпляры модификаторов риска: динамический (CDynamicRisk), на основе процентов (CModifierDynamicRisk) и на основе функции Kevin (CMathDynamicRisk).

    CDynamicRisk riesgo_c(InpStepMod, InpStart, LP_GMLPO, InpMethodDr);
    CModifierDynamicRisk risk_modificator(InpStrPercentagesToApplyRiesgoCts);
    CMathDynamicRisk risk_modificator_kevin(InpConstante);

    В завершение определяется глобальная переменная opera, которая используется как флаг для включения или отключения торговой деятельности советника, а также создается экземпляр класса CAtrUltraOptimized, реализующий расчет ATR.

    bool opera = true;
    CAtrUltraOptimized atr_ultra_optimized;

    Полный код.

    CRiskManagemet *risk;
    CBreakEven break_even(InpMagic, _Symbol);
    CPartials g_partials;
    
    //--- Handles
    int order_block_indicator_handle;
    int hanlde_ma;
    
    //--- Global buffers
    double tp1[];
    double tp2[];
    double sl1[];
    double sl2[];
    
    //--- Session
    datetime start_sesion;
    datetime end_sesion;
    
    //--- General
    CBarControler bar_d1(PERIOD_D1, _Symbol);
    CBarControler bar_w1(PERIOD_W1, _Symbol);
    CBarControler bar_mn1(PERIOD_MN1, _Symbol);
    CBarControler bar_curr(PERIOD_CURRENT, _Symbol);
    CDynamicRisk riesgo_c(InpStepMod, InpStart, LP_GMLPO, InpMethodDr);
    CModifierDynamicRisk risk_modificator(InpStrPercentagesToApplyRiesgoCts);
    CMathDynamicRisk risk_modificator_kevin(InpConstante);
    
    //---
    bool opera = true;
    CAtrUltraOptimized atr_ultra_optimized;

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

    Настройка ATR
    Сначала инициализируется класс CAtrUltraOptimized, которому задаются период, символ и внутренние параметры.

    //--- Atr 
      atr_ultra_optimized.SetVariables(_Period, _Symbol, 0, 14);
      atr_ultra_optimized.SetInternalPointer();
    

    Настройка перевода в безубыток
    Если включена опция перевода в безубыток, задаются различные способы расчета: по ATR, по фиксированным пунктам и по соотношению риск/прибыль.

    Кроме того, задается уровень логирования.

    //--- We set the breakeven values so its use is allowed
      if(InpUseBe)
       {
        break_even.SetBeByAtr(InpBeAtrMultiplier, InpBeAtrMultiplierExtra, GetPointer(atr_ultra_optimized));
        break_even.SetBeByFixedPoints(InpBeFixedPointsToPutBe, InpBeFixedPointsExtra);
        break_even.SetBeByRR(InpBeRrDbl, InpBeTypeExtraRr, InpBeExtraPointsRrOrAtrMultiplier, GetPointer(atr_ultra_optimized));
        break_even.SetInternalPointer(InpTypeBreakEven);
        break_even.obj.AddLogFlags(InpLogLevelBe);
       }
    

    Настройки частичного закрытия позиций
    Инициализируется класс CPartials. Если функция Init возвращает true, экземпляр добавляется в управление счетом с помощью account_status.AddItemFast().

    //--- Partials
      g_partials.AddLogFlags(InpLogLevelPartials);
      if(g_partials.Init(InpMagic, _Symbol, InpVolumenQueSeQuitaraDeLaPosicionEnPorcentaje, InpPartesDelTpDondeSeTomaraParciales))
       {
        account_status.AddItemFast(&g_partials);
       }
    

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

    Создание индикаторов
    Создаются дескрипторы для индикатора блоков ордеров и для экспоненциальной скользящей средней. Если дескриптор EMA недопустимый, возвращается ошибка инициализации.

    //--- Indicators
    // Create ob handle
      order_block_indicator_handle = CreateIndicatorObHandle();
    
    // Create ma handle
      hanlde_ma = iMA(_Symbol, InpTimeframeOrderBlock, 30, 0, MODE_EMA, PRICE_CLOSE);
    
    // Check ma
      if(hanlde_ma == INVALID_HANDLE)
       {
        Print("The ema indicator is not available latest error: ", _LastError);
        return INIT_FAILED;
       }
    
      ChartIndicatorAdd(0, 0, hanlde_ma);
    

    Настройка управления рисками
    В этой новой версии управление рисками настраивается с помощью класса CRiskPointer.

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

    CRiskPointer* manager = new CRiskPointer(InpMagic, InpGetMode);

    Если выбран режим управления PropFirm, задается соответствующий баланс.

      manager.SetPropirm(InpPropFirmBalance);

    Далее получаем указатель и присваиваем его переменной risk из объекта manager.

      risk = manager.GetRiskPointer(InpRiskMode);

    Добавляются флаги логирования и устанавливается указатель CGetLote.

     risk.AddLogFlags(InpLogLevelRiskManagement);
     risk.SetLote(CreateLotePtr(_Symbol));

    Настройка максимальной прибыли и убытка
    Добавляются правила для максимального дневного убытка, GMLPO, максимального убытка, максимального недельного убытка и максимальной дневной прибыли (Maximum Daily Loss, GMLPO, Maximum Loss, Maximum Weekly Loss и Maximum Daily Profit).

    // We set the parameters
      string to_apply = InpStrPercentagesToApply, to_modfied = InpStrPercentagesToBeReviewed;
    
      if(InpModeGmlpo == DYNAMIC_GMLPO_FIXED_PARAMETERS)
        SetDynamicUsingFixedParameters(InpBalancePercentageToActivateTheRisk1, InpBalancePercentageToActivateTheRisk2, InpBalancePercentageToActivateTheRisk3
                                       , InpBalancePercentageToActivateTheRisk4, InpPercentageToBeModified1, InpPercentageToBeModified2, InpPercentageToBeModified3, InpPercentageToBeModified4
                                       , to_modfied, to_apply);
    
      risk.AddLoss(InpPercentageOrMoneyMdlInput, InpAppliedPercentagesMdl, InpModeCalculationMdl, LP_MDL);
      risk.AddLoss(InpPercentageOrMoneyGmlpoInput, InpAppliedPercentagesGmlpo, InpModeCalculationGmlpo, LP_GMLPO, CLOSE_POSITION_AND_EQUITY, (InpModeGmlpo != NO_DYNAMIC_GMLPO), to_modfied, to_apply);
      risk.AddLoss(InpPercentageOrMoneyMlInput, InpAppliedPercentagesMl, InpModeCalculationMl, LP_ML);
      risk.AddLoss(InpPercentageOrMoneyMwlInput, InpAppliedPercentagesMwl, InpModeCalculationMwl, LP_MWL);
      risk.AddProfit(InpPercentageOrMoneyMdpInput, InpAppliedPercentagesMdp, InpModeCalculationMdp, LP_MDP, InpMdpIsStrict);
      risk.EndAddProfitLoss(); // Execute this every time we finish setting the maximum losses and profits
    
    

    Если тип управления рисками — "динамический PropFirm", настраивается обновление максимального дневного убытка (Maximum Daily Loss).

    //--- If the risk-management type is prop we need to configure whether to update the MDL (as FTMO)
      if(risk.ModeRiskManagement() == risk_mode_propfirm_dynamic_daiy_loss)
       {
        CRiskManagemetPropFirm* temp_ptr = dynamic_cast<CRiskManagemetPropFirm *>(risk); // Convert from normal to prop firm
        temp_ptr.UpdateLoss(InpUpdateDailyLossRiskModeProp);
       }

    Добавление модификаторов риска
    Активные модификаторы добавляются в указатель риска.

    // We add modifiers
      if(InpActivarModificadorDeRiesgo)
        risk.AddModificator(riesgo_c);
    
      if(InpActivarModificadorDeRiesgo2)
    
        risk.AddModificator(risk_modificator);
    
      if(InpActivarModificadorDeRiesgo3)
        risk.AddModificator(risk_modificator_kevin);
    

    Затем управление рисками регистрируется в account_status, после чего временный указатель удаляется.

    // We finish by adding it
      account_status.AddItemFast(risk);
    
    // We delete the temporary pointer
      delete manager;

    Инициализация account_status
    Вызывается событие инициализации глобального экземпляра account_status.

    //--- We initialize the account
      account_status.AddLogFlagTicket(InpLogLevelAccountStatus);
      account_status.AddLogFlags(InpLogLevelAccountStatus);
      account_status.OnInitEvent();

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

    //--- We configure the arrays in series
      ArraySetAsSeries(tp1, true);
      ArraySetAsSeries(tp2, true);
      ArraySetAsSeries(sl1, true);
      ArraySetAsSeries(sl2, true);
      return(INIT_SUCCEEDED);

    OnTick
    В функции OnTick организуется общий порядок выполнения советника.

    Сначала создается локальная переменная time_curr, содержащая текущее время символа. Также вызывается макрос CAccountStatus_OnTickEvent, который обновляет текущую прибыль счета. Эти данные используются классом CRiskManagement для проверки достижения максимального убытка или прибыли.

    Затем вызываются функции account_status: OnNewDay, OnNewWeek и OnNewMonth. Этот блок выполняется при открытии каждой новой дневной свечи и устанавливает глобальную переменную opera в значение true.

    //--- General
      const datetime time_curr = TimeCurrent();
      CAccountStatus_OnTickEvent
    
    //--- Code that runs every new day
      if(bar_d1.IsNewBar(time_curr))
       {
        account_status.OnNewDay();
    
        if(bar_w1.IsNewBar(time_curr))
          account_status.OnNewWeek();
    
        if(bar_mn1.IsNewBar(time_curr))
          account_status.OnNewMonth();
    
        opera = true;
       }

    Затем проверяется торговая сессия. Если текущее время превышает время окончания сессии, рассчитываются значения начала и окончания следующей сессии.

    //--- Check Session
      if(time_curr > end_sesion)
       {
        start_sesion = HoraYMinutoADatetime(InpPaSessionStartHour, InpPaSessionStartMinute, time_curr);
        end_sesion = HoraYMinutoADatetime(InpPaSessionEndHour, InpPaSessionEndMinute, time_curr);
    
        if(start_sesion > end_sesion)
          end_sesion = end_sesion + 86400;
       }

    Далее вызываются основные функции глобальных объектов break_even и g_partials. Если переменная opera имеет значение false, выполняется выход из функции без дальнейшего выполнения.

    //--- Breakeven
      if(InpUseBe)
        break_even.obj.BreakEven();
        
    //--- Partials
      g_partials.CheckTrackedPositions();
          
    
    //--- Check to operate
      if(!opera)
        return;
    

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

    //--- Strategy
      if(bar_curr.IsNewBar(time_curr))
       {
        if(time_curr > start_sesion && time_curr < end_sesion)
         {
          if(risk.GetPositionsTotal() == 0)
           {
            CopyBuffer(order_block_indicator_handle, 2, 0, 5, tp1);
            CopyBuffer(order_block_indicator_handle, 3, 0, 5, tp2);
            CopyBuffer(order_block_indicator_handle, 4, 0, 5, sl1);
            CopyBuffer(order_block_indicator_handle, 5, 0, 5, sl2);
    
            if(tp1[0] > 0 && tp2[0]  > 0 && sl1[0] > 0 &&  sl2[0] > 0)
             {
              if(tp2[0] > sl2[0])  // buy orders
               {
                const double ASK = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
                risk.SetStopLoss(ASK - sl1[0]);
                const double lot = (InpLoteType == Dinamico ? risk.GetLote(ORDER_TYPE_BUY, ASK, 0, 0) : InpLote);
                tradep.Buy(lot, _Symbol, ASK, sl1[0], tp2[0], "Order Block EA Buy");
               }
              else
                if(sl2[0] > tp2[0])  // sell orders
                 {
                  const double BID = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);
                  risk.SetStopLoss(sl1[0] - BID);
                  const double lot = (InpLoteType == Dinamico ? risk.GetLote(ORDER_TYPE_SELL, BID, 0, 0) : InpLote);
                  tradep.Sell(lot, _Symbol, BID, sl1[0], tp2[0], "Order Block EA Sell");
                 }
             }
           }
         }
       }
    
    В заключение проверяются условия достижения максимального убытка или прибыли. Если какое-либо из этих значений достигнуто, позиции закрываются, или выполняются действия, определенные в соответствии с настроенным режимом риска.
    //--- Checking maximum losses and profits
      if(account_status_positions_open)
       {
        if(risk[LP_ML].IsSuperated())
         {
          if(InpRiskMode == risk_mode_propfirm_dynamic_daiy_loss)
           {
            Print("The expert advisor lost the funding test");
            Remover();
           }
          else
           {
            risk.CloseAllPositions();
            Print("Maximum loss exceeded now");
            opera = false;
           }
         }
    
        if(risk[LP_MDL].IsSuperated())
         {
          risk.CloseAllPositions();
          Print("Maximum daily loss exceeded now");
          opera = false;
         }
    
        if(risk[LP_MDP].IsSuperated())
         {
          risk.CloseAllPositions();
          Print("Excellent Maximum daily profit achieved");
          opera = false;
         }
       }

    OnTradeTransaction
    Эта функция обрабатывает все транзакции, совершенные с данного счета. Для этого вызывается публичная функция OnTradeTransactionEvent глобального экземпляра account_status.  

    //+------------------------------------------------------------------+
    //| TradeTransaction function                                        |
    //+------------------------------------------------------------------+
    void OnTradeTransaction(const MqlTradeTransaction & trans,
                            const MqlTradeRequest & request,
                            const MqlTradeResult & result)
     {
      account_status.OnTradeTransactionEvent(trans);
     }

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

    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
     {
    //---
      ChartIndicatorDelete(0, 0, ChartIndicatorName(0, 0, GetMovingAverageIndex()));
      ChartIndicatorDelete(0, 0, "Order Block Indicator");
    
      if(hanlde_ma != INVALID_HANDLE)
        IndicatorRelease(hanlde_ma);
    
      if(order_block_indicator_handle != INVALID_HANDLE)
        IndicatorRelease(order_block_indicator_handle);
     }


    Тестирование советника Order Blocks: влияние частичного закрытия на производительность

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

    Для этой проверки параметр "Percentage of TP distance where partials trigger" (процент расстояния Take Profit, на котором срабатывает частичное закрытие) устанавливается в значение "25, 45, 65", а параметр "Volume to close at each defined level" (объем закрытия на каждом определенном уровне) — в значение "30, 30, 30". Поскольку целью является анализ выполнения частичного закрытия позиций, были активированы все уровни логирования, связанные с этой функциональностью.

    Настройка частичного закрытия

    Рисунок 5. Настройка частичного закрытия для первого теста

    После завершения настройки мы выполнили тестирование на инструменте золото, таймфрейм M5, в период с 01.01.2024 по 22.09.2025 в режиме реальных тиков.

    При каждом выполнении сделки советником выводятся уровни Take Profit. В данном случае, поскольку мы определили три уровня, отображаются три цены. Их анализ показывает, что в позиции на продажу цена, соответствующая уровню с индексом 0, является наибольшей, тогда как цена, соответствующая уровню с индексом 2, — наименьшей. Это ожидаемо, поскольку в позициях на продажу уровни выхода располагаются по убыванию.

    Вывод сообщений в журнал при выполнении ордера на продажу

    Рисунок 6. Вывод в журнал уровней Take Profit для позиции на продажу

    Далее в журнале видно, что позиция была частично закрыта.

    В данном примере было закрыто 0.05 лота от открытой позиции.

    Частичное закрытие реализовано

    Рисунок 7. Вывод в журнал информации об успешном частичном закрытии позиции

    Значение 0,05 получается следующим образом: объем исходной позиции составлял 0,15 лота; при применении первого частичного закрытия производится следующий расчет 0,15 * 0,30 = 0,045. Однако у этого брокера минимальный шаг объема составляет 0,01, поэтому закрыть ровно 0,045 лота невозможно.

    В коде частичного закрытия предусмотрена проверка для такого случая, при которой рассчитанный объем округляется до ближайшего допустимого значения, в соответствии с шагом объема:

    volume_to_close = RoundToStep(volume_to_close, m_volume_step);

    С учетом этой корректировки итоговый объем для закрытия составляет 0.05, что соответствует тому, что было реализовано на практике.

    С помощью этого теста мы проверили корректность работы частичного закрытия позиций. Теперь перейдем к наиболее интересной части этого раздела — сравнению производительности советника с использованием частичного закрытия и без него.

    Выполним тестирование с использованием следующих настроек:

    Настройки тестирования в тестере стратегий

    Рисунок 8. Настройки тестирования в тестере стратегий, использованные в тестах 1 и 2

    В этих тестах не применялись ограничения по максимальной дневной прибыли или убытку, единственным действующим ограничением был GMLPO (10%). Значения Take Profit и Stop Loss были одинаковыми в обоих сценариях, как и заданная торговая сессия с 3 до 14. Единственным различием между двумя тестами является включение частичного закрытия позиций.

    Первый тест: сравнение результативности с частичным закрытием и без него
    В первом тесте использовались значения Stop Loss = 2.0 и Take Profit = 6.0 (Take Profit и Stop Loss по ATR, заданные как множители ATR).

    backtest_without_partials

    Рисунок 9. Результаты тестирования советника без частичного закрытия позиций

    Сравним этот результат с результатами тестирования советника с использованием частичного закрытия позиций:

    backtest_with_partials

    Рисунок 10. Результаты тестирования советника с частичным закрытием позиций

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

    комиссия

    Рисунок 11. Комиссии, начисленные в тесте с частичным закрытием позиций

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

    Второй тест: более крупные таймфреймы со стратегией swing
    Во втором тесте были заданы значения Stop Loss = 4.0 и Take Profit = 25.0. Эти значения больше соответствуют стратегии swing, при которой сделки могут длиться несколько дней или даже недель.

    Тестирование с использованием частичного закрытия позиций:

    backtest_with_partials_2

    Рисунок 12: Результаты тестирования советника с частичным закрытием позиций

    Тестирование без использования частичного закрытия позиций:

    backtest_without_partials_2

    Рисунок 13. Результаты тестирования советника без частичного закрытия позиций

    В этом случае разница гораздо более заметна. Тестирование с частичным закрытием позиций завершилось с итоговым балансом около 33 000 USD, тогда как тест без частичного закрытия — с балансом примерно 22 234 USD. Разница превышает 11 000 USD.

    Этот результат объясняется тем, что при более широком Take Profit вероятность его полного достижения ценой снижается. В результате частичное закрытие позиций позволяет фиксировать промежуточную прибыль. Наглядный пример наблюдался в декабре 2024 года: в ходе тестирования без частичного закрытия кривая эквити показала значительный рост, который в итоге не был зафиксирован, поскольку позиция не достигла полного уровня Take Profit. В то же время в тесте с частичным закрытием эта же позиция принесла частичную прибыль, что позволило зафиксировать результат по данной сделке.

    Подводя итог проведенным тестам, можно сделать вывод, что эффективность частичного закрытия позиций зависит от типа используемой стратегии. В подходах типа scalping, где цели по Stop Loss и Take Profit невелики, дополнительные комиссии, как правило, снижают результативность.

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

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


    Заключение

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

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

    Также хочу отметить, что для этой и предыдущих статей мною был создан публичный репозиторий в MQL Algo Forge, где собран весь рассмотренный код: индикатор Order Block, библиотеки управления рисками, перевода в безубыток, частичного закрытия позиций и другие практические примеры. Репозиторий будет регулярно обновляться и улучшаться.

    Нажмите, чтобы перейти к публичному репозиторию.

    Папка Файлы Описание 
     Examples  - Get_Lot_By_Risk_Per_Trade_and_SL.mq5 
     - Get_Sl_by_risk_per_operation_and_lot.mq5
     - Risk_Management_Panel.mq5
    Практические примеры использования библиотеки управления рисками (RM).
     OrderBlock  - Main.mqh
     - Order Block EA MetaTrader 5.mq5
     - OrderBlockIndPart2.mq5
    Содержит индикатор и советник на основе Order Block, используемые в качестве примеров в серии статей по управлению рисками, переводу в безубыток и частичному закрытию позиций.
     PosManagement  - Breakeven.mqh
     - Partials.mqh
    Специализированные библиотеки для управления позициями: перевод в безубыток и частичное закрытие позиций.
     RM  - AccountStatus.mqh
     - LossProfit.mqh
     - LoteSizeCalc.mqh
     - Modificators.mqh
     - OcoOrder.mqh
     - OrdersGestor.mqh
     - RiskManagement.mqh
     - RiskManagementBases.mqh
     - RM_Defines.mqh
     - RM_Functions.mqh
    Все модули, входящие в библиотеку управления рисками RM.
     Utils  - FA \
             - Atr.mqh
             - AtrCts.ex5
             - BarControler.mqh
             - ClasesBases.mqh
             - Events.mqh
             - FuncionesBases.mqh
             - Managers.mqh
             - SimpleLogger.mqh
             - Sort.mqh
             - StringToArray.mqh

     - CustomOptimization.mqh
     - Fibonacci.mqh
     - File.mqh
     - Funciones Array.mqh
     - Objectos 2D.mqh
     - RandomSimple.mqh
     
    Библиотека утилит для разработки библиотек, советников и индикаторов. Включает в себя функции для работы с массивами, временем, преобразованиями, строками, простыми математическими операциями, а также классы для обработки паттернов свечей, оптимизированного расчета ATR, проверки приостановки работы ПК и многого другого.
    Sets   - Article_Partials \
                                                                                                                              - ARTICLE_PARTIAL_SET_TEST_2_OB_WITHOUT_PARTIALS.set 
      - ARTICLE_PARTIAL_SET_OB_TEST_2_WITH_PARTIALS.set   
      - ARTICLE_PARTIAL_SET_TEST_1_WITH_PARTIALS.set 
      - ARTICLE_PARTIAL_SET_TEST_1_WITHOUT_PARTIALS.set
                 
    Папка с наборами настроек (set-файлами), использованными в тестах 1 и 2.
     


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

    Прикрепленные файлы |
    PartialsArticle.zip (209.79 KB)
    Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий Переосмысливаем классические стратегии (Часть 14): Анализ нескольких стратегий
    В этой статье мы продолжаем построение ансамбля торговых стратегий с использованием генетического оптимизатора MT5 для настройки параметров стратегий. Сегодня мы проанализируем данные в Python, чтобы проверить, сможет ли такая модель лучше предсказывать, какая стратегия окажется более успешной и какая сработает точнее, и окажется ли это эффективнее прямого прогнозирования доходности. Сразу скажу, что тестирование приложения с такой статистической моделью показало резкое ухудшение в результатах. Все дело в генетическом оптимизаторе — к сожалению, он отдает предпочтение коррелированным стратегиям. Поэтому мы пересмотрим метод, чтобы сохранить фиксированные веса голосов и сосредоточить оптимизацию на настройках индикаторов.
    Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин
    В этой статье мы усовершенствуем нашу систему зонального восстановления (Zone Recovery System), внедрив трейлинг-стопы и возможности торговли несколькими корзинами. Мы исследуем, как усовершенствованная архитектура использует динамические трейлинг-стопы для фиксации прибыли и систему управления корзинами для эффективной обработки множества торговых сигналов. В ходе реализации и тестирования на истории мы продемонстрируем более надежную торговую систему, приспособленную к адаптивным рыночным условиям.
    Торговые инструменты на MQL5 (Часть 11):  Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом Торговые инструменты на MQL5 (Часть 11): Панель корреляционной матрицы (Пирсон, Спирман, Кенделл) с тепловой картой и стандартным режимом
    В этой статье мы создаем панель мониторинга корреляционной матрицы в MQL5 для вычисления взаимосвязей между активами с использованием методов Пирсона (Pearson), Спирмена (Spearman) и Кенделла (Kendall) за заданный таймфрейм и количество баров. Система предлагает стандартный режим с цветовыми порогами и звездочками p-значений, а также режим тепловой карты с градиентными визуальными элементами силы корреляции. Он включает в себя интерактивный пользовательский интерфейс с селекторами таймфреймов, переключателями режимов и динамической легендой для эффективного анализа взаимозависимостей символов.
    Архитектура коллективных торговых решений ИИ-агентов Архитектура коллективных торговых решений ИИ-агентов
    Статья описывает архитектуру мультиагентной торговой системы на базе языковой модели grok-4-fast, где вместо одного системного промпта работают четыре независимых аналитика с принципиально разными ролями: бык, медведь, риск-менеджер и арбитр. Три аналитика запускаются параллельно через ThreadPoolExecutor и за 3–5 секунд формируют аргументированные позиции по одним и тем же рыночным данным, после чего детерминированный судья выносит финальный вердикт по жёстким правилам.