构建新兴的社交技术, 第一部分: 发布您的 MetaTrader 5 信号·进阶篇
信号系统的库表怎么落
顺着 REST API 的字段往回看,这套信号服务后端其实就两张核心表:eas 存 EA 本体,signals 存具体喊单。两者用 id_ea 做外键关联,写信号的程序必须先有合法的 EA 记录才能落库。 建库语句里用了 InnoDB 引擎,mediumint UNSIGNED 给 eas 的 id 留了约 1677 万的上限,对多数私用信号池够用;signals 的 value 用 DECIMAL(9,5),意味着价格类字段最大到 999999.99999,外汇报价精度完全覆盖。 示例数据直接塞了 Bollinger Bands 与 Two EMA 两个 EA,描述里写明跑 H4 周期。你在本地 MySQL 跑下面这段,就能复刻出可对接 MT5 信号推送的最小库结构。 生产环境务必改掉 'password' 这个授权口令,原文也标了注释提醒。
# MySQL database creation... CREATE DATABASE IF NOT EXISTS sdss; use sdss; # Please, change the user&class="macro">#x27;s password in production GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `sdss`.* TO &class="macro">#x27;laplacianlab&class="macro">#x27;@&class="macro">#x27;localhost&class="macro">#x27; IDENTIFIED BY &class="macro">#x27;password&class="macro">#x27;; CREATE TABLE IF NOT EXISTS eas( id mediumint UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(class="num">32), description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 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, 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 DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id), FOREIGN KEY(id_ea) REFERENCES eas(id) ) ENGINE=InnoDB; # Dump some sample data... 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;);
◍ 用 Slim 把 MT5 信号写进本地 API
Slim 是个极轻量的 PHP 微框架,几行代码就能撑起一个 RESTful API。教学示例里我们只实现 HTTP POST 方法,路径定为 /signal/add,用来接收 EA 抛过来的信号并落库。 本地跑通前有两件脏活:在 C:\Windows\System32\Drivers\etc\hosts 里加一条 api.laplacianlab.local 的解析,再给 Apache 配一个对应的虚拟主机。缺了这两步,浏览器和 MT5 都找不到你的服务。 用 Firefox 的 RESTClient 插件发 POST 到 http://api.laplacianlab.local/signal/add,正常会回 HTTP 200 加 JSON 实体。下面这段是核心骨架,包含常量定义、单例数据库连接、重写规则和路由处理。 目录结构实测是 config/config.php、model/DBConnection.php、public/.htaccess、public/index.php、composer.json 这五块。生产环境务必加认证层,否则任何人都可能往你的 signals 表塞脏数据。
<?php class=class="str">"cmt">// Creating constants. class=class="str">"cmt">// General define(&class="macro">#x27;BASE_URL&class="macro">#x27;, &class="macro">#x27;laplacianlab.local&class="macro">#x27;); define(&class="macro">#x27;APPLICATION_PATH&class="macro">#x27;, realpath(dirname(__FILE__)) . &class="macro">#x27;/../&class="macro">#x27;); class=class="str">"cmt">// Database connection define(&class="macro">#x27;DB_NAME&class="macro">#x27;, &class="macro">#x27;sdss&class="macro">#x27;); define(&class="macro">#x27;DB_USER&class="macro">#x27;, &class="macro">#x27;laplacianlab&class="macro">#x27;); define(&class="macro">#x27;DB_PASSWORD&class="macro">#x27;, &class="macro">#x27;password&class="macro">#x27;); class=class="str">"cmt">// Don&class="macro">#x27;t forget to change this in your production server! define(&class="macro">#x27;DB_SERVER&class="macro">#x27;, &class="macro">#x27;localhost&class="macro">#x27;); <?php class DBConnection { class="kw">private class="kw">static $instance; class="kw">private $mysqli; class="kw">private function __construct() { $this->mysqli = new MySQLI(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME); } class="kw">public class="kw">static function getInstance() { if (!self::$instance instanceof self) self::$instance = new self; class="kw">return self::$instance; } class="kw">public function getHandler() { class="kw">return $this->mysqli; } } <IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews </IfModule> RewriteEngine On # Redirect Trailing Slashes... RewriteRule ^(.*)/$ /$class="num">1 [L,R=class="num">301] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] </IfModule> <?php 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;DBConnection.php&class="macro">#x27;; use \Slim\Slim; Slim::registerAutoloader(); $app = new Slim(); $app->response->headers->set(&class="macro">#x27;Content-Type&class="macro">#x27;, &class="macro">#x27;application/json&class="macro">#x27;); class=class="str">"cmt">// RESTful API methods class=class="str">"cmt">// POST signal/add $app->post(&class="macro">#x27;/signal/add&class="macro">#x27;, function() { $request = Slim::getInstance()->request(); $signal = json_decode($request->getBody()); $sql = &class="macro">#x27;INSERT INTO signals(id_ea, symbol, operation, value) VALUES(&class="macro">#x27; . mysql_real_escape_string($signal->ea_id) . ",&class="macro">#x27;" . mysql_real_escape_string($signal->symbol) . "&class="macro">#x27;,&class="macro">#x27;" . mysql_real_escape_string($signal->operation) . "&class="macro">#x27;," . mysql_real_escape_string($signal->value) . &class="macro">#x27;)&class="macro">#x27;; DBConnection::getInstance()->getHandler()->query($sql); $signal->id = DBConnection::getInstance()->getHandler()->insert_id; echo json_encode($signal); }); class=class="str">"cmt">// More API methods here!.., according to your API spec $app->run(); { "require": { "slim/slim": "class="num">2.*",
「EA 信号回传的本地接口与报文结构」
把 MQL5 写的 EA 接到自建分析中枢,第一步往往是让策略把信号推到本地虚拟主机。上面这段配置把 api.laplacianlab.local 绑到 127.0.0.1,并用 Apache 2.4.4(Win64)+ PHP 5.4.12 在 80 端口起了只接受本机访问的虚拟站点,目录权限里 Deny from all 之后单独 Allow from 127.0.0.1,外网扫不到。 HTTP 响应头里 Content-Length 为 70,Status Code 200 OK,Keep-Alive 设了 timeout=5, max=100,说明这条连接最多复用 100 次、空闲 5 秒就断。EA 侧拿到的 JSON 体很小:ea_id 为 1,symbol 是 AUDUSD,operation 为 BUY,value 为 0.9281,id 为 22。 你在 MT5 里验证时,可直接用 WebRequest 向 http://api.laplacianlab.local 发 POST,看能否收回同样字段。外汇与贵金属杠杆高、滑点大,这类本地回传只解决信号落地,不替代风控,实盘前务必在策略测试器跑够样本。
::class="num">1 localhost # Copyright(c) class="num">1993-class="num">2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments(such as these) may be inserted on individual # lines or following the machine name denoted by a &class="macro">#x27;#&class="macro">#x27; symbol. # # For example: # # class="num">102.54.class="num">94.97 rhino.acme.com # source server # class="num">38.25.class="num">63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. # class="num">127.0.class="num">0.1 localhost # ::class="num">1 localhost class="num">127.0.class="num">0.1 localhost class="num">127.0.class="num">0.1 api.laplacianlab.local # Virtual Hosts # # Required modules: mod_log_config # If you want to maintain multiple domains/hostnames on your # machine you can setup VirtualHost containers for them. Most configurations # use only name-based class="kw">virtual hosts so the server doesn&class="macro">#x27;t need to worry about # IP addresses. This is indicated by the asterisks in the directives below. # # Please see the documentation at # <URL:http:class=class="str">"cmt">//httpd.apache.org/docs/class="num">2.4/vhosts/> # for further details before you try to setup class="kw">virtual hosts. # # You may use the command line option &class="macro">#x27;-S&class="macro">#x27; to verify your class="kw">virtual host # configuration. # # VirtualHost example: # Almost any Apache directive may go into a VirtualHost container. # The first VirtualHost section is used for all requests that do not # match a ServerName or ServerAlias in any <VirtualHost> block. # <VirtualHost *:class="num">80> ServerAdmin webmaster@laplacianlab.local DocumentRoot "c:/wamp/www/laplacianlab/class="kw">public" ServerName api.laplacianlab.local ErrorLog "logs/api.laplacianlab.local-error.log" CustomLog "logs/api.laplacianlab.local-access.log" common <directory "c:/wamp/www/laplacianlab/class="kw">public"> Options FollowSymLinks AllowOverride all Order Deny,Allow Deny from all Allow from class="num">127.0.class="num">0.1 </directory> </VirtualHost> Status Code: class="num">200 OK Connection: Keep-Alive Content-Length: class="num">70 Content-Type: application/json Date: Mon, class="num">07 Apr class="num">2014 class="num">18:class="num">12:class="num">34 GMT Keep-Alive: timeout=class="num">5, max=class="num">100 Server: Apache/class="num">2.4.class="num">4 (Win64) PHP/class="num">5.4.class="num">12 X-Powered-By: PHP/class="num">5.4.class="num">12 { "ea_id": class="num">1, "symbol": "AUDUSD", "operation": "BUY", "value": class="num">0.9281, "id": class="num">22 }
把交易信号推到 Twitter 的 OAuth 接法
想让推友授权你的 SDSS 代发交易信号,先去 Twitter 开发者后台建应用,权限必须选「读和写」,否则拿不到发推能力。应用建完会拿到三组常量:API_KEY、API_SECRET、OAUTH_CALLBACK,把它们写进 config/config.php 供后续调用。 SDSS 用 TwitterOAuth 这个 PHP 库走 OAuth 2.0 流程,若之前用 Composer 装 Slim 时带了配套 composer.json,该库已在 vendor 目录,不必另下。流程核心是:用户访问 /tweet-signals,系统用 API_KEY 和 API_SECRET 换 request_token,再跳转到 Twitter 授权页。 授权后回调里要落库的是 access_token 和 access_token_secret,这两块是长期有效的,用户注销前 SDSS 不会碰账户密码。下面这张表专门存授权推友,twitter_id 用 VARCHAR(255) 足够覆盖普通用户 ID 字符串。 [CODE] 段里的 CREATE TABLE 建了 twitterers 表,id 自增主键,access_token 与 access_token_secret 用 TEXT 存原文。POST /signal/add 那段把 EA 信号(ea_id、symbol、operation、value)插进 signals 表,并返回带新 id 的 JSON——这部分接上前面章节的 RESTful 服务就能跑。 别把密钥写死在公网代码 示例里的 API_KEY 'akMnfXR45MkoaWbZoPiu3' 只是演示,真实部署务必从环境变量或加密配置读,泄露后任何人可冒用你的 Twitter 应用发推。
class=class="str">"cmt">// Twitter OAuth define(&class="macro">#x27;API_KEY&class="macro">#x27;, &class="macro">#x27;akMnfXR45MkoaWbZoPiu3&class="macro">#x27;); define(&class="macro">#x27;API_SECRET&class="macro">#x27;, &class="macro">#x27;45Mkoa54NcvQRBbf119qWerty0DnIW45MncvFgqw&class="macro">#x27;); define(&class="macro">#x27;OAUTH_CALLBACK&class="macro">#x27;, &class="macro">#x27;http:class=class="str">"cmt">//api.laplacianlab.local/twitter/oauth_callback&class="macro">#x27;); 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 DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id) ) ENGINE=InnoDB; <?php 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;DBConnection.php&class="macro">#x27;; use \Slim\Slim; session_start(); Slim::registerAutoloader(); $app = new Slim(); $app->response->headers->set(&class="macro">#x27;Content-Type&class="macro">#x27;, &class="macro">#x27;application/json&class="macro">#x27;); class=class="str">"cmt">// RESTful API methods class=class="str">"cmt">// POST signal/add $app->post(&class="macro">#x27;/signal/add&class="macro">#x27;, function() { $request = Slim::getInstance()->request(); $signal = json_decode($request->getBody()); $sql = &class="macro">#x27;INSERT INTO signals(id_ea, symbol, operation, value) VALUES(&class="macro">#x27; . mysql_real_escape_string($signal->ea_id) . ",&class="macro">#x27;" . mysql_real_escape_string($signal->symbol) . "&class="macro">#x27;,&class="macro">#x27;" . mysql_real_escape_string($signal->operation) . "&class="macro">#x27;," . mysql_real_escape_string($signal->value) . &class="macro">#x27;)&class="macro">#x27;; DBConnection::getInstance()->getHandler()->query($sql); $signal->id = DBConnection::getInstance()->getHandler()->insert_id; echo json_encode($signal); }); class=class="str">"cmt">// More API methods here!.., according to your API spec class=class="str">"cmt">// Twitter OAuth flow class=class="str">"cmt">// This method is for users to give permissions to Laplacianlab&class="macro">#x27;s SDSS to tweet class=class="str">"cmt">// on their behalf. $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;],