
Разрабатываем мультивалютный советник (Часть 12): Риск-менеджер как для проп-трейдинговых компаний
Введение
На протяжении всего цикла статей мы уже неоднократно обращались к теме контроля рисков. Были введены понятия нормированной торговой стратегии, параметры которой обеспечивают достижение уровня просадки 10% на протяжении периода тестирования. Однако, нормировка таким образом экземпляров торговых стратегий, а также групп торговых стратегий, может обеспечить заданную просадку только на историческом периоде. При запуске тестирования нормированной группы стратегий на форвард-периоде, или запуске её уже на торговом счёте, мы не можем быть уверены в соблюдении заданного уровня просадки.
За последнее время тема контроля рисков поднималась, например, в статьях Риск-менеджер для ручной торговли и Риск-менеджер для алгоритмической торговли. В них автор предлагал программную реализацию, контролирующую соответствие различных параметров торговли заранее заданным показателям. Например, при превышении установленного уровня убытка за день, неделю или месяц, торговля приостанавливается.
Также весьма интересной показалась статья Учимся у проп-трейдинговых компаний, в которой автор рассматривает требования к торговле, предъявляемые проп-трейдинговыми компаниями для прохождения испытаний трейдеров, желающих получить капитал в управление. Несмотря на неоднозначное отношение к деятельности подобных компаний, которое можно встретить на различных ресурсах, посвящённых трейдингу, использование чётких правил контроля рисков является одной из важнейших составляющих успешной торговли. Поэтому, почему бы нам не воспользоваться уже накопленным чужим опытом и не реализовать свой собственный риск-менеджер, взяв за основу модель контроля рисков, применяемую в проп-трейдинговых компаниях?
Модель и понятия
Для риск-менеджера нам пригодятся следующие понятия:
- Базовый баланс — это начальный баланс счёта (или часть баланса счёта) от которого могут рассчитываться значения остальных параметров. В нашем примере будем использовать значение этого показателя равное 10000.
- Дневной базовый баланс — это баланс торгового счёта на начало текущего дневного периода. Для простоты будем считать, что начало дневного периода совпадает с появлением нового бара в терминале на таймфрейме D1.
- Дневные базовые средства — это размер средств на торговом счёте на начало текущего дневного периода.
- Дневной уровень — это максимум из дневного базового баланса и средств. Определяется в начале дневного периода и сохраняет своё значение для начала следующего дневного периода.
- Максимальный дневной убыток — это размер отклонения в меньшую сторону средств на счёте от дневного уровня, при котором следует остановить торговлю на текущем дневном периоде. На следующем дневном периоде торговля будет возобновлена. Под остановкой можно понимать различные действия, направленные на уменьшение размеров открытых позиций, вплоть до полного закрытия. Для начала будем использовать именно такую простую модель: при достижении максимального дневного убытка все открытые рыночные позиции будут закрыты.
- Максимальный общий убыток — это размер отклонения в меньшую сторону средств на счёте от значения базового баланса, при котором торговля останавливается полностью, то есть не будет возобновляться на следующих периодах. При достижении этого уровня все открытые позиции закрываются.
Мы ограничимся только двумя уровнями для остановки торговли: дневным и общим. Можно ещё добавить аналогично недельный или месячный уровень. Но раз их нет у проп-трейдинговых компаний, то и мы не будем усложнять первую реализацию нашего риск-менеджера. При необходимости их можно будет добавить позднее.
У разных проп-трейдинговых компаний подходы к вычислению максимального дневного и общего убытка могут несколько отличаться. Поэтому предусмотрим в нашем риск-менеджере три возможных способа задания числового значения для вычисления максимального убытка:
- Фиксированный в валюте депозита. При таком способе мы напрямую в параметре передаём значение убытка, выраженное в единицах валюты торгового счёта. Будем задавать его в виде положительного числа.
- В процентах от базового баланса. В этом случае значение воспринимается как процент от установленного базового баланса. Поскольку базовый баланс в нашей модели — величина постоянная (до перезапуска счёта и советника с заданным вручную другим значением базового баланса), то и рассчитанный таким способом максимальный убыток тоже будет величиной постоянной. Можно было бы свести этот случай к первому, но поскольку обычно указывается как раз процент максимального убытка, то оставим его как отдельный случай.
- В процентах от дневного уровня. В данном варианте мы в начале каждого дневного периода заново рассчитываем уровень максимального убытка, как заданный процент от только что рассчитанного дневного уровня. При росте баланса или средств, размер максимального убытка будет тоже возрастать. Такой способ в основном будет применяться для расчёта только максимального дневного убытка. Максимальный общий убыток обычно является фиксированным относительно базового баланса.
Приступим к реализации нашего класса риск-менеджера, как всегда руководствуясь принципом наименьшего действия. Сделаем сначала минимально необходимую реализацию, заложив возможности её дальнейшего усложнения при необходимости.
Класс CVirtualRiskManager
При разработке этого класса было пройдено несколько стадий. Сначала он был сделан как полностью статический, чтобы им можно было свободно пользоваться из всех объектов. Затем возникло предположение, что параметры риск-менеджера мы тоже сможем оптимизировать, и было бы неплохо иметь возможность сохранять их в виде строки инициализации. Для этого класс был сделан наследником класса CFactorable. Для обеспечения возможности использовать риск-менеджер в объектах разных классов, был реализован шаблон Singleton. Но далее выяснилось, что риск-менеджер нужен пока только в одном единственном классе — классе эксперта CVirtualAdvisor. Поэтому реализацию шаблона Singleton мы из класса риск-менеджера убрали.
Прежде всего создадим перечисления для возможных состояний риск-менеджера и возможных способов расчёта лимитов:
// Возможные состояния риск-менеджера enum ENUM_RM_STATE { RM_STATE_OK, // Лимиты не превышены RM_STATE_DAILY_LOSS, // Превышен дневной лимит RM_STATE_OVERALL_LOSS // Превышен общий лимит }; // Возможные способы расчёта лимитов enum ENUM_RM_CALC_LIMIT { RM_CALC_LIMIT_FIXED, // Fixed (USD) RM_CALC_LIMIT_FIXED_PERCENT, // Fixed (% from Base Balance) RM_CALC_LIMIT_PERCENT // Relative (% from Daily Level) };
В описании класса риск-менеджера у нас будет несколько свойств для хранения входных параметров, передаваемых через строку инициализации в конструктор. Также добавим свойства для хранения различных расчётных характеристик — текущего баланса, средств, прибыли и прочих. Объявим несколько вспомогательных методов в защищённой секции. В открытой секции у нас останется по сути только конструктор и метод обработки каждого тика. Методы сохранения/загрузки и оператор преобразования в строку пока только упомянем, а реализацию напишем позднее.
Тогда описание класса будет выглядеть примерно так:
//+------------------------------------------------------------------+ //| Класс управления риском (риск-менеждер) | //+------------------------------------------------------------------+ class CVirtualRiskManager : public CFactorable { protected: // Основные параметры конструктора bool m_isActive; // Риск менеджер активен? double m_baseBalance; // Базовый баланс ENUM_RM_CALC_LIMIT m_calcDailyLossLimit; // Способ расчёта максимального дневного убытка double m_maxDailyLossLimit; // Параметр расчёта максимального дневного убытка ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Способ расчёта максимального общего убытка double m_maxOverallLossLimit; // Параметр расчёта максимального общего убытка // Текущее состояние ENUM_RM_STATE m_state; // Обновляемые значения 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_virtualProfit; // Прибыль открытых виртуальных позиций // Управление размером открытых позиций double m_prevDepoPart; // Используемая часть общего баланса // Защищённые методы double DailyLoss(); // Максимальный дневной убыток double OverallLoss(); // Максимальный общий убыток void UpdateProfit(); // Обновление текущих значений прибыли void UpdateBaseLevels(); // Обновление дневных базовых уровней void CheckLimits(); // Проверка превышения допустимых убытков void CheckDailyLimit(); // Проверка превышения допустимого дневного убытка void CheckOverallLimit(); // Проверка превышения допустимого общего убытка double VirtualProfit(); // Определение реального размера виртуальной позиции public: CVirtualRiskManager(string p_params); // Конструктор virtual void Tick(); // Обработка тика в риск-менеджере virtual bool Load(const int f); // Загрузка состояния virtual bool Save(const int f); // Сохранение состояния virtual string operator~() override; // Преобразование объекта в строку };
Конструктор объекта риск-менеджера будет ожидать наличия в строке инициализации шести числовых значений, которые, после преобразования в соответствующие типы данных, будут присваиваться основным свойствам объекта. Также при создании мы устанавливаем состояние, соответствующее нормальному (то есть лимиты не превышены). Если объект пересоздаётся при перезапуске советника где-то в середине дня, то при загрузке сохранённой информации состояние должно быть исправлено на то, которое было в момент последнего сохранения. То же самое относится и к установке доли баланса счёта, выделенного для торговли, — значение, установленное в конструкторе, может быть предопределено при загрузке сохранённой информации о риск-менеджере.
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualRiskManager::CVirtualRiskManager(string p_params) { // Запоминаем строку инициализации m_params = p_params; // Читаем строку инициализации и устанавливаем значения свойств m_isActive = (bool) ReadLong(p_params); m_baseBalance = ReadDouble(p_params); m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxDailyLossLimit = ReadDouble(p_params); m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxOverallLossLimit = ReadDouble(p_params); // Устанавливаем состояние: Лимиты не превышены m_state = RM_STATE_OK; // Запоминаем долю баланса счёта, выделенного на торговлю m_prevDepoPart = CMoney::DepoPart(); // Обновляем базовые дневные уровни UpdateBaseLevels(); // Корректируем базовый баланс, если он не задан if(m_baseBalance == 0) { m_baseBalance = m_balance; } }
Основную работу риск-менеджер будет выполнять на каждом тике в обработчике этого события. Она будет заключаться в проверке активности риск-менеджера и, если он активен, выполнении обновления текущих значений прибыли и базовых дневных уровней при необходимости, а также проверки превышения допустимых пределов убытка:
//+------------------------------------------------------------------+ //| Обработка тика в риск-менеджере | //+------------------------------------------------------------------+ void CVirtualRiskManager::Tick() { // Если риск-менеджер неактивен, то выходим if(!m_isActive) { return; } // Обновляем текущие значения прибыли UpdateProfit(); // Если наступил новый дневной период, то обновляем базовые дневные уровни if(IsNewBar(Symbol(), PERIOD_D1)) { UpdateBaseLevels(); } // Проверяем превышение пределов убытка CheckLimits(); }
Отдельно отметим такой момент. Благодаря разработанной схеме с использованием виртуальных позиций, которые получатель торговых объемов превращает в реальные рыночные позиции, и модуля управления капиталом, который позволяет задавать необходимый коэффициент масштабирования между размерами виртуальных и реальных позиций, мы можем очень просто реализовать безопасное закрытие рыночных позиций, не нарушающее торговую логику работающих стратегий. Для этого достаточно всего лишь установить коэффициент масштабирования в модуле управления капиталом равный 0:
CMoney::DepoPart(0); // Устанавливаем используемую часть общего баланса в 0
Если перед этим мы запомним предыдущий коэффициент в свойстве m_prevDepoPart, то после наступления нового дня и обновления дневного лимита, мы сможем восстановить ранее закрытые реальные позиции, просто вернув этот коэффициент к прежнему значению:
CMoney::DepoPart(m_prevDepoPart); // Возвращаем используемую часть общего баланса
При этом, конечно, мы не можем заранее знать, будут ли позиции переоткрыты по худшей или лучшей цене. Зато мы можем быть уверены, что добавление риск-менеджера никак не повлияло на работу всех экземпляров торговых стратегий.
Теперь приступим к рассмотрению остальных методов класса риск-менеджера.
В методе UpdateProfits() мы обновляем текущие значения баланса, средств и прибыли, а также вычисляем дневную прибыль как разность текущих средств и дневного уровня. Следует отметить, что эта величина не всегда будет совпадать с текущей прибылью. Разница появится в том случае, если с начала нового дневного периода уже были закрыты некоторые сделки. Общий убыток мы рассчитываем как разность текущих средств и базового баланса.
//+------------------------------------------------------------------+ //| Обновление текущих значений прибыли | //+------------------------------------------------------------------+ void CVirtualRiskManager::UpdateProfit() { m_equity = AccountInfoDouble(ACCOUNT_EQUITY); m_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_profit = m_equity - m_balance; m_dailyProfit = m_equity - m_baseDailyLevel; m_overallProfit = m_equity - m_baseBalance; m_virtualProfit = VirtualProfit(); if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) { PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); } }
Ещё в этом методе мы рассчитываем так называемую текущую виртуальную прибыль. Она вычисляется по открытым виртуальным позициям. Если мы будем оставлять виртуальные позиции открытыми при срабатывании ограничений риск-менеджера, то даже при отсутствии реальных открытых позиций мы можем в любой момент оценить, какая бы сейчас была примерная прибыль, если бы закрытые риск-менеджером реальные позиции оставались бы открытыми. К сожалению, эта расчётная характеристика даёт не совсем точный результат (с погрешностью в несколько процентов). Однако, даже эта информация гораздо лучше её отсутствия.
Вычисление текущей виртуальной прибыли выполняет метод VirtualProfit(). В нём мы получаем указатель на объект получателя виртуальных объёмов, так как нам надо у него узнать общее количество виртуальных позиций и иметь возможность обращения к каждой виртуальной позиции. После этого мы проходимся в цикле по всем виртуальным позициям, и просим наш модуль управления капиталом рассчитать виртуальную прибыль каждой позиции с учётом масштабирования для текущего размера средств для торговли:
//+------------------------------------------------------------------+ //| Определение прибыли открытых виртуальных позиций | //+------------------------------------------------------------------+ double CVirtualRiskManager::VirtualProfit() { // Обращаемся к объекту получателя CVirtualReceiver *m_receiver = CVirtualReceiver::Instance(); double profit = 0; // Для всех виртуальных позиций находим сумму их прибыли FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i))); return profit; }
В этом методе мы применили новый макрос FORI, который будет рассмотрен ниже.
При наступлении нового дневного периода мы будем пересчитывать базовый дневной баланс, средства и уровень. Также мы будем проверять, что если в прошлый день был достигнут дневной лимит убытка, то нам надо восстановить торговлю и переоткрыть реальные позиции в соответствии с открытыми виртуальными позициями. Этим будет заниматься метод UpdateBaseLevels():
//+------------------------------------------------------------------+ //| Обновление дневных базовых уровней | //+------------------------------------------------------------------+ void CVirtualRiskManager::UpdateBaseLevels() { // Обновляем баланс, средства и базовый дневной уровень m_baseDailyBalance = m_balance; m_baseDailyEquity = m_equity; m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity); PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f", m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel); // Если ранее был достигнут дневной уровень убытка, то if(m_state == RM_STATE_DAILY_LOSS) { // Восстанавливаем состояние до нормального: CMoney::DepoPart(m_prevDepoPart); // Возвращаем используемая часть общего баланса m_state = RM_STATE_OK; // Устанавливаем риск-менеджер в нормальное состояние CVirtualReceiver::Instance().Changed(); // Оповещаем получатель об изменениях PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f", m_prevDepoPart); } }
Для расчёта максимальных убытков по заданным в параметрах способам у нас будут два метода: DailyLoss() и OverallLoss(). Их реализация очень похожа между собой, разница только в том, какой числовой параметр и параметр способа используются для расчёта:
//+------------------------------------------------------------------+ //| Максимальный дневной убыток | //+------------------------------------------------------------------+ double CVirtualRiskManager::DailyLoss() { if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) { // Для фиксированного значения просто возвращаем его return m_maxDailyLossLimit; } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) { // Для заданного процента от базового баланса вычисляем его return m_baseBalance * m_maxDailyLossLimit / 100; } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT) // Для заданного процента от дневного уровня вычисляем его return m_baseDailyLevel * m_maxDailyLossLimit / 100; } } //+------------------------------------------------------------------+ //| Максимальный общий убыток | //+------------------------------------------------------------------+ double CVirtualRiskManager::OverallLoss() { if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) { // Для фиксированного значения просто возвращаем его return m_maxOverallLossLimit; } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) { // Для заданного процента от базового баланса вычисляем его return m_baseBalance * m_maxOverallLossLimit / 100; } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT) // Для заданного процента от дневного уровня вычисляем его return m_baseDailyLevel * m_maxOverallLossLimit / 100; } }
Метод проверки лимитов CheckLimits() будет просто вызывать два вспомогательных метода проверки дневного и общего убытка:
//+------------------------------------------------------------------+ //| Проверка лимитов убытка | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckLimits() { CheckDailyLimit(); // Проверка дневного лимита CheckOverallLimit(); // Проверка общего лимита }
Метод проверки дневного убытка использует метод DailyLoss() для получения максимального допустимого лимита дневного убытка и сравнивает его с текущей дневной прибылью. При превышении лимита риск-менеджер переводится в состояние "Превышен дневной лимит", и инициируется закрытие открытых позиций через установку размера используемого торгового баланса равного 0:
//+------------------------------------------------------------------+ //| Проверка дневного лимита убытка | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckDailyLimit() { // Если достинут дневной убыток и позиции ещё открыты if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) { // Переводим риск-менеджер в состояние достигнутого дневного убытка: m_prevDepoPart = CMoney::DepoPart(); // Запоминаем предыдущее значение используемой части общего баланса CMoney::DepoPart(0); // Устанавливаем используемую часть общего баланса в 0 m_state = RM_STATE_DAILY_LOSS; // Устанавливаем риск-менеджер в состояние достигнутого дневного убытка CVirtualReceiver::Instance().Changed();// Оповещаем получатель об изменениях PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f", CMoney::DepoPart()); } }
Метод проверки общего убытка работает аналогично, с той лишь разницей, что производится сравнение общей прибыли и общего допустимого убытка. При превышении общего лимита риск-менеджер переводится в состояние "Превышен общий лимит".
Сохраним полученный код в файле VirtualRiskManager.mqh в текущей папке.
Рассмотрим теперь изменения и дополнения, которые нам понадобится внести в ранее созданные файлы проекта, чтобы получить возможность использования нашего нового класса риск-менеджера.
Полезные макросы
В состав полезных макросов для работы с массивами мы добавили новый макрос FORI(N, D), который организует цикл с переменной i, выполняющий N раз выражение D:
// Полезные макросы для операций с массивами #ifndef __MACROS_INCLUDE__ #define APPEND(A, V) A[ArrayResize(A, ArraySize(A) + 1) - 1] = V; #define FIND(A, V, I) { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } } #define ADD(A, V) { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } } #define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} } #define FORI(N, D) { for(int i=0; i<N;i++) {D;} } #define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);} #define REMOVE(A, V) { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) } #define __MACROS_INCLUDE__ #endif
Сохраним эти изменения в файле Macros.mqh в текущей папке.
Класс управления капиталом СMoney
В этом классе мы добавим метод расчёта прибыли виртуальной позиции с учётом коэффициента масштабирования её объема. Собственно, подобную операцию мы как раз проделываем в методе Volume() для определения расчётного размера виртуальной позиции: исходя из информации о текущем доступном размере баланса для торговли, и размера баланса, соответствующего объёму виртуальной позиции, мы находим коэффициент масштабирования, равный отношению этих балансов. На этот коэффициент затем умножается объем виртуальной позиции, чтобы получить расчётный объем, то есть тот, который будет открыт на торговом счёте.
Поэтому вынесем сначала из метода Volume() ту часть кода, которая находит коэффициент масштабирования в отдельный метод Coeff():
//+------------------------------------------------------------------+ //| Вычисление коэфф. масштабирования объёма виртуальной позиции | //+------------------------------------------------------------------+ double CMoney::Coeff(CVirtualOrder *p_order) { // Запрашиваем нормированный баланс стретегии для этой виртуальной позиции double fittedBalance = p_order.FittedBalance(); // Если он равен 0, то коэффициент масштабирования равен 1 if(fittedBalance == 0.0) { return 1; } // Иначе находим величину общего баланса для торговли double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE); // Возвращаем коэффициент масштабирования объёма return totalBalance * s_depoPart / fittedBalance; }
После этого реализация методов Volume() и Profit() становится очень похожей: мы берем нужную величину (объём или прибыль) у виртуальной позиции и умножаем её на полученный коэффициент масштабирования:
//+------------------------------------------------------------------+ //| Определение расчётного размера виртуальной позиции | //+------------------------------------------------------------------+ double CMoney::Volume(CVirtualOrder *p_order) { return p_order.Volume() * Coeff(p_order); } //+------------------------------------------------------------------+ //| Определение расчётной прибыли виртуальной позиции | //+------------------------------------------------------------------+ double CMoney::Profit(CVirtualOrder *p_order) { return p_order.Profit() * Coeff(p_order); }
Ну и конечно, нам надо добавить новые методы в описание класса:
//+------------------------------------------------------------------+ //| Базовый класс управления капиталом | //+------------------------------------------------------------------+ class CMoney { ... // Вычисление коэффициента масштабирования объёма виртуальной позиции static double Coeff(CVirtualOrder *p_order); public: CMoney() = delete; // Запрещаем конструктор // Определение расчётного размера виртуальной позиции static double Volume(CVirtualOrder *p_order); // Определение расчётной прибыли виртуальной позиции static double Profit(CVirtualOrder *p_order); ... };
Сохраним сделанные изменения в файле Money.mqh в текущей папке.
Класс СVirtualFactory
Поскольку созданный нами класс риск-менеджера является наследником класса CFactorable, то для обеспечения возможности его создания необходимо расширить состав объектов, создаваемых фабрикой CVirtualFactory. Добавим внутри статического метода Create() блок кода, отвечающий за создание объекта класса CVirtualRiskManager:
//+------------------------------------------------------------------+ //| Класс фабрики объектов | //+------------------------------------------------------------------+ class CVirtualFactory { public: // Создание объекта из строки инициализации static CFactorable* Create(string p_params) { // Читаем имя класса объекта string className = CFactorable::ReadClassName(p_params); // Указатель на создаваемый объект CFactorable* object = NULL; // В зависимости от имени класса вызываем соответствующий конструктор if(className == "CVirtualAdvisor") { object = new CVirtualAdvisor(p_params); } else if(className == "CVirtualRiskManager") { object = new CVirtualRiskManager(p_params); } else if(className == "CVirtualStrategyGroup") { object = new CVirtualStrategyGroup(p_params); } else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); } ... return object; } };
Сохраним полученный код в файле VirtualFactory.mqh в текущей папке.
Класс CVirtualAdvisor
Более существенные изменения нам понадобится внести в класс эксперта CVirtualAdvisor. Поскольку мы решили, что объект риск-менеджера будет использоваться только внутри этого класса, то добавим соответствующее свойство в описание класса:
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Объект получателя, выводящий позиции на рынок CVirtualInterface *m_interface; // Объект интерфейса для показа состояния пользователю CVirtualRiskManager *m_riskManager; // Объект риск-менеджера ... };
Также давайте договоримся, что строка инициализации риск-менеджера будет встроена в строку инициализации эксперта сразу после строки инициализации группы стратегий. Тогда добавим в конструкторе чтение этой строки инициализации в переменную riskManagerParams, и последующее создание риск-менеджера из неё:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { // Запоминаем строку инициализации m_params = p_params; // Читаем строку инициализации объекта группы стратегий string groupParams = ReadObject(p_params); // Читаем строку инициализации объекта риск-менеджера string riskManagerParams = ReadObject(p_params); // Читаем магический номер ulong p_magic = ReadLong(p_params); // Читаем название эксперта string p_name = ReadString(p_params); // Читаем признак работы на только на открытии бара m_useOnlyNewBar = (bool) ReadLong(p_params); // Если нет ошибок чтения, то if(IsValid()) { ... // Создаём объект риск-менеджера m_riskManager = NEW(riskManagerParams); } }
Раз уж мы создали объект в конструкторе, то надо позаботиться и об удалении его в деструкторе:
//+------------------------------------------------------------------+ //| Деструктор | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_receiver) delete m_receiver; // Удаляем получатель if(!!m_interface) delete m_interface; // Удаляем интерфейс if(!!m_riskManager) delete m_riskManager; // Удаляем риск-менеджер DestroyNewBar(); // Удаляем объекты отслеживания нового бара }
Ну и самое главное — вызов обработчика Tick() для риск-менеджера из соответствующего обработчика эксперта. Обратите внимание, что обработчик риск-менеджера запускается перед корректировкой рыночных объёмов, чтобы если обнаружилось превышение лимитов по убытку, или наоборот, случилось обновление лимитов, то на обработке этого же тика получатель смог скорректировать открытые объёмы рыночных позиций:
//+------------------------------------------------------------------+ //| Обработчик события OnTick | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Определяем новый бар по всем нужным символам и таймфреймам bool isNewBar = UpdateNewBar(); // Если нигде нового бара нет, а мы работаем только по новым барам, то выходим if(!isNewBar && m_useOnlyNewBar) { return; } // Получатель обрабатывает виртуальные позиции m_receiver.Tick(); // Запуск обработки в стратегиях CAdvisor::Tick(); // Риск-менеджер обрабатывает виртуальные позиции m_riskManager.Tick(); // Корректировка рыночных объемов m_receiver.Correct(); // Сохранение состояния Save(); // Отрисовка интерфейса m_interface.Redraw(); }
Сохраним сделанные изменения в файле VirtualAdvisor.mqh в текущей папке.
Советник SimpleVolumesExpertSingle
Для тестирования риск-менеджера остаётся только добавить возможность указывать его параметры в советнике и формирование нужной строки инициализации. Вынесем пока что все шесть параметров риск-менеджера в отдельные входные переменные советника:
input group "=== Управление рисками" input bool rmIsActive_ = true; input double rmStartBaseBalance_ = 10000; input ENUM_RM_CALC_LIMIT rmCalcDailyLossLimit_ = RM_CALC_LIMIT_FIXED; input double rmMaxDailyLossLimit_ = 200; input ENUM_RM_CALC_LIMIT rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED; input double rmMaxOverallLossLimit_ = 500;
В функции OnInit() надо добавить создание строки инициализации риск-менеджера и встраивание её в строку инициализации эксперта. Заодно немного перепишем код создания строк инициализации для стратегии и группы, включающей одну эту стратегию, выделив строки инициализации отдельных объектов в разные переменные:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CMoney::FixedBalance(fixedBalance_); CMoney::DepoPart(1.0); // Подготавливаем строку инициализации для одного экземпляра стратегии string strategyParams = StringFormat( "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)", Symbol(), Period(), signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_ ); // Подготавливаем строку инициализации для группы с одним экземпляром стратегии string groupParams = StringFormat( "class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " )", strategyParams, scale_ ); // Подготавливаем строку инициализации для риск-менеджера string riskManagerParams = StringFormat( "class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%d,%.2f" " )", rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_ ); // Подготавливаем строку инициализации для эксперта с группой из одной стратегии и риск-менеджером string expertParams = StringFormat( "class CVirtualAdvisor(\n" " %s,\n" " %s,\n" " %d,%s,%d\n" ")", groupParams, riskManagerParams, magic_, "SimpleVolumesSingle", true ); PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams); // Создаем эксперта, работающего с виртуальными позициями expert = NEW(expertParams); if(!expert) return INIT_FAILED; return(INIT_SUCCEEDED); }
Сохраним полученный код в файле SimpleVolumesExpertSingle.mq5 в текущей папке. Теперь всё готово к тестированию работы риск-менеджера.
Тестирование работы
Возьмём параметры одного из экземпляров торговых стратегий, полученных в процессе оптимизации на предыдущих этапах разработки. Будем называть этот экземпляр торговой стратегии модельной стратегией. Параметры модельной стратегии показаны на рис. 1.
Рис. 1. Параметры модельной стратегии
Запустим одиночный проход тестера с такими параметрами и выключенным риск-менеджером на промежутке 2021-2022 годов. Получим следующие результаты:
Рис. 2. Результаты модельной стратегии без риск-менеджера
На графике видно, что на выбранном временном отрезке случалось несколько заметных просадок по средствам. Наиболее крупные из них встретились в конце октября 2021 года (~$380) и в июне 2022 года (~$840).
Включим теперь риск-менеджер и поставим в его настройках максимальный дневной лимит убытка равный $150, а максимальный общий убыток равный $450. Получим следующие результаты:
Рис. 3. Результаты модельной стратегии с риск-менеджером (макс. убытки: $150 и $450)
На графике видно, что в октябре 2021 риск-менеджер дважды закрывал убыточные рыночные позиции, но виртуальные позиции продолжали оставаться открытыми. Поэтому, при наступлении следующего дня, рыночные позиции вновь открывались. К сожалению, переоткрытие происходило по менее выгодной цене, поэтому суммарная просадка по балансу и средствам немного превысила просадку по средствам в случае выключенного риск-менеджера. Также видно, что после закрытия позиций стратегией вместо получения небольшой прибыли (как в случае без риск-менеджера) был получен некоторый убыток.
В июне 2022 риск-менеджер срабатывал уже семь раз, закрывая рыночные позиции по достижении дневного убытка в $150. Опять получалось, что переоткрытие происходило по менее выгодным ценам, и по итогу этой серии сделок был получен убыток. Но если бы такой советник работал бы на демо-счёте проп-трейдинговой компании с такими параметрами максимально допустимого дневного и общего убытков, то без риск-менеджера счёт был бы остановлен за нарушение правил торговли, а с риск-менеджером счёт продолжил бы свою работу, получив по итогу несколько меньшую прибыль.
Несмотря на то, что мы поставили в качестве значения общего убытка $450, а в июне суммарная просадка по балансу превышала $1000, достижения общего максимального убытка не наступало, так как он отсчитывается от базового баланса. То есть он достигается, если средства опустятся ниже (10000 - 450) = $9550. Но за счёт накопленной ранее прибыли, размер средств в тот период точно не опускался ниже $10000. Поэтому эксперт продолжал свою работу, сопровождаемую открытием рыночных позиций.
Давайте теперь смоделируем срабатывание достижения общего убытка. Для этого увеличим коэффициент масштабирования размеров позиций так, чтобы в октябре 2021 общий максимальный убыток ещё не был бы превышен, а в июне 2022 превышение бы произошло. Поставим значение параметра scale_ = 50 и посмотрим на результат:
Рис. 4. Результаты модельной стратегии с риск-менеджером (макс. убытки: $150 и $450), scale_ = 50
Как видно, в июне 2022 торговля заканчивается — в последующий период советник не открыл ни одной позиции. Это сработало достижение общего лимита убытка ($9550). Также можно отметить, что дневной убыток теперь достигался чаще, то есть он встретился не только в октябре 2021, но и еще в нескольких местах на шкале времени.
Так что оба наших ограничителя работают корректно.
Разработанный риск-менеджер может оказаться полезным даже без использования торговли на счетах проп-трейдинговых компаний. В качестве иллюстрации, попробуем провести оптимизацию параметров риск-менеджера нашей модельной стратегии, пробуя увеличивать размеры открываемых позиций, но при этом не выходя за пределы допустимой просадки в 10%. Для этого в параметрах риск-менеджера мы установим максимальный общий убыток равный 10% от дневного уровня. Максимальный дневной убыток, также рассчитываемый в процентах от дневного уровня мы будем тоже перебирать в ходе оптимизации.
Рис. 5. Результаты оптимизации модельной стратегии с риск-менеджером
Полученные результаты позволяют сказать, что нормированная прибыль за один год при использовании риск-менеджера возросла почти в полтора раза: с $1560 до $2276 (это результат во второй колонке Result). Вот как выглядит лучший проход отдельно:
Рис. 6. Результаты модельной стратегии с риск-менеджером (макс. убытки: 7.6% и 10%, scale_ = 88)
Заметим, что на протяжении всего периода тестирования советник продолжал открывать сделки. Значит, общий лимит в 10% ни разу не был нарушен. Понятно, что применять риск менеджер к отдельным экземплярам торговых стратегий смысла особого нет, так как мы не планируем их запускать на реальном счёте поодиночке. Но то, что работает для одного экземпляра, должно аналогично работать и для советника с большим количеством экземпляров. Поэтому даже такие беглые результаты позволяют сказать, что риск-менеджер определенно может принести пользу.
Заключение
Итак, у нас теперь есть базовая реализация риск-менеджера для торговли, позволяющая соблюдать заданные уровни максимально допустимого дневного и общего убытков. В ней пока ещё не реализована поддержка сохранения и загрузки состояния при перезапуске советника, поэтому использовать её при торговле на реальном счёте нежелательно. Но эта доработка не представляет особых сложностей, мы вернёмся к ней в дальнейшем.
Заодно можно будет попробовать добавить возможности ограничения торговли по различным временным периодам, начиная от исключения торговли в определённые часы определённых дней недели, и заканчивая запретом открытия новых позиций в временные промежутки около моментов выхода важных экономических новостей. Также возможными направлениями развития риск-менеджера является более плавное изменение размеров позиций (например, уменьшение в 2 раза при превышении половины лимита), и более "умное" восстановление объёмов (например, только при превышении убытком того уровня, на котором произошло уменьшение размеров позиций).
Но это мы отложим пока на более позднее время, а пока что вернёмся к дальнейшей автоматизации процесса оптимизации разрабатываемого советника. Первый этап уже был реализован в предыдущей статье, пора приступать ко второму этапу.
Спасибо за внимание, до следующих встреч!
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Спасибо за отзыв!
Наверное, неправильно. Метод DailyLoss() не производит оценку насколько большая случилась просадка. Он только переводит заданный максимальный уровень просадки в валюту счета из процентов при необходимости. Само сравнение происходит в методе CheckDailyLimit():
Значение m_dailyProfit обновляется на каждом тике и вычисляется как разность текущих средств (эквити) и дневного уровня (максимума из значения баланса и средств на начало дневного периода):
Поэтому вроде бы просадка по средствам как раз и учитывается. Или я неправильно понял вопрос?
Для компактности кода. Еще макросы позволяют передать себе в качестве параметра блок кода, а при реализации подобных операций через функции передавать функциям блок кода как параметр нельзя.
Спасибо за отзыв!
Наверное, неправильно. Метод DailyLoss() не производит оценку насколько большая случилась просадка. Он только переводит заданный максимальный уровень просадки в валюту счета из процентов при необходимости. Само сравнение происходит в методе CheckDailyLimit():
Значение m_dailyProfit обновляется на каждом тике и вычисляется как разность текущих средств (эквити) и дневного уровня (максимума из значения баланса и средств на начало дневного периода):
Поэтому вроде бы просадка по средствам как раз и учитывается. Или я неправильно понял вопрос?
Для компактности кода. Еще макросы позволяют передать себе в качестве параметра блок кода, а при реализации подобных операций через функции передавать функциям блок кода как параметр нельзя.
Спасибо большое за расширенный ответ )) Будем ждать новых статей! )
Уважаемый Юрий,
Я пытаюсь скомпилировать код, но получаю следующую ошибку в VirtualRiskManager.mqh:
"Changed - undeclared identifier" в строке CVirtualReceiver::Instance().Changed(); // Уведомление получателя об изменениях
Я проверил код несколько раз, но ничего не вышло. Не могли бы вы объяснить мне, что я упускаю?
С нетерпением жду следующей статьи из этой серии.
Спасибо
Здравствуйте!
Прошу прощения, я забыл прикрепить по крайней мере ещё один файл, в который вносились правки. Начиная с части 16, к каждой статье прикрепляется полный архив файлов проекта. А к этой статье приложу его здесь.