计算数学表达式(第一部分)。 递归下降解析器·综合运用
🧮

计算数学表达式(第一部分)。 递归下降解析器·综合运用

(3/3)· 编译成语法树再执行,EA 不必重编即可切换目标函数与指标原型

进阶 第 3/3 篇
把计算公式写死在 MQL 代码里,每次换策略就要重新编译发版。其实只需在运行时解析文本表达式,用户改一行公式就能换目标函数,EA 本体不动。

◍ 把数学函数批量封装成可调用算子

在 MT5 里做价格行为量化时,常要把一堆标准数学函数暴露给策略表达式引擎。手写每个类太啰嗦,用宏把类名、显示名、参数个数一次性绑定,能省掉大量重复代码。 下面这段宏定义加实例化,把 fabs、acos 到 tanh 共 25 个函数统一注册。其中 rand 参数个数为 0,max/min/mod/pow 为 2,其余多数单参,直接在代码里数 FUNCTOR 行就能核对覆盖面的完整度。 宏展开后每个函数生成一个继承类并重写 execute,入参走 const double &params[] 数组,按 ARITY 取数后调原生 CLAZZ。你复制进 EA 的 include 头里,编译后就能在表达式里写 abs(-1.5) 或 pow(close[1],2) 做特征构造。外汇与贵金属波动剧烈,这类算子仅用于信号计算,实盘前务必在策略测试器跑历史回测验证。

MQL5 / C++
class="kw">public:
Func_#class="macro">#CLAZZ(): FuncN(NAME) {}
class="type">class="kw">double execute(const class="type">class="kw">double &params[]) class="kw">override
{
class="kw">return CLAZZ(PARAMS#class="macro">#ARITY);
}
};
Func_#class="macro">#CLAZZ __#class="macro">#CLAZZ;
FUNCTOR(fabs, "abs", class="num">1);
FUNCTOR(acos, "acos", class="num">1);
FUNCTOR(acosh, "acosh", class="num">1);
FUNCTOR(asin, "asin", class="num">1);
FUNCTOR(asinh, "asinh", class="num">1);
FUNCTOR(atan, "atan", class="num">1);
FUNCTOR(atanh, "atanh", class="num">1);
FUNCTOR(ceil, "ceil", class="num">1);
FUNCTOR(cos, "cos", class="num">1);
FUNCTOR(cosh, "cosh", class="num">1);
FUNCTOR(exp, "exp", class="num">1);
FUNCTOR(floor, "floor", class="num">1);
FUNCTOR(log, "log", class="num">1);
FUNCTOR(log10, "log10", class="num">1);
FUNCTOR(fmax, "max", class="num">2);
FUNCTOR(fmin, "min", class="num">2);
FUNCTOR(fmod, "mod", class="num">2);
FUNCTOR(pow, "pow", class="num">2);
FUNCTOR(rand, "rand", class="num">0);
FUNCTOR(round, "round", class="num">1);
FUNCTOR(sin, "sin", class="num">1);
FUNCTOR(sinh, "sinh", class="num">1);
FUNCTOR(sqrt, "sqrt", class="num">1);
FUNCTOR(tan, "tan", class="num">1);
FUNCTOR(tanh, "tanh", class="num">1);

「表达式求值与 NaN 陷阱」

在 MQL5 里做动态表达式计算,先 new 一个 ExpressionEvaluator,把变量塞进去,再拿字符串调 evaluate 就能跑。语法对不对可以用 success() 探一下,但它只管解析阶段,运行时炸不炸它不担保。 上面这段代码把 a 设成 -10,再算 1 + sqrt(a),返回值是 -nan(ind),而 success() 居然还是 true。也就是说,负变量开根这种事在语法层面完全合法,算出来却是 NaN,不拦你。 外汇和贵金属脚本里动态算指标常踩这坑,建议每次 evaluate 完都用 MathIsValidNumber 兜一道,把无效数挡在信号逻辑外。MT5 里贴下面这段就能复现。

MQL5 / C++
ExpressionEvaluator ee("a=-class="num">10");
  class="type">class="kw">double result = ee.evaluate("class="num">1 + sqrt(a)"); class=class="str">"cmt">// -nan(ind)
  class="type">bool success = ee.success();                class=class="str">"cmt">// true

用语法树批量算表达式比解释器快

在 MT5 里做变量扫描时,与其每次都重新解析字符串,不如先建一棵语法树。做法是 new 一个 ExpressionCompiler,把 VariableTable 传进去,再拿表达式字符串调 evaluate,拿回的是 Promise 引用,真正算值要再调一次 resolve。 循环里往表写 a、b、c 的数值,前面必须开 vt.adhocAllocation(true)。这个开关让解析器容忍未事先声明的变量名,把它们先挂成 nan;不开的话,空表遇上变量直接报错,因为默认假定变量已预定义。 c.evaluate() 之后可以调 c.success() 抓解析错误,日志也会记。和解释器一样,evaluate 永远返回点东西——变量未知就在树上留个 nan 节点,算出来也是 nan,但树本身能帮你定位哪错了,Promise 的 print 方法可直接把树打出来看。 下面这段是实测可用的批量求值骨架:先开 adhocAllocation,再 evaluate 一次,循环里只改表不重解析,10 组数组跑完即可。外汇与贵金属行情跳动快,这种预编译方式在多次重算时延迟倾向更低,但杠杆品种高风险仍在,参数请自行回测。

MQL5 / C++
class="type">class="kw">double a[class="num">10] = {...}, b[class="num">10] = {...}, c[class="num">10] = {...};
  
  VariableTable vt;
  ExpressionCompiler с(vt);
  vt.adhocAllocation(true);
  const class="type">class="kw">string expr = "(a + b) * sqrt(c)";
  Promise *p = c.evaluate(expr);
  
  for(class="type">int i = class="num">0; i < class="num">10; i++)
  {
    vt.set("a", a[i]);
    vt.set("b", b[i]);
    vt.set("c", c[i]);
    Print(p.resolve());
  }

◍ 顺手把测试脚本跑起来

这一节原本只是收尾,但落到一个能立刻动手的点上更实在:原文附带了 parsers1.zip(14.43 KB),里面是可直接用的 MQL5 表达式解析器测试脚本。解压后拖进 MT5 的 MQL5/Scripts 目录,编译运行,就能看到字符串公式被实时求值的过程。 外汇与贵金属杠杆高、滑点随机,任何解析逻辑上线前都得在策略测试器里用历史数据验证,别凭直觉信任输出。 下一篇会换普拉特和分流场两类解析器做性能对照,并给到交易任务里的具体用法。现在先把这一版的字节码跑通,后面对比才有基线。

把公式解析交给小布盯盘
这些表达式诊断与树形求值的小工具,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时公式评估结果,你只管调参决策。

常见问题

正则难以处理嵌套括号与运算符优先级,递归下降按语法规则自顶向下展开,能稳定构建语法树并支持函数与三元条件。
可以,小布盯盘内置了表达式解析与求值模块,粘贴公式文本即可获得变量绑定后的计算结果,省去自己写解析器的功夫。
MQL 标准数学函数 pow 支持非整数幂,而 ^ 仅整数幂且右关联会令解析复杂,故统一用函数更省事。
能,树结构分离了运算符与数据项,外部改完变量属性重走求值即可,不必重新解析文本。