在交易中应用 OLAP(第四部分):定量和可视化分析测试器报告·综合运用
(3/3)·优化结果堆成数字山却看不出规律?把测试器报告丢进 OLAP 多维切片里
◍ 复用交易报告界面做优化结果可视化
优化结果的分析界面可以直接复用交易报告的 OLAP 图形框架。实操上把 OLAPGUI_Trade.mqh 复制成 OLAPGUI_Opts.mqh,再微调虚拟方法 setup 与 process 即可,字段和选择器差异极小——因为任一字段本质都是针对自身的量化选择器,量化选择器包揽了全部逻辑。 早期交易/报价项目里,我们给盈利、星期、烛条类型等字段分别写了独立选择器;而优化分析里下拉列表的元素名由 OPT_CACHE_RECORD_FIELDS 枚举名加上 EA 参数的 customFields 数组构成,标准字段按枚举顺序排,其后接 opt 文件参数顺序对应的自定义字段。 控制状态读取与分析启动都落在 process 方法。下面这段模板方法展示了 setup 如何装配聚合器、字段、排序与曲线类型,以及 process 如何把轴映射成选择器并调核心处理。
class="kw">template<class="kw">typename S, class="kw">typename F> class="type">void OLAPDialog::setup() class="kw">override { class="kw">static class="kw">const class="type">class="kw">string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] = { class=class="str">"cmt">// enum AGGREGATORS class="num">1:class="num">1, class="kw">default - sum {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"}, class=class="str">"cmt">// enum RECORD_FIELDS class="num">1:class="num">1, class="kw">default - profit amount {""}, class=class="str">"cmt">// enum SORT_BY, class="kw">default - none {"none", "value ascending", "value descending", "label ascending", "label descending"}, class=class="str">"cmt">// enum ENUM_CURVE_TYPE partially, class="kw">default - points {"points", "lines", "points/lines", "steps", "histogram"} }; class="kw">static class="kw">const class="type">int _defaults[ALGO_NUMBER] = {class="num">0, FIELD_PROFIT, class="num">0, class="num">0}; class="kw">const class="type">int std = EnumSize<F,PackedEnum>(class="num">0); class="kw">const class="type">int fields = std + customFieldCount; ArrayResize(settings, fields); ArrayResize(selectors, fields); selectors[class="num">0] = "(<selector>/field)"; class=class="str">"cmt">// none selectors[class="num">1] = "<serial number>"; class=class="str">"cmt">// the only selector, which can be chosen explicitly, it corresponds to the &class="macro">#x27;index&class="macro">#x27; field for(class="type">int i = class="num">0; i < ALGO_NUMBER; i++) { if(i == class="num">1) class=class="str">"cmt">// pure fields { for(class="type">int j = class="num">0; j < fields; j++) { settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } } else { for(class="type">int j = class="num">0; j < MAX_ALGO_CHOICES; j++) { settings[j][i] = _settings[i][j]; } } } for(class="type">int j = class="num">2; j < fields; j++) class=class="str">"cmt">// class="num">0-th is none { selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } ArrayCopy(defaults, _defaults); } class="kw">template<class="kw">typename S, class="kw">typename F> class="type">int OLAPDialog::process() class="kw">override { SELECTORS Selectors[class="num">4]; ENUM_FIELDS Fields[class="num">4]; AGGREGATORS at = (AGGREGATORS)m_algo[class="num">0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[class="num">1].Value(); SORT_BY sb = (SORT_BY)m_algo[class="num">2].Value(); if(at == AGGREGATOR_IDENTITY) { Print("Sorting is disabled for Identity"); sb = SORT_BY_NONE; } ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); class="type">int matches[class="num">2] = { SELECTOR_NONE, SELECTOR_INDEX }; for(class="type">int i = class="num">0; i < AXES_NUMBER; i++) { if(!m_axis[i].IsVisible()) class="kw">continue; class="type">int v = (class="type">int)m_axis[i].Value(); if(v < class="num">2) class=class="str">"cmt">// selectors(which is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; } else class=class="str">"cmt">// pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (ENUM_FIELDS)(v); } } m_plot.CurvesRemoveAll(); if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE; m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), class="kw">false) : NULL); m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL); m_button_ok.Text("Processing..."); class="kw">return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); }
「用 OLAP 把多参数优化报告拆开看」
MT5 测试器自带的 2D 可视化只把两个组合参数映射到最大利润轴,优化若跑了 PricePeriod、Momentum、Sigma 三个参数,图上每个点其实混着其它参数的不同组合结果,直接看容易对盈利过于乐观。更稳的做法是按平均利润加值域跨度来考核,这套逻辑能用 OLAP 引擎在优化报告上直接跑。
非交易 EA OLAPGUI_Opts.mq5 专门干这事:结构和 OLAPGUI.mq5 一致,但不用按文件类型挂适配器,永远只读 opt 文件。输入里填文件名、给量化步长,注意目前每个字段共用同一个 QuantGranularity,一个步长可能对某字段太大、对另一个太小,切下拉字段前必要时回 EA 属性改因子。
以 Integrity.opt、步长 100 试跑,优化时选了 PricePeriod、Momentum、Sigma 三个参数。按 PricePeriod 拆盈利,直方图显示平均利润分散极小;切到利润分散图对比,能估出哪些参数值离散没超过均值、接近盈亏平衡。同图自动比对更直观,但本文没做。
换成利润因子(所有通关盈亏比)看参数盈利能力,或用 identity 聚合器按 100 增量看周期利润分布、按 Momentum 和 Sigma 细分平均利润,都能在 GUI 下拉里换字段和聚合器实现。X 轴选 profit 字段、count 聚合器,就能看每 100 单位利润区间的频次;identity 聚合器还能直观评估交易数对利润的影响。外汇与贵金属优化结果受样本过拟合影响大,这类分析仅降低误判概率,不保证实盘表现。
class="kw">input class="type">class="kw">string OptFileName = "Integrity.opt"; class="kw">input class="type">uint QuantGranularity = class="num">0; class="macro">#include <OLAP/OLAPOpts.mqh> class="macro">#include <OLAP/GUI/OLAPGUI_Opts.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine); class="type">int OnInit() { _defaultEngine.setAdapter(&_defaultOptCacheAdapter); _defaultEngine.setShortTitles(true); _defaultEngine.setQuant(QuantGranularity); _defaultOptCacheAdapter.load(OptFileName); dialog.setCustomFields(_defaultOptCacheAdapter); if(!dialog.Create(class="num">0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), class="num">0, class="num">0, class="num">0, class="num">750, class="num">560)) class="kw">return INIT_FAILED; if(!dialog.Run()) class="kw">return INIT_FAILED; class="kw">return INIT_SUCCEEDED; }
收束
这一节把整套 OLAP 分析框架的文件清单摊开了:从 OLAPCommon.mqh 主类到 SingleTesterCache、TesterCache 两组 fxsaber 缓存桥接头文件,覆盖了账户历史、报价、优化结果的立方分析。实际落地时,直接把 MQLOLAP4.zip(365.7 KB)解进 MQL5/Include 与 Experts 目录,编译 OLAPGUI_Opts.mq5 就能读 OPT 测试器文件做交互过滤。 当前实现只是最小起点,3D 可视化和坐标轴量化过滤仍空白;MT5 自 2020 年内置的 SQLite 引擎也能做同类聚合,但要求你会写 SQL。外汇与贵金属回测数据噪声大,OLAP 得出的关联倾向仅作决策参考,实盘仍属高风险。 代码层留了一行类型自省的底子,便于后续扩展自定义立方体: _typename_ = TYPENAME(this); 把它接进 ReportCubeBase 派生类,就能在运行时打印 cube 实例名,排查多维切片错位比硬调试快得多。
_typename_ = TYPENAME(this);