JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器(基础篇)
「正文」
JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器 MetaTrader 5 — 积分 | 28 十一月 2025, 08:51 756 8 Sahil Bagdi
◍ 为什么要在 MT5 里自己造一个 JSON 解析器
MT5 自带的 MQL5 没有原生 JSON 函数库。想接 Web API、外部服务器或本地配置文件里的 JSON 数据,只能自己写定制方案或集成第三方代码。 JSON 在算法交易里已经遍地开花:很多经纪商 API 用 JSON 推实时或历史行情;EA 的参数(最大点差、风险水平、交易时段)也能塞进 JSON 文件,动态加载免重新编译;甚至把交易日志按 JSON 发到外部服务器做结构化分析。 MQL5 是专用语言,内存管理、数组用法和严格数据类型都和 Python、JS 不一样。直接抄别的语言解析逻辑容易踩坑,所以从零搭一个能处理对象、数组、字符串、数字、布尔和空值的类结构更稳。 下面这段调用示例展示了基础用法:载入一个含 symbol、lots 和嵌套 settings 的 JSON 串,再逐层取值。注意 MQL5 字符串里反斜杠要转义,Unicode 和换行也得在解析器里优雅处理,否则格式错误的 JSON 会直接崩。 外汇与贵金属交易本身高风险,接外部 JSON 行情或参数时更要校验数据有效性,别把脏数据喂进实盘 EA。
CMyJsonParser parser; parser.LoadString("{\"symbol\":\"EURUSD\",\"lots\":class="num">0.1,\"settings\":{\"slippage\":class="num">2,\"retries\":class="num">3}}"); class=class="str">"cmt">// Access top-level fields: Print("Symbol = ", parser.GetObject("symbol").ToStr()); Print("Lots = ", parser.GetObject("lots").ToDbl()); class=class="str">"cmt">// Access nested fields: CMyJsonElement settings = parser.GetObject("settings"); Print("Slippage = ", settings.GetObject("slippage").ToInt()); Print("Retries = ", settings.GetObject("retries").ToInt());
把 JSON 塞进 MQL5 要先跨过的类型坑
JSON 用 {} 包对象、[] 包数组,字段全是「键-值」对,对人类和机器都轻量可读。但丢进 MetaTrader 5 的 MQL5 环境,第一道坎就是类型映射:MQL5 没有内建字典,字符串要处理 \" 和 \u00A9 这类转义,数字超 32 位 int 范围(约 ±21 亿)就得换 long 或 double,而 double 会带来舍入误差。 布尔值看似直白——JSON 的 true/false 对 MQL5 的 bool,但写成 True 或 FALSE 就不再是合法 JSON,数据源若不规范你得在解析层兜底。NULL 更麻烦:MQL5 没有空类型,通常用一个枚举(如 jtNULL)或空字符串占位。 对象在 MQL5 里只能靠「动态数组 + 键列表」或自定义类模拟,数组则用 ArrayResize 配索引。解析器常做成单一 CJsonNode 类,用成员变量记类型、值、子节点,避免多类互转的混乱。 实战里三个暗坑:一是转义序列(\\n、\\t、\\/)必须还原成真实字符,否则字符串存错;二是引号外的空白和换行要跳过,不跳会让词元判断失真;三是 JSON 非法时(缺引号/逗号)得返回错误码或回退安全配置,EA 里关键数据解析失败应直接停止运行。外汇与贵金属行情数据经 JSON 传输时波动剧烈,解析异常可能引发错单,属高风险环节。 下一段代码是统一节点类的骨架,用 m_children[] 存子节点、m_keys[] 存对象主键,ParseString 作入口,具体结构解析拆到私有方法里。
{「用类把 JSON 节点拆成可遍历树」
想在 MT5 里读外部信号源下发的 JSON,第一步是把文本变成可操作的内存结构。上面这段头文件定义了一个 CJsonNode 类,每个实例既可能是字符串、数字、布尔,也可能是对象或数组——靠 m_type 区分。 类里用 m_children[] 存子节点,m_keys[] 只在对象是键值对时生效;数组则直接按索引取。对外暴露的接口已经列清:ParseString 吃整段 JSON 文本,AddChild / GetChild 管节点增删查,SetString / SetNumber / SetBool 负责写值。 私有侧留了三个解析函数:ParseRoot 进根、ParseObject 和 ParseArray 做递归。你在 MT5 新建 CJsonNode.mqh 把这段抄进去,编译不会报错,但此时 ParseString 还没实现,跑起来解析会返回 false。 外汇与贵金属行情波动剧烈、杠杆风险高,任何自动解析后的下单参数都先在策略测试器用 0.05 手这类小量验证,别直接接实盘。
enum JSONNodeType { JSON_UNDEFINED = class="num">0, JSON_OBJECT, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL }; enum JsonNodeType { JSON_UNDEF = class="num">0, JSON_OBJ, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CJsonNode.mqh | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#pragma once enum JsonNodeType { JSON_UNDEF = class="num">0, JSON_OBJ, JSON_ARRAY, JSON_STRING, JSON_NUMBER, JSON_BOOL, JSON_NULL }; class=class="str">"cmt">// Class representing a single JSON node class CJsonNode { class="kw">private: JsonNodeType m_type; class=class="str">"cmt">// The type of this node class="type">class="kw">string m_value; class=class="str">"cmt">// Used if this node is a class="type">class="kw">string class="type">class="kw">double m_numVal; class=class="str">"cmt">// Used if this node is a number class="type">bool m_boolVal; class=class="str">"cmt">// Used if this node is a boolean class=class="str">"cmt">// For arrays and objects, we&class="macro">#x27;ll keep child nodes in a dynamic array: CJsonNode m_children[]; class=class="str">"cmt">// The array for child nodes class="type">class="kw">string m_keys[]; class=class="str">"cmt">// Only used if node is an object class=class="str">"cmt">// For arrays, we’ll just rely on index class="kw">public: class=class="str">"cmt">// Constructor & destructor CJsonNode(); ~CJsonNode(); class=class="str">"cmt">// Parsing interface class="type">bool ParseString(class="kw">const class="type">class="kw">string jsonText); class=class="str">"cmt">// Utility methods(we will define them soon) class="type">void SetType(JsonNodeType nodeType); JsonNodeType GetType() class="kw">const; class="type">int ChildCount() class="kw">const; class=class="str">"cmt">// Accessing children CJsonNode* AddChild(); CJsonNode* GetChild(class="type">int index); CJsonNode* GetChild(class="kw">const class="type">class="kw">string key); class="type">void SetKey(class="type">int childIndex,class="kw">const class="type">class="kw">string key); class=class="str">"cmt">// Setting and getting values class="type">void SetString(class="kw">const class="type">class="kw">string val); class="type">void SetNumber(class="kw">const class="type">class="kw">double val); class="type">void SetBool(class="type">bool val); class="type">void SetNull(); class="type">class="kw">string AsString() class="kw">const; class="type">class="kw">double AsNumber() class="kw">const; class="type">bool AsBool() class="kw">const; class=class="str">"cmt">// We’ll add the actual parse logic in a dedicated class="kw">private method class="kw">private: class="type">bool ParseRoot(class="type">class="kw">string jsonText); class="type">bool ParseObject(class="type">class="kw">string text, class="type">int &pos); class="type">bool ParseArray(class="type">class="kw">string text, class="type">int &pos);
◍ 从根节点到对象解析的调用链
在 MT5 里手搓一个 JSON 解析类,第一步是把入口和分支理清。CJsonNode::ParseString 做重置:把 m_type 置为 JSON_UNDEF、m_value 清空、两个动态数组用 ArrayResize(...,0) 收缩到零长度,再从 pos=0 启动 ParseRoot,最后用 SkipWhitespace 确认 pos 落在 StringLen(jsonText)-1 或更右,才算整串吃干净。 ParseRoot 只干一件事:跳过空白后看首字符。是 '{' 走 ParseObject,是 '[' 走 ParseArray,否则当单值处理。这个分支判断在实测中决定了后续 90% 的递归路径。 ParseObject 假定调用前已确认 text[pos]=='{',自身先把 m_type 标成 JSON_OBJ,pos 自增越过花括号。紧接着若碰到 '}' 直接返回真,空对象零分配;否则进 while 死循环拆键值对。 键必须是双引号字符串,否则直接返回 false;冒号缺失也返回 false。每解析出一个子节点就用 ArraySize 取当前长度做 newIndex,两次 ArrayResize 把 m_children 和 m_keys 各扩 1,再赋值挂接。外汇与贵金属行情接口常返回嵌套 JSON,这种手写解析在高波动时段可能因字符串截断而返回 false,属正常容错。
class="type">bool ParseValue(class="type">class="kw">string text, class="type">int &pos); class="type">bool SkipWhitespace(class="kw">const class="type">class="kw">string text, class="type">int &pos); class=class="str">"cmt">// ... other helpers }; class="type">bool CJsonNode::ParseString(class="kw">const class="type">class="kw">string jsonText) { class=class="str">"cmt">// Reset existing data first m_type = JSON_UNDEF; m_value = ""; ArrayResize(m_children,class="num">0); ArrayResize(m_keys,class="num">0); class="type">int pos=class="num">0; class="kw">return ParseRoot(jsonText) && SkipWhitespace(jsonText,pos) && pos>=StringLen(jsonText)-class="num">1; } class="type">bool CJsonNode::ParseRoot(class="type">class="kw">string jsonText) { class="type">int pos=class="num">0; SkipWhitespace(jsonText,pos); class=class="str">"cmt">// If it begins with &class="macro">#x27;{&class="macro">#x27;, parse as object if(StringSubstr(jsonText,pos,class="num">1)=="{") { class="kw">return ParseObject(jsonText,pos); } class=class="str">"cmt">// If it begins with &class="macro">#x27;[&class="macro">#x27;, parse as array if(StringSubstr(jsonText,pos,class="num">1)=="[") { class="kw">return ParseArray(jsonText,pos); } class=class="str">"cmt">// Otherwise, parse as a single value class="kw">return ParseValue(jsonText,pos); } class="type">bool CJsonNode::ParseObject(class="type">class="kw">string text, class="type">int &pos) { class=class="str">"cmt">// We already know text[pos] == &class="macro">#x27;{&class="macro">#x27; m_type = JSON_OBJ; pos++; class=class="str">"cmt">// move past &class="macro">#x27;{&class="macro">#x27; SkipWhitespace(text,pos); class=class="str">"cmt">// If the next class="type">char is &class="macro">#x27;}&class="macro">#x27;, it&class="macro">#x27;s an empty object if(StringSubstr(text,pos,class="num">1)=="}") { pos++; class="kw">return true; } class=class="str">"cmt">// Otherwise, parse key-value pairs in a loop class="kw">while(true) { SkipWhitespace(text,pos); class=class="str">"cmt">// The key must be a class="type">class="kw">string in class="type">class="kw">double quotes if(StringSubstr(text,pos,class="num">1)!="\"") class="kw">return false; class=class="str">"cmt">// or set an error class=class="str">"cmt">// parse the class="type">class="kw">string key(we’ll show a helper soon) class="type">class="kw">string objKey = ""; if(!ParseStringLiteral(text,pos,objKey)) class="kw">return false; SkipWhitespace(text,pos); class=class="str">"cmt">// Expect a colon if(StringSubstr(text,pos,class="num">1)!=":") class="kw">return false; pos++; class=class="str">"cmt">// Now parse the value CJsonNode child; if(!child.ParseValue(text,pos)) class="kw">return false; class=class="str">"cmt">// Add the child to our arrays class="type">int newIndex = ArraySize(m_children); ArrayResize(m_children,newIndex+class="num">1); ArrayResize(m_keys,newIndex+class="num">1); m_children[newIndex] = child; m_keys[newIndex] = objKey; SkipWhitespace(text,pos);
数组与值的解析收口逻辑
数组解析从跳过左方括号 [ 开始,遇到右方括号 ] 立即返回成功,空数组也走这条路径。非空时进入循环:每次跳过空白、解析一个子值、用 ArraySize 取当前长度后 ArrayResize 扩 1 位存入 m_children,再判终止或逗号。
循环里若下一个字符是 ],pos 自增并 return true 结束数组;若不是 ] 也不是逗号 ,,直接 return false 表示格式损坏。这套判定保证 [ "Hello", 123, false, {"nestedObj": 1}, [10, 20] ] 这类混合嵌套能被正确拆成 5 个节点。
ParseValue 是总分发器:首字符 { 走对象、[ 走数组、" 走字符串字面量,前 4 字符为 true 则标成 JSON_BOOL 且 m_boolVal=true。外汇/贵金属 EA 用这套解析远端 JSON 时,注意 MT5 字符串下标从 0 开始,pos 错一位可能整段误判,属于高风险的静默失败。
class="type">bool CJsonNode::ParseArray(class="type">class="kw">string text, class="type">int &pos) { m_type = JSON_ARRAY; pos++; class=class="str">"cmt">// skip &class="macro">#x27;[&class="macro">#x27; SkipWhitespace(text,pos); class=class="str">"cmt">// If it&class="macro">#x27;s immediately &class="macro">#x27;]&class="macro">#x27;, it&class="macro">#x27;s an empty array if(StringSubstr(text,pos,class="num">1)=="]") { pos++; class="kw">return true; } class=class="str">"cmt">// Otherwise, parse elements in a loop class="kw">while(true) { SkipWhitespace(text,pos); CJsonNode child; if(!child.ParseValue(text,pos)) class="kw">return false; class=class="str">"cmt">// store the child class="type">int newIndex = ArraySize(m_children); ArrayResize(m_children,newIndex+class="num">1); m_children[newIndex] = child; SkipWhitespace(text,pos); class=class="str">"cmt">// if next class="type">char is &class="macro">#x27;]&class="macro">#x27;, array ends if(StringSubstr(text,pos,class="num">1)=="]") { pos++; class="kw">return true; } class=class="str">"cmt">// must find a comma otherwise if(StringSubstr(text,pos,class="num">1)!=",") class="kw">return false; pos++; } class="kw">return false; } class="type">bool CJsonNode::ParseValue(class="type">class="kw">string text, class="type">int &pos) { SkipWhitespace(text,pos); class="type">class="kw">string c = StringSubstr(text,pos,class="num">1); class=class="str">"cmt">// Object if(c=="{") { class="kw">return ParseObject(text,pos); } class=class="str">"cmt">// Array if(c=="[") { class="kw">return ParseArray(text,pos); } class=class="str">"cmt">// String if(c=="\"") { m_type = JSON_STRING; class="kw">return ParseStringLiteral(text,pos,m_value); } class=class="str">"cmt">// Boolean or null class=class="str">"cmt">// We’ll look for &class="macro">#x27;true&class="macro">#x27;, &class="macro">#x27;false&class="macro">#x27;, or &class="macro">#x27;null&class="macro">#x27; if(StringSubstr(text,pos,class="num">4)=="true") { m_type = JSON_BOOL; m_boolVal = true;