preview
Разрабатываем мультивалютный советник (Часть 14): Адаптивное изменение объёмов в риск-менеджере

Разрабатываем мультивалютный советник (Часть 14): Адаптивное изменение объёмов в риск-менеджере

MetaTrader 5Трейдинг | 12 июля 2024, 09:40
571 0
Yuriy Bykov
Yuriy Bykov

Введение

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

Напомним, что возможными направлениями развития риск-менеджера мы считали более плавное изменение размеров позиций (например, уменьшение в 2 раза при превышении половины лимита), и более "умное" восстановление объёмов (например, только при превышении убытком того уровня, на котором произошло уменьшение размеров позиций). Также можно добавить параметр максимальной целевой прибыли, по достижении которой также происходит остановка торговли. Этот параметр вряд ли будет полезен при торговле на личном счёте, но для торговли на счетах проп-трейдинговых компаний окажется весьма востребован, поскольку обычно там по достижении запланированного уровня прибыли торговля может быть продолжена только на другом счёте.

Введение ограничений торговли по времени мы тоже упоминали как одно из возможных направлений развития риск-менеджера, но в рамках данной статьи мы не будем касаться этой темы, оставив её на будущее. А пока попробуем реализовать адаптивное изменение объёмов в риск-менеджере и посмотрим, будет ли с этого хоть какая-то польза.


Базовый случай

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

Во-первых, зафиксируем конкретный состав одиночных экземпляров торговых стратегий, которые будут использоваться в тестовом советнике. Укажем в параметре passes_ идентификаторы лучших проходов после второго этапа оптимизации для каждого из трёх оптимизируемых символов и каждого из трёх таймфреймов (всего 9 идентификаторов). За каждым идентификатором будет скрываться нормированная группа из 16 одиночных экземпляров торговых стратегий. Таким образом итоговая группа будет содержать всего 144 экземпляра торговых стратегий, разбитых на 9 групп по 16 стратегий. Итоговая группа не будет нормированной, поскольку мы не подбирали для неё нормирующий множитель.

Во-вторых, будем использовать фиксированный баланс для торговли, равный $10000 и нашу стандартную ожидаемую максимальную просадку 10% для коэффициента масштабирования, равного 1. Последний мы будем пытаться изменять в диапазоне от 1 до 10. При этом максимальная допустимая просадка тоже будет возрастать, но её мы теперь будем дополнительно контролировать нашим риск-менеджером, не позволяя выйти за пределы 10%.

Для этого включим риск-менеджер и поставим значение максимального общего убытка, равным 10% от базового баланса $10000, то есть $1000. Для максимального дневного убытка поставим значение в два раза меньше, то есть $500. При росте баланса эти два параметра меняться не будут.

Установим все значения входных параметров согласно вышеописанному:


Рис. 1. Входные параметры для тестового советника с исходным риск-менеджером.


Запустим оптимизацию на интервале 2021 и 2022 годов, чтобы посмотреть на работу советника с разными значениями масштабирующего множителя для размеров позиций (scale_). Получим следующие результаты:


Рис. 2. Результаты оптимизации параметра scale_ в тестовом советнике с исходным риск-менеджером.


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

На протяжении интервала тестирования нам встретилось несколько просадок различной глубины. В тех проходах, где никакая просадка не опускала уровень средств ниже $9000, торговля продолжалась до конца интервала. А в тех проходах, в которых уровень средств опускался во время просадки ниже $9000, торговля останавливалась в этот момент и больше не возобновлялась. В этих проходах мы и получили убыток в районе $1000. Его небольшое превышение расчётной величины объясняется скорее всего тем, что мы использовали режим работы советника только на новых минутных барах. Поэтому цены в течение минуты успевали ещё немного измениться с момента точного достижения заданного убытка до момента проверки риск-менеджером и принятия решения о закрытии всех позиций.

Эти отличия в большинстве проходов незначительны, и мы можем ими пренебречь, договорившись, что будем устанавливать в параметрах несколько меньший лимит, или поменяв, как планировали, способ работы риск-менеджера. Единственный проход, который внушает опасения — это проход для scale_ = 5.5, когда после закрытия всех позиций убыток превысил расчётный более чем на 20% и составил примерно $1234.

Для дальнейшего анализа давайте посмотрим на график кривых баланса и средств для самого первого прохода (scale_ = 1.0). Поскольку остальные проходы отличаются только размером открываемых позиций, то их графики баланса и средств будут иметь тот же вид, что и для первого прохода, только будут более растянуты по вертикали.

Рис. 3. Результаты прохода с параметром scale_ = 1.0 в тестовом советнике с исходным риск-менеджером


Сравним их с результатами без риск-менеджера:


Рис. 4. Результаты прохода с параметром scale_ = 1.0 в тестовом советнике без риск-менеджера


Без риск-менеджера общая прибыль получилась на несколько процентов больше, а максимальная просадка по средствам осталась такой же. Это говорит о том, что нормировка стратегий при объединении в группы и последующая их совместная работа даёт хорошие результаты: риск-менеджеру пришлось закрывать позиции при превышении дневного убытка всего около трёх раз за два года.

Посмотрим теперь на результаты прохода с риск-менеджером и значением параметра scale_ = 3. Прибыль составила примерно на 50% больше, но и просадка увеличилась в три раза.



Рис. 5. Результаты прохода с параметром scale_ = 3.0 в тестовом советнике с исходным риск-менеджером


Однако, несмотря на увеличение просадки в абсолютном выражении почти до $3000, риск-менеджер должен был не допустить, чтобы в день просадка превышала $500. То есть было несколько последовательных дней, когда риск-менеджер закрывал все позиции и открывал их заново в начале следующего дня. От прошлого максимального значения средств текущий размер средств в итоге уходил в минус до $3000, но за каждый отдельный день относительно максимума баланса или средств на начало дня просадка не превышала $500. Тем не менее использовать такой повышенный размер позиций опасно, так как есть ещё и ограничение по общему убытку. В данном случае нам повезло, что большая просадка случилась уже спустя некоторое время после начала периода тестирования. Размер баланса успел подрасти и тем самым увеличил значение максимального общего убытка, который составлял в момент старта $1000 и отсчитывался от значения начального баланса. Если бы период тестирования начинался прямо перед тем моментом, когда просадка достигала $3000, то сработало бы превышение общего лимита, и торговля была бы остановлена.

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


Модернизация CVirtualRiskManager

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


Обновление перечислений

Перед описанием класса у нас были объявлены несколько перечислений, используемых в дальнейшем. Мы несколько расширим состав этих перечислений. Например, в перечисление ENUM_RM_STATE, содержащее возможные состояния риск-менеджера, добавим два новых состояния:

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

  • RM_STATE_OVERALL_PROFIT — состояние, возникающее после достижения заданной прибыли. После этого события торговля останавливается.

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

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

Эти варианты указаны в комментариях к значениям перечислений.

// Возможные состояния риск-менеджера
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Лимиты не превышены
   RM_STATE_DAILY_LOSS,    // Превышен дневной лимит
   RM_STATE_RESTORE,       // Восстановление после дневного лимита
   RM_STATE_OVERALL_LOSS,  // Превышен общий лимит
   RM_STATE_OVERALL_PROFIT // Достигнута общая прибыль
};

// Возможные способы расчёта дневных лимитов
enum ENUM_RM_CALC_DAILY_LOSS {
   RM_CALC_DAILY_LOSS_MONEY_BB,    // [$] to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_BB,  // [%] from Base Balance to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_DL   // [%] from/to Daily Level
};

// Возможные способы расчёта общих лимитов
enum ENUM_RM_CALC_OVERALL_LOSS {
   RM_CALC_OVERALL_LOSS_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_BAL,       // [$] to HW Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL,    // [$] to HW Equity or Balance
   RM_CALC_OVERALL_LOSS_PERCENT_BB,         // [%] from/to Base Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL,     // [%] from/to HW Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL   // [%] from/to HW Equity or Balance
};

// Возможные способы расчёта общей прибыли
enum ENUM_RM_CALC_OVERALL_PROFIT {
   RM_CALC_OVERALL_PROFIT_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_PROFIT_PERCENT_BB,         // [%] from/to Base Balance
};


Описание класса

В описании класса CVirtualRiskManager мы добавили новые свойства и методы в защищённой секции. В публичной секции всё осталось без изменений. Добавленное выделено зелёным фоном:

//+------------------------------------------------------------------+
//| Класс управления риском (риск-менеждер)                          |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Основные параметры конструктора
   bool              m_isActive;             // Риск менеджер активен?

   double            m_baseBalance;          // Базовый баланс

   ENUM_RM_CALC_DAILY_LOSS   m_calcDailyLossLimit; // Способ расчёта максимального дневного убытка
   double            m_maxDailyLossLimit;          // Параметр расчёта максимального дневного убытка
   double            m_closeDailyPart;             // Значение пороговой части дневного убытка

   ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit;  // Способ расчёта максимального общего убытка
   double            m_maxOverallLossLimit;           // Параметр расчёта максимального общего убытка
   double            m_closeOverallPart;              // Значение пороговой части общего убытка

   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Способ расчёта максимальной общей прибыли
   double            m_maxOverallProfitLimit;            // Параметр расчёта максимальной общей прибыли

   double            m_maxRestoreTime;             // Время ожидания лучшего входа на просадке
   double            m_lastVirtualProfitFactor;    // Множитель начальной лучшей просадки


// Текущее состояние
   ENUM_RM_STATE     m_state;                // Состояние
   double            m_lastVirtualProfit;    // Прибыль открытых виртуальных позиций на момент лимита убытка
   datetime          m_startRestoreTime;     // Время начала восстановления размеров открытых позиций

// Обновляемые значения
   double            m_balance;              // Текущий баланс
   double            m_equity;               // Текущие средства
   double            m_profit;               // Текущая прибыль
   double            m_dailyProfit;          // Дневная прибыль
   double            m_overallProfit;        // Общая прибыль
   double            m_baseDailyBalance;     // Дневной базовый баланс
   double            m_baseDailyEquity;      // Дневные базовые средства
   double            m_baseDailyLevel;       // Дневной базовый уровень
   double            m_baseHWBalance;        // High Watermark баланса
   double            m_baseHWEquityBalance;  // High Watermark средств или баланса
   double            m_virtualProfit;        // Прибыль открытых виртуальных позиций

// Управление размером открытых позиций
   double            m_baseDepoPart;         // Используемая часть общего баланса (исходная)
   double            m_dailyDepoPart;        // Множитель используемой части общего баланса по дневному убытку
   double            m_overallDepoPart;      // Множитель используемой части общего баланса по общему убытку

// Защищённые методы
   double            DailyLoss();            // Максимальный дневной убыток
   double            OverallLoss();          // Максимальный общий убыток

   void              UpdateProfit();         // Обновление текущих значений прибыли
   void              UpdateBaseLevels();     // Обновление дневных базовых уровней

   void              CheckLimits();          // Проверка превышения допустимых убытков
   bool              CheckDailyLossLimit();     // Проверка превышения допустимого дневного убытка
   bool              CheckOverallLossLimit();   // Проверка превышения допустимого общего убытка
   bool              CheckOverallProfitLimit(); // Проверка достижения заданной прибыли

   void              CheckRestore();         // Проверка необходимости восстановления размеров открытых позиций
   bool              CheckDailyRestore();       // Проверка необходимости восстановления дневного множителя
   bool              CheckOverallRestore();     // Проверка необходимости восстановления общего множителя

   double            VirtualProfit();        // Определение прибыли открытых виртуальных позиций
   double            RestoreVirtualProfit(); // Определение прибыли открытых виртуальных позиций для восстановления

   void              SetDepoPart();          // Установка значения используемой части общего баланса

public:
   ...
};

Свойства m_closeDailyPart и m_closeOverallPart позволят нам проводить более плавное изменение размеров позиций. Их использование похоже между собой, а разница состоит только в том к какому лимиту (дневному или общему) относится каждое свойство. Например, если мы зададим значение m_closeDailyPart = 0.5, то когда убыток достигнет половины от дневного лимита, то размеры позиций уменьшатся вдвое. Если убыток продолжит расти, и достигнет половины от оставшейся половины дневного лимита, то размеры позиций (уже уменьшенные вдвое ранее) будут снова уменьшены в два раза.

Уменьшение размеров позиций будет осуществляться через изменение свойств m_dailyDepoPart и m_overallDepoPart. Их значения используются в методе установки значения используемой части общего баланса для торговли. Они входят в формулу как множители, поэтому уменьшение какого-то из них в два раза приведёт к уменьшению общего объема в два раза:

//+------------------------------------------------------------------+
//| Установка значения используемой части общего баланса             |
//+------------------------------------------------------------------+
void CVirtualRiskManager::SetDepoPart() {
   CMoney::DepoPart(m_baseDepoPart * m_dailyDepoPart * m_overallDepoPart);
}

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

Свойства m_maxRestoreTime и m_lastVirtualProfitFactor используются при определении возможности восстановления размеров открытых позиций.

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

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

Стоит также отметить, что теперь моментом достижения лимита считается не только превышение просадкой установленного лимита, но и, например, достижение половины дневного лимита, если параметр m_closeDailyPart равен 0.5.


Расчёт прибыли виртуальных позиций

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

В прошлый раз этот метод нам не понадобился для каких-либо расчётов, от результатов которых зависят дальнейшие действия. Теперь он нам уже нужен, причём для корректного расчёта прибыли виртуальных позиций нам нужно внести в него небольшие дополнения. Дело в том, что используемый метод определения прибыли одной виртуальной позиции CMoney::Profit() использует для расчёта значение множителя использования баланса CMoney::DepoPart(). Если мы этот множитель уменьшили, то и расчёт виртуальной прибыли будет производиться для уменьшенных размеров позиций, а не для тех размеров, которые были до уменьшения.

Нас же интересует именно прибыль для исходных размеров позиций, поэтому перед её расчётом мы временно возвращаем исходный множитель использования баланса. После этого рассчитываем прибыль виртуальных позиций и затем снова устанавливаем текущий множитель использования баланса, вызывая метод SetDepoPart():

//+------------------------------------------------------------------+
//| Определение прибыли открытых виртуальных позиций                 |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
// Обращаемся к объекту получателя
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();

// Устанавливаем исходный множитель использования баланса
   CMoney::DepoPart(m_baseDepoPart);

   double profit = 0;

// Для всех виртуальных позиций находим сумму их прибыли
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));

// Восстанавливаем текущий множитель использования баланса
   SetDepoPart();

   return profit;
}


High Watermark

Для лучшего анализа нам желательно добавить возможность отсчитывать максимальный уровень убытка не только от начального баланса счёта, но и, например, от последнего достигнутого максимума баланса. Поэтому в состав свойств, которые должны постоянно обновляться, мы добавили m_baseHWBalance и m_baseHWEquityBalance. В методе UpdateProfit() мы добавили их расчёт с проверкой, что общая прибыль считается именно относительно наивысших значений баланса или средств, а не базового баланса:

//+------------------------------------------------------------------+
//| Обновление текущих значений прибыли                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
// Текущие средства
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);

// Текущий баланс
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);

// Наивысший баланс (High Watermark)
   m_baseHWBalance = MathMax(m_balance, m_baseHWBalance);

// Наивысший баланс или средства (High Watermark)
   m_baseHWEquityBalance = MathMax(m_equity, MathMax(m_balance, m_baseHWEquityBalance));

// Текущая прибыль
   m_profit = m_equity - m_balance;

// Текущая дневная прибыль относительно дневного уровня
   m_dailyProfit = m_equity - m_baseDailyLevel;

// Текущая общая прибыль относительно базового баланса
   m_overallProfit = m_equity - m_baseBalance;

// Если общую прибыль берём относительно наивысшего баланса, то
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL) {
      // Пересчитаем её
      m_overallProfit = m_equity - m_baseHWBalance;
   }

// Если общую прибыль берём относительно наивысшего баланса или средств, то
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL) {
      // Пересчитаем её
      m_overallProfit = m_equity - m_baseHWEquityBalance;
   }

// Текущая прибыль виртуальных открытых позиций
   m_virtualProfit = VirtualProfit();

   ...
}


Метод проверки лимитов

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

//+------------------------------------------------------------------+
//| Проверка лимитов убытка                                          |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   if(false
         || CheckDailyLossLimit()     // Проверка дневного лимита
         || CheckOverallLossLimit()   // Проверка общего лимита
         || CheckOverallProfitLimit() // Проверка общей прибыли
     ) {
      // Запоминаем текущий уровень виртуальной прибыли
      m_lastVirtualProfit = m_virtualProfit;

      // Оповещаем получатель об изменениях
      CVirtualReceiver::Instance().Changed();
   }
}

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

//+------------------------------------------------------------------+
//| Проверка дневного лимита убытка                                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyLossLimit() {
// Если достигнут дневной убыток и позиции ещё открыты
   if(m_dailyProfit < -DailyLoss() * (1 - m_dailyDepoPart * (1 - m_closeDailyPart))
      && CMoney::DepoPart() > 0) {

      // Уменьшаем множитель используемой части общего баланса по дневному убытку
      m_dailyDepoPart *= (1 - m_closeDailyPart);

      // Если множитель уже слишком мал, то
      if(m_dailyDepoPart < 0.05) {
         // Устанавливаем его в 0
         m_dailyDepoPart = 0;
      }

      // Устанавливаем значение используемой части общего баланса
      SetDepoPart();
    
      ... 

      // Устанавливаем риск-менеджер в состояние достигнутого дневного убытка
      m_state = RM_STATE_DAILY_LOSS;

      return true;
   }

   return false;
}

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

//+------------------------------------------------------------------+
//| Проверка общего лимита убытка                                    |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallLossLimit() {
// Если достигнут общий убыток и позиции ещё открыты
   if(m_overallProfit < -OverallLoss() * (1 - m_overallDepoPart * (1 - m_closeOverallPart))
         && CMoney::DepoPart() > 0) {
      // Уменьшаем множитель используемой части общего баланса по общему убытку
      m_overallDepoPart *= (1 - m_closeOverallPart);

      // Если множитель уже слишком мал, то
      if(m_overallDepoPart < 0.05) {
         // Устанавливаем его в 0
         m_overallDepoPart = 0;

         // Устанавливаем риск-менеджер в состояние достигнутого общего убытка
         m_state = RM_STATE_OVERALL_LOSS;
      }

      // Устанавливаем значение используемой части общего баланса
      SetDepoPart();
      
      ...

      return true;
   }

   return false;
}

Ещё проще выглядит проверка достижения заданной прибыли: при её достижении мы обнуляем соответствующий множитель и устанавливаем риск-менеджер в состояние достигнутой общей прибыли:

//+------------------------------------------------------------------+
//| Проверка достижения заданной прибыли                             |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// Если достигнут общий убыток и позиции ещё открыты
   if(m_overallProfit > m_maxOverallProfitLimit && CMoney::DepoPart() > 0) {
      // Уменьшаем множитель используемой части общего баланса по общему убытку
      m_overallDepoPart = 0;

      // Устанавливаем риск-менеджер в состояние достигнутой общей прибыли
      m_state = RM_STATE_OVERALL_PROFIT;

      // Устанавливаем значение используемой части общего баланса
      SetDepoPart();
      
      ... 

      return true;
   }

   return false;
}

Все три метода возвращают логическое значение, отвечающее на вопрос: был ли достигнут соответствующий предел? Если да, то в методе CheckLimits() мы запоминаем текущий уровень виртуальной прибыли и оповещаем получатель объёмов рыночных позиций о наличии изменений в составе позиций.

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

//+------------------------------------------------------------------+
//| Обновление дневных базовых уровней                               |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Обновляем баланс, средства и базовый дневной уровень
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   m_dailyProfit = m_equity - m_baseDailyLevel;

   ...

// Если ранее был достигнут дневной уровень убытка, то
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Переходим в состояние восстановления размеров открытых позиций
      m_state = RM_STATE_RESTORE;

      // Запоминаем время начала восстановления
      m_startRestoreTime = TimeCurrent();
   }
}


Восстановление размеров позиций

Метод, выполнявший ранее эту работу мы тоже разбили на несколько методов. На верхнем уровне вызывается метод проверки необходимости восстановления:

//+------------------------------------------------------------------+
//| Проверка необходимости восстановления размеров открытых позиций  |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckRestore() {
// Если нужно восстанавливать состояние до нормального, то
   if(m_state == RM_STATE_RESTORE) {
      // Проверяем возможность восстановить до нормального множитель дневного убытка
      bool dailyRes = CheckDailyRestore();

      // Проверяем возможность восстановить до нормального множитель общего убытка
      bool overallRes = CheckOverallRestore();

      // Если хотя бы один из них восстановился, то
      if(dailyRes || overallRes) {
         
...

         // Устанавливаем значение используемой части общего баланса
         SetDepoPart();

         // Оповещаем получатель об изменениях
         CVirtualReceiver::Instance().Changed();

         // Если оба множителя восстановлены до нормальных, то
         if(dailyRes && overallRes) {
            // Устанавливаем нормальное состояние
            m_state = RM_STATE_OK;
         }
      }
   }
}

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

//+------------------------------------------------------------------+
//| Проверка необходимости восстановления дневного множителя         |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyRestore() {
// Если текущая виртуальная прибыль меньше желаемой для восстановления, то
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Восстанавливаем множитель дневного убытка
      m_dailyDepoPart = 1.0;
      return true;
   }

   return false;
}

//+------------------------------------------------------------------+
//| Проверка необходимости восстановления общего множителя           |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallRestore() {
// Если текущая виртуальная прибыль меньше желаемой для восстановления, то
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Восстанавливаем множитель общего убытка
      m_overallDepoPart = 1.0;
      return true;
   }

   return false;
}

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

//+------------------------------------------------------------------+
//| Определение прибыли виртуальных позиций для восстановления       |
//+------------------------------------------------------------------+
double CVirtualRiskManager::RestoreVirtualProfit() {
// Если максимальное время восстановления не задано, то
   if(m_maxRestoreTime == 0) {
      // Возвращаем текущее значение виртуальной прибыли
      return m_virtualProfit;
   }

// Находим прошедшее время с начала восстановления в минутах
   double t = (TimeCurrent() - m_startRestoreTime) / 60.0;

// Возвращаем расчётное значение желаемой виртуальной прибыли
// в зависимости от прошедшего времени с начала восстановления
   return m_lastVirtualProfit * m_lastVirtualProfitFactor * (1 - t / m_maxRestoreTime);
}

Сохраним сделанные изменения в файле VirtualRiskManager.mqh в текущей папке.


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

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

Рис. 6. Результаты советника с параметром scale_ = 1.0 с настройками как у исходного риск-менеджера

Результаты по просадке совпадают полностью, по прибыли и нормированной среднегодовой прибыли (OnTester result) результат чуть больше ожидаемого. Что ж, это обнадёживает: в процессе правок мы не сломали то, что было сделано раньше.

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


Рис. 7. Результаты советника с параметрами scale_ = 1.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5

Просадка немного уменьшилась, что вызвало небольшой рост нормированной среднегодовой прибыли. Попробуем теперь подключить восстановление размеров позиций по лучшей цене. Поставим в параметрах время ожидания лучшего входа на просадке равным 1440 минут (1 сутки), а множитель начальной лучшей просадки возьмём равным 1.5. Это просто значения, взятые интуитивно.


Рис. 8. Результаты советника с параметрами scale_ = 1.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

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

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


Рис. 9. Результаты советника с параметрами scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

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

Интересно также посмотреть, а что будет если размер открываемых позиций увеличить ещё больше, например, в 10 раз?


Рис. 10. Результаты советника с параметрами scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440, rmLastVirtualProfitFactor_ = 1.5

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

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


Рис. 11. Результаты оптимизации советника с риск-менеджером

Как видно, подбор хороших параметров риск-менеджера может не только защитить торговлю от повышенных просадок, но и улучшить торговые результаты: разница для разных проходов составила до 20% дополнительной прибыли. Хотя даже самый лучший из уже найденных наборов параметров чуть-чуть уступает результатам, полученным на рис. 8 с интуитивно выбранными значениями. Скорее всего, дальнейшая оптимизация могла бы ещё немного улучшить и этот результат, но в этом нет особой необходимости.


Заключение

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

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

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

Спасибо за внимание, до новых встреч!

Прикрепленные файлы |
Проблема разногласий: объяснимость и объяснители в ИИ Проблема разногласий: объяснимость и объяснители в ИИ
В этой статье мы будем говорить о проблемах, связанных с объяснителями и объяснимостью в ИИ. Модели ИИ часто принимают решения, которые трудно объяснить. Более того, использование нескольких объяснителей часто приводит к так называемой "проблеме разногласий". А ведь ясное понимание того, как работают модели, является ключевым для повышения доверия к ИИ.
Алгоритм искусственного электрического поля — Artificial Electric Field Algorithm (AEFA) Алгоритм искусственного электрического поля — Artificial Electric Field Algorithm (AEFA)
Статья представляет алгоритм искусственного электрического поля (AEFA), вдохновленный законом Кулона об электростатической силе. Алгоритм моделирует электрические явления для решения сложных задач оптимизации, используя заряженные частицы и их взаимодействие. AEFA демонстрирует уникальные свойства в контексте других алгоритмов, связанных с законами природы.
Нейросети в трейдинге: Пространственно-временная нейронная сеть (STNN) Нейросети в трейдинге: Пространственно-временная нейронная сеть (STNN)
В данной статье мы поговорим об использовании пространственно-временных преобразований для эффективного прогнозирования предстоящего ценового движения. Для повышения точности численного прогнозирования в STNN был предложен механизм непрерывного внимания, который позволяет модели лучше учитывать важные аспекты данных.
Модель глубокого обучения GRU на Python с использованием ONNX в советнике, GRU vs LSTM Модель глубокого обучения GRU на Python с использованием ONNX в советнике, GRU vs LSTM
Статья посвящена разработке модели глубокого обучения GRU ONNX на Python. В практической части мы реализуем эту модель в торговом советнике, а затем сравним работу модели GRU с LSTM (долгой краткосрочной памятью).