利用MQL进行MQL解析(基础篇)
📘

利用MQL进行MQL解析(基础篇)

第 1/3 篇

用 MQL 自己解析 MQL 代码

在 MT5 的 MQL5 环境里,代码不仅能下单和画指标,还能反过来读取并解析 MQL 源码文本。这种「用 MQL 解析 MQL」的思路,适合做自动化代码检查、策略碎片提取,或者给「小布盯盘」类工具做本地语法预处理。

  • 年 4 月 2 日社区曾发布过相关示例,原文累计浏览 3729、评论 14 条,说明这类偏底层的玩法虽冷门但确有刚需。外汇与贵金属市场高杠杆、滑点无常,跑这类自解析脚本前请在策略测试器用历史数据验证,避免实盘误读脚本逻辑。

核心做法是把 .mq5 文件以字符串读入,再用 MQL5 的字符串函数做分词与结构匹配。你可以先写个最小例子:读入自身源码,数出里面有多少个 void 函数声明,以此确认解析链路通了再去扩成完整 AST。

◍ 为什么要在 MT5 里直接啃源代码

写程序本质是把流程形式化、自动化。MT5 内嵌的 MQL 能解决不少交易员的实务问题,但多数编码只处理应用数据,很少去碰源代码本身。其实对源码做分析,往往比跑策略更有用。 举个例子:你想在大型项目里找变量 notification 在哪被调用,单纯按字符串搜,会连方法名、注释、文本里的同名片段一起捞出来,语义全丢。MetaEditor 自带的向导能生成 EA 框架或头文件,但元编程的潜力远不止于此。 静态分析能算代码质量指标、抓编译器漏掉的运行时隐患。编译器只报基础警告,全量潜在错误检查通常交给外部程序。MQL 跟 C++ 亲缘近,用 Doxygen 很轻松,但上 lint 级工具就得自己适配——因为 MQL 终究不是 C++。 本文只做静态分析,不碰动态分析器(那种能在虚拟环境跟内存、锁、变量值)。简单需求如搜输入变量,正则库够用;但要正经干,得按 MQL 语法写解析器。我们接下来就在 MQL 里写一个 MQL 解析器,吐出源码元数据,后面还能延展到解释器或跨语言转译(transpiling),不过那属于后话。

「从 BNF 到递归下降:自己写解析器的起点」

描述一门语言语法,最通用的写法是 Backus-Naur 范式(BNF)。在 BNF 里,不可分割的字符或符号叫终端(如 '='、';'、单个字母),由终端按规则拼出来的结构叫非终端。上面那段示例语法里,program 由 operator 递归组成,operator 必须以 name 开头、接 '='、再接 expression、以 ';' 收尾——这就是一套能算变量和数字的最小语言。 拿输入 "A = 10;" 走一遍:解析器先匹配字母 'A' 得到 name,再吃下终端 '=',随后把 "10" 按 digit 序列识别成 number,分号闭合 operator。整个过程就是不断拿文本当前位置去套规则,套中就把光标推进一步,直到整段文本都挂到语法树上。 左递归是坑。像 "number ::= number digit | {empty}" 这种非终端在规则左右两侧都出现,部分下降解析器会无限循环不读新字符。自己攒 MQL 类语法时,要么改写规则消去左递归,要么在代码层加死循环保护——C++/MQL 语法分支太密,手工查错不现实。 递归下降解析器最易上手:从起始非终端 program 整体切入,顺规则匹配终端,不匹配就回滚到备选分支,这正是前面纯理论走查的过程。但回滚(回溯)会让最坏情况复杂度指数膨胀。LL(k)/LR(k) 用预计算转移表避免回溯,LR 对左递归更宽容;不过 MQL 和 C++ 都不是上下文无关语法,光看 k 个前瞻字符不够——比如 "x*y;" 可能是乘法也可能是指针声明,">>" 在模板里还曾和右移运算符冲突,都得靠符号表或预处理器辅助。 对外汇、贵金属自动化而言,这类解析器开发高风险:语法歧义处理不当会让 EA 逻辑静默错乱。本文先收口在递归下降,能跑通再谈进阶。

MQL5 / C++
program ::= <span class="keyword">class="kw">operator</span> more_operators
more_operators ::= program | {empty}
<span class="keyword">class="kw">operator</span> ::= name <span class="class="type">class="kw">string">&class="macro">#x27;=&class="macro">#x27;</span> expression <span class="class="type">class="kw">string">&class="macro">#x27;;&class="macro">#x27;</span>
name ::= letter more_letters
more_letters ::= name | {empty}
letter ::= [A-Z]
expression ::= operand more_operands
more_operands ::= operation expression | {empty}
operand ::= name | number
number ::= digit more_digits
more_digits ::= number | {empty}
digit ::= [<span class="number">class="num">0</span>-<span class="number">class="num">9</span>]
operation ::= <span class="class="type">class="kw">string">&class="macro">#x27;+&class="macro">#x27;</span> | <span class="class="type">class="kw">string">&class="macro">#x27;-&class="macro">#x27;</span>
A = <span class="number">class="num">10</span>;
X = A - <span class="number">class="num">5</span>;
program(程序)
&nbsp;&nbsp;<span class="keyword">class="kw">operator(操作符)</span>
&nbsp;&nbsp;&nbsp;&nbsp;name(名称)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;letter(字母)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">&class="macro">#x27;A&class="macro">#x27;</span>
number ::= number digit | {empty}
x * y;
vector&lt;pair&lt;<span class="keyword">class="type">int</span>,<span class="keyword">class="type">int</span>&gt;&gt; v;
vector&lt;pair&lt;<span class="keyword">class="type">int</span>,<span class="keyword">class="type">int</span>&gt; &gt; v;

从合并源码到令牌流的解析链路

解析器的第一要务是把输入文本切成不可再分的片段流,再拿去比对 BNF 或近似 MQL 语法描述出的合法结构。MQL 工程常靠 #include 拼装多文件,所以先用 FileReader 读单文件、用 FileReaderController 管多个实例;文本在类间传递不能靠字符串指针(MQL 不允许成员引用字符串),于是套一层 Source 包装类,顺便记录合并后偏移到原始文件名与行号的映射。 为找 #include,控制器得先预览文本,这部分交给 Preprocessor。第一版不碰宏替换——那需要额外语法描述和运行期解释,容易发散;预处理器只透传非指令块、按文件或片段整块往外抛。随后 Scanner 在文本里切出令牌:关键字、标点、运算符、标识符、数字、字符串、日期常量都是独立令牌,空格和制表符直接忽略,仅保留换行计数以便报错的行号定位。reserved.txt 里附了 MQL 关键字全表并编进扫描器源码。 Scanner 吐出令牌列表后,Parser 才基于令牌做结构解析。语法用 Terminal / NonTerminal 体系以重载运算符写法表达,比如 value 绑 CONST_INTEGER、plus 绑 PLUS、star 绑 STAR,operation = plus | star 表示逻辑或,expression = value + operation + value 表示顺序与。这套类层级就是抽象语法树(AST),描述了一组无限可能的合法字符串。 实际匹配时解析器从 expression 往下钻,到终端级就拿令牌比对当前字符,匹配则游标推进、不匹配则换备选;全部落空即语法违规。输出不强制建具体语法树(CST 可能过大),改走 Callback 接口,每产出一条合法规则就通知外层,边解析边做语义检查。

MQL5 / C++
Terminal value = CONST_INTEGER;
Terminal plus = PLUS;
Terminal star = STAR;
NonTerminal operation = plus | star;
NonTerminal expression = value + operation + value;

◍ 用符号链接突破 MT5 的沙箱读取限制

MetaTrader 5 的 FileOpen 只能访问 MQL5/Files 这个沙箱目录,而我们要解析的源码散落在 MQL5/Include、MQL5/Experts、MQL5/Scripts、MQL5/Indicators 里。直接让 EA 去读这些路径会被终端拒绝,这是做静态分析工具的第一道墙。 绕过办法是用 Windows 的目录连接(junction)把真实文件夹映射进 Files 下。命令本质是 mklink /J new_name existing_target,新建的虚拟文件夹左下角带小箭头,删的时候认准箭头别误删原目录。 实操时在 MQL5/Files 里建子目录 Sources,把随附的 makelink.bat 丢进去,脚本内容只有一行:mklink /J %1 ..\..\%1。在 Sources 里执行 makelink Include,就会生成指向 MQL5/Include 的虚拟文件夹;对 Experts、Scripts 同理。之后预处理器参数 IncludesFolder 填 Sources/Include 就能真正读到标准头文件,SourceFile 可填 Sources/Scripts/test.mq5 做样例。 别把连接建在 MQL5 根目录常开。所有 MQL 程序都能透过链接读源码,若那里面存了登录信息、密码或绝密策略,等于变相暴露。偶尔开、用完断,是更稳的做法。

「用 Parser 类把语法跑成可调试的计算器」

Parser 的成员保存了令牌输入列表、当前位置 cursor、最远位置 maxcursor(用于错误诊断)、回滚栈 state,以及源文引用 source。它用 stack 跟踪语法元素调用的整个层次结构,错误则累积进 errors 栈,这套结构让长表达式 1+2+3+4+5 的嵌套匹配可被逐步回放。 构造函数接收令牌列表、源文本,以及一个可选 flag 决定是否生成语法树。开启树模式后,每次成功的 production 会被串到根变量 tree 上;跑完可用 getTree 取出,直接在 MT5 调试器里看节点层级。 match 的简化逻辑是:遇到可选元素且令牌为 EOF 即视为正确结尾;终端则比对标记 me 与当前令牌,命中就 advance 推进光标。值得注意的是 advance 超过 maxcursor 时会清掉累积错误,因为那些是试探分支留下的噪音。 production 方法通过回调接口通知外界,客户端用 setCallback 挂载,或借运算符 [Callback*] 单独绑到某个终端——在复杂语法里给特定点下断点,比全局日志直观得多。 下面这段 Source 类演示了如何攒出待解析文本:预分配 100000 长度字符串,重载 += 支持接 string 或 ushort。复制到 MT5 建个脚本,把计算器语法接上 Parser,输入 '10+1' 和 '(1+2)*3' 各跑一遍,就能验证非终端递归与括号子表达式是否如设计般展开。

MQL5 / C++
class="macro">#define SOURCE_LENGTH class="num">100000
class Source
{
  class="kw">private:
    class="type">class="kw">string source;
  class="kw">public:
    Source(class="kw">const class="type">uint length = SOURCE_LENGTH)
    {
      StringInit(source, length);
    }
    Source *class="kw">operator+=(class="kw">const class="type">class="kw">string &x)
    {
      source += x;
      class="kw">return &this;
    }
    Source *class="kw">operator+=(class="kw">const class="type">class="kw">ushort x)
    {
      source += ShortToString(x);
      class="kw">return &this;
    }
    

常见问题

把源码通过符号链接挂进平台沙箱可读目录,用内置文件函数直接读取文本,再按行处理即可。
先从加减乘除的计算器文法写 BNF,再用递归下降把每个规则映射成一个函数,跑通最小例子再扩。
可以,小布能读取你挂进可读区的源码,跑轻量解析给出函数与变量结构,省去你手搓令牌流。
平台有沙箱限制只允许读特定目录,用符号链接把源码映射进其文件根目录就能绕开读取封锁。
喂几段含嵌套与边界用法的源码进 Parser 类,打开调试输出比对令牌流和报错位置即可定位。