在交易中应用 OLAP(第 2 部分):可视化交互式多维数据分析的结果·综合运用
(3/3)· 当 OLAP 超立方体遇上可拉伸窗口,硬编码坐标的静态弹窗终于可以退休了
「用下拉控件搭一个可交互的 OLAP 分析面板」
新建一个 EA(OLAPGUI),把窗口、控件响应和 OLAP 调用全部收进 OLAPGUI.mqh 头文件,EA 自身只留与 HTML/CSV 导入有关的输入:ReportFile、Prefix、Suffix。若 ReportFile 留空,EA 直接分析当前账户的历史成交,这一点在 MT5 里打开任意实盘或模拟账户都能验证。 面板第一排放 3 个下拉列表对应 X/Y/Z 三个条件轴的选择器,右边缘放“处理”按钮触发分析;第二排两个下拉分别选聚合函数和聚合字段,再加排序与图表样式下拉,筛选器刻意拿掉以降低 UI 复杂度。下拉选项把选择器类型和输出字段合并显示,例如 symbol 对应 FIELD_SYMBOL,lot 对应 FIELD_LOT,带 * 的 TradeSelector/QuantizationSelector 由聚合器决定——IdentityAggregator 用前者,其余用后者。 支持的聚合函数共 9 种:sum、average、max、min、count、profit factor、progressive total、identity,除 identity 外都需在右侧下拉指定聚合字段。progressive total 必须配 X 轴选 ordinal 选择器,表示按记录顺序累加。若只选了 X 选择器,排序下拉才可用的逻辑能在代码里直接核对。 Z 轴不用三维投影,而是用“处理”按钮在多个 Z 平面间翻页:存在 z 坐标时按钮标题变成“i/n title”,i 是当前层编号,n 是样本总数;改了构建立即复位成“处理”。identity 聚合器例外,永远两维且 X/Y/Z 三条曲线同图不滚动。 除图形外每个超立方体都会写日志。用 lot 作 X、profit amount 作 sum 时,图表 X 轴只显示整数单元格索引(0、1、2…),真实手数在日志里以 title 给出。下面这段日志样本来自 0.01~1.0 手区间的成交:0 号格利润 365.96 对应手数 0.01,2 号格 4.65 对应 0.03,3 号格 15.98 对应 0.06——说明小仓位反而出了最大单笔利润,外汇贵金属高杠杆下这种分布很常见,复盘时别只看聚合总额。 GUI 与 OLAP 核心通过 OLAPWrapper 层衔接,原 OLAPDEMO 的 process 函数变成类方法,设置改由控件状态填参传入。因 MQL5 不支持多重继承,OLAPDialog 继承自 MaximableAppDialog 后,由辅助类 OLAPDisplay 实现 display 接口,核心创建立方体后调用它把维度、标题和向量推给窗口。标准柱状图样式无法偏移同索引多数组,可能重叠,自定义绘制可借 CGraphic 解决但不在本节流中。
Selectors: class="num">1 SumAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [class="num">6] X: QuantizationSelector(FIELD_LOT) [class="num">6] ===== QuantizationSelector(FIELD_LOT) ===== [value] [title] [class="num">0] class="num">365.96000 "class="num">0.01" [class="num">1] class="num">0.00000 "class="num">0.0" [class="num">2] class="num">4.65000 "class="num">0.03" [class="num">3] class="num">15.98000 "class="num">0.06" [class="num">4] class="num">34.23000 "class="num">0.02" [class="num">5] class="num">0.00000 "class="num">1.0" class OLAPWrapper { class="kw">protected: Selector<TRADE_RECORD_FIELDS> *createSelector(class="kw">const SELECTORS selector, class="kw">const TRADE_RECORD_FIELDS field); class="kw">public: class="type">void process( class="kw">const SELECTORS &selectorArray[], class="kw">const TRADE_RECORD_FIELDS &selectorField[], class="kw">const AGGREGATORS AggregatorType, class="kw">const TRADE_RECORD_FIELDS AggregatorField, Display &display, class="kw">const SORT_BY SortBy = SORT_BY_NONE, class="kw">const class="type">class="kw">double Filter1value1 = class="num">0, class="kw">const class="type">class="kw">double Filter1value2 = class="num">0) { class="type">int selectorCount = class="num">0; for(class="type">int i = class="num">0; i < MathMin(ArraySize(selectorArray), class="num">3); i++) { selectorCount += selectorArray[i] != SELECTOR_NONE; } ... HistoryDataAdapter<CustomTradeRecord> history; HTMLReportAdapter<CustomTradeRecord> report; CSVReportAdapter<CustomTradeRecord> external; DataAdapter *adapter = &history; if(ReportFile != "") { if(StringFind(ReportFile, ".htm") > class="num">0 && report.load(ReportFile)) { adapter = &report; } else if(StringFind(ReportFile, ".csv") > class="num">0 && external.load(ReportFile)) { adapter = &external; } else { Alert("未知的文件格式: ", ReportFile); class="kw">return; } } else {
把成交记录塞进 OLAP 面板跑多维聚合
MQL5 不支持多重继承,所以显示层得用委托对象 OLAPDisplay 包一层,把 Dialog 指针传进去,才能在不破坏类结构的前提下接管立方体数据的渲染。 核心触发在 OnClickButton:从下拉框读出聚合算法 at、字段 af、排序 sb,再把四个轴的 Selectors/Fields 初始化成 NONE,交给 olapcore.process 跑实际计算。 [CODE] void OLAPDialog::OnClickButton(void) { SELECTORS Selectors[4]; TRADE_RECORD_FIELDS Fields[4]; AGGREGATORS at = (AGGREGATORS)m_algo[0].Value(); TRADE_RECORD_FIELDS af = (TRADE_RECORD_FIELDS)(AGGREGATORS)m_algo[1].Value(); SORT_BY sb = (SORT_BY)m_algo[2].Value(); ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); ... olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); } [/CODE] 上面这段里,m_algo[0] 取聚合器类型,m_algo[1] 取目标字段,m_algo[2] 取排序方式;ArrayInitialize 把轴选择清空,避免上一次筛选污染本次结果。 当维度只有 1 个时,display 会走 getVector 拿到 PairArray,先 Print 出标题与维度范围,再调 parent.accept1D 把曲线塞给 CPlot,最后 finalize 刷新图表、按钮文字复原为 Process。 [CODE] if(selectorCount == 1) { PairArray *result; if(metaData.getVector(0, consts, result, sortby)) { Print("===== " + metaData.getDimensionTitle(0) + " ====="); ArrayPrint(result.array); parent.accept1D(result, metaData.getDimensionTitle(0)); } parent.finalize(); return; } [/CODE] 你在 MT5 里加载后,点一次 Process 按钮,日志会打出类似「EURUSD [120]」这样的维度规模——表示当前轴下有 120 个离散值,据此可判断历史样本是否够薄。外汇与贵金属杠杆高,历史聚合仅反映过去分布,对后续走势只具概率参考。
class="type">void OLAPDialog::OnClickButton(class="type">void) { SELECTORS Selectors[class="num">4]; TRADE_RECORD_FIELDS Fields[class="num">4]; AGGREGATORS at = (AGGREGATORS)m_algo[class="num">0].Value(); TRADE_RECORD_FIELDS af = (TRADE_RECORD_FIELDS)(AGGREGATORS)m_algo[class="num">1].Value(); SORT_BY sb = (SORT_BY)m_algo[class="num">2].Value(); ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); ... olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); } if(selectorCount == class="num">1) { PairArray *result; if(metaData.getVector(class="num">0, consts, result, sortby)) { Print("===== " + metaData.getDimensionTitle(class="num">0) + " ====="); ArrayPrint(result.array); parent.accept1D(result, metaData.getDimensionTitle(class="num">0)); } parent.finalize(); class="kw">return; }
◍ 文件清单与落地路径
这套可缩放 GUI 方案最终落在四个 MT5 项目里:OLAPGUI 负责把交易报告做成可拖拽的 OLAP 面板,SlidingPuzzle3、Controls3、PlotDemo 则分别演示网格布局、可改尺寸控件和图表控件的复用方式。想直接看效果,把 MQLOLAP2.zip(73.86 KB)解进 MT5 的 MQL5 目录,编译 Experts/OLAP/OLAPGUI.mq5 即可。 头文件层级值得记一下:Include/Layouts 下的 Box.mqh、MaximizableAppDialog.mqh 是尺寸自适应的底层,Include/OLAP 里的 OLAPcube.mqh 与 PairArray.mqh 承接了第一篇的立方体逻辑。若你用较新的 MQL5 编译器,fxsaber 在 2022 年提到过语言变动导致需小幅重构,作者也确认会按反馈修兼容性。 外汇与贵金属复盘本身高风险,这类多维交叉分析只帮你识别历史样本里的隐藏偏好,不预示未来盈亏;拿去优化系统前,先在策略测试器跑几组不同品种验证稳定性。