English 中文 Español Deutsch 日本語 Português
Управляемая оптимизация: метод отжига

Управляемая оптимизация: метод отжига

MetaTrader 5Тестер | 27 февраля 2018, 08:46
8 639 10
Aleksey Zinovik
Aleksey Zinovik

Введение

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

Алгоритм метода отжига

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

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

Процесс поиска оптимума целевой функции можно представить в виде следующего рисунка:

Поиск оптимума целевой функции

Рис. 1. Поиск оптимума целевой функции

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

Перейдем непосредственно к рассмотрению этапов алгоритма метода отжига. Для определенности будем рассматривать поиск глобального минимума целевой функции. Есть 3 основных варианта реализации алгоритма метода отжига: больцмановский отжиг, отжиг Коши (быстрый отжиг), сверхбыстрый отжиг. Они отличаются друг от друга способом генерации новой точки x(i) и законом уменьшения температуры.

Введем переменные, используемые в алгоритме:

  • Fopt — оптимальное значение целевой функции;
  • Fbegin — начальное значение целевой функции;
  • x(i) — значение текущей точки (параметра, от которого зависит значение целевой функции);
  • F(x(i)) — значение целевой функции для точки x(i);
  • i — счетчик итераций;
  • T0 — начальная температура;
  • T — текущая температура;
  • Xopt — значение параметра, при котором достигается оптимум целевой функции;
  • Tmin — минимальное значение температуры;
  • Imax — максимальное количество итераций.

Алгоритм метода отжига состоит из следующих этапов:

  • Этап 0. Инициализация алгоритма: Fopt = Fbegin, i=0, T=T0, Xopt = 0.
  • Этап 1. Случайный выбор текущей точки x(0) и вычисление целевой функции F(x(0)) для данной точки. ЕслиF(x(0))<Fbegin, то Fopt=F(x(0)).
  • Этап 2. Генерация новой точки x(i).
  • Этап 3. Вычисление целевой функции F(x(i)).
  • Этап 4. Проверка перехода в новое состояние. Далее рассматриваются два модификации алгоритма: 
    • a). Если переход в новое состояние выполнен, уменьшаем текущую температуру и переходим к этапу 5, иначе переходим к этапу 2.
    • b). Вне зависимости от результата проверки перехода в новое состояние уменьшаем текущую температуру и переходим к этапу 5.
  • Этап 5. Проверка критерия выхода из алгоритма (температура достигла минимального значения Tmin или достигнуто заданное количество итераций Imax). Если критерий выхода из алгоритма не выполняется: увеличиваем счетчик итераций (i=i+1) и переходим к этапу 2.

Рассмотрим подробнее каждый из этапов для случая поиска минимума целевой функции.

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

Этап 1. Под текущей точкой понимается значение параметра советника, который нужно оптимизировать. Таких параметров может быть несколько. Каждому параметру присваивается случайное значение, равномерно распределенное на интервале от Pmin до  Pmax с заданным шагом Step (Pmin, Pmax — минимальное и максимальное значение оптимизируемого параметра). Выполняется один прогон советника в тестере со сгенерированными параметрами и вычисляется значение целевой функции F(x(0)) — результата оптимизации параметров советника (значение заданного критерия оптимизации).  Если F(x(0))<Fbegin, Fopt=F(x(0)).

Этап 2. Генерация новой точки в зависимости от варианта реализации алгоритма выполняется по формулам, приведенным в таблице 1.

Таблица 1

Вариант реализации алгоритма Формула для вычисления новой начальной точки
Больцмановский отжиг Формулы для вычисления новой начальной точки. Больцмановский отжиг, где N(0,1) — стандартное нормальное распределение 
Отжиг Коши (быстрый отжиг) Формулы для вычисления новой начальной точки. Отжиг Коши, где C(0,1) — распределение Коши
Сверхбыстрый отжиг Формулы для вычисления новой начальной точки. Сверхбыстрый отжиг, где Pmax, Pmin — минимальное и максимальное значение оптимизируемого параметра,
величина Zвычисляется по следующей формуле:
Сверхбыстрый отжиг. Величина z, где a — случайная величина, равномерно распределенная на интервале [0,1),
Sign

Этап 3. Выполняется прогон советника в тестере с параметрами, сгенерированными на этапе 2. Целевой функции F(x(i)) присваивается значение выбранного критерия оптимизации. 

Этап 4. Проверка перехода в новое состояние выполняется следующим образом:

  • Шаг 1. Если F(x(i))<Fopt, переходим в новое состояние Xopt =x(i), Fopt=F(x(i)), иначе переходим к шагу 2.
  • Шаг 2. Генерируем случайную величину a, равномерно распределенную на интервале [0,1).
  • Шаг 3. Вычисляем вероятность перехода в новое состояние: Вероятность
  • Шаг 4. Если P>a, то переходим в новое состояние Xopt =x(i), Fopt=F(x(i)), иначе, если выбрана модификация алгоритма а), переходим к этапу 2.
  • Шаг 5. Уменьшаем текущую температуру по формулам, приведенным в таблице 2.

Таблица 2

Вариант реализации алгоритма Формула для уменьшения температуры
Больцмановский отжиг  Закон уменьшения температуры для варианта 1


Отжиг Коши (быстрый отжиг) Закон уменьшения температуры для варианта 2, где n — количество параметров, значения которых оптимизируются
Сверхбыстрый отжиг Закон уменьшения температуры для варианта 3,
где c(i)>0 и вычисляется по следующей формуле:
Вычисление c, где m(i), p(i) — дополнительные параметры алгоритма.
Как правило, для простоты настройки алгоритма значения параметров m(i) и p(i) не изменяются в процессе работы алгоритма: m(i)=const, p(i) = const 

Этап 5. Выход из алгоритма происходит при выполнении следующих условий: T(i)<=Tmin или i=Imax.

  • Если выбран закон изменения температуры, при котором температура уменьшается очень быстро, то предпочтительнее завершить работу алгоритма при T(i)<=Tmin, не дожидаясь завершения всех итераций.
  • Если температура уменьшается очень медленно, то выход из алгоритма будет выполнен при достижении максимального количества итераций. Скорее всего, в этом случае следует изменить параметры закона уменьшения температуры.

Рассмотрев подробно все этапы работы алгоритма, перейдем к его реализации на языке MQL5.

Реализация алгоритма

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

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

  • класс AnnealingMethod.mqh — содержит набор методов, реализующих отдельные этапы алгоритма;
  • класс FrameAnnealingMethod.mqh — содержит методы для работы графического интерфейса, отображаемого в окне графика терминала.

Также для работы алгоритма требуется включить дополнительный код в функцию OnInit и добавить в код советника функции OnTester, OnTesterInit, OnTesterDeInit,  OnTesterPass. Процесс подключения алгоритма к советнику показан на рис. 2.


Рис. 2. Подключение алгоритма к советнику

Перейдем к описанию классов AnnealingMethod и FrameAnnealingMethod.

Класс AnnealingMethod

Приведем описание класса AnnealingMethod и рассмотрим подробнее его методы.

#include "Math/Alglib/alglib.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class AnnealingMethod
  {
private:
   CAlglib           Alg;                   //экземпляр класса для работы с методами библиотеки Alglib
   CHighQualityRandStateShell state;        //экземпляр класса для генерации случайных чисел
public:
                     AnnealingMethod();
                    ~AnnealingMethod();
   struct Input                             //структура для работы с параметрами советника
     {
      int               num;
      double            Value;
      double            BestValue;
      double            Start;
      double            Stop;
      double            Step;
      double            Temp;
     };
   uint              RunOptimization(string &InputParams[],int count,double F0,double T);
   uint              WriteData(Input &InpMass[],double F,int it);
   uint              ReadData(Input &Mass[],double &F,int &it);
   bool              GetParams(int Method,Input &Mass[]);
   double            FindValue(double val,double step);
   double            GetFunction(int Criterion);
   bool              Probability(double E,double T);
   double            GetT(int Method,double T0,double Tlast,int it,double D,double p1,double p2);
   double            UniformValue(double min,double max,double step);
   bool              VerificationOfVal(double start,double end,double val);
   double            Distance(double a,double b);
  };

Для работы методов класса AnnealingMethod используются функции работы со случайными величинами из библиотеки ALGLIB. Эта библиотека входит в стандартный комплект поставки терминала MetaTrader 5 и находится в папке Include/Math/Alglib, как показано на рисунке:

alglib

Рис. 3. Библиотека  ALGLIB

В блоке Private объявлены экземпляры классов CAlglib и CHighQualityRandStateShell для работы с функциями библиотеки ALGLIB.

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

  • номер параметра num;
  • текущее значение параметра Value;
  • наилучшее значение параметра BestValue;
  • начальное значение Start;
  • конечное значение Stop;
  • шаг изменения значения параметра Step;
  • текущая температура для данного параметра Temp.

Рассмотрим методы класса AnnealingMethod.mqh.

Метод RunOptimization

Предназначен для инициализации алгоритма метода отжига. Код метода:

uint AnnealingMethod::RunOptimization(string &InputParams[],int count,double F0,double T)
  {
   Input Mass[];
   ResetLastError();
   bool Enable=false;
   double Start= 0;
   double Stop = 0;
   double Step = 0;
   double Value= 0;
   int j=0;
   Alg.HQRndRandomize(&state);                //инициализация
   for(int i=0;i<ArraySize(InputParams);i++)
     {
      if(!ParameterGetRange(InputParams[i],Enable,Value,Start,Step,Stop))
         return GetLastError();
      if(Enable)
        {
         ArrayResize(Mass,ArraySize(Mass)+1);
         Mass[j].num=i;
         Mass[j].Value=UniformValue(Start,Stop,Step);
         Mass[j].BestValue=Mass[j].Value;
         Mass[j].Start=Start;
         Mass[j].Stop=Stop;
         Mass[j].Step=Step;
         Mass[j].Temp=T*Distance(Start,Stop);
         j++;
         if(!ParameterSetRange(InputParams[i],false,Value,Start,Stop,count))
            return GetLastError();
        }
      else
         InputParams[i]="";
     }
   if(j!=0)
     {
      if(!ParameterSetRange("iteration",true,1,1,1,count))
         return GetLastError();
      else
         return WriteData(Mass,F0,1);
     }
   return 0;
  }

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

  • строковый массив имен всех параметров советника InputParams[] ;
  • количество итераций алгоритма count ;
  • начальное значение целевой функции F0 ;
  • начальная температура T.

Метод RunOptimization работает следующим образом:

  • выполняется поиск параметров советника, которые необходимо оптимизировать. Такие параметры должны быть отмечены "галочкой" на вкладке "Параметры" тестера стратегий;
  • значения каждого найденного параметра сохраняются в массив структур Mass[] типаInput, и параметр исключается из оптимизации. В массиве структур Mass[] хранятся:
    • номер параметра;
    • значение параметра, сгенерированное методом UniformValue (будет рассмотрен ниже);
    • максимальное (Start) и максимальное (Stop) значение параметра;
    • шаг изменения значения параметра (Step);
    • начальная температура, вычисляемая по формуле: T*Distance(Start,Stop), метод Distance будет рассмотрен ниже.
  • после завершения поиска все параметры оказываются отключенными и включается параметр iteration, определяющий число итераций алгоритма;
  • значения массива Mass[], целевая функция и номер итерации записываются в бинарный файл с помощью метода WriteData. 

Метод WriteData

Предназначен для записи массива параметров, значения целевой функции и номера итерации в файл.

Код метода WriteData:

uint AnnealingMethod::WriteData(Input &Mass[],double F,int it)
  {
   ResetLastError();
   int file_handle=0;
   int i=0;
   do
     {
      file_handle=FileOpen("data.bin",FILE_WRITE|FILE_BIN);
      if(file_handle!=INVALID_HANDLE) break;
      else
        {
         Sleep(MathRand()%10);
         i++;
         if(i>100) break;
        }
     }
   while(file_handle==INVALID_HANDLE);
   if(file_handle!=INVALID_HANDLE)
     {
      if(FileWriteArray(file_handle,Mass)<=0)
        {FileClose(file_handle); return GetLastError();}
      if(FileWriteDouble(file_handle,F)<=0)
        {FileClose(file_handle); return GetLastError();}
      if(FileWriteInteger(file_handle,it)<=0)
        {FileClose(file_handle); return GetLastError();}
     }
   else
      return GetLastError();
   FileClose(file_handle);
   return 0;
  }

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

Метод ReadData

Предназначен для считывания из файла data.bin массива параметров, значения целевой функции и номера итерации. Код метода ReadData:

uint AnnealingMethod::ReadData(Input &Mass[],double &F,int &it)
  {
   ResetLastError();
   int file_handle=0;
   int i=0;
   do
     {
      file_handle=FileOpen("data.bin",FILE_READ|FILE_BIN);
      if(file_handle!=INVALID_HANDLE) break;
      else
        {
         Sleep(MathRand()%10);
         i++;
         if(i>100) break;
        }
     }
   while(file_handle==INVALID_HANDLE);
   if(file_handle!=INVALID_HANDLE)
     {
      if(FileReadArray(file_handle,Mass)<=0)
        {FileClose(file_handle); return GetLastError();}
      F=FileReadDouble(file_handle);
      it=FileReadInteger(file_handle);
     }
   else
      return GetLastError();
   FileClose(file_handle);
   return 0;
  }

Данные считываются из файла с помощью функций FileReadArray,FileReadDouble,FileReadInteger в той же последовательности, как они были записаны методом WriteData. 

Метод GetParams

Метод GetParams предназначен для вычисления новых значений оптимизируемых параметров советника, для которых будет выполнен очередной прогон советника. Формулы для вычисления новых значений оптимизируемых параметров советника приведены в таблице 1.

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

  • вариант реализации алгоритма (Больцмановский отжиг, отжиг Коши или сверхбыстрый отжиг);
  • массив оптимизируемых параметров типа Input;
  • коэффициент CoeffTmin для расчета минимальной температуры, при достижении которой алгоритм завершает работу.

Код метода GetParams:

bool AnnealingMethod::GetParams(int Method,Input &Mass[],double CoeffTmin)
  {
   double delta=0;
   double x1=0,x2=0;
   double count=0;

   Alg.HQRndRandomize(&state);         //инициализация
   switch(Method)
     {
      case(0):
        {
         for(int i=0;i<ArraySize(Mass);i++)
           {
            if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
              {
               do
                 {
                  if(count==100)
                    {
                     delta=Mass[i].Value;
                     count= 0;
                     break;
                    }
                  count++;
                  delta=Mass[i].Temp*Alg.HQRndNormal(&state);
                  delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
                 }
               //  while((delta<Mass[i].Start) || (delta>Mass[i].Stop));
               while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
               Mass[i].Value=delta;
              }
           }
         break;
        }
      case(1):
        {
         for(int i=0;i<ArraySize(Mass);i++)
           {
            if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
              {
               do
                 {
                  if(count==100)
                    {
                     delta=Mass[i].Value;
                     count=0;
                     break;
                    }
                  count++;
                  Alg.HQRndNormal2(&state,x1,x2);
                  delta=Mass[i].Temp*x1/x2;
                  delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
                 }
               while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
               Mass[i].Value=delta;
              }
           }
         break;
        }
      case(2):
        {
         for(int i=0;i<ArraySize(Mass);i++)
           {
            if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
              {
               do
                 {
                  if(count==100)
                    {
                     delta=Mass[i].Value;
                     count=0;
                     break;
                    }
                  count++;
                  x1=Alg.HQRndUniformR(&state);
                  if(x1-0.5>0)
                     delta=Mass[i].Temp*(MathPow(1+1/Mass[i].Temp,MathAbs(2*x1-1))-1)*Distance(Mass[i].Start,Mass[i].Stop);
                  else
                    {
                     if(x1==0.5)
                        delta=0;
                     else
                        delta=-Mass[i].Temp*(MathPow(1+1/Mass[i].Temp,MathAbs(2*x1-1))-1)*Distance(Mass[i].Start,Mass[i].Stop);
                    }
                  delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
                 }
               while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
               Mass[i].Value=delta;
              }
           }
         break;
        }
      default:
        {
         Print("Annealing method was chosen incorrectly");
         return false;
        }
     }
   return true;
  }

Рассмотрим код этого метода более подробно.

В методе организован оператор-переключатель Switсh, который запускает вычисление новых значений параметров в зависимости от выбранного варианта реализации алгоритма. Вычисление новых значений параметров выполняется, только если текущая температура выше минимальной. Минимальная температура рассчитывается по формуле: CoeffTmin*Distance(Start,Stop), где Start, Stop — минимальное и максимальное значение параметра. Метод Distance будет рассмотрен ниже.

Для инициализации методов работы со случайными числами вызывается метод HQRndRandomize класса CAlglib:

 Alg.HQRndRandomize(&state);

Для вычисления значения стандартного нормального распределения используется функция HQRndNormal класса CAlglib:

Alg.HQRndNormal(&state);

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

C(0,1)=X1/X2, где X1 и X2 — независимые нормальные величины, X1,X2 = N(0,1). Для генерации двух независимых нормально распределенных величин используется функция HQRndNormal2 класса CAlglib:

 Alg.HQRndNormal2(&state,x1,x2);

Значения независимых нормально распределенных величин сохраняются в переменные x1,x2.

С помощью метода HQRndUniformR(&state) класса CAlglibгенерируется случайное число, равномерно распределенное в интервале от 0 до 1:

Alg.HQRndUniformR(&state);

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

Метод FindValue

Значение каждого оптимизируемого параметра должно изменяться с заданным шагом. Новое значение параметра, сформированное в методе GetParams, может не соответствовать этому условию, и его нужно округлить до значения, кратного заданному шагу. Для этого используется метод FindValue. Входные параметры метода: значение, которое нужно округлить (val), и шаг изменения параметра (step).

Приведем код метода FindValue:

double AnnealingMethod::FindValue(double val,double step)
  {
   double buf=0;
   if(val==step)
      return val;
   if(step==1)
      return round(val);
   else
     {

      buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step);
      if(MathAbs(val)-buf*MathAbs(step)>=MathAbs(step)/2)
        {
         if(val<0)
            return -(buf + 1)*MathAbs(step);
         else
            return (buf + 1)*MathAbs(step);
        }
      else
        {
         if(val<0)
            return -buf*MathAbs(step);
         else
            return buf*MathAbs(step);
        }
     }
  }

Рассмотрим код метода более подробно.

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

   if(val==step)
      return val;

Если шаг равен 1, требуется только округлить до целого входное значение параметра:

   if(step==1)
      return round(val);

Иначе находим количество шагов во входном значении параметра:

buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step);

и вычисляем новое значение, кратное шагу step.

Метод GetFunction

Метод GetFunction предназначен для получения нового значения целевой функции. Входной параметр метода — выбранный пользователем критерий оптимизации.

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

double AnnealingMethod::GetFunction(int Criterion)
  {
   double Fc=0;
   switch(Criterion)
     {
      case(0):
         return TesterStatistics(STAT_PROFIT);
      case(1):
         return TesterStatistics(STAT_PROFIT_FACTOR);
      case(2):
         return TesterStatistics(STAT_RECOVERY_FACTOR);
      case(3):
         return TesterStatistics(STAT_SHARPE_RATIO);
      case(4):
         return TesterStatistics(STAT_EXPECTED_PAYOFF);
      case(5):
         return TesterStatistics(STAT_EQUITY_DD);//min
      case(6):
         return TesterStatistics(STAT_BALANCE_DD);//min
      case(7):
         return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_PROFIT_FACTOR);
      case(8):
         return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_RECOVERY_FACTOR);
      case(9):
         return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_SHARPE_RATIO);
      case(10):
         return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_EXPECTED_PAYOFF);
      case(11):
        {
         if(TesterStatistics(STAT_BALANCE_DD)>0)
            return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DD);
         else
            return TesterStatistics(STAT_PROFIT);
        }
      case(12):
        {
         if(TesterStatistics(STAT_EQUITY_DD)>0)
            return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_EQUITY_DD);
         else
            return TesterStatistics(STAT_PROFIT);
        }
      case(13):
        {
         //укажите пользовательский критерий, например
         return TesterStatistics(STAT_TRADES)*TesterStatistics(STAT_PROFIT);
        }
      default: return -10000;
     }
  }

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

Метод Probability

Метод Probability предназначен для определения перехода в новое состояние. Входные параметры метода: разность между предыдущим и текущим значениями целевой функции (E) и текущая температура (T). Код метода:

bool AnnealingMethod::Probability(double E,double T)
  {
   double a=Alg.HQRndUniformR(&state);
   double res=exp(-E/T);
   if(res<=a)
      return false;
   else
      return true;
  }

В методе генерируется случайное значение а, равномерно распределенное в интервале от 0 до 1:

a=Alg.HQRndUniformR(&state);

Полученное значение сравнивается с выражением exp(-E/T). Если a>exp(-E/T), то метод возвращает true (переход в новое состояние выполнен).

Метод GetT

Метод GetT вычисляет новое значение температуры. Входные параметры метода:

  • вариант реализации алгоритма (Больцмановский отжиг, отжиг Коши или сверхбыстрый отжиг);
  • начальное значение температуры T0;
  • предыдущее значение температуры Tlast;
  • номер итерации it;
  • количество оптимизируемых параметров D;
  • вспомогательные параметры p1 и p2 для сверхбыстрого отжига.

Код метода:

double AnnealingMethod::GetT(int Method,double T0,double Tlast,int it,double D,double p1,double p2)
  {
   int Iteration=0;
   double T=0;
   switch(Method)
     {
      case(0):
        {
         if(Tlast!=T0)
            Iteration=(int)MathRound(exp(T0/Tlast)-1)+1;
         else
            Iteration=1;
         if(Iteration>0)
            T=T0/log(Iteration+1);
         else
            T=T0;
         break;
        }
      case(1):
        {
         if(it!=1)
            Iteration=(int)MathRound(pow(T0/Tlast,D))+1;
         else
            Iteration=1;
         if(Iteration>0)
            T=T0/pow(Iteration,1/D);
         else
            T=T0;
         break;
        }
      case(2):
        {
         if((T0!=Tlast) && (-p1*exp(-p2/D)!=0))
            Iteration=(int)MathRound(pow(log(Tlast/T0)/(-p1*exp(-p2/D)),D))+1;
         else
            Iteration=1;
         if(Iteration>0)
            T=T0*exp(-p1*exp(-p2/D)*pow(Iteration,1/D));
         else
            T=T0;
         break;
        }
     }
   return T;
  }

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

Метод UniformValue

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

  • максимальное значение параметра max;
  • минимальное значение параметра min;
  • шаг изменения параметра step.

Код метода:

double AnnealingMethod::UniformValue(double min,double max,double step)
  {
   Alg.HQRndRandomize(&state);       //инициализация
   if(max>min)
      return FindValue(Alg.HQRndUniformR(&state)*(max-min)+min,step);
   else
      return FindValue(Alg.HQRndUniformR(&state)*(min-max)+max,step);
  }

Метод VerificationOfVal

Метод VerificationOfVal проверяет, не выходит ли заданное значение переменной (val) за границы диапазона (start,end). Данный метод используется в методе GetParams.

Код метода:

bool AnnealingMethod::VerificationOfVal(double start,double end,double val)
  {
   if(start<end)
     {
      if((val>=start) && (val<=end))
         return true;
      else
         return false;
     }
   else
     {
      if((val>=end) && (val<=start))
         return true;
      else
         return false;
     }
  }

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

Метод Distance

Метод Distance вычисляет расстояние между двумя параметрами (a и b) и используется в алгоритме для вычисления размера диапазона изменения параметра с начальным значением a и конечным значением b.

>Код метода:

double AnnealingMethod::Distance(double a,double b)
  {
   if(a<b)
      return MathAbs(b-a);
   else
      return MathAbs(a-b);
  }

Класс FrameAnnealingMethod

Класс FrameAnnealingMethod предназначен для отображения в окне терминала процесса выполнения алгоритма. Приведем описание класса FrameAnnealingMethod:

#include <SimpleTable.mqh>
#include <Controls\BmpButton.mqh>
#include <Controls\Label.mqh>
#include <Controls\Edit.mqh>
#include <AnnealingMethod.mqh>
//+------------------------------------------------------------------+
//|  Класс для вывода результатов оптимизации                        |
//+------------------------------------------------------------------+
class FrameAnnealingMethod
  {
private:
   CSimpleTable      t_value;
   CSimpleTable      t_inputs;
   CSimpleTable      t_stat;
   CBmpButton        b_playbutton;
   CBmpButton        b_backbutton;
   CBmpButton        b_forwardbutton;
   CBmpButton        b_stopbutton;
   CLabel            l_speed;
   CLabel            l_stat;
   CLabel            l_value;
   CLabel            l_opt_value;
   CLabel            l_temp;
   CLabel            l_text;
   CLabel            n_frame;
   CEdit             e_speed;
   long              frame_counter;

public:
   //--- конструктор/деструктор
                     FrameAnnealingMethod();
                    ~FrameAnnealingMethod();
   //--- события тестера стратегий
   void              FrameTester(double F,double Fbest,Input &Mass[],int num,int it);
   void              FrameInit(string &SMass[]);
   void              FrameTesterPass(int cr);
   void              FrameDeinit(void);
   void              FrameOnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam,int cr);
   uint              FrameToFile(int count);
  };

Класс FrameAnnealingMethod содержит следующие методы:

  • FrameInit — предназначен для создания графического интерфейса в окне терминала;
  • FrameTester — предназначен для добавления текущего фрейма данных;
  • FrameTesterPass — вывода в окно терминала текущего фрейма данных;
  • FrameDeInit — предназначен для отображения текстовой информации о завершении оптимизации советника;
  • FrameOnChartEvent — предназначен для обработки событий нажатия кнопок;
  • FrameToFile — предназначен для сохранения результатов тестирования в текстовый файл.

Код методов приведен в файле FrameAnnealingMethod.mqh (приложен к статье). Отметим, что для работы методов класса FrameAnnealingMethod потребуется файл SimpleTable.mqh (приложен к статье), который необходимо переместить в папку MQL5/Include. Файл заимствован из этого проекта и дополнен методом GetValue, позволяющий считывать значение из ячейки таблицы.

Приведем пример графического интерфейса, созданного в окне терминала с помощью класса FrameAnnealingMethod.


Рис. 4. Графический интерфейс для демонстрации работы алгоритма

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

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

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

Мы рассмотрели классы AnnealingMethod и FrameAnnealingMethod. Теперь перейдем к тестированию алгоритма на примере советника Moving Average.

Тестирование алгоритма на советнике Moving Average

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

Для работы алгоритма необходимо модифицировать код советника:

  • подключить классы AnnealingMethod и FrameAnnealingMethod и объявить вспомогательные переменные для работы алгоритма;
  • добавить код в функции OnInit, добавить функции OnTester, OnTesterInit, OnTesterDeInit,  OnTesterPass, OnChartEvent.

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

Итак, начнем.

Подключим файл с начальными параметрами, сформированный функцией OnTesterInit:

#property tester_file "data.bin"

Подключим классы AnnealingMethod и FrameAnnealingMethod:

//подключения классов
#include <AnnealingMethod.mqh>
#include <FrameAnnealingMethod.mqh>

Объявим экземпляры подключенных классов:

AnnealingMethod Optim;
FrameAnnealingMethod Frame;

Объявим вспомогательные переменные для работы алгоритма:

Input InputMass[];            //массив входных параметров
string SParams[];             //массив имен входных параметров
double Fopt=0;                //наилучшее значений функции
int it_agent=0;               //номер итерации алгоритма для агента тестирования
uint alg_err=0;               //номер ошибки

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

double MaximumRisk_Optim=MaximumRisk;
double DecreaseFactor_Optim=DecreaseFactor;
int MovingPeriod_Optim=MovingPeriod;
int MovingShift_Optim=MovingShift;

Во всех функциях советника заменим параметры: MaximumRisk наMaximumRisk_Optim, DecreaseFactor на DecreaseFactor_Optim, MovingPeriod на MovingPeriod_Optim,  MovingShift наMovingShift_Optim

Введем переменные для настройки работы алгоритма:

sinput int iteration=50;         //Количество итераций
sinput int method=0;             //0 - Больцмановский отжиг,1 - отжиг Коши, 2 - сверхбыстрый отжиг 
sinput double CoeffOfTemp=1;     //Масштабный коэффициент для начальной температуры
sinput double CoeffOfMinTemp=0;  //Коэффициент для минимальной температуры
sinput double Func0=-10000;      //Начальное значение целевой функции
sinput double P1=1;              //Дополнительный параметр для сверхбыстрого отжига p1
sinput double P2=1;              //Дополнительный параметр для сверхбыстрого отжига p2
sinput int Crit=0;               //Метод вычисления целевой функции
sinput int ModOfAlg=0;           //Тип модификаций алгоритма
sinput bool ManyPoint=false;     //Оптимизация из нескольких точек

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

В таблице 3 поясним назначение объявленных переменных.

Таблица 3

Имя переменной Назначение
iteration Задает количество итерации алгоритма
method Задает вариант реализации алгоритма: 0 — Больцмановский отжиг, 1 — отжиг Коши, 2 — сверхбыстрый отжиг 
CoeffOfTemp Задает коэффициент для установки начальной температуры, которая вычисляется по формуле: T0=CoeffOfTemp*Distance(Start,Stop), где Start, Stop — минимальное и максимальное значение параметра, Distance — метод класса AnnealingMethod (описан выше)
CoeffOfMinTemp Задает коэффициент для установки минимальной температуры, при достижении которой алгоритм завершает работу. Минимальная температура вычисляется аналогично начальной температуре: Tmin=CoeffOfMinTemp*Distance(Start,Stop), где Start, Stop — минимальное и максимальное значение параметра, Distance — метод класса AnnealingMethod (описан выше)
Func0 Начальное значение целевой функции
P1,P2 Параметры, используемые для вычисления текущей температуры для сверхбыстрого отжига (см. Таблицу 2) 
Crit Критерий оптимизации:
0 — чистая прибыль;
1 — прибыльность;
2 — фактор восстановления;
3 — коэффициент Шарпа;
4 —мат. ожидание выигрыша;
5 — максимальная просадка по средствам;
6 — максимальная просадка по балансу;
7 — чистая прибыль + прибыльность;
8 — чистая прибыль + фактор восстановления;
9 — чистая прибыль + коэффициент Шарпа;
10 — чистая прибыль + мат. ожидание выигрыша;
11 — чистая прибыль + максимальная просадка по балансу;
12 — чистая прибыль + максимальная просадка по средствам;
13 — пользовательский критерий.
Вычисление целевой функции выполняется в методе GetFunction класса AnnealingMethod
ModOfAlg  Тип модификации алгоритма:
0 - если переход в новое состояние выполнен, уменьшаем текущую температуру и переходим к проверке завершения работы алгоритма, иначе вычисляем новые значения оптимизируемых параметров;
1 - вне зависимости от результата проверки перехода в новое состояние уменьшаем текущую температуру и переходим к проверке завершения работы алгоритма
ManyPoint  true — для каждого агента тестирования будут сформированы различные начальные значения оптимизируемых параметров,
false — для каждого агента тестирования будут сформированы одинаковые начальные значения оптимизируемых параметров

Добавляем код в начало функции OnInit:

//+------------------------------------------------------------------+
//|Метод отжига                                                      |
//+------------------------------------------------------------------+
 if(MQL5InfoInteger(MQL5_OPTIMIZATION))
    {
     //открываем файл и считываем данные
     //  if(FileGetInteger("data.bin",FILE_EXISTS,false))
     //  {
         alg_err=Optim.ReadData(InputMass,Fopt,it_agent);
         if(alg_err==0)
           {
            //Если первый запуск, генерируем параметры случайно, если поиск выполняется из разных точек
            if(Fopt==Func0)
              {
               if(ManyPoint)
                  for(int i=0;i<ArraySize(InputMass);i++)
                    {
                     InputMass[i].Value=Optim.UniformValue(InputMass[i].Start,InputMass[i].Stop,InputMass[i].Step);
                     InputMass[i].BestValue=InputMass[i].Value;
                    }
              }
            else
               Optim.GetParams(method,InputMass,CoeffOfMinTemp);    //генерируем новые параметры
            //заполняем параметры советника
            for(int i=0;i<ArraySize(InputMass);i++)
               switch(InputMass[i].num)
                 {
                  case (0): {MaximumRisk_Optim=InputMass[i].Value; break;}
                  case (1): {DecreaseFactor_Optim=InputMass[i].Value; break;}
                  case (2): {MovingPeriod_Optim=(int)InputMass[i].Value; break;}
                  case (3): {MovingShift_Optim=(int)InputMass[i].Value; break;}
                 }
           }
         else
           {
            Print("Error reading file");
            return(INIT_FAILED);
           }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

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

if(MQL5InfoInteger(MQL5_OPTIMIZATION))

Далее выполняется считывание данных из файла data.bin, сформированного методом RunOptimization класса AnnealingMethod. Этот метод вызывается в функции OnTesterInit, код функции будет приведен ниже.

alg_err=Optim.ReadData(InputMass,Fopt,it_agent);

Если данные считались без ошибок (alg_err=0), выполняется проверка нахождения алгоритма на первой итерации (Fopt==Func0), иначе инициализация советника завершится ошибкой. Если итерация первая, то при ManyPoint = true сформируем начальные значения оптимизируемых параметров и записываем их в InputMass структуры Input (описана в классе AnnealingMethod), иначе вызывается метод GetParams

 Optim.GetParams(method,InputMass,CoeffOfMinTemp);//генерируем новые параметры

и заполняются значения параметров  MaximumRisk_Optim, DecreaseFactor_Optim, MovingPeriod_Optim, MovingShift_Optim.

Теперь рассмотрим код функции OnTesterInit:

void OnTesterInit()
  {
  //заполняем массив имен всех параметров советника
   ArrayResize(SParams,4);
   SParams[0]="MaximumRisk";
   SParams[1]="DecreaseFactor";
   SParams[2]="MovingPeriod";
   SParams[3]="MovingShift";
   //запускаем оптимизацию
   Optim.RunOptimization(SParams,iteration,Func0,CoeffOfTemp);
   //создаем графический интерфейс
   Frame.FrameInit(SParams);
  }

Сначала заполним строковый массив, содержащий имена всех параметров советника. Далее запустим метод RunOptimization и создадим графический интерфейс методом FrameInit.

После прогона советника на заданном временном интервале управление будет передано в функцию OnTester. Рассмотрим ее код:

double OnTester()
  {
   int i=0;                                                       //счетчик цикла
   int count=0;                                                   //вспомогательная переменная
  //проверка на завершение алгоритма при достижении минимальной температуры
   for(i=0;i<ArraySize(InputMass);i++)
      if(InputMass[i].Temp<CoeffOfMinTemp*Optim.Distance(InputMass[i].Start,InputMass[i].Stop))
         count++;
   if(count==ArraySize(InputMass))
      Frame.FrameTester(0,0,InputMass,-1,it_agent);               //добавляем новый фрейм c нулевыми параметрами и id=-1 
   else
     {
      double Fnew=Optim.GetFunction(Crit);                        //вычислим текущее значение функции
      if((Crit!=5) && (Crit!=6) && (Crit!=11) && (Crit!=12))      //если требуется максимизировать целевую функцию
        {
         if(Fnew>Fopt)
            Fopt=Fnew;
         else
           {
            if(Optim.Probability(Fopt-Fnew,CoeffOfTemp*InputMass[0].Temp/Optim.Distance(InputMass[0].Start,InputMass[0].Stop)))
               Fopt=Fnew;
           }
        }
      else                                                        //если требуется минимизировать целевую функцию
        {
         if(Fnew<Fopt)
            Fopt=Fnew;
         else
           {
            if(Optim.Probability(Fnew-Fopt,CoeffOfTemp*InputMass[0].Temp/Optim.Distance(InputMass[0].Start,InputMass[0].Stop)))
               Fopt=Fnew;
           }
        }
      //перезапишем наилучшие значения параметров
      if(Fopt==Fnew)
         for(i=0;i<ArraySize(InputMass);i++)
            InputMass[i].BestValue=InputMass[i].Value;
      //уменьшаем температуру
      if(((ModOfAlg==0) && (Fnew==Fopt)) || (ModOfAlg==1))
        {
         for(i=0;i<ArraySize(InputMass);i++)
            InputMass[i].Temp=Optim.GetT(method,CoeffOfTemp*Optim.Distance(InputMass[i].Start,InputMass[i].Stop),InputMass[i].Temp,it_agent,ArraySize(InputMass),P1,P2);
        }
      Frame.FrameTester(Fnew,Fopt,InputMass,iteration,it_agent);          //добавляем новый фрейм
      it_agent++;                                                         //увеличиваем счетчик итераций    
      alg_err=Optim.WriteData(InputMass,Fopt,it_agent);                   //записываем новые значения в файл
      if(alg_err!=0)
         return alg_err;
     }
   return Fopt;
  }

Рассмотрим код функции более подробно.

  • Выполняется проверка на завершение алгоритма при достижении минимальной температуры. Если температура каждого параметра достигла минимального значения, добавляется фрейм с id=-1, значения параметров больше не изменяются. В графическом интерфейсе окна терминала пользователю предлагается завершить оптимизацию кнопкой "Стоп" в тестере стратегий. 
  • С помощью метода GetFunction вычисляется новое значение целевой функции Fnew, используя результаты прогона советника.
  • В зависимости от критерия оптимизации (см. таблицу 3), значение Fnew сравнивается с наилучшим значением Fopt, и выполняется проверка перехода в новое состояние.
  • Если переход в новое состояние выполнен, текущие значения оптимизируемых параметров становятся наилучшими:
 for(i=0;i<ArraySize(InputMass);i++)
         InputMass[i].BestValue = InputMass[i].Value;
  • Выполняется проверка условия на уменьшение текущей температуры, при его выполнении новая температура вычисляется с помощью метода GetT класса AnnealingMethod.
  • Добавляется новый фрейм, значения оптимизируемых параметров записываются в файл.

В функции OnTester мы добавляем фреймы для их последующей обработки в функции OnTesterPass. Рассмотрим ее код:

void OnTesterPass()
  {
      Frame.FrameTesterPass(Crit);//метод отображения фреймов в графическом интерфейсе
  }

В функции OnTesterPass вызывается метод FrameTesterPass класса FrameAnnealingMethod для отображения процесса оптимизации в окне терминала.

После завершения оптимизации вызывается функция OnTesterDeInit:

void OnTesterDeinit()
  {
   Frame.FrameToFile(4);
   Frame.FrameDeinit();
  }

В данной функции вызываются два метода класса FrameAnnealingMethod: FrameToFile и FrameDeinit. В методе FrameToFile выполнятся запись результатов оптимизации в текстовый файл. Входной параметр этого метода — количество оптимизируемых параметров советника. Метод FrameDeinit в окне терминала выводит пользователю сообщение о завершении оптимизации.

После завершения оптимизации графический интерфейс, созданный с помощью методов класса  FrameAnnealingMethod, позволяет проиграть фреймы с заданной скоростью. Можно останавливать воспроизведение фреймов и запускать заново. Для этого предусмотрены соответствующие кнопки графического интерфейса (см. рис. 4). Для обработки событий в окне терминала в код советника добавлен метод OnChartEvent:

void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Frame.FrameOnChartEvent(id,lparam,dparam,sparam,Crit); //метод работы с графическим интерфейсом 
  }

В методе OnChartEvent вызывается метод FrameOnChartEvent класса FrameAnnealingMethod, который управляет воспроизведением фреймов в окне терминала.

На этом модификация кода советника MovingAverage завершена. Приступим к тестированию алгоритма.

Тестирование алгоритма

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

Для тестирования алгоритма будет использоваться советник TestAnnealing.mq5 (приложен к статье в архиве test.zip). Он загружает таблицу результатов оптимизации, полученных методом полного перебора из текстового файла, содержащего 5 столбцов с данными: столбцы 1-4 — значения переменных, столбец 5 — значения целевой функции. Алгоритм, реализованный в советнике TestAnnealing, с помощью метода отжига будет перещаться по таблице и находить значения целевой функции. Этот способ тестирования позволяет проверить работу метода отжига на различных данных, полученных методом полного перебора.

Итак, приступим. Сначала проверим работу алгоритма на примере оптмизации одной переменной советника — Moving Average period.

Запустим оптимизацию советника в режиме полного перебора со следующими исходными параметрами:

  • Maximum Risk in percentage — 0,02; Descrease factor — 3;Moving Average period: 1 - 120, шаг: 1; Moving Average shift — 6.
  • Период: 01.01.2017 — 31.12.2017, режим торговли — без задержки, тики: OHLC на M1, начальный депозит 10000, плечо — 1:100, валюта — EURUSD.
  • Оптимизация будет выполняться по критерию Balance max.

Сохраним результат и создадим тестовый файл с полученными данными. Данные в текстовом файле отсоритруем по возрастанию значения параметра Moving Average period, как показано на рис.5.


Рис. 5. Оптимизация параметра Moving Average period.Текстовый файл с данными для тестирования работы алгоритма

В режиме полного перебора было выполнено 120 итераций. Тестировать алгоритм метода отжига будем со следующим количеством итераций: 30 (вариант 1), 60 (вариант 2), 90 (вариант 3). Наша цель — проверить работу алгоритма при снижении количества итераций.

Для каждого варианта было выполнено 10000 запусков оптимизации методом отжига, на данных, полученных методом полного перебора. Алгоритм, реализованный в советнике TestAnnealing.mq5, подсчитывает, сколько раз было найдено наилучшее значение целевой функции и сколько раз было найдены значения целевой функции, отличающиеся от наилучшего на 5%, 10%, 15%, 20%, 25%. 

Получены следующие результаты тестирования.

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

Отклонение от наилучшего значения целевой функции, % Результат, %
0 33
5 44
10 61
15 61
20 72
25 86

Данные этой таблицы интерпретируются так: наилучшее значение целевой функции получено в 33% запусков (в 3 300 запусков из 10 000), отконение от наилучшего значения на 5% было получено в 44% запусков, и так далее.

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

Отклонение от наилучшего значения целевой функции, % Результат, %
0 47
5 61
10 83
15 83
20 87
25 96

Таким образом, при снижении итерации в 2 раза, по сравнению с полным перебором, алгоритм метода отжига находит наилучшее значение целевой функции в 47% случаев.

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

Отклонение от наилучшего значения целевой функции, % Результат, %
0 62
5 71
10 93
15 93
20 95
25 99

Таким образом, при снижении итерации на 1/3 по сравнению с полным перебором, алгоритм метода отжига находит наилучшее значение целевой функции в 62% случаев. Но уже с отклонением в 10-15% процентов можно получить вполне приемлемые результаты.

Метод сверхбыстрого отжига тестировался с параметрами p1=1, p2=1, и с увеличением количества итераций полученный результат был хуже, чем у Больцмановского отжига и отжига Коши. Однако в алгоритме сверхбыстрого отжига есть одна особенность: меняя коэффициенты p1, p2 мы можем настраивать скорость уменьшения температуры.

Рассмотрим график изменения температуры для сверхбыстрого отжига (рис. 6):

t1t2

Рис. 6. График изменения температуры для сверхбыстрого отжига (T0=100, n=4)

Из рис. 6 следует, что для уменьшения скорости изменения темпрературы необходимо увеличить коэффициент p1 и уменьшить коэффициент p2. Соответсвенно, для увеличения скорости изменения температуры необходимо уменьшить коэффициент p1 и увеличить коэффициент p2.

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

Количество итераций p1 p2 0% 5% 10% 15% 20% 25% 
60   0,5 57 65  85   85   91  98 
90 0,25 1 63 78 93 93 96 99 

Из таблицы следует, что наилучшее значение целевой функции было получено в 57% запусков на 60 итерациях и в 63% запусков на 90 итерациях.

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

Как говорилось выше, алгоритм метода отжига — стохастический, поэтому сравним его работу со случайным поиском. Для этого на каждой итерации будем генерировать случайное значение параметра с заданным шагом и в заданном диапазоне. В нашем случае значения параметра Moving Average period будут генерироваться с шагом 1, в диапазоне от 1 до 120.

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

  • количество итераций: 30, 60, 90
  • количество запусков в каждом варианте: 10000

Результаты случайного поиска представлены в таблице:

Количество итераций 0% 5% 10% 15% 20% 25% 
30 22 40 54 54 64 84 
60 40 64 78 78 87 97 
90 52 78 90 90 95 99 

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

Количество итераций 0% 5% 10% 15% 20% 25% 
30 50 10 12,963 12,963 12,5 2,381
60 42,5 1,563 8,974 8,974 4,6 1,031
90 21,154 0 3,333 3,333 1,053 0

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

Теперь перейдем к тестированию алгоритма на оптимизации двух переменных советника — Moving Average period и Moving Average shift. Сначала сформируем входные данные, запустив метод полного перебора в тестере стратерий со следующими параметрами:

  • Maximum Risk in percentage — 0,02; Descrease factor — 3; Moving Average period: 1-120; Moving Average shift - 6-60.
  • Период: 01.01.2017 — 31.12.2017, режим торговли — без задержки, тики: OHLC на M1, начальный депозит 10000, плечо — 1:100, валюта — EURUSD
  • Оптимизация будет выполняться по критерию Balance max.

Сохраним результат и создадим тестовый файл с полученными данными. Данные в текстовом файле отсоритруем по возрастанию значения параметра Moving Average period. Сформированный файл показан на рис. 7.


Рис. 7. Оптимизация параметров Moving Average period и Moving Average shift.Текстовый файл с данными для тестирования работы алгоритма

Метод полного перебора для двух переменных выполняется за 6600 итераций. Постараемся снизить это количество, используя метод отжига. Протестируем алгоритм при следующем количестве итераций: 330, 660, 1665, 3300, 4950. Количество запусков в каждом варианте: 10000.   

Результаты тестирования следующие.

330 итераций: неплохие результаты показал отжиг Коши, но лучший результат — у алгоритма сверхбыстрого отжига с вариантом снижения температуры на каждой итерации и коэффициентами p1= 1, p2=1.

660 итераций: отжиг Коши и свехбыстрый отжиг с вариантом снижения температуры на каждой итерации и коэффициентами p1= 1, p2=2 показали примерно одинаковые результаты.

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

  • 1665 итераций: p1= 0,5, p2=1
  • 3300 итераций: p1= 0,25, p2=1
  • 4950 итераций: p1= 0,5, p2=3

Наилучшие результаты сведем в таблицу:

Количество итераций 0% 5% 10% 15% 20% 25% 
330 11 11 18 40 66 71
 660  17 17  27  54  83  88 
 1665  31 31  41  80  95  98 
 3300  51 51  62  92  99  99 
 4950  65 65  75 97  99  100 

Из таблицы можно сделать следующие выводы:

  • при сокращении итераций в 10 раз алгоритм свербыстрого отжига находит наилучшее значение целевой функции в 11% случаев, но в 71% проценте случаев мы получаем значение целевой функции, которое всего на 25% хуже наилучшего.
  • при сокращении итераций в 2 раза алгоритм свербыстрого отжига находит наилучшее значение целевой функции в 51% случаев, но почти со 100% вероятностью алгоритм находит значение целевой, которое всего на 20% хуже наилучшего.

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

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

Количество итераций 0% 5% 10% 15% 20% 25% 
330 5 5 10 14 33 42
660 10 10 19 27 55 67
1665 22 22 41 53 87 94
 3300  40 40 64 79  98   99
 4950  55  55  79  90  99  99

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

Количество итераций 0% 5% 10% 15% 20% 25% 
330 120 120 80 185.714 100 69
660 70 70 42.105 100 50,909 31,343
1665 40,909 40,909 0 50,9434 9,195 4,255
 3300 27,5  27,5 -3,125 16,456 1,021 0
 4950 18,182 18,182 -5,064 7,778 0 1,01

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

Теперь перейдем к главному: сравним алгоритм сверхбыстрого отжига и генетический алгоритм (ГА), встроенный в тестер стратегий.

Сравнение ГА и сверхбыстрого отжига при оптимизации двух переменных: Moving Average period и Moving Average shift

Запускать алгоритмы будем при следующих исходных параметрах:

  • Maximum Risk in percentage — 0,02; Descrease factor — 3; Moving Average period: 1 — 120, шаг: 1; Moving Average shift — 6-60, шаг: 1
  • Период: 01.01.2017 — 31.12.2017, режим торговли — без задержки, тики: OHLC на M1, начальный депозит — 10000, плечо — 1:100, валюта — EURUSD
  • Оптимизация будет выполняться по критерию Balance max

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

После 20 запусков ГА получены следующие значения целевой функции: 1392.29; 1481.32; 2284.46; 1665.44; 1435.16; 1786.78; 1431.64; 1782.34; 1520.58; 1229.36; 1482.23; 1441.36; 1763.11; 2286.46; 1476.54; 1263.21; 1491.09; 1076.9; 913.42; 1391.72.

Среднее количество итераций: 175, среднее значение целевой функции: 1529.771.

Учитывая, что наилучшее значение целевой функции: 2446.33, ГА дает не очень хороший результат, среднее значение целевой функции составляет всего 62,53% от наилучшего значения.

Теперь выполним 20 запусков алгоритма свехбыстрого отжига на 175 итерациях с параметрами: p1=1, p2=1.

Алгоритм свехбыстрого отжига был запущен на 4 агентах тестирования, при этом поиск значения целевой функции выполнялся на каждом агенте автономно, таким образом, в каждом агенте тестирования выполнилось по 43-44 итерации. Получены следующие результаты: 1996.83; 1421.87; 1391.72; 1727.38; 1330.07; 2486.46; 1687.51; 1840.69; 1687.51; 1472.19; 1665.44; 1607.19; 1496.9; 1388.37; 1496.9; 1491.09; 1552.02; 1467.08; 2446.33; 1421.15.

Среднее значение целевой функции: 1653.735, 67.6% от наилучшего значения целевой функции, немного выше, чем полученные ГА.

Запустим алгоритм свехбыстрого отжига на одном агенте тестирования, выполнив на нем 175 итераций, в резутальте среднее значение целевой функции составило 1731.244 (70.8% от наилучшего значения).

Сравнение ГА и сверхбыстрого отжига при оптимизации четырех переменных: Moving Average period, Moving Average shift, Descrease factor и Maximum Risk in percentage.

Запускать алгоритмы будем при следующих исходных параметрах:

  • Moving Average period: 1 — 120, шаг 1; Moving Average shift — 6-60, шаг 1; Descrease factor: 0.02 — 0.2, шаг: 0,002; Maximum Risk in percentage: 3-30, шаг: 0.3.
  • Период: 01.01.2017 - 31.12.2017, режим торговли - без задержки, тики: OHLC на M1, начальный депозит - 10000, плечо - 1:100, валюта - EURUSD
  • Оптимизация будет выполняться по критерию Balance max

ГА выполнился за 4870 итераций с наилучшим значением: 32782.91. Полный перебор не запускался из-за большого количества вариантов, поэтому просто сравним результаты, полученные ГА и алгоритмом сверхбыстрого отжига.

Алгоритм сверхбыстрого отжига был запущен с параметрами p1=0.75, p2=1, на 4 агентах тестирования и завершился результатом: 26676.22. При данных настройках алгоритм работает не очень хорошо. Попробуем ускорить снижение температуры задав p1=2, p2=1. Также заметим, что температура, вычисляемая по формуле:
T0*exp(-p1*exp(-p2/4)*n^0.25), где n - номер итерации,

резко снижается уже на первой итерации (при n=1, T=T0*0.558). Поэтому увеличим коэффицент при начальной температуре, задав параметр CoeffOfTemp=4. При запуске алгоритма с этими настройками результат значительно улучшился: 39145.25. Работа алгоритма продемонстрирована на следующем видео:


Демонстрация работы сверхбыстрого отжига с параметрами p1=2, p2=1

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

Заключение

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

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

Выделим основные преимущества метода отжига:

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

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

  • невозможность запуска алгоритма в облачном тестировании;
  • сложность подключения к советнику и необходимость выбора параметров для получения наилучших результатов.

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

К статье приложены следующие файлы:

Наименование файла Комментарий
AnnealingMethod.mqh Класс для работы алгоритма метода отжига, необходимо переместить в папку /MQL5/Include
FrameAnnealingMethod.mqh Класс для отображения в окне терминала процесса выполнения алгоритма, необходимо переместить в папку /MQL5/Include
SimpleTable.mqh Вспомогательный класс для работы с таблицами графического интерфейса, необходимо переместить в папку /MQL5/Include
Moving Average_optim.mq5 Модифицированный советник Moving Average
test.zip Архив, содержащий советник TestAnnealing.mq5 для тестирования алгоритма метода отжига на входных данных, загруженных из тестового файла, и вспомогательные файлы
AnnealingMethod.zip
Zip-файл с картинками для создания интерфейса плеера. Файлы нужно разместить в папке MQL5/Images/AnnealingMethod


Прикрепленные файлы |
AnnealingMethod.mqh (31.57 KB)
test.zip (42.69 KB)
simpletable.mqh (22.9 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
fxsaber
fxsaber | 28 февр. 2018 в 08:45

tester_file считывается только в том случае, если он существовал (содержимое не важно) на момент компиляции.

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

Поэтому если в OnTesterInit генерируете файл для tester_file, то убедитесь, что компилировали советник при наличии хотя бы пустого передаваемого файла.


@Renat Fatkhullin несколько ошибся в своем заявлении

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Может ли советник без DLL функций отправить куда-нибудь данные?

Renat Fatkhullin, 2017.06.21 11:12

fxsaber:

Не проверял, но предполагаю, этот файл можно сгенерировать прямо в OnTesterInit.

Нет, он не попадет в расчетный пакет данных в самом проходе.


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

Aleksandr Masterskikh
Aleksandr Masterskikh | 28 февр. 2018 в 09:51

Автор проделал большую работу. Очень интересная статья.

Спасибо.

fxsaber
fxsaber | 28 февр. 2018 в 14:46
Andrey Dik:

были реальные попытки сравнить на различных тестах штатный ГА и другие АО. боги сказали - не бывать единоборствам и аминь на этом.

ЗЫ. 1, 2 оптимизируемых параметра? это просто тьфу.... попробуйте оптить несколько сотен, тысяч параметров своим отжигом... глаза откроются

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

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

Aleksey Zinovik
Aleksey Zinovik | 28 февр. 2018 в 15:48
Sergey Pavlov:

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

Да, вы правы так сравнивать нельзя. Справка по MQL5 предлагает воспользоваться следующей функцией:

bool CompareDoubles(double number1,double number2) 
  { 
   if(NormalizeDouble(number1-number2,8)==0) return(true); 
   else return(false); 
  } 

Но даже, если сравнение будет выполнено неправильно, функция FindValue выдаст верный результат 

Andrey Dik
Andrey Dik | 28 февр. 2018 в 18:08
fxsaber:

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

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

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

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

Глубокие нейросети (Часть VI). Ансамбль нейросетевых классификаторов: bagging Глубокие нейросети (Часть VI). Ансамбль нейросетевых классификаторов: bagging
Рассмотрим методы построения и обучения ансамблей нейросетей со структурой bagging. Определим особенности оптимизации гиперпараметров индивидуальных нейросетевых классификаторов, составляющих ансамбль. Сравним качество оптимизированной нейросети, полученной в предыдущей статье серии, и созданного ансамбля нейросетей. Рассмотрим возможности дальнейшего улучшения качества классификации полученного ансамбля.
Визуализируем оптимизацию торговой стратегии в MetaTrader 5 Визуализируем оптимизацию торговой стратегии в MetaTrader 5
В статье реализовано MQL-приложение с графическим интерфейсом для расширенной визуализации процесса оптимизации. Графический интерфейс создан с помощью последней версии библиотеки EasyAndFast. У многих пользователей возникает вопрос, зачем нужны графические интерфейсы в MQL-приложениях. В настоящей статье продемонстрирован один из множества случаев, когда они могут быть полезными для трейдеров.
Сравниваем скорость самокэширующихся индикаторов Сравниваем скорость самокэширующихся индикаторов
В статье проводится сравнение классического MQL5-доступа к индикаторам с альтернативными способами в стиле MQL4. Рассматриваются несколько вариантов MQL4-стиля доступа к индикаторам: с кэшированием хэндлов индикаторов и без него. Исследован учет хэндлов индикаторов внутри ядра MQL5.
ZUP - зигзаг универсальный с паттернами Песавенто. Поиск паттернов ZUP - зигзаг универсальный с паттернами Песавенто. Поиск паттернов
Индикаторная платформа ZUP позволяет производить поиск множества известных паттернов, параметры которых уже заданы. Но можно также и подстраивать эти параметры в соответствии со своими требованиями. Есть и возможность создавать новые паттерны с помощью графического интерфейса ZUP и сохранять их параметры в файл. После этого можно быстро проверить, встречаются ли новые паттерны на графиках.