Криптовалюты уже давно стали средством инвестиции, а криптобиржы заняли прочную позицию относительно рынка ценных бумаг и 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 лет. Программирование, нейронки, хакатоны - это все мои хобби.
Всем спасибо, что дочитали до конца!