Соединение MetaTrader 5 и Python: получение и отправка данных
Maxim Dmitrievsky | 17 марта, 2019
Зачем нужно соединение 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 не позволяет запускать его в тестере стратегий, поэтому полноценное тестирование и замер производительности такой связки в тестере не производились, но будем ожидать обновления функционала сокетов для тестера.