
为 MetaTrader 5 开发 MQTT 客户端:TDD 方法 - 最终篇
“我们的目标始终是在给定问题及其解决方案的限制条件下,尽可能达到最高的抽象水平。”(Bjarne Stroustrup,《使用 C++ 进行编程原理和实践》)
概述
因为这是本系列的最后一篇文章,所以快速回顾一下可能会很有用,或者至少很方便。
在 本系列的第一篇文章中,我们了解到 MQTT 是一种基于发布/订阅交互模型 (pub/sub) 的消息共享协议,它在交易环境中很有用,因为它允许用户实时共享任何类型的数据:交易事务、账户信息、机器学习管道提取的统计数据,以纯文本、XML、JSON 或二进制数据(包括图像)的形式处理。MQTT 重量轻、能抵御网络的不稳定或中断,并且与内容无关。除此之外,该协议已经成熟、久经考验,并且是由 OASIS 维护的开放标准。它最常用的两个版本,即之前的 3.1.1 和当前的 5.0,是用于连接所谓的物联网中几乎无限数量的设备的最常用协议之一。MQTT 可用于任何需要解耦机器之间实时数据共享的场景。
有许多 MQTT 代理可用于开发、测试和生产环境,既有开源的也有商业的,而且有很多适用于几乎所有现代编程语言的 MQTT 客户端。您可以在这个 MQTT 软件列表中查看它们,其中包括代理、库和工具。
在本系列的第二篇文章中,我们描述了此客户端开发的代码组织,并讨论了一些初步的设计选择,例如面向对象范式。在我们第一次重大重构时,大部分代码发生了变化,但功能保持不变。在那篇文章中,我们还描述了与在 Windows Subsystem for Linux (WSL) 上运行的本地代理建立的一些连接,只是为了意识到我们的 CONNECT 类生成了错误数据包。我们对其进行了改进,并在下一篇文章中做了报告。
本系列的第三篇文章专门介绍了该协议的操作行为部分及其与 CONNECT 标志的关系。我们描述了这些标志的语义以及如何设置它们。我们还对该项目所使用的测试驱动开发实践做了一些记录。最后,我们解释了如何测试我们类中的受保护方法。
在本系列的第四篇文章中,我们深入探讨了 MQTT 5.0 属性的重要性。我们对它们中的每一个以及它们各自的数据类型或 MQTT 术语中的数据表示进行了讨论。我们在那里做了一些关于如何使用 MQTT 5.0 属性(特别是用户属性)来扩展协议的记录。
PUBLISH 控制包及其独特(非保留)固定头标志是 本系列第五篇文章的主题。我们花费了大量空间来尝试展示这些 PUBLISH 固定头标志如何在位的级别上运行。我们标出了不同 MQTT 服务质量级别(QoS 0、QoS 1 和 QoS 2)的特点,并用一些示意图展示了每个 QoS 中客户端和代理之间的数据包交换。
我们在这个系列的第六篇文章中描述了一个间奏曲。这是我们第一次重大的代码重构。我们改变了控制包类的蓝图,删除了重复的函数和过时的测试代码。那篇文章主要记录了这些变化,并包含一些有关 PUBACK 控制包的注释。该文章中注释了 PUBACK 原因代码及其各自的原因字符串的语义。
最后,在这第七部分、也是最后一部分中,我们想与您分享一些工作代码,该代码旨在解决在构建用于 EA 交易的指标信号时交易者非常常见的需求:交易账户中缺少指标所需的交易品种。
我们建议一种可能的解决方案是使用自定义交易品种和一对在 Metatrader 5 终端上作为服务运行的 MQTT 客户端。尽管演示代码过于简单并在单个终端实例上运行,但由于 MQTT 协议本身的主要特性 - 通过“代理”中介将发送方和接收方解耦 - 该解决方案可以扩展以适应任意数量的设备实例和交易品种。
在文章的最后,我们指出了该库的当前状态、我们的开发重点和可能的路线图,以及您可以关注和为项目做出贡献的地方。
在后面的描述中,我们使用的术语 MUST 和 MAY 是 OASIS 标准使用的,而 OASIS 标准使用的术语 MUST 和 MAY 又是 IETF RFC 2119 中描述的。
此外,除非另有说明,所有引文均来自 OASIS 标准。
交易者的需求
假设您是一名交易者,专门从事加密货币交易。您了解到,标准普尔 500 指数(S&P 500)与一种几乎不为人知的、名为 AncapCoin 的加密货币之间存在一致的负相关性。您注意到,当标准普尔 500 指数上涨时,AncapCoin 就会下跌,反之亦然。这些知识让您在市场上占据优势,并且您已经通过交易 AncapCoin 赚钱,因为它与标准普尔 500 指数呈负相关。现在,为了最大化您的收益,您希望实现操作自动化,最终在 VPS 中全天候运行您的 EA 交易。您所需要的只是一个基本的标准普尔 500 指标来支持您的 EA 买卖决策。
但 AncapBroker 也是专注于加密货币的,所以他们的交易品种中没有标准普尔 500 指数。而提供标准普尔 500 指数的经纪商并不提供您喜欢的 AncapCoin。如何在 AncapBroker 提供的交易账户中运行标准普尔 500 指标?
尽管 AncapBroker 和 AncapCoin 只是虚构的名字,但这种虚构的场景本身不是虚构的。通常,操作指数的交易者会希望有一个由精选股票组成的指标,而这些股票并不是由提供指数的经纪商所提供的,反之亦然。考虑在中心化交易所中可供交易的商品,以及在差价合约经纪商中可供交易的相应差价合约,你会发现它们之中典型的不匹配,即交易者可以合法访问数据,但数据来自不同的提供商。对于手动、自主交易来说,这个问题影响不大。您可以使用单独的显示器,甚至使用同一显示器中的独立窗口来关注缺失的交易品种市场。但如果是自动化的话,即使两家提供商都提供 Metatrader 5,对于普通零售交易者来说,在需要做出交易决策的地方(交易账户)实时获得报价是非常困难的,甚至几乎是不可能的。
作为开发人员,如果您将此场景视为客户需求,那么您不妨将发布/订阅消息传递模式作为满足此需求的候选方案。而在现有的开放且维护良好的发布/订阅规范中,MQTT 可能是最简单、最便宜且最强大的规范之一。这里要明确一点,MQTT 的设计正是为了满足我们虚构场景中上面概述的那种需求,即实时地以最小的网络开销从多个可能地理位置分散的来源收集和分发数据。
在下面的部分中,我们将看到如何使用我们当前状态的客户端在 MetaTrader 5 中实现该解决方案,直至您将数据源帐户中的报价实时传送到您的交易帐户。在那里,它将可用于构建所需的指标。
在交易账户中,我们将
- 创建自定义交易品种来表示我们缺失的标准普尔 500 指数
- 编写一个 SUBSCRIBE MQTT 服务,从 MQTT 代理接收标准普尔 500 指数报价
- 使用 MQL5 函数更新代表标准普尔 500 指数的自定义交易品种
在数据源帐户中,我们将
- 编写一个 PUBLISH MQTT 服务来收集标准普尔 500 指数报价并将其发送到 MQTT 代理
请注意,为了简单起见,我们讨论的是一个交易账户和一个数据源账户,但这些账户的数量理论上是无限的。实际上,两侧连接的账户数量仅受设备物理限制(内存、CPU、网络带宽等)以及 MQTT 代理施加的限制。
另外,请记住,我们的目标不是向您提供现成的可用于实际使用的解决方案。相反, 我们的目标是向您展示一种可能的实现所需的主要步骤 ,当然,也是为了激发您对 MQTT 发布/订阅模式在您的交易操作中或为您的客户提供定制应用程序的潜在用途的兴趣。我们确信,这远远超出了从虚拟加密货币中复制报价的范围。
创建自定义交易品种
自定义交易品种是您根据自己想要或要求的规格创建的交易品种,并使用您提供的报价进行更新。由于您可以控制其规格和报价,自定义交易品种对于创建综合指数以及测试 EA 交易和指标很有用。
您可以使用 Metatrader 5 编辑器图形用户界面创建自定义交易品种,也可以通过特定的 MQL5 函数以编程方式创建自定义交易品种。该文档包含有关如何以编程方式或通过图形用户界面创建、自定义和使用自定义交易品种的详细说明。对于我们的目的来说,图形用户界面就足够了。
在托管您交易账户的 MetaTrader 5 上,单击查看 > 交易品种 > 创建自定义交易品种。
图 01 - MetaTrader 5 - 使用图形用户界面创建自定义交易品种
在接下来弹出的窗口中,在“复制自:”字段中,选择标准普尔 500 指数交易品种,以根据真实指数创建自定义交易品种。输入自定义交易品种的名称和简短描述。
图 02 - MetaTrader 5 - 使用图形用户界面设置自定义交易品种规格
警告:细心的读者可能已经注意到,我们正在交易账户中创建我们的自定义交易品种,但根据我们的示例,该账户没有标准普尔 500 交易品种可用作我们的自定义交易品种的模型。我们应该在数据源帐户上。毕竟,我们在这里所做的正是因为我们的交易账户中没有标准普尔 500 指数。你说得对! 在实际情况下,您需要根据需要填写这些交易品种规范,可能要手动复制标准普尔 500 规范。我们在这里过于简单化,因为创建自定义交易品种不是我们的重点。相反,我们感兴趣的是通过 MQTT 连接账户。当需要根据您的实际情况定制交易品种时,请参阅上面链接的文档。
单击“确定”后,您新创建的自定义交易品种应出现在左侧的树列表中。
图 03 - MetaTrader 5 - 使用图形用户界面创建自定义交易品种后检查交易品种树
然后将其添加到您的市场报价中,以便可以进行图表可视化。此步骤是更新分时报价所必需的。
“CustomTicksAdd 函数仅适用于在“市场报价”窗口中打开的自定义交易品种。如果未在“市场报价”中选择交易品种,则应使用 CustomTicksReplace 添加报价。”(MQL5 参考文档)
图 04 - MetaTrader 5 - 使用图形用户界面创建自定义交易品种后检查市场报价窗口
在市场报价窗口,您会注意到它尚无报价。因此,让我们将此帐户订阅到 MQTT 代理,该代理将发送实时报价以更新我们新创建的 MySPX500 自定义交易品种。
编写 SUBSCRIBE MQTT 服务
在现阶段,我们的客户端可以订阅 QoS 0 和 QoS 1,但为了更新报价/分时报价,我们认为 QoS 0 就足够了,因为在这种情况下最终丢失的分时报价并不重要。我们每 500 毫秒发送一个分时报价,因此如果其中一个丢失,它的位置将立即被下一个占据。
我们使用代理主机和端口的输入参数来启动订阅服务的代码。请记住将其替换为您的实际代理数据。请参阅下一节中有关如何使用本地代理设置开发/测试环境的一些说明。
#property service //--- input parameters input string host = "172.20.106.92"; input int port = 80;
接下来,我们从 Connect 和 Subscribe 类中为对象声明一些全局变量。当停止或从错误返回时,我们将需要这些变量以供删除(清理)。
//--- global vars int skt; CConnect *conn; CSubscribe *sub;
“所有通过 object_pointer=new Class_name 表达式创建的对象都必须通过 delete(object_pointer) 运算符删除。” (MQL5 参考文档)
就在我们服务的 OnStart() 方法中,我们将连接对象配置为 Clean Start 连接 - 这意味着我们没有此连接的代理会话 - 并使用宽松的 Keep Alive 以避免在开发、设置客户端标识符以及最终构建我们的 CONNECT 数据包时需要发送定期的 ping 请求。请确保设置与发布服务中使用的客户端标识符不同的客户端标识符。
uchar conn_pkt[]; conn = new CConnect(host, port); conn.SetCleanStart(true); conn.SetKeepAlive(3600); conn.SetClientIdentifier("MT5_SUB"); conn.Build(conn_pkt);
在相同的 OnStart() 方法中,我们还使用其主题过滤器(Topic Filter)配置和构建我们的订阅包。为了清晰直观,我们使用为自定义交易品种选择的名称作为主题过滤器。这是任意指定的,当然,只要您在发布服务中使用相同的主题过滤器。
uchar sub_pkt[]; sub = new CSubscribe(); sub.SetTopicFilter("MySPX500"); sub.Build(sub_pkt);
最后,我们调用函数按顺序发送两个数据包,委托这些函数进行错误处理,如果我们的订阅请求出现问题,则返回 -1。
if(SendConnect(host, port, conn_pkt) == 0) { Print("Client connected ", host); } if(!SendSubscribe(sub_pkt)) { return -1; }
SendConnect 函数只有几行与 MQTT 相关的代码。其中大部分与网络/套接字相关,我们不会在此详细介绍。相反,请参阅有关网络函数的文档。如果您想更深入地了解 MQL5 网络函数,我们强烈推荐 AlgoBook 中的相关章节,您将在其中找到有关使用 MQL5 进行简单和安全(TLS)网络的详细解释和有用示例。
在 AlgoBook 中,您将找到类似以下摘录的信息,这有助于我们识别并设法解决我们函数中的间歇性和不确定性行为(请参阅附件中的注释代码)。
“熟悉 Windows/Linux 套接字系统 API 的程序员都知道,当套接字的内部缓冲区中没有传入数据时,值 0 也可以是正常状态。然而,此函数在 MQL5 中的行为有所不同。当系统套接字缓冲区为空时,它会推测返回 1,将实际的数据可用性检查推迟到下次调用其中一个读取函数的时候。具体来说,这种虚拟结果为 1 字节的情况通常会在接收内部缓冲区仍然为空的情况下第一次在套接字上调用函数时发生。” (AlgoBook)
对于 MQTT 端,我们在 SendConnect 中所做的一切就是检查来自代理的 CONNACK 响应,以及相关原因代码的值。
if(rsp[0] >> 4 != CONNACK) { Print("Not Connect acknowledgment"); CleanUp(); return -1; } if(rsp[3] != MQTT_REASON_CODE_SUCCESS) // Connect Return code (Connection accepted) { Print("Connection Refused"); CleanUp(); return -1; }
如您所见,在清理指向我们的类对象的动态指针后,我们均返回 -1 以表示错误。
相同比例的网络/MQTT相关代码适用于 SendSubscribe 函数。在检查来自代理的 SUBACK 响应及其各自的原因代码后,如果发生任何错误,我们将删除这些类对象动态指针。
if(((rsp[0] >> 4) & SUBACK) != SUBACK) { Print("Not Subscribe acknowledgment"); } else Print("Subscribed"); if(rsp[5] > 2) // Suback Reason Code (Granted QoS 2) { Print("Subscription Refused with error code %d ", rsp[4]); CleanUp(); return false; }
在无限循环中,我们等待代理消息并在 Publish 类静态方法的帮助下读取它们。
msg += CPublish().ReadMessageRawBytes(inpkt); //printf("New quote arrived for MySPX500: %s", msg); //UpdateRates(msg); printf("New tick arrived for MySPX500: %s", msg); UpdateTicks(msg);
您会注意到,我们留下了注释的开发代码,用于更新报价而不是分时报价。如果您想通过这种方式测试,可以取消注释这些行。请记住,当仅更新报价而不更新分时报价时,某些信息可能不会出现在市场报价窗口和图表中。但是,如果您想减少 RAM、CPU 和带宽消耗,并且对交易自动化数据而不是视觉效果更感兴趣,那么这是一个合理的选择。
UpdateRates 函数与发布服务上的对应函数协同工作。在我们开发 MQTT 用户属性并实现更可靠的二进制数据交换的同时,我们正在为双方支付字符串转换的代价。这是我们准路线图中的首要任务。
void UpdateTicks(string new_ticks) { string new_ticks_arr[]; StringSplit(new_ticks, 45, new_ticks_arr); MqlTick last_tick[1]; last_tick[0].time = StringToTime(new_ticks_arr[0]); last_tick[0].bid = StringToDouble(new_ticks_arr[1]); last_tick[0].ask = StringToDouble(new_ticks_arr[2]); last_tick[0].last = StringToDouble(new_ticks_arr[3]); last_tick[0].volume = StringToInteger(new_ticks_arr[4]); last_tick[0].time_msc = StringToInteger(new_ticks_arr[5]); last_tick[0].flags = (uint)StringToInteger(new_ticks_arr[6]); last_tick[0].volume_real = StringToDouble(new_ticks_arr[7]); if(CustomTicksAdd("MySPX500", last_tick) < 1) { Print("Update ticks failed: ", _LastError); } }
通过启动订阅服务,您应该在日志专家选项卡中看到类似的内容。
图 05 - MetaTrader 5 - 专家选项卡上的日志输出显示 5270 错误
我们的订阅服务正在沙漠中独自交谈。让我们通过运行 MQTT 代理来解决这个问题。
设置本地代理以进行开发和测试
我们用于开发的环境使用了 Windows 机器上的 Windows Subsystem For Linux (WSL)。如果您想要的只是运行示例,则可以在同一台机器上回环同时运行客户端和代理,前提是您对发布和订阅服务使用不同的客户端标识符 。但是如果您想要运行多个示例并且还要设置开发环境,我们建议您为它们设置一台单独的机器。您可能知道,在开发客户端/服务器应用程序时(就此而言,发布/订阅模式可能包含在此架构中),让每一端在其自身主机上运行被认为是一种很好的做法。通过这样做,您可以更早地解决连接、身份验证和其他网络问题。
使用 WSL 的这个设置非常简单。我们在一年前发布的另一篇文章中详细介绍了WSL 的安装、激活和配置 。下面是一些专门针对在 WSL 上使用 Mosquitto 代理的提示。这些小细节使我们的生活变得更加轻松,也许您会发现它们也很有用。
- 如果您使用默认设置激活 WSL,并以简单且推荐的方式安装 Mosquitto,您可能会使用包管理器安装它并将其作为 Ubuntu 服务运行。也就是说,当你启动 WSL shell 时,Mosquitto 将会自动启动。这对于常规使用来说很好也很方便,但对于开发来说,我们建议您停止 Mosquitto 服务并通过带有详细(-v)标志的命令行手动重新启动它。这将可以避免使用 tail 命令来跟踪日志,因为 Mosquitto 将在前台运行并将所有日志重定向到 STDOUT。除此之外,日志不包含使用详细标志启动时获得的所有信息。
图 06 - Windows 子系统 Linux - 停止 Mosquitto 服务器并使用详细标志重新启动
- 请记住,您必须在 Metatrader 5 终端的联网允许 URL 中包含 WSL 主机名。
图 07-适用于 Linux 的 Windows 子系统 - 获取 WSL 主机名
图 08 - MetaTrader 5 - 在终端选项菜单中包含允许的 URL
- 为了提高安全性,Mosquitto 的最新版本默认只允许本地连接,即来自同一台机器的连接。要从另一台机器连接 - 在这种情况下,您的 Windows 机器被视为另一台机器 - 您必须在 Mosquitto.conf 文件中为端口 1883(或您选择的另一个端口)包含一个单行侦听器。
图 09 - Windows 子系统 Linux - 配置 Mosquitto 服务器以侦听端口 1883
- 最后,请记住,对于非 TLS 连接,Mosquitto 将默认在端口 1883 上运行。Metatrader 5 终端仅允许连接端口 80 (HTTP) 和 443 (HTTPS)。因此您需要将流量从端口 80 重定向到端口 1883。如果您安装了名为 redir 的 Linux 实用程序,则可以通过一行简短的命令完成此操作。您也可以使用包管理器来安装它。
图 10 -适用于 Linux 的 Windows 子系统 - 使用 Redir 实用程序执行端口重定向
- 如果你忘记将 WSL 主机名包含在允许的 URL 列表中或重定向端口,则最终将导致连接被拒绝,并且你的日志“专家”选项卡中可能会弹出类似这样的错误
图 11 - MetaTrader 5 - 日志输出显示错误 5273 - 无法从套接字发送/接收数据
即使您不熟悉 Linux,如果一切顺利的话,此设置也不会花费超过十到十五分钟的时间。完成后,您可以检查您的订阅服务是否按预期运行。
图 12 - MetaTrader 5 日志输出显示 MQTT 订阅服务已启动并已订阅
编写 PUBLISH MQTT 服务
发布服务遵循与订阅服务相同的结构,因此我们将节省您的时间,不再重复。除非再次提醒您对发布和订阅服务使用不同的客户端标识符。(我们强调这个客户端标识符是因为,在懒惰的开发人员通常的复制粘贴程序中,我们忽略了这个小细节,浪费了一些时间来调试一个“非错误”,导致 Mosquitto 无法正确传递我们的消息。)
uchar conn_pkt[]; conn = new CConnect(host, port); conn.SetCleanStart(true); conn.SetKeepAlive(3600); conn.SetClientIdentifier("MT5_PUB"); conn.Build(conn_pkt);
发布将不断循环运行,直到停止为止。当然,请记住设置您在发布服务上使用的相同主题过滤器,如果您想更新报价 - 而不是分时报价 - 取消注释对 GetRates 的有效负载分配(并注释对 GetLastTick 的分配)就足够了。
do { uchar pub_pkt[]; pub = new CPublish(); pub.SetTopicName("MySPX500"); //string payload = GetRates(); string payload = GetLastTick(); pub.SetPayload(payload); pub.Build(pub_pkt); delete(pub); //ArrayPrint(pub_pkt); if(!SendPublish(pub_pkt)) { return -1; CleanUp(); } ZeroMemory(pub_pkt); Sleep(500); } while(!IsStopped());
我们关于订阅服务中的网络代码和 MQTT 特定代码的比例所做的注释也适用于此。我们甚至不需要检查 PUBACK,因为我们不会收到它,因为我们使用的是 QoS 0。因此,这只是构建数据包、连接和发送数据包的问题。
值得注意的是,在发布服务中我们也需要付出字符串转换的代价,至少在我们的用户属性完全实现之前,我们就可以放心地交换二进制数据。
string GetLastTick() { MqlTick last_tick; if(SymbolInfoTick("#USSPX500", last_tick)) { string format = "%G-%G-%G-%d-%I64d-%d-%G"; string out; out = TimeToString(last_tick.time, TIME_SECONDS); out += "-" + StringFormat(format, last_tick.bid, //double last_tick.ask, //double last_tick.last, //double last_tick.volume, //ulong last_tick.time_msc, //long last_tick.flags, //uint last_tick.volume_real);//double Print(last_tick.time, ": Bid = ", last_tick.bid, " Ask = ", last_tick.ask, " Last = ", last_tick.last, " Volume = ", last_tick.volume, " Time msc = ", last_tick.time_msc, " Flags = ", last_tick.flags, " Vol Real = ", last_tick.volume_real ); Print(out); return out; } else Print("Failed to get rates for #USSPX500"); return ""; }
通过在 MetaTrader 5 终端上启动发布服务,您应该在日志专家选项卡中看到类似以下内容。
图 13 - MetaTrader 5 日志输出显示 MQTT 发布服务已启动并已连接
您可以在 Mosquitto 代理上订阅该主题并检查代理的详细输出。
图 14 - Windows Subsystem For Linux 显示带有详细标志的 Mosquitto 服务器日志记录
如果消息成功接收,您将在订阅选项卡上看到它。
图 15 - Windows 子系统 Linux 显示 mosquitto_sub 实用程序输出
更新自定义交易品种
在两种服务都到位并与您的代理进行过检查后,是时候在 Metatrader 5 终端中运行它们并查看您的辛勤工作的成果了。
图 16 - MetaTrader 5 Navigator 已启动 MQTT 发布和订阅服务
如果一切顺利,您将在“市场报价”窗口的“分时报价”选项卡上看到类似以下内容。
图 17 - MetaTrader 5 市场报价分时报价标签,包含自定义交易品种报价更新
在您的自定义交易品种图上,分时报价更新也应该反映出来。
图 18 - MetaTrader 5 市场图表及自定义交易品种分时报价更新
在日志专家选项卡上您应该会发现一些详细的输出。这是因为我们暂时留下了一些调试信息。除其他信息外,您还可以在那里看到正在交换的数据包的 PrintArray 函数的输出。此输出可以避免需要使用数据包分析器(如 Wireshark)来检查数据包的内容。
图 19 - MetaTrader 5 在“专家”选项卡上输出日志,使用 MQTT 服务日志进行开发和调试
结论
本文介绍了通过 MQTT 在经纪商和账户之间共享实时行情的功能代码。虽然演示在相同的 Metatrader 5 实例和机器上运行,但它展示了 MQTT 的灵活性和稳健性。在有限的空闲时间内,我们的原生 MQTT 客户端仍在开发中。我们的目标是在六月推出完全合规的版本,解决 QoS_2 实现和用户属性等挑战。我们计划在 4 月底完善代码并在 GitHub 上公开。我们欢迎您加入我们的开源项目,无论是否具备 MQL5 专业知识。我们的测试驱动开发方法确保即使对于非专家而言也能实现无错误的进展。从基础测试开始,我们在稳步推进 MQL5 文档的编写。我们致力于完善我们的客户端,直到它满足所有 MQTT 标准。
加入我们吧,即使只具备基本技能。您的意见非常宝贵。欢迎加入!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/14677




非常好、非常有趣的文章。我学到了很多有用的新东西。坦率地说,这种情况并不常见,但就是这样。我会仔细研读你的其他文章。
嘿,彼得!很高兴你能在文章中找到有用的信息。
顺便说一句,代码在GitHub 上,可供免费使用、研究和开发。
摘自 README:
"2025 年 1 月 6 日更新
俗话说,最好的开源代码是从 "挠痒痒 "开始的。这里就是这种情况。
但事实证明,我们最终找到了更好的解决方法,所以我们不再继续开发这段代码。
如果你认为它可以作为一个有用的起点,或者想从我们的错误中学习,那就叉掉它,随意使用吧。
新年快乐