preview
Разрабатываем мультивалютный советник (Часть 15): Готовим советник к реальной торговле

Разрабатываем мультивалютный советник (Часть 15): Готовим советник к реальной торговле

MetaTrader 5Тестер | 22 июля 2024, 13:19
634 2
Yuriy Bykov
Yuriy Bykov

Введение

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

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

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


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

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

  • Подмена символов. Мы проводили оптимизацию и формировали строки инициализации советников используя вполне конкретные названия торговых инструментов (символов). Но может так случиться, что на реальном счёте названия торговых инструментов отличаются от использованных нами. Отличия могут быть, например, в наличии суффиксов или префиксов в названиях (EURGBP.x или xEURGBP вместо EURGBP), записью в другом регистре (eurgbp вместо EURGBP). В будущем возможно расширение списка торговых инструментов и такими, у которых различия в названиях будут ещё более существенны. Поэтому необходима возможность задать правила подмены названий торговых инструментов, чтобы советник смог работать на тех символах, которые использует конкретный брокер.

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

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

Приступим к реализации задуманного.


Подмена символов

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

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

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

<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>

Здесь <Symbol[i]> это исходные названия торговых инструментов, которые используются в строке инициализации, а <TargetSymbol[i]>  это целевые названия торговых инструментов, которые будут использоваться для реальной торговли. Например:

EURGBP=EURGBPx;EURUSD=EURUSDx;GBPUSD=GBPUSDx

Значение этого параметра мы будем передавать специальному методу объекта эксперта (класса CVirtualAdvisor), который будет выполнять все необходимые дальнейшие действия. Если этому методу будет передаваться пустая строка, то никаких изменений в названия торговых инструментов вносить не требуется.

Назовём этот метод SymbolsReplace, и добавим его вызов в код функции инициализации советника:

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
...

input string   symbolsReplace_   = "";       // - Правила замены символов


datetime fromDate = TimeCurrent();


CVirtualAdvisor     *expert;             // Объект эксперта

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

// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

// Если эксперт не создан, то возвращаем ошибку
   if(!expert) return INIT_FAILED;

// Если при замене символов возникла ошибка, то возвращаем ошибку
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

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


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

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

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

public:
   ...

   bool SymbolsReplace(const string p_symbolsReplace); // Замена названий символов
};

...

//+------------------------------------------------------------------+
//| Замена названий символов                                         |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) {
   // Если строка замен пустая, то ничего не делаем
   if(p_symbolsReplace == "") {
      return true;
   }

   // Переменная для результата
   bool res = true;

   string symbolKeyValuePairs[]; // Массив для отдельных замен
   string symbolPair[];          // Массив для двух имён в одной замене

   // Делим строку замен на части, представляющие одну отдельную замену
   StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs);

   // Словарь для соответствия целевого символа исходному символу
   CHashMap<string, string> symbolsMap;

   // Для всех отдельных замен
   FOREACH(symbolKeyValuePairs, {
      // Получаем исходный и целевой символы как два элемента массива
      StringSplit(symbolKeyValuePairs[i], '=', symbolPair);

      // Проверяем наличие целевого символа в списке доступных символов (не кастомных)
      bool custom = false;
      res &= SymbolExist(symbolPair[1], custom);

      // Если целевой символ не найден, то сообщаем об ошибке и выходим
      if(!res) {
         PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]);
         return res;
      }
      
      // Добавляем в словарь новый элемент: ключ - исходный символ, значение - целевой символ
      res &= symbolsMap.Add(symbolPair[0], symbolPair[1]);
   });

   // Если ошибок не возникло, то для всех стратегий вызываем соответствующий метод замены
   if(res) {
      FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap));
   }

   return res;
}
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Класс торговой стратегии с виртуальными позициями                |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...
public:
   ...
   // Замена названий символов
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { 
      return true;
   }
};

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


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

//+------------------------------------------------------------------+
//| Торговая стратегия с использованием тиковых объемов              |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
protected:
   string            m_symbol;         // Символ (торговый инструмент)
   ...

public:
   ...

   // Замена названий символов
   virtual bool      SymbolsReplace(CHashMap<string, string> &p_symbolsMap);
};

...

//+------------------------------------------------------------------+
//| Замена названий символов                                         |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) {
   // Если в словаре есть ключ, совпадающий с текущим символом
   if(p_symbolsMap.ContainsKey(m_symbol)) {
      string targetSymbol; // Целевой символ
      
      // Если целевой символ для текущего успешно получен из словаря
      if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) {
         // Обновляем текущий символ
         m_symbol = targetSymbol;
      }
   }
   
   return true;
}

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

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

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


Режим завершения торговли

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
...

input bool     useOnlyCloseMode_ = false;    // - Включить режим закрытия
input double   onlyCloseDays_    = 0;        // - Предельное время режима закрытия (дней)

...

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

// Подготавливаем строку инициализации для эксперта с группой из нескольких стратегий
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            "    ,%d,%.2f\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_,
                            useOnlyCloseMode_, onlyCloseDays_
                         );

// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

   ...

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

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

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

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

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
...

input group ":::  Риск-менеджер"

...

input ENUM_RM_CALC_OVERALL_PROFIT
rmCalcOverallProfitLimit_                    = RM_CALC_OVERALL_PROFIT_MONEY_BB;  // - Способ расчёта общей прибыли
input double      rmMaxOverallProfitLimit_   = 1000000;                          // - Значение общей прибыли
input datetime    rmMaxOverallProfitDate_    = 0;                                // - Предельное время ожидания общей прибыли (дней)

...

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

// Подготавливаем строку инициализации для эксперта с группой из нескольких стратегий
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],%f\n"
                            "    ),\n"
                            "    class CVirtualRiskManager(\n"
                            "       %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f"
                            "    )\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategiesParams, scale_,
                            rmIsActive_, rmStartBaseBalance_,
                            rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_,
                            rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_,
                            rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,rmMaxOverallProfitDate_,
                            rmMaxRestoreTime_, rmLastVirtualProfitFactor_,
                            magic_, "SimpleVolumes", useOnlyNewBars_
                         );
   
// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

...

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

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


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

//+------------------------------------------------------------------+
//| Класс управления риском (риск-менеждер)                          |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Основные параметры конструктора
   ...
   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Способ расчёта максимальной общей прибыли
   double            m_maxOverallProfitLimit;            // Параметр расчёта максимальной общей прибыли
   datetime          m_maxOverallProfitDate;             // Предельное время для достижения общей прибыли

   ...

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

   ...
};


//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Запоминаем строку инициализации
   m_params = p_params;

// Читаем строку инициализации и устанавливаем значения свойств
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_closeDailyPart = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);
   m_closeOverallPart = ReadDouble(p_params);
   m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params);
   m_maxOverallProfitLimit = ReadDouble(p_params);
   m_maxOverallProfitDate  = (datetime) ReadLong(p_params);
   m_maxRestoreTime = ReadDouble(p_params);
   m_lastVirtualProfitFactor = ReadDouble(p_params);

   ...
}


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

//+------------------------------------------------------------------+
//| Максимальный общая прибыль                                       |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallProfit() {
   // Текущее время
   datetime tc = TimeCurrent();
   
   // Если текущее время больше заданного максимально допустимого, то
   if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) {
      // Возвращаем значение, гарантирующее закрытие позиций
      return m_overallProfit;
   } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) {
      // Для заданного процента от базового баланса вычисляем его
      return m_baseBalance * m_maxOverallProfitLimit / 100;
   } else {
      // Для фиксированного значения просто возвращаем его
      // RM_CALC_OVERALL_PROFIT_MONEY_BB
      return m_maxOverallProfitLimit;
   }
}


Использовать этот метод мы будем при проверке необходимости закрытия внутри метода CheckOverallProfitLimit():

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

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

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

      ...

      return true;
   }

   return false;
}

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

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


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

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

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

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

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
...

input bool     usePrevState_     = true;     // - Загружать предыдущее состояние

...

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

// Создаем эксперта, работающего с виртуальными позициями
   expert = NEW(expertParams);

// Если эксперт не создан, то возвращаем ошибку
   if(!expert) return INIT_FAILED;

// Если при замене символов возникла ошибка, то возвращаем ошибку
   if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED;


// Если требуется восстанавливать состояние, то
   if(usePrevState_) {
      // Загружаем прошлое состояние при наличии
      expert.Load();
      expert.Tick();
   }

// Успешная инициализация
   return(INIT_SUCCEEDED);
}

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


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

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

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

Для этого добавим новый метод HashParams() и внесём изменения в конструктор эксперта:

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

public:
   ...
};

...

//+------------------------------------------------------------------+
//| Хеш-значение параметров эксперта                                 |
//+------------------------------------------------------------------+
string CVirtualAdvisor::HashParams(string p_params) {
   uchar hash[], key[], data[];

   // Вычисляем хеш от строки инициализации 
   StringToCharArray(p_params, data);
   CryptEncode(CRYPT_HASH_MD5, data, key, hash);

   // Переводим его из массива чисел в строку с шестнадцатеричной записью
   string res = "";
   FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-");
   
   return res;
}

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {


       ... 

// Если нет ошибок чтения, то
   if(IsValid()) {

      
    ... 

      // Формируем из имени эксперта и параметров имя файла для сохранения состояния
      m_name = StringFormat("%s-%d-%s%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            HashParams(groupParams),
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );;

      
    ... 
   }
}


Добавляем сохранение/загрузку риск-менеджера в соответствующие методы эксперта: 

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

// Сохраняем состояние, если:
   if(true
// появились более поздние изменения
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
// и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // Если файл открыт, то сохраняем
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Время последних изменений

         // Все стратегии
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         m_riskManager.Save(f);

         FileClose(f);

         // Обновляем время последнего сохранения
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

// Загружаем состояние, если:
   if(true
// файл существует
         && FileIsExist(m_name)
// и сейчас не оптимизация
         && !MQLInfoInteger(MQL_OPTIMIZATION)
// и сейчас не тестирование либо сейчас визуальное тестирование
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // Если файл открыт, то загружаем
         m_lastSaveTime = FileReadDatetime(f);     // Время последнего сохранения
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Загружаем все стратегии
         FOREACH(m_strategies, {
            res &= ((CVirtualStrategy*) m_strategies[i]).Load(f);
            if(!res) break;
         });

         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
         }

         res &= m_riskManager.Load(f);
         
         if(!res) {
            PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name);
         }

         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }

   return res;
}

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


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

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

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

// Обновляемые значения
   
    ... 

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


С учётом сказанного реализация этих методов может выглядеть так:

//+------------------------------------------------------------------+
//| Сохранение состояния                                             |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Save(const int f) {
   FileWrite(f,
             m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime,
             m_dailyDepoPart, m_overallDepoPart);

   return true;
}

//+------------------------------------------------------------------+
//| Загрузка состояния                                               |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::Load(const int f) {
   m_state = (ENUM_RM_STATE) FileReadNumber(f);
   m_lastVirtualProfit = FileReadNumber(f);
   m_startRestoreTime = FileReadDatetime(f);
   m_startTime = FileReadDatetime(f);
   m_dailyDepoPart = FileReadNumber(f);
   m_overallDepoPart = FileReadNumber(f);

   return true;
}

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


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

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

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

  • если время остановки перед перезапуском не указано (равно нулю или 1970-01-01 00:00:00) или оно не попадает в интервал тестирования, то советник работает как исходный советник;
  • если указано конкретное время остановки, попадающее в интервал тестирования, то при достижении этого времени советник перестаёт выполнять обработчик тика у объекта эксперта до тех пор, пока не наступит время, указанное во втором параметре.

В коде эти два параметра будут выглядеть так:

input datetime restartStopTime_  = 0;        // - Время остановки перед перезапуском
input datetime restartStartTime_ = 0;        // - Время начала перезапуска

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
// Если время остановки указано, то
   if(restartStopTime_ != 0) {
      // Определяем текущее время
      datetime tc = TimeCurrent();

      // Если мы находимся в промежутке между остановкой и возобновлением, то
      if(tc >= restartStopTime_ && tc <= restartStartTime_) {
         // Запоминаем это состояние и выходим
         isRestarting = true;
         return;
      }

      // Если мы находились в состоянии между остановкой и возобновлением
      // и пришло время возобновления работы, то
      if(isRestarting && tc > restartStartTime_) {
         // Загружем состояние эксперта
         expert.Load();
         // Сбрасываем флаг состояния между остановкой и возобновлением
         isRestarting = false;
      }
   }

// Выполняем обработку тика
   expert.Tick();
}

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


Посмотрим на результаты без прерывания на интервале 2021-2022 годов.

Рис. 1. Результаты тестирования без прерывания торговли


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

Чтобы увидеть разницу, сделаем перерыв побольше, например, продолжительностью 4 месяца. Получим следующие результаты:

Рис. 2. Результаты тестирования с перерывом торговли с 2021.07.27 по 2021.11.29

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


Заключение

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

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

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

Спасибо, что дочитали до конца, увидимся!

Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Максим Курбатов
Максим Курбатов | 23 июл. 2024 в 17:26
Здравствуйте! Не компилируется советник. Сначала требует наличие разных включаемых файлов mqh - попробовал взять соответствующие файлы из предыдущих Ваших статей. Все равно не компилируется - видимо, я загружаю не те версии файлов... Не могли бы Вы сказать, какие версии включаемых файлов являются актуальными для данного кода советника? Спасибо!
Yuriy Bykov
Yuriy Bykov | 24 июл. 2024 в 07:43

Здравствуйте!

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

Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 6) Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 6)
Статья является шестой частью серии, описывающей этапы разработки нативного MQL5-клиента для протокола MQTT 5.0. В этой части я опишу основные изменения в нашем первом рефакторинге, получение рабочего проекта наших классов построения пакетов, создание пакетов PUBLISH и PUBACK, а также семантику кодов причин PUBACK.
Нейросети в трейдинге: Снижение потребления памяти методом оптимизации Adam (Adam-mini) Нейросети в трейдинге: Снижение потребления памяти методом оптимизации Adam (Adam-mini)
Одним из направлений повышения эффективности процесса обучения и сходимости моделей является улучшение методов оптимизации. Adam-mini представляет собой адаптивный метод оптимизации, разработанный для улучшения базового алгоритма Adam.
Теория хаоса в трейдинге (Часть 1): Введение, применение на финансовых рынках и индикатор Ляпунова Теория хаоса в трейдинге (Часть 1): Введение, применение на финансовых рынках и индикатор Ляпунова
Можно ли применять теорию хаоса на финансовых рынках? Чем классическая теория Хаоса и хаотические системы отличаются от концепции, предложенной Биллом Вильямсом, рассмотрим в этой статье.
Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы
Статья является третьей в серии материалов об основных аспектах программирования на MQL5. Здесь описываются сложные типы данных, которые не были описаны в предыдущей статье, включая структуры, объединения, классы и тип данных "функция". Также рассказано, как добавить модульности нашей программе с помощью директивы препроцессора #include.