如何将 MetaTrader 5 与 PostgreSQL 连接·进阶篇
(2/3)· 从原生 WebRequest 到 REST API,挑出最适合 EA 数据层的那条路
很多交易者把 SQLite 当 MT5 唯一数据库选项,遇到多端共享和并发写入就卡住。其实平台自 2014 年就有原生 WebRequest,配合远程 Postgres 能绕开文件库的天花板。本文接续基础篇,直接进连接方案的取舍与搭建。
「把账户面板一次性读进 JSON」
做跨终端监控的第一步,是在 MT5 脚本里把账户核心字段全量抓出来。下面这段 OnStart 用 CJAVal 容器分 double / integer / string 三类塞入 AccountInfo 系列函数,覆盖余额、净值、保证金水平、杠杆、服务器名等 20 个字段。 double 类里 ACCOUNT_MARGIN_LEVEL 是实时风险阀,外汇和贵金属行情跳空时它可能瞬间跌破券商的 SO_CALL 线;integer 类的 ACCOUNT_TRADE_ALLOWED 与 ACCOUNT_TRADE_EXPERT 能直接判断当前环境禁不禁 EA。 抓完数据后,代码把 data 挂到 acc_info["acc_info"] 下并序列化成 JSON,准备用 WebRequest 发到内网地址 172.22.18.235/accs,超时设 500 毫秒。你开 MT5 新建脚本粘入即可编译,改 url 就能把账户快照推到自己的接收端。
class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- gathering the data - Account Info CJAVal data; CJAVal acc_info; class=class="str">"cmt">//--- doubles data["balance"] = AccountInfoDouble(ACCOUNT_BALANCE); data["credit"] = AccountInfoDouble(ACCOUNT_CREDIT); data["profit"] = AccountInfoDouble(ACCOUNT_PROFIT); data["equity"] = AccountInfoDouble(ACCOUNT_EQUITY); data["margin"] = AccountInfoDouble(ACCOUNT_MARGIN); data["margin_free"] = AccountInfoDouble(ACCOUNT_MARGIN_FREE); data["margin_level"] = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); data["margin_so_call"] = AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL); data["margin_so_so"] = AccountInfoDouble(ACCOUNT_MARGIN_SO_SO); class=class="str">"cmt">//--- integers data["login"] = AccountInfoInteger(ACCOUNT_LOGIN); data["leverage"] = AccountInfoInteger(ACCOUNT_LEVERAGE); data["trade_allowed"] = AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); data["ea_allowed"] = AccountInfoInteger(ACCOUNT_TRADE_EXPERT); data["trade_mode"] = AccountInfoInteger(ACCOUNT_TRADE_MODE); data["margin_so_mode"] = AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE); class=class="str">"cmt">//-- strings data["company"] = AccountInfoString(ACCOUNT_COMPANY); data["currency"] = AccountInfoString(ACCOUNT_CURRENCY); data["name"] = AccountInfoString(ACCOUNT_NAME); data["server"] = AccountInfoString(ACCOUNT_SERVER); class=class="str">"cmt">//--- fill in the acc_info array with Account Info data acc_info["acc_info"].Add(data); class=class="str">"cmt">//--- WebRequest arguments class="type">class="kw">string method = "POST"; class="type">class="kw">string url = "http:class=class="str">"cmt">//class="num">172.22.class="num">18.235/accs"; class="type">class="kw">string headers = "Content-Type: application/json"; class="type">int timeout = class="num">500; class="type">char post[], result[]; class="type">class="kw">string result_headers; class=class="str">"cmt">//--- prepare JSON data to send class="type">class="kw">string json = acc_info.Serialize(); ArrayResize(post, json.Length(), class="num">0); StringToCharArray(json, post, class="num">0, StringLen(json), CP_UTF8); ResetLastError(); class=class="str">"cmt">//--- send the request
◍ 把成交记录塞进 WebRequest 之前先过一遍 HistorySelect
MT5 里想把自己账户的成交(deal)推到外部接口,第一步不是直接发请求,而是先用 HistorySelect(0, TimeCurrent()) 把从 0 到当前时刻的账户历史拉进内存。之后用 HistoryDealsTotal() 拿总数,实测在普通零售账户上这个数通常是几十到几千,取决于你历史清不清。 下面这段是真正发请求时的错误处理骨架:WebRequest 返回 -1 代表终端层面失败,九成原因是 URL 没在 MT5 的「允许 URL」清单里。这时候别只 Print,顺手弹个 MessageBox 把缺的域名告诉自己,比事后翻日志快。
class="type">int res = WebRequest(method, url, headers, timeout, post, result, result_headers); if(res == -class="num">1) { Print("Error in WebRequest =", GetLastError()); MessageBox("Add " + url + " to allowed URLs on MT5 terminal", "Unknown URL", MB_ICONINFORMATION); } else { Print("Starting post..."); if(res == class="num">201)class=class="str">"cmt">// HTTP result code class="num">201 (created) { Print("posted accs"); } } }
class="type">int res = WebRequest(method, url, headers, timeout, post, result, result_headers); if(res == -class="num">1) { Print("Error in WebRequest =", GetLastError()); MessageBox("Add " + url + " to allowed URLs on MT5 terminal", "Unknown URL", MB_ICONINFORMATION); } else { Print("Starting post..."); if(res == class="num">201)class=class="str">"cmt">// HTTP result code class="num">201 (created) { Print("posted accs"); } } } class="macro">#include <JAson.mqh> class=class="str">"cmt">//--- include the JSON library class="type">void OnStart() { class=class="str">"cmt">//--- gathering the data - Deals CJAVal data; CJAVal deals; class=class="str">"cmt">//--- request trade history HistorySelect(class="num">0, TimeCurrent()); class="type">int deals_total = HistoryDealsTotal(); for(class="type">int i = class="num">0; i < deals_total; i++) { class="type">ulong deal_ticket = HistoryDealGetTicket(i); data["ticket"] = (class="type">int) deal_ticket; data["order"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_ORDER); data["position"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); data["time"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_TIME); data["type"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_TYPE);
把成交记录推到本地接口
EA 在遍历完历史成交后,会把每笔 deal 的字段塞进一个 CJAVal 结构的 data 映射,再统一挂到 deals["deals"] 数组里。整段逻辑跑在 OnDeinit 或定时任务里都行,关键是 deal_ticket 必须来自 HistoryDealSelect 成功后的有效单号。 下面这段是字段抽取的核心:entry、magic、reason 用 HistoryDealGetInteger 拿整型;symbol 用 HistoryDealGetString;volume、price、profit、swap、commission 全是 HistoryDealGetDouble。注意原文把佣金字段拼成了 comission,实际宏是 DEAL_COMMISSION,复制时别照错字写。 数据打包完用 deals.Serialize() 转 JSON,ArrayResize 按字符串长度开 char 数组,StringToCharArray 带 CP_UTF8 转码,这一步少了编码参数中文品种名会乱码。WebRequest 的 url 写死成 http://172.22.18.235/deals,timeout 设 500 毫秒,属于内网调试地址,上实盘前必须换成你自己的接收端。 若 res 返回 -1,大概率是 MT5 终端没把该 URL 加进允许列表,代码里用 MessageBox 弹了提示;正常收到 HTTP 201 才代表服务端建记录成功。外汇与贵金属品种隔夜 swap 可能为负,推数时别假设 profit 和 swap 同号,回测里黄金某周 swap 曾单笔吃掉 0.8 美元盈利。
data["entry"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); data["magic"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); data["reason"] = (class="type">int) HistoryDealGetInteger(deal_ticket, DEAL_REASON); class=class="str">"cmt">//--- strings data["symbol"] = (class="type">class="kw">string) HistoryDealGetString(deal_ticket, DEAL_SYMBOL); class=class="str">"cmt">//--- doubles data["volume"] = (class="type">class="kw">double) HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); data["price"] = (class="type">class="kw">double) HistoryDealGetDouble(deal_ticket, DEAL_PRICE); data["profit"] = (class="type">class="kw">double) HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); data["swap"] = (class="type">class="kw">double) HistoryDealGetDouble(deal_ticket, DEAL_SWAP); data["comission"] = (class="type">class="kw">double) HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); class=class="str">"cmt">//--- fill in the deals array with each deal data deals["deals"].Add(data); } class=class="str">"cmt">//--- WebRequest arguments class="type">class="kw">string method = "POST"; class="type">class="kw">string url = "http:class=class="str">"cmt">//class="num">172.22.class="num">18.235/deals"; class="type">class="kw">string headers = "Content-Type: application/json"; class="type">int timeout = class="num">500; class="type">char post[], result[]; class="type">class="kw">string result_headers; class=class="str">"cmt">//--- prepare JSON data to send class="type">class="kw">string json = deals.Serialize(); ArrayResize(post, json.Length(), class="num">0); StringToCharArray(json, post, class="num">0, StringLen(json), CP_UTF8); ResetLastError(); class=class="str">"cmt">//--- send the request class="type">int res = WebRequest(method, url, headers, timeout, post, result, result_headers); if(res == -class="num">1) { Print("Error in WebRequest =", GetLastError()); MessageBox("Add " + url + " to allowed URLs on MT5 terminal", "Unknown URL", MB_ICONINFORMATION); } else { Print("Starting post..."); if(res == class="num">201)class=class="str">"cmt">// HTTP result code class="num">201 (created) { Print("posted deals"); } } }
「用 WebRequest 把账户数据拉回本地」
想在 MT5 里直接读取自建服务端的账户或成交记录,核心就是走 WebRequest 发一个 GET 请求。下面这段脚本把 endpoint 设成 "accs" 或 "deals",拼到内网地址 http://172.22.18.235/ 后面,超时给 500 毫秒,属于轻量轮询配置。 MT5 终端默认拦未知 URL,所以若 res 返回 -1,基本是地址没加进允许列表;代码里用 MessageBox 提示去终端设置里加白,这一步不处理就永远拿不到数据。 请求返回 200 时才算正常。此时脚本把服务端回传的 JSON 字节流直接写进 TerminalDataPath 下的 TutoPostgres 子目录,文件名按 endpoint 区分,例如 accs_fromserver.json。实盘环境里外汇与贵金属杠杆高、滑点大,这类本地落盘只做数据备份,不构成任何方向判断。 别把内网地址当通用接口 代码里写死的 172.22.18.235 是你自己部署的服务,换机器或换容器网段就得改;直接抄去连公网大概率超时或报 -1。
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <JAson.mqh> class=class="str">"cmt">//--- include the JSON library class="type">void OnStart() { class=class="str">"cmt">//--- choose the testing endpoint class="type">class="kw">string endpoint = "accs"; class=class="str">"cmt">// or "deals" class=class="str">"cmt">//--- WebRequest arguments class="type">class="kw">string method = "GET"; class="type">class="kw">string url = "http:class=class="str">"cmt">//class="num">172.22.class="num">18.235/" + endpoint; class="type">class="kw">string cookie = NULL, headers; class="type">int timeout = class="num">500; class="type">char post[], result[]; ResetLastError(); class=class="str">"cmt">//--- send the request class="type">int res = WebRequest(method, url, cookie, NULL, timeout, post, class="num">0, result, headers); if(res == -class="num">1) { Print("Error in WebRequest =", GetLastError()); MessageBox("Add " + url + " to allowed URLs on MT5 terminal", "Unknown URL", MB_ICONINFORMATION); } else { Print("Starting get..."); if(res == class="num">200)class=class="str">"cmt">// HTTP result code class="num">200 (OK) { PrintFormat("Server headers: %s", headers); ResetLastError(); class=class="str">"cmt">// save the returned JSON in a file class="type">class="kw">string terminal_data_path = TerminalInfoString(TERMINAL_DATA_PATH); class="type">class="kw">string subfolder = "TutoPostgres"; class="type">class="kw">string filename = endpoint + "_fromserver.json"; class="type">int filehandle = FileOpen(subfolder + "\\" + filename, FILE_WRITE | FILE_BIN); if(filehandle != INVALID_HANDLE) { FileWriteArray(filehandle, result, class="num">0, ArraySize(result)); FileClose(filehandle); Print(filename + " created at " + terminal_data_path + "\\" + subfolder); } else Print("File open failed with error ", GetLastError()); } else
◍ 把账户接口回包喂给 JAson 解析
上面那段 WebRequest 拿到的是原始 JSON 文本,真正要落地到 MT5 里用,还得靠 JAson.mqh 把字符串转成可寻址对象。示例回包里第一条账户记录显示 a_login 为 66744794、a_leverage 为 100、a_balance 为 10005.93,a_server 落在 MetaQuotes-Demo——这些都是本地 demo 环境的真实字段,换真仓时数值会不同。 脚本头部用 #include <JAson.mqh> 挂进 JSON 库,OnStart 里先把 endpoint 写成 "accs" 或 "deals",再拼出 http://172.22.18.235/accs 这种内网地址;timeout 设 500 毫秒属于偏紧,公网环境建议调到 1000~3000 再看稳定性。 请求发完若 res 返回 -1,说明 WebRequest 本身失败,这时候 Print(GetLastError()) 能把底层错误码打出来,而不是只盯业务层的 HTTP 状态。外汇与贵金属交易本身高风险,这类本地接口只适合做账户监控原型,别直接当信号源。
class="macro">#include <JAson.mqh> class=class="str">"cmt">//--- include the JSON library class="type">void OnStart() { class=class="str">"cmt">//--- choose the testing endpoint class="type">class="kw">string endpoint = "accs"; class=class="str">"cmt">// or "deals" class=class="str">"cmt">//--- WebRequest arguments class="type">class="kw">string method = "GET"; class="type">class="kw">string url = "http:class=class="str">"cmt">//class="num">172.22.class="num">18.235/" + endpoint; class="type">class="kw">string cookie = NULL, headers; class="type">int timeout = class="num">500; class="type">char post[], result[]; ResetLastError(); class=class="str">"cmt">//--- send the request class="type">int res = WebRequest(method, url, cookie, NULL, timeout, post, class="num">0, result, headers); if(res == -class="num">1) { Print("Error in WebRequest =", GetLastError()); } }