preview
Разрабатываем мультивалютный советник (Часть 24): Подключаем новую стратегию (I)

Разрабатываем мультивалютный советник (Часть 24): Подключаем новую стратегию (I)

MetaTrader 5Тестер | 4 марта 2025, 09:02
489 0
Yuriy Bykov
Yuriy Bykov

Введение

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

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

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

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

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


Намечаем путь

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

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

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

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

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

Приступим!


Стратегия SimpleCandles

Создадим в папке MQL5/Experts новую папку для проекта. Назовём её, например, Article.17277. Наверное сразу стоит сделать оговорку для избежания путаницы в дальнейшем. Мы будем использовать понятие "проект" в двух смыслах. В одном случае, будет иметься в виду просто папка с файлами советников, которые будут использоваться для автоматической оптимизации торговой стратегии определённого. В коде этих советников будут использоваться включаемые файлы из библиотеки Advisor. То есть в этом контексте проект — это просто рабочая папка в папке экспертов терминала. В другом случае, слово "проект" будет означать созданную в базе данных оптимизации структуру данных, описывающую задачи оптимизации, которые должны выполняться в автоматическом режиме для получения результатов, используемых потом в итоговом советнике, предназначенном для работы на торговом счёте. В таком контексте проект — это, по сути, начинка базы данных оптимизации, до начала самой оптимизации.

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

Повторим путь, проделанный в части 1 при разработке торговой стратегии SimpleVolumes. Начнём также с формулировки торговой идеи.  

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

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

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

Хорошо, с моментами открытия позиций как-то определились, а что с закрытием? Используем самый простой вариант: при открытии позиции будут выставляться уровни StopLoss и TakeProfit, на которых позиция будет закрываться.

Теперь мы можем дать такое описание нашей стратегии:

Сигналом к открытию позиции будет ситуация, когда в момент начала нового бара (свечи) все из нескольких предыдущих свечей были направлены в одну сторону (вверх или вниз). Если свечи были направлены вверх, то открываем позицию SELL. Иначе открываем позицию BUY. 

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

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

Вот какие вопросы возникли:

  • "... из нескольких предыдущих свечей ..." — Несколько свечей  — это сколько?
  • "... можно открывать дополнительные позиции... " — Сколько всего может быть открыто позиций?
  • "... имеет уровни StopLoss и TakeProfit ..."  — Как значения использовать для них, как их вычислять?

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

Сколько всего может быть открыто позиций? Это тоже можно сделать параметром стратегии и подобрать наилучшие значения в процессе оптимизации. 

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

Как найти это самое значение волатильности? Способов это сделать довольно много. Можно, например, воспользоваться готовым индикатором волатильности ATR (Average True Range) или придумать и реализовать какой-то свой способ расчёта величины волатильности. Но скорее всего, одними из параметров в таких расчётах могут быть количество периодов, на которых рассматриваются размах колебаний цены торгового инструмента и величина одного периода. Если мы добавим эти величины в параметры стратегии, то можно будет на их основе производить расчёт волатильности. 

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

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

Советник запускается на определенном символе и периоде (таймфрейме)

Задаем входные параметры:

  • Символ
  • Таймфрейм для подсчёта однонаправленных свечей
  • Количество однонаправленных свечей (signalSeqLen)
  • Период ATR (periodATR)
  • Stop Loss (в пунктах или % ATR) (stopLevel)
  • Take Profit (в пунктах или % ATR) (takeLevel)
  • Максимальное количество одновременно отрытых позиций (maxCountOfOrders)
  • Размер позиций

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

Если направления одинаковые, и количество открытых позиций меньше maxCountOfOrders, то:

  • Вычисляем StopLoss и TakeProfit. Если periodATR = 0, то просто отступаем от текущей цены на количество пунктов, взятых из параметров stopLevel и takeLevel. Если periodATR > 0, то рассчитываем величину ATR, используя параметр periodATR для дневного таймфрейма. От текущей цены отступаем на величины ATR * stopLevel и ATR * takeLevel.

  • Открываем позицию SELL, если направления свечей были вверх и позицию BUY, если направления свечей были вниз. При открытии устанавливаем рассчитанные ранее уровни StopLoss и TakeProfit.

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

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


Реализация стратегии

Воспользуемся имеющимся классом CSimpleVolumesStrategy и создадим по его образу класс CSimpleCandlesStrategy. Его надо объявить наследником класса CVirtualStrategy. Перечислим необходимые параметры стратегии в виде полей класса и будем иметь в виду, что наш новый класс наследует ещё некоторые поля и методы от своих предков.

//+------------------------------------------------------------------+
//| Торговая стратегия c использованием однонаправленных свечей      |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Символ (торговый инструмент)
   ENUM_TIMEFRAMES   m_timeframe;         // Период графика (таймфрейм)

   //---  Параметры сигнала к открытию
   int               m_signalSeqLen;      // Количество однонаправленных свечей
   int               m_periodATR;         // Период ATR

   //---  Параметры позиций
   double            m_stopLevel;         // Stop Loss (в пунктах или % ATR)
   double            m_takeLevel;         // Take Profit (в пунктах или % ATR)

   //---  Параметры управление капиталом
   int               m_maxCountOfOrders;  // Макс. количество одновременно отрытых позиций

   CSymbolInfo       *m_symbolInfo;       // Объект для получения информации о свойствах символа

  // ...   

public:
   // Конструктор
                     CSimpleCandlesStrategy(string p_params);
   
   virtual string    operator~() override;   // Преобразование объекта в строку
   virtual void      Tick() override;        // Обработчик события OnTick
};

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

Класс нашей новой торговой стратегии является потомком и класса CFactorable. Таким образом, мы сможем в новом классе реализовать конструктор, который будет читать значения параметров из строки инициализации, используя методы чтения, реализованные в классе CFactorable. Если при чтении не возникло ошибок, то метод IsValid() будет возвращать истину.

Для работы с виртуальными позициями в предке CVirtualStrategy объявлен массив m_orders, который предназначен для хранения указателей на объекты класса CVirtualOrder, то есть виртуальных позиций. Поэтому, в конструкторе мы попросим создать столько экземпляров объектов виртуальных позиций, сколько указано в параметре m_maxCountOfOrders, и поместить их в наш массив m_orders. Выполнять эту работу будет статический метод CVirtualReceiver::Get().

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

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

Полный код конструктора будет выглядеть так:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) {
   // Читаем параметры из строки инициализации
   m_params = p_params;
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalSeqLen = (int) ReadLong(p_params);
   m_periodATR = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);

   if(IsValid()) {
      // Запрашиваем нужное количество объектов для виртуальных позиций
      CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders);

      // Добавляем отслеживание нового бара на нужном таймфрейме
      IsNewBar(m_symbol, m_timeframe);
      
      // Создаём информационный объект для нужного символа
      m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol];
   }
}

Далее нам надо реализовать абстрактный виртуальный оператор тильда (~), который возвращает строку инициализации объекта стратегии. Реализация его стандартная:

//+------------------------------------------------------------------+
//| Преобразование объекта в строку                                  |
//+------------------------------------------------------------------+
string CSimpleCandlesStrategy::operator~() {
   return StringFormat("%s(%s)", typename(this), m_params);
}

Ещё один обязательный виртуальный метод, который надо реализовать, — это метод обработки тика Tick(). В нём мы проверяем наступление нового бара и то, что количество открытых позиций ещё не достигло максимальной величины. Если эти условия выполнены, то проверяем наличие сигнала на открытие. Если сигнал есть, то открываем позицию соответствующего направления. Остальные методы, которые мы добавим к классу, играют вспомогательную роль. 

//+------------------------------------------------------------------+
//| "Tick" event handler function                                    |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::Tick() override {
// Если наступил новый бар по заданному символу и таймфрейму
   if(IsNewBar(m_symbol, m_timeframe)) {
// Если количество открытых позиций меньше допустимого
      if(m_ordersTotal < m_maxCountOfOrders) {
         // Получаем сигнал на открытие
         int signal = SignalForOpen();

         if(signal == 1) {          // Если сигнал на покупку, то
            OpenBuy();              // открываем позицию BUY
         } else if(signal == -1) {  // Если сигнал на продажу, то
            OpenSell();             // открываем позицию SELL_STOP
         }
      }
   }
}

Проверку наличия сигнала открытия мы вынесли в отдельный метод SignalForOpen(). В нём мы получаем массив котировок предыдущих свечей и проверяем по очереди, не являются ли все они направленными вниз или вверх:

//+------------------------------------------------------------------+
//| Сигнал для открытия отложенных ордеров                           |
//+------------------------------------------------------------------+
int CSimpleCandlesStrategy::SignalForOpen() {
// По-умолчанию сигнала на открытие нет
   int signal = 0;

   MqlRates rates[];
// Копируем значения котировок (свечей) в массив-приёмник
   int res = CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates);

// Если скопировалось нужное количество свечей
   if(res == m_signalSeqLen) {
      signal = 1; // сигнал на покупку

      // Перебираем все свечи
      for(int i = 0; i < m_signalSeqLen; i++) {
         // Если встречается хоть одна свеча вверх, то отменяем сигнал
         if(rates[i].open < rates[i].close ) {
            signal = 0;
            break;
         }
      }

      if(signal == 0) {
         signal = -1; // иначе - сигнал на продажу

         // Перебираем все свечи
         for(int i = 0; i < m_signalSeqLen; i++) {
            // Если встречается хоть одна свеча вниз, то отменяем сигнал
            if(rates[i].open > rates[i].close ) {
               signal = 0;
               break;
            }
         }
      }

   }

   return signal;
}

За открытие позиций отвечают созданные методы OpenBuy() и OpenSell(). Поскольку они очень похожи, то приведём код только одного из них. Ключевыми моментами в этом методе являются вызов метода обновления уровней StopLoss и TakeProfit, который обновляет значения двух соответствующих полей класса m_sl и m_tp, а также вызов метода открытия первой попавшейся неоткрытой виртуальной позиции из массива m_orders.

//+------------------------------------------------------------------+
//| Открытие ордера BUY                                              |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::OpenBuy() {
// Берем необходимую нам информацию о символе и ценах
   double point = m_symbolInfo.Point();
   int digits = m_symbolInfo.Digits();

// Цена открытия
   double price = m_symbolInfo.Ask();

// Обновим уровни SL и TP, рассчитав ATR
   UpdateLevels();

// Уровни StopLoss и TakeProfit
   double sl = NormalizeDouble(price - m_sl * point, digits);
   double tp = NormalizeDouble(price + m_tp * point, digits);

   bool res = false;
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Перебираем все виртуальные позиции
      if(!m_orders[i].IsOpen()) {                  // Если нашли не открытую, то открываем
         // Открытие виртуальной позиции SELL
         res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                0,
                                NormalizeDouble(sl, digits),
                                NormalizeDouble(tp, digits));

         break; // и выходим
      }
   }

   if(!res) {
      PrintFormat(__FUNCTION__" | ERROR opening BUY virtual order", 0);
   }
}

Метод обновления уровней сначала проверяет, задано ли какое-то ненулевое значение периода расчёта ATR. Если да, то вызывается функция расчёта ATR. Её результат попадает в переменную channelWidth. Когда значение периода равно 0, то в эту переменную присваивается 1. В этом случае, значения из входных параметров m_stopLevel и m_takeLevel интерпретируются, как значения в пунктах и попадают в переменные m_sl и m_tp без изменений. Иначе они интерпретируются, как доля от величины ATR и умножаются на вычисленное значение ATR:

//+------------------------------------------------------------------+
//| Обновление уровней SL и TP по рассчитанному ATR                  |
//+------------------------------------------------------------------+
void CSimpleCandlesStrategy::UpdateLevels() {
// Рассчитываем ATR
   double channelWidth = (m_periodATR > 0 ? ChannelWidth() : 1);

// Обновляем уровни SL и TP
   m_sl = m_stopLevel * channelWidth;
   m_tp = m_takeLevel * channelWidth;
}

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

//+------------------------------------------------------------------+
//| Расчёт величины ATR (нестандартная реализация)                   |
//+------------------------------------------------------------------+
double CSimpleCandlesStrategy::ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1) {
   int n = m_periodATR; // Количество баров для расчёта
   MqlRates rates[];    // Массив для котировок

   // Копируем котировки дневного (по умолчанию) таймфрейма
   int res = CopyRates(m_symbol, p_tf, 1, n, rates);

   // Если скопировалось нужное количество
   if(res == n) {
      double tr[];         // Массив для диапазонов цены
      ArrayResize(tr, n);  // Изменяем его размер
   
      double s = 0;        // Сумма для подсчёта среднего
      FOREACH(rates, {
         tr[i] = rates[i].high - rates[i].low; // Запоминаем размер бара
      });
      
      ArraySort(tr); // Сортируем размеры

      // Суммируем внутренние две четверти размеров баров
      for(int i = n / 4; i < n * 3 / 4; i++) {
         s += tr[i];
      }
      
      // Возвращаем средний размер в пунктах
      return 2 * s / n / m_symbolInfo.Point();
   }

   return 0.0;
}

Сохраним сделанные изменения в файле Strategies/SimpleCandlesStrategy.mqh в рабочей папке проекта.


Подключение стратегии

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

  • MQL5/Experts/Article.17277/Stage1.mq5— файл текущего проекта для исследования стратегии SimpleCandles;
  • MQL5/Include/antekov/Advisor/Experts/Stage1.mqh— библиотечный файл, общий для всех проектов.

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

  1. Определить константу __NAME__, присвоив ей какое-то уникальное значение, отличающееся от имён в других проектах.
  2. Подключить файл с разработанным классом торговой стратегии.
  3. Подключить общую часть советника первого этапа из библиотеки Advisor.
  4. Перечислить входные параметры для торговой стратегии.
  5. Создать функцию с именем GetStrategyParams(), преобразующую значения входных параметров в строку инициализации объекта стратегии.
В коде это может выглядеть примерно так:

// 1. Определяем константу с именем советника
#define  __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME)

// 2. Подключаем нужную стратегию
#include "Strategies/SimpleCandlesStrategy.mqh";

// 3. Подключаем общую часть советника первого этапа из библиотеки Advisor
#include <antekov/Advisor/Experts/Stage1.mqh>

//+------------------------------------------------------------------+
//| 4. Входные параметры  для стратегии                              |
//+------------------------------------------------------------------+
sinput string     symbol_              = "GBPUSD";
sinput ENUM_TIMEFRAMES period_         = PERIOD_H1;

input group "===  Параметры сигнала к открытию"
input int         signalSeqLen_        = 5;     // Количество однонаправленных свечей
input int         periodATR_           = 30;    // Период ATR

input group "===  Параметры отложенных ордеров"
input double      stopLevel_           = 3750;  // Stop Loss (в пунктах)
input double      takeLevel_           = 50;    // Take Profit (в пунктах)

input group "===  Параметры управление капиталом"
input int         maxCountOfOrders_    = 3;     // Макс. количество одновременно отрытых ордеров


//+------------------------------------------------------------------+
//| 5. Функция формирования строки инициализации стратегии           |
//|    из входных параметров                                         |
//+------------------------------------------------------------------+
string GetStrategyParams() {
   return StringFormat(
             "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d)",
             symbol_, period_,
             signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_
          );
}
//+------------------------------------------------------------------+

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

2018.01.01 00:00:00   CVirtualFactory::Create | ERROR: Constructor not found for:
2018.01.01 00:00:00   class CSimpleCandlesStrategy("GBPUSD",16385,5,30,2.95,3.92,3)

Её причина состоит в том, что для создания объектов всех классов-наследников CFactorable используется отдельная функция CVirtualFactory::Create() из файла Virtual/VirtualFactory.mqh. Она вызывается в макросах NEW(C) и CREATE(C, O, P), объявленных в Base/Factorable.mqh.

В этой функции из строки инициализации читается имя класса объекта в переменную className. При этом прочитанная часть удаляется из строки инициализации. А дальше идёт простой перебор всех возможных имён классов (наследников CFactorable), до нахождения совпадения с только что прочитанным именем. В этом случае, создаётся новый объект нужного класса, и указатель на него через переменную object возвращается, как результат работы функции создания:

// Создание объекта из строки инициализации
   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);
      } else if(className == "CHistoryStrategy") {
         object = new CHistoryStrategy(p_params);
      } 
            
      // Если объект не создан или создан в неисправном состоянии, то сообщаем об ошибке
      if(!object) {
         ...
      }

      return object;
   }

Когда весь наш код располагался в одной папке, мы просто добавляли здесь дополнительные ветви условного оператора для новых используемых классов-наследников CFactorable. Так, например, возникла часть, ответственная за создание объектов нашей первой модельной стратегии SimpleVolumes:

} else if(className == "CSimpleVolumesStrategy") {
   object = new CSimpleVolumesStrategy(p_params);
}

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

} else if(className == "CSimpleCandlesStrategy") {
   object = new CSimpleCandlesStrategy(p_params);
}

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

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


Доработка CFactorable

Надо признаться, что эта задача не такая уж и простая. Она заставила сильно подумать над её решением, и перепробовать не один вариант реализации перед тем, как был найден наконец-то тот, который останется пока в качестве используемого. Если бы в языке MQL5 была возможность выполнения кода из строки в уже скомпилированной программе, то всё решалось бы очень просто. Но по соображениям безопасности, функция, аналогичная функции eval() из других языков программирования, у нас отсутствует. Поэтому пришлось выкручиваться с теми возможностями, которые есть.

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

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

static CFactorable* Create(string p_params)

Поэтому, мы можем создать какой-нибудь статический массив, куда помещать указатели на такого рода функции для всех классов наследников. Те классы, которые входят в состав библиотеки Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager) будут как-то добавляться в этот массив внутри кода библиотеки. А классы торговых стратегий будут добавляться в этот массив из кода, расположенного в рабочей папке проекта. Таким образом, будет достигнуто желаемое разделение кода.

Далее встаёт вопрос, а как нам всё это сделать? В каком классе объявлять этот статический массив и как обеспечить его пополнение? Как сохранить привязку имени класса к элементу такого массива?

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

Но как нам наполнять эти массивы? Очень не хотелось создавать какие-то функции, которые надо будет обязательно вызывать из OnInit(). Хотя такой подход, как оказалось, вполне работоспособен. Но в конце концов, мы пришли к другому решению.

Основная идея была в том, что нам бы хотелось иметь возможность вызвать некоторый код не из OnInit(), а прямо из файлов с описанием классов объектов-наследников CFactorable. Однако, если разместить код просто вне описания класса, то он не будет выполнен. Но если вне описания класса объявить глобальную переменную, которая является объектом некоторого класса, то в этом месте будет вызван её конструктор!

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

// Предварительное определение класса
class CFactorable;

// Объявление типа - указатель на функцию создания объектов класса CFactorable
typedef CFactorable* (*TCreateFunc)(string);

//+------------------------------------------------------------------+
//| Класс создателей, связывающих названия и статические             |
//| конструкторы классов-наследников CFactorable                     |
//+------------------------------------------------------------------+
class CFactorableCreator {
public:
   string            m_className;   // Название класса
   TCreateFunc       m_creator;     // Статический конструктор для этого класса

   // Конструктор создателя
                     CFactorableCreator(string p_className, TCreateFunc p_creator);

   // Статический массив всех созданных объектов-создателей
   static CFactorableCreator* creators[];
};

// Статический массив всех созданных объектов-создателей
CFactorableCreator* CFactorableCreator::creators[];

//+------------------------------------------------------------------+
//| Конструктор создателя                                            |
//+------------------------------------------------------------------+
CFactorableCreator::CFactorableCreator(string p_className, TCreateFunc p_creator) :
   m_className(p_className),
   m_creator(p_creator) {
// Добавляем текущий объект создателя в статический массив
   APPEND(creators, &this);
}
//+------------------------------------------------------------------+

Посмотрим, как можно организовать пополнение массива CFactorableCreator::creators на примере класса CVirtualAdvisor. Мы перенесём конструктор CVirtualAdvisor в секцию protected, добавим функцию статического конструктора Create() и после описания класса, создадим глобальный объект класса CFactorableCreator с именем CVirtualAdvisorCreator. Именно там, при вызове конструктора  CFactorableCreator, произойдёт пополнение массива CFactorableCreator::creators.

//+------------------------------------------------------------------+
//| Класс эксперта, работающего с виртуальными позициями (ордерами)  |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {

protected:
   //...
                     CVirtualAdvisor(string p_param);    // Закрытый конструктор
public:
                     static CFactorable* Create(string p_params) { return new CVirtualAdvisor(p_params) };
                    //...
};

CFactorableCreator CVirtualAdvisorCreator("CVirtualAdvisor", CVirtualAdvisor::Create);

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

// Объявление статического конструктора внутри класса
#define STATIC_CONSTRUCTOR(C) static CFactorable* Create(string p) { return new C(p); }

// Добавление статического конструктора для нового класса-наследника CFactorable
// в специальный массив через создание глобального объекта класса CFactorableCreator 
#define REGISTER_FACTORABLE_CLASS(C) CFactorableCreator C##Creator(#C, C::Create);

Они просто повторяют тот шаблон кода, что мы уже написали для класса CVirtualAdvisor. Теперь мы можем выполнить правки таким образом:

//+------------------------------------------------------------------+
//| Класс эксперта, работающего с виртуальными позициями (ордерами)  |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   // ...
                     CVirtualAdvisor(string p_param);    // Конструктор
public:
                     STATIC_CONSTRUCTOR(CVirtualAdvisor);
                    // ...
};

REGISTER_FACTORABLE_CLASS(CVirtualAdvisor);

Подобные изменения надо внести в файлы трёх классов в библиотеке Advisor (CVirtualAdvisor, CVirtualStrategyGroup, CVirtualRiskManager), но сделать это нужно было один раз. Теперь, когда эти изменения уже есть в библиотеке, про них можно забыть. 

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

//+------------------------------------------------------------------+
//| Торговая стратегия c использованием однонаправленных свечей      |
//+------------------------------------------------------------------+
class CSimpleCandlesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;            // Символ (торговый инструмент)
   ENUM_TIMEFRAMES   m_timeframe;         // Период графика (таймфрейм)

   //---  Параметры сигнала к открытию
   int               m_signalSeqLen;      // Количество однонаправленных свечей
   int               m_periodATR;         // Период ATR

   //---  Параметры позиций
   double            m_stopLevel;         // Stop Loss (в пунктах или % ATR)
   double            m_takeLevel;         // Take Profit (в пунктах или % ATR)

   //---  Параметры управление капиталом
   int               m_maxCountOfOrders;  // Макс. количество одновременно отрытых позиций

   CSymbolInfo       *m_symbolInfo;       // Объект для получения информации о свойствах символа

   double            m_tp;                // Stop Loss в пунктах
   double            m_sl;                // Take Profit в пунктах

   //--- Методы
   int               SignalForOpen();     // Сигнал для открытия позиции
   void              OpenBuy();           // Открытие позиции BUY
   void              OpenSell();          // Открытие позиции SELL

   double            ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); // Расчёт величины ATR
   void              UpdateLevels();      // Обновление уровней SL и TP

   // Закрытый конструктор
                     CSimpleCandlesStrategy(string p_params);

public:
   // Статический конструктор
                     STATIC_CONSTRUCTOR(CSimpleCandlesStrategy);

   virtual string    operator~() override;   // Преобразование объекта в строку
   virtual void      Tick() override;        // Обработчик события OnTick
};

// Регистрация класса-наследника CFactorable
REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);

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

Остаётся только применить наполненный массив создателей объектов в общей функции создания объектов из строки инициализации CVirtualFactory::Create(). Тут мы тоже кое-что поменяем. Как выяснилось, теперь нет необходимости помещать эту функцию в отдельный класс. Раньше так было сделано из-за того, что формально класс CFactorable не обязан знать имена всех своих наследников. После уже сделанных изменений мы можем не знать поимённо всех потомков, но можем создать любой из них, обратившись к статическим конструкторам через элементы единого массива CFactorableCreator::creators. Так что, перенесём код этой функции в новый статический метод класса CFactorable::Create():

//+------------------------------------------------------------------+
//| Базовый класс объектов, создаваемых из строки                    |
//+------------------------------------------------------------------+
class CFactorable {
 // ...

public:
   // ...

   // Создание объекта из строки инициализации
   static CFactorable* Create(string p_params);
};


//+------------------------------------------------------------------+
//| Создание объекта из строки инициализации                         |
//+------------------------------------------------------------------+
CFactorable* CFactorable::Create(string p_params) {
// Указатель на создаваемый объект
   CFactorable* object = NULL;

// Читаем имя класса объекта
   string className = CFactorable::ReadClassName(p_params);

// В зависимости от имени класса находим и вызываем соответствующий конструктор
   int i;
   SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i);
   if(i != -1) {
      object = CFactorableCreator::creators[i].m_creator(p_params);
   }

// Если объект не создан или создан в неисправном состоянии, то сообщаем об ошибке
   if(!object) {
      PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\n%s",
                  p_params);
   } else if(!object.IsValid()) {
      PrintFormat(__FUNCTION__
                  " | ERROR: Created object is invalid for:\n%s",
                  p_params);
      delete object; // Удаляем неисправный объект
      object = NULL;
   }

   return object;
}

Как видно, мы также сначала получаем имя класса из строки инициализации, после чего, выполняем поиск индекса элемента в массиве создателей, у которого имя класса совпадает с требуемым. Нужный индекс попадает в переменную i. Если индекс найден, то вызывается статический конструктор объекта нужного класса через соответствующий указатель на функцию. В этом коде уже нет никаких упоминаний об именах классов-потомков CFactorable. Файл с классом CVirtualFactory стал после этого ненужным, и будет исключён из библиотеки.


Проверка советника первого этапа

Скомпилируем советник первого этапа и запустим оптимизацию, пока что вручную. Интервал оптимизации возьмём, например, с 2018 по 2023 год включительно, символ GBPUSD и таймфрейм H4. Оптимизация успешно стартует, и спустя какое-то время мы можем посмотреть на полученные результаты:

Рис. 1. Настройки оптимизации и визуализация результатов оптимизации для советника Stage1.mq5

Посмотрим на пару одиночных проходов, показавшихся более-менее неплохими.

Рис. 2. Результаты прохода с параметрами: class CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)

В представленных на рис. 2 результатах открытие происходило после четырёх однонаправленных свечей, а соотношение между уровнями StopLoss и TakeProfit составляло примерно 1:2. 

Рис. 3. Результаты прохода с параметрами: class CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1)

На рис. 3 показаны результаты прохода, где открытие происходило после семи однонаправленных свечей. При этом использовался очень короткий StopLoss и большой TakeProfit. Этот хорошо видно по графику, где подавляющее большинство сделок закрываются с небольшим убытком, и только десяток сделок за 6 лет были закрыты с прибылью, но большой.

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


Заключение

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

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

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


Важное предупреждение

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


Содержание архива

#
 Имя
Версия  Описание   Последние изменения
  MQL5/Experts/Article.17277   Рабочая папка проекта  
1 CreateProject.mq5 1.01
Советник-скрипт создания проекта с этапами, работами и задачами оптимизации.
Часть 23
2 Optimization.mq5
1.00 Советник для автоматической оптимизации проектов  Часть 23
3 SimpleCandles.mq5
1.00 Итоговый советник для параллельной работы нескольких групп модельных стратегий. Параметры будут браться из встроенной библиотеки групп.
Часть 24
4 Stage1.mq5 1.22  Советник оптимизации одиночного экземпляра торговой стратегии (Этап 1)
Часть 24
5 Stage2.mq5
1.00 Советник оптимизации группы экземпляров торговых стратегий (Этап 2)
Часть 23
Stage3.mq5
1.00 Советник, сохраняющий сформированную нормированную группу стратегий в базу данных эксперта с заданным именем.
Часть 23
  MQL5/Experts/Article.17277/Strategies   Папка стратегий проекта  
7 SimpleCandlesStrategy.mqh 1.01   Часть 24
  MQL5/Include/antekov/Advisor/Base
  Базовые классы, от которых наследуются другие классы проекта    
8 Advisor.mqh 1.04 Базовый класс эксперта Часть 10
9 Factorable.mqh
1.05
Базовый класс объектов, создаваемых из строки
Часть 24
10 FactorableCreator.mqh
1.00   Часть 24
11 Interface.mqh 1.01
Базовый класс визуализации различных объектов
Часть 4  
12 Receiver.mqh
1.04  Базовый класс перевода открытых объемов в рыночные позиции
Часть 12
13 Strategy.mqh
1.04
Базовый класс торговой стратегии
Часть 10
  MQL5/Include/antekov/Advisor/Database
  Файлы для работы со всеми типами баз данных, используемых советниками проекта
 
14 Database.mqh 1.10 Класс для работы с базой данных Часть 22
15 db.adv.schema.sql 1.00
Схема базы данных итогового советника Часть 22
16 db.cut.schema.sql
1.00 Схема урезанной базы данных оптимизации
Часть 22
17 db.opt.schema.sql
1.05  Схема базы данных оптимизации
Часть 22
18 Storage.mqh   1.01
Класс работы с хранилищем Key-Value для итогового советника в базе данных эксперта
Часть 23
  MQL5/Include/antekov/Advisor/Experts
  Файлы с общими частями используемых советников разного типа
 
19 Expert.mqh  1.22 Библиотечный файл для итогового советника. Параметры групп могут браться базы данных эксперта
Часть 23
20 Optimization.mqh  1.04 Библиотечный файл для советника, управляющего запуском задач оптимизации
Часть 23
21 Stage1.mqh
1.19 Библиотечный файл для советника оптимизации одиночного экземпляра торговой стратегии (Этап 1)
Часть 23
22 Stage2.mqh 1.04 Библиотечный файл для советника оптимизации группы экземпляров торговых стратегий (Этап 2)   Часть 23
23 Stage3.mqh
1.04 Библиотечный файл для советника, сохраняющего сформированную нормированную группу стратегий в базу данных эксперта с заданным именем. Часть 23  
  MQL5/Include/antekov/Advisor/Optimization
  Классы, отвечающие за работу автоматической оптимизации
 
24 Optimizer.mqh
1.03  Класс для менеджера автоматической оптимизации проектов
Часть 22
25 OptimizerTask.mqh
1.03
Класс для задачи оптимизации
Часть 22  
  MQL5/Include/antekov/Advisor/Strategies    Примеры торговых стратегий, используемые для демонстрации работы проекта
 
26 HistoryStrategy.mqh 
1.00 Класс торговой стратегии воспроизведения истории сделок
Часть 16
27 SimpleVolumesStrategy.mqh
1.11
Класс торговой стратегии с использованием тиковых объемов
Часть 22  
  MQL5/Include/antekov/Advisor/Utils
  Вспомогательные утилиты, макросы для сокращения кода
 
28 ExpertHistory.mqh 1.00 Класс для экспорта истории сделок в файл Часть 16
29 Macros.mqh 1.05 Полезные макросы для операций с массивами Часть 22   
30 NewBarEvent.mqh 1.00  Класс определения нового бара для конкретного символа  Часть 8
31 SymbolsMonitor.mqh  1.00 Класс получения информации о торговых инструментах (символах) Часть 21
  MQL5/Include/antekov/Advisor/Virtual
  Классы для создания различных объектов, объединённых использованием системы виртуальных торговых ордеров и позиций
 
32 Money.mqh 1.01  Базовый класс управления капиталом
Часть 12
33 TesterHandler.mqh  1.07 Класс для обработки событий оптимизации  Часть 23   
34 VirtualAdvisor.mqh  1.10  Класс эксперта, работающего с виртуальными позициями (ордерами) Часть 24
35 VirtualChartOrder.mqh  1.01  Класс графической виртуальной позиции Часть 18  
36 VirtualHistoryAdvisor.mqh 1.00  Класс эксперта воспроизведения истории сделок  Часть 16
37 VirtualInterface.mqh  1.00  Класс графического интерфейса советника  Часть 4  
38 VirtualOrder.mqh 1.09  Класс виртуальных ордеров и позиций  Часть 22
39 VirtualReceiver.mqh 1.04 Класс перевода открытых объемов в рыночные позиции (получатель)  Часть 23
40 VirtualRiskManager.mqh  1.05 Класс управления риском (риск-менеждер)  Часть 24
41 VirtualStrategy.mqh 1.09  Класс торговой стратегии с виртуальными позициями  Часть 23
42 VirtualStrategyGroup.mqh  1.03  Класс группы торговых стратегий или групп торговых стратегий Часть 24 
43 VirtualSymbolReceiver.mqh  1.00 Класс символьного получателя  Часть 3
Прикрепленные файлы |
MQL5.zip (108.88 KB)
Переосмысливаем классические стратегии (Часть VI): Анализ нескольких таймфреймов Переосмысливаем классические стратегии (Часть VI): Анализ нескольких таймфреймов
В данной серии статей мы вновь рассматриваем классические стратегии, чтобы выяснить, можно ли улучшить их с помощью ИИ. В сегодняшней статье мы рассмотрим популярную стратегию анализа нескольких таймфреймов, чтобы оценить, можно ли улучшить эту стратегию с помощью ИИ.
Анализ всех вариантов движения цены на квантовом компьютере IBM Анализ всех вариантов движения цены на квантовом компьютере IBM
Используем квантовый компьютер от IBM для открытия всех вариантов движения цены. Звучит как научная фантастика? Добро пожаловать в мир квантовых вычислений для трейдинга!
Нейросети в трейдинге: Интеграция теории хаоса в прогнозирование временных рядов (Attraos) Нейросети в трейдинге: Интеграция теории хаоса в прогнозирование временных рядов (Attraos)
Фреймворк Attraos интегрирует теорию хаоса в долгосрочное прогнозирование временных рядов, рассматривая их как проекции многомерных хаотических динамических систем. Используя инвариантность аттрактора, модель применяет реконструкцию фазового пространства и динамическую память с несколькими разрешениями для сохранения исторических структур.
Оптимизация хаотичной игрой — Chaos Game Optimization (CGO) Оптимизация хаотичной игрой — Chaos Game Optimization (CGO)
Представляем новый метаэвристический алгоритм Chaos Game Optimization (CGO), демонстрирующий уникальную способность сохранять высокую эффективность при работе с задачами большой размерности. В отличие от большинства оптимизационных алгоритмов, CGO не только не теряет, но иногда даже увеличивает производительность при масштабировании задачи, что является его ключевой особенностью.