Neste artigo, vamos criar e aplicar um algoritmo de aprendizagem de reforço para aprender como fazer um robô de negociação de Bitcoin. Neste tutorial, vamos usar o gym da OpenAI e o robô PPO do banco de dados stable-baselines, um ramo do banco de dados baseado na OpenAI.
Muito obrigado ao OpenAI e ao DeepMind pelo software de código aberto que eles forneceram aos pesquisadores de aprendizado profundo nos últimos anos. Se você ainda não viu os incríveis feitos que eles fizeram com tecnologias como AlphaGo, OpenAI Five e AlphaStar, você pode ter vivido fora do isolamento durante o ano passado, mas você também deve ir vê-los.
Formação AlphaStarhttps://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/
Embora não tenhamos criado nada de impressionante, a negociação de robôs Bitcoin no dia-a-dia ainda não é fácil.
Por isso, não só devemos aprender a negociar sozinhos... mas também deixar os robôs negociarem por nós.
1.为我们的机器人创建gym环境以供其进行机器学习
2.渲染一个简单而优雅的可视化环境
3.训练我们的机器人,使其学习一个可获利的交易策略
Se você ainda não está familiarizado com a criação de ambientes de ginástica a partir do zero, ou com a simples visualização de renderização desses ambientes. Antes de continuar, vá ao google para um artigo como este.
在本教程中,我们将使用Zielak生成的Kaggle数据集。如果您想下载源代码,我的Github仓库中会提供,同时也有.csv数据文件。好的,让我们开始吧。
Primeiro, vamos importar todas as bibliotecas necessárias.
import gym
import pandas as pd
import numpy as np
from gym import spaces
from sklearn import preprocessing
Em seguida, vamos criar nossas classes para o ambiente. Precisamos passar um ponto de dados de pandas, além de um inicial_balance opcional e um lookback_window_size, que indicará o número de passos de tempo observados pelo robô em cada passo. Vamos definir a comissão por transação como o padrão de 0.075%, ou seja, a taxa de câmbio atual do Bitmex, e definir o parâmetro da sequência como o padrão de false, o que significa que, por padrão, nosso ponto de dados será percorrido em fragmentos aleatórios.
Também chamamos os dados dropna e reset_index, removendo primeiro as linhas com o valor NaN e colocando novamente o índice do número de bits, já que removemos os dados.
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)
O nosso action_space aqui é representado como um conjunto de 3 opções (comprar, vender ou manter) e outro grupo de 10 quantidades (/1/10, 2/10, 3/10 etc.). Quando escolhemos a ação de comprar, nós vamos comprar * self.balance worth of BTC.
Nosso observation_space é definido como um conjunto de pontos flutuantes contínuos entre 0 e 1 e tem a forma de ((10, lookback_window_size + 1)); + 1 é usado para calcular o tempo atual. Para cada tempo na janela, vamos observar o valor do OHCLV. Nosso valor líquido é igual ao número de BTC comprados ou vendidos e ao total de dólares que gastamos ou recebemos nesses BTC.
Em seguida, precisamos de escrever um método de reset para inicializar o ambiente.
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()
Aqui usamos self._reset_session e self._next_observation, que ainda não definimos.
我们环境的一个重要部分是交易会话的概念。如果我们将这个机器人部署到市场外,我们可能永远不会一次运行它超过几个月。出于这个原因,我们将限制self.df中连续帧数的数量,也就是我们的机器人连续一次能看到的帧数。
Em nosso método_reset_session, primeiro reiniciamos o current_step para 0; em seguida, configuramos o steps_left para um número aleatório entre 1 e MAX_TRADING_SESSION, que definimos no topo do programa.
MAX_TRADING_SESSION = 100000 # ~2个月
Em seguida, se quisermos percorrer o número de bits continuamente, temos que configurá-lo para percorrer todo o número de bits, caso contrário, configuramos o frame_start como um ponto aleatório no self.df e criamos um novo número de bits de dados chamado active_df, que é apenas um pedaço do self.df e é derivado do frame_start para 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]
Um importante efeito colateral de percorrer o número de bits de dados em pedaços aleatórios é que nossos robôs terão mais dados únicos para usar em treinamentos de longa duração. Por exemplo, se apenas percorrermos o número de bits de dados de forma serial (ou seja, na sequência de 0 a len (df)), teremos apenas o único ponto de dados que é tão grande quanto o número de bits de dados. Nosso espaço de observação pode até mesmo ter apenas um número discreto de estados em cada passo de tempo.
No entanto, ao percorrermos o conjunto de dados de forma aleatória, podemos criar um conjunto de resultados de transações mais significativos para cada passo do tempo no conjunto de dados inicial, ou seja, uma combinação de comportamento de negociação e comportamento de preço visto anteriormente para produzir conjuntos de dados mais exclusivos.
Com um tempo de tempo de 10 passos após a reinicialização do ambiente de sequência, nosso robô sempre irá executar simultaneamente no conjunto de dados e terá três opções após cada passo de tempo: comprar, vender ou manter. Para cada uma dessas três opções, uma outra opção é necessária: 10%, 20%,... ou 100% de praticidade específica. Isso significa que nosso robô pode encontrar qualquer um dos 10 estados em 103, um total de 1030 situações.
Voltando agora ao ambiente do nosso corte aleatório. Quando o passo de tempo é 10, nosso robô pode estar em qualquer passo de tempo len (df) dentro do número de bits de dados. Supondo que a mesma escolha seja feita após cada passo de tempo, significa que o robô pode experimentar um único estado de 30 segundos de qualquer passo de tempo len (df) no mesmo passo de tempo 10.
Embora isso possa causar bastante barulho para grandes conjuntos de dados, acredito que o robô deve ser permitido aprender mais com nosso volume limitado de dados. Ainda vamos percorrer nossos dados de teste de forma serializada para obter dados atualizados, que parecem estar em tempo real, com a esperança de obter uma compreensão mais precisa da eficácia dos algoritmos.
A observação de um ambiente visual eficaz é geralmente útil para entender o tipo de função que nosso robô vai usar. Por exemplo, aqui está a visualização de um espaço observável usando a ressonância OpenCV.
Observações do ambiente de visualização do OpenCV
Cada linha na imagem representa uma linha no nosso observation_space. As primeiras 4 linhas de frequência semelhante em vermelho representam dados OHCL, e os pontos laranja e amarelo abaixo representam transações completas. As barras azuis flutuantes abaixo representam o valor líquido do robô e as barras mais leves abaixo representam transações do robô.
Se você observar atentamente, você pode até fazer um gráfico por si mesmo. Abaixo da barra de volume de transações há uma interface semelhante a um código Morse, que mostra o histórico das transações. Parece que nosso robô deve ser capaz de aprender o suficiente com os dados do nosso observation_space, então vamos continuar.
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
Nós já criamos nosso espaço de observação e agora é hora de escrever nossa função de escada e tomar as ações previstas pelo robô. Sempre que estivermos no momento da transação, nós venderemos o BTC que temos e chamaremos _reset_session ().
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 uma ação de negociação é tão simples como obter o preço atual, determinar as ações a serem executadas, bem como o número de compras ou vendas.
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
最后,在同一方法中,我们会将交易附加到self.trades并更新我们的净值和账户历史。
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)
Os nossos robôs agora podem iniciar um novo ambiente, completá-lo gradualmente e tomar medidas que afetem o ambiente.
Nosso método de renderização pode ser tão simples quanto chamar print ((self.net_worth), mas isso não é divertido o suficiente. Em vez disso, vamos desenhar um gráfico simples, que contém gráficos separados de volume de transações e nosso patrimônio líquido.
我们将从我上一篇文章中获取StockTradingGraph.py中的代码,并重新设计它以适应比特币环境。你可以从我的Github中获取代码。
A primeira mudança que faremos é atualizar o self.df [
from datetime import datetime
Primeiro, importamos o datetime, e então usamos o método utcfromtimestamp para obter a string UTC de cada time stamp e strftime, tornando-a uma string do 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, nós alteramos o self.df [
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)
Agora podemos ver nossos robôs negociando bitcoins.
Visualizar as nossas transações com o Matplotlib
O rótulo verde representa a compra do BTC e o vermelho representa a venda. O rótulo branco no canto superior direito é o valor líquido atual do robô e o rótulo inferior direito é o preço atual do Bitcoin. É simples e elegante.
Uma das críticas que recebi em artigos anteriores foi a falta de verificação cruzada, que não dividiu os dados em conjuntos de treinamento e conjuntos de teste. O objetivo de fazer isso era testar a precisão do modelo final em novos dados que nunca foram vistos anteriormente. Embora não seja o foco do artigo, ele é muito importante.
Por exemplo, uma forma comum de verificação cruzada é chamada de verificação k-fold, na qual você divide os dados em k grupos iguais, separando um grupo como grupo de teste e o restante como grupo de treinamento. No entanto, os dados da sequência de tempo são altamente dependentes do tempo, o que significa que os dados posteriores são altamente dependentes dos dados anteriores.
Quando aplicados a dados de sequência de tempo, o mesmo defeito é aplicado à maioria das outras estratégias de verificação cruzada. Portanto, precisamos apenas usar uma parte do conjunto de bits de dados completos como um conjunto de treinamento, começando com o número de bits até algum índice arbitrário, e usar o restante dos dados como um conjunto de testes.
slice_point = int(len(df) - 100000)
train_df = df[:slice_point]
test_df = df[slice_point:]
Em seguida, como nosso ambiente é configurado para processar apenas o número de bits de dados individuais, vamos criar dois ambientes, um para treinar dados e outro para testar dados.
train_env = DummyVecEnv([lambda: BitcoinTradingEnv(train_df, commission=0, serial=False)])
test_env = DummyVecEnv([lambda: BitcoinTradingEnv(test_df, commission=0, serial=True)])
现在,训练我们的模型就像使用我们的环境创建机器人并调用model.learn一样简单。
model = PPO2(MlpPolicy,
train_env,
verbose=1,
tensorboard_log="./tensorboard/")
model.learn(total_timesteps=50000)
Aqui, usamos um painel de tensão, para que possamos visualizar facilmente o nosso fluxograma de tensão e ver alguns indicadores quantitativos sobre o nosso robô. Por exemplo, aqui está o gráfico de recompensas descontadas de muitos robôs com mais de 200.000 passos de tempo:
O nosso melhor robô é capaz de atingir 1000 vezes o equilíbrio em até 200.000 passos, com o resto aumentando em média pelo menos 30 vezes!
Foi nesse momento que percebi que havia um erro no ambiente... e depois de corrigir esse erro, este é o novo mapa de recompensas:
Como você pode ver, alguns de nossos robôs estão indo bem e outros estão se arruinando sozinhos. No entanto, os robôs de bom desempenho podem atingir no máximo 10 vezes ou até 60 vezes o saldo inicial. Eu tenho que admitir que todos os robôs lucrativos são treinados e testados sem comissão, por isso é impraticável que nossos robôs ganhem dinheiro real. Mas pelo menos encontramos uma direção!
Vamos testar nossos robôs em um ambiente de teste (usando novos dados que eles nunca viram antes) e ver como eles se comportam.
Os nossos robôs treinados para negociar novos dados de testes vão à falência.
Obviamente, ainda temos muito trabalho a fazer. Simplesmente mudando o modelo para usar A2C em uma base estável, em vez do robô PPO2 atual, podemos melhorar drasticamente nosso desempenho neste conjunto de dados. Finalmente, de acordo com a sugestão de Sean O'Gorman, podemos atualizar um pouco nosso recurso de recompensa para que possamos aumentar a recompensa no valor líquido, em vez de apenas alcançar um valor líquido alto e ficar lá.
reward = self.net_worth - prev_net_worth
Somente essas duas mudanças podem significativamente melhorar o desempenho dos conjuntos de dados de teste, e, como você verá abaixo, finalmente podemos obter lucro com novos dados que não existem nos conjuntos de treinamento.
Mas podemos fazer melhor. Para melhorar esses resultados, precisamos otimizar nossos hiperparâmetros e treinar nossos robôs por mais tempo.
Até agora, este artigo já é um pouco longo, e ainda temos muitos detalhes para considerar, então vamos parar aqui. No próximo artigo, usaremos a otimização de Bayes para definir os melhores superparâmetros para o nosso espaço de problemas e nos prepararmos para treinar / testar no GPU usando o CUDA.
Neste artigo, começamos a criar um robô de negociação de Bitcoin lucrativo a partir do zero usando a aprendizagem reforçada.
1.使用OpenAI的gym从零开始创建比特币交易环境。
2.使用Matplotlib构建该环境的可视化。
3.使用简单的交叉验证对我们的机器人进行训练和测试。
4.略微调整我们的机器人以实现盈利
Embora nossos robôs de negociação não tenham sido tão lucrativos quanto esperávamos, estamos no caminho certo. Na próxima vez, vamos garantir que nossos robôs vençam os mercados de forma consistente e veremos como nossos robôs de negociação lidam com dados em tempo real.