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

  1. Простое прогнозирование временных рядов (и сделанные ошибки)
  2. Корректное прогнозирование одномерных временных рядов + бэктестинг
  3. Многомерное прогнозирование временных рядов
  4. Прогнозирование волатильности и нестандартные убытки
  5. Многозадачное и мультимодальное обучение
  6. Оптимизация гиперпараметров
  7. Улучшение классических стратегий с помощью нейронных сетей
  8. Вероятностное программирование и пиропрогнозы

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

Это очень легко сделать с помощью 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 соответственно (даже обе стратегии и рост с потерями в портфеле в этот период).

Заключение

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

  1. Простые длинные / короткие стратегии можно легко протестировать без каких-либо инструментов тестирования на истории.
  2. Всегда вычислять матрицу неточностей, точность и отзыв классификаторов
  3. Хорошая точность прогнозов (более 55%) не означает высокой доходности стратегии.
  4. Даже простые нейронные сети с регуляризацией могут работать очень хорошо.

Скоро я опубликую новые материалы по вопросам оптимизации и алгоритмической торговли, так что… следите за обновлениями! :)

P.S.
Следите за мной также в Facebook, чтобы увидеть статьи AI, которые слишком короткие для Medium, Instagram для личных вещей и Linkedin!