在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价·综合运用
📊

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

(3/3)· 前两篇搭好了 OLAP 引擎,这一篇把它直接压到报价上榨出策略信号

实战向进阶 第 3/3 篇
很多人把 OLAP 只当成回测报告的美化工具,报价来了还是肉眼翻 K 线。其实把多维切片逻辑套到 tick 和柱体上,能直接看到传统指标藏起来的时段偏好和波动结构。

「用时间切片给 EURUSD 的净波动称重」

把 EURUSD 的 H1 数据按「小时 + 星期」二维切片后,净波动(剔除点差的开收区间)在 2019-01-01 至 2019-12-31 区间内共处理 6196 条记录。周五 00 点窗口的加权值冲到 6.34,周一 00 点也有 5.64,而周五 23 点只有 0.19,时段间的非对称非常明显。 下面这段重写逻辑把每根新棒的开收区间减去点差,再按符号拆成「幅度」和「方向」两个自定义字段,方便后续按时间聚合。 [CODE] virtual void fillCustomFields() override { const double newBarRange = get(FIELD_PRICE_RANGE_OC); const double spread = get(FIELD_SPREAD); set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread)); set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread)); // ... } [/CODE] 虚函数 override 父类的字段填充;newBarRange 取开收价差,spread 取当前点差。CUSTOM1 是「带符号的净幅度」,CUSTOM2 是「净方向正负 1」,后续 OLAP 就靠这两个字段排序。 实盘落地时,可以用输入参数直接圈定交易小时。下面这段把逗号分隔的字符串解析成整型数组,并在 OnTick 里按当前小时匹配买/卖窗口。 [CODE] input string BuyHours = ""; input string SellHours = ""; input uint ActiveDayOfWeek = 0; int buyHours[], sellHours[]; int parseHours(const string &data, int &result[]) { string str[]; const int n = StringSplit(data, ',', str); ArrayResize(result, n); for(int i = 0; i < n; i++) { result[i] = (int)StringToInteger(str[i]); } return n; } int OnInit() { const int trend = parseHours(BuyHours, buyHours); const int reverse = parseHours(SellHours, sellHours);

return trend > 0reverse > 0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT;

} template<typename T> int ArrayFind(const T &array[], const T value) { const int n = ArraySize(array); for(int i = 0; i < n; i++) { if(array[i] == value) return i; } return -1; } void OnTick() { MqlTick tick; if(!SymbolInfoTick(_Symbol, tick)) return; const int h = TimeHour(TimeCurrent()); int mode = 0; if(ArrayFind(buyHours, h) > -1) { mode = +1; } else if(ArrayFind(sellHours, h) > -1) { mode = -1; } if(ActiveDayOfWeek != 0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = 0; // skip all days except specified // pick up existing orders (if any) } [/CODE] BuyHours 填 "0,1" 就只在 0 点和 1 点尝试做多,SellHours 同理;ActiveDayOfWeek 非 0 时只在该星期几生效。外汇和贵金属属高风险品种,时段高值只是概率倾向,不代表该窗口必然走出同向行情,开 MT5 把这段挂上 EURUSD 的 H1 回测 2019 全年即可验证分布。

MQL5 / C++
class="kw">virtual class="type">void fillCustomFields() class="kw">override
{
class="kw">const class="type">class="kw">double newBarRange = get(FIELD_PRICE_RANGE_OC);
class="kw">const class="type">class="kw">double spread = get(FIELD_SPREAD);
set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread));
set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread));
class=class="str">"cmt">// ...
}

class="kw">input class="type">class="kw">string BuyHours = "";
class="kw">input class="type">class="kw">string SellHours = "";
class="kw">input class="type">uint ActiveDayOfWeek = class="num">0;
class="type">int buyHours[], sellHours[];
class="type">int parseHours(class="kw">const class="type">class="kw">string &data, class="type">int &result[])
{
class="type">class="kw">string str[];
class="kw">const class="type">int n = StringSplit(data, &class="macro">#x27;,&class="macro">#x27;, str);
ArrayResize(result, n);
for(class="type">int i = class="num">0; i < n; i++)
{
result[i] = (class="type">int)StringToInteger(str[i]);
}
class="kw">return n;
}
class="type">int OnInit()
{
class="kw">const class="type">int trend = parseHours(BuyHours, buyHours);
class="kw">const class="type">int reverse = parseHours(SellHours, sellHours);
class="kw">return trend > class="num">0 || reverse > class="num">0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT;
}
class="kw">template<class="kw">typename T>
class="type">int ArrayFind(class="kw">const T &array[], class="kw">const T value)
{
class="kw">const class="type">int n = ArraySize(array);
for(class="type">int i = class="num">0; i < n; i++)
{
if(array[i] == value) class="kw">return i;
}
class="kw">return -class="num">1;
}
class="type">void OnTick()
{
class="type">MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick)) class="kw">return;
class="kw">const class="type">int h = TimeHour(TimeCurrent());
class="type">int mode = class="num">0;
if(ArrayFind(buyHours, h) > -class="num">1)
{
mode = +class="num">1;
}
else
if(ArrayFind(sellHours, h) > -class="num">1)
{
mode = -class="num">1;
}
if(ActiveDayOfWeek != class="num">0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = class="num">0; class=class="str">"cmt">// skip all days except specified
class=class="str">"cmt">// pick up existing orders(if any)
}

◍ 同向持仓过滤与时段盈亏剖面

这段逻辑先通过 CurrentOrderDirection() 判断当前有无同向单,再决定是加仓、反向清场还是直接退出。mode 为 0 时代表不交易,若已有持仓则全平后 return,避免无信号时占用保证金。 const int direction = CurrentOrderDirection(); // 取当前持仓方向,+1买 -1卖 0无 define mode == 0 分支:有单就 OrdersCloseAll() 然后 return,不新开。 若存在持仓且与 mode 同向,直接 return 让原单继续跑;若反向则先全平再开新向单。 开单价格用 SymbolInfoDouble 取实时 ASK/BID,滑点容差写死 100 点(10美元/标准手量级),Lot 为外部传入手数。 CurrentOrderDirection() 倒序遍历 OrdersTotal(),只认 OP_BUY/OP_SELL 两类市价单,命中即返回方向,否则回 0。OrdersCloseAll() 支持按 symbol 与 type 过滤,平仓价取对手价。 EURUSD H1 的 OLAP 回测切片(2014-2019,137606 根 BAR,命中 24832 条记录)显示:周三 01 点盈亏比 2.04 居首,周一 01 点 1.75 次之;而周三 11 点仅 0.498,时段选择对概率影响显著。外汇与贵金属属高风险品种,上述剖面仅代表历史样本,实盘可能偏离。 别把时段剖面当开仓信号 它只说明某小时窗历史盈亏比偏高,不等于未来同窗必出趋势;建议把这段代码挂 MT5 策略测试器,把 DayHourSelector 的 24 格逐一跑一遍验证。

MQL5 / C++
class="kw">const class="type">int direction = CurrentOrderDirection();
if(mode == class="num">0)
{
if(direction != class="num">0)
{
OrdersCloseAll();
}
class="kw">return;
}
if(direction != class="num">0) class=class="str">"cmt">// there exist open orders
{
if(mode == direction) class=class="str">"cmt">// keep direction
{
class="kw">return; class=class="str">"cmt">// existing trade goes on
}
OrdersCloseAll();
}
class="kw">const class="type">int type = mode > class="num">0 ? OP_BUY : OP_SELL;
class="kw">const class="type">class="kw">double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
OrderSend(_Symbol, type, Lot, p, class="num">100, class="num">0, class="num">0);
}
class="type">int CurrentOrderDirection(class="kw">const class="type">class="kw">string symbol = NULL)
{
for(class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--)
{
if(OrderSelect(i, SELECT_BY_POS))
{
if(OrderType() <= OP_SELL && (symbol == NULL || symbol == OrderSymbol()))
{
class="kw">return OrderType() == OP_BUY ? +class="num">1 : -class="num">1;
}
}
}
class="kw">return class="num">0;
}
class="type">void OrdersCloseAll(class="kw">const class="type">class="kw">string symbol = NULL, class="kw">const class="type">int type = -class="num">1) class=class="str">"cmt">// OP_BUY or OP_SELL
{
for(class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--)
{
if(OrderSelect(i, SELECT_BY_POS))
{
if(OrderType() <= OP_SELL && (type == -class="num">1 || OrderType() == type) && (symbol == NULL || symbol == OrderSymbol()))
{
OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? SymbolInfoDouble(OrderSymbol(), SYMBOL_BID) : SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK), class="num">100);
}
}
}
}

用相邻柱线不对称协方差筛趋势与反转窗口

柱线方向大概率受前一根牵引,这种依赖和日内、周内波动的周期特征同源。除了按时辰和周内日辰累积柱线尺寸与方向,还得把前一根柱线的特性塞进自定义字段,才有办法做下一步策略切分。 第三个自定义字段算的是相邻两根柱线的“不对称”协方差:普通协方差用两根柱线价格走势范围相乘并带方向符号,但前后互换结果一样,对只针对下一根做决策没用。这里只取下一根的范围乘上前一根的符号,让历史高波动不至于污染预测值。 该字段配利润因子(PF)聚合器就能直接分策略:PF>1 倾向沿前一根方向交易盈利,PF<1 则反转更划算;极值越远,趋势或反转的胜算概率越高。第四个字段存相邻柱线同向(+1)/异向(-1)符号,用来统计反转次数与两类策略的入场成效。 静态变量存前一根的 range 和 spread 即可,因为适配器按时间顺序喂数据;示例里用默认单例够用,但正规写法应让适配器只传 CustomQuotesBaseRecord 并给 fillCustomFields 一个可还原状态的容器引用,比如 fillCustomFields(double &bundle[])。 OLAPQTS 输入把 AggregatorField 指向 custom 3,X/Y 仍是 hour-of-day 与 day-of-week,PF 降序,日期框在 2018 整年。NextBar EA 拿这组参数跑:星期一 PositiveHours=23,3、NegativeHours=10,13,2018.01.01–2019.05.01 测试。 实测该组合 2019 年 1 月还能盈利,之后连续亏损——外汇高杠杆品种形态存活期有限,得想办法动态换窗口。下面代码里 custom3 在 previousBarRange 绝对值大于 spread 时才写值,并扣掉双向最大点差近似损耗。 日志里 EURUSD H1 读 137642 根、处理 6203 根,周一 23 点 PF 2.65、03 点 2.38 最亮眼;周五 17 点 PF 0.29、周一 10 点 0.38 是反转候选。开 MT5 把这段代码挂上 OLAPQTS 就能复现这张表。

MQL5 / C++
class CustomQuotesRecord: class="kw">public QuotesRecord
  {
    class="kw">private:
      class="kw">static class="type">class="kw">double previousBarRange;
      class="kw">static class="type">class="kw">double previousSpread;
      
    class="kw">public:
      class=class="str">"cmt">// ...
      
      class="kw">virtual class="type">void fillCustomFields() class="kw">override
      {
        class="kw">const class="type">class="kw">double newBarRange = get(FIELD_PRICE_RANGE_OC);
        class="kw">const class="type">class="kw">double spread = get(FIELD_SPREAD);
  
        class=class="str">"cmt">// ...
  
        if(MathAbs(previousBarRange) > previousSpread)
        {
          class="type">class="kw">double mult = newBarRange * previousBarRange;
          class="type">class="kw">double value = MathSign(mult) * MathAbs(newBarRange);
  
          class=class="str">"cmt">// this is an attempt to approximate average losses due to spreads
          value += MathSignNonZero(value) * -class="num">1 * MathMax(spread, previousSpread);
          
          set(FIELD_CUSTOM3, value);
          set(FIELD_CUSTOM4, MathSign(mult));
         }
        else
        {
          set(FIELD_CUSTOM3, class="num">0);
          set(FIELD_CUSTOM4, class="num">0);
         }
  
        previousBarRange = newBarRange;
        previousSpread = spread;
       }
      
  };
  OLAPQTS(EURUSD,H1)	Bars read: class="num">137642
  OLAPQTS(EURUSD,H1)	Aggregator: ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [class="num">120]
  OLAPQTS(EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[class="num">1514764800.0 ... class="num">1546300800.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">6203
  OLAPQTS(EURUSD,H1)	      [value]           [title]
  OLAPQTS(EURUSD,H1) [  class="num">0] class="num">2.65010 "class="num">23; class="num">1`Monday"   
  OLAPQTS(EURUSD,H1) [  class="num">1] class="num">2.37966 "class="num">03; class="num">1`Monday"   
  OLAPQTS(EURUSD,H1) [  class="num">2] class="num">2.33875 "class="num">04; class="num">4`Thursday" 
  OLAPQTS(EURUSD,H1) [  class="num">3] class="num">1.96317 "class="num">20; class="num">3`Wednesday"
  OLAPQTS(EURUSD,H1) [  class="num">4] class="num">1.91188 "class="num">18; class="num">2`Tuesday"  
  OLAPQTS(EURUSD,H1) [  class="num">5] class="num">1.89293 "class="num">23; class="num">3`Wednesday"
  OLAPQTS(EURUSD,H1) [  class="num">6] class="num">1.87159 "class="num">12; class="num">1`Monday"   
  OLAPQTS(EURUSD,H1) [  class="num">7] class="num">1.78903 "class="num">15; class="num">5`Friday"   
  OLAPQTS(EURUSD,H1) [  class="num">8] class="num">1.74461 "class="num">01; class="num">4`Thursday" 
  OLAPQTS(EURUSD,H1) [  class="num">9] class="num">1.73821 "class="num">13; class="num">2`Tuesday"  
  OLAPQTS(EURUSD,H1) [ class="num">10] class="num">1.73244 "class="num">14; class="num">2`Tuesday"
  ...  
  OLAPQTS(EURUSD,H1) [class="num">110] class="num">0.57331 "class="num">22; class="num">4`Thursday" 
  OLAPQTS(EURUSD,H1) [class="num">111] class="num">0.51515 "class="num">07; class="num">5`Friday"   
  OLAPQTS(EURUSD,H1) [class="num">112] class="num">0.50202 "class="num">05; class="num">5`Friday"   
  OLAPQTS(EURUSD,H1) [class="num">113] class="num">0.48557 "class="num">04; class="num">2`Tuesday"  
  OLAPQTS(EURUSD,H1) [class="num">114] class="num">0.46313 "class="num">23; class="num">2`Tuesday"  
  OLAPQTS(EURUSD,H1) [class="num">115] class="num">0.44182 "class="num">00; class="num">2`Tuesday"  
  OLAPQTS(EURUSD,H1) [class="num">116] class="num">0.40907 "class="num">13; class="num">1`Monday"   
  OLAPQTS(EURUSD,H1) [class="num">117] class="num">0.38230 "class="num">10; class="num">1`Monday"   
  OLAPQTS(EURUSD,H1) [class="num">118] class="num">0.36296 "class="num">22; class="num">5`Friday"   
  OLAPQTS(EURUSD,H1) [class="num">119] class="num">0.29462 "class="num">17; class="num">5`Friday"

「把 OLAP 引擎塞进 EA 里自动调时间表」

此前做 OLAP 报价分析要借非交易 EA 和外挂脚本,麻烦且割裂。更顺手的思路是直接在内置 OLAP 引擎的机器人里跑:它能按设定周期自算报价、改写交易时段,主参数还可套用前向验证思路来优化。这个内置引擎的 EA 叫 OLAPQRWF,即 OLAP Quotes with Rolling Walk-Forward。 核心输入里,BarNumberLookBack 决定取多少根 H1 历史柱做分析;Threshold 是触发成交的利润因子门槛;Strategy 分 0(单根柱方向统计)和 1(相邻两根柱方向统计)。重算 OLAP 块的频率也要单独指定,且可切自定义聚合字段——字段 1、3 带柱线区间,字段 2、4 只看方向计数。 CustomQuotesRecord 类继承自旧分析器,原选择器、过滤器参数改成常量或全局变量,按策略切换但不改名。注意柱线用 FIELD_INDEX 按数量过滤而非时间,_Filter1value1 实际等于总柱数减 BarNumberLookBack,所以 EA 永远只算最近那段窗口。 OnTick 里走柱线模式交易,等月或周切换时调 calcolap 跑 OLAP。分析完由特殊显示对象 stats 接手,它从统计里挑出最优交易时间,并用自己的 trade 方法下单。MyOLAPStats 类用二维数组 index 存时间表绩效,display 顺序填向量、saveVector 拷某日 24 小时数值,数组按利润因子排序,日志里能直接看。 优化用 2015–2019 年数据,再拿 2019 年做前向验证。元参数寻优范围:BarNumberLookBack 480–5760、Threshold 0.5–5.0、Strategy 0–1、CustomField 0–1,Update 固定按月。合成适应度 = 夏普比率 × 交易次数,最优解落在 BarNumberLookBack=2160、Threshold=3.0、Strategy=0、CustomField=count。 EURUSD H1 上,用 2018 年起的聚合窗口做 2015–2020 测试,EA 在 2019 年自动识别盈利时段并交出正收益。外汇与贵金属属高风险品种,该结果仅说明工具可用,仍需后续深挖。

MQL5 / C++
class="kw">input class="type">int BarNumberLookBack = class="num">2880; class=class="str">"cmt">// BarNumberLookBack(week: class="num">120 H1, month: class="num">480 H1, year: class="num">5760 H1)
class="kw">input class="type">class="kw">double Threshold = class="num">2.0; class=class="str">"cmt">// Threshold(PF >= Threshold && PF <= class="num">1/Threshold)
class="kw">input class="type">int Strategy = class="num">0; class=class="str">"cmt">// Strategy(class="num">0 - single bar, class="num">1 - adjacent bars)
enum UPDATEPERIOD
{
monthly,
weekly
};
class="kw">input UPDATEPERIOD Update = monthly;
enum CUSTOMFIELD
{
range,
count
};
class="kw">input CUSTOMFIELD CustomField = range;
class="kw">const SELECTORS SelectorX = SELECTOR_DAYHOUR;
class="kw">const ENUM_FIELDS FieldX = FIELD_DATETIME;
class="kw">const SELECTORS SelectorY = SELECTOR_WEEKDAY;
class="kw">const ENUM_FIELDS FieldY = FIELD_DATETIME;
class="kw">const SELECTORS SelectorZ = SELECTOR_NONE;
class="kw">const ENUM_FIELDS FieldZ = FIELD_NONE;
class="kw">const SELECTORS _Filter1 = SELECTOR_FILTER;
class="kw">const ENUM_FIELDS _Filter1Field = FIELD_INDEX;
class="type">int _Filter1value1 = -class="num">1; class=class="str">"cmt">// to be filled with index of first bar to process
class="kw">const class="type">int _Filter1value2 = -class="num">1;
class="kw">const AGGREGATORS _AggregatorType = AGGREGATOR_PROFITFACTOR;
ENUM_FIELDS _AggregatorField = FIELD_CUSTOM1;
class="kw">const SORT_BY _SortBy = SORT_BY_NONE;
class="type">bool freshStart = true;
class="type">void OnTick()
{
if(!isNewBar()) class="kw">return;
if(Bars(_Symbol, _Period) < BarNumberLookBack) class="kw">return;
class="kw">const class="type">int m0 = TimeMonth(iTime(_Symbol, _Period, class="num">0));
class="kw">const class="type">int w0 = _TimeDayOfWeek();
class="kw">const class="type">int m1 = TimeMonth(iTime(_Symbol, _Period, class="num">1));
class="kw">const class="type">int w1 = _TimeDayOfWeek();
class="kw">static class="type">bool success = class="kw">false;
if((Update == monthly && m0 != m1)
|| (Update == weekly && w0 < w1)
|| freshStart)
{
success = calcolap();
freshStart = !success;
}
class=class="str">"cmt">//...
}
class="type">bool calcolap()
{
_Filter1value1 = Bars(_Symbol, _Period) - BarNumberLookBack;
_AggregatorField = Strategy == class="num">0 ? (ENUM_FIELDS)(FIELD_CUSTOM1 + CustomField) : (ENUM_FIELDS)(FIELD_CUSTOM3 + CustomField);
_defaultQuotesAdapter.reset();
class="kw">const class="type">int processed =
_defaultEngine.process(_selectorArray, _selectorField,
_AggregatorType, _AggregatorField,
stats,                              class=class="str">"cmt">// custom display object
_SortBy,
_Filter1value1, _Filter1value2);
class="kw">return processed == BarNumberLookBack;
}

◍ 把多维统计结果接进下单逻辑

上面那段类定义把 OLAP 风格的统计封装成 MyOLAPStats,核心是把 5 个交易日 × 24 小时的价格强度塞进 index[][] 三维数组(值、小时、星期),display() 里用 ArraySort 排完直接打印,方便在 MT5 专家日志里肉眼核对分布。 trade() 的落地关键在于用 TimeHour(lastBar) 和 _TimeDayOfWeek()-1 定位当前小时与星期,再遍历 120 行 index 找匹配格。若强度 ≥ threshold 记为 +1,≤ 1/threshold 记为 -1,否则平掉已有仓位——阈值对称设计意味着你调一个参数就能同时管多空触发。 策略 0 的逻辑是:已有同向单就持有,反向单先全平再开。外汇与贵金属杠杆高,这种基于历史分位的触发在震荡周可能频繁假信号,上机前先用策略测试器跑 24×5 的样本窗口验证 index 填充是否如预期。 别把阈值当圣杯 threshold 设 1.5 还是 2.0 直接决定成交密度,建议从 1.8 起在 EURUSD 的 H1 回测,观察被 +Rule / -Rule 打印命中的小时是否集中在伦敦段。

MQL5 / C++
class MyOLAPStats: class="kw">public Display
{
class="kw">private:
   class="type">bool filled;
   class="type">class="kw">double index[][class="num">3]; class=class="str">"cmt">// value, hour, day
   class="type">int cursor;
class="kw">protected:
   class="type">bool saveVector(MetaCube *cube, class="kw">const class="type">int &consts[], class="kw">const SORT_BY sortby = SORT_BY_NONE)
   {
      PairArray *result = NULL;
      cube.getVector(class="num">0, consts, result, sortby);
      if(CheckPointer(result) == POINTER_DYNAMIC)
      {
         class="kw">const class="type">int n = ArraySize(result.array);
         if(n == N_HOURS)
         {
            for(class="type">int i = class="num">0; i < n; i++)
            {
               index[cursor][class="num">0] = result.array[i].value;
               index[cursor][class="num">1] = i;
               index[cursor][class="num">2] = consts[AXIS_DAYS];
               cursor++;
            }
         }
         class="kw">delete result;
         class="kw">return n == N_HOURS;
      }
      class="kw">return class="kw">false;
   }
class="kw">public:
   class="kw">virtual class="type">void display(MetaCube *cube, class="kw">const SORT_BY sortby = SORT_BY_NONE, class="kw">const class="type">bool identity = class="kw">false) class="kw">override
   {
      class="type">int consts[];
      class="kw">const class="type">int n = cube.getDimension();
      ArrayResize(consts, n);
      ArrayInitialize(consts, class="num">0);
      filled = class="kw">false;
      ArrayResize(index, N_HOURS * N_DAYS);
      ArrayInitialize(index, class="num">1);
      cursor = class="num">0;
      if(n == class="num">2)
      {
         class="kw">const class="type">int i = AXIS_DAYS;
         class="type">int m = cube.getDimensionRange(i); class=class="str">"cmt">// should be class="num">5 work days
         for(class="type">int j = class="num">0; j < m; j++)
         {
            consts[i] = j;
            if(!saveVector(cube, consts, sortby)) class=class="str">"cmt">// class="num">24 hours(values) per current day
            {
               Print("Bad data format");
               class="kw">return;
            }
            consts[i] = class="num">0;
         }
         filled = true;
         ArraySort(index);
         ArrayPrint(index);
      }
      else
      {
         Print("Incorrect cube structure");
      }
   }
};

class="type">void trade(class="kw">const class="type">class="kw">double threshold, class="kw">const class="type">class="kw">double lots, class="kw">const class="type">int strategy = class="num">0)
{
   class="kw">const class="type">int h = TimeHour(lastBar);
   class="kw">const class="type">int w = _TimeDayOfWeek() - class="num">1;
   class="type">int mode = class="num">0;
   for(class="type">int i = class="num">0; i < N_HOURS * N_DAYS; i++)
   {
      if(index[i][class="num">1] == h && index[i][class="num">2] == w)
      {
         if(index[i][class="num">0] >= threshold)
         {
            mode = +class="num">1;
            Print("+ Rule ", i);
            class="kw">break;
         }
         if(index[i][class="num">0] <= class="num">1.0 / threshold)
         {
            mode = -class="num">1;
            Print("- Rule ", i);
            class="kw">break;
         }
      }
   }
   class="kw">const class="type">int direction = CurrentOrderDirection();
   if(mode == class="num">0)
   {
      if(direction != class="num">0)
      {
         OrdersCloseAll();
      }
      class="kw">return;
   }
   if(strategy == class="num">0)
   {
      if(direction != class="num">0)
      {
         if(mode == direction)
         {
            class="kw">return;
         }
         OrdersCloseAll();
      }
   }
}

用位运算一键开仓的止损挂法

这段片段把方向判断和止损计算压进了几行内联表达式,适合做成「小布盯盘」里的快捷下单宏。mode 大于 0 视为做多,否则做空,省去了写 if-else 的冗余。 价格取当前品种卖价或买价:做多读 SYMBOL_ASK,做空读 SYMBOL_BID;止损若 StopLoss 参数大于 0,则按点数 _Point 反向偏移,否则传 0 表示不挂止损。 OrderSend 的滑点参数写死 100 点,在流动性正常的欧美盘口通常能成交,但黄金跳空时可能被动扩滑点,属高风险场景,实盘前应在 MT5 策略测试器用 2023 年 1—6 月数据跑一遍验证。

MQL5 / C++
class="kw">const class="type">int type = mode > class="num">0 ? OP_BUY : OP_SELL;
class="kw">const class="type">class="kw">double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
class="kw">const class="type">class="kw">double sl = StopLoss > class="num">0 ? (type == OP_BUY ? p - StopLoss * _Point : p + StopLoss * _Point) : class="num">0;
OrderSend(_Symbol, type, Lot, p, class="num">100, sl, class="num">0);
}
class=class="str">"cmt">// ...
}

「一点提醒」

把 OLAP 引擎接进 EA 时,优化器要额外吃下一批元参数——它们直接改写统计集合的口径,而不是只动手数或止损。这类在线分析工具在震荡无序的行情里容易失准,外汇和贵金属的高波动特性会放大误判概率,别当成开箱即用的圣杯。 作者早年在 2016 年就用 OLAP 做季节性研究,当时 MT 环境还没有内置 SQL;后来读者问是否已被 SQLite 取代,结论是相互补充而非重叠——SQL 写聚合查询繁琐,OLAP 提供的是更顺手的多维切片界面。 真要落地,建议先跑一个只吐统计、不下单的非交易 EA 把字段逻辑调通,再谈集成。高风险品种上任何统计优势都只是概率倾斜,实盘前务必用 MT5 历史数据复算一遍。

把切片诊断交给小布盯盘
这些报价维度的 OLAP 切片,小布盯盘的 AIGC 已内置,打开对应品种页即可看到分时段波动和热区分布,你只管判断要不要跟。

常见问题

均线只给一维平滑结果,OLAP 能按小时、星期、波动区间同时切分报价,暴露条件分布里的非对称结构,策略参数更容易对准真实市况。
通用选择器、聚合器和记录基类留在 OLAPCommon,交易特有的字段和适配器挪到 OLAPTrades,新数据源只需继承通用层,不必重写整套逻辑。
倾向用滚动窗口,比如每 500~1000 根柱体或部分重叠时段重算,过于频繁会吃掉 CPU,过慢则策略滞后于 regime 切换。
可以,小布盯盘品种页内置了多维报价切片和热力呈现,不用自己编译 mqh,适合先快速验证想法再决定要不要写 EA。
贵金属和外汇受数据和流动性冲击大,优势可能随波动率 regime 衰减,建议配合样本外和前向验证,高风险品种尤需谨慎。