计算数学表达式(第一部分)。 递归下降解析器(基础篇)
让 EA 在运行时吃进自定义公式
把交易逻辑写死在 MQL5 里,调参时只能改外部变量,遇到要换目标函数或快速验证指标原型就很笨。更灵活的做法是让程序在运行阶段直接读一段文本公式,现场算结果,不重新编译。 这类需求靠解析器解决:它把字符串形式的数学表达式即时解释,先建成语法树,再落成一段计算指令(类似字节码),最后跑出数值。外汇和贵金属波动剧烈、杠杆高风险大,这种动态计算只解决“算得对”,不等于信号更稳。 本文系列要拆几种解析器与求值路径,本篇先落地的思路是递归下降解析器——结构清晰、易嵌进 MT5,适合先开个脚本验证。
「算术解析器里的运算符与关联性陷阱」
| 在 MT5 里自己写表达式解析器时,先把规则钉死:单行算术表达式只由数字、已命名变量和运算符构成,变量值从外部通过解析器属性注入,表达式内部没有赋值号 '=' 来存中间结果。支持的二元运算符按优先级排下来是 !、-、+(一元),() 分组,*、/、%,+、-,>、<、>=、<=,==、!=,&&、 | (这两者优先级相同,必须靠括号显式分组),以及 ?: 三元分支。 |
|---|
标准数学函数直接复用 MQL 的 25 个,其中 pow 负责幂运算,所以运算符表不收 '^'。这点很关键:' ^' 若做成运算符只支持整数幂,而 pow 无此限制。另外 '^' 在常规运算符里是右关联,其余二元运算符全是左关联——左关联意味着相同优先级从左往右算,例如 1 - 2 - 3 等价于 ((1 - 2) - 3) 得 -4;若错当成右关联就会变成 (1 - (2 - 3)) 得 2。 右关联会显著加大递归下降解析的算法复杂度。为省事,本解析器直接弃用指数运算符、改调 pow,于是只需实现左关联;一元运算符固定右关联,统一处理即可。所有数字按实数对待,相等比较走公差值,逻辑值遵循零为假、非零为真。不支持位运算和数组。 变量名走 MQL 标识符规则:字母数字下划线、不开头于数字、不能与内置函数重名。输入串逐字符扫描,字符分类、报错、变量表、函数表这些脏活放在基类,具体解析器继承它。下面这段基类雏形可直接拷进 MT5 看结构。
◍ 表达式解析的基类骨架怎么搭
做 MT5 自定义指标或 EA 时,若想让用户输入字符串公式(比如 "MA(close,20)+ATR(14)*0.5"),第一步是有一个能容纳标量结果和语法树的解析基类。模板类 AbstractExpressionProcessor<T> 就是干这个的:它不绑定具体输出类型,子类决定 evaluate 返回 double 还是语法节点树。 基类内部维护几个关键状态:_expression 存原始串,_length 是其长度,_index 是读取光标,_token 是当前字符,_failed 标记语法错误,_precision 控制浮点比较容差。变量表用指针(因为表达式未必带变量),函数表是对象(至少内置一批函数,用户可扩)。 数字解析不支持科学计数法指数部分,例如 "1.5e3" 会被 _readNumber 拒掉,只会顺序吃数字和单个小数点。下面这段是基类里最常被调用的三个方法的原貌,逐行看一遍就能明白光标怎么走: _nextToken 先自增 _index,跳过所有 isspace 空白,若未越界就把 _expression[_index] 赋给 _token 并返回 true,否则 _token 置 0 返回 false。 _match(ushort c,...) 是“期望当前字符等于 c”:相等就吞掉走下一个;不等且之前没失败过,就调 error 报语法错,避免连锁报错刷屏。 _readNumber 边读边拼 number 串,遇到第二个 '.' 直接 error("Too many floating points") 并返回 false,最后靠 StringLen(number)>0 判断是否真读到东西。 公开接口里 evaluate 只是初始化(清 _failed、设 _index=-1),真正的计算留给子类覆盖。构造函数可吃 "name1=value1; name2=value2" 这样的变量串,也可直接接现成 VariableTable。 即便 success() 返回 true,计算仍可能出 NaN(除零、负根等)。调用方务必用 MathIsValidNumber 验返回值,否则解析器在某些行情下可能吐出非数字类型而不崩——外汇和贵金属波动剧烈,这类边界在实盘里更容易被触发,需自行兜底。
class="kw">template<class="kw">typename T> class AbstractExpressionProcessor { class="kw">protected: class="type">class="kw">string _expression; class="type">int _index; class="type">int _length; class="type">class="kw">ushort _token; class="type">bool _failed; class="type">class="kw">double _precision; VariableTable *_variableTable; FunctionTable _functionTable; class="kw">virtual class="type">void registerFunctions(); class="type">bool _nextToken(); class="type">void _match(class="type">class="kw">ushort c, class="type">class="kw">string message, class="type">class="kw">string context = NULL); class="type">bool _readNumber(class="type">class="kw">string &number); class="kw">virtual class="type">void error(class="type">class="kw">string message, class="type">class="kw">string context = NULL, const class="type">bool warning = false); class="kw">static class="type">bool isspace(class="type">class="kw">ushort c); class="kw">static class="type">bool isalpha(class="type">class="kw">ushort c); class="kw">static class="type">bool isalnum(class="type">class="kw">ushort c); class="kw">static class="type">bool isdigit(class="type">class="kw">ushort c); class="kw">template<class="kw">typename T> class="type">bool AbstractExpressionProcessor::_nextToken() { _index++; class="kw">while(_index < _length && isspace(_expression[_index])) _index++; if(_index < _length) { _token = _expression[_index]; class="kw">return true; } else { _token = class="num">0; } class="kw">return false; } class="kw">template<class="kw">typename T> class="type">void AbstractExpressionProcessor::_match(class="type">class="kw">ushort c, class="type">class="kw">string message, class="type">class="kw">string context = NULL) { if(_token == c) { _nextToken(); } else if(!_failed) class=class="str">"cmt">// prevent chained errors { error(message, context); } } class="kw">template<class="kw">typename T> class="type">bool AbstractExpressionProcessor::_readNumber(class="type">class="kw">string &number) { class="type">bool point = false; class="kw">while(isdigit(_token) || _token == &class="macro">#x27;.&class="macro">#x27;) { if(_token == &class="macro">#x27;.&class="macro">#x27; && point) { error("Too many floating points", __FUNCTION__); class="kw">return false; } number += ShortToString(_token); if(_token == &class="macro">#x27;.&class="macro">#x27;) point = true; _nextToken(); } class="kw">return StringLen(number) > class="num">0; } class="kw">public: class="kw">virtual T evaluate(const class="type">class="kw">string expression) { _expression = expression; _length = StringLen(_expression); _index = -class="num">1; _failed = false; class="kw">return NULL; } class="kw">public: AbstractExpressionProcessor(const class="type">class="kw">string vars = NULL); AbstractExpressionProcessor(VariableTable &vt); class="type">bool success() { class="kw">return !_failed; }; class="type">void setPrecision(const class="type">class="kw">double p) { _precision = p; }; class="type">class="kw">double getPrecision(class="type">void) const { class="kw">return _precision; }; class="kw">virtual VariableTable *variableTable(); class="kw">virtual FunctionTable *functionTable(); };
把表达式拆成递归调用的层级
递归下降解析器的核心是一组互相调用的函数,按运算符优先级自顶向下匹配字符流。以 EBNF 为例,Expr 先落到 Sum,Sum 由两个 Product 用 + 或 - 连接,Product 再拆成 Value 用 * 或 / 连接,Value 要么是数字要么是括号里的嵌套 Expr。表达式 "1 + 2 * 3" 会展开成 Expr→Sum→Product→Value(1) 遇 + 再 Product→Value(2) 遇 * 再 Value(3) 的树状路径。
| 在 MT5 里这套逻辑封装成 ExpressionProcessor 模板类,公开方法 evaluate 调用受保护的 _parse,_parse 再按优先级下钻到 _if、_logic、_eq 直到 _factor。三元运算符 ?: 由 _if 处理:遇到 '?' 就递归解析真假分支,缺 ':' 直接报错;逻辑与或 _logic 用 while 循环支持连续 "a>0 && b>0 && c>0",但 && 和 | 优先级被设成相等,复杂顺序得靠括号强制。 |
|---|
_factor 负责操作数,碰到 '(' 就递归 _if 处理子表达式,标识符走 _identifier 区分变量和函数,_number 则调 StringToDouble 转常量。下面这段模板声明和 _parse / _if / _logic 骨架可直接贴进 MQ5 头文件验证编译: 解释器模式在解析时顺手算值,最简单也最慢。若要在每次即时报价用变动的 price、volume 重算同一公式,最好把解析和执行拆开——解析一次存成 Promise 对象描述运算符与操作数关系,之后只做快速重算。外汇与贵金属波动剧烈,这类重算逻辑若用于实盘信号,须先在策略测试器跑历史数据确认稳定性,高风险不可忽视。
<span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> <span class="keyword">class </span>ExpressionProcessor: <span class="keyword">class="kw">public</span> AbstractExpressionProcessor<T> { <span class="keyword">class="kw">public</span>: ExpressionProcessor(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span> vars = <span class="macro">NULL</span>); ExpressionProcessor(VariableTable &vt); T evaluate(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span> expression) <span class="keyword">class="kw">override</span> { AbstractExpressionProcessor<T>::evaluate(expression); <span class="keyword">if</span>(_length > class="num">0) { _nextToken(); <span class="keyword">class="kw">return</span> _parse(); } <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>; } <span class="keyword">class="kw">protected</span>: T _parse(); T _if(); <span class="comment">class=class="str">"cmt">// ?:</span> T _logic(); <span class="comment">class=class="str">"cmt">// && ||</span> T _eq(); <span class="comment">class=class="str">"cmt">// == !=</span> T _compare(); <span class="comment">class=class="str">"cmt">// ><>=<=</span> T _expr(); <span class="comment">class=class="str">"cmt">// +-</span> T _term(); <span class="comment">class=class="str">"cmt">// */%</span> T _unary(); <span class="comment">class=class="str">"cmt">// !-+</span> T _factor(); <span class="comment">class=class="str">"cmt">// ()</span> T _identifier(); T _number(); T _function(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span> &name); }; <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> T ExpressionProcessor::_parse(<span class="keyword">class="type">void</span>) { T result = _if(); <span class="keyword">if</span>(_token != &class="macro">#x27;\class="num">0&class="macro">#x27;) { error(<span class="class="type">class="kw">string">"Tokens after end of expression."</span>, <span class="macro">__FUNCTION__</span>); } <span class="keyword">class="kw">return</span> result; } <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> T ExpressionProcessor::_if() { T result = _logic(); <span class="keyword">if</span>(_token == &class="macro">#x27;?&class="macro">#x27;) { _nextToken(); T truly = _if(); <span class="keyword">if</span>(_token == &class="macro">#x27;:&class="macro">#x27;) { _nextToken(); T falsy = _if(); <span class="keyword">class="kw">return</span> result ? truly : falsy; <span class="comment">class=class="str">"cmt">// NB: to be refined</span> } <span class="keyword">else</span> { error(<span class="class="type">class="kw">string">"Incomplete ternary <span class="keyword">if</span>-condition"</span>, <span class="macro">__FUNCTION__</span>); } } <span class="keyword">class="kw">return</span> result; } <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> T ExpressionProcessor::_logic() { T result = _eq(); <span class="keyword">class="kw">while</span>(_token == &class="macro">#x27;&&class="macro">#x27; || _token == &class="macro">#x27;|&class="macro">#x27;) { <span class="keyword">class="type">class="kw">ushort</span> previous = _token; _nextToken(); <span class="keyword">if</span>(previous == &class="macro">#x27;&&class="macro">#x27; && _token == &class="macro">#x27;&&class="macro">#x27;) { _nextToken(); result = _eq() && result; } <span class="keyword">else</span> <span class="keyword">if</span>(previous == &class="macro">#x27;|&class="macro">#x27; && _token == &class="macro">#x27;|&class="macro">#x27;) { _nextToken(); result = _eq() || result; } <span class="keyword">else</span> {
「表达式求值器的收尾实现」
上面这段把表达式处理器的剩余模板方法补齐了,从相等比较到因子解析、标识符与数字读取,最后派生出 ExpressionEvaluator 类直接用于双精度求值。 _eq() 里用 fabs(result - _compare()) <= _precision 判断浮点相等,注释标了 NB: to be refined,说明精度阈值 _precision 目前是写死的,实盘跑之前最好按品种点值调一下,否则黄金和欧美对‘相等’的容忍度会差出数量级。 _factor() 处理了括号、字母开头的变量或函数、以及纯数字三种情况;_identifier() 拼完变量名后若遇到左括号就转去 _function(),否则从变量表取值。_number() 靠 _readNumber 捞字符串再 StringToDouble 转,这里同样标了待 refinement,意味着目前不认科学计数法。 ExpressionEvaluator 只是给 ExpressionProcessor<double> 套了层构造壳,支持传变量字符串或外部 VariableTable 引用。开 MT5 建个 EA 把这套贴进去,喂 'a*(b+1)' 这种串,挂不同 _precision 就能直观看到相等判断的边界漂移。外汇与贵金属波动剧烈,这类自写解析器用于信号逻辑时须警惕精度误判带来的高风险。
error("Unexpected tokens " + ShortToString(previous) + " and " + ShortToString(_token), __FUNCTION__); } } class="kw">return result; } class="kw">template<class="kw">typename T> T ExpressionProcessor::_eq() { T result = _compare(); if(_token == &class="macro">#x27;!&class="macro">#x27; || _token == &class="macro">#x27;=&class="macro">#x27;) { const class="type">bool equality = _token == &class="macro">#x27;=&class="macro">#x27;; _nextToken(); if(_token == &class="macro">#x27;=&class="macro">#x27;) { _nextToken(); const class="type">bool equal = fabs(result - _compare()) <= _precision; class=class="str">"cmt">// NB: to be refined class="kw">return equality ? equal : !equal; } else { error("Unexpected token " + ShortToString(_token), __FUNCTION__); } } class="kw">return result; } class="kw">template<class="kw">typename T> T ExpressionProcessor::_factor() { T result; if(_token == &class="macro">#x27;(&class="macro">#x27;) { _nextToken(); result = _if(); _match(&class="macro">#x27;)&class="macro">#x27;, ") expected!", __FUNCTION__); } else if(isalpha(_token)) { result = _identifier(); } else { result = _number(); } class="kw">return result; } class="kw">template<class="kw">typename T> T ExpressionProcessor::_identifier() { class="type">class="kw">string variable; class="kw">while(isalnum(_token)) { variable += ShortToString(_token); _nextToken(); } if(_token == &class="macro">#x27;(&class="macro">#x27;) { _nextToken(); class="kw">return _function(variable); } class="kw">return _variableTable.get(variable); class=class="str">"cmt">// NB: to be refined } class="kw">template<class="kw">typename T> T ExpressionProcessor::_number() { class="type">class="kw">string number; if(!_readNumber(number)) { error("Number expected", __FUNCTION__); } class="kw">return StringToDouble(number); class=class="str">"cmt">// NB: to be refined } class ExpressionEvaluator: class="kw">public ExpressionProcessor<class="type">class="kw">double> { class="kw">public: ExpressionEvaluator(const class="type">class="kw">string vars = NULL): ExpressionProcessor(vars) { } ExpressionEvaluator(VariableTable &vt): ExpressionProcessor(vt) { } };