рекомендации

четверг, 7 октября 2021 г.

Нейронные сети для алгоритмического трейдинга. Простое прогнозирование временных рядов.


ВАЖНОЕ ПРИМЕЧАНИЕ:

Я допустил ошибку в этом посте во время предварительной обработки данных для проблемы регрессии - проверьте эту проблему https://github.com/Rachnog/Deep-Trading/issues/1, чтобы исправить. Это приводит к худшим результатам, которые могут быть частично улучшены за счет лучшего поиска гиперпараметров с использованием полных данных OHLC и обучения для > 50 эпох.

Это первая часть моих экспериментов по применению глубокого обучения для финансов, в частности, для алгоритмического трейдинга.

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

Сейчас я планирую поработать над следующими разделами:
  1. Прогнозирование временных рядов с необработанными данными.
  2. Прогнозирование временных рядов с использованием пользовательских функций.
  3. Оптимизация гиперпараметров.
  4. Реализация торговой стратегии, тестирование на истории и управление рисками.
  5. Более сложные торговые стратегии, обучение с подкреплением.
  6. Оставаться на плаву, API брокеров, зарабатывать (терять) деньги.
Я настоятельно рекомендую вам проверить код и IPython Notebook в этом репозитории.

В этой первой части я хочу показать, как MLP, CNN и RNN могут использоваться для прогнозирования финансовых временных рядов. В этой части мы не собираемся использовать какую-либо функциональность. Давайте просто рассмотрим исторический набор данных о ценовых движениях индекса S&P500. У нас есть информация с 1950 по 2016 год о ценах открытия, закрытия, наибольших, наименьших ценах на каждый день в году и объеме торгов. Во-первых, мы попытаемся просто предсказать цену закрытия в конце следующего дня, во-вторых, мы попытаемся предсказать доходность (цена закрытия - цена открытия). Загрузите набор данных из Yahoo Finance или из этого репозитория.


Ежедневные цены закрытия S&P500 с 2006 по 2016 год

Определение проблемы

Мы будем рассматривать нашу проблему как 1) регрессионную проблему (пытаясь точно спрогнозировать цену закрытия или прибыль на следующий день) 2) бинарную классификационную проблему (цена будет расти [1; 0] или уменьшаться [0; 1]).

Для обучения НС мы будем использовать фреймворк Keras.

Сначала давайте подготовим наши данные для обучения. Мы хотим предсказать значение t + 1 на основе информации за N дней. Например, имея на рынке цены закрытия за последние 30 дней, мы хотим предсказать, какой будет цена завтра, на 31-й день.

Мы используем первые 90% временных рядов в качестве обучающего набора (рассмотрим его как исторические данные), а последние 10% - в качестве тестового набора для оценки модели.

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

def load_snp_close():
    f = open('table.csv', 'rb').readlines()[1:]
    raw_data = []
    raw_dates = []
    for line in f:
        try:
            close_price = float(line.split(',')[4])
            raw_data.append(close_price)
            raw_dates.append(line.split(',')[0])
        except:
            continue
    return raw_data, raw_dates

def split_into_chunks(data, train, predict, step, binary=True, scale=True):
    X, Y = [], []
    for i in range(0, len(data), step):
        try:
            x_i = data[i:i+train]
            y_i = data[i+train+predict]
            if binary:
                if y_i > 0.:
                    y_i = [1., 0.]
                else:
                    y_i = [0., 1.]
                if scale: x_i = preprocessing.scale(x_i)
            else:
                timeseries = np.array(data[i:i+train+predict])
                if scale: timeseries = preprocessing.scale(timeseries)
                x_i = timeseries[:-1]
                y_i = timeseries[-1]
        except:
            break
        X.append(x_i)
        Y.append(y_i)
    return X, Y

Проблема регрессии. MLP

Это будет просто 2-скрытый слой персептрона. Количество скрытых нейронов выбирается опытным путем, мы будем работать над оптимизацией гиперпараметров в следующих разделах. Между двумя скрытыми слоями мы добавляем один слой Dropout, чтобы предотвратить наложение.

Важная вещь - это Dense (1), Activation («linear») и «mse» в разделе компиляции. Нам нужен один выход, который может быть в любом диапазоне (мы прогнозируем реальное значение), а наша функция потерь определяется как среднеквадратическая ошибка.

model = Sequential()
model.add(Dense(500, input_shape = (TRAIN_SIZE, )))
model.add(Activation('relu'))
model.add(Dropout(0.25))
model.add(Dense(250))
model.add(Activation('relu'))
model.add(Dense(1))
model.add(Activation('linear'))
model.compile(optimizer='adam', loss='mse')

Давайте посмотрим, что произойдет, если мы просто передадим куски 20-дневных цен закрытия и спрогнозируем цену на 21-й день. Окончательный MSE = 46.3635263557, но это не очень представительная информация. Ниже приведен график прогнозов для первых 150 точек набора тестовых данных. Черная линия - фактические данные, синяя - предсказанная. Мы можем ясно видеть, что наш алгоритм даже не приближается к фактическим данным по значению, но может распознать тренд.

predicted = model.predict(X_test)
try:
    fig = plt.figure(figsize=(width, height))
    plt.plot(Y_test[:150], color='black')
    plt.plot(predicted[:150], color='blue')
    plt.show()
except Exception as e:
    print str(e)


Прогнозирование результатов обучения MLP на необработанных данных

Давайте масштабируем наши данные, используя метод preprocessing.scale () для sklearn, чтобы у нашего временного ряда было нулевое среднее значение и единичная дисперсия, и обучим одну и ту же MLP. Теперь у нас есть MSE = 0,0040424330518 (но это на масштабированных данных). На графике ниже вы можете увидеть фактический масштабированный временной ряд (черный) и наш прогноз (синий) для него:


Прогнозирование результатов обучения MLP по масштабированным данным, масштабированный прогноз

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

params = []
for xt in X_testp: # taking training data from unscaled array
    xt = np.array(xt)
    mean_ = xt.mean()
    scale_ = xt.std()
    params.append([mean_, scale_])

predicted = model.predict(X_test)
new_predicted = []

# restoring data
for pred, par in zip(predicted, params):
    a = pred*par[1]
    a += par[0]
    new_predicted.append(a)

MSE в этом случае равняется 937,963649937. Вот график восстановленного прогноза (красный) и реальных данных (зеленый):


Прогнозирование результатов обучения MLP по масштабированным данным, восстановление прогноза

Не плохо, не правда ли? Но давайте попробуем более сложные алгоритмы для этой проблемы!

Проблема регрессии. CNN

Я не собираюсь углубляться в теорию сверточных нейронных сетей, вы можете проверить эти удивительные ресурсы:

cs231n.github.io - Стэнфордский курс CNN для компьютерного зрения

http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/ - CNN для распознавания текста, может быть полезен для понимания того, как это работает для данных 1D.

Давайте определим двухслойную сверточную нейронную сеть (комбинацию слоев свертки и макс-пула) с одним полностью подключенным слоем и тем же выходом, что и ранее:

model = Sequential()
model.add(Convolution1D(input_shape = (TRAIN_SIZE, EMB_SIZE), 
                        nb_filter=64,
                        filter_length=2,
                        border_mode='valid',
                        activation='relu',
                        subsample_length=1))
model.add(MaxPooling1D(pool_length=2))

model.add(Convolution1D(input_shape = (TRAIN_SIZE, EMB_SIZE), 
                        nb_filter=64,
                        filter_length=2,
                        border_mode='valid',
                        activation='relu',
                        subsample_length=1))
model.add(MaxPooling1D(pool_length=2))

model.add(Dropout(0.25))
model.add(Flatten())

model.add(Dense(250))
model.add(Dropout(0.25))
model.add(Activation('relu'))

model.add(Dense(1))
model.add(Activation('linear'))

Давайте проверим результаты. MSE для масштабированных и восстановленных данных: 0,227074542433; +935,520550172. Графики ниже:


Прогнозирование результатов обучения CNN по масштабированным данным, масштабированный прогноз


Прогнозирование результатов обучения CNN по масштабированным данным, восстановленный прогноз

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

Проблема регрессии. RNN

В качестве рекуррентной архитектуры я хочу использовать два сложенных слоя LSTM (подробнее о LSTM читайте здесь).

model = Sequential()
model.add(LSTM(input_shape = (EMB_SIZE,), input_dim=EMB_SIZE, output_dim=HIDDEN_RNN, return_sequences=True))
model.add(LSTM(input_shape = (EMB_SIZE,), input_dim=EMB_SIZE, output_dim=HIDDEN_RNN, return_sequences=False))
model.add(Dense(1))
model.add(Activation('linear'))

Графики прогнозов ниже, MSE = 0.0246238639582; +939,948636707.



Прогнозирование результатов RNN, обученных по масштабированным данным, масштабированный прогноз


Прогнозирование результатов обучения РНН по масштабированным данным, восстановленный прогноз

Прогнозирование RNN больше похоже на модель скользящего среднего, оно не может предсказать все колебания.

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


Дневная доходность индекса S&P500

Проблема классификации. MLP

Код немного изменился - мы изменили наш последний слой Dense, чтобы иметь выход [0; 1] или [1; 0] и добавили вывод softmax, чтобы ожидать вероятностный вывод.

Чтобы загрузить двоичные исходы, измените в коде следующую строку:

split_into_chunks(timeseries, TRAIN_SIZE, TARGET_TIME, LAG_SIZE, binary=False, scale=True)

split_into_chunks(timeseries, TRAIN_SIZE, TARGET_TIME, LAG_SIZE, binary=True, scale=True)

Также мы меняем функцию потерь на бинарную кросс-энтропию и добавляем метрики точности.

model = Sequential()
model.add(Dense(500, input_shape = (TRAIN_SIZE, )))
model.add(Activation('relu'))
model.add(Dropout(0.25))
model.add(Dense(250))
model.add(Activation('relu'))
model.add(Dense(2))
model.add(Activation('softmax'))
model.compile(optimizer='adam', 
  loss='binary_crossentropy', 
  metrics=['accuracy'])

Train on 13513 samples, validate on 1502 samples
Epoch 1/5
13513/13513 [==============================] - 2s - loss: 0.1960 - acc: 0.6461 - val_loss: 0.2042 - val_acc: 0.5992
Epoch 2/5
13513/13513 [==============================] - 2s - loss: 0.1944 - acc: 0.6547 - val_loss: 0.2049 - val_acc: 0.5965
Epoch 3/5
13513/13513 [==============================] - 1s - loss: 0.1924 - acc: 0.6656 - val_loss: 0.2064 - val_acc: 0.6019
Epoch 4/5
13513/13513 [==============================] - 1s - loss: 0.1897 - acc: 0.6738 - val_loss: 0.2051 - val_acc: 0.6039
Epoch 5/5
13513/13513 [==============================] - 1s - loss: 0.1881 - acc: 0.6808 - val_loss: 0.2072 - val_acc: 0.6052
1669/1669 [==============================] - 0s

Test loss and accuracy: [0.25924376667510113, 0.50209706411917387]

Это не лучше, чем случайное угадывание (точность 50%), давайте попробуем что-нибудь получше. Проверьте результаты ниже.

Проблема классификации. CNN

model = Sequential()
model.add(Convolution1D(input_shape = (TRAIN_SIZE, EMB_SIZE), 
                        nb_filter=64,
                        filter_length=2,
                        border_mode='valid',
                        activation='relu',
                        subsample_length=1))
model.add(MaxPooling1D(pool_length=2))

model.add(Convolution1D(input_shape = (TRAIN_SIZE, EMB_SIZE), 
                        nb_filter=64,
                        filter_length=2,
                        border_mode='valid',
                        activation='relu',
                        subsample_length=1))
model.add(MaxPooling1D(pool_length=2))

model.add(Dropout(0.25))
model.add(Flatten())

model.add(Dense(250))
model.add(Dropout(0.25))
model.add(Activation('relu'))
model.add(Dense(2))
model.add(Activation('softmax'))

history = TrainingHistory()

model.compile(optimizer='adam', 
  loss='binary_crossentropy', 
  metrics=['accuracy'])

Train on 13513 samples, validate on 1502 samples
Epoch 1/5
13513/13513 [==============================] - 3s - loss: 0.2102 - acc: 0.6042 - val_loss: 0.2002 - val_acc: 0.5979
Epoch 2/5
13513/13513 [==============================] - 3s - loss: 0.2006 - acc: 0.6089 - val_loss: 0.2022 - val_acc: 0.5965
Epoch 3/5
13513/13513 [==============================] - 4s - loss: 0.1999 - acc: 0.6186 - val_loss: 0.2006 - val_acc: 0.5979
Epoch 4/5
13513/13513 [==============================] - 3s - loss: 0.1999 - acc: 0.6176 - val_loss: 0.1999 - val_acc: 0.5932
Epoch 5/5
13513/13513 [==============================] - 4s - loss: 0.1998 - acc: 0.6173 - val_loss: 0.2015 - val_acc: 0.5999
1669/1669 [==============================] - 0s
Test loss and accuracy: [0.24841217570779137, 0.54463750750737105]

Проблема классификации. RNN

model = Sequential()
model.add(LSTM(input_shape = (EMB_SIZE,), input_dim=EMB_SIZE, output_dim=HIDDEN_RNN, return_sequences=True))
model.add(LSTM(input_shape = (EMB_SIZE,), input_dim=EMB_SIZE, output_dim=HIDDEN_RNN, return_sequences=False))
model.add(Dense(2))
model.add(Activation('softmax'))
model.compile(optimizer='adam', 
  loss='binary_crossentropy', 
  metrics=['accuracy'])

Train on 13513 samples, validate on 1502 samples
Epoch 1/5
13513/13513 [==============================] - 18s - loss: 0.2130 - acc: 0.5988 - val_loss: 0.2021 - val_acc: 0.5992
Epoch 2/5
13513/13513 [==============================] - 18s - loss: 0.2004 - acc: 0.6142 - val_loss: 0.2010 - val_acc: 0.5959
Epoch 3/5
13513/13513 [==============================] - 21s - loss: 0.1998 - acc: 0.6183 - val_loss: 0.2013 - val_acc: 0.5959
Epoch 4/5
13513/13513 [==============================] - 17s - loss: 0.1995 - acc: 0.6221 - val_loss: 0.2012 - val_acc: 0.5965
Epoch 5/5
13513/13513 [==============================] - 18s - loss: 0.1996 - acc: 0.6160 - val_loss: 0.2017 - val_acc: 0.5965
1669/1669 [==============================] - 0s
Test loss and accuracy: [0.24823409688551315, 0.54523666868172693]

Выводы

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

Что меня удивило, так это то, что MLP лучше обрабатывают данные последовательности, чем CNN или RNN, которые должны лучше работать с временными рядами. Я объясняю это довольно маленьким набором данных (~ 16 тыс. временных меток) и выбором плохих гиперпараметров.

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

Я думаю, что мы можем получить лучшие результаты как в регрессии, так и в классификации, используя различные функции (не только масштабированные временные ряды), такие как некоторые технические индикаторы, объемы торгов. Также мы можем использовать более частые данные, скажем, минутные, чтобы получить больше обучающих данных. Все эти вещи я собираюсь сделать позже, так что следите за обновлениями :)

1 комментарий: