请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3353786 新帖:37

无需 DLL 的原生 MT4/MT5 推特(Twitter)客户端

大师做交易发表于:9 月 13 日 23:00回复(1)

概述


Twitter 提供了一个免费平台,任何人都可以在其网站上发布任何内容。 它可以如同财务提示一样有价值,也可以像任何知名人士表白自己见解一样无聊。 由于本文主要关注媒介而不是内容,所以我们就开始吧。

请注册 Twitter,访问 Twitter API 所需的一堆令牌会令您难以自拔。

起初,这些令牌可能会给您造成很大的困扰,仅仅是因为其中有许多类似的名称。 基本上,您需要以下令牌才能使用 Twitter API:
  • customer_token
  • customer_token_secret
  • access_token
  • access_token_secret

注意:

如果您熟悉数字签名的公钥和私钥,那么您已立于正确的方向。

customer_token 和 customer_token_secret 是标识 “Twitter app” 的公钥和私钥。 简而言之,Twitter app 就是用 Twitter API 对您的服务和/或访问进行甄别。 

access_token 和 access_token_secret 是将您标识为 “Twitter user” 的公钥和私钥,在 Twitter 的术语中称为 “用户身份验证”或“用户上下文”。 基于该 access_token,Twitter API 可以甄别正在访问它的是谁。

还有另一个所谓的 bearer_token ,它允许用 Twitter API 进行“匿名”访问。 这种访问方法称为 “app 身份验证”或 “app 上下文”。 如果没有“用户上下文”,则无法访问某些 Twitter API,这些 API 在Twitter API 参考中有很好的归档。

对于那些会用其他编程语言进行编码的人,可能会发现这些Twitter 函数库对于参考非常有用。 它们是非常有用的资源,可为实现细节提供深刻洞察,而有时仅通过阅读 API 文档无法了解这些细节。


Twitter API 和授权

我们专注于上述令牌:

  • customer_token
  • customer_token_secret
  • access_token
  • access_token_secret

YouTube 上有很多有关如何获取这些令牌的指南和/或教程。

OAuth 是基于 Web 的身份验证和授权的公认标准 API,被广泛使用,它也是 Twitter API 正在使用的。
简而言之,OAuth 是数字签名,一种针对数字内容进行签名的方法,故此任何操纵内容的尝试都将导致其无效。

为了正确验证内容及其签名,创建方法和过程都是特制的,所有内容均应精确遵循该过程。

再次指出,Twitter API 文档在整个归档工作中极为出色。

  • 创建签名
  • 授权请求
为了令此过程保持简化,Twitter API 需要一种特定的编码方法,可凌驾于 HTTP 传输请求。 该方法将在下个章节里介绍。

URL 编码和参数排序

为了确保数字签名内容的绝对正确性,必须针对内容本身进行明确定义。 为此目的,Twitter API(准确地说是 OAuth 方法)需要 HTTP POST 和/或 HTTP GET 的参数在进行数字签名之前,要经过明确定义的步骤。

  • URL 编码
  • 按字母顺序排序的参数

必须对 HTTP POST / HTTP GET 参数如下进行编码:

  • 所有字符应该均为“百分号编码 (%XX)”,但以下字符除外:字母加数字(0-9,A-Z,a-z),和这些特殊字符( ‘-‘, ‘.’, ‘_’, ‘~’
  • “百分号编码”的十六进制值应使用大写字母(0-9 A B C D E F)

请注意,在参考文档和本文档中有关 “%” 和 “+” 字符的信息,它们也应正确编码。

另请注意此处引述来自参考文档里的有关参数排序的信息:

  • OAuth 规范里说,按字典顺序排序,这是许多函数库的默认字母顺序排序。
  • 对于两个参数具有相同编码密钥的情况,OAuth 规范里说要继续基于该值进行排序。 然而,Twitter 不接受 API 请求中的重复关键字

此需求的简单实现如下:

string hex(int i)
  {
   static string h="0123456789ABCDEF";
   string ret="";
   int a = i % 16;
   int b = (i-a)/16;
   if(b>15)
      StringConcatenate(ret,ret,hex(b),StringSubstr(h,a,1));
   else
      StringConcatenate(ret,ret,StringSubstr(h,b,1),StringSubstr(h,a,1));
   return (ret);
  }

string URLEncode(string toCode)
  {
   int max=StringLen(toCode);

   string RetStr="";
   for(int i=0; i<max; i++)
     {
      string c = StringSubstr(toCode,i,1);
      ushort asc = StringGetCharacter(c, 0);

      if((asc >= '0' && asc <= '9')
         || (asc >= 'a' && asc <= 'z')
         || (asc >= 'A' && asc <= 'Z')
         || (asc == '-')
         || (asc == '.')
         || (asc == '_')
         || (asc == '~'))
         StringAdd(RetStr,c);
      else
        {
         StringConcatenate(RetStr,RetStr,"%",hex(asc));
        }
     }
   return (RetStr);
  }

string arrayEncode(string &array[][2])
  {
   string ret="";
   string key,val;
   int l=ArrayRange(array,0);
   for(int i=0; i<l; i++)
     {
      key = URLEncode(array[i,0]);
      val = URLEncode(array[i,1]);
      StringConcatenate(ret,ret,key,"=",val);
      if(i+1<l)
         StringConcatenate(ret,ret,"&");
     }
   return (ret);
  }

void sortParam(string&arr[][2])
  {
   string k1, k2;
   string v1, v2;
   int n = ArrayRange(arr,0);

// bubble sort
   int i, j;
   for(i = 0; i < n-1; i++)
     {
      // Last i elements are already in place
      for(j = 0; j < n-i-1; j++)
        {
         int x = j+1;
         k1 = arr[j][0];
         k2 = arr[x][0];
         if(k1 > k2)
           {
            // swap values
            v1 = arr[j][1];
            v2 = arr[x][1];
            arr[j][1] = v2;
            arr[x][1] = v1;
            // swap keys
            arr[j][0] = k2;
            arr[x][0] = k1;
           }
        }
     }
  }

void addParam(string key,string val,string&array[][2])
  {
   int x=ArrayRange(array,0);
   if(ArrayResize(array,x+1)>-1)
     {
      array[x][0]=key;
      array[x][1]=val;
     }
  }


使用上述函数的示例如下:

   string params[][2];

   addParam("oauth_callback", "oob", params);
   addParam("oauth_consumer_key", consumer_key, params);

   sortParam(params);

请注意:
为了简化,参数排序是不完整的,它不考虑多个相同的关键字参数。 如果您期望使用相同关键字的参数,即 html 表单上的单选按钮、复选框,则可能需要改进它。


HMAC-SHA1 尽可能的容易

在创建 OAuth 签名之前,另一个障碍是 MQL 中缺乏 HMAC-SHA1 原生支持。 事实证明,MQL CryptEncode() 在此只能用到一点点,因为它仅支持构建 SHA1-HASH,因此标记为:CRYPT_HASH_SHA1

因此,我们借助 CryptEncode() 编写自己的 HMAC-SHA1


string hmac_sha1(string smsg, string skey, uchar &dstbuf[])
  {
// HMAC as described on:
// https://en.wikipedia.org/wiki/HMAC
//
   uint n;
   uint BLOCKSIZE=64;
   uchar key[];
   uchar msg[];
   uchar i_s[];
   uchar o_s[];
   uchar i_sha1[];
   uchar keybuf[];
   uchar i_key_pad[];
   uchar o_key_pad[];
   string s = "";

   if((uint)StringLen(skey)>BLOCKSIZE)
     {
      uchar tmpkey[];
      StringToCharArray(skey,tmpkey,0,StringLen(skey));
      CryptEncode(CRYPT_HASH_SHA1, tmpkey, keybuf, key);
      n=(uint)ArraySize(key);
     }
   else
      n=(uint)StringToCharArray(skey,key,0,StringLen(skey));

   if(n<BLOCKSIZE)
     {
      ArrayResize(key,BLOCKSIZE);
      ArrayFill(key,n,BLOCKSIZE-n,0);
     }

   ArrayCopy(i_key_pad,key);
   for(uint i=0; i<BLOCKSIZE; i++)
      i_key_pad[i]=key[i]^(uchar)0x36;

   ArrayCopy(o_key_pad,key);
   for(uint i=0; i<BLOCKSIZE; i++)
      o_key_pad[i]=key[i]^(uchar)0x5c;

   n=(uint)StringToCharArray(smsg,msg,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(int i=0; i < ArraySize(dstbuf); i++)
      StringConcatenate(s, s, StringFormat("%02x",(dstbuf[i])));

   return s;
  }


为了验证其正确性,我们拿其与在Twitter API 文档上创建的哈希进行比较:

   uchar hashbuf[];
   string base_string = "POST&https%3A%2F%2Fapi.twitter.com%2F1.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.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521";
   string signing_key = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE";  
   string hash = hmac_sha1(base_string, signing_key, hashbuf);

   Print(hash); // 842b5299887e88760212a056ac4ec2ee1626b549

   uchar not_use[];
   uchar base64buf[];
   CryptEncode(CRYPT_BASE64, hashbuf, not_use, base64buf);
   string base64 = CharArrayToString(base64buf);
   
   Print(base64); // hCtSmYh+iHYCEqBWrE7C7hYmtUk=


WebRequest 救援


感谢 WebRequest(),现在可以轻松地经由 Web 访问任何 REST API,而无需使用任何外部 DLL。
以下代码将简化 WebRequest() 访问 Twitter API 的过程

#define WEBREQUEST_TIMEOUT 5000
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
string SendRequest(string method, string url, string headers="", string params="", int timeout=WEBREQUEST_TIMEOUT)
  {
   char data[];
   char result[];
   string resp_headers;
   ResetLastError();
   StringToCharArray(params, data);
   ArrayResize(data, ArraySize(data)-1);
   int res = WebRequest(method, url, headers, timeout, data, result, resp_headers);
   if(res != -1)
     {
      string resp = CharArrayToString(result);
      if(verbose)
        {
         Print("***");
         Print("Data:");
         Print(CharArrayToString(data));
         Print("Resp Headers:");
         Print(resp_headers);
         Print("Resp:");
         Print("***");
         Print(resp);
        }
      return resp;
     }
   else
     {
      int err = GetLastError();
      PrintFormat("* WebRequest error: %d (%d)", res, err);
      if(verbose)
        {
         Print("***");
         Print("Data:");
         Print(CharArrayToString(data));
        }
      if (err == 4014)
      {
         string msg = "* PLEASE allow https://api.tweeter.com in WebRequest listed URL";
         Print(msg);
      }
     }
   return "";
  }

请查阅 WebRequest 的文档。


引述:

为了使用 WebRequest() 函数,请在“选项”窗口的“智能系统”选卡”允许 URL“ 列表中添加所需服务器的地址。 服务器端口会根据指定的协议自动选择 - “http://” 为 80,“https://” 为 443。


构建 Twitter REST API 的辅助函数

下面是一些有助于构建 Twitter API 签名的辅助函数。

string getNonce()
  {
   const string alnum = "abcdef0123456789";
   char base[];
   StringToCharArray(alnum, base);
   int x, len = StringLen(alnum);
   char res[32];
   for(int i=0; i<32; i++)
     {
      x = MathRand() % len;
      res[i] = base[x];
     }
   return CharArrayToString(res);
  }

string getBase(string&params[][2], string url, string method="POST")
  {
   string s = method;
   StringAdd(s, "&");
   StringAdd(s, URLEncode(url));
   StringAdd(s, "&");
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      if(first)
         first = false;
      else
         StringAdd(s, "%26");  // URLEncode("&")
      StringAdd(s, URLEncode(params[i][0]));
      StringAdd(s, "%3D"); // URLEncode("=")
      StringAdd(s, URLEncode(params[i][1]));
     }
   return s;
  }

string getQuery(string&params[][2], string url = "")
  {
   string key;
   string s = url;
   string sep = "";
   if(StringLen(s) > 0)
     {
      if(StringFind(s, "?") < 0)
        {
         sep = "?";
        }
     }
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      key = params[i][0];
      if(StringFind(key, "oauth_")==0)
         continue;
      if(first)
        {
         first = false;
         StringAdd(s, sep);
        }
      else
         StringAdd(s, "&");
      StringAdd(s, params[i][0]);
      StringAdd(s, "=");
      StringAdd(s, params[i][1]);
     }
   return s;
  }

string getOauth(string&params[][2])
  {
   string key;
   string s = "OAuth ";
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      key = params[i][0];
      if(StringFind(key, "oauth_")!=0)
         continue;
      if(first)
         first = false;
      else
         StringAdd(s, ", ");
      StringAdd(s, URLEncode(key));
      StringAdd(s, "=\"");
      StringAdd(s, URLEncode(params[i][1]));
      StringAdd(s, "\"");
     }
   return s;
  }


例样脚本

现在,我们准备发送第一个 Twitter API 请求。

void verifyCredentials()
  {

   string _api_key = consumer_key;
   string _api_secret = consumer_secret;
   string _token = access_token;
   string _secret = access_secret;

   string url = "https://api.twitter.com/1.1/account/verify_credentials.json";
   
   string params[][2];
   addParam("oauth_consumer_key", _api_key, params);
   string oauth_nonce = getNonce();
   addParam("oauth_nonce", oauth_nonce, params);
   addParam("oauth_signature_method", "HMAC-SHA1", params);
   
   string oauth_timestamp = IntegerToString(TimeGMT());
   addParam("oauth_timestamp", oauth_timestamp, params);
   
   addParam("oauth_token", _token, params);
   addParam("oauth_version", "1.0", params);
   
   sortParam(params);
   
   string query = getQuery(params, url);
   string base = getBase(params, url, "GET");
   uchar buf[];
   string key = URLEncode(_api_secret);
   StringAdd(key, "&");
   StringAdd(key, URLEncode(_secret));
   
   uchar hashbuf[], base64buf[], nokey[];
   string hash = hmac_sha1(base, key, hashbuf);
   CryptEncode(CRYPT_BASE64, hashbuf, nokey, base64buf);
   string base64 = CharArrayToString(base64buf);

   addParam("oauth_signature", base64, params);
   sortParam(params);
   string o = getOauth(params);

   string headers = "Host:api.twitter.com\r\nContent-Encoding: identity\r\nConnection: close\r\n";
   StringAdd(headers, "Authorization: ");
   StringAdd(headers, o);
   StringAdd(headers, "\r\n\r\n");
   
   string resp = SendRequest("GET", query, headers);
   Print(resp);

   // if everything works well, we shall receive JSON-response similar to the following
   // NOTE: Identity has been altered to protect the innocent.
   // {"id":122,"id_str":"122","name":"xxx","screen_name":"xxx123","location":"","description":"", ... 
  }


MT5 图表上的 Twitter 客户端例样


下图显示了一个 Twitter 客户端,其内显示的是印度尼西亚新闻频道的推文。 我正在准备有关更多 Twitter API 实现的后续文章,希望能尽快发布。



MT5 图表上的 Twitter 客户端

图例 1. 在图表上显示推文


另一个截图显示的是从 MT5 终端发布推文。



来自 MT5 终端的推文
图例 2. 从 MT5 终端发布的推文 


未来强化


上述方法已能够很出色地工作,尽管距完整覆盖所有 Twitter API 还很遥远。 对于那些想深入了解 Twitter API 的人来说,这是一个很好的练习。 下一篇后续文章里将介绍有关发布媒介的更多详细信息,包括图表屏幕截图。

您也许想要构建一个 Twitter API 类,甚至是一个通用的 OAuth 客户端类。

有关三足式授权和基于 PIN 的授权的说明


您也许想更深入地研究所谓的三足式授权和基于 PIN 的授权。

深入讲述它们超出了本文的范围,若您需要进一步的指导,请随时与我联系。

结束语

Twitter 是一个广为接受的平台,任何人均可在其上发布几乎所有内容。
借助本文及里面讲述的代码,我希望以自己对访问 Twitter API 的 OAuth 的微薄了解,为 MQL 社区做出贡献。

期待鼓励和改进反馈。 您可随意在自己的免费和/或商业项目中使用本文提供的所有代码。

我要鸣谢 Grzegorz Korycki 所提供的这个函数库代码 (SHA256, SHA384 和 SHA512 + HMAC - 用于 MetaTrader 4 的函数库) 因它而启发我创建了 HMAC-SHA1 函数。

我们发推文来获取乐趣和收益吧!

尽享快乐。


全部回复

0/140

量化课程

    移动端课程