Управление рисками (Часть 4): Завершение ключевых методов класса
- Введение
- Краткое содержание статьи
- Новые перечисления и структуры
- Улучшения в конструкторе и общих функциях
- Новые переменные для класса
- Управление открытыми позициями
- Методы обработки превышения лимитов
- Управление событиями
- Функции для динамического риска
- Построение динамического риска
- Заключение
Введение
В продолжение предыдущей статьи об управлении рисками мы объясним основные переменные и некоторые начальные методы для нашего специализированного класса. В этот раз мы сосредоточимся на завершении необходимых методов для проверки того, были ли превышены установленные максимальные пределы убытков или прибыли. Кроме того, мы представим два динамических подхода к управлению риском на сделку.
Краткое содержание статьи
Мы начнем эту статью с оптимизации некоторых функций, а также конструктора и деструктора класса. Затем определим новые структуры, перечисления и основные константы. Далее реализуем функции для управления позициями, открытыми советником (EA), включая специальные механизмы определения превышения установленных максимальных значений прибыли или убытков. Наконец, в качестве дополнительного элемента мы добавим методы для управления динамическим риском на сделку.
Новые перечисления и структуры
Давайте начнем с определения новых структур, перечислений и констант, которые будем использовать далее в статье.
1. Динамический риск на сделку
Как уже упоминалось ранее, я добавил новое понятие: динамический риск на сделку.
Что он собой представляет?
Динамический риск на сделку, как следует из названия, не является фиксированным значением, а представляет собой величину, изменяющуюся в зависимости от прибыли или убытков относительно заданного начального баланса. Это может быть особенно полезно для защиты торгового счета. Например, если баланс уменьшается до определенного процента от первоначального баланса, можно автоматически скорректировать риск, чтобы минимизировать дальнейшие потери. Это означает, что каждый раз при достижении заданного уровня риск на сделку изменяется.
Рассмотрим практический пример, представленный в таблице. Предположим, что начальный баланс счета составляет 10 000 USD.
Выберем следующие пороговые значения для изменения риска: 3%, 5% и 7%.
| Условие | Новый процент |
|---|---|
| Если баланс уменьшается на 3% (300 USD, то есть ниже 9700 USD) | Риск корректируется до 0,7% |
| Если баланс уменьшается на 5% (500 USD, то есть ниже 9500 USD) | Риск корректируется до 0,5% |
| Если баланс уменьшается на 7% (700 USD, то есть ниже 9300 USD) | Риск корректируется до 0,25% |
Важные замечания о параметрах и работе динамического риска:
- Все значения, упомянутые в этой статье, являются полностью настраиваемыми параметрами, которые можно изменять в зависимости от потребностей и предпочтений пользователя. Сюда входят как проценты, определяющие, когда изменяется риск, так и начальный баланс, к которому эти проценты применяются.
Начальный баланс может определяться двумя способами:
- Счет типа PropFirm (например, FTMO): если этот вариант выбран через параметр управления рисками (profitm_ftmo), начальный баланс должен вводиться вручную через параметр input в настройках советника. Этот баланс остается фиксированным на протяжении всей торговли, то есть не изменяется со временем и является статическим значением, определенным пользователем при запуске торговли.
- Личный счет: если выбран этот тип счета (personal_account), начальный баланс будет автоматически определен советником при вызове функции AccountInfoDouble(), которая возвращает текущий баланс счета в момент инициализации системы. В этом случае баланс будет динамическим и будет точно соответствовать текущему значению средств на счете пользователя.
- значительно снижает риск "сжечь" счет или полностью потерять торговый капитал,
- повышает общую безопасность торговли за счет снижения подверженности продолжительным убыточным сериям.
Недостатки динамического риска:
-
процесс восстановления после убыточной серии или значительной просадки может быть более медленным, за счет превентивного снижения экспозиции. Хотя такое восстановление происходит медленнее, оно более безопасно и контролируемо.
Подробное объяснение структуры, необходимой для реализации динамического риска
Для корректного управления и настройки динамического риска на сделку, нам потребуется получить доступ и изменить свойство, называемое assigned_percentage внутри переменной с именем gmlpo. Для осуществления этих автоматических изменений на основе баланса необходимо создать структуру, содержащую два основных элемента:
- конкретный баланс, при котором будет активироваться изменение риска (например, когда баланс упадет до 9700 USD при начальном балансе 10 000 USD, как показано в таблице).
- новый процент риска, который следует применить к балансу по достижении указанного уровня (например, изменить риск на 0,7% в данном конкретном случае).
Изначально можно было бы подумать о создании простой структуры с двумя отдельными переменными (double), но этого решения недостаточно. Вместо этого мы будем использовать два отдельных массива внутри структуры.
Зачем использовать массивы вместо отдельных переменных
Мы используем массивы вместо отдельных переменных, потому что нам нужно удобно сортировать несколько связанных значений. Необходимость сортировки возникает из-за специфического метода, который мы будем применять для эффективного и быстрого управления динамическим риском. В частности, мы будем использовать функцию ArraySort для сортировки балансов, по достижению которых активируется изменение риска (balance_to_activate_the_risk[]).
Почему необходимо использовать такой метод сортировки, как ArraySort
Основная причина заключается в том, что мы реализуем метод без циклов, предназначенный для постоянной проверки того, не превысил ли текущий баланс определенные лимиты, чтобы скорректировать риск. Такой подход без циклов выбран для оптимизации производительности и скорости системы, что особенно важно при выполнении частых проверок (например, на каждом тике или при закрытии каждой сделки).
Если не отсортировать правильно значения — по возрастанию, можно столкнуться с серьезными проблемами. Рассмотрим практический пример, чтобы прояснить такую ситуацию.
Предположим, что изначально мы определяем следующие проценты:
- первый процент для изменения: 3% (баланс активации – 9700 USD)
- второй процент для изменения: 7% (баланс активации – 9300 USD)
- третий процент для изменения: 5% (баланс активации – 9500 USD)
Как можно заметить, эти значения отсортированы неправильно (второй процент больше третьего). Проблема с отсутствием правильного порядка возникает из-за того, что наш метод без циклов использует целочисленную переменную (вроде счетчика) для отслеживания текущего состояния риска.
Представим, что произойдет:
- Когда начальный баланс уменьшается на 3% (до 9700 USD), счетчик увеличивается на единицу, корректируя риск до 0,7%.
- Затем, когда баланс уменьшается до следующего уровня (7%), счет достигает 9300 USD, и счетчик снова увеличивается, пропуская промежуточное значение (5% при балансе 9500 USD).
- Промежуточное значение (9 500 USD) остается неиспользованным, создавая путаницу и серьезные проблемы в расчетах, поскольку динамический риск не корректируется должным образом.
Более того, если счет начнет восстанавливаться с самого низкого уровня (в данном случае ошибочно с 5%), то для возвращения к предыдущему значению ему придется превысить значение 9300, что неправильно. Но поскольку исходный метод не учитывал порядок корректно, восстановление также не будет работать должным образом, вызывая дополнительные ошибки.
По этим причинам правильная сортировка имеет решающее значение для обеспечения оптимального функционирования метода без циклов. Самая простая структура, которую мы предлагаем для начала, выглядит следующим образом:
struct Dynamic_gmlpo { double balance_to_activate_the_risk[]; double risk_to_be_adjusted[]; };
Однако, хотя эта структура проста, все еще существует важное ограничение. Поскольку эти два массива представляют собой пары ключ-значение (баланс — скорретированный процент риска), крайне важно сохранить это соответствие неизменным при сортировке. Именно здесь вступает в действие структура CHashMap.
Реализация с CHashMap позволяет напрямую связывать каждый конкретный баланс с соответствующим ему процентом риска. При сортировке основного массива (балансов) связь с соответствующим риском будет автоматически сохранена. Таким образом, гарантируется абсолютная точность всех операций и вычислений.
Итак, то, что мы реализовали на данный момент — это простое начальное решение с использованием двух отдельных массивов, позволяющее временно упростить применение метода ArraySort. Однако для более надежной и точной реализации, особенно если планируется упорядоченно и эффективно обрабатывать несколько пар ключ-значение, мы рекомендуем впоследствии использовать CHashMap. Эта структура обеспечивает правильную привязку каждого баланса к соответствующему скорректированному риску, что позволяет избежать потенциальных ошибок при сортировке и запросе динамических значений.
Далее в статье мы подробно рассмотрим, как правильно реализовать это решение с CHashMap, приведя практические примеры и пошаговые объяснения.
Проверка баланса
Продолжая рассмотрение перечислений, необходимых для правильной реализации динамического риска, мы должны добавить две дополнительные опции, которые будут четко определять, в какой момент выполняется проверка баланса. Эта проверка является ключевой, поскольку именно она определяет точный момент, когда нужно оценить, упал ли текущий баланс ниже ранее определенных процентных значений, тем самым активируя изменение динамического риска.
Существует два основных типа проверки:
-
Проверка на каждом тике рынка: в этом методе проверка выполняется постоянно, при каждом движении цены (tick) рынка. Этот вариант отличается высокой точностью, так как непрерывно проверяет, не опустилось ли эквити (текущий баланс) ниже определенного порогового значения. Однако есть и существенный недостаток: постоянное сравнение с текущим эквити может привести к неловким или неэффективным ситуациям.
Например, предположим, что первоначальный баланс составляет 10 000 USD, а первый уровень активации риска наступает, когда баланс достигает 9 700 USD. Если эквити будет колебаться между 9701 и 9699, динамическое переопределение риска будет срабатывать и деактивироваться многократно, что не только неудобно, но и создает ненужную нагрузку на ресурсы системы из-за высокой частоты выполнения этой проверки.
-
Проверка при закрытии сделок: второй метод выполняет проверку только при закрытии сделки, а не на каждом тике. Этот вариант более эффективен с точки зрения использования системных ресурсов, поскольку баланс проверяется только в определенные моменты. Однако он может быть менее точным, поскольку проверка производится только при закрытии сделок и может не учитывать существенные промежуточные колебания, которые могли бы своевременно активировать изменение динамического риска.
Чтобы облегчить выбор между этими двумя методами, мы четко определим их в коде с помощью перечисления:
//--- enum ENUM_REVISION_TYPE { REVISION_ON_CLOSE_POSITION, //Check GMLPO only when closing positions REVISION_ON_TICK //Check GMLPO on all ticks };
Таким образом, мы предоставляем пользователю четкий выбор того, как и когда выполнять проверку баланса, позволяя ему настроить поведение динамического риска в соответствии со своими индивидуальными потребностями и предпочтениями относительно производительности и точности системы.
Режим GMLPO
Для управления режимом, в котором применяется динамический риск на сделку (GMLPO), мы будем использовать специальное перечисление, предлагающее три четко различающихся варианта. Далее я подробно объясню назначение каждого из этих вариантов и почему я решил реализовать их с помощью перечисления.
Первоначально для установки конкретных процентов, по достижении которых должен быть изменен риск, а также для указания новых значений риска, использовались текстовые строки (string), вводимые пользователем. В этом исходном методе пользователь должен был вручную вписать в одну строку (string input) отрицательные процентные значения баланса, которые активировали бы изменение риска, а в другую строку — новые значения риска, которые будут применяться. Несмотря на то, что подход был вполне рабочим, он имел существенный недостаток: текстовые строки не могли быть автоматически оптимизированы с помощью функции оптимизации советника (EA). Это делало процесс настройки и тестирования различных сценариев медленным, непрактичным и довольно утомительным.
Чтобы преодолеть эти ограничения, я решил реализовать перечисление, которое позволяет легко выбирать между тремя четко определенными режимами, обеспечивая гибкость и удобство при оптимизации:
-
DYNAMIC_GMLPO_FULL_CUSTOM: этот режим полностью настраиваемый и дает возможность пользователю вручную задавать несколько уровней активации риска и связанные с ними новые процентные значения. Хотя этот режим сохраняет использование текстовых строк, пользователь может определить желаемое количество изменений, получая максимальную гибкость — в ущерб возможности автоматической оптимизации.
-
DYNAMIC_GMLPO_FIXED_PARAMETERS: этот режим значительно упрощает настройку динамического риска, ограничивая максимальное количество допустимых изменений до четырех. Здесь пользователь задает отрицательные проценты баланса и соответствующие им проценты риска напрямую, через числовые параметры, что значительно облегчает процесс оптимизации. Этот вариант обеспечивает баланс между гибкостью настройки и эффективностью автоматического тестирования и оптимизации.
-
NO_DYNAMIC_GMLPO: последний режим полностью отключает функцию динамического риска. Он идеально подходит для пользователей, которые предпочитают сохранять фиксированный риск на протяжении всей торговли — без динамических изменений, основанных на колебаниях баланса.
//--- enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO { DYNAMIC_GMLPO_FULL_CUSTOM, //Customisable dynamic risk per operation DYNAMIC_GMLPO_FIXED_PARAMETERS,//Risk per operation with fixed parameters NO_DYNAMIC_GMLPO //No dynamic risk for risk per operation };
Такая реализация на основе перечисления обеспечивает наглядность, удобство использования и возможность легко оптимизировать различные конфигурации, позволяя быстро находить наилучшее решение в соответствии с конкретными предпочтениями и стратегиями пользователя.
Учет сделок
Чтобы улучшить нашу систему управления рисками, мы добавим специальный функционал, который позволит вести точный контроль всех открытых позиций на счете, независимо от того, были ли они открыты вручную пользователем или торговым роботом.
В случае позиций, открытых советником, мы также сможем четко определить, соответствует ли конкретный тикет магическому номеру, присвоенному данным советником. Это особенно полезно, когда нужно точно знать, сколько позиций открыто данным советником, и легко отличать их от других позиций, открытых вручную.
Для достижения этой цели мы определим простую, но эффективную структуру, которая будет сохранять основную информацию о каждой позиции:
struct Positions { ulong ticket; //position ticket ENUM_POSITION_TYPE type; //position type };
Превышение максимальной прибыли или убытка
Теперь мы добавим отдельное перечисление, указывающее, какие критерии будут приниматься во внимание для определения было ли превышено заранее установленное значение максимальной прибыли или убытка.
Функция, отвечающая за выполнение этой проверки, будет возвращать true, когда заданные условия выполняются в соответствии с выбранным критерием.
Перечисление будет состоять из трех четко различающихся возможных вариантов:
//--- Mode to check if a maximum loss or gain has been exceeded enum MODE_SUPERATE { EQUITY, //Only Equity CLOSE_POSITION, //Only for closed positions CLOSE_POSITION_AND_EQUITY//Closed positions and equity };
-
EQUITY:
Этот вариант оценивает исключительно текущее эквити счета (то есть баланс в реальном времени с учетом открытых и закрытых позиций). Он не учитывает прибыль или убыток по уже закрытым сделкам за текущий день. Функция будет указывать на превышение максимальной прибыли или убытка только в том случае, если эквити в реальном времени непосредственно превысит установленный лимит. -
CLOSED_POSITIONS:
Этот метод учитывает только прибыль или убытки по позициям, закрытым в течение текущего дня. Он полностью игнорирует открытые позиции и текущее эквити. Таким образом, превышение лимита определяется исключительно по совокупному результату закрытых сделок. -
CLOSED_POSITION_AND_EQUITY:
Это наиболее полный и точный метод, поскольку он объединяет оба упомянутых выше. Функция одновременно оценивает прибыль или убыток по закрытым позициям за день и текущее эквити в реальном времени. Это означает, что анализируется общий результат за день, что обеспечивает более точную и строгую оценку превышения установленных лимитов.
Используя это перечисление в системе управлении рисками, мы обеспечиваем пользователю гибкость выбора способа проверки лимитов, что позволяет легко адаптироваться к различным стратегиям и уровням точности, необходимым при управлении рисками.
Defines
Продолжая работу с новыми переменными, важно определить некоторые константы с помощью директивы #define.
Сначала зададим префикс define, чтобы легко идентифицировать операции и сообщения, генерируемые нашим советником. Такой префикс может включать название советника, что делает его легко различимым в журналах (логах) или комментариях, генерируемых системой. Например, в данном случае мы будем использовать:
#define EA_NAME "CRiksManagement | " //Prefix
Кроме того, необходимо будет определить несколько констант (флагов), которые мы будем использовать в дальнейшем для точного управления как открытыми позициями, так и отложенными ордерами. Эти флаги позволят быстро определять тип сделки (покупка, продажа, лимитные ордера, стоп-ордера и т. д.), что упростит эффективное управление контролем рисков, закрытием позиций, а также выполнение специфических запросов о текущем состоянии рынка и наших позиций.
Ниже приведены соответствующие defines:
//--- positions #define FLAG_POSITION_BUY 2 #define FLAG_POSITION_SELL 4 //--- orders #define FLAG_ORDER_TYPE_BUY 1 #define FLAG_ORDER_TYPE_SELL 2 #define FLAG_ORDER_TYPE_BUY_LIMIT 4 #define FLAG_ORDER_TYPE_SELL_LIMIT 8 #define FLAG_ORDER_TYPE_BUY_STOP 16 #define FLAG_ORDER_TYPE_SELL_STOP 32 #define FLAG_ORDER_TYPE_BUY_STOP_LIMIT 64 #define FLAG_ORDER_TYPE_SELL_STOP_LIMIT 128 #define FLAG_ORDER_TYPE_CLOSE_BY 256
Эти константы обеспечат более ясную и эффективную реализацию в будущих функциях, значительно упрощая чтение, сопровождение и масштабируемость кода советника.
Улучшения в конструкторе и общих функциях
Мы начнем оптимизацию управления рисками с конструктора класса CRiskManagement. Теперь, когда был добавлен динамический риск на сделку, в код внесено несколько важных улучшений:
Во-первых, был четко введен тип используемого лота (type_get_lot) и параметр, связанный с начальным балансом (account_propfirm_balance), что полезно при использовании счета типа PropFirm. Кроме того, можно заметить, что определение EA_NAME будет постоянно отображаться в информационных комментариях, создаваемых основными функциями класса. Это поможет быстро идентифицировать их в логах терминала.
Улучшенная реализация конструктора выглядит следующим образом:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CRiskManagemet::CRiskManagemet(bool mdp_strict_, ENUM_GET_LOT type_get_lot_, ulong magic_number_ = NOT_MAGIC_NUMBER, ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance = 0) { if(magic_number_ == NOT_MAGIC_NUMBER) { Print(EA_NAME, " (Warning) No magic number has been chosen, taking into account all the magic numbers and the user's trades"); } //--- this.mdp_is_strict = mdp_strict_; this.type_get_lot = type_get_lot_; //--- this.account_balance_propfirm = account_propfirm_balance ; trade = new CTrade(); trade.SetExpertMagicNumber(this.magic_number); this.account_profit = GetNetProfitSince(true, this.magic_number, D'1972.01.01 00:00'); this.magic_number = magic_number_; this.mode_risk_managemet = mode_risk_management_; this.ActivateDynamicRiskPerOperation = false; //--- this.last_day_time = iTime(_Symbol, PERIOD_D1, 0); this.last_weekly_time = iTime(_Symbol, PERIOD_W1, 0); this.init_time = magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00'; //--- this.positions_open = false; this.curr_profit = 0; UpdateProfit(); //--- for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER) { this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = position_ticket; ExtraFunctions::AddArrayNoVerification(open_positions, new_pos); } } }
Добавлены новые важные переменные, такие как:
- curr_profit — сохраняет текущую прибыль, тем самым облегчая постоянный контроль результатов;
- ActivateDynamicRiskPerOperation — логическая переменная, определяющая, будет ли использоваться динамический риск во время работы советника;
- mdp_is_strict — переменная, которая определяет, будет ли управление рисками строго отслеживать mdp;
- type_get_lot — переменная, в которой будет храниться тип лота.
Улучшения в деструкторе
В дестуктор также был внесен ряд существенных улучшений, особенно касающихся корректного управления динамической памятью. Теперь выполняется проверка указателя на класс CTrade с помощью функции CheckPointer, гарантируя его удаление только в том случае, если он динамический, тем самым предотвращая возможные ошибки при освобождении памяти.
Оптимизированная реализация деструктора выглядит следующим образом:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CRiskManagemet::~CRiskManagemet() { if(CheckPointer(trade) == POINTER_DYNAMIC) delete trade; ArrayFree(this.open_positions); ArrayFree(this.dynamic_gmlpos.balance_to_activate_the_risk); ArrayFree(this.dynamic_gmlpos.risk_to_be_adjusted); }
Корректное использование CheckPointer гарантирует, что указатель trade освобождается правильно и только тогда, когда это необходимо. Кроме того, с помощью функции ArrayFree, мы эффективно освобождаем память, используемую массивами, принадлежащими классу, что обеспечивает надлежащее управление памятью и делает работу советника, в котором реализовано данное управление рисками, более стабильной и результативной.
Общие улучшения
Я внес ряд важных изменений, направленных на общее усиление системы управления рисками и предотвращение потенциальных ошибок в работе советника (EA). Ниже они описаны подробней.
1. Проверки при получении объема лота и стоп-лосса (SL)
Добавлены ключевые проверки, гарантирующие, что значение риска на сделку (gmlpo.assigned_percentage) не является недопустимым или нулевым. Эти проверки позволяют своевременно выявлять критические ошибки и выводить информационные сообщения пользователю, облегчая быструю корректировку неверных настроек.
Основным критерием является непосредственная проверка gmlpo.assigned_percentage. Если это значение меньше или равно нулю, на консоль выводится критическое сообщение, и функция завершается, возвращая безопасные значения, чтобы предотвратить некорректную работу советника.
if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; }
Пример в функции GetSL:
//+----------------------------------------------------------------------------------+ //| Get the ideal stop loss based on a specified lot and the maximum loss per trade | //+----------------------------------------------------------------------------------+ long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; } double lot; return CalculateSL(type, this.gmlpo.value, lot, DEVIATION, STOP_LIMIT); }
Пример в функции GetLote:
//+-----------------------------------------------------------------------------------------------+ //| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss | //+-----------------------------------------------------------------------------------------------+ double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); this.lote = 0.00; return this.lote; } //--- if(this.type_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION) { double MaxLote = GetMaxLote(order_type); SetNMPLO(this.lote, MaxLote); PrintFormat("%s Maximum loss in case the next operation fails %.2f ", EA_NAME, this.nmlpo); } else { this.lote = GetLotByRiskPerOperation(this.gmlpo.value, order_type); } //--- return this.lote; }
2. Улучшения в функции назначения параметров (SetEnums)
Эта функция является критически важной и должна обязательно выполняться в событии OnInit. Теперь она включает дополнительную проверку, которая обеспечивает корректное присвоение денежных или процентных значений в зависимости от выбора пользователя. Эта проверка предотвращает назначение неверных или отрицательных значений, особенно при использовании фиксированной суммы (money) в качестве критерия.
Улучшенная реализация функции SetEnums:
//+----------------------------------------------------------------------------------------+ //| Function to set how losses or gains are calculated, | //| by percentage applied to (balance, equity, free margin or net profit) or simply money. | //+----------------------------------------------------------------------------------------+ //Note: This method is mandatory, it must be executed in the OnInit event. void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_, ENUM_RISK_CALCULATION_MODE mode_mdp_) { this.gmlpo.mode_calculation_risk = mode_gmlpo_; this.mdl.mode_calculation_risk = mode_mdl_; this.mdp.mode_calculation_risk = mode_mdp_; this.ml.mode_calculation_risk = mode_ml_; this.mwl.mode_calculation_risk = mode_mwl_; //-- En caso se haya escojido el modo dinero, asignamos la variable que guarda el dinero o porcentage alas varialbes correspondientes if(this.gmlpo.mode_calculation_risk == money) { this.gmlpo.value = this.gmlpo.percentage_applied_to; this.ActivateDynamicRiskPerOperation = false; } else this.gmlpo.value = 0; this.mdp.value = this.mdp.mode_calculation_risk == money ? (this.mdp.percentage_applied_to > 0 ? this.mdp.percentage_applied_to : 0) : 0; this.mdl.value = this.mdl.mode_calculation_risk == money ? (this.mdl.percentage_applied_to > 0 ? this.mdl.percentage_applied_to : 0) : 0; this.ml.value = this.ml.mode_calculation_risk == money ? (this.ml.percentage_applied_to > 0 ? this.ml.percentage_applied_to : 0) : 0; this.mwl.value = this.mwl.mode_calculation_risk == money ? (this.mwl.percentage_applied_to > 0 ? this.mwl.percentage_applied_to : 0) : 0; }
Новые переменные для класса
Продолжая усовершенствования, добавим переменные, которые позволят нам осуществлять более точный и эффективный контроль открытых сделок.
Сначала определим специальный массив для хранения информации обо всех открытых позициях на счете, как вручную пользователем, так и автоматически советником:
//--- Positions open_positions[];
Кроме того, добавим две дополнительные ключевые переменные:
- Логическую (булеву) переменную positions_open, которая будет указывать, есть ли открытые позиции на рынке. Эта переменная играет важную роль в оптимизации производительности и позволяет избегать лишних проверок при отсутствии активных позиций:
//--- Boolean variable to check if there are any open operations by the EA or user bool positions_open;
-
Переменную типа double, называемую curr_profit, которая будет хранить текущую накопленную прибыль или убыток в режиме реального времени. Эта переменная облегчает быстрые расчеты текущего состояния сделок:
//--- Variable to store the current profit of the EA or user double curr_profit;
Динамический риск на сделку
Для эффективного управления динамическим риском на сделку (динамический GMLPO) необходимо добавить и четко определить несколько специфических переменных в нашем классе. Эти переменные обеспечат эффективный контроль, позволяя автоматически корректировать риск в соответствии с заданными пользователем параметрами. Ниже подробно объясняется назначение каждой из них:
1. Структура для хранения балансов и динамических рисков
Мы будем использовать пользовательскую структуру, называемую Dynamic_gmlpo, которая будет содержать два динамических массива:
Dynamic_gmlpo dynamic_gmlpos;
Эта структура позволяет хранить несколько уровней конкретных балансов вместе с соответствующими процентами риска, тем самым облегчая управление динамическим риском.
2. Переменная для определения типа проверки динамического риска
Мы определим переменную перечисляемого типа с именем revision_type, которая разрешит пользователю выбирать, как будут выполняться проверки динамического риска (на каждом тике или при закрытии позиций):
ENUM_REVISION_TYPE revision_type;
3. Логическая (булева) переменная для активации или деактивации динамического риска
Эта логическая переменная будет хранить решение о том, использовать ли динамический риск на сделку или нет:
bool ActivateDynamicRiskPerOperation; 4. Переменная для хранения текущего индекса динамического риска
Чтобы поддерживать точный контроль за изменением уровня динамического риска, используется переменная-индекс, указывающая, на каком элементе динамического массива мы находимся в данный момент:
int index_gmlpo; 5. Переменная для хранения базового (начального) баланса
Эта переменная хранит начальный или эталонный баланс, выбранный пользователем, к которому впоследствии будут применены заданные проценты для активации динамического риска:
double chosen_balance; 6. Переменная, указывающая следующий целевой баланс для изменения риска на сделку (в положительном направлении)
В эту переменную сохраняется следующий уровень баланса, который необходимо превысить, чтобы динамически скорректировать процент риска:
double NewBalanceToOvercome; 7. Логическая переменная для предотвращения ошибок при выходе индекса за допустимый диапазон
Эта логическая переменная указывает, достигнут ли минимально возможный баланс, установленный пользователем (максимально допустимый отрицательный процент). Если значение переменной становится true, увеличение индекса будет приостановлено, чтобы предотвратить превышение максимально допустимого диапазона:
bool TheMinimumValueIsExceeded; 8. Переменная для хранения начального процента риска на сделку
Эта переменная хранит начальный процент, определенный для риска на сделку. Она используется главным образом для восстановления исходного значения, когда баланс восстанавливается после достижения нижних уровней:
double gmlpo_percentage; Эти переменные позволяют эффективно реализовать надежный, безопасный и простой в управлении механизм, обеспечивающий понятность и точный контроль динамического риска на сделку в различных рабочих сценариях.
Переменная для контроля максимальной дневной прибыли
Для более строгого контроля дневной прибыли мы введем логическую переменную mdp_is_strict. Эта переменная поможет системе управления рисками определить, должен ли расчет максимальной прибыли корректироваться с учетом дневных убытков.
- Если mdp_is_strict имеет значение true: максимальная дневная прибыль будет считаться превышенной только в том случае, если будут восстановлены все предыдущие убытки, и затем достигнут исходный целевой уровень. Например, если максимальная дневная прибыль составляет 50 USD, и в течение дня было потеряно 20 USD, то потребуется заработать в общей сложности 70 USD (восстановить 20 USD убытков и получить 50 USD чистой прибыли), чтобы считать целевой уровень достигнутым.
- Если mdp_is_strict равно false: дневные убытки не влияют на расчет максимальной прибыли. В этом случае, если целевая прибыль составляет 50 USD, и было потеряно 40 USD, достаточно будет заработать дополнительно 10 USD (компенсировать 40 USD убытков плюс 10 USD чистой прибыли), чтобы достичь максимальной дневной прибыли.
bool mdp_is_strict; Переменная для типа лота
Чтобы упростить функциональность назначения лотов, мы изменим функцию GetBatch() так, чтобы больше не требовалось указывать тип лота вручную. Вместо этого тип лота будет инициализирован в конструкторе или может быть изменен через дополнительные функции, которые будут нами разработаны. Такой подход обеспечит более прямую настройку и снизит риск ошибок, связанных с ручным вводом параметров.
ENUM_GET_LOT type_get_lot;
Благодаря этим улучшениям, мы надеемся повысить эффективность и точность как в управлении рисками, так и в назначении лотов на нашей торговой платформе.
Управление открытыми позициями
В этом разделе мы сосредоточимся на корректном управлении всеми позициями, открытыми по магическому номеру или самим пользователем. Для этого мы создадим несколько полезных и понятных функций, позволяющих держать под контролем открытые сделки в любой момент времени.
1. Функция проверки наличия тикета во внутреннем массиве (open_positions)
Функция TheTicketExists предназначена для быстрой проверки, содержится ли определенный тикет в нашем внутреннем массиве открытых позиций (open_positions). Эта операция особенно важна, когда нужно подтвердить, ведется ли уже управление определенной позицией, или требуется предпринять дополнительные действия.
Логика проста: мы проходим по массиву и сравниваем каждый элемент с переданным тикетом. Если совпадение найдено, возвращаем true, в противном случае — false.
Объявление:
bool TheTicketExists(const ulong ticket); //Check if a ticket is in the operations array
Реализация:
//+------------------------------------------------------------------+ //| Function to check if the ticket is in the array | //+------------------------------------------------------------------+ bool CRiskManagemet::TheTicketExists(const ulong ticket) { for(int i = 0; i < this.GetPositionsTotal() ; i++) if(this.open_positions[i].ticket == ticket) return true; return false; }
2. Функция для получения общего количества открытых позиций
Функция GetPositionsTotal просто возвращает количество элементов, присутствующих во внутреннем массиве open_positions. Она позволяет легко и быстро определить, сколько позиций управляется в реальном времени.
Объявление и реализация:
inline int GetPositionsTotal() const { return (int)this.open_positions.Size(); } //Get the total number of open positions
3. Функция получения общего количества позиций с флагами
Функция GetPositions использует систему флагов (flags) для обеспечения большей гибкости при подсчете открытых позиций по определенным критериям, таким, например, как тип сделки (покупка или продажа). Для этого мы преобразуем тип позиции (ENUM_POSITION_TYPE) в значения, совместимые с бинарными флагами (обычно степени числа 2 — например, 2, 4, 8 и т.д.).
Логика заключается в переборе всех открытых позиций и проверке каждой из них на соответствие указанным флагам, с последующим увеличением счетчика для каждого найденного совпадения.
Реализация:
//+------------------------------------------------------------------+ //| Function to obtain the number of open positions | //+------------------------------------------------------------------+ int CRiskManagemet::GetPositions(int flags) const { int count = 0; for(int i = 0; i < ArraySize(this.open_positions) ; i++) { if(this.open_positions[i].type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_BUY) != 0 ) { count++; } else if(this.open_positions[i].type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_SELL) != 0 ) { count++; } } return count; }
4. Функция проверки наличия открытых позиций на данный момент
Наконец, функция ThereAreOpenOperations предоставляет быстрый и эффективный способ узнать, обрабатывает ли наш советник в данный момент открытые позиции. Она просто возвращает значение внутренней логической переменной (positions_open).
Объявление и реализация:
inline bool ThereAreOpenOperations() const { return this.positions_open; } //Check if there are any open operations
5. Дополнительные полезные функции
Помимо функций, которые являются непосредственной частью класса CRiskManagement, будут созданы внешние дополнительные функции для облегчения выполнения специфических задач, таких, например, как закрытие ордеров на основе флагов.
5.1 Функция закрытия отложенных ордеров по флагам
Перед закрытием ордеров на основе определенных критериев, необходимо преобразовать типы ордеров (ENUM_ORDER_TYPE) в совместимые бинарные флаги. Этот шаг крайне важен для предотвращения ошибок при выполнении побитовых операций (&).
Ниже представлена простая функция, которая преобразует конкретный тип ордера в соответствующий флаг.
Если передано недопустимое или обобщенное значение (WRONG_VALUE), функция возвращает комбинацию всех флагов:
// Converts an order type to its corresponding flag int OrderTypeToFlag(ENUM_ORDER_TYPE type) { if(type == ORDER_TYPE_BUY) return FLAG_ORDER_TYPE_BUY; else if(type == ORDER_TYPE_SELL) return FLAG_ORDER_TYPE_SELL; else if(type == ORDER_TYPE_BUY_LIMIT) return FLAG_ORDER_TYPE_BUY_LIMIT; else if(type == ORDER_TYPE_SELL_LIMIT) return FLAG_ORDER_TYPE_SELL_LIMIT; else if(type == ORDER_TYPE_BUY_STOP) return FLAG_ORDER_TYPE_BUY_STOP; else if(type == ORDER_TYPE_SELL_STOP) return FLAG_ORDER_TYPE_SELL_STOP; else if(type == ORDER_TYPE_BUY_STOP_LIMIT) return FLAG_ORDER_TYPE_BUY_STOP_LIMIT; else if(type == ORDER_TYPE_SELL_STOP_LIMIT) return FLAG_ORDER_TYPE_SELL_STOP_LIMIT; else if(type == ORDER_TYPE_CLOSE_BY) return FLAG_ORDER_TYPE_CLOSE_BY; return (FLAG_ORDER_TYPE_BUY | FLAG_ORDER_TYPE_SELL | FLAG_ORDER_TYPE_BUY_LIMIT | FLAG_ORDER_TYPE_SELL_LIMIT | FLAG_ORDER_TYPE_BUY_STOP | FLAG_ORDER_TYPE_SELL_STOP | FLAG_ORDER_TYPE_BUY_STOP_LIMIT | FLAG_ORDER_TYPE_SELL_STOP_LIMIT | FLAG_ORDER_TYPE_CLOSE_BY); }
Основная функция проходит по всем существующим ордерам, закрывая те, которые соответствуют указанным флагам:
// Close all orders that match the flags in `flags` void CloseAllOrders(int flags, CTrade &obj_trade, ulong magic_number_ = NOT_MAGIC_NUMBER) { ResetLastError(); for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(OrderSelect(ticket)) { ENUM_ORDER_TYPE type_order = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); ulong magic = OrderGetInteger(ORDER_MAGIC); int bandera = OrderTypeToFlag(type_order); if((bandera & flags) != 0 && (magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { if(type_order == ORDER_TYPE_BUY || type_order == ORDER_TYPE_SELL) obj_trade.PositionClose(ticket); else obj_trade.OrderDelete(ticket); } } else { PrintFormat("Error selecting order %d, last error %d", ticket, GetLastError()); } } }
5.2 Функции для получения общего количества открытых позиций
Также добавляется простая внешняя функция для подсчета общего количества открытых позиций, без необходимости полагаться исключительно на класс CRiskManagement. Сначала необходимо преобразовать типы позиций (ENUM_POSITION_TYPE) в совместимые флаги.
5.2.1 Функция преобразования ENUM_POSITION_TYPE в допустимый флаг
int PositionTypeToFlag(ENUM_POSITION_TYPE type) { if(type == POSITION_TYPE_BUY) return FLAG_POSITION_BUY; else if(type == POSITION_TYPE_SELL) return FLAG_POSITION_SELL; return FLAG_POSITION_BUY | FLAG_POSITION_SELL; }
5.2.2 Функция для получения общего количества открытых позиций по флагам
Эта функция проходит по всем существующим позициям и считает только те, которые соответствуют указанным флагам:
//--- int GetPositions(int flags = FLAG_POSITION_BUY | FLAG_POSITION_SELL, ulong magic_number_ = NOT_MAGIC_NUMBER) { int counter = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; // Si la selección falla, pasa a la siguiente posición ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position type matches the flags if((flags & PositionTypeToFlag(type)) != 0 && (position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { counter++; } } return counter; }
Методы обработки превышения лимитов
Теперь мы определим несколько функций, которые позволяют определить, превысили ли мы максимальный убыток или максимальную дневную прибыль.
Начнем с разработки двух основных функций: одна предназначена для проверки превышения убытков, а другая — для подтверждения достижения максимальной ожидаемой прибыли.
Каждый из этих режимов заключен в функцию, которая в зависимости от выбранного нами варианта сообщит, сумели мы достичь поставленной цели (возвращая значение true), или нам еще предстоит долгий путь (возвращая значение false). Например, для режима "CLOSED_POSITIONS" крайне важно знать, какая прибыль уже получена, так как необходимо сравнить ее с установленными лимитами максимальных убытков или прибыли — дневных или недельных.
Посмотрим, как это можно реализовать в коде:
//+------------------------------------------------------------------+ //| Boolean function to check if a loss was overcome | //+------------------------------------------------------------------+ bool CRiskManagemet::IsSuperated(double profit_, double loss_, const MODE_SUPERATE mode) const { if(loss_ <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit * -1 > loss_) return true; } else if(mode == CLOSE_POSITION) { if(profit_ * -1 > loss_) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_loss = profit_ < 0 ? loss_ - MathAbs(profit_) : loss_; if(this.curr_profit * -1 > new_loss) return true; } return false; }
Разбор режимов:
-
EQUITY: в этом режиме мы напрямую сравниваем текущую прибыль (эквити минус баланс счета). Если это отрицательное значение превышает заданный уровень убытка, значит, мы превысили установленный лимит.
-
CLOSE_POSITION: в этом режиме анализируется прибыль по уже закрытым позициям и, для корректного сравнения, умножается на -1.
-
CLOSE_POSITION_AND_EQUITY: этот режим немного более сложный. Здесь мы корректируем максимальный убыток с учетом текущей прибыли. Если день оказался убыточным, и прибыль отрицательна, мы вычитаем это значение из допустимого предела убытка. Если отрицательная текущая прибыль превышает скорректированное значение, значит, порог также был превышен.
Специализированные функции для максимальной прибыли
Как и в случае с убытками, нам также необходим способ проверить, не превысили ли мы максимально ожидаемую прибыль. Для этого создадим специальную функцию, которая гарантирует, что наши сделки не приведут к непреднамеренному изменению значения максимальной дневной прибыли (mdp).
Ниже представлен код этой функции:
//+------------------------------------------------------------------+ //| Function to check if the maximum profit per day was exceeded | //+------------------------------------------------------------------+ bool CRiskManagemet::MDP_IsSuperated(const MODE_SUPERATE mode) const { if(this.mdp.value <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION) { if(this.daily_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_mdp = this.daily_profit > 0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -1)); if(this.curr_profit > new_mdp) return true; } //--- return false; }
Подробности работы функции:
-
EQUITY: здесь напрямую сравнивается текущая прибыль (curr_profit) со значением максимальной дневной прибыли (mdp). Если текущая прибыль больше, цель считается превышенной.
-
CLOSE_POSITION: здесь проверяется, превышает ли прибыль, полученная только по позициям, закрытым в течение дня (daily_profit), значение mdp.
-
CLOSE_POSITION_AND_EQUITY: этот случай наиболее сложный и включает две ситуации:
- Если дневная прибыль положительная, мы просто вычитаем это значение из mdp, чтобы установить новую скорректированную цель.
- Если дневная прибыль отрицательная, а политика управления прибылью строгая (mdp_is_strict), мы добавляем абсолютное значение отрицательной прибыли к mdp, чтобы компенсировать убытки, прежде чем можно будет считать целевой показатель превышенным. Если же политика нестрогая, используется исходное значение mdp, а дневные убытки игнорируются.
Эта функция обеспечивает точное управление целями по прибыли, гарантируя, что даже в волатильные торговые дни можно будет правильно оценить, достигли ли мы или превысили свои финансовые ожидания.
Максимальный убыток в PropFirm
Продолжая тему управления убытками, следует отметить важную особенность, характерную для PropFirm-счетов, особенно таких, как FTMO. В этих счетах предел максимального убытка фиксирован — он не изменяется и остается постоянным с начала теста. Например, если мы начнем со счета в 10 000 USD, то максимальный лимит убытка составит 9 000 USD. Это означает, что если в какой-либо момент эквити счета опустится ниже этого порога, возможность получения финансирования автоматически будет потеряна.
Чтобы упростить контроль этого порога и избежать дополнительных осложнений, мы реализуем специальный метод, который будет проверять, достигнут ли или превышен максимальный предел убытка:
//--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type inline bool IsSuperatedMLPropFirm() const { return (this.ml.value == 0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); }
Логика проста и понятна: если у нас нет открытых позиций или переменная, контролирующая максимальный убыток (ml), равна нулю, проверка не выполняется, и возвращается false. И наоборот, если есть открытые позиции, и переменной ml присвоено значение, функция сравнивает текущее эквити с разницей между начальным балансом теста и максимально допустимым убытком.
Функции для проверки
Помимо описанного выше метода, мы создадим несколько практических функций, которые позволят быстро определить, были ли превышены или достигнуты различные заранее установленные уровни прибыли или убытков. Эти функции служат упрощенными вызовами (обертками) для общего метода IsSuperated:
//--- functions to verify if the established losses were exceeded inline bool ML_IsSuperated(const MODE_SUPERATE mode) const {return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm(); } inline bool MWL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.weekly_profit, this.mwl.value, mode); } inline bool MDL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.daily_profit, this.mdl.value, mode); } inline bool GMLPO_IsSuperated() const {return IsSuperated(0, this.gmlpo.value, EQUITY); } inline bool NMLPO_IsSuperated() const {return IsSuperated(0, this.nmlpo, EQUITY); } bool MDP_IsSuperated(const MODE_SUPERATE mode) const;
Эти функции обеспечивают эффективный и простой способ оценки конкретных условий для дневных, недельных и так далее убытков.
Максимальный динамический дневной убыток в FTMO
Ключевым аспектом управления рисками в FTMO является понимание того, что максимальный дневной убыток не является фиксированным значением, а динамическим. Этот показатель изменяется в зависимости от прибыли, накопленной в течение дня. То есть, чем больше прибыли получено за день, тем больше допустимый диапазон для возможных убытков, и следовательно этот лимит колеблется, а не остается постоянным.
Ниже представлена функция, которая обновляет значение максимального дневного убытка на основе полученной прибыли:
//--- Update Loss (only if ftmo propfirm FTMO is selected) inline void UpdateDailyLossFTMO() { this.mdl.value += this.daily_profit > 0 ? this.daily_profit : 0; PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }
Эта функция должна быть включена в метод, который обрабатывает транзакции советника, а именно — в OnTradeTransaction.
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result)
Управление событиями
Теперь перейдем к новым "событиям", которые будут выполняться в рамках событий советника.
OnTradeTransaction
Метод OnTradeTransaction играет ключевую роль в динамическом управлении рисками, поскольку запускается при любом событии, связанном с торговыми операциями счета, например, открытием или закрытием позиций, изменением существующих ордеров или пополнением счета.
Функция имеет три важных параметра:
- trans — подробная информация о текущей транзакции,
- request — сведения о торговых запросах (ожидающих или недавно выполненных),
- result — результаты, полученные после обработки этих запросов.
Для надлежащего управления рисками нас в первую очередь интересует информация, содержащаяся в trans, поскольку именно там можно найти точные сведения о типе транзакции и связанной с ней операции.
Существует несколько типов торговых транзакций, которые определены в перечислении:
ENUM_TRADE_TRANSACTION_TYPE Типы транзакций:
| Идентификатор | Описание |
|---|---|
| TRADE_TRANSACTION_ORDER_ADD | Добавление нового открытого ордера. |
| TRADE_TRANSACTION_ORDER_UPDATE | Изменение открытого ордера. К таким изменениям относятся не только явные правки со стороны клиентского терминала или торгового сервера, но и изменения его состояния во время размещения (например, переход из состояния ORDER_STATE_STARTED в ORDER_STATE_PLACED или из ORDER_STATE_PLACED в ORDER_STATE_PARTIAL и так далее). |
| TRADE_TRANSACTION_ORDER_DELETE | Удаление ордера из списка открытых ордеров. Ордер может быть удален из открытых либо в результате размещения соответствующей заявки, либо после ее исполнения (заполнения) и занесения в историю. |
| TRADE_TRANSACTION_DEAL_ADD | Добавление сделки (deal) в историю. Происходит в результате исполнения ордера или проведения операции с балансом счета. |
| TRADE_TRANSACTION_DEAL_UPDATE | Изменение сделки (deal) в истории. Возможна ситуация, когда ранее выполненная сделка (deal) изменяется на сервере. Например, если она была скорректирована во внешней торговой системе (бирже), куда была передана брокером. |
| TRADE_TRANSACTION_DEAL_DELETE | Удаление сделки (deal) из истории. Может случиться так, что ранее выполненная сделка (deal) удаляется на сервере. Например, если она была удалена во внешней торговой системе (бирже), куда она была передана брокером. |
| TRADE_TRANSACTION_HISTORY_ADD | Добавление ордера в историю в результате его исполнения или отмены. |
| TRADE_TRANSACTION_HISTORY_UPDATE | Изменение ордера, находящегося в истории ордеров. Этот тип также предусмотрен для расширения серверной функциональности. |
| TRADE_TRANSACTION_HISTORY_DELETE | Удаление ордера из истории ордеров. Этот тип также предусмотрен для расширения серверной функциональности. |
| TRADE_TRANSACTION_POSITION | Изменение позиции, не связанное с исполнением сделки (deal). Этот тип транзакции означает, что позиция была изменена на стороне торгового сервера. Позиция может подвергаться изменениям по объему, цене открытия, а также уровням Stop Loss и Take Profit. Информация об изменениях передается в структуре MqlTradeTransaction через обработчик OnTradeTransaction. Изменение позиции (добавление, изменение или удаление) в результате совершения сделки (deal) не приводит впоследствии к появлению транзакции (transaction) типа TRADE_TRANSACTION_POSITION. |
| TRADE_TRANSACTION_REQUEST | Уведомление о том, что торговый запрос был обработан сервером, и уже получен результат его обработки. Для транзакций данного типа в структуре MqlTradeTransaction необходимо проанализировать только одно поле — type (тип транзакции). Для получения дополнительной информации нужно проанализировать второй и третий параметры функции OnTradeTransaction (request y result). |
Однако нас интересует следующий тип транзакции:
- TRADE_TRANSACTION_DEAL_ADD — указывает, что новая транзакция была добавлена в историю (закрытая сделка или подтвержденное открытие позиции).
Создание функции OnTradeTransactionEvent
Далее мы определим специальную функцию для четкого и эффективного управления этим событием:
void OnTradeTransactionEvent(const MqlTradeTransaction& trans);
Для функции потребуется только параметр trans, который содержит всю необходимую информацию о текущей транзакции.
Базовая структура нашей функции начнется с проверки того, что это тип транзакции TRADE_TRANSACTION_DEAL_ADD и предварительного выбора сделки из истории:
//+------------------------------------------------------------------+ //| OnTradeTransaction Event | //+------------------------------------------------------------------+ void CRiskManagemet::OnTradeTransactionEvent(const MqlTradeTransaction &trans) { HistoryDealSelect(trans.deal); if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY); ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC); bool is_select = PositionSelectByTicket(trans.position); if(entry == DEAL_ENTRY_IN && is_select && (this.magic_number == position_magic || this.magic_number == NOT_MAGIC_NUMBER)) { Print(EA_NAME, " New position opened with ticket: ", trans.position); this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = trans.position; AddArrayNoVerification(open_positions, new_pos); return; } if(entry == DEAL_ENTRY_OUT && TheTicketExists(trans.position) == true && !is_select) { Print(EA_NAME, " Position with ticket ", trans.position, " has been closed"); DeleteTicket(trans.position); //--- if(this.revision_type == REVISION_ON_CLOSE_POSITION) CheckAndModifyThePercentageOfGmlpo(); //--- if(GetPositionsTotal() == 0) this.positions_open = false; //--- UpdateProfit(); //--- if(this.mode_risk_managemet == propfirm_ftmo) UpdateDailyLossFTMO(); SetGMLPO(); Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value)); } } }
Внутри этой функции мы обрабатываем два ключевых сценария:
-
Открытие позиций: мы подтверждаем, что позиция действительно открыта (is_select), и что ее магический номер совпадает с заданным, прежде чем включить ее в наш внутренний список (open_positions).
-
Закрытие позиций: мы проверяем, что позиция была фактически закрыта (не может быть выбрана), и что она присутствует в нашем списке управляемых позиций. В этом случае мы удаляем тикет из регистра, обновляем ключевые переменные, такие как накопленная прибыль и общее состояние открытых позиций, а также динамически корректируем максимальный дневной убыток, если используется режим FTMO.
Таким образом, система может заблаговременно реагировать на каждую зарегистрированную транзакцию, обеспечивая четкое, эффективное и динамическое управление рисками.
OnTickEvent
Событие OnTick является одним из ключевых в работе советников (EA), поскольку оно срабатывает при каждом новом тике на рынке.
В нашем конкретном случае оно будет использоваться для обновления общей накопленной прибыли по всем открытым позициям, с фильтрацией по магическому номеру, если он задан. Если магический номер не установлен, обновление будет выполняться для всех открытых позиций, без учета значения magic.
Базовое определение метода будет следующим:
void OnTickEvent(); Эта функция будет выполняться только при наличии открытых позиций, чтобы избежать ненужных вычислительных процессов.
Структура функции реализуется следующим образом:
//+------------------------------------------------------------------+ //| Function to execute in OnTick | //+------------------------------------------------------------------+ void CRiskManagemet::OnTickEvent(void) { if(!positions_open) return; //--- GetPositionsProfit(); //--- if(this.revision_type == REVISION_ON_TICK) CheckAndModifyThePercentageOfGmlpo(); }Подробное объяснение процесса:
-
Обновление общей прибыли:
-
метод GetPositionsProfit() отвечает за получение и обновление информации о текущей прибыли или убытке по управляемым позициям. Это гарантирует, что у нас всегда есть актуальные и точные данные о совокупной эффективности открытых сделок.
-
-
Проверка и динамическая модификация GMLPO:
-
если мы выбрали опцию REVIEW_ON_TICK, советник на каждом новом тике постоянно будет проверять, был ли превышен ранее определенный порог прибыли или убытка, тем самым динамически корректируя допустимый риск на сделку. Это позволяет регулировать объем открытых позиций (уровень рыночной экспозиции) в режиме реального времени и повышает эффективность управления рисками.
-
Функции для динамического риска и работы с массивом позиций
В этом разделе мы реализуем ключевые функции, которые позволят эффективно работать с динамическим риском и выполнять практические задачи, связанные с массивами позиций. Эти инструменты будут полезны на различных этапах нашей стратегии управления рисками.
Функция преобразования строки (string) в тип данных
Эта функция использует шаблоны для легкой адаптации к различным простым типам данных (за исключением сложных классов или структур). Ее основная цель — преобразовать строку в требуемый тип данных, упрощая динамическую обработку информации в реальном времени.
Реализация:
template <typename S> void StringToType(string token, S &value, ENUM_DATATYPE type) { if(StringLen(token) == 0) { Print("Error: String is empty."); return; } switch(type) { case TYPE_BOOL: value = (S)(StringToInteger(token) != 0); // Convertir a bool break; case TYPE_CHAR: value = (S)((char)StringToInteger(token)); // Convertir a char break; case TYPE_UCHAR: value = (S)((uchar)StringToInteger(token)); // Convertir a uchar break; case TYPE_SHORT: value = (S)((short)StringToInteger(token)); // Convertir a short break; case TYPE_USHORT: value = (S)((ushort)StringToInteger(token)); // Convertir a ushort break; case TYPE_COLOR: value = (S)((color)StringToInteger(token)); // Convertir a color break; case TYPE_INT: value = (S)(StringToColor(token)); // Convertir a int break; case TYPE_UINT: value = (S)((uint)StringToInteger(token)); // Convertir a uint break; case TYPE_DATETIME: value = (S)(StringToTime(token)); // Convertir a datetime break; case TYPE_LONG: value = (S)((long)StringToInteger(token)); // Convertir a long break; case TYPE_ULONG: value = (S)((ulong)StringToInteger(token)); // Convertir a ulong break; case TYPE_FLOAT: value = (S)((float)StringToDouble(token)); // Convertir a float break; case TYPE_DOUBLE: value = (S)(StringToDouble(token)); // Convertir a double break; case TYPE_STRING: value = (S)(token); // Mantener como string break; default: Print("Error: Unsupported data type in ConvertToType."); break; } }
Эта функция будет особенно важна при работе с динамическим риском, поскольку параметры будут поступать в виде текстовых строк, и потребуется преобразовать их в числовые переменные для последующего использования.
Функция для преобразования текстовой строки в массив простых типов
Аналогичным образом эта функция использует универсальность шаблонов для упрощения процесса преобразования текстовой строки в массив нужного типа. Функция выполняет следующее:
- разделяет исходную строку, используя определенный разделитель (по умолчанию – запятую ',');
- сохраняет каждый полученный элемент во временный массив;
- преобразует каждый отдельный элемент в указанный тип данных и присваивает их целевому массиву.
//--- template <typename S> void StringToArray(S &array_receptor[], string cadena, ENUM_DATATYPE type_data, ushort separator = ',') { string result[]; int num = StringSplit(cadena, separator, result); ArrayResize(array_receptor, ArraySize(result)); for(int i = 0; i < ArraySize(array_receptor) ; i++) { S value; StringToType(result[i], value, type_data); array_receptor[i] = value; } }
Например, в практических ситуациях, связанных с динамическим риском, мы можем получать данные вида «5.0,4.5,3.0», которые будут автоматически преобразованы в массив типа double. Это значительно упрощает управление динамическими параметрами внутри нашей системы.
Расширенные функции для работы с массивами
Ниже представлено несколько полезных функций, которые облегчат эффективное управление массивами, особенно при работе с динамическими стратегиями и сложными структурами в рамках управления рисками.
Функция для удаления нескольких элементов из массива по индексу
Эта функция позволяет одновременно удалять несколько элементов из массива, значительно повышая эффективность и читаемость кода. Основная логика заключается в предварительной сортировке индексов элементов, которые мы хотим удалить, а затем копировании только тех элементов, которые нужно сохранить, в новый индекс исходного массива.
Реализация:
//--- template <typename T> void RemoveMultipleIndexes(T &arr[], int &indexes_to_remove[]) { int oldSize = ArraySize(arr); int removeSize = ArraySize(indexes_to_remove); if(removeSize == 0 || oldSize == 0) return; // Ordenamos los índices para garantizar eficiencia al recorrerlos ArraySort(indexes_to_remove); int writeIndex = 0, readIndex = 0, removeIndex = 0; while(readIndex < oldSize) { if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex]) { removeIndex++; } else { arr[writeIndex] = arr[readIndex]; writeIndex++; } readIndex++; } ArrayResize(arr, writeIndex); }
Функция для добавления элементов в массив
Следующая функция позволяет легко добавлять новые элементы в массив любого типа, автоматически адаптируясь к типу данных.
Реализация:
//--- template <typename X> void AddArrayNoVerification(X &array[], const X &value) { ArrayResize(array, array.Size() + 1); array[array.Size() - 1] = value; }
Все просто: мы увеличиваем размер массива на одну единицу и присваиваем новое значение последнему элементу.
Специализированная функция для удаления одного элемента по тикету
Эта конкретная функция предназначена для массивов структур, которые содержат поле "ticket". Она позволяет удалить конкретный элемент на основе этого уникального идентификатора.
Реализация:
//--- template<typename T> bool RemoveIndexFromAnArrayOfPositions(T &array[], const ulong ticket) { int size = ArraySize(array); int index = -1; // Search index and move elements in a single loop for(int i = 0; i < size; i++) { if(array[i].ticket == ticket) { index = i; } if(index != -1 && i < size - 1) { array[i] = array[i + 1]; // Move the elements } } if(index == -1) return false; // Reducir el tamaño del array if(size > 1) ArrayResize(array, size - 1); else if(size <= 1) ArrayFree(array); return true; }
Важно отметить, что эта функция в обязательном порядке требует, чтобы структура включала элемент с именем "ticket". Если мы попытаемся использовать его со структурами, в которых такого элемента нет, возникнет ошибка:
'ticket' - undeclared identifier
in template 'bool RemoveIndexFromAnArrayOfPositions(T&[],const ulong)' specified with [T=Message]
see template instantiation 'ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>'
1 errors, 0 warnings
В данном случае структура "Message" не содержит элемента "ticket".
Дополнительная функция для повторения текстовой строки
Наконец, у нас есть полезная функция, которая генерирует повторяющийся текст столько раз, сколько нам требуется. Это особенно удобно для печати таблиц или визуального разделения информации.
Реализация:
string StringRepeat(string str, int count) { string result = ""; for(int i = 0; i < count; i++) result += str; return result; }
Например, при вызове
StringRepeat("-", 10) мы получим "----------".
Эти расширенные функции значительно упрощают работу с массивами и повышают читаемость кода, предоставляя эффективные и универсальные инструменты для динамического и точного управления рисками в наших торговых системах.
Построение динамического риска
Наконец, мы подошли к самой важной части — реализации динамического риска. Для этого мы воспользуемся двумя ключевыми функциями, которые значительно упростят его обработку и адаптивность.
Прежде чем начать, необходимо подключить следующую библиотеку:
#include <Generic\HashMap.mqh> Эта библиотека поможет нам организовать и корректно обрабатывать данные, связанные с динамическим риском.
Функция инициализации динамического риска
Следуя первоначальной концепции, представленной в предыдущих статьях, наша динамическая система риска будет основана на структуре, содержащей два массива типа double: один — для хранения новых значений риска, которые будут применяться, и другой — для указания конкретных уровней баланса или процентов, которые активируют эти изменения.
Из-за ограничений языка MQL5 невозможно напрямую передавать массивы типа double в качестве параметров советника. Чтобы обойти это ограничение, мы будем использовать текстовые строки (strings), разделенные запятыми, которые затем преобразуем в числовые массивы с помощью функций, реализованных ранее.
Основная функция инициализации динамического риска будет отвечать за преобразование этих строк в числовые массивы, а также за проверку на отсутствие дубликатов и корректную сортировку значений.
Объявление функции будет выглядеть следующим образом:
void SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_);
Внутренняя процедура будет такой:
1. Проверка использования динамического риска
Перед выполнением любой операции проверяем корректность определения процента GMLPO:
if(this.gmlpo.assigned_percentage == 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; }
2. Назначение выбранного типа проверки
this.revision_type = revision_type_;
3. Преобразование строк в числовые массивы
Используя ранее созданные функции, преобразуем строки в массивы типа double:
//--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');
4. Проверка полученных массивов
//--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
Далее необходимо убедиться, что оба массива имеют одинаковую длину и не являются пустыми. В противном случае мы деактивируем динамический риск, чтобы избежать ошибок.
5. Очистка и подготовка итоговой структуры
В завершение мы очищаем структуру HashMap, выбираем соответствующий эталонный баланс на основе типа счета (FTMO или личный) и подготавливаем вспомогательный массив для возможных дублирующихся или недействительных индексов:
balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[];
С помощью этой функции мы обеспечиваем надежную и устойчивую настройку динамического риска, адаптированную к конкретным потребностям каждого счета или торговой стратегии, что гарантирует эффективное и точное управление рисками.
6. Цикл для добавления допустимых элементов в HashMap
Далее реализуется ключевой цикл, который будет добавлять в HashMap только те элементы, которые являются допустимыми, тем самым обеспечивая упорядоченное и эффективное управление динамическим риском. Элементы считаются недопустимыми при следующих обстоятельствах:
- если процент, активирующий риск, меньше или равен нулю;
- если новое значение риска меньше или равно нулю;
- если элемент уже присутствует в HashMap (дубликат).
При обнаружении недопустимого элемента, мы временно добавляем его в массив indexes_to_remove для последующего удаления. При оценке обоих массивов (balance_to_activate_the_risk и risk_to_be_adjusted) достаточно, чтобы хотя бы одно из пары значений было некорректным, чтобы исключить всю пару, сохраняя целостность и согласованность данных.
Реализация цикла:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); }
Например, предположим, что у нас есть массивы:
- [1, 3, 5]
- [0.1, 0.3, 0.0]
В этом случае последняя пара (5 - 0,0) будет считаться недопустимой, поскольку скорректированное значение риска равно нулю, и поэтому "5" будет впоследствии удалено из соответствующего массива.
7. Удаление дубликатов и недопустимых элементов
После завершения идентификации недействительных или дублирующихся элементов, мы переходим к их удалению. Кроме того, мы реорганизуем основной массив balance_to_activate_the_risk, чтобы обеспечить надлежащий порядок и согласованность данных:
//--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk));
Корректировка размера массива risk_to_be_adjusted необходима для сохранения согласованности между обоими массивами после удаления.
8. Корректировка и преобразование значений риска
Далее мы скорректируем и преобразуем процентные значения, хранящиеся в balance_to_activate_the_risk, в конкретные денежные величины на основе выбранного баланса счета. Массив risk_to_be_adjusted будет обновлен соответствующими значениями:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); }
9. Финальная инициализация динамического риска
Наконец, мы инициализируем необходимые переменные, чтобы убедиться, что динамический риск готов к корректной работе:
//--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
Таким образом, динамическая настройка риска будет точной, эффективной и постоянно адаптируемой к текущим показателям нашей торговой стратегии.
Полная функция:
//+------------------------------------------------------------------+ //| Function to set dynamic risks per operation | //+------------------------------------------------------------------+ void CRiskManagemet::SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_) { if(this.gmlpo.assigned_percentage <= 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; } //--- this.revision_type = revision_type_; //--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ','); //--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); //--- balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[]; //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); } //--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk)); //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); } //--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); }
Понятное и подробное объяснение функции изменения динамического процента риска (GMLPO)
Далее мы шаг за шагом объясним функцию CheckAndModifyThePercentageOfGmlpo, которая позволяет динамически управлять риском на сделку, автоматически адаптируясь в соответствии с уровнями эквити (equity), достигнутыми на торговом счете.
1. Начальная проверка
Функция начинается с проверки, активировано ли динамическое управление риском. Если оно отключено, функция немедленно завершает свое выполнение.
if(!this.ActivateDynamicRiskPerOperation) return;
2. Проверка текущего баланса
Затем функция получает текущее эквити счета (фактическую стоимость счета с учетом плавающих прибыли или убытков). Это значение сравнивается с ранее выбранным балансом (chosen_balance).
Если текущее эквити превышает выбранный баланс, и следующий целевой уровень баланса еще не достигнут, функция не вносит никаких изменений и завершает свое выполнение.
double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) return;
3. Изменение риска при уменьшении эквити
Если текущее эквити уменьшается ниже заданных уровней, система определяет следующий, более низкий уровень баланса.
-
Если обнаруживается, что баланс опустился ниже определенного уровня:
- определяется новый процент риска, соответствующий этому уровню;
- внутренние переменные обновляются, чтобы отразить новый уровень риска.
if(this.TheMinimumValueIsExceeded == false) { if(account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) { PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); while(IsStopped() == false && index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { if(index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity && account_equity > this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo + 1]) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; index_gmlpo ++; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); break; } } else if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; this.TheMinimumValueIsExceeded = true; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); PrintFormat("%s The minimum value %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); break; } } index_gmlpo++; } PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } }
4. Восстановление риска при повторном росте эквити
Если в последствии эквити снова повышается и превышает ранее достигнутый целевой уровень:
-
система снова корректирует риск на сделку, восстанавливая предыдущие уровни в соответствии с уровнями, определенными в массивах.
if(this.NewBalanceToOvercome > 0.00) { if(account_equity > this.NewBalanceToOvercome) { PrintFormat("%s Equity %.2f exceeded balance to shift risk to %.2f", EA_NAME, account_equity, NewBalanceToOvercome); while(!IsStopped() && index_gmlpo > 0) { if(index_gmlpo > 0) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] < account_equity && account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]) { break; } this.index_gmlpo--; } } this.TheMinimumValueIsExceeded = false; if(this.index_gmlpo == 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.gmlpo.assigned_percentage = this.gmlpo_percentage; this.NewBalanceToOvercome = 0.00; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } else if(index_gmlpo > 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - 1]; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); SetGMLPO(); } } }
Заключение
На протяжении этого цикла статей мы шаг за шагом разобрали, как разработать полноценную и надежную систему управления рисками. В заключительной части мы сосредоточились на проработке всех деталей и представили продвинутую и крайне полезную концепцию —динамический риск на сделку. Этот инструмент позволяет автоматически корректировать уровень риска в зависимости от реальных результатов счета, что является ключевым фактором для сохранения капитала и повышения эффективности трейдера.
Я искренне надеюсь, что этот материал будет полезен, особенно для тех, кто только начинает свой путь в увлекательном мире программирования на MQL5.
В следующей части, посвященной управлению рисками, мы выйдем на новый уровень и научимся на практике применять все изученное, интегрируя эту продвинутую систему управления в торгового робота.
Для этого мы будем использовать индикатор Order Blocks, разработанный ранее:
Кроме того, у нас будет возможность наглядно увидеть конкретные преимущества эффективного управления рисками по сравнению с работой без какого-либо контроля. В завершение мы настроим специфические параметры, которые значительно упростят ежедневное использование этих продвинутых инструментов в любой автоматизированной стратегии.
Файлы, использованные/улучшенные в этой статье:
| Имя файла | Тип | Описание |
|---|---|---|
| Risk_Management.mqh | .mqh (включаемый файл) | Основной файл, содержащий общие функции и реализацию класса CRiskManagement, отвечающего за управление рисками в системе. В этом файле определяются, разрабатываются и расширяются все функции, связанные с управлением прибылью и убытками. |
Перевод с испанского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/es/articles/17508
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий на MQL5 (Часть 15): Гармонический паттерн «Шифр» (Cypher) ценового действия с визуализацией
Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (Окончание)
От начального до среднего уровня: Struct (VI)
Разработка инструментария для анализа движения цен (Часть 11): Советник Heikin Ashi Signal
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования