Entornos de Inteligencia Artificial para Trading y Simulando Ticks

Share this

A estas alturas, si has estado intentando diseñar modelos de machine learning  o inteligencia artificial para hacer trading, ya te habrás dado cuenta de que la cosa no es tan fácil como normalizar unos cuantos datos históricos de Forex o Bolsa, meterlos en una red neuronal y esperar que tu output tipo {mantener, comprar, vender} te salga lo suficiente bien balanceado como para que la red negocie por ti.

De hecho, en el pasado, mi experiencia es que arquitecturas como XGBOOST daban mejores predicciones con datos suavizados con medias móviles de 2 o 3 muestras que perceptrones o LSTMs.

Todo depende del tiempo que uno tiene disponible. Si es poco, como en mi caso, lo mejor es ir primero a los más seguro y luego ir refinando el trabajo para poder, con la mayor brevedad posible, aprobar o descartar una metodología. Lo de «la mayor brevedad» es un concepto bastante relativo.

En mi caso, la mayor brevedad han sido unos cinco años de trabajo intenso más un esfuerzo financiero relevante. En mi caso, como ya he comentado en otras entradas, he estudiado casi todas las técnicas de trading, las he probado, validado; una tras otra, durante años. Tanto el trading manual como el trading algorítmico. Es decir, que no solo he estudiado trading o pagado por aprender, sino que además he tenido que investigar mucho sobre algoritmos, estructuras, lenguajes, estadística (incluyendo un master en Ciencia de Datos), probabilidad, machine learning, inteligencia artificial y otras materias relevantes.

En ese sentido, se puede decir que me he hecho una idea genérica relativamente amplia del asunto. Escribo este rollo, no para ponerme medallas, sino para que quien lea este texto entienda que el trading no es llegar y triunfar. En el mejor de los casos hacen falta entre 3 o 5 años para empezar a entenderlo y puede que la tasa de fracaso sea del 95%. Esto es, que de cada 100 personas que intentan hacer trading, solo 5 conseguiran cubrir costes y un porcentaje menor ganar algo. Y no solo depende de lo buena que sea la técnica utilizada, el 80% de los factores van a ser ajenos al dominio de una técnica de trading determinada. De hecho, no me pongo medallas porque no enseño mis técnicas gratuitamente nunca. Tengo por ahí una mención a un seminario de 5 días de trading por 12000 euros por si alguien tiene curiosidad por saber qué principios y técnicas utilizo.

Lo que voy a mostrar ahora es una implementación personalizada de un proveedor de datos virtual; que es más sencillo de programar que un canal de comunicación con mi broker, de forma que puedo hacer simulaciones y estudios exactamente igual que si estuviera conectado, pero con mucho menos esfuerzo. Con bastante esfuerzo, pero con mucho menos.

A estas alturas ya funciona razonablemente bien, es un borrador del final, pero ya tiene suficiente complejidad para que valga la pena como ejemplo. Me he programado un broker virtual también, porque me gusta la modularidad. Este broker puede recibir datos de distintas fuentes. Para ello, uso una interfazclase abstracta (depende del lenguaje) que define unos métodos y propiedades iguales, sea cual sea la fuente de datos.

Una de las fuentes de datos que utilizo son ficheros csv en formato tick (date, ask, bid). Estos los he convertido en OHLC para Ask y OHLC para bid en marcos temporales de 1-minuto y 5-minutos. Para mí es suficiente, pero si alguien quiere más granularidad y exactitud, puede usar directamente los datos ask-bid, o hacer un muestreo a 30-segundos, 10-segundos, etc. para los datos tick. Para que se entienda mejor, tengo:

  1. Un único csv con todos los datos OHLC en el marco temporal de 5-minutos entre 2003 y 2019. Este fichero se utiliza para dibujar la gráfica OHLC, entrenar un modelo estadístico o lo que sea.
  2. Un fichero por año de datos OHLC en el marco temporal de 1-minuto. Este fichero se utiliza para extraer los ticks que tendrán lugar hasta que la siguiente barra OHLC se tenga que dibujar. Son como los datos que nos van llegando del broker en tiempo real y muestran cómo va cambiando el precio del instrumento.

Como observación, estuve decidiendo si utilizar datos tick ASK/BID, pero me pareció que tanta precisión no era necesaria y que iba a utilizar un generador de ruído para calcular el spread mediante un generador de números aleatorios con una distribución T de student o bien promediando High-Low de mi muestra histórica. Pero eso es otra historia. Recordemos que compramos a precio Ask, pero vendemos a precio Bid. La comisión por operación también tiene que tenerse en cuenta, por ejemplo.

Vamos a plantear el sistema:

Datos históricos en 5 minutos
(última fila, núm. filas=n)
(AMD = año-mes-día)

Datos en «tiempo real» en 1 minuto
(todos los ticks desde que cierra la vela histórica hasta que abre la siguiente).
Date Open High Low Close
AMD 08:00:00 On Hn Ln Cn

Tengamos en cuenta que la vela de 8:00 en el TF de 5m contiene todos los precios entre las 8:00 y las 8H 4' 59''.

Por lo que las señales en tiempo real tienen que empezar a las 8H 5' 00''.

Date Open High Low Close
AMD 08:05:00  O1 H1  L1  C
AMD 08:06:00 O2 H2  L2  C
AMD 08:07:00 O3 H3  L3  C
AMD 08:08:00 O4 H4  L4  C
08:09:00 O5 H5  L5  C

Empecemos con el código:

# Como está orientado a csv, entonces no voy a incluir información sobre conexiones mediante sockets, puertos, ejecutables, etc...
# Definimos una clase abstracta que sirva de interfaz para el resto que vengan.
 
class Provider_abstract():
 
    def __init__(self):
        raise NotImplementedError
 
    def getRTPricesNext(self):
        raise NotImplementedError
 
    def getHistPricesNext(self):
        raise NotImplementedError
 
    def setTickDataPath(self):
        raise NotImplementedError
 
    def setHistoricalFile(self):
        raise NotImplementedError

Los métodos tendrán las siguientes funciones:

  • getRTPricesNext: Devuelve el dataframe con los ticks.
  • getHistPricesNext: Devuelve un histórico de precios.
  • setTickDataPath: El directorio donde están los datos tick.
  • setHistoricalFile: La ruta al fichero con los datos históricos.

Y ahora procedemos a la implementación, veamos el código completo primero.

from gym_gspfx.gspProviders.provider import Provider_abstract
import pandas as pd
 
# Reads TF data and generates tick data from the same or other data source.
# For instance, reads 5m data nd generates ticks from 1m data.
 
class Provider(Provider_abstract):
 
    def __init__(self, historical_file, tick_data_path, start_row=0, number_of_samples=300, \
                 tick_file_prefix="EURUSD_1M_"):
 
        self.min_year=None
        self.max_year=None
        self.__historical_file = ""
        self.__tick_data_path = ""
        self.setHistoricalFile(historical_file)
        self.setTickDataPath(tick_data_path)
        self.current_start_row_index = None
        self.number_of_samples = number_of_samples
        self.next_future_start_row = start_row
        self.last_closed_row = None
        self.next_future_row_to_deliver_index = 0
        self.next_two_future_rows_to_deliver = None
        self.__tick_data_files = {"EMPTY": None}
        self.tick_file_prefix=tick_file_prefix
 
 
    def setHistoricalFile(self,fileName):
        self.__historical_file = pd.read_csv(fileName, parse_dates=["date"]).set_index("date")
 
    def setTickDataPath(self, tick_data_path):
        self.__tick_data_path=tick_data_path
 
 
    def getHistPricesNext(self):
        self.current_start_row_index = self.next_future_row_to_deliver_index
        self.next_future_row_to_deliver_index += 1
 
        __df = self.__historical_file.iloc[self.current_start_row_index:self.current_start_row_index + self.number_of_samples]
 
        self.last_closed_row = __df.tail(1)
 
        self.next_two_future_rows_to_deliver = pd.DataFrame( \
            self.__historical_file.iloc[self.current_start_row_index + self.number_of_samples:\
                                        self.next_future_row_to_deliver_index + self.number_of_samples + 1])
        return __df.copy()
 
 
    def getRTPricesNext(self):
 
        if self.last_closed_row is None:
            return None
 
        if "EMPTY" in self.__tick_data_files.keys() and self.__historical_file.shape[0]>0:
            del self.__tick_data_files["EMPTY"]
 
        self.min_year = self.last_closed_row.index.year.values[0]
        self.max_year = self.next_two_future_rows_to_deliver.head(1).index.year.values[0]
 
        if self.max_year not in self.__tick_data_files.keys():
            self.__tick_data_files[self.max_year] = \
                pd.read_csv(self.__tick_data_path + "EURUSD_1M_" + str(self.max_year) + ".csv", parse_dates=["date"])
 
            if self.min_year in self.__tick_data_files.keys() and self.min_year != self.max_year:
                del self.__tick_data_files[self.min_year]
 
        initial_date = self.next_two_future_rows_to_deliver.head(1).index.values[0]
        end_date = self.next_two_future_rows_to_deliver.tail(1).index.values[0]
 
        __df = self.__tick_data_files[self.max_year]
        __df = __df[(__df["date"] >= initial_date) & (__df["date"] < end_date)]
 
        return __df.copy()

El método __init__ se encarga de parametrizar los valores iniciales de la clase, como la fila del histórico desde el que se comenzará a extraer datos, el número de filas del histórico que se extraerán. También se han de definir al instanciar la clase la ruta completa al fichero de históricos y la carpeta donde están los ficheros de ticks.

Los ficheros de ticks los guardo en un diccionario conforme los necesito por motivos prácticos. He optado por usar ficheros de 1m, pero podría tener otra estructura y he preferido prever la posibilidad de tener que utilizar ficheros de tick múltiples. Todo ello se define en el constructor, junto con atributos que serán utilizados por la clase.

 def __init__(self, historical_file, tick_data_path, start_row=0, number_of_samples=300, \
                 tick_file_prefix="EURUSD_1M_"):
 
        self.min_year=None
        self.max_year=None
        self.__historical_file = ""
        self.__tick_data_path = ""
        self.setHistoricalFile(historical_file)
        self.setTickDataPath(tick_data_path)
        self.current_start_row_index = None
        self.number_of_samples = number_of_samples
        self.next_future_start_row = start_row
        self.last_closed_row = None
        self.next_future_row_to_deliver_index = 0
        self.next_two_future_rows_to_deliver = None
        self.__tick_data_files = {"EMPTY": None}
        self.tick_file_prefix=tick_file_prefix

Tras ello, los métodos que cargan el fichero de históricos y configuran la ruta de los ficheros de ticks.
Nótese que utilizo la librería de pandas y que asumo que los datos vienen ya ordenados por fecha, ascendente.

    def setHistoricalFile(self,fileName):
        self.__historical_file = pd.read_csv(fileName, parse_dates=["date"]).set_index("date")
 
   def setTickDataPath(self, tick_data_path):
       self.__tick_data_path=tick_data_path

Cada vez que queremos avanzar 1 vela, tenemos que llamara a este procedimiento.
El procedimiento devuelve un DataFrame con el número de filas indicado en el constructor o modificado en el atributo number_of_samples.
Cada vez que se le llama, avanza 1 fila.

    def getHistPricesNext(self):
        self.current_start_row_index = self.next_future_row_to_deliver_index
        self.next_future_row_to_deliver_index += 1
 
        __df = self.__historical_file.iloc[self.current_start_row_index:self.current_start_row_index + self.number_of_samples]
 
        self.last_closed_row = __df.tail(1)
 
        self.next_two_future_rows_to_deliver = pd.DataFrame( \
            self.__historical_file.iloc[self.current_start_row_index + self.number_of_samples:\
                                        self.next_future_row_to_deliver_index + self.number_of_samples + 1])
        return __df.copy()

Por útlimo, tenemos el método que nos devuelve todos los ticks que se producirán desde el cierre de la última vela de los históricos hasta justo antes de la apertura del siguiente.

   def getRTPricesNext(self):
 
        if self.last_closed_row is None:
            return None
 
        if "EMPTY" in self.__tick_data_files.keys() and self.__historical_file.shape[0]>0:
            del self.__tick_data_files["EMPTY"]
 
        self.min_year = self.last_closed_row.index.year.values[0]
        self.max_year = self.next_two_future_rows_to_deliver.head(1).index.year.values[0]
 
        if self.max_year not in self.__tick_data_files.keys():
            self.__tick_data_files[self.max_year] = \
                pd.read_csv(self.__tick_data_path + "EURUSD_1M_" + str(self.max_year) + ".csv", parse_dates=["date"])
 
            if self.min_year in self.__tick_data_files.keys() and self.min_year != self.max_year:
                del self.__tick_data_files[self.min_year]
 
        initial_date = self.next_two_future_rows_to_deliver.head(1).index.values[0]
        end_date = self.next_two_future_rows_to_deliver.tail(1).index.values[0]
 
        __df = self.__tick_data_files[self.max_year]
        __df = __df[(__df["date"] >= initial_date) & (__df["date"] < end_date)]
 
        return __df.copy()

Estudiando el código anterior se puede hacer una idea de cómo realizar un proveedor de ticks y datos financieros personalizado, así como adaptarlo a las necesidades propias. Una aplicación puede ser un simulador de trading por ejemplo. Yo lo uso para mis proyectos de inteligencia artificial, de manera que no necesito estar conectado a ningún proveedor de precios online para simular series temporales y mercados financieros.

En la siguiente imagen se muestra cómo usar la clase creada con un ejemplo real.

Generador de datos históricos y tick en Python

Y si volvemos a ejecutar los métodos, obtenemos la vela siguiente con sus históricos y los ticks correspondientes.

Si alguien no conoce la librería Pandas y no entiende alguna de las expresiones del código, se pueden consultar en la documentación oficial, en el siguiente enlace: http://pandas.pydata.org/pandas-docs/stable/

Y preguntados:

  • ¿Cómo puede obtenerse algo negociando si no es desarrollando una técnica para acertar las suficientes veces como para cubrir los costes de los fallos?
  • ¿Cómo puede mantenerse un negocio sin conocer el riesgo asociado y una buena gestión financiera y de riesgos conforme?
  • ¿cómo podría un cerebro humano ser mejor calculando aspectos técnicos y numéricos que una compleja computadora?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.