
从头开始开发智能交易系统(第 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



