[Перевод] Пишем Твиттер-бота, который предсказывает курс биткойна

Программист Огњен Гатало, автор статьи, перевод который мы публикуем сегодня, уже некоторое время интересуется криптовалютами. Особенно его занимает прогнозирование курса на ближайшие несколько дней. Он говорит, что испробовал некоторые алгоритмы, но в итоге у него сложилось ощущение, что ни один из подходов не позволяет с уверенностью давать краткосрочные прогнозы. Тогда он, сосредоточившись на лидере рынка — биткойне, решил поработать над собственным методом предсказания курсов. Предлагаемый им алгоритм, равно как и его реализация в виде твиттер-бота, конечно, с некоторыми изменениями, подходят и для работы с другими цифровыми валютами.

image


Общий обзор алгоритма и его реализации


Твиттер кажется мне удобной платформой для реализации проектов в сфере криптовалют, подобных моему. Тут можно, в частности, организовать удобное взаимодействие пользователей и программ, для которых открыты собственные учётные записи. Именно поэтому я решил оформить мою разработку в виде бота для Твиттера. В частности, мой бот должен был, каждые 2 часа, твитить прогноз стоимости биткойна на следующие N дней. N — это период в днях, прогноз на который интересует больше всего людей, запросивших у бота соответствующую информацию. Так, например, если 3 человека сделали запрос на 5-дневный прогноз, а 7 человек интересует прогноз на 2 дня, бот твитнет прогноз на 2 дня.

Бот реализован на Node.js, разработал я его довольно быстро. Некоторое время занял поиск источников исторических и текущих данных по ценам и подбор способов работы с ними. Главный вопрос заключался в алгоритме. В итоге я решил, что будут использовать метод K ближайших соседей в комбинации с ещё некоторыми вещами.

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

  1. Сбор запросов пользователей на очередной прогноз.
  2. Нахождение длительности прогноза, за которой обращаются чаще всего.
  3. Получение текущей стоимости биткойна.
  4. Нахождение K(10) ближайших дат в последние 2 месяца, в которых цена биткойна была наиболее сильно похожа на его текущую цену.
  5. Для цены на каждую из найденных дат (PAST_DATE), нахождение цены BTC через N дней после неё (N_DAYS_AFTER_PAST_DATE).
  6. Для каждой даты вычисление разницы между значениями цен N_DAYS_AFTER_PAST_DATE и PAST_DATE.
  7. Сложение всех найденных разниц и деление того, что получилось, на K.
  8. Получение результата в видео среднего изменения курса биткойна между всеми группами значений PAST_DATE и N_DAYS_AFTER_PAST_DATE. Этот результат используется при формировании твита.


Рассмотрим основные шаги этого процесса подробнее.

Шаг 1. Сбор запросов пользователей


На этом шаге, с помощью модуля twit и API Twitter, выполняется поиск твитов, которые содержат следующую конструкцию: @coin_instinct Predict for days. Затем из найденных твитов извлекаются числа, символизирующие число дней, на которое пользователи хотят получить прогноз, и создаётся массив данных чисел.

Шаг 2. Нахождение длительности прогноза, которая интересует больше всего пользователей


Когда бот, раз в 2 часа, твитит прогноз, число дней, которое использовалось в этом прогнозе, сохраняется в чёрном списке, представленном массивом. Этот массив содержит длительности прогнозов для 4-х последних твитов. Такой подход позволяет избежать частого появления похожих твитов и выдачи тех же прогнозов, которые уже были твитнуты в последние 8 часов.

async function findMostFrequent(array, blackListArr)
{
    if(array.length == 0)
        return null;
    var modeMap = {};
    var maxEl = array[0], maxCount = 1;

    // Сначала пройдёмся по массиву blackList и запишем в maxEl первый элемент, отсутствующий в чёрном списке. Если все числа в массиве в чёрном списке присутствуют, вернём generateRandom

    var containsValidNumbers = false;
    for(var i = 0; i < array.length; i++) {
      if(!blackListArr.includes(array[i])) {
        maxEl = array[i];
        containsValidNumbers = true;
        break;
      }
    }
    if(!containsValidNumbers) return await generateRandom(blackListArr);

    for(var i = 0; i < array.length; i++)
    {
        var el = array[i];
        if(blackListArr.includes(el)) continue;
        if(modeMap[el] == null)
            modeMap[el] = 1;
        else
            modeMap[el]++;  
        if(modeMap[el] > maxCount)
        {
            maxEl = el;
            maxCount = modeMap[el];
        }
    }
    await addToBlackList(maxEl);
    return maxEl;
    
}


Представленная здесь функция очень проста. Она выбирает наиболее часто запрашиваемые длительности прогнозов из массива чисел. Если найденное число уже есть в blackListArr, функция возвращает второй наиболее запрашиваемый прогноз, и так далее. Если все запрошенные длительности прогнозов уже есть в чёрном списке — тогда бот выдаёт предсказание на случайно выбранный период.

Шаг 3. Получение текущей стоимости биткойна


Узнать текущую стоимость биткойна можно с использованием API blockchain.info. Полученное значение сохраняется в переменной.

function refreshBitcoinPrices() {
  const request = async () => {
    var results = await fetch("https://blockchain.info/ticker");
    results = await results.json();

    bitcoinData.results = results;
    if(bitcoinData.results) {console.log('Blockchain API works!');}
    else console.log('Blockchain API is down at the moment.');

    // Узнаем сегодняшнюю дату
    this.todayDate = new Date();
    this.todayDate = this.todayDate.toISOString().split('T')[0];
    console.log(this.todayDate);

    console.log('New prices fetched.');
    console.log('Most recent bitcoin price: '+bitcoinData.results.USD.last);
    console.log('Time: '+new Date().getHours()+':'+new Date().getMinutes());
  }

    request();

    // Включено в рабочем боте
    setInterval(() => {
      request();
    },COIN_FETCH_TIMEOUT); // 1000*60*118
}


Эта функция запускается через 2 минуты после начала работы алгоритма.

Шаг 4. Поиск K ближайших соседей


Тут я не привожу описание всех используемых на данном шаге функций, таких, как выполнение запроса к API Coindesk для загрузки данных, необходимых для показателей PAST_DATE и N_DAYS_AFTER_PAST_DATE. Здесь показан поиск ближайших соседей, основанный на том, насколько они похожи на текущее значение цены. Полный код проекта можно найти в моём GitHub-репозитории.

async function getNearestNeighbours(similarities) {
  // Пройдёмся по массиву и найдём k(10), которые ближе всего к 0
  var absSimilarities = [];
  similarities.forEach( (similarity) => {
    absSimilarities.push({
      date: similarity.date,
      similarityScore: Math.abs(similarity.similarityScore)
    })
  })
  absSimilarities = absSimilarities.sort(function(a,b) {
    return (a.similarityScore > b.similarityScore) ? 1 : ((b.similarityScore > a.similarityScore) ? -1 : 0);
  });
  var kNearest = [];
  for(var i = 0; i < K; i++) {
    kNearest.push(absSimilarities[i].date);
  }
  return kNearest;
}


Так как мы вычислили разницы между всем ценами на биткойн в последние 2 месяца и его текущей стоимостью, нам нужно найти те даты, в которых эти значения ближе всего к 0. Поэтому мы сначала вызываем Math.abs для всех свойств similatityScore объектов, которые входят в массив, а затем сортируем массив в нисходящем порядке, опираясь на эти свойства.

На данном этапе можно найти первые 10 дат, в которых цена биткойна была ближе всего к его текущей цене.

Шаг 5. Формирование массива результатов


На данном шаге мы получим массив объектов, каждый из которых содержит свойства start и end. Свойство start представляет цену биткойна на некую из предыдущих дат, свойство end используется для хранения цены через N дней после этого дня. Основываясь на этих данных, мы можем сформировать прогноз, сделать вывод о том, вырастет цена или упадёт.

async function getFinalResults(kNearest,nDays) {
  var finalResults = [];
  var finalResult = {};
  
  await forEach(kNearest, async(date) => {
    var dateTime = new Date(date);
    var pastDate = dateTime.toISOString().split('T')[0];

    var futureDate = new Date(date);
    futureDate.setDate(futureDate.getDate() + nDays);
    futureDate = futureDate.toISOString().split('T')[0];
    
    var valueForThatDay = this.coinDeskApiResults.bpi[pastDate];
    var valueForFutureDay = this.coinDeskApiResults.bpi[futureDate];

    finalResult = {
      start: valueForThatDay,
      end: valueForFutureDay
    }

    finalResults.push(finalResult);
  })
  return finalResults;

}


Тут мы проходим по всем элементам из kNearest и получаем данные для конкретных дат, после чего сохраняем полученные результаты в массиве finalResults и возвращаем его.

Шаг 6. Прогнозирование


Теперь осталось лишь сформировать прогноз. Делается это с помощью следующей функции.

/**
 * Вычисление прогнозных данных
 * Возвращает объект, содержащий данные для прогноза
 * @param {*Array} data Массив объектов, содержащий цены биткойна в свойствах start и end
 * @param {*Float} currentBitcoinValue Текущая цена биткойна
 */
async function calculatePrediction(data,currentBitcoinValue) {
  
  var finalPredictionData = {
    raw: 0,
    percentage: 0,
    positive: '',
    finalValue: 0
  }
  var sum = 0;
  await forEach(data, async (value) => {
    sum += value.end - value.start;
  })

  sum = sum / K;
  finalPredictionData.raw = sum;
  finalPredictionData.finalValue = currentBitcoinValue + sum;
  finalPredictionData.positive = sum > 0 ? 'true' : 'false';
  finalPredictionData.percentage = ((finalPredictionData.finalValue - currentBitcoinValue) / currentBitcoinValue) * 100;
  return finalPredictionData;
}


Вот и всё, прогноз сформирован, осталось лишь его твитнуть, соответствующим образом оформив.

Итоги


Если вы хотите познакомиться с моим ботом поближе — предлагаю заглянуть в вышеупомянутый репозиторий, в котором хранится полный код проекта. Кроме того, мне хотелось бы отметить, что в данном материале описан мой подход к предсказанию цены биткойна на ближайшие несколько дней, он всё ещё в разработке, поэтому если вам есть что сказать по поводу предложенного алгоритма — дайте мне знать. Прогнозы, которые делает бот, не всегда точны, однако я заметил, что в большинстве случаев предсказанное значение отличается от реального лишь на 100–200$. Так что, видимо, можно сказать, что бот обычно ошибается не так уж и сильно, особенно учитывая то, как безумно ведут себя курсы криптовалют.

Основная проблема описанного алгоритма заключается в том, что он анализирует только исторические данные по биткойну и делает прогнозы на их основе. Здесь пока нет механизма предсказания, например, резких падений цены. Я работаю над тем, чтобы учесть в прогнозе, скажем так, «человеческий фактор». Реализовать это планируется путём сбора, например, статей с веб-сайтов, проанализировав которые можно найти намёк на возможность резких изменений курса и добавить эти данные в уравнение.

Кстати, всё это время я рассказывал о боте, но так и не представил его вам. Для него открыт специальный аккаунт, coin_instinct. Можете твитнуть ему запрос на прогноз.

Уважаемые читатели! Занимаетесь ли вы разработкой ботов для твиттера или прогнозированием курсов криптовалют? Если да — просим поделиться опытом.

© Habrahabr.ru