preview
Использование алгоритмов оптимизации для настройки параметров советника "на лету"

Использование алгоритмов оптимизации для настройки параметров советника "на лету"

MetaTrader 5Тестер | 16 февраля 2024, 10:33
780 24
Andrey Dik
Andrey Dik

Содержание:

1. Введение
2. Архитектура торгового советника с самооптимизацией
3. Виртуализация индикаторов
4. Виртуализация стратегии
5. Тестирование функциональности



1. Введение

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

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

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

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

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

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

  • Оптимизация параметров торговых стратегий. Алгоритмы оптимизации могут использоваться для настройки параметров торговых стратегий. С помощью этих методов можно определить наилучшие значения параметров, таких как периоды скользящих средних, уровни стоп-лосса и тейк-профита, или другие параметры, связанные с торговыми сигналами и правилами.
  • Оптимизация времени входа и выхода из позиций. Алгоритмы оптимизации могут помочь определить оптимальные моменты для входа и выхода из позиций на основе исторических данных и текущих условий рынка. Например, можно использовать алгоритмы оптимизации для определения оптимальных временных интервалов для торговых сигналов.
  • Портфельное управление. Алгоритмы оптимизации могут помочь определить оптимальное распределение активов в портфеле для достижения заданных целей. Например, можно использовать методы оптимизации, такие как средневзвешенная ковариационная матрица (Mean-Variance Optimization), для нахождения наиболее эффективного набора активов с учетом ожидаемой доходности и риска. Это может включать определение оптимального соотношения между акциями, облигациями и другими активами, а также оптимизацию размеров позиций и диверсификацию портфеля.
  • Разработка торговых стратегий. Алгоритмы оптимизации могут быть использованы для разработки новых торговых стратегий. Например, можно использовать генетическое программирование для эволюционного поиска оптимальных правил входа и выхода из позиций на основе исторических данных.
  • Риск-менеджмент. Алгоритмы оптимизации могут помочь в управлении рисками в трейдинге. Например, можно использовать алгоритмы оптимизации для расчета оптимального размера позиции или определения динамического стоп-лосс уровня, который минимизирует потенциальные убытки.
  • Выбор лучших торговых инструментов. Алгоритмы оптимизации могут помочь в выборе лучших торговых инструментов или активов для торговли. Например, можно использовать алгоритмы оптимизации для ранжирования активов на основе различных критериев, таких как доходность, волатильность или ликвидность.
  • Прогнозирование финансовых рынков. Алгоритмы оптимизации могут быть использованы для прогнозирования финансовых рынков. Можно применять алгоритмы оптимизации для настройки параметров прогностических моделей или для выбора оптимальных комбинаций прогностических моделей.

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


2. Архитектура торгового советника с самооптимизацией

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

На линии времени "History" советник находится в точке "time now", где принимается решение об оптимизации. Советник "EA" вызывает функцию менеджера "Manager function" , которая выполняет управление процессом оптимизации, советник передает в эту функцию настройки оптимизации "optimization parameters".

В свою очередь менеджер запрашивает набор параметров у алгоритма оптимизации "optimization ALGO" или "AO", который сейчас и далее будет именоваться как "set". После чего менеджер передает сет в виртуальную торговую стратегию "EA Virt", которая является полным аналогом реальной стратегии, работающей и выполняющей торговые операции, "EA".

"EA Virt" производит виртуальную торговлю от точки "past" на истории до точки "time now". Менеджер выполняет запуски "EA Virt" столько раз, сколько указано в размере популяции в параметрах "optimization parameters". "EA Virt" в свою очередь возвращает результат прогона на истории в виде "ff result".

"ff result" - результат фитнес-функции, или приспособленности, или критерий оптимизации, который может быть каким угодно, на усмотрение пользователя. Это может быть, например, баланс, профит фактор, математическое ожидание, или комплексный критерий, интегральный, или совокупный дифференциальный, измеренный во многих точках времени "History". Таким образом, результат фитнес-функции или "ff result", является то, что пользователь считает важным показателем качества торговой стратегии.

Далее, "ff result", который является оценкой конкретного сета, передается менеджером в алгоритм оптимизации.

При достижении условия останова менеджер передает лучший сет в торгующий советник "EA", после чего советник продолжает работу (торгует) уже с новыми обновленными параметрами от точки "time now" до точки реоптимизации "reoptimiz", в которой производится повторная оптимизация на заданную глубину истории.

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

Scheme

Рисунок 1. Схема организации работы самооптимизации советника.

Согласно схеме работы алгоритма оптимизации "optimization ALGO", он может быть рассмотрен как "черный ящик", который выполняет свою работу автономно (впрочем, для него вcё, что снаружи - тоже является "черным ящиком"), независимо от конкретной торговой стратегии, менеджера и виртуальной стратегии. Менеджер запрашивает сет у алгоритма оптимизации, а в обратную сторону отправляет оценку этого сета, которую алгоритм оптимизации использует для определения следующего сета. Этот цикл продолжается до тех пор, пока не будет найден наилучший набор параметров, соответствующий требованиям пользователя. Таким образом, алгоритм оптимизации ищет оптимальные параметры именно те, которые удовлетворяют потребности пользователя, заданные через фитнес-функцию в "EA Virt".


3. Виртуализация индикаторов

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

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

Для решения проблемы нам потребуется "виртуализировать" расчет индикатора в рамках исполняемого советника, чтобы избежать использования хендлов. Для примера возьмём индикатор Stochastic.

Расчетная часть каждого индикатора содержит стандартную функцию "OnCalculate". Эту функцию нужно переименовать, например в "Calculate" и оставить практически без изменений. 

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

//——————————————————————————————————————————————————————————————————————————————
class C_iStochastic
{
  public: void Init (const int InpKPeriod,       // K period
                     const int InpDPeriod,       // D period
                     const int InpSlowing)       // Slowing
  {
    inpKPeriod = InpKPeriod;
    inpDPeriod = InpDPeriod;
    inpSlowing = InpSlowing;
  }

  public: int Calculate (const int rates_total,
                         const int prev_calculated,
                         const double &high  [],
                         const double &low   [],
                         const double &close []);

  //--- indicator buffers
  public:  double ExtMainBuffer   [];
  public:  double ExtSignalBuffer [];
  private: double ExtHighesBuffer [];
  private: double ExtLowesBuffer  [];

  private: int inpKPeriod; // K period
  private: int inpDPeriod; // D period
  private: int inpSlowing; // Slowing
};
//——————————————————————————————————————————————————————————————————————————————

И, соответственно, сам расчёт индикатора в методе "Calculate". Расчёт индикатора абсолютно ничем не отличается от индикатора в штатной поставке терминала. Единственное отличие — распределение размера для буферов индикатора и их инициализация

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

//——————————————————————————————————————————————————————————————————————————————
int C_iStochastic::Calculate (const int rates_total,
                              const int prev_calculated,
                              const double &high  [],
                              const double &low   [],
                              const double &close [])
{
  if (rates_total <= inpKPeriod + inpDPeriod + inpSlowing) return (0);

  ArrayResize (ExtHighesBuffer, rates_total);
  ArrayResize (ExtLowesBuffer,  rates_total);
  ArrayResize (ExtMainBuffer,   rates_total);
  ArrayResize (ExtSignalBuffer, rates_total);

  ArrayInitialize (ExtHighesBuffer, 0.0);
  ArrayInitialize (ExtLowesBuffer,  0.0);
  ArrayInitialize (ExtMainBuffer,   0.0);
  ArrayInitialize (ExtSignalBuffer, 0.0);

  int i, k, start;

  start = inpKPeriod - 1;

  if (start + 1 < prev_calculated)
  {
    start = prev_calculated - 2;
    Print ("start ", start);
  }
  else
  {
    for (i = 0; i < start; i++)
    {
      ExtLowesBuffer  [i] = 0.0;
      ExtHighesBuffer [i] = 0.0;
    }
  }

  //--- calculate HighesBuffer[] and ExtHighesBuffer[]
  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double dmin =  1000000.0;
    double dmax = -1000000.0;

    for (k = i - inpKPeriod + 1; k <= i; k++)
    {
      if (dmin > low  [k]) dmin = low  [k];
      if (dmax < high [k]) dmax = high [k];
    }

    ExtLowesBuffer  [i] = dmin;
    ExtHighesBuffer [i] = dmax;
  }

  //--- %K
  start = inpKPeriod - 1 + inpSlowing - 1;

  if (start + 1 < prev_calculated) start = prev_calculated - 2;
  else
  {
    for (i = 0; i < start; i++) ExtMainBuffer [i] = 0.0;
  }

  //--- main cycle
  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double sum_low  = 0.0;
    double sum_high = 0.0;

    for (k = (i - inpSlowing + 1); k <= i; k++)
    {
      sum_low  += (close [k] - ExtLowesBuffer [k]);
      sum_high += (ExtHighesBuffer [k] - ExtLowesBuffer [k]);
    }

    if (sum_high == 0.0) ExtMainBuffer [i] = 100.0;
    else                 ExtMainBuffer [i] = sum_low / sum_high * 100;
  }

  //--- signal
  start = inpDPeriod - 1;

  if (start + 1 < prev_calculated) start = prev_calculated - 2;
  else
  {
    for (i = 0; i < start; i++) ExtSignalBuffer [i] = 0.0;
  }

  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double sum = 0.0;
    for (k = 0; k < inpDPeriod; k++) sum += ExtMainBuffer [i - k];
    ExtSignalBuffer [i] = sum / inpDPeriod;
  }

  //--- OnCalculate done. Return new prev_calculated.
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Дополнительно приведу пример виртуализации индикатора "MACD":

//——————————————————————————————————————————————————————————————————————————————
class C_iMACD
{
  public: void Init (const int InpFastEMA,       // Fast   EMA period
                     const int InpSlowEMA,       // Slow   EMA period
                     const int InpSignalSMA)     // Signal SMA period
  {
    inpFastEMA   = InpFastEMA;
    inpSlowEMA   = InpSlowEMA;
    inpSignalSMA = InpSignalSMA;

    maxPeriod = InpFastEMA;
    if (maxPeriod < InpSlowEMA)   maxPeriod = InpSlowEMA;
    if (maxPeriod < InpSignalSMA) maxPeriod = InpSignalSMA;
  }

  public: int Calculate (const int rates_total,
                         const int prev_calculated,
                         const double &close []);

  //--- indicator buffers
  public:  double ExtMacdBuffer   [];
  public:  double ExtSignalBuffer [];
  private: double ExtFastMaBuffer [];
  private: double ExtSlowMaBuffer [];

  private: int ExponentialMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []);
  private: int SimpleMAOnBuffer      (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []);

  private: int inpFastEMA;   // Fast EMA period
  private: int inpSlowEMA;   // Slow EMA period
  private: int inpSignalSMA; // Signal SMA period
  private: int maxPeriod;
};
//——————————————————————————————————————————————————————————————————————————————

Расчетная часть индикатора:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::Calculate (const int rates_total,
                        const int prev_calculated,
                        const double &close [])
{
  if (rates_total < maxPeriod) return (0);

  ArrayResize (ExtMacdBuffer,   rates_total);
  ArrayResize (ExtSignalBuffer, rates_total);
  ArrayResize (ExtFastMaBuffer, rates_total);
  ArrayResize (ExtSlowMaBuffer, rates_total);

  ArrayInitialize (ExtMacdBuffer,   0.0);
  ArrayInitialize (ExtSignalBuffer, 0.0);
  ArrayInitialize (ExtFastMaBuffer, 0.0);
  ArrayInitialize (ExtSlowMaBuffer, 0.0);

  ExponentialMAOnBuffer (rates_total, prev_calculated, 0, inpFastEMA, close, ExtFastMaBuffer);
  ExponentialMAOnBuffer (rates_total, prev_calculated, 0, inpSlowEMA, close, ExtSlowMaBuffer);

  int start;
  if (prev_calculated == 0) start = 0;

  else start = prev_calculated - 1;

  //--- calculate MACD
  for (int i = start; i < rates_total && !IsStopped (); i++) ExtMacdBuffer [i] = ExtFastMaBuffer [i] - ExtSlowMaBuffer [i];

  //--- calculate Signal
  SimpleMAOnBuffer (rates_total, prev_calculated, 0, inpSignalSMA, ExtMacdBuffer, ExtSignalBuffer);

  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Расчет экспоненциального сглаживания вообще не нужно изменять:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::ExponentialMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer [])
{
  //--- check period
  if (period <= 1 || period > (rates_total - begin)) return (0);

  //--- save and clear 'as_series' flags
  bool as_series_price  = ArrayGetAsSeries (price);
  bool as_series_buffer = ArrayGetAsSeries (buffer);

  ArraySetAsSeries (price, false);
  ArraySetAsSeries (buffer, false);

  //--- calculate start position
  int    start_position;
  double smooth_factor = 2.0 / (1.0 + period);

  if (prev_calculated == 0) // first calculation or number of bars was changed
  {
    //--- set empty value for first bars
    for (int i = 0; i < begin; i++) buffer [i] = 0.0;

    //--- calculate first visible value
    start_position = period + begin;
    buffer [begin] = price [begin];

    for (int i = begin + 1; i < start_position; i++) buffer [i] = price [i] * smooth_factor + buffer [i - 1] * (1.0 - smooth_factor);
  }
  else start_position = prev_calculated - 1;

  //--- main loop
  for (int i = start_position; i < rates_total; i++) buffer [i] = price [i] * smooth_factor + buffer [i - 1] * (1.0 - smooth_factor);

  //--- restore as_series flags
  ArraySetAsSeries (price,  as_series_price);
  ArraySetAsSeries (buffer, as_series_buffer);
  //---
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Также не требует изменения и расчет простого сглаживания:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::SimpleMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer [])
{
  //--- check period
  if (period <= 1 || period > (rates_total - begin)) return (0);

  //--- save as_series flags
  bool as_series_price = ArrayGetAsSeries (price);
  bool as_series_buffer = ArrayGetAsSeries (buffer);

  ArraySetAsSeries (price, false);
  ArraySetAsSeries (buffer, false);

  //--- calculate start position
  int start_position;

  if (prev_calculated == 0) // first calculation or number of bars was changed
  {
    //--- set empty value for first bars
    start_position = period + begin;

    for (int i = 0; i < start_position - 1; i++) buffer [i] = 0.0;

    //--- calculate first visible value
    double first_value = 0;

    for (int i = begin; i < start_position; i++) first_value += price [i];

    buffer [start_position - 1] = first_value / period;
  }
  else start_position = prev_calculated - 1;

  //--- main loop
  for (int i = start_position; i < rates_total; i++) buffer [i] = buffer [i - 1] + (price [i] - price [i - period]) / period;

  //--- restore as_series flags
  ArraySetAsSeries (price, as_series_price);
  ArraySetAsSeries (buffer, as_series_buffer);

  //---
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————


4. Виртуализация стратегии

Один из уважаемых читателей моих статей об алгоритмах оптимизации, LUIS ALBERTO BIANUCCI, любезно предоставил код советника на основе индикатора Stochastic. Он попросил меня создать пример на основе этого кода с целью продемонстрировать способ организации самообучения в советнике с подключением библиотеки "AO Core" и рассмотрения этого примера в статье. Таким образом, другие пользователи смогут использовать этот метод при подключении алгоритмов оптимизации в своих собственных разработках. Я хотел бы подчеркнуть, что данный способ подходит для подключения любых алгоритмов оптимизации, рассмотренных в серии моих статей "Популяционные алгоритмы оптимизации", благодаря тому, что алгоритмы оформлены в универсальном виде и могут быть успешно применены в любых проектах пользователей.

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

Далее следуют "input" - параметры советника, среди которых отметим "InpKPeriod_P" и "InpUpperLevel_P", эти параметры необходимо оптимизировать, которые представляют период индикатора "Stochastic" и уровни.

input string   InpKPeriod_P        = "18|9|3|24";  //STO K period:      it is necessary to optimize
input string   InpUpperLevel_P  = "96|88|2|98"; //STO upper level: it is necessary to optimize

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

//——————————————————————————————————————————————————————————————————————————————
#import "\\Market\\AO Core.ex5"
bool   Init (int colonySize, double &range_min [], double &range_max [], double &range_step []);
//------------------------------------------------------------------------------
void   Preparation    ();
void   GetVariantCalc (double &variant [], int pos);
void   SetFitness     (double value,       int pos);
void   Revision       ();
//------------------------------------------------------------------------------
void   GetVariant     (double &variant [], int pos);
double GetFitness     (int pos);
#import
//——————————————————————————————————————————————————————————————————————————————

#include <Trade\Trade.mqh>;
#include "cStochastic.mqh"


input group         "==== GENERAL ====";
sinput long         InpMagicNumber      = 132516;       //Magic Number
sinput double       InpLotSize          = 0.01;         //Lots

input group         "==== Trading ====";
input int           InpStopLoss         = 1450;         //Stoploss
input int           InpTakeProfit       = 1200;         //Takeprofit

input group         "==== Stochastic ==|value|start|step|end|==";
input string        InpKPeriod_P        = "18|9|3|24";  //STO K period   : it is necessary to optimize
input string        InpUpperLevel_P     = "96|88|2|98"; //STO upper level: it is necessary to optimize

input group         "====Self-optimization====";
sinput bool         SelfOptimization    = true;
sinput int          InpBarsOptimize     = 18000;        //Number of bars in the history for optimization
sinput int          InpBarsReOptimize   = 1440;         //After how many bars, EA will reoptimize
sinput int          InpPopSize          = 50;           //Population size
sinput int          NumberFFlaunches    = 10000;        //Number of runs in the history during optimization
sinput int          Spread              = 10;           //Spread

MqlTick Tick;
CTrade  Trade;

C_iStochastic IStoch;

double Set        [];
double Range_Min  [];
double Range_Step [];
double Range_Max  [];

double TickSize = 0.0;

В инициализации советника в функции "OnInit" зададим размер массивов параметров в соответствии с количеством оптимизируемых параметров: "Set" - набор параметров, "Range_Min" - минимальные значения параметров (стартовые значения),  "Range_Step" - шаг параметров и "Range_Max" - максимальные значения параметров. Из строковых параметров извлечём соответствующие значения и присвоим массивам.

//——————————————————————————————————————————————————————————————————————————————
int OnInit ()
{
  TickSize = SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE);

  ArrayResize (Set,        2);
  ArrayResize (Range_Min,  2);
  ArrayResize (Range_Step, 2);
  ArrayResize (Range_Max,  2);

  string result [];
  if (StringSplit (InpKPeriod_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED;

  Set        [0] = (double)StringToInteger (result [0]);
  Range_Min  [0] = (double)StringToInteger (result [1]);
  Range_Step [0] = (double)StringToInteger (result [2]);
  Range_Max  [0] = (double)StringToInteger (result [3]);

  if (StringSplit (InpUpperLevel_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED;

  Set        [1] = (double)StringToInteger (result [0]);
  Range_Min  [1] = (double)StringToInteger (result [1]);
  Range_Step [1] = (double)StringToInteger (result [2]);
  Range_Max  [1] = (double)StringToInteger (result [3]);

  IStoch.Init ((int)Set [0], 1, 3);

  //  set magicnumber to trade object
  Trade.SetExpertMagicNumber (InpMagicNumber);

  //---
  return (INIT_SUCCEEDED);
}
//——————————————————————————————————————————————————————————————————————————————

В коде советника в функции "OnTick" вставляем блок вызова самооптимизации - функцию "Optimize", которая является "менеджером" на схеме рисунка 1, запускает оптимизацию. Там, где должны были быть использованы внешние переменные, которые требуется оптимизировать, используем значения из массива "Set".

//——————————————————————————————————————————————————————————————————————————————
void OnTick ()
{
  //----------------------------------------------------------------------------
  if (!IsNewBar ())
  {
    return;
  }

  //----------------------------------------------------------------------------
  if (SelfOptimization)
  {
    //--------------------------------------------------------------------------
    static datetime LastOptimizeTime = 0;

    datetime timeNow  = iTime (_Symbol, PERIOD_CURRENT, 0);
    datetime timeReop = iTime (_Symbol, PERIOD_CURRENT, InpBarsReOptimize);

    if (LastOptimizeTime <= timeReop)
    {
      LastOptimizeTime = timeNow;
      Print ("-------------------Start of optimization----------------------");

      Print ("Old set:");
      ArrayPrint (Set);

      Optimize (Set,
                Range_Min,
                Range_Step,
                Range_Max,
                InpBarsOptimize,
                InpPopSize,
                NumberFFlaunches,
                Spread * SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE));

      Print ("New set:");
      ArrayPrint (Set);

      IStoch.Init ((int)Set [0], 1, 3);
    }
  }

  //----------------------------------------------------------------------------
  if (!SymbolInfoTick (_Symbol, Tick))
  {
    Print ("Failed to get current symbol tick"); return;
  }

  //data preparation------------------------------------------------------------
  MqlRates rates [];
  int dataCount = CopyRates (_Symbol, PERIOD_CURRENT, 0, (int)Set [0] + 1 + 3 + 1, rates);

  if (dataCount == -1)
  {
    Print ("Data get error");
    return;
  }

  double hi [];
  double lo [];
  double cl [];

  ArrayResize (hi, dataCount);
  ArrayResize (lo, dataCount);
  ArrayResize (cl, dataCount);

  for (int i = 0; i < dataCount; i++)
  {
    hi [i] = rates [i].high;
    lo [i] = rates [i].low;
    cl [i] = rates [i].close;
  }

  int calc = IStoch.Calculate (dataCount, 0, hi, lo, cl);
  if (calc <= 0) return;

  double buff0 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 2];
  double buff1 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 3];

  //----------------------------------------------------------------------------
  // count open positions
  int cntBuy, cntSell;
  if (!CountOpenPositions (cntBuy, cntSell))
  {
    Print ("Failed to count open positions");
    return;
  }

  //----------------------------------------------------------------------------
  // check for buy
  if (cntBuy == 0 && buff1 <= (100 - (int)Set [1]) && buff0 > (100 - (int)Set [1]))
  {
    ClosePositions (2);

    double sl = NP (Tick.bid - InpStopLoss   * TickSize);
    double tp = NP (Tick.bid + InpTakeProfit * TickSize);

    Trade.PositionOpen (_Symbol, ORDER_TYPE_BUY, InpLotSize, Tick.ask, sl, tp, "Stochastic EA");
  }

  //----------------------------------------------------------------------------
  // check for sell
  if (cntSell == 0 && buff1 >= (int)Set [1] && buff0 < (int)Set [1])
  {
    ClosePositions (1);

    double sl = NP (Tick.ask + InpStopLoss   * TickSize);
    double tp = NP (Tick.ask - InpTakeProfit * TickSize);

    Trade.PositionOpen (_Symbol, ORDER_TYPE_SELL, InpLotSize, Tick.bid, sl, tp, "Stochastic EA");
  }
}
//——————————————————————————————————————————————————————————————————————————————

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

1. Инициализация алгоритма оптимизации.
2.1. Подготовка популяции.
2.2. Получение набора параметров от алгоритма оптимизации.
2.3. Расчет фитнес-функции с переданными ей параметрами.
2.4. Обновление лучшего решения.
2.5. Получение лучшего решения из алгоритма.

//——————————————————————————————————————————————————————————————————————————————
void Optimize (double      &set        [],
               double      &range_min  [],
               double      &range_step [],
               double      &range_max  [],
               const int    inpBarsOptimize,
               const int    inpPopSize,
               const int    numberFFlaunches,
               const double spread)
{
  //----------------------------------------------------------------------------
  double parametersSet [];
  ArrayResize(parametersSet, ArraySize(set));

  //----------------------------------------------------------------------------
  int epochCount = numberFFlaunches / inpPopSize;

  Init(inpPopSize, range_min, range_max, range_step);

  // Optimization-------------------------------------------------------------
  for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++)
  {
    Preparation ();

    for (int set = 0; set < inpPopSize; set++)
    {
      GetVariantCalc (parametersSet, set);
      SetFitness     (VirtualStrategy (parametersSet, inpBarsOptimize, spread), set);
    }

    Revision ();
  }

  Print ("Fitness: ", GetFitness (0));
  GetVariant (parametersSet, 0);
  ArrayCopy (set, parametersSet, 0, 0, WHOLE_ARRAY);
}
//——————————————————————————————————————————————————————————————————————————————

Функция "VirtualStrategy" выполняет тестирование стратегии на исторических данных (на схеме рисунка 1 — это "EA Virt"). Она принимает массив параметров "set", количество баров для оптимизации "barsOptimize" и значение "spread".

Сначала происходит подготовка данных. Загружаются исторические данные в массив "rates". Затем создаются массивы "hi", "lo" и "cl", которые требуются для расчета Stochastic.

Далее инициализируется индикатор Stochastic и выполняется его расчет на основе исторических данных. Если расчет не удался, функция возвращает значение "-DBL_MAX" (наихудшее возможное значение фитнес-функции).

Далее следует тестирование стратегии на исторических данных, логика которого полностью соответствует основному коду советника. Создается объект "deals" для хранения сделок. Затем происходит проход по историческим данным, где для каждого бара проверяются условия для открытия и закрытия позиций на основе значения индикатора и уровней "upLevel" и "dnLevel". Если условия выполнены, происходит открытие или закрытие позиции.

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

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

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

//——————————————————————————————————————————————————————————————————————————————
double VirtualStrategy (double &set [], int barsOptimize, double spread)
{
  //data preparation------------------------------------------------------------
  MqlRates rates [];
  int dataCount = CopyRates(_Symbol, PERIOD_CURRENT, 0, barsOptimize + 1, rates);

  if (dataCount == -1)
  {
    Print ("Data get error");
    return -DBL_MAX;
  }

  double hi [];
  double lo [];
  double cl [];

  ArrayResize (hi, dataCount);
  ArrayResize (lo, dataCount);
  ArrayResize (cl, dataCount);

  for (int i = 0; i < dataCount; i++)
  {
    hi [i] = rates [i].high;
    lo [i] = rates [i].low;
    cl [i] = rates [i].close;
  }

  C_iStochastic iStoch;
  iStoch.Init ((int)set [0], 1, 3);

  int calc = iStoch.Calculate (dataCount, 0, hi, lo, cl);
  if (calc <= 0) return -DBL_MAX;

  //============================================================================
  //test of strategy on history-------------------------------------------------
  S_Deals deals;

  double iStMain0 = 0.0;
  double iStMain1 = 0.0;
  double upLevel  = set [1];
  double dnLevel  = 100.0 - set [1];
  double balance  = 0.0;

  //running through history-----------------------------------------------------
  for (int i = 2; i < dataCount; i++)
  {
    if (i >= dataCount)
    {
      deals.ClosPos (-1, rates [i].open, spread);
      deals.ClosPos (1, rates [i].open, spread);
      break;
    }

    iStMain0 = iStoch.ExtMainBuffer [i - 1];
    iStMain1 = iStoch.ExtMainBuffer [i - 2];

    if (iStMain0 == 0.0 || iStMain1 == 0.0) continue;

    //buy-------------------------------
    if (iStMain1 <= dnLevel && dnLevel < iStMain0)
    {
      deals.ClosPos (-1, rates [i].open, spread);

      if (deals.GetBuys () == 0) deals.OpenPos (1, rates [i].open, spread);
    }

    //sell------------------------------
    if (iStMain1 >= upLevel && upLevel > iStMain0)
    {
      deals.ClosPos (1, rates [i].open, spread);

      if (deals.GetSels () == 0) deals.OpenPos (-1, rates [i].open, spread);
    }
  }
  //----------------------------------------------------------------------------

  if (deals.histSelsCNT + deals.histBuysCNT <= 0) return -DBL_MAX;
  return deals.balance;
}
//——————————————————————————————————————————————————————————————————————————————

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

#include "AO_ESG.mqh"

В функции "Optimize" объявить объект алгоритма ESG и настроить граничные значения оптимизируемых параметров. Тогда для использования ESG функция "Optimize" будет выглядеть так:

//——————————————————————————————————————————————————————————————————————————————
void Optimize (double      &set        [],
               double      &range_min  [],
               double      &range_step [],
               double      &range_max  [],
               const int    inpBarsOptimize,
               const int    inpPopSize,
               const int    numberFFlaunches,
               const double spread)
{
  //----------------------------------------------------------------------------
  int epochCount = numberFFlaunches / inpPopSize;

  C_AO_ESG AO;
  
  int    Population_P     = 200;   //Population size
  int    Groups_P         = 100;   //Number of groups
  double GroupRadius_P    = 0.1;   //Group radius
  double ExpansionRatio_P = 2.0;   //Expansion ratio
  double Power_P          = 10.0;  //Power
  
  AO.Init (ArraySize (set), Population_P, Groups_P, GroupRadius_P, ExpansionRatio_P, Power_P);
  
  for (int i = 0; i < ArraySize (set); i++)
  {
    AO.rangeMin  [i] = range_min  [i];
    AO.rangeStep [i] = range_step [i];
    AO.rangeMax  [i] = range_max  [i];
  }
  
  // Optimization-------------------------------------------------------------
  for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++)
  {
    AO.Moving ();
    
    for (int set = 0; set < ArraySize (AO.a); set++)
    {
      AO.a [set].f = VirtualStrategy (AO.a [set].c, inpBarsOptimize, spread);
    }

    AO.Revision ();
  }

  Print ("Fitness: ", AO.fB);
  ArrayCopy (set, AO.cB, 0, 0, WHOLE_ARRAY);
}
//——————————————————————————————————————————————————————————————————————————————

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


5. Тестирование функциональности

Протестируем наш самооптимизирующийся советник на основе индикатора "Stochastic" за период один год с параметрами, которые указаны ниже на скриншоте, сначала в режиме "false" параметра "SelfOptimization", т.е. без самооптимизации.

photo_

Рисунок 2. Настройки советника.

OriginalTest

Рисунок 3. Результаты с отключенной самооптимизацией.

SelfOpt

Рисунок 4. Результаты с включенной самооптимизацией.


Выводы

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

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

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

Walk-forward тестирование (WFT) - это методика оценки и проверки стратегий торговли на финансовых рынках. Он используется для определения эффективности и устойчивости торговых стратегий на исторических данных и их способности обеспечивать прибыльность в будущем.

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

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

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

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

Прикрепленные файлы |
Self_Optimization.zip (106.71 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (24)
Andrey Dik
Andrey Dik | 2 мар. 2024 в 18:30
Rorschach #:

Возможно это.

Вопрос был у Сабера в телеге, сейчас его там нету.

Хорошо, если есть интерес к теме влияния качества ГСЧ на поисковые показатели алгоритмов оптимизации, значит статья на эту тему будет полезна. Меня самого интересует этот вопрос, ответ на который не очевиден.
Rorschach
Rorschach | 2 мар. 2024 в 18:51
Andrey Dik #:
Хорошо, если есть интерес к теме влияния качества ГСЧ на поисковые показатели алгоритмов оптимизации, значит статья на эту тему будет полезна. Меня самого интересует этот вопрос, ответ на который не очевиден.

По статье на хабре, стратификация улучшает результат.

Точно не стоит использовать обычный алгоритм.

Еще в истории был случай, когда из-за плохого ГСЧ спутник улетел нетуда.

Так что тема должна быть интересной.

Andrey Dik
Andrey Dik | 2 мар. 2024 в 19:15
Rorschach #:

По статье на хабре, стратификация улучшает результат.

Точно не стоит использовать обычный алгоритм.

Еще в истории был случай, когда из-за плохого ГСЧ спутник улетел нетуда.

Так что тема должна быть интересной.

По приведённым ссылкам не обнаружил обсуждения влияние качества ГСЧ на АО. Но, тема от этого не становится менее интересной.

Rorschach
Rorschach | 2 мар. 2024 в 21:06
Andrey Dik #:

По приведённым ссылкам не обнаружил обсуждения влияние качества ГСЧ на АО. Но, тема от этого не становится менее интересной.

Да, влияние на АО не исследовано, ссылки только намекают, что качество ГСЧ важно.

Andrey Dik
Andrey Dik | 22 мар. 2024 в 17:42
Rorschach #:

Да, влияние на АО не исследовано, ссылки только намекают, что качество ГСЧ важно.

Вышла статья о влиянии качества ГСЧ на результаты алгоритмов оптимизации.
Разрабатываем мультивалютный советник (Часть 3): Ревизия архитектуры Разрабатываем мультивалютный советник (Часть 3): Ревизия архитектуры
Мы уже несколько продвинулись в разработке мультивалютного советника с несколькими параллельно работающими стратегиями. С учетом накопленного опыта проведем ревизию архитектуры нашего решения и попробуем ее улучшить, пока не ушли слишком далеко вперед.
Разметка данных в анализе временных рядов (Часть 3):Пример использования разметки данных Разметка данных в анализе временных рядов (Часть 3):Пример использования разметки данных
В этой серии статей представлены несколько методов разметки временных рядов, которые могут создавать данные, соответствующие большинству моделей искусственного интеллекта (ИИ). Целевая разметка данных может сделать обученную модель ИИ более соответствующей пользовательским целям и задачам, повысить точность модели и даже помочь модели совершить качественный скачок!
Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT) Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT)
В своих моделях мы часто используем различные алгоритмы внимание. И, наверное, чаще всего мы используем Трансформеры. Основным их недостатком является требование к ресурсам. В данной статье я хочу предложить Вам познакомиться с алгоритмом, который поможет снизить затраты на вычисления без потери качества.
Разработка системы репликации (Часть 28): Проект советника — класс C_Mouse (II) Разработка системы репликации (Часть 28): Проект советника — класс C_Mouse (II)
Когда начали создаваться первые системы, способные что-то считать, всё потребовало вмешательства инженеров, обладающих обширными знаниями о том, что проектируется. Мы говорим о рассвете компьютерной техники, о времени, когда не было даже терминалов, позволяющих что-либо программировать. По мере развития и роста интереса к тому, чтобы большее число людей могли создавать что-либо, появлялись новые идеи и методы программирования этих машин, которые раньше сводились к изменению положения соединителей. Именно тогда появились первые терминалы.