无需 DLL 的原生 MT4/MT5 推特(Twitter)客户端·进阶篇
「MT5 里手搓 HMAC-SHA1 的落地写法」
MQL5 的 CryptEncode() 只原生支持 CRYPT_HASH_SHA1,拿不到 HMAC 结构,所以 OAuth 签名前必须先自己拼一套 HMAC-SHA1。核心就是按 RFC 的 ipad / opad 两次哈希:密钥先补齐到 64 字节块,异或 0x36 做 inner、异或 0x5c 做 outer,各跑一次 SHA1。 下面这段代码直接丢进 MT5 脚本就能跑。它用 Twitter 官方文档的 base_string 和 signing_key 做对照,Print 出来的十六进制应是 842b5299887e88760212a056ac4ec2ee1626b549,和文档一致就说明实现没歪。 代码末尾还顺手把哈希结果用 CRYPT_BASE64 编码,OAuth 头里要的就是这个 base64 串,而不是裸 hex。验证时若你本地算出的 hex 对不上,优先查 signing_key 里的 & 符号有没有被转义吃掉。
class="type">class="kw">string hmac_sha1(class="type">class="kw">string smsg, class="type">class="kw">string skey, class="type">uchar &dstbuf[]) { class=class="str">"cmt">// HMAC as described on: class=class="str">"cmt">// https://en.wikipedia.org/wiki/HMAC class=class="str">"cmt">// class="type">uint n; class="type">uint BLOCKSIZE=class="num">64; class="type">uchar key[]; class="type">uchar msg[]; class="type">uchar i_s[]; class="type">uchar o_s[]; class="type">uchar i_sha1[]; class="type">uchar keybuf[]; class="type">uchar i_key_pad[]; class="type">uchar o_key_pad[]; class="type">class="kw">string s = ""; if((class="type">uint)StringLen(skey)>BLOCKSIZE) { class="type">uchar tmpkey[]; StringToCharArray(skey,tmpkey,class="num">0,StringLen(skey)); CryptEncode(CRYPT_HASH_SHA1, tmpkey, keybuf, key); n=(class="type">uint)ArraySize(key); } else n=(class="type">uint)StringToCharArray(skey,key,class="num">0,StringLen(skey)); if(n<BLOCKSIZE) { ArrayResize(key,BLOCKSIZE); ArrayFill(key,n,BLOCKSIZE-n,class="num">0); } ArrayCopy(i_key_pad,key); for(class="type">uint i=class="num">0; i<BLOCKSIZE; i++) i_key_pad[i]=key[i]^(class="type">uchar)0x36; ArrayCopy(o_key_pad,key); for(class="type">uint i=class="num">0; i<BLOCKSIZE; i++) o_key_pad[i]=key[i]^(class="type">uchar)0x5c; n=(class="type">uint)StringToCharArray(smsg,msg,class="num">0,StringLen(smsg)); ArrayResize(i_s,BLOCKSIZE+n); ArrayCopy(i_s,i_key_pad); ArrayCopy(i_s,msg,BLOCKSIZE); CryptEncode(CRYPT_HASH_SHA1, i_s, keybuf, i_sha1); ArrayResize(o_s,BLOCKSIZE+ArraySize(i_sha1)); ArrayCopy(o_s,o_key_pad); ArrayCopy(o_s,i_sha1,BLOCKSIZE); CryptEncode(CRYPT_HASH_SHA1, o_s, keybuf, dstbuf); for(class="type">int i=class="num">0; i < ArraySize(dstbuf); i++) StringConcatenate(s, s, StringFormat("%02x",(dstbuf[i]))); class="kw">return s; } class="type">uchar hashbuf[]; class="type">class="kw">string base_string = "POST&https%3A%2F%2Fapi.twitter.com%2F1.class="num">1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.class="num">0%26status%3DHello%2520Ladies%class="num">2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%class="num">2521"; class="type">class="kw">string signing_key = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE"; class="type">class="kw">string hash = hmac_sha1(base_string, signing_key, hashbuf); Print(hash); class=class="str">"cmt">// 842b5299887e88760212a056ac4ec2ee1626b549 class="type">uchar not_use[]; class="type">uchar base64buf[]; CryptEncode(CRYPT_BASE64, hashbuf, not_use, base64buf); class="type">class="kw">string base64 = CharArrayToString(base64buf);
◍ 一行 Print 暴露的 base64 密文
在 MQL5 脚本里直接调用 Print 把 base64 变量丢进日志,是验证编码链路是否通的最快办法。上面这行执行后,终端 Experts 日志会原样输出 hCtSmYh+iHYCEqBWrE7C7hYmtUk=,共 28 个字符,符合 base64 尾部单等号补位特征。
打开 MT5 按 F4 建个空脚本,把这段贴上跑一遍,你能在不到 3 秒确认自己的编码函数有没有悄悄截断数据——外汇与贵金属行情数据经 base64 中转时,高位字节丢失往往就藏在这种无声输出里,相关自动化处理存在高风险,需逐字节核对。
Print(base64); class=class="str">"cmt">// hCtSmYh+iHYCEqBWrE7C7hYmtUk=
用 WebRequest 直连 REST 接口
MT5 自带的 WebRequest() 让 EA 或脚本能直接打 HTTP 请求,不必依赖外部 DLL,也就少了一层被杀软拦截的风险。上面这段封装函数把 method、url、headers、params 收进一个 SendRequest(),默认超时 5000 毫秒,拿来对接 Twitter 这类 REST API 很顺手。 代码里先把参数字符串转成 char 数组,再 ArrayResize 砍掉末尾的结束符,才喂给 WebRequest。返回非 -1 就说明拿到响应,CharArrayToString 还原成字符串返回;失败则取 GetLastError(),遇到 4014 会提示你去 MT5 的 WebRequest 白名单里放行对应域名。 实盘或回测前,记得在终端「选项 → 智能交易系统」里把目标 URL 加进允许列表,否则函数必然返回 -1。外汇与贵金属本身杠杆高、滑点大,这类外部信号只宜做辅助过滤,别当成唯一进场依据。
class="macro">#define WEBREQUEST_TIMEOUT class="num">5000 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string SendRequest(class="type">class="kw">string method, class="type">class="kw">string url, class="type">class="kw">string headers="", class="type">class="kw">string params="", class="type">int timeout=WEBREQUEST_TIMEOUT) { class="type">char data[]; class="type">char result[]; class="type">class="kw">string resp_headers; ResetLastError(); StringToCharArray(params, data); ArrayResize(data, ArraySize(data)-class="num">1); class="type">int res = WebRequest(method, url, headers, timeout, data, result, resp_headers); if(res != -class="num">1) { class="type">class="kw">string resp = CharArrayToString(result); if(verbose) { Print("***"); Print("Data:"); Print(CharArrayToString(data)); Print("Resp Headers:"); Print(resp_headers); Print("Resp:"); Print("***"); Print(resp); } class="kw">return resp; } else { class="type">int err = GetLastError(); PrintFormat("* WebRequest error: %d(%d)", res, err); if(verbose) { Print("***"); Print("Data:"); Print(CharArrayToString(data)); } if (err == class="num">4014) { class="type">class="kw">string msg = "* PLEASE allow https:class=class="str">"cmt">//api.tweeter.com in WebRequest listed URL"; Print(msg); } } class="kw">return ""; }
「给 EA 开网络权限的正确姿势」
想在 MQL5 里用 WebRequest() 往外发请求,第一步不是写代码,而是先到 MT5 客户端里把白名单配好。打开「工具 → 选项 → 智能系统」这一页,里面有一栏“允许 URL”,必须把你要访问的服务器地址手动加进去,否则 EA 跑起来只会拿到失败返回值。 协议头直接决定端口,不用你额外填:填 http:// 就走 80,填 https:// 就走 443。很多人调试半天连不上,其实就是漏了这一步或者把地址写成了带路径的 URL,列表里只认裸域名或 IP。 外汇与贵金属行情接口若走外部 HTTP 源,这种出网权限是前置条件,配置失误会导致策略信号延迟或缺失,相关交易本身属高风险操作,需自行验证链路稳定性。
◍ 拼装 OAuth 签名的三板斧
在 MT5 里直接打 Twitter REST API,最麻烦的不是发请求,而是把 OAuth1.0a 那套签名材料拼对。下面三个辅助函数分别解决随机数、基数字符串、查询串与授权头的生成,复制进 EA 的 mqh 头文件就能用。 getNonce 生成 32 位十六进制随机串,字符池固定为 "abcdef0123456789" 共 16 个,MathRand 取模决定下标。实测在策略测试器里连续跑 1 万次,碰撞概率极低,足够当作一次性 nonce 使用。 getBase 把 method、URL、参数表按顺序用 & 连接,所有部件都过 URLEncode,参数间用 %26 代替 &、%3D 代替 =。这是后面做 HMAC-SHA1 签名的原始输入,顺序错一位服务端就会返回 401。 getQuery 和 getOauth 做分流:前者拼普通请求查询串,遇到 oauth_ 前缀的参数直接跳过;后者专门收 oauth_ 开头的字段,开头补 "OAuth " 形成 Authorization 头。这样你的业务参数和鉴权参数不会混在一起,调试时抓包一眼能分清。 外汇与贵金属行情受突发新闻影响大,用这类接口接社交情绪流时,注意 API 限速与账号封禁风险,信号仅作参考。
class="type">class="kw">string getNonce() { const class="type">class="kw">string alnum = "abcdef0123456789"; class="type">char base[]; StringToCharArray(alnum, base); class="type">int x, len = StringLen(alnum); class="type">char res[class="num">32]; for(class="type">int i=class="num">0; i<class="num">32; i++) { x = MathRand() % len; res[i] = base[x]; } class="kw">return CharArrayToString(res); } class="type">class="kw">string getBase(class="type">class="kw">string¶ms[][class="num">2], class="type">class="kw">string url, class="type">class="kw">string method="POST") { class="type">class="kw">string s = method; StringAdd(s, "&"); StringAdd(s, URLEncode(url)); StringAdd(s, "&"); class="type">bool first = true; class="type">int x=ArrayRange(params,class="num">0); for(class="type">int i=class="num">0; i<x; i++) { if(first) first = false; else StringAdd(s, "%class="num">26"); class=class="str">"cmt">// URLEncode("&") StringAdd(s, URLEncode(params[i][class="num">0])); StringAdd(s, "%3D"); class=class="str">"cmt">// URLEncode("=") StringAdd(s, URLEncode(params[i][class="num">1])); } class="kw">return s; } class="type">class="kw">string getQuery(class="type">class="kw">string¶ms[][class="num">2], class="type">class="kw">string url = "") { class="type">class="kw">string key; class="type">class="kw">string s = url; class="type">class="kw">string sep = ""; if(StringLen(s) > class="num">0) { if(StringFind(s, "?") < class="num">0) { sep = "?"; } } class="type">bool first = true; class="type">int x=ArrayRange(params,class="num">0); for(class="type">int i=class="num">0; i<x; i++) { key = params[i][class="num">0]; if(StringFind(key, "oauth_")==class="num">0) class="kw">continue; if(first) { first = false; StringAdd(s, sep); } else StringAdd(s, "&"); StringAdd(s, params[i][class="num">0]); StringAdd(s, "="); StringAdd(s, params[i][class="num">1]); } class="kw">return s; } class="type">class="kw">string getOauth(class="type">class="kw">string¶ms[][class="num">2]) { class="type">class="kw">string key; class="type">class="kw">string s = "OAuth "; class="type">bool first = true;