RNN для алгоритмической торговли

Рекуррентные нейронные сети (RNN) часто используются для прогнозирования временных рядов и последовательных данных, поскольку они могут эффективно фиксировать временные зависимости и закономерности, присутствующие в таких данных.

Вот несколько причин, по которым RNN хорошо подходят для этих задач:

  1. Возможность работы с последовательными данными: RNN предназначены для обработки последовательных данных, что означает, что они могут обрабатывать данные в последовательностях, таких как данные временных рядов, звуковые сигналы и текст на естественном языке. Они также могут работать с входными данными переменной длины, что делает их хорошим выбором для моделирования данных временных рядов.
  2. Память о прошлых входах: RNN имеют компонент памяти, который позволяет им запоминать прошлые входные данные и использовать эту информацию для прогнозирования будущего. Это особенно полезно для прогнозирования временных рядов, поскольку исторические данные часто содержат информацию, которая может помочь предсказать будущие тенденции.
  3. Формы ввода-вывода и ввода-вывода: Гибкость входных и выходных размеров
    RNN могут обрабатывать входы и выходы с переменными размерами, что делает их полезными при обработке различных типов данных временных рядов, таких как многомерные данные временных рядов.
  4. Обратное распространение: Обучение с обратным распространением во времени
    RNN могут быть обучены с помощью обратного распространения во времени, что позволяет им учиться на исторических данных и обновлять свои параметры на основе этой информации. Это позволяет RNN адаптироваться к изменениям в данных и улучшать свои прогнозы с течением времени.
  5. Архитектурные вариации: Существует множество архитектурных вариантов RNN, которые были разработаны для решения конкретных задач прогнозирования временных рядов и последовательной обработки данных, таких как сеть длинной краткосрочной памяти (LSTM) и закрытые рекуррентные блоки (GRU). Эти архитектуры предназначены для улучшения способности RNN захватывать долгосрочные зависимости в данных и смягчать проблему исчезающего градиента, которая может возникнуть при обучении глубоких нейронных сетей.

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

Реализация:

  1. Импорт модулей/пакетов/библиотек Python
#Importing Python Modules/Packages

import os
from pathlib import Path
from math import *
import numpy as np
import pandas as pd
import pandas_datareader.data as web
from datetime import datetime, timedelta
import statsmodels.api as sm
import matplotlib.pyplot as plt
import pandas_datareader as pdr
import seaborn as sns

from scipy.stats import spearmanr, norm
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import MinMaxScaler
import scipy


import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM
from tensorflow import keras

2. Импорт данных по индексному фонду S&P 500

snp500 = web.DataReader('SP500', 'fred', start='2013', end='2024').dropna()
ax = snp500.plot(title='S&P 500 Index Fund', figsize=(20, 8), c='b')
ax.set_xlabel('Time')
ax.set_ylabel('Price $')
sns.despine()

3. Scikit изучает предварительную обработку: преобразование скейлера MinMax

snp500.head()

scaler = MinMaxScaler() # The default scale for the MinMaxScaler is to rescale variables into the range [0,1]
sp500_scaled = pd.Series(scaler.fit_transform(snp500).squeeze(), index=snp500.index) # transform(X) will Scale features of X according to feature_range.

sp500_scaled.describe()

4. Создадим функцию входного набора данных для нашей модели RNN:

def data_for_rnn(data, window_size):
n = len(data)
y = data[window_size:]
print("previous data shape", data.shape)
data = data.values.reshape(-1, 1) # make 2D
print("updated data shape", data.shape)
X = np.hstack(tuple([data[i: n-j, :] for i, j in enumerate(range(window_size, 0, -1))]))
print(X.shape, y.shape)
return pd.DataFrame(X, index=y.index), y

window_size = 50
X, y = data_for_rnn(sp500_scaled, window_size=window_size)

5. Определимся с разделением обучающих и тестовых данных:
Мы можем обучить модель до 2021 года и протестировать ее на данных 2022 года.

X_train = X[:'2021'].values.reshape(-1, window_size, 1)
y_train = y[:'2021']

# keep the last year for testing
X_test = X['2022'].values.reshape(-1, window_size, 1)
y_test = y['2022']

print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
# >> (2161, 50, 1) (2161,) (251, 50, 1) (251,)

n_obs, window_size, n_features = 2161, 50, 1

6. Определение модели RNN:

# Define a Sequential model
rnn = Sequential([
# Add an LSTM layer with 10 units or neurons, input_shape of (window_size, n_features), and name 'LSTM'
LSTM(units=10, input_shape=(window_size, n_features), name='LSTM'),
# Add a Dense layer with 1 neuron and name 'Output'
Dense(1, name='Output')
])

# Print the summary of the model
print(rnn.summary())

# Create an RMSprop optimizer with a learning rate of 0.001, rho of 0.9, and epsilon of 1e-08
optimizer = keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9, epsilon=1e-08)

# Compile the model with a mean squared error loss function and the RMSprop optimizer created above
rnn.compile(loss='mean_squared_error', optimizer=optimizer)

# Define the path to save the model results
results_path = Path('/content/drive/MyDrive/HFT/data/results', 'univariate_time_series') # Tested on colab, so created a repo in my GDrive

# Define the path to save the best performing weights of the RNN model
rnn_path = (results_path / 'rnn.h5').as_posix()

# Create a ModelCheckpoint callback that saves the weights of the RNN model to rnn_path if validation loss improves
checkpointer = ModelCheckpoint(filepath=rnn_path, verbose=1, monitor='val_loss', save_best_only=True)

# Create an EarlyStopping callback that stops training if validation loss does not improve for 40 epochs, and restore the best performing weights
early_stopping = EarlyStopping(monitor='val_loss', patience=40, restore_best_weights=True)

Пояснение к коду:
Код создает модель рекуррентной нейронной сети (RNN) с использованием библиотеки Keras.

Модель RNN состоит из двух слоев:

  1. Слой LSTM: Слой LSTM состоит из 10 единиц или нейронов и принимает входные данные в форме (window_size, n_features). Здесь window_size представляет количество временных шагов, которые необходимо учитывать для слоя LSTM, а n_features представляет количество объектов на каждом временном шаге.
  2. Плотный слой: Выход слоя LSTM подается на плотный слой одним нейроном. Этот слой генерирует одно выходное значение.

Класс Sequential используется для определения линейного стека слоев. В этом случае слой LSTM является первым слоем в стеке, за которым следует плотный слой.

Параметр name является необязательным и используется для присвоения имен каждому слою модели. Это может быть полезно при отладке или визуализации модели.

  1. optimizer: Он создает объект оптимизатора, который является оптимизатором RMSprop со скоростью обучения 0,001, значением rho 0,9 и значением epsilon 1e-08. Этот оптимизатор будет использоваться для обновления весов нейронной сети во время обучения.
  2. rnn.compile(): Он компилирует модель RNN, что означает установку функции потерь и оптимизатора, который будет использоваться во время обучения. В этом случае используется функция потери среднего квадрата ошибки, а созданный ранее объект оптимизатора используется для оптимизации весов модели.
  3. checkpointer: Он создает обратный вызов, который сохраняет веса модели RNN в указанном пути к файлу (rnn_path) после каждой эпохи, только если потеря проверки улучшилась. verbose задано значение 1 для отображения обновлений хода выполнения во время обучения.
  4. early_stopping: Он создает обратный вызов, который останавливает процесс обучения, если потеря проверки не улучшается в течение определенного количества эпох (параметр patience). Весовые коэффициенты модели восстанавливаются до наиболее эффективной эпохи restore_best_weights если для параметра True задано значение True.

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

7. Давайте тренируем его

 lstm_training = rnn.fit(X_train,
y_train,
epochs=200,
batch_size=10,
shuffle=True,
validation_data=(X_test, y_test),
callbacks=[early_stopping, checkpointer],
verbose=1)
#>> Epoch 200: val_loss did not improve from 0.00036

Пояснение к коду:

  • lstm_training = rnn.fit(): Он обучает rnn с помощью fit() Возвращаемый lstm_training содержит сведения о процессе обучения, такие как потери и точность в каждую эпоху.
  • X_train, y_train y_train: Это входные данные обучения (X_train) и соответствующие целевые значения (y_train) для модели LSTM.
  • epochs=200: указывает, сколько раз модель обучается на всем наборе обучающих данных (X_trainy_train).
  • batch_size=10: Здесь указывается количество образцов, которые должны быть обработаны в каждом обучающем пакете. Здесь одновременно обрабатывается 10 образцов.
  • shuffle=True: указывает, следует ли перетасовывать обучающие данные перед каждой эпохой. Установив для него значение True, порядок выборок рандомизируется в каждой эпохе.
  • validation_data=(X_test, y_test), y_test): определяет набор данных проверки (X_testy_test), который будет использоваться для мониторинга производительности модели во время обучения.
  • callbacks=[early_stopping, checkpointer]: указывает обратные вызовы, которые будут использоваться во время обучения. Обратный вызов early_stopping останавливает процесс обучения, если потеря проверки не улучшается в течение 40 эпох и восстанавливает наиболее эффективные веса. Обратный вызов checkpointer сохраняет наиболее эффективные веса модели в файл.
  • verbose=1: определяет уровень ведения журнала во время обучения. Здесь установлено значение 1 для отображения обновлений прогресса во время обучения.

В целом, код обучает модель LSTM, используя X_train и y_train для 200 эпох с размером пакета 10. Производительность модели отслеживается с помощью X_test и y_test данных проверки, а наиболее эффективные веса сохраняются в файл с помощью обратного вызова checkpointer. Если потеря валидации не улучшается в течение 40 эпох, тренировка прекращается досрочно, а наиболее эффективные веса восстанавливаются с помощью обратного вызова early_stopping.

8. Метрика RMSE для обучения и тестирования

train_rmse_scaled = np.sqrt(rnn.evaluate(X_train, y_train, verbose=0))
test_rmse_scaled = np.sqrt(rnn.evaluate(X_test, y_test, verbose=0))
print(f'Train RMSE: {train_rmse_scaled} | Test RMSE: {test_rmse_scaled:}')

#>> Train RMSE: 0.0096 | Test RMSE: 0.0191

9. Ранговая корреляция Спирмена

Ранговая корреляция Спирмена — это непараметрический статистический метод, который измеряет силу и направление связи между двумя переменными, не предполагая линейной зависимости или нормального распределения. Метод включает в себя ранжирование значений двух переменных и расчет коэффициента корреляции на основе рангов. Результирующий коэффициент корреляции колеблется от -1 до 1, при этом отрицательные значения указывают на отрицательную корреляцию, ноль указывает на отсутствие корреляции, а положительные значения указывают на положительную корреляцию между двумя переменными.

train_predict_scaled = rnn.predict(X_train)
test_predict_scaled = rnn.predict(X_test)

# Spearman rank correlation is a non-parametric test that is used to measure the degree of association between two variables.

train_ic = spearmanr(y_train, train_predict_scaled)[0]
test_ic = spearmanr(y_test, test_predict_scaled)[0]
print(f'Train IC: {train_ic} | Test IC: {test_ic}')

#>> Train IC: 0.9985345555974086 | Test IC: 0.9720402200720925

10. Построение графика результатов:

fig, ax = plt.subplots(figsize=(20, 10))

loss_history = pd.DataFrame(lstm_training.history).pow(.5)
loss_history.index += 1
best_rmse = loss_history.val_loss.min()

best_epoch = loss_history.val_loss.idxmin()

title = f'(Best Validation RMSE: {best_rmse} for window length of 50, testing on 2022 s&P500)'
loss_history.columns=['Train RMSE', 'Validation RMSE']
loss_history.rolling(5).mean().plot(logy=True, lw=2, title=title, ax=ax)

ax.axvline(best_epoch, ls='-.', lw=1, c='r')
ax.set_xlabel('Epochs')
ax.set_ylabel('RMSE')
sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'rnn_sp500_error', dpi=300);

Изменение масштаба до исходных значений

train_predict = pd.Series(scaler.inverse_transform(train_predict_scaled).squeeze(), index=y_train.index)
test_predict = (pd.Series(scaler.inverse_transform(test_predict_scaled).squeeze(), index=y_test.index))

y_train_rescaled = scaler.inverse_transform(y_train.to_frame()).squeeze()
y_test_rescaled = scaler.inverse_transform(y_test.to_frame()).squeeze()

train_rmse = np.sqrt(mean_squared_error(train_predict, y_train_rescaled))
test_rmse = np.sqrt(mean_squared_error(test_predict, y_test_rescaled))
print(f'Train RMSE: {train_rmse} | Test RMSE: {test_rmse}')
#>>Train RMSE: 31.162661786164964 | Test RMSE: 62.09228035778334

snp500['Train Predictions'] = train_predict
snp500['Test Predictions'] = test_predict
snp500 = snp500.join(train_predict.to_frame('predictions').assign(data='Train') .append(test_predict.to_frame('predictions').assign(data='Test')))

Давайте построим резюме:

fig, ax = plt.subplots(figsize=(20, 10))
snp500.loc['2014':, 'SP500'].plot(lw=4, ax=ax, c='k')
snp500.loc['2014':, ['Test Predictions', 'Train Predictions']].plot(lw=1, ax=ax, ls='--')
ax.set_title('Predictions')
'''
It plots the values of the 'SP500' column of the snp500 DataFrame,
starting from the year 2014 with a linewidth of 4 and color black.
Additionally, it plots the values of the 'Test Predictions' and 'Train Predictions'
columns of the snp500 DataFrame, also starting from the year 2014, with a
linewidth of 1 and linestyle dashed.
'''

fig, ax3 = plt.subplots(figsize=(20, 10))
sns.scatterplot(x='S&P500', y='predictions', data=snp500, hue='data', ax=ax3)
ax3.text(x=.02, y=.95, s=f'Test IC ={test_ic}', transform=ax3.transAxes)
ax3.text(x=.02, y=.87, s=f'Train IC={train_ic}', transform=ax3.transAxes)
ax3.set_title('Correlation Plot')
ax3.legend(loc='lower right')


'''

It creates a scatter plot using sns.scatterplot() with the 'S&P500'
column of the snp500 DataFrame as the x-axis, the 'predictions' column
as the y-axis, and the 'data' column as the hue. The hue parameter is
used to distinguish between the training and testing data points. The code
then adds text to the plot using ax3.text(), displaying the test and train IC
(information coefficients) with their respective values calculated elsewhere
in the code. It sets the title of the plot to 'Correlation Plot' using
ax3.set_title() and adds a legend to the lower right of the plot using ax3.legend().

'''

fig, ax2 = plt.subplots(figsize=(10, 5))
fig, ax4 = plt.subplots(figsize=(10, 5))

ax4 = plt.subplot(sharex = ax2, sharey=ax2)
sns.distplot(train_predict.squeeze()- y_train_rescaled, ax=ax2)
ax2.set_title('Training Error')
ax2.text(x=.03, y=.92, s=f'Train RMSE ={train_rmse}', transform=ax2.transAxes)
sns.distplot(test_predict.squeeze()-y_test_rescaled, ax=ax4)
ax4.set_title('Testing Error')
ax4.text(x=.03, y=.92, s=f'Test RMSE ={test_rmse}', transform=ax4.transAxes)

Абстрактный:

RNN (рекуррентные нейронные сети) хорошо подходят для прогнозов алгоритмической торговли по нескольким причинам:

  1. Анализ временных рядов: RNN особенно хороши при анализе последовательных данных, таких как данные временных рядов, что распространено в финансах и торговле. Они могут фиксировать временные зависимости между точками данных, что может помочь в прогнозировании будущих цен на акции или тенденций.
  2. Память: RNN имеют возможность хранить информацию из прошлых входных данных, что особенно полезно в торговых прогнозах. Запоминая прошлые данные, они могут обнаруживать закономерности и изменения на рынке, что может быть полезно для принятия торговых решений.
  3. Нелинейное отображение: RNN могут выполнять нелинейное отображение между входами и выходами, что полезно в финансовых прогнозах, где отношения между переменными часто нелинейны.
  4. Гибкость: RNN можно обучать на различных типах данных, включая временные ряды, изображения и текст. Такая гибкость делает их подходящими для различных задач прогнозирования торговли.

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

=============================================================

Спасибо!
Кодовая база: https://github.com/Saad2714/Quant-Research-Algorithms LinkedIn: https://www.linkedin.com/in/patelsaadn/

Источник