在交易中应用 OLAP(第 3 部分):为开发交易策略而分析报价·综合运用
(3/3)· 前两篇搭好了 OLAP 引擎,这一篇把它直接压到报价上榨出策略信号
「用时间切片给 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 > 0 | reverse > 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 全年即可验证分布。
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 格逐一跑一遍验证。
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 就能复现这张表。
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 年自动识别盈利时段并交出正收益。外汇与贵金属属高风险品种,该结果仅说明工具可用,仍需后续深挖。
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 打印命中的小时是否集中在伦敦段。
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 月数据跑一遍验证。
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 历史数据复算一遍。