构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端·综合运用
「信号推送的时间窗闸门」
做信号入库前,先得卡住推送节奏,不然同一窗口内反复写信号只会污染数据库。下面这段分支就是按时间窗类型判断距离上次推送的间隔 $diff 是否超限:1 分钟窗看 60 秒、1 小时窗看 3600 秒、1 天窗看 86400 秒,没超就禁发,超了才放行。 放行后直接 return true 让调用链继续;被拦下则抛异常提示等时间窗走完。这种写法把节奏控制埋在类方法里,EA 侧不用重复判断,适合多账号跟推的场景。 紧跟着的 addSignal 负责落库:把 id_ea、id_twitterer、symbol、operation、value 拼成 INSERT 语句写进 signals 表,再返回新行 id。注意这里用的是字符串拼接而非预处理,实盘接外部数据时有注入隐患,自己改的时候建议加转义。 exists 方法开头接着做账号校验,确认 twitterer 存在才允许后续写入,避免脏 id 进表。
class="kw">switch($timeWindow) { case &class="macro">#x27;class="num">1 minute&class="macro">#x27;: $diff <= class="num">60 ? $canTweet = false : $canTweet = true; class="kw">break; case &class="macro">#x27;class="num">1 hour&class="macro">#x27;: $diff <= class="num">3600 ? $canTweet = false : $canTweet = true; class="kw">break; case &class="macro">#x27;class="num">1 day&class="macro">#x27;: $diff <= class="num">86400 ? $canTweet = false : $canTweet = true; class="kw">break; class="kw">default: $canTweet = false; class="kw">break; } if($canTweet) { class="kw">return true; } else { throw new Exception(&class="macro">#x27;Please wait until the time window has elapsed.&class="macro">#x27;); } } class=class="str">"cmt">/** * Adds a new signal * @param type $id_twitterer * @param stdClass $data * @class="kw">return integer The new row id */ class="kw">public function addSignal($id_twitterer, stdClass $data) { $sql = &class="macro">#x27;INSERT INTO signals(id_ea, id_twitterer, symbol, operation, value) VALUES(&class="macro">#x27; . $data->id_ea . "," . $id_twitterer . ",&class="macro">#x27;" . $data->symbol . "&class="macro">#x27;,&class="macro">#x27;" . $data->operation . "&class="macro">#x27;," . $data->value . &class="macro">#x27;)&class="macro">#x27;; DBConnection::getInstance()->query($sql); class="kw">return DBConnection::getInstance()->getHandler()->insert_id; } class=class="str">"cmt">/** * Checks whether the given twitterer exists * @param class="type">class="kw">string $id * @class="kw">return boolean */ class="kw">public function exists($id) {
◍ 信号源与EA的数据库读写封装
把推特账号和EA的配置落库,核心就是几个轻量PHP方法:用 twitter_id 查重、插入新记录拿自增id、按 id 更新 token,以及按倒序取最近一条信号。getLastSignal 里 LIMIT 1 配 ORDER BY id DESC,保证每轮轮询只比对最新信号,避免重复下单。 下面这段是 Twitterer 类的实际写法,注意它直接拼 SQL 字符串,没有预处理——在私有闭环脚本里能用,但挂公网就有注入风险。 $sql = "SELECT * FROM $this->table WHERE twitter_id='$id'"; $result = DBConnection::getInstance()->query($sql); return (boolean)$result->num_rows; } /**
- Creates a new twitterer
- @param stdClass $data
- @return integer The new row id
*/ public function create(stdClass $data) { $sql = "INSERT INTO $this->table(twitter_id, access_token, access_token_secret) " . "VALUES ('" . $data->twitter_id . "','" . $data->access_token . "','" . $data->access_token_secret . "')"; DBConnection::getInstance()->query($sql); return DBConnection::getInstance()->getHandler()->insert_id; } /**
- Updates the twitterer's data
- @param stdClass $data
- @return Mysqli object
*/ public function update(stdClass $data) { $sql = "UPDATE $this->table SET " . "access_token = '" . $data->access_token . "', " . "access_token_secret = '" . $data->access_token_secret . "' " . "WHERE twitter_id ='" . $data->twitter_id . "'"; return DBConnection::getInstance()->query($sql); } /**
- Gets the last trading signal sent by the twitterer
- @param type $id The twitterer id
- @return mixed The last trading signal
*/ public function getLastSignal($id) { $sql = "SELECT * FROM signals WHERE id_twitterer=$id ORDER BY id DESC LIMIT 1"; $result = DBConnection::getInstance()->query($sql); if($result->num_rows == 1) { return $result->fetch_object(); } else { $signal = new stdClass; $signal->created_at = null; return $signal; } } } EA 类更薄,只有一个 get($id) 按主键直查 eas 表,返回对象供后续反序列化参数用。 require_once 'DBConnection.php'; /**
- EA's simple OOP wrapper
*
- @author Jordi Bassagañas
- @copyright 2014 Jordi Bassagañas
- @link [MQL5官方文档]
*/ class EA { protected $table = 'eas'; /**
- Gets an EA by id
- @param integer $id
- @return stdClass
*/ public function get($id) { $sql = "SELECT * FROM $this->table WHERE id=$id"; return DBConnection::getInstance()->query($sql)->fetch_object(); } } 在 MT5 侧接这套接口时,外汇与贵金属波动剧烈、杠杆风险高,信号延迟几秒就可能让跟单偏移;建议本地缓存 last_signal 的 created_at,比对时间戳而非每次全量拉取。
$sql = "SELECT * FROM $this->table WHERE twitter_id=&class="macro">#x27;$id&class="macro">#x27;"; $result = DBConnection::getInstance()->query($sql); class="kw">return (boolean)$result->num_rows; } class=class="str">"cmt">/** * Creates a new twitterer * @param stdClass $data * @class="kw">return integer The new row id */ class="kw">public function create(stdClass $data) { $sql = "INSERT INTO $this->table(twitter_id, access_token, access_token_secret) " . "VALUES(&class="macro">#x27;" . $data->twitter_id . "&class="macro">#x27;,&class="macro">#x27;" . $data->access_token . "&class="macro">#x27;,&class="macro">#x27;" . $data->access_token_secret . "&class="macro">#x27;)"; DBConnection::getInstance()->query($sql); class="kw">return DBConnection::getInstance()->getHandler()->insert_id; } class=class="str">"cmt">/** * Updates the twitterer&class="macro">#x27;s data * @param stdClass $data * @class="kw">return Mysqli object */ class="kw">public function update(stdClass $data) { $sql = "UPDATE $this->table SET " . "access_token = &class="macro">#x27;" . $data->access_token . "&class="macro">#x27;, " . "access_token_secret = &class="macro">#x27;" . $data->access_token_secret . "&class="macro">#x27; " . "WHERE twitter_id =&class="macro">#x27;" . $data->twitter_id . "&class="macro">#x27;"; class="kw">return DBConnection::getInstance()->query($sql); } class=class="str">"cmt">/** * Gets the last trading signal sent by the twitterer * @param type $id The twitterer id * @class="kw">return mixed The last trading signal */ class="kw">public function getLastSignal($id) { $sql = "SELECT * FROM signals WHERE id_twitterer=$id ORDER BY id DESC LIMIT class="num">1"; $result = DBConnection::getInstance()->query($sql); if($result->num_rows == class="num">1) { class="kw">return $result->fetch_object(); } else { $signal = new stdClass; $signal->created_at = null; class="kw">return $signal; } } } <?php require_once &class="macro">#x27;DBConnection.php&class="macro">#x27;; class=class="str">"cmt">/** * EA&class="macro">#x27;s simple OOP wrapper * * @author Jordi Bassagañas * @copyright class="num">2014 Jordi Bassagañas * @link [MQL5官方文档] */ class EA { class="kw">protected $table = &class="macro">#x27;eas&class="macro">#x27;; class=class="str">"cmt">/** * Gets an EA by id * @param integer $id * @class="kw">return stdClass */ class="kw">public function get($id) { $sql = "SELECT * FROM $this->table WHERE id=$id"; class="kw">return DBConnection::getInstance()->query($sql)->fetch_object(); } }
用单例封装 MT5 外的 MySQL 连接
把数据库访问收进一个类里,能避免 EA 多次初始化时反复建连。下面这段 PHP 风格的单例类(常见于 MT5 外接 Web 服务桥接层)只保留一个 DBConnection 实例,构造函数私有,外部无法直接 new。
getInstance() 用 instanceof 判断静态成员是否为自身实例,不是才新建,是则直接返回。这样在 1 分钟 K 线回调里反复取连接,底层 MySQL 握手次数倾向稳定在 1 次而非 N 次。
query() 对 false 显式抛异常而不是静默返回,配合 mysqli_report(MYSQLI_REPORT_STRICT) 能把连不上库的问题在日志里立刻暴露,而不是让策略在空数据上继续跑。外汇与贵金属信号外接库时延迟敏感,连不上就抛比假信号更可控,属高风险环节需本地先测。
class=class="str">"cmt">/** * DBConnection class * * @author Jordi Bassagañas * @copyright class="num">2014 Jordi Bassagañas * @link [MQL5官方文档] */ class DBConnection { class=class="str">"cmt">/** * @var DBConnection Singleton instance */ class="kw">private class="kw">static $instance; class=class="str">"cmt">/** * @var mysqli Database handler */ class="kw">private $mysqli; class=class="str">"cmt">/** * Opens a new connection to the MySQL server */ class="kw">private function __construct() { mysqli_report(MYSQLI_REPORT_STRICT); try { $this->mysqli = new MySQLI(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME); } class="kw">catch (Exception $e) { throw new Exception(&class="macro">#x27;Unable to connect to the database, please, try again later.&class="macro">#x27;); } } class=class="str">"cmt">/** * Gets the singleton instance * @class="kw">return type */ class="kw">public class="kw">static function getInstance() { if (!self::$instance instanceof self) self::$instance = new self; class="kw">return self::$instance; } class=class="str">"cmt">/** * Gets the database handler * @class="kw">return mysqli */ class="kw">public function getHandler() { class="kw">return $this->mysqli; } class=class="str">"cmt">/** * Runs the given query * @param class="type">class="kw">string $sql * @class="kw">return mixed */ class="kw">public function query($sql) { $result = $this->mysqli->query($sql); if ($result === false) { throw new Exception(&class="macro">#x27;Unable to run query, please, try again later.&class="macro">#x27;); } else { class="kw">return $result; } } }
「把工具请下神坛」
这套 SDSS 客户端的落地,最终靠的是 MQL5 的 WebRequest() 而非完整 REST 会话。它本只是用来发 GET/POST 访问网页,拿来当轻量通信通道虽不优雅,但比自己造控件省心也更稳。 服务端收的是 URL 编码数据,再转成 PHP 能管的格式;瘦框架与数据库无关,SQL 注入得自己兜底。MetaTrader 5 终端和 PHP 应用之间的链路加密本文没碰,所以这套只能当 demo,别直接扔实盘环境跑。 眼下个人信号能发了,单用户跑得通,离生产级还差一截。工具是手段,真要上外汇或贵金属,高波动和断连风险都在,先开 MT5 把 dummy_ontimer.mq5 跑一遍看看心跳再说。