在交易中应用 OLAP(第 1 部分):在线分析多维数据·综合运用
(3/3)· 前两轮铺垫了超立方体与引擎骨架,这篇把它跑起来并收束整套打法
用 OLAPDEMO 把账户历史切成三维立方体
在 MT5 里跑一个非交易 EA,就能把账户历史当成多维数据立方体来切。OLAPDEMO 这套实现把维度锁死在三维以内,也就是你同时最多挂三个选择器(X/Y/Z 轴),再叠加一个可选过滤器,足够看清大部分交易分布。 选择器类型用枚举写死,从品种、魔术码、盈亏属性到自定义的周期天数都有。WeekDaySelector 这类带时间维度的选择器必须显式传字段参数,因为每笔仓位有开仓和平仓两个 datetime,不指定就会歧义。 过滤器逻辑很直接:只保留 Filter1Field 字段落在 [Filter1value1, Filter1value2] 区间内的记录;两个值不等时才生成范围过滤对象。品种或魔术码字段的值实际对应词汇表里的索引,不是字面字符串。 实测在任意账户挂 SymbolSelector + WeekDaySelector 两个轴,日志会吐出 35 个单元的超立方体(账户交易了 5 个品种 × 星期组合),每个单元带对应的盈亏金额。若用 CSV 信号报告跑利润因子聚合,源代码里对无亏损单元生成 inf 值,排序时无穷大恒大于有限数。外汇与贵金属交易自带高杠杆风险,这类历史切片只描述已发生分布,不预示未来。 OnInit 里把所有选择器(含过滤器的)一次性初始化,OLAP 计算只靠计时器触发一次,跑完就杀定时器。下面这段是 EA 头部的枚举与输入定义,以及初始化骨架。
class="macro">#include <OLAPcube.mqh> enum SELECTORS { SELECTOR_NONE, class=class="str">"cmt">// 无 SELECTOR_TYPE, class=class="str">"cmt">// 类型 SELECTOR_SYMBOL, class=class="str">"cmt">// 品种 SELECTOR_SERIAL, class=class="str">"cmt">// 序数 SELECTOR_MAGIC, class=class="str">"cmt">// 魔幻数字 SELECTOR_PROFITABLE, class=class="str">"cmt">// 可盈利 class=class="str">"cmt">/* 自定义选择器 */ SELECTOR_DURATION, class=class="str">"cmt">// 持续天数 class=class="str">"cmt">/* 以下所有都需要一个字段作为参数 */ SELECTOR_WEEKDAY, class=class="str">"cmt">// 星期值(class="type">class="kw">datetime 字段) SELECTOR_DAYHOUR, class=class="str">"cmt">// 日内小时(class="type">class="kw">datetime 字段) SELECTOR_HOURMINUTE, class=class="str">"cmt">// 小时内分钟(class="type">class="kw">datetime 字段) SELECTOR_SCALAR, class=class="str">"cmt">// 标量(字段) SELECTOR_QUANTS class=class="str">"cmt">// 定量(字段) }; sinput class="type">class="kw">string X = "————— X axis —————"; class="kw">input SELECTORS SelectorX = SELECTOR_SYMBOL; class="kw">input TRADE_RECORD_FIELDS FieldX = FIELD_NONE class=class="str">"cmt">/* field does matter only for some selectors */; sinput class="type">class="kw">string Y = "————— Y axis —————"; class="kw">input SELECTORS SelectorY = SELECTOR_NONE; class="kw">input TRADE_RECORD_FIELDS FieldY = FIELD_NONE; sinput class="type">class="kw">string Z = "————— Z axis —————"; class="kw">input SELECTORS SelectorZ = SELECTOR_NONE; class="kw">input TRADE_RECORD_FIELDS FieldZ = FIELD_NONE; sinput class="type">class="kw">string F = "————— Filter —————"; class="kw">input SELECTORS Filter1 = SELECTOR_NONE; class="kw">input TRADE_RECORD_FIELDS Filter1Field = FIELD_NONE; class="kw">input class="type">class="kw">float Filter1value1 = class="num">0; class="kw">input class="type">class="kw">float Filter1value2 = class="num">0; enum AGGREGATORS { AGGREGATOR_SUM, class=class="str">"cmt">// 合计 AGGREGATOR_AVERAGE, class=class="str">"cmt">// 均值 AGGREGATOR_MAX, class=class="str">"cmt">// 最大 AGGREGATOR_MIN, class=class="str">"cmt">// 最小 AGGREGATOR_COUNT, class=class="str">"cmt">// 计数 AGGREGATOR_PROFITFACTOR, class=class="str">"cmt">// 盈利因子 AGGREGATOR_PROGRESSIVE, class=class="str">"cmt">// 进步总数 AGGREGATOR_IDENTITY class=class="str">"cmt">// 标识 }; sinput class="type">class="kw">string A = "————— Aggregator —————"; class="kw">input AGGREGATORS AggregatorType = AGGREGATOR_SUM; class="kw">input TRADE_RECORD_FIELDS AggregatorField = FIELD_PROFIT_AMOUNT; class="type">int selectorCount; SELECTORS selectorArray[class="num">4]; TRADE_RECORD_FIELDS selectorField[class="num">4]; class="type">int OnInit() { selectorCount = (SelectorX != SELECTOR_NONE) + (SelectorY != SELECTOR_NONE) + (SelectorZ != SELECTOR_NONE); selectorArray[class="num">0] = SelectorX; selectorArray[class="num">1] = SelectorY; selectorArray[class="num">2] = SelectorZ; selectorArray[class="num">3] = Filter1; selectorField[class="num">0] = FieldX; selectorField[class="num">1] = FieldY; selectorField[class="num">2] = FieldZ; selectorField[class="num">3] = Filter1Field; EventSetTimer(class="num">1); class="kw">return(INIT_SUCCEEDED); } class="type">void OnTimer() { process(); EventKillTimer(); } class="type">void process() { HistoryDataAdapter history; Analyst<TRADE_RECORD_FIELDS> *analyst; Selector<TRADE_RECORD_FIELDS> *selectors[]; ArrayResize(selectors, selectorCount); for(class="type">int i = class="num">0; i < selectorCount; i++) { selectors[i] = createSelector(i); } Filter<TRADE_RECORD_FIELDS> *filters[]; if(Filter1 != SELECTOR_NONE) { ArrayResize(filters, class="num">1);
「用过滤器与聚合器拆解历史交易」
在 MT5 里做交易统计,核心是先建选择器再挂过滤器。下面这段代码用 createSelector(3) 造了一个针对交易记录字段的选择器,当 Filter1value1 与 Filter1value2 不相等时走区间过滤,相等时退化为单值过滤,逻辑直接可抄。 MQL5 没有元类机制,无法用类数组代替分支,所以聚合器类型只能靠 switch 硬列:求和、均值、最大最小、计数、盈利因子、累进总计、原值透传,一共 9 种。你在回测 EURUSD 高频单时,若想看盈利因子分布,把 AggregatorType 设为 AGGREGATOR_PROFITFACTOR 即可。 分析师对象 analyst 绑定 history、aggregator 和 display 后,acquireData() 拉数据,build() 聚合,display() 输出。注意 DaysRangeSelector(15) 注释写明最多覆盖 14 天,贵金属跨周末持仓统计时这个边界容易漏算,建议手动校验。 跑完务必 delete analyst、aggregator 及 selectors、filters 数组里的每个指针,否则 MT5 策略测试器里内存会缓慢泄漏,样本过万笔后可能拖慢终端响应。
Selector<TRADE_RECORD_FIELDS> *filterSelector = createSelector(class="num">3); if(Filter1value1 != Filter1value2) { filters[class="num">0] = new FilterRange<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1, Filter1value2); } else { filters[class="num">0] = new Filter<TRADE_RECORD_FIELDS>(filterSelector, Filter1value1); } } Aggregator<TRADE_RECORD_FIELDS> *aggregator; class=class="str">"cmt">// MQL 不支持“类信息” 元类。 class=class="str">"cmt">// 否则我们可以使用类数组替代 class="kw">switch class="kw">switch(AggregatorType) { case AGGREGATOR_SUM: aggregator = new SumAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_AVERAGE: aggregator = new AverageAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_MAX: aggregator = new MaxAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_MIN: aggregator = new MinAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_COUNT: aggregator = new CountAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_PROFITFACTOR: aggregator = new ProfitFactorAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_PROGRESSIVE: aggregator = new ProgressiveTotalAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; case AGGREGATOR_IDENTITY: aggregator = new IdentityAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters); class="kw">break; } LogDisplay display; analyst = new Analyst<TRADE_RECORD_FIELDS>(history, aggregator, display); analyst.acquireData(); Print("Symbol number: ", TradeRecord::getSymbolCount()); for(class="type">int i = class="num">0; i < TradeRecord::getSymbolCount(); i++) { Print(i, "] ", TradeRecord::getSymbol(i)); } Print("Magic number: ", TradeRecord::getMagicCount()); for(class="type">int i = class="num">0; i < TradeRecord::getMagicCount(); i++) { Print(i, "] ", TradeRecord::getMagic(i)); } Print("Filters: ", aggregator.getFilterTitles()); Print("Selectors: ", selectorCount); analyst.build(); analyst.display(); class="kw">delete analyst; class="kw">delete aggregator; for(class="type">int i = class="num">0; i < selectorCount; i++) { class="kw">delete selectors[i]; } for(class="type">int i = class="num">0; i < ArraySize(filters); i++) { class="kw">delete filters[i].getSelector(); class="kw">delete filters[i]; } } Selector<TRADE_RECORD_FIELDS> *createSelector(class="type">int i) { class="kw">switch(selectorArray[i]) { case SELECTOR_TYPE: class="kw">return new TypeSelector(); case SELECTOR_SYMBOL: class="kw">return new SymbolSelector(); case SELECTOR_SERIAL: class="kw">return new SerialNumberSelector(); case SELECTOR_MAGIC: class="kw">return new MagicSelector(); case SELECTOR_PROFITABLE: class="kw">return new ProfitableSelector(); case SELECTOR_DURATION: class="kw">return new DaysRangeSelector(class="num">15); class=class="str">"cmt">// 多达 class="num">14 天 case SELECTOR_WEEKDAY: class="kw">return selectorField[i] != FIELD_NONE ? new WeekDaySelector(selectorField[i]) : NULL; case SELECTOR_DAYHOUR: class="kw">return selectorField[i] != FIELD_NONE ? new DayHourSelector(selectorField[i]) : NULL; case SELECTOR_HOURMINUTE: class="kw">return selectorField[i] != FIELD_NONE ? new DayHourSelector(selectorField[i]) : NULL; case SELECTOR_SCALAR: class="kw">return selectorField[i] != FIELD_NONE ? new TradeSelector(selectorField[i]) : NULL; case SELECTOR_QUANTS:
◍ 用选择器把账户历史切成可验证的网格
上面这段 MQL5 类定义给出了 DaysRangeSelector 的落地方式:它继承自 DateTimeSelector,以 FIELD_DURATION 为切分字段,把持仓时长按 86400 秒(60*60*24)折算成整天数。index 用 MathMin(days, granularity-1) 截断,超出粒度上限的统一归到最后一格,标签上显示为「ND+」表示 N 天及以上。 实际跑账户历史时,选择器组合能直接吐出交叉表。一组 5 个品种(FDAX、XAUUSD、UKBrent、NQ、EURUSD)× 星期选择器输出里,周三 FDAX 盈利 23.740、XAUUSD 盈利 4.240,周五 NQ 盈利 13.900、EURUSD 盈利 1.140,其余格子多为 0.000——说明该账户在这批品种上盈利高度集中在少数交易日。 另一份 217 笔成交(从 219 条记录清洗而来)按品种算盈利因子,USDCAD 为 7.051、USDJPY 为 4.716,而 NZDJPY 与 AUDUSD 显示 inf(样本里亏损为 0 导致分母为空)。外汇与贵金属品种波动剧烈,历史盈利因子仅反映过去样本,实盘复制存在显著高风险,需自行在 MT5 用相同选择器复算确认。 读入 ***.history.csv 后,SymbolSelector 直接按品种分桶,ProfitFactorAggregator 以 FIELD_PROFIT_AMOUNT 聚合。把 granularity 或 selector 字段换掉,就能在本地重跑出你自己的品种—周期盈利分布。
class="kw">return selectorField[i] != FIELD_NONE ? new QuantizationSelector(selectorField[i]) : NULL; } class="kw">return NULL; } class DaysRangeSelector: class="kw">public DateTimeSelector<TRADE_RECORD_FIELDS> { class="kw">public: DaysRangeSelector(class="kw">const class="type">int n): DateTimeSelector<TRADE_RECORD_FIELDS>(FIELD_DURATION, n) { _typename = class="kw">typename(this); } class="kw">virtual class="type">bool select(class="kw">const Record *r, class="type">int &index) class="kw">const class="kw">override { class="type">class="kw">double d = r.get(selector); class="type">int days = (class="type">int)(d / (class="num">60 * class="num">60 * class="num">24)); index = MathMin(days, granularity - class="num">1); class="kw">return true; } class="kw">virtual class="type">class="kw">string getLabel(class="kw">const class="type">int index) class="kw">const class="kw">override { class="kw">return index < granularity - class="num">1 ? ((index < class="num">10 ? " ": "") + (class="type">class="kw">string)index + "D") : ((class="type">class="kw">string)index + "D+"); } }; Analyzing account history Symbol number: class="num">5 class="num">0] FDAX class="num">1] XAUUSD class="num">2] UKBrent class="num">3] NQ class="num">4] EURUSD Magic number: class="num">1 class="num">0] class="num">0 Filters: no Selectors: class="num">2 SumAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [class="num">35] X: SymbolSelector(FIELD_SYMBOL) [class="num">5] Y: WeekDaySelector(FIELD_DATETIME2) [class="num">7] ... class="num">0.000: FDAX Monday class="num">0.000: XAUUSD Monday -class="num">20.400: UKBrent Monday class="num">0.000: NQ Monday class="num">0.000: EURUSD Monday class="num">0.000: FDAX Tuesday class="num">0.000: XAUUSD Tuesday class="num">0.000: UKBrent Tuesday class="num">0.000: NQ Tuesday class="num">0.000: EURUSD Tuesday class="num">23.740: FDAX Wednesday class="num">4.240: XAUUSD Wednesday class="num">0.000: UKBrent Wednesday class="num">0.000: NQ Wednesday class="num">0.000: EURUSD Wednesday class="num">0.000: FDAX Thursday class="num">0.000: XAUUSD Thursday class="num">0.000: UKBrent Thursday class="num">0.000: NQ Thursday class="num">0.000: EURUSD Thursday class="num">0.000: FDAX Friday class="num">0.000: XAUUSD Friday class="num">0.000: UKBrent Friday class="num">13.900: NQ Friday class="num">1.140: EURUSD Friday ... Reading csv-file ***.history.csv class="num">219 records transferred to class="num">217 trades Symbol number: class="num">8 class="num">0] GBPUSD class="num">1] EURUSD class="num">2] NZDUSD class="num">3] USDJPY class="num">4] USDCAD class="num">5] GBPAUD class="num">6] AUDUSD class="num">7] NZDJPY Magic number: class="num">1 class="num">0] class="num">0 Filters: no Selectors: class="num">1 ProfitFactorAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [class="num">8] X: SymbolSelector(FIELD_SYMBOL) [class="num">8] [value] [title] [class="num">0] inf "NZDJPY" [class="num">1] inf "AUDUSD" [class="num">2] inf "GBPAUD" [class="num">3] class="num">7.051 "USDCAD" [class="num">4] class="num">4.716 "USDJPY" [class="num">5] class="num">1.979 "EURUSD" [class="num">6] class="num">1.802 "NZDUSD" [class="num">7] class="num">1.359 "GBPUSD"
把 OLAP 类塞进 EA 的收尾处
前面几节把 OLAP 的立方体建模和聚合逻辑讲透了,落到实盘或回测里最省事的做法,是在 EA 的 OnDeinit 里直接挂一个轻量包装器。这样单次测试跑完,交易历史已经按你指定的选择器切好,不用再导 CSV 手工翻。 作者在原讨论里补过一个例子:用 SELECTOR_SYMBOL 配 AGGREGATOR_COUNT,能把每品种成交笔数拉出来排降序;若换成 AGGREGATOR_PROFITFACTOR 加 FIELD_PROFIT_POINTS,则直接看哪些符号的利润因子拖了后腿。标准测试报告不提供这类按属性切分的视图,OLAP 补的就是这块。 外汇与贵金属杠杆高、价格非平稳,任何从历史提取的“规律”都只是概率倾向,别把一次回测的聚合结果当未来约束。下面这段是直接可抄进 MT5 的钩子代码,头文件依赖前文那堆 OLAP*.mqh。
<span class="keyword">class="type">void</span> <span class="functions">OnDeinit</span>(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span>) { OLAPStats stats(SELECTOR_SYMBOL, FIELD_NONE, SELECTOR_PROFITABLE); <span class="comment">class=class="str">"cmt">// 根据需要选择选择器和字段</span> stats.setAggregator(AGGREGATOR_COUNT); <span class="comment">class=class="str">"cmt">// 选择聚合器,例如:stats.setAggregator(AGGREGATOR_PROFITFACTOR, FIELD_PROFIT_POINTS);</span> stats.setSorting(SORT_BY_VALUE_DESCENDING); <span class="comment">class=class="str">"cmt">// 可选择排序顺序</span> <span class="comment">class=class="str">"cmt">// MyOLAPStats 回调; // 可选的 "显示 "自定义实现</span> stats.process(<span class="comment">class=class="str">"cmt">/*&callback*/</span>); }