构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端·进阶篇
🔌

构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端·进阶篇

(2/3)· 从 OnTimer 事件到 HTTP 头字段,手把手拼出 SDSS 客户端通信链

进阶 第 2/3 篇
不少交易者以为 EA 只需调一次 WebRequest 就能把信号丢出去,却忽略了服务端防剥头皮的频率闸门。本文承接上篇架构,直接扎进 MQL5 客户端怎么把请求头拼对、怎么绕开误杀。

「用 MT5 搭一个能发信号的 REST 客户端骨架」

想在 MT5 里把交易信号推到外部系统,第一步不是写策略,而是先把 EA 跟 REST 接口的通道跑通。下面这段骨架把版权、版本、Trade 库引用和核心变量一次声明清楚,stopLoss、takeProfit 都先写死 20 点,仓位 size 设 0.1,纯属教学用占位值。 Tweet() 函数是关键:它用 WebRequest 发 POST,把信号字符串转成 char 数组丢给指定 uri。超时设了 50 毫秒,res 返回 -1 就打印 GetLastError(),否则把服务端回显用 CharArrayToString 打出来,你开 MT5 挂上 EA 就能在日志里看到往返是否通。 OnInit 目前只 return(0),没加载任何指标。外汇和贵金属杠杆高、滑点随机,这套骨架只是通道验证,别直接接真仓。

MQL5 / C++
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 "Dummy REST client(for learning purposes)."
class="macro">#include <Trade\Trade.mqh>
CPositionInfo PositionInfo;
CTrade trade;
class="type">MqlTick tick;
class="type">int stopLoss = class="num">20;
class="type">int takeProfit = class="num">20;
class="type">class="kw">double size = class="num">0.1;
class="type">void Tweet(class="type">class="kw">string uri, class="type">class="kw">string signal)
  {
   class="type">char post[];
   class="type">char result[];
   class="type">class="kw">string headers;
   class="type">int res;
   StringToCharArray(signal,post);
   ResetLastError();
   res=WebRequest("POST",uri,NULL,NULL,class="num">50,post,ArraySize(post),result,headers);
   if(res==-class="num">1)
     {
      Print("Error code =",GetLastError());
     }
   else
     {
      Print("REST client&class="macro">#x27;s POST: ",signal);
      Print("Server response: ",CharArrayToString(result,class="num">0,-class="num">1));
     }         
  }
class="type">int OnInit()
  {             
   class="kw">return(class="num">0);  
  }

EA 下单与信号外传的裸代码

这段 MT5 专家顾问的骨架把「每跳触发卖单 + 把信号推到外部 API」两件事揉在一起。OnTick 里先抓当前 tick,再用 stopLoss、takeProfit 乘 _Point 算出 sl/tp,注意这里是卖单:sl 挂在 ask 之上、tp 挂在 bid 之下,点值偏移写反就会变成双向止损。 trade.PositionOpen 直接用 tick.bid 市价卖,随后拼出一串 URL 编码信号(含品种、SELL、报价)发给 http://api.laplacianlab.com/signal/add。这种把成交动作和外部回传绑死的做法,在外汇/贵金属高滑点时段可能让实盘单与记录信号不一致,验证前先断开外网调用更安全。 下面的 MySQL 建表只是配套存储:twitterers 存授权、eas 存策略名(示例塞了 Bollinger Bands 与 Two EMA,都标 H4)、signals 用外键链回前两者,value 精度 DECIMAL(9,5)。想本地复现,把 CREATE DATABASE 到 INSERT 两段直接跑进 MySQL 即可看到空库结构。

MQL5 / C++
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {  
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">void OnTick()
  {
class=class="str">"cmt">//--- update tick
   SymbolInfoTick(_Symbol, tick);
class=class="str">"cmt">//--- calculate Take Profit and Stop Loss levels
   class="type">class="kw">double tp;
   class="type">class="kw">double sl;  
   sl = tick.ask + stopLoss * _Point;
   tp = tick.bid - takeProfit * _Point;
class=class="str">"cmt">//--- open position
   trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,size,tick.bid,sl,tp);
class=class="str">"cmt">//--- trade URL-encoded signal "id_ea=class="num">1&symbol=AUDUSD&operation=BUY&value=class="num">0.9281&";
   class="type">class="kw">string signal = "id_ea=class="num">1&symbol=" + _Symbol + "&operation=SELL&value=" + (class="type">class="kw">string)tick.bid + "&";
   Tweet("http:class=class="str">"cmt">//api.laplacianlab.com/signal/add",signal);
}
# MySQL database creation...
CREATE DATABASE IF NOT EXISTS laplacianlab_com_sdss;
use laplacianlab_com_sdss;
CREATE TABLE IF NOT EXISTS twitterers(
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT,
    twitter_id VARCHAR(class="num">255),
    access_token TEXT,
    access_token_secret TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY(id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS eas(
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(class="num">32),
    description TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),    
    PRIMARY KEY(id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS signals(
    id class="type">int UNSIGNED NOT NULL AUTO_INCREMENT,
    id_ea mediumint UNSIGNED NOT NULL,
    id_twitterer mediumint UNSIGNED NOT NULL,
    symbol VARCHAR(class="num">10) NOT NULL,
    operation VARCHAR(class="num">6) NOT NULL,
    value DECIMAL(class="num">9,class="num">5) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY(id),
    FOREIGN KEY(id_ea) REFERENCES eas(id),
    FOREIGN KEY(id_twitterer) REFERENCES twitterers(id)
) ENGINE=InnoDB;
# Dump some sample data...
# As explained in Part I, there&class="macro">#x27;s one single twitterer
INSERT INTO eas(name, description) VALUES(&class="macro">#x27;Bollinger Bands&class="macro">#x27;, &class="macro">#x27;<p>Robot based on Bollinger Bands. Works with H4 charts.</p>&class="macro">#x27;),
(&class="macro">#x27;Two EMA&class="macro">#x27;, &class="macro">#x27;<p>Robot based on the crossing of two MA. Works with H4 charts.</p>&class="macro">#x27;);

◍ 用瘦 PHP 接口接住 MT5 信号推送

把 MQL5 的订单事件转到社交平台,核心不在前端花活,而在服务端那层薄 API。这里采用 Slim 框架只做路由与 JSON 出入,不堆业务逻辑,EA 只需 POST 到 /signal/add 就能把品种、方向、价位甩给服务器。 目录里 model 下放三个类:Tweeterer.php 管发推与频次,EA.php 存 EA 元数据,DBConnection.php 封装 MySQL 面向对象连接。这种分法让 MT5 侧的 HTTP 请求代码可以极简,复杂查询全藏进类方法。 代码里有一道硬限流:canTweet 比对上一条信号时间,间隔小于 '1 minute' 直接拒绝。对外汇、贵金属这类波动快、滑点高的品种,这能避免 scalper 把账号刷成垃圾信息源,也可能降低你被平台限流的概率。 路由 /tweet-signals 走 OAuth 握手,首次访问若会话里没有 access_token 就跳 Twitter 授权。整套返回都是 {"status": "ok"} 或带错误文本的 JSON,MT5 里用 WebRequest 解析很直接。

MQL5 / C++
<?php
class=class="str">"cmt">/**
* Laplacianlab&class="macro">#x27;s SDSS - A REST API for tweeting MQL5 trading signals
*
* @author      Jordi Bassagañas
* @copyright   class="num">2014 Jordi Bassagañas
* @link        [MQL5官方文档]
*/
class=class="str">"cmt">/* Bootstrap logic */
require_once &class="macro">#x27;config/config.php&class="macro">#x27;;
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . &class="macro">#x27;/vendor/&class="macro">#x27;);
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . &class="macro">#x27;/model/&class="macro">#x27;);
require_once &class="macro">#x27;slim/slim/Slim/Slim.php&class="macro">#x27;;
require_once &class="macro">#x27;abraham/twitteroauth/twitteroauth/twitteroauth.php&class="macro">#x27;;
require_once &class="macro">#x27;Tweeterer.php&class="macro">#x27;;
require_once &class="macro">#x27;EA.php&class="macro">#x27;;
session_start();
class=class="str">"cmt">/* Init Slim */
use \Slim\Slim;
Slim::registerAutoloader();
$app = new Slim(array(&class="macro">#x27;debug&class="macro">#x27; => false));
$app->response->headers->set(&class="macro">#x27;Content-Type&class="macro">#x27;, &class="macro">#x27;application/json&class="macro">#x27;);
class=class="str">"cmt">/**
* Slim&class="macro">#x27;s exception handler
*/
$app->error(function(Exception $e) use($app) {
    echo &class="macro">#x27;{"status": "error", "message": {"text": "&class="macro">#x27; . $e->getMessage() . &class="macro">#x27;"}}&class="macro">#x27;;
});
class=class="str">"cmt">/**
* REST method.
* Custom class="num">404 error.
*/
$app->notFound(function() use($app) {
    echo &class="macro">#x27;{"status": "error class="num">404", "message": {"text": "Not found."}}&class="macro">#x27;;
});
class=class="str">"cmt">/**
* REST method.
* Home page.
*/
$app->get(&class="macro">#x27;/&class="macro">#x27;, function() {
    echo &class="macro">#x27;{"status": "ok", "message": {"text": "Service available, please check API."}}&class="macro">#x27;;
});
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();
        $message = "{$ea->get($signal->id_ea)->name} on $signal->symbol. $signal->operation at $signal->value";
        $connection->post(&class="macro">#x27;statuses/update&class="macro">#x27;, array(&class="macro">#x27;status&class="macro">#x27; => $message));                
        echo &class="macro">#x27;{"status": "ok", "message": {"text": "Signal processed."}}&class="macro">#x27;;
    }   
});
class=class="str">"cmt">/**
* REST implementation with TwitterOAuth.
* Gives permissions to Laplacianlab&class="macro">#x27;s SDSS to tweet on the user&class="macro">#x27;s behalf.
* Please, visit https:class=class="str">"cmt">//github.com/abraham/twitteroauth
*/
$app->get(&class="macro">#x27;/tweet-signals&class="macro">#x27;, function() use($app) {  
    if (empty($_SESSION[&class="macro">#x27;twitter&class="macro">#x27;][&class="macro">#x27;access_token&class="macro">#x27;]) || empty($_SESSION[&class="macro">#x27;twitter&class="macro">#x27;][&class="macro">#x27;access_token_secret&class="macro">#x27;]))
    {
        $connection = new TwitterOAuth(API_KEY, API_SECRET);
        $request_token = $connection->getRequestToken(OAUTH_CALLBACK);
        if ($request_token)
        {
            $_SESSION[&class="macro">#x27;twitter&class="macro">#x27;] = array(
                &class="macro">#x27;request_token&class="macro">#x27; => $request_token[&class="macro">#x27;oauth_token&class="macro">#x27;],

「OAuth 回调节点的令牌落库逻辑」

这段 PHP 路由处理的是 Twitter OAuth 的回调入口:当用户在 Twitter 侧授权后,浏览器被重定向回 /twitter/oauth_callback,代码先检查 GET 参数里有没有 oauth_token,没有就直接中断流程。 拿到 oauth_token 后,用会话里暂存的 request_token 和 request_token_secret 构造 TwitterOAuth 实例,再调用 getAccessToken($_REQUEST['oauth_verifier']) 换出长期可用的 access_token。若返回非空,说明换票成功,随即用新令牌重建连接并把 host 写死为 https://api.twitter.com/1.1/,这是调用 v1.1 接口的必要一步。 之后以 account/verify_credentials 校验凭证有效性,参数里 include_entities 设为 false 可减少返回体积,对只想确认绑定关系的场景够用。整套流程跑通后,access_token 与 secret 应当写进数据库,否则下次服务重启又得重走授权。 在外接社交账号做信号分发的系统里,这种回调落库是高风险环节:令牌泄露等同账户被代管,建议库内加密存储并支持用户随时 revoke。

Twitter 授权回调里的用户落库逻辑

这段 PHP 处理的是 OAuth 回调后的落库动作:当 $content 存在且带 screen_namename 时,实例化 Tweeterer,把推特 ID 与 access token 对组装成对象。 若 exists($content->id) 返回真就走 update,否则 create,随后向前端回一段 JSON 提示“SDSS 已可代管账号”,并 session_destroy() 清状态;任一环节缺字段就抛 Login error.Tweeterer 类里 getTokens($id) 直接拼 SELECT access_token, access_token_secret FROM twitterers WHERE id=$id,无预处理,注入风险先记一笔。 canTweet()time()-strtotime($timeLastTweet) 算秒差判断是否过窗口;不传参默认允许发,实际限频靠调用方传 $timeWindow 自行比较。 把这段接进 MT5 的 WebRequest 回调地址,能在信号源授权后自动存 token,但外汇/贵金属信号转发属高风险操作,密钥泄露可能触发账户越权。

MQL5 / C++
if($content && isset($content->screen_name) && isset($content->name))
{
    $tweeterer = new Tweeterer();
    $data = (object)array(
        &class="macro">#x27;twitter_id&class="macro">#x27; => $content->id,
        &class="macro">#x27;access_token&class="macro">#x27; => $access_token[&class="macro">#x27;oauth_token&class="macro">#x27;],
        &class="macro">#x27;access_token_secret&class="macro">#x27; => $access_token[&class="macro">#x27;oauth_token_secret&class="macro">#x27;]);
    $tweeterer->exists($content->id)
        ?$tweeterer->update($data)
        : $tweeterer->create($data);
    echo &class="macro">#x27;{"status": "ok", "message": {"text": "Laplacianlab\&class="macro">#x27;s SDSS can &class="macro">#x27;
        . &class="macro">#x27;now access your Twitter account on your behalf. Please, if you no &class="macro">#x27;
        . &class="macro">#x27;longer want this, log in your Twitter account and revoke access."}}&class="macro">#x27;;
    session_destroy();
}
else
{
    throw new Exception(&class="macro">#x27;Login error.&class="macro">#x27;);
}

$app->run();

class Tweeterer
{
    class="kw">protected $table = &class="macro">#x27;twitterers&class="macro">#x27;;
    class="kw">public function getTokens($id)
    {
        $sql = "SELECT access_token, access_token_secret FROM $this->table WHERE id=$id";
        class="kw">return DBConnection::getInstance()->query($sql)->fetch_object();
    }
    class="kw">public function canTweet($timeLastTweet=null, $timeWindow=null)
    {
        if(!isset($timeLastTweet)) class="kw">return true;
        $diff = time() - strtotime($timeLastTweet);
    }
}
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到请求频率与响应异常的实时提示,把重复劳动交给小布,你专注决策。

常见问题

需要在 EA 的通用属性里勾选允许 WebRequest 并填写目标 URL,否则调用会被终端拦截,返回空响应。
它按每秒请求数做阈值判断,高频 EA 易触线,本质是用轻量限流挡掉类 DoS 的剥头皮行为,误杀时需调服务端配置。
OnTimer 靠固定间隔轮询,可能漏掉瞬时平仓;OnTrade 更实时但需注意重入,二者取舍看信号时效要求。
可以,小布盯盘的 AIGC 模块能解析品种页里的外部请求日志,标记超时与 403 类异常,省去手动抓包。
WebRequest 默认带自身 UA 与连接头,服务端若严格校验 content-type 需手动补字段,否则 PHP 端取不到 body。