English 中文 Español Deutsch 日本語 Português
preview
Риск-менеджер для ручной торговли

Риск-менеджер для ручной торговли

MetaTrader 5Примеры | 22 марта 2024, 16:26
1 576 8
Aleksandr Seredin
Aleksandr Seredin

Содержание


Введение

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

Риск-менеджер — это мой первый класс, который я написал в 2019 году, как только изучил основы программирования, в первую очередь для себя. На тот момент я понимал на своём опыте, что психологическое состояние трейдера очень сильно влияет на эффективность торговли через "трезвость" и "беспристрастность" принятия торговых решений. Гэмблинг (gambling), эмоциональные сделки и завышение рисков в надежде "отыграться" способны обнулить любой счёт, даже с применением эффективной торговой стратегии, которая на тестах показывала очень хорошие результаты.

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


Определение функционала

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


Входные параметры и конструктор класса

Мы определились, что ограничимся контролем риска по периодам и критерием достижения нормы дневной прибыли. Для этого введём несколько переменных типа double с модификатором класса памяти input для ручного ввода пользователем значений риска в процентах от депозита на каждый период времени, а также плановый процент прибыли на день для фиксации прибыли. Для признака контроля плановой дневной прибыли введём дополнительную переменную типа bool для возможности включения/выключения данного функционала, если трейдер захочет рассматривать каждый вход отдельно и уверен в отсутствии корреляции между выбранными инструментами. Такой тип переменных "выключателей" также иногда называют "флагами". Объявим на глобальном уровне код в следующем виде, для удобства "завернув" его предварительно в именованный блок с помощью ключевого слова group.

input group "RiskManagerBaseClass"
input double inp_riskperday    = 1;          // riskperday - риск на день, в процентах от депозита
input double inp_riskperweek   = 3;          // riskperweek - риск на неделю
input double inp_riskpermonth  = 9;          // riskpermonth - риск на месяц
input double inp_plandayprofit = 3;          // pdp - плановая прибыль

input bool dayProfitControl = true;          // dayProfitControl - кроем ли позы при достижении дневной прибыли

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

Мы определились, что нам будет комфортно торговать с дневным риском в 1% от депозита. Если лимит на день превышен, мы закрываем терминал до завтра. Далее мы определяем лимит на неделю следующим образом. В неделе как правило 5 торговых дней, значит если мы получим 3 убыточных дня подряд, то мы уже останавливаем торговлю до начала следующей недели. Просто потому, что более вероятно, что вы просто не поняли рынок на этой неделе, либо что-то поменялось и если вы продолжите торговлю, то вы накопите такой большой убыток за этот период, что не сможете покрыть его даже за счёт следующей недели. Схожая логика и в установлении месячного лимита при торговле внутри дня. Мы принимаем условие, что если за месяц у нас было 3 убыточные недели, то четвертую лучше не торговать, так как потребуется очень много времени, чтобы "выровнять" кривую доходности за счет будущих периодов и не "пугать" инвесторов большим убытком по какому-то отдельному месяцу.

Размер плановой дневной прибыли мы устанавливаем от дневного риска с учётом особенностей вашей торговой системы. Что здесь нужно учитывать. В первую очередь это торгуете ли вы коррелируемые инструменты, как часто ваша торговая система даёт сигналы на входы, торгуете ли вы с фиксированными значениями пропорций между stop-loss и take-profit по каждой отдельной сделке, либо на размер депозита. Здесь отдельно хочу отметить, что ОЧЕНЬ НАСТОЯТЕЛЬНО НЕ РЕКОМЕНДУЮ торговать без stop-loss и без риск-менеджера для ограничений убытка по дню одновременно. Это получиться гарантированный "слив" депозита и здесь лишь вопрос времени. Поэтому либо ставим стопы на каждую сделку в отдельности, либо используем риск-менеджер на ограничение по периодам. В нашем же текущем примере параметров по умолчанию я заложил условия на дневную прибыль как 1 к 3 относительно дневного риска. Также эти параметры лучше использовать с обязательным установлением риск-доходности по КАЖДОЙ сделке через соотношение stop-loss и take-profit также 1 к 3 (take-profit больше stop-loss).

Схематично структуру наших лимитов можно изобразить следующим образом.

Рисунок 1. Структура лимитов

Рисунок 1. Структура лимитов

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

protected:

   double    riskperday,                     // риск на день в процентах от депозита
             riskperweek,                    // риск на неделю в процентах от депозита
             riskpermonth,                   // риск на месяц в процентах от депозита
             plandayprofit                   // плановая дневная прибыль, в процентах от депозита
             ;

   double    RiskPerDay,                     // в валюте риск на день
             RiskPerWeek,                    // в валюте риск на неделю
             RiskPerMonth,                   // в валюте риск на месяц
             StartBalance,                   // в валюте баланс счёта на момент запуска советника
             StartEquity,                    // в валюте средства счёта на момент обновления лимитов
             PlanDayEquity,                  // в валюте плановое значение средства счёта на день
             PlanDayProfit                   // в валюте плановое значение прибыли на день
             ;

   double    CurrentEquity,                  // текущее значение средств
             CurrentBallance;                // текущий баланс

Для удобства вычислений лимитов риска по периодам в валюте депозита, исходя из входных параметров, объявим внутри нашего класса метод RefreshLimits() также с модификатором доступа protected. Опишем данный метод вне класса следующим образом. Предусмотрим на перспективу тип возвращаемого значения типа bool в случае дальнейшей необходимости расширения нашего метода с возможностью проверки корректности полученных данных. А пока опишем метод в следующем виде.

//+------------------------------------------------------------------+
//|                        RefreshLimits                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::RefreshLimits(void)
  {
   CurrentEquity    = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // запросили текущее значение equity
   CurrentBallance  = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // запросили текущий баланс

   StartBalance     = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // установили стартовый баланс
   StartEquity      = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // запросили текущее значение equity

   PlanDayProfit    = NormalizeDouble(StartEquity * plandayprofit/100,2);     // в валюте плановый профит на день
   PlanDayEquity    = NormalizeDouble(StartEquity + PlanDayProfit/100,2);     // в валюте плановый equity

   RiskPerDay       = NormalizeDouble(StartEquity * riskperday/100,2);        // в валюте риск на день
   RiskPerWeek      = NormalizeDouble(StartEquity * riskperweek/100,2);       // в валюте риск на неделю
   RiskPerMonth     = NormalizeDouble(StartEquity * riskpermonth/100,2);      // в валюте риск на месяц

   return(true);
  }

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

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
RiskManagerBase::RiskManagerBase()
  {
   riskperday         = inp_riskperday;                                 // присвоили значение внутренней переменной
   riskperweek        = inp_riskperweek;                                // присвоили значение внутренней переменной
   riskpermonth       = inp_riskpermonth;                               // присвоили значение внутренней переменной
   plandayprofit      = inp_plandayprofit;                              // присвоили значение внутренней переменной

   RefreshLimits();                                                     // обновили лимиты
  }

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


Работа с периодами лимитов риска

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

   bool              RiskTradePermission;    // общая переменная - можно ли открывать новые сделки
   bool              RiskDayPermission;      // флаг для запрета торговли если лимит на день закончился
   bool              RiskWeekPermission;     // флаг для запрета торговли если лимит на неделю закончился
   bool              RiskMonthPermission;    // флаг запрета торговли если лимит на месяц закончился

   bool              DayProfitArrive;        // переменная контролирующая достижение плановой прибыли за день
   bool              NewTradeDay;            // переменная нового торгового дня

   //--- факт лимиты
   double            DayorderLoss;           // накопленный убыток за день
   double            DayorderProfit;         // накопленный профит за день
   double            WeekorderLoss;          // накопленный убыток за неделю
   double            WeekorderProfit;        // накопленный профит за неделю
   double            MonthorderLoss;         // накопленный убыток за месяц
   double            MonthorderProfit;       // накопленный профит за месяц
   double            MonthOrderSwap;         // своп за месяц
   double            MonthOrderCommis;       // комиссия за месяц

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


Контроль использования лимитов

Для контроля фактического использования лимитов нам нужно будет обработать по коду события связанные с наступлением каждого нового периода, а также события связанные с появлением уже завершённых торговых операций. Для того чтобы правильно вести учёт фактически использованных лимитов объявим в зоне доступа protected нашего класса внутренний метод ForOnTrade().

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

   MqlDateTime local, start_day, start_week, start_month;               // создали структуры для фильтруемых дат
   TimeLocal(local);                                                    // заполнили первоначально
   TimeLocal(start_day);                                                // заполнили первоначально
   TimeLocal(start_week);                                               // заполнили первоначально
   TimeLocal(start_month);                                              // заполнили первоначально

Здесь стоит отметить, что для первоначальной инициализации текущего времени мы используем предопределённую функцию TimeLocal(), а не TimeCurrent() только потому, что первая использует локальное время, а вторая берёт время из последнего полученного тика от брокера, что может вызвать некорректный учёт лимитов из-за разности часовых поясов у разных брокеров. Далее нам нужно обнулить время начала каждого периода чтобы получить значения даты начала каждого из них. Сделаем это через обращение к публичным полям наших структур следующим образом.

//--- обнуляем, чтобы отчёт был с начала периода
   start_day.sec     = 0;                                               // с начала дня
   start_day.min     = 0;                                               // с начала дня
   start_day.hour    = 0;                                               // с начала дня

   start_week.sec    = 0;                                               // с начала недели
   start_week.min    = 0;                                               // с начала недели
   start_week.hour   = 0;                                               // с начала недели

   start_month.sec   = 0;                                               // с начала месяца
   start_month.min   = 0;                                               // с начала месяца
   start_month.hour  = 0;                                               // с начала месяца

Чтобы корректно получить данные за неделю и месяц, нам нужно определить логику нахождения начала недели и месяца. В случае с месяцем всё достаточно просто, мы знаем, что каждый месяц начинается с первого числа. А вот в случае с неделей, всё немного сложнее, ведь здесь нет конкретной точки отчёта и дата каждый раз будет меняться. Здесь нам может помочь специальное поле day_of_week структуры  MqlDateTime. Оно позволяет получить номер дня недели от текущей даты начиная с нуля. Зная это значение, нам не составит труда узнать дату начала текущей недели следующим образом.

//--- определения начала недели
   int dif;                                                             // переменная разницы дня недели
   if(start_week.day_of_week==0)                                        // если это первый день недели
     {
      dif = 0;                                                          // то обнуляем
     }
   else
     {
      dif = start_week.day_of_week-1;                                   // если не первый, то вычисляем разницу
      start_week.day -= dif;                                            // вычитаем из числа дня разницу на начало недели
     }

//---month
   start_month.day         = 1;                                         // с месяцем всё просто

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

//---
   uint     total  = 0;                                                 // количество выбранных сделок
   ulong    ticket = 0;                                                 // номер ордера
   long     type;                                                       // тип ордера
   double   profit = 0,                                                 // профит ордера
            commis = 0,                                                 // коммиссия ордера
            swap   = 0;                                                 // своп ордера

   DayorderLoss      = 0;                                               // дневной убыток без комиссии
   DayorderProfit    = 0;                                               // дневной профит
   WeekorderLoss     = 0;                                               // недельный убыток без комиссии
   WeekorderProfit   = 0;                                               // недельный профит
   MonthorderLoss    = 0;                                               // месячный убыток без комиссии
   MonthorderProfit  = 0;                                               // месячный профит
   MonthOrderCommis  = 0;                                               // месячная комиссия
   MonthOrderSwap    = 0;                                               // месячный своп

Запрос исторических данных по закрытым ордерам мы будем делать через предопределённую функцию терминала HistorySelect() для параметров которой, как раз и требуются полученные нами ранее даты по каждому периоду. Для этого нам нужно будет привести наш тип переменных  MqlDateTime в требуемый параметрами функции HistorySelect() тип данных datetime также с помощью предопределённой функции терминала StructToTime(). Будем запрашивать данные по сделкам аналогично подставляя необходимые значения начала и конца необходимого периода.

После каждого вызова функции  HistorySelect() нам необходимо получить количество выбранных ордеров с помощью предопределённой функции терминала HistoryDealsTotal() и поместить это значение в нашу локальную переменную total. Когда мы получили количество закрытых сделок мы можем организовать цикл через оператор for, запрашивая номер каждого ордера через предопределённую функцию терминала HistoryDealGetTicket(), чтобы иметь возможность получить доступ к данным каждого ордера. Доступ к данным каждого ордера мы получим с помощью предопределённых функций терминала  HistoryDealGetDouble() и HistoryDealGetInteger(), передав в них ранее полученный номер ордера. Также нам будет нужно указать соответствующий идентификатор свойства сделки из перечисления ENUM_DEAL_PROPERTY_INTEGER и ENUM_DEAL_PROPERTY_DOUBLE. Также нам нужно будет добавить фильтр через оператор логического выбора if, чтобы учитывать только сделки от торговых операций с помощью проверки на значения DEAL_TYPE_BUY и DEAL_TYPE_SELL перечисления ENUM_DEAL_TYPE, чтобы отсеять другие операции по счёту, например балансовые операции и начисления бонусов. В общем виде данная выборка будет выглядеть следующим образом.

//---теперь выбираем данные по --==ДНЮ==--
   HistorySelect(StructToTime(start_day),StructToTime(local));          // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовый результат
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               DayorderProfit += profit;                                // добавляем к профиту
              }
            else
              {
               DayorderLoss += MathAbs(profit);                         // если убыток, то всё в сумму
              }
           }
        }
     }

//---теперь выбираем данные по --==НЕДЕЛЕ==--
   HistorySelect(StructToTime(start_week),StructToTime(local));         // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовому результату
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               WeekorderProfit += profit;                               // добавляем к профиту
              }
            else
              {
               WeekorderLoss += MathAbs(profit);                        // если убыток, то всё в сумму
              }
           }
        }
     }

//---теперь выбираем данные по --==МЕСЯЦУ==--
   HistorySelect(StructToTime(start_month),StructToTime(local));        // выбрали нужную историю
//---смотрим
   total  = HistoryDealsTotal();                                        // количество выбранных сделок
   ticket = 0;                                                          // номер ордера
   profit = 0;                                                          // профит ордера
   commis = 0;                                                          // комиссия ордера
   swap   = 0;                                                          // своп ордера

//--- for all deals
   for(uint i=0; i<total; i++)                                          // идём по всем выбранным ордерам
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // получаем по порядку номер каждого
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // получили данные по финансовому резу
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // получили данные по комиссии
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // получили данные по свопу
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // получили данные по типу операции

         MonthOrderSwap    += swap;                                     // суммируем своп
         MonthOrderCommis  += commis;                                   // суммируем комиссию

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // если сделка от торговой операции
           {
            if(profit>0)                                                // если финансовый результат текущего ордера больше 0, то
              {
               MonthorderProfit += profit;                              // добавляем к профиту
              }
            else
              {
               MonthorderLoss += MathAbs(profit);                       // если убыток, то всё в сумму
              }
           }
        }
     }

Мы описали метод, который мы можем вызывать каждый раз, когда нам нужно будет обновить текущие значения использования лимитов. Обновление значений фактических лимитов, как и вызов этой функции можно проводить при генерации различных событий терминала. Так как смысл этого метода в том, чтобы обновлять лимиты, то это можно делать, как при наступлении событий связанных с изменениями текущих ордеров таких как Trade и TradeTransaction, так и при наступлении каждого нового тика через событие NewTick. С учётом того, что наш метод получился не очень ресурсоёмким, мы будем обновлять фактические лимиты каждый тик. Теперь давайте реализуем обработчик событий, необходимый для обработки событий связанных с динамической отменой и разрешением торговли.


Обработчик событий класса

Для обработки событий определим внутренний метод нашего класса ContoEvents() с уровнем доступа protected. Для этого объявим с тем же уровнем доступа дополнительные вспомогательные поля. Чтобы нам в моменте отслеживать время наступления нового торгового периода, для изменения флагов разрешения торговли, нам понадобится хранить значения последнего зафиксированного периода и текущего периода. Для этих целей нам подойдут простые массивы объявленные с типом данных datetime для хранения значений соответствующих периодов.

   //--- дополнительные вспомогательные
   datetime          Periods_old[3];         // 0-day,1-week,2-mn
   datetime          Periods_new[3];         // 0-day,1-week,2-mn

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

   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1);                       // инициализировали текущий день прошлым периодом
   Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1);                       // инициализировали текущую неделю прошлым периодом
   Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1);                      // инициализировали текущий месяц прошлым периодом

Каждый соответствующий период мы будем инициализировать с помощью предопределённой функции терминала iTime() передав в параметры соответствующий период перечисления типа ENUM_TIMEFRAMES с предыдущего от текущего периода. Мы умышленно не будем инициализировать массив Periods_old[] именно для того, чтобы после вызова конструктора и вызова нашего метода ContoEvents() у нас гарантированно срабатывало событие наступление нового торгового периода и открывались все флаги для начала торговли, а уже потом по коду закрывались, если лимитов не окажется. В противном случае, при повторной инициализации класс может работать некорректно. Описанный метод будет содержать простую логику: если текущий период не равен предыдущему, значит наступил новый соответствующий период и можно обнулять лимиты и разрешать торговлю меняя значения во флагах. Также по каждому периоду будем вызывать наш уже описанный метод RefreshLimits() для пересчёта входных лимитов.

//+------------------------------------------------------------------+
//|                     ContoEvents                                  |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoEvents()
  {
// проверяем начало нового торгового дня
   NewTradeDay    = false;                                              // переменная нового торгового дня в фолс
   Periods_old[0] = Periods_new[0];                                     // скопировали в старый, новый
   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0);                       // обновили новый по дню
   if(Periods_new[0]!=Periods_old[0])                                   // если не совпали, то начался новый день
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!");  // проинформировали
      NewTradeDay = true;                                               // переменная в тру

      DayProfitArrive     = false;                                      // обнуляем флаг достижения профита с новым днём
      RiskDayPermission = true;                                         // разрешаем открывать новые позиции

      RefreshLimits();                                                  // обновили лимиты

      DayorderLoss = 0;                                                 // обнуляем значения дневного финансового результата
      DayorderProfit = 0;                                               // обнуляем значения дневного финансового результата
     }

// проверяем начало новой торговой недели
   Periods_old[1]    = Periods_new[1];                                  // скопировали в старый период данные
   Periods_new[1]    = iTime(_Symbol, PERIOD_W1, 0);                    // заполнили новый период по неделе
   if(Periods_new[1]!= Periods_old[1])                                  // если периоды не совпали, то началась новая неделя
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // проинформировали

      RiskWeekPermission = true;                                        // разрешаем открывать новые позиции

      RefreshLimits();                                                  // обновили лимиты

      WeekorderLoss = 0;                                                // обнулили лосы недели
      WeekorderProfit = 0;                                              // обнулили профиты недели
     }

// проверяем начало нового торгового месяца
   Periods_old[2]    = Periods_new[2];                                  // скопировали период в старый
   Periods_new[2]    = iTime(_Symbol, PERIOD_MN1, 0);                   // обновили новый период по месяцу
   if(Periods_new[2]!= Periods_old[2])                                  // если не совпали, то начался новый месяц
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!");   // проинформировали

      RiskMonthPermission = true;                                       // разрешаем открывать новые позы

      RefreshLimits();                                                  // обновили лимиты

      MonthorderLoss = 0;                                               // обнулили убытки месяца
      MonthorderProfit = 0;                                             // обнулили профиты месяца
     }

// корректируем разрешение на открытие новых позиций true только если все true
// корректируем на тру
   if(RiskDayPermission    == true &&                                   // если есть дневной лимит
      RiskWeekPermission   == true &&                                   // если есть недельный лимит
      RiskMonthPermission  == true                                      // если есть месячный лимит
     )                                                                  //
     {
      RiskTradePermission=true;                                         // если всё можно, то можно торговать
     }

// корректируем на фолс если хоть одна фолс
   if(RiskDayPermission    == false ||                                  // или нет дневного лимита
      RiskWeekPermission   == false ||                                  // или нет недельного лимита
      RiskMonthPermission  == false ||                                  // или нет месячного лимита
      DayProfitArrive      == true                                      // или если есть достижение планового профита
     )                                                                  // то
     {
      RiskTradePermission=false;                                        // запрещаем торговать
     }
   }

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


Механизм контроля плановой прибыли на день

В предыдущей части нашей статьи мы уже объявили флаг для запуска процедуры контроля плановой прибыли и входную переменную для определения её величины относительно размера депозита счёта. Логика работы нашего класса по контролю достижения плановой прибыли будет заключаться в закрытии всех открытых позиций, если совокупная величина прибыли по всем достигла необходимого значения. Для закрытия всех позиций по счёту, объявим в нашем классе внутренний метод AllOrdersClose() с уровнем доступа  public. Для работы этого метода нам будет необходимо будет получать данные по открытым позициям и автоматически посылать ордера их на закрытие. 

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

   CTrade            r_trade;                // экземпляр
   CPositionInfo     r_position;             // экземпляр

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

//+------------------------------------------------------------------+
//|                       AllOrdersClose                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::AllOrdersClose()                                  // закрытие рыночных поз
  {
   ulong ticket = 0;                                                    // тикет ордера
   string symb;

   for(int i = PositionsTotal(); i>=0; i--)                             // идём по открытым позам
     {
      if(r_position.SelectByIndex(i))                                   // если позиция выбрана
        {
         ticket = r_position.Ticket();                                  // запомнили тикет позы

         if(!r_trade.PositionClose(ticket))                             // закрываем по тикету
           {
            Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // если нет, проинформировали
            return(false);                                              // вернули фолс
           }
         else
           {
            Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // если нет, проинформировали
            continue;                                                   // если всё ок - продолжаем
           }
        }
     }
   return(true);                                                        // вернём тру
  }

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

//--- дневная
   if(dayProfitControl)							// проверка на включение функционала пользователем
     {
      if(CurrentEquity >= (StartEquity+PlanDayProfit))                  // если эквити превысило или равно старт плюс план профит,
        {
         DayProfitArrive = true;                                        // ставим флаг по достижению дневного планового профита
         Print(__FUNCTION__+", PlanDayProfit has been arrived.");       // проинформировали о событии
         Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+
               ", StartEquity = "+DoubleToString(StartEquity)+
               ", PlanDayProfit = "+DoubleToString(PlanDayProfit));
         AllOrdersClose();                                              // закрыли все открытые ордера

         StartEquity = CurrentEquity;                                   // переписали значение стартового эквити

         //---даём пуш уведомление
         ResetLastError();                                              // сбросили последнюю ошибку
         if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// уведомление
           {
            Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// если нет, принтуем
           }
        }
     }

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


Определение метода для запуска мониторинга в структуре советника

Чтобы нам в структуре советника "включить" функционал мониторинга из экземпляра нашего класса риск-менеджера объявим публичный метод ContoMonitor(). В этом методе мы соберём все ранее объявленные методы обработки событий, а также дополним его функционалом для сравнения фактически использованных лимитов со значениями, утверждёнными пользователем во входных параметрах. Объявим данный метод с уровнем доступа  public и опишем его вне класса следующим образом.

//+------------------------------------------------------------------+
//|                       ContoMonitor                               |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoMonitor()                                    // мониторинг
  {
   ForOnTrade();                                                        // обновляем факт каждый тик

   ContoEvents();                                                       // событийный блок

//---
   double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT);
   
   if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay &&    // если эквити меньше или равен стартовому балансу за вычетом риска на день
       currentProfit<0                                            &&    // профит меньше нуля
       RiskDayPermission==true)                                         // есть разрешение на дневную торговлю
      ||                                                                // ИЛИ
      (RiskDayPermission==true &&                                       // есть разрешение на дневную торговлю
       MathAbs(DayorderLoss) >= RiskPerDay)                             // зафиксированный лосс превысил риск на день
   )                                                                    

     {
      Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = "  +DoubleToString(currentProfit));// проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = "      +DoubleToString(RiskPerDay));   // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = "    +DoubleToString(DayorderLoss)); // проинформировали
      RiskDayPermission=false;                                          // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто
     }

// смотрим есть ли лимит на НЕДЕЛЮ для открытия новой позиции если нет открытых?
   if(
      MathAbs(WeekorderLoss)>=RiskPerWeek &&                            // если недельный убыток больше или равен риска не неделю
      RiskWeekPermission==true)                                         // и торговля велась
     {
      RiskWeekPermission=false;                                         // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто

      Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss));  // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek));      // проинформировали
     }

// смотрим есть ли лимит на МЕСЯЦ для открытия новой позиции если нет открытых?
   if(
      MathAbs(MonthorderLoss)>=RiskPerMonth &&                          // если месячный убыток больше риска на месяц и
      RiskMonthPermission==true)                                        // торговля велась
     {
      RiskMonthPermission=false;                                        // запрещаем открытие новых орденов на день
      AllOrdersClose();                                                 // закрываем то, что не закрыто

      Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss));  // проинформировали
      Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth));      // проинформировали
     }
  }

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

На данном этапе для полного завершения класса нам остаётся лишь добавить метод для контроля пользователем текущих лимитов. Самым простым и достаточно удобным способом будет вывод необходимой информации через стандартную предопределённую функцию терминала Comment(). Для работы с этой функцией нам нужно будет передавать в нее параметр типа string с необходимой пользователю информацией для вывода на график. Чтобы получить эти значения от нашего класса, объявим метод Message() с уровнем доступа public, который будет возвращать тип данных string с собранными данными по всем необходимым пользователю переменным.

//+------------------------------------------------------------------+
//|                        Message                                   |
//+------------------------------------------------------------------+
string RiskManagerBase::Message(void)
  {
   string msg;                                                          // сообщение

   msg += "\n"+" ----------Risk-Manager---------- ";                    // общие
//---
   msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission;           // разрешение общее торговли
   msg += "\n"+"RiskDayPer   = "+(string)RiskDayPermission;             // риск на день есть
   msg += "\n"+"RiskWeekPer  = "+(string)RiskWeekPermission;            // риск на неделю есть
   msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission;           // риск на месяц есть

//---лимиты и входные параметры
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"RiskPerDay   = "+DoubleToString(RiskPerDay,2);          // риск на день в usd
   msg += "\n"+"RiskPerWeek  = "+DoubleToString(RiskPerWeek,2);         // риск на неделю в usd
   msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2);        // риск на месяц в usd
//---текущие прибыли убытки по периодам
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"DayLoss     = "+DoubleToString(DayorderLoss,2);         // дневной лосс
   msg += "\n"+"DayProfit   = "+DoubleToString(DayorderProfit,2);       // дневной профит
   msg += "\n"+"WeekLoss    = "+DoubleToString(WeekorderLoss,2);        // недельный лосс
   msg += "\n"+"WeekProfit  = "+DoubleToString(WeekorderProfit,2);      // недельный профит
   msg += "\n"+"MonthLoss   = "+DoubleToString(MonthorderLoss,2);       // месячный лосс
   msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2);     // месячный профит
   msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2);     // месячный коммис
   msg += "\n"+"MonthSwap   = "+DoubleToString(MonthOrderSwap,2);       // месячный своп
//---для тек мониторинга

   if(dayProfitControl)                                                 // если дневной профит контролируем
     {
      msg += "\n"+" ---------dayProfitControl-------- ";                //
      msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive;         // достижение дневной прибыли
      msg += "\n"+"StartBallance   = "+DoubleToString(StartBalance,2);  // стартовый баланс
      msg += "\n"+"PlanDayProfit   = "+DoubleToString(PlanDayProfit,2); // плановый профит
      msg += "\n"+"PlanDayEquity   = "+DoubleToString(PlanDayEquity,2); // плановый эквити
     }
   return(msg);                                                         // вернули значение
  }

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

Рисунок 2. Формат вывода данных для пользователя.

Рисунок 2. Формат вывода данных для пользователя.

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


Итоговый вариант реализации и возможности расширения класса

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

  • Контроль размера спрэда при торговле с коротким stop-loss
  • Контроль проскальзывания по уже открытым позициям
  • Контроль плановой месячной прибыли

По первому пункту может быть реализован дополнительный функционал для торговых систем использующих торговлю с коротким  stop-loss. Можно объявить метод SpreadMonitor(int intSL) принимающий в качестве параметра технический или расчётный stop-loss по инструменту в пунктах для сравнения его с текущим уровнем спреда. Данный метод будет выдавать запрет на выставление ордера при сильном расширении спреда относительно stop-loss в пропорции определённой пользователем, для избегания высокого риска закрытия позиции по  stop-loss из-за спреда.

Чтобы контролировать проскальзывания по открытым позициям в момент открытия в соответствии со вторым пунктом можно объявить метод SlippageCheck(). Данный метод будет закрывать каждую в отдельности сделку, если брокер открыл её по цене сильно отличающейся от заявленной и соответственно риск на сделку превысил плановое значение. Это позволит в случае срабатывания stop-loss не портить статистику по торговле повышенным риском на один отдельный вход. Также при торговле с фиксированным соотношением  stop-loss и take-profit это соотношение ухудшается за счёт проскальзывания и лучше закрыть сделку с небольшим убытком, чем портить ею последующую статистику.

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

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

#include <RiskManagerBase.mqh>

Далее объявим на глобальном уровне пойнтер нашего объекта риск-менеджера.

RiskManagerBase *RMB;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   RMB = new RiskManagerBase();

//---
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

   delete RMB;

  }

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

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMB.ContoMonitor();

   Comment(RMB.Message());
  }
//+------------------------------------------------------------------+

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


Пример использования

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

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

Рисунок 3. Входы по тестовой стратегии

Рисунок 3. Входы по тестовой стратегии

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

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+
struct TradeInputs
  {
   string             symbol;                                           // символ
   ENUM_POSITION_TYPE direction;                                        // направление
   double             price;                                            // цена
   datetime           tradedate;                                        // дата
   bool               done;                                             // флаг отработки
  };

Основным классом, который будет отвечать за моделирование торговых сигналов будет класс TradeModel. Конструктор данного класса будет принимать контейнер с входными параметрами сигналов, а его основной метод Processing() будет каждый тик следить не наступило ли время точки входа по введённым значениям. Так как мы моделируем внутридневную торговлю, в конце дня мы будем удалять все позиции с помощью объявленного ранее метода AllOrdersClose() в нашем классе риск-менеджера. В общем виде наш вспомогательный класс будет выглядеть следующим образом.

//+------------------------------------------------------------------+
//|                        TradeModel                                |
//+------------------------------------------------------------------+
class TradeModel
  {
protected:

   CTrade               *cTrade;                                        // длятрэйда
   TradeInputs       container[];                                       // хранилище входов

   int               size;                                              // размер хранилища

public:
                     TradeModel(const TradeInputs &inputs[]);
                    ~TradeModel(void);

   void              Processing();                                      // основной метод моделирования
  };

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

//+------------------------------------------------------------------+
//|                          TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::TradeModel(const TradeInputs &inputs[])
  {
   size = ArraySize(inputs);                                            // получили размер хранилища
   ArrayResize(container, size);                                        // ресайзнули

   for(int i=0; i<size; i++)                                            // идём по инпутам
     {
      container[i] = inputs[i];                                         // копируем во внутренний
     }

//---класс трэйда
   cTrade=new CTrade();                                                 // создали экземпляр трэйда
   if(CheckPointer(cTrade)==POINTER_INVALID)                            // если не создался экземпляр, ТО
     {
      Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!");   // проинформировали
     }
   cTrade.SetTypeFillingBySymbol(Symbol());                             // исполнение по символу
   cTrade.SetDeviationInPoints(1000);                                   // отклонение
   cTrade.SetExpertMagicNumber(123);                                    // мэджик
   cTrade.SetAsyncMode(false);                                          // асинхронный метод
  }

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

Деструктор нашего класса TradeModel потребует только удаление объекта типа CTrade и поэтому будет определён просто.

//+------------------------------------------------------------------+
//|                         ~TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::~TradeModel(void)
  {
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // если есть экземпляр, ТО
     {
      delete cTrade;                                                    // удаляем
     }
  }

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

//+------------------------------------------------------------------+
//|                         Processing                               |
//+------------------------------------------------------------------+
void TradeModel::Processing(void)
  {
   datetime timeCurr = TimeCurrent();                                   // запросили текущее время

   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // взяли бид
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // взяли аск

   for(int i=0; i<size; i++)                                            // идём по инпутам
     {
      if(container[i].done==false &&                                    // если ещё не торговали И
         container[i].tradedate <= timeCurr)                            // дата что нужно
        {
         switch(container[i].direction)                                 // проверяем направление трейда
           {
            //---
            case  POSITION_TYPE_BUY:                                    // если покупка
               if(container[i].price >= ask)                            // смотрим дошла ли цена, то
                 {
                  if(cTrade.Buy(0.1))                                   // покупаем одинаковый лот
                    {
                     container[i].done = true;                          // если прошло, ставим отметку
                     Print("Buy has been done");                        // информируем
                    }
                  else                                                  // если не прошло,
                    {
                     Print("Error: buy");                               // информируем
                    }
                 }
               break;                                                   // завершение кейса
            //---
            case  POSITION_TYPE_SELL:                                   // если это продажа
               if(container[i].price <= bid)                            // смотрим дошла ли цена, то
                 {
                  if(cTrade.Sell(0.1))                                  // продаем одинаковый лот
                    {
                     container[i].done = true;                          // если прошло, ставим отметку
                     Print("Sell has been done");                       // информируем
                    }
                  else                                                  // если не прошло,
                    {
                     Print("Error: sell");                              // информируем
                    }
                 }
               break;                                                   // завершение кейса

            //---
            default:
               Print("Wrong inputs");                                   // информируем
               return;
               break;
           }
        }
     }
  }

Логика этого метода достаточно проста. Если в контейнере есть неотработанные входы, по которым время моделирования подошло мы выставляем эти ордера в соответствии с направлением и ценой фрактала отмеченного на Рисунке 3. Данного функционала нам будет достаточно, чтобы протестировать наш риск-менеджер, поэтому можно встраивать его в наш основной проект.

В первую очередь подключим наш класс для тестов в код советника следующим образом.

#include <TradeModel.mqh>

Теперь в функции OnInit() создадим экземпляр нашей структуры массива входных данных TradeInputs и передадим этот массив в конструктор класса TradeModel для его инициализации.

//---
   TradeInputs modelInputs[] =
     {
        {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.794, D'2024-02-05',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.882, D'2024-02-08',false},
        {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false}
     };

//---
   tModel = new TradeModel(modelInputs);

При этом в функции DeInit() не забудем очистить память нашего объекта tModel. Основной же функционал будет выполняться в функции OnTick(), дополненной следующим кодом.

   tModel.Processing();                                                 // выставляем ордера

   MqlDateTime time_curr;                                               // структура текущего времени
   TimeCurrent(time_curr);                                              // запросили текущее время

   if(time_curr.hour >= 23)                                             // если конец дня
     {
      RMB.AllOrdersClose();                                             // кроем все позы
     }

Теперь сравним результаты работы одной и той же стратегии при наличии и отсутствии класса контроля рисков. Запустим файл юнитеста ManualRiskManager(UniTest1), в котором не будет запуска метода контроля рисков по входным данным. Период с января по март 2024г. В итоге получим следующий результат работы нашей стратегии.

Рисунок 4. Тестовые данные без применения риск менеджера

Рисунок 4. Тестовые данные без применения риск-менеджера

В результате мы получаем положительное математическое ожидание по данной стратегии со следующими параметрами.

№ п\п Наименование параметра Значение параметра
 1  Советник  ManualRiskManager(UniTest1)
 2  Символ  USDJPY
 3  Период графика  М15
 4  Интервал  2024.01.01 - 2024.03.18
 5  Форвард  нет 
 6  Задержки   Без задержек, идеальное исполнение
 7  Моделирование  Все тики 
 8  Начальный депозит  10 000 usd 
 9  Плечо  1:100 

Таблица 1. Входные параметры для тестера стратегий


Теперь запустим файл юнитеста ManualRiskManager(UniTest2), где мы задействуем наш класс риск-менеджера со следующими входными параметрами.

Наименование входного параметра Значение переменной
inp_riskperday 0.25
inp_riskperweek 0.75
inp_riskpermonth 2.25
inp_plandayprofit  0.78 
dayProfitControl  true

Таблица 2. Входные параметры для риск-менеджера

Логика формирования входных параметров аналогична логики, описанной выше при проектировании структуры входных параметров в Главе 3. Кривая доходности будет выглядеть следующим образом.

Рисунок5. Тестовые данные с применением риск менеджера

Рисунок 5. Тестовые данные с применением риск-менеджера


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

№ п\п Показатель Без РМ С РМ Изменение
 1  Чистая прибыль:  41.1 144.48  +103.38
 2  Максимальная просадка по балансу:  0.74% 0.25%  Снижение в 3 раза
 3  Максимальная просадка по средствам:  1.13% 0.58%  Снижение в 2 раза
 4  Матожидание выигрыша:  10.28 36.12  Рост более чем в 3 раза
 5  Коэффициент Шарпа:  0.12 0.67  Рост в 5 раз
 6  Прибыльные трейды (% от всех):  75% 75%  -
 7  Средний прибыльный трейд:  38.52 56.65  Рост на 50%
 8  Средний убыточный трейд:  -74.47 -25.46  Снижение в 3 раза
 9  Средняя риск доходность  0.52  2.23  Рост в 4 раза

Таблица 3. Сравнение финансового результата торговли с риск-менеджером и без него

По результатам наших юнитестов можно сделать вывод, что применение контроля рисков с помощью нашего класса риск-менеджера позволило существенно увеличить эффективность торговли по одной и той же простой стратегии, за счёт ограничения рисков и фиксации прибыли по каждой сделке относительно зафиксированного риска. Это позволило снизить просадку по балансу в 3 раза, а по эквити в 2 раза. Матожидание по стратегии выросло более чем в 3 раза, а коэффициент Шарпа увеличился более чем в 5 раз. Средний прибыльный трейд увеличился на 50%, а средний убыточный трейд снизился в три раза, что позволило вывести среднюю риск доходность по счёту почти до целевого значения 1 к 3. Подробное сравнение финансового результата по каждой отдельной сделке из нашего пула представлено в следующей таблице.


Дата Инструмент Направление Лот Без РМ С РМ Изменение
2024.01.31 USDJPY buy 0.1 25.75 78 + 52.25
2024.02.05
USDJPY sell 0.1
13.19 13.19 -
2024.02.08
USDJPY sell 0.1
76.63 78.75 + 2.12
2024.02.08
USDJPY buy 0.1
-74.47 -25.46 + 49.01
Итого - - - 41.10 144.48 + 103.38

Таблица 4. Сравнение исполненных сделок по торговле с риск-менеджером и без него


Заключение

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

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


Прикрепленные файлы |
RiskManagerBase.mqh (61.79 KB)
TradeModel.mqh (13.18 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (8)
Aleksandr Seredin
Aleksandr Seredin | 23 мар. 2024 в 06:06
ZlobotTrader #:
Полезная статья. Спасибо!

Спасибо большое) Очень приятно. Как считаете, писать следующую про алгоритмического наследника риск-менеджера?

Aleksei Iakunin
Aleksei Iakunin | 26 мар. 2024 в 20:40

Конечно пишите-новичкам очень полезно, но раз Ваши статьи ориентированы на новичков( субъективное мнение),

то уделите внимание чуть больше "разжевыванию" кода.

Удачи)

Aleksandr Seredin
Aleksandr Seredin | 27 мар. 2024 в 06:32
Алексей #:

Конечно пишите-новичкам очень полезно, но раз Ваши статьи ориентированы на новичков( субъективное мнение),

то уделите внимание чуть больше "разжевыванию" кода.

Удачи)

Принято, спасибо)

HasiTrader
HasiTrader | 12 авг. 2024 в 21:40
Здравствуйте @Aleksandr Seredin
Вы проделали очень хорошую работу.
Главное, чтобы советник мог предотвратить открытие новой сделки после превышения установленных ограничений.

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

Удачи

Aleksandr Seredin
Aleksandr Seredin | 13 авг. 2024 в 17:01
HasiTrader #:
Здравствуйте @Aleksandr Seredin
Вы проделали очень хорошую работу.
Главное, чтобы советник мог предотвратить открытие новой сделки после превышения установленных ограничений.

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

Удачи

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

Роль качества генератора случайных чисел в эффективности алгоритмов оптимизации Роль качества генератора случайных чисел в эффективности алгоритмов оптимизации
В этой статье мы рассмотрим генератор случайных чисел Mersenne Twister и сравним со стандартным в MQL5. Узнаем влияние качества случайных чисел генераторов на результаты алгоритмов оптимизации.
Нейросети — это просто (Часть 82): Модели Обыкновенных Дифференциальных Уравнений (NeuralODE) Нейросети — это просто (Часть 82): Модели Обыкновенных Дифференциальных Уравнений (NeuralODE)
В данной статье я предлагаю познакомиться Вас с еще одним типом моделей, которые направлены на изучение динамики состояния окружающей среды.
DoEasy. Сервисные функции (Часть 2): Паттерн "Внутренний бар" DoEasy. Сервисные функции (Часть 2): Паттерн "Внутренний бар"
В статье продолжим рассматривать ценовые паттерны в библиотеке DoEasy. Создадим класс паттерна "Внутренний бар" формаций Price Action.
Создаем простой мультивалютный советник с использованием MQL5 (Часть 4): Треугольная скользящая средняя — Сигналы индикатора Создаем простой мультивалютный советник с использованием MQL5 (Часть 4): Треугольная скользящая средняя — Сигналы индикатора
Под мультивалютным советником в этой статье понимается советник, или торговый робот, который может торговать (открывать/закрывать ордера, управлять ордерами, например, трейлинг-стоп-лоссом и трейлинг-профитом) более чем одной парой символов с одного графика. На этот раз мы будем использовать только один индикатор, а именно треугольную скользящую среднюю на одном или нескольких таймфреймах.