Соединение MetaTrader 5 и Python: получение и отправка данных

17 марта 2019, 07:36
Maxim Dmitrievsky
10
2 677

Зачем нужно соединение MQL5 и Python

Работа с данными в наше время требует обширного инструментария и зачастую не ограничивается "песочницей" какого-то отдельного приложения. Существуют специализированные общепризнанные языки программирования для обработки и анализа данных, статистики и машинного обучения. Лидером в этой области является язык Python. Соответственно, всегда хочется использовать мощь языка и подключаемых библиотек для разработки торговых систем.

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

Сетевой сокет является конечной точкой межпроцессного взаимодействия через компьютерную сеть. В стандартной библиотеке MQL5 есть группа функций Socket, которые обеспечивают низкоуровневый интерфейс для работы в сети интернет. Этот интерфейс является общим для разных языков программирования, так как он использует системные вызовы на уровне операционной системы.

Обмен информацией между процессами происходит по протоколу TCP/IP (Transmission Control Protocol/Internet protocol). Таким образом, процессы могут взаимодействовать как в рамках одного компьютера, так и по локальной сети, либо через интернет.

Для того чтобы установить соединение, необходимо создать и инициализировать TCP сервер, к которому будет подключаться клиентский процесс. После завершения взаимодействия процессов соединение должно быть принудительно закрыто. Данные при TCP-обмене представляют собой поток байтов.

При создании сервера необходимо связать сокет с одним или несколькими хостами (IP-адресами) и каким-нибудь незанятым портом. Если же список хостов не задан или задан в виде "0.0.0.0", сокет будет прослушивать все хосты. В то же время, если указать "127.0.0.1" или ''localhost', то подключение можно будет установить только в пределах "внутренней петли", то есть одного компьютера.

Поскольку в MQL5 доступен только клиент, сервер мы создадим на языке Python.


Создание сокет-сервера на языке Python

Целью статьи не является обучение основам программирования на Python. Предполагается, что читатель знаком с этим языком. 

Будет использоваться версия 3.7.2 и одноименный встроенный пакет socket, поэтому вы можете обратиться к документации для уточнения каких-то нюансов.

Мы напишем простую программу, которая будет создавать сокет-сервер и принимать необходимую информацию от клиента (MQL5 программы), обрабатывать ее, после чего отправлять полученный результат обратно. Такое взаимодействие видится наиболее востребованным. Допустим, нам необходимо воспользоваться какой-нибудь библиотекой машинного обучения, например, scikit learn, которая будет рассчитывать линейную регрессию от цен, а затем возвращать координаты линии, по которым ее можно отобразить в терминале MetaTrader 5. Возьмем этот пример за базовый, но подобное взаимодействие может быть также использовано для обучения нейронной сети, передачи в нее данных из терминала (котировок), обучения и возвращения результата в терминал.

Создадим программу socketserver.py и импортируем описанные выше библиотеки:

import socket, numpy as np
from sklearn.linear_model import LinearRegression

Теперь можно приступить к созданию класса, отвечающего за манипуляцию с сокетами:

class socketserver:
    def __init__(self, address = '', port = 9090):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.address = address
        self.port = port
        self.sock.bind((self.address, self.port))
        self.cummdata = ''
        
    def recvmsg(self):
        self.sock.listen(1)
        self.conn, self.addr = self.sock.accept()
        print('connected to', self.addr)
        self.cummdata = ''

        while True:
            data = self.conn.recv(10000)
            self.cummdata+=data.decode("utf-8")
            if not data:
                break    
            self.conn.send(bytes(calcregr(self.cummdata), "utf-8"))
            return self.cummdata
            
    def __del__(self):
        self.sock.close()

При создании объекта класса, конструктор получает имя хоста (IP адрес) и номер порта. Далее создается объект sock, который связывается с адресом и портом sock.bind().

Метод recvmsg прослушивает сокет на предмет входящего подключения sock.listen(1). После того, как клиент "постучался", сервер принимает входящее подключение self.sock.accept().

После этого в бесконечном цикле сервер ожидает входящее сообщение от клиента в виде потока байтов. Поскольку длина сообщения заранее неизвестна, то он получает ее частями, допустим, по 1к байтов за раз, пока не прочтет все сообщение целиком self.conn.recv(10000). Очередная порция данных преобразуется в строку data.decode("utf-8") и прибавляется к остальной строке summdata.

Далее, когда все данные приняты (if not data:), сервер отправляет клиенту строку, в которой содержится правая и левая координаты рассчитанной линии регрессии. Предварительно строка преобразуется в байтовый массив conn.send(bytes(calcregr(self.cummdata), "utf-8"))

В конце метод возвращает полученную от клиента строку, ее можно использовать, например, для визуализации полученных котировок.

Деструктор закрывает сокет полностью при завершении Python программы.

Стоит отметить, что данная реализация класса ни в коей мере не является чем-то необходимым и единственно верным. Например, можно разделить методы приема и отправки сообщений и использовать их в разные моменты времени по разному. Я описал лишь базовую технологию создания подключения, а конкретные реализации каждый сделает для себя сам.

В текущей реализации полезно остановиться на методе обучения линейной регрессии:

def calcregr(msg = ''):
    chartdata = np.fromstring(msg, dtype=float, sep= ' ') 
    Y = np.array(chartdata).reshape(-1,1)
    X = np.array(np.arange(len(chartdata))).reshape(-1,1)
        
    lr = LinearRegression()
    lr.fit(X, Y)
    Y_pred = lr.predict(X)
    type(Y_pred)
    P = Y_pred.astype(str).item(-1) + ' ' + Y_pred.astype(str).item(0)
    print(P)
    return str(P)

Вспомним, что полученный поток байтов преобразуется в строку utf-8, которую затем принимает метод calcregr(msg = ' '). Поскольку строка содержит последовательность цен, разделенных пробелами (реализовано в клиенте), то она преобразуется в NumPy-массив типа float. После этого массив цен преобразуется в колонку (формат получения данных sclearn) Y = np.array(chartdata).reshape(-1,1), а предиктором для модели является линейное время (последовательность значений, размер которой равен длине обучающей выборки) X = np.array(np.arange(len(chartdata))).reshape(-1,1).

После этого происходит обучение и предсказание модели, а в переменную "P" записываются первое и последнее значение линии (концов отрезка), которые преобразуются в строку и передаются клиенту в байтовом виде.

Осталось только создать объект класса и в цикле вызывать метод recvmsg():

serv = socketserver('127.0.0.1', 9090)

while True:  
    msg = serv.recvmsg()


Создание сокет-клиента на языке MQL5

Создадим простого эксперта, который будет подключаться к серверу, передавать заданное количество последних цен закрытия и получать обратно координаты линии регрессии, после чего рисовать ее на графике. 

Функция socksend() будет передавать данные на сервер:

bool socksend(int sock,string request) 
  {
   char req[];
   int  len=StringToCharArray(request,req)-1;
   if(len<0) return(false);
   return(SocketSend(sock,req,len)==len); 
  }

Она принимает строку, преобразует ее в байтовый массив, который отправляет на сервер.

Функция socketreceive() прослушивает порт, и при появлении ответа от сервера возвращает его в виде строки:

string socketreceive(int sock,int timeout)
  {
   char rsp[];
   string result="";
   uint len;
   uint timeout_check=GetTickCount()+timeout;
   do
     {
      len=SocketIsReadable(sock);
      if(len)
        {
         int rsp_len;
         rsp_len=SocketRead(sock,rsp,len,timeout);
         if(rsp_len>0) 
           {
            result+=CharArrayToString(rsp,0,rsp_len); 
           }
        }
     }
   while((GetTickCount()<timeout_check) && !IsStopped());
   return result;
  }

Последняя функция drawlr() получает строку, в которой записаны правая и левая координаты линии, парсит ее в строковый массив и выводит линию линейной регрессии на график:

void drawlr(string points) 
  {
   string res[];
   StringSplit(points,' ',res);

   if(ArraySize(res)==2) 
     {
      Print(StringToDouble(res[0]));
      Print(StringToDouble(res[1]));
      datetime temp[];
      CopyTime(Symbol(),Period(),TimeCurrent(),lrlenght,temp);
      ObjectCreate(0,"regrline",OBJ_TREND,0,TimeCurrent(),NormalizeDouble(StringToDouble(res[0]),_Digits),temp[0],NormalizeDouble(StringToDouble(res[1]),_Digits)); 
     }
  

Реализация функций выполнена в обработчике OnTick()

void OnTick() {
 socket=SocketCreate();
 if(socket!=INVALID_HANDLE) {
  if(SocketConnect(socket,"localhost",9090,1000)) {
   Print("Connected to "," localhost",":",9090);
         
   double clpr[];
   int copyed = CopyClose(_Symbol,PERIOD_CURRENT,0,lrlenght,clpr);
         
   string tosend;
   for(int i=0;i<ArraySize(clpr);i++) tosend+=(string)clpr[i]+" ";       
   string received = socksend(socket, tosend) ? socketreceive(socket, 10) : ""; 
   drawlr(recieved); }
   
  else Print("Connection ","localhost",":",9090," error ",GetLastError());
  SocketClose(socket); }
 else Print("Socket creation error ",GetLastError()); }


Тестирование клиент-серверного приложения MQL5-Python

Для запуска программы должен быть установлен интерпретатор Python, который можно скачать и установить с официального сайта.

После этого запустим программу-сервер socketserver.py, он создаст сокет и будет прослушивать его на предмет новых подключений от программы MQL5 socketclientEA.mq5.

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



В терминале MetaTrader 5 также будет отображаться активность подключения и цены привязки линии регрессии, как и сама линия регрессии на графике, которая будет обновляться на каждом новом тике:

Мы рассмотрели способ непосредственного взаимодействия двух программ через сокет соединение. В то же время компания MetaQuotes разработала пакет для Python, который позволяет получать информацию напрямую из терминала. Подробности и обсуждение в теме как использовать Python в Метатрейдере.

Давайте напишем скрипт, демонстрирующий возможности получения котировок из терминала.


Получение и анализ котировок через MetaTrader 5 Python API

Сначала необходимо установить python-модуль MetaTrader5, подробные инструкции по ссылке выше. 

pip install MetaTrader5

Импортируем его в программу и инициализируем подключение к терминалу, дождавшись его загрузки:

from MetaTrader5 import *
from datetime import date
import pandas as pd 
import matplotlib.pyplot as plt 

# Initializing MT5 connection 
MT5Initialize()
MT5WaitForTerminal()

print(MT5TerminalInfo())
print(MT5Version())

После этого создадим список интересующих нас инструментов и последовательно запросим цены закрытия для каждой валютной пары из терминала в пандас датафрейм:

# Create currency watchlist for which correlation matrix is to be plotted
sym = ['EURUSD','GBPUSD','USDJPY','USDCHF','AUDUSD','GBPJPY']

# Copying data to dataframe
d = pd.DataFrame()
for i in sym:
     rates = MT5CopyRatesFromPos(i, MT5_TIMEFRAME_M1, 0, 1000)
     d[i] = [y.close for y in rates]

Теперь мы можем отключиться от терминала, после чего привести цены валютных пар к процентным изменениям, посчитав корреляционную матрицу и выведя ее на экран:

# Deinitializing MT5 connection
MT5Shutdown()

# Compute Percentage Change
rets = d.pct_change()

# Compute Correlation
corr = rets.corr()

# Plot correlation matrix
plt.figure(figsize=(10, 10))
plt.imshow(corr, cmap='RdYlGn', interpolation='none', aspect='auto')
plt.colorbar()
plt.xticks(range(len(corr)), corr.columns, rotation='vertical')
plt.yticks(range(len(corr)), corr.columns);
plt.suptitle('FOREX Correlations Heat Map', fontsize=15, fontweight='bold')
plt.show()

Видно, что валютные пары GBPUSD и GBPJPY хорошо коррелируют. Мы можем провести тесты на коинтеграцию, импортировав библиотеку statmodels:

# Importing statmodels for cointegration test
import statsmodels
from statsmodels.tsa.stattools import coint

x = d['GBPUSD']
y = d['GBPJPY']
x = (x-min(x))/(max(x)-min(x))
y = (y-min(y))/(max(y)-min(y))

score = coint(x, y)
print('t-statistic: ', score[0], ' p-value: ', score[1])

Также можно быстро отобразить отношение двух валютных пар в виде z-score:

# Plotting z-score transormation
diff_series = (x - y)
zscore = (diff_series - diff_series.mean()) / diff_series.std()

plt.plot(zscore)
plt.axhline(2.0, color='red', linestyle='--')
plt.axhline(-2.0, color='green', linestyle='--')

plt.show()



Визуализация рыночных данных с помощью библиотеки Plotly

Часто необходимо визуализировать сами котировки удобным образом. Для этого можно использовать библиотеку Plotly, которая также позволяет сохранять графики в интерактивном .html формате.

Загрузим котировки символа "EURUSD" и отобразим их в виде свечного графика:

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 14 16:13:03 2019

@author: dmitrievsky
"""
from MetaTrader5 import *
from datetime import datetime
import pandas as pd
# Initializing MT5 connection 
MT5Initialize()
MT5WaitForTerminal()

print(MT5TerminalInfo())
print(MT5Version())

# Copying data to pandas data frame
stockdata = pd.DataFrame()
rates = MT5CopyRatesFromPos("EURUSD", MT5_TIMEFRAME_M1, 0, 5000)
# Deinitializing MT5 connection
MT5Shutdown()

stockdata['Open'] = [y.open for y in rates]
stockdata['Close'] = [y.close for y in rates]
stockdata['High'] = [y.high for y in rates]
stockdata['Low'] = [y.low for y in rates]
stockdata['Date'] = [y.time for y in rates]

import plotly.graph_objs as go
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot

trace = go.Ohlc(x=stockdata['Date'],
                open=stockdata['Open'],
                high=stockdata['High'],
                low=stockdata['Low'],
                close=stockdata['Close'])

data = [trace]
plot(data)

Также можно загрузить и отобразить историю bid и ask на любую глубину:

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 14 16:13:03 2019

@author: dmitrievsky
"""
from MetaTrader5 import *
from datetime import datetime

# Initializing MT5 connection 
MT5Initialize()
MT5WaitForTerminal()

print(MT5TerminalInfo())
print(MT5Version())

# Copying data to list
rates = MT5CopyTicksFrom("EURUSD", datetime(2019,3,14,13), 1000, MT5_COPY_TICKS_ALL)
bid = [x.bid for x in rates]
ask = [x.ask for x in rates]
time = [x.time for x in rates]

# Deinitializing MT5 connection
MT5Shutdown()

import plotly.graph_objs as go
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
data = [go.Scatter(x=time, y=bid), go.Scatter(x=time, y=ask)]

plot(data)


Заключение

В данной статье мы рассмотрели возможность коммуникации терминала и программы, написанной на Python, при помощи сокетов, а также напрямую, с помощью предоставленной компанией MetaQuotes библиотеки. К сожалению, текущая реализация сокет-клиента в MetaTrader 5 не позволяет запускать его в тестере стратегий, поэтому полноценное тестирование и замер производительности такой связки в тестере не производились, но будем ожидать обновления функционала сокетов для тестера.

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
Alexander Fedosov
Alexander Fedosov | 17 мар 2019 в 19:02
Igor Makanu:

С удовольствием всегда читаю статьи @Maxim Dmitrievsky , нравится мне его подход к написанию статей - сжато описана сама реализация идеи, не люблю в последнее время "километровые портянки" кода и разжевывание каждой строчки до "а теперь мы назовем переменную XY, где сохраним ..."

Спасибо!

Так читатель то разный бывает как искушенный, так и новичок. 
Igor Makanu
Igor Makanu | 17 мар 2019 в 19:24
Alexander Fedosov:
Так читатель то разный бывает как искушенный, так и новичок. 

Ну как бы Вы правы, так и не очень - человеческое внимание очень ограниченный ресурс, и даже опытный читатель, чтобы вникнуть в суть статьи вынужден читать "по диагонали" иначе к концу статьи просто не будет усвоения материала.

Если не ошибаюсь, то человек может усваивать новый материал лишь первые 15 минут, затем внимание снижается и эффект от новых знаний будет нулевой - как говорится  "лучше меньше да лучше" 

Возможно конечно растянуть чтение большой статьи, как чтение журнала или книги на несколько дней, но это не мой вариант

slukin
slukin | 26 мар 2019 в 14:04

socketclientEA (Si-6.19,M15) Connection localhost:9090 error 4014

https://www.mql5.com/ru/docs/network/socketconnect

При вызове из индикатора GetLastError() вернет ошибку 4014 – "Системная функция не разрешена для вызова".

Решение:

Адрес для подключения должен быть добавлен в список разрешенных на стороне клиентского терминала (раздел Сервис \ Настройки \ Советники).

Разрешить Webrequest для следующих url:

http://localhost

Сергей Иванов
Сергей Иванов | 4 июн 2019 в 19:19
Как побороть сообщение "IPC call failed", появляющееся при запуске ...= MT5CopyTicksRange(...)
transcendreamer
transcendreamer | 21 июн 2019 в 02:15
slukin:

socketclientEA (Si-6.19,M15) Connection localhost:9090 error 4014

https://www.mql5.com/ru/docs/network/socketconnect

При вызове из индикатора GetLastError() вернет ошибку 4014 – "Системная функция не разрешена для вызова".

Решение:

Адрес для подключения должен быть добавлен в список разрешенных на стороне клиентского терминала (раздел Сервис \ Настройки \ Советники).

Разрешить Webrequest для следующих url:

http://localhost

Спасибо, это было весьма не самоочевидно сначала, разработчики злые гении просто!

Извлечение структурированных данных из HTML-страниц с помощью CSS-селекторов Извлечение структурированных данных из HTML-страниц с помощью CSS-селекторов

В статье описан универсальный метод анализа и конвертации данных из HTML-документов, основанный на CSS-селекторах. Торговые отчеты, отчеты тестера, ваши любимые экономические календари, публичные сигналы и мониторы счетов, дополнительные источники онлайн котировок - все это становится доступным из MQL.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть III): Коллекция рыночных ордеров и позиций, поиск и фильтрация Библиотека для простого и быстрого создания программ для MetaTrader (Часть III): Коллекция рыночных ордеров и позиций, поиск и фильтрация

В первой статье мы начали создавать большую кроссплатформенную библиотеку для легкого создания программ на платформах MetaTrader 5 и MetaTrader 4. Далее продолжили развитие библиотеки и сделали коллекцию исторических ордеров и сделок. Теперь создадим класс для удобного выбора и фильтрации ордеров, сделок и позиций в списках коллекций, а именно создадим базовый объект библиотеки — Engine, и добавим в библиотеку коллекцию рыночных ордеров и позиций.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть IV): Торговые события Библиотека для простого и быстрого создания программ для MetaTrader (Часть IV): Торговые события

В предыдущих статьях мы начали создавать большую кроссплатформенную библиотеку, целью которой является облегчение написания программ для платформы MetaTrader 5 и MetaTrader 4. У нас уже есть коллекции исторических ордеров и сделок, рыночных ордеров и позиций, класс для удобного выбора и фильтрации ордеров. В данной части продолжим развитие базового объекта и научим библиотеку Engine отслеживать торговые события на счёте.

MTF-индикаторы как инструмент технического анализа MTF-индикаторы как инструмент технического анализа

Большинство из нас согласны с мнением, что процесс анализа текущей рыночной ситуации начинается с рассмотрения старших периодов графика. Происходит это до тех пор, пока мы не перейдем на тот график, на котором совершаем сделки. Данный вариант анализа является одним из условий успешной торговли и профессиональным подходом к делу. В статье пойдет речь о мультитаймфреймовых индикаторах и способах их создания. Будут приведены примеры кода MQL5, дана общая оценка достоинств и недостатков каждой версии, а также будет предложен новый подход к индикаторам с использованием режима MTF.