Телеграм-бот «Курсы валют • Украина» как путь в Node.js
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Zip file, православные! Приветствую Вас, уважаемые коллеги!
Позвольте представиться. Я — C++ Back-End Engineer c более чем в какой-то момент около года назад решил: жизнь слишком коротка, чтоб писать на Assembler C++. Таким образом было примято решение изучить что-то новенькое, трендовое и высокоуровневое (C++ — единственный язык среднего уровня, так что сильно страдать не придётся) с большим арсеналом готовых решений и пригодное для backend’а. Так как в юные годы я баловался с JavaScript, а Node.js серверная платформа для работы с JS, то выбор пал именно на Ноду.
Лучшим способом изучить что-то новое всегда считал — применить это в «боевых» условиях с последующим набиванием шишек. Так я начал искать свой проект. Первым, что пришло в голову — это парсить reddit. Но данные надо было куда-то выводить, а к GUI и frontend’у у меня неприязнь ещё со школы, как у любителя писать «чёрные ящики». Так, довольно быстро, смекнул, что богоподобный Telegram подойдёт как нельзя кстати. Благо есть telegraf фреймворк и API довольно интуитивно понятный, да и примеров в Сети завались. Так и был написан мой первый проект (хотя это сложно так назвать) с использованием Node.js, JavaScript и SQLite. И тут я вошёл во вкус=)
Первом камнем преткновения для написания более-менее объемного кода стал именно JavaScript. Без строгой типизации плюсовику очень сложно жить. Благо, мудрый человек мне порекомендовал использовать TypeScript. И тут дело пошло! Ещё несколько недопроектов, ещё десяток Node.js-пакетов, и некоторое просветление пришло.
Мне захотелось большего и чего-то серьёзного. Так как украинской разработчик всегда имеет дело с валютой, а finance.ua неудобно мониторить на мобилке, то решил запились бота с подобным функционалом. Этот момент стал днём «зачатия» @BrokerUA_bot.
Первыми, кто попал под регулярную «слежку», стали ПриватБанк, monobank и НБУ, ибо эти хорошие люди не скрывают свои API. Их данные стали коллекционироваться в моей базе данных. Первое решение: каждый новый запрос не создает новую запись, если курс не изменился, а лишь обновляет поле «close_time». Второе решение: для каждого финансового учреждения была эмпирическим путём выбрана своя задержка между опросами, чтобы попросту не нагружать их бедные сервера. Также реализовал рассылку сообщений всем подписавшимся об изменении курса. Но сообщений стало приходить очень много, и далеко не все валюты были интересны. Последующим крупным шагом стала реализация профиля пользователя и реализация scene «Настройки» (scene термин telegraf фреймворка). «Настройки» стали позволять выбрать финансовые учреждения и валютные пары, от которых пользователь желает получать обновления, или вообще их отключить, если желает просматривать курс по запросу.
Далее был долгий период исследования сайтов различных банков и обменников с целью заполучить Святой Грааль их API, ибо парсить сайты дело не богоугодное (они же могут frontend поменять в любой момент, а переписывать код уж очень не хочется). Таким образом я заполучил все, кроме парочки, API финансовых учреждений, обрабатываемые ботом.
«Преждевременная оптимизация — корень всех зол»
@Дональд Кнут
А это значит пришло время что-то оптимизировать! Хоть и большой нагрузки нет, но оптимизировать очень хочется=)
Дано (сильно упрощенно):
class Currency { code: string; isSame(value: Currency): boolean { return this.code === value.code; } } class CurrencyPair { base: Currency; quote: Currency; isSame(value: Currency): boolean { return this.base.isSame(value.base) && this.quote.isSame(value.quote); } } class Rate extends CurrencyPair { bid: number; ask: number; }
Класс User содержит массив CurrencyPair (что пользователь хочет видеть). Класс FinancialInstitution — массив Rate. Фильтр для получения интересующих курсов выглядит так: userRates = finInst.rates.filter(rate => user.pairs.some(pair.isSame(rate))). И так для каждого пользователя! Тут каждый увидит чрезмерно (алгоритмически) сложною операцию. Большое O (при каждом обновлении курса на одного пользователя) = N(user_pair) * N(finInst_rate) = N^2. Да, ещё и сравнение строк — линейная сложность, но код валюты всегда 3, так что это константа.
Решение (описательно):
1. Используем паттерн Impl для CurrencyPair: CurrencyPairImpl содержит всё, что было в CurrencyPair. CurrencyPair содержит CurrencyPairImpl. Оптимизировали медот CurrencyPair.isSame, ибо сравниваем лишь указатели на CurrencyPairImpl.
2. В CurrencyPairImpl добавляем поле orderIndex, уникальное для каждой валютной пары. Теперь всегда сортируем массив CurrencyPair у User (одиночная операция на старте или при изменении) и массив Rate у FinancialInstitution (единожды при получении новых данных). Это нам дает возможность фильтровать (получать userRates) за линейное время: бежим параллельно по двум массивам, учитывая orderIndex, чтоб не обгонять.
Таким образом первым шагом почти ничего не оптимизировали, но подготовились ко второму. А вторым упростили фильтрацию на один порядок!
Пришло время для следующего решающего шага — «переезд» на хостинг. И, конечно же, бесплатный! Самым приемлемым мне показался Heroku (если знаете что-то лучше — напишите). Так как предоставляет бесплатный хостинг и логи можно смотреть через web-морду с помощью Add-on’а «Papertrailapp».
Всё было здорово, пока не пропали данные из базы. Оказывается (конечно же, условия использования до этого я не читал) это «наказание» для неплательщиков: раз в сутки сервер перезагружается, база SQLite создается в памяти. А ещё бот засыпал, когда через web никто не стучится. Первую проблему решил путём перехода на PostgreSQL, путём добавления Add-on’а «Heroku Postgres». Бесплатно предоставляется возможность хранить до 10k строк (что породило новую проблему, но об этом позже). Переход на новую БД проблем не создал: Sequelize — молодец. Вторая решилась с помощью хитрой манипуляции со вторым echo-ботом и прокси телеграм-группой и телеграм-каналом. Да, извращение! Но чего только не сделаешь, чтобы не платить=)
Далее захотелось видеть графики изменения курсов. Для чего-то же я эти данные храню. После недолгих поисков остановился на nСhart. Немого магии и вуаля!
Тут была решена проблема с ограничением данных. Так как на графике за большой период перестаём видеть промежуточные значения, то их следует удалить. Так был реализован фильтр из нетривиального SQL-запроса и щепотки магии=)
Курсы валют были отсортированы по наименьшей относительной разнице ((ask-bid)/(bid+ask)). Но один пользователь попросил меня добавить сортировку.
Обратная связь и желание клиента — закон! Хоть мне никто и ничего не платит=(
В конце списка идут валютные курсы, которые не торгуются.
Моей жене понадобился калькулятор. Сделано!
Заметил, что некоторые пользователи пытаются говорить с ботом. Как же я могу Вам не угодить?
RegExp наше всё!
Появилась ещё одна проблема: многие боятся заходить в настройки. Спасибо Васе (мой коллега).
Повозился, но сделал.
Далее локализация, оптимизация действий пользователей, украшательства и многое другое. Уже, как-никак, версия 3.5.
На данный момент вот такой результат (t.me/BrokerUA_bot):
💵 Валюты: USD 🇺🇸, EUR 🇪🇺, RUB 🇷🇺, PLN 🇵🇱, CZK 🇨🇿, GBP 🇬🇧, CHF 🇨🇭, CAD 🇨🇦, BYN 🇧🇾, TRY 🇹🇷, CNY 🇨🇳, ILS 🇮🇱, JPY 🇯🇵, HUF 🇭🇺, BTC 🏴
🏦 Банки: ПриватБанк, OTP Bank, monobank, megabank, Ощадбанк, УкрСиббанк, ПУМБ, Банк «Пивденный», Кредобанк, МежБанк
🏦 Обменники: Менора (Днепр), КИТ Групп (Днепр, Киев, Винница, Запорожье, Мариуполь, Николаев, Харьков, Ивано-Франковск, Сумы, Львов, Кривой Рог, Одесса, Хмельницкий, Черновцы, Ужгород, Луцк, Полтава), Money24 (Днепр, Киев, Винница, Николаев, Харьков, Львов, Одесса), Обмен24 (Днепр, Каменск), Obmenka (Харьков), Цент (Нововолынск, Луцк, Ковель, с. Старовойтово)
📈 История курсов
💱 Калькулятор с учетом покупки-продажи
🛎 Уведомления об изменениях курсов
🛎 Ежедневный отчёт
Бот регулярно модернизируется и улучшается под желание пользователей. Чат t.me/BrokerUA_chat для просьб и предложений, а также там информация об обновлениях.
Итог: полгода внерабочего времени воплотились в интересный и, надеюсь, полезный проект. На плюсах это было бы нереально даже за больший срок и сильно муторно. Да простят меня Боги++.
P.S. Node.js разработчиком так ещё и не стал: «у Вас нет необходимого опыта», «у нас тут микросервисы и всё такое...» говорят мне рекрутеры. Но надежды пока не теряю.
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів