El mundo de las criptomonedas es inmenso y existen distintas formas de obtener un beneficio en este mundo. Entre ellas, se encuentra la compra y venta de estos activos digitales. Igual que en los mercados tradicionales hay una gran variedad de estrategias, pero este proyecto está centrado en el arbitraje de criptomonedas.
El arbitraje es una técnica que consiste en aprovechar las diferencias de precios entre distintos mercados para obtener un beneficio.
Por ejemplo, supongamos que existen tres activos A, B y C y los mercados A/B, A/C y B/C que permiten operar entre estos activos. Nuestra cartera inicialmente tiene 1 unidad del activo A y ninguna de los demás. Ahora podemos vender 1 unidad de A en el mercado A/B y obtenemos 2 unidades del activo B. A continuación, las condiciones del mercado permiten intercambiar estas 2 unidades del activo B por 4 unidades del activo C utilizando el mercado B/C. Finalmente, podemos comprar 2 unidades del activo A con las 4 unidades del activo C a través del mercado A/C. Evidentemente este caso es poco probable ya que hemos duplicado nuestra cantidad del activo inicial, pero sirve para ilustrar el funcionamiento del arbitraje.
Los mercados financieros y de criptomonedas son muy rápidos y es prácticamente imposible realizar esta estrategia sin el uso de una herramienta automatizada que detecte estas oportunidades y las ejecute. Por tanto, el objetivo de este proyecto es el desarrollo de una aplicación que identifique las oportunidades de arbitraje y las ejecute.
El funcionamiento de la aplicación se puede resumir en los siguientes pasos:
Esta fase es crucial para el correcto funcionamiento de la aplicación. Si los precios no se actualizan correctamente o se realiza de una forma ineficiente los resultados no serán válidos.
El primer paso es determinar cómo guardar los distintos precios de los activos. La solución más simple y adecuada para este problema es utiliar un grafo dirigido en el cual cada nodo representa un activo y las aristas representan los mercados. Además, el peso de cada arista representa la cantidad que se puede obtener al realizar el intercambio con una unidad del activo origen.
Para aprovechar al máximo las diferencias de precio entre mercados se tienen que combinar distintos exchanges. Este planteamiento no supone ningún problema a la estructura de datos porqué cada nodo es el activo dentro de un exchange. Así, para cada activo y exchange se genera un nodo del grafo.
Tampoco es viable intentar construir este grafo de manera manual, ya que es posible que los mercados dejen de estar disponibles. Es necesario construirlo de manera automática a partir de las APIs de cada uno de los exchanges. Estas APIs permiten obtener las restricciones de cada uno de los mercados (cantidad mínima para operar o números de decimales permitidos) y facilitan la creación del grafo comentado anteriormente.
Una vez se tiene esta estructura de datos ya se pueden consultar los precios. Como se ha comentado, estos mercados son muy rápidos e interesa trabajar siempre con la última información disponible. Por tanto, el uso del protocolo HTTP para la obtención de los precios no es una opción viable y es necesario utilizar técnicas que permitan el intercambio de datos en tiempo real, como los WebSockets. El uso de WebSockets no es ningún problema porqué los propios exchanges proporcionan esta información. Así, para cada exchange se inicializa una conexión con su servidor de WebSockets y se escuchan los cambios a los mercados que interesan, es decir, aquellos que se encuentran dentro del grafo.
Una vez se tienen los precios de cada uno de los mercados se pueden buscar las oportunidades de arbitraje y determinar si son rentables o no.
Para determinar si un camino es rentable se puede simular la cantidad final que se obtendría tras ejecutar todas las operaciones del camino. Si el resultado final es superior al inicial, el camino es rentable y se puede ejecutar. Ahora bien, también se deben tener en cuenta las comisiones de cada una de las casas de cambio y las restricciones de cada uno de los mercados:
Una primera aproximación consiste en generar todos los caminos que empiezan y terminan en un nodo del mismo activo, permitiendo que sean de distinto exchange.
Esta opción funciona y es fácil de implementar pero tiene un problema de rendimiento, ya que cada vez que se produce un cambio en el precio de un mercado se está explorando todo el grafo. Y aunque el grafo solo tenga unos 20 nodos se están explorando caminos que no han cambiado, y por tanto, si no eran rentables antes, tampoco lo serán ahora.
Una opción para intentar reducir el impacto de este proceso puede consistir en no explorar el grafo a cada actualización de los mercados, sino hacerlo de manera periódica. Esta solución tampoco es la más adecuada porqué es posible que cuando se inicie el proceso de exploración los precios ya estén obsoletos.
La solución óptima al problema consiste en explorar solo aquellos caminos que potencialmente han cambiado, es decir, aquellos que contienen el mercado que ha cambiado de precio. Para ello se tendrán que:
Así cuando se recibe una actualización de un mercado se marcan todos aquellos caminos vinculados con el mercado como pendientes de revisar.
El proceso para realizar una operación es simple, consiste en realizar la petición a la API respetando los distintos campos, entre ellos una firma que solo puede generar el usuario con la clave privada de su cuenta. También se incluyen otros campos como la fecha actual para evitar realizar operaciones que se han enviado hace algunos segundos y no han llegado a tiempo al servidor final.
Utilizando la abstracción de los lenguages de programación orientados a objetos este proceso es muy simple, ya que se tiene una clase que representa un mercado, una operación y una casa de cambio. Así realizar una operación consiste en crear un objeto operación a partir de una determinada casa de cambio.
Un punto a favor del arbitraje es que es una técnica perfecta si se consigue ejecutar todas las operaciones al mismo tiempo e immediatamente. Ahora bien, esto en el mundo real es imposible por diversos factores, pero el principal es la latencia entre el servidor de la casa de cambio y la máquina que ejecuta la aplicación.
Desde el momento que el servidor del exchange envía la información de los precios de los mercados y esta información llega a la máquina, se realizan los cálculos y se ejecutan las operaciones pasan unos milisegundos, pero es posible que al final de todo el camino la información de los precios ya no sea válida. Como reflexión, los únicos que pueden ejecutar sin riesgo esta técnica es el propio exchange, ya que ellos podrían inyectar esta lógica del arbitraje en la ejecución de las órdenes, antes que la información sea pública para todo el mundo.
Para aumentar la probabilidad de éxito de las operaciones es necesario ejecutar las operaciones en paralelo, todas al mismo tiempo. En el caso de la aplicación desarrollada se han utilizado unos procesos auxiliares o workers que solo realizan la función de realizar una petición HTTP. Al ser procesos independientes del sistema operativo realmente se pueden aprovechar los distintos núcleos de las máquinas modernas.
En la sección anterior se ha comentado que las operaciones deben realizarse siempre en paralelo, pero esto introduce un nuevo problema. ¿Qué pasa si no se dispone de un activo porqué se obtiene en la operación anterior?
La solución más fácil a este problema es disponer de activos "en reserva" de los distintos criptoactivos. Evidentmente este hecho ya introduce un riesgo adicional a la técnica, que es la revalorización de esto criptoactivos en reserva.
{ "allowSequentialSequences": false, "orderMaxAmount": "25.00", "minProfit": "0.05", "enabledCurrencies": [ "USDT", "USDC", "BUSD", "BTC", "LTC", "ETH" ], "multipleSequences": true, "maximumSequences": 2, "timeBetweenSequences": 1, "maxDepth": 4 }
La aplicación desarrollada tiene opciones de configuración que permite activar o desactivar este tipo de operaciones. Por ejemplo, se puede configurar la aplicación para solo ejecutar aquellas operaciones que se puedan ejecutar en paralelo o se puede configurar para realizar operaciones que incluyan operaciones secuenciales. En este último modo, si no se dispone de un activo se realizarán todas las operaciones posibles en paralelo y aquellas que no se pueda, se ejecutarán secuencialmente.
El desarrollo de esta aplicación ha sido un reto interesante porqué se han combinado conocimientos de distintas áreas. Desde el uso adecuado de estructuras de datos para crear programas eficientes y el uso de APIs externas para obtener información.
Algunos lenguajes de programación son más adecuados para determinadas funciones, ya que algunos ofrecen más rendimiento que otros. En este caso, se desarrollaron dos prototipos de la aplicación:
Sería interesante desarrollar una versión utilizando un lenguaje de más bajo nivel como C++ y realizar comparaciones entre las distintas versiones, aunque todo apunta a que el cuello de botella de la aplicación es la propia distancia entre el servidor origen y la máquina que ejecuta la aplicación. Y este problema aumenta cuando se combinan distintas casas de cambio porqué no es posible ubicar la aplicación en el mismo datacenter que la casa de cambio.