В этой статье мы попытаемся предсказать цену закрытия BTC, используя несколько функций, связанных с биткойнами — историческую цену, объем, данные по цепочке и т. д. Вы можете обратиться к 1-й части этой статьи, чтобы понять намерение LSTM и данные. Я курировал. Мы начнем с извлечения данных из CyptoCompare API, создания последовательных данных, затем построения и обучения нашей модели LSTM и, наконец, использования ее для прогнозирования наших тестовых данных.

Импорт данных

Сначала вам нужно будет зарегистрировать бесплатную учетную запись на CryptoCompare и нажать Получить бесплатный ключ. После завершения регистрации вы должны передать свой личный ключ API в любой запрос, отправляемый на конечные точки API. Учетная запись бесплатного уровня позволяет вам извлекать только 2000 точек данных для каждого вызова, но вы можете продолжать запрашивать отдельные пакеты, явно указав метку времени в своем коде.

Приведенный ниже код загружает ваш ключ API, сохраненный в файле yaml (вы всегда должны хранить свой ключ API в безопасном и надежном месте, файл yaml — один из вариантов), и определяет функцию, которая позволяет нам запрашивать данные из конечных точек API:

# Import your API key into Google colab/notebook directory
# Set the API key from yaml file
yaml_file = open("sample_data/chain_api.yml")
p = yaml.load(yaml_file, Loader=yaml.FullLoader)
api_key = p['api_key']

# Number of days we retrieve data
data_limit = 2000

# Define pairs
fysm = 'BTC'
tysm = 'USD' 

# Query data
def api_call(url):
    # Set API key as Header
    headers = {'authorization':'Apikey' + api_key,}
    session = requests.Session()
    session.headers.update(headers)

    # API call to cryptocompare
    response = session.get(url)

    # Conversion of the response to dataframe
    historic_blockdata_dict = json.loads(response.text)
    df = pd.DataFrame.from_dict(historic_blockdata_dict.get('Data').get('Data'), orient='columns', dtype=None, columns=None)

    return df

Затем мы определяем функцию, которая объединяет отдельные вызовы, так как каждый вызов может извлекать только 2000 точек данных, в моем случае я определил 3 вызова в функции, так как это будет извлекать исторические данные биткойнов вплоть до 2010 года:

def call_all(base):
    dfs = []
    # 1st call
    df = api_call(f'{base_url}BTC&tsym=USD&limit={data_limit}')
    dfs.append(df)
    # defining the latest time point for the next call
    latest = df['time'][0]

    # 2nd call
    df = api_call(f'{base_url}BTC&tsym=USD&limit={data_limit}&toTs={latest}')
    dfs.append(df)
    latest = df['time'][0]

    # 3rd call
    df = api_call(f'{base_url}BTC&tsym=USD&limit={data_limit}&toTs={latest}')
    dfs.append(df)

    btcprice = pd.concat(dfs, ignore_index=True)
    btcprice['date'] = pd.to_datetime(btcprice['time'], unit='s')

    return btcprice

Затем это должно вернуть фрейм данных (df), в котором есть столбец «цена закрытия» биткойнов и множество других ненужных столбцов, таких как «максимум», «минимум», «открытие» и т. д., поэтому я их удалю. Другие дополнительные шаги, которые я бы сделал, - это отсортировать df по дате, удалить значения с 0, удалить повторяющиеся строки и сбросить индекс.

# sort df by date, drop values with 0 and unnecessary columns, drop duplicates row and reset index 
base_url = 'https://min-api.cryptocompare.com/data/v2/histoday?fsym='
btcprice = call_all(base_url)
btcprice.sort_values(by='date', inplace=True)
btcprice = btcprice.loc[~(btcprice['close']==0)]
btcprice.drop(columns=['time', 'conversionType', 'conversionSymbol', 'high', 'low', 'open', 'volumefrom', 'volumeto'], inplace=True)
btcprice = btcprice.drop_duplicates(subset='date', keep='first')
btcprice = btcprice.reset_index(drop=True)

Фрейм данных в итоге будет выглядеть так:

Затем вы должны использовать тот же код для извлечения данных для других функций, которые вас интересуют, но я хотел бы упомянуть одну вещь: вам следует перейти в раздел «Документация» на веб-сайте CryptoCompare и сначала проверить формат данных для каждой функции. , например, в моем коде есть небольшое изменение для извлечения исторических данных по сравнению с объемными данными.

# historical price data
# Conversion of the response to dataframe
historic_blockdata_dict = json.loads(response.text)
df = pd.DataFrame.from_dict(historic_blockdata_dict['Data']['Data'], orient='columns', dtype=None, columns=None)

# historical volume data
# Conversion of the response to dataframe
historic_blockdata_dict = json.loads(response.text)
df = pd.DataFrame.from_dict(historic_blockdata_dict['Data'], orient='columns', dtype=None, columns=None)

Хороший ! Теперь вы должны были получить несколько фреймов данных для интересующих вас функций. Для моего случая это столбцы, которые у меня есть:

  1. Историческая цена (я создал столбец, известный как «Вчера», который записывает вчерашнюю цену закрытия)
  2. Исторический объем биржевой торговли
  3. Адреса с нулевым балансом всех времен
  4. Новые адреса
  5. Активные адреса
  6. Количество транзакций за все время
  7. Хешрейт
  8. Сложность
  9. Количество адресов, которые держат от 1000 до 10000 $ BTC
  10. Количество адресов, которые держат от 10 000 до 100 000 $ BTC

Затем я объединил все фреймы данных, чтобы сформировать один окончательный фрейм данных, и заполнил все NA предыдущими или следующими значениями. Заглянув в голову фрейма данных, вот как это выглядит:

final = btcprice.merge(onchain, on='date', how='left').merge(distr, on='date', how='left').merge(volume, on='date', how='left')
final['yesterday'] = final['close'].shift(1)

# fill values with preceding or next values
final.fillna(method='ffill', inplace=True)
final.fillna(method='backfill', inplace=True)
final.head()

Корреляции Пирсона

Поскольку существует так много функций, некоторые из которых, как мне кажется, могут быть данными о шуме. Затем я решил использовать тест корреляции Пирсона, чтобы определить 5 основных функций, которые больше всего коррелируют с ценой закрытия BTC, иначе для обучения нашей модели потребуется много вычислительной мощности.

Следует отметить, что тест корреляции Пирсона имеет ограничения, одно из которых заключается в том, что он фиксирует только линейные отношения, поэтому он может пропустить некоторые нелинейные или сложные модели, влияющие на цену. Некоторые расширенные методы выбора признаков, которые приходят мне на ум, включают ANOVA, регрессию Лассо, тест теории информации и т. Д. Кроме того, корреляция не подразумевает причинно-следственную связь! В моем случае я просто использую тест корреляции Пирсона, который является самым простым способом.

Вот как выглядит корреляционный тест Пирсона:

Я удивлен, что объем имеет отрицательную корреляцию с ценой закрытия BTC в долларах. По моему мнению, больший объем может уменьшить ценовое искажение? Но в целом, 5 лучших функций, показанных в этом тесте, — это «вчера», «нулевой_баланс_адресов_все_время», «количество_транзакций_все_время», «большой_счет_транзакций» и «хешрейт».

Построение последовательных данных

Следующее, что мы хотим сделать, это построить последовательные данные для подачи в нашу модель LSTM, вы должны знать, что формат данных, необходимый для построения LSTM, следующий:

[n_samples, time_steps, n_features]

где n_samples — это количество ваших выборок, time_steps — это временные интервалы, которые обычно являются вашим количеством ячеек LSTM, а n_features — это количество имеющихся у вас функций.

Сначала мы разделим наши данные на данные тренировочного теста и масштабируем наши данные, используя MinMaxScaler(). Вы должны подгонять свой скейлер только к тренировочным данным и преобразовывать тестовые данные с помощью скейлера, чтобы предотвратить утечку данных (проще говоря, вы не хотите, чтобы ваши тренировочные данные знали какую-либо информацию о тестовых данных). В этом случае я разделяю свои данные тренировочного теста на 80%-20%.

scaler = MinMaxScaler()

# skipping first column (data)
train_features = df.iloc[:int(len(df)*0.80), 1:].values.reshape(-1, 6)
test_features = df.iloc[int(len(df)*0.80):, 1:].values.reshape(-1, 6)

scaled_train_features = scaler.fit_transform(train_features)
scaled_test_features = scaler.transform(test_features)

Затем я использую эти данные для создания последовательных данных, где я создал функцию to_sequences, которая представляет собой метод скользящего окна для построения последовательностей входных данных. Чтобы расширить, скажем, у нас есть набор данных временных рядов с 365 точками данных, размер окна seq_len означает, что мы будем перемещать окно по данным, начиная с первого дня года, и извлекать фрагмент из 100 точек данных, и повторять этот процесс, пока мы достичь конца набора данных, всего 265 фрагментов по 100 точек в каждом. Надеюсь, это не слишком запутанно, но эта статья может дать более четкое объяснение, поскольку она обеспечивает визуализацию!

Затем мы определяем функцию preprocess, которая специально берет каждый блок из 100 точек данных, использует первые 99 точек данных в качестве входных данных для модели, а 100-е точки данных — в качестве выходных данных.

SEQ_LEN = 100

def to_sequences(data, seq_len):
    d = []

    for index in range(len(data) - seq_len):
        d.append(data[index: index + seq_len])

    return np.array(d)

def preprocess(train_features, test_features, seq_len):

    train = to_sequences(train_features, seq_len)
    # X_trainc and X_test is a tensor 
    # with shape (n_sequences, seq_len-1, num_features)
    # y_train is a tensor 
    # with shape (num_sequences, 1)
    X_train = train[:, :-1, :]
    y_train = train[:, -1, -1].reshape(-1, 1)

    test = to_sequences(test_features, seq_len)
    X_test = test[:, :-1, :]
    y_test = test[:, -1, -1].reshape(-1, 1)

    return X_train, y_train, X_test, y_test


X_train, y_train, X_test, y_test = preprocess(scaled_train_features, scaled_test_features, SEQ_LEN)

Построение нашей модели

Мы создаем трехслойную рекуррентную нейронную сеть LSTM с отсевом 20%, чтобы предотвратить боевое переоснащение во время обучения:

# model building
DROPOUT = 0.2
WINDOW_SIZE = SEQ_LEN - 1

model = keras.Sequential()

model.add(Bidirectional(CuDNNLSTM(WINDOW_SIZE, return_sequences=True),
                        input_shape=(WINDOW_SIZE, X_train.shape[-1])))
model.add(Dropout(rate=DROPOUT))

model.add(Bidirectional(CuDNNLSTM((WINDOW_SIZE * 2), return_sequences=True)))
model.add(Dropout(rate=DROPOUT))

model.add(Bidirectional(CuDNNLSTM(WINDOW_SIZE, return_sequences=False)))

model.add(Dense(units=1))

model.add(Activation('linear'))

На высоком уровне модель представляет собой рекуррентную нейронную сеть (RNN) с ячейками LSTM. В частности, это двунаправленная сеть LSTM (BiLSTM), что означает наличие двух уровней LSTM: один обрабатывает последовательность от начала до конца, другой уровень обрабатывает от конца к началу. Это позволяет сети фиксировать временные зависимости (близкие отношения) в обоих направлениях и улучшает ее способность изучать долгосрочные зависимости (дальние отношения).

Вам может быть интересно, что такое CuDNNLSTM, это означает CUDA Deep Neural Network LSTM. Это реализация сети LSTM, оптимизированная для графических процессоров с использованием библиотеки NVIDIA CUDA, которая может выполнять вычисления намного быстрее, чем обычная LSTM. Вот почему он обычно используется в приложениях глубокого обучения, требующих высокой производительности, таких как компьютерное зрение и НЛП.

Обучение нашей модели

Я не буду углубляться в каждую часть кода, иначе эта статья получилась бы очень длинной. Короче говоря, мы используем среднеквадратичную ошибку в качестве функции потерь и оптимизатор Адама со скоростью обучения 0,001. Не забудьте установить shuffle=False, так как мы используем данные временных рядов. Почему? Обратитесь к этой статье для ясного объяснения. Но, по сути, мы не хотим испортить временную последовательность данных, что может привести к плохим прогнозам.

# compile
model.compile(
    loss='mean_squared_error', 
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
)

# model fitting
BATCH_SIZE = 64

history = model.fit(
    X_train, 
    y_train, 
    epochs=50, 
    batch_size=BATCH_SIZE, 
    shuffle=False,
    validation_split=0.1
)

Полученные результаты

Оценивая нашу модель, мы получаем среднюю потерю при обучении ~ 0,05, что довольно хорошо.

model.evaluate(X_test, y_test)

Визуализируя функцию потерь, это выглядит так:

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

Прогнозирование цены биткойна

Затем мы используем нашу модель для прогнозирования цен на биткойны (на основе данных тестирования).

y_hat = model.predict(X_test)

y_test_reshaped = np.zeros_like(scaled_test_features[:, -1:])
y_test_reshaped[-y_test.shape[0]:, :] = y_test.reshape(-1, 1)

y_hat_reshaped = np.zeros_like(scaled_test_features[:, -1:])
y_hat_reshaped[-y_hat.shape[0]:, :] = y_hat.reshape(-1, 1)

# inverse transform y_test and y_hat
y_test_inverse = scaler.inverse_transform(np.concatenate((scaled_test_features[:, :-1], y_test_reshaped), axis=1))[:, -1]
y_hat_inverse = scaler.inverse_transform(np.concatenate((scaled_test_features[:, :-1], y_hat_reshaped), axis=1))[:, -1]

# visualize
plt.figure(figsize=(10, 6))
plt.plot(y_test_inverse[(len(y_test_inverse)-y_test.shape[0]):], label='Actual')
plt.plot(y_hat_inverse[(len(y_hat_inverse)-y_test.shape[0]):], label='Predicted')

plt.title('Bitcoin price prediction')
plt.xlabel('Time [days]')
plt.ylabel('Price')
plt.legend(loc='best')

Фактическая цена, кажется, не так хорошо предсказывает реальную цену, но, сосредоточив внимание на модели ценового действия, она выглядит нормально для меня, поэтому я решил построить ее логарифмическую версию исключительно для сравнения моделей ценового действия.

График паттерна ценового действия выглядит довольно хорошо (есть некоторые части, которые отстают, но некоторые части дают хороший прогноз), но очевидно, что это очень простая модель, которую еще предстоит улучшить. Некоторые дополнительные улучшения/работы, которые могут быть выполнены, включают:

  1. Включение более релевантных функций (используя различные методы выбора функций — убедитесь, что они надежны)
  2. Настройка гиперпараметров
  3. Тестирование нашей модели на истории

Заключение

Это все на данный момент! Помните, что я строю эту модель просто для развлечения, она определенно недостаточно хороша, чтобы использовать ее в качестве руководства для торговли биткойнами/любой криптовалютой. Исходный код доступен в Google Colab:

https://colab.research.google.com/drive/1yXfOA9L9OgaWQtIdpOG4Pt2xdRnW5QKA?usp=sharing