如何将 MetaTrader 5 与 PostgreSQL 连接·进阶篇
🐘

如何将 MetaTrader 5 与 PostgreSQL 连接·进阶篇

(2/3)· 从原生 WebRequest 到 REST API,挑出最适合 EA 数据层的那条路

实战向进阶 第 2/3 篇

很多交易者把 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 就能把账户快照推到自己的接收端。

MQL5 / C++
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 把缺的域名告诉自己,比事后翻日志快。

MQL5 / C++
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");
    }
  }
}
逐行拆:第 1 行调 WebRequest 发 HTTP,result 存响应体;res==-1 即终端拦截,第 4 行打错误码,第 5 行弹窗提示去终端设置加白名单;res==201 是服务端返回「已创建」,说明成交数据落地成功。外汇和贵金属杠杆高,把真实成交往外推之前,先确认接收端是你自己可控的服务器,避免敏感账户数据外泄。 真正收集字段时,要在 for 循环里用 HistoryDealGetTicket(i) 拿每个成交的 ticket,再喂给 HistoryDealGetInteger 取 DEAL_ORDER、DEAL_POSITION_ID、DEAL_TIME、DEAL_TYPE 等整数字段,转成 int 后写进 CJAVal 这样的 JSON 对象,后面才能序列化 POST 出去。

MQL5 / C++
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 美元盈利。

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

MQL5 / C++
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 状态。外汇与贵金属交易本身高风险,这类本地接口只适合做账户监控原型,别直接当信号源。

MQL5 / C++
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());
     }
  }
让小布替你盯数据通道
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 与远程库之间的延迟和写入失败计数,你专注策略逻辑。

常见问题

SQLite 适合单机缓存与本地参数表,PostgreSQL 适合跨终端共享行情快照和交易日志,二者可通过 WebRequest 同步。
可能。建议把数据库读写放到独立线程或定时器,避免阻塞 OnTick,高频场景倾向异步批处理。
概率上小型实盘可行,但外汇贵金属属高风险,免费层有速率限制和休眠策略,生产用建议最低付费计划。
可以。小布支持接入外部库视图,把 EA 写入的持仓表映射成监控卡,触发条件后推送,不用自己写轮询。
自己搭中间网关转发二进制协议的方案维护成本最高,进阶用户也倾向直接用托管 REST 层省事。