English Русский Español Deutsch 日本語 Português
preview
用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试

用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试

MetaTrader 5EA交易 | 8 八月 2024, 09:42
1 331 0
Yuqiang Pan
Yuqiang Pan

概述

在前几篇文章中,我讨论了如何根据我们自己的需要标注数据,并用它们来训练时间序列预测模型,但你可能还不太清楚如何更好地使用这些模型。现在是时候讨论如何在 MetaTrader 5 的历史回测中验证我们创建的模型,并将其纳入我们的 EA。但是你要知道,在 EA 中,你需要有一个策略作为关键逻辑,而一个真正可用的策略需要有特定的理论基础,并经过大量的验证和调整,以确保其稳健性。

本文中的策略非常简单,只是一个简单的演示实例,请勿随意搬到实际交易中!当然,因为有大量各种库的支持,只用 python 也可以完成这项工作,但 MetaTrader 5 提供了如此方便、全面的回测工具,可以更准确地模拟我们的交易环境,所以我们还是必定要选择 MetaTrader 5 客户端作为我们的回测平台。但由于我们的模型创建环境是 python,而 MetaTrader 5 的历史回测必须使用 MQL5 来实现,这给回测任务的执行带来了一些困难,但我们并非没有解决方案。在本文中,我们将讨论在 MetaTrader 5 环境中使用三种不同的方法对我们的模型进行回测,以帮助我们改进和提高模型的质量。我将在接下来的几篇文章中介绍不同的方法,本文将讨论 WebSocket 方法。

目录

  1. 概述
  2. 实现原则
  3. Python 服务器函数实现
  4. MQL5 客户端功能实现
  5. 如何进行回测
  6. 结论


实现原则

首先,我们在 python 脚本中添加一个网络服务器实例,并在其中添加我们的模型推理。然后,我们使用 MQL5 创建一个网络客户端,请求服务器中的推理服务。

f0

您可能认为这不是一个好方法,只要将模型转换为 MQL5 原生支持的 ONNX,然后在 EA 中添加调用即可,对吗?答案是肯定的,但请不要忘记,有些特定模型非常庞大,推理过程需要通过各种方法进行优化,这可能需要您将推理逻辑和跨语言实现一起迁移,而这将是一个庞大的工程。而且这种方法可以跨系统和跨语言实现不同的功能组合。例如,如果您的 MetaTrader 5 客户端是在 windows 上,而您的服务器端甚至可以部署到远程服务器上。服务器可以是任何支持模型推理的操作系统,因此无需安装额外的虚拟机。当然,你也可以将服务器部署到 wsl 或 docker 中。通过这种方法,我们就不会局限于单一的操作系统或单一的编程语言。这种方法其实很常见,我们可以自由扩大使用范围。

我们假设 EA 的逻辑如下:

  • 首先,每次触发 OnTick() 事件时,都会通过客户端向服务器发送最新的 300 个柱的图表数据。
  • 收到信息后,服务器会通过模型推理向 EA 客户端发送接下来 6 个柱形的预测趋势。在这里,我们使用上一篇文章中提到的 Nbeats 模型,因为它可以将预测分解为趋势。
  • 如果是下降趋势,则卖出,如果是上升趋势,则买入。

Python 服务器函数实现

python 提供的套接字主要包括以下函数:

  • socket.bind():将地址(host, port)绑定到套接字。在 AF_INET 中,地址用元组(host, port)表示。
  • socket.listen():开始 TCP 侦听。backlog 参数指定了操作系统在拒绝连接之前可以暂停的最大连接数。该值至少为 1,大多数应用程序将其设置为 5。
  • socket.accept():被动接受 TCP 客户端连接,(阻塞)等待连接到达。
  • socket.connect():主动初始化 TCP 服务器连接。一般来说,地址的格式是元组(hostname、port)。如果连接失败,则返回 socket.error 错误。
  • socket.connect_ex():connect() 函数的扩展版本,当发生错误时返回错误代码,而不是抛出异常。 socket.recv():接收 TCP 数据,数据以字符串形式返回,bfsize 参数指定要接收的最大数据量,flag 参数提供有关报文的附加信息,通常可以忽略。
  • socket.send():发送 TCP 数据,将数据以字符串形式发送到连接的套接字。返回值是要发送的字节数,可能小于字符串的字节大小。
  • socket.sendall():完全发送 TCP 数据。向连接的套接字发送字符串中的数据,但在返回前尽量发送完所有数据。成功时返回 "None",失败时引发异常。
  • socket.recvfrom():接收 UDP 数据,与 recv() 类似,但返回值为(data,address)。其中,data 是包含接收数据的字符串,address 是发送数据的套接字的地址。
  • socket.sendto():发送 UDP 数据,向套接字发送数据,地址为 (ipaddr,port) 形式的元组,指定远程地址。返回值是发送的字节数。
  • socket.close():关闭套接字
  • socket.getpeername():返回已连接套接字的远程地址。返回值通常是一个元组(ipaddr,port)。
  • socket.getsockname():返回套接字自身的地址。通常是一个元组(ipaddr,port)
  • socket.setsockopt(level,optname,value):设置给定套接字选项的值。
  • socket.getsockopt(level,optname[.buflen]):返回套接字选项的值。
  • socket.settimeout(timeout):设置套接字操作的超时时间,timeout 是一个浮点数,单位为秒。值为 "None" 表示没有超时时间。一般来说,超时时间应在套接字刚创建时设置,因为它们可能会被用于连接操作(如 connect())
  • socket.gettimeout():返回当前超时时间值(以秒为单位),如果未设置超时时间,则返回 "None"。
  • socket.fileno():返回套接字的文件描述符。
  • socket.setblocking(flag):如果 flag 为 0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。在非阻塞模式下,如果调用 recv() 时没有找到数据,或者 send() 调用无法立即发送数据,就会导致 socket.error 异常。
  • socket.makefile():创建与套接字相关的文件。


    1.导入所需软件包

    该类的实现不需要安装额外的软件包,通常默认情况下(在 conda 环境下)会包含套接字库。如果认为某些警告信息过于杂乱,可以添加警告模块,并添加 warnings.filterwarnings("ignore") 语句。同时,我们还需要定义所需的全局变量:

    • max_encoder_length=96
    • max_prediction_length=20
    • info_file="results.json"

    这些全局变量是根据我们在前一篇文章中训练的模型定义的。

    代码:

    import socket
    import json
    from time import sleep
    import pandas as pd
    import numpy as np
    import warnings
    from pytorch_forecasting import NBeats
    
    warnings.filterwarnings("ignore")
    max_encoder_length=96
    max_prediction_length=20
    info_file="results.json"


    2.创建 server 类

    创建一个 server 类,初始化套接字的一些基本设置,包括以下函数:

    socket.socket():我们将两个参数设置为 socket.AF_INET 和 socket.SOCK_STREAM。

    socket.socket() 的 bind() 方法:该函数将主机参数设置为 "127.0.0.1",端口参数设置为 "8989",其中主机不建议更改,如果 8989 被占用,端口可以设置为其他值。

    稍后将引入模型,因此我们暂时将其初始化为 "None"。

    我们需要监听服务器端口:self.sk.listen(1),被动接受 TCP 客户端连接,并等待连接的到来:self.sk_, self.ad_ = self.sock.accept()。我们在类初始化时完成这些任务,以避免在循环接收信息时重复初始化。


    class server_:
        def __init__(self, host = '127.0.0.1', port = 8989):
            self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.host = host
            self.port = port
            self.sk.bind((self.host, self.port))
            self.re = ''
            self.model=None
            self.stop=None
            self.sk.listen(1)
            self.sk_, self.ad_ = self.sk.accept()
            print('server running:',self.sk_, self.ad_)  

    请注意: 如果在 docker 或类 docker 容器中部署服务器,可能需要将主机设置为 "0.0.0.0",否则客户端可能无法找到服务器。


    3.处理接收信息的逻辑

    我们定义了一个类方法 msg() 来处理收到的信息,使用 while 循环来处理收到的信息。这里需要注意的是,接收到的数据需要用 decode("utf-8") 解码,然后将处理过的信息发送到推理逻辑处理函数 self.sk_.send(bytes(eva(self.re), "utf-8")) 中,其中推理逻辑函数定义为 eva(),参数是我们接收到的信息,我们稍后将实现它。接下来,我们还有一件事要做,那就是确保在 EA 回测停止时,我们的服务器也会停止,否则它会占用后台资源。我们可以在 EA 结束后向服务器发送一个 "stop" 字符串,如果收到这个字符串,就可以让服务器停止循环并终止进程。我们已经在服务器类的初始化中添加了该类属性,只需在收到该信号时将其设置为 true 即可。

    def msg(self):
            self.re = ''
            while True:
                data = self.sk_.recv(2374)
                if not data:
                    break
                data=data.decode("utf-8")
                # print(len(data))
                if data=="stop":
                    self.stop=True
                    break
                self.re+=data
                bt=eva(self.re, self.model)
                bt=bytes(bt, "utf-8") 
                self.sk_.send(bt)
            return self.re

    注意:在示例中,我们将参数 self.sk_.recv(2374) 设置为 2374,这正好是 300 个浮点数的长度。如果您发现收到的数据不完整,可以调整该值。


    4. 回收资源

    服务器停止后,我们需要回收资源。

    def __del__(self):
            print("server closed!")
            self.sk_.close()
            self.ad_.close()
            self.sock.close()


    5.定义推理逻辑

    这个例子的推理逻辑非常简单。我们只需加载模型,使用客户端提供的柱形图预测结果,然后将结果分解为趋势,并将结果发回给客户端。这里我们需要注意的是,我们可以在服务器类的初始化过程中初始化模型,而不是在这里,这样模型就会被预加载,随时可以进行推理。

    首先,我们定义了一个加载模型的函数,然后在服务器类的初始化中调用该函数来获取实例化的模型。在上一篇文章中,我们已经介绍了模型保存和加载的处理过程。训练结束后,模型会将信息保存在文件夹根目录下的 "results.json" json 文件中。我们可以读取并加载模型。当然,我们的 server.py 文件也需要放在文件夹的根目录下。


    def load_model():
        with open(info_file) as f:
                m_p=json.load(fp=f)['last_best_model']
        model = NBeats.load_from_checkpoint(m_p)
        return model
    然后在类 server_() 的 init() 函数中添加:self.model=load_model() 进行初始化,然后将初始化后的模型传递给推理函数。

        def __init__(self, host = '127.0.0.1', port = 8989):
            self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.host = host
            self.port = port
            self.sk.bind((self.host, self.port))
            self.re = ''
            self.model=load_model()
            self.stop=None
            self.sk.listen(1)
            self.sk_, self.ad_ = self.sk.accept()
            print('server running:',self.sk_, self.ad_) 

    接下来,让我们继续完成推理函数。

    这里需要特别注意的是,模型需要输入的数据格式必须是 DataFrame 格式,因此我们需要先将接收到的数据转换为 numpy 数组:msg=np.fromstring(msg, dtype=float, sep= ','), 然后再将其转换为 DataFrame:dt=pd.DataFrame(msg)。推理完成后,将返回结果。我们设定,如果最后一个 trend(趋势)值大于趋势值的平均值,则为上升趋势,否则为下降趋势。如果是上升趋势,则返回 "buy"(买入),如果是下降趋势,则返回 "sell"(卖出)。本文不再讨论具体的推理过程,请参考本系列前几篇文章的推理过程。这里还有一点需要强调。由于我们将模型的预测因子设置为 DataFrame 的 "close "列,因此需要在转换为 DataFrame 的数据中添加 "close "列:dt['close']=dt。

    def eva(msg,model):
            offset=1
            msg=np.fromstring(msg, dtype=float, sep= ',') 
            # print(msg)
            dt=pd.DataFrame(msg)
            dt=dt.iloc[-max_encoder_length-offset:-offset,:]
            last_=dt.iloc[-1] 
            for i in range(1,max_prediction_length+1):
                dt.loc[dt.index[-1]+1]=last_
            dt['close']=dt
            dt['series']=0
            dt['time_idx']=dt.index-dt.index[0]
            print(dt)
            predictions = model.predict(dt, mode='raw',trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True)
            trend =predictions.output["trend"][0].detach().cpu()
            if (trend[-1]-trend.mean()) >= 0:
                return "buy" 
            else:
                return "sell"

    接下来,我们需要添加主循环。

    首先,我们初始化 service 类,然后在 while 循环中添加信息处理函数。当收到停止信号时,我们终止循环并退出程序。请注意,我们不希望循环运行得太快,因此我们添加了 sleep(0.5) 来限制循环速度,避免 CPU 占用率过高。

    while True:
         rem=sv.msg()
         if sv.stop:
              break
        sleep(0.5)

    到目前为止,我们已经完成了一个简单的服务器,接下来我们需要在 EA 中实现客户端。


    MQL5 客户端功能实现

    1.MQL5 中的套接字函数

    套接字模块目前包括以下函数:

    • SocketCreate:使用指定的标识符创建套接字并返回句柄
    • SocketClose:关闭套接字
    • SocketConnect:通过超时控制连接服务器
    • SocketIsConnected:检查套接字当前是否已连接
    • SocketIsReadable:获取可从套接字读取的字节数
    • SocketIsWritable:检查当前时间是否可以向套接字写入数据
    • SocketTimeouts:设置系统套接字对象的数据接收和传输超时
    • SocketRead:从套接字读取数据
    • SocketSend:向套接字写入数据
    • SocketTlsHandshake:使用 TLS 握手协议启动与指定主机的安全 TLS(SSL)连接
    • SocketTlsCertificate:获取用于安全网络连接的证书数据
    • SocketTlsRead:从安全 TLS 连接读取数据
    • SocketTlsReadAvailable:从安全 TLS 连接中读取所有可用数据
    • SocketTlsSend:通过安全的 TLS 连接发送数据

    通过引用这些方法,我们可以轻松地在客户端添加其他函数。


    2.实施 EA 功能

    首先,我们来讨论一下 EA 的功能逻辑:

    在 "int OnInit()" 中初始化套接字。

    然后,在 "void OnTick()" 中,实现从客户端接收数据、向客户端发送当前柱形图数据以及我们的 EA 回测逻辑。

    在 "void OnDeinit(const int reason)" 中,你需要向服务器发送一条 "stop" 信息并关闭套接字。


    3.初始化 EA

    首先,我们需要定义一个全局变量 "int sk",用于在创建套接字后接收句柄。

    在 OnInit() 函数中,我们使用 SocketCreate() 创建客户端:int sk=SocketCreate()。

    然后定义服务器地址:string host="127.0.0.1";
    服务器端口:int port= 8989;
    要发送的数据长度,我们之前讨论过一次发送 300 个数据: int data_len=300;

    在 OnInit() 函数中,我们需要判断初始化情况。如果创建失败,初始化也会失败。

    然后,我们创建一个与服务器的连接 SocketConnect(sk,host,port,1000),其中端口必须与服务器端一致。如果连接失败,初始化也会失败。

    int sk=-1;
    
    string host="127.0.0.1";
    int port= 8989;
    
    int OnInit()
      {
    //---
        sk=SocketCreate();
        Print(sk);
        Print(GetLastError());
        if (sk==INVALID_HANDLE) {
            Print("Failed to create socket");
            return INIT_FAILED;
        }
    
        if (!SocketConnect(sk,host, port,1000)) 
        {
            Print("Failed to connect to server");
            return INIT_FAILED;
        }
    //---
       return(INIT_SUCCEEDED);
      }


    不要忘记在 EA 结束时回收资源

    void OnDeinit(const int reason) {
        socket.Disconnect();
    }


    4.交易逻辑

    在这里,我们需要在 void OnTick() 中定义主要数据处理逻辑和交易逻辑。

    创建变量 "MqlTradeRequest request "和 "MqlTradeResult result",用于执行订单任务;

    创建字符数组变量 "char recv_data[]",用于接收服务器信息;

    创建一个 double 型数组变量 "double priceData[300]",用于复制图表数据;

    创建变量 "string dataToSend" 和 "char ds[]",以便将 double 数组转换为 socket 可以使用的字符数组;

    首先,我们需要复制要从图表中发送的数据: int nc=CopyClose(Symbol(),0,0,data_len,priceData);

    然后将数据转换为字符串格式: for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+",", 我们使用", "分隔每个数据;

    然后使用 "int dsl=StringToCharArray(dataToSend,ds)" 将字符串数据转换为可被 socket 使用的字符数组。

    数据转换完成后,我们需要使用 SocketIsWritable(sk) 来判断套接字是否可以发送数据,如果可以,则使用 SocketSend(sk,ds,dsl) 发送数据。

    我们还需要从服务器读取信息,使用 "uint len=SocketIsReadable(sk)" 检查当前端口是否有可用数据,如果信息不为空,则执行交易逻辑:int rsp_len=SocketRead(sk,recv_data,len,500),"len" 为缓冲区大小,"500" 为超时设置(单位是毫秒)。

    如果收到 "buy",则打开买入订单,设置请求如下:

    • 重置交易请求结构请求:ZeroMemory(request)
    • 设置为立即执行交易指令: request.action = TRADE_ACTION_DEAL
    • 设置交易货币对: request.symbol = Symbol()
    • 订单交易量: request.volume = 0.1
    • 订单类型: request.type = ORDER_TYPE_BUY
    • SymbolInfoDouble 函数需要 2 个输入参数,第一个是货币对字符串,第二个是 ENUM_SYMBOL_INFO_DOUBLE 枚举中的类型: request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK)
    • 允许的交易滑点: request.deviation = 5
    • 然后发送交易指令:OrderSend(request, result)

    如果收到 "sell",则打开卖出订单,设置请求如下(设置参照买入订单,此处不作详细说明):

    • ZeroMemory(request)
    • request.action = TRADE_ACTION_DEAL
    • request.symbol = Symbol()
    • request.volume = 0.1
    • request.type = ORDER_TYPE_SELL
    • request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID)
    • request.deviation = 5
    • 然后发送交易指令:OrderSend(request, result)

    在此,为了避免测试代码出现问题,我们注释掉真实订单发送函数,并在回测中打开它。


    完整代码:
    void OnTick() {
        MqlTradeRequest request;
        MqlTradeResult result;
        char recv_data[];
        double priceData[300];
        string dataToSend;
        char ds[];
        int nc=CopyClose(Symbol(),0,0,300,priceData);
        for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; 
        int dsl=StringToCharArray(dataToSend,ds);
        if (SocketIsWritable(sk))
            {
            Print("Send data:",dsl);
            int ssl=SocketSend(sk,ds,dsl);     
            }
        uint len=SocketIsReadable(sk); 
        if (len)
        {
          int rsp_len=SocketRead(sk,recv_data,len,500);
          if(rsp_len>0)
          {
            string result; 
            result+=CharArrayToString(recv_data,0,rsp_len);
            Print("The predicted value is:",result);
            if (StringFind(result,"buy"))
            {
               ZeroMemory(request);
                request.action = TRADE_ACTION_DEAL;      
                request.symbol = Symbol();  
                request.volume = 0.1;  
                request.type = ORDER_TYPE_BUY;  
                request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);  
                request.deviation = 5; 
                //OrderSend(request, result);
            }
            else{
                ZeroMemory(request);
                request.action = TRADE_ACTION_DEAL;      
                request.symbol = Symbol();  
                request.volume = 0.1;  
                request.type = ORDER_TYPE_SELL;  
                request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);  
                request.deviation = 5; 
                //OrderSend(request, result);
                 }
            }
         }
    }

    请注意: SocketSend() 函数中的缓冲区最大长度参数必须与服务器设置一致。执行 StringToCharArray() 函数时,将自动计算并返回该值。

    现在,我们先运行 server.py,然后在 MetaTrader 5 客户端的图表中添加 EA,结果如下:



    但我们还不能使用历史回测,因为 SocketCreate() 和一系列套接字操作在测试中是不允许的。接下来,我们将继续探讨如何解决这一问题。



    如何进行回测


    前面我们提到了 MQL5 中套接字的局限性,接下来我们需要在 MQL5 文件和 python 文件中添加 websocket 支持。
    1.为客户端添加 websocket 支持

    在回测中,我们可以使用 windows api 中的 winhttp.mqh 来实现我们想要的功能。有关此 api 的详细介绍,请参阅

    微软官方文档:https://learn.microsoft.com/zh-cn/windows/win32/winhttp/winhttp-functions,这里只列出主要函数:

    • WinHttpOpen():初始化程序库,准备供应用程序使用
    • WinHttpConnect():设置应用程序要与之通信的服务器域名
    • WinHttpOpenRequest():创建 HTTP 请求句柄
    • WinHttpSetOption:设置 HTTP 连接的各种配置选项
    • WinHttpSendRequest:向服务器发送请求
    • WinHttpReceiveResponse:发送请求后接收服务器的响应
    • WinHttpWebSocketCompleteUpgrade:确认从服务器收到的响应符合 WebSocket 协议
    • WinHttpCloseHandle:用于丢弃任何先前使用过的资源描述符
    • WinHttpWebSocketSend:用于通过 WebSocket 连接发送数据
    • WinHttpWebSocketReceive:使用 WebSocket 连接接收数据
    • WinHttpWebSocketClose:关闭 WebSocket 连接
    • WinHttpWebSocketQueryCloseStatus:检查从服务器发送的关闭状态消息

    下载 "winhttp.mqh" 文件,并将其复制到客户端数据文件夹 "Include/WinAPI/"路径下。现在让我们完成代码部分。

    在全局变量 "HINTERNET ses_h、cnt_h、re_h、ws_h" 中添加我们需要使用的句柄变量,并在 OnInit() 中初始化它们:

    • 首先将变量设置为 NULL:ses_h=cnt_h=re_h=ws_h=NULL,以它们被随机设置;
    • 然后启动 http 会话:ses_h=WinHttpOpen("MT5",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,NULL,NULL,0),如果失败,则初始化失败;
    • 连接服务器:cnt_h=WinHttpConnect(ses_h,host,port,0),如果连接失败,则初始化失败;
    • 执行请求初始化:re_h=WinHttpOpenRequest(cnt_h, "GET",NULL,NULL,NULL,NULL,0),如果失败,则初始化失败;
    • 设置 websocket:WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0),如果失败则初始化失败;
    • 执行 websocket 握手请求:WinHttpSendRequest( re_h,NULL, 0,nullpointer, 0, 0, 0),如果失败则初始化失败;
    • 接收服务器的握手响应:WinHttpReceiveResponse(re_h,nullpointer),如果失败则初始化失败;
    • 升级到 websocket,初始化后获取句柄:WinHttpWebSocketCompleteUpgrade(re_h,nv),如果失败则初始化失败;
    • 升级完成后,我们不再需要原来的请求句柄,因此要关闭它:WinHttpCloseHandle(re_h);

    这样我们就完成了客户端和服务器之间的整个连接过程,这些过程必须严格按照顺序执行,我们需要注释掉初始化失败语句的原始设置,因为它们在回测过程中始终有效,会导致我们无法成功初始化。

    
    int sk=-1;
    
    string host="127.0.0.1";
    int port= 8989;
    int data_len=300;
    
    HINTERNET ses_h,cnt_h,re_h,ws_h;
    
    int OnInit()
      {
    //---
       ses_h=cnt_h=re_h=ws_h=NULL;
    
       ses_h=WinHttpOpen("MT5",
                         WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                         NULL,
                         NULL,
                         0);
       Print(ses_h);
       if (ses_h==NULL){
          Print("Http open failed!");
          return INIT_FAILED;
          }
       cnt_h=WinHttpConnect(ses_h,
                            host,
                            port,
                            0);
       Print(cnt_h);
       if (cnt_h==-1){
          Print("Http connect failed!");
          return INIT_FAILED;
          }
       re_h=WinHttpOpenRequest(cnt_h,
                               "GET",
                               NULL,
                               NULL,
                               NULL,
                               NULL,
                               0);
       if(re_h==NULL){
          Print("Request open failed!");
          return INIT_FAILED;
       }
       uchar nullpointer[]= {};
       if(!WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0))
         {
              Print("Set web socket failed!");
              return INIT_FAILED;
           }
       bool br;   
       br = WinHttpSendRequest( re_h,
                                 NULL, 
                                 0,
                                 nullpointer, 
                                 0, 
                                 0, 
                                 0);
       if (!br)
          {
             Print("send request failed!");
             return INIT_FAILED;
             }
       br=WinHttpReceiveResponse(re_h,nullpointer);         
       if (!br)
         {
           Print("receive response failed!",string(kernel32::GetLastError()));
           return INIT_FAILED;
           }
       ulong nv=0; 
       ws_h=WinHttpWebSocketCompleteUpgrade(re_h,nv);  
       if (!ws_h)
       {
          Print("Web socket upgrade failed!",string(kernel32::GetLastError()));
          return INIT_FAILED;
             }
       
      
       WinHttpCloseHandle(re_h);
       re_h=NULL;
     
    
        sk=SocketCreate();
        Print(sk);
        Print(GetLastError());
        if (sk==INVALID_HANDLE) {
            Print("Failed to create socket");
            //return INIT_FAILED;
        }
    
        if (!SocketConnect(sk,host, port,1000)) 
        {
            Print("Failed to connect to server");
            //return INIT_FAILED;
        }
    //---
       return(INIT_SUCCEEDED);
      }

    然后,我们在 OnTick() 函数中添加相关的逻辑代码。

    首先,我们需要确定工作在什么环境下,因为我们已经定义了套接字句柄全局变量,我们可以通过判断套接字是否初始化成功来区分工作在正常状态还是测试状态,所以 "sk!=-1" 如果为 true 就表示套接字初始化成功,这部分代码我们不需要修改。如果 "sk!=-1" 不为 true,那么我们就需要完成 websocket 工作逻辑:

    • 首先向服务器发送数据:WinHttpWebSocketSend(ws_h,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,ds,dsl),这里需要特别注意的是,如果此过程成功,函数的返回值为 0,否则将返回相关的错误代码
    • 如果成功,则清空接收到的数据变量:ZeroMemory(recv_data)
    • 接收数据:get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st),如果数据接收成功,返回值为 0,否则返回错误代码
    • 如果收到数据,则解码数据 :pre+=CharArrayToString(recv_data,0)

    如果服务器发送 "buy" 指令,则打开买入订单,否则打开卖出订单。不同的是,我们还增加了额外的判断逻辑,如果已经有订单,我们将首先判断是否有未结订单 "numt=PositionsTotal()>0",如果有,则获取订单类型:tpt=OrderGetInteger(ORDER_TYPE),然后查看订单类型是 ORDER_TYPE_SELL 还是 ORDER_TYPE_BUY,如果订单类型与服务器发送的趋势相同,则无需任何操作。如果订单类型与趋势相反,则关闭当前订单,并打开与趋势相匹配的订单。

    我们以服务器信息是 "buy" 为例介绍这一过程。

    如果 tpt==ORDER_TYPE_BUY 直接返回,如果 tpt==ORDER_TYPE_SELL 则表示有卖出订单,然后设置 :request.order=tik, 设置:request.action=TRADE_ACTION_REMOVE,当执行到 OrderSend(request, result) 时,将关闭卖出订单。

    如果没有订单,则设置:

    • request.action = TRADE_ACTION_DEAL;
    • request.action = TRADE_ACTION_DEAL;
    • request.symbol = Symbol();
    • request.volume = 0.1;
    • request.type = ORDER_TYPE_BUY;
    • request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
    • request.deviation = 5;
    • request.type_filling=ORDER_FILLING_IOC;

    执行 OrderSend(request,result)时,将打开一个买入订单。同样,如果服务器信息是 "sell",也可以用同样的方法进行设置,本文不再详细讨论。

    void OnTick()
      {
    //---
        MqlTradeRequest request;
        MqlTradeResult result;
        char recv_data[5];
        double priceData[300];
        string dataToSend;
        char ds[];
        int nc=CopyClose(Symbol(),0,0,data_len,priceData);
        for(int i=0;i<ArraySize(priceData);i++) dataToSend+=(string)priceData[i]+","; 
        int dsl=StringToCharArray(dataToSend,ds);
        
        
        if (sk!=-1)
        {
           if (SocketIsWritable(sk))
               {
               Print("Send data:",dsl);
               int ssl=SocketSend(sk,ds,dsl);     
                }
           uint len=SocketIsReadable(sk); 
           if (len)
           {
             int rsp_len=SocketRead(sk,recv_data,len,500);
             if(rsp_len>0)
             {
               string result=NULL; 
               result+=CharArrayToString(recv_data,0,rsp_len);
               Print("The predicted value is:",result);
               if (StringFind(result,"buy"))
               {
                  ZeroMemory(request);
                   request.action = TRADE_ACTION_DEAL;      
                   request.symbol = "EURUSD";  
                   request.volume = 0.1;  
                   request.type = ORDER_TYPE_BUY;  
                   request.price = SymbolInfoDouble("EURUSD", SYMBOL_ASK);  
                   request.deviation = 5; 
                   //OrderSend(request, result);
               }
               else{
                   ZeroMemory(request);
                   request.action = TRADE_ACTION_DEAL;      
                   request.symbol = "EURUSD";  
                   request.volume = 0.1;  
                   request.type = ORDER_TYPE_SELL;  
                   request.price = SymbolInfoDouble("EURUSD", SYMBOL_BID);  
                   request.deviation = 5; 
                   //OrderSend(request, result);
                   }
                }
              }
         }
        else
        {
        ulong send=0;                         
           if (ws_h)
           { 
             send=WinHttpWebSocketSend(ws_h,
                                 WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE,
                                 ds,
                                 dsl);
              //Print("Send data failed!",string(kernel32::GetLastError()));    
             if(!send)
                {
                   ZeroMemory(recv_data);
                   ulong rb=0;
                   WINHTTP_WEB_SOCKET_BUFFER_TYPE st=-1;
                   ulong get=WinHttpWebSocketReceive(ws_h,recv_data,ArraySize(recv_data),rb,st);
                    if (!get)
                    {
                        string pre=NULL; 
                        pre+=CharArrayToString(recv_data,0);
                        Print("The predicted value is:",pre);
                        ulong numt=0;
                        ulong tik=0;
                        bool sod=false;
                        ulong tpt=-1;
                        numt=PositionsTotal();
                        if (numt>0)
                         {  tik=OrderGetTicket(numt-1);
                            sod=OrderSelect(tik);
                            tpt=OrderGetInteger(ORDER_TYPE);//ORDER_TYPE_BUY or ORDER_TYPE_SELL
                             }
                        if (pre=="buy")
                        {   
                           if (tpt==ORDER_TYPE_BUY)
                               return;
                           else if(tpt==ORDER_TYPE_SELL)
                               {
                               request.order=tik;
                               request.action=TRADE_ACTION_REMOVE;
                               Print("Close sell order.");
                                    }
                           else{
                            ZeroMemory(request);
                            request.action = TRADE_ACTION_DEAL;      
                            request.symbol = Symbol();  
                            request.volume = 1;  
                            request.type = ORDER_TYPE_BUY;  
                            request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);  
                            request.deviation = 5; 
                            request.type_filling=ORDER_FILLING_IOC;
                            Print("Open buy order.");
                            
                                     }
                            OrderSend(request, result);
                               }
                        else{
                           if (tpt==ORDER_TYPE_SELL)
                               return;
                           else if(tpt==ORDER_TYPE_BUY)
                               {
                               request.order=tik;
                               request.action=TRADE_ACTION_REMOVE;
                               Print("Close buy order.");
                                    }
                           else{
                               ZeroMemory(request);
                               request.action = TRADE_ACTION_DEAL;      
                               request.symbol = Symbol();  
                               request.volume = 1;  
                               request.type = ORDER_TYPE_SELL;  
                               request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);  
                               request.deviation = 5; 
                               request.type_filling=ORDER_FILLING_IOC;
                               Print("OPen sell order.");
                                    }
                            
                            OrderSend(request, result);
                              }
                        }
                }
            }
            
            
        }
        
      }

    至此,我们完成了 MQL5 websocket 客户端的配置。


    2.服务器端配置

    我们需要在 server.py 中添加 websocket 支持。

    首先,我们需要导入所需的库。

    import base64
    import hashlib
    import struct
    主要工作由 server 类的 msg(self) 函数完成:

    首先,添加 websocker 标志变量 wsk=False,然后判断我们接收的数据是否有掩码。

    如果有掩码,则数据第二个字节的高位为 1,我们只需判断 (data[1] & 0x80) >> 7 的值。

    如果没有掩码,只需使用 data.decode("utf-8")。

    如果有掩码,我们需要找到掩码键:mask = data[4:8] 和有效传输数据:payload = data[8:],然后解除掩码:for i in range(len(payload)):message += chr(payload[i] ^ mask[i % 4]),并将标志变量 wsk 设为 true。

    解决了掩码问题后,我们还需要添加网络套接字握手过程:

    首先,判断这是否是一个握手过程:如果数据中存在"\r\n\r\n";

    如果是握手过程,则获取键值:data.split("\r\n")[4].split(":")[1];

    计算 Sec-WebSocket-Accept 值:base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8')).digest()),其中 GUID 为固定值 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"。

    然后定义握手响应头:

     response_tpl="HTTP/1.1 101 Switching Protocols\r\n" \
                  "Upgrade:websocket\r\n" \
                  "Connection: Upgrade\r\n" \
                  "Sec-WebSocket-Accept: %s\r\n" \
                  "WebSocket-Location: ws://%s/\r\n\r\n"


    填写响应头: response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.1:8989").

    最后,发送握手响应: self.sk_.send(bytes(response_str, encoding='utf-8')).


    还有一点需要补充,那就是处理要作为 websocket 可接受信息发送的信息:

    if wsk:
       tk=b'\x81'
       lgt=len(bt)
       tk+=struct.pack('B',lgt)
       bt=tk+bt

    现在,服务器端需要修改的部分已经基本完成。


    def msg(self):
            self.re = ''
            wsk=False
            while True:
                data = self.sk_.recv(2500)
                if not data:
                    break
    
                if (data[1] & 0x80) >> 7:
                    fin = (data[0] & 0x80) >> 7 # FIN bit
                    opcode = data[0] & 0x0f # opcode
                    masked = (data[1] & 0x80) >> 7 # mask bit
                    mask = data[4:8] # masking key
                    payload = data[8:] # payload data
                    print('fin is:{},opcode is:{},mask:{}'.format(fin,opcode,masked))
                    message = ""
                    for i in range(len(payload)):
                        message += chr(payload[i] ^ mask[i % 4])
                    data=message
                    wsk=True
                else:
                    data=data.decode("utf-8")
    
                if '\r\n\r\n' in data: 
                    key = data.split("\r\n")[4].split(": ")[1]
                    print(key)
                    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    
                    ac = base64.b64encode(hashlib.sha1((key+GUID).encode('utf-8')).digest())
    
                    response_tpl="HTTP/1.1 101 Switching Protocols\r\n" \
                                "Upgrade:websocket\r\n" \
                                "Connection: Upgrade\r\n" \
                                "Sec-WebSocket-Accept: %s\r\n" \
                                "WebSocket-Location: ws://%s/\r\n\r\n"                
                    response_str = response_tpl % (ac.decode('utf-8'), "127.0.0.1:8989")
                    self.sk_.send(bytes(response_str, encoding='utf-8')) 
                    
                    data=data.split('\r\n\r\n',1)[1]
                if "stop" in data:
                    self.stop=True
                    break
                if len(data)<200:
                     break
                self.re+=data
                bt=eva(self.re, self.model)
                bt=bytes(bt, "utf-8")
    
                if wsk:
                     tk=b'\x81'
                     lgt=len(bt)
                     tk+=struct.pack('B',lgt)
                     bt=tk+bt
                self.sk_.sendall(bt)
            return self.re


    3.使用

    首先,我们需要运行服务器端,在命令行中找到 server.py 文件目录,然后运行 python server.py 启动服务。

    f1

    然后返回 MetaTrader 5 客户端,打开源代码,直接按 Ctrl+F5 或点击测试按钮开始测试:

      此时,信息测试图表的工具箱栏将显示相关信息:

      f2


    回测的结果如下:

      f3

    我们可以看到,我们的整个系统运行完美,可以根据模型的预测执行相关的订单操作。


    注意:

    1. 如果您想直接在图表中进行测试,请注意:到目前为止,我们的代码将同时初始化 websocket 和套接字,当然,如果套接字初始化成功,执行逻辑将不会执行 websocket 逻辑部分,但为了避免不必要的麻烦,建议在这种情况下,请在 OnInit() 中注释掉 websocket 初始化部分。
    2. 除了使用 OnTick() 完成主逻辑外,我们还可以考虑在 OnTimer() 中实现逻辑,这样就可以设置发送数据的具体时间,例如每 15 分钟发送一次数据。这样可以避免在每次报价到达时频繁发送数据。本文不给出具体的实现代码,读者可以参考本文的实现方法编写自己的实现代码。


    结论

    本文将介绍如何使用服务器-客户端方法对我们之前训练的模型进行回溯测试,以及如何在回溯测试和非回溯测试两种情况下测试我们的系统。不可否认,这篇文章涉及很多跨语言、跨领域的知识,其中最难理解的应该是 websocket 部分,这是一个复杂的工程项目。但是,只要您按照本文中的步骤去做,就一定会成功。需要强调的是,本文仅提供了一个示例,让您可以用一个相当简单的策略来测试我们的模型。请勿将其用于实际交易!在实际交易中,您可能需要优化该系统的每个部分才能稳定运行,因此请再次注意,不要在实际交易中直接使用本示例!本文到此结束,下一篇文章我们将讨论如何摆脱套接字依赖,直接在 EA 中使用我们的模型。

    希望你们有所收获,祝你们生活愉快!


    参考文献:

    MetaTrader 5 的 WebSocket — 使用 Windows API

    本文由MetaQuotes Ltd译自英文
    原文地址: https://www.mql5.com/en/articles/13254

    附加的文件 |
    winhttp.mqh (8.13 KB)
    socket_test.mq5 (21.34 KB)
    server.py (4 KB)
    数据科学和机器学习(第 17 部分):摇钱树?外汇交易中随机森林的艺术与科学 数据科学和机器学习(第 17 部分):摇钱树?外汇交易中随机森林的艺术与科学
    探索算法炼金术的秘密,我们将引导您融会贯通如何在解码金融领域时将艺术性和精确性相结合。揭示随机森林如何将数据转化为预测能力,为驾驭股票市场的复杂场景提供独特的视角。加入我们的旅程,进入金融魔法的心脏地带,此处我们会揭开随机森林在塑造市场命运、及解锁赚钱机会之门方面之角色的神秘面纱
    在 MQL5 中实现增广迪基–富勒检验 在 MQL5 中实现增广迪基–富勒检验
    在本文中,我们演示了增广迪基–富勒(Augmented Dickey-Fuller,ADF)检验的实现,并将其应用于使用 Engle-Granger 方法进行协整检验。
    种群优化算法:进化策略,(μ,λ)-ES 和 (μ+λ)-ES 种群优化算法:进化策略,(μ,λ)-ES 和 (μ+λ)-ES
    本文研究一套称为进化策略(ES)的优化算法。它们是最早使用进化原理来寻找最优解的种群算法之一。我们将针对传统的 ES 变体实现变更,并修改算法的测试函数和测试台方法。
    数据科学和机器学习(第 16 部分):全新面貌的决策树 数据科学和机器学习(第 16 部分):全新面貌的决策树
    在我们的数据科学和机器学习系列的最新一期中,深入到错综复杂的决策树世界。本文专为寻求策略洞察的交易者量身定制,全面回顾了决策树在分析市场趋势中所发挥的强大作用。探索这些算法树的根和分支,解锁它们的潜力,从而强化您的交易决策。加入我们,以全新的视角审视决策树,并探索它们如何在复杂的金融市场航行中成为您的盟友。