交易策略的色彩优化

Dmitry Fedoseev | 4 四月, 2019

简介

经过优化后,我们只需要从各种各样的参数集中选择一个。在选择这样的一个集合时,没有明确的答案可以使用什么标准:盈利能力、回撤、恢复系数或这些或其他参数的某种组合。但是如何评价参数组合呢?

在本文中,我们将进行一个实验:我们将使用颜色优化结果。颜色由三个参数决定:红色、绿色和蓝色(RGB)的级别。还有其他的颜色编码方法,它们也使用三个参数。因此,可以将三个测试参数转换为一种颜色,以直观方式表示这些值。在本文的最后,我们将了解这种表示是否有用。

初始数据

在文章使用 HTML 报告分析交易结果中, 我们为分析报告文件创建了一个函数库, HTMLReport.mqh. 库中包含 OptimizerXMLReportToStruct()函数,该函数是为具有优化结果的操作而设计的。我们将使用此函数向函数传递两个参数:

  • string aFileName 是包含优化报告的文件名,该文件必须在终端数据目录的 MQL5/Files文件夹中可用。
  • SOptimization & aOptimization 是通过引用传递的,函数执行后,从报告中提取的数据将位于该结构中。

SOptimisation 结构:

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

该结构包括两个数组:string ParameterName[] 和 SPAS 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[] — 优化的参数数值数组.

以下是分析交易结果最常用的参数:

  • Profitability(盈利能力),每笔交易的平均利润
  • Drawdown(回撤), 相对最大值的净值下降值
  • Recovery factor(采收系数),绝对利润与最大回撤之比

首先,我们将使用这些参数。但是,报告包含其他值,因此我们需要提供使用这些值的可能性。 

为了启用任意参数选择,我们将创建一个额外的结构来替换SPass。优化参数将位于该结构中的双精度数组中。我们不会完全重写结构,而是使用继承可能性。让我们继续实现部分:

1. 创建 ColorOptimization.mqh 文件,创建彩色报告的所有函数都将位于此文件中。

2. 在 ColorOptimization.mqh 文件的开头关联 HtmlReport.mqh 文件:

#include <HTMLReport.mqh>

3. 创建一个新的结构,它继承 SPass 结构的所有栏位,并且在其中加上 factor[] 和 dParameters[] 数组:

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

两个数组都是双精度类型的,在 factor[] 数组中将有九个结果值,也就是除了 Pass (测试编号) 的所有将要优化的参数。优化参数的值位于sParameters[]数组中。虽然结构中已经有了所有的数据,但是它们是以字符串格式呈现的,所以每次使用数据时,我们都需要将它们转换为数字。数组允许以方便的格式保存数据。

4. 为优化数据创建最终结构:

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

5. 创建一个将数据从sOptimeization结构转换为sOptimeization2的函数:

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. 创建一个函数,用于将报告文件加载到sOptimeization2结构中,该结构与 HtmlReport.mqh 中的 OptimizerXMLReportToStruct()类似:

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

报告文件名作为第一个参数传递给函数,填充的 sOptimeration2 结构作为第二个参数返回。

现在一切就绪,可以解决本文的主要任务了。 

创建彩色报告

用于创建彩色报告的函数将位于ColorOptimization.mqh中。调用这些函数将从脚本执行。

1. 让我们创建一个脚本 ColorOptimization.mq5.

2. 把 ColorOptimization.mqh 与 ColorOptimization.mq5 关联.

#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("错误的 OptimizerXMLReportToStruct2");
   return;
}

5. 由于RGB只是许多不同颜色模型中的一种,因此让我们提供进一步修改库的可能性,特别是添加其他颜色模型。这就是为什么我们将从0到1的抽象值计算开始,而不是计算RGB组件的值。然后我们将这些值转换为从0到255的RGB组件。每个优化过程都使用单独的颜色指示,因此我们需要为SPass2添加三个颜色组件字段。我们不添加三个字段,而是添加一个三元素数组:

double ColorComponent[3];

6. ColorOptimization.mqh 中的 SolveColorComponents() 函数将计算颜色组成部分,应该向函数中传入以下参数:

  • SOptimization2 & aOpt — 源优化报告中的数据
  • int i1, int i2, int i3 — 源优化报告中数值的索引 (SPass 结构中的 factor[9] 数组)
  • 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;
         }
      }
   }
}

从脚本调用这些函数。排序因子不仅用于对表进行排序,因此无论 Sort 变量的值如何,都将调用 SolveSortFactor():

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

现在一切就绪,可以创建报告了。报告由两部分组成,第一个是带有附加颜色按钮的优化数据表的副本(图1)。第二部分由多个彩色平面(表)组成,每对优化参数,每个单元将显示给定一对优化参数测试结果中反映变化的梯度(图2)。

带颜色指示的表格

在 TableContent()函数中创建带有附加颜色指示的表。此函数位于ColorOptimization.mqh文件中,并返回表的HTML代码。

创建HTML表是一项简单的任务。颜色指示单元格的颜色是通过指定单元格样式“background-color”属性来设置的。范围在0到1之间的颜色分量可以通过乘以值很容易地转换为范围在1到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"};

表的HTML代码将在其形成过程中添加到变量s中,因此在变量声明过程中,我们将添加表开始标记:

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>";      

然后是颜色指示单元,首先使用 RGBToStr()函数将 RGB 组分转换为字符串;然后生成单元代码:

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. 优化参数平面

第一行显示对应于平面轴的参数:沿X轴(水平)显示 Inp_Signal_MACD_PeriodSlow的值,沿Y轴显示 Inp_Signal_MACD_PeriodSlow的值。单元中的梯度显示了当其他参数发生变化时,这对x和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() 函数。下面是输入函数:由优化报告中的所有数据和两个 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变量包含其他参数的唯一值。平面的HTML代码将在其形成过程中添加到变量s。在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;
      }
   }
}

执行此代码部分后,mximni变量将包含最佳和最差优化过程的索引。 

然后需要将抽象颜色组件转换为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>";

在以下Web浏览器中检查了渐变显示:Opera、Google Chrome、Yandex浏览器和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();
    

我使用了来自文章使用 HTML 报告分析交易结果的 HTMLStart()和 HTMLEnd()函数,来自同一篇文章的样式文件被稍微更改并重命名为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之间的差异是,最佳值以黑色显示,其他颜色也将不同(图4、5)。


图 4. 使用CMY颜色模型创建的报告片段


图 5. CMY颜色模型中的颜色平面

如何解释颜色指示

RGB中的最佳选项接近白色,而CMY中的最佳选项接近黑色。为了正确解释其他颜色,我们需要了解颜色模型中的各个组件是如何组合的,以及结果颜色是如何形成的。

让我们更详细地查看RGB模型。当所有分量的值等于0时,我们得到黑色。当所有组件都等于最大值时,颜色为白色。所有其他组合提供不同的颜色。如果其中一个组件的值最高,而另外两个值等于0,则得到相应组件的清晰颜色:红色、绿色或蓝色。如果两个分量有最大值,而第三个分量为零,则结果颜色也很清楚。红色和绿色的结果是黄色,绿色和蓝色提供青色,红色和蓝色显示为洋红。图6显示了几种RGB组分的组合。


图 7. RGB组分的基本组合

基于阴影,我们可以了解哪些参数指标对测试结果的贡献更为积极。如果为红色,则为第一个参数;如果颜色为黄色,则为第一个和第二个参数;绿色表示第三个参数等。

RGB模型中的颜色与彩色灯光的添加类似。在CMY模型中,值从白色中减去,因此所有组件的最大值对应于黑色。CMY模型类似于混合颜料:如果没有颜料,我们就有一张白纸;如果混合了太多不同的颜料,你就会得到黑色(或者更确切地说,在处理真正的颜料时是一种肮脏的颜色)。图8. 显示CMY组分的基本组合。


图 7. CMY组分的基本组合

与RGB相比,CMY中的颜色发生了转换。解释如下:青色为第一参数,蓝色为第一和第二参数,洋红为第二参数,红色为第二和第三值,黄色为第三参数,绿色为第一和第三参数。

如您所见,在使用 RGB 或 CMY 模型方面没有根本区别。 

结论

颜色的感知是一个主观的过程,因此很难对颜色表示的便利性和益处作出明确的结论。至少有一个视觉指示,即亮度(即与RGB中的白色接近),允许评估三个参数的组合。这可以简化报表分析。当选择是自动化的(如本文中所述)时,基于排序表的决策是根据三个值的算术平均值做出的。这可以看作是进入模糊逻辑领域的第一步,利用模糊逻辑,最终值的计算不是简单的算术平均值,而是更复杂的方法。然而,我们需要更多的实际实验来评估这种方法的有效性。

附件

除了脚本创建的报告以外,所有文件都安排在文件夹中,因为它们应该位于终端文件夹中。打开终端数据文件夹并将 MQL5 文件夹复制到其中。