计算数学表达式(第一部分)。 递归下降解析器·综合运用
(3/3)· 编译成语法树再执行,EA 不必重编即可切换目标函数与指标原型
◍ 把数学函数批量封装成可调用算子
在 MT5 里做价格行为量化时,常要把一堆标准数学函数暴露给策略表达式引擎。手写每个类太啰嗦,用宏把类名、显示名、参数个数一次性绑定,能省掉大量重复代码。 下面这段宏定义加实例化,把 fabs、acos 到 tanh 共 25 个函数统一注册。其中 rand 参数个数为 0,max/min/mod/pow 为 2,其余多数单参,直接在代码里数 FUNCTOR 行就能核对覆盖面的完整度。 宏展开后每个函数生成一个继承类并重写 execute,入参走 const double ¶ms[] 数组,按 ARITY 取数后调原生 CLAZZ。你复制进 EA 的 include 头里,编译后就能在表达式里写 abs(-1.5) 或 pow(close[1],2) 做特征构造。外汇与贵金属波动剧烈,这类算子仅用于信号计算,实盘前务必在策略测试器跑历史回测验证。
class="kw">public: Func_#class="macro">#CLAZZ(): FuncN(NAME) {} class="type">class="kw">double execute(const class="type">class="kw">double ¶ms[]) 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 里贴下面这段就能复现。
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 组数组跑完即可。外汇与贵金属行情跳动快,这种预编译方式在多次重算时延迟倾向更低,但杠杆品种高风险仍在,参数请自行回测。
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 目录,编译运行,就能看到字符串公式被实时求值的过程。 外汇与贵金属杠杆高、滑点随机,任何解析逻辑上线前都得在策略测试器里用历史数据验证,别凭直觉信任输出。 下一篇会换普拉特和分流场两类解析器做性能对照,并给到交易任务里的具体用法。现在先把这一版的字节码跑通,后面对比才有基线。