1. Введение. Где я напишу как докатился до написания текстов.

    Перед написанием, я погуглил тексты на заданную тему, чтобы понять о чем и как люди пишут и какие отзывы получают. И вывод был, что пишут часто неконкретно. Очень много статьей так сказать базовых, в них повествование начинается с - "что такое акция", "что такое фундаментальный анализ", "что такое машинное обучение" итп итд, начиная с такого трудно дойти до чего то практически полезного. Мне как практику читать и писать такое мало интересно, хотя быть может для читателей - новичков это и познавательно. Я хочу написать пост от лица действующего трейдера, поэтому сразу опускаю разжевывание или пересказ прочитанного в книжках бывших трейдеров, околорыночные истории людей не торговавших или введение в теорию без всякой привязки к практике. Я попробую написать текст, который был бы мне самому интересно прочитать, много лет назад, после довольно бесполезных книжек о рынке и трейдинге и 2,5 года назад, когда я ничего не понимал в ML и программировании на Python. У каждого свой способ познания, я начинаю с повторения сделанного другими, с обязательной проверкой прочитанного, с последующими творческими переработками. Поэтому я выложу коды с разъяснениями - для того чтобы каждый мог проверить написанное, немножко теории ML - желательно на пальцах, немножко "Грааля" - чтобы было с чего начать торговать, и немножко философских рассуждений (в рамках разумного) что такое рынок - чтобы знать куда примерно двигаться и развиваться и некий переводчик общих рассуждений в практическую плоскость.

  1. Часть где я кратко изложу свою концепцию фондового рынка и правильной торговли.

    Для нахождения успешного алгоритма трейдинга необходимо иметь концепцию фондового рынка, и уже на его основе нужно определить технику прогнозирования, понять свои конкурентные преимущества, провести отбор признаков, выбрать правильное их представление, выбрать модель прогнозирования и его архитектуру. Фондовый рынок является высоко шумовым, с затяжными периодами случайного блуждания, сложной динамической системой. Однако в определенных ситуациях вероятность движения рынка становится  более определенным (используя язык нелинейной динамики все это можно описать через понятие: точка бифуркации, аттрактор, фазовое пространство). Перевожу на практический язык - те кто пытается спрогнозировать цену на любом рынке, по любому активу, в любое время - тратят впустую свое время. Поиск относительно редких ситуаций на фондовой бирже, которые описываются ограниченным набором признаков и обыгрывание которых предоставляет трейдеру статистическое преимущество для получения прибыли - вот чем следует заняться тем, кто хочет чтобы с годами у него рос счет а не диагональ монитора и живот. Даже используя производные от цен можно смоделировать очень многие ситуаций на рынке. Среди них есть относительно редкие, это "хвосты распределения" - какие то предельные случаи, и именно в этих хвостах существуют та самая прибыль которую можно получить и смею предположить, что она будет существовать там долго. В первую очередь те, в основе которых лежат объективные факторы - например ограничения в ликвидности. Наверно я даже опишу один алгоритм, который позволял работать в плюс и в 2006 (дальше не заглядывал) и 2007 и 2008 и далее по списку вплоть до 2021 года, спалю так сказать "маленький Грааль". Без цифр, без порогов, без признаков, без входов и выходов, просто логику. Вот представим ситуацию - Америка закрылась в плюс, все видят закрытие США, все хотят расти, как результат наш отечественный рынок на открытии показал хороший ГЭП вверх. Но это общее желание отыграть рост упирается в объективное ограничение в виде ликвидности - не все могут набрать позицию, прежде всего я говорю о крупных игроках и тех кого давит жаба входить по цене которая вчера была гораздо ниже. Что последует после этого?! Ваши варианты?! Кто хочет может протестировать. Можно ли описать эту ситуацию с помощью производных от цен?! Легко. Данный алгоритм я нашел конечно не так, я нашел его вслепую перебирая варианты, долго, упорно, а понимание почему это работает пришло гораздо позже. И нейросеть, пусть она всего лишь калькулятор, может быть полезной в этой черновой работе. Не надо подгонять реальность под прочитанное в книжках, находите закономерности это и есть реальность, и объясняя эту реальность у вас появится понимание как этот рынок работает и где лежит прибыль. И тогда вы перестанете пытаться прогнозировать цену завтра, подавая на входу цены вчера.

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

    Занявшись машинным обучением, у меня долгое время ничего не получалось практически полезного, хотя я знал какие признаки лучше подавать и в каком месте искать прибыль, но "комбинаторика на коленках" напрямую оптимизирующая финансовый результат оказывалась лучше, чем результаты через оптимизацию функции потерь в моделях 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
  2. Часть для любителей вопросов о «таймфреймах, на чем обучал, какие акции, что в качестве целевого признака, какие параметры, время удержании позиции».

    Акции МосБиржы, из числа наиболее ликвидных. Ликвидность по акциям очень сильно менялась, поэтому прикручен фильтр по обьему торгов, в размере не менее 300 млн рублей на предыдущей сессии. Таймфремы не важны так как нейросеть может обучиться хорошо на разных внутридневных таймфремах. Я представлю результаты полученные на часовиках. Задачу решал классификации, можно было и регрессии (но конечно не цены, а return), практика показывает что это все не суть, можно так и так. Направление движения на какой период вперед — они могут быть разными, но на ночь сделку я переносил, уж извините, значительная часть динамики на российском рынке заключена в утреннем гэпе и эта доля увеличивается. До перебора параметров модели я не дошел, взяв параметры модели "из коробки". О параметрах MultyHeadAttantion - сердцевины трансформеров. Тут мы говорим о размерность Key, Query, Value, числе HeadAttantion и EncoderLayer. Это все что касается только части механизма attantion, дальше идут полносвязные слои у которых свои параметры. Так что настраивать тут можно долго и упорно. Кое что в плане перебора я сделал: существует 3 варианта в ансамблевых подходе: bagging, boosting, stacking. Я простакал модели обученные на разных наборах признаков, каждый раз получая отличающиеся результаты. Да, если кто то думает что если можно в нейросеть засунуть много много признаков, и нейросетка сама разберется, так вот - до конца не разберется, даже при всех описанных преимуществах трансформера. Говоря о stacking мы касаемся еще одного момента который многие не допонимают. Я не буду рисовать график цены акции и свой прогноз, возможность такого прогноза противоречит самой природе фондового рынка. Прогнозировать как цена будет двигаться в следующие n баров - это хуже чем глупо и невозможно, это не нужно. Нам не нужен прогноз для каждой фишки, нам не нужен прогноз в любой момент времени, нам нужно с помощью моделей находить редкие, устойчивые паттерны увеличивая число дней в году когда мы в рынке и повышая среднюю доходность в эти день. Вот откуда stacking. Результаты стаканья можно использовать увеличивая число сделок (беря сигналы из моделей с разным набором признаков), можно увеличивать профитность на сделку (беря сделки только если они "подтверждены" сразу несколькими моделями), тут уж как кому нравится, главное, что дисперсия финансовых результатов по годам стал меньше, для меня это важно.

  3. Часть где я прощаюсь с благодарными читателями, но обещаю вернуться для главного и интересного.

    Во второй части я напишу главное и самое интересное (для чего собственно все это затевалось). Главное это признаки и то как я их представил. Как оказалось, подав правильные признаки, можно получить хорошие результаты используя не только трансформеры, но и LSTM и градиентный бустинг. А самое интересное это финансовый результат, полученный по прогнозам модели.

Комментарии (7)


  1. twistfire92
    27.01.2022 10:39

    может весь пост в превьюху не надо загонять? А то общая лента с постами не оч смотрится


  1. Marat1980 Автор
    27.01.2022 10:39

    спасибо за реплику, а то даже не заметил бы


  1. Skyu
    27.01.2022 15:42

    На смартлабе никнейм такой же, если хочу блог почитать?


    1. Marat1980 Автор
      27.01.2022 15:43

      Если захотите почитать там https://smart-lab.ru/my/afecn19/


  1. Ka_Wabanga
    27.01.2022 15:43

    Спасибо за статью. НО

    по моему скромному мнению:

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

    Я понимаю, что "кидаясь" такими словами, нужно приводить примеры, но их не будет (понимаю и принимаю минуса). Просто не смог пройти мимо и оставить как есть. Многие читатели, интересующиеся временными рядами и нейросесями, могут получить неправильную информацию.

    Будьте осторожны, читая статью.


  1. tumikosha
    27.01.2022 15:46

    Key, query, volume? Что за key, query?


    1. Skyu
      27.01.2022 16:44

      Параметры слоя Attention в неросетях, если кратко