在本文中,我们将创建和应用一个强化学习帧数,学习如何制作一个比特币交易机器人。在本教程中,我们将使用OpenAI的gym和来自stable-baselines库的PPO机器人,这是OpenAI基线库的一个分支。
非常感谢OpenAI和DeepMind在过去几年中为深度学习研究人员提供的开源软件。如果你还没有看到他们用AlphaGo,OpenAI Five和AlphaStar等技术所取得的惊人成就,你去年可能一直生活在与世隔绝之外,但你也应该去看看它们。
AlphaStar培训 https://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/
虽然我们不会创造任何令人印象深刻的东西,但在日常交易中比特币机器人的交易仍然是一件不容易的事。 然而,正如泰迪罗斯福曾经说过的那样,
因此,不仅要学会自我交易…也要让机器人为我们交易。
1.为我们的机器人创建gym环境以供其进行机器学习
2.渲染一个简单而优雅的可视化环境
3.训练我们的机器人,使其学习一个可获利的交易策略
如果您还不熟悉如何从头开始创建gym环境,或者如何简单的渲染这些环境的可视化。在继续之前,请随意google一篇此类文章。这两个动作即使作为最初级的程序员,也不会感到困难。
在本教程中,我们将使用Zielak生成的Kaggle数据集。如果您想下载源代码,我的Github仓库中会提供,同时也有.csv数据文件。好的,让我们开始吧。
首先,让我们导入所有必要的库。 确保用pip安装好您缺少的任何库。
import gym
import pandas as pd
import numpy as np
from gym import spaces
from sklearn import preprocessing
接下来,让我们为环境创建我们的类。我们需要传入一个pandas的数据帧数,以及一个可选的initial_balance和一个lookback_window_size,它将指示机器人在每个步骤中观察到的过去的时间步数。我们将每笔交易的佣金默认为0.075%,即Bitmex的当前汇率,并将串行参数默认为false,这意味着默认情况下我们的数据帧数将以随机片段遍历。
我们还对数据调用dropna()和reset_index(),首先删除具有NaN值的行,然后重置帧数的索引,因为我们已经删除了数据。
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)
我们的action_space在这里表示为一组3个选项(买入,卖出或持有)和另一组10个金额(1/10, 2/10, 3/10等等)。当选择买入行动时,我们将buy amount * self.balance worth of BTC。对于卖出行为,我们将sell amount * self.btc_held worth of BTC。当然,持有动作会忽略金额并且什么都不做。
我们的observation_space被定义为0到1之间的连续浮点集,其形状为(10,lookback_window_size + 1)。+ 1用于计算当前时间步长。 对于窗口中的每个时间步长,我们将观察OHCLV值。我们的净值等于买入或卖出的BTC数量,以及我们在这些BTC上花费或收到的美元总额。
接下来,我们需要编写reset方法来初始化环境。
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()
这里我们使用self._reset_session和self._next_observation,我们还没有定义它们。让我们先定义它们。
我们环境的一个重要部分是交易会话的概念。如果我们将这个机器人部署到市场外,我们可能永远不会一次运行它超过几个月。出于这个原因,我们将限制self.df中连续帧数的数量,也就是我们的机器人连续一次能看到的帧数。
在我们的_reset_session方法中,我们首先将current_step重置为0。接下来,我们将把 steps_left设置为1到MAX_TRADING_SESSION之间的随机数,这部分我们将在程序顶部定义。
MAX_TRADING_SESSION = 100000 # ~2个月
接下来,如果我们要连续遍历帧数,我们必须设置为遍历整个帧数,否则我们将frame_start设置为self.df中的随机点,并创建一个名为active_df的新数据帧数,它只是self.df的一个切片且是从frame_start到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]
在随机切片中遍历数据帧数的一个重要副作用是我们的机器人将具有更多独特的数据,以便在长时间训练时使用。例如,如果我们只是以串行方式遍历数据帧数(即从0到len(df)的顺序),那么我们将只拥有与数据帧数中一样多的唯一数据点。我们的观察空间在每个时间步骤甚至只能采用离散数量的状态。
但是,通过随机遍历数据集的切片,我们可以为初始数据集中的每个时间步长创建更有意义的交易结果集合,也就是交易行为和先前看到的价格行为组合来制造更多独特的数据集。让我举个例子来解释一下。
在重置串行环境之后的时间步长为10时,我们的机器人将始终在数据集内同时运行,并且在每个时间步长后有3个选择:买入,卖出或持有。对于这三种选择中的每一种,都需要另外一种选择:10%,20% ,… 或100%的具体实行量。这意味着我们的机器人可能会遇到任何103的10次方中的其中一种状态,一共1030种情况。
现在回到我们随机切片的环境。在时间步长为10时,我们的机器人可能处于数据帧数内的任何 len(df)时间步长中。假设在每个时间步长后做出相同的选择,意味着该机器人可以在相同的10个时间步长中经历任何len(df)的30次方中的的唯一状态。
虽然这可能会给大型数据集带来相当大的噪音,但我相信应该允许机器人从我们有限的数据量中学到更多。我们仍然会遍历我们的测试数据以串行的方式,以获取最新鲜,看似‘实时’的数据,以期通过算法的有效性来获得一个更准确的理解。
通过有效的视觉环境的观察通常很有帮助,方便了解我们的机器人将要使用的功能类型。例如,这里是使用OpenCV渲染的可观察空间的可视化。
OpenCV可视化环境的观察
图像中的每一行代表我们的observation_space中的一行。前4行类似频率的红线代表OHCL数据,正下方的橙色和黄色点代表成交量。下方波动的蓝色条是机器人的净值,而下方的较轻的条代表机器人的交易。
如果你仔细观察,你甚至可以自己制作一个蜡烛图。在交易量条的下面是一个类似莫尔斯代码界面,显示的是交易历史。看起来我们的机器人应该能够从我们的observation_space中的数据中充分学习,所以让我们继续。在这里,我们将定义_next_observation方法,我们将观察到的数据从0缩放到1。
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
我们已经建立了我们的观察空间,现在是时候编写我们的阶梯函数了,然后采取机器人预定的行动。每当我们当前交易时段的self.steps_left == 0时,我们将卖出我们持有的BTC并调用_reset_ session()。否则,我们将reward设置为当前的净值,如果我们的资金用完,则设置done为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, {}
采取交易行动就像获取current_price一样简单,确定需要执行的行动,以及买入或出售的数量。让我们快速编写_take_action,以便我们可以测试我们的环境。
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)
我们的机器人现在可以启动新环境,逐步完成该环境,并采取影响环境的操作。是时候看他们交易了。
我们的渲染方法可以像调用print(self.net_worth)一样简单,但这并不足够有趣。相反,我们将绘制一个简单的蜡烛图,其中包含交易量栏和我们净值的单独图表。
我们将从我上一篇文章中获取StockTradingGraph.py中的代码,并重新设计它以适应比特币环境。你可以从我的Github中获取代码。
我们要做的第一个更改是将 self.df [ ‘Date’] 更新 到self.df [‘Timestamp’],并删除对date2num的所有调用,因为我们的日期已经是unix时间戳格式。 接下来,在我们的渲染方法中,我们将更新日期标签以打印人类可读的日期,而不是数字。
from datetime import datetime
首先,导入datetime库,然后我们将使用utcfromtimestampmethod从每个时间戳和strftime获取UTC字符串,使其格式为: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]])
最后,我们将self.df[‘Volume’] 更改为self.df[‘Volume_(BTC)’]以匹配我们的数据集,完成这些,我们就准备好了。回到我们的BitcoinTradingEnv,我们现在可以编写渲染方法来显示图形了。
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)
瞧!我们现在可以看着我们的机器人交易比特币了。
用Matplotlib可视化我们的机器人交易
绿色幻影标签代表BTC的买入,红色幻影标签代表卖出。右上角的白色标签是机器人当前的净值,右下角的标签是比特币的当前价格。简单而优雅。现在,是时候训练我们的机器人了,看看我们能赚多少钱!
我在之前的文章中收到的一个批评是缺乏交叉验证,没有将数据分成训练集和测试集。这样做的目的是测试最终模型在以前从未见过的新数据上的准确性。虽然这不是那篇文章的关注点,但它确实十分重要。由于我们使用的是时间序列数据,因此在交叉验证方面我们没有太多选择。
例如,一种常见的交叉验证形式称为k-fold验证,在该验证中,您将数据拆分为k个相等的组,逐个单独将一个组作为测试组,并将其余数据用作训练组。然而,时间序列数据高度依赖于时间,这意味着后来的数据高度依赖于先前的数据。所以k-fold将不起作用,因为我们的机器人将在交易之前从未来的数据中学习,这是一种不公平的优势。
当应用于时间序列数据时,同样的缺陷也适用于大多数其他交叉验证策略。因此,我们只需将完整数据帧数的一部分用作从帧数开始到一些任意索引的训练集,并将其余数据用作测试集。
slice_point = int(len(df) - 100000)
train_df = df[:slice_point]
test_df = df[slice_point:]
接下来,由于我们的环境仅设置为处理单个数据帧数,因此我们将创建两个环境,一个用于训练数据,一个用于测试数据。
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)
在这里,我们使用张量板,因此我们可以轻松地可视化我们的张量流图并查看有关我们的机器人的一些量化指标。例如,以下是许多机器人超过200,000个时间步长的discounted rewards图表:
哇,看起来我们的机器人非常有利可图!我们最好的机器人甚至能够在200,000步的过程中实现1000x的平衡,其余的平均至少增加30倍!
就在这时,我意识到环境中存在一个错误…在修复该错误之后,这是新的奖励图:
正如你所看到的,我们的一些机器人做得很好,其余的则自行破产。然而,表现良好的机器人最多能够达到初始余额的10倍甚至60倍。我必须承认,所有有利可图的机器人人都是在没有佣金的情况下接受培训和测试的,所以我们的机器人赚到任何真钱都是不切实际的。但是我们至少找到了方向!
让我们在测试环境中测试我们的机器人(使用他们以前从未见过的新数据),看看他们会表现的怎么样。
我们训练有素的机器人在交易新的测试数据时会破产
显然,我们还有很多工作要做。通过简单地切换模型以使用稳定基线的A2C,而不是当前的PPO2机器人,我们可以大大提高我们在此数据集上的性能。最后,根据Sean O’Gorman的建议,我们可以略微更新我们的reward功能,以便我们在净值中增加reward,而不仅仅是实现高净值并留在那里。
reward = self.net_worth - prev_net_worth
仅这两个变化就可以大大提高测试数据集的性能,正如您在下面所看到的,我们终于能够在训练集中没有的新数据上实现盈利。
但是,我们可以做得更好。为了让我们改进这些结果,我们需要优化我们的超参数并训练我们的机器人更长时间。是时候让GPU开始工作且火力全开了!
到此为止,这篇文章已经有点长了,我们还有很多细节需要考虑,所以我们打算在这里休息一下。 在下一篇文章中,我们将使用贝叶斯优化来为我们的问题空间划分最佳超参数,并为使用CUDA在GPU上训练/测试做好准备。
在本文中,我们开始使用强化学习从零开始创建一个有利可图的比特币交易机器人。我们能够完成以下任务:
1.使用OpenAI的gym从零开始创建比特币交易环境。
2.使用Matplotlib构建该环境的可视化。
3.使用简单的交叉验证对我们的机器人进行训练和测试。
4.略微调整我们的机器人以实现盈利
虽然我们的交易机器人并没有像我们希望的那样盈利,但我们已经在朝正确的方向前进了。下一次,我们将确保我们的机器人能够始终如一地击败市场,我们将看到我们的交易机器人对实时数据的处理方式。请继续关注我的下一篇文章,以及比特币万岁!