使用Tensorflow作为二进制分类问题预测加密货币价格

介     绍
在本教程中,我们将介绍神经网络的原型,该模型将使我们能够使用Keras和Tensorflow作为我们的主要clarvoyance工具来估计未来的加密货币价格(作为二进制分类问题)。
然这很可能不是解决问题的最佳方法(毕竟投资银行在开发这种算法上投入了数十亿美元),但如果我们能够在55%以上的时间里把问题解决好,我们就有钱了!
们要做什么
1. 使用Binance API下载数据
2. 预处理数据
3. 训练我们的模型
4. 特征工程
5. 评估性能最佳的模型
使用Binance API下载数据
对于此示例,我们将下载单个调用中可获取的最大数据量。如果您想训练更多更好的东西并在现实世界中使用它(不建议这样做,那么您可能会浪费真钱),我建议您使用多次调用收集更多数据。
import requests
import json
import pandas as pd
import datetime as dt
START_DATE = ‘2019-01-01’
END_DATE = ‘2019-10-01’
INTERVAL = ’15m’
def parse_date(x):
    return str(int(dt.datetime.fromisoformat(x).timestamp()))
def get_bars(symbol, interval):
    root_url = ‘https://api.binance.com/api/v1/klines’
    url = root_url + ‘?symbol=’ + symbol + ‘&interval=’ + interval + ‘&startTime=’ + parse_date(START_DATE) + ‘&limit=1000’
    data = json.loads(requests.get(url).text)
    df = pd.DataFrame(data)
    df.columns = [‘open_time’,
                  ‘o’, ‘h’, ‘l’, ‘c’, ‘v’,
                  ‘close_time’, ‘qav’, ‘num_trades’,
                  ‘taker_base_vol’, ‘taker_quote_vol’, ‘ignore’]
    df.drop([‘ignore’, ‘close_time’], axis=1, inplace=True)
    return df
ethusdt = get_bars(‘ETHUSDT’, INTERVAL)
ethusdt.to_csv(‘./data.csv’, index=False)
在这段简单的代码中,我们需要必要的程序包,设置几个参数(我选择了15分钟的时间间隔,但是您可以选择更精细的时间间隔以进行更高频率的交易)并设置一些方便的函数,然后将数据保存到csv以供将来重用。这应该是不言而喻的,但如果有什么事情让你困惑,请随时留下评论,要求澄清:)
数据预处理
由于价格是顺序数据的一种形式,因此我们将使用LSTM层(长期短期记忆)作为我们网络中的第一层。我们希望将数据提供为一系列事件,这些事件将预测时间t + n处的价格,其中t是当前时间,n定义我们要预测的未来时间,为此,我们将数据作为 w长度的时间窗口。查看代码后,一切将变得更加清晰,让我们开始导入所需的软件包。
import pandas as pd
import numpy as np
import seaborn as sns
import random
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Dropout
from tensorflow.keras.callbacks import TensorBoard
import time
import matplotlib.pyplot as plt
这将导入Pandas,Numpy,我们训练模型所需的所有Tensorflow函数以及其他一些有用的软件包。
接下来,我们要定义一些常量,并从csv加载我们的数据(以防您在其他文件上编写训练代码:
WINDOW = 10 # how many time units we are going to use to evaluate 
the future value, in our case each time unit is 15 minutes so we 
are going to look at 15 * 10 = 150 minutes trading data
LOOKAHEAD = 5 # how far ahead we want to estimate if the future 
prices is going to be higher or lower? In this case is 5 * 15 = 75 
minutes in the future
VALIDATION_SAMPLES = 100 # We want to validate our model on data 
that wasn’t used for the training, we are establishing how many 
data point we are going to use here.
data = pd.read_csv(‘./data.csv’)
data[‘future_value’] = data[‘c’].shift(-LOOKAHEAD) # This allows us to 
define a new column future_value with as the value of c 5 time units 
in the future
data.drop([
    ‘open_time’
], axis=1, inplace=True) # we don’t care about the timestamp for 
predicting future prices
让我们定义一个函数,该函数使我们可以定义将来的价格是高于还是低于当前收盘价:
def define_output(last, future):
    if future > last:
        return 1
    else:
        return 0
如果价格低于或等于当前收盘价,只需将目标设置为0,如果价格高于或高于当前收盘价,则将其设置为1。 现在让我们定义一个函数,该函数使我们能够创建需要输入神经网络的移动时间窗口:
def sequelize(x):
    data = x.copy()
    buys = []
    sells = []
    holds = []
    data_length = len(data)
    for index, row in data.iterrows():
        if index <= data_length – WINDOW:
            last_index = index + WINDOW -1
            rowset = data[index : index + WINDOW]
            row_stats = rowset.describe().transpose()
            last_close = rowset[‘c’][last_index]
            future_close = rowset[‘future_value’][last_index]
            rowset = 2 * (rowset – row_stats[‘min’])  / (row_stats[‘max’] – row_stats[‘min’]) – 1
            rowset.drop([‘future_value’], axis=1, inplace=True)
            rowset.fillna(0, inplace=True)
            category = define_output(last_close, future_close)
            if category == 1:
                buys.append([rowset, category])
            elif category == 0:
                sells.append([rowset, category])
    min_len = min(len(sells), len(buys))
    results = sells[:min_len] + buys[:min_len]
    return results
sequences = sequelize(data)
哦,好吧,这里有很多东西。让我们一点一点地看:
data = x.copy() # let’s copy the dataframe, just in case
    buys = []
    sells = []
    holds = []
    data_length = len(data)
在这里,我们正在做一些初步的工作,复制数据框以确保我们不覆盖它(例如如果您使用Jupyter Notebook可能会很烦人),并设置用于买卖的数组,我们将使用它们来平衡数据。
for index, row in data.iterrows():
        if index <= data_length – WINDOW:
            last_index = index + WINDOW -1
            rowset = data[index : index + WINDOW]
当我们迭代数据集中的每一行时,如果索引大于我们定义的窗口大小,我们可以创建一个新的数据块,即窗口大小。在将此数据存储到另一个数组中之前,我们需要使用以下代码对其进行规范化:
row_stats = rowset.describe().transpose()
last_close = rowset[‘c’][last_index]
future_close = rowset[‘future_value’][last_index] # we’ll need to save this separately from the rest of the data
rowset = 2 * (rowset – row_stats[‘min’])  / (row_stats[‘max’] – row_stats[‘min’]) – 1
而且我们还想从数据集中删除future_value,并用0替换任何可能的NaN(对于我们的目的而言,理想情况还不够好):
rowset.drop([‘future_value’], axis=1, inplace=True)
rowset.fillna(0, inplace=True)
最后我们要确保我们的买卖平衡,如果其中一种发生的频率比另一种发生的频率高,我们的网络将迅速偏向偏斜,并且无法为我们提供可靠的估计:
       if category == 1:
                buys.append([rowset, category])
            elif category == 0:
                sells.append([rowset, category])
    # the following 2 lines will ensure that we have an equal amount of buys and sells
    min_len = min(len(sells), len(buys))
    results = sells[:min_len] + buys[:min_len]
    return results
最后我们在数据序列上运行此函数= sequelize(data)
随机化我们的数据也是个好主意,这样我们的模型就不会受到数据集排序的精确顺序的影响,以下代码将对数据集进行随机化,将训练数据集与测试数据集进行拆分,并同时显示这两种数据中的买入与卖出分布数据集。随时重新运行此代码段,以确保更均衡地分配购买和出售:
random.shuffle(sequences)
def split_label_and_data(x):
    length = len(x)
    data_shape = x[0][0].shape
    data = np.zeros(shape=(len(x),data_shape[0],data_shape[1]))
    labels = np.zeros(shape=(length,))
    for index in range(len(x)):
        labels[index] = x[index][1]
        data[index] = x[index][0]
    return data, labels
x_train, y_train = split_label_and_data(sequences[: -VALIDATION_SAMPLES])
x_test, y_test = split_label_and_data(sequences[-VALIDATION_SAMPLES :])
sns.distplot(y_test)
sns.distplot(y_train)

len(y_train)

在运行了一段代码后,您应该得到类似的东西,两个数据集之间的买卖均分(左对右)。
训练模型
现在我们已经准备好训练模型,但是由于我们还没有探索哪种超参数最适合我们的模型和数据,因此我们将尝试一种稍微复杂一些的方法。 首先让我们定义四个超参数数组:
DROPOUTS = [
    0.1,
    0.2,
]
HIDDENS = [
    32,
    64,
    128
]
OPTIMIZERS = [
    ‘rmsprop’,
    ‘adam’
]
LOSSES = [
    ‘mse’,
    ‘binary_crossentropy’
]
然后,我们将遍历每个数组以使用超参数组合来训练模型,以便以后可以使用TensorBoard比较它们:
for DROPOUT in DROPOUTS:
    for HIDDEN in HIDDENS:
        for OPTIMIZER in OPTIMIZERS:
            for LOSS in LOSSES:
                train_model(DROPOUT, HIDDEN, OPTIMIZER, LOSS)
现在我们需要定义train_model函数,该函数将实际创建和训练模型:
def train_model(DROPOUT, HIDDEN, OPTIMIZER, LOSS):
    NAME = f”{HIDDEN} – Dropout {DROPOUT} – Optimizer {OPTIMIZER} – Loss {LOSS} – {int(time.time())}”
    tensorboard = TensorBoard(log_dir=f”logs/{NAME}”, histogram_freq=1)
    model = Sequential([
        LSTM(HIDDEN, activation=’relu’, input_shape=x_train[0].shape),
        Dropout(DROPOUT),
        Dense(HIDDEN, activation=’relu’),
        Dropout(DROPOUT),
        Dense(1, activation=’sigmoid’)
    ])
    model.compile(
        optimizer=OPTIMIZER,
        loss=LOSS,
        metrics=[‘accuracy’]
    )
    model.fit(
        x_train,
        y_train,
        epochs=60,
        batch_size=64,
        verbose=1,
        validation_data=(x_test, y_test),
        callbacks=[
            tensorboard
        ]
    )
目前,这是一个非常简单的模型,其中的LSTM层为第一层,一个Dense中间层,一个Dense输出层,其大小为1,且为S型激活。该层将输出概率(从0到1),在LOOKAHEAD间隔之后,特定大小的WINDOW序列将跟随较高的收盘价,其中0是较低的收盘价的高概率,1是较高的更高的收盘价。
我们还添加了一个Tensorboard回调,这将使我们能够看到每种模型在每个训练周期(EPOCH)的表现。
随意运行此代码,然后在终端tensorboard –logdir = logs中运行Tensorboard
特征工程
最好的模型在验证数据上的准确性应该高于60%,这已经相当不错了。但是我们可以通过从现有数据集中提取更多数据来快速改进模型。从现有要素中提取新要素的过程称为要素工程。特征工程的示例是从数据中提取周末布尔值列,或从坐标对中提取国家/地区。在我们的案例中,我们将技术分析数据添加到我们的OHLC数据集中。
在笔记本或文件的顶部,添加ta包:from ta import *。
从csv加载数据后,添加以下行,它将以新列的形式将TA数据追加到我们现有的数据集中
data = pd.read_csv(‘./data.csv’)
#add the following line
add_all_ta_features(data, “o”, “h”, “l”, “c”, “v”, fillna=True) 
data[‘future_value’] = data[‘c’].shift(-LOOKAHEAD)
就是这样,在几行中我们极大地丰富了我们的数据集。 现在我们可以运行模型生成器循环来弄清楚我们的模型如何使用新的数据集,这将花费更长的时间,但值得等待。

有意义的数据集应确保模型更准确,在上图中,我们可以清楚地看到丰富数据集的性能比简单数据集更好,验证准确性徘徊在80%左右!
评估性能最佳的模型
现在我们有了一些看起来在纸面上表现不错的模型,我们如何评估假设的交易系统中应该使用哪个模型?
这可能是非常主观的,但我认为一种好的方法是从已知的验证标签分别查看买卖,并绘制相应预测的分布。希望于所有购买,我们的模型都可以预测购买,而不是很多出售,反之亦然。
让我们定义一个显示每个模型的图表的函数:
def display_results(NAME, y_test, predictions):
    plt.figure()
    buys = []
    sells = []
    for index in range(len(y_test)):
        if y_test[index] == 0:
            sells.append(predictions[index])
        elif y_test[index] == 1:
            buys.append(predictions[index])
    sns.distplot(buys, bins=10, color=’green’).set_title(NAME)
    sns.distplot(sells, bins=10, color=’red’)
    plt.show()
现在让我们在每次完成模型训练时都调用此函数:
  model.fit(
        x_train,
        y_train,
        epochs=60,
        batch_size=64,
        verbose=0,
        validation_data=(x_test, y_test),
        callbacks=[
            tensorboard
        ]
    )
    # after the model.fit call, add the following 2 lines.
    predictions = model.predict(x_test)
    display_results(NAME, y_test, predictions)
随着不同模型的训练,我们现在应该看到类似于下图的图像,其中买入以绿色绘制(并且我们希望它们在右端,聚集在1值附近),卖出以红色绘制(聚集在 左侧为0个值)。这些应有助于我们确定哪种模型可以提供更可靠的未来价格估算。

就是这样,我们现在有一些原型可以使用,它们可以对未来的价格提供合理的估计。作为练习,请尝试以下操作:
1. 如果增加网络的隐藏层数会怎样?
2. 如果您的数据集不平衡会怎样?
3. 如果增加DROPOUT值会怎样?
4. 如果您在新数据上测试最佳模型会怎样?(例如通过从币安获取不同的时间戳记?)