
从头开始开发智能交易系统(第 17 部分):访问 web 上的数据(III)
概述
在上一篇文章从头开始开发智能交易系统(第 16 部分):访问 web 上的数据(II)中,我们讨论了从 web 抓取数据的问题和后果。 我们还研究了如何在智能系统中使用它,并讨论了三种可能的解决方案,每种方案都有其优缺点。
在第一种解决方案中,其意味着直接经由智能系统抓取数据,我们研究了一个可能的问题,与服务器响应缓慢有关。 我们还提到了这可能对交易系统造成的后果。
在第二种解决方案中,我们实现了基于客户机-服务器模型的信道,其中 EA 充当客户机,脚本充当服务器,对象充当信道。 该模型一直表现优越,直到在您决定更改时间帧那一刻,而它确实在这方面变得不方便。 尽管事实如此,它仍然是最好的系统,因为采用客户机-服务器模型可以确保 EA 不会因等待远程服务器响应而阻塞 — 它简单地读取对象中包含的数据,而不管这些信息来自何处。
第三种也是最后一个解决方案中,我们采用服务改进了客户机-服务器系统。 因此,我们开始使用 MetaTrader 5 平台资源,但很少研究:终端的全局变量。 该方案解决了时间帧变更的问题,而这却是采用脚本的模型的最大缺点。 不过,我们遇到一个新问题:终端的全局变量系统只允许使用双精度型数据。 许多人不知道如何避免这种情况,因此他们通过 MetaTrader 5 提供的通道传递各种信息,例如一段文本。
在本文中,我们将讨论如何绕过此限制。 但不要期待奇迹,因为要让系统按您所希望的方式运行需要大量工作。
这次我们将着手开发一个替代系统。
1. 计划
正如我们所知,我们只能在 MetaTrader 5 提供的通道系统中使用 双精度型变量。 此类型由 8 个字节组成。 您可能会认为这不是非常有用的信息。 但我们来想象计算以下时刻:
计算机系统按字节操作,尽管许多人已经忘记了这个概念。 了解这个系统非常重要。 每个字节由 8 位组成。 1 位是计算系统中的最小数字。 该语言中最小且最简单的类型是布尔型,它由一位组成。 这是最简单的基础。
因此,任何信息,无论多么复杂,都将包含在 1 字节内。 同样,无论有多复杂的信息,它总能纳于一个字节之内,该字节由 8 位组成。 当我们连接 2 个字节时,我们得到系统中的第一个复合集。 第一个复合集称为字(WORD);第二个复合集称为双字(DWORD),其内含 2 个字;第三个复合集称为 QWORD,其内含 2 个双字(4 个字)。 这是汇编语言中所采用的命名法,它是所有现代语言的母语,因此大多数系统延用相同的类型。 仅有的区别在于这些类型的命名方式。
我希望直至此刻,您已能领会这些道理。 为了让初学者更容易理解,请查看下图:
上面的图像展示了当前可用的主要类型,它们涵盖 1 到 64 位。 您也许会想,“为什么我需要这个解释?”。 了解这些信息对于理解我们在本文中将要做的事情非常重要,因为我们将操纵这些类型,以便能够传递具有不同内部属性的信息。
取决于所采用的语言,这些类型中的每一种都可以获得不同的名称。对于 MQL5,它们显示在下表中:
名称 | 字节数量 | 基于汇编语言的名称(上图) |
---|---|---|
bool(布尔) | 仅用 1 位;一个字节可以有 8 个布尔值。 | 仅用 1 位;一个字节可以有 8 个布尔值。 |
char(字符) | 1 | Byte(字节) |
short(短整数) | 2 | Word(整数字) |
int(整数) | 4 | DWord(双字) |
long(长整数) | 8 | QWord(四字) |
此表涵盖有符号整数,有关 MQL5 中的更多详细信息,请参见整数型,其中定义了其它名称。 接下来,实类型(real)与整数型有一些相似之处,但有自己的内部格式和样式。 格式化和建模的示例可以在双精度型数字处看到,但基本上它将与下表匹配:
名称 | 字节数量 | 基于汇编语言的名称(上图) |
---|---|---|
Float(浮点) | 4 | DWord(双字) |
Doble(双精度) | 8 | QWord(四字) |
值得注意的是,浮点和整数模型使用相同的数据根基,但长度不同。 现在我们到了我们真正感兴趣的地方。 如果您理解了逻辑,您最终可以得出以下结论,如下图所示:
QWORD 是 8 个字节,故此 “double(双精度)”允许放置 8 个信息字节。 例如,您可以将 8 个可打印字符传递到终端全局变量中,您将获得服务和 EA 之间的连接结果,如下所示。
数据都可以,我认为这个想法本身是可以理解的。 最大的细节是,如果消息包含超过 8 个可打印字符,则必须将其分割成更多部分。 但是,如果要非常快速地传递信息,譬如说,在一个周期内,那么您必须使用尽可能多的全局终端变量来在一个周期中传输消息。 然后需要将它们粘合在一起以恢复原始消息。 但是,如果它能以分组形式传递,我们必须为服务器创建一个表单,如此服务才能知道客户端(在本例中为 EA),将读取消息,并等待下一个消息块。
这类问题有多种解决方案。 如果您想了解或实现这些解决方案,您不需要从头开始创建所有内容 — 您可以利用网络通信协议(如 TCP/IP 或 UDP)相同的方式建模,并用全局终端变量将该思路适配到信息传输系统。 一旦您了解了协议是如何工作的,这个任务就不再复杂,而是转化为您所采用的语言技能和知识的问题。 这是一个非常广泛的主题,值得针对每种情况和问题进行单独研究。
2. 实现
既然现在我们理解了我们将要采取的思路,我们就可以进行初步实现,来测试系统如何在服务和 EA 之间传输信息。 但我们只传递可打印字符。
2.1. 基本模型
我们将使用来自上一篇文章中的系统,需要修改的文件,将从头文件开始。 其新内容在以下代码中完整展示:
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_GlobalNameChannel "InnerChannel" //+------------------------------------------------------------------+ union uDataServer { double value; char Info[sizeof(double)]; }; //+------------------------------------------------------------------+
头文件是基本。 它包含全局终端值的声明。 它还具有一个新的结构,一个 union。 union 与 structure 的不同之处在于,structure 数据组合不涉及物理存储空间交错,而 union 总是共享物理存储空间,即较短数据占取较长数据内部的一截。 在前一种情况下,我们使用一个双精度值作为基础,其中包含 8 个字节。 但请注意,我使用了一个系统来捕获长度 sizeof,因此,如果将来双精度变更长的话(这是不可能的),此代码将自动适应它。
我们得到的结果如下:
请注意,这与上面的图片类似,但这是 union 做到的。
下一个要修改的代码是对应于客户端的 EA。 完整代码如下所示:
#property copyright "Daniel Jose" #property description "Testing internal channel\nvia terminal global variable" #property version "1.04" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ int OnInit() { EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ void OnTimer() { uDataServer loc; if (GlobalVariableCheck(def_GlobalNameChannel)) { GlobalVariableGet(def_GlobalNameChannel, loc.value); Print(CharArrayToString(loc.Info, 0, sizeof(uDataServer))); } } //+------------------------------------------------------------------+
请注意,这里我们调用函数 CharArrayToString 将 uchar 数组转换为字符串。 不过,要注意,我们仍然得到一个双精度值,因为它是唯一可以从终端的全局变量里接收的值。 与之对比,MQL5 中的 sting 遵循 C/C++ 的原则,因此我们不能使用任何扩展字符码,但我们只能自创。 但那是另一回事。 在此,我们不会详细介绍如何做到这一点:您可能希望使用建模数据压缩来突破 8 字节的限制。
但我们仍然需要一个充当服务器的程序。 在我们的例子中,服务器就是一个服务。 下面是测试系统的代码:
//+------------------------------------------------------------------+ #property service #property copyright "Daniel Jose" #property version "1.03" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ void OnStart() { uDataServer loc; char car = 33; while (!IsStopped()) { if (!GlobalVariableCheck(def_GlobalNameChannel)) GlobalVariableTemp(def_GlobalNameChannel); for (char c0 = 0; c0 < sizeof(uDataServer); c0++) { loc.Info[c0] = car; car = (car >= 127 ? 33 : car + 1); } GlobalVariableSet(def_GlobalNameChannel, loc.value); Sleep(1000); } } //+------------------------------------------------------------------+
这是一种简单,但非常有效和实用的方法。
在平台上启动程序,我们将获得以下结果
这也许看起来很愚蠢、毫无重点,但只要有一点创造力,任何人都可以让这个系统变得堪当大任,令它做到他人甚至难以想象的事情。
为了演绎这一点,我们来修改系统,并展示一个非常简单的东西,只是为了引起好奇心和兴趣。 想想对于这样的通信系统,可以发现哪些非常奇特的功能。
2.2. 交换纸条
交换纸条是客户端和服务器之间的信息交换,在此期间,服务器知道客户端想要接收什么信息,因此服务器可以开始生成或查找该信息。
这个概念很容易理解。 但其实现可能是一个相当大的挑战,尤其是在数据建模方面,当通道用于传输数据时,我们只有 8 个字节可用。
2.2.1. 客户端-服务器通信测试
查看服务代码,如下所示:
#property service #property copyright "Daniel Jose" #property version "1.03" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ void OnStart() { uDataServer loc, loc1, loc2; char car = 33; while (!IsStopped()) { if (!GlobalVariableCheck(def_GlobalValueInChannel)) { GlobalVariableTemp(def_GlobalValueInChannel); GlobalVariableTemp(def_GlobalMaskInfo); GlobalVariableTemp(def_GlobalPositionInfos); } for (char c0 = 0; c0 < sizeof(uDataServer); c0++) { loc.Info[c0] = car; car = (car >= 127 ? 33 : car + 1); } GlobalVariableSet(def_GlobalValueInChannel, loc.value); GlobalVariableGet(def_GlobalMaskInfo, loc1.value); GlobalVariableGet(def_GlobalPositionInfos, loc2.value); Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), " ",loc2.Position[0], " ", loc2.Position[1]); Sleep(1000); } } //+------------------------------------------------------------------+
注意在新的服务代码(充当服务器)中一些特别有趣的部分。 现在我们有三个变量,替代原来的一个。 它们要做的就是创建一个足够大的通道,以便支持客户端(或在某些情况下是 EA)和服务器(我们的服务)之间的通信。 请注意以下几行:
Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), " ",loc2.Position[0], " ", loc2.Position[1]);
这些是客户端发布的数据。 注意,我们使用 2 个变量传递 3 条不同的信息。 但这怎么可能呢? 为了理解这一点,我们需要查看头文件代码,如下所示。
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_GlobalValueInChannel "Inner Channel" #define def_GlobalMaskInfo "Mask Info" #define def_GlobalPositionInfos "Positions Infos" //+------------------------------------------------------------------+ union uDataServer { double value; uint Position[2]; char Info[sizeof(double)]; }; //+------------------------------------------------------------------+
您也许会认为这个 union 中的每个变量都是彼此独立的。 我建议您看看本文的开头,因为尽管我们有不同名称的变量,但在这里它们被视为一个变量,宽度为 8 字节。 为了更清楚,请查看下面的图片,它准确地反映了正在发生的情况:
此规划图展示出 uDataServer 内部逻辑。
如果您觉得它太复杂,您应该尝试使用 union 来了解它们的实际工作方式,因为它们在编程中非常有用。
但我们还是回到系统上来。 接下来要做的是为客户机 EA 创建代码。 它如下面所见。
#property copyright "Daniel Jose" #property description "Testing internal channel\nvia terminal global variable" #property version "1.04" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ enum eWhat {DOW_JONES, SP500}; input eWhat user01 = DOW_JONES; //Search //+------------------------------------------------------------------+ int OnInit() { EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ void OnTimer() { uDataServer loc; SetFind(); if (GlobalVariableCheck(def_GlobalValueInChannel)) { GlobalVariableGet(def_GlobalMaskInfo, loc.value); Print(CharArrayToString(loc.Info, 0, sizeof(uDataServer)), " ", GlobalVariableGet(def_GlobalValueInChannel)); } } //+------------------------------------------------------------------+ inline void SetFind(void) { static int b = -1; uDataServer loc1, loc2; if ((GlobalVariableCheck(def_GlobalValueInChannel)) && (b != user01)) { b = user01; switch (user01) { case DOW_JONES : StringToCharArray("INDU:IND", loc1.Info, 0, sizeof(uDataServer)); loc2.Position[0] = 172783; loc2.Position[1] = 173474; break; case SP500 : StringToCharArray("SPX:IND", loc1.Info, 0, sizeof(uDataServer)); loc2.Position[0] = 175484; loc2.Position[1] = 176156; break; } GlobalVariableSet(def_GlobalMaskInfo, loc1.value); GlobalVariableSet(def_GlobalPositionInfos, loc2.value); } }; //+------------------------------------------------------------------+
注意,在这个 EA 中,我们发送和接收信息,也就是说,我们可以控制服务应该如何工作。 在一个变量中,我们传递一个小字符串,指示服务应该查找什么;而在另一个变量里,我们传递 2 个地址点。
作为响应,服务将返回信息。 但为了理解第一点,请查看下面视频中的结果:
3.1.2.2 - 创建实用版本
现在我们已经看明白了系统的工作原理,我们可以让一些东西真正起作用。 这次我们将从 web 服务器收集信息。 这需要大量的修改,来确保对正在发生的事情有完美的理解。 有时我们也许会想象我们正在接收更新的数据,尽管事实上我们在分析中所用的都是垃圾。 在编程阶段,您必须非常小心,不要让自己暴露在这种风险中。 您可以做的是添加尽可能多的测试,并尝试让系统运行时尽可能报告检测到的任何异常活动。
记住: 信息只有在您信任它时才会对您有用。
首先,我们来编辑头文件,令其看起来像这样:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #define def_GlobalValueInChannel "Inner Channel" #define def_GlobalMaskInfo "Mask Info" #define def_GlobalPositionInfos "Positions Infos" //+------------------------------------------------------------------+ #define def_MSG_FailedConnection "BAD" #define def_MSG_FailedReturn "FAILED" #define def_MSG_FailedMask "ERROR" #define def_MSG_FinishServer "FINISH" //+------------------------------------------------------------------+ union uDataServer { double value; uint Position[2]; char Info[sizeof(double)]; }; //+------------------------------------------------------------------+
高亮显示的部分表示我们将用于报告某些奇怪活动的代码。 您应该使用最多 8 个字符,但您还需要创建一个不太可能由市场创建的序列,这不是一件容易的事情。 即使一切看起来都很好,市场仍有可能生成一个数值,对应于您所用的服务器错误消息序列。 无论如何,您也可以为此目的使用终端的全局变量,这将增加可能的组合数量,从而允许您创建更多的东西。 但我打算尽可能少的采用全局终端变量。 不过,在实际情况下,我会考虑它,并可能采用一个变量来指示和报告错误或异常活动。
下一部分是 EA 的完整代码。
#property copyright "Daniel Jose" #property description "Testing internal channel\nvia terminal global variable" #property version "1.04" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ enum eWhat {DOW_JONES, SP500}; input eWhat user01 = DOW_JONES; //Search //+------------------------------------------------------------------+ int OnInit() { EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTick() { } //+------------------------------------------------------------------+ void OnTimer() { ClientServer(); } //+------------------------------------------------------------------+ inline void ClientServer(void) { uDataServer loc1, loc2; string sz0; SetFind(); if (GlobalVariableCheck(def_GlobalValueInChannel)) { GlobalVariableGet(def_GlobalMaskInfo, loc1.value); loc2.value = GlobalVariableGet(def_GlobalValueInChannel); sz0 = CharArrayToString(loc2.Info, 0, sizeof(uDataServer)); if (sz0 == def_MSG_FailedConnection) Print("Failed in connection."); else if (sz0 == def_MSG_FailedReturn) Print("Error in Server Web."); else if (sz0 == def_MSG_FailedMask) Print("Bad Mask or position."); else if (sz0 == def_MSG_FinishServer) Print("Service Stop."); else Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), " ", loc2.value); } } //+------------------------------------------------------------------+ inline void SetFind(void) { static int b = -1; uDataServer loc1, loc2; if ((GlobalVariableCheck(def_GlobalValueInChannel)) && (b != user01)) { b = user01; switch (user01) { case DOW_JONES : StringToCharArray("INDU:IND", loc1.Info, 0, sizeof(uDataServer)); loc2.Position[0] = 172783; loc2.Position[1] = 173474; break; case SP500 : StringToCharArray("SPX:IND", loc1.Info, 0, sizeof(uDataServer)); loc2.Position[0] = 175487; loc2.Position[1] = 176159; break; } GlobalVariableSet(def_GlobalMaskInfo, loc1.value); GlobalVariableSet(def_GlobalPositionInfos, loc2.value); } }; //+------------------------------------------------------------------+
高亮显示的行非常重要,应该仔细思考,因为我们确实想知道发生了什么。 正如您所看到的,我们可以告诉用户一些比头文件中创建的序列更详细的信息,从而令编程、以及维护解决方案更轻松。 代码的其余部分没有太大变化。 查看下面的服务代码。
#property service #property copyright "Daniel Jose" #property version "1.03" //+------------------------------------------------------------------+ #include <Inner Channel.mqh> //+------------------------------------------------------------------+ void OnStart() { uDataServer loc1, loc2; while (!IsStopped()) { if (!GlobalVariableCheck(def_GlobalValueInChannel)) { GlobalVariableTemp(def_GlobalValueInChannel); GlobalVariableTemp(def_GlobalMaskInfo); GlobalVariableTemp(def_GlobalPositionInfos); } GlobalVariableGet(def_GlobalMaskInfo, loc1.value); GlobalVariableGet(def_GlobalPositionInfos, loc2.value); if (!_StopFlag) { GlobalVariableSet(def_GlobalValueInChannel, GetDataURL( "https://tradingeconomics.com/stocks", 100, "<!doctype html>", 2, CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), loc2.Position[0], loc2.Position[1], 0x0D ) ); Sleep(1000); } } GlobalVariableSet(def_GlobalValueInChannel, Codification(def_MSG_FinishServer)); } //+------------------------------------------------------------------+ double GetDataURL(const string url, const int timeout, const string szTest, int iTest, const string szFind, int iPos, int iInfo, char cLimit) { string headers, szInfo = ""; char post[], charResultPage[]; int counter; if (WebRequest("GET", url, NULL, NULL, timeout, post, 0, charResultPage, headers) == -1) return Codification(def_MSG_FailedConnection); for (int c0 = 0, c1 = StringLen(szTest); (c0 < c1) && (!_StopFlag); c0++) if (szTest[c0] != charResultPage[iTest + c0]) return Codification(def_MSG_FailedReturn); for (int c0 = 0, c1 = StringLen(szFind); (c0 < c1) && (!_StopFlag); c0++) if (szFind[c0] != charResultPage[iPos + c0]) return Codification(def_MSG_FailedMask); if (_StopFlag) return Codification(def_MSG_FinishServer); for (counter = 0; charResultPage[counter + iInfo] == 0x20; counter++); for (;charResultPage[counter + iInfo] != cLimit; counter++) szInfo += CharToString(charResultPage[counter + iInfo]); return StringToDouble(szInfo); } //+------------------------------------------------------------------+ inline double Codification(const string arg) { uDataServer loc; StringToCharArray(arg, loc.Info, 0, sizeof(uDataServer)); return loc.value; } //+------------------------------------------------------------------+
高亮显示的行也很重要 — 服务将发出警告:它不再运行。
那么,当您执行此系统时,将得到以下结果:
结束语
我希望我已经解释清楚了在 MetaTrader 5 平台上研究、搜索和使用 web 数据的相关思路。 我明白这在一开始可能不是很清晰,特别是对于那些在编程方面尚无深厚、广泛基础知识的人,但随着时间的推移,通过纪律和学习,您最终会掌握大部分的材料。 在此,我尝试分享至少一点我所知道的。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10447



