Когда я пишу это в 2023 году, машинное обучение берет верх.

Чат-боты помогают людям создавать альтернативные реальности… Изображения в середине путешествия могут выглядеть так же реально, как и сама жизнь… ИИ может даже создавать музыку, которая на удивление забавна.

Итак, можно ли использовать эти замечательные технологии для чего-то… менее благородного? Как насчет прогнозирования цен на биткойны?

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

Но эти стратегии часто бесполезны для обычных инвесторов, которые не заинтересованы в алгоритмическом обучении.

Отсюда возникает интересный вопрос:

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

Битва в гору

Изначально я не был уверен, будет ли машинное обучение иметь какую-либо эффективность. Многие популярные стратегии технического анализа уже с трудом зарабатывают деньги на более длительных периодах удержания. Им также не хватает фундамента в первых принципах. Почему 14-дневная стратегия относительной силы (RSI) должна быть лучше, чем 15-дневная? И, что более важно, почему RSI вообще работает?

(Вы можете сказать, почему меня никогда не приглашают на коктейльные вечеринки)

Тем не менее, давайте убедимся, что мы не слишком критичны, проверив Биткойн с помощью одного из этих популярных методов. Здесь я выберу стратегию SMA-200, где трейдеры покупают всякий раз, когда цены поднимаются выше 200-дневной скользящей средней, и продают, когда она падает ниже нее.

Данные в данном случае взяты из CoinGecko, которые вы можете скачать здесь, чтобы попробовать сами.

import numpy as np
import matplotlib.pyplot as plt
import datetime as dt
from datetime import datetime
import pandas as pd

#  Prepare the data
df = pd.read_csv("btc-usd-max.csv")
df["date"] = pd.to_datetime(df["snapped_at"],
  format="%Y-%m-%d %H:%M:%S UTC")
df = df.sort_values(by="date")
df["SMA200"] = df["price"].rolling(200, min_periods=200).mean()
df["SMA200BUY"] = np.zeros(len(df))  # buy signals
df["SMA200SELL"] = np.zeros(len(df))  # sell signals

#  Get our shifted dates
df["priceT-1"] = df["price"].shift(1)
df["SMA200T-1"] = df["SMA200"].shift(1)

#  Create our simple SMA-200 trading algorithm
df.loc[(df['price'] >= df["SMA200"])&
       (df['priceT-1'] < df["SMA200T-1"]), "SMA200BUY"] = 1

df.loc[(df['price'] <= df["SMA200"])&
       (df['priceT-1'] > df["SMA200T-1"]), "SMA200SELL"] = 1

#  Create trading profit columns
df["buy_price"]=df["SMA200BUY"]*df["price"]
df["sell_price"] = df["SMA200SELL"]*df["price"]

#  Check our ending position is zero
df["SMA200SELL"].sum()-df["SMA200BUY"].sum()  # output => 0

#  Add up profits and losses
df["sell_price"].sum()-df["buy_price"].sum() # output => -3713.70955

#  Compare to buy-and-hold
df.price[-1:]-df.price[0] # output => 26662.82627

Как мы и ожидали, результаты не велики. SMA-200 принес бы отрицательный доход в размере 3713 долларов США по сравнению с прибылью в размере 26 662 долларов США.

Давайте также построим наши результаты, чтобы понять, почему. И, может быть, выпьем коктейль, чтобы заглушить наши печали.

df["buy_price"] = df["buy_price"].replace({0:np.nan})
df["sell_price"] = df["sell_price"].replace({0:np.nan})

plt.figure(figsize=(10,5))
ax = plt.axes()
ax.scatter(df["date"],df["buy_price"], label="Buy", color="#00a777",s=50)
ax.scatter(df["date"],df["sell_price"], label="Sell", color="#e3dc0e",s=50)
ax.plot(df["date"],df["price"], label="BTC Price", color="#003254", linewidth=2.0)
ax.plot(df["date"], df["SMA200"], '--', label="SMA-200", color="#00818a",linewidth=2.0)
ax.legend(loc=2)
plt.show()

Когда мы поднимаем глаза от нашего напитка, мы видим, что SMA-200 работает плохо, когда цены колеблются вокруг среднего значения. Инвесторы, по сути, много зарабатывают на прорывах, но затем теряют все это, когда цены движутся вбок и создают всплески ненужных, убыточных сделок.

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

Давай посмотрим что происходит.

Машинное обучение с биткойнами

В этом проекте я буду использовать данные, полученные из нескольких источников, включая CoinGecko, общедоступный репозиторий Wall Street Journal и Thomson Reuters. Мы добавим данные, не связанные с биткойнами, которые сыграют заметную роль в криптопрогнозировании. Вы можете следить за примером данных здесь.

Есть также три вещи, которые следует отметить, прежде чем мы начнем.

  • Период обучения. Мы будем использовать обучающие данные за период с 2018 по 2023 год, пятилетний период, который включает данные только после того, какфьючерсы на биткойны начали торговаться на CME.
  • Платформа. Мы будем использовать TensorFlow, платформу машинного обучения с открытым исходным кодом, выпущенную Google в 2016 году. Это мощная система, которая очень хорошо подходит для наших целей машинного обучения.
  • Чего ожидать. Вывод показывает, что машинное обучение работает. Так что, если вы не кодер (или просто хотите получить товары заранее), прокрутите вниз до конца, чтобы увидеть некоторые очень убедительные выступления.

Приступим к нашему анализу.

Создание нашего набора данных

Начнем с импорта необходимых библиотек в Python:

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import datetime as dt
from datetime import datetime
import pandas as pd
from btc_time_series_helpers import WindowGenerator as wg
from btc_time_series_helpers import models as m

#  Import data
df = pd.read_csv("btc_and_stock_data.csv")

Вы можете найти класс и модели WindowGenerator здесь из учебника Tensorflow по анализу временных рядов. Мы будем использовать уже предоставленные вспомогательные функции, чтобы вам было легко следовать. (Также нет смысла переписывать код, который уже работает).

Далее давайте посмотрим на данные.

df.info()

<class 'pandas.core.frame.DataFrame'>
Int64Index: 1358 entries, 1357 to 0
Data columns (total 28 columns):
 #   Column       Non-Null Count  Dtype         
---  ------       --------------  -----         
 0   date         1358 non-null   datetime64[ns]
 1   price        1358 non-null   float64       
 2   BTC_VOLUME   1358 non-null   float64       
 3   ETH_PRICE    1358 non-null   float64       
 4   ETH_VOLUME   1358 non-null   float64       
 5   USDT_VOLUME  1357 non-null   float64       
 6   SPY_PRICE    1358 non-null   float64       
 7   SPY_VOLUME   1358 non-null   int64         
 8   QQQ_PRICE    1358 non-null   float64       
 9   QQQ_VOLUME   1358 non-null   int64         
 10  GLD_PRICE    1358 non-null   float64       
 11  GLD_VOLUME   1358 non-null   int64         
 12  USO_PRICE    1358 non-null   float64       
 13  USO_VOLUME   1358 non-null   int64         
 14  JNK_PRICE    1358 non-null   float64       
 15  JNK_VOLUME   1358 non-null   int64         
 16  SHY_PRICE    1358 non-null   float64       
 17  SHY_VOLUME   1358 non-null   int64         
 18  IEF_PRICE    1358 non-null   float64       
 19  IEF_VOLUME   1358 non-null   int64         
 20  TLT_PRICE    1358 non-null   float64       
 21  TLT_VOLUME   1358 non-null   int64         
 22  DXY_INDEX    1357 non-null   float64       
 23  VIX_INDEX    1357 non-null   float64       
 24  PUT_INDEX    1357 non-null   float64       
 25  BCOM_INDEX   1358 non-null   float64       
 26  STOXX_INDEX  1350 non-null   float64       
 27  SSEC_INDEX   1265 non-null   float64       
dtypes: datetime64[ns](1), float64(19), int64(8)
memory usage: 307.7 KB

Вы можете видеть, что есть много функций, которые явно не будут иметь значения. И я уверен, что мы тоже что-то упускаем, особенно в отношении настроений.

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

#  Arrange our data by date
df["date"] = pd.to_datetime(df["date"], format="%m/%d/%Y")
df = df.sort_values(by="date",ascending=True)

#  Fill NaNs with forward fill and add features
df = df.fillna(method="ffill")
df["SPY_VOL_CHANGE"] = df["SPY_VOLUME"].pct_change()
df["QQQ_VOL_CHANGE"] = df["QQQ_VOLUME"].pct_change()
df["USDT_VOL_CHANGE"] = df["USDT_VOLUME"].pct_change()
df["ETH_VOL_CHANGE"] = df["ETH_VOLUME"].pct_change()

df["GLD_TURNOVER"] = df["GLD_VOLUME"]/df["GLD_PRICE"]
df["ETH_TURNOVER"] = df["ETH_VOLUME"]/df["ETH_PRICE"]
df["SHY_TURNOVER"] = df["SHY_VOLUME"]/df["SHY_PRICE"]
df["IEF_TURNOVER"] = df["IEF_VOLUME"]/df["IEF_PRICE"]
df["TLT_TURNOVER"] = df["TLT_VOLUME"]/df["TLT_PRICE"]

#  Remove unnecessary parameters to lessen chances of overfitting
df.pop("USDT_VOLUME")
df.pop("SPY_VOLUME")
df.pop("QQQ_VOLUME")
df.pop("USO_VOLUME")
df.pop("JNK_VOLUME")
df.pop("DXY_INDEX")
df.pop("SPY_VOL_CHANGE")
df.pop("QQQ_VOL_CHANGE")
df.pop("USDT_VOL_CHANGE")

df = df.fillna(0)
date_time = df.pop("date")

#  Split data into testing and training buckets
column_indices = {name: i for i, name in enumerate(df.columns)}
n = len(df)
train_df = df[:int(n*0.75)]
val_df = df[int(n*0.75):int(n*0.90)]
test_df = df[int(n*0.90):]

#  Normalize data on training set information
num_features = df.shape[1]

train_mean = train_df.mean()
train_std = train_df.std()

train_df = (train_df - train_mean) / train_std
val_df = (val_df - train_mean) / train_std
test_df = (test_df - train_mean) / train_std

В этой последней части я жестко кодирую нормализацию данных вместо использования метода TensorFlow tf.keras.layers.Normalization, чтобы избежать деления на среднее значение, которое еще предстоит узнать. Мы не узнаем среднюю цену тестового набора до тех пор, пока это не произойдет.

Наконец, мы добавим наши генераторы и запустим наш базовый сценарий. Там есть дополнительный код, который нам понадобится позже.

single_step_window = wg.WindowGenerator(
    input_width=1, label_width=1, shift=1,
    train_df=train_df, val_df=val_df, test_df=test_df,
    label_columns=['price'])

wide_window = wg.WindowGenerator(
    input_width=48, label_width=24, shift=1,
    train_df=train_df, val_df=val_df, test_df=test_df,
    label_columns=['price'])

conv_window = wg.WindowGenerator(
    input_width=3, label_width=1, shift=1,
    train_df=train_df, val_df=val_df, test_df=test_df,
    label_columns=['price'])

LABEL_WIDTH = 24
CONV_WIDTH = 3
INPUT_WIDTH = LABEL_WIDTH + (CONV_WIDTH - 1)
wide_conv_window = wg.WindowGenerator(
    input_width=INPUT_WIDTH, label_width=LABEL_WIDTH, shift=1,
    train_df=train_df, val_df=val_df, test_df=test_df,
    label_columns=['price'])

OUT_STEPS = 24
multi_window = wg.WindowGenerator(
    input_width=48,label_width=OUT_STEPS, shift=OUT_STEPS,
    train_df=train_df, val_df=val_df, test_df=test_df,
    label_columns=['price'])

MAX_EPOCHS=300

class MultiStepLastBaseline(tf.keras.Model):
  def call(self, inputs):
    return tf.tile(inputs[:, -1:, :], [1, OUT_STEPS, 1])

last_baseline = MultiStepLastBaseline()
last_baseline.compile(loss=tf.keras.losses.MeanSquaredError(),
                      metrics=[tf.keras.metrics.MeanAbsoluteError()])


multi_val_performance['Last'] = last_baseline.evaluate(multi_window.val)
multi_performance['Last'] = last_baseline.evaluate(multi_window.test, verbose=0)
multi_window.plot(last_baseline, title="Multistep Baseline Repeat T-1")

Базовый прогноз биткойнов

Наш базовый сценарий прост: мы просто предполагаем, что завтра цены на биткойны будут такими же, как и сегодня.

Вот как это выглядит. Зеленые точки представляют реальную производительность, а желтые — нашу «прогнозируемую» модель, которая повторяет последнюю цену биткойна в нашем окне.

Эти исходные допущения может оказаться на удивление трудно превзойти, и это известно любому инвестору или игроку. Здесь наша средняя абсолютная ошибка проверочного набора составляет 1,147787.

Далее мы создадим простую линейную модель и посмотрим, станет ли она лучше:

multi_linear_model = tf.keras.Sequential([
    tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = m.compile_and_fit(multi_linear_model, multi_window,
                            MAX_EPOCHS, patience=5)

Ах! У нас есть небольшое улучшение, даже если производительность все еще выглядит изменчивой. Средняя абсолютная ошибка теперь составляет 0,698771, что примерно на треть ниже, чем раньше. Также обратите внимание, что мы используем раннюю остановку, чтобы предотвратить переоснащение.

Добавление сложности в прогнозирование биткойнов

К счастью, ситуация продолжает улучшаться по мере того, как мы переходим к более сложным нейронным сетям. Вот Плотный, один скрытый слой с 512 нейронами.

multi_dense_model = tf.keras.Sequential([
    tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])
history = m.compile_and_fit(multi_dense_model, multi_window,
                            MAX_EPOCHS, patience=5)

С нашей плотной нейронной сетью наша средняя абсолютная ошибка падает до 0,262746, что является значительным шагом вперед по сравнению с нашими более ранними моделями.

Далее у нас есть слой одномерной свертки (также известный как временная свертка или CNN). CNN заслуживают особого внимания, потому что это тот же метод, который используется в программном обеспечении для распознавания изображений — то, чем занимается технический анализ. Мы ожидаем, что CNN уловит закономерности. (Подробнее о CNN можно прочитать здесь).

multi_conv_model = tf.keras.Sequential([
    tf.keras.layers.Lambda(lambda x: x[:, -CONV_WIDTH:, :]),
    tf.keras.layers.Conv1D(256, activation='relu', kernel_size=(CONV_WIDTH)),
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = m.compile_and_fit(multi_conv_model, multi_window,
                            MAX_EPOCHS, patience=10)

Средняя абсолютная ошибка падает до 0,2349405. Мы видим улучшения, хотя добиться их становится все труднее.

Наконец, у нас есть долговременная кратковременная память (LSTM), рекуррентная нейронная сеть (RNN), специально разработанная для временных рядов. Есть отличная схема процесса, которую вы можете найти здесь.

multi_lstm_model = tf.keras.Sequential([
    tf.keras.layers.LSTM(32, return_sequences=False),
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

history = m.compile_and_fit(multi_lstm_model, multi_window,
                            MAX_EPOCHS, patience=10)

Однако мы видим случай, когда интуиция иногда может нас подвести. Модель LSTM достигает производительности только 1,189562 на проверочном наборе — по сути, не лучше, чем базовая модель. Модель авторегрессии LSTM еще хуже, поэтому мы пока отложим эти методы. (Я мог бы продемонстрировать в последующих статьях, как мы можем заставить LSTM работать на удивление хорошо!)

Улучшение нашей модели V1

Это многообещающие первоначальные результаты — наши ошибки прогнозирования для проверочного набора на четыре пятых меньше, чем в базовом случае.

Но мы можем добиться большего.

Это потому, что вы быстро заметите, что у моделей возникают проблемы с предсказанием наборов тестов (т. е. средние абсолютные ошибки выше).

Это говорит нам о том, что наша модель переоснащается быстрее, чем наша точка отсечки в 5 эпох может ее остановить. Другими словами, у моделей Dense и Convolution возникают проблемы с обобщением тестового набора. Чтобы решить эту проблему, мы обратимся к слоям Dropout.

Отсева

Выпадающие слои были впервые представлены Джеффри Хинтоном и др. в 2012 году, чтобы помочь нейронным сетям работать. Случайным образом удаляя соединения во время обучения, этот метод «заставляет» оставшиеся нейроны компенсировать слабину.

В нашем эксперименте Dropout мы сосредоточимся на CNN, модели, которая показывает особенно большую разницу между проверкой и производительностью набора тестов. Это будет самый низко висящий плод.

Добавить выпадающие слои довольно просто:

multi_conv_model = tf.keras.Sequential([
    tf.keras.layers.Lambda(lambda x: x[:, -CONV_WIDTH:, :]),
    tf.keras.layers.Conv1D(256, activation='relu', kernel_size=(CONV_WIDTH),
      padding="same"),
    tf.keras.layers.Dropout(0.4),
    tf.keras.layers.Conv1D(128, activation='relu', kernel_size=(CONV_WIDTH)),
    tf.keras.layers.Dropout(0.15),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.4),
    tf.keras.layers.Dense(OUT_STEPS*num_features,
                          kernel_initializer=tf.initializers.zeros()),
    tf.keras.layers.Reshape([OUT_STEPS, num_features])
])

И это позволяет нам довольно быстро запускать несколько моделей. Вот как выглядит наша модель, если мы запустим CNN с одним выпадающим слоем:

А затем добавление второго отсева слоя с плотностью 64 нейрона.

И 3 слоя… 4… и больше.

Конечно, мы в конечном итоге столкнемся с уменьшением отдачи по мере добавления новых слоев. К тому времени, когда мы превысим два слоя отсева, наша тестовая ошибка снова начнет расти, и производительность станет неустойчивой. Мы выберем нашу сеть свертки с 2 отсевами в качестве предпочтительной модели.

Тестирование наших моделей на истории

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

Здесь мы будем использовать нашу лучшую модель из предыдущих:

Одномерная сверточная сеть с двумя выпадающими слоями

Я также добавлю скользящую нормализацию ценовых данных, поскольку цены на биткойны нестационарны. (Без этого алгоритм борется всякий раз, когда BTC входит в новый ценовой диапазон, и дает нам бессмысленные результаты).

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

# create our output dataframe
df_out_cnn = df[["date", "price"]].copy()
df_out_cnn["CNN_forecast_price_T1"] = ""
df_out_cnn["CNN_forecast_price_T6"] = ""
df_out_cnn["CNN_forecast_price_T12"] = ""
df_out_cnn["CNN_forecast_price_T18"] = ""
df_out_cnn["CNN_forecast_price_T24"] = ""

#  Convert prices to a % deviation from rolling 100-day averages
df["price_rolling_100"] = df["price"].rolling(100, min_periods=0).mean()
df["price"] = (df["price"]-df["price_rolling_100"])/df["price_rolling_100"]*100
df_out_cnn["normalized_price"] = df["price"]

#  Loop through the last 24-850 datapoints 
for i in range(24, 850, 1):
    # create our training, validation and prediction set
    n = len(df) - i
    train_df = df[:int(n*0.8)]
    val_df = df[int(n*0.8):n-24] # hide the last-24 days from the model
    test_df = df[n-48:n] # give predictor the last-48 days of data

    # create our normalized datasets for creating a model
    train_mean = train_df.mean()
    train_std = train_df.std()
    train_df = (train_df - train_mean) / train_std
    val_df = (val_df - train_mean) / train_std
    test_df = (test_df - train_mean) / train_std

    # create a window of new test data for each iteration
    multi_window = wg.WindowGenerator(
        input_width=48,label_width=OUT_STEPS, shift=OUT_STEPS,
        train_df=train_df, val_df=val_df, test_df=test_df,
        label_columns=['price'])

    # recreate a new LSTM model from scratch and fit the data
    multi_lstm_model = tf.keras.Sequential([
        tf.keras.layers.LSTM(32, return_sequences=False),
        tf.keras.layers.Dense(OUT_STEPS*num_features,
                            kernel_initializer=tf.initializers.zeros()),
        tf.keras.layers.Reshape([OUT_STEPS, num_features])
    ])
  
    history = m.compile_and_fit(multi_lstm_model, multi_window,
                                MAX_EPOCHS, patience=5)

    # take test dataset and predict output
    x = tf.convert_to_tensor(test_df)
    model_input = tf.expand_dims(x, 0)
    predictions = multi_lstm_model(model_input)[0,:,0].numpy()

    # save 5 datapoints
    df_out_cnn.at[n-1, "CNN_forecast_price_T1"] = predictions[0]
    df_out_cnn.at[n-1, "CNN_forecast_price_T6"] = predictions[5]
    df_out_cnn.at[n-1, "CNN_forecast_price_T12"] = predictions[11]
    df_out_cnn.at[n-1, "CNN_forecast_price_T18"] = predictions[17]
    df_out_cnn.at[n-1, "CNN_forecast_price_T24"] = predictions[23]

df_out.to_csv("CNN_out.csv")

Через несколько часов мы получили результаты. Чтобы убедиться, что все работает, давайте сначала взглянем на 50-дневную скользящую корреляцию между ожидаемой24-дневной доходностью и фактической 24-дневный доход.

df = pd.read_csv("CNN_out_850_normalized.csv")

df["24_day_forward_return"] = df["price"].shift(periods=-24)/df["price"]*100-100
df["24_day_predicted_return"] = df["CNN_forecast_price_T24"]-df["CNN_forecast_price_T1"]
df["50_day_rolling_correlation"] = df["24_day_predicted_return"].rolling(50).corr(df["24_day_forward_return"])
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")

plt.plot(df["date"],df["50_day_rolling_correlation"],c="#003254",linewidth=3.0)
plt.axhline(y = 0, color = '#00a777', linestyle = '--')
plt.ylabel("Rolling 50-day correlation")
plt.title('Actual vs. Predicted 24-Day Return')
plt.xticks(rotation = 45)
plt.show()

На первый взгляд кажется, что результаты потрясающие. Наши скользящие 50-дневные корреляции в среднем находятся в диапазоне 0,40 — намного выше, чем в базовом сценарии 0,00!

Однако следует отметить два предостережения. Во-первых, в конце 2020 и 2021 годах было несколько ужасных моментов, когда цены на биткойны неожиданно выросли с диапазона 6000 долларов до более чем 60 000 долларов. Исключив обучающие данные до 2018 года, мы лишили нашу программу машинного обучения нулевого опыта работы с криптовалютными пузырями. Изучение данных показывает, что алгоритм говорит нам продавать все вверх и покупать все вниз.

Во-вторых, удивительно сложно перевести эти высокие корреляции в точные прогнозы. Если вы изучите результаты, которые я разместил в начале этой части, вы быстро заметите, что желтые «прогнозируемые» точки часто отклоняются далеко от зеленых «фактических»… даже в наших обучающих данных. Набор тестов усиливает эту проблему. Любой, кто следует коду, быстро поймет, насколько плохи некоторые прогнозы на день T+1, когда цены волатильны.

Тем не менее, можно добиться некоторого превосходства, установив достаточно высокие требования к закупкам. Например, если мы попросим нашу систему покупать биткойны только тогда, когда прогнозируется рост цен на 12% или более…

df["buy_signal"] = df["24_day_predicted_return"].apply(lambda x: 1 if x>12 else 0)
df["trading_profit"] = df["buy_signal"]*df["24_day_forward_return"]
df.loc[df['trading_profit'] != 0,]["trading_profit"].mean() # output = 7.139155
df.loc[df['trading_profit'] != 0,]["trading_profit"].count() # output = 114

… наш алгоритм обеспечивает среднюю доходность в размере 7,14% на каждую 24-дневную сделку. Это значительное улучшение по сравнению с базовым случаем в 1,40 %, если бы мы совершали случайные покупки, начиная с 1 января 2021 года.

Установка нашей системы на 15% увеличивает доходность до 26% по 22 сделкам. Это отличный результат.

df["buy_signal"] = df["24_day_predicted_return"].apply(lambda x: 1 if x>15 else 0)
df["trading_profit"] = df["buy_signal"]*df["24_day_forward_return"]
df.loc[df['trading_profit'] != 0,]["trading_profit"].mean() # output = 26.017008
df.loc[df['trading_profit'] != 0,]["trading_profit"].count() # output  = 22

Система не идеальна, конечно. Вот график сигналов «покупки», когда мы настраиваем нашу стратегию на покупку всякий раз, когда прогнозируется рост цен на 12% или более.

Как видите, наша модель полностью упускает из виду всплеск 2021 года. Поскольку CNN впервые видит пузырь, он продолжает ожидать, что цены полностью откатятся вверх.

Тем не менее наше терпение в конце концов вознаграждается, когда начинают проявляться закономерности. Алгоритм довольно легко определяет, когда достигнуто дно, а наш относительно короткий 24-дневный период удержания защищает нас от значительных просадок, когда он ошибается.

Насколько это реально?

Очевидно, что любой должен скептически относиться к таким результатам.

Конечно.

Это связано с тем, что результаты машинного обучения ничего не говорят нам о том, почему процесс работал изначально. Возможно, выступление зависело от удачи. Или, может быть, мы натолкнулись на краткий тренд, который исчезнет, ​​как только мы попробуем торговую стратегию в реальной жизни.

К счастью, некоторые модели машинного обучения позволяют нам «заглянуть внутрь» и посмотреть, что происходит. Вот простое линейное приближение, которое позволяет нам проанализировать, какие факторы имеют наибольшее значение.

linear = tf.keras.Sequential([tf.keras.layers.Dense(units=1)
])
#  Set patience high to force overfitting
history = m.compile_and_fit(linear, single_step_window,
                            MAX_EPOCHS, patience=48)
val_performance['Linear'] = linear.evaluate(single_step_window.val)

plt.bar(x = range(len(train_df.columns)),
        height=linear.layers[0].kernel[:,0].numpy(), color="#003254")
axis = plt.gca()
axis.set_xticks(range(len(train_df.columns)))
_ = axis.set_xticklabels(train_df.columns, rotation=90)
plt.show()

Когда мы запускаем анализ для прогнозирования цен на биткойны T + 1 день, мы, к счастью, получаем ожидаемые результаты того, что важно для прогнозирования цены на следующий день:

Цена биткойнов за предыдущий день.

Это показывает нам, насколько трудно может быть лучше, чем базовый уровень. Если биткойн торгуется на уровне 24 000 долларов в один день, велики шансы, что на следующий день он будет стоить около 24 000 долларов.

Однако мы заставили эту модель работать, добавив внешние факторы, которые биткойн-трейдеры и технические специалисты часто игнорируют. К ним относятся цены на товары… значения фондовых индексов… доходность облигаций и другие параметры, которые оценивают общее настроение инвесторов в любой момент времени. Помните, что большая часть криптовалюты торгуется за пределами Соединенных Штатов, поэтому такие показатели, как Shanghai Composite Index и сила доллара, имеют значение.

Вот тот же график, который заставляет линейную модель предсказывать цены T+24.

Здесь мы начинаем видеть, что цены на биткойны имеют гораздо меньшее значение на 24-й день прогнозов. На его месте мы начинаем видеть такие функции, как индекс Nasdaq QQQ, индекс сырьевых товаров Bloomberg и цены на золото. По сути, сложные модели реализуют то, что уже знают профессиональные криптотрейдеры: цены на биткойны будут расти, когда инвесторы оптимистичны, и снижаться, когда они боятся.

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

Собираем все вместе: торговля BTC с ML

Полноценная торговая стратегия, очевидно, потребует более тонкой настройки. Вот смысл других моих экспериментов:

  • Сокращение функций. Дополнительное тестирование показывает, что LSTM будет работать так же хорошо, как CNN, если мы уменьшим набор функций.
  • Методы ансамбля. Производительность дополнительно повышается за счет добавления работающей модели LSTM и усреднения прогнозов. Это известно как ансамблевый метод.
  • Адаптивные стратегии. Активная торговая стратегия работает лучше, когда обновляется свежими данными, и продает, если прогнозы становятся отрицательными.

Модели машинного обучения также не являются полной заменой человеческого суждения. Наша модель CNN, например, имеет проблемы с определением новых тенденций (также распространенная критика других методов ML). Поэтому, если биткойн внезапно рухнет до 0 долларов, мы можем ожидать, что наш алгоритм машинного обучения сделает вывод, что актив перепродан. Модель понятия не имеет, почему биткойн внезапно падает.

Сигналы «убежденной покупки» в нашей модели также были относительно редки. Хотя это хорошо для инвесторов в особых ситуациях, которые не возражают против того, чтобы держать сухой порошок, это не рабочая стратегия для постоянных инвесторов биткойнов, чье начальство требует действий. Кроме того, простой бэктест говорит нам, что инвесторы должны были бы использовать кредитное плечо как минимум в 2,5 раза (в зависимости от стратегии), чтобы компенсировать периоды, когда они все в наличных деньгах.

Вот почему большинство профессиональных биткойн-трейдеров придерживаются высокочастотных данных, чтобы совершать больше сделок, даже если каждая сделка менее прибыльна.

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

В заключение: ChatGPT принес в массы мастерство письма. И любой не-художник теперь может создавать красивые работы с помощью генераторов изображений, если он умеет просить.

Теперь, возможно, обычные инвесторы также могут получить прибыль от рынков, которые годами обогащали более обеспеченные хедж-фонды. И, возможно, когда-нибудь эти алгоритмы помогут нам приготовить еще лучшие коктейли, чтобы насладиться ими на наших собственных маленьких праздниках.

Удачной торговли!