市场模拟(第 15 部分):套接字(九)
概述
在上一篇文章“市场模拟(第 14 部分):套接字(八)”中,我们探讨了如何不依赖第三方包或工具,仅使用 Python 来完成某些任务。我并不想阻止你使用那些能帮助你在 Python 中完成任务的功能包,但这些演示的目的是为了激发你更大的兴趣去学习相关概念。如果我们能完全依靠我们所喜爱的语言所提供的功能来完成所有事情,那么依赖外部包或解决方案就毫无意义。
我相信,在那篇文章中,我成功地激发了你对某些问题的兴趣,这些问题的内在运作方式很多人并不了解。我认为,如果大多数人选修过相关课程或研究过相关标准资料,他们也从未听说过某些技术。一个例子就是我们在上一篇文章中简要提及的 COM 技术。然而,了解如何使用这些方法将有助于你在编程的各个阶段。
在本文中,我们将解释我们一直试图展示的一个可能解决方案 —— 即如何让 Excel 用户在 MetaTrader 5 中执行操作,而无需发送订单或开仓或平仓。其思路是用户利用 Excel 对特定股票交易品种进行基本面分析。他们只需使用 Excel,就可以指示在 MetaTrader 5 中运行的 EA 交易开仓或平仓。
到目前为止,所有展示和演示的内容都仅限于数据传输,就像我们在聊天一样。然而,我认为对于那些对套接字不太熟悉的人来说,通过这些方法更容易理解核心概念。记住,我们只是触及了“套接字”这座庞大冰山的一角。我建议你进一步研究这项技术,因为一旦你理解了它的工作原理,你就会爱上它,并为其带来的可能性感到惊叹。
但我们还是直接进入今天的主题吧。由于需要实现的大多数事情都相对复杂,而其他事情则相当简单,我们将以从容的步伐逐一完成。这样,即使我们没有展示全部内容,你也能理解它是如何运作的。如果这一目标得以实现,你将能够根据所展示的内容进行调整,并创造出自己的解决方案 —— 这个方案很可能会远远超越今天在此所展示的。我们的主要目标是教育,所以让我们开始吧。
用 Python 创建服务器
如前文所述,无法直接在 Excel 中创建和运行服务器。我们可以这么做,但我们必须努力确保服务器不会干扰我们对 Excel 的使用。实现这一目标的一种方法是在线程中创建服务器。然而,在 Excel 线程上创建和运行服务器是一项复杂的任务。当然,这并不是最困难的任务,但它肯定比我们在这里将要呈现的解决方案要复杂得多。
首先要做的是使用 Python 创建一个独立服务器。该服务器将实际访问 Excel 电子表格。然而,有几点需要考虑。再次强调,这个想法纯粹是为了教育目的。我们所呈现的内容还有很大的改进空间。下面,我们来看主要的 Python 服务器代码。我称之为“主要”代码,因为它可以根据我们的需要进行改进和调整,只需要在必要的地方进行调整即可。但在我们这么做之前,先来了解一下它是如何运作的。这将有助于我们了解能够且应该做出哪些改变。
001. import socket as sock 002. import select 003. import sys 004. from win32com import client as win32 005. 006. class LinkMT5Excel: 007. def __init__(self, host, port, sheet, cell) -> None: 008. try: 009. self.__CMD = '' 010. self.__MT5 = None 011. self.__server = sock.socket(sock.AF_INET, sock.SOCK_STREAM) 012. self.__server.setblocking(False) 013. self.__server.bind((host, int(port))) 014. self.__server.listen() 015. self.__CONN_LIST = [self.__server] 016. EXCEL = win32.GetActiveObject('Excel.Application') 017. self.__SHEET = EXCEL.Worksheets(sheet) 018. self.__COLUNN, L = cell.split('$') 019. self.__LINE = int(L) 020. except: 021. self.__CONN_LIST.clear() 022. 023. def __WriteInExcel(self, index, msg) -> None: 024. try: 025. if self.__SHEET: 026. self.__SHEET.Range(self.__COLUNN + str(self.__LINE + index)).Value = msg 027. except: 028. self.__SHEET = None 029. self.__ShutdownServer() 030. 031. def __ReadInExcel(self, index) -> str: 032. try: 033. if self.__SHEET: 034. return self.__SHEET.Range(self.__COLUNN + str(self.__LINE + index)).Value 035. except: 036. self.__SHEET = None 037. self.__ShutdownServer() 038. return '' 039. 040. def __MT5_Disconnect(self, conn) -> None: 041. if conn == self.__MT5: 042. self.__WriteInExcel(2, 'MetaTrader 5 is offline.') 043. self.__MT5 = None 044. if conn: 045. conn.close() 046. if self.__CONN_LIST: 047. self.__CONN_LIST.remove(conn) 048. 049. def __ShutdownServer(self) -> None: 050. for conn in self.__CONN_LIST: 051. conn.close() 052. self.__CONN_LIST.clear() 053. 054. def __Command(self, cmd, conn) -> bool: 055. if cmd.lower() == '/force server shutdown': 056. self.__ShutdownServer() 057. return True 058. elif cmd.lower() == '/shutdown': 059. self.__MT5_Disconnect(conn) 060. return True 061. self.__CMD = '' if cmd.lower() == 'n/d' else cmd 062. return False 063. 064. def __Refused(self, conn) -> bool: 065. conn.send('Connection Refused...\n\r'.encode()) 066. conn.close() 067. return False 068. 069. def __Checking(self, conn) -> bool: 070. try: 071. info, cmd = conn.recv(512).decode().rstrip(None).split(':') 072. if info.lower() != '<mt5 with excel>': 073. return self.__Refused(conn) 074. if self.__Command(cmd, conn): 075. return True 076. if (self.__MT5) or (cmd.lower() != 'mt5'): 077. return self.__Refused(conn) 078. self.__MT5 = conn 079. self.__WriteInExcel(2, 'MetaTrader 5 is online.') 080. except: 081. return self.__Refused(conn) 082. return True 083. 084. def __SwapMsg(self, rec, conn) -> None: 085. try: 086. if rec: 087. data = conn.recv(1024).decode().rstrip(None) 088. if data: 089. if '/' in data: 090. if self.__Command(data, conn): 091. return 092. else: 093. self.__WriteInExcel(4, data) 094. else: 095. self.__MT5_Disconnect(conn) 096. return 097. conn.send((self.__ReadInExcel(3) + f'[{self.__CMD}]').encode()) 098. except: 099. self.__MT5_Disconnect(conn) 100. 101. def Run(self) -> None: 102. self.__WriteInExcel(0, 'Server online.') 103. self.__MT5_Disconnect(None) 104. while self.__CONN_LIST: 105. read, write, err = select.select(self.__CONN_LIST, [], [], 0.5) 106. for slave in read: 107. if slave is self.__server: 108. conn, addr = slave.accept() 109. conn.setblocking(False) 110. if not self.__Checking(conn): 111. continue 112. self.__CONN_LIST.append(conn) 113. self.__SwapMsg(False, conn) 114. else: 115. self.__SwapMsg(True, slave) 116. 117. def __del__(self) -> None: 118. for n in range(3): 119. self.__WriteInExcel(n, '') 120. self.__WriteInExcel(0, 'Server offline...') 121. 122. if __name__ == '__main__': 123. if len(sys.argv) == 5: 124. Mt5_Excel = LinkMT5Excel(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) 125. Mt5_Excel.Run() 126. del Mt5_Excel 127. else: 128. print(f'Usage: {sys.argv[0]} <HOST> <PORT> <SHEET> <CELL>')
Python 代码
如果您一直关注回放/模拟服务的这个阶段(我们重点解释了套接字),那么这段代码对您来说应该不会有任何陌生之处。如果你已经学习 Python 很长时间了,这段代码将更容易理解。然而,如果这并不适用于你的情况,别担心 —— 我们现在就来详细分析一下这里的情况。所以,对于那些按顺序跟进的人,我们要对其他人保持耐心,尤其是那些在这方面经验不足的人,因为这段代码中的有些内容需要被充分理解。
现在,在第 01 行到第 04 行之间,我们导入了一些必要的包。请注意,我们没有使用标准 Python 安装之外的任何包,并且在这个阶段我们也没有做任何太复杂的事情。您可以使用 pandas、xlwings、openpyxl 或任何其他软件包来执行本文中所示的一些步骤。但为了避免给人留下对某些包存在歧视的印象,我们将仅使用标准的 Python 安装。这样一来,我们就能更容易地选择我们偏好的套餐,并使用我们认为最合适的选项。我们的目标是在不偏向任何特定方向的情况下,实现最大程度的简洁。
完成上述导入后,我们在第 06 行开始创建一个类。我们可以不使用类,但如果我们使用类,就能获得其他好处。因此,在第 07 行,我们首先定义类初始化器。在这里,我们收到了一些参数,这些参数告诉我们服务器正常运行所必需的重要元素。查看这些参数的使用位置,以了解预期结果以及如何改进这段代码。
在第 09 行和第 10 行,我们声明并初始化了两个稍后会用到的重要变量。第 13 行,我们开始使用传递的参数。请注意,我们需要告诉服务器主机地址,也就是我们希望接收连接的位置。如果主机地址值为 0.0.0.0 ,则表示我们将接受任何客户端地址,但也可以是特定值。这由你自行决定。需要注意的是,由于服务器初始化将在 Excel 中完成,因此传递的参数将是字符串。因此,有必要将字符串值转换为适当的值,在本例中为整数,以指定要使用的端口。之后,Python 将知道如何正确解释所使用的值。其余代码行(即第 11 行到第 14 行)负责初始化套接字,以便服务器可以监视端口并等待连接。对此如有疑问,请参阅之前的文章。
现在让我们回到第 08 行,因为尽管之前的步骤在没有充分理由的情况下不应产生故障,但在第 08 行,我们为分析代码中可能存在的错误留出了空间。此类故障发生在程序执行过程中。因此,为了确保脚本运行时用户不会看到异常情况,我们确保代码本身能够处理所有异常。
在第 16 行,我们尝试捕获活动的 Excel 会话。由于服务器会自行初始化,我们将顺利完成这项任务。然而,在第17行中,存在可能导致失败的因素。此时,我们尝试访问一个 Excel 工作表。这种情况随时可能发生,原因很简单,就是工作簿中不存在该工作表,而我们需要它存在。如果成功,在第18行,我们会捕获一个将被服务器使用的特定单元格。请注意,我们将此过程分为两个步骤。在第 18 行,我们输入列名和一个数值。在第 19 行,这个数值被转换为整数,以便在服务器上进行一些简单的计算。但别担心,稍后就会清楚。
现在你需要明白,如果在初始化过程中发生故障,第 21 行代码将关闭服务器,并阻止任何向 Excel 发送数据的尝试。这样做很重要,因为之前可能没有获取到某些内容的访问权限,或者配置不正确,我们不希望用户看到他们看不懂的消息。
现在,我们要着手解决让所有东西都能正常工作的艰巨任务。我们从第 23 行开始,其中有一个过程会尝试向 Excel 写入一些内容。但我们不会随意写入,我们会以一种相当有控制力的方式来写。因此,我们需要一个索引来相对于初始化时指定的列和行进行移动。由于目标纯粹是教育性的,我们不会使用列,只使用行。因此,根据第一行的值,我们将尝试写入初始化时指定的电子表格。然而,由于此过程可能会失败,在第 24 行,我们告诉 Python 等待出现错误。
在第 25 行,我们检查变量是否包含有效内容。这很重要,因为在关闭过程中,可能会出现工作表不存在的情况,我们不希望无端地进入一个循环:服务器尝试关闭,生成一个异常,这又触发另一个退出尝试,程序再次被调用,又生成一个异常,如此循环往复。为了避免这种情况,我们在第 25 行执行检查。然后,在第 26 行,我们尝试将数据写入电子表格。此时,可能会出现错误,因为用户可能不小心从工作簿中删除了服务器所需的表格。在这种情况下,将执行第 27 行及其对应的代码,服务器将关闭。
我们来看第 31 行。此行捕获了电子表格中特定单元格的内容。此单元格以初始化过程中指定的单元格为参考。这就是为什么不要急于求成,要理解正在发生的一切,这一点如此重要。正如尝试写入单元格可能会导致进程失败一样,这里也可能发生同样的情况。但如果读取操作按预期进行,第 34 行将返回单元格的值供服务器使用。
你可能会想:既然第 31 行的这个函数能读取 Excel 电子表格,为何不在初始化时直接指定它是哪个表格和单元格,这样服务器就能直接在 Excel 中查找其余信息了?如果你想到了这一点,我真诚地祝贺你 —— 这意味着你已经注意到 Python 脚本中可以改进的地方。但让我们继续前行,因为还有更多值得一看的地方。
在第 40 行中,有一个过程可帮助 Excel 用户了解 MetaTrader 5 因某种原因何时变得不可用。当设备断开连接时,就会发生这种情况。这个过程很简单,无需进一步解释。第 49 行还提供了一种同样简单的过程,可以因任何原因关闭服务器。其中许多原因与服务器脚本执行过程中的失败有关,但还有一些我们稍后会讨论。目前,不必为此担心。此外,第 54 行有一个非常有趣的函数。从这里开始,事情变得真正有趣起来。但首先,让我们来看看第 54 行的函数中发生了什么。
它非常简单,旨在解析任何可能发送到套接字的命令。此命令必须由服务器执行。目前,我们只有两条命令。第一个命令在第 55 行,目的是强制关闭服务器 —— 简单直接。第二个命令在第 58 行,强制断开客户端连接,在本例中是 MetaTrader 5。如果服务器未收到任何命令执行请求,则第 62 行将返回 false。换句话说,发送的并不是命令。
但为什么我们要返回 false 值呢?原因恰恰在于第 69 行的函数。其主要任务是检查尝试连接到服务器的客户端是否可以被接受。这就是有趣的地方,因为原则上,任何人都可以连接到服务器。然而,这个函数就是为了防止这种情况而设计的。它是如何做到的?它只是测试服务器同意检查连接后,客户端立即向套接字发送的内容。在第 72 行,我们现在检查初始连接头。此标头仅在尝试连接时出现;在其他时候,则不需要。稍后,你就会明白这一切是如何运作的。
现在你应该清楚,在第71行中,标头和任何传输的命令之间是分隔开的。原因很简单,您将在下一篇文章中了解这一点,我们将介绍 Excel 中 VBA 代码的相应部分。如果客户端提供的标头与服务器期望的标头不匹配,则会执行第 64 行的函数。此函数用于通知客户端,由于未被识别,服务器正在断开与该客户端的连接。
如果标头与预期相符,则在第 74 行,我们尝试执行客户端发送的命令。如果不是命令,第 76 行将检查是否已与 MetaTrader 5 建立连接。如果没有连接,或者命令与预期不同(表明客户端不是 MetaTrader 5),则连接将被拒绝。在后一种情况下,在第 78 行和第 79 行,我们将获得 MetaTrader 5 已在线的信息。如果在任何时候发生异常,由于第 80 行的执行,连接将被拒绝。
现在我们进入关键步骤,即第 84 行。它有什么作用?它负责管理 MetaTrader 5 与 Excel 之间的消息交换。请密切关注,因为这个过程虽然相对简单,但如果不理解,你就无法根据需要调整代码。诚然,在通读接下来的几篇文章后,所有这些都会变得合情合理。但就目前而言,让我们试着理解一下 Python 脚本中发生了什么。
由于可能会出现错误,我们运行代码,告诉 Python 我们期望出现错误 —— 这是在第 85 行完成的。如果调用者指示此过程应读取套接字,则第 86 行将成功完成,然后我们继续读取套接字的一部分。这是在第 87 行完成的。
现在请仔细阅读,因为接下来的解释在后续内容中极其重要。在第 88 行,我们检查是否从套接字上接收到任何内容。否则,我们将断开 MetaTrader 5 的连接。如果第 89 行的套接字上有内容,我们会检查接收到的消息中是否存在指定的字符。如果消息中存在这样的字符,则表示这是来自 MetaTrader 5 的命令,将执行第 90 行代码。但如果这次执行失败,将不会再发布任何内容。查看命令代码,您将能够理解 MetaTrader 5 预期接收的数据。当我们讨论 MQL5 代码时,会再次回到这一点。目前,只需知道 MetaTrader 5 可以向服务器发送命令以供执行就足够了。
好吧,但如果不是命令呢?这是因为第 89 行的检查失败了。那么,接下来会发生什么呢?在这种情况下,第 93 行代码将会执行。它将根据 Excel 的请求,将 MetaTrader 5 发送的信息写入特定的单元格。但这个请求是什么?现在我们来到了真正有趣的部分:第 97 行。看看我们在这一行上做了什么。在这里,我们使用服务器通过套接字向 MetaTrader 5 发送请求。这样的说法可以用两种方式表示:
- 第一种方式是从 Excel 中的特定单元格或单元格范围读取的内容。为了简单起见,我们不使用范围,而是使用特定单元格的内容。
- 第二种方式是,在括号内,我们将按照 Excel 的指示,向 MetaTrader 5 发送一些命令来执行。
稍后,我们将讨论如何在 MetaTrader 5 中执行 Excel 中的此类命令或请求。目前,请注意,我们的交互协议相当简单。也就是说,基本上,单元格中的所有内容或 Excel 发送的命令都将传递给 MetaTrader 5。服务器将不会主动参与此阶段的消息交换,但将仅在此过程中指定的点记录消息,以便 VBA 和 MQL5 可以执行其工作。
然而,如果我们想提高或改善 Excel 与 MetaTrader 5 之间信息交换的数量和质量,我们可以使用 Python 来简化原本需要在 VBA 或 MQL5 中完成的工作。但是,由于我们希望尽可能具有教育意义,我们将使用一个非常简单的消息协议,在 Excel 中仅使用一个单元格。如果你明白这是如何做到的,你将能够随心所欲地做任何事情。
请注意,此处使用的所有单元位置都从服务器初始化期间指定的第一个单元开始。理解这些要点很重要,因为其他所有操作都将直接在 Excel VBA 中完成。这将非常简单,易于实施和执行。
好的,我们已经到达了终点。这就把我们带到了第 101 行。关于此过程,几乎所有内容在之前关于套接字的文章中都已经涉及,但仍有几点值得注意。例如,在第 113 行,我们乍一看可能会觉得有些奇怪,但如果你仔细思考并顺着逻辑推理,就会发现其实并不奇怪。如果第 110 行的检查成功,则会向刚刚连接的客户端发送一条消息。通常,这是一种问候。
但在这里,我们将采取完全不同的做法。由于Excel无法长时间保持在线状态,它明白要发送的信息是直接发送到 MetaTrader 5 的,因此理解第 84 行的过程非常重要,这是我们之前描述过的。因此,在第一次调用中,我们不会读取套接字,而只会向其写入数据。当服务器接收到来自 MetaTrader 5 的响应时,第 115 行代码将会执行,而这一次,我们将读取套接字并进行写入操作。这样,我们就能保持信息畅通,不断从 MetaTrader 5 接收信息,并将这些信息传递到 Excel 中。
最后,我们有一个过程,许多人会觉得它相当奇怪。这是因为它是类析构函数。就像我们有一个名为 __init__ 的构造函数一样,我们也有析构函数,在本例中它被称为 __del__。通常,Python 脚本代码中不包含此析构函数,因为 Python 有一个垃圾回收器,当脚本执行完毕后,它会删除对象并释放它们占用的内存。然而,这个析构函数的作用却更为高级。从本质上讲,它迫使 Excel 在通常显示服务器在线的位置显示一条消息,报告服务器处于离线状态。您可以直接在 Excel 中查看它,无需使用其他媒介或资源。其余代码非常简单,我们无需再对其进行关注。
总结性思考
根据此处提供的说明,我们已经完成了关于 Python 服务器的部分。此服务器的目的是尽可能具有教学意义且易于使用 Python 进行操作,仅使用标准语言安装中包含的包。然而,读者自己可以通过使用其他处理 Excel 文件的包和组件,创造出更加复杂且实用的东西。甚至可以直接在这段 Python 代码中生成整个 Excel 电子表格,以及实现许多其他功能。
但我不想因为偏爱任何特定的 Python 包而使情况复杂化。我知道有许多优秀的软件包,它们能让你执行各种各样的任务,甚至完全不需要使用 Excel 来创建内容。然而,正如本系列文章其他部分所述,这里的想法是满足那些拒绝使用除 Excel 以外的任何工具的客户或用户。我们的目标是创建一个电子表格,用于对给定交易品种进行基本面分析,以便我们能够在特定价格范围内进行买卖。你们中有些人可能会问:“为什么不直接在交易品种的图表上,选择想要的价位下单呢?这样,你就不必一直使用 Excel,可以直接在 MetaTrader 5 中完成。”
我完全理解那些有这种想法的人。但也有一些人缺乏查看图表、下单和撤单、开仓和平仓的耐心或意愿。他们只是更倾向于使用几个程序来处理所有内容 —— 通常是那些他们已经熟练掌握,并在此基础上开发出自己的分析方法的程序 —— 而且他们不想放弃这些程序。因此,这些人希望你们这些程序员能够开发出一种方法或手段,利用特定的程序(在本例中是 Excel)在特定价格范围内发送订单、平仓或建仓。
但与此同时,他们也不希望为理解具体如何操作而感到担忧。你被雇佣并领取薪水的目的就是尽一切可能去做事。这正是我要展示的内容:如何实现它,至少是部分实现。作为程序员,你们可能希望以不同的方式或稍作变动来完成这项工作,就我个人而言,这并不太影响我。真正让我感到满足的是,通过阅读这些文章,你将能够创造出一些你原本认为不可能实现的事物。只要你努力学习并投入时间,你就能实现任何目标。在下一篇文章中,我们将探讨 Excel 的 VBA 部分,分析如何使此服务器与 Excel 协同工作。本系列下一篇文章再见。
| 文件 | 描述 |
|---|---|
| Experts\Expert Advisor.mq5 | 演示 Chart Trade 与 EA 交易之间的交互(需要 Mouse Study 才能进行交互)。 |
| Indicators\Chart Trade.mq5 | 创建一个窗口,用于配置要发送的订单(需要 Mouse Study 进行交互)。 |
| Indicators\Market Replay.mq5 | 创建用于与回放/模拟服务交互的控件(交互需要 Mouse Study)。 |
| Indicators\Mouse Study.mq5 | 提供图形控件与用户之间的交互(回放系统和真实市场操作都需要)。 |
| Services/MarketReplay.mq5 | 创建并维护市场回放/模拟服务(整个系统的主文件)。 |
| VS Code C++ Server.cpp | 创建并维护一个用 C++ 开发的套接字服务器(迷你聊天版本)。 |
| Code in Python\Server.py | 创建并维护用于 MetaTrader 5 和 Excel 之间通信的 Python 套接字。 |
| Scripts\CheckSocket.mq5 | 允许测试与外部套接字的连接。 |
| Indicators\Mini Chat.mq5 | 通过指标实现迷你聊天(需要服务器运行)。 |
| Experts\Mini Chat.mq5 | 通过 EA 交易实现迷你聊天(需要服务器运行)。 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12827
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
新手在交易中的10个基本错误
从基础到中级:结构(三)