在交易中应用 OLAP(第 1 部分):在线分析多维数据·综合运用
🧊

在交易中应用 OLAP(第 1 部分):在线分析多维数据·综合运用

(3/3)· 前两轮铺垫了超立方体与引擎骨架,这篇把它跑起来并收束整套打法

新手友好 第 3/3 篇
很多人导完交易报告只看总盈亏和胜率,品种、星期、买卖方向混在一起,根本分不清钱从哪天哪个品种来的。用二维表格硬筛,维度一多就卡死,还容易漏掉交叉效应。OLAP 的思路是先算好超立方体,再随手切横截面,这才是看清结构的方式。

用 OLAPDEMO 把账户历史切成三维立方体

在 MT5 里跑一个非交易 EA,就能把账户历史当成多维数据立方体来切。OLAPDEMO 这套实现把维度锁死在三维以内,也就是你同时最多挂三个选择器(X/Y/Z 轴),再叠加一个可选过滤器,足够看清大部分交易分布。 选择器类型用枚举写死,从品种、魔术码、盈亏属性到自定义的周期天数都有。WeekDaySelector 这类带时间维度的选择器必须显式传字段参数,因为每笔仓位有开仓和平仓两个 datetime,不指定就会歧义。 过滤器逻辑很直接:只保留 Filter1Field 字段落在 [Filter1value1, Filter1value2] 区间内的记录;两个值不等时才生成范围过滤对象。品种或魔术码字段的值实际对应词汇表里的索引,不是字面字符串。 实测在任意账户挂 SymbolSelector + WeekDaySelector 两个轴,日志会吐出 35 个单元的超立方体(账户交易了 5 个品种 × 星期组合),每个单元带对应的盈亏金额。若用 CSV 信号报告跑利润因子聚合,源代码里对无亏损单元生成 inf 值,排序时无穷大恒大于有限数。外汇与贵金属交易自带高杠杆风险,这类历史切片只描述已发生分布,不预示未来。 OnInit 里把所有选择器(含过滤器的)一次性初始化,OLAP 计算只靠计时器触发一次,跑完就杀定时器。下面这段是 EA 头部的枚举与输入定义,以及初始化骨架。

MQL5 / C++
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 策略测试器里内存会缓慢泄漏,样本过万笔后可能拖慢终端响应。

MQL5 / C++
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 字段换掉,就能在本地重跑出你自己的品种—周期盈利分布。

MQL5 / C++
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。

MQL5 / C++
<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>)
{
&nbsp;&nbsp;OLAPStats stats(SELECTOR_SYMBOL, FIELD_NONE, SELECTOR_PROFITABLE); <span class="comment">class=class="str">"cmt">// 根据需要选择选择器和字段</span>
&nbsp;&nbsp;stats.setAggregator(AGGREGATOR_COUNT); <span class="comment">class=class="str">"cmt">// 选择聚合器,例如:stats.setAggregator(AGGREGATOR_PROFITFACTOR, FIELD_PROFIT_POINTS);</span>
&nbsp;&nbsp;stats.setSorting(SORT_BY_VALUE_DESCENDING); <span class="comment">class=class="str">"cmt">// 可选择排序顺序</span>
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// MyOLAPStats 回调; // 可选的 "显示 "自定义实现</span>
&nbsp;&nbsp;stats.process(<span class="comment">class=class="str">"cmt">/*&amp;callback*/</span>);
}
把切片诊断交给小布盯盘
这些多维切分小布盯盘的 AIGC 已内置,打开对应品种页就能直接看按星期和方向的盈亏分布,你只管判断要不要调策略。

常见问题

示例通过 DataAdapter 逐条读取账户历史,把每笔交易的品种、星期、买卖标记映射成 Record 的字段向量,具体映射在派生类里定义,与抽象基类解耦。
可以,只要两者字段语义对齐,比如都用品种、魔法数字、星期做维度,引擎不关心数据来源,只处理 Record 向量,天然支持混合切片。
优先固定高频维度如品种和星期,把机器人或方向作为可切换层,避免一次展开全部组合,MetaTrader 5 的优化切换逻辑也是这个思路。
能,小布盯盘内置了按多维条件拆交易报告的视图,不用自己写 MQL 引擎也能看交叉分布,适合快速排查某品种周几容易亏。
可以,通用引擎面向任意 Record 来源,指标值只要封装成适配器输出的向量,就能复用全部切片逻辑,完整讨论见《在交易中应用 OLAP:在线分析多维数据·基础篇》。