计算数学表达式(第一部分)。 递归下降解析器·进阶篇
(2/3)· 当EA需要让用户改计算公式而不动代码,解析器选型直接决定执行效率与坑位
很多交易者把数学表达式写死在MQL代码里,每次换目标函数就得重新编译。其实只需在运行时解析文本公式,就能让用户在对话框里改算符和变量,省去反复出包的麻烦。
◍ 用 Promise 把变量延迟到求值那一刻
在表达式解析里,遇到变量名如果立刻取 double 值,就失去了后续重算的余地。MQL5 里更稳的做法是建一个 Promise 对象,把变量名存进去,等真正 resolve 时再按名查表拿当前值,这样变量变动后重新求值不会失效。
Promise 用 code 字段标记元素类型:'n' 是数字常量、'v' 是变量、'f' 是函数,其余符号当运算符。数字直接落 value;变量存 name,并在首次访问后把编号缓进 index,之后按索引取表,比每次按名查更快。函数一开始就靠 index 编号定位,因为函数表内置、变量表解析时可能还空着。
left / right / last 三个引用决定算子形态:数字和变量全 NULL;一元用 left;二元用 left+right;三元条件运算才三个全占(条件、真分支、假分支)。当前解析器把函数参数限制在 3 个以内,多传直接忽略。
重载运算符后,整条表达式会拼成一棵 Promise 对象树,根节点代表整个表达式。调一次 resolve,就从底层递归把每个 promise 算成 double,最终“折叠”出总结果;任何一层出错返回 nan(非数字,由 NaNs.mqh 生成)。
_expressionProcessor 模板过去返回 T(如 double),现在要统一成 Promise*。原来那行 return _variableTable.get(variable); // NB: to be refined 不适用通用模板,应改成构造并返回指向该变量的 Promise 对象,并把取变量的动作包进派生类重写的虚方法里。
class="kw">return _variableTable.get(variable); class=class="str">"cmt">// NB: to be refined class Promise { class="kw">protected: class="type">uchar code; class="type">class="kw">double value; class="type">class="kw">string name; class="type">int index; Promise *left; Promise *right; Promise *last; class="kw">public: Promise(const class="type">uchar token, Promise *l = NULL, Promise *r = NULL, Promise *v = NULL): code(token), left(l), right(r), last(v), value(class="num">0), name(NULL), index(-class="num">1) { } Promise(const class="type">class="kw">double v): class=class="str">"cmt">// value(const) code(&class="macro">#x27;n&class="macro">#x27;), left(NULL), right(NULL), last(NULL), value(v), name(NULL), index(-class="num">1) { } Promise(const class="type">class="kw">string n, const class="type">int idx = -class="num">1): class=class="str">"cmt">// name of variable code(&class="macro">#x27;v&class="macro">#x27;), left(NULL), right(NULL), last(NULL), value(class="num">0), name(n), index(idx) { } Promise(const class="type">int f, Promise *¶ms[]): class=class="str">"cmt">// index of function code(&class="macro">#x27;f&class="macro">#x27;), left(NULL), right(NULL), last(NULL), value(class="num">0), name(NULL) { index = f; if(ArraySize(params) > class="num">0) left = params[class="num">0]; if(ArraySize(params) > class="num">1) right = params[class="num">1]; if(ArraySize(params) > class="num">2) last = params[class="num">2]; class=class="str">"cmt">// more params not supported } Promise *class="kw">operator+(Promise *r) { class="kw">return new Promise(&class="macro">#x27;+&class="macro">#x27;, &this, r); } Promise *class="kw">operator-(Promise *r) { class="kw">return new Promise(&class="macro">#x27;-&class="macro">#x27;, &this, r); } class="type">class="kw">double resolve() { class="kw">switch(code) { case &class="macro">#x27;n&class="macro">#x27;: class="kw">return value; class=class="str">"cmt">// number constant case &class="macro">#x27;v&class="macro">#x27;: value = _variable(); class=class="str">"cmt">// variable name class="kw">return value; case &class="macro">#x27;f&class="macro">#x27;: value = _execute(); class=class="str">"cmt">// function index class="kw">return value; class="kw">default: value = _calc(); class="kw">return value; } class="kw">return class="num">0; }; class="kw">static class="type">void environment(AbstractExpressionProcessor<Promise *> *e) { variableTable = e.variableTable(); functionTable = e.functionTable(); } class="kw">protected: class="kw">static VariableTable *variableTable; class="kw">static FunctionTable *functionTable; class="type">class="kw">double _variable() { class="type">class="kw">double result = class="num">0; if(index == -class="num">1) { index = variableTable.index(name); if(index == -class="num">1) { class="kw">return nan; class=class="str">"cmt">// error: Variable undefined } result = variableTable[index]; } else { result = variableTable[index]; } class="kw">return result; } class="type">class="kw">double _execute() { class="type">class="kw">double params[]; if(left) {
「运算符节点的动态参数装配与求值」
这段 MQL5 片段展示了一个表达式树节点如何把左右子节点(以及可选的第三节点)动态塞进数组,再交给函数表或内置 switch 去算值。ArrayResize 按 1→2→3 的节奏扩容,说明该节点支持一元、二元、三元运算,且只在对应子节点非空时才追加参数。 函数表查不到索引时直接返回 nan,相当于把越界错误显式暴露给上层,而不是静默崩掉。自己写指标时若复用这套结构,开 MT5 把 functionTable 长度打印出来,能立刻看出 index 是否越界。 下面 switch 里覆盖了 + - * / % 以及比较、逻辑、精度相等('=' 用 _precision 容差而非硬等)等 17 个分支。注意 '`' 和 '=' 用 fabs(first-second) 与 _precision 比较来实现浮点不等/相等,这对价格行为里判断突破是否“真破”有参考价值——贵金属跳空常使硬等失效,容差匹配更稳。 末段 _variableTable.get(variable) 带 NB 注释,表明变量取值还是半成品。外汇与贵金属波动剧烈、杠杆高风险,这类表达式引擎若用于实盘信号,须先在小周期回测验证容差参数。
ArrayResize(params, class="num">1); params[class="num">0] = left.resolve(); if(right) { ArrayResize(params, class="num">2); params[class="num">1] = right.resolve(); if(last) { ArrayResize(params, class="num">3); params[class="num">2] = last.resolve(); } } } IFunctor *ptr = functionTable[index]; class=class="str">"cmt">// TBD: functors if(ptr == NULL) { class="kw">return nan; class=class="str">"cmt">// error: Function index out of bound } class="kw">return ptr.execute(params); } class="type">class="kw">double _calc() { class="type">class="kw">double first = class="num">0, second = class="num">0, third = class="num">0; if(left) { first = left.resolve(); if(right) { second = right.resolve(); if(last) { third = last.resolve(); } } } class="kw">switch(code) { case &class="macro">#x27;+&class="macro">#x27;: class="kw">return first + second; case &class="macro">#x27;-&class="macro">#x27;: class="kw">return first - second; case &class="macro">#x27;*&class="macro">#x27;: class="kw">return first * second; case &class="macro">#x27;/&class="macro">#x27;: class="kw">return safeDivide(first, second); case &class="macro">#x27;%&class="macro">#x27;: class="kw">return fmod(first, second); case &class="macro">#x27;!&class="macro">#x27;: class="kw">return !first; case &class="macro">#x27;~&class="macro">#x27;: class="kw">return -first; case &class="macro">#x27;<&class="macro">#x27;: class="kw">return first < second; case &class="macro">#x27;>&class="macro">#x27;: class="kw">return first > second; case &class="macro">#x27;{&class="macro">#x27;: class="kw">return first <= second; case &class="macro">#x27;}&class="macro">#x27;: class="kw">return first >= second; case &class="macro">#x27;&&class="macro">#x27;: class="kw">return first && second; case &class="macro">#x27;|&class="macro">#x27;: class="kw">return first || second; case &class="macro">#x27;`&class="macro">#x27;: class="kw">return _precision < fabs(first - second); class=class="str">"cmt">// first != second; case &class="macro">#x27;=&class="macro">#x27;: class="kw">return _precision > fabs(first - second); class=class="str">"cmt">// first == second; case &class="macro">#x27;?&class="macro">#x27;: class="kw">return first ? second : third; } class="kw">return nan; class=class="str">"cmt">// error: Unknown class="kw">operator } class="kw">return _variableTable.get(variable); class=class="str">"cmt">// NB: to be refined
用模板类补齐解析器的运算短板
MQL5 没有多重继承,没法像 trait 那样给解析器随手挂一批通用方法。直接在每个继承自 AbstractExpressionProcessor 的子类里写空实现又不地道,所以把变量读取、常量转换、逻辑非、函数调用、三元判断、等值比较这些活儿,统一塞进一个 ExpressionHelper 模板类,哪个解析器要用就在内部持有一个 helper 实例。 helper 对 double 和 Promise* 各有一套实现。double 版里 _isEqual 用 fabs(result - next) <= _precision 判定相等,返回布尔转成的 double;Promise 版则把 '!'、'?'、函数调用全包成新的 Promise 对象,留到后面真正求值再展开。 逻辑非没在 Promise 里重载 operator!,是因为 '!' 只能作用于对象不能作用于指针,Promise *p 写 !p 不会触发重载,必须 !*p,但解引用对 T=double 又无效,所以用 _negate 方法显式兜底。 下面这段是 double 与 Promise 两套 helper 的核心代码,直接反映了上面说的分工。开 MT5 建个继承试验工程,把 _precision 调到 1e-8 再跑几组浮点比较,能直观看到等值判定的边界行为。外汇与贵金属脚本里若复用这套解析逻辑,注意未定义变量在 double 版会返回 nan,实盘前务必先校验变量表。
class="kw">template<class="kw">typename T> class ExpressionHelper { class="kw">protected: VariableTable *_variableTable; FunctionTable *_functionTable; class="kw">public: ExpressionHelper(AbstractExpressionProcessor<T> *owner): _variableTable(owner.variableTable()), _functionTable(owner.functionTable()) { } class="kw">virtual T _variable(const class="type">class="kw">string &name) = class="num">0; class="kw">virtual T _literal(const class="type">class="kw">string &number) = class="num">0; class="kw">virtual T _negate(T result) = class="num">0; class="kw">virtual T _call(const class="type">int index, T &args[]) = class="num">0; class="kw">virtual T _ternary(T condition, T truly, T falsy) = class="num">0; class="kw">virtual T _isEqual(T result, T next, const class="type">bool equality) = class="num">0; }; class ExpressionHelperDouble: class="kw">public ExpressionHelper<class="type">class="kw">double> { class="kw">public: ExpressionHelperDouble(AbstractExpressionProcessor<T> *owner): ExpressionHelper(owner) { } class="kw">virtual class="type">class="kw">double _variable(const class="type">class="kw">string &name) class="kw">override { if(!_variableTable.exists(name)) { class="kw">return nan; } class="kw">return _variableTable.get(name); } class="kw">virtual class="type">class="kw">double _literal(const class="type">class="kw">string &number) class="kw">override { class="kw">return StringToDouble(number); } class="kw">virtual class="type">class="kw">double _call(const class="type">int index, class="type">class="kw">double ¶ms[]) class="kw">override { class="kw">return _functionTable[index].execute(params); } class="kw">virtual class="type">class="kw">double _isEqual(class="type">class="kw">double result, class="type">class="kw">double next, const class="type">bool equality) class="kw">override { const class="type">bool equal = fabs(result - next) <= _precision; class="kw">return equality ? equal : !equal; } class="kw">virtual class="type">class="kw">double _negate(class="type">class="kw">double result) class="kw">override { class="kw">return !result; } class="kw">virtual class="type">class="kw">double _ternary(class="type">class="kw">double condition, class="type">class="kw">double truly, class="type">class="kw">double falsy) class="kw">override { class="kw">return condition ? truly : falsy; } }; class ExpressionHelperPromise: class="kw">public ExpressionHelper<Promise *> { class="kw">public: ExpressionHelperPromise(AbstractExpressionProcessor<T> *owner): ExpressionHelper(owner) { } class="kw">virtual Promise *_negate(Promise *result) class="kw">override { class="kw">return new Promise(&class="macro">#x27;!&class="macro">#x27;, result); } class="kw">virtual Promise *_call(const class="type">int index, Promise *¶ms[]) class="kw">override { class="kw">return new Promise(index, params); } class="kw">virtual Promise *_ternary(Promise *condition, Promise *truly, Promise *falsy) class="kw">override { class="kw">return new Promise(&class="macro">#x27;?&class="macro">#x27;, condition, truly, falsy); } class="kw">virtual Promise *_variable(const class="type">class="kw">string &name) class="kw">override { if(CheckPointer(_variableTable) != POINTER_INVALID) { class="type">int index = _variableTable.index(name); if(index == -class="num">1) { class="kw">return new Promise(nan); class=class="str">"cmt">// error: Variable is undefined }
◍ 表达式求值器的双通道实现
MQL5 里把字符串公式变成可计算对象,靠的是 ExpressionProcessor 模板类派生两条路:一条直接算 double 值,一条返回 Promise* 做延迟求值。ExpressionEvaluator 绑定 ExpressionHelperDouble,适合即时算指标阈值;ExpressionCompiler 绑定 ExpressionHelperPromise,把 '=' 与 '`' 转成 uchar 标记再串起结果链,适合异步条件树。 _helper 的析构由 AbstractExpressionProcessor 接管,CheckPointer(helper)==POINTER_DYNAMIC 才 delete,避免空指针或栈对象误删。_eq() 里碰到 '!' 或 '=' 会再看下一个字符,连续两个 '=' 才走 _isEqual,单 '=' 也按相等处理,这套分支在写自定义表达式解析时容易漏掉 '!' 的非等分支。 _identifier() 用 isalnum 逐字符收进 variable 字符串,再交 helper._variable 解析;_number() 调 _readNumber 失败就抛 "Number expected"。这两段说明变量名只认字母数字,带下划线以外的符号会直接断词。 开 MT5 把这段塞进 EA 的 include,用 ExpressionCompiler 传 "Close[0]=Open[0]" 这类串,返回的 Promise* 可在 tick 里惰性比对,省掉每根 K 线重算的开销。外汇与贵金属波动剧烈,这类表达式仅作逻辑容器,实盘信号误触发概率不低,需自行加滑点过滤。
class="kw">return new Promise(name, index); } class="kw">return new Promise(name); } class="kw">virtual Promise *_literal(const class="type">class="kw">string &number) class="kw">override { class="kw">return new Promise(StringToDouble(number)); } class="kw">virtual Promise *_isEqual(Promise *result, Promise *next, const class="type">bool equality) class="kw">override { class="kw">return new Promise((class="type">uchar)(equality ? &class="macro">#x27;=&class="macro">#x27; : &class="macro">#x27;`&class="macro">#x27;), result, next); } }; class="kw">protected: ExpressionHelper<T> *helper; class="kw">public: ~AbstractExpressionProcessor() { if(CheckPointer(helper) == POINTER_DYNAMIC) class="kw">delete helper; } 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(); class="kw">return helper._isEqual(result, _compare(), equality); class=class="str">"cmt">// OK } } 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(); } ... class="kw">return helper._variable(variable); class=class="str">"cmt">// OK } 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 helper._literal(number); class=class="str">"cmt">// OK } 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) { helper = new ExpressionHelperDouble(&this); } ExpressionEvaluator(VariableTable &vt): ExpressionProcessor(vt) { helper = new ExpressionHelperDouble(&this); } }; class ExpressionCompiler: class="kw">public ExpressionProcessor<Promise *> { class="kw">public: ExpressionCompiler(const class="type">class="kw">string vars = NULL): ExpressionProcessor(vars) { helper = new ExpressionHelperPromise(&this); } ExpressionCompiler(VariableTable &vt): ExpressionProcessor(vt) { helper = new ExpressionHelperPromise(&this); } class="kw">virtual Promise *evaluate(const class="type">class="kw">string expression) class="kw">override { Promise::environment(&this); class="kw">return ExpressionProcessor<Promise *>::evaluate(expression); } };
「用函子把数学函数塞进解析表」
变量表和函数表本质都是 key=value 的映射,key 是字符串名,value 是类型 T。基类 Table 已经封装了按名/按索引增改查,VariableTable 继承它并把 T 定为 double,所以表达式里的数值变量全走这张表。 函数不能只当裸代码,得包成 IFunctor 接口对象:name() 给名字,arity() 报参数个数,execute() 吃一个 double 数组算结果。把 MQL 内置数学函数逐个包装后,用 FunctionTable.add 塞进表,解释器和语法树解析器就能统一调用。 为了让所有函子自动入库,AbstractFunc 基类里放了一个静态 AbstractFuncStorage,构造函数里直接 storage.add(this)。目前函数 arity 上限是 3,用 sizeof(T)%4 推算:arity0 结构体的 char x[4] 大小为 4,取模得 0,所以大小 4 对应零参函数。 宏 FUNCTOR 配合 FuncN<arityN> 能批量生成函子类,参数列表 PARAMS0~PARAMS3 自动展开数组下标。这样填完表之后,两个递归下降解析器就可以直接跑了——一个解释执行,一个建树后算。外汇和贵金属杠杆高,这类自研解析器先在策略测试器里用小资金验证再上实盘。
class="kw">template<class="kw">typename T> class Table { class="kw">public: class="kw">virtual T class="kw">operator[](const class="type">int index) const; class="kw">virtual class="type">int index(const class="type">class="kw">string variableName); class="kw">virtual T get(const class="type">class="kw">string variableName) const; class="kw">virtual class="type">int add(const class="type">class="kw">string variableName, T value); class="kw">virtual class="type">void update(const class="type">int index, T value); ... }; class VariableTable: class="kw">public Table<class="type">class="kw">double> { class="kw">public: VariableTable(const class="type">class="kw">string pairs = NULL) { if(pairs != NULL) assign(pairs); } class="type">void assign(const class="type">class="kw">string pairs); }; interface IFunctor { class="type">class="kw">string name(class="type">void) const; class="type">int arity(class="type">void) const; class="type">class="kw">double execute(const class="type">class="kw">double ¶ms[]); }; class FunctionTable: class="kw">public Table<IFunctor *> { class="kw">public: class="type">void add(IFunctor *f) { Table<IFunctor *>::add(f.name(), f); } class="type">void add(IFunctor *&f[]) { for(class="type">int i = class="num">0; i < ArraySize(f); i++) { add(f[i]); } } }; class AbstractFuncStorage { class="kw">protected: IFunctor *funcs[]; class="type">int total; class="kw">public: ~AbstractFuncStorage() { for(class="type">int i = class="num">0; i < total; i++) { CLEAR(funcs[i]); } } class="type">void add(IFunctor *f) { ArrayResize(funcs, total + class="num">1); funcs[total++] = f; } class="type">void fill(FunctionTable &table) { table.add(funcs); } }; class AbstractFunc: class="kw">public IFunctor { class="kw">private: const class="type">class="kw">string _name; const class="type">int _arity; class="kw">static AbstractFuncStorage storage; class="kw">public: AbstractFunc(const class="type">class="kw">string n, const class="type">int a): _name(n), _arity(a) { storage.add(&this); } class="type">class="kw">string name(class="type">void) const class="kw">override { class="kw">return _name; } class="type">int arity(class="type">void) const class="kw">override { class="kw">return _arity; } class="kw">static class="type">void fill(FunctionTable &table) { storage.fill(table); } }; class="kw">static AbstractFuncStorage AbstractFunc::storage; class="kw">template<class="kw">typename T> class FuncN: class="kw">public AbstractFunc { class="kw">public: FuncN(const class="type">class="kw">string n): AbstractFunc(n, class="kw">sizeof(T) % class="num">4) {} }; class="kw">struct arity0 { class="type">char x[class="num">4]; }; class="macro">#define _ARITY(N) class="kw">struct arity#class="macro">#N { class="type">char x[N]; }; _ARITY(class="num">1); _ARITY(class="num">2); _ARITY(class="num">3); class="macro">#define PARAMS0 class="macro">#define PARAMS1 params[class="num">0] class="macro">#define PARAMS2 params[class="num">0],params[class="num">1] class="macro">#define PARAMS3 params[class="num">0],params[class="num">1],params[class="num">2] class="macro">#define FUNCTOR(CLAZZ,NAME,ARITY) \ class Func_#class="macro">#CLAZZ: class="kw">public FuncN<arity#class="macro">#ARITY> \