English 日本語
preview
Разметка данных в анализе временных рядов (Часть 5):Применение и тестирование советника с помощью Socket

Разметка данных в анализе временных рядов (Часть 5):Применение и тестирование советника с помощью Socket

MetaTrader 5Эксперты | 29 апреля 2024, 16:12
324 0
Yuqiang Pan
Yuqiang Pan

Введение

В предыдущих статьях я рассматривал, как проводить разметку данных в соответствии с нашими собственными потребностями и использовать их для обучения моделей прогнозирования временных рядов. Но как наилучшим образом использовать эти модели? Пришло время обсудить, как проверить наши созданные модели в ходе исторического тестирования MetaTrader 5 и включить их в наш советник. В советнике нам нужна стратегия в качестве ключевой логики, а реальная и пригодная для использования стратегия требует конкретной теоретической основы, а также большого количества проверок и корректировок, чтобы обеспечить ее надежность.

Стратегия, приведенная в статье, очень проста и служит лишь демонстрационным примером. Не используйте его в реальной торговле! Конечно, при поддержке большого количества различных библиотек вы также можете выполнить эту работу и с помощью Python, но MetaTrader 5 предоставляет удобный и комплексный инструмент для тестирования на истории и может более точно моделировать нашу торговую среду, поэтому мы всё равно выберем клиент MetaTrader 5 в качестве платформы для тестирования на истории. Но поскольку наша среда создания модели — Python, тестирование на истории MetaTrader 5 должно быть реализовано с помощью MQL5, что немного усложняет реализацию тестирования, но у нас есть решение. Мы обсудим использование трех различных методов для тестирования на истории наших моделей в среде MetaTrader 5, чтобы помочь нам улучшить и повысить их качество. В этой статье будет обсуждаться метод WebSocket. Остальные будут рассмотрены в следующих статьях.

Содержание:

  1. Введение
  2. Принцип реализации
  3. Реализация функций сервера Python
  4. Реализация функций клиента MQL5
  5. Тестирование на истории
  6. Заключение


Принцип реализации

Сначала мы добавляем экземпляр веб-сервера в наш скрипт Python и добавляем к нему вывод нашей модели. Затем мы используем MQL5 для создания веб-клиента для запроса службы вывода (inference service) на сервере.

f0

Вы можете подумать, что это не очень хороший способ. Почему бы просто не преобразовать модель в ONNX, который изначально поддерживается MQL5, а затем добавить вызов в советник? Да, это разумно, но не забывайте, что некоторые конкретные модели огромны, а процесс вывода оптимизируется различными методами, что может потребовать совместной миграции логики вывода и межъязыковой реализации. В результате, проект вырастет до огромных размеров. Метод может объединять системы и языки для достижения различных комбинаций функций. Например, если ваш MetaTrader 5-клиент работает на Windows, то вашу серверную часть можно даже развернуть на удаленном сервере. Вашим сервером может быть любая операционная система, поддерживающая вывод модели, поэтому вам не придется устанавливать дополнительные виртуальные машины. Конечно, вы также можете развернуть сервер на wsl или docker. Таким образом, мы не будем ограничены одной операционной системой или одним языком программирования. Этот метод очень распространен, и мы можем свободно расширять его использование.

Мы предполагаем, что логика работы советника следующая:

  • Во-первых, каждый раз, когда запускается событие OnTick(), последние 300 данных гистограммы отправляются на сервер через клиент.
  • После получения информации сервер отправляет прогнозируемый тренд следующих 6 гистограмм клиенту советника посредством вывода модели. Здесь мы используем модель Nbeats, упомянутую в предыдущей статье, поскольку она может разложить прогноз на тенденции.
  • Если тренд нисходящий, то продавайте, если тренд восходящий, покупайте.

Реализация функций сервера Python

Сокет в Python в основном включает в себя следующие функции:

  • socket.bind(): привязывает адрес (хост, порт) к сокету. В AF_INET адрес представлен кортежем (хост, порт).
  • socket.listen(): начать прослушивание TCP. backlog указывает максимальное количество соединений, которые операционная система может приостановить, прежде чем отклонить соединение. Значение равно как минимум 1, большинство приложений устанавливают его равным 5.
  • socket.accept(): пассивно принимать соединение TCP-клиента, (blocking) ждать соединения.
  • socket.connect(): активно инициализировать соединение с TCP-сервером. Обычно формат адреса представляет собой кортеж (имя хоста, порт). Если соединение не удалось, возвращаем ошибку socket.error.
  • socket.connect_ex(): расширенная версия функции connect(). Возвращает код ошибки при возникновении ошибки вместо выдачи исключения. socket.recv(): Получает данные TCP, данные возвращаются в виде строки, bufsize определяет максимальный объем данных для получения. 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): устанавливает таймаут для операций с сокетами, таймаут — это число с плавающей запятой в секундах. Значение None означает отсутствие таймаута. Как правило, таймаут следует устанавливать, если сокет был создан только что, поскольку он может использоваться для операций подключения (например, Connect()).
  • socket.gettimeout(): возвращает текущее значение таймаута в секундах или None, если таймаут не установлен.
  • socket.fileno(): возвращает файловый дескриптор сокета.
  • socket.setblocking(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. Создание класса сервера

    Создайте класс сервера, в котором мы инициализируем некоторые основные настройки сокета, включая следующие функции:

    socket.socket(): устанавливаем два параметра на socket.AF_INET и socket.SOCK_STREAM.

    Метод bind() socket.socket(): функция устанавливает параметр хоста на "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_)  

    Примечание: Если вы развертываете сервер в докере или контейнере, похожем на докер, вам может потребоваться установить хост на "0.0.0.0", иначе ваш клиент не сможет найти сервер.


    3. Логика обработки полученной информации

    Мы определяем метод класса msg() для обработки полученной информации, используя цикл while для обработки полученной информации. Здесь следует отметить одну вещь: полученные данные необходимо декодировать с помощью decode("utf-8"), а затем обработанная информация отправляется в функцию обработки логики вывода self.sk_.send(bytes(eva(self.re), "utf-8")), где функция логики вывода определена как eva(), а параметром является полученная нами информация, которую мы реализуем позже. Далее нам нужно убедиться, что наш сервер также останавливается при остановке тестирования советника на истории, иначе он будет занимать ресурсы в фоновом режиме. Мы можем сделать это, отправив на сервер строку "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. Определение логики вывода

    Логика вывода примера очень проста. Мы просто загружаем модель и используем гистограмму, предоставленную клиентом, для прогнозирования результатов, а затем разделяем их на тренды и отправляем результаты обратно клиенту. Здесь нам нужно обратить внимание на то, что мы можем инициализировать модель при инициализации класса сервера, чтобы модель была предварительно загружена и готова к выводу в любое время.

    Сначала мы определяем функцию для загрузки модели, а затем вызываем эту функцию при инициализации класса сервера, чтобы получить экземпляр модели. В предыдущей статье мы представили обработку сохранения и загрузки модели. После обучения модель сохранит информацию в json-файле results.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
    Затем добавим функцию init() класса server_(): 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). После завершения вывода возвращается результат. Мы установили, что если последнее значение тренда больше среднего значения, это восходящий тренд, в противном случае — нисходящий. Если тренд восходящий, возвращается "buy", если нисходящий - "sell". Процесс вывода в этой статье не рассматривается. Вы можете почитать о нем в предыдущих частях серии. Здесь необходимо подчеркнуть еще один момент. Поскольку мы устанавливаем предиктор модели как столбец "close" DataFrame, нам нужно добавить столбец "close" к данным, преобразованным в DataFrame: 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"

    Далее нам нужно добавить основной цикл.

    Сначала мы инициализируем класс обслуживания, затем добавляем функцию обработки информации в цикл while. Мы завершаем цикл и выходим из программы, когда получаем сигнал остановки. Мы не хотим, чтобы цикл выполнялся слишком быстро, поэтому добавляем Sleep(0.5), чтобы ограничить скорость цикла и избежать высокой загрузки процессора.

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

    Мы создали простой сервер, теперь нужно реализовать клиент в советнике.


    Реализация функций клиента MQL5

    1. Функции сокета в MQL5

    Модуль сокета в настоящее время включает в себя следующие функции:

    • SocketCreate: создает сокет с указанным идентификатором и возвращает его хэндл
    • SocketClose: закрывает сокет
    • SocketConnect: выполняет подключение к серверу с контролем таймаута
    • SocketIsConnected: проверяет, подключен ли сокет в текущий момент
    • SocketIsReadable: получает количество байт, которое можно прочитать из сокета
    • SocketIsWritable: проверяет, возможна ли запись данных в сокет в текущий момент
    • SocketTimeouts: устанавливает таймауты получения и передачи данных для системного объекта сокета
    • SocketRead: читает данные из сокета
    • SocketSend: записывает данные в сокет
    • SocketTlsHandshake: инициирует защищенное TLS (SSL)-соединение с указанным хостом по протоколу TLS Handshake
    • SocketTlsCertificate: получает данные о сертификате, используемом для защиты сетевого соединения
    • SocketTlsRead: читает данные из защищенного TLS-соединения
    • SocketTlsReadAvailable: читает все доступные данные из защищенного TLS-соединения
    • SocketTlsSend: отправляет данные через защищенное TLS-соединение

    Обратившись к этим методам, мы можем легко добавить дополнительные функции на стороне клиента.


    2. Реализация функций советника

    Для начала рассмотрим функциональную логику советника:

    Инициализируем сокет в "int OnInit()".

    Затем в "void OnTick()" реализуем получение данных от клиента и отправку текущих данных гистограммы клиенту, а также логику тестирования нашего советника на истории.

    В "void OnDeinit(const int Reason)" нам необходимо отправить на сервер сообщение "stop" и закрыть сокет.


    3. Инициализация советника

    Во-первых, нам нужно определить глобальную переменную "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);
      }


    Не забудьте освободить ресурсы в конце советника.

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


    4. Торговая логика

    Здесь нам нужно определить основную логику обработки данных и торговую логику в void OnTick().

    Создайте переменные "MqlTradeRequest request" и "MqlTradeResult result" для выполнения задач ордера;

    Создадим переменную массива символов "char Recv_data[]" для получения информации о сервере;

    Создадим переменную двойного массива "doublepriceData[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)", чтобы преобразовать строковые данные в массив символов, который может использоваться сокетом.

    После преобразования данных нам нужно использовать SocketIsWritable(sk), чтобы определить, может ли наш сокет отправлять данные. Если да, то используйте SocketSend(sk,ds,dsl) для отправки данных.

    Также нам нужно считать информацию с сервера. Используем "uint len=SocketIsReadable(sk)", чтобы проверить, есть ли доступные данные на текущем порту. Если информация не пустая, выполняем торговую логику: int rsp_len=SocketRead( sk,recv_data,len,500), "len" — размер буфера, "500" — таймаут (в миллисекундах).

    Если получено "buy", то открываем ордер на покупку, устанавливаем запрос следующим образом:

    • Сбросить структуру торгового запроса request: 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);
                 }
            }
         }
    }

    Примечание: Параметр buffer_maxlen в функции SocketSend() должен соответствовать настройке сервера. Это значение будет автоматически рассчитано и возвращено при выполнении функции StringToCharArray().

    Теперь мы сначала запускаем server.py, а затем добавляем советник на график в клиенте MetaTrader 5. Результаты следующие:



    Но мы пока не можем использовать тестирование на истории, поскольку SocketCreate() и серия операций с сокетами не разрешены при тестировании. Далее рассмотрим решение этой проблемы.



    Тестирование на истории


    Ранее мы упоминали об ограничениях сокетов в MQL5, а теперь нам нужно добавить поддержку веб-сокетов как в файл MQL5, так и в файл Python.


    1. Добавление поддержки веб-сокетов в клиент

    При тестировании на истории мы можем использовать winhttp.mqh в API Windows для достижения желаемой функциональности. Подробное введение в API здесь:

    Официальная документация Microsoft: https://docs.microsoft.com/ru-ru/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), в случае ошибки инициализация завершается неудачно;
    • Настроим веб-соект: WinHttpSetOption(re_h,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nullpointer,0), при ошибке инициализация завершается неудачно;
    • Выполним запрос на установление связи через веб-сокет: WinHttpSendRequest( re_h,NULL, 0,nullpointer, 0, 0, 0), при ошибке инициализация завершается неудачно;
    • Получим ответ на рукопожатие сервера: WinHttpReceiveResponse(re_h,nullpointer), при ошибке инициализация завершается неудачно;
    • Обновим веб-сокет, получим хэндл после инициализации: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, то нам нужно доработать логику работы веб-сокета:

    • Сначала отправим данные на сервер: 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, set: 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);
                              }
                        }
                }
            }
            
            
        }
        
      }

    На этом этапе мы завершили настройку нашего WebSocket-клиента MQL5.


    2. Настройка на стороне сервера

    Нам нужно добавить поддержку веб-сокетов в server.py.

    Сначала нам нужно импортировать необходимые библиотеки.

    import base64
    import hashlib
    import struct
    Основная работа выполняется функцией msg(self) серверного класса:

    Сначала добавим переменную флага веб-сокета 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.

    После решения проблемы скрытия данных нам также необходимо добавить установку связи через веб-сокет:

    Во-первых, убедимся, что это действительно установка связи: if ‘\r\n\r\n’ in data;

    Если да, получим значение ключа: 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’)).


    Осталось добавить еще кое-что: нужно обрабатывать информацию, которая будет отправлена, как информацию, приемлемую для веб-сокета:

    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. Если вы хотите провести тест непосредственно на графике, обратите внимание: до сих пор наш код инициализировал и веб-сокет, и сокет одновременно. Конечно, если инициализация сокета успешна, логика выполнения не будет выполнять логическую часть веб-сокета, но во избежание ненужных проблем в этом случае рекомендуется закомментировать часть инициализации веб-сокета в OnInit().
    2. Помимо использования OnTick() для завершения нашей основной логики, мы также можем рассмотреть возможность реализации логики в OnTimer(), чтобы вы могли установить конкретное время для отправки данных, например, отправлять данные каждые 15 минут. Это позволит избежать частой отправки данных при поступлении котировок. В этой статье не приводится конкретный код реализации. Читатели могут обратиться к методу реализации в этой статье, чтобы написать свой собственный код реализации.


    Заключение

    Мы рассмотрели метод сервер-клиент для тестирования на истории ранее обученной модели. Также я показал, как протестировать нашу систему в сценариях тестирования на истории и вне их. Статья требует большого объема знаний о взаимодействии разных языков и дисциплин. Самой сложной для понимания частью является концепция веб-сокетов, представляющих собой сложный инженерный проект. Но если вы будете следовать шагам, описанным в этой статье, у вас все получится. Следует подчеркнуть, что в этой статье представлен лишь пример, который позволяет протестировать нашу модель с помощью довольно простой стратегии. Пожалуйста, не используйте его для реальной торговли! Реальная торговля требует оптимизации каждой части этой системы для стабильной работы, поэтому еще раз: не используйте этот пример в своей реальной торговле! В следующей статье мы обсудим, как избавиться от зависимости от сокетов и использовать нашу модель непосредственно в советнике.

    Надеюсь, информация была полезной для вас.


    Ссылки:

    WebSocket для MetaTrader 5 — Использование 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)
    Как добавить Trailing Stop по индикатору Parabolic SAR Как добавить Trailing Stop по индикатору Parabolic SAR
    При создании торговой стратегии нам нужно проверить самые разные варианты защитных стопов. И тут напрашивается динамическое подтягивание уровня Stop Loss вслед за ценой. Наилучшим кандидатом для этого является индикатор Parabolic SAR —трудно придумать что-либо проще и нагляднее.
    Разработка системы репликации (Часть 38): Прокладываем путь (II) Разработка системы репликации (Часть 38): Прокладываем путь (II)
    Многие люди, которые считают себя программистами на MQL5, не обладают базовыми знаниями, которые мы изложим в этой статье. Многие считают MQL5 ограниченным инструментом, однако всё дело в недостатке знаний. Так что если вы чего-то не знаете, не стыдитесь этого. Лучше пусть вам будет стыдно за то, что вы не спросили. Простое принуждение MetaTrader 5 к запрету дублирования индикатора никоим образом не обеспечивает двустороннюю связь между индикатором и советником. Мы еще очень далеки от этого, но тот факт, что индикатор не дублируется на графике, дает нам некоторое утешение.
    Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 3): Создание автоматических ходов и тестовых скриптов на MQL5 Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 3): Создание автоматических ходов и тестовых скриптов на MQL5
    В этой статье рассматривается реализация автоматических ходов в игре "Крестики-нолики" на языке Python, интегрированная с функциями MQL5 и модульными тестами. Цель - улучшить интерактивность игры и обеспечить надежность системы с помощью тестирования на MQL5. Изложение охватывает разработку игровой логики, интеграцию и практическое тестирование, а завершается созданием динамической игровой среды и надежной интегрированной системы.
    Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 2): Функции MQL5 для HTTP-взаимодействия с REST API игры "крестики-нолики" Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 2): Функции MQL5 для HTTP-взаимодействия с REST API игры "крестики-нолики"
    В этой статье расскажем о том, как MQL5 может взаимодействовать с Python и FastAPI, используя HTTP-вызовы в MQL5 для взаимодействия с игрой "крестики-нолики" на Python. В статье рассматривается создание API с помощью FastAPI для этой интеграции и приводится тестовый скрипт на MQL5, подчеркивающий универсальность MQL5, простоту Python и эффективность FastAPI в соединении различных технологий для создания инновационных решений.