计算数学表达式(第二部分)。 普拉特和分流场解析器·进阶篇
(2/3)·递归下降之外,用优先级表把表达式解析压到更紧凑的实现
◍ 字节码里的栈式运算与跳转
这段实现把表达式最终编译成字节码,再用一个栈机逐条执行。函数调用先按元数 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 前做策略表达式求值。
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%,这就是逆波兰压缩的直接证据。外汇与贵金属行情高波动,这类自写解析器仅用于策略原型验证,实盘前务必做边界测试。
在循环中读取表达式的下一个标记(直到表达式结束) 如果令牌是一元操作,则将其保存在堆栈中 如果是一个数字,则将其写入字节码 如果是一个变量,则将其索引写入字节码 如果它是函数标识符,则将其索引保存在堆栈上 如果令牌是中缀运算符 只要 “(” 不在堆栈顶部,且堆栈顶部的运算符优先级 >= 当前运算符优先级,或函数位于顶部 将堆栈顶部推入输出字节码 将运算符保存到堆栈上 如果令牌是 “(”,则将其保存在堆栈中 如果令牌是 &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,证明这套栈式转换在自定义指标里能跑通基础代数。 外汇与贵金属脚本里若拿它做条件表达式预编译,需注意硬编码变量表在高波动品种上仍属高风险,参数错配可能让信号整体失效。
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)。外汇与贵金属杠杆高,这类自定义解析逻辑先在策略测试器跑通再上实盘。
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 ¶ms[]) 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 线触发创建可能带来微秒级延迟,高频场景需自行压测。
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 };