构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端·进阶篇
(2/3)· 从 OnTimer 事件到 HTTP 头字段,手把手拼出 SDSS 客户端通信链
「用 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),没加载任何指标。外汇和贵金属杠杆高、滑点随机,这套骨架只是通道验证,别直接接真仓。
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 即可看到空库结构。
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 解析很直接。
<?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_name、name 时,实例化 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,但外汇/贵金属信号转发属高风险操作,密钥泄露可能触发账户越权。
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); } }