Crear un robot de comercio de Bitcoin que no pierda dinero

El autor:- ¿ Por qué?, Creado: 2023-02-01 11:52:21, Actualizado: 2023-09-18 19:40:25

img

Crear un robot de comercio de Bitcoin que no pierda dinero

Usemos el aprendizaje de refuerzo en IA para construir un robot de comercio de moneda digital.

En este artículo, crearemos y aplicaremos un número de marco de aprendizaje mejorado para aprender a hacer un robot de negociación de Bitcoin.

Muchas gracias por el software de código abierto proporcionado por OpenAI y DeepMind para los investigadores de aprendizaje profundo en los últimos años.

img

Entrenamiento AlphaStar:https://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/

Aunque no vamos a crear nada impresionante, todavía no es fácil comerciar robots Bitcoin en transacciones diarias.

No hay valor en nada que sea demasiado simple.

Por lo tanto, no sólo debemos aprender a comerciar nosotros mismos, sino también dejar que los robots comercien por nosotros.

El plan

img

  1. Crear un entorno de gimnasio para que nuestro robot realice aprendizaje automático

  2. Render un entorno visual simple y elegante

  3. Entrenamos a nuestro robot para que aprenda una estrategia comercial rentable.

Si no estás familiarizado con cómo crear entornos de gimnasio desde cero, o cómo simplemente renderizar la visualización de estos entornos. Antes de continuar, no dudes en buscar en Google un artículo de este tipo. Estas dos acciones no serán difíciles incluso para los programadores más jóvenes.

Cómo empezar

En este tutorial, usaremos el conjunto de datos de Kaggle generado por Zielak. Si desea descargar el código fuente, se proporcionará en mi repositorio de Github, junto con el archivo de datos.csv.

Primero, importemos todas las bibliotecas necesarias. Asegúrate de usar pip para instalar las bibliotecas que te faltan.

import gym
import pandas as pd
import numpy as np
from gym import spaces
from sklearn import preprocessing

A continuación, vamos a crear nuestra clase para el entorno. Necesitamos pasar en un número de marco de datos de Pandas y un opcional initial_balance y un lookback_window_size, que indicará el número de pasos de tiempo pasado observado por el robot en cada paso. Defaultamos la comisión de cada transacción a 0.075%, es decir, el tipo de cambio actual de Bitmex, y por defecto el parámetro de serie a falso, lo que significa que nuestro número de marco de datos será atravesado por fragmentos aleatorios por defecto.

También llamamos dropna() y reset_index() en los datos, primero borrar la fila con valor NaN, y luego restablecer el índice del número de fotogramas, porque hemos borrado los datos.

class BitcoinTradingEnv(gym.Env):
  """A Bitcoin trading environment for OpenAI gym"""
  metadata = {'render.modes': ['live', 'file', 'none']}
  scaler = preprocessing.MinMaxScaler()
  viewer = None
def __init__(self, df, lookback_window_size=50, 
                         commission=0.00075,  
                         initial_balance=10000
                         serial=False):
    super(BitcoinTradingEnv, self).__init__()
self.df = df.dropna().reset_index()
    self.lookback_window_size = lookback_window_size
    self.initial_balance = initial_balance
    self.commission = commission
    self.serial = serial
# Actions of the format Buy 1/10, Sell 3/10, Hold, etc.
    self.action_space = spaces.MultiDiscrete([3, 10])
# Observes the OHCLV values, net worth, and trade history
    self.observation_space = spaces.Box(low=0, high=1, shape=(10, lookback_window_size + 1), dtype=np.float16)

Nuestro action_space está representado como un grupo de 3 opciones (comprar, vender o mantener) aquí y otro grupo de 10 cantidades (1/10, 2/10, 3/10, etc.). Cuando elegimos comprar, vamos a comprar cantidad * auto.balance palabra de BTC. Para vender, vamos a vender cantidad * auto.btc_held valor de BTC. Por supuesto, el mantenimiento ignorará la cantidad y no hará nada.

Nuestro observation_space se define como un punto flotante continuo establecido entre 0 y 1, y su forma es (10, lookback_window_size+1). + 1 se utiliza para calcular el paso de tiempo actual. Para cada paso de tiempo en la ventana, observaremos el valor OHCLV. Nuestro patrimonio neto es igual al número de BTC que compramos o vendemos, y la cantidad total de dólares que gastamos o recibimos en estos BTC.

A continuación, tenemos que escribir el método de restablecimiento para iniciar el entorno.

def reset(self):
  self.balance = self.initial_balance
  self.net_worth = self.initial_balance
  self.btc_held = 0
self._reset_session()
self.account_history = np.repeat([
    [self.net_worth],
    [0],
    [0],
    [0],
    [0]
  ], self.lookback_window_size + 1, axis=1)
self.trades = []
return self._next_observation()

Aquí usamos self._reset_session y self._next_observation, que aún no hemos definido. Vamos a definirlos primero.

Sesión comercial

img

Una parte importante de nuestro entorno es el concepto de sesiones comerciales. Si desplegamos este robot fuera del mercado, es posible que nunca lo ejecutemos por más de unos meses a la vez. Por esta razón, limitaremos el número de fotogramas consecutivos en self.df, que es el número de fotogramas que nuestro robot puede ver a la vez.

En nuestro método de _reset_session, primero restablecemos el current_step a 0. Luego, estableceremos steps_left a un número aleatorio entre 1 y MAX_TRADING_SESSIONS, que definiremos en la parte superior del programa.

MAX_TRADING_SESSION = 100000 # ~2 months

A continuación, si queremos recorrer el número de fotogramas consecutivamente, debemos establecerlo para recorrer el número completo de fotogramas, de lo contrario fijamos frame_start en un punto aleatorio en self.df y creamos un nuevo marco de datos llamado active_df, que es solo una porción de self.df y está pasando de frame_start a frame_start + steps_left.

def _reset_session(self):
  self.current_step = 0
if self.serial:
    self.steps_left = len(self.df) - self.lookback_window_size - 1
    self.frame_start = self.lookback_window_size
  else:
    self.steps_left = np.random.randint(1, MAX_TRADING_SESSION)
    self.frame_start = np.random.randint(self.lookback_window_size, len(self.df) - self.steps_left)
self.active_df = self.df[self.frame_start - self.lookback_window_size:self.frame_start + self.steps_left]

Un efecto secundario importante de recorrer el número de marcos de datos en la rebanada aleatoria es que nuestro robot tendrá más datos únicos para su uso en el entrenamiento a largo plazo. Por ejemplo, si solo recorremos el número de marcos de datos de manera seria (es decir, de 0 a len(df)), solo tendremos tantos puntos de datos únicos como el número de marcos de datos. Nuestro espacio de observación solo puede usar un número discreto de estados en cada paso de tiempo.

Sin embargo, atravesando las rebanadas del conjunto de datos al azar, podemos crear un conjunto más significativo de resultados comerciales para cada paso de tiempo en el conjunto de datos inicial, es decir, la combinación de comportamiento comercial y comportamiento de precios visto anteriormente para crear conjuntos de datos más únicos.

Cuando el paso de tiempo después de restablecer el entorno serial es 10, nuestro robot siempre se ejecutará en el conjunto de datos al mismo tiempo, y hay tres opciones después de cada paso de tiempo: comprar, vender o mantener. Para cada una de las tres opciones, necesita otra opción: 10%, 20%,... o 100% de la cantidad de implementación específica. Esto significa que nuestro robot puede encontrar uno de los 10 estados de cualquier 103, un total de 1030 casos.

Ahora volvamos a nuestro entorno de corte aleatorio. Cuando el paso de tiempo es 10, nuestro robot puede estar en cualquier paso de tiempo len(df) dentro del número de marcos de datos. Suponiendo que se haga la misma elección después de cada paso de tiempo, significa que el robot puede experimentar el estado único de cualquier len(df) a la potencia 30 en los mismos 10 pasos de tiempo.

Aunque esto puede traer un ruido considerable a los grandes conjuntos de datos, creo que se debe permitir que los robots aprendan más de nuestros datos limitados. Seguiremos recorriendo nuestros datos de prueba de manera seria para obtener los datos más frescos y aparentemente en tiempo real, con el fin de obtener una comprensión más precisa a través de la efectividad del algoritmo.

Observado con los ojos de un robot

A través de una observación visual eficaz del entorno, a menudo es útil entender el tipo de funciones que nuestro robot usará.

Observación del entorno de visualización OpenCV

Cada línea en la imagen representa una fila en nuestro observation_space. Las primeras cuatro líneas de líneas rojas con frecuencias similares representan datos OHCL, y los puntos naranja y amarillo directamente debajo representan el volumen de operaciones. La barra azul fluctuante a continuación representa el valor neto del robot, mientras que la barra más ligera a continuación representa la transacción del robot.

Si observa con cuidado, incluso puede hacer un mapa de velas usted mismo. Debajo de la barra de volumen de negociación hay una interfaz de código Morse, que muestra el historial de negociación. Parece que nuestro robot debería ser capaz de aprender lo suficiente de los datos en nuestro observation_space, así que continuemos. Aquí definiremos el método de _next_observation, escalamos los datos observados de 0 a 1.

  • Es importante ampliar únicamente los datos observados por el robot hasta ahora para evitar la desviación de conducción.
def _next_observation(self):
  end = self.current_step + self.lookback_window_size + 1
obs = np.array([
    self.active_df['Open'].values[self.current_step:end],  
    self.active_df['High'].values[self.current_step:end],
    self.active_df['Low'].values[self.current_step:end],
    self.active_df['Close'].values[self.current_step:end],
    self.active_df['Volume_(BTC)'].values[self.current_step:end],])
scaled_history = self.scaler.fit_transform(self.account_history)
obs = np.append(obs, scaled_history[:, -(self.lookback_window_size + 1):], axis=0)
return obs

Haga algo

Hemos establecido nuestro espacio de observación, y ahora es el momento de escribir nuestra función de escalera, y luego tomar la acción programada del robot. Siempre que self.steps_left == 0 para nuestra sesión de negociación actual, venderemos nuestro BTC y llamaremos _reset_session(). De lo contrario, estableceremos la recompensa al valor neto actual. Si nos quedamos sin fondos, estableceremos hecho a True.

def step(self, action):
  current_price = self._get_current_price() + 0.01
  self._take_action(action, current_price)
  self.steps_left -= 1
  self.current_step += 1
if self.steps_left == 0:
    self.balance += self.btc_held * current_price
    self.btc_held = 0
    self._reset_session()
obs = self._next_observation()
  reward = self.net_worth
  done = self.net_worth <= 0
return obs, reward, done, {}

Tomar acción comercial es tan simple como obtener el precio actual, determinar las acciones a ejecutar y la cantidad a comprar o vender.

def _take_action(self, action, current_price):
  action_type = action[0]
  amount = action[1] / 10
btc_bought = 0
  btc_sold = 0
  cost = 0
  sales = 0
if action_type < 1:
    btc_bought = self.balance / current_price * amount
    cost = btc_bought * current_price * (1 + self.commission)
    self.btc_held += btc_bought
    self.balance -= cost
elif action_type < 2:
    btc_sold = self.btc_held * amount
    sales = btc_sold * current_price  * (1 - self.commission)
    self.btc_held -= btc_sold
    self.balance += sales

Por último, en el mismo método, vamos a unir la transacción a auto.trades y actualizar nuestro valor neto y el historial de la cuenta.

if btc_sold > 0 or btc_bought > 0:
    self.trades.append({
      'step': self.frame_start+self.current_step,
      'amount': btc_sold if btc_sold > 0 else btc_bought,
      'total': sales if btc_sold > 0 else cost,
      'type': "sell" if btc_sold > 0 else "buy"
    })
self.net_worth = self.balance + self.btc_held * current_price
  self.account_history = np.append(self.account_history, [
    [self.net_worth],
    [btc_bought],
    [cost],
    [btc_sold],
    [sales]
  ], axis=1)

Nuestro robot puede comenzar un nuevo entorno ahora, completar el entorno gradualmente, y tomar acciones que afectan el medio ambiente.

Mira nuestro comercio de robots.

Nuestro método de representación puede ser tan simple como llamar a imprimir (self.net_word), pero no es lo suficientemente interesante. En su lugar, vamos a dibujar un simple gráfico de velas, que contiene un gráfico separado de la columna de volumen de negociación y nuestro patrimonio neto.

Vamos a conseguir el código enStockTrackingGraph.pyPuedes obtener el código de mi Github.

El primer cambio que debemos hacer es actualizar self.df ['Date '] a self.df [Timestamp] y eliminar todas las llamadas a date2num, porque nuestra fecha ya está en formato de timestamp unix.

from datetime import datetime

Primero, importa la biblioteca de fecha y hora, y luego usaremos el método utcfromtimestamp para obtener la cadena UTC de cada marca de tiempo y strftime para que se formate como una cadena: formato Y-m-d H: M.

date_labels = np.array([datetime.utcfromtimestamp(x).strftime('%Y-%m-%d %H:%M') for x in self.df['Timestamp'].values[step_range]])

Finalmente, cambiaremos self. df['Volume '] a self. df[Volume_ (BTC) ] para que coincida con nuestro conjunto de datos. Después de completar estos, estamos listos. Volviendo a nuestro BitcoinTradingEnv, podemos escribir métodos de representación para mostrar el gráfico ahora.

def render(self, mode='human', **kwargs):
  if mode == 'human':
    if self.viewer == None:
      self.viewer = BitcoinTradingGraph(self.df,
                                        kwargs.get('title', None))
self.viewer.render(self.frame_start + self.current_step,
                       self.net_worth,
                       self.trades,
                       window_size=self.lookback_window_size)

Podemos ver a nuestros robots comerciar Bitcoin ahora.

Visualice nuestro robot negociando con Matplotlib

La etiqueta fantasma verde representa la compra de BTC, y la etiqueta fantasma roja representa la venta. La etiqueta blanca en la esquina superior derecha es el valor neto actual del robot, y la etiqueta en la esquina inferior derecha es el precio actual de Bitcoin. Es simple y elegante. Ahora, es hora de entrenar a nuestros robots y ver cuánto dinero podemos ganar!

Tiempo de formación

Una de las críticas que recibí en el artículo anterior fue la falta de validación cruzada y la falta de división de los datos en conjuntos de entrenamiento y conjuntos de pruebas. El propósito de esto es probar la exactitud del modelo final en nuevos datos que nunca se han visto antes. Aunque este no es el foco de ese artículo, es realmente muy importante.

Por ejemplo, una forma común de validación cruzada se llama validación k-fold. En esta validación, divides los datos en k grupos iguales, uno por uno, individualmente, como el grupo de prueba y usas el resto de los datos como el grupo de entrenamiento. Sin embargo, los datos de series temporales son altamente dependientes del tiempo, lo que significa que los datos posteriores son altamente dependientes de los datos anteriores.

Cuando se aplica a los datos de series temporales, el mismo defecto se aplica a la mayoría de las otras estrategias de validación cruzada. Por lo tanto, solo necesitamos usar una parte del número de marco de datos completo como el conjunto de entrenamiento desde el número de marco a algunos índices arbitrarios, y usar el resto de los datos como el conjunto de prueba.

slice_point = int(len(df) - 100000)
train_df = df[:slice_point]
test_df = df[slice_point:]

A continuación, dado que nuestro entorno está configurado para manejar un solo número de marcos de datos, crearemos dos entornos, uno para los datos de entrenamiento y otro para los datos de prueba.

train_env = DummyVecEnv([lambda: BitcoinTradingEnv(train_df, commission=0, serial=False)])
test_env = DummyVecEnv([lambda: BitcoinTradingEnv(test_df, commission=0, serial=True)])

Entrenar a nuestro modelo es tan simple como crear un robot usando nuestro entorno y llamarlo modelo.aprender.

model = PPO2(MlpPolicy,
             train_env,
             verbose=1, 
             tensorboard_log="./tensorboard/")
model.learn(total_timesteps=50000)

Aquí, usamos placas tensoriales, por lo que podemos visualizar nuestros diagramas de flujo tensorial fácilmente y ver algunos indicadores cuantitativos sobre nuestro robot.

img

¡Guau, parece que nuestro robot es muy rentable! ¡Nuestro mejor robot puede incluso lograr el equilibrio 1000x en 200.000 pasos, y el resto aumentará al menos 30 veces en promedio!

En ese momento, me di cuenta de que había un error en el entorno... después de corregir el error, este es el nuevo gráfico de recompensas:

img

Como se puede ver, algunos de nuestros robots están haciendo bien, mientras que otros están en quiebra. Sin embargo, los robots con buen rendimiento pueden alcanzar 10 veces o incluso 60 veces el saldo inicial como máximo. Debo admitir que todas las máquinas rentables son entrenadas y probadas sin comisión, por lo que es poco realista que nuestros robots ganen dinero real. Pero al menos encontramos la manera!

Probemos nuestros robots en el entorno de prueba (usando nuevos datos que nunca antes han visto) para ver cómo se comportarán.

img

Nuestros robots bien entrenados se van a la quiebra al intercambiar nuevos datos de prueba

Obviamente, todavía tenemos mucho trabajo por hacer. Simplemente cambiando los modelos para usar A2C con una línea base estable en lugar del robot PPO2 actual, podemos mejorar enormemente nuestro rendimiento en este conjunto de datos. Finalmente, según la sugerencia de Sean OGorman, podemos actualizar nuestra función de recompensa ligeramente, para que podamos agregar recompensa al patrimonio neto, en lugar de solo realizar un alto patrimonio neto y quedarse allí.

reward = self.net_worth - prev_net_worth

Estos dos cambios por sí solos pueden mejorar el rendimiento del conjunto de datos de prueba en gran medida, y como pueden ver a continuación, finalmente pudimos beneficiarnos de nuevos datos que no estaban disponibles en el conjunto de entrenamiento.

img

Pero podemos hacerlo mejor. Para mejorar estos resultados, necesitamos optimizar nuestros súper parámetros y entrenar a nuestros robots por más tiempo. ¡Es hora de que la GPU comience a trabajar y disparar a todos los cilindros!

Hasta ahora, este artículo ha sido un poco largo, y todavía tenemos muchos detalles que considerar, por lo que planeamos tomar un descanso aquí.

Conclusión

En este artículo, comenzamos a usar el aprendizaje por refuerzo para crear un robot de negociación rentable de Bitcoin desde cero.

  1. Crea un entorno de comercio de Bitcoin desde cero usando el gimnasio de OpenAI.

  2. Utilice Matplotlib para construir la visualización del entorno.

  3. Utilice una simple validación cruzada para entrenar y probar a nuestro robot.

  4. Ajustar nuestros robots ligeramente para obtener ganancias.

Aunque nuestro robot de negociación no fue tan rentable como habíamos esperado, ya estamos avanzando en la dirección correcta. La próxima vez, nos aseguraremos de que nuestros robots puedan vencer constantemente al mercado. Veremos cómo nuestros robots de negociación procesan datos en tiempo real. Por favor, siga mi próximo artículo y Viva Bitcoin!


Relacionados

Más.