Predicción del precio de Bitcoin con machine learning y Python

Predicción del precio de Bitcoin con Python, cuando el pasado no se repite

Javier Escobar Ortiz
Marzo, 2022

Introducción


Una serie temporal (time series) es una sucesión de datos ordenados cronológicamente y espaciados a intervalos iguales o desiguales. El proceso de forecasting consiste en predecir el valor futuro de una serie temporal, bien modelando la serie únicamente en función de su comportamiento pasado o empleando otras variables adicionales.

En términos generales, al crear un modelo de forecasting se utilizan datos históricos con el objetivo de obtener una representación matemática capaz de predecir futuros valores. Esta idea se fundamenta sobre una asunción muy importante, el comportamiento futuro de un fenómeno se puede explicar a partir de su comportamiento pasado. Sin embargo, esto raramente ocurre en la relaidad, o al menos, no en su totalidad. Para profundizar en esto, vease la siguiente definición:

$Forecast = patrones + varianza\;no\;explicada$

El primer término de la ecuación hace referencia a todo aquello que tiene un carácter repetitivo a lo largo del tiempo (tendencia, estacionalidad, factores cíclicos...). El segundo término, representa todo aquello que influye en la variable respuesta pero que no está recogido (explicado) por el pasado de la serie temporal.

Cuanto mayor importancia tenga el primer término respecto al segundo, mayor será la probabilidad de exito al tratar de crear modelos de forecasting de tipo autoregresivo. A medida que el segundo término adquiere peso, se hace necesario incorporar al modelo variables adicionales (si es que existen) que ayuden a explicar el comportamiento observado.

Realizar un buen estudio del fenómeno modelado y saber reconocer en qué medida su comportamiento puede explicarse gracias a su pasado, puede ahorrar muchos esfuerzos inecesarios.

En este documento se muestra un ejemplo de cómo identificar situaciones en las que el proceso de forecasting autorregresivo no consigue resultados útiles. Como ejemplo, se intenta predecir el precio de cierre diario de Bitcoin utlizando métodos de machine learning. Se hace uso de Skforecast, una sencilla librería de Python que permite, entre otras cosas, adaptar cualquier regresor de Scikit-learn a problemas de forecasting.

Caso de uso


Bitcoin (₿) es una criptomoneda descentralizada que puede enviarse de un usuario a otro mediante la red bitcoin peer-to-peer sin necesidad de intermediarios. Las transacciones son verificadas y registradas en un libro de contabilidad público distribuido llamado blockchain. Los Bitcoins se crean como recompensa por un proceso conocido como minería y pueden intercambiarse por otras monedas, productos y servicios.

Aunque puedan existir diversas opiniones sobre Bitcoin, bien como un activo especulativo de alto riesgo o, por otro lado, como una reserva de valor, es innegable que este se ha convertido en uno de los activos financieros más valiosos a nivel mundial. La página web Infinite Market Cap muestra un listado de todos los activos financieros ordenados según su capitalización de mercado y, Bitcoin, a fecha de este artículo, se encuentra en el top 10 cerca de empresas mundialmente conocidas como Tesla o, incluso, de la plata, un valor refugio globalmente aceptado. El creciente interés en Bitcoin, y el mundo de las criptomonedas en general, por parte de los inversores lo convierte en un fenómeno interesante de modelar.

Se pretende generar un modelo de forecasting capaz de predecir el precio de Bitcoin. Se dispone de una serie temporal con los precios de apertura (Open), cierre (Close), máximo (High) y mínimo (Low) de Bitcoin en dólares estadounidenses (USD) desde el 2013-04-28 al 2022-01-01.

Librerias

In [1]:
# Tratamiento de datos
# ==============================================================================
import pandas as pd
import numpy as np
import datetime
from cryptocmd import CmcScraper

# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
%matplotlib inline
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
import seaborn as sns
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
plt.style.use('ggplot')

# Colores Bitcoin
# ==============================================================================
palette_btc = {'naranja': '#f7931a',
               'blanco' : '#ffffff',
               'gris'   : '#4d4d4d',
               'azul'   : '#0d579b',
               'verde'  : '#329239'
              }

# Modelado y Forecasting
# ==============================================================================
from lightgbm import LGBMRegressor

from sklearn.metrics import mean_absolute_error

from skforecast.ForecasterAutoreg import ForecasterAutoreg
from skforecast.model_selection import backtesting_forecaster

Datos


La descarga de datos se realiza mediante cryptocmd. Esta librería es útil para descargar datos históricos de criptomonedas de la página Coinmarketcap. La información de cada columna es:

  • Date: fecha del registro.

  • Open: precio de apertura, precio al que cotiza un activo, en este caso el Bitcoin, en el comienzo del día. Expresado en dólares estadounidenses (USD).

  • High: precio máximo del día, precio más alto alcanzado por el Bitcoin en ese día, (USD).

  • Low: precio mínimo del día, precio más bajo alcanzado por el Bitcoin en ese día, (USD).

  • Close: precio de cierre, precio al que cotiza el Bitcoin a la finalización del día, (USD).

  • Volume: volumen, suma de las operaciones reales realizadas durante el día, (USD).

  • Market Cap: capitalización de mercado, es el valor total de todas las acciones de una empresa o, en el caso de Bitcoin u otra criptomoneda, de todas las monedas que hay en circulación, (USD).

Nota: el mercado de las criptomonedas es un mercado ininterrumpido, opera las 24 horas del día, los 7 días de la semana. De todas maneras, no es estrictamente necesario que el precio close coincida con el precio open del día siguiente debido a las fluctuaciones que pueda sufrir el valor de Bitcoin, o cualquier criptomoneda, durante el último segundo del día.

In [2]:
# Descarga de datos
# ==============================================================================

# Se inicializa el Scraper, se incluye simbolo, inicio y fin de la descarga
scraper = CmcScraper('BTC', '28-04-2013', '01-01-2022')

# Transformar datos recogidos en un dataframe
data = scraper.get_dataframe()
data.sort_values(by='Date', ascending=True, inplace=True)

pd.set_option('display.max_columns', None)
display(data)
pd.reset_option('display.max_columns')
Date Open High Low Close Volume Market Cap
3170 2013-04-28 135.300003 135.979996 132.100006 134.210007 0.000000e+00 1.488567e+09
3169 2013-04-29 134.444000 147.488007 134.000000 144.539993 0.000000e+00 1.603769e+09
3168 2013-04-30 144.000000 146.929993 134.050003 139.000000 0.000000e+00 1.542813e+09
3167 2013-05-01 139.000000 139.889999 107.720001 116.989998 0.000000e+00 1.298955e+09
3166 2013-05-02 116.379997 125.599998 92.281898 105.209999 0.000000e+00 1.168517e+09
... ... ... ... ... ... ... ...
4 2021-12-28 50679.859377 50679.859377 47414.209925 47588.854777 3.343038e+10 9.000762e+11
3 2021-12-29 47623.870463 48119.740950 46201.494371 46444.710491 3.004923e+10 8.784788e+11
2 2021-12-30 46490.606049 47879.965500 46060.313166 47178.125843 2.668649e+10 8.923863e+11
1 2021-12-31 47169.372859 48472.527490 45819.954553 46306.446123 3.697417e+10 8.759394e+11
0 2022-01-01 46311.744663 47827.310995 46288.486095 47686.811509 2.458267e+10 9.021042e+11

3171 rows × 7 columns

In [3]:
# Preparación del dato
# ==============================================================================
data['date'] = pd.to_datetime(data['Date'], format='%Y-%m-%d %H:%M:%S')
data = data.loc[:, ['date', 'Open', 'Close', 'High', 'Low']]
data = data.rename({'Open': 'open', 'Close': 'close', 'High': 'high', 'Low': 'low'}, 
                    axis=1)
data = data.set_index('date')
data = data.asfreq('D')
data = data.sort_index()

Al establecer una frecuencia con el método asfreq(), Pandas completa los huecos que puedan existir en la serie temporal con el valor de Null con el fin de asegurar la frecuencia indicada, ejemplo. Por ello, se debe comprobar si han aparecido missing values tras esta transformación.

In [4]:
print(f'Número de filas con missing values: {data.isnull().any(axis=1).mean()}')
Número de filas con missing values: 0.0

Halving del Bitcoin como variable exógena


El Halving es un evento programado y forma parte del diseño y funcionamiento de algunas criptomonedas. Los mineros se dedican a validar los bloques de transacciones de la red, en este caso Bitcoin, y, cada vez que lo logran, reciben como recompensa una cantidad de esa moneda digital. Esta cantidad es fija pero solo durante un tiempo.

En la blockchain de Bitcoin, cada vez que se añaden 210.000 bloques ocurre el cambio de recompensa. Este hecho, denominado como halving, se produce aproximadamente cada 4 años y reduce a la mitad las monedas que reciben los mineros.

En la historia de Bitcoin han existido 3 halvings. Cuando se lanzó la minería de Bitcoin, los mineros recibían 50 BTC al extraer con éxito un bloque. En 2012 esta recompensa se redujo a 25 BTC, en 2016 bajó a 12,5 BTC, y en 2020 a 6,25 BTC, después del tercer halving. Por lo general, cada halving ha tenido un impacto en el precio aunque no necesariamente ha sido en el corto plazo.

Se pretende utilizar los días restantes para el próximo halving y sus recompensas de minado como variables exógenas para predecir el precio de Bitcoin. Se calcula que el próximo halving ocurrirá aproximadamente en 2024 aunque se desconoce su fecha exacta. Para estimarla, se toman los bloques restantes a fecha de 2022-01-14 de la página web Coinmarketcap, 121.400, y se utiliza el promedio de los bloques de la red Bitcoin minados por día, 144 (tiempo de bloque promedio $\approx$ 10 minutos).

Nota: Al incorporar datos predichos como una variable exógena, se introduce, dado que se trata de predicciones, su error en el modelo de forecasting.

In [5]:
# Dict con la info de los halvings del Bitcoin
# ==============================================================================
btc_halving = {'halving'              : [0, 1 , 2, 3, 4],
               'date'                 : ['2009-01-03', '2012-11-28', 
                                         '2016-07-09', '2020-05-11', np.nan],
               'reward'               : [50, 25, 12.5, 6.25, 3.125],
               'halving_block_number' : [0, 210000, 420000 ,630000, 840000]
              }
In [6]:
# Cálculo siguiente halving
# Se toma como base de partida los bloques restantes según la web 
# coinmarketcap.com para el próximo halving a fecha de 2022-01-14
# ==============================================================================
bloques_restantes = 121400
bloques_por_dia = 144

dias = bloques_restantes / bloques_por_dia

next_halving = pd.to_datetime('2022-01-14', format='%Y-%m-%d') + datetime.timedelta(days=dias)
next_halving = next_halving.replace(microsecond=0, second=0, minute=0, hour=0)
next_halving = next_halving.strftime('%Y-%m-%d')

btc_halving['date'][-1] = next_halving

print(f'El próximo halving ocurrirá aproximadamente el: {next_halving}')
EL próximo halving ocurrirá aproximadamente el: 2024-05-06
In [7]:
# Incluir recompensas y cuenta regresiva para próximo halving en el dataset
# ==============================================================================
data['reward'] = np.nan
data['countdown_halving'] = np.nan

for i in range(len(btc_halving['halving'])-1):
     
    # Fecha inicial y final de cada halving
    if btc_halving['date'][i] < data.index.min().strftime('%Y-%m-%d'):
        start_date = data.index.min().strftime('%Y-%m-%d')
    else:
        start_date = btc_halving['date'][i]
        
    end_date = btc_halving['date'][i+1]
    mask = (data.index >= start_date) & (data.index < end_date)
        
    # Rellenar columna 'reward' con las recompensas de minería
    data.loc[mask, 'reward'] = btc_halving['reward'][i]
    
    # Rellenar columna 'countdown_halving' con los días restantes
    time_to_next_halving = pd.to_datetime(end_date) - pd.to_datetime(start_date)
    
    data.loc[mask, 'countdown_halving'] = np.arange(time_to_next_halving.days)[::-1][:mask.sum()]
In [8]:
# Comprobar que se han creado los datos correctamente
# ==============================================================================
print('Segundo halving:', btc_halving['date'][2])
display(data.loc['2016-07-08':'2016-07-09'])
print('')
print('Tercer halving:', btc_halving['date'][3])
display(data.loc['2020-05-10':'2020-05-11'])
print('')
print('Próximo halving:', btc_halving['date'][4])
data.tail(2)
Segundo halving: 2016-07-09
open close high low reward countdown_halving
date
2016-07-08 640.687988 666.523010 666.706970 636.466980 25.0 0.0
2016-07-09 666.383972 650.960022 666.383972 633.398987 12.5 1401.0
Tercer halving: 2020-05-11
open close high low reward countdown_halving
date
2020-05-10 9591.169231 8756.431142 9595.580629 8395.107451 12.50 0.0
2020-05-11 8755.535639 8601.796202 9033.471176 8374.322975 6.25 1455.0
Próximo halving: 2024-05-06
Out[8]:
open close high low reward countdown_halving
date
2021-12-31 47169.372859 46306.446123 48472.527490 45819.954553 6.25 856.0
2022-01-01 46311.744663 47686.811509 47827.310995 46288.486095 6.25 855.0

Exploración gráfica


Cuando se quiere generar un modelo de forecasting, es importante representar los valores de la serie temporal. Esto permite identificar patrones tales como tendencias y estacionalidad.

Gráfico de velas

Un gráfico de velas japonesas es un tipo de gráfico muy utilizado en el mundo del análisis técnico. El cuerpo de la vela indica la variación entre el precio de apertura y cierre, para un periodo determinado, mientras que los pelos o sombras indican los valores mínimo y máximo alcanzados durante ese periodo.

Construcción de una vela japonesa. La primera barra es un gráfico de barra, el segundo una vela japonesa al alza y la última una vela japonesa a la baja. Fuente Wikipedia.
In [9]:
# Gráfico de velas japonesas interactivo con Plotly
# ==============================================================================
candlestick = go.Candlestick(
                    x     = data.index,
                    open  = data.open,
                    close = data.close,
                    low   = data.low,
                    high  = data.high,
                    )

fig = go.Figure(data=[candlestick])

fig.update_layout(
    width       = 900,
    height      = 450,
    title       = dict(text='<b>Chart Bitcoin/USD</b>', font=dict(size=30)),
    yaxis_title = dict(text='Precio (USD)', font=dict(size=15)),
    margin      = dict(l=10, r=20, t=80, b=20),
    shapes      = [dict(x0=btc_halving['date'][2], x1=btc_halving['date'][2], 
                        y0=0, y1=1, xref='x', yref='paper', line_width=2),
                   dict(x0=btc_halving['date'][3], x1=btc_halving['date'][3], 
                        y0=0, y1=1, xref='x', yref='paper', line_width=2),
                   dict(x0=btc_halving['date'][4], x1=btc_halving['date'][4], 
                        y0=0, y1=1, xref='x', yref='paper', line_width=2)
                  ],
    annotations = [dict(x=btc_halving['date'][2], y=1, xref='x', yref='paper',
                      showarrow=False, xanchor='left', text='Segundo halving'),
                   dict(x=btc_halving['date'][3], y=1, xref='x', yref='paper',
                      showarrow=False, xanchor='left', text='Tercer halving'),
                   dict(x=btc_halving['date'][4], y=1, xref='x', yref='paper',
                      showarrow=False, xanchor='left', text='Cuarto halving')
                  ],
    xaxis_rangeslider_visible = False,
)

fig.show()