Integración de MetaTrader 5 y Python: recibiendo y enviando datos

Maxim Dmitrievsky | 7 junio, 2019

¿Para qué sirve la integración de MQL5 y Python?

En nuestra época, el procesamiento de datos requiere un extenso instrumental y muchas veces no se limita al entorno protegido (sandbox) de alguna determinada aplicación. Existen los lenguajes de programación especializados y universalmente reconocidos para procesar y analizar los datos, para la estadística y el aprendizaje automático. Python es el líder en este campo. Por tanto, una solución muy eficaz es usar la potencia del lenguaje y las bibliotecas de inclusión para el desarrollo de sistemas comerciales.

Existen diferentes modos para solucionar el problema de la interacción de dos o más programas, pero los sockets parecen ser una solución más rápida y flexible.

Un socket de red es un punto final de la interacción entre procesos a través de una red de ordenadores. La biblioteca estándar MQL5 incluye un grupo de las funciones Socket que ofrecen una interfaz de bajo nivel para trabajar en Internet. Esta interfaz es común para diferentes lenguajes de programación, ya que utiliza las llamadas de sistema al nivel del sistema operativo.

El intercambio de la información entre procesos se realiza usando el protocolo TCP/IP (Transmission Control Protocol/Internet protocol). De esta manera, los procesos pueden interactuar tanto en un único ordenador, como en una red local, o bien a través de Internet.

Para establecer la conexión, es necesario crear e inicializar un servidor TCP al que va a conectarse el proceso de cliente. Una vez terminada la interacción de procesos, la conexión tiene que cerrarse forzosamente. Los datos en el intercambio TCP representan un flujo de bytes.

Al crear el servidor, es necesario asociar un socket a uno o más anfitriones (direcciones IP, o hosts en inglés) y a algún puerto libre. Si la lista de los hosts no está definida o está especificada como "0.0.0.0", el socket va a escuchar a todos los anfitriones. A mismo tiempo, si especificamos "127.0.0.1" o ''localhost', la conexión será posible sólo dentro del «bucle interno» (internal loop), es decir, dentro de un ordenador.

Como MQL5 dispone sólo del cliente, vamos a crear el servidor en el lenguaje Python.


Creación del servidor socket en Python

El objetivo del presente artículo no consiste en enseñar los fundamentos de la programación en Python. Se supone que el lector ya está familiarizado con este lenguaje. 

Vamos a usar la versión3.7.2 y, al mismo tiempo, el paquete integrado socket, por eso, puede consultar la documentación para más detalles.

Escribiremos un programa simple que va a crear un servidor socket y recibir la información necesario de parte del cliente (programa MQL), procesarla, y luego devolver el resultado obtenido. Esta interacción parece la más exigida. Supongamos que necesitamos usar alguna biblioteca del aprendizaje automático, por ejemplo, scikit learn, que va a calcular la regresión lineal usando los precios, y devolver posteriormente las coordenadas de la línea que servirán para visualizarla en el terminal MetaTrader 5. Este será nuestro ejemplo base. No obstante, esta interacción también puede ser usada para entrenar una red neuronal, para enviarle los datos desde el terminal (cotizaciones), aprender y devolver el resultado al terminal.

Vamos a crear el programa socketserver.py e importar las bibliotecas arriba descritas:

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

Ahora, podemos proceder a crear la clase responsable de la manipulación de los sockets:

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 = ''

        whileTrue:
            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()

Al crear un objeto de la clase, el constructor el nombre del host (dirección IP) y el número del puerto. Luego, se crea el objeto sock que se asocia a la dirección y el puerto sock.bind().

El método recvmsg escucha al socket respeto a la conexión de entrada sock.listen(1). En cuanto aparezca una nueva conexión de cliente, el servidor acepta self.sock.accept().

Después de eso, el servidor espera en un ciclo infinito el mensaje de entrada de parte del cliente, que llega en forma de un flujo de bytes. Como la longitud del mensaje se desconoce de antemano, el servidor recibe el mensaje por partes, digamos cada vez 1 Kbyte, hasta que no lea el mensaje entero self.conn.recv(10000). La parte consecutiva de datos se convierte en una cadena y se añade al resto de la cadena data.decode("utf-8") y se summdata.

Una vez recibidos todos los datos (if not data:), el servidor envía al cliente una cadena que contiene la coordenada derecha e izquierda de la línea de regresión calculada. Previamente, la cadena se convierte en un array de bytes conn.send(bytes(calcregr(self.cummdata), "utf-8"))

Al final, el método devuelve la cadena recibida del cliente. Se puede usarla, por ejemplo, para visualizar las cotizaciones obtenidas.

El destructor cierra el socket completamente cuando la ejecución del programa en Python se termina.

Cabe mencionar que esta forma de la implementación de la clase de ninguna manera pretende a ser algo necesario y lo único correcto. Por ejemplo, se puede separar los métodos de la recepción y el envío de los mensajes, y usarlos de forma diferente en diferentes momentos del tiempo. He descrito sólo la tecnología básica de cómo crear una conexión. Usted puede implementar sus propias soluciones.

Vamos a considerar más detalladamente el método del entrenamiento de la regresión lineal dentro de esta implementación:

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)

Recordamos que el flujo de bytes obtenido se convierte en una cadena utf-8 que luego se recibe por el método calcregr(msg = ' '). Como la cadena contiene una secuencia de precios separados con espacios (conforme lo implementado en el cliente), ella se convierte en el array NumPy tipo float. Después de eso, el array de precios se convierte en una columna (el formato de recepción de datos es sclearn) Y = np.array(chartdata).reshape(-1,1),. El predictor para el modelo es el tiempo lineal (una secuencia de valores cuyo tamaño es igual a la longitud de la muestra de aprendizaje) X = np.array(np.arange(len(chartdata))).reshape(-1,1).

Después de eso, ocurre el entrenamiento y la predicción del modelo, mientras que en la variable "P" se escribe el primer y el último valor de la línea (extremos del segmento) que se convierten en una cadena y se envían al cliente en forma de byetes.

Sólo nos queda crear el objeto de la clase y llamar el método recvmsg() en el ciclo:

serv = socketserver('127.0.0.1', 9090)

while True:  
    msg = serv.recvmsg()


Creación del cliente socket en MQL5

Vamos a crear un Asesor Experto simple que va a conectarse al servidor, enviar un número especificado de últimos precios de cierre, recibir de vuelta las coordenadas de la línea de regresión lineal y trazarla en el gráfico. 

La función socksend() se encargara de enviar los datos al servidor:

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); 
  }

Ella recibe la cadena y la convierte en array de bytes que se envía al servidor.

La función socketreceive() escucha el puerto, y cuando se recibe una respuesta del servidor, la devuelve en forma de una cadena:

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;
  }

La última función drawlr() recibe la cadena que contiene escritas la coordenadas derecha e izquierda de la línea, analiza la cadena en el array string y visualiza la línea de la regresión lineal en el gráfico.

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)); 
     }
  

Las funciones se implementan el el manejador OnTick()

voidOnTick() {
 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); }
   
  elsePrint("Connection ","localhost",":",9090," error ",GetLastError());
  SocketClose(socket); }
 elsePrint("Socket creation error ",GetLastError()); }


Prueba de la aplicación cliente/servidor de MQL5-Python

Para ejecutar el programa, es necesario tener instalado el interpretador Python. Puede descargarlo desde la web oficial.

Después de eso, ejecute el programa servidor socketserver.py. El creará un socket y va a escucharlo respecto a nuevas conexiones de parte del programa MQL5 socketclientEA.mq5.

Tras una conexión con éxito, en la ventana del programa, se mostrará el proceso de la conexión y los precios del anclaje de la línea de regresión que se enviarán de vuelta al cliente:



La actividad de la conexión y los precios de la línea de regresión también serán mostrados en el terminal MetaTrader 5, así como, la propia línea de la regresión será diseñada en el gráfico, que va a actualizarse en cada nuevo tick:

Hemos considerado la interacción directa de dos programas a través la conexión el socket. Al mismo tiempo, MetaQuotes desarrolló un paquete para Python, que permite recibir la información directamente desde el terminal. Para más detalles, consulte la discusión en el foro respecto al uso de Python en MetaTrader.

Vamos a escribir un script para demostrar las posibilidades de recibir las cotizaciones desde el terminal.


Recibir y analizar cotizaciones a través de MetaTrader 5 Python API

Primero, es necesario instalar el modulo python MetaTrader5 (para instrucciones detalladas, siga el enlace de arriba). 

pip install MetaTrader5

Lo importamos al programa e inicializamos la conexión con el terminal:

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())

Después de eso, creamos una lista de los símbolos deseados, y solicitamos sucesivamente los precios de cierre para cada par de divisas desde el terminal en pandas dataframe:

# 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]

Ahora, podemos desconectarse del terminal, y representar los precios de los pares de divisas como variaciones porcentuales, calculando la matriz de correlación y mostrándola en la pantalla:

# 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()

Como podemos observar, hay una buena correlación entre GBPUSD y GBPJPY. Podemos testear la cointegración importando la biblioteca 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])

Además, se puede visualizar rápidamente la relación entre dos pares de divisas como 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()



Visualización de los datos de mercado usando la biblioteca Plotly

A menudo es necesario visualizar las cotizaciones de una forma conveniente. Para eso, se puede usar la biblioteca Plotly que también permite guardar los gráficas en el formato interactivo .html.

Vamos a cargar las cotizaciones para "EURUSD" y visualizarlas como gráfico de velas:

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 1416:13:032019

@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)

Además, puede cargar y visualizar cualquier profundidad del histrorial de bid y ask:

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 1416:13:032019

@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)


Conclusiones

En este artículo, hemos considerado las opciones para implementar la comunicación entre el terminal y el programa escrito en Python a través de los sockets, y usando directamente la biblioteca especializada de MetaQuotes. Lamentablemente, la presente implementación del los socket cliente no permite ejecutarlo en el Simulador de Estrategias. Por tanto, la simulación y la medición de la funcionalidad de esta integración no han sido realizadas. Vamos a esperar la actualización de la funcionalidad de los sockets para el Simulador de Estrategias.