计算数学表达式(第二部分)。 普拉特和分流场解析器·综合运用
「跑通解析器测试脚本看速度差异」
ExpresSParserS.mq5 这套测试脚本干三件事:功能校验、四种解析器算速测量、语法树与字节码演示,还顺带把指标当内置函数用了一遍。 功能测试里既有正常表达式,也有故意写错的(未声明变量、零除等)。判断对不对不看是否报错,而看实际结果和预期是否一致——所以日志里 19 个测试全过,其中 2 个是预期内的错误,这也算“正确”。 速度只在一个反复计算的循环里测,解释器模式每次算完立刻重解表达式,另外三种解析器把解析放在循环外。四种方法首次解析耗时差不多,但预编译后算速快好几倍。 上面日志里 10000 次循环实测:解释执行 104572 微秒,编译式 25011,Pratt 解析 24967,分流场 23147,而 Pratt 字节码仅 23738。字节码路线最快,获取字节码用 Pratt 还是分流场区别很小。外汇与贵金属脚本测算属高风险环境,实盘前请在策略测试器充分验证。 选哪种看你顺手程度和任务:要语法扩展灵活就挑能改树的,要嵌进现有程序就挑接口近的,别光看那点微秒差。
Running class="num">19 tests on ExpressionPratt* … class="num">1 passed, ok: a > b ? b > c ? class="num">1 : class="num">2 : class="num">3 = class="num">3.0; expected = class="num">3.0 class="num">2 passed, ok: class="num">2 > class="num">3 ? class="num">2 : class="num">3 > class="num">4 ? class="num">3 : class="num">4 = class="num">4.0; expected = class="num">4.0 class="num">3 passed, ok: class="num">4 > class="num">3 ? class="num">2 > class="num">4 ? class="num">2 : class="num">4 : class="num">3 = class="num">4.0; expected = class="num">4.0 class="num">4 passed, ok: (a + b) * <span class="functions">sqrt</span>(c) = class="num">8.944271909999159; expected = class="num">8.944271909999159 class="num">5 passed, ok: (b == c) > (a != class="num">1.5) = class="num">0.0; expected = class="num">0.0 class="num">6 passed, ok: (b == c) >= (a != class="num">1.5) = class="num">1.0; expected = class="num">1.0 class="num">7 passed, ok: (a > b) || <span class="functions">sqrt</span>(c) = class="num">1.0; expected = class="num">1.0 class="num">8 passed, ok: (!class="num">1 != !(b - c/class="num">2)) = class="num">1.0; expected = class="num">1.0 class="num">9 passed, ok: -class="num">1 * c == -<span class="functions">sqrt</span>(-c * -c) = class="num">1.0; expected = class="num">1.0 class="num">10 passed, ok: <span class="functions">pow</span>(class="num">2, class="num">5) % class="num">5 = class="num">2.0; expected = class="num">2.0 class="num">11 passed, ok: min(max(a,b),c) = class="num">2.5; expected = class="num">2.5 class="num">12 passed, ok: <span class="functions">atan</span>(<span class="functions">sin</span>(class="num">0.5)/<span class="functions">cos</span>(class="num">0.5)) = class="num">0.5; expected = class="num">0.5 class="num">13 passed, ok: .class="num">2 * .class="num">3 + .class="num">1 = class="num">0.16; expected = class="num">0.16 class="num">14 passed, ok: (a == b) + (b == c) = class="num">0.0; expected = class="num">0.0 class="num">15 passed, ok: -(a + b) * !!<span class="functions">sqrt</span>(c) = -class="num">4.0; expected = -class="num">4.0 class="num">16 passed, ok: sin( max( class="num">2 * class="num">1.5, class="num">3 ) / class="num">3 * class="num">3.14159265359 ) = -class="num">2.068231111547469e-13; expected = class="num">0.0 lookUpVariable error: Variable is undefined: _1c @ class="num">7: class="num">1 / _1c^ class="num">17 passed, er: class="num">1 / _1c = nan; expected = nan safeDivide error: Error : Division by class="num">0! @ class="num">15: class="num">1 / (class="num">2 * b - c)^ class="num">18 passed, er: class="num">1 / (class="num">2 * b - c) = inf; expected = inf class="num">19 passed, ok: <span class="functions">sqrt</span>(b-c) = -nan(ind); expected = -nan(ind) class="num">19 tests passed of class="num">19 class="num">17 for correct expressions, class="num">2 for invalid expressions >>> Performance tests(timing per method) Evaluation: class="num">104572 Compilation: class="num">25011 Pratt bytecode: class="num">23738 Pratt: class="num">24967 ShuntingYard: class="num">23147
用表达式喂信号给EA
把买卖条件写成字符串表达式,比在代码里写死参数要灵活得多。解析器在运行时才编译这些公式,所以不用像改MQL5源码那样重新编译EA,也能随时换策略逻辑。 上面这段演示里,SignalBuy 和 SignalSell 两个输入项直接放均线交叉公式:EMA_OPEN_{Fast}(0)/EMA_OPEN_{Slow}(0) 大于或小于 1±Threshold。Fast 默认 10、Slow 默认 21,Threshold 写死 0.01 仅作演示,实盘里这类常数最好外挂成输入参数。 OnInit 里分别建了两个 ExpressionCompiler 实例 ecb 和 ecs,各自塞入 Fast、Slow 变量后编译表达式。用两个实例是因为买卖公式的变量表可能不同,算之前得切上下文。任一表达式语法出错就 INIT_FAILED 并打印错误,EA 直接起不来。 OnTick 只在新K线触发,p1.resolve() 和 p2.resolve() 算出 bool 标志。若买卖同为真,代码强制双双置 false,避免自相矛盾;之后按方向平掉反手仓再开新仓,MQL 逻辑保证同品种同时只有一单。 在策略测试器跑这套,结果大概率不漂亮——它本就不是成品盈利系统,而是给你一套不用编译就能试信号的工具。外汇和贵金属波动大、杠杆高,用表达式随便改阈值都可能放大回撤,上真金前务必在模拟盘验过。 代码逐行拆解: #define INDICATOR_FUNCTORS —— 开启指标函子支持,让表达式里能调 EMA_OPEN 这类现成函数。 #include <ExpresSParserS/ExpressionCompiler.mqh> —— 引入表达式编译器头文件。 input string SignalBuy = "EMA_OPEN_{Fast}(0)/EMA_OPEN_{Slow}(0) > 1 + Threshold"; —— 买入信号表达式,快线除慢线大于1加阈值。 input string SignalSell = "EMA_OPEN_{Fast}(0)/EMA_OPEN_{Slow}(0) < 1 - Threshold"; —— 卖出信号表达式。 input string Variables = "Threshold=0.01"; —— 变量表初始值,阈值0.01。 input int Fast = 10; / input int Slow = 21; —— 快慢均线周期输入。 ExpressionCompiler ecb(Variables), ecs(Variables); —— 两个编译器实例,分别管买卖。 Promise *p1, *p2; —— 存编译后的语法树承诺对象。 int OnInit() —— 初始化:ecb.variableTable().set 注入 Fast、Slow;p1=ecb.evaluate 编译买入式,失败则打印并返回 INIT_FAILED。ecs 同理编译卖出式。 #define _Ask / _Bid —— 宏取当前卖买价。 void OnTick() —— 新bar才动;buy/sell 取 resolve 结果;同真则清零;buy 时平卖开买,sell 时平买开卖,否则全平。
class="macro">#define INDICATOR_FUNCTORS class="macro">#include <ExpresSParserS/ExpressionCompiler.mqh> input class="type">class="kw">string SignalBuy = "EMA_OPEN_{Fast}(class="num">0)/EMA_OPEN_{Slow}(class="num">0) > class="num">1 + Threshold"; input class="type">class="kw">string SignalSell = "EMA_OPEN_{Fast}(class="num">0)/EMA_OPEN_{Slow}(class="num">0) < class="num">1 - Threshold"; input class="type">class="kw">string Variables = "Threshold=class="num">0.01"; input class="type">int Fast = class="num">10; input class="type">int Slow = class="num">21; ExpressionCompiler ecb(Variables), ecs(Variables); Promise *p1, *p2; class="type">int OnInit() { ecb.variableTable().set("Fast", Fast); ecb.variableTable().set("Slow", Slow); p1 = ecb.evaluate(SignalBuy, true); if(!ecb.success()) { Print("Syntax error in Buy signal:"); p1.print(); class="kw">return INIT_FAILED; } ecs.variableTable().set("Fast", Fast); ecs.variableTable().set("Slow", Slow); p2 = ecs.evaluate(SignalSell, true); if(!ecs.success()) { Print("Syntax error in Sell signal:"); p2.print(); class="kw">return INIT_FAILED; } class="kw">return INIT_SUCCEEDED; } class="macro">#define _Ask SymbolInfoDouble(_Symbol, SYMBOL_ASK) class="macro">#define _Bid SymbolInfoDouble(_Symbol, SYMBOL_BID) class="type">void OnTick() { if(!isNewBar()) class="kw">return; class="type">bool buy = p1.resolve(); class="type">bool sell = p2.resolve(); if(buy && sell) { buy = false; sell = false; } if(buy) { OrdersCloseAll(_Symbol, OP_SELL); if(OrdersTotalByType(_Symbol, OP_BUY) == class="num">0) { OrderSend(_Symbol, OP_BUY, Lot, _Ask, class="num">100, class="num">0, class="num">0); } } else if(sell) { OrdersCloseAll(_Symbol, OP_BUY); if(OrdersTotalByType(_Symbol, OP_SELL) == class="num">0) { OrderSend(_Symbol, OP_SELL, Lot, _Bid, class="num">100, class="num">0, class="num">0); } } else { OrdersCloseAll(); } }
◍ 一点提醒
把表达式解析塞进 MQL 程序,本质是把「写死的逻辑」换成「用户填的字符串」。前面三篇跑下来的四种解析器共用同一套语法:基础数学运算加 25 个函数,足够覆盖多数指标与价格计算场景;真要扩,就往运算符列表、内置函数(指标、成交统计)和数组语法上加。 对终端用户来说,在输入参数里改一行表达式就能换算法,比翻代码、躲编译错误轻松得多;对开发者,这套机制能把「配置」和「不可变 MQL 核心」切开,甚至演化成 MQL 之上的轻量脚本层,绕开函数库和编译器的版本纠缠。 下面这段是讨论里针对多品种篮子定价的实拍思路:用方向标志位决定每个成分取 Bid 还是 Ask,而不是让一个变量同时背两个值。外汇与贵金属波动剧烈,这类合成报价只反映数学拼接结果,实盘滑点和点差可能让回测倾向完全走形,务必在 MT5 里用真实点差跑一遍。 代码里的 evaluator 字符串直接给了 EURUSD=1.5;GBPUSD=2.5;AUDUSD=5 的测试用例,复制进你的解析器工程就能验证标志位分支是否算对。
(EURUSD^class="num">2) / (GBPUSD * AUDUSD) Value_Bid = (EURUSD_Bid * EURUSD_Bid / (GBPUSD_Ask * AUDUSD__Ask); Value_Ask = (EURUSD_Ask * EURUSD_Ask / (GBPUSD_Bid * AUDUSD__Bid); F(EURUSD, GBPUSD, AUDUSD) = (EURUSD^class="num">2) / (GBPUSD * AUDUSD); class="type">bool EURUSD_flag = (F(class="num">1, class="num">1, class="num">1) < F(class="num">2, class="num">1, class="num">1)); class="type">bool GBPUSD_flag = (F(class="num">1, class="num">1, class="num">1) < F(class="num">1, class="num">2, class="num">1)); class="type">bool AUDUSD_flag = (F(class="num">1, class="num">1, class="num">1) < F(class="num">1, class="num">1, class="num">2)); Value_Bid = F(EURUSD_flag ? EURUSD_Bid : EURUSD_Ask, GBPUSD_flag ? GBPUSD_Bid : GBPUSD_Ask, AUDUSD_flag ? AUDUSD_Bid : AUDUSD_Ask); Value_Ask = F(EURUSD_flag ? EURUSD_Ask : EURUSD_Bid, GBPUSD_flag ? GBPUSD_Ask : GBPUSD_Bid, AUDUSD_flag ? AUDUSD_Ask : AUDUSD_Bid); TestSuiteEvaluator evaluator("EURUSD=class="num">1.5;GBPUSD=class="num">2.5;AUDUSD=class="num">5"); TestSuiteEvaluator evaluator("EURUSD=class="num">1.5;GBPUSD=class="num">2.5;AUDUSD=class="num">5");