在交易中应用 OLAP(第四部分):定量和可视化分析测试器报告·综合运用
📊

在交易中应用 OLAP(第四部分):定量和可视化分析测试器报告·综合运用

(3/3)·优化结果堆成数字山却看不出规律?把测试器报告丢进 OLAP 多维切片里

含代码示例偏理论 第 3/3 篇
不少人跑完 MT5 策略优化,只盯着利润和回撤两个数就定稿参数,把几百组测试记录里的时段、品种、持仓分布差异全忽略了。报表自带表格翻不动,关联维度靠肉眼比对,漏掉的稳定区间往往正是实盘容易踩坑的地方。

◍ 复用交易报告界面做优化结果可视化

优化结果的分析界面可以直接复用交易报告的 OLAP 图形框架。实操上把 OLAPGUI_Trade.mqh 复制成 OLAPGUI_Opts.mqh,再微调虚拟方法 setup 与 process 即可,字段和选择器差异极小——因为任一字段本质都是针对自身的量化选择器,量化选择器包揽了全部逻辑。 早期交易/报价项目里,我们给盈利、星期、烛条类型等字段分别写了独立选择器;而优化分析里下拉列表的元素名由 OPT_CACHE_RECORD_FIELDS 枚举名加上 EA 参数的 customFields 数组构成,标准字段按枚举顺序排,其后接 opt 文件参数顺序对应的自定义字段。 控制状态读取与分析启动都落在 process 方法。下面这段模板方法展示了 setup 如何装配聚合器、字段、排序与曲线类型,以及 process 如何把轴映射成选择器并调核心处理。

MQL5 / C++
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 聚合器还能直观评估交易数对利润的影响。外汇与贵金属优化结果受样本过拟合影响大,这类分析仅降低误判概率,不保证实盘表现。

MQL5 / C++
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 实例名,排查多维切片错位比硬调试快得多。

MQL5 / C++
    _typename_ = TYPENAME(this);
把优化报表的切片交给小布
这些多维透视和轴标签优化在小布盯盘的 AIGC 诊断里已内置,打开对应品种页就能直接看分维度稳定性,不用自己搭 OLAP GUI 跑代码。

常见问题

Y 轴默认直接输出汇总秒值,未换算成时间帧柱线数,数值过大且不直观;改进后除以当前周期柱线时长即可得到柱线数量,更易理解持仓跨度。
早期实现水平 X 轴仅特定选择器显示名称,其余按原样显数字;Y 轴误把汇总值当选择器值四舍五入,导致如平均持仓秒数难以解读,需传标志给 CAxis 改渲染逻辑。
可以,小布内置的 AIGC 看板已覆盖多维度切片与稳定性诊断,省去自行重构 GUI 与适配器的开发量,交易者专注参数决策即可。
文中将 OLAPGUI 抽象为通用可视层,优化结果分析器复用同一套坐标与绘图逻辑,后续接入其他数据域只需写新选择器与聚合器。