Когда я пишу это в 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 принес в массы мастерство письма. И любой не-художник теперь может создавать красивые работы с помощью генераторов изображений, если он умеет просить.
Теперь, возможно, обычные инвесторы также могут получить прибыль от рынков, которые годами обогащали более обеспеченные хедж-фонды. И, возможно, когда-нибудь эти алгоритмы помогут нам приготовить еще лучшие коктейли, чтобы насладиться ими на наших собственных маленьких праздниках.
Удачной торговли!