原生推特(Twitter)客户端:第二部(基础篇)
MT5 里的原生推特信号源
MetaTrader 5 客户端在 2020 年 9 月已内置原生推特(Twitter)接入能力,交易者无需第三方桥接即可在终端内接收公开账户发布的信号与观点。 官方示例帖记录于 21 九月 2020, 09:24,单条展示数据含 6 738 次浏览与 2 条互动,发布者为 Soewono Effendi。这一量级说明即便非大 V 的技术账号,其 MT5 内嵌推文也能触达数千终端用户。 外汇与贵金属属高杠杆高风险品种,把社交信号直接当下单依据极易放大回撤;建议仅用作异动预警,开 MT5 的「新闻/社交」面板核对源账号后再决定是否验证策略。
「不依赖 DLL 的推特图文发布思路」
这一节先把目标收窄:只解决 MT5 里如何不调用外部 DLL,直接向 Twitter 发带图推文。原文作者延续了前一篇“原生客户端”的路线,把范围限定在图像上传,避免一上来就铺开整套 API 增加理解成本。 到本小节收尾,读者应能跑通一个纯 MQL5 的推特客户端,可附最多 4 张照片发消息。这个 4 张的上限不是客户端自己定的,而是 Twitter API 在 media_ids 参数里写死的,写代码时别试图绕过它。 对外汇和贵金属交易者来说,这类原生工具的意义在于低风险自动化:行情截图定时发到私域,全程不依赖第三方 DLL,也就少了一层被注入或封号的概率。
◍ 用 media_id 串起多图推文
向社交平台传图本质是一次带 OAuth 授权的 multipart/form-data POST。每成功上传一张图,服务端会回一个 media_id,这个 ID 只在有限时间内有效,必须赶在过期前塞进要发的推文里。 单条推文最多挂 4 张图。做法是把每次上传拿到的 media_id 依次收集,最后拼成逗号分隔的字符串,发推时一次性带过去。 具体落点:先传第一张拿 media_id;重复传,直到 4 张上限,全程保留返回的 media_id;写好推文正文;发送时把逗号分隔的 media_id 列表作为媒体参数提交。 注意这套流程示例代码省略了错误处理,实盘接生产环境前得自己补上超时与重试。外汇贵金属相关自动化发布同样属高风险操作,API 限制变动可能让脚本静默失效。
把图像塞进 multipart 请求体
推特上传图片有两种办法:直接发原始二进制,或者先 Base64 编码再发。后者体积会膨胀到约三倍,所以走原始二进制更省流量和耗时。 HTTP multipart/form-data 的结构在 RFC-2388 里有正式定义,本质就是一个 HTTP POST,请求主体被切成多个 part,彼此用 MIME 边界串隔开。下面这段 MQL5 把图像文件读成 uchar 数组,再拼到 data[] 末尾,并补上边界头。 [CODE] void appendPhoto(string filename, string hash, uchar &data[], bool common_flag=false) {
| int flags=FILE_READ | FILE_BIN | FILE_SHARE_WRITE | FILE_SHARE_READ; |
|---|
if(common_flag) flags|=FILE_COMMON; //--- int handle=FileOpen(filename,flags); if(handle==INVALID_HANDLE) return; //--- int size=(int)FileSize(handle); uchar img[]; ArrayResize(img,size); FileReadArray(handle,img,0,size); FileClose(handle); int pos = ArraySize(data); int offset = pos + size; int hlen = StringLen(hash)+6; int newlen = offset + hlen; ArrayResize(data, newlen); ArrayCopy(data, img, pos); StringToCharArray("\r\n--"+hash+"\r\n", data, offset, hlen); } [/CODE] 逐行拆解:FileOpen 以二进制只读方式打开图像,失败直接 return;FileSize 取字节数,ArrayResize 给 img[] 腾地方,FileReadArray 一次性读入。随后算好 data[] 现有长度 pos,把图像拷到尾部,再用 StringToCharArray 把 "\r\n--边界\r\n" 写进偏移处,完成一个 part 的封装。 uploadPhoto 里用 getNonce() 生成随机串当 boundary,拼进 Content-Type 头,这样服务端才能正确切分字段。 调试时建议把拼好的完整 HTTP 请求额外写到 MT5 数据文件夹,用文本比对工具看边界和换行是否严丝合缝——少一个 \r\n 都可能让服务端拒收。
POST /submit.cgi HTTP/class="num">1.1 Host: example.com User-Agent: curl/class="num">7.46.class="num">0 Accept: */* Content-Length: class="num">313 Expect: class="num">100-class="kw">continue Content-Type: multipart/form-data; boundary=------------------------d74496d66958873e --------------------------d74496d66958873e Content-Disposition: form-data; name="person" anonymous --------------------------d74496d66958873e Content-Disposition: form-data; name="secret"; filename="file.txt" Content-Type: text/plain contents of the file --------------------------d74496d66958873e-- <span class="keyword">class="type">void</span> appendPhoto(<span class="keyword">class="type">class="kw">string</span> filename, <span class="keyword">class="type">class="kw">string</span> hash, <span class="keyword">class="type">uchar</span> &data[], <span class="keyword">class="type">bool</span> common_flag=<span class="macro">class="kw">false</span>) { <span class="keyword">class="type">int</span> flags=<span class="macro">FILE_READ</span>|<span class="macro">FILE_BIN</span>|<span class="macro">FILE_SHARE_WRITE</span>|<span class="macro">FILE_SHARE_READ</span>; <span class="keyword">if</span>(common_flag) flags|=<span class="macro">FILE_COMMON</span>; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> handle=<span class="functions">FileOpen</span>(filename,flags); <span class="keyword">if</span>(handle==<span class="macro">INVALID_HANDLE</span>) <span class="keyword">class="kw">return</span>; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> size=(<span class="keyword">class="type">int</span>)<span class="functions">FileSize</span>(handle); <span class="keyword">class="type">uchar</span> img[]; <span class="functions">ArrayResize</span>(img,size); <span class="functions">FileReadArray</span>(handle,img,<span class="number">class="num">0</span>,size); <span class="functions">FileClose</span>(handle); <span class="keyword">class="type">int</span> pos = <span class="functions">ArraySize</span>(data); <span class="keyword">class="type">int</span> offset = pos + size; <span class="keyword">class="type">int</span> hlen = <span class="functions">StringLen</span>(hash)+<span class="number">class="num">6</span>; <span class="keyword">class="type">int</span> newlen = offset + hlen; <span class="functions">ArrayResize</span>(data, newlen); <span class="functions">ArrayCopy</span>(data, img, pos); <span class="functions">StringToCharArray</span>(<span class="class="type">class="kw">string">"\r\n--"</span>+hash+<span class="class="type">class="kw">string">"\r\n"</span>, data, offset, hlen); } <span class="keyword">class="type">class="kw">string</span> uploadPhoto(<span class="keyword">class="type">class="kw">string</span> filename) { <span class="comment">class=class="str">"cmt">// POST multipart/form-data</span> <span class="keyword">class="type">class="kw">string</span> url = <span class="class="type">class="kw">string">"https:class=class="str">"cmt">//upload.twitter.com/class="num">1.1/media/upload.json"</span>; <span class="comment">class=class="str">"cmt">//class="type">class="kw">string url = "https://httpbin.org/anything";</span> <span class="keyword">class="type">class="kw">string</span> params[][<span class="number">class="num">2</span>]; <span class="keyword">class="type">class="kw">string</span> query = oauthRequest(params, url, <span class="class="type">class="kw">string">"POST"</span>); <span class="keyword">class="type">class="kw">string</span> o = getOauth(params); <span class="keyword">class="type">class="kw">string</span> custom_headers = <span class="class="type">class="kw">string">"Content-Type: multipart/form-data;"</span> <span class="class="type">class="kw">string">" boundary="</span>; <span class="keyword">class="type">class="kw">string</span> boundary = getNonce(); <span class="functions">StringAdd</span>(custom_headers, boundary); <span class="comment">class=class="str">"cmt">// use nonce as boundary class="type">class="kw">string</span> <span class="functions">StringAdd</span>(custom_headers, <span class="class="type">class="kw">string">"\r\n"</span>); <span class="keyword">class="type">class="kw">string</span> headers = getHeaders(o, custom_headers, <span class="class="type">class="kw">string">"upload.twitter.com"</span>); <span class="comment">class=class="str">"cmt">//class="type">class="kw">string query = getQuery(params, url);</span> <span class="comment">class=class="str">"cmt">//Print(query);</span> <span class="keyword">class="type">uchar</span> data[];