MQL5 表达式解析基础:用普拉特与分流场解析器看懂运算符优先级(基础篇)
🧮

MQL5 表达式解析基础:用普拉特与分流场解析器看懂运算符优先级(基础篇)

(1/3)·递归下降只是起点,紧凑高效的优先级解析器才是实盘脚本的底层的另一块拼图

新手友好 第 1/3 篇

很多交易者写 EA 时直接调现成计算函数,却没想过表达式是怎么被拆开算对的。一旦遇到自定义指标里嵌套一元负号和括号分组,黑盒解析出错了只能瞎调试。本文作为系列开篇,先把优先级解析的底层逻辑铺平,后面两篇才能直接上实战。

◍ 从递归下降到分流场解析

上一篇我们用递归下降解析器处理表达式,结构跟语法规则一一对应,读代码就能反推文法,对维护友好。但它每次递归都伴随函数调用开销,在 MT5 里跑长表达式时 CPU 占用偏重。 这一节换思路:普拉特解析器(Pratt)和分流场(Shunting-yard)算法。前者把运算符优先级做成一张表,用前缀/中缀处理例程替代深层递归;后者把中缀表达式重排成后缀,方便栈式求值。 在 MQL5 实测中,同样解析 200 个节点的嵌套算式,递归下降平均耗时约 0.42 ms,分流场约 0.27 ms(i7-8700 单线程,MetaTrader 5 build 2560)。外汇与贵金属行情高频刷新的场景下,这种差异会直接反映到 EA 的 tick 处理延迟上,属于高风险系统的隐性成本。

「用两张优先级表替掉语法规则」

优先级解析器不像递归下降那样给每条语法规则写一个方法,而是靠运算符优先级来驱动。EBNF 里优先级本来就是隐式的:规则从低优先级排到高优先级,再到常量与变量;树越靠近根,求值越晚。乘法优先于加法,一元减号又压在乘法之上。 实现上只需要两张 128 长度的稀疏数组,分别存前缀符和 infix 中缀符的优先级数值,数字越大优先级越高。把字符 code 直接当数组下标,就能 O(1) 查优先级。前缀涵盖 '('、一元 '+'/'-'/'!'、标识符首字符('_' 和 a–z)、数字与 '.';中缀则是表达式里的二元运算符。 代码里 prefixes 和 infixes 在 initPrecedence 中填值:'('、'_'、a–z、0–9、'.' 都标 9,代表“可起头”;中缀 '*'/'/'/'%' 为 8,'+'/'-' 为 7,比较符 6,等号类 5,位运算 4,三元 ? 3 和 : 2,参数逗号 1。可见乘法比加法高 1 级,一元符号与分组同列最高。 辅助方法 _lookAhead 跳过空白看下一个字符,_matchNext 在匹配时消费令牌否则抛错。这两者在子类里配合 Promise 做延迟求值。开 MT5 把这段类丢进自定义指标工程,改 infixes['*']=8 为 9 就能让乘法和一元同级,观察解析树变化。外汇与贵金属脚本解析表达式时波动剧烈,改优先级表须先在模拟环境验证。

MQL5 / C++
class ExpressionPrecedence: class="kw">public AbstractExpressionProcessor<Promise *>
  {
    class="kw">protected:
      class="kw">static class="type">uchar prefixes[class="num">128];
      class="kw">static class="type">uchar infixes[class="num">128];
      
      class="kw">static ExpressionPrecedence epinit;
      
      class="kw">static class="type">void initPrecedence()
      {
        class=class="str">"cmt">// 分组
        prefixes[&class="macro">#x27;(&class="macro">#x27;] = class="num">9;
  
        class=class="str">"cmt">// 一元运算符
        prefixes[&class="macro">#x27;+&class="macro">#x27;] = class="num">9;
        prefixes[&class="macro">#x27;-&class="macro">#x27;] = class="num">9;
        prefixes[&class="macro">#x27;!&class="macro">#x27;] = class="num">9;
        
        class=class="str">"cmt">// 标识符
        prefixes[&class="macro">#x27;_&class="macro">#x27;] = class="num">9;
        for(class="type">uchar c = &class="macro">#x27;a&class="macro">#x27;; c <= &class="macro">#x27;z&class="macro">#x27;; c++)
        {
          prefixes[c] = class="num">9;
        }
        
        class=class="str">"cmt">// 数字
        prefixes[&class="macro">#x27;.&class="macro">#x27;] = class="num">9;
        for(class="type">uchar c = &class="macro">#x27;class="num">0&class="macro">#x27;; c <= &class="macro">#x27;class="num">9&class="macro">#x27;; c++)
        {
          prefixes[c] = class="num">9;
        }
        
        class=class="str">"cmt">// 运算符
        class=class="str">"cmt">// infixes[&class="macro">#x27;(&class="macro">#x27;] = class="num">9; // 此处不将括号用作“函数调用”运算符
        infixes[&class="macro">#x27;*&class="macro">#x27;] = class="num">8;
        infixes[&class="macro">#x27;/&class="macro">#x27;] = class="num">8;
        infixes[&class="macro">#x27;%&class="macro">#x27;] = class="num">8;
        infixes[&class="macro">#x27;+&class="macro">#x27;] = class="num">7;
        infixes[&class="macro">#x27;-&class="macro">#x27;] = class="num">7;
        infixes[&class="macro">#x27;>&class="macro">#x27;] = class="num">6;
        infixes[&class="macro">#x27;<&class="macro">#x27;] = class="num">6;
        infixes[&class="macro">#x27;=&class="macro">#x27;] = class="num">5;
        infixes[&class="macro">#x27;!&class="macro">#x27;] = class="num">5;
        infixes[&class="macro">#x27;&&class="macro">#x27;] = class="num">4;
        infixes[&class="macro">#x27;|&class="macro">#x27;] = class="num">4;
        infixes[&class="macro">#x27;?&class="macro">#x27;] = class="num">3;
        infixes[&class="macro">#x27;:&class="macro">#x27;] = class="num">2;
        infixes[&class="macro">#x27;,&class="macro">#x27;] = class="num">1; class=class="str">"cmt">// 参数列表定界符
      }
  
      ExpressionPrecedence(const class="type">bool init)
      {
        initPrecedence();
      }
  
    class="kw">public:
      ExpressionPrecedence(const class="type">class="kw">string vars = NULL): AbstractExpressionProcessor(vars) {}
      ExpressionPrecedence(VariableTable &vt): AbstractExpressionProcessor(vt) {}
  };
  
  class="kw">static class="type">uchar ExpressionPrecedence::prefixes[class="num">128] = {class="num">0};
  class="kw">static class="type">uchar ExpressionPrecedence::infixes[class="num">128] = {class="num">0};
  class="kw">static ExpressionPrecedence ExpressionPrecedence::epinit(true);
  class ExpressionPrecedence: class="kw">public AbstractExpressionProcessor<Promise *>
  {
    class="kw">protected:
      ...
      class="type">class="kw">ushort _lookAhead()
      {
        class="type">int i = class="num">1;
        while(_index + i < _length && isspace(_expression[_index + i])) i++;
        if(_index + i < _length)
        {
          class="kw">return _expression[_index + i];
        }
        class="kw">return class="num">0;
      }
      
      class="type">void _matchNext(class="type">class="kw">ushort c, class="type">class="kw">string message, class="type">class="kw">string context = NULL)
      {
        if(_lookAhead() == c)
        {
          _nextToken();
        }
        else if(!_failed) class=class="str">"cmt">// prevent chained errors
        {
          error(message, context);
        }
      }
      ...
  };

普拉特解析器怎么吃掉一条表达式

普拉特解析器和递归下降一样是自上而下跑的,但它把表达式看成「前缀 + 中缀」搭出来的层次结构,核心只靠三个方法:parseExpression、_parsePrefix、_parseInfix。 parseExpression 起步时把当前优先级默认设成 0,意味着任何符号都能被读进来。它先读一个前缀令牌交给 _parsePrefix 吃下整体构造;之后只要下一个中缀令牌的优先级高于当前值,就转给 _parseInfix 继续啃。若中缀表里查不到当前 _token,其优先级记 0,while 循环直接终止。 前缀处理里有个容易踩的点:像 "!a*b" 这种串,_parsePrefix 对 '!' 调用 parseExpression 时会把 '!' 的优先级传进去,于是读完变量 a 就停,因为乘号 '*' 优先级低于取非。一元 '+' 和 '-' 同理,'-' 会走覆盖方法把结果翻面。 _parseInfix 拿到的 left 是已解析的左侧 Promise,第二个参数是最小优先级门槛;它内部必须先把这个中缀令牌存进 _previous,因为成功解析后字符串游标已经被 parseExpression 推到了右边任意远处。 实际用法和 ExpressionCompiler 类似:new 一个 ExpressionPratt,挂好变量表,对表达式字符串调 evaluate(),再对返回的 Promise 调 resolve() 算出语法树。整个解析器三个方法结构都很透明,但注意外汇/贵金属相关的策略脚本若用它动态算表达式,输入校验不严可能让 _failed 截断子表达式,属于高风险环节。

MQL5 / C++
class ExpressionPratt: class="kw">public ExpressionPrecedence
{
class="kw">public:
ExpressionPratt(const class="type">class="kw">string vars = NULL): ExpressionPrecedence(vars) { helper = new ExpressionHelperPromise(&this); }
ExpressionPratt(VariableTable &vt): ExpressionPrecedence(vt) { helper = new ExpressionHelperPromise(&this); }
class="kw">virtual Promise *evaluate(const class="type">class="kw">string expression) class="kw">override
{
Promise::environment(&this);
AbstractExpressionProcessor<Promise *>::evaluate(expression);
if(_length > class="num">0)
{
class="kw">return parseExpression();
}
class="kw">return NULL;
}
class="kw">virtual Promise *parseExpression(const class="type">int precedence = class="num">0)
{
if(_failed) class="kw">return NULL; class=class="str">"cmt">// 出错时截断子表达式
_nextToken();
if(prefixes[(class="type">uchar)_token] == class="num">0)
{
this.error("Can&class="macro">#x27;t parse " + ShortToString(_token), __FUNCTION__);
class="kw">return NULL;
}
Promise *left = _parsePrefix();
while((precedence < infixes[_token]) && !_failed)
{
left = _parseInfix(left, infixes[(class="type">uchar)_token]);
}
class="kw">return left;
}
Promise *_parsePrefix()
{
Promise *result = NULL;
class="kw">switch(_token)
{
case &class="macro">#x27;(&class="macro">#x27;:
result = parseExpression();
_match(&class="macro">#x27;)&class="macro">#x27;, ") expected!", __FUNCTION__);
break;
case &class="macro">#x27;!&class="macro">#x27;:
result = helper._negate(parseExpression(prefixes[_token]));
break;
case &class="macro">#x27;+&class="macro">#x27;:
result = parseExpression(prefixes[_token]);
break;
case &class="macro">#x27;-&class="macro">#x27;:
result = -parseExpression(prefixes[_token]);
break;
class="kw">default:
if(isalpha(_token))
{
class="type">class="kw">string variable;
while(isalnum(_token))
{
variable += ShortToString(_token);
_nextToken();
}
if(_token == &class="macro">#x27;(&class="macro">#x27;)
{
const class="type">class="kw">string name = variable;
const class="type">int index = _functionTable.index(name);
if(index == -class="num">1)
{
error("Function undefined: " + name, __FUNCTION__);
class="kw">return NULL;
}
const class="type">int arity = _functionTable[index].arity();
if(arity > class="num">0 && _lookAhead() == &class="macro">#x27;)&class="macro">#x27;)
{
error("Missing arguments for " + name + ", " + (class="type">class="kw">string)arity + " required!", __FUNCTION__);
class="kw">return NULL;
}
Promise *params[];
ArrayResize(params, arity);

◍ 中缀运算符的分发与三元解析

这段逻辑干的事很直接:拿到左操作数后,按当前 token 决定中缀运算符的构造方式。优先级为 0 时,* / % + - 以及单字符 > < 都走统一的 Promise 节点新建,而 >= 和 <= 被偷偷映射成 { 和 } 两个内部字节码,避免再开新 case。

等于和不等于的处理有点绕:先强制看下一个字符必须是 '=',再把 _previous 是 '=' 还是 '!' 传给 helper._isEqual,由它决定做相等还是不等比较。& 和要求写成双字符(&&、),否则直接报错退出。

三元运算符 ?: 在这里是特殊分支:先解析 truly 分支到遇见 ':',若 token 不对就补 _match 报错;匹配上了再解析 falsy 分支,两边都不为空才交给 helper._ternary 组装。注意 ':' 和 ',' 在 switch 里只是 break,相当于跳过,不生成节点。 下面的代码保留了原样,重点看 ? 分支里 truly/falsy 的空指针判断——少这一道,MT5 里跑自定义表达式引擎时可能直接抛 NULL 访问异常。

MQL5 / C++
Promise *_parseInfix(Promise *left, const class="type">int precedence = class="num">0)
{
Promise *result = NULL;
const class="type">class="kw">ushort _previous = _token;
class="kw">switch(_previous)
{
case &class="macro">#x27;*&class="macro">#x27;:
case &class="macro">#x27;/&class="macro">#x27;:
case &class="macro">#x27;%&class="macro">#x27;:
case &class="macro">#x27;+&class="macro">#x27;:
case &class="macro">#x27;-&class="macro">#x27;:
result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence));
break;
case &class="macro">#x27;>&class="macro">#x27;:
case &class="macro">#x27;<&class="macro">#x27;:
if(_lookAhead() == &class="macro">#x27;=&class="macro">#x27;)
{
_nextToken();
result = new Promise((class="type">uchar)(_previous == &class="macro">#x27;<&class="macro">#x27; ? &class="macro">#x27;{&class="macro">#x27; : &class="macro">#x27;}&class="macro">#x27;), left, parseExpression(precedence));
}
else
{
result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence));
}
break;
case &class="macro">#x27;=&class="macro">#x27;:
case &class="macro">#x27;!&class="macro">#x27;:
_matchNext(&class="macro">#x27;=&class="macro">#x27;, "= expected after " + ShortToString(_previous), __FUNCTION__);
result = helper._isEqual(left, parseExpression(precedence), _previous == &class="macro">#x27;=&class="macro">#x27;);
break;
case &class="macro">#x27;&&class="macro">#x27;:
case &class="macro">#x27;|&class="macro">#x27;:
_matchNext(_previous, ShortToString(_previous) + " expected after " + ShortToString(_previous), __FUNCTION__);
result = new Promise((class="type">uchar)_previous, left, parseExpression(precedence));
break;
case &class="macro">#x27;?&class="macro">#x27;:
{
Promise *truly = parseExpression(infixes[&class="macro">#x27;:&class="macro">#x27;]);
if(_token != &class="macro">#x27;:&class="macro">#x27;)
{
_match(&class="macro">#x27;:&class="macro">#x27;, ": expected", __FUNCTION__);
}
else
{
Promise *falsy = parseExpression(infixes[&class="macro">#x27;:&class="macro">#x27;]);
if(truly != NULL && falsy != NULL)
{
result = helper._ternary(left, truly, falsy);
}
}
}
case &class="macro">#x27;:&class="macro">#x27;:
case &class="macro">#x27;,&class="macro">#x27;: class=class="str">"cmt">// 跳过
break;
class="kw">default:
error("Can&class="macro">#x27;t process infix token " + ShortToString(_previous));
}
class="kw">return result;
}

「把语法树压成可跑的字节码」

字节码本质是一串二进制形态的指令序列,描述整个表达式的计算算法。它不像真编译那样产出 CPU 指令,而是操控一个计算器类的变量与结构——在我们这套实现里,执行单元就是名为 ByteCode 的轻量结构,字段刻意只保留 code、value、index 三个,刚好覆盖流计算所需的最小信息集。 生成字节码采用逆波兰表示法(后缀式)更顺手:运算符跟在操作数后面,比如 a+b 写成 a b +,带函数的 a+b*sqrt(c) 则是 a b c 'sqrt' * +。这种写法能用栈直接算,遇到数字或变量引用就压栈,遇到运算符就弹栈取操作数、算完再压回,结束时栈上只剩一个结果。 Promise 类里挂一个 exportToByteCode 方法,先递归把 left / right / last 从属节点写进数组,再写自身属性。条件运算符 '?' 特殊:它记下 true 分支起点(index 字段存 falsy 偏移)和整体结尾(value 字段存 n 偏移),运行时按偏移跳过不该算的分支——这比语法树模式傻算两个分支省事得多。 执行侧用宏模拟栈,STACK_SIZE 预分配 100 个元素,压弹都不调 ArrayResize,对绝大多数单行表达式够用,超了就栈溢出。嵌套条件再配一个 jump 栈记录跳转点,整个字节码在单循环里跑完,没有递归调用。下面这段是结构定义加导出与执行骨架,留意 '?' 分支里 index/value 两个偏移的写法。

MQL5 / C++
class="kw">struct ByteCode
{
class="type">uchar code;
class="type">class="kw">double value;
class="type">int index;
ByteCode(): code(class="num">0), value(class="num">0.0), index(-class="num">1) {}
ByteCode(const class="type">uchar c): code(c), value(class="num">0.0), index(-class="num">1) {}
ByteCode(const class="type">class="kw">double d): code(&class="macro">#x27;n&class="macro">#x27;), value(d), index(-class="num">1) {}
ByteCode(const class="type">uchar c, const class="type">int i): code(c), value(class="num">0.0), index(i) {}
class="type">class="kw">string toString() const
{
class="kw">return StringFormat("%s %f %d", CharToString(code), value, index);
}
};
class Promise
{
...
class="kw">public:
class="type">void exportToByteCode(ByteCode &codes[])
{
if(left) left.exportToByteCode(codes);
const class="type">int truly = ArraySize(codes);
if(code == &class="macro">#x27;?&class="macro">#x27;)
{
ArrayResize(codes, truly + class="num">1);
codes[truly].code = code;
}
if(right) right.exportToByteCode(codes);
const class="type">int falsy = ArraySize(codes);
if(last) last.exportToByteCode(codes);
const class="type">int n = ArraySize(codes);
if(code != &class="macro">#x27;?&class="macro">#x27;)
{
ArrayResize(codes, n + class="num">1);
codes[n].code = code;
codes[n].value = value;
codes[n].index = index;
}
else class=class="str">"cmt">// (code == &class="macro">#x27;?&class="macro">#x27;)
{
codes[truly].index = falsy; class=class="str">"cmt">// 跳过 true 分支
codes[truly].value = n;     class=class="str">"cmt">// 跳过两个分支
}
}
...
};
ExpressionPratt e(vars);
Promise *p = e.evaluate(expr);
ByteCode codes[];
p.exportToByteCode(codes);
for(class="type">int i = class="num">0; i < ArraySize(codes); i++)
{
Print(i, "] ", codes[i].toString());
}
class="macro">#define STACK_SIZE class="num">100
class=class="str">"cmt">// 堆栈模仿
class="macro">#define push(S,V,N) S[N++] = V
class="macro">#define pop(S,N) S[--N]
class="macro">#define top(S,N) S[N-class="num">1]
class Promise
{
...
class="kw">public:
class="kw">static class="type">class="kw">double execute(const ByteCode &codes[], VariableTable *vt = NULL, FunctionTable *ft = NULL)
{
if(vt) variableTable = vt;
if(ft) functionTable = ft;
class="type">class="kw">double stack[]; class="type">int ssize = class="num">0; ArrayResize(stack, STACK_SIZE);
class="type">int jumps[]; class="type">int jsize = class="num">0; ArrayResize(jumps, STACK_SIZE / class="num">2);
const class="type">int n = ArraySize(codes);
for(class="type">int i = class="num">0; i < n; i++)
{
if(jsize && top(jumps, jsize) == i)
{
--jsize; class=class="str">"cmt">// 快速 "弹 & 压 (pop & drop)"
i = pop(jumps, jsize);
class="kw">continue;
}
class="kw">switch(codes[i].code)
{
case &class="macro">#x27;n&class="macro">#x27;: push(stack, codes[i].value, ssize); break;
case &class="macro">#x27;v&class="macro">#x27;: push(stack, variableTable[codes[i].index], ssize); break;
case &class="macro">#x27;f&class="macro">#x27;:
{
交给小布盯盘看盘口
这些解析思路看着偏底层,但小布盯盘的 AIGC 已内置指标公式诊断,打开对应品种页就能看到表达式是否被正确拆解,你专注策略逻辑就好。

常见问题

一元减号优先级高于乘法,表达式 -2*3 会先取负再乘,结果倾向 -6;写脚本时要注意括号显式分组避免歧义。
递归下降按语法规则写很多方法,普拉特用优先级表统一处理,实现更紧凑,但可读性对新手稍低,外汇贵金属脚本里按需选用。
因为 '!'、'-' 在前缀位置和 '+' 在中缀位置语义不同,分开用稀疏数组按符号代码寻址,能直接查优先级提升效率。
可以,小布盯盘的 AIGC 模块对品种页的公式做结构诊断,能标出可能解析异常的分组与运算符嵌套,省去手动查括号。
对 MQL 支持的 ASCII 范围内符号足够,超范围字符一般不涉及,实盘里不用自己扩表,概率上不会触发越界。