开发具有 RestAPI 集成的 MQL5 强化学习代理(第 4 部分):在 MQL5 中组织类中的函数(基础篇)
「把 RestAPI 请求塞进 MQL5 类里」
在 MQL5 里做强化学习代理,绕不开和 Python 训练端通信,而通信骨架最好直接收进一个类,而不是散在 EA 的 OnTick 里。把 RestAPI 相关函数组织成类方法,EA 只负责调 agent.Act(),逻辑边界会清楚很多。
一个最小可用的类结构通常包含:初始化时建 socket、发送状态向量、接收动作、处理超时。下面这段是原文给出的基础类骨架,20 行出头,能在 MT5 里直接编译跑通。
外汇与贵金属杠杆高,这类自动代理若接实盘,单笔失误可能放大成大幅回撤,务必先在策略测试器用历史数据验证通信稳定性。
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 对接本身高风险,代码不一致可能发错单或漏风控。
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 逻辑接到你自己的解析前,能少踩很多接口坑。外汇与贵金属行情接口调用受网络与经纪商限制,失败概率不低,需以实际返回码为准。
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 参数能不能正确发出。
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 时倾向先熔断而不是重试。
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