English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Оптимизировать оптимизацию: несколько простых идей

Оптимизировать оптимизацию: несколько простых идей

MetaTrader 5Примеры | 1 декабря 2014, 10:08
4 799 3
Jose Miguel Soriano
Jose Miguel Soriano

Введение

Найдя однажды торговую стратегию, по которой советник совершает покупки и продажи, мы тут же запускаем советник на графике EURUSD, верно? Или все-таки есть другие валютные пары, на которых стратегия будет более прибыльной? Существуют ли другие валютные пары, на которых стратегия даст прибыль и улучшит результаты без необходимости увеличения размера лота в геометрической прогрессии?

Что будет, если по инерции мы выберем EURUSD и H1 в качестве таймфрейма графика, а затем, если нас не удовлетворят результаты, изменим на EURJPY и H4?

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

Эти «небольшие проблемы» я решаю в «одиночестве» программиста, и в этой статье делюсь эффективными решениями, которые использую сам. Хотя я и понимаю, что найдутся более оптимальные решения.


Оптимизация по таймфреймам

MQL5 предоставляет полный набор таймфреймов: от M1, M2 , M3, M4,... H1, H2,... до месячных графиков. Всего в MQL5 21 временной диапазон или таймфрейм. Но на практике, в процессе оптимизации, нам больше всего интересно выявить, какие таймфреймы лучше подходят для нашей стратегии: короткие - М1 и М5, средние - H2 и H4 или длительные - D1 и W1? 

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

Если в качестве входного параметра мы будем использовать переменную типа ENUM_TIMEFRAMES:

input ENUM_TIMEFRAMES marcoTF= PERIOD_M5; 

то оптимизатор предоставит нам 21 вариант оптимизаций. Но нужно ли нам столько?

Стандартные опции таймфрейма

Изначально — нет. Как упростить? Определим перечисление:

enum mis_MarcosTMP
{
   _M1= PERIOD_M1,
   _M5= PERIOD_M5,
   _M15=PERIOD_M15,
//   _M20=PERIOD_M20,
   _M30=PERIOD_M30,
   _H1= PERIOD_H1,
   _H2= PERIOD_H2,
   _H4= PERIOD_H4,
//   _H8= PERIOD_H8,
   _D1= PERIOD_D1,
   _W1= PERIOD_W1,
   _MN1=PERIOD_MN1
};

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

input mis_MarcosTMP таймфрейм= _H1;

и определим новую функцию в библиотеке .mqh:

//----------------------------------------- УСТАНАВЛИВАЕМ ТАЙМФРЕЙМ  ----------------------------------------------------------
ENUM_TIMEFRAMES defMarcoTiempo(mi_MARCOTMP_CORTO marco)
{
   ENUM_TIMEFRAMES resp= _Period;
   switch(marco)
   {
      case _M1: resp= PERIOD_M1; break;
      case _M5: resp= PERIOD_M5; break;
      case _M15: resp= PERIOD_M15; break;
      //case _M20: resp= PERIOD_M20; break;
      case _M30: resp= PERIOD_M30; break;
      case _H1: resp= PERIOD_H1; break;
      case _H2: resp= PERIOD_H2; break;
      case _H4: resp= PERIOD_H4; break;
      //case _H8: resp= PERIOD_H8; break;
      case _D1: resp= PERIOD_D1; break;
      case _W1: resp= PERIOD_W1; break;
      case _MN1: resp= PERIOD_MN1;
   }
return(resp);
}

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

ENUM_TIMEFRAMES marcoTmp= defMarcoTiempo(marcoTiempo);          //таймфрейм определен как глобальная переменная

"marcoTmp" является глобальной переменной, с помощью которой советник будет устанавливать необходимый для работы график таймфрейма. В таблице параметров оптимизатора можно определить интервалы запуска переменной "marcoTiempo", охватывающей только интересующие нас шаги, не теряя времени и ресурсов на анализ М6 или М12. Таким образом можно проанализировать результаты советника в различных таймфреймах.

Пользовательские опции таймфрейма 

Конечно, также это можно решить и с помощью

ENUM_TIMEFRAMES marcoTmp= (ENUM_TIMEFRAMES)marcoTiempo;

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


Оптимизация символа или совокупности символов

В тестере стратегий MetaTrader 5 имеется режим оптимизации, который позволяет прогнать советник на всех символах, выбранных в окне "Обзор рынка" (MarketWatch). Но эта функция не позволяет оптимизировать так, как если бы выбранный символ был еще одним параметром. Так что, если есть 15 выбранных символов, то тестер произведет 15 прогонов. Как мы узнаем какой символ является лучшим для нашего советника? И если это мультивалютный эксперт, то какая группа символов дает лучший результат и с каким набором параметров? Переменные string (переменные строки) не оптимизируются в MQL5. Как это сделать?

Кодируем символ или пару символов значением входного параметра таким образом:

input int selecDePar= 0;

string cadParesFX= selecPares(selecDePar);

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

//------------------------------------- ВЫБИРАЕМ КОМБИНАЦИЮ ПАР -------------------------------------
string selecPares(int combina= 0)
{
   string resp="EURUSD";
   switch(combina)               
      {
         case 1: resp= "EURJPY"; break;
         case 2: resp= "USDJPY"; break;
         case 3: resp= "USDCHF"; break;      
         case 4: resp= "GBPJPY"; break;
         case 5: resp= "GBPCHF"; break;      
         case 6: resp= "GBPUSD"; break;
         case 7: resp= "USDCAD"; break;
         case 8: resp= "CADJPY"; break;      
         case 9: resp= "XAUUSD"; break;
       
         case 10: resp= "EURJPY;USDJPY"; break;
         case 11: resp= "EURJPY;GBPJPY"; break;
         case 12: resp= "GBPCHF;GBPJPY"; break;
         case 13: resp= "EURJPY;GBPCHF"; break;
         case 14: resp= "USDJPY;GBPCHF"; break;

         case 15: resp= "EURUSD;EURJPY;GBPJPY"; break;
         case 16: resp= "EURUSD;EURJPY;GBPCHF"; break;
         case 17: resp= "EURUSD;EURJPY;USDJPY"; break;
         case 18: resp= "EURJPY;GBPCHF;USDJPY"; break;
         case 19: resp= "EURJPY;GBPUSD;GBPJPY"; break;
         case 20: resp= "EURJPY;GBPCHF;GBPJPY"; break;
         case 21: resp= "USDJPY;GBPCHF;GBPJPY"; break;
         case 22: resp= "EURUSD;USDJPY;GBPJPY"; break;
       
         case 23: resp= "EURUSD;EURJPY;USDJPY;GBPUSD;USDCHF;USDCAD"; break;
         case 24: resp= "EURUSD;EURJPY;USDJPY;GBPUSD;USDCHF;USDCAD;AUDUSD"; break;
      }
   return(resp);
}

В зависимости от того, что нас интересует, определяем различные комбинации пар и информируем тестер об интервале, который необходимо проанализировать. Даем тестеру стратегий команду оптимизировать параметр "selecDePar" на интервале от 15 до 22 (см. картинку ниже). Но что делать, если мы хотим сравнить одновалютные результаты? Тогда прогоняем оптимизацию на интервале от 0 до 9.

Оптимизация совокупности пар 

К примеру, советник получает значение параметра cadParesFX= "EURUSD;EURJPY;GBPCHF". В OnInit() вызываем функцию "cargaPares()", которая заполняет динамический массив arrayPares[] строками, разделенными символом ";" в параметре cadParesFX. Все глобальные переменные советника нужно загрузить в динамические массивы, которые сохраняют значения каждого символа, включая контроль за открытием нового бара на символе, если это возможно. В случае, когда мы работаем с одним символом, размеры массивов будут равны единице.

//-------------------------------- КОНВЕРСИЯ СТРОКИ ИЗ ПАР FX В МАССИВ  -----------------------------------------------
int cargaPares(string cadPares, string &arrayPares[])
{            //convierte "EURUSD;GBPUSD;USDJPY" a {"EURUSD", "GBPUSD", "USDJPY"}; devuelve el número de paresFX
   string caract= "";
   int i= 0, k= 0, contPares= 1, longCad= StringLen(cadPares);
   if(cadPares=="")
   {
      ArrayResize(arrayPares, contPares);
      arrayPares[0]= _Symbol;
   }
   else
   {
      for (k= 0; k<longCad; k++) if (StringSubstr(cadPares, k, 1)==";") contPares++;
      ArrayResize(arrayPares, contPares);    
      ZeroMemory(arrayPares);
      for(k=0; k<longCad; k++)
      {
         caract= StringSubstr(cadPares, k, 1);
         if (caract!=";") arrayPares[i]= arrayPares[i]+caract;
         else i++;
      }
    }
   return(contPares);
}

В OnInit() эта функция применяется следующим образом:

string ar_ParesFX[];    //массив, который будет содержать названия пар, с которыми будет работать советник
int numSimbs= 1;        //переменная, которая содержит информацию о количестве символов, с которыми работает

int OnInit()
{
   
   //...
   numSimbs= cargaPares(cadParesFX, ar_ParesFX);     //возвращает массив ar_ParesFX с парами для работы в советнике
   //...
   
}

Если numSimbs>1, то вызывается функция OnChartEvent(), работающая с мультивалютной системой. В противном случае, выполняется функция OnTick():

void OnTick()
{
   string simb="";
   bool entrar= (nSimbs==1);
   if(entrar)
   {   
      .../...
      simb= ar_ParesFX[0];
      gestionOrdenes(simb);
      .../...
   }
   return;
}

//+------------------------------------------------------------------+
//| ОБРАБОТЧИК СОБЫТИЙ                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int idEvento, const long& lPeriodo, const double& dPrecio, const string &simbTick)
{
   bool entrar= nSimbs>1 && (idEvento>=CHARTEVENT_CUSTOM);
   if(entrar)      
   {
      .../...
      gestionOrdenes(simbTick);
      .../...
   }
}
  

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

//--------------------------------- ЗНАКИ СИМВОЛА ---------------------------------------
int digitosSimb(string simb= NULL)
{
   int numDig= (int)SymbolInfoInteger(simb, SYMBOL_DIGITS);
   return(numDig);
}

Иными словами, мы должны забыть о функциях Symbol() или Point(), а также о других привычных переменных для МetaТtarder 4, таких, как Ask или Bid.

//----------------------------------- ЗНАЧЕНИЕ ПУНКТА в цене (Point())---------------------------------
double valorPunto(string simb= NULL) 
{
   double resp= SymbolInfoDouble(simb, SYMBOL_POINT);
   return(resp);
}
//--------------------------- precio ASK-BID  -----------------------------------------
double precioAskBid(string simb= NULL, bool ask= true)
{
   ENUM_SYMBOL_INFO_DOUBLE precioSolic= ask? SYMBOL_ASK: SYMBOL_BID;
   double precio= SymbolInfoDouble(simb, precioSolic);
   return(precio);
}

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

//------------------------------------- НОВАЯ МУЛЬТИВАЛЮТНАЯ СВЕЧА -------------------------------------
bool nuevaVelaMD(string simb= NULL, int numSimbs= 1, ENUM_TIMEFRAMES marcoTmp= PERIOD_CURRENT)
{
        static datetime arrayHoraNV[];
        static bool primVez= true;
        datetime horaVela= iTime(simb, marcoTmp, 0);    //получение времени открытия текущей свечи
        bool esNueva= false;
        int codS= buscaCadArray(simb, nombreParesFX);      
        if(primVez)
        {
           ArrayResize(arrayHoraNV, numSimbs);
           ArrayInitialize(arrayHoraNV, 0);     
           primVez= false;
        }
        esNueva= codS>=0? arrayHoraNV[codS]!= horaVela: false;
        if(esNueva) arrayHoraNV[codS]= horaVela;
        return(esNueva); 
}

Описанным методом за одну оптимизацию я смог выяснить то, что: 

  • советник очень хорошо работает в EURUSD,
  • отвратительно в EURJPY,
  • удовлетворительно в USDJPY
  • и очень хорошо на тройке EURUSD, GBPCHF, EURJPY (реальный случай).

И это на периоде М5, а не для Н1 или Н2, и только для определенного набора остальных параметров оптимизации.

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


Оптимизация комбинации параметров

Иногда из всех комбинаций параметров, которые перебираются при оптимизации, подходящими оказыватся некоторые нелогичные, или даже такие, на которых которых эта стратегия становится абсурдной. Например, если переменная входа "maxSpread" определяет значение спрэда, принятого для выпонения торговой операции операции и мы оптимизируем эту перемнную для различных пар, в которых средний спрэд брокера меньше 30, а XAUUSD составляет 400, то абсурдно анализировать эти пары если они превышают 50 или XAUUSD меньше 200. Передав данные оптимизатору, задаем "evalua maxSpread между 0 и 600, с интервалом 20". Очевидно, что такой набор, в комбинации с другими параметрами,  производит бесконечное множество абсурдных комбинаций.

Следуя схеме, описанной в предыдущих разделах, мы определили пары для оптимизации в функции "selecPares()". EURUSD занимает опцию 0, а XAUUSD опцию 9. В дальнейшем определяем глобальную переменную типа bool "paramCorrect".

bool paramCorrect= (selecDePar<9 && maxSpread<50) ||
                   (selecDePar==9 && maxSpread>200);

В OnInit() выполняем действия, только если paramCorrect имеет правильное значение - true.

int OnInit()
{   
   ENUM_INIT_RETCODE resp= paramCorrect? INIT_SUCCEEDED: INIT_PARAMETERS_INCORRECT;
   if (paramCorrect)
   {
      //...
      nSimbs= cargaPares(cadParesFX, nombreParesFX);     //возвращает массив nombreParesFX с парами  для работы в советнике
      //... функции инициализации советника
   }
   return(resp);
}

Если paramCorrect имеет неправильное значение (false), то советник ничего не делает в OnInit() и возвращает тестеру код INIT_PARAMETERS_INCORRECT, которое означает неправильный набор входных данных. Когда тестер стратегий получает из OnInit() значение INIT_PARAMETERS_INCORRECT, то этот набор парамтеров гарантированно не передается на выполенние другим агентам тестирования, и строка в таблице результатов оптимизации заполнится нулями и будет подсвечена красным фоном (смотри картинку ниже).

Результаты неправильных параметров 

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

void OnDeinit(const int motivo)
{
   if(paramCorrect)
   {
      
      //функции закрытия программы
      
   }
   infoDeInit(motivo);
   return;
}

//+-------------------------------------- ИНФОРМАЦИЯ О ЗАКРЫТИИ ПРОРАММЫ----------------------------
string infoDeInit(int codDeInit)
{                       //сообщает о причине закрытия программы
   string texto= "инициализация программы...", text1= "CIERRE por: ";
   switch(codDeInit)
   {
      case REASON_PROGRAM:     texto= text1+"Советник закончил свою работу с помощью функции ExpertRemove()"; break;  //0
      case REASON_ACCOUNT:     texto= text1+"Был изменен счет"; break;                                                //6
      case REASON_CHARTCHANGE: texto= text1+"Смена символа или таймфрейма"; break;                                    //3
      case REASON_CHARTCLOSE:  texto= text1+"График был закрыт"; break;                                               //4
      case REASON_PARAMETERS:  texto= text1+"Входные параметры изменены пользователем"; break;                        //5
      case REASON_RECOMPILE:   texto= text1+"Программа была перекомпилирована"; break//2
      case REASON_REMOVE:      texto= text1+"Программа удалена с графика"; break;                                     //1
      case REASON_TEMPLATE:    texto= text1+"Применен друой шаблон графика"; break;                                   //7
      case REASON_CLOSE:       texto= text1+"Терминал был закрыт"; break;                                             //9
      case REASON_INITFAILED:  texto= text1+"Обработчик OnInit() вернул ненулевое значение"; break;                   //8
      default:                 texto= text1+"Иная причина";
   }
   Print(texto);
   return(texto);
}

Дело в том, что если набор параметров, которые получает оптимизатор на заданном шаге устанавливает "paramCorrect" значение в false (например, если спрэд EURUSD был установлен в 100 пунктов), то мы не запускаем прогон советника, и шаг оптимизации станет нулем без ненужной загрузки компьютера или расходов за аренду агентов на вашем счете в MQL5.сommunity. 

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


Заключение

Мы увидели как можно ускорить поиск оптимальных таймфреймов в МetaТrader 5; как оптимизировать параметр "символ", когда МТ5 не позволяет оптимизировать строковые переменные; и как сделать его безразличным к количеству символов, которые использует советник. Кроме того в статье рассказано как уменьшить число шагов оптимизатора, пропуская абсурдные для логики рынка наборы входных параметров, сохраняя производительность вашего компьютера и экономя средства на счете.

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

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

Перевод с испанского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/es/articles/1052

Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Andrey Khatimlianskii
Andrey Khatimlianskii | 3 дек. 2014 в 00:21

Боже мой, что за кошмарный перевод!

Интересно, обратные переводы на испанский такие же?

Нужно что-то с этим делать. 

Rashid Umarov
Rashid Umarov | 3 дек. 2014 в 16:15
Перевод адаптировали. Спасибо за замечание!
[Удален] | 6 дек. 2014 в 17:28

Вообще-то интересно... Но не слишком на много отличается от обычной оптимизации по выбранным трейдером на своём опыте параметрам...

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

Т.е. возможна ли автоматическая оптимизация и переоптимизация советника через определённый трейдером период времени?

Статистические рецепты для трейдера - Гипотезы Статистические рецепты для трейдера - Гипотезы
В данной статье рассматривается базовое понятие математической статистики "гипотеза". На примерах, с применением методов математической статистики, исследуются и проверяются различные гипотезы. Реальные данные обобщаются с помощью непараметрических методов. При обработке данных используются пакет Statistica и портированная библиотека численного анализа ALGLIB MQL5.
Третье поколение нейросетей: "Глубокие нейросети" Третье поколение нейросетей: "Глубокие нейросети"
Статья посвящена новому и очень перспективному направлению в машинном обучении — так называемому "глубокому обучению" и конкретней "глубоким нейросетям". Сделан краткий обзор нейросетей 2 поколения, их архитектуры связей и основных видов, методов и правил обучения и их основных недостатков. Далее рассмотрена история появления и развития нейросетей 3 поколения, их основные виды, особенности и методы обучения. Проведены практические эксперименты по построению и обучению на реальных данных глубокой нейросети, инициируемой весами накапливающего автоэнкодера. Рассмотрены все этапы от выбора исходных данных до получения метрик. В последней части статьи приведена программная реализация глубокой нейросети в виде индикатора-эксперта на MQL4/R.
Основы биржевого ценообразования на примере срочной секции Московской биржи Основы биржевого ценообразования на примере срочной секции Московской биржи
Статья описывает теорию биржевого ценообразования и специфику клиринговых расчетов срочной секции Московской биржи. Материал будет интересен как начинающим трейдерам, желающим получить свой первый биржевой опыт по торговле деривативами, так и опытным форекс-трейдерам, рассматривающих возможность переноса своей торговли на централизованную биржевую площадку.
Нейросети бесплатно и сердито - соединяем NeuroPro и MetaTrader 5 Нейросети бесплатно и сердито - соединяем NeuroPro и MetaTrader 5
Если специализированные нейросетевые программы для трейдинга вам кажутся дорогими и сложными (или наоборот - примитивными), то попробуйте NeuroPro - она на русском языке, бесплатна и содержит оптимальный набор возможностей для любителей. О том, как использовать ее с MetaTrader 5, вы узнаете из этой статьи.