计算数学表达式(第二部分)。 普拉特和分流场解析器·综合运用
📘

计算数学表达式(第二部分)。 普拉特和分流场解析器·综合运用

第 3/3 篇

「跑通解析器测试脚本看速度差异」

ExpresSParserS.mq5 这套测试脚本干三件事:功能校验、四种解析器算速测量、语法树与字节码演示,还顺带把指标当内置函数用了一遍。 功能测试里既有正常表达式,也有故意写错的(未声明变量、零除等)。判断对不对不看是否报错,而看实际结果和预期是否一致——所以日志里 19 个测试全过,其中 2 个是预期内的错误,这也算“正确”。 速度只在一个反复计算的循环里测,解释器模式每次算完立刻重解表达式,另外三种解析器把解析放在循环外。四种方法首次解析耗时差不多,但预编译后算速快好几倍。 上面日志里 10000 次循环实测:解释执行 104572 微秒,编译式 25011,Pratt 解析 24967,分流场 23147,而 Pratt 字节码仅 23738。字节码路线最快,获取字节码用 Pratt 还是分流场区别很小。外汇与贵金属脚本测算属高风险环境,实盘前请在策略测试器充分验证。 选哪种看你顺手程度和任务:要语法扩展灵活就挑能改树的,要嵌进现有程序就挑接口近的,别光看那点微秒差。

MQL5 / C++
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) &gt; (a != class="num">1.5) = class="num">0.0; expected = class="num">0.0
  class="num">6 passed, ok: (b == c) &gt;= (a != class="num">1.5) = class="num">1.0; expected = class="num">1.0
  class="num">7 passed, ok: (a &gt; 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
  &gt;&gt;&gt; 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 时平买开卖,否则全平。

MQL5 / C++
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 的测试用例,复制进你的解析器工程就能验证标志位分支是否算对。

MQL5 / C++
(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");

常见问题

写个循环批量跑几百条随机表达式并打点计时,对比不同解析器耗时可直观看出速度差异,重点看复杂嵌套表达式的耗时。
在EA的OnTick里调用解析器算表达式值,将结果映射成多空条件;注意给非法表达式留兜底返回,避免崩单。
小布可替你批量跑解析测试脚本并标出慢点,也能按你给的表达式直接产出EA信号代码片段,省去手搓框架。
混合使用要在词法切分处统一接口,否则优先级会错乱;建议先单测各自再封一层适配,外汇贵金属波动大更需严谨。
会,表达式求值偏差可能触发错误信号;上线前用历史 tick 回放验证,且永远加风控开关,外汇贵金属属高风险。