构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端(基础篇)
◍ 用 MQL5 手搓一个 REST 客户端
在 MT5 里直接对接外部 Web 服务,是社交交易与跨平台信号聚合的常见需求。MQL5 标准库并未内置高级 HTTP 客户端,但借助 WinAPI 的 InternetOpen 系列函数,可以在 EA 或脚本中发起 REST 请求。
- 年 9 月 11 日发布的这篇技术文,在 MetaTrader 5 专栏获得了 2352 次浏览,说明当时社区对「终端内直连 REST」的渴求很真实。即便放在现在,原生实现依然比依赖第三方 DLL 更可控。
核心思路是:先调用 InternetOpen 建立会话,再用 InternetOpenUrl 打目标地址,最后用 InternetReadFile 循环读响应体。外汇与贵金属行情接口若走这套,需注意网络延迟与断线重连,这类品种杠杆高、滑点风险大,任何自动拉取都只是辅助判断。
「从架构到客户端:MQL5 怎么把信号送出去」
上一阶段我们已经搭好一套社交决策支持系统(SDSS)的骨架:MT5 客户端负责把 EA 的自动决策发往服务器,另一端是用瘦 PHP 框架写的 Twitter 应用,信号进 MySQL 后再推给相关人。它的核心目的不是全自动下单,而是把机器产生的交易信号摊在大量专业观众面前,由人来做最终执行判断。 本篇要做的,是用 MQL5 把这套体系的客户端真正写出来。我们会顺带比较几种可行的替代实现路径,把各自的利弊摊开,再把这些零散模块拼起来,最终塑出能接收 EA 信号的 PHP REST API。 想立刻动手验证的话,打开 MT5 的 MQL5 编辑器新建 EA,先确认你本地能向一个测试用 REST 接口 POST 数据——这一步通了,后面整条信号链路才算立住。外汇与贵金属市场波动剧烈、杠杆风险高,任何信号展示系统都只是辅助,人工决策仍需自行承担后果。
用 OnTimer 把信号推给远端 API
在 MT5 里做信号中转,最省事的办法是把 HTTP 通信交给 MQL5 自带的 WebRequest,而不是自己写底层控件。让 MetaQuotes 的语言特性去管会话,比手写 wininet.dll 方案更稳——后者一旦终端升级,调用就可能直接崩。 下面这段客户端每 10 秒触发一次 OnTimer,向远端接口 POST 一条 URL 编码的信号。注意 WebRequest 默认按 HTML 表单发,所以我们把信号拼成 id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281& 这种格式,服务端再转成内部对象。
class="macro">#class="kw">property copyright "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate class="num">3.0" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property description "Simple REST client built on the OnTimer event for learning purposes." class="type">int OnInit() { EventSetTimer(class="num">10); class="kw">return(class="num">0); } class="type">void OnDeinit(class="kw">const class="type">int reason) { } class="type">void OnTimer() { class=class="str">"cmt">//--- REST client&class="macro">#x27;s HTTP vars class="type">class="kw">string uri="http:class=class="str">"cmt">//api.laplacianlab.com/signal/add"; class="type">char post[]; class="type">char result[]; class="type">class="kw">string headers; class="type">int res; class="type">class="kw">string signal = "id_ea=class="num">1&symbol=AUDUSD&operation=BUY&value=class="num">0.9281&"; StringToCharArray(signal,post); class=class="str">"cmt">//--- reset last error ResetLastError(); class=class="str">"cmt">//--- post data to REST API res=WebRequest("POST",uri,NULL,NULL,class="num">50,post,ArraySize(post),result,headers); class=class="str">"cmt">//--- check errors if(res==-class="num">1) { Print("Error code =",GetLastError()); MessageBox("Add address &class="macro">#x27;"+uri+"&class="macro">#x27; in Expert Advisors tab of the Options window","Error",MB_ICONINFORMATION); } else { class=class="str">"cmt">//--- successful Print("REST client&class="macro">#x27;s POST: ",signal); Print("Server response: ",CharArrayToString(result,class="num">0,-class="num">1)); } }
class="macro">#class="kw">property copyright "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate class="num">3.0" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property description "Simple REST client built on the OnTimer event for learning purposes." class="type">int OnInit() { EventSetTimer(class="num">10); class="kw">return(class="num">0); } class="type">void OnDeinit(class="kw">const class="type">int reason) { } class="type">void OnTimer() { class=class="str">"cmt">//--- REST client&class="macro">#x27;s HTTP vars class="type">class="kw">string uri="http:class=class="str">"cmt">//api.laplacianlab.com/signal/add"; class="type">char post[]; class="type">char result[]; class="type">class="kw">string headers; class="type">int res; class="type">class="kw">string signal = "id_ea=class="num">1&symbol=AUDUSD&operation=BUY&value=class="num">0.9281&"; StringToCharArray(signal,post); class=class="str">"cmt">//--- reset last error ResetLastError(); class=class="str">"cmt">//--- post data to REST API res=WebRequest("POST",uri,NULL,NULL,class="num">50,post,ArraySize(post),result,headers); class=class="str">"cmt">//--- check errors if(res==-class="num">1) { Print("Error code =",GetLastError()); MessageBox("Add address &class="macro">#x27;"+uri+"&class="macro">#x27; in Expert Advisors tab of the Options window","Error",MB_ICONINFORMATION); } else { class=class="str">"cmt">//--- successful Print("REST client&class="macro">#x27;s POST: ",signal); Print("Server response: ",CharArrayToString(result,class="num">0,-class="num">1)); } }
◍ 服务端时间窗拦截了重复发单
上面这段 MT5 日志来自一个挂在 EURUSD H1 图表上的 RESTClient 脚本,它不断向远端服务提交 AUDUSD 的 BUY 信号,价格锚定 0.9281,EA 编号 id_ea=1。从 15:43:54 到 15:45:04,每隔约 10 秒就 POST 一次同样内容,但服务端回了 11 次 Please wait until the time window has elapsed.,只在 15:43:45 和 15:44:54 两次真正写入了 id 77、id 78 两条信号。
服务端并不是收到就存,而是用了一个最小时间间隔闸门。底下那段 PHP 路由 /signal/add 里的 canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute') 就是关键:它拿上一条信号时间和当前比,若不足 1 分钟就直接拒绝。日志里两次成功插入相差 69 秒,正好跨过了这个窗口。
别把这种限流当摆设。外汇和贵金属波动快、滑点多,若 EA 在 10 秒级疯狂重发同一方向单,服务端拦得住,但本地已经吃掉来回延迟和连接开销;把重发间隔在 EA 侧就设成 ≥60 秒,比等远端驳回更干净。
下面这段是服务端接收信号的接口骨架,注意注释里写明了「prevent hyperactive scalpers」——限流本来就是防过度交易的:
/**
- REST method.
- Adds and tweets a new trading signal.
*/ $app->post('/signal/add', function() { $tweeterer = new Tweeterer(); // This condition is a simple mechanism to prevent hyperactive scalpers if ($tweeterer->canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute')) { $signal = (object)($_POST); $signal->id = $tweeterer->addSignal(1, $signal); $tokens = $tweeterer->getTokens(1); $connection = new TwitterOAuth( API_KEY, API_SECRET, $tokens->access_token, $tokens->access_token_secret); $connection->host = "https://api.twitter.com/1.1/"; $ea = new EA();
class=class="str">"cmt">/** * REST method. * Adds and tweets a new trading signal. */ $app->post(&class="macro">#x27;/signal/add&class="macro">#x27;, function() { $tweeterer = new Tweeterer(); class=class="str">"cmt">// This condition is a simple mechanism to prevent hyperactive scalpers if ($tweeterer->canTweet($tweeterer->getLastSignal(class="num">1)->created_at, &class="macro">#x27;class="num">1 minute&class="macro">#x27;)) { $signal = (object)($_POST); $signal->id = $tweeterer->addSignal(class="num">1, $signal); $tokens = $tweeterer->getTokens(class="num">1); $connection = new TwitterOAuth( API_KEY, API_SECRET, $tokens->access_token, $tokens->access_token_secret); $connection->host = "https:class=class="str">"cmt">//api.twitter.com/class="num">1.1/"; $ea = new EA();
「信号推送的频控与服务器防刷配置」
把 EA 信号往外推时,最容易被忽略的是频率控制。上面这段 PHP 逻辑用 canTweet() 卡住了最小发声间隔:1 分钟窗口对应 60 秒、1 小时对应 3600 秒、1 天对应 86400 秒,未到窗口就直接抛异常而不是静默吞掉,能避免同一条黄金突破信号在 MT5 触发后被刷成几十条推文。 服务器端也不能裸奔。mod_evasive20 那段配置把 DOSPageCount 设为 2、DOSSiteCount 设为 50,页级间隔和站级间隔都是 1 秒,命中后封锁 60 秒——这对接了公开 API 的信号中转站算是基础门槛,外汇和贵金属行情波动剧烈,接口被刷会影响你跟单的时效性,这类资产本身杠杆高风险也大。 真正落地时,把时间窗从『1 hour』改成你策略的平均持仓周期更合理。例如剥头皮 EA 平均 20 分钟平仓,硬卡 3600 秒就会漏掉后半段信号;直接调 case 里的数值或加一个『20 minutes』分支就能在 MT5 外接脚本里验证效果。