MQL5 中的信号服务客户端程序
因此我们决定,服务消息中的文本将采用 JSON 格式。
在最常见的版本中,JSON 是对象的文本说明,类似于 MQL5 中对结构的说明。对象带有花括号,在花括号内,它的特性用逗号分隔:每个特性都有一个带引号的标识符,后跟一个冒号和特性值。此处支持几种基本类型的特性:字符串、整数和实数、布尔值 true/false 和空值 null。此外,特性值可以是对象或数组。数组用方括号说明,方括号中的元素用逗号分隔。例如,
{
|
顶层的数组基本上都是有效的 JSON。例如,
[
|
为了减少使用 JSON 的应用程序协议中的流量,通常将字段名缩写成几个字母(通常是一个字母)。
特性名称和字符串值带有双引号。如果要在字符串中指定引号,必须用反斜杠对其进行转义。
JSON 的使用使得该协议具有通用性和可扩展性。例如,对于正在设计的服务(交易信号,以及在更一般的情况下,账户状态复制),可以假设消息结构如下:
{
|
其中某些特征可能支持也可能不支持客户端程序的特定实现(它们会忽略不“理解”的任何内容)。此外,在同级的特性名称没有冲突的情况下,每个信息提供者都可以向 JSON 添加自己的特定数据。消息服务将转发该信息。当然,接收端的程序必须能够解析这些特定的数据。
本书还介绍了一个名为 ToyJson 的 JSON 解析器("toy" JSON,toyjson.mqh 文件),它很小,效率很低,并且不支持格式规范的全部功能(例如,在处理 escape 序列方面)。它是专门为这个演示服务编写的,针对预期的、不太复杂的交易信号信息结构进行了调整。这里不再赘述,我们从信号服务的 MQL 客户端的源代码中就能看出它的使用原理。
为你的项目和此项目的进一步开发考虑,你可以选择 mql5.com 网站上代码库中可用的其他 JSON 解析器。
每个 ToyJson 的一个元素(容器或特性)由 JsValue 类对象说明。定义了 put(key, value) 方法的几个重载,可用于在 JSON 对象或 put(value) 中添加命名内部特性,以在 JSON 数组中添加值。该对象还可以表示基本类型的单个值。要读取 JSON 对象的属性,可以对 JsValue 应用运算符的表示法 [],后跟带圆括号的特性名。显然支持通过整数索引来访问 JSON 数组内部。
形成相关对象 JsValue 的所需配置后,可以使用 stringify(string&buffer) 方法将其序列化为 JSON 文本。
toyjson.mqh 中的第二个类 - JsParser - 允许你执行相反的操作:将带有 JSON 说明的文本转换成 JsValue 对象的层次结构。
考虑到使用 JSON 的类,让我们编写一个 EA 交易 MQL5/Experts/MQL5Book/p7/wsTradeCopier/wstradecopier.mq5,它将能够在事务复制服务中扮演两个角色:提供有关账户交易的信息,或者从服务接收这些信息以复制此类交易。
从政治角度来看,发送信息的数量和内容由提供商决定,并且可能会因使用服务的场景(目的)而有很大不同。特别是,可以只复制正在进行的交易或整个账户余额以及挂单和保护级别。在本例中,我们将只指出信息传输的技术实现,然后你可以自行选择一组特定的对象和特性。
在代码中,我们将描述从内置结构继承的 3 种结构体,它们用 JSON 进行信息“打包”:
- MqlTradeRequestWeb MqlTradeRequest
- MqlTradeResultWeb MqlTradeResult
- DealMonitorWeb DealMonitor*
严格地说,列表中的最后一个结构体不是内置的,而是由我们在 DealMonitor.mqh 文件中定义的,但它是在交易特性的标准集中填写的。
每个派生结构的构造函数根据传输的主要来源(交易请求、其结果或成交)填充字段。每个结构都实现了 asJsValue 方法,该方法返回一个指向 JsValue 对象的指针,该对象反映了结构的所有特性:使用 JsValue::put 方法将这些特性添加到 JSON 对象中。例如,下面是 MqlTradeRequest 的使用情况:
struct MqlTradeRequestWeb: public MqlTradeRequest
|
我们将所有特性转移到 JSON(这适用于账户监控服务),但是你可以保留有限的一组特性。
对于枚举的特性,我们提供了两种方法用 JSON 将它们表示为整数和枚举元素的字符串名称。方法的选择是使用 VerboseJson 输入参数(理想情况下,它不应该直接写在结构代码中,而是通过构造函数参数编写)。
input bool VerboseJson = false; |
只传递数字可以简化编码,因为在接收端,为了执行“镜像”操作,将数字转换为所需的枚举类型就足够了。但是人们难以通过数字来感知信息,可能还需要分析具体情况(消息)。因此,支持字符串表示的方法是有意义的,因为它更“友好”,尽管这需要对接收算法进行额外操作。
输入参数还分别为提供者和订阅者指定服务器地址、应用程序角色和连接细节。
enum TRADE_ROLE
|
提供者组中的参数 SymbolFilter 和 MagicFilter 允许你将受监控的交易活动限制为给定的交易品种和幻数。SymbolFilter 中的空值表示只控制图表的当前交易品种,要拦截任何交易,请输入 * 符号。为此,信号提供者将使用 FilterMatched 函数,该函数接受交易的交易品种和幻数。
bool FilterMatched(const string s, const ulong m)
|
订阅者输入组中的 SymbolSubstitute 参数允许用另一个交易品种替换消息中收到的交易品种,该交易品种将用于复制交易。在跨经纪商环境中,该功能尤其有用,可确保同一金融工具的交易代码名称不同。但该参数也执行重复信号许可筛选功能:只有这里指定的交易品种能进行交易。例如,要允许 EURUSD 交易品种的信号交易(即使没有交易代码替换),你需要在参数中设置 "EURUSD=EURUSD" 字符串。信号消息的交易品种显示在 = 交易品种的左边,交易的交易品种显示在右边。
字符替换列表在初始化期间由 FillSubstitutes 函数处理,然后由 FindSubstitute 函数用来替换和解析交易。
string Substitutes[][2];
|
为了与服务通信,我们定义了一个从 WebSocketClient 派生的类。首先,当消息到达 onMessage 处理程序时,需要根据信号开始交易。在分析了提供者端信号的形成和发送之后,我们稍后会回到这个问题。
class MyWebSocket: public WebSocketClient<Hybi>
|
OnInit 中的初始化可开启计时器(用于定期调用 wss.checkMessages(false))并编写带用户详情的自定义头文件,具体取决于所选的角色。然后我们用 wss.open(custom) 调用打开连接。
int OnInit()
|
复制机制,即拦截交易并将有关交易的信息发送到 Web 服务,是在 OnTradeTransaction 处理程序中启动的。正如我们所知,这不是唯一的方法,在 OnTrade 中分析账户状态的“快照”是有可能的。
void OnTradeTransaction(const MqlTradeTransaction &transaction,
|
我们跟踪成功完成的交易请求的事件(即满足指定筛选条件)。随后,请求的结构、请求的结果和交易都被转换成 JSON 对象。所有这些都放在一个公共容器 msg 中,分别名为 "req"、"res" 和 "deal"。回顾将容器本身作为 "msg" 特性包含在 web 服务消息中。
// container to attach to service message will be visible as "msg" property: // {"origin" : "this_publisher_id", "msg" : { our data is here }}
|
填充后,容器作为字符串输出进入 buffer,打印到日志,并发送到服务器。
我们可以向这个容器添加其他信息:账户状态(资金回撤率、杠杆率)、挂单的数量和特性等等。因此,为了展示扩展信息内容的可能性,我们在上面增加了未平仓仓位的数量。为了根据筛选器选择仓位,我们使用 PositionFilter 类对象 (PositionFilter.mqh):
PositionFilter Positions;
|
基本上,为了增加可靠性,对于复制器来说,分析仓位的状态是有意义的,而不仅仅是拦截交易。
这就是对信号提供者角色中涉及的 EA 交易部分的分析总结。
正如我们所声明的,作为订阅者,EA 交易接收 MyWebSocket::onMessage 方法中的消息。此处用 JsParser::jsonify 解析传入的消息,从 obj["msg"] 特性中检索由传输方形成的容器。
class MyWebSocket: public WebSocketClient<Hybi>
|
RemoteTrade 函数实现信号分析和交易操作。这里给出了缩写,省略了潜在错误处理。该函数支持两种表示枚举的方式:表示为整数值或字符串元素名称。通过应用运算符 [],对传入的 JSON 对象进行必要的特性(命令和信号属性)检查,包括连续几次(访问嵌套的 JSON 对象)。
bool RemoteTrade(JsValue *obj)
|
该实现不分析交易价格、对交易的可能限制、止损水平和其他时刻。我们只是以当前的本地价格重复交易。此外,平仓时,会检查交易量是否完全相等,这适用于对冲账户,但不适用于净额结算,如果交易量少于仓位(在冲销的情况下可能更多,但此处不支持 DEAL_ENTRY_INOUT 选项),则可能部分平仓。所有这些要点都应最终落实到实际应用中。
我们在同一个终端的不同图表上启动服务器 node.exewspubsub.js 和 EA 交易 wstradecopier.mq5 的两个副本。通常的场景假设需要在不同的账户上启动 EA 交易,但是“矛盾的”选项也适合于检查性能:我们将把信号从一个交易品种复制到另一个交易品种。
在 EA 交易的一个副本中,我们将保留默认设置,保留发布者的角色。应放在 EURUSD 图表上。在 GBPUSD 图表上运行的第二个副本中,我们将角色更改为订阅者。输入参数 SymbolSubstitute 中的字符串 "EURUSD=GBPUSD" 允许基于 EURUSD 信号的 GBPUSD 交易。
连接数据将被记录,带有 HTTP 头文件和问候语(我们之前已看到过),这里略去不讲。
我们来买入 EURUSD,并确保它以 GBPUSD 的相同数量被“复制”。
以下是日志的片段(切记,由于在终端的同一副本中使用了两个 EA 交易,交易消息将被发送到两个图表,因此,为了便于分析日志,你可以交替设置筛选 "EURUSD" 和 "USDUSD"):
(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 0.99886, #=1461313378 (EURUSD,H1) DONE, D=1439023682, #=1461313378, V=0.01, @ 0.99886, Bid=0.99886, Ask=0.99886, Req=2 (EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01, » "f" : "ORDER_FILLING_FOK", "p" : 0.99886, "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682, » "o" : 1461313378, "v" : 0.01, "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682, » "o" : 1461313378, "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY", » "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, "p" : 0.99886, » "s" : "EURUSD"}, "pos" : {"n" : 1}}
|
该片段显示了所执行请求的内容和结果,以及向服务器发送一个 JSON 字符串的缓冲区。
在接收端,立即在 GBPUSD 图表上显示一个警报,给出一条来自服务器的 "raw" 形式的消息,该消息在 JsParser 中成功解析后被格式化。"origin" 特性以原始形式存储,服务器以这种形式告诉我们信号的源。
(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL", » "s" : "EURUSD", "t" : "ORDER_TYPE_BUY", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99886, » "o" : 1461313378}, "res" : {"code" : 10009, "d" : 1439023682, "o" : 1461313378, "v" : 0.01, » "p" : 0.99886, "b" : 0.99886, "a" : 0.99886}, "deal" : {"d" : 1439023682, "o" : 1461313378, » "t" : "2022.09.19 16:45:50", "tmsc" : 1663605950086, "type" : "DEAL_TYPE_BUY", » "entry" : "DEAL_ENTRY_IN", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, » "p" : 0.99886, "s" : "EURUSD"}, "pos" : {"n" : 1}}} (GBPUSD,H1) { (GBPUSD,H1) req = (GBPUSD,H1) { (GBPUSD,H1) a = TRADE_ACTION_DEAL (GBPUSD,H1) s = EURUSD (GBPUSD,H1) t = ORDER_TYPE_BUY (GBPUSD,H1) v = 0.01 (GBPUSD,H1) f = ORDER_FILLING_FOK (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) } (GBPUSD,H1) res = (GBPUSD,H1) { (GBPUSD,H1) code = 10009 (GBPUSD,H1) d = 1439023682 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) v = 0.01 (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) b = 0.99886 (GBPUSD,H1) a = 0.99886 (GBPUSD,H1) } (GBPUSD,H1) deal = (GBPUSD,H1) { (GBPUSD,H1) d = 1439023682 (GBPUSD,H1) o = 1461313378 (GBPUSD,H1) t = 2022.09.19 16:45:50 (GBPUSD,H1) tmsc = 1663605950086 (GBPUSD,H1) type = DEAL_TYPE_BUY (GBPUSD,H1) entry = DEAL_ENTRY_IN (GBPUSD,H1) pid = 1461313378 (GBPUSD,H1) r = DEAL_REASON_CLIENT (GBPUSD,H1) v = 0.01 (GBPUSD,H1) p = 0.99886 (GBPUSD,H1) s = EURUSD (GBPUSD,H1) } (GBPUSD,H1) pos = (GBPUSD,H1) { (GBPUSD,H1) n = 1 (GBPUSD,H1) } (GBPUSD,H1) } (GBPUSD,H1) 警报:订购交易:市场买入 ORDER_TYPE_BUY 0.01 GBPUSD - 成功 |
上面最后一次买入表示 GBPUSD 的一次成功交易。在账户的交易选项卡上,应显示 2 个仓位。
一段时间后,我们平仓 EURUSD 仓位,GBPUSD 仓位应自动平仓。
(EURUSD,H1) TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_SELL, V=0.01, ORDER_FILLING_FOK, @ 0.99881, #=1461315206, P=1461313378 (EURUSD,H1) DONE, D=1439025490, #=1461315206, V=0.01, @ 0.99881, Bid=0.99881, Ask=0.99881, Req=4 (EURUSD,H1) {"req" : {"a" : "TRADE_ACTION_DEAL", "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01, » "f" : "ORDER_FILLING_FOK", "p" : 0.99881, "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009, » "d" : 1439025490, "o" : 1461315206, "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881}, » "deal" : {"d" : 1439025490, "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990, » "type" : "DEAL_TYPE_SELL", "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", » "v" : 0.01, "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}} |
如果第一次交易的类型为 DEAL_ENTRY_IN,现在则为 DEAL_ENTRY_OUT。该警报确认接收消息以及重复仓位被成功平仓。
(GBPUSD,H1) Alert: {"origin":"publisher PUB_ID_001", "msg":{"req" : {"a" : "TRADE_ACTION_DEAL", » "s" : "EURUSD", "t" : "ORDER_TYPE_SELL", "v" : 0.01, "f" : "ORDER_FILLING_FOK", "p" : 0.99881, » "o" : 1461315206, "q" : 1461313378}, "res" : {"code" : 10009, "d" : 1439025490, "o" : 1461315206, » "v" : 0.01, "p" : 0.99881, "b" : 0.99881, "a" : 0.99881}, "deal" : {"d" : 1439025490, » "o" : 1461315206, "t" : "2022.09.19 16:46:52", "tmsc" : 1663606012990, "type" : "DEAL_TYPE_SELL", » "entry" : "DEAL_ENTRY_OUT", "pid" : 1461313378, "r" : "DEAL_REASON_CLIENT", "v" : 0.01, » "p" : 0.99881, "m" : -0.05, "s" : "EURUSD"}, "pos" : {"n" : 0}}} ... (GBPUSD,H1) 警报:认购交易:市场卖出 ORDER_TYPE_SELL 0.01 GBPUSD - 成功 |
最后,在 EA 交易 wstradecopier.mq5 旁,我们创建一个项目文件 wstradecopier.mqproj,向其中添加说明和必要的服务器文件(在 MQL5/Experts/p7/MQL5Book/Web/ 旧目录中)。
综上所述:我们已经组织了一个技术上可扩展的多用户系统,用于通过套接字服务器交换交易信息。由于 web sockets(永久开放连接)的技术特性,信号服务的这种实现更适合于短期和高频交易,以及控制报价中的套利情况。
解决这个问题需要将不同平台上的多个程序结合起来,并连接大量依赖项,这通常象征了到项目级别的过渡。开发环境也得到扩展,超越了编译器和源代码编辑器的领域。特别是,项目中的客户端或服务器部分,通常涉及不同程序员的工作,这些程序员对这些客户端或服务器负责。在这种情况下,将项目放入云端共享以及版本控制变得必不可少。
请注意,通过 MetaEditor 在 MQL5/Shared Projects 文件夹中开发项目时,来自标准目录 MQL5/Include 的头文件不包含在共享存储中。另一方面,在你的项目中创建一个专用的文件夹 Include,并将必要的标准 mqh 文件转移到其中,这将导致信息的重复和头文件版本的潜在差异。而在 MetaEditor 中进行这种操作,情况可能会改善。
公共项目的另一特点是需要管理用户并为他们授权。在上一个例子中,这一点只是被识别出来,而没有被实施。然而,mql5.com 网站提供了一个基于 Oauth 协议(该协议广为人知)的现成解决方案。任何拥有 mql5.com 账户的个人都可以熟悉 Oauth 的原理,并为他们的 Web 服务配置该协议:你只需在配置文件中找到 Applications 一节(类似于 https://www.mql5.com/zh/users/<login> /apps 的链接)。通过在 mql5.com 应用程序中注册 Web 服务,你将能够通过 mql5.com 网站为用户授权。