在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价·进阶篇
🧊

在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价·进阶篇

(2/3)· 还在用肉眼翻 K 线找规律?多维聚合能把报价压成可筛选的立方体

含代码示例实战向 第 2/3 篇
把报价当扁平序列看,很容易漏掉时段和品种交叉里的隐性结构。OLAP 的思路是把行情铺成多维立方体,按你关心的轴随意切片和汇总,策略假设才有据可依。

把 MqlRates 塞进可分析的记录结构

做价格行为统计的第一步,是把 MT5 每根 K 线的原始结构体 MqlRates 转成带字段标签的记录对象。下面这段构造逻辑直接把 high/low/close 落库,同时算了两根区间:OC 用收盘价减开盘价除以 _Point,HL 则乘上收盘相对开盘的方向符号,正数代表阳线区间、负数代表阴线区间,单位都是点。 [CODE] set(FIELD_PRICE_HIGH, rate.high); set(FIELD_PRICE_LOW, rate.low); set(FIELD_PRICE_CLOSE, rate.close); set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point); set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point); set(FIELD_SPREAD, (double)rate.spread); set(FIELD_TICK_VOLUME, (double)rate.tick_volume); set(FIELD_REAL_VOLUME, (double)rate.real_volume); [/CODE] 逐行拆解:前 3 行存高低收价格;第 4 行算实体点数(close-open 转点);第 5 行算影线加实体的带符号区间,阴线时整体取负;后 3 行把 spread 与两类成交量从整型强转 double,方便后续统一聚合。 数据类型表用 13+4 个字段描述一条记录:i 表示整型(序号、方向、点差、tick、real),d 表示双精度(价格与各类区间、4 个自定义字段)。你在 MT5 里改自定义字段数量时,要同步动 QUOTE_RECORD_FIELDS_LAST 和 datatypes 数组,否则 legend() 会返回 unknown。 适配器 QuotesDataAdapter 在 reset() 里用 MathMin(Bars(_Symbol,_Period), TerminalInfoInteger(TERMINAL_MAXBARS)) 限制读取上限,避免历史太多拖死回测。getNext() 从最新一根倒序取 CopyRates,每调一次 cursor 减 1,直到 0 返回 NULL,这套机制让你可以直接拿它喂给 OLAP 引擎做形状分组。外汇与贵金属波动受杠杆与事件影响,倒序回测结果仅反映历史概率,实盘仍属高风险。

MQL5 / C++
set(FIELD_PRICE_HIGH, rate.high);
set(FIELD_PRICE_LOW, rate.low);
set(FIELD_PRICE_CLOSE, rate.close);
set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point);
set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point);
set(FIELD_SPREAD, (class="type">class="kw">double)rate.spread);
set(FIELD_TICK_VOLUME, (class="type">class="kw">double)rate.tick_volume);
set(FIELD_REAL_VOLUME, (class="type">class="kw">double)rate.real_volume);

◍ 选择器分支与引擎初始化的落地写法

上面这段是 OLAP 行情引擎里按枚举派发选择器实例的核心分支。SELECTOR_WEEKDAY、SELECTOR_DAYHOUR、SELECTOR_HOURMINUTE 都直接挂到 FIELD_DATETIME 上,说明时间维度的切片在框架里被当成一类统一处理;而 SCALAR、QUANTS、FILTER 则要求 field 不等于 FIELD_NONE 才 new 对象,否则返回 NULL,避免了对空字段建选择器导致的运行期异常。 注意 SELECTOR_DAYHOUR 和 SELECTOR_HOURMINUTE 在复用时都走了 DayHourSelector 模板,只是语义层分开,如果你要在 MT5 里做分钟级聚合,得自己补一个独立的 MinuteSelector,否则会沿用小时切片逻辑。外汇与贵金属行情具有高杠杆高风险,这类聚合偏差可能放大止损误差,先在策略测试器里跑小周期验证。 initialize() 里只做了一件事:Print 出 QuotesRecord::getRecordCount() 的读数。这个数字就是实际载入的 K 线条数,开 MT5 跑完 EA 初始化后去专家日志看,若和你指定的历史范围不符,八成是 DataAdapter 没正确绑定 symbol 或周期。 _defaultQuotesAdapter 和 _defaultEngine 是栈上全局实例,构造时走默认或带 DataAdapter* 的重载。想换数据源,直接传自定义 adapter 指针给 OLAPEngineQuotes(ptr) 即可,不用改选择器逻辑。

MQL5 / C++
case SELECTOR_WEEKDAY:
class="kw">return new WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
case SELECTOR_DAYHOUR:
class="kw">return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
case SELECTOR_HOURMINUTE:
class="kw">return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
case SELECTOR_SCALAR:
class="kw">return field != FIELD_NONE ? new BaseSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
case SELECTOR_QUANTS:
class="kw">return field != FIELD_NONE ? new QuantizationSelector<QUOTE_RECORD_FIELDS>(field, QuantGranularity) : NULL;
case SELECTOR_FILTER:
class="kw">return field != FIELD_NONE ? new FilterSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
}
class="kw">return NULL;
}
class="kw">virtual class="type">void initialize() class="kw">override
{
Print("Bars read: ", QuotesRecord::getRecordCount());
}
class="kw">public:
OLAPEngineQuotes(): OLAPEngine() {}
OLAPEngineQuotes(DataAdapter *ptr): OLAPEngine(ptr) {}
};
QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
OLAPEngineQuotes _defaultEngine;

「用 OLAP 思路把报价切成可分析的维度」

非交易型 EA OLAPQTS.mq5 的结构和分析交易报告的脚本类似,核心是把每根报价封装成带自定义字段的记录类。CustomQuotesRecord 继承自 QuotesRecord,在 fillCustomFields 里算形态字段,这些字段后续能当策略地基用。 要让默认适配器认得自己的记录类,必须在 include OLAPQuotes.mqh 之前定义宏 RECORD_CLASS。代码里这行 #define RECORD_CLASS CustomQuotesRecord 就是插进默认适配器的接口,漏掉会导致引擎只建基类实例、自定义字段全空。 EA 用 X/Y/Z 三个轴选维度,再叠加一个过滤器(支持单值或范围)和聚合器。所有选择器与字段都塞进 _selectorArray[4] 和 _selectorField[4] 两个数组,直接丢给引擎处理,不用在 UI 之外写死逻辑。 过滤器值以字符串输入,按字段类型自动分流:若 datatype 返回 't'(时间型),用 StringToTime 转成 double;否则走 StringToDouble。这比第一篇文章里强制手敲数字友好,用户填 YYYY.MM.DD 就能按日期切报价。 OnInit 里 EventSetTimer(1) 拉起 1 秒计时器,OnTimer 首行先 EventKillTimer 自杀,保证只跑一次数据处理——周末没即时报价也能回看历史。处理完经 LogDisplay 把结果打印到 EA 日志,外汇与贵金属报价波动剧烈、杠杆风险高,这类离线切片仅作概率倾向参考,别直接当入场信号。

MQL5 / C++
class CustomQuotesRecord: class="kw">public QuotesRecord
{
class="kw">public:
CustomQuotesRecord(): QuotesRecord() {}
CustomQuotesRecord(class="kw">const class="type">MqlRates &rate): QuotesRecord(rate)
{
}
class="kw">virtual class="type">void fillCustomFields() class="kw">override
{
class=class="str">"cmt">// ...
}
class="kw">virtual class="type">class="kw">string legend(class="kw">const class="type">int index) class="kw">const class="kw">override
{
class=class="str">"cmt">// ...
class="kw">return QuotesRecord::legend(index);
}
};
class=class="str">"cmt">// this line plugs our class into class="kw">default adapter in OLAPQuotes.mqh
class="macro">#define RECORD_CLASS CustomQuotesRecord
class="macro">#include <OLAP/OLAPQuotes.mqh>
sinput class="type">class="kw">string X = "————— X axis —————"; class=class="str">"cmt">// · X ·
class="kw">input SELECTORS SelectorX = DEFAULT_SELECTOR_TYPE; class=class="str">"cmt">// · SelectorX
class="kw">input ENUM_FIELDS FieldX = DEFAULT_SELECTOR_FIELD class=class="str">"cmt">/* field does matter only for some selectors */; class=class="str">"cmt">// · FieldX
sinput class="type">class="kw">string Y = "————— Y axis —————"; class=class="str">"cmt">// · Y ·
class="kw">input SELECTORS SelectorY = SELECTOR_NONE; class=class="str">"cmt">// · SelectorY
class="kw">input ENUM_FIELDS FieldY = FIELD_NONE; class=class="str">"cmt">// · FieldY
sinput class="type">class="kw">string Z = "————— Z axis —————"; class=class="str">"cmt">// · Z ·
class="kw">input SELECTORS SelectorZ = SELECTOR_NONE; class=class="str">"cmt">// · SelectorZ
class="kw">input ENUM_FIELDS FieldZ = FIELD_NONE; class=class="str">"cmt">// · FieldZ
sinput class="type">class="kw">string F = "————— Filter —————"; class=class="str">"cmt">// · F ·
class="kw">input SELECTORS _Filter1 = SELECTOR_NONE; class=class="str">"cmt">// · Filter1
class="kw">input ENUM_FIELDS _Filter1Field = FIELD_NONE; class=class="str">"cmt">// · Filter1Field
class="kw">input class="type">class="kw">string _Filter1value1 = ""; class=class="str">"cmt">// · Filter1value1
class="kw">input class="type">class="kw">string _Filter1value2 = ""; class=class="str">"cmt">// · Filter1value2
sinput class="type">class="kw">string A = "————— Aggregator —————"; class=class="str">"cmt">// · A ·
class="kw">input AGGREGATORS _AggregatorType = DEFAULT_AGGREGATOR_TYPE; class=class="str">"cmt">// · AggregatorType
class="kw">input ENUM_FIELDS _AggregatorField = DEFAULT_AGGREGATOR_FIELD; class=class="str">"cmt">// · AggregatorField
class="kw">input SORT_BY _SortBy = SORT_BY_NONE; class=class="str">"cmt">// · SortBy
SELECTORS _selectorArray[class="num">4];
ENUM_FIELDS _selectorField[class="num">4];
class="type">int OnInit()
{
_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;
_defaultEngine.setAdapter(&_defaultQuotesAdapter);
EventSetTimer(class="num">1);
class="kw">return INIT_SUCCEEDED;
}
LogDisplay _display(class="num">11, _Digits);
class="type">void OnTimer()
{
EventKillTimer();
class="type">class="kw">double Filter1value1 = class="num">0, Filter1value2 = class="num">0;
if(CustomQuotesRecord::datatype(_Filter1Field) == &class="macro">#x27;t&class="macro">#x27;)
{
Filter1value1 = (class="type">class="kw">double)StringToTime(_Filter1value1);
Filter1value2 = (class="type">class="kw">double)StringToTime(_Filter1value2);
}
else
{
Filter1value1 = StringToDouble(_Filter1value1);
Filter1value2 = StringToDouble(_Filter1value2);
}
_defaultQuotesAdapter.reset();
_defaultEngine.process(_selectorArray, _selectorField,
_AggregatorType, _AggregatorField,
_display,
_SortBy,
Filter1value1, Filter1value2);
}

用 OLAP 切片 EURUSD 报价看真实点差

在 MT5 里挂上 OLAPQTS 这类分析 EA,把 EURUSD 切到 D1 并限定 2019 全年(Filter1Value1=2019.01.01,Value2=2020.01.01),默认 X 轴取形态、COUNT 聚合。日志显示总共读了 12626 根 D1 柱,但落进时间窗的只有 259 根:134 根偏空、125 根偏多,多空分布基本对称。 切到 H1 同样过滤 2019 年,命中 6196 根小时柱,空头 3051、多头 3090,量级和 D1 结论一致但样本更密。 点差才是容易踩坑的地方。MT5 的 MqlRates 只存最小点差,回测若直接拿它算成本,利润倾向被高估。用 hour-of-day 做 X 轴、AVERAGE 聚合 spread 字段,H1 下各小时平均点差在 3.19~4.72 点之间,午夜 01 点最低、09–21 点偏高。 但这只是最小点差均值。换 M1 重跑同样过滤,00 点平均点差跳到 14.06,23 点 6.95,比 H1 结论放大 2–3 倍;用 MAX 聚合更狠:00 点峰值 157 点,而白天多数小时在 11–16 点。做剥头皮或短线,入场离场条件要按这种分钟级极值重新校准。 再用 DEVIATION 聚合能拿到单小时点差标准差,用来给波动跟踪型机器人设过滤阈值。最后切回 D1,QuantGranularity=100(5 位报价点)、SelectorX=quants、FieldX=price range(OC)、Aggregator=COUNT、SortBy=value 降序,结果 72 根柱落在 ±100 点内,更远区间数量递减——这是柱内波动分布的直观切片,不是策略,但能告诉你样本长什么样。

MQL5 / C++
OLAPQTS(EURUSD,D1)	Bars read: class="num">12626
OLAPQTS(EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [class="num">3]
OLAPQTS(EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,D1)	Selectors: class="num">1
OLAPQTS(EURUSD,D1)	X: ShapeSelector(FIELD_SHAPE) [class="num">3]
OLAPQTS(EURUSD,D1)	Processed records: class="num">259
OLAPQTS(EURUSD,D1)	  class="num">134.00000: bearish
OLAPQTS(EURUSD,D1)	    class="num">0.00000: flat
OLAPQTS(EURUSD,D1)	  class="num">125.00000: bullish
OLAPQTS(EURUSD,H1)	Bars read: class="num">137574
OLAPQTS(EURUSD,H1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [class="num">3]
OLAPQTS(EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,H1)	Selectors: class="num">1
OLAPQTS(EURUSD,H1)	X: ShapeSelector(FIELD_SHAPE) [class="num">3]
OLAPQTS(EURUSD,H1)	Processed records: class="num">6196
OLAPQTS(EURUSD,H1)	 class="num">3051.00000: bearish
OLAPQTS(EURUSD,H1)	   class="num">55.00000: flat
OLAPQTS(EURUSD,H1)	 class="num">3090.00000: bullish
OLAPQTS(EURUSD,H1)	Bars read: class="num">137574
OLAPQTS(EURUSD,H1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [class="num">24]
OLAPQTS(EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,H1)	Selectors: class="num">1
OLAPQTS(EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [class="num">24]
OLAPQTS(EURUSD,H1)	Processed records: class="num">6196
OLAPQTS(EURUSD,H1)	    class="num">4.71984: class="num">00
OLAPQTS(EURUSD,H1)	    class="num">3.19066: class="num">01
OLAPQTS(EURUSD,H1)	    class="num">3.72763: class="num">02
OLAPQTS(EURUSD,H1)	    class="num">4.19455: class="num">03
OLAPQTS(EURUSD,H1)	    class="num">4.38132: class="num">04
OLAPQTS(EURUSD,H1)	    class="num">4.28794: class="num">05
OLAPQTS(EURUSD,H1)	    class="num">3.93050: class="num">06
OLAPQTS(EURUSD,H1)	    class="num">4.01158: class="num">07
OLAPQTS(EURUSD,H1)	    class="num">4.39768: class="num">08
OLAPQTS(EURUSD,H1)	    class="num">4.68340: class="num">09
OLAPQTS(EURUSD,H1)	    class="num">4.68340: class="num">10
OLAPQTS(EURUSD,H1)	    class="num">4.64479: class="num">11
OLAPQTS(EURUSD,H1)	    class="num">4.57915: class="num">12
OLAPQTS(EURUSD,H1)	    class="num">4.62934: class="num">13
OLAPQTS(EURUSD,H1)	    class="num">4.64865: class="num">14
OLAPQTS(EURUSD,H1)	    class="num">4.61390: class="num">15
OLAPQTS(EURUSD,H1)	    class="num">4.62162: class="num">16
OLAPQTS(EURUSD,H1)	    class="num">4.50579: class="num">17
OLAPQTS(EURUSD,H1)	    class="num">4.56757: class="num">18
OLAPQTS(EURUSD,H1)	    class="num">4.61004: class="num">19
OLAPQTS(EURUSD,H1)	    class="num">4.59459: class="num">20
OLAPQTS(EURUSD,H1)	    class="num">4.67054: class="num">21
OLAPQTS(EURUSD,H1)	    class="num">4.50775: class="num">22
OLAPQTS(EURUSD,H1)	    class="num">3.57312: class="num">23
OLAPQTS(EURUSD,M1)	Bars read: class="num">1000000
OLAPQTS(EURUSD,M1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [class="num">24]
OLAPQTS(EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,M1)	Selectors: class="num">1
OLAPQTS(EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [class="num">24]
OLAPQTS(EURUSD,M1)	Processed records: class="num">371475
OLAPQTS(EURUSD,M1)	   class="num">14.05653: class="num">00
OLAPQTS(EURUSD,M1)	    class="num">6.63397: class="num">01
OLAPQTS(EURUSD,M1)	    class="num">6.00707: class="num">02
OLAPQTS(EURUSD,M1)	    class="num">5.72516: class="num">03
OLAPQTS(EURUSD,M1)	    class="num">5.72575: class="num">04
OLAPQTS(EURUSD,M1)	    class="num">5.77588: class="num">05
OLAPQTS(EURUSD,M1)	    class="num">5.82541: class="num">06
OLAPQTS(EURUSD,M1)	    class="num">5.82560: class="num">07
OLAPQTS(EURUSD,M1)	    class="num">5.77979: class="num">08
OLAPQTS(EURUSD,M1)	    class="num">5.44876: class="num">09
OLAPQTS(EURUSD,M1)	    class="num">5.32619: class="num">10
OLAPQTS(EURUSD,M1)	    class="num">5.32966: class="num">11
OLAPQTS(EURUSD,M1)	    class="num">5.32096: class="num">12
OLAPQTS(EURUSD,M1)	    class="num">5.32117: class="num">13
OLAPQTS(EURUSD,M1)	    class="num">5.29633: class="num">14
OLAPQTS(EURUSD,M1)	    class="num">5.21140: class="num">15
OLAPQTS(EURUSD,M1)	    class="num">5.17084: class="num">16
OLAPQTS(EURUSD,M1)	    class="num">5.12794: class="num">17
OLAPQTS(EURUSD,M1)	    class="num">5.27576: class="num">18
OLAPQTS(EURUSD,M1)	    class="num">5.48078: class="num">19
OLAPQTS(EURUSD,M1)	    class="num">5.60175: class="num">20
OLAPQTS(EURUSD,M1)	    class="num">5.70999: class="num">21
OLAPQTS(EURUSD,M1)	    class="num">5.87404: class="num">22
OLAPQTS(EURUSD,M1)	    class="num">6.94555: class="num">23
OLAPQTS(EURUSD,M1)	Bars read: class="num">1000000
OLAPQTS(EURUSD,M1)	MaxAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [class="num">24]
OLAPQTS(EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,M1)	Selectors: class="num">1
OLAPQTS(EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [class="num">24]
OLAPQTS(EURUSD,M1)	Processed records: class="num">371475
OLAPQTS(EURUSD,M1)	  class="num">157.00000: class="num">00
OLAPQTS(EURUSD,M1)	   class="num">31.00000: class="num">01
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">02
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">03
OLAPQTS(EURUSD,M1)	   class="num">13.00000: class="num">04
OLAPQTS(EURUSD,M1)	   class="num">11.00000: class="num">05
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">06
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">07
OLAPQTS(EURUSD,M1)	   class="num">11.00000: class="num">08
OLAPQTS(EURUSD,M1)	   class="num">11.00000: class="num">09
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">10
OLAPQTS(EURUSD,M1)	   class="num">13.00000: class="num">11
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">12
OLAPQTS(EURUSD,M1)	   class="num">13.00000: class="num">13
OLAPQTS(EURUSD,M1)	   class="num">12.00000: class="num">14
OLAPQTS(EURUSD,M1)	   class="num">14.00000: class="num">15
OLAPQTS(EURUSD,M1)	   class="num">16.00000: class="num">16
OLAPQTS(EURUSD,M1)	   class="num">14.00000: class="num">17
OLAPQTS(EURUSD,M1)	   class="num">15.00000: class="num">18
OLAPQTS(EURUSD,M1)	   class="num">21.00000: class="num">19

◍ 正文

OLAPQTS (EURUSD,M1) 17.00000: 20 OLAPQTS (EURUSD,M1) 25.00000: 21 OLAPQTS (EURUSD,M1) 31.00000: 22 OLAPQTS (EURUSD,M1) 70.00000: 23 OLAPQTS (EURUSD,M1) Bars read: 1000000 OLAPQTS (EURUSD,M1) VarianceAggregator&lt;QUOTE_RECORD_FIELDS&gt; FIELD_SPREAD [24] OLAPQTS (EURUSD,M1) Filters: FilterRange::FilterSelector&lt;QUOTE_RECORD_FIELDS&gt;(FIELD_DATETIME)[1546300800.0 ... 1577836800.0]; OLAPQTS (EURUSD,M1) Selectors: 1 OLAPQTS (EURUSD,M1) X: DayHourSelector&lt;QUOTE_RECORD_FIELDS&gt;(FIELD_DATETIME) [24] OLAPQTS (EURUSD,M1) Processed records: 371475 OLAPQTS (EURUSD,M1) 9.13767: 00 OLAPQTS (EURUSD,M1) 3.12974: 01 OLAPQTS (EURUSD,M1) 2.72293: 02 OLAPQTS (EURUSD,M1) 2.70965: 03 OLAPQTS (EURUSD,M1) 2.68758: 04 OLAPQTS (EURUSD,M1) 2.64350: 05 OLAPQTS (EURUSD,M1) 2.64158: 06 OLAPQTS (EURUSD,M1) 2.64934: 07 OLAPQTS (EURUSD,M1) 2.62854: 08 OLAPQTS (EURUSD,M1) 2.72117: 09 OLAPQTS (EURUSD,M1) 2.80259: 10 OLAPQTS (EURUSD,M1) 2.79681: 11 OLAPQTS (EURUSD,M1) 2.80850: 12 OLAPQTS (EURUSD,M1) 2.81435: 13 OLAPQTS (EURUSD,M1) 2.83489: 14 OLAPQTS (EURUSD,M1) 2.90745: 15 OLAPQTS (EURUSD,M1) 2.95804: 16 OLAPQTS (EURUSD,M1) 2.96799: 17 OLAPQTS (EURUSD,M1) 2.88021: 18 OLAPQTS (EURUSD,M1) 2.76605: 19 OLAPQTS (EURUSD,M1) 2.72036: 20 OLAPQTS (EURUSD,M1) 2.85615: 21 OLAPQTS (EURUSD,M1) 2.94224: 22 OLAPQTS (EURUSD,M1) 4.60560: 23 OLAPQTS (EURUSD,D1) Bars read: 12627 OLAPQTS (EURUSD,D1) CountAggregato

「用利润因子挖出 EURUSD 的时段偏向」

把报价按「日内时辰 × 周内日辰」切成 24×5=120 个格子,再用 ProfitFactorAggregator 算每个格子的 OC 价差利润因子(正增量和 ÷ 负增量和取模),就能看出哪些时段多头占优、哪些空头占优。因子远大于 1 倾向做多,远小于 1(空头实际是倒数)倾向做空;作者设的可用门槛是 ≥2 或 ≤0.5。 以 EURUSD H1、2019 全年为样本跑 OLAP,排序后头部格子显示:周四 0/1/4 时做多因子 5.85、5.79、4.10,周五 0 时做多因子 5.79;尾部如周三 9 时做空因子 0.49、周四 13 时 0.48。这些数字没扣点差,用自定义字段 1(柱线范围减点差)重算后,结论收敛到:周四凌晨 0/1/4 时多、晚 19/23 时空,周五 0/3/4/9 时多、11/14/23 时空,但周五 23 时临近周末缺口风险偏高。 把上面时间表写进 SingleBar EA(BuyHours/SellHours 逗号分隔,ActiveDayOfWeek=5 锁周五),用 2019 年数据回测结果漂亮,但把测试起点拉回 2018 年初,形态从 2018 年中才稳定生效——说明 OLAP 找出的周期有有效期,不是永久结构。 拉长样本到 2015–2019 再前向验证 2019,单时辰盈利普遍衰减,周三相对最稳却仍起伏;这指向一个事实:外汇和贵金属属高风险品种,时段形态会退化,OLAP 只是研究筛子,真上线前得配合正向验证和多因子扩展,不能单靠一张利润因子表就固定参数。

MQL5 / C++
OLAPQTS(EURUSD,H1)	Bars read: class="num">137597
OLAPQTS(EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_PRICE_RANGE_OC [class="num">120]
OLAPQTS(EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1546300800.0 ... class="num">1577836800.0];
OLAPQTS(EURUSD,H1)	Selectors: class="num">2
OLAPQTS(EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [class="num">24]
OLAPQTS(EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [class="num">5]
OLAPQTS(EURUSD,H1)	Processed records: class="num">6196
OLAPQTS(EURUSD,H1)	      [value]           [title]
OLAPQTS(EURUSD,H1) [  class="num">0] class="num">5.85417 "class="num">00; class="num">1`Monday"
OLAPQTS(EURUSD,H1) [  class="num">1] class="num">5.79204 "class="num">00; class="num">5`Friday"
OLAPQTS(EURUSD,H1) [  class="num">2] class="num">5.25194 "class="num">00; class="num">4`Thursday"
OLAPQTS(EURUSD,H1) [  class="num">3] class="num">4.10104 "class="num">01; class="num">4`Thursday"
OLAPQTS(EURUSD,H1) [  class="num">4] class="num">4.00463 "class="num">01; class="num">2`Tuesday"
OLAPQTS(EURUSD,H1) [  class="num">5] class="num">2.93725 "class="num">01; class="num">3`Wednesday"
OLAPQTS(EURUSD,H1) [  class="num">6] class="num">2.50000 "class="num">00; class="num">3`Wednesday"
OLAPQTS(EURUSD,H1) [  class="num">7] class="num">2.44557 "class="num">15; class="num">1`Monday"
OLAPQTS(EURUSD,H1) [  class="num">8] class="num">2.43496 "class="num">04; class="num">5`Friday"
OLAPQTS(EURUSD,H1) [  class="num">9] class="num">2.36278 "class="num">20; class="num">3`Wednesday"
OLAPQTS(EURUSD,H1) [ class="num">10] class="num">2.33917 "class="num">04; class="num">4`Thursday"
...
OLAPQTS(EURUSD,H1) [class="num">110] class="num">0.49096 "class="num">09; class="num">3`Wednesday"
OLAPQTS(EURUSD,H1) [class="num">111] class="num">0.48241 "class="num">13; class="num">4`Thursday"
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多维切片后的异动提示,把重复劳动交给小布,你专注决策。

常见问题

均线是固定公式的单维输出,OLAP 允许你按时间粒度、波动区间、品种等多个轴自由聚合,看到的是结构化分布而非一条线。
通用部分抽到了 OLAPCommon.mqh,交易相关移入 OLAPTrades.mqh,旧逻辑通过包含新文件仍可运行,只是分层更清晰。
目前小布内置的是预处理后的多维异动看板,逻辑同源;若需自定义立方体,可参考文中 EA 结构自行扩展后对接。
文中测试章节给出具体聚合维度的回测倾向,实际外汇贵金属属高风险,信号仅在概率上提供参考,需结合实时盘况。
本篇只搭出从立方体到规则原型的链路,完整策略打磨放在第 3 篇,关于信号过滤的完整讨论见《在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价·收尾篇》。