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

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

MetaTrader 5Трейдинг | 9 мая 2024, 10:28
744 2
Aleksandr Seredin
Aleksandr Seredin

Содержание


Введение

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

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

Дополнительные понятия и определения, используемые в данной статье:

High\low – верхнее либо нижнее значение цены инструмента за определенный период времени, ограниченный одним баром или свечой.

Стоп-лосс (stop-loss, стоп) – предельная цена выхода из позиции по убытку. То есть, если цена идёт в сторону противоположную открытой нами позиции, мы ограничиваем потери по открытой позиции за счёт того, что закрываем её раньше, чем убыток превысит рассчитанные нами значения при её открытии.

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

Технический  stop-loss – цена stop-loss, установленная исходя из технического анализа, например, за high/low свечи, излом, фрактал и т.д. в зависимости от применяемой торговой стратегии. Главный отличительный признак данного способа заключается в том, что мы берём размер стопа в пунктах именно с графика по какой-либо формации. В данном случае, точка входа может меняться, а значения цены stop-loss – нет. Это обусловлено тем, что мы принимаем утверждение, что если цена дойдёт до нашего значения stop-loss, то техническая формация будет считаться сломанной, и направление инструмента соответственно неактуальным для дальнейшего нахождения в данной позиции. 

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

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

Слиппэдж(Slippage) – проскальзывание ордера по цене открытия, когда брокер открывает ордер по цене, отличающейся от изначально заявленной. Такая ситуация может возникнуть, если вы торгуете, открывая ордера по рынку. Например, при отправке ордера, объем позиции считается исходя из заложенного риска на сделку в валюте депозита и расчётного\технического stop-loss в пунктах. При этом, после открытия позиции брокером может выясниться, что позиция открылась не по тем ценам, по которым считался стоп в пунктах. Скажем, вместо 100 пунктов он стал 150 на нестабильном рынке. Такие "открытия" нужно мониторить, и если получившийся риск по открытому ордеру стал "намного" (это из параметров риск-менеджера) больше, чем ожидалось, то сделку нужно закрывать досрочно, чтобы не портить статистику торговли, и ждать нового входа.

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

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

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


Класс наследник для алгоритмической торговли

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

Архитектура построения проекта будет строиться из следующих принципов:

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

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

Второй пункт затрагивает базовые принципы построения классов в программировании. В первую очередь мы будем использовать принцип "Open closed Principle", руководствуясь тем, что наш класс будет расширяться, не теряя базовых принципов и подходов к контролю рисков. Каждый отдельный метод, который мы будем добавлять, позволит нам обеспечить принцип единственной ответственности "Single Responsibility Principle" для удобной разработки и логического восприятия кода. Отсюда вытекает принцип, описанный в следующем пункте.

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

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

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

//+------------------------------------------------------------------+
//|                       RiskManagerAlgo                            |
//+------------------------------------------------------------------+
class RiskManagerAlgo : public RiskManagerBase
  {
protected:
   CSymbolInfo       r_symbol;                     // экземпляр
   double            slippfits;                    // на сколько допустим слипедж по открытой сделке
   double            spreadfits;                   // на сколько допустим спрэд относительно открываемого стопа
   double            riskPerDeal;                  // риск на сделку в валюте депозита

public:
                     RiskManagerAlgo(void);        // конструктор
                    ~RiskManagerAlgo(void);        // деструктор

   //---геттеры
   bool              GetRiskTradePermission() {return RiskTradePermission;};


   //---реализация интерфейса
   virtual bool      SlippageCheck() override;  // проверка открытого ордера на проскальзывание
   virtual bool      SpreadMonitor(int intSL) override;           // контроль спрэда
  };
//+------------------------------------------------------------------+

В дополнение к полям и методам уже имеющегося базового класса RiskManagerBase, в нашем классе наследнике RiskManagerAlgo мы предусмотрели следующие элементы для обеспечения дополнительного функционала на алгоритмических советниках. Во-первых, нам будет нужен геттер для получения данных защищенного поля уровня protected класса наследника RiskTradePermission от базового класса RiskManagerBase. Этот метод станет основным способом получения разрешения от риск-менеджера для открытия новых позиций в секции условий выставления ордеров алгоритмически. Принцип работы достаточно прост, если в этой переменной содержится значение true, то советник может продолжать выставлять ордера в соответствии с сигналами своей торговой стратегии, а если false, то выставлять нельзя, даже если торговая стратегия говорит о появлении новой точки входа.

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

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

Из этого получается, что риск сумма всех рисков по всем входам не должна превышать риск на день. Либо, если вход всего один на день, то эти суммы могут быть равны, но такое внутри дня встречается достаточно редко, как правило, входов намного больше. Объявим на глобальном уровне код в следующем виде, для удобства "завернув" его предварительно в именованный блок с помощью ключевого слова group:

input group "RiskManagerAlgoClass"
input double inp_slippfits    = 2.0;  // inp_slippfits - на сколько допустим слипедж по открытой сделке
input double inp_spreadfits   = 2.0;  // inp_spreadfits - на сколько допустим спрэд относительно открываемого стопа
input double inp_risk_per_deal   = 100;  // inp_risk_per_deal - риск на сделку в валюте депозита

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

В публичной секции public нашего класса RiskManagerAlgo объявим для переопределения виртуальные функции нашего интерфейса в следующем виде:

//---реализация интерфейса
   virtual bool      SlippageCheck() override;  // проверка открытого ордера на проскальзывание
   virtual bool      SpreadMonitor(int intSL) override;           // контроль спрэда

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

Инициализацию будем проводить в конструкторе класса наследника RiskManagerAlgo копированием значений, введённых пользователем через переменные входных параметров в соответствующие значения полей, создаваемого экземпляра класса:

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void)
  {
   slippfits   = inp_slippfits;           // скопировали условие слипэджа
   spreadfits  = inp_spreadfits;          // скопировали условие спрэд
   riskPerDeal  = inp_risk_per_deal;      // скопировали условие риска на сделку
  }

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

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void):slippfits(inp_slippfits),
                                       spreadfits(inp_spreadfits),
                                       rispPerDeal(inp_risk_per_deal)
  {

  }

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

//+------------------------------------------------------------------+
//|                         ~RiskManagerAlgo                         |
//+------------------------------------------------------------------+
RiskManagerAlgo::~RiskManagerAlgo(void)
  {

  }

Теперь, когда все необходимые функции объявлены в классе RiskManagerAlgo, перейдём к выбору способа реализации нашего интерфейса для работы с коротким stop-loss по открываемым позициям.


Интерфейс для работы с коротким stop-loss

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

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

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

//+------------------------------------------------------------------+
//|                         CShortStopLoss                           |
//+------------------------------------------------------------------+
class CShortStopLoss
  {
public:
                     CShortStopLoss(void) {};         // класс будет абстрактным, даже если хоть одна функция в нем виртуальна
   virtual          ~CShortStopLoss(void) {};         // деструктор тоже

   virtual bool      SlippageCheck()         = NULL;  // проверка открытого ордера на проскальзывание
   virtual bool      SpreadMonitor(int intSL)= NULL;  // контроль спрэда
  };

С учётом того, что язык программирования mql5 предлагает нам особый тип данных interface для обобщающих сущностей, запись которого будет выглядеть гораздо компактнее и проще, мы будем использовать именно его, так как разницы для нас в функционале не будет. По сути interface, это тоже класс, который не может содержать члены/поля и не может иметь конструктор и/или деструктор. Все объявленные в интерфейсе методы являются чисто виртуальными, даже без явного определения, что и делает его использование более изящным и компактным. Реализация через обобщенную сущность, такую как интерфейс, будет выглядеть следующим образом:

interface IShortStopLoss
  {
   virtual bool   SlippageCheck();           // проверка открытого ордера на проскальзывание
   virtual bool   SpreadMonitor(int intSL);  // контроль спрэда
  };

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


Контроль проскальзывания на отрытых ордерах

В первую очередь для реализации метода SlippageCheck(void) нам нужно будет обновить данные по символу, на котором запущен наш график. Сделаем мы это с помощью метода Refresh() экземпляра нашего класса CSymbolInfo, чтобы обновить все поля, характеризующие инструмент для дальнейшей работы следующей записью:

   r_symbol.Refresh();                                                  // обновили данные по символу

Здесь стоит отметить, что метод Refresh() обновляет все данные полей в классе CSymbolInfo, в отличии от близкого по назначению метода того же класса RefreshRates(void), который обновляет только данные по текущим ценам указанного символа. Метод Refresh() в данной реализации будет вызываться каждый тик, для использования актуальной информации на каждой итерации работы нашего советника.

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

   double PriceClose = 0,                                               // цена закрытия для ордера
          PriceStopLoss = 0,                                            // цена стоп лосса для ордера
          PriceOpen = 0,                                                // цена открытия ордера
          LotsOrder = 0,                                                // объём лота ордера
          ProfitCur = 0;                                                // текущий профит ордера

   ulong  Ticket = 0;                                                   // тикет ордера
   string Symbl;                                                        // инструмент

Для получения данных по стоимости тика при убытке, будем использовать метод TickValueLoss() экземпляра класса CSymbolInfo, объявленного внутри нашего класса RiskManagerAlgo. Получаемое от него значение означает на сколько изменится баланс счёта при изменении цены на один минимальный пункт при стандартном лоте. Это значение мы будем использовать в дальнейшем для расчёта потенциального убытка по фактически открытым ценам позиции. Здесь мы используем понятие "потенциальный", так как данный метод будет работать на каждом тике и сразу же после открытия позиции, точнее, сразу на следующем полученном тике мы сможем проверить, сколько можно потерять на сделке, хотя цена ещё пока находится вблизи цены открытия, чем цены stop-loss. 

   double lot_cost = r_symbol.TickValueLoss();                          // получили цену тика
   bool ticket_sc = 0;                                                  // переменная успешного закрытия

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

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

r_position.SelectByIndex(i)

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

         Symbl = r_position.Symbol();                                   // получили символ
         if(Symbl==Symbol())                                            // проверили тот ли инструмент

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

            PriceStopLoss = r_position.StopLoss();                      // фиксируем его стоплосс
            PriceOpen = r_position.PriceOpen();                         // фиксируем его цену открытия
            ProfitCur = r_position.Profit();                            // фиксируем его фин рез
            LotsOrder = r_position.Volume();                            // фиксируем объём лота ордера
            Ticket = r_position.Ticket();

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

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

            int dir = r_position.Type();                                // определили тип ордера

            if(dir == POSITION_TYPE_BUY)                                // если это бай
              {
               PriceClose = r_symbol.Bid();                             // то цена закрытия бид
              }
            if(dir == POSITION_TYPE_SELL)                               // если это селл
              {
               PriceClose = r_symbol.Ask();                             // то цена закрытия аск
              }

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

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

Размер будем вычислять, как абсолютную разницу между ценой открытия и stop-loss по позиции. Получение абсолютной разницы будем осуществлять с помощью предопределённой функции терминала MathAbs(). Чтобы из дробного значения цены получить целое значение в пунктах, полученное значение MathAbs() разделим на значение одного пункта в дробном значении. Значение одного пункта получим с помощью метода Point() экземпляра нашего стандартного класса терминала CPositionInfo.

int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // смотрим сколько получился стоп

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

double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // считаем какой риск при достижение стопа

Теперь организуем проверку полученного значения на соответствие введённому пользователем отклонению риска по сделке в переменной с именем slippfits. И если вы вышли за границы этого диапазона, то закрываем выбранную позицию:

             if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // если полученный стоп больше, чем риск на сделку с учётом порогового значения
                  ProfitCur<0                                                  &&   // и ордер в убытке
                  PriceStopLoss != 0                                                // если не выставлен стоп лосс, то не косим
               )

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

Во-первых, условие "ProfitCur<0" добавлено для того, чтобы проскальзывание отрабатывалось только в убыточной зоне открытой позиции. Это связано со следующими условиями по торговой стратегии. Так как проскальзывание, как правило, имеет место при высокой волатильности рынка, и сделка открывается, проскальзывая именно в сторону тейк-профита, тем самым увеличивая стоп и сокращая тейк. Это снижает ожидаемую риск/доходность по сделке и увеличивает потенциальный убыток относительно запланированного, но в тоже время и повышает вероятность получения тейка, так как импульс, который "протащил" нашу позицию по слипэджу, вероятнее всего продолжится в моменте. Данное условие говорит о том, что мы будем закрывать позицию, только если импульс нас "протащил" по слипэджу и прекратился раньше, чем достиг тейка, при этом вернувшись в убыточную зону.

Второе условие "PriceStopLoss != 0" необходимо для реализации логики, что если трейдер не выставил stop-loss, то мы НЕ закрываем эту позицию из-за неограниченного риска по ней. Это означает, что открывая позицию, трейдер понимает, что эта позиция в потенциале может покрыть весь его риск на день, если цена пойдёт против него. Это может быть чревато тем, что может не хватить лимитов по всем запланированным к торговле на день инструментам, которые в потенциале могли быть положительны и принести прибыль, а позиция без стопа просто сделала эти входы невозможными. Здесь каждый трейдер решает самостоятельно: включать это условие или нет, исходя из персональной стратегии торговли, но в нашей реализации мы не будем торговать несколько инструментов одновременно, поэтому не будем удалять позицию, если на ней нет цены стопа.

Если все условия, необходимые для идентификации проскальзывания на позиции были выполнены, мы будем закрывать позицию с помощью метода PositionClose() стандартного класса с открытым кодом CTrade, объявленного в нашем базовом классе RiskManagerBase. Входным параметром мы передаём заранее сохраненный номер тикета позиции для закрытия, и результат вызова функции закрытия сохраняем в переменную ticket_sc, для контроля исполнения ордера.

ticket_sc = r_trade.PositionClose(Ticket);                        // закрываем ордер

В общем виде наш метод будет описан в следующем виде:

//+------------------------------------------------------------------+
//|                         SlippageCheck                            |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SlippageCheck(void) override
  {
   r_symbol.Refresh();                                                  // обновили данные по символу

   double PriceClose = 0,                                               // цена закрытия для ордера
          PriceStopLoss = 0,                                            // цена стоп-лосса для ордера
          PriceOpen = 0,                                                // цена открытия ордера
          LotsOrder = 0,                                                // объём лота ордера
          ProfitCur = 0;                                                // текущий профит ордера

   ulong  Ticket = 0;                                                   // тикет ордера
   string Symbl;                                                        // инструмент
   double lot_cost = r_symbol.TickValueLoss();                          // получили цену тика
   bool ticket_sc = 0;                                                  // переменная успешного закрытия

   for(int i = PositionsTotal(); i>=0; i--)                             // начинаем цикл по ордерам
     {
      if(r_position.SelectByIndex(i))
        {
         Symbl = r_position.Symbol();                                   // получили символ
         if(Symbl==Symbol())                                            // проверили тот ли инструмент
           {
            PriceStopLoss = r_position.StopLoss();                      // фиксируем его стоп-лосс
            PriceOpen = r_position.PriceOpen();                         // фиксируем его цену открытия
            ProfitCur = r_position.Profit();                            // фиксируем его фин рез
            LotsOrder = r_position.Volume();                            // фиксируем объём лота ордера
            Ticket = r_position.Ticket();

            int dir = r_position.Type();                                // определили тип ордера

            if(dir == POSITION_TYPE_BUY)                                // если это бай
              {
               PriceClose = r_symbol.Bid();                             // то цена закрытия бид
              }
            if(dir == POSITION_TYPE_SELL)                               // если это селл
              {
               PriceClose = r_symbol.Ask();                             // то цена закрытия аск
              }

            if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)
              {
               int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // смотрим сколько получился стоп

               double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // считаем какой риск при достижение стопа

               if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // если полученный стоп больше, чем риск на сделку с учётом порогового значения
                  ProfitCur<0                                                  &&   // и ордер в убытке
                  PriceStopLoss != 0                                                // если не выставлен стоп-лосс, то косим
               )
                 {
                  ticket_sc = r_trade.PositionClose(Ticket);                        // закрываем ордер

                  Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal));                  //
                  Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits));                      //
                  Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal));  //
                  Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder));                      //
                  Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord));                 //

                  if(!ticket_sc)
                    {
                     Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); // информируем в журнал
                    }
                  else
                    {
                     Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // информируем в журнал
                    }
                  continue;
                 }
              }
           }
        }
     }
   return(ticket_sc);
  }
//+------------------------------------------------------------------+

На этом переопределение метода контроля проскальзывания завершено, и мы переходим к описанию метода контроля размера спреда перед тем, как открывать новую позицию.


Контроль спреда для открытия позиции

Контроль спреда в нашей реализации метода SpreadMonitor() будет заключаться в предварительном сравнении текущего спреда прямо перед открытием сделки с расчётным/техническим стопом, передаваемым как параметр метода в целочисленном значении. Функция будет возвращать true в случае, если размер текущего спреда находится в допустимом пользователем диапазоне, при этом, если размер спреда превышает этот диапазон, то метод вернёт false.

Результат работы функции будет храниться в логической переменной типа данных bool, по умолчанию инициализированной true:

   bool SpreadAllowed = true;

Значение текущего спреда по инструменту будем получать с помощью метода Spread() класса CSymbolInfo в следующем виде:

   int SpreadCurrent = r_symbol.Spread();

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

if(SpreadCurrent>intSL*spreadfits)

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

//+------------------------------------------------------------------+
//|                          SpreadMonitor                           |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SpreadMonitor(int intSL)
  {
//---контроль спрэда
   bool SpreadAllowed = true;                                           // разрешаем торговлю по спрэду и проверяем соотношение далее
   int SpreadCurrent = r_symbol.Spread();                               // текущие показатели спрэда

   if(SpreadCurrent>intSL*spreadfits)                                   // если текущий спрэд больше чем стоп и коэффициент
     {
      SpreadAllowed = false;                                            // запрещаем торговлю
      Print(__FUNCTION__+IntegerToString(__LINE__)+
            ". Spread is to high! Spread:"+
            IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));// проинформировали
     }
   return SpreadAllowed;                                                // вернули что получилось
  }
//+------------------------------------------------------------------+

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


Реализация интерфейса

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

Для этого нам нужно дополнить наш базовый класс RiskManagerBase наследованием от описанного ранее интерфейса IShortStopLoss в следующем виде:

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
class RiskManagerBase:IShortStopLoss                        // суть класса - контролировать риски по терминалу

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

Итоговая структура наследования нашего пользовательского класса RiskManagerAlgo с отображением инкапсуляции публичных методов для обеспечения полного функционала изображена на Рисунке 1.

Рисунок 1. Иерархия структуры наследования класса RiskManagerAlgo

Рисунок 1. Иерархия структуры наследования класса RiskManagerAlgo

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


Реализация торгового блока

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

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

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

ENUM_TIMEFRAMES   TF;                     // используемый таймфрейм

Далее объявим переменную указатель стандартного класса терминала с открытым кодом для работы с техническим индикатором CiFractals, в котором удобно реализованы все необходимые нам функции, и нам не придётся заново всё это писать:

   CiFractals        *cFractals;             // фракталы

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

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+

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

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

   TradeInputs       fract_Up, fract_Dn;     // текущий сигнал

Нам осталось лишь объявить переменные, которые будут хранить текущие значения, получаемые от класса CiFractals для получения данных о новых сформированных фракталах на дневном графике.

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

Метод для контроля обновления состояния данных класса мы назовём Process(). Он не будет ничего возвращать и не будет принимать никаких параметров, а будет просто выполнять обновление состояния данных при каждом поступающем тике. Методы для получения сигнала на покупку и продажу будут называться BuySignal() и SellSignal(). Они не будут принимать никаких параметров, но будут возвращать значение типа bool, в случае необходимости открытия позиции в соответствующем направлении. Методы BuyDone() и SellDone() нужно будет вызывать после проверки ответа сервера брокера об успешном открытии соответствующей позиции. В общем виде описание нашего класса будет выглядеть следующим образом:

//+------------------------------------------------------------------+
//|                       CFractalsSignal                            |
//+------------------------------------------------------------------+
class CFractalsSignal
  {
protected:
   ENUM_TIMEFRAMES   TF;                     // используемый таймфрейм
   CiFractals        *cFractals;             // фракталы

   TradeInputs       fract_Up, fract_Dn;     // текущий сигнал

   double            FrUp;                   // верхние фракталы
   double            FrDn;                   // нижние фракталы

public:
                     CFractalsSignal(void);  // конструктор
                    ~CFractalsSignal(void);  // деструктор

   void              Process();              // метод запуска обновлений

   bool              BuySignal();            // сигнал на покупку
   bool              SellSignal();           // сигнал на продажу

   void              BuyDone();              // покупка отработана
   void              SellDone();             // продажа отработана
  };

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

//+------------------------------------------------------------------+
//|                        CFractalsSignal                           |
//+------------------------------------------------------------------+
CFractalsSignal::CFractalsSignal(void)
  {
   TF  =  PERIOD_D1;                                                    // используемый таймфрейм

//---класс фрактала
   cFractals=new CiFractals();                                          // создали экземпляр фрактала
   if(CheckPointer(cFractals)==POINTER_INVALID ||                       // если не создался экземпляр, ИЛИ
      !cFractals.Create(Symbol(),TF))                                   // не создан вариант
      Print("INIT_FAILED");                                             // дальше не идём
   cFractals.BufferResize(4);                                           // ресайзнули буфер фракталов
   cFractals.Refresh();                                                 // обновили

//---
   FrUp = EMPTY_VALUE;                                                  // прировняли при старте верхний
   FrDn = EMPTY_VALUE;                                                  // прировняли при старте нижний

   fract_Up.done  = true;                                               //
   fract_Up.price = EMPTY_VALUE;                                        //

   fract_Dn.done  = true;                                               //
   fract_Dn.price = EMPTY_VALUE;                                        //
  }

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

//+------------------------------------------------------------------+
//|                        ~CFractalsSignal                          |
//+------------------------------------------------------------------+
CFractalsSignal::~CFractalsSignal(void)
  {
//---
   if(CheckPointer(cFractals)!=POINTER_INVALID)                         // если экземпляр создан, то
      delete cFractals;                                                 // удаляем
  }

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

//+------------------------------------------------------------------+
//|                         Process                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::Process(void)
  {
//---
   cFractals.Refresh();                                                 // обновили фракталы
  }

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

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

   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // цена покупки

Далее нам необходимо будет запросить актуальное значение именно верхнего фрактала через метод Upper() экземпляра класса CiFractals, с передачей в качестве параметра индекса необходимого нам бара в таймсерии:

   FrUp=cFractals.Upper(3);                                             // запрашиваем актуальное значение

В этот метод мы передаём значение `3` именно потому, что мы будем использовать только окончательно сформированные фрактальные изломы. Так как в таймсериях отсчет буфера идёт от самых свежих с `0`– до самых поздних в сторону увеличения, значение "три" на дневном графике означает день до позавчера, чтобы исключить фрактальные изломы, которые могут сформироваться на дневке в определённый момент, а потом цена в тот же день переписывает high/low, и фрактальный уровень пропадает.

Теперь сделаем логическую проверку на обновление актуального фрактального излома, если цена последнего актуального излома на дневном графике изменилась. Для этого сравним текущее значение индикатора фрактала, обновленного выше в переменной FrUp, с последним актуальным значением верхнего фрактала, хранящегося в поле price нашей пользовательской структуры TradeInputs. Для того, чтобы поле price всегда хранило значение последней актуальной цены и не "обнулялось" при отсутствии данных возвращаемых индикатором, если излом не обнаружен, добавим еще одну проверку на пустое значение индикатора FrUp != EMPTY_VALUE. Комбинация из этих двух условий позволит нам обновлять только значимые значения цены последнего фрактала (отличные от нуля, которые в индикаторе соответствуют значению EMPTY_VALUE) и не переписывать эту переменную на пустое значение. В общем виде такие проверки будут выглядеть следующим образом:

   if(FrUp != fract_Up.price           &&                               // если данные обновились
      FrUp != EMPTY_VALUE)                                              // нулевое не смотрим

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

   if(fract_Up.price != EMPTY_VALUE    &&                               // нулевые не берём
      ask            >= fract_Up.price &&                               // если цена покупки больше или равна фракталу
      fract_Up.done  == false)                                          // сигнал ещё не отрабатывался
     {
      return true;                                                      // даём сигнал на отработку
     }

В этом блоке также проверка начинается с наличия нулевого значения переменной последнего актуального фрактала fract_Up в поле price, на случай первичного старта советника после первичной инициализации данной переменной в конструкторе класса. Следующим условием проверяется, пробила ли текущая  цена покупки на рынке последнее актуальное значение фрактального излома с дневки в виде ask >= fract_Up.price, можно сказать, это главное логическое условие этого метода. В завершение нам нужно проверить текущий сигнал на условие, был ли данный фрактальный уровень уже отработан. Смысл здесь в том, что сигналы по фрактальному излому приходят с дневного графика, и если текущая цена покупки на рынке достигла нужного нам значения, мы должны отработать этот сигнал один раз в день, так как торговля у нас хоть и внутридневная, но позиционная, без добора одновременно открытых позиций. При одновременном выполнении всех трех условий, наш метод должен возвращать true для отработки этого сигнала нашим советником.

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

//+------------------------------------------------------------------+
//|                         BuySignal                                |
//+------------------------------------------------------------------+
bool CFractalsSignal::BuySignal(void)
  {
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // цена покупки

//---проверяем обновление фракталов
   FrUp=cFractals.Upper(3);                                             // запрашиваем актуальное значение

   if(FrUp != fract_Up.price           &&                               // если данные обновились
      FrUp != EMPTY_VALUE)                                              // нулевое не смотрим
     {
      fract_Up.price = FrUp;                                            // учитываем новый фрактал
      fract_Up.done = false;                                            // не отработан
     }

//---проверяем сигнал
   if(fract_Up.price != EMPTY_VALUE    &&                               // нулевые не берём
      ask            >= fract_Up.price &&                               // если цена покупки больше или равна фракталу
      fract_Up.done  == false)                                          // сигнал ещё не отрабатывался
     {
      return true;                                                      // даём сигнал на отработку
     }

   return false;                                                        // иначе фолс
  }

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

//+------------------------------------------------------------------+
//|                         BuyDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::BuyDone(void)
  {
   fract_Up.done = true;                                                // отработано
  }

Логика очень проста, при вызове этого публичного метода мы будем ставить флаг успешной отработки сигнала в поле соответствующего последнего сигнала экземпляра структуры fract_Up в поле done. Соответственно и вызываться данный метод по коду основного советника будет только тогда, когда пройдёт проверка на успешное открытие ордера от сервера брокера.

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

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

//+------------------------------------------------------------------+
//|                         SellSignal                               |
//+------------------------------------------------------------------+
bool CFractalsSignal::SellSignal(void)
  {
   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // цена покупки

//---проверяем обновление фракталов
   FrDn=cFractals.Lower(3);                                             // запрашиваем актуальное значение

   if(FrDn != EMPTY_VALUE        &&                                     // нулевое не смотрим
      FrDn != fract_Dn.price)                                           // если данные обновились
     {
      fract_Dn.price = FrDn;                                            // учитываем новый фрактал
      fract_Dn.done = false;                                            // не отработан
     }

//---проверяем сигнал
   if(fract_Dn.price != EMPTY_VALUE    &&                               // нулевое не смотрим
      bid            <= fract_Dn.price &&                               // если цена покупки больше или равна фракталу И
      fract_Dn.done  == false)                                          // сигнал не отработан
     {
      return true;                                                      // даём сигнал на отработку
     }

   return false;                                                        // иначе фолс
  }

Отработка сигнала на продажу также аналогична логике как и на покупку, просто поле done будет уже заполняться по экземпляру структуры fract_Dn, которая отвечает за последний актуальный фрактал на продажу:

//+------------------------------------------------------------------+
//|                        SellDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::SellDone(void)
  {
   fract_Dn.done = true;                                                // отработано
  }
//+------------------------------------------------------------------+

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


Сборка и тестирование проекта

Сборку проекта начнём, подключив все описанные выше файлы, включая необходимый код в начале основного файла точки входа проекта, с помощью препроцессорной команды #include. Файлы <RiskManagerAlgo.mqh>, <TradeModel.mqh> и <CFractalsSignal.mqh> это наши пользовательские классы, о которых шла речь в предыдущих главах. Оставшиеся две записи <Indicators\BillWilliams.mqh> и <Trade\Trade.mqh> – это стандартные классы терминала с открытым кодом для работы с фракталами и торговыми операциями соответственно.

#include <RiskManagerAlgo.mqh>
#include <Indicators\BillWilliams.mqh>
#include <Trade\Trade.mqh>
#include <TradeModel.mqh>
#include <CFractalsSignal.mqh>

Для настройки метода контроля проскальзывания, введём ещё одну дополнительную целочисленную переменную типа int класса памяти input для того, чтобы пользователь мог самостоятельно вводить допустимое значение стопа в размере минимальных пунктов по инструменту:

input group "RiskManagerAlgoExpert"
input int inp_sl_in_int       = 2000;  // inp_sl_in_int - какой стоп по отдельной сделке

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

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

RiskManagerAlgo *RMA;                                                   // риск-менеджер
CTrade          *cTrade;                                                // трэйд
CFractalsSignal *cFract;                                                // фрактал

Инициализировать указатели будем в функции обработчике события инициализации нашего советника OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   RMA = new RiskManagerAlgo();                                         // алго риск менеджер

//---
   cFract =new CFractalsSignal();                                       // фрактальный сигнал

//---класс трэйда
   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);                                          // асинхронный метод

//---
   return(INIT_SUCCEEDED);
  }

При настройке объекта CTrade укажем в методе SetTypeFillingBySymbol() в качестве параметра символ текущего инструмента, на котором будет запущен советник. Возврат символа текущего инструмента, на котором запущен советник, будем осуществлять предопределённым методом терминала Symbol(). В качестве максимально приемлемого отклонения от запрашиваемой цены в методе SetDeviationInPoints() укажем значение с запасом. Так как этот параметр для нашего исследования не так важен, мы не будем выносить его во входные параметры, а оставим его хардкодом. Также пропишем статично номер мэджика для всех открываемых позиций на советнике.

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // если есть экземпляр, ТО
     {
      delete cTrade;                                                    // удаляем
     }

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

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

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

   RMA.ContoMonitor();                                                  // запускаем риск менеджера

Следующим методом вызываем контроль слипэджа SlippageCheck(), который, как уже говорилось ранее, в этой реализации будет отрабатывать каждый новый тик и проверять открытые позиции на соответствие плановым значениям риска и их фактическому значению относительно установленного стоп-лосса:

   RMA.SlippageCheck();                                                 // проверка позиций на проскальзывание

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

Далее нам нужно обновить данные индикатора фрактальных изломов через наш пользовательский класс CFractalsSignal с помощью публичного метода Process() следующей записью:

   cFract.Process();                                                    // запускаем процесс фракталов

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

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // если есть сигнал на покупку

В первую очередь мы проверяем наличие сигнала на покупку через метод BuySignal() экземпляра класса CFractalsSignal, и если он даёт этот сигнал, то мы проверяем одновременное наличие разрешения от риск-менеджера на соответствие спреда разрешенному пользователем значению через метод SpreadMonitor(). В качестве единственного параметра в метод SpreadMonitor() передаем входной пользовательский параметр inp_sl_in_int.

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

      if(cTrade.Buy(0.1))                                               // если купили, то
        {
         cFract.BuyDone();                                              // сигнал отработан
         Print("Buy has been done");                                    // информируем
        }
      else                                                              // если не купили, то
        {
         Print("Error: buy");                                           // информируем
        }

Выставляем ордер с помощью метода Buy() экземпляра класса CTrade, передав в качестве параметра значение лота равное 0.1. Для более объективной оценки работы риск-менеджера мы не будем менять данное значение, чтобы "сгладить" статистику по параметру объема. Это значит, что все входы будут иметь одинаковое весовое значение в статистике работы нашего советника.

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

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

Блок закрытия ордеров в конце торгового дня возьмем без изменений из прошлой статьи в том же виде:

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

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

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

Также не забудем вывести на экран актуальное состояние данных риск-менеджера через предопределённую функцию терминала Comment(), передав в неё метод Message() класса риск-менеджера:

   Comment(RMA.Message());                                              // выводим состояние данных на монитор
В итоговом виде код отработки события нового тика будет выглядеть следующим образом:
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMA.ContoMonitor();                                                  // запускаем риск менеджера

   RMA.SlippageCheck();                                                 // проверка позиций на проскальзывание

   cFract.Process();                                                    // запускаем процесс фракталов

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // если есть сигнал на покупку
     {
      if(cTrade.Buy(0.1))                                               // если купили, то
        {
         cFract.BuyDone();                                              // сигнал отработан
         Print("Buy has been done");                                    // информируем
        }
      else                                                              // если не купили, то
        {
         Print("Error: buy");                                           // информируем
        }
     }

   if(cFract.SellSignal())                                              // если есть сигнал на продажу
     {
      if(cTrade.Sell(0.1))                                              // если продали, то
        {
         cFract.SellDone();                                             // сигнал отработан
         Print("Sell has been done");                                   // информируем
        }
      else                                                              // если не продали, то
        {
         Print("Error: sell");                                          // информируем
        }
     }

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

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

   Comment(RMA.Message());                                              // выводим состояние данных на монитор
  }
//+------------------------------------------------------------------+

Теперь можно собирать проект и протестировать на исторических данных. Для примера тестирования возьмем пару USDJPY и протестируем её в течение 2023 года на оптимизаторе стратегий со следующими входными настройками (см. Таблица 2):

№ п/п Наименование настройки Значение настройки
 1  Советник  RiskManagerAlgo.ex5
 2  Символ  USDJPY
 3  Период графика  M15
 4  Интервал  2023.01.01 - 2023.12.31
 5  Форвард  нет
 6  Задержки  Без задержек, идеальное исполнение
 7  Моделирование  Все тики
 8  Начальный депозит  10 000 usd
 9  Плечо  1:100
 10  Оптимизация  Медленная (полный перебор параметров) 

Таблица 1. Настройки тестера стратегий для советника RiskManagerAlgo

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

№ п/п
Наименование параметра Старт Шаг Стоп
 1  inp_riskperday  0.1  0.5  1
 2  inp_riskperweek  0.5  0.5  3
 3  inp_riskpermonth  2  1  8
 4  inp_plandayprofit  0.1  0.5  3
 5  dayProfitControl  false  -  true

Таблица 2 Параметры оптимизатора стратегий для советника RiskManagerAlgo

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

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

Рисунок 2. График с результатами оптимизации советника RiskManagerAlgo

Рисунок 2. График с результатами оптимизации советника RiskManagerAlgo

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

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

Рисунок 3. Диаграмма зависимости параметров дневного риска и плановой дневной прибыли

Рисунок 3. Диаграмма зависимости параметров дневного риска и плановой дневной прибыли

На Рисунке 3 виден излом величины параметра риска на день, при котором эффективность стратегии пробоев фракталов сначала увеличивается, при увеличении этой величины, а потом начинает снижаться. Это и будет являться точкой экстрема (излом функции) нашей модели по двум этим параметрам. В определённый момент и само наличие такого момента в модели, когда увеличение значения параметра риска на день начинает не повышать профит, а снижать его, и доказывает то, что рыночный импульс становится меньше относительно рисков, которые мы заложили в нашу модель. На лицо явное превышение стоимости риска относительно ожидаемой прибыли. Данный излом хорошо виден на графике и не требует дополнительных математических расчётов для вычисления экстрема, таких как производные от функций.

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

Излом величины риска на день очевидно будет являться оптимальной точкой оптимизации по лучшему проходу со следующими параметрами, представленными в Таблице 3, что и показал нам оптимизатор:

№ п/п
Наименование параметра Значение параметра
 1  inp_riskperday  0.6
 2  inp_riskperweek  3
 3  inp_riskpermonth  8
 4  inp_plandayprofit  3.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

Таблица 3 Параметры лучшего прохода оптимизатора стратегий для советника RiskManagerAlgo

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

График прогона лучшей итерации представлен на Рисунке 4.

Рисунок 4. График прогона лучшей итерации оптимизатора стратегии

Рисунок 4. График прогона лучшей итерации оптимизатора стратегии

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

Оценить риск доходность по худшему проходу со следующими параметрами можно по данным Таблицы 4:

№ п/п
Наименование параметра
Значение параметра
 1  inp_riskperday  1.1
 2  inp_riskperweek  0.5
 3  inp_riskpermonth  2
 4  inp_plandayprofit  0.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

Таблица 4 Параметры лучшего прохода оптимизатора стратегий для советника RiskManagerAlgo

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

График прогона худшей итерации представлен на Рисунке 5:

Рисунок 5. График прогона худшей итерации оптимизатора стратегии

Рисунок 5. График прогона худшей итерации оптимизатора стратегии

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


Заключение

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

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

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

Буду рад обратной связи в комментариях к этой статье, в особенности по темам, которые ещё могут заинтересовать вас. Попутного тренда, друзья!


Прикрепленные файлы |
IShortStopLoss.mqh (1.24 KB)
RiskManagerAlgo.mq5 (10.61 KB)
RiskManagerAlgo.mqh (16.87 KB)
RiskManagerBase.mqh (61.88 KB)
TradeModel.mqh (13.18 KB)
CFractalsSignal.mqh (13.97 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Yevgeniy Koshtenko
Yevgeniy Koshtenko | 9 мая 2024 в 12:34
Топ. Все мы думаем, как заработать. Профи думают, как не потерять
Aleksandr Seredin
Aleksandr Seredin | 9 мая 2024 в 17:57
Yevgeniy Koshtenko #:
Топ. Все мы думаем, как заработать. Профи думают, как не потерять

Хорошая фраза) Возьму на заметку))

Модифицированный советник Grid-Hedge в MQL5 (Часть II): Создание простого сеточного советника Модифицированный советник Grid-Hedge в MQL5 (Часть II): Создание простого сеточного советника
В статье рассматривается классическая сеточная стратегия, подробно описана ее автоматизация с помощью советника на MQL5 и проанализированы первоначальные результаты тестирования на истории. Также подчеркивается необходимость в долгом удержании позиций и рассматривается возможность оптимизации ключевых параметров (таких как расстояние, тейк-профит и размеры лотов) в будущих частях. Целью этой серии статей является повышение эффективности торговой стратегии и ее адаптируемости к различным рыночным условиям.
Разрабатываем мультивалютный советник (Часть 10): Создание объектов из строки Разрабатываем мультивалютный советник (Часть 10): Создание объектов из строки
План разработки советника предусматривает несколько этапов с сохранением промежуточных результатов в базе данных. Заново достать их оттуда можно только в виде строк или чисел, а не объектов. Поэтому нам нужен способ воссоздания в советнике нужных объектов из строк, прочитанных из базы данных.
Прогнозирование на основе глубокого обучения и открытие ордеров с помощью пакета MetaTrader 5 python и файла модели ONNX Прогнозирование на основе глубокого обучения и открытие ордеров с помощью пакета MetaTrader 5 python и файла модели ONNX
Проект предполагает использование Python для прогнозирования на финансовых рынках на основе глубокого обучения. Мы изучим тонкости тестирования производительности модели с использованием таких ключевых показателей, как средняя абсолютная ошибка (MAE), средняя квадратичная ошибка (MSE) и R-квадрат (R2), а также научимся объединять это всё в исполняемом файле. Мы также создадим файл модели ONNX и советник.
Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA) Алгоритм эволюции панциря черепахи (Turtle Shell Evolution Algorithm, TSEA)
Уникальный алгоритм оптимизации, вдохновленный эволюцией панциря черепахи. Алгоритм TSEA эмулирует постепенное формирование ороговевших участков кожи, которые представляют собой оптимальные решения задачи. Лучшие решения становятся более "твердыми" и располагаются ближе к внешней поверхности, в то время как менее удачные решения остаются "мягкими" и находятся внутри. Алгоритм использует кластеризацию решений по качеству и расстоянию, позволяя сохранять менее успешные варианты и обеспечивая гибкость и адаптивность.