JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器(基础篇)
📘

JSON 从入门到精通: 创建自己的 MQL5 版本 JSON 解读器(基础篇)

第 1/3 篇

「正文」

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。

MQL5 / C++
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 作入口,具体结构解析拆到私有方法里。

MQL5 / C++
{

「用类把 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 手这类小量验证,别直接接实盘。

MQL5 / C++
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,属正常容错。

MQL5 / C++
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 错一位可能整段误判,属于高风险的静默失败。

MQL5 / C++
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;

常见问题

因为平台自带环境没有内置 JSON 解析,需自己写解析器把字符串转成可访问的对象树,否则只能硬匹配文本易出错。
先在建树阶段用明确类型标记每个节点(如 string/number/object),解析时按标记分支处理,避免后期强制转换丢精度。
可以,小布能直接对接品种页的接口数据,自动解析 JSON 并高亮你关心的字段,省去自己写解析器的重复劳动。
用类把每个 JSON 节点封装成树节点,从根节点递归向下按 key 访问,数组和值统一在末端收口,结构清晰易维护。
在数组解析收口处逐个读元素类型标记,数值直接存、对象继续递归建子树,按标记分流就不会漏解析。