Anteriormente en este tutorial estábamos tratando de predecir el precio del Bitcoin del día siguiente y luego usar esa predicción para tomar decisiones financieras. Existen dos problemas con este método. El primero es que la data es heterogénea, al principio los precios son bajos y la volatilidad es alta; luego los precios son altos y la volatilidad es ligeramente menor. Segundo, nos interesan los retornos; queremos saber si el precio del día siguiente va a ser mayor o menor, no el valor exacto (a menos que estuviéramos comprando/vendiendo opciones). Es por eso que vamos a transformar la data a retornos (cambios de precio).
Tal vez te estés preguntando “¿Por qué no hicimos esto desde el principio?”. Por que el fin de este tutorial no es solo aprender el cómo si no también el por qué de algunas transformaciones o del uso de algunos parámetros. Si no entendemos el por qué, no podremos obtener mejores resultados a la hora de usar éstas herramientas en otros casos.
En esta parte y la próxima sólo voy a explicar las secciones de código que sean significativamente diferentes a las de las partes anteriores. Recuerda que todo el código utilizado está en mi repositorio de github.
Para hacer las transformaciones voy a utilizar unas nuevas líneas de código al principio, justo después de eliminar las primeras 800 filas, ese valor ahora va a ser 100.
El otro cambio que haré es cambiar la cantidad de observaciones que serán utilizadas para hacer las predicciones de 20 a 12.
Obs = 12
Al principio utilizamos el error medio porcentual como parámetro a minimizar ya que antes nos interesaba la menor diferencia porcentual posible entre el precio y las predicciones. Ahora que la data está transformada, no necesitamos minimizar el error medio porcentual si no el error cuadrático medio.
En la parte 3 dije que que explicaría como elegir un optimizador en la parte 5 así que ahí vamos. Este tema puede ser muy técnico así que puedes saltarlo si quieres no confundirte o si prefieres entenderlo luego por tu cuenta, pero es muy importante a la hora de querer implementarlo en un proyecto real.
La optimización de una función de minimización se hace utilizando el descenso de gradiente estocástico (Stochastic Gradient Descent en inglés), el modelo se moverá por pasos en ciertas direcciones tratando de conseguir los valores que van a minimizar la función. En el próximo gráfico imagina que las zonas rojas son los peores valores (malas predicciones utilizando esos valores) y las zonas azules son las que tienen los mejores valores predictivos. El optimizador es una serie de reglas (en qué dirección y que tanto moverse) para que el modelo pueda conseguir las zonas azules.
Algunos optimizadores van a conseguir la mejor zona azul (llamada óptimo global) y algunos conseguirán alguna zona azul pero no la mejor (llamada óptimo local, por ejemplo el camino que lleva al lado derecho del gráfico). Algunos optimizadores conseguirán una zona azul luego de mucho tiempo (horas) y otros lo harán muy rápidamente (minutos). Para aprender más sobre los optimizadores puedes chequear este buen post (en inglés): An overview of gradient descent optimization algorithms.
Ahora que sabemos un poco más sobre optimizadores ¿Cuál deberíamos utilizar para llegar al óptimo global lo más rápido posible?. Esto depende mucho de la data y la función a optimizar, algunos optimizadores funcionan mejor con reconocimiento de imágenes, otros con traducción de texto, etc… Por suerte para ti, probé todos los optimizadores con 1200 epochs, un batch size de 50, output_dim de 20 y el resto de los parámetros en default. Para cada optimizador guardé los erores por epoch para visualizar como avanza la minimización de la función cada vez que analiza la data. Mientras menor sean los errores (loss), mejor.
Vamos a revisar los errores con un único algoritmo para empezar, en este caso el mismo que usamos anteriormente: “rmsprop”.
En el eje Y tenemos los errores (loss) del modelo, eso es lo que queremos minimizar; en el eje X tenemos los epochs. Como podemos ver, a mayor cantidad de epochs, los errores son menores, pero también el modelo toma más en entrenarse. Con mi hardware actual cada epoch toma 1 segundo, eso son 20 minutos para 1200 epochs (¡Utilizando CUDA!). Nos interesa usar el optimizador que pueda conseguir la menor cantidad de errores utilizando la menor cantidad de epochs (y por ende tiempo). A continuación un gráfico con los errores de todos los optimizadores.
Observando la gráfica “nadam” (rosado) parece ser el optimizador más rapido para nuestra data. Vamos a hacer zoom en las últimas observaciones para comprobar cual tiene el menor nivel de errores.
Sin temor a dudas dudas podemos decir que “nadam” (rosado) es el mejor y el más rápido. “Adam” (marrón) fue el segundo y “rmsprop” (naranja) fue el tercero.
Ahora tenemos todos los parámetros listos:
- Obs = 12
- loss = “mse”
- optimizer = “nadam”
- output_dim = 20
- epochs = 1200 (puede ser menor)
- batch_size = 50 (igual que antes)
Batch_size, output_dim y otros parámetros pueden ser optimizados un poco más pero eso tomaría mas tiempo y aumentaría las probabilidades de incurrir en overfitting. Vamos a proceder a entrenar el modelo y esperar a que esté listo.
Las métricas para para probar las predicciones también cambian. Vamos a hacer lo siguiente para los retornos y las predicciones: Si los valores son mayores a 3%, vamos a llamarlos “Higher” (mayor), si los valores están entre 3% y -3% los llamaremos “Neutral” y si los valores están por debajo de 3% los llamaremos “Lower” (menor).
Es hora de visualizar las métricas, con este loop veremos cuántas veces el algoritmo predijo el movimiento del precio (Higher, Neutral, Lower).
Mi output es el siguiente:
% of Total Correct: 88.8
Éste valor es bastante alto, lo cual es bueno. Lo próximo que haremos es visualizar la matriz de confusión, ya que algunos errores tienen mayor peso que otros. Por ejemplo, si el modelo predice “Higher” y el movimiento del precio del día siguiente es “Lower”, estamos en problemas. Algunos errores son menos importantes que otros, por ejemplo si el movimiento del precio del día siguiente es “Lower” o “Higher” y el modelo predijo “Neutral”, perdimos una oportunidad, pero no tomamos una decisión tan negativa como el caso anterior. Generaremos ganancias cuando el modelo prediga “Higher” o “Lower” y ese sea el movimiento del precio del día siguiente ya que habremos tomado la decisión correcta antes de un movimiento importante (+/- 3%).
Para visualizar la matriz de confusión primero importamos una función de SKLearn y luego la usamos con dos listas de retornos y predicciones.
Here’s the confusion_matrix:
[[105 0 22]
[ 0 81 5]
[ 20 9 258]]
Algunas personas no entenderán el output así que tomaré prestada una función del sitio web de SKLearn para graficar de una mejor manera el resultado.
Para ejecutarla corremos el siguiente código:
Para entender este gráfico vamos fila por fila. En la primera fila los números son 105, 0 y 22, en ese orden. Eso significa que el número de “Higher” en la data es 105+0+22=127 y de esos 127, el modelo predijo 105 de ellos como “Higher”, 0 “Lower” y 22 como “Same” (Neutral). Para la segunda fila, la cantidad de “Lower” en la data es 0+81+5=86 y de esos 86 el modelo predijo 0 como “Higher” (Excelente!), 81 como “Lower” y 5 como “Same”. Para la última fila el número de “Same” en la data es 20+9+258=287 y de esos 287 el modelo predijo 20 de ellos como “Higher, 9 como “Lower” y 258 (Casi todos) como “Same”.
Ahora vamos de vuelta al análisis anterior, el modelo NUNCA predice “Lower” cuando el movimiento de precio es “Higher” o viceversa. También logramos predecir correctamente muchos “Higher” o “Lower”; deberíamos poder lograr ganar dinero utilizando este modelo.
Vamos a escribir una estrategia rápida: si el modelo predice un movimiento mayor a 3% (Higher), compramos, y si predice -3% (Lower), vendemos. Aquí está la función, llamada ret_pred por “returns predictor” (predictor de retornos).
Voy a correr la función utilizando 3% como dije anteriormente para que coincida con los valores de ejemplo de la matriz de confusión pero se puede correr con otros valores.
ret_pred(3.0, 3.0)
Strategy vs BuyNHold: 90641.3858858
Es un valor bastante alto, vamos a chequear el gráfico:
Y con algo de zoom en los últimos meses:
Ahora veamos el resto de los gráficos como en la parte 4.
Strategy vs buy and hold en términos nominales.
Strategy vs buy and hold términos porcentuales.
Comparar la volatilidad (30 días) de ambos portafolios.
La estrategia funcinó mejor que Buy and Hold durante todo el ejercicio. Respecto a la volatilidad, fue menor que en casos anteriores. Puedes jugar con otros parámeros para ver diferentes resultados.
Todavía tenemos algunos problemas: no hemos eliminado el look-ahead bias y no estamos incluyendo costos de trading. El resultado del modelo es tan bueno que probablemente está adaptándose muy bien a la data (overfitting). Trataremos de resolver todo esto en la parte final.
Parte 6
Bienvenido a la parte final de este tutorial, aquí vamos a aprender como hacer pruebas reales de estrategias de inversión y eso significa dos cosas: re-entrenar el modelo y agregar costos de trading. Antes de proceder debo advertir que esta parte del tutorial puede ser computacionalmente intensa y puede tomar mucho tiempo dependiendo del hardware.
Como dije anteriormente, en esta parte solo explicaré las secciones de código que sean significativamente diferentes del resto. El código completo utilizado para cada paso puede se encuentra en mi github.
Anteriormente en este tutorial hablé del look-ahead bias, eso significa utilizar data para entrenar un modelo y que luego el modelo tenga que predecir estos mismos valores (que ya vió). Para evitar esto vamos a entrenar el modelo cada N días y luego probar el testing N días hacia adelante. Mientras menor sea N, tendremos que re-entrenar más veces el modelo. Para este ejericio voy a re-entrenar el modelo cada 50 días, para los últimos 200 días, lo que da un total de 4 modelos (o el mismo modelo entrenado 4 veces). Los parámetros que vamos a utilizar son buenos pero no los mejores que he encontrado, así que te invito a experimentar con distintos parámetros para seguir mejorando.
Luego de abrir, editar y transformar “btc.csv” como en las partes previas de este tutorial vamos a correr un loop que entrenará el modelo 4 veces. Lo que esta parte del código hará es lo siguiente: segmentar el dataframe eliminando las últimas 50, 100, 150 y 200 observaciones para cada modelo. Con cada segmento el modelo será entrenado de mayor a menor (todo la data menos las últimas 50 observaciones, luego todo el modelo menos las últimas 100 observaciones y así). Cada modelo será guardado de la siguiente forma: Model_(número).json para los parámetros y Model_(número)_weights.h5 para los resultados. Luego de correr el código los 4 modelos serán entrenados. Si estás usando pycharm u otro IDE, los modelos serán guardados en el directorio del proyecto. Si no, recuerda agregar los directorios completos de donde guardar los archivos.
La última vez utilicé 12 observaciones en batches de 50, error medio cuadrático y el optimizador “nadam”. Para remover algo de overfitting y ahorrar tiempo utilizaré 600 epocs por modelo. El código anterior tomará unos 50 minutos en estar listo (12 minutos por modelo incluyendo 2 minutos de descanso entre ellos) usando CUDA con una GTX 1060; sin CUDA deberías ser paciente por que tomará mucho más tiempo.
Luego de que los modelos están listos podemos cerrar la consola y abrir una nueva. Ahora en la nueva cónsola vamos a cargar todos los modelos y empezar a predecir secuencialmente. Imaginemos que esta es nuestra data:
El primer modelo va a predecir los primeros valores (no los usaremos para probar la estrategia) y los siguientes 50 (predicción sin look-ahead bias).
El segundo modelo va a predecir los primeros valores, los próximos 50 (igual que el modelo anterior) y los sub-siguientes 50 (predicción sin look-ahead bias). Y así para los otros dos modelos. Esto significa que entrenamos cada modelo hasta cierto punto y luego lo utilizamos para predecir los siguientes 50 días.
Abrimos, editamos y transformamos “btc.csv” como siempre y corremos el siguiente código para cargar todos los modelos en orden y guardarlos en una lista llamada “loaded_models”. Si previamente guardaste los modelos con diferentes nombres o en diferentes directorios recuerda cambiarlos aquí.
Con la siguiente pieza de código vamos a predecir los valores del primer modelo y luego guardarlos en una lista llamada Predictions.
Ahora es momento de hacer las predicciones con el resto de los modelos, este loop va a realizar las predicciones para cada modelo para los 50 días siguientes a partir de la última observación utilizada para entrenar el modelo.
El output debería ser:
2 2551 2601 50
3 2601 2651 50
4 2651 2701 50
Este output significa que el segundo modelo fue usado para hacer las predicciones del día 2551 a 2601, el modelo 3 para los días 2601 hasta 2651 y el modelo 4 para los días 2651 al 2701. El primer modelo fue entrenado a parte ya que contiene los primeros valores (1 a 2501) los cuales no serán utilizados para probar la estrategia.
El dataframe para las pruebas va a ser sólo de 200 observaciones (50 x 4 modelos) ya que éstas son las que no tienen look-ahead bias.
dff = df[-200:]
Para visualizar la matriz de confusión y el % de predicciones correctas usaremos el mismo código de la parte 5, aquí está mi output:
% Total Correct: 44.0
[[24 12 24]
[13 20 18]
[27 18 44]]
Y aquí utilizando la función de SKLearn:
Para un modelo (o serie de modelos) sin look-ahead bias, esto es bastante bueno, predice correctamente una alta cantidad de movimientos grandes (20+24) comparado con los movimientos grandes que predice incorrectamente (12+13) y un buen porcentaje de neutrales.
Luego de que los 4 modelos realicen todas sus predicciones, procedemos a agregar los costos de trading. Para esto solo necesitamos realizar unos cambios dentro de la función de la estrategia de inversión. Los costos que usaremos son 0.5% para todas las transacciones, tanto compra como venta. A un precio de $10.000 por bitcoin, esto se traduce en $50 por transacción, o 100$ por compra y venta. El resto de la función es el mismo, solo cambiando lo siguiente:
Ahora vamos a ejecutar la función para ver los resultados. Como siempre utilizaremos 3%, pero pueden utilizarse otros valores.
ret_pred(3.0, 3.0)
Strategy vs BuyNHold: 29715.2779994
¡El modelo sin look-ahead bias está por encima de buy and hold! Veamos ahora el gráfico:
Nada mal, ahora a chequear la estrategia en términos porcentuales:
Los primeros meses es mejor que buy and hold, luego cae ligeramente por algunas semanas (antes de los últimos topes). Después de que el precio de bitcoin llega a su tope en diciembre, el performance comparado con buy and hold empieza a mejorar bastante por el resto del ejercicio. Cabe destacar que el algoritmo no sabe que el precio va a caer, esa es la magia de un modelo non-bias, aprendió de eventos anteriores en los cuales el precio subió y luego cayó.
En conclusión, durante un mercado alcista le fue tan bien como buy and hold (muy difícil que le vaya mejor), durante un mercado bajista le fue mejor y durante un mercado lateral (sin grandes aumentos o caídas) le fue más o menos igual. Es necesario notar que esta estrategia sólo estuvo por debajo de buy and hold brevemente, cosa que es impresionante considerando la volatilidad y la cantidad de mercados alcistas y bajistas que ha tenido el precio del bitcoin.
Para finalizar haré un último test: probar la estrategia con diferentes puntos de entrada y de salida, a parte de 3%. Vamos a ver que tal nos va con todos los valores del 0% al 7%.
Strategy vs BuyNHold: 15568.4576103
Strategy vs BuyNHold: 14864.1219979
Strategy vs BuyNHold: 2275.9673744
Strategy vs BuyNHold: 29965.230983
Strategy vs BuyNHold: 29715.2779994
Strategy vs BuyNHold: 19623.5128092
Strategy vs BuyNHold: 16705.205473
Strategy vs BuyNHold: 22206.5157544
Bueno, esto son buenas noticias. No solo el modelo es mejor que buy and hold utilizando 3%, también le gana a buy and hold con cualquier valor de entrada y salida utilizado (el mejor fue 4%). Esto significa que el modelo es bastante bueno para predecir los grandes movimientos del día siguiente. Recuerda que el performance puede ser mejorado con acceso a crédito, tomando posiciones cortas, invirtiendo el dinero sin usar en bonos o índices, entre otras cosas.
Para comentarios o preguntas, escríbeme en twitter: @jsusran, aquí está todo el código utilizado: Bitcoin Deep Learning. No olvides agregarme en LinkedIn. Contáctame para cualquier comentario, recomendación, dudas o si quieres crear tu propia estrategia de trading usando redes neuronales ¡Me encantaría ver los resultados!