Криптовалюты уже давно стали средством инвестиции, а криптобиржы заняли прочную позицию относительно рынка ценных бумаг и Forex. Трейдеры и инвесторы хотят знать, что будет завтра, через неделю, через час. Технический анализ - хороший инструмент, но он не может предсказать эмоции и настроения людей. В тоже время социальные сети каждую секунду пополняются новыми статьями, публикациями, сториз и пр. И все это несет в себе и оптимизм, и панику и пр. настроения, влияющие на решения людей. Причем людей, пишущих в своих блогах про криптовалюты, мы будем разделять на просто людей и лидеров мнений. Так вот идея заключается в том, что происходящее на рынках является источников всей этой big data'ы постов и мнений в соц. сетях. НО! Но и наоборот все, что "новостится", обсуждается, поститься, пампится и дампится в соц. сетях влияет на рыночные котировки. Конечно киты о своих разворотах никогда никого не предупредят, на то они и киты. Но не исключено, что они как раз постараются опубликовать что-то, что подготовит почву для разворота. И та нейросеть, которую нужно построить, должна в идеале и такие сигналы интерпретировать корректно. Да и в принципе не так важно, для чего этот подход применять: для акций или криптовалют. Да и на последок, перед тем как заняться этой темой, я немного погуглил и понял, что идея конечно же не новая, и есть те, кто уже пробовал это сделать. С одной стороны это хорошо - значит кто-то еще в это верит, а с другой стороны - это не отвечает на вопрос, решаема ли задача.

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

Я решил не анализировать все твитты всех пользователей, где упоминается биткойн, и ограничился лидерами мнений в количестве 75 шт. Лидеров мнений я отобрал, сматчив 2 статьи на тему влияния отдельных людей на курс битка. Вот список их учеток в Twitter (крипто энтузиасты должны узнать большинство по никнеймам):

@elonmusk, @CathieDWood, @VitalikButerin, @rogerkver, @brockpierce, @SatoshiLite,
@JihanWu, @TuurDemeester, @jimmysong, @mikojava, @aantonop, @peterktodd, @adam3us, 
@VinnyLingham , @bobbyclee, @ErikVoorhees, @barrysilbert, @TimDraper, 
@brian_armstrong,@koreanjewcrypto, @MichaelSuppo, @DiaryofaMadeMan, @coinbase, 
@bitfinex, @krakenfx, @BittrexExchange, @BithumbOfficial, @binance, @Bitstamp, 
@bitcoin, @NEO_Blockchain, @Ripple, @StellarOrg, @monero, @litecoin, @Dashpay, 
@Cointelegraph, @coindesk, @BitcoinMagazine, @CoinMarketCap,@cz_binance, 
@justinsuntron, @CointelegraphMT, @AweOlaleye, @davidmarcus, @99BitcoinsHQ, 
@NickSzabo4, @cdixon, @bgarlinghouse, @ethereumJoseph, @chrislarsensf, 
@starkness, @ChrisDunnTV, @tyler, @cameron, @CryptoHayes, @woonomic, 
@CremeDeLaCrypto, @PeterLBrandt, @bytemaster7, @APompliano, @jack, @MatiGreenspan,
@theRealKiyosaki, @twobitidiot, @TraceMayer,@IOHK_Charles, @fluffypony, 
@CharlieShrem,@pwuille,@novogratz, @jchervinsky, @lopp, @IvanOnTech, @pierre_rochard 

По временному интервалу я ограничился тремя года (2019, 2020, 2021 гг.) Для реализации я использовал библиотеки Keras, язык Python, среда Jupyter notebook, все делал на своем личном ноуте.

Парсинг Twitter. API Binance.

И так, данные Twitter. Первый челлендж был в том чтобы получить именно их. Twitter отказал 2 или 3 раза в ответ на мои запросы подключиться к их официальным API. При чем немного погуглив, я понял, что это нормальная практика, и я не один такой. К тому же при работе с официальными API Twitter'а есть неприятные ограничения, поэтому я не сильно расстроился. Пришлось "взять в зубы" Selenium, XPath и спарсить Twitter самостоятельно. Я делал это первый раз, но разобрался довольно быстро. Плюс очень боялся, что в процессе меня забанят и придется покупать ip и пр. Мне так говорили люди, которые уже не раз что-то парсили, и трудно было им не верить. Но вопреки всем страхам данные за 3 года (2019, 2020, 2021 гг.) я выкачал без банов примерно за 8 часов. На выходе получил  43 842 твиттов,12 Мб. По крупному хочу отметить только одну проблему. Когда Twitter перестал открываться в России, пришлось воспользоваться VPN. Благо на то время для обучения у меня уже был скачан основной 3-х годичный датасет, и мне нужно было допарсить январь и февраль 2022 г. Я пробовал два разных VPN, и у меня постоянно крашился DOM, и парсинг останавливался. Один мой товарищ, специализирующийся на парсинге, посоветовал мне включить headless режим (это когда парсинг происходит со скрытым браузером и мы не видим глазами имитацию действий). Headless mode сразу помог. Кстати с товарищем мы познакомились, когда я искал исполнителя для скрапинга(парсинга) на Яндекс услугах. В итоге когда мы начали обсуждать детали, пришло осознание, что YouTube и мои навыки программирования будут достаточны для того, чтобы я справился со всем сам. И еще, оказалось, что не так много людей, профессионально занимающихся парсингом, парсили хотя бы раз Twitter. На графиках ниже динамика твиттов по авторам за три года:

Далее нужно было найти исторические данные изменения цены BTC, причем с гранулярностью 1ч. Единственное, где я смог это достать - оказались API известной крипто-биржи Binance (https://github.com/binance/binance-public-data/). Очень понятно и очень быстро. Спасибо Binance.

Конкатенация и сопоставление данных Twitter и Binance

И так, гранулярность, с которой я решил начать эксперименты = 1ч. А значит твитты также были распределены по часам. Итого за три года получилось: 26246 записей. Как вы догадались данные твиттов также нелинейно растянулись по временной шкале 3-х лет. И так как в одном часе могло быть пусто, а в другом наоборот написал блогер, да еще не один, да еще не одному посту, то твитты внутри часа нужно было как-то конкатенировать. И конкатенировать их внутри часа я решил по следующему правилу: автор1::: твитт_автора1 автор2::: твитт_автора2 автор3::: твитт_автора3 и т.д. Идея c ":::" заключается в том, что нейронка рано или поздно должна начать выделять на основе своих эмпирических вычислений авторов от своих твиттов. Также во избежание шума те авторы, чье количество твиттов с упоминанием BTC было < 20 за 2019-21 гг. были обезличены, т.е. их никнеймы заменены на OTHER_less20. Туда чуть было не угодил Илон Макс, т.к. оказалось, что его твиттов с упоминанием BTC было не сильно выше порога, всего 37. Он чудом сохранил индивидуальность:) Но это всего лишь гипотезы, как и вся идея, описанная в этой статье. И правило ":::" и "<20" можно и нужно менять. И еще одна важная деталь про датасет. Я сопоставил твитты T против котировок T+1. Т.е. предполагается, что нейронка по окончании часа Т будет предсказывать тренд на конец часа Т+1. Опять же момент для калибровки, плюс вообще никто не говорит, что час - это единственная атомарная величина. Можно пробовать с днями и другими гранулами времени.

Перевод текстов твиттов в цифры, а цифры в OHE

Дальше пошла подготовка данных к обучению. Нейросети не принимают на вход слова и символы. А значит сконкатенированные твитты нужно было преобразовать в индексы. Я воспользовался керасовским Tokenizer'ом и создал словарь, задав при этом максимальный индекс=15000. Вначале я пробовал 10000, затем 15000. Разницы в финальных результатах после обучения нейронки выявлено не было. При этом Tokenizer обнаружил 47 742 уникальных значения. Далее в целях нормализации я преобразовал индексы в OneHotEncoding (OHE). Таким образом в качестве X я получил numpy матрицу 26246Х15000 со значениями 0 и 1. А в качестве Y получилась матрица 26246Х3. Почему 3 ? Потому что 3 значения было решено использовать для изменения тренда:

  • 0: больше -10$ и меньше +10$

  • 1: меньше -10$ 

  • 2: больше +10$

Я решил не учитывать изменения меньшие 10$, потому что мне понравилось распределение:

количество 0 равно:  (5452,)
количество 1 равно:  (10612,)
количество 2 равно:  (10182,)

Ну и чтобы еще лучше это нормализовать, к Y был также применен OneHotEncoding. Еще раз для начинающих нейронщиков, которые читают эту статью: X - это то, что подадим на вход нейросети, а Y - то, что хотим получить на выходе. X, Y были разделены на тренировочную выборку Xtrain, Ytrain (24 806 строк) и проверочную Xtest, Ytest (1440 строк).

TimeseriesGenerator

Ну и последняя трансформация с данными перед тем, как врубить обучение. Т.к. мы имеем дело с временными рядами, то при помощи TimeseriesGenerator устанавливаем величину шага для анализа = 48 ч. Чтобы было понятней приведу фрагмент кода:

xLen=48    #48 hours step for analyze
valLen=1440#24hours*60days=1440hours for Test

trainLen=dataX.shape[0] - valLen #size of Train

xTrain, xTest = dataX[:trainLen],   dataX[trainLen+xLen:]
yTrain, yTest = dataTTRcategor[:trainLen], dataTTRcategor[trainLen+xLen:]

#train TimeSeriesGenerator
TRtrainDataGen = TimeseriesGenerator(xTrain, yTrain, 
                                   length=xLen, stride=1, sampling_rate=1,
                                   batch_size=12)
#test TimeSeriesGenerator
TRtestDataGen = TimeseriesGenerator(xTest, yTest, 
                                   length=xLen, stride=1, 
                                   batch_size=12)

Как видно из кода батч = 12, т.е. веса при обучении модели будут меняться после 12-ти прохождений через нейронку. Резюме: когда начинается час Х, то для предсказания тренда цены биткойна к концу часа X нейросеть будет анализировать твитты, написанные по тематике биткойна лидерами мнений в диапазоне с X-48 часа до Х часа.

Нейросети и их обучение

Ну и наконец долгожданное обучение. Я пробовал три разных архитектуры:

  • один полносвязный слой;

  • полносвязные + сверточные слои;

  • UNET сеть;

Обучение 50-ти эпох для каждой занимает в среднем по 3-3,5 часа.

Полносвязная сеть

Слои: Dense, Flatten

Число параметров: 1 014 503

Архитектура:

modelD = Sequential()
modelD.add(Dense(100,input_shape = (xLen,maxWordsCount), activation="relu" )) 
modelD.add(Flatten())
modelD.add(Dense(dataTTRcategor.shape[1], activation="sigmoid"))

#Compile
modelD.compile(loss="binary_crossentropy", optimizer=Adam(learning_rate=1e-4), metrics=['accuracy'])

Результаты (синим - тренировочная выборка, желтым - проверочная):

Сверточная сеть

Слои: Dense, FlattenDense, Flatten, RepeatVector, Conv1D, MaxPooling1D, Dropout

Число параметров: 1 100 523

Архитектура:

drop = 0.4
input = Input(shape=(xLen,maxWordsCount))
x = Dense(100, activation='relu')(input)
x = Flatten()(x)
x = RepeatVector(4)(x)
x = Conv1D(20, 1, padding='same', activation='relu')(x)
x = MaxPooling1D(pool_size=2)(x)
x = Flatten()(x)
x = Dense(100, activation='relu')(x)
x = Dropout(drop)(x)
x = Dense(dataTTRcategor.shape[1], activation='sigmoid')(x)
modelUPD = Model(input, x)

Результаты (синим - тренировочная выборка, желтым - проверочная):

UNET сеть

Слои: Dense, FlattenDense, Flatten, RepeatVector, Conv1D,MaxPooling1D, Dropout, BatchNormalization

Число параметров: 1 933 155

Архитектура:

 img_input = Input(input_shape) 

    # Блок 1
    x = Conv1D(32 * k , 3, padding='same')(img_input) 
    x = BatchNormalization()(x) 
    x = Activation('relu')(x)

    x = Conv1D(32 * k, 3, padding='same')(x)  
    x = BatchNormalization()(x)     
    block_1_out = Activation('relu')(x) 

    # Блок 2
    x = MaxPooling1D()(block_1_out)
    x = Conv1D(64 * k, 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)  

    x = Conv1D(64 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    block_2_out = Activation('relu')(x)

    # Блок 3
    x = MaxPooling1D()(block_2_out)
    x = Conv1D(128 * k, 3, padding='same')(x)
    x = BatchNormalization()(x)               
    x = Activation('relu')(x)                     

    x = Conv1D(128 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv1D(128 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    block_3_out = Activation('relu')(x)

    # Блок 4
    x = MaxPooling1D()(block_3_out)
    x = Conv1D(256 * k, 3, padding='same')(x)
    x = BatchNormalization()(x) 
    x = Activation('relu')(x)

    x = Conv1D(256 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv1D(256 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)      
    block_4_out = Activation('relu')(x)
    x = block_4_out 

    # UP 2
    x = Conv1DTranspose(128 * k, kernel_size=3, strides=2, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x) 

    x = concatenate([x, block_3_out]) 
    x = Conv1D(128 * k , 3, padding='same')(x) 
    x = BatchNormalization()(x) 
    x = Activation('relu')(x)

    x = Conv1D(128 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    
    # UP 3
    x = Conv1DTranspose(64 * k, kernel_size=3, strides=2, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x) 

    x = concatenate([x, block_2_out])
    x = Conv1D(64 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv1D(64 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    # UP 4
    x = Conv1DTranspose(32 * k, kernel_size=3, strides=2, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = concatenate([x, block_1_out])
    x = Conv1D(32 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv1D(32 * k , 3, padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    
    x = Flatten()(x)
    x = Dense(dataTTRcategor.shape[1], activation='sigmoid')(x)

    model = Model(img_input, x) 

    model.compile(optimizer=Adam(learning_rate=1e-4), 
                  loss='binary_crossentropy',
                  metrics=[dice_coef])

Результаты (синим - тренировочная выборка, желтым - проверочная):

Ну вот пока так. Удовлетворяющего результата пока нет. Признаться честно я возлагал очень не малые надежды на UNET. Эта архитектура считается в кругах опытных нейронщиков, как best practice. Но, как видите, результаты она дала еще более дикие, чем обычная полносвязка, которая тоже в свою очередь пока дает эффект, равносильный бросанию монетки. Нейронками я увлекаюсь не так давно, поэтому в планах попробовать применить: Q-learning, Сети с вниманием, Генетические алгоритмы и др. Не обещаю, что вторая часть выйдет скоро, но триггером для ее написания должна стать положительная динамика в результатах.

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

Если есть идеи по сотрудничеству и/или, как допилить эту идею)) - велкам в личку! Доведя нейронку до ума, можно будет подумать на тему SaaS продукта. Пару слов о себе: я продакт/проджект в банкинге с ИТ бэкграндом > 17 лет. Программирование, нейронки, хакатоны - это все мои хобби.

Всем спасибо, что дочитали до конца!

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


  1. evaledi31
    15.04.2022 12:13
    +1

    Думаю пока идёт война на Украине, курс будет только расти..


    1. savostin
      16.04.2022 12:11
      +1

      Судя по курсу, война уже давно закончилась. Ан-нет.


  1. Alex_ME
    15.04.2022 14:18
    +4

    И так, гранулярность, с которой я решил начать эксперименты = 1ч. А
    значит твитты также были распределены по часам. Итого за три года получилось: 26246 записей.

    ...

    А значит сконкатенированные твитты нужно было преобразовать в индексы. Я воспользовался керасовским Tokenizer'ом и создал словарь, задав при
    этом максимальный индекс=15000. Вначале я пробовал 10000, затем 15000.
    Разницы в финальных результатах после обучения нейронки выявлено не
    было. При этом Tokenizer обнаружил 47 742 уникальных значения. Далее в
    целях нормализации я преобразовал индексы в OneHotEncoding (OHE). Таким
    образом в качестве X я получил numpy матрицу 26246Х15000 со значениями 0
    и 1.

    Эта часть не очень понятна. Т.е. вы разбили твитты на слова/токены (каждый твитт представляется как вектор из токенов), верно? Как тогда вы закодировали последовательность токенов в виде one-hot вектора? Или весь твитт - это один токен? Т.е. получается, что вы подаете на вход нейросети информацию о том, какие слова есть в твитте, без структуры (порядка слов)?

    Я не датасайентист, но, ИМХО, ваша модель (2 млн параметров) просто не в состоянии обрабатывать язык. Для этого я бы рекомендовал вам использовать _языковые модели_, такие как BERT (или что-то более новое). Это существенно более объемная сеть специально для Natural Language Processing.

    Берете предобученный BERT, к выходу BERT добавляется классификатор из нескольких (одного? я не датасейентист) полносвязанных слоев с выходами как у вас (растет, не изменна, падает) и дообучаете на своем датасете. Примеры есть в интернете. Я так делал задачу оценки тональности текста (негативный, нейтральный, положительный). Выглядит точно так же, только данные другие.

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

    У вас три класса, вероятность угадать ~33%. Accuracy 0.5 (в последнем случае где-то как раз около 0.3). Иными словами, ваша сеть не лучше просто случайного выбора.

    С 2019 года биткоин вырос более, чем в 10 раз. Можно утверждать, что количество выборок, где курс растет больше, чем тех, где он падает или нейтрален. Проверьте эту гипотезу! Если у вас классы в датасете представлены не в равных долях, то нейросеть обучится плохо. Условно, она может всегда выдавать ответ "растет" вне зависимости от входных данных и угадывать лучше, чем случайный выбор (наверное, этим и объясняется, что accuracy 0.5, а не 0.33)


    1. bellerofonte
      15.04.2022 18:04

      С 2019 года биткоин вырос более, чем в 10 раз. Можно утверждать, что количество выборок, где курс растет больше, чем тех, где он падает или нейтрален. Проверьте эту гипотезу!

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

      кроме того, все современные пакеты умеют в stratify


      1. scolfield Автор
        15.04.2022 21:59

        вычислил пропорции на обучающей и выборочной.

        Ytrain:

        number when DOWN<10$ [1., 0., 0]:  9454
        number when UP>=10$ [0., 1., 0]:  9926
        number when NOT change [0., 0., 1]:  5426

        Ytest:

        number when DOWN<10$ [1., 0., 0]:  707
        number when UP>=10$ [0., 1., 0]:  660
        number when NOT change [0., 0., 1]:  25


    1. scolfield Автор
      15.04.2022 18:20

      Одно слово - один индекс. Одна запись - один набор индексов. Т.е. если в записи один твит из одного слова, то из 15000 колонок только одна будет равна 1, остальные 0. Два слова - две единицы.

      За идею с BERT - спасибо. Беру в бэклог


  1. minalexpro
    15.04.2022 14:24

    Неужели есть примеры верного\удачного предсказания ИИ любого курса валюты, акции и тп?


    1. bugkon
      15.04.2022 14:34
      +1

      Их скрывают.


    1. bellerofonte
      15.04.2022 18:01

      ответ на этот вопрос надо делить на несколько частей.

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

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

      3) на любом рынке, будь то акции, облигации, фьючерсы, криптовалюты и т.п. распределение доходностей (aka изменений цен) сильно неравномерно, с толстыми хвостами распределения, поэтому само по себе accuracy тут не решает - модель может 99 раз угадать, но на 100ый потерять все заработанное и еще полдепозита.


  1. shambho
    16.04.2022 02:20
    +2

    курс битка определяется не твитами, а "китами", и ведут они курс туда, где можно больше "сбрить" ликвидности с толпы


  1. savostin
    16.04.2022 12:12

    Можно попробовать отбрасывать лидеров мнений, может кого-то ошибочно занесли.


  1. Dmi3Ut
    17.04.2022 15:30

    Попытка зачетная.
    1. Модели действительно переобучаются. Значит их надо упрощать.
    2. Возможно основная проблема мешающая принципиальной возможности предсказать курс по высказываниям - это временной лаг.
    Кто-то обсуждал долгосрочный курс, кто-то краткосрочный. Часовая привязка скорее характерна паническому поведению. А такое на рынке случается не так уж и часто. Как вариант можно попробовать дневки.
    3. Можно попробовать оптимизировать архитектуру через https://github.com/509981/kerasin


  1. Saddamko
    18.04.2022 09:42

    С одной стороні - очень серьезная работа, и инструментарий, с другой - я совершенно не уловил смысл токенизации твитов. Ведь это слишком сложно уловить смысл твита. Мое предложение - первым этапом сделать все то же самое, но упростить - просто оценить зависимость при помощи нейронки от самого факт появления твита кого-то из указаных лидеров мнений, включающего "BTC" или "crypto", или "coin" на любое значимое изменение курса коина через n-часов. Подобрать n, конечно.