Всем снова привет! Если вы регулярно читаете мой блог, то знаете, что я опубликовал кучу руководств по прогнозированию финансовых временных рядов с помощью нейронных сетей. Если вы здесь впервые, вот их список:
- Простое прогнозирование временных рядов (и сделанные ошибки)
- Корректное прогнозирование одномерных временных рядов + бэктестинг
- Многомерное прогнозирование временных рядов
- Прогнозирование волатильности и нестандартные убытки
- Многозадачное и мультимодальное обучение
- Оптимизация гиперпараметров
- Улучшение классических стратегий с помощью нейронных сетей
- Вероятностное программирование и пиропрогнозы
Раньше мы в основном концентрировались на точности прогнозов и экспериментировали с этим, и мы очень кратко коснулись вопроса тестирования на истории, используя код из книги Майка Холлса-Мура. Конечно, наличие хорошего кода для тестирования на истории позволяет создавать действительно хорошие стратегии с управлением рисками, управлением капиталом и рассматривать различные сценарии, но если вы исследуете только сигналы, полученные из разных источников данных (даже это просто исторические цены) с использованием машинного обучения, вы нужно что-то более простое, чтобы понять, действительно ли полезны эти сигналы для открытия длинной или короткой позиции. По сути, мы просто хотим проверить разницу между ценами и умножить ее на знак сигнала - вверх или вниз и накапливать эту информацию в течение тестового периода.
Это очень легко сделать с помощью Pandas! Я снова позаимствовал кодовую основу у Майка Холлса-Мура QuantStart, но изменил ее для наших целей. Код, использованный в этом руководстве, вы можете проверить в моем Github.
Сценарий тестирования на истории
Предположим, мы хотим торговать криптовалютой Litecoin, начиная с 1 января 2018 года, используя индикаторы на основе нейронных сетей, и сравним эту производительность со сценарием, когда мы только что в нее инвестировали (стратегия покупки и удержания). Сначала мы обучим нейронную сеть на ежедневных данных с 2015 по 2017 год. Затем мы подготовим сигналы и протестируем их на истории. Как видите, держать эту монету с исторической точки зрения - не очень хорошая идея, но давайте посмотрим, сможем ли мы превзойти ее с помощью машинного обучения.
Загрузка данных
Сначала я загрузил ежедневные данные LTC с 01.01.2015 по 10.03.2018, используя API https://www.coinapi.io, у них есть бесплатный вариант для небольшого количества данных, которых нам будет достаточно, чтобы проверить нашу гипотезу. .
Теперь нам нужно загрузить данные и разделить их на образцы данных для обучения и тестирования:
symbol = 'LTC1518' bars = pd.read_csv('./data/%s.csv' % symbol, header=0, parse_dates=['Date']) START_TRAIN_DATE = '2015-01-01' END_TRAIN_DATE = '2017-12-31' START_TEST_DATE = '2018-01-01' END_TEST_DATE = '2018-03-09' train_set = bars[(bars['Date'] > START_TRAIN_DATE) & (bars['Date'] < END_TRAIN_DATE)] test_set = bars[(bars['Date'] > START_TEST_DATE) & (bars['Date'] < END_TEST_DATE)]
После этого мы просто перебираем эти данные (OHLCV) с некоторым окном (я выбрал 7 дней) для создания выборок данных и взяли знак изменения на следующий день для создания меток (код здесь) и после создания наборов данных для обучения и тестирования:
X_train, Y_train = create_dataset(train_set) X_test, Y_test = create_dataset(test_set)
Обучение нейронной сети
В образовательных целях мы создадим очень простую модель, которая будет перцептроном, но «на стероидах»: мы добавим к ней регуляризацию шума, отсев, пакетную нормализацию входных данных и правильно обучим модель:
def get_lr_model(x1, x2): main_input = Input(shape=(x1, x2, ), name='main_input') x = GaussianNoise(0.01)(main_input) x = Flatten()(x) x = BatchNormalization()(x) x = Dropout(0.5)(x) output = Dense(1, activation = "sigmoid", name = "out")(x) final_model = Model(inputs=[main_input], outputs=[output]) final_model.compile(optimizer=Adam(lr=0.001, amsgrad=True), loss='binary_crossentropy', metrics = ['accuracy']) return final_model
Я специально добавляю гауссовский шум на вход - он должен работать как своего рода регуляризация и увеличение данных - нейронная сеть должна научиться игнорировать этот шум (что за calembour - данные шума, дополненные искусственным шум) и изучите репрезентативные особенности.
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.9, patience=50, min_lr=0.000001, verbose=0) checkpointer = ModelCheckpoint(filepath="test.hdf5", verbose=0, save_best_only=True) es = EarlyStopping(patience=100) model = get_lr_model(X_train.shape[1], X_train.shape[-1]) history = model.fit(X_train, Y_train, epochs = 1000, batch_size = 64, verbose=0, validation_data=(X_test, Y_test), callbacks=[reduce_lr, checkpointer, es], shuffle=True)
После обучения я проверяю наиболее информативные метрики для двоичной классификации, такие как матрица путаницы, точность, отзыв и коэффициент корреляции Мэтьюза (все в scikit-learn):
pred = [1 if p > 0.5 else 0 for p in pred] C = confusion_matrix(Y_test, pred) print matthews_corrcoef(Y_test, pred) print(C / C.astype(np.float).sum(axis=1)) print classification_report(Y_test, pred) print '-' * 20
Результаты следующие:
MATTHEWS CORRELATION 0.17021289926939803 CONFUSION MATRIX [[0.42857143 0.53333333] [0.28571429 0.73333333]] CLASSIFICATION REPORT precision recall f1-score support 0 0.60 0.43 0.50 28 1 0.58 0.73 0.65 30 avg / total 0.59 0.59 0.58 58
А графики точности / потерь выглядят так:
что, скорее всего, может означать, что мы действительно можем что-то предсказать в течение нашего тестового периода, но давайте посмотрим, как это будет торговаться!
Подготовка сигналов
Эта часть проста и в то же время сложна. Сначала мы изменим выходы на {-1, 1} как признаки направления движения рыночной цены.
pred = [1 if p == 1 else -1 for p in pred]
Далее мы хотим рассмотреть ситуацию, когда мы прогнозируем изменение не на один день вперед, а на 2, 3 или целую неделю. Для стратегии это будет означать, что мы открываем длинную или короткую позицию в день X и ждем неделю, не совершая никаких сделок - для этих дней мы будем говорить, что сигнал равен 0:
pred = [p if i % FORECAST == 0 else 0 for i, p in enumerate(pred)]
После этого мы хотим сделать все хорошо с Pandas, поэтому, если мы хотим вычесть цену целевого дня (скажем, завтра) из цены дня покупки / продажи (скажем, сегодня), нам нужно сместить столбец целевого дня для период прогноза назад:
test_set['Close'] = test_set['Close'].shift(-FORECAST)
И, наконец, нам нужно исключить первые цены из нашего тестового периода. Почему? Потому что мы используем их для создания нашего первого прогноза (мы используем 7 дней, поэтому в эти первые 7 дней торгового периода мы не делаем никаких действий в нашем фрейме данных Pandas). Более того, после того как мы переместили столбец целевой цены закрытия назад на некоторый период, у нас осталось пустое место, и мы должны заполнить его нулями в столбце сигналов:
pred = [0.] * (LOOKBACK) + pred + [0.] * FORECAST
После того, как мы соберем все эти столбцы в один фрейм данных Pandas, чтобы иметь следующую структуру:
Здесь мы вычисляем price_diff как разницу между ценой открытия «сегодня» и ценой закрытия в столбце смещенного целевого дня, умножаем ее на нашу ставку и знак сигнала (последний определяет, правильно ли мы угадали направление или нет).
Бэктестинг
В этом абзаце я воспользуюсь некоторыми классами, которые заимствую здесь. Вы можете найти их в моем репозитории, в этой статье я покажу использование. Вот класс стратегии:
class MachineLearningForecastingStrategy(Strategy): def __init__(self, symbol, bars, pred): self.symbol = symbol self.bars = bars def generate_signals(self): signals = pd.DataFrame(index=self.bars.index) signals['signal'] = pred return signals
По сути, он просто берет прогнозы от нейронной сети и помещает их в столбец Pandas. После создадим класс Portfolio, в котором производятся все вычисления:
rfs = MachineLearningForecastingStrategy('LTC', test_set, pred) signals = rfs.generate_signals() portfolio = MarketIntradayPortfolio('LTC', test_set, signals, INIT_CAPITAL, STAKE) returns = portfolio.backtest_portfolio()
Он принимает данные о ценах тестового набора, сигналах, начальном капитале и сумме, которой мы рискуем при каждой сделке (фиксировано для этого руководства).
После его запуска мы можем отобразить результаты того, как наш портфель может развиваться с помощью сигналов нейронной сети, а не просто удерживать активы:
Мы видим, что наша стратегия имеет гораздо меньшую просадку, менее волатильна и дает немного более высокий доход - от 10000 до 10053 (что, по сути, ничто, хе-хе). Коэффициенты Шарпа для этих стратегий следующие: -3,04 для стратегии ML и -4,68 для стратегии покупки и удержания - оба варианта совсем не круты, но давайте попробуем весь процесс на другой валюте, например ETH.
Нейронная сеть обучена со следующими результатами:
MATTHEWS CORRELATION 0.07782916852651416 CONFUSION MATRIX [[0.43333333 0.60714286] [0.33333333 0.64285714]] CLASSIFICATION REPORT precision recall f1-score support 0 0.57 0.43 0.49 30 1 0.51 0.64 0.57 28 avg / total 0.54 0.53 0.53 58
Бэктестинг показывает следующее:
с коэффициентом Шарпа 6,90 и 7,57 соответственно (даже обе стратегии и рост с потерями в портфеле в этот период).
Заключение
Из приведенных выше результатов мы можем извлечь следующие уроки:
- Простые длинные / короткие стратегии можно легко протестировать без каких-либо инструментов тестирования на истории.
- Всегда вычислять матрицу неточностей, точность и отзыв классификаторов
- Хорошая точность прогнозов (более 55%) не означает высокой доходности стратегии.
- Даже простые нейронные сети с регуляризацией могут работать очень хорошо.
Скоро я опубликую новые материалы по вопросам оптимизации и алгоритмической торговли, так что… следите за обновлениями! :)
P.S.
Следите за мной также в Facebook, чтобы увидеть статьи AI, которые слишком короткие для Medium, Instagram для личных вещей и Linkedin!