计算数学表达式(第一部分)。 递归下降解析器·进阶篇
🌲

计算数学表达式(第一部分)。 递归下降解析器·进阶篇

(2/3)· 当EA需要让用户改计算公式而不动代码,解析器选型直接决定执行效率与坑位

新手友好 第 2/3 篇

很多交易者把数学表达式写死在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 对象,并把取变量的动作包进派生类重写的虚方法里。

MQL5 / C++
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 *&params[]): 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 注释,表明变量取值还是半成品。外汇与贵金属波动剧烈、杠杆高风险,这类表达式引擎若用于实盘信号,须先在小周期回测验证容差参数。

MQL5 / C++
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,实盘前务必先校验变量表。

MQL5 / C++
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 &params[]) 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 *&params[]) 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 线重算的开销。外汇与贵金属波动剧烈,这类表达式仅作逻辑容器,实盘信号误触发概率不低,需自行加滑点过滤。

MQL5 / C++
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 自动展开数组下标。这样填完表之后,两个递归下降解析器就可以直接跑了——一个解释执行,一个建树后算。外汇和贵金属杠杆高,这类自研解析器先在策略测试器里用小资金验证再上实盘。

MQL5 / C++
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 &params[]);
};
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> \
把公式解析交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到表达式求值与变量表的可视化,你专注决策而不是调解析器。

常见问题

递归下降会先把表达式建成语法树再算,能支持变量表和函数表,且出错位置更明确;直接eval类方案在MQL里往往受限且难扩展。
可以,小布盯盘的品种页支持把文本表达式和变量代入后即时计算,省去你自己写解析器的过程。
右关联意味着相同优先级从右往左算,如3^2^5先算2^5;左关联则从左往右,减法左联才得到预期结果。
通过函数表注册25个标准函数名到对应调用,解析到函数词素时查表代入参数执行,pow可用于任意实数幂。