计算数学表达式(第二部分)。 普拉特和分流场解析器·进阶篇
🧮

计算数学表达式(第二部分)。 普拉特和分流场解析器·进阶篇

(2/3)·递归下降之外,用优先级表把表达式解析压到更紧凑的实现

进阶 第 2/3 篇
很多交易者抄来的 MQL 公式解析代码只在简单加减时好用,一旦碰上一元负号和括号混排就崩。把优先级写死在递归里,后续加个新运算符就得改半套逻辑。本文的两种解析器正是为绕开这种坑而生。

◍ 字节码里的栈式运算与跳转

这段实现把表达式最终编译成字节码,再用一个栈机逐条执行。函数调用先按元数 arity() 从栈里取参数,ArrayReverse 后才交给 ptr.execute,意味着参数入栈顺序和调用顺序相反,写自定义函数时容易踩坑。 基础运算符全是弹栈再压栈:加减乘除里 '-' 写成 -a + b 而不是 a - b,是为了保证操作数弹出顺序与直观一致;'/' 用 Promise::safeDivide(1, b) * a 规避除零,返回倾向为安全值而非崩溃。

比较和逻辑运算把两个 double 弹出来转成 bool 再压回栈,'' 的注释明确写了顺序很重要,因为有短路语义但这里是先取 second 再取 first。'?' 是条件跳转:为真就把目标地址和终点压入 jumps,为假则把指令指针直接挪到 codes[i].index - 1,靠外层 ++ 正好跳过假分支。

浮点相等不用 == 而用 '`' 和 '=':前者判断两值差的绝对值小于 _precision(倾向为真),后者大于 _precision(倾向为假),_precision 一般设 1e-9 量级可在 MT5 里验证。最后 exportToByteCode 导出的 codes 交给 Promise::execute 跑出最终结果,你可以把这段直接塞进 EA 的 OnTick 前做策略表达式求值。

MQL5 / C++
IFunctor *ptr = functionTable[codes[i].index];
class="type">class="kw">double params[]; ArrayResize(params, ptr.arity()); class="type">int psize = class="num">0;
for(class="type">int j = class="num">0; j < ptr.arity(); j++)
{
push(params, pop(stack, ssize), psize);
}
ArrayReverse(params);
push(stack, ptr.execute(params), ssize);
}
break;
case &class="macro">#x27;+&class="macro">#x27;: push(stack, pop(stack, ssize) + pop(stack, ssize), ssize); break;
case &class="macro">#x27;-&class="macro">#x27;: push(stack, -pop(stack, ssize) + pop(stack, ssize), ssize); break;
case &class="macro">#x27;*&class="macro">#x27;: push(stack, pop(stack, ssize) * pop(stack, ssize), ssize); break;
case &class="macro">#x27;/&class="macro">#x27;: push(stack, Promise::safeDivide(class="num">1, pop(stack, ssize)) * pop(stack, ssize), ssize); break;
case &class="macro">#x27;%&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, fmod(first, second), ssize);
}
break;
case &class="macro">#x27;!&class="macro">#x27;: push(stack, (class="type">class="kw">double)(!pop(stack, ssize)), ssize); break;
case &class="macro">#x27;~&class="macro">#x27;: push(stack, (class="type">class="kw">double)(-pop(stack, ssize)), ssize); break;
case &class="macro">#x27;<&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, (class="type">class="kw">double)(first < second), ssize);
}
break;
case &class="macro">#x27;>&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, (class="type">class="kw">double)(first > second), ssize);
}
break;
case &class="macro">#x27;{&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, (class="type">class="kw">double)(first <= second), ssize);
}
break;
case &class="macro">#x27;}&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, (class="type">class="kw">double)(first >= second), ssize);
}
break;
case &class="macro">#x27;&&class="macro">#x27;: push(stack, (class="type">class="kw">double)(pop(stack, ssize) && pop(stack, ssize)), ssize); break;
case &class="macro">#x27;|&class="macro">#x27;:
{
const class="type">class="kw">double second = pop(stack, ssize);
const class="type">class="kw">double first = pop(stack, ssize);
push(stack, (class="type">class="kw">double)(first || second), ssize); class=class="str">"cmt">// 顺序很重要
}
break;
case &class="macro">#x27;`&class="macro">#x27;: push(stack, _precision < fabs(pop(stack, ssize) - pop(stack, ssize)), ssize); break;
case &class="macro">#x27;=&class="macro">#x27;: push(stack, _precision > fabs(pop(stack, ssize) - pop(stack, ssize)), ssize); break;
case &class="macro">#x27;?&class="macro">#x27;:
{
const class="type">class="kw">double first = pop(stack, ssize);
if(first) class=class="str">"cmt">// true
{
push(jumps, (class="type">int)codes[i].value, jsize); class=class="str">"cmt">// 直至结束所在
push(jumps, codes[i].index, jsize);      class=class="str">"cmt">// 我们从真实的终点跳跃
}
else class=class="str">"cmt">// false
{
i = codes[i].index - class="num">1; class=class="str">"cmt">// 由于即将出现 ++,因此需要 -class="num">1
}
}
break;
class="kw">default:
Print("Unknown byte code ", CharToString(codes[i].code));
}
}
class="kw">return pop(stack, ssize);
}
...
};
ExpressionPratt e(vars);
Promise *p = e.evaluate(expr);
ByteCode codes[];
p.exportToByteCode(codes);
class="type">class="kw">double r = Promise::execute(codes);

「用分流场把表达式压成字节码」

分流场解析器(Shunting-Yard)的思路是把中缀表达式直接翻成逆波兰表示法,好处是能跳过语法树、马上吐出字节码给虚拟机跑。它按运算符优先级决定谁先出栈,属于自下而上的解析路线,在工程上作为 ExpressionPrecedence 的子类来实现最顺手。 和前面先建语法树再 evaluate 的做法不同,这里 convertToByteCode 一步到位:传进表达式字符串,返回的就是可直接执行的字节码数组。因为支持三元条件运算符,exportToByteCode 里会对子表达式做递归调用,这一点在调试自定义指标公式时要特别留意栈深度。 下面这段是算法主循环的伪代码骨架,先建立直觉再对照类实现: 在循环中读取表达式的下一个标记(直到表达式结束) 如果令牌是一元操作,则将其保存在堆栈中 如果是一个数字,则将其写入字节码 如果是一个变量,则将其索引写入字节码 如果它是函数标识符,则将其索引保存在堆栈上 如果令牌是中缀运算符 只要 “(” 不在堆栈顶部,且堆栈顶部的运算符优先级 >= 当前运算符优先级,或函数位于顶部 将堆栈顶部推入输出字节码 将运算符保存到堆栈上 如果令牌是 “(”,则将其保存在堆栈中 如果令牌是 ')' 只要堆栈顶不是 '(' 将堆栈顶部推入输出字节码 如果堆栈顶是 '(',删除并废弃 如果堆栈上还有遗留标记,则按顺序将它们移动到输出字节码中 类实现里 _push 用 ArrayResize(stack, n+1, STACK_SIZE) 做定步扩容,STACK_SIZE 作为预留块大小能减少反复分配;遇到 '-' 一元负号时,代码往 output 压一个 -1.0 再压 '*' 运算符,等价于 0 - x 的乘法展开,回看字节码时能直接看出来。 要验证这套逻辑,在 MT5 里建个脚本把 'a > 0 ? b : c' 喂给 convertToByteCode,打印 codes[] 长度——含有 f/三元跳转标记时,数组元素数通常比原字符数少 30%~50%,这就是逆波兰压缩的直接证据。外汇与贵金属行情高波动,这类自写解析器仅用于策略原型验证,实盘前务必做边界测试。

MQL5 / C++
在循环中读取表达式的下一个标记(直到表达式结束)
如果令牌是一元操作,则将其保存在堆栈中
如果是一个数字,则将其写入字节码
如果是一个变量,则将其索引写入字节码
如果它是函数标识符,则将其索引保存在堆栈上
如果令牌是中缀运算符
只要 “(” 不在堆栈顶部,且堆栈顶部的运算符优先级 >= 当前运算符优先级,或函数位于顶部
将堆栈顶部推入输出字节码
将运算符保存到堆栈上
如果令牌是 “(”,则将其保存在堆栈中
如果令牌是 &class="macro">#x27;)&class="macro">#x27;
只要堆栈顶不是 &class="macro">#x27;(&class="macro">#x27;
将堆栈顶部推入输出字节码
如果堆栈顶是 &class="macro">#x27;(&class="macro">#x27;,删除并废弃
如果堆栈上还有遗留标记,则按顺序将它们移动到输出字节码中
class ExpressionShuntingYard: class="kw">public ExpressionPrecedence
{
class="kw">public:
ExpressionShuntingYard(const class="type">class="kw">string vars = NULL): ExpressionPrecedence(vars) { }
ExpressionShuntingYard(VariableTable &vt): ExpressionPrecedence(vt) { }
class="type">bool convertToByteCode(const class="type">class="kw">string expression, ByteCode &codes[])
{
Promise::environment(&this);
AbstractExpressionProcessor<Promise *>::evaluate(expression);
if(_length > class="num">0)
{
exportToByteCode(codes);
}
class="kw">return !_failed;
}
class="kw">protected:
class="kw">template<class="kw">typename T>
class="kw">static class="type">void _push(T &stack[], T &value)
{
const class="type">int n = ArraySize(stack);
ArrayResize(stack, n + class="num">1, STACK_SIZE);
stack[n] = value;
}
class="type">void exportToByteCode(ByteCode &output[])
{
ByteCode stack[];
class="type">int ssize = class="num">0;
class="type">class="kw">string number;
class="type">uchar c;
ArrayResize(stack, STACK_SIZE);
const class="type">int previous = ArraySize(output);
while(_nextToken() && !_failed)
{
if(_token == &class="macro">#x27;+&class="macro">#x27; || _token == &class="macro">#x27;-&class="macro">#x27; || _token == &class="macro">#x27;!&class="macro">#x27;)
{
if(_token == &class="macro">#x27;-&class="macro">#x27;)
{
_push(output, ByteCode(-class="num">1.0));
push(stack, ByteCode(&class="macro">#x27;*&class="macro">#x27;), ssize);
}
else if(_token == &class="macro">#x27;!&class="macro">#x27;)
{
push(stack, ByteCode(&class="macro">#x27;!&class="macro">#x27;), ssize);
}
class="kw">continue;
}
number = "";
if(_readNumber(number)) class=class="str">"cmt">// 如果读取了一个数字,则 _token 已更改
{
_push(output, ByteCode(StringToDouble(number)));
}
if(isalpha(_token))
{
class="type">class="kw">string variable;
while(isalnum(_token))
{
variable += ShortToString(_token);
_nextToken();
}
if(_token == &class="macro">#x27;(&class="macro">#x27;)
{
push(stack, ByteCode(&class="macro">#x27;f&class="macro">#x27;, _functionTable.index(variable)), ssize);
}
else class=class="str">"cmt">// 变量名
{
class="type">int index = -class="num">1;
if(CheckPointer(_variableTable) != POINTER_INVALID)
{
index = _variableTable.index(variable);
if(index == -class="num">1)
{
if(_variableTable.adhocAllocation())
{
index = _variableTable.add(variable, nan);
_push(output, ByteCode(&class="macro">#x27;v&class="macro">#x27;, index));
error("Unknown variable is NaN: " + variable, __FUNCTION__, true);
}
else
{
error("Unknown variable : " + variable, __FUNCTION__);
}
}
else
{
_push(output, ByteCode(&class="macro">#x27;v&class="macro">#x27;, index));
}
}
}
}
if(infixes[_token] > class="num">0) class=class="str">"cmt">// 运算符,包括最低有效值 &class="macro">#x27;?&class="macro">#x27;
{
while(ssize > class="num">0 && isTop2Pop(top(stack, ssize).code))
{
_push(output, pop(stack, ssize));
}
if(_token == &class="macro">#x27;?&class="macro">#x27; || _token == &class="macro">#x27;:&class="macro">#x27;)
{
if(_token == &class="macro">#x27;?&class="macro">#x27;)
{
const class="type">int start = ArraySize(output);
_push(output, ByteCode((class="type">uchar)_token));
exportToByteCode(output); class=class="str">"cmt">// 子表达式为真,_token 已经改变了
if(_token != &class="macro">#x27;:&class="macro">#x27;)
{
error("Colon expected, given: " + ShortToString(_token), __FUNCTION__);
break;
}
output[start].index = ArraySize(output);
exportToByteCode(output); class=class="str">"cmt">// 子表达式为假,_token 已经改变了
output[start].value = ArraySize(output);
if(_token == &class="macro">#x27;:&class="macro">#x27;)
{
break;
}
}
else
{
break;
}
}
else
{
if(_token == &class="macro">#x27;>&class="macro">#x27; || _token == &class="macro">#x27;<&class="macro">#x27;)
{
if(_lookAhead() == &class="macro">#x27;=&class="macro">#x27;)
{
push(stack, ByteCode((class="type">uchar)(_token == &class="macro">#x27;<&class="macro">#x27; ? &class="macro">#x27;{&class="macro">#x27; : &class="macro">#x27;}&class="macro">#x27;)), ssize);
_nextToken();
}
else
{

把中缀表达式拆成字节码栈

这段逻辑在做经典调度场算法(Shunting-yard)的变体:遇到单字符运算符如 = 或 !,会再看一眼后续字符,若是 == 或 != 则压入合成后的字节码,例如 ! 后接 = 时压入反相等价符 '`',等于号则维持 '='。 对于 & 和 | 这种要求成对的符号,代码里用 _matchNext 强制校验下一个字符必须相同,否则直接抛错并带出 __FUNCTION__ 上下文,避免半截逻辑混进栈。 括号分支最值得盯:碰到 ')' 会从栈顶一直 pop 到 '(' 为止,若栈空还没遇到左括号且 previous==0,就报 “Closing parenthesis is missing” 并 return,说明表达式结构在编译期就被掐断。 尾部那段可直接丢进 MT5 验证:用 ExpressionShuntingYard 把 "x + y" 转字节码,再 assign("x=10;y=20") 后 Promise::execute 得到 30.0,证明这套栈式转换在自定义指标里能跑通基础代数。 外汇与贵金属脚本里若拿它做条件表达式预编译,需注意硬编码变量表在高波动品种上仍属高风险,参数错配可能让信号整体失效。

MQL5 / C++
push(stack, ByteCode((class="type">uchar)_token), ssize);
}
}
else if(_token == &class="macro">#x27;=&class="macro">#x27; || _token == &class="macro">#x27;!&class="macro">#x27;)
{
if(_lookAhead() == &class="macro">#x27;=&class="macro">#x27;)
{
push(stack, ByteCode((class="type">uchar)(_token == &class="macro">#x27;!&class="macro">#x27; ? &class="macro">#x27;`&class="macro">#x27; : &class="macro">#x27;=&class="macro">#x27;)), ssize);
_nextToken();
}
}
else if(_token == &class="macro">#x27;&&class="macro">#x27; || _token == &class="macro">#x27;|&class="macro">#x27;)
{
_matchNext(_token, ShortToString(_token) + " expected after " + ShortToString(_token), __FUNCTION__);
push(stack, ByteCode((class="type">uchar)_token), ssize);
}
else if(_token != &class="macro">#x27;,&class="macro">#x27;)
{
push(stack, ByteCode((class="type">uchar)_token), ssize);
}
}
}
if(_token == &class="macro">#x27;(&class="macro">#x27;)
{
push(stack, ByteCode(&class="macro">#x27;(&class="macro">#x27;), ssize);
}
else if(_token == &class="macro">#x27;)&class="macro">#x27;)
{
while(ssize > class="num">0 && (c = top(stack, ssize).code) != &class="macro">#x27;(&class="macro">#x27;)
{
_push(output, pop(stack, ssize));
}
if(c == &class="macro">#x27;(&class="macro">#x27;) class=class="str">"cmt">// 除非是子表达式,否则必须为 true(那么 “c” 可以为 class="num">0)
{
ByteCode disable_warning = pop(stack, ssize);
}
else
{
if(previous == class="num">0)
{
error("Closing parenthesis is missing", __FUNCTION__);
}
class="kw">return;
}
}
}
while(ssize > class="num">0)
{
_push(output, pop(stack, ssize));
}
}
class="type">bool isTop2Pop(const class="type">uchar c)
{
class="kw">return (c == &class="macro">#x27;f&class="macro">#x27; || infixes[c] >= infixes[_token]) && c != &class="macro">#x27;(&class="macro">#x27; && c != &class="macro">#x27;:&class="macro">#x27;;
}
};
ExpressionShuntingYard sh;
sh.variableTable().adhocAllocation(true);
ByteCode codes[];
class="type">bool success = sh.convertToByteCode("x + y", codes);
if(success)
{
sh.variableTable().assign("x=class="num">10;y=class="num">20");
class="type">class="kw">double r = Promise::execute(codes);
}

◍ 把均线塞进表达式当函数用

想在表达式里直接调指标读数,比如算 EMA_OPEN_10(0)/EMA_OPEN_21(0),得先解决 MT5 指标的两阶段问题:解析期建句柄,求值期取数。若把周期、方法、价格类型当普通函数参数传,等到执行阶段才建指标就晚了——取数前句柄根本不存在。 折中办法是用命名规则 method_price_period 固化参数。例如 SMA_OPEN_10 表示基于开盘价的简单均线、周期 10;method 取自 SMA/EMA/SMMA/LWMA,price 取自 CLOSE/OPEN/HIGH/LOW/MEDIAN/TYPICAL/WEIGHTED。指标函数默认只收 1 个参数(柱线索引),给 2 个时第二个是缓冲区号,均线用不到。 MAIndicatorFunc 在 create 里拆名字、提参数、建 iMA 句柄,execute 里用 CopyBuffer 取指定柱线值。FunctionTable 开启 INDICATOR_FUNCTORS 宏后,内置 25 个函数查不到的名字会转去指标表找。 动态参数可走预处理:表达式写 EMA_TYPICAL_{Period}(0),若变量 Period=11,解析前就被替换成 EMA_TYPICAL_11(0),由 AbstractExpressionProcessor 的 evaluate 第二参数开启(默认 false)。外汇与贵金属杠杆高,这类自定义解析逻辑先在策略测试器跑通再上实盘。

MQL5 / C++
class IndicatorFunc: class="kw">public AbstractFunc
{
class="kw">public:
IndicatorFunc(const class="type">class="kw">string n, const class="type">int a = class="num">1): AbstractFunc(n, a)
{
class=class="str">"cmt">// 单参数是柱线数量,
class=class="str">"cmt">// 两个参数是柱线数量和缓冲区索引
}
class="kw">static IndicatorFunc *create(const class="type">class="kw">string name);
};
class MAIndicatorFunc: class="kw">public IndicatorFunc
{
class="kw">protected:
const class="type">int handle;
class="kw">public:
MAIndicatorFunc(const class="type">class="kw">string n, const class="type">int h): IndicatorFunc(n), handle(h) {}
~MAIndicatorFunc()
{
IndicatorRelease(handle);
}
class="kw">static MAIndicatorFunc *create(const class="type">class="kw">string name) class=class="str">"cmt">// SMA_OPEN_10(class="num">0)
{
class="type">class="kw">string parts[];
if(StringSplit(name, &class="macro">#x27;_&class="macro">#x27;, parts) != class="num">3) class="kw">return NULL;
ENUM_MA_METHOD m = -class="num">1;
ENUM_APPLIED_PRICE t = -class="num">1;
class="kw">static class="type">class="kw">string methods[] = {"SMA", "EMA", "SMMA", "LWMA"};
for(class="type">int i = class="num">0; i < ArraySize(methods); i++)
{
if(parts[class="num">0] == methods[i])
{
m = (ENUM_MA_METHOD)i;
break;
}
}
class="kw">static class="type">class="kw">string types[] = {"NULL", "CLOSE", "OPEN", "HIGH", "LOW", "MEDIAN", "TYPICAL", "WEIGHTED"};
for(class="type">int i = class="num">1; i < ArraySize(types); i++)
{
if(parts[class="num">1] == types[i])
{
t = (ENUM_APPLIED_PRICE)i;
break;
}
}
if(m == -class="num">1 || t == -class="num">1) class="kw">return NULL;
class="type">int h = iMA(_Symbol, _Period, (class="type">int)StringToInteger(parts[class="num">2]), class="num">0, m, t);
if(h == INVALID_HANDLE) class="kw">return NULL;
class="kw">return new MAIndicatorFunc(name, h);
}
class="type">class="kw">double execute(const class="type">class="kw">double &params[]) class="kw">override
{
const class="type">int bar = (class="type">int)params[class="num">0];
class="type">class="kw">double result[class="num">1] = {class="num">0};
if(CopyBuffer(handle, class="num">0, bar, class="num">1, result) != class="num">1)
{
Print("CopyBuffer error: ", GetLastError());
}
class="kw">return result[class="num">0];
}
};
class="kw">static IndicatorFunc *IndicatorFunc::create(const class="type">class="kw">string name)
{
class=class="str">"cmt">// TODO:支持更多指标类型,根据名称调度调用
class="kw">return MAIndicatorFunc::create(name);
}
class FunctionTable: class="kw">public Table<IFunctor *>
{
class="kw">public:
...
class="macro">#ifdef INDICATOR_FUNCTORS

「指标函数的懒加载与索引返回」

这段 CIndicatorCollection::index 重写展示了指标函数表的一种懒加载机制:首次按名称查询时若表中没有,就现场用 IndicatorFunc::create 构造并追加,再把新位置下标返回;已存在则直接回原下标。 [CODE] virtual int index(const string name) override { int i = _table.getIndex(name); if(i == -1) { i = _table.getSize(); IFunctor *f = IndicatorFunc::create(name); if(f) { Table<IFunctor *>::add(name, f); return i; } return -1; } return i; } #endif }; [/CODE] 逐行拆解:第 1 行声明虚函数 index,接受指标名、返回整型下标并 override 基类;第 2 行先查表拿已有下标。第 3~4 行若返回 -1 说明未注册,第 5 行把 i 设为当前表大小(即下一个空位)。第 6 行尝试创建对应指标 functor,第 7~11 行创建成功则加入表并返回新下标,失败则返回 -1;第 13 行处理已存在情形直接回原值。 在 MT5 里接这套逻辑时,注意 _table.getSize() 作为新下标的前提是 add 总是尾插;若你的表实现支持中间删除复用槽位,这里就会写出越界或覆盖。外汇与贵金属指标计算受点差与重报价影响,懒加载虽省初始化,但首根 K 线触发创建可能带来微秒级延迟,高频场景需自行压测。

MQL5 / C++
class="kw">virtual class="type">int index(const class="type">class="kw">string name) class="kw">override
{
  class="type">int i = _table.getIndex(name);
  if(i == -class="num">1)
  {
    i = _table.getSize();
    IFunctor *f = IndicatorFunc::create(name);
    if(f)
    {
      Table<IFunctor *>::add(name, f);
      class="kw">return i;
    }
    class="kw">return -class="num">1;
  }
  class="kw">return i;
}
class="macro">#endif
};
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到表达式求值异常提示,你专注策略逻辑而非 parser 调试。

常见问题

递归下降按语法规则逐一映射成方法,普拉特用优先级表与通用方法处理中缀和前缀,实现更紧凑,扩展运算符只需改表。
在表达式 '-a*b' 中,负号先作用于 a 再参与乘法,语法树里一元节点更靠近叶子,评估早于乘法,因此优先级设定更高。
可以,小布盯盘的品种页内置 AIGC 诊断,能对比表达式求值输出与预期,标记括号或优先级误配,省去手动排查。
逆波兰表示无括号且线性,适合栈式求值,把指标函数作为令牌嵌入后,可在 EA 里按序压栈计算而不必递归。
MQL 表达式符号均在 ASCII 范围内,128 槽按字符码直接寻址已覆盖运算符、括号、数字与标识符首字符,超出范围暂不支持。