交易策略的色彩优化(基础篇)
用颜色把策略状态钉在图表上
在 MT5 里做策略可视化,最容易被忽略的一步是给不同状态上色。很多人写好了逻辑,却只在日志里打印结果,盯盘时根本分不清当前处在哪一段行情。 Dmitry Fedoseev 在 2019 年 4 月 4 日发布的测试者示例里,用 PlotIndexSetInteger 直接改指标线的颜色,让趋势段和震荡段一眼可辨。这类做法不增加计算量,只在渲染层做区分,对贵金属和外汇这类高波动品种尤其实用。 外汇与贵金属杠杆高、滑点随机,单看数值容易误判。把策略状态用颜色固化到主图,能降低盯盘认知负荷,但颜色只是辅助,不代表任何方向确定性。
◍ 把优化结果染成颜色看
做 EA 参数优化后,真正麻烦的是从一堆解里挑一个。盈利、回撤、恢复系数单独看都不够,组合权重也没有标准答案。 我们可以换一种思路:把三个测试参数直接映射成 RGB 颜色值。红绿蓝各代表一个维度,参数空间就变成一张肉眼能扫的色图,不必先定好评分公式。 这次实验就干这件事——用颜色编码优化结果,到最后再判断这种可视化到底有没有用,而不是预先假定它有效。外汇与贵金属测试请认清:历史拟合不代表实盘概率。
「从优化报告到可计算数据结构」
做策略优化后处理,第一步是把 MT5 优化器导出的 XML 报告变成能直接算数的结构。原报告解析库里,SOptimization 套着 SPass[],但 SPass 所有字段都是 string:Result、Profit、SharpeRatio 这些全是文本,每次用都得现转 double,跑大量 Pass 时既慢又容易写错。 SPass 里我们真正要用的栏位就那几个:Profit 是结果利润值,RecoveryFactor 是绝对利润与最大回撤之比,EquityDD_perc 是回撤百分比,SharpeRatio 是夏普比率,Trades 是成交笔数。一个优化 Pass 对应数组里一个元素,参数名另存在 ParameterName[]。 直接改原结构风险大,所以用继承:新建 SPass2 : SPass,只加两个 double 数组——factor[9] 和 dParameters[]。factor[9] 正好装除 Pass 编号外的 9 个数值字段,dParameters[] 装优化参数本身。这样老代码不破,新数据随手取。 转换函数 ConvertOptimizationStruct 干的事很直白:先 ArraySize 拿 Pass 总数,ArrayResize 给新结构扩好,然后逐条把字符串字段原样拷过去,再把 9 个关键值用 StringToDouble 填进 factor[]。注意 factor 下标从 0 到 8 依次对应 Result、Profit、ExpectedPayoff、ProfitFactor、RecoveryFactor、SharpeRatio、Custom、EquityDD_perc、Trades——这个顺序建议用枚举锁死,后面按索引取数才不会错位。 外汇与贵金属策略优化结果受样本外漂移影响大,这类结构只解决“算得动”的问题,不代表某组参数在未来实盘有概率优势。开 MT5 把这段 struct 和转换函数丢进 ColorOptimization.mqh,引一下 HTMLReport.mqh,就能直接吃优化报告做后续筛选。
class="kw">struct SOptimization{ class="type">class="kw">string ParameterName[]; SPass Pass[]; }; class="kw">struct SPass{ class="type">class="kw">string Pass; class="type">class="kw">string Result; class="type">class="kw">string Profit; class="type">class="kw">string ExpectedPayoff; class="type">class="kw">string ProfitFactor; class="type">class="kw">string RecoveryFactor; class="type">class="kw">string SharpeRatio; class="type">class="kw">string Custom; class="type">class="kw">string EquityDD_perc; class="type">class="kw">string Trades; class="type">class="kw">string Parameters[]; }; class="macro">#include <HTMLReport.mqh> class="kw">struct SPass2:SPass{ class="type">class="kw">double factor[class="num">9]; class="type">class="kw">double dParameters[]; }; class="kw">struct SOptimization2{ class="type">class="kw">string ParameterName[]; SPass2 Pass[]; }; class="type">void ConvertOptimizationStruct(SOptimization & src,SOptimization2 & dst){ ArrayCopy(dst.ParameterName,src.ParameterName); class="type">int cnt=ArraySize(src.Pass); ArrayResize(dst.Pass,cnt); for(class="type">int i=class="num">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[class="num">0]=StringToDouble(src.Pass[i].Result); dst.Pass[i].factor[class="num">1]=StringToDouble(src.Pass[i].Profit); dst.Pass[i].factor[class="num">2]=StringToDouble(src.Pass[i].ExpectedPayoff); dst.Pass[i].factor[class="num">3]=StringToDouble(src.Pass[i].ProfitFactor); dst.Pass[i].factor[class="num">4]=StringToDouble(src.Pass[i].RecoveryFactor); dst.Pass[i].factor[class="num">5]=StringToDouble(src.Pass[i].SharpeRatio); dst.Pass[i].factor[class="num">6]=StringToDouble(src.Pass[i].Custom); dst.Pass[i].factor[class="num">7]=StringToDouble(src.Pass[i].EquityDD_perc); dst.Pass[i].factor[class="num">8]=StringToDouble(src.Pass[i].Trades); class="type">int pc=ArraySize(src.Pass[i].Parameters);
把优化报告的字符串参数转成数值数组
做 MT5 策略优化结果二次处理时,XML 导出的参数全是字符串,直接算夏普、盈利因子没门。上面这段就是把 src.Pass[i].Parameters 里的 "true"/"false"/数字串,逐一塞进 dst.Pass[i].dParameters 双精度数组:布尔映射成 1 和 0,其余走 StringToDouble。
枚举 EOptizatrionFactor 定义了 9 个排序维度,从 Result=0 到 Trades=8,回测里按哪个字段挑参数全靠它。注意原文拼写 EOptimzatrionFactor 漏了字母,复制代码时最好顺手改对,不然编译报错查半天。
OptimizerXMLReportToStruct2 只是薄封装:先调 OptimizerXMLReportToStruct 读 XML,成功再 ConvertOptimizationStruct 转成你要的结构体。想验证,开 MT5 跑一次 EA 优化、导出 XML,把这段贴进脚本看 dParameters 是否对齐。外汇与贵金属品种波动大,优化结果过拟合概率高,参数上线前务必样本外验证。
ArrayResize(dst.Pass[i].dParameters,pc); for(class="type">int j=class="num">0;j<pc;j++){ if(src.Pass[i].Parameters[j]=="true"){ dst.Pass[i].dParameters[j]=class="num">1; } else if(src.Pass[i].Parameters[j]=="class="kw">false"){ dst.Pass[i].dParameters[j]=class="num">0; } else{ dst.Pass[i].dParameters[j]=StringToDouble(src.Pass[i].Parameters[j]); } } } } enum EOptimizatrionFactor{ Result=class="num">0, Profit=class="num">1, ExpectedPayoff=class="num">2, ProfitFactor=class="num">3, RecoveryFactor=class="num">4, SharpeRatio=class="num">5, Custom=class="num">6, EquityDD_perc=class="num">7, Trades=class="num">8 }; class="type">bool OptimizerXMLReportToStruct2(class="type">class="kw">string aFileName,SOptimization2 & aOptimization){ SOptimization tmp; if(!OptimizerXMLReportToStruct(aFileName,tmp)){ class="kw">return(class="kw">false); } ConvertOptimizationStruct(tmp,aOptimization); class="kw">return(true); }
◍ 把优化报告染成三通道色图
想直观看 EA 优化结果,光看 XML 报表不够。把每次 pass 的三个因子映射成 RGB 三原色,白格就意味着三项都接近最优,眼睛一扫就能定位好参数区。外汇与贵金属优化含极高风险,色图只是辅助筛选,不代表实盘盈利概率。 具体落地先建脚本 ColorOptimization.mq5,挂上 ColorOptimization.mqh 头文件。外部参数里 ReportName 接源报告、OutputName 定输出、Factor1~3 挑三个染色因子,各自带 Invert 反转开关,Sort 控制是否按色排序。 核心在 SolveColorComponents():先扫全部 pass 找出三因子的最大最小值,把每个值归一到 0~1,再转 255 阶 RGB 写进 SPass 的 ColorComponents[3]。抽象值从 0~1 算起,以后换 HSL 等其他色彩模型也只需改这一层。 排序因子取三分量算术平均,最佳 pass 趋近纯白。脚本里无论 Sort 开关都跑一遍 SolveSortFactor(),气泡法排完序,报告就由带颜色按钮的数据表加多张双因子梯度平面组成。
class="macro">#include <ColorOptimization.mqh> class="macro">#class="kw">property script_show_inputs class="kw">input class="type">class="kw">string ReportName = "*.xml"; class="kw">input class="type">class="kw">string OutputName = "ColorOptimization1-class="num">1.htm"; class="kw">input EOptimizatrionFactor Factor1 = Profit; class="kw">input EOptimizatrionFactor Factor2 = EquityDD_perc; class="kw">input EOptimizatrionFactor Factor3 = RecoveryFactor; class="kw">input class="type">bool Factor1Invert = class="kw">false; class="kw">input class="type">bool Factor2Invert = true; class="kw">input class="type">bool Factor3Invert = class="kw">false; class="kw">input class="type">bool Sort = true; SOptimization2 opt; if(!OptimizerXMLReportToStruct2(ReportName,opt)){ Alert("错误的 OptimizerXMLReportToStruct2"); class="kw">return; } class="type">class="kw">double ColorComponent[class="num">3]; class="type">void SolveColorComponents( SOptimization2 & aOpt, class="type">int i1,class="type">int i2,class="type">int i3, class="type">bool r1=class="kw">false,class="type">bool r2=class="kw">false,class="type">bool r3=class="kw">false){ class="type">class="kw">double mx[class="num">3]={class="num">0,class="num">0,class="num">0}; class="type">class="kw">double mn[class="num">3]={DBL_MAX,DBL_MAX,DBL_MAX}; class="type">int size=ArraySize(aOpt.Pass); for(class="type">int i=class="num">0;i<size;i++){ mx[class="num">0]=MathMax(mx[class="num">0],aOpt.Pass[i].factor[i1]); mx[class="num">1]=MathMax(mx[class="num">1],aOpt.Pass[i].factor[i2]); mx[class="num">2]=MathMax(mx[class="num">2],aOpt.Pass[i].factor[i3]); mn[class="num">0]=MathMin(mn[class="num">0],aOpt.Pass[i].factor[i1]);
「把多因子压成可排序的彩色权重」
这段逻辑干的事,是把优化结果里三个因子分别做极差归一化,再合成一个 SortFactor 用于排序。归一化前先取每组因子的全局最大 mx[] 与最小 mn[],分母用 mx[k]-mn[k],若跨度为 0 则 c 值保持 0,避免除零崩脚本。 r1/r2/r3 三个反转开关很关键:打开时 c = 1.0 - c,意味着该因子越小反而越优(比如回撤类指标)。算完的 c1/c2/c3 直接写进 ColorComponent[0..2],供可视化着色用。 SortFactor 取三个分量均值,即 (c1+c2+c3)/3,范围落在 0~1。SortFactorSort 是手写冒泡,从尾到头两两比较,SortFactor 小的往后沉,最终数组按综合分降序排列。 在 MT5 里跑这套,重点看 r1~r3 的勾选:同一组优化结果,反转某个因子可能让排名前 10 的 Pass 完全换血。外汇与贵金属市场高杠杆、滑点漂移大,回测排序靠前不等于实盘概率占优,须用样本外数据复核。
mn[class="num">1]=MathMin(mn[class="num">1],aOpt.Pass[i].factor[i2]); mn[class="num">2]=MathMin(mn[class="num">2],aOpt.Pass[i].factor[i3]); } class="type">class="kw">double c1,c2,c3,d; for(class="type">int i=class="num">0;i<size;i++){ c1=class="num">0; c2=class="num">0; c3=class="num">0; d=mx[class="num">0]-mn[class="num">0]; if(d!=class="num">0){ c1=(aOpt.Pass[i].factor[i1]-mn[class="num">0])/d; } d=mx[class="num">1]-mn[class="num">1]; if(d!=class="num">0){ c2=(aOpt.Pass[i].factor[i2]-mn[class="num">1])/d; } d=mx[class="num">2]-mn[class="num">2]; if(d!=class="num">0){ c3=(aOpt.Pass[i].factor[i3]-mn[class="num">2])/d; } if(r1)c1=class="num">1.0-c1; if(r2)c2=class="num">1.0-c2; if(r3)c3=class="num">1.0-c3; aOpt.Pass[i].ColorComponent[class="num">0]=c1; aOpt.Pass[i].ColorComponent[class="num">1]=c2; aOpt.Pass[i].ColorComponent[class="num">2]=c3; } } SolveColorComponents(opt,Factor1,Factor2,Factor3,Factor1Invert,Factor2Invert,Factor3Invert); class="type">class="kw">double SortFactor; class="type">void SolveSortFactor(SOptimization2 & aOpt){ class="type">int size=ArraySize(aOpt.Pass); for(class="type">int i=class="num">0;i<size;i++){ aOpt.Pass[i].SortFactor=class="num">0; for(class="type">int j=class="num">0;j<class="num">3;j++){ aOpt.Pass[i].SortFactor+=aOpt.Pass[i].ColorComponent[j]; } aOpt.Pass[i].SortFactor/=class="num">3; } } class="type">void SortFactorSort(SOptimization2 & aOpt){ class="type">int size=ArraySize(aOpt.Pass); for(class="type">int i=size-class="num">1;i>class="num">0;i--){ for(class="type">int j=class="num">0;j<i;j++){ if(aOpt.Pass[j].SortFactor<aOpt.Pass[j+class="num">1].SortFactor){ SPass2 tmp=aOpt.Pass[j]; aOpt.Pass[j]=aOpt.Pass[j+class="num">1]; aOpt.Pass[j+class="num">1]=tmp; } } } } SolveSortFactor(opt); if(Sort){ SortFactorSort(opt); }