Нейросети (на примере трансформеров) на фондовом рынке. Коды, «граали», финансовый результат

635e9d05a721984e3c08c85c5b9a540b.jpg

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

Занявшись машинным обучением, у меня долгое время ничего не получалось практически полезного, хотя я знал какие признаки лучше подавать и в каком месте искать прибыль, но «комбинаторика на коленках» напрямую оптимизирующая финансовый результат оказывалась лучше, чем результаты через оптимизацию функции потерь в моделях ML. Получалось вроде неравенства «мой опыт» > «классические модели ML» > «нейросети». Вердикт был неутешительным, и с точки зрения практического трейдинга, особой перспективы я уже не видел, но в один прекрасный день у меня в нейросеть на фондовом рынке получилось. Говоря получилось, я имею в виду получение нового полезного опыта, который я могу применить в реальном трейдинге. Это полезное заключалось в нахождении зависимостей между признаками, которые я с помощью тестирования идей в WealthLab вряд ли смог бы найти. Когда я тестирую идея как алготрейдер, я беру какую то идею (свою, увиденную в интернете) и с помощью нескольких показателей ее описываю, после чего смотрю на финансовый результат. А теперь представим что существует неэффективность на рынке, которую можно увидеть, если смоделироват плавную динамику изменения какого показателя, например в виде параболы (например на 20 барах RSI принимает форму параболы вершиной вверх). Как алготрейдер я так смоделировать не смогу, нейросеть такую параболу построить сможет.

Моим входным билетом в «нейросети не так уж и бесполезны на фондовых рынках» оказались трансформерами. Интересная архитектура, очень сейчас модная. Появившись для решения задач машинного обучения, затем как часто это бывает в ML, такую нейросеть стали применять для решения других задач, на других данных, в частности на временных рядов. Данная архитектура расширяет свои степени свободы, позволяя признакам выражать себя через другие признаки. Звучит странно. Очень заманчиво рассказать о архитектуре трансформеров используя краcивые картинки нарисованные не мною, и без того мой текст страдает отсутствием какой то яркой палитры, которая привлекает читателей как цветные фломастеры детей. Но мы пойдем другим путем — аналогиями разной степени сомнительности. Когда я читал разбор трансформеров у меня в голове нарисовался такой вот пример — человек хочет получить представление о том как он выглядит, но его зеркало треснуло. Он подходит к зеркалу и фотографирует с него свое искаженное отражение. Еще у него четкая фотография себя 20 летней давности, фотографии его родителей в его нынешнем возрасте, и соседей. Все эти фотографии, представляющие собой вектора, мы выражаем через другие вектора key, query, volume, полученные через умножение векторов на матрицы Key, Query, Volume. Вектора key, query нужны чтобы каждая фотография могла измерить свою близость с другой. Это такая абстракция, в которой фотография посылает запрос и сверяет свое сходство с запрашиваемой фотографией через скалярное произведение key и query. Полученное число умножается на volume, и чем больше сходства между двумя фотографиями тем с большим весом мы берем volume. Теперь вместо начальных фотографий мы имеем новые фотографии, которые могут быть шире, уже, которые могут могут представлять себя как усреднение других фотографий, а могут сохранить свой первоначальный вид итп итд. А теперь ответ на вопрос который возникает когда не очень понятно — «а зачем все это нужно?». Представим что в процессе обучения, нейросеть через подгонку матриц Key, Query, Volume может каждый признак представить через самого себя и через другие признаки с какими то весами, и эти веса настраиваются в процессе обучения. Мы даем нейросети возможность игнорировать ненужные признаки, мы позволяем выражать нейросети одни признаки через другие (учитывать контекст), мы устраняем шум в признаке, восстанавливая его через другие признаки, и наоборот, выражая признак через самого мы подтверждаем важность признака вне контекста. Пример фотографий я привел чтобы показать как все наши признаки могут быть взаимосвязаны друг с другом, образую контекст. Фотография 1 это и есть лучший целевой таргет, но он сильно зашумлен, есть хорошая фотография 2, но 20 летней давности, есть фотографии 3 — родителей в его возрасте и есть фотографии соседей, которые не имеют ценности. И мы «восстанавливаем» первую фотографию по фотографиям 1, 2 и 3, фотографию 2 по 1, 2 и 3 беря их с разными весами.

Вообще, какая истории как из необходимости решать задачу машинного перевода мы докатились до трансформеров. При машинном переводе у нас слева стоит предложение на 1 языке, справа на другом, и нужно найти алгоритм который успешно справится с переводом, при этом на входе и выходе число слов в предложении может не совпадать. То есть это модель SeqtoSeq. Логично решать ее с помощью рекуррентных сетей, которые считывают данные как человек читает предложение — последовательно, с каждым новым словом учитывая контекст ранее прочитанных слов. А предложение сами понимаете, имеет  контекст — каждое отдельно взятое слово можно выразить через самого себя, но и через другие слова в данном предложении. И вот пробежавшись по всему предложению от первого слова до последнего, наша рекуррентная сеть — кодер, получает какое то скрытое представление h которое передается декодеру который из этого скрытого состояния, слово за словом разматывает его в предложение, но уже на другом языке. Затем додумались, что нужно использовать не только скрытое состояние после прочтения кодером всего предложения, но и скрытые состояния после прочтения каждого нового слова и назвали этот механизм attention. Предоставили нейросети возможность определять какой скрытое состояние ей важней для перевода. Затем вышла статья что мол «All you need is love», ну то есть «Attention is all you need» в котором вообще отказались от рекуррентных нейросетей,  перейдя к полносвязным слоям (кто то иронично называет это реваншем полносвязных слоев), назвав все это transformer. Но идея что в рекурентных сетях с attention, что в трансформерах одна — мы даем возможность нейросети творчески поработать с нашими признаки. Впрочем может мое понимание трансформеров не совсем верное, так что с удовольствием выслушаю замечания, поправки, комментарии.

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

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

2. Трансформеры в процессе обучения могут позволить себе забыть ненужные признаки, сделать более важными нужные, убрать шум из них. На рынке полезных признаков на самом деле очень мало, и они сильно зашумлены. Подав много много признаков неплохо иметь возможность аппроксимировать данные убирая ненужные или выражая их через другие признаки, тем самым очищая их от шума.

3. Идея с attention появилась из машинного перевода, где на вход подают предложение, в котором каджый ее элемент занимает строго определенное место. Мы не можем произвольно выкинуть слово из предложение или поменять его местами. На рынке тоже существуют такие логические блоки — «предложения», состоящий из элементов связанные между собой и которые лучше не путать, если вы хотите получить результат. Например день, неделя итп итд. Из-за дня в день воспроизводится нечто похожее, например объёмы торгов в первый час и последний час максимальные. Подозреваю что у тех у кого не получилось в трансформеры на фондовой бирже упускают этот момент.

4. В трансформерах несколько слоев MultiHeadAttention, каждая из которых имеет свои начальные веса при инициализации матриц Key, Query, Volume. Можно представить что мы аппроксимируем исходя из разных контекстов. Как в предложении может быть несколько контекстов, так и на фондовой бирже признаки могут взаимоджейстовать друг с другом в разных контекстах — долгосрочных, краткосрочных.

Немножко кода. Есть готовые реализации трансформера для временных рядов, я пользовался написанной на Keras. Все весьма лаконично и хорошо оптимизированно для обучения, в том числе на GPU. Предпочитающие pytorch так же без труда найдут реализации, которые можно подправить для своих нужд. В реализации на Keras крайней выступает функция build_model которую я запускаю в следующем коде:

n_classes = len(np.unique(y_train))
matrix_all = pd.DataFrame()
epochs = 100
patience = 10
start_year = 2011
end_year = 2022

for year_from in range(start_year, end_year, 1):  
  
  feature_multy, x_train, y_train, x_test, y_test  = reshape_concat_of_split_1year(df_data, feature_name=feature_name, year_from = year_from)
  input_shape = x_train.shape[1:]
  
  checkpoint_filepath = '/content/drive/MyDrive/Colab Notebooks/ROBO/My_Feature_from_NET/keras/keras_from10_1830to60min/40/Gep/'\
                         + model_param + '_' + shape_set + '/' + str(year_from)
  print(checkpoint_filepath)
  print(f'Test year: {year_from}')

  checkpoint = ModelCheckpoint(filepath=checkpoint_filepath, 
                              monitor='val_accuracy',
                              verbose=1, 
                              save_best_only=True,
                              mode='auto')
  
  EarlyStopping = keras.callbacks.EarlyStopping(monitor='val_accuracy', patience = patience, restore_best_weights=True)
  model = build_model(
      input_shape,
      head_size=40,
      num_heads=8,
      ff_dim=4,
      num_transformer_blocks=4,
      mlp_units=[128],
      mlp_dropout=0.4,
      dropout=0.25,
  )

  model.compile(
      loss="sparse_categorical_crossentropy",
      optimizer=keras.optimizers.Adam(learning_rate=1e-4),
      metrics=['sparse_categorical_accuracy', 'accuracy'],
  )
  model.summary()

  callbacks = [checkpoint, EarlyStopping]

  model.fit(
      x_train,
      y_train,
      validation_split=0.2,
      epochs=epochs,
      batch_size=64,
      callbacks=callbacks)

  model.evaluate(x_test, y_test, verbose=1)

  matrix_year = df_data[(df_data['Date'].dt.year == year_from)].copy()
  matrix_year[proba_1] = model.predict(x_test)[:,-1]
  matrix_all = matrix_all.append(matrix_year)
  print(matrix_all.shape, matrix_year.shape)

Тут в цикле по годам происходит обучение трансформера — на каждом новом цикле, какой то год становится out-sample, по которому мы делаем прогноз, записывая его в matrix_year. Оставшиеся годы разбиваются в пропорции 80 на 20 на test и train. Обучение длится пока на test идет какое то улучшение по метрике accuracy ('val_accuracy'), c patience = 10. Полученные matrix_year мы конкатенируем в matrix_all, с которой и будем работать для оценки эффективности модели. Функция reshape_concat_of_split_1year, это своего рода train_test_split из sklearn, но разбивающая по годам и попутно преобразующий данные в матрицу number_features*sequence_feature. Я приведу два варианта кода, один быстрый, но из разряда «смотри не перепутай Кутузофф, меняя размерности в numpy», и по которому ясно как мы образуем наши данные в виде матрицы number_feature*feature_sequence:

#Первый вариант
def reshape_concat_of_split_1year(df_data, feature_name, year_from):

  x_train_reshape = df_data[(df_data.Date.dt.year != year_from)][feature_name]
  x_train_reshape = np.array(x_train_reshape).reshape(-1, len(features), bars_in_day)
  x_train_reshape = x_train_reshape.transpose(0,2,1)

  x_test_reshape = df_data[(df_data.Date.dt.year == year_from)][feature_name]
  x_test_reshape = np.array(x_test_reshape).reshape(-1, len(features), bars_in_day)
  x_test_reshape = x_test_reshape.transpose(0,2,1)
  
  y_train = np.array(df_data[(df_data.Date.dt.year != year_from)][name_profit_label])
  y_test = np.array(df_data[df_data.Date.dt.year == year_from][name_profit_label])

  feature_multy = np.concatenate([x_train_reshape, x_test_reshape], axis = 0)

  return feature_multy, x_train_reshape, y_train, x_test_reshape, y_test
#Второй вариант
def concat_of_split_1year(df_data, year_from, axis = 2, lag = 2):
  fr = 0
  to = bars_in_day
  
  profit_label = df_data[name_profit_label]
  profit_pr = df_data[name_profit]
  name_feature = feature_name

  feature1 = [str('feature1') + '_' + str(i) for i in range(fr, to)] 
  feature2 = [str('feature2') + '_' + str(i) for i in range(fr, to)] 
  feature3 = [str('feature3') + '_' + str(i) for i in range(fr, to)] 
  feature4 = [str('feature4')  + '_' + str(i) for i in range(fr, to)]

  x_train = np.zeros(shape = np.expand_dims(df_data[(df_data.Date.dt.year != year_from)][feature1], axis = axis).shape)
  x_test  = np.zeros(shape = np.expand_dims(df_data[df_data.Date.dt.year == year_from][feature1], axis = axis).shape)

  for i in [feature1,feature2, feature3, feature4]:
    for_add_test = np.expand_dims(df_data[df_data.Date.dt.year == year_from][i], axis = axis)
    x_test = np.concatenate([x_test, for_add_test], axis = axis)
  y_test = np.array(df_data[df_data.Date.dt.year == year_from][name_profit_label])

  for i in [feature1,feature2, feature3, feature4]:
    for_add_train = np.expand_dims(df_data[(df_data.Date.dt.year != year_from)][i], axis = axis)
    x_train = np.concatenate([x_train, for_add_train], axis = axis)
  y_train = np.array(df_data[(df_data.Date.dt.year != year_from)][name_profit_label])

  x_train = x_train[:,:,1:]
  x_test  =  x_test[:,:,1:]
  feature_multy = np.concatenate([x_train, x_test], axis = 0)

  print(f'Учим по: {name_profit_label}, Взят от: {name_profit}, С помощью: {feature_name}')
  display(feature_multy.shape, x_train.shape, x_test.shape, y_train.shape, y_test.shape)
  return feature_multy, x_train, y_train, x_test, y_test

© Habrahabr.ru