MQL5 中的信号服务客户端程序

因此我们决定,服务消息中的文本将采用 JSON 格式。

在最常见的版本中,JSON 是对象的文本说明,类似于 MQL5 中对结构的说明。对象带有花括号,在花括号内,它的特性用逗号分隔:每个特性都有一个带引号的标识符,后跟一个冒号和特性值。此处支持几种基本类型的特性:字符串、整数和实数、布尔值 true/false 和空值 null。此外,特性值可以是对象或数组。数组用方括号说明,方括号中的元素用逗号分隔。例如,

{
   "string": "this is a text",
   "number": 0.1,
   "integer": 789735095,
   "enabled": true,
   "subobject" :
   {
      "option": null
   },
   "array":
   [
      1, 2, 3, 5, 8
   ]
}

顶层的数组基本上都是有效的 JSON。例如,

[
   {
      "command": "buy",
      "volume": 0.1,
      "symbol": "EURUSD",
      "price": 1.0
   },
   {
      "command": "sell",
      "volume": 0.01,
      "symbol": "GBPUSD",
      "price": 1.5
   }
]

为了减少使用 JSON 的应用程序协议中的流量,通常将字段名缩写成几个字母(通常是一个字母)。

特性名称和字符串值带有双引号。如果要在字符串中指定引号,必须用反斜杠对其进行转义。

JSON 的使用使得该协议具有通用性和可扩展性。例如,对于正在设计的服务(交易信号,以及在更一般的情况下,账户状态复制),可以假设消息结构如下:

{
  "origin": "publisher_id",    // message sender ("Server" in technical message)
  "msg" :                      // message (text or JSON) as received from the sender
   {
     "trade" :                 // current trading commands (if there is a signal)
      {
        "operation": ...,      // buy/sell/close
         "symbol": "ticker",
         "volume": 0.1,
        ... // other signal parameters
      },
     "account":                // account status
      {
        "positions":           // positions
         {
           "n": 10,            // number of open positions
           [ { ... },{ ... } ] // array of properties of open positions
         },
        "pending_orders":      // pending orders
         {
            "n": ...
            [ { ... } ]
         }
         "drawdown": 2.56,
         "margin_level": 12345,
        ... // other status parameters
      },
     "hardware":               // remote control of the "health" of the PC
      {
         "memory": ...,
         "ping_to_broker": ...
      }
   }
}

其中某些特征可能支持也可能不支持客户端程序的特定实现(它们会忽略不“理解”的任何内容)。此外,在同级的特性名称没有冲突的情况下,每个信息提供者都可以向 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 MqlTradeRequestWebpublic MqlTradeRequest
{
   MqlTradeRequestWeb(const MqlTradeRequest &r)
   {
      ZeroMemory(this);
      action = r.action;
      magic = r.magic;
      order = r.order;
      symbol = r.symbol;
      volume = r.volume;
      price = r.price;
      stoplimit = r.stoplimit;
      sl = r.sl;
      tp = r.tp;
      type = r.type;
      type_filling = r.type_filling;
      type_time = r.type_time;
      expiration = r.expiration;
      comment = r.comment;
      position = r.position;
      position_by = r.position_by;
   }
   
   JsValue *asJsValue() const
   {
      JsValue *req = new JsValue();
      // main block: action, symbol, type
      req.put("a"VerboseJson ? EnumToString(action) : (string)action);
      if(StringLen(symbol) != 0req.put("s"symbol);
      req.put("t"VerboseJson ? EnumToString(type) : (string)type);
      
      // volumes
      if(volume != 0req.put("v"TU::StringOf(volume));
      req.put("f"VerboseJson ? EnumToString(type_filling) : (string)type_filling);
      
      // block with prices
      if(price != 0req.put("p"TU::StringOf(price));
      if(stoplimit != 0req.put("x"TU::StringOf(stoplimit));
      if(sl != 0req.put("sl"TU::StringOf(sl));
      if(tp != 0req.put("tp"TU::StringOf(tp));
   
      // block of pending orders
      if(TU::IsPendingType(type))
      {
         req.put("t"VerboseJson ? EnumToString(type_time) : (string)type_time);
         if(expiration != 0req.put("d"TimeToString(expiration));
      }
   
      // modification block
      if(order != 0req.put("o"order);
      if(position != 0req.put("q"position);
      if(position_by != 0req.put("b"position_by);
      
      // helper block
      if(magic != 0req.put("m"magic);
      if(StringLen(comment)) req.put("c"comment);
   
      return req;
   }
};

我们将所有特性转移到 JSON(这适用于账户监控服务),但是你可以保留有限的一组特性。

对于枚举的特性,我们提供了两种方法用 JSON 将它们表示为整数和枚举元素的字符串名称。方法的选择是使用 VerboseJson 输入参数(理想情况下,它不应该直接写在结构代码中,而是通过构造函数参数编写)。

input bool VerboseJson = false;

只传递数字可以简化编码,因为在接收端,为了执行“镜像”操作,将数字转换为所需的枚举类型就足够了。但是人们难以通过数字来感知信息,可能还需要分析具体情况(消息)。因此,支持字符串表示的方法是有意义的,因为它更“友好”,尽管这需要对接收算法进行额外操作。

输入参数还分别为提供者和订阅者指定服务器地址、应用程序角色和连接细节。

enum TRADE_ROLE
{
   TRADE_PUBLISHER,  // Trade Publisher
   TRADE_SUBSCRIBER  // Trade Subscriber
};
   
input string Server = "ws://localhost:9000/";
input TRADE_ROLE Role = TRADE_PUBLISHER;
input bool VerboseJson = false;
input group "Publisher";
input string PublisherID = "PUB_ID_001";
input string PublisherPrivateKey = "PUB_KEY_FFF";
input string SymbolFilter = ""// SymbolFilter (empty - current, '*' - any)
input ulong MagicFilter = 0;    // MagicFilter (0 - any)
input group "Subscriber";
input string SubscriberID = "SUB_ID_100";
input string SubscribeToPublisherID = "PUB_ID_001";
input string SubscriberAccessKey = "fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a";
input string SymbolSubstitute = "EURUSD=GBPUSD"// SymbolSubstitute (<from>=<to>,...)
input ulong SubscriberMagic = 0;

提供者组中的参数 SymbolFilterMagicFilter 允许你将受监控的交易活动限制为给定的交易品种和幻数。SymbolFilter 中的空值表示只控制图表的当前交易品种,要拦截任何交易,请输入 * 符号。为此,信号提供者将使用 FilterMatched 函数,该函数接受交易的交易品种和幻数。

bool FilterMatched(const string sconst ulong m)
{
   if(MagicFilter != 0 && MagicFilter != m)
   {
      return false;
   }
   
   if(StringLen(SymbolFilter) == 0)
   {
      if(s != _Symbol)
      {
         return false;
      }
   }
   else if(SymbolFilter != s && SymbolFilter != "*")
   {
      return false;
   }
   
   return true;
}

订阅者输入组中的 SymbolSubstitute 参数允许用另一个交易品种替换消息中收到的交易品种,该交易品种将用于复制交易。在跨经纪商环境中,该功能尤其有用,可确保同一金融工具的交易代码名称不同。但该参数也执行重复信号许可筛选功能:只有这里指定的交易品种能进行交易。例如,要允许 EURUSD 交易品种的信号交易(即使没有交易代码替换),你需要在参数中设置 "EURUSD=EURUSD" 字符串。信号消息的交易品种显示在 = 交易品种的左边,交易的交易品种显示在右边。

字符替换列表在初始化期间由 FillSubstitutes 函数处理,然后由 FindSubstitute 函数用来替换和解析交易。

string Substitutes[][2];
   
void FillSubstitutes()
{
   string list[];
   const int n = StringSplit(SymbolSubstitute, ',', list);
   ArrayResize(Substitutesn);
   for(int i = 0i < n; ++i)
   {
      string pair[];
      if(StringSplit(list[i], '=', pair) == 2)
      {
         Substitutes[i][0] = pair[0];
         Substitutes[i][1] = pair[1];
      }
      else
      {
         Print("Wrong substitute: "list[i]);
      }
   }
}
   
string FindSubstitute(const string s)
{
   for(int i = 0i < ArrayRange(Substitutes0); ++i)
   {
      if(Substitutes[i][0] == sreturn Substitutes[i][1];
   }
   return NULL;
}

为了与服务通信,我们定义了一个从 WebSocketClient 派生的类。首先,当消息到达 onMessage 处理程序时,需要根据信号开始交易。在分析了提供者端信号的形成和发送之后,我们稍后会回到这个问题。

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   MyWebSocket(const string address): WebSocketClient(address) { }
   
   void onMessage(IWebSocketMessage *msgoverride
   {
      ...
   }
};
   
MyWebSocket wss(Server);

OnInit 中的初始化可开启计时器(用于定期调用 wss.checkMessages(false))并编写带用户详情的自定义头文件,具体取决于所选的角色。然后我们用 wss.open(custom) 调用打开连接。

int OnInit()
{
   FillSubstitutes();
   EventSetTimer(1);
   wss.setTimeOut(1000);
   Print("Opening...");
   string custom;
   if(Role == TRADE_PUBLISHER)
   {
      custom = "Sec-Websocket-Protocol: X-MQL5-publisher-"
         + PublisherID + "-" + PublisherPrivateKey + "\r\n";
   }
   else
   {
      custom = "Sec-Websocket-Protocol: X-MQL5-subscriber-"
         + SubscriberID + "-" + SubscribeToPublisherID
         + "-" + SubscriberAccessKey + "\r\n";
   }
   return wss.open(custom) ? INIT_SUCCEEDED : INIT_FAILED;
}

复制机制,即拦截交易并将有关交易的信息发送到 Web 服务,是在 OnTradeTransaction 处理程序中启动的。正如我们所知,这不是唯一的方法,在 OnTrade 中分析账户状态的“快照”是有可能的。

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   if(transaction.type == TRADE_TRANSACTION_REQUEST)
   {
      Print(TU::StringOf(request));
      Print(TU::StringOf(result));
      if(result.retcode == TRADE_RETCODE_PLACED           // successful action
         || result.retcode == TRADE_RETCODE_DONE
         || result.retcode == TRADE_RETCODE_DONE_PARTIAL)
      {
         if(FilterMatched(request.symbolrequest.magic))
         {
            ... // see next block of code
         }
      }
   }
}

我们跟踪成功完成的交易请求的事件(即满足指定筛选条件)。随后,请求的结构、请求的结果和交易都被转换成 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 }}
            JsValue msg;
            MqlTradeRequestWeb req(request);
            msg.put("req"req.asJsValue());
            
            MqlTradeResultWeb res(result);
            msg.put("res"res.asJsValue());
            
            if(result.deal != 0)
            {
               DealMonitorWeb deal(result.deal);
               msg.put("deal"deal.asJsValue());
            }
            ulong tickets[];
            Positions.select(tickets);
            JsValue pos;
            pos.put("n"ArraySize(tickets));
            msg.put("pos", &pos);
            string buffer;
            msg.stringify(buffer);
            
            Print(buffer);
            
            wss.send(buffer);

填充后,容器作为字符串输出进入 buffer,打印到日志,并发送到服务器。

我们可以向这个容器添加其他信息:账户状态(资金回撤率、杠杆率)、挂单的数量和特性等等。因此,为了展示扩展信息内容的可能性,我们在上面增加了未平仓仓位的数量。为了根据筛选器选择仓位,我们使用 PositionFilter 类对象 (PositionFilter.mqh):

PositionFilter Positions;
   
int OnInit()
{
   ...
   if(MagicFilterPositions.let(POSITION_MAGICMagicFilter);
   if(SymbolFilter == ""Positions.let(POSITION_SYMBOL_Symbol);
   else if(SymbolFilter != "*"Positions.let(POSITION_SYMBOLSymbolFilter);
   ...
}

基本上,为了增加可靠性,对于复制器来说,分析仓位的状态是有意义的,而不仅仅是拦截交易。

这就是对信号提供者角色中涉及的 EA 交易部分的分析总结。

正如我们所声明的,作为订阅者,EA 交易接收 MyWebSocket::onMessage 方法中的消息。此处用 JsParser::jsonify 解析传入的消息,从 obj["msg"] 特性中检索由传输方形成的容器。

class MyWebSocketpublic WebSocketClient<Hybi>
{
public:
   void onMessage(IWebSocketMessage *msgoverride
   {
      Alert(msg.getString());
      JsValue *obj = JsParser::jsonify(msg.getString());
      if(obj && obj["msg"])
      {
         obj["msg"].print();
         if(!RemoteTrade(obj["msg"])) { /* error processing */ }
         delete obj;
      }
      delete msg;
   }
};

RemoteTrade 函数实现信号分析和交易操作。这里给出了缩写,省略了潜在错误处理。该函数支持两种表示枚举的方式:表示为整数值或字符串元素名称。通过应用运算符 [],对传入的 JSON 对象进行必要的特性(命令和信号属性)检查,包括连续几次(访问嵌套的 JSON 对象)。

bool RemoteTrade(JsValue *obj)
{
   bool success = false;
   
   if(obj["req"]["a"] == TRADE_ACTION_DEAL
      || obj["req"]["a"] == "TRADE_ACTION_DEAL")
   {
      const string symbol = FindSubstitute(obj["req"]["s"].s);
      if(symbol == NULL)
      {
         Print("Suitable symbol not found for "obj["req"]["s"].s);
         return false// not found or forbidden
      }
      
      JsValue *pType = obj["req"]["t"];
      if(pType == ORDER_TYPE_BUY || pType == ORDER_TYPE_SELL
         || pType == "ORDER_TYPE_BUY" || pType == "ORDER_TYPE_SELL")
      {
         ENUM_ORDER_TYPE type;
         if(pType.detect() >= JS_STRING)
         {
            if(pType == "ORDER_TYPE_BUY"type = ORDER_TYPE_BUY;
            else type = ORDER_TYPE_SELL;
         }
         else
         {
            type = obj["req"]["t"].get<ENUM_ORDER_TYPE>();
         }
         
         MqlTradeRequestSync request;
         request.deviation = 10;
         request.magic = SubscriberMagic;
         request.type = type;
         
         const double lot = obj["req"]["v"].get<double>();
         JsValue *pDir = obj["deal"]["entry"];
         if(pDir == DEAL_ENTRY_IN || pDir == "DEAL_ENTRY_IN")
         {
            success = request._market(symbollot) && request.completed();
            Alert(StringFormat("Trade by subscription: market entry %s %s %s - %s",
               EnumToString(type), TU::StringOf(lot), symbol,
               success ? "Successful" : "Failed"));
         }
         else if(pDir == DEAL_ENTRY_OUT || pDir == "DEAL_ENTRY_OUT")
         {
            // closing action assumes the presence of a suitable position, look for it
            PositionFilter filter;
            int props[] = {POSITION_TICKETPOSITION_TYPEPOSITION_VOLUME};
            Tuple3<long,long,doublevalues[];
            filter.let(POSITION_SYMBOLsymbol).let(POSITION_MAGIC,
               SubscriberMagic).select(propsvalues);
            for(int i = 0i < ArraySize(values); ++i)
            {
              // need a position that is opposite in direction to the deal
               if(!TU::IsSameType((ENUM_ORDER_TYPE)values[i]._2type))
               {
                  // you need enough volume (exactly equal here!)
                  if(TU::Equal(values[i]._3lot))
                  {
                     success = request.close(values[i]._1lot) && request.completed();
                     Alert(StringFormat("Trade by subscription: market exit %s %s %s - %s",
                        EnumToString(type), TU::StringOf(lot), symbol,
                        success ? "Successful" : "Failed"));
                  }
               }
            }
            
            if(!success)
            {
               Print("No suitable position to close");
            }
         }
      }
   }
   return success;
}

该实现不分析交易价格、对交易的可能限制、止损水平和其他时刻。我们只是以当前的本地价格重复交易。此外,平仓时,会检查交易量是否完全相等,这适用于对冲账户,但不适用于净额结算,如果交易量少于仓位(在冲销的情况下可能更多,但此处不支持 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 网站为用户授权。