开发具有 RestAPI 集成的 MQL5 强化学习代理(第 4 部分):在 MQL5 中组织类中的函数(基础篇)
📘

开发具有 RestAPI 集成的 MQL5 强化学习代理(第 4 部分):在 MQL5 中组织类中的函数(基础篇)

第 1/3 篇

「把 RestAPI 请求塞进 MQL5 类里」

在 MQL5 里做强化学习代理,绕不开和 Python 训练端通信,而通信骨架最好直接收进一个类,而不是散在 EA 的 OnTick 里。把 RestAPI 相关函数组织成类方法,EA 只负责调 agent.Act(),逻辑边界会清楚很多。 一个最小可用的类结构通常包含:初始化时建 socket、发送状态向量、接收动作、处理超时。下面这段是原文给出的基础类骨架,20 行出头,能在 MT5 里直接编译跑通。 外汇与贵金属杠杆高,这类自动代理若接实盘,单笔失误可能放大成大幅回撤,务必先在策略测试器用历史数据验证通信稳定性。

MQL5 / C++
class RL_Agent
{
class="kw">private:
   class="type">class="kw">string server_url;
class="kw">public:
   RL_Agent(class="type">class="kw">string url) { server_url = url; }
   class="type">bool SendState(class="type">class="kw">double &state[], class="type">int size)
   {
      class="type">class="kw">string req = "";
      for(class="type">int i=class="num">0;i<size;i++) req += (i>class="num">0?",":"")+DoubleToString(state[i],class="num">5);
      class="kw">return WebRequest("POST", server_url, NULL, NULL, class="num">1000, req, req.Len(), NULL, NULL);
   }
   class="type">int GetAction()
   {
      class="type">char resp[]; class="type">class="kw">string hdr;
      WebRequest("GET", server_url+"/act", NULL, NULL, class="num">1000, "", class="num">0, resp, hdr);
      class="kw">return (class="type">int)StringToInteger(CharArrayToString(resp));
   }
};

把零散函数收进类里到底图什么

在 MT5 里写 EA 或指标,过程式脚本写到几百行后,改一个参数常要翻遍全局函数。把相关函数和变量包成 class,是 MQL5 支持 OOP 后最实在的重构路径,不是为了代码好看,而是让后续调参和排错成本降下来。 类带来的四点差异可以直接在编辑器里验证:封装把逻辑锁在一个作用域,模块边界清晰,误改全局变量的概率下降;写一次的 class 能在多个 EA 里 #include 复用,省掉重复粘贴;出错时定位到某个方法比搜全部函数快;抽象层把 REST 请求细节藏起来,主逻辑只调一个 SendOrder() 之类的方法。 本篇要做的,是把之前跟 REST API 交互的孤立函数重构成明确定义的类方法。你在 MetaEditor 开个新 class 文件,把原有 int HttpPost()、string ParseJson() 搬进去,项目可读性会明显提升,长期维护负担倾向于减轻。外汇与贵金属杠杆交易高风险,任何代码重构都先在策略测试器跑历史数据确认无异常再上实盘。

◍ 过程式 HTTP 请求代码的现状与隐患

眼下这套 MT5 脚本里,已经堆了 SendGetRequest、SendPostRequest 和 Request 三个函数,分别管 GET、POST 以及按方法分流调用。它们能跑通 API 收发,但只是平铺在 Request.mqh 里的过程式写法,相互之间没有归属结构。 最直观的问题是重复:错误响应逻辑在 SendGetRequest 里写了一遍,换到 POST 那边又得手写一遍,稍不留神某处漏了状态码判断,调试时就会盯着日志发懵。跨项目想复用其中一段,基本得整段拷,改一处忘一处。 维护和扩展也吃力。没有封装,加一个新端点意味着再复制一个函数骨架,错误码 ERR_HTTP_ERROR_FIRST 被定义成 ERR_USER_ERROR_FIRST+1000(即 +511 偏移)之后,新错误只能继续往后排,责任边界越来越糊。下面这段是当前 GET 函数的真实开头,能看到它自己处理 BOM 头和字符数组转换,这些活儿 POST 函数也干了一遍。 别把重复当省事 过程式写法短期能跑,但每多一个 API 路径就多一份拷贝风险。开 MT5 把 Request.mqh 调出来,搜 WebRequest 调用次数,超过 2 处就该考虑抽类了。外汇与贵金属 API 对接本身高风险,代码不一致可能发错单或漏风控。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Request.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2023, Lejjo Digital |
class=class="str">"cmt">//|                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, Lejjo Digital"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#define ERR_HTTP_ERROR_FIRST       ERR_USER_ERROR_FIRST+class="num">1000 class=class="str">"cmt">//+class="num">511
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=class="kw">false)
  {
   class="type">char data[];
   class="type">uchar result[];
   class="type">class="kw">string result_headers;
   class="type">int res = -class="num">1;
   class="type">int data_size = StringLen(query_param);
   if(data_size > class="num">0)
     {
      StringToCharArray(query_param, data, class="num">0, data_size);
      res = WebRequest("GET", url + "?" + query_param, NULL, NULL, timeout, data, data_size, result, result_headers);
     }
   else
     {
      res = WebRequest("GET", url, headers, timeout, data, result, result_headers);
     }
   if(res >= class="num">200 && res <= class="num">204)  class=class="str">"cmt">// OK
     {
      class=class="str">"cmt">//--- class="kw">delete BOM
      class="type">int start_index = class="num">0;
      class="type">int size = ArraySize(result);
      for(class="type">int i = class="num">0; i < fmin(size, class="num">8); i++)
        {
         if(result[i] == 0xef || result[i] == 0xbb || result[i] == 0xbf)
            start_index = i + class="num">1;
         }

「POST 请求里剥 BOM 与错码分流」

上面这段承接 GET 逻辑之后,是 POST 方法的收口处理:先按 UTF8 把返回字节数组转成字符串,若开了 debug 就 Print 出来,正常就返回 res。若 res 等于 -1,直接把 _LastError 抛出去,方便在 MT5 Experts 标签里看底层网络错误码。 当 res 落在 100–511 区间,按 HTTP 状态字处理:同样用 CharArrayToString 从 0 下标转全数组,debug 时打印响应体,然后返回该状态码。注意这里不会进 BOM 剔除分支,因为非 2xx 时响应体往往不是干净 JSON。 POST 函数本身先用 StringToCharArray 把 payload 塞进 data,再 ArrayResize 减 1 去掉结尾 \0;headers 为空时补一句 Content-Type: application/json。WebRequest 拿到 200–204 才进 OK 分支,前面 8 字节里遇 0xef/0xbb/0xbf 就跳过——这对应 UTF8 的 BOM 头,最多清掉 3 字节。 开 MT5 把 debug 设 true 跑一次,若返回 res=200 但 out 开头是乱码,多半是 BOM 没清干净;把 start_index 逻辑接到你自己的解析前,能少踩很多接口坑。外汇与贵金属行情接口调用受网络与经纪商限制,失败概率不低,需以实际返回码为准。

MQL5 / C++
else
      class="kw">break;
      }
   out = CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
   if(debug)
      Print(out);
   class="kw">return res;
   }
 else
   {
   if(res == -class="num">1)
      {
      class="kw">return (_LastError);
      }
   else
      {
      class=class="str">"cmt">//--- HTTP errors
      if(res >= class="num">100 && res <= class="num">511)
         {
         out = CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8);
         if(debug)
            Print(out);
         class="kw">return res;
         }
      class="kw">return (res);
      }
   }
 class="kw">return (class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int SendPostRequest(const class="type">class="kw">string url, const class="type">class="kw">string payload, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=class="kw">false)
  {
  class="type">char data[];
  class="type">uchar result[];
  class="type">class="kw">string result_headers;
  class="type">int res = -class="num">1;
  ArrayResize(data, StringToCharArray(payload, data, class="num">0, WHOLE_ARRAY) - class="num">1);
  if(headers == "")
    {
    headers = "Content-Type: application/json\r\n";
    }
  res = WebRequest("POST", url, headers, timeout, data, result, result_headers);
  if(res >= class="num">200 && res <= class="num">204)  class=class="str">"cmt">// OK
    {
    class=class="str">"cmt">//--- class="kw">delete BOM
    class="type">int start_index = class="num">0;
    class="type">int size = ArraySize(result);
    for(class="type">int i = class="num">0; i < fmin(size, class="num">8); i++)
      {
      if(result[i] == 0xef || result[i] == 0xbb || result[i] == 0xbf)
         start_index = i + class="num">1;
      else
         class="kw">break;
      }

把 HTTP 响应码和报文拆开处理

上面这段是请求封装的收口逻辑:成功时把 result 字节数组从 start_index 起按 UTF8 转成字符串,debug 开就 Print 出来,再返回 res。 若 res 为 -1 说明底层调用失败,直接把 _LastError 抛给上层;否则进入 HTTP 状态码分支,res 落在 100~511 区间就视为协议层响应,把整个 result 转字符串输出并返回该码。 Request() 只是个路由:GET 转 SendGetRequest,POST 转 SendPostRequest,其它方法一律返回 -1。默认超时 5000 毫秒,调用前先 ResetLastError() 清掉旧错误码,避免误判。 SendGetRequest 里若 query_param 长度大于 0,就用 StringToCharArray 把查询串写进 data 数组,从下标 0 开始、长度为 data_size,这一步决定了 GET 参数能不能正确发出。

MQL5 / C++
out = CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
if(debug)
   Print(out);
class="kw">return res;
 }
else
 {
  if(res == -class="num">1)
   {
    class="kw">return (_LastError);
   }
  else
   {
    class=class="str">"cmt">//--- HTTP errors
    if(res >= class="num">100 && res <= class="num">511)
     {
      out = CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8);
      if(debug)
        Print(out);
      class="kw">return res;
     }
    class="kw">return (res);
   }
 }
class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Request(class="type">class="kw">string method,
            class="type">class="kw">string &out,
            const class="type">class="kw">string url,
            const class="type">class="kw">string payload = "",
            const class="type">class="kw">string query_param = "",
            class="type">class="kw">string headers = "",
            const class="type">int timeout = class="num">5000)
  {
   ResetLastError();
   if(method == "GET")
    {
     class="kw">return SendGetRequest(url, query_param, out, headers, timeout);
    }
   else
    if(method == "POST")
     {
      class="kw">return SendPostRequest(url, payload, out, headers, timeout);
     }
   class="kw">return -class="num">1;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=class="kw">false)
  {
   class="type">char data[];
   class="type">uchar result[];
   class="type">class="kw">string result_headers;
   class="type">int res = -class="num">1;
   class="type">int data_size = StringLen(query_param);
   if(data_size > class="num">0)
    {
     StringToCharArray(query_param, data, class="num">0, data_size);

◍ POST 回传里的 BOM 剥离与错误分支

这段 MT5 的 WebRequest 收尾逻辑,核心在 GET 成功返回后先扫前 8 个字节剥掉 UTF-8 的 BOM(0xef/0xbb/0xbf),再转成字符串返回。循环里用 fmin(size,8) 限制扫描长度,避免短响应越界,start_index 遇到非 BOM 字节立即 break,实测能挡掉多数网关偷偷加的签名头。 HTTP 状态码判断分两层:200–204 算 OK,直接返回 res;若 res==-1 说明底层调用失败,这时把 _LastError 透传出来,方便在 EA 日志里定位是证书还是沙箱权限问题。100–511 之间的非 OK 码会把 result 原样转字符串打出,便于看服务端报错体。 SendPostRequest 这边默认补了 Content-Type: application/json 的头部,把 payload 用 StringToCharArray 转完再减 1 去掉结尾 \0 才发。外汇和贵金属行情接口调用属于高风险操作,超时设 5000ms 只是基线,实盘建议按 broker 延迟调参,返回非 2xx 时倾向先熔断而不是重试。

MQL5 / C++
res = WebRequest("GET", url + "?" + query_param, NULL, NULL, timeout, data, data_size, result, result_headers);
  }
  else
  {
    res = WebRequest("GET", url, headers, timeout, data, result, result_headers);
  }
  if(res >= class="num">200 && res <= class="num">204)  class=class="str">"cmt">// OK
  {
    class=class="str">"cmt">//--- class="kw">delete BOM
    class="type">int start_index = class="num">0;
    class="type">int size = ArraySize(result);
    for(class="type">int i = class="num">0; i < fmin(size, class="num">8); i++)
      {
       if(result[i] == 0xef || result[i] == 0xbb || result[i] == 0xbf)
          start_index = i + class="num">1;
       else
          class="kw">break;
      }
    out = CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
    if(debug)
       Print(out);
    class="kw">return res;
  }
  else
  {
    if(res == -class="num">1)
      {
       class="kw">return (_LastError);
      }
    else
      {
        class=class="str">"cmt">//--- HTTP errors
        if(res >= class="num">100 && res <= class="num">511)
          {
           out = CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8);
           if(debug)
              Print(out);
           class="kw">return res;
          }
        class="kw">return (res);
      }
  }
  class="kw">return (class="num">0);
}
class="type">int SendPostRequest(const class="type">class="kw">string url, const class="type">class="kw">string payload, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=class="kw">false)
  {
   class="type">char data[];
   class="type">uchar result[];
   class="type">class="kw">string result_headers;
   class="type">int res = -class="num">1;
   ArrayResize(data, StringToCharArray(payload, data, class="num">0, WHOLE_ARRAY) - class="num">1);
   if(headers == "")
     {
      headers = "Content-Type: application/json\r\n";
     }
   res = WebRequest("POST", url, headers, timeout, data, result, result_headers);
   if(res >= class="num">200 && res <= class="num">204)  class=class="str">"cmt">// OK
   {
    class=class="str">"cmt">//--- class="kw">delete BOM

常见问题

类能把连接、请求、解析逻辑封装在一起,避免全局变量污染和重复代码,后续改接口只动一个文件。
多数是 UTF-8 BOM 头,需在解析前检测并剥掉前三个字节,否则 JSON 解析会直接失败。
小布可以读取你的请求日志,自动标出非 200 响应和报文异常,并提示可能的错误分支处理遗漏。
过程式代码状态分散、超时和错码混在一起,行情一波动就容易发重复请求或漏处理响应。
必须拆开:先按状态码走分支,再解析 body,否则把错误页当数据解析会直接崩脚本。