Снижение размерности финансовых временных рядов и построение индексов

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

Заявление об ограничении ответственности: исследование, представленное в этой статье, является результатом нашего зимнего семестрового проекта 2019 года для Курса глубокого обучения в Школе непрерывного обучения Университета Торонто. Это было сделано в сотрудничестве с Humberto Ribeiro de Souza. Концепции и идеи - наши собственные. Мы никоим образом не представляем наших нынешних или предыдущих работодателей.

Часть 1: Уменьшение размерности с помощью вариационного автокодировщика

В этом разделе мы обсудим:

  • Создание набора данных геометрического скользящего среднего
  • Дополнение данных с помощью стохастического моделирования
  • Построение вариационной модели автоэнкодера
  • Получение прогнозов.

Создание набора данных геометрического скользящего среднего

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

Мы выбрали d = 5, поскольку это типичная торговая неделя продолжительностью 5 рабочих дней.

Набор данных, используемый в этой статье, содержит 423 временных ряда геометрических скользящих средних за период с 4 января 2016 года по 1 марта 2019 года.

Читатели могут выполнить шаги, описанные в Блокноте обработки данных, чтобы создать свой собственный набор данных. Он должен быть похож на этот:

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

Затем только что построенный фрейм данных можно разделить на два периода времени равной длины, транспонируя один только для первого периода. Период 1 длится с 12 января 2016 года по 4 августа 2017 года. Период 2 длится с 7 августа 2017 года по 1 марта 2019 года.

Мы будем использовать данные только за период 1 для получения прогнозов.

# Divide in two
geoMA_5d_stocks_p1 = geoMA_5d_stocks.head(int(len(geoMA_5d_stocks)/2))
geoMA_5d_stocks_p2 = geoMA_5d_stocks.tail(int(len(geoMA_5d_stocks)/2))
# Transpose the dataframe for period 1
geoMA_5d_stocks_p1_T = geoMA_5d_stocks_p1.T

Мы транспонируем фрейм данных так, чтобы каждая строка представляла временной ряд для данной акции:

Дополнение данных стохастическим моделированием

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

Синтетические кривые построены с использованием геометрического броуновского движения. Мы выполнили следующие шаги:

  • Используя фрейм данных первого периода, случайным образом выберите 100 тикеров.
  • Для каждого выбранного тикера вычислите вектор возвращаемых результатов журнала таким образом, чтобы:

  • Затем для каждого выбранного тикера мы сгенерируем 100 путей, таких как:

Вот образец смоделированной кривой и реальной кривой:

Мы расширили набор данных из 423 временных рядов до 100 * 100 = 10 000 новых временных рядов, аналогичных (но не равных) набору данных акций.

Это позволит нам сохранить фактический набор данных по запасам для прогнозов и даже не использовать его для проверки.

Перед построением модели VAE создайте обучающий и тестовый наборы (используя соотношение 80% -20%):

# Shuffle the generated curves
shuffled_array = np.random.permutation(sim_paths_matrix)
# Split the simulated time series into a training and test set
x_train = shuffled_array[0:8000]
x_test = shuffled_array[8000:]

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

Построение модели вариационного автоэнкодера (VAE)

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

Автоэнкодеры - это неконтролируемые алгоритмы, используемые для сжатия данных. Они построены с кодировщиком, декодером и функцией потерь для измерения потерь информации между сжатыми и распакованными представлениями данных.

Наша цель - не написать еще одну статью об автоэнкодере. Читатели, не знакомые с автоэнкодерами, могут прочитать больше в Блоге Кераса и в статье Дидерика Кингмы и Макса Веллинга Вариационное байесовское кодирование.

Мы будем использовать простую архитектуру VAE, аналогичную описанной в блоге Keras.

Модель кодировщика имеет:

  1. Один входной вектор длиной 388
  2. Один промежуточный слой длиной 300 с функцией активации выпрямленного линейного блока (ReLu)
  3. Один кодировщик с двумя измерениями.

В расшифрованной модели есть:

  1. Один входной вектор двух измерений (выбранный из скрытых переменных)
  2. Один промежуточный слой длиной 300 с функцией активации выпрямленного линейного блока (ReLu)
  3. Декодированный вектор длиной 388 с сигмовидной функцией активации.

Приведенный ниже код адаптирован из varational_autoencoder.py команды Keras на Github. Он используется для построения и обучения модели VAE.

После обучения строим кривые потерь при обучении и проверке:

Получение прогнозов

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

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

# Obtaining the predictions:
encoded_p1 = encoder.predict(matrix_to_test_p1, batch_size=batch_size)
# Convert the predictions into a dataframe
encoded_p1_df = pd.DataFrame(data = encoded_p1, columns = ['x','y'], index = dataframe_to_test_p1.T.index)

Мы получили следующие результаты:

Перед нанесением результатов мы должны:

  1. Рассчитайте расстояние между точкой фьючерсного контракта и всеми другими акциями в фрейме данных.
  2. Выберите 50 пинт, ближайших к фьючерсному контракту.
# Calculate the distances between the futures contract point and all other points in the stocks dataset
ref_point = encoded_p1_df.loc['Futures'].values
encoded_p1_df['Distance'] = scipy.spatial.distance.cdist([ref_point], encoded_p1_df, metric='euclidean')[0]
# Get the 50 closest points:
closest_points = encoded_p1_df.sort_values('Distance', ascending = True)
closest_points_top50 = closest_points.head(51)[1:] #We take head(51), because the Futures reference point is the first entry
closest_points_top50['Ticker'] = closest_points_top50.index

Теперь мы можем построить график полученных результатов для визуализации ближайших 50 акций:

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

Часть 2: Построение индекса

Давайте воспользуемся результатами, полученными в Части 1, для создания индекса.

Из-за случайности модели VAE мы не сможем получить один и тот же точный список из 50 крупнейших акций при каждом запуске. Чтобы получить достоверное представление о ближайших 50 точках, мы запустим модель VAE 10 раз (повторно инициализируя и повторно обучая ее при каждом запуске). Затем мы возьмем 50 ближайших точек, найденных при каждом запуске, чтобы создать фрейм данных closest_points_df длиной 500.

После создания фрейма данных closest_points_df:

  1. Сортировать точки по расстоянию
  2. Отбросьте повторяющиеся тикеры, сохранив только первое вхождение
sorted_by_dist = results_df.sort_values('Distance', ascending = True)
sorted_by_dist.drop_duplicates(subset='Ticker', keep='first', inplace = True)

После удаления дубликатов мы сохраним только 50 ближайших точек.

Вычислите вес каждой акции

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

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

При работе с неанонимными данными о запасах важно отфильтровать полученные результаты перед вычислением веса запасов. Следует удалить выбросы и уточнить диапазон рыночной капитализации.

# Calculate the weights
top50 = sorted_by_dist.head(50).copy() # Keep the closest 50 points
top50['Weight'] = (1/top50['Distance'])/np.sum(1/top50['Distance'])

Подсчитайте количество акций каждой акции

После вычисления весов мы вычисляем количество акций каждой акции в нашем индивидуальном индексе. Нам нужно:

  • Узнайте цену каждой акции 4 января 2016 г. (первый день периода 1).
  • Определите сумму чистых активов
  • Подсчитайте количество акций
#Get the stock prices on January 4th 2016
jan4_2016_stockPrice = np.zeros(len(stock_data_top50.columns))
for i in range(len(jan4_2016_stockPrice)):
  if stock_data_top50.columns[i] == top50['Ticker'].iloc[i]:
    jan4_2016_stockPrice[i] = stock_data_top50[stock_data_top50.columns[i]].iloc[0]
top50['Price Jan4_2016'] = jan4_2016_stockPrice

# We compute the number of shares
net_assets = 10000000 # We chose net assets = 10 million (in the currency of the stock market)
numShares = np.zeros(len(stock_data_top50.columns))
for i in range(len(jan4_2016_stockPrice)):
  if stock_data_top50.columns[i] == top50['Ticker'].iloc[i]:
    numShares[i] = int(net_assets*top50['Weight'].iloc[i]/top50['Price Jan4_2016'].iloc[i])
    
top50['numShares'] = numShares

Создайте индекс

Для построения индекса мы будем использовать индекс Ласпейреса, рассчитанный как:

stock_index = np.zeros(len(stock_data_top50))
for i in range(len(stock_data_top50)):
  sum_num = 0
  sum_denom = 0
  for j in range(len(stock_data_top50.columns)):
    sum_num = sum_num + stock_data_top50[stock_data_top50.columns[j]].iloc[i]*top50['numShares'].iloc[j]
    sum_denom = sum_denom + stock_data_top50[stock_data_top50.columns[j]].iloc[0]*top50['numShares'].iloc[j]
  stock_index[i] = sum_num /sum_denom
# We arbitrarily start the index at 100
stock_index_df = pd.DataFrame(stock_index*100, columns =  ['stock_index'], index = stock_data_top50.index)

Строим полученный пользовательский индекс:

Сравните наш специальный индекс с временными рядами фьючерсов

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

  • Рассчитать дневное процентное изменение данных о ценах фьючерсов
  • Установите S_0 = 100
# Calculate the percentage change
futures_data_stock_data_pct_change = futures_data_stock_data.pct_change()
futures_data_stock_data_pct_change.dropna(inplace = True)
# Scale the time series
futures_theoretical = np.zeros(len(stock_index_df))
futures_theoretical[0] = stock_index_df.iloc[0]
for i in range(len(futures_theoretical)-1):
  futures_theoretical[i+1] = (1+futures_data_stock_data_pct_change.iloc[i])*futures_theoretical[i]

Теперь построим обе кривые на одном графике:

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

Пользовательский индекс вполне может превзойти фьючерсный индекс, если будут идентифицированы тикеры и удалены выбросы.

Мы призываем наших читателей воспользоваться бесплатными экземплярами GPU, доступными в Интернете, для создания собственных индексов. Для нас это был забавный эксперимент, и мы обнаружили несколько интересных моделей акций.

Не стесняйтесь загружать две записные книжки, доступные на GitHub:

  1. Проект глубокого обучения 3546 - Data Treatment.ipynb
  2. Проект глубокого обучения 3546 - VAE & Index Construction.ipynb

Заключение

Использование вариационных автоэнкодеров может ускорить разработку новых индексов на зарубежных фондовых рынках, даже если аналитики с ними не знакомы. Кроме того, могут быть созданы нишевые индексы или портфели, соответствующие интересам клиентов.

Хотя этот метод можно использовать для создания ETF, мы полагаем, что он также может создать новые инвестиционные возможности для компаний по прямому индексированию и Robo Advisors по всему миру.