Цветная оптимизация торговых стратегий

Dmitry Fedoseev | 28 марта, 2019

Введение

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

В данной статье будет проведен эксперимент по раскрашиванию результатов оптимизации. Как известно, цвет определяется тремя параметрами: уровнями красного, зеленого и синего цветов (RGB от английского: Red — красный, Green — зеленый, Blue — синий). Существуют и другие способы кодирования цвета, но и в них цвет кодируется тремя параметрами. Таким образом, три показателя тестирования можно превратить в один, визуально воспринимаемый человеком — в цвет. На сколько такой показатель будет полезен,  покажет итог статьи.

Исходные данные

В статье Анализ торговли по HTML-отчетам была создана библиотека функций для разбора файлов с отчетами — файл HTMLReport.mqh. В этой библиотеке есть функция OptimizerXMLReportToStruct(), предназначенная для работы с отчетами оптимизации, воспользуемся ею. В функцию передается два параметра:

  • string aFileName — имя файла с отчетом оптимизации. Файл должен находиться в папке MQL5/Files папки данных терминала.
  • SOptimization & aOptimization — передается по ссылке. После выполнения функции в этой структуре будут располагаться данные, извлеченные из отчета.

Структура SOptimisation:

struct SOptimization{
   string ParameterName[];
   SPass Pass[];
};

Структура включает в себя два массива: string ParameterName[] и SPass Pass[]. В массиве ParameterName[] располагаются имена оптимизируемых параметров. Основной интерес для нас будет представлять второй массив — SPass[], один элемент этого массива содержит данные об одном проходе оптимизации.

Структура SPass:

struct SPass{
   string Pass;
   string Result;
   string Profit;
   string ExpectedPayoff;
   string ProfitFactor;
   string RecoveryFactor;
   string SharpeRatio;
   string Custom;
   string EquityDD_perc;
   string Trades;
   string Parameters[];
};

Назначение полей структуры:

  • Pass — номер прохода оптимизации;
  • Result — итоговый баланс после оптимизации;
  • Profit — прибыль;
  • ExpectedPayoff — математическое ожидание прибыли;
  • ProfitFactor — профит-фактор;
  • RecoveryFactor — фактор восстановления;
  • SharpeRatio — коэффициент Шарпа;
  • Custom — пользовательский показатель;
  • EquityDD_perc — просадка в процентах;
  • Trades — количество трейдов;
  • Parameters[] — массив со значениями оптимизируемых параметров.

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

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

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

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

1. Создаем файл ColorOptimization.mqh. В этом файле будем располагать все функции для создания цветных отчетов.

2. В начале файла ColorOptimization.mqh подключаем файл HTMLReport.mqh:

#include <HTMLReport.mqh>

3. Создаем новую структуру, наследующую все поля структуры SPass, и добавляем в нее массивы factor[] и dParameters[]:

struct SPass2:SPass{
   double factor[9];
   double dParameters[];  
};

Оба массива имеют тип double. В массиве factor[] будет располагаться 9 итоговых показателей — все, кроме Pass (номера прохода) и кроме оптимизируемых параметров. В массиве sParameters[] — значения оптимизируемых параметров. Несмотря на то, что все данные уже имеются в структуре, они представлены в строковом формате, при их использовании каждый раз потребовалось бы преобразовывать их в число, а так в нашем распоряжении будут данные в удобном для использования формате.

4. Создаем итоговую структуру для данных оптимизации:

struct SOptimization2{
   string ParameterName[];
   SPass2 Pass[];
};

5. Создаем функцию для конвертации данных из структуры SOptimization в SOptimization2:

void ConvertOptimizationStruct(SOptimization & src,SOptimization2 & dst){

   ArrayCopy(dst.ParameterName,src.ParameterName);
   int cnt=ArraySize(src.Pass);
   ArrayResize(dst.Pass,cnt);   
   for(int i=0;i<cnt;i++){
      ArrayCopy(dst.Pass[i].Parameters,src.Pass[i].Parameters);
      
      dst.Pass[i].Pass=src.Pass[i].Pass;
      dst.Pass[i].Result=src.Pass[i].Result;
      dst.Pass[i].Profit=src.Pass[i].Profit;
      dst.Pass[i].ExpectedPayoff=src.Pass[i].ExpectedPayoff;
      dst.Pass[i].ProfitFactor=src.Pass[i].ProfitFactor;
      dst.Pass[i].RecoveryFactor=src.Pass[i].RecoveryFactor;
      dst.Pass[i].SharpeRatio=src.Pass[i].SharpeRatio;
      dst.Pass[i].Custom=src.Pass[i].Custom;
      dst.Pass[i].EquityDD_perc=src.Pass[i].EquityDD_perc;
      dst.Pass[i].Trades=src.Pass[i].Trades;

      dst.Pass[i].factor[0]=StringToDouble(src.Pass[i].Result);
      dst.Pass[i].factor[1]=StringToDouble(src.Pass[i].Profit);
      dst.Pass[i].factor[2]=StringToDouble(src.Pass[i].ExpectedPayoff);
      dst.Pass[i].factor[3]=StringToDouble(src.Pass[i].ProfitFactor);
      dst.Pass[i].factor[4]=StringToDouble(src.Pass[i].RecoveryFactor);
      dst.Pass[i].factor[5]=StringToDouble(src.Pass[i].SharpeRatio);
      dst.Pass[i].factor[6]=StringToDouble(src.Pass[i].Custom);
      dst.Pass[i].factor[7]=StringToDouble(src.Pass[i].EquityDD_perc);
      dst.Pass[i].factor[8]=StringToDouble(src.Pass[i].Trades);
      
      int pc=ArraySize(src.Pass[i].Parameters);
      
      ArrayResize(dst.Pass[i].dParameters,pc);
      
      for(int j=0;j<pc;j++){
         if(src.Pass[i].Parameters[j]=="true"){
            dst.Pass[i].dParameters[j]=1;
         }
         else if(src.Pass[i].Parameters[j]=="false"){
            dst.Pass[i].dParameters[j]=0;         
         }
         else{
            dst.Pass[i].dParameters[j]=StringToDouble(src.Pass[i].Parameters[j]);
         }
      }
   }   
}

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

Для доступа к элементам массива factor[] будет удобно пользоваться перечислением:

enum EOptimizatrionFactor{
   Result=0,
   Profit=1,
   ExpectedPayoff=2,
   ProfitFactor=3,
   RecoveryFactor=4,
   SharpeRatio=5,
   Custom=6,
   EquityDD_perc=7,
   Trades=8
};

Несмотря на то, что значения вариантов перечисления начинаются с нуля и увеличиваются на 1, то есть при описании перечисления можно было бы не указывать значения, тем не менее они указаны, потому что для данного перечисления важно соответствие с массивом factor[], это подстрахует от возможных ошибок при выполнении каких-либо доработок в дальнейшем. 

6. Создаем функцию для загрузки файла отчета в структуру SOptimization2, подобную функции  OptimizerXMLReportToStruct() из файла HTMLReport.mqh:

bool OptimizerXMLReportToStruct2(string aFileName,SOptimization2 & aOptimization){
   SOptimization tmp;
   if(!OptimizerXMLReportToStruct(aFileName,tmp)){
      return(false);
   }
   ConvertOptimizationStruct(tmp,aOptimization);
   return(true);
}

В функцию первым параметром передается имя файла с отчетом, а вторым параметром возвращается заполненная структура SOptimization2.

Теперь все готово для решения основной задачи статьи. 

Создание цветного отчета

Функции для создания цветного отчета будут располагаться в файле ColorOptimization.mqh, а их вызов будет выполняться из скрипта.

1. Создадим скрипт с именем ColorOptimization.mq5.

2. К скрипту ColorOptimization.mq5 подключим файл ColorOptimization.mqh.

#include <ColorOptimization.mqh>

3. В скрипт добавим внешние параметры. Сначала добавим свойство указывающее на существование окна свойств, затем внешние переменные.

Свойство:

#property script_show_inputs

Внешние переменные:

input string               ReportName     =  "*.xml";
input string               OutputName     =  "ColorOptimization1-1.htm";
input EOptimizatrionFactor Factor1        =  Profit;
input EOptimizatrionFactor Factor2        =  EquityDD_perc;
input EOptimizatrionFactor Factor3        =  RecoveryFactor;
input bool                 Factor1Invert  =  false;
input bool                 Factor2Invert  =  true;
input bool                 Factor3Invert  =  false;
input bool                 Sort           =  true;

Назначение переменных:

  • ReportName — имя файла исходного отчета оптимизации;
  • OutputName — имя файла отчета создаваемого скриптом;
  • Factor1 — первый фактор, на основе которого определяется цветной показатель отчета;
  • Factor2 — второй фактор, на основе которого определяется цветной показатель отчета;
  • Factor3 — третий фактор, на основе которого определяется цветной показатель отчета;
  • Factor1Invert — инвертирование первого фактора;
  • Factor2Invert — инвертирование второго фактора;
  • Factor3Invert — инвертирование третьего фактора;
  • Sort — включение сортировки итогового отчета в соответствии с цветовым показателем;

4. В функции OnStart() скрипта объявляем переменную типа SOptimisation2 и получаем в нее данные исходного отчета:

SOptimization2 opt;

if(!OptimizerXMLReportToStruct2(ReportName,opt)){
   Alert("Error OptimizerXMLReportToStruct2");
   return;
}

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

double ColorComponent[3];

6. Для расчета цветовых компонентов в файле ColorOptimization.mqh напишем функцию SolveColorComponents(). В функцию надо будет передать следующие параметры:

  • SOptimization2 & aOpt — данные исходного отчета оптимизации;
  • int i1, int i2, int i3 — индексы показателей исходного отчета оптимизации (массива factor[9] структуры SPass);
  • bool r1=false, bool r2=false, bool r3=false — переменные для инвертирования показателей.

В результате работы функции в массиве структур SPass будет заполнен значениями массив ColorComponents[3]. 

Для расчета цветовых компонентов необходимо найти максимальные и минимальные значения каждого показателя, затем для каждого прохода рассчитать величину показателя в диапазоне от 0 до 1. Ниже приведен весь код функции SolveColorComponents():

void SolveColorComponents(  SOptimization2 & aOpt,
                              int i1,int i2,int i3,
                              bool r1=false,bool r2=false,bool r3=false){
   
   double mx[3]={0,0,0};
   double mn[3]={DBL_MAX,DBL_MAX,DBL_MAX};
   
   int size=ArraySize(aOpt.Pass);
   
   for(int i=0;i<size;i++){
      mx[0]=MathMax(mx[0],aOpt.Pass[i].factor[i1]);
      mx[1]=MathMax(mx[1],aOpt.Pass[i].factor[i2]);
      mx[2]=MathMax(mx[2],aOpt.Pass[i].factor[i3]);
      mn[0]=MathMin(mn[0],aOpt.Pass[i].factor[i1]);
      mn[1]=MathMin(mn[1],aOpt.Pass[i].factor[i2]);
      mn[2]=MathMin(mn[2],aOpt.Pass[i].factor[i3]);      
   }

   double c1,c2,c3,d;
   
   for(int i=0;i<size;i++){      
   
      c1=0;
      c2=0;
      c3=0;
   
      d=mx[0]-mn[0];
      if(d!=0){
         c1=(aOpt.Pass[i].factor[i1]-mn[0])/d;
      }
      
      d=mx[1]-mn[1];
      if(d!=0){
         c2=(aOpt.Pass[i].factor[i2]-mn[1])/d; 
      }
      
      d=mx[2]-mn[2];
      if(d!=0){
         c3=(aOpt.Pass[i].factor[i3]-mn[2])/d;       
      }
      
      if(r1)c1=1.0-c1;
      if(r2)c2=1.0-c2;
      if(r3)c3=1.0-c3;
      
      aOpt.Pass[i].ColorComponent[0]=c1;
      aOpt.Pass[i].ColorComponent[1]=c2;      
      aOpt.Pass[i].ColorComponent[2]=c3;   
   }

}

Вызовем эту функцию из скрипта:

SolveColorComponents(opt,Factor1,Factor2,Factor3,Factor1Invert,Factor2Invert,Factor3Invert);

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

В структуру SPass2 добавим еще одно поле:

double SortFactor; 

В файле ColorOptimization.mqh напишем функции расчета фактора сортировки:

void SolveSortFactor(SOptimization2 & aOpt){

   int size=ArraySize(aOpt.Pass);
   
   for(int i=0;i<size;i++){
      aOpt.Pass[i].SortFactor=0;
      for(int j=0;j<3;j++){
         aOpt.Pass[i].SortFactor+=aOpt.Pass[i].ColorComponent[j];
      }
      aOpt.Pass[i].SortFactor/=3;
   }
}

Напишем функцию сортировки (используется метод пузырьковой сортировки):

void SortFactorSort(SOptimization2 & aOpt){
   int size=ArraySize(aOpt.Pass);
   for(int i=size-1;i>0;i--){
      for(int j=0;j<i;j++){
         if(aOpt.Pass[j].SortFactor<aOpt.Pass[j+1].SortFactor){
            SPass2 tmp=aOpt.Pass[j];
            aOpt.Pass[j]=aOpt.Pass[j+1];
            aOpt.Pass[j+1]=tmp;
         }
      }
   }
}

Вызовем эти функции из скрипта. Фактор сортировки потребуется не только для сортировки таблицы, поэтому функция SolveSortFactor() будет вызываться независимо от значения переменной Sort:

SolveSortFactor(opt);
if(Sort){   
   SortFactorSort(opt);
}

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

Таблица с цветным показателем

Вся работа по созданию таблицы с дополнительным цветным показателем выполняется в функции TableContent(), функция располагается в файле ColorOptimization.mqh, возвращает функция HTML-код таблицы.

Создание HTML таблицы является довольно простой задачей, раскрашивание ячейки с цветным показателем выполняется через указания стиля ячейки — атрибута background-color. Цветовые компоненты, меняющиеся в диапазоне от 0 до 1, легко конвертируются в компоненты меняющиеся от 0 до 255 — достаточно произвести умножение. Однако, хотелось бы сделать таблицу более наглядной, для этого в верхней ячейке колонки цветного показателя надо указать какой показатель оптимизации какому цвету соответствует, а верхние ячейки этих показателей раскрасить соответствующим цветом (рис. 1).

Фрагмент отчета с цветным показателем
Рис. 1. Фрагмент отчета с цветным показателем

Ниже приведен весь код функции TableContent():

string TableContent(SOptimization2 & aOpt,int i1,int i2,int i3){
   
   int size=ArraySize(aOpt.Pass);
     
   int pc=ArraySize(aOpt.ParameterName);

   int nc=ArraySize(co_names);
   
   string s="<table>";
   
   s=s+"<tr>";
   s=s+"<th>Pass</td>";
   
   for(int i=0;i<nc;i++){
      s=s+"<th"+HStyle(i,i1,i2,i3)+">"+co_names[i]+"</th>";   
   }
   
   s=s+"<th>"+ColorCollHeader(i1,i2,i3)+"</th>";  
   
   for(int j=0;j<pc;j++){
      s=s+"<th>"+aOpt.ParameterName[j]+"</th>";       
   }
   s=s+"</tr>";     
   
   int r,g,b;
   
   for(int i=0;i<size;i++){    
   
      ComponentsToRGB(aOpt.Pass[i].ColorComponent[0],
                      aOpt.Pass[i].ColorComponent[1],
                      aOpt.Pass[i].ColorComponent[2],
                      r,g,b);
   
      s=s+"<tr>";
   
      s=s+"<td>"+aOpt.Pass[i].Pass+"</td>";
      s=s+"<td>"+aOpt.Pass[i].Result+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].Profit+"</td>";         
      s=s+"<td>"+aOpt.Pass[i].ExpectedPayoff+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].ProfitFactor+"</td>";               
      s=s+"<td>"+aOpt.Pass[i].RecoveryFactor+"</td>";        
      s=s+"<td>"+aOpt.Pass[i].SharpeRatio+"</td>";               
      s=s+"<td>"+aOpt.Pass[i].Custom+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].EquityDD_perc+"</td>";        
      s=s+"<td>"+aOpt.Pass[i].Trades+"</td>";               
      
      string cs=RGBToStr(r,g,b);
      s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>";        
      
      for(int j=0;j<pc;j++){
         s=s+"<td>"+aOpt.Pass[i].Parameters[j]+"</td>";       
      }

      s=s+"</tr>";   
   
   }
   
   s=s+"</table>";   

   return(s);
   
}

Рассмотрим функцию подробно. В переменную size получаем количество проходов оптимизации, в переменную pc — количество оптимизируемых параметров, в переменную nc — размер массива с названиями показателей (объявлен на глобальном уровне):

int size=ArraySize(aOpt.Pass);
     
int pc=ArraySize(aOpt.ParameterName);

int nc=ArraySize(co_names);

Глобальный массив co_names[]:

string co_names[]={"Result","Profit","Expected Payoff",
                   "Profit Factor","Recovery Factor",
                   "Sharpe Ratio","Custom","Equity DD","Trades"};

В строковую переменную s будем добавлять HTML-код таблицы по мере ее формирования, поэтому сразу при объявлении переменной присваиваем ей тег начала таблицы:

string s="<table>";

Затем добавляем тег начала строки и первую ячейку заголовка с текстом "Pass":

s=s+"<tr>";
s=s+"<th>Pass</th>";

После столбца "Pass" следуют столбцы с показателями, любой из которых может использоваться для формирования цветного показателя, нужно пометить те три столбца, которые используются в данный момент. Формируем HTML-код ячеек:

for(int i=0;i<nc;i++){
   s=s+"<th"+HStyle(i,i1,i2,i3)+">"+co_names[i]+"</th>";   
}

Функция HStyle(), в случае необходимости, формирует код изменяющий цвет фона ячейки:

string HStyle(int i,int i1,int i2,int i3){
   if(i==i1)return(" style='background-color: rgb(255,0,0);'");
   if(i==i2)return(" style='background-color: rgb(0,255,0);'");
   if(i==i3)return(" style='background-color: rgb(0,0,255);'");
   return("");
}

Формируем текст для ячейки с заголовком цветного показателя:

s=s+"<th>"+ColorCollHeader(i1,i2,i3)+"</th>";

Код функции ColorCollHeader():

string ColorCollHeader(int i1,int i2,int i3){
   return(co_names[i1]+"-R,<br>"+co_names[i2]+"-G,<br>"+co_names[i3]+"-B");
}

Затем формируем HTML-код для ячеек с именами оптимизируемых параметров и заканчиваем строку таблицы:

for(int j=0;j<pc;j++){
   s=s+"<th>"+aOpt.ParameterName[j]+"</th>";       
}
s=s+"</tr>";     

После этого объявлены три вспомогательные переменные r, g, b и идет цикл, в котором выполняется формирование HTML-кода всех строк отчета. В начале каждого прохода цикла рассчитываются значения RGB компонентов:

ComponentsToRGB(aOpt.Pass[i].ColorComponent[0],
                aOpt.Pass[i].ColorComponent[1],
                aOpt.Pass[i].ColorComponent[2],
                r,g,b);

Код функции ComponentsToRGB():

void ComponentsToRGB(double c1,double c2,double c3,int & r,int & g,int & b){
   r=(int)(c1*255.0);
   g=(int)(c2*255.0);
   b=(int)(c3*255.0);
}

Затем формируется HTML-код строки, а в ней сначала ячейки с результатами тестирования:

s=s+"<tr>";
   
s=s+"<td>"+aOpt.Pass[i].Pass+"</td>";
s=s+"<td>"+aOpt.Pass[i].Result+"</td>";   
s=s+"<td>"+aOpt.Pass[i].Profit+"</td>";         
s=s+"<td>"+aOpt.Pass[i].ExpectedPayoff+"</td>";   
s=s+"<td>"+aOpt.Pass[i].ProfitFactor+"</td>";               
s=s+"<td>"+aOpt.Pass[i].RecoveryFactor+"</td>";        
s=s+"<td>"+aOpt.Pass[i].SharpeRatio+"</td>";               
s=s+"<td>"+aOpt.Pass[i].Custom+"</td>";   
s=s+"<td>"+aOpt.Pass[i].EquityDD_perc+"</td>";        
s=s+"<td>"+aOpt.Pass[i].Trades+"</td>";      

После этого идет ячейка с цветным показателем. Сначала компоненты RGB преобразуются в строку функцией RGBToStr(), затем формируется код ячейки:

string cs=RGBToStr(r,g,b);
s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>"; 

Код функции RGBToStr():

string RGBToStr(int r,int g,int b){
   return("rgb("+(string)r+","+(string)g+","+(string)b+")");
}

Завершают строку ячейки со значениями оптимизируемых параметров:

for(int j=0;j<pc;j++){
   s=s+"<td>"+aOpt.Pass[i].Parameters[j]+"</td>";       
}

s=s+"</tr>"

В конце функции таблица закрывается и содержимое переменной s возвращается:

s=s+"</table>";   

return(s);

Плоскости с оптимизируемыми параметрами

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


Рис. 2. Плоскость оптимизируемых параметров

В начале изображения показано соответствие параметров осям, в частности, на показанном изображении по оси Х (по горизонтали) расположены значения параметра Inp_Signal_MACD_PeriodSlow, а по оси Y (по вертикали) — Inp_Signal_MACD_PeriodFast. На пересечениях, в ячейках градиентом показано, как менялись результаты тестирования для данной пары значений параметра Х и Y при изменении других оптимизируемых параметров. С левого края градиента находится цвет наихудшего варианта, справа — наилучшего. Определение наилучшего и наихудшего вариантов выполняется по упоминавшемуся ранее фактору сортировки, рассчитываемому как среднее арифметическое абстрактных компонентов цвета.

Формирование HTML-кода плоскостей выполняется в функции Color2DPlanes(), в этой функции выполняется перебор всех возможных сочетаний по два оптимизируемых параметра и для каждой пары формируется HTML-код плоскости. Код функции Color2DPlanes():

string Color2DPlanes(SOptimization2 & aOpt){
   string s="";
   int pc=ArraySize(aOpt.ParameterName);
   for(int y=0;y<pc;y++){
      for(int x=y+1;x<pc;x++){
         s=s+Color2DPlane(aOpt,x,y);         
      }   
   }
   return(s);
}

Формирование HTML-кода одной плоскости выполняется в функции  Color2DPlane():

string Color2DPlane(SOptimization2 & aOpt,int xi,int yi){

   double xa[];
   double ya[];
   
   int cnt=ArraySize(aOpt.Pass);

   ArrayResize(xa,cnt);
   ArrayResize(ya,cnt);
   
   for(int i=0;i<cnt;i++){
      xa[i]=aOpt.Pass[i].dParameters[xi];
      ya[i]=aOpt.Pass[i].dParameters[yi];      
   }
   
   ArraySort(xa);
   ArraySort(ya);
   
   int xc=1;
   int yc=1;
   
   for(int i=1;i<cnt;i++){
      if(xa[i]!=xa[i-1]){
         xa[xc]=xa[i];
         xc++;
      }
      if(ya[i]!=ya[i-1]){
         ya[xc]=ya[i];
         yc++;
      }
   }   

   string s="<hr><h3>X - "+aOpt.ParameterName[xi]+", Y - "+aOpt.ParameterName[yi]+"</h3><table>";


   s=s+"<tr>";   
      s=s+"<td>&nbsp;</td>";
      for(int x=0;x<xc;x++){
         s=s+"<td>"+(string)xa[x]+"</td>";
      }
   s=s+"</tr>";   
   for(int y=0;y<yc;y++){
      
      s=s+"<tr>";
      
      s=s+"<td>"+(string)ya[y]+"</td>";
      for(int x=0;x<xc;x++){

         double mx=0;
         double mn=DBL_MAX;
         int mxi=0;
         int mni=0; 
         
         for(int i=0;i<cnt;i++){
            if(aOpt.Pass[i].dParameters[yi]==ya[y] && 
               aOpt.Pass[i].dParameters[xi]==xa[x]
            ){
               if(aOpt.Pass[i].SortFactor>mx){
                  mx=aOpt.Pass[i].SortFactor;
                  mxi=i;
               }
               if(aOpt.Pass[i].SortFactor<mn){
                  mn=aOpt.Pass[i].SortFactor;
                  mni=i;
               }
            }
         }
         
         int mnr,mng,mnb;
         int mxr,mxg,mxb;
         
         ComponentsToRGB(aOpt.Pass[mni].ColorComponent[0],
                         aOpt.Pass[mni].ColorComponent[1],
                         aOpt.Pass[mni].ColorComponent[2],
                         mnr,mng,mnb);
                         
         ComponentsToRGB(aOpt.Pass[mxi].ColorComponent[0],
                         aOpt.Pass[mxi].ColorComponent[1],
                         aOpt.Pass[mxi].ColorComponent[2],
                         mxr,mxg,mxb);         
        
         string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
               
         int digits[]={2,2,6,6,6,6,6,4,0};

         for(int k=0;k<ArraySize(co_names);k++){
            title=title+co_names[k]+": "+DoubleToString(aOpt.Pass[mni].factor[k],digits[k])+
            "/"+DoubleToString(aOpt.Pass[mxi].factor[k],digits[k])+"\n";
         }

         s=s+"<td title='"+title+"' style='background: linear-gradient(to right, rgb("+
         (string)mnr+","+(string)mng+","+(string)mnb+"), rgb("+
         (string)mxr+","+(string)mxg+","+(string)mxb+"));'>&nbsp;"+
         (string)mni+"-"+(string)mxi+"</td>";
      }
      s=s+"</tr>";
   }
   
   s=s+"<table>";   

   return(s);

}

Рассмотрим функцию Color2DPlane() подробно. В функцию передается структура SOptimization2 со всеми данными из отчета оптимизации и две переменные int: xi и yi — индексы пары оптимизируемых параметров, для которых надо построить плоскость. Сначала соберем в массивы все возможные варианты значений каждого из пары параметров, для этого объявим два массива, изменим их размеры в соответствии с количеством проходов оптимизации и заполним их всеми вариантами значений:

double xa[];
double ya[];

int cnt=ArraySize(aOpt.Pass);

ArrayResize(xa,cnt);
ArrayResize(ya,cnt);

for(int i=0;i<cnt;i++){
   xa[i]=aOpt.Pass[i].dParameters[xi];
   ya[i]=aOpt.Pass[i].dParameters[yi];      
}

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

ArraySort(xa);
ArraySort(ya);

int xc=1;
int yc=1;

for(int i=1;i<cnt;i++){
   if(xa[i]!=xa[i-1]){
      xa[xc]=xa[i];
      xc++;
   }
   if(ya[i]!=ya[i-1]){
      ya[xc]=ya[i];
      yc++;
   }
}   

После этого в переменной xc имеем количество уникальных значений одного параметра, а в переменной yc — другого. В строковую переменную s будем добавлять HTML-код плоскости, по мере его формирования. При объявлении переменной s сразу добавим в нее информацию об именах переменных и открывающий тег таблицы:

string s="<hr><h3>X - "+aOpt.ParameterName[xi]+", Y - "+aOpt.ParameterName[yi]+"</h3><table>";

Сформируем первую строку таблицы со значениями параметра x:

s=s+"<tr>";   
   s=s+"<td>&nbsp;</td>";
   for(int x=0;x<xc;x++){
      s=s+"<td>"+(string)xa[x]+"</td>";
   }
s=s+"</tr>"; 

Далее цикл по всем вариантам параметра y:

for(int y=0;y<yc;y++){

В цикле на каждом проходе начинаем строку и добавляем ячейку со значением параметра y:

s=s+"<tr>";
      
s=s+"<td>"+(string)ya[y]+"</td>";

Следом добавляем ячейки с градиентом, они добавляются в цикле по всем вариантам параметра x:

for(int x=0;x<xc;x++){

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

double mx=0;
double mn=DBL_MAX;
int mxi=0;
int mni=0; 

for(int i=0;i<cnt;i++){
   if(aOpt.Pass[i].dParameters[yi]==ya[y] && 
      aOpt.Pass[i].dParameters[xi]==xa[x]
   ){
      if(aOpt.Pass[i].SortFactor>mx){
         mx=aOpt.Pass[i].SortFactor;
         mxi=i;
      }
      if(aOpt.Pass[i].SortFactor<mn){
         mn=aOpt.Pass[i].SortFactor;
         mni=i;
      }
   }
}

После выполнения этой части кода в переменных mxi и mni будут находиться индексы наилучшего и наихудшего проходов оптимизации. 

Конвертируем абстрактные компоненты цвета в RGB:

ComponentsToRGB(aOpt.Pass[mni].ColorComponent[0],
                aOpt.Pass[mni].ColorComponent[1],
                aOpt.Pass[mni].ColorComponent[2],
                mnr,mng,mnb);
                         
ComponentsToRGB(aOpt.Pass[mxi].ColorComponent[0],
                aOpt.Pass[mxi].ColorComponent[1],
                aOpt.Pass[mxi].ColorComponent[2],
                mxr,mxg,mxb);  

Для удобства анализа плоскостей добавим всплывающие подсказки (делаются при помощи HTML-атрибута title):

string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
      
int digits[]={2,2,6,6,6,6,6,4,0};

for(int k=0;k<ArraySize(co_names);k++){
   title=title+co_names[k]+": "+DoubleToString(aOpt.Pass[mni].factor[k],digits[k])+
   "/"+DoubleToString(aOpt.Pass[mxi].factor[k],digits[k])+"\n";
}

Что из себя представляют эти подсказки показано на рис. 3.


Рис. 3. Всплывающая подсказка для одной ячейки плоскости

В этой подсказке имеются все данные о худшем и лучшем проходе оптимизации (сначала худший, потом, после знака "/" лучший). В первой строке подсказки указаны значения компонентов RGB градиента. 

Теперь самое главное — градиент:

s=s+"<td title='"+title+"' style='background: linear-gradient(to right, rgb("+
(string)mnr+","+(string)mng+","+(string)mnb+"), rgb("+
(string)mxr+","+(string)mxg+","+(string)mxb+"));'>&nbsp;"+
(string)mni+"-"+(string)mxi+"</td>";

Отображение градиента проверялось в браузерах: Opera, Google Chrome, Яндекс-браузер и Microsoft-Edge, во всех работает.

В конце каждой строки добавляем тег окончания строки:

s=s+"</tr>";

В конце таблицы добавляем тег окончания таблицы и возвращаем сформированный HTML-код:

s=s+"<table>";   

return(s);

Осталось вызвать функции из скрипта:

string report=HTMLStart("Color Optimization","style2.css")+
TableContent(opt,Factor1,Factor2,Factor3)+
Color2DPlanes(opt)+HTMLEnd();
    

Функции HTMLStart() и HTMLEnd() взяты из статьи  Анализ торговли по HTML-отчетам, файл стилей тоже взят из этой статьи, немного изменен и переименован в style2.css.

В приложении к статье можно найти готовые файлы ColorOptimization.mqh, скрипт ColorOptimization.mq5. 

Изменение цветовой модели

Код в файле ColorOptimization.mqh структурирован таким образом, чтобы его было легко доработать для другой цветовой модели. Добавим вариант с цветовой моделью CMY. Сначала выполним несколько предварительных шагов.

1. Делаем копии файлов ColorOptimization.mqh и ColorOptimization.mq5 с именами ColorOptimization2.mqh, скрипт ColorOptimization2.mq5. 

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

#define MODEL_RGB 0
#define MODEL_CMY 1

int co_ColorModel;

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

enum EColorModel{
   RGB=MODEL_RGB,
   CMY=MODEL_CMY
};

input EColorModel          ColorModel     =  RGB;

В начале функции OnStart() скрипта переменной co_ColorModel присвоим значение выбранное в окне свойств:

co_ColorModel=ColorModel;

Теперь основная доработка функций файла ColorOptimization2.mqh. Главный момент доработки — изменение в функции ComponentsToRGB(). В модели CMY значения компонентов изменяются от 0 до 1, значит значения компонентов из структуры с данными отчета соответствуют компонентам CMY, остается перевести их в RGB. Получаем такую функцию ComponentsToRGB():

void ComponentsToRGB(double c1,double c2,double c3,int & r,int & g,int & b){
   if(co_ColorModel==MODEL_RGB){
      r=(int)(c1*255.0);
      g=(int)(c2*255.0);
      b=(int)(c3*255.0);
   }
   else if(co_ColorModel==MODEL_CMY){
      CMYtoRGB(c1,c2,c3,r,g,b);
   }
}

Чтобы было понятней как выполняется перевод модели CMY в RGB, он выполнен в отдельной функции:

void CMYtoRGB(double C,double M,double Y,int & R,int & G,int & B){
   R=(int)((1.0-C)*255.0);
   G=(int)((1.0-M)*255.0);
   B=(int)((1.0-Y)*255.0);
}

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

string HStyle(int i,int i1,int i2,int i3){
   if(co_ColorModel==MODEL_RGB){
      if(i==i1)return(" style='background-color: rgb(255,0,0);'");
      if(i==i2)return(" style='background-color: rgb(0,255,0);'");
      if(i==i3)return(" style='background-color: rgb(0,0,255);'");
   }
   else if(co_ColorModel==MODEL_CMY){
      if(i==i1)return(" style='background-color: rgb(0,255,255);'");
      if(i==i2)return(" style='background-color: rgb(255,0,255);'");
      if(i==i3)return(" style='background-color: rgb(255,255,0);'");      
   }
   return("");
}

Доработка функции ColorCollHeader() для правильного заголовка колонки цветного показателя:

string ColorCollHeader(int i1,int i2,int i3){
   if(co_ColorModel==MODEL_RGB){
      return(co_names[i1]+"-R,<br>"+co_names[i2]+"-G,<br>"+co_names[i3]+"-B");
   }
   else if(co_ColorModel==MODEL_CMY){
      return(co_names[i1]+"-C,<br>"+co_names[i2]+"-M,<br>"+co_names[i3]+"-Y");   
   }
   return "";
}

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

string cs=RGBToStr(r,g,b);
s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>";   

меняются на код:

string ts="",cs=RGBToStr(r,g,b);

if(co_ColorModel==MODEL_RGB){    
   ts=cs;
}
else if(co_ColorModel==MODEL_CMY){
   ts=CMYToStr(aOpt.Pass[i].ColorComponent[0],
               aOpt.Pass[i].ColorComponent[1],
               aOpt.Pass[i].ColorComponent[2]);
}
s=s+"<td title='"+ts+"' style='background-color: "+cs+"'>&nbsp</td>";     

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

string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";

меняем на код:

string title="";

if(co_ColorModel==MODEL_RGB){
   title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
}
else if(co_ColorModel==MODEL_CMY){         
   title=CMYToStr(aOpt.Pass[mni].ColorComponent[0],
                  aOpt.Pass[mni].ColorComponent[1],
                  aOpt.Pass[mni].ColorComponent[2])+"/"+
         CMYToStr(aOpt.Pass[mxi].ColorComponent[0],
                  aOpt.Pass[mxi].ColorComponent[1],
                  aOpt.Pass[mxi].ColorComponent[2])+"\n";                            
}

Теперь, при запуске скрипта можно выбирать тип цветовой модели. Отличие цветовой модели CMY от RGB заключается в том, что в CMY лучшие показатели будут черными и оттенки будут другими (рис. 4, 5).


Рис. 4. Фрагмент отчета созданного с использованием цветовой модели CMY


Рис. 5. Цветовая плоскость полученная с использованием цветовой модели CMY

Интерпретация цветных показателей

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

Рассмотрим подробно модель RGB. Когда значения всех компонентов равно 0, получаем черный цвет. Когда все компоненты равны максимальному значению — белый, любые другие сочетания значений дают различные оттенки. Если один компонент имеет максимальное значение, а два других равны 0, то, очевидно, в качестве результирующего имеем чистый цвет соответствующего компонента: красный, зеленый или синий. Если два компонента имеют максимальные значения, а третий равен нулю, то тоже имеем чистые цвета. Красный и зеленый дают желтый, зеленый и синий — циан, красный и синий — маджента. На рис. 6 показано несколько  сочетаний компонентов RGB.


Рис. 7. Основные сочетания компонентов RGB

На основании оттенка можно судить о том, какой показатель вносит больше положительного влияние на результат тестирования. Красный оттенок — первый показатель, желтый — первый и второй показатели, зеленый — третий показатель и т.д.

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


Рис. 7. Основные сочетания компонентов CMY

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

Как видим, принципиальной разницы в использовании модели RGB или CMY нет. 

Заключение

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

Файлы приложения

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