From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gnuweeb.org; s=default; t=1726004660; bh=yOx7h9jv9Yb+vq4SJTVNnhM+V95Jeo3Y7I/WcTc75xY=; h=From:To:Cc:Subject:Message-Id:In-Reply-To:References:MIME-Version: Content-Transfer-Encoding:From; b=rDzzmaiHqB+Q2IpzQWAQTxgHff48aAytec7cQVzC5ID37PyeNpKlG1NysUC9mLzgX NnFMnuRhxPk6Fotih8dupm9Of/pGxRevu5JYSPRk4SmII4+Oica/ebeLmdj3TWm4s3 dMxoDvQ530TUN/JV8FkNQ8l77oqDCX94oIPLlPa9Sh73cvViQYlTMx+lsaO5SRomUi dqkoywJcSuQBl4LRh8WeuLBnUC+Vcg+D4b0QfLV9O5lJpwyxShNGLDOSPFlsIdDLVL 4GJMVVgROSNaka2Dt9axsWUMYKkOFbn7s4Z3JTiYbH8e/RrxHnC0OfzIHMVMZ0FaKu dQz1x321boddA== Received: from server-vie001.gnuweeb.org (unknown [192.168.57.1]) by server-vie001.gnuweeb.org (Postfix) with ESMTPSA id 78FE93106507; Tue, 10 Sep 2024 21:44:20 +0000 (UTC) From: Alviro Iskandar Setiawan To: Ammar Faizi , Michael William Jonathan Cc: Alviro Iskandar Setiawan , Ravel Kevin Ethan , GNU/Weeb Mailing List Subject: [RFC PATCH 9/9] gwarnt: Add Telegram bot Date: Tue, 10 Sep 2024 23:44:14 +0200 Message-Id: <20240910214414.3401712-10-alviro.iskandar@gnuweeb.org> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240910214414.3401712-1-alviro.iskandar@gnuweeb.org> References: <20240910214414.3401712-1-alviro.iskandar@gnuweeb.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: Send a notification to Telegram using bot if arbitrage opportunities detected. Signed-off-by: Alviro Iskandar Setiawan --- Makefile | 1 + src/gwarnt/entry.cpp | 258 ++++++++++++++++++++++++++++++++++++++----- src/gwarnt/net.cpp | 2 + src/gwarnt/tgbot.cpp | 75 +++++++++++++ src/gwarnt/tgbot.hpp | 46 ++++++++ 5 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 src/gwarnt/tgbot.cpp create mode 100644 src/gwarnt/tgbot.hpp diff --git a/Makefile b/Makefile index f775bfc..8de277b 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ CXX_SOURCES := \ src/gwarnt/helpers.cpp \ src/gwarnt/net.cpp \ src/gwarnt/p2p_ad.cpp \ + src/gwarnt/tgbot.cpp \ src/gwarnt/p2p/binance.cpp \ src/gwarnt/p2p/okx.cpp diff --git a/src/gwarnt/entry.cpp b/src/gwarnt/entry.cpp index 044234e..9510c3c 100644 --- a/src/gwarnt/entry.cpp +++ b/src/gwarnt/entry.cpp @@ -1,47 +1,253 @@ // SPDX-License-Identifier: GPL-2.0-only #include +#include +#include +#include #include +#include + +#include #include #include #include +#include -int main(void) +template +std::string to_string_wp(const T a_value, const int n = 2) +{ + std::ostringstream out; + out.precision(n); + out << std::fixed << a_value; + return std::move(out).str(); +} + +class gwarnt_bot { +public: + inline void set_token(const std::string &token) { tg_.set_token(token); } + inline void set_chat_id(const std::string &chat_id) { chat_id_ = chat_id; } + + void init_get_me(void); + void broadcast_opp(const gwarnt::arb_opp &opp); + void garbage_collect(void); + +private: + static constexpr time_t BROADCAST_INTERVAL = 300; + gwarnt::tgbot tg_; + std::string chat_id_; + std::unordered_map broadcast_map_; + + bool opp_need_broadcast(const gwarnt::arb_opp &opp); + void insert_broadcast_map(const gwarnt::arb_opp &opp); + std::string get_opp_hash(const gwarnt::arb_opp &opp); +}; + +inline +void gwarnt_bot::init_get_me(void) +{ + auto j = tg_.get_me(); + auto username = j["result"]["username"].get(); + + printf("========================================\n"); + printf("Bot username : %s\n", username.c_str()); + printf("Target Chat ID : %s\n", chat_id_.c_str()); + printf("========================================\n"); +} + +inline +std::string gwarnt_bot::get_opp_hash(const gwarnt::arb_opp &opp) +{ + std::string hash = ""; + + hash += opp.buy.ad_id_; + hash += opp.sell.ad_id_; + hash += to_string_wp(opp.buy.price_); + hash += to_string_wp(opp.sell.price_); + + return hash; +} + +inline +void gwarnt_bot::garbage_collect(void) +{ + auto it = broadcast_map_.begin(); + + while (it != broadcast_map_.end()) { + if (time(NULL) - it->second > BROADCAST_INTERVAL) + it = broadcast_map_.erase(it); + else + ++it; + } +} + +inline +bool gwarnt_bot::opp_need_broadcast(const gwarnt::arb_opp &opp) +{ + std::string hash = get_opp_hash(opp); + auto it = broadcast_map_.find(hash); + + if (it == broadcast_map_.end()) + return false; + + if (time(NULL) - it->second > BROADCAST_INTERVAL) { + broadcast_map_.erase(it); + return false; + } + + return true; +} + +inline +void gwarnt_bot::insert_broadcast_map(const gwarnt::arb_opp &opp) +{ + std::string hash = get_opp_hash(opp); + broadcast_map_[hash] = time(NULL); + printf("Inserted hash %s\n", hash.c_str()); +} + +static std::string merge_vec_str(const std::vector &vec) +{ + std::string ret = ""; + size_t i = 0; + + for (const auto &s : vec) + ret += (++i > 1 ? ", " : "") + s; + + return ret; +} + +inline +void gwarnt_bot::broadcast_opp(const gwarnt::arb_opp &opp) +{ + double min_avail; + double est_profit; + + if (opp_need_broadcast(opp)) + return; + + std::string msg = "Arbitrage opportunity detected!\n\n"; + const auto &b = opp.buy; + const auto &s = opp.sell; + + msg += "Buy on " + s.exchange_ + "\n"; + msg += "m: " + s.ad_id_ + " (" + s.merchant_name_ + ")\n"; + msg += "price: " + to_string_wp(b.price_, 2) + "\n"; + msg += "min_buy: " + to_string_wp(s.min_amount_, 2) + " " + s.fiat_ + "\n"; + msg += "max_buy: " + to_string_wp(s.max_amount_, 2) + " " + s.fiat_ + "\n"; + msg += "available: " + to_string_wp(s.tradable_amount_, 2) + " " + s.crypto_ + "\n"; + msg += "methods: " + merge_vec_str(s.methods_) + "\n"; + + msg += "\n"; + + msg += "Sell on " + b.exchange_ + "\n"; + msg += "m: " + b.ad_id_ + " (" + b.merchant_name_ + ")\n"; + msg += "price: " + to_string_wp(s.price_, 2) + "\n"; + msg += "min_sell: " + to_string_wp(b.min_amount_, 2) + " " + b.fiat_ + "\n"; + msg += "max_sell: " + to_string_wp(b.max_amount_, 2) + " " + b.fiat_ + "\n"; + msg += "available: " + to_string_wp(b.tradable_amount_, 2) + " " + b.crypto_ + "\n"; + msg += "methods: " + merge_vec_str(b.methods_) + "\n"; + + msg += "------------------------------------------\n"; + + if (b.tradable_amount_ < s.tradable_amount_) + min_avail = b.tradable_amount_; + else + min_avail = s.tradable_amount_; + + est_profit = s.price_ - b.price_; + msg += "est_profit_per_unit: " + to_string_wp(est_profit, 2) + " " + s.fiat_ + "\n"; + msg += "maximum_extractable_value: " + to_string_wp(min_avail * est_profit, 2) + " " + s.fiat_ + "\n"; + + tg_.send_message(chat_id_, msg); + insert_broadcast_map(opp); + sleep(3); +} + +static int broadcast_opp(gwarnt_bot *bot, const gwarnt::arb_opp &opp) +{ + bot->broadcast_opp(opp); + return 0; +} + +static int broadcast_opps(gwarnt_bot *bot, + const std::vector &opps) +{ + std::string msg; + gwarnt::tgbot::json j; + + if (opps.size() == 0) + return 0; + + for (const auto &opp : opps) + broadcast_opp(bot, opp); + + return 0; +} + +static void run(gwarnt_bot *bot, gwarnt::p2p::binance *bnc, + gwarnt::p2p::okx *okx) { std::vector buy_bnc, sell_bnc; std::vector buy_okx, sell_okx; - std::vector opportunities; + std::vector opps; + + printf("Fetching data...\n"); + + buy_bnc = bnc->get_data("IDR", "USDT", "BUY"); + sell_bnc = bnc->get_data("IDR", "USDT", "SELL"); + buy_okx = okx->get_data("IDR", "USDT", "BUY"); + sell_okx = okx->get_data("IDR", "USDT", "SELL"); + + printf("Binance : %lu buy, %lu sell\n", buy_bnc.size(), sell_bnc.size()); + printf("OKX : %lu buy, %lu sell\n", buy_okx.size(), sell_okx.size()); + + opps = gwarnt::find_arbitrage_opps(sell_okx, buy_bnc); + printf("Opp1 : %zu\n", opps.size()); + broadcast_opps(bot, opps); + + opps = gwarnt::find_arbitrage_opps(sell_bnc, buy_okx); + printf("Opp2 : %zu\n", opps.size()); + broadcast_opps(bot, opps); +} + +int main(void) +{ gwarnt::p2p::binance bnc; gwarnt::p2p::okx okx; + const char *tmp; + gwarnt_bot bot; + unsigned u = 5; + + tmp = std::getenv("TG_BOT_TOKEN"); + if (!tmp) { + printf("Please set TG_BOT_TOKEN environment variable!\n"); + return 1; + } + bot.set_token(tmp); + + tmp = std::getenv("TG_CHAT_ID"); + if (!tmp) { + printf("Please set TG_CHAT_ID environment variable!\n"); + return 1; + } + bot.set_chat_id(tmp); + bot.init_get_me(); while (1) { - printf("Fetching data ...\n"); - buy_bnc = bnc.get_data("IDR", "USDT", "BUY"); - sell_bnc = bnc.get_data("IDR", "USDT", "SELL"); - - buy_okx = okx.get_data("IDR", "USDT", "BUY"); - sell_okx = okx.get_data("IDR", "USDT", "SELL"); - printf("Binance: %lu buy, %lu sell\n", buy_bnc.size(), sell_bnc.size()); - printf("OKX: %lu buy, %lu sell\n", buy_okx.size(), sell_okx.size()); - - opportunities = gwarnt::find_arbitrage_opps(sell_okx, buy_bnc); - for (const auto &i : opportunities) { - std::cout << i.sell.dump() << std::endl; - std::cout << "====================" << std::endl; - std::cout << i.buy.dump() << std::endl; - std::cout << "----------------------------------------------" << std::endl; - } - opportunities = gwarnt::find_arbitrage_opps(sell_bnc, buy_okx); - for (const auto &i : opportunities) { - std::cout << i.sell.dump() << std::endl; - std::cout << "====================" << std::endl; - std::cout << i.buy.dump() << std::endl; - std::cout << "----------------------------------------------" << std::endl; + try { + run(&bot, &bnc, &okx); + u = 5; + } catch (std::exception &e) { + printf("Exception: %s\n", e.what()); + + // Double the sleep time if exception occurs. + u *= 2; } - printf("Sleeping...\n"); - sleep(5); + bot.garbage_collect(); + printf("Sleeping for %u seconds...\n", u); + sleep(u); } return 0; diff --git a/src/gwarnt/net.cpp b/src/gwarnt/net.cpp index 8aa78f4..da38047 100644 --- a/src/gwarnt/net.cpp +++ b/src/gwarnt/net.cpp @@ -129,6 +129,8 @@ void net::exec(void) curl_easy_setopt(ch_, CURLOPT_PROXY, proxy_.c_str()); curl_easy_setopt(ch_, CURLOPT_WRITEFUNCTION, &curl_write_callback); curl_easy_setopt(ch_, CURLOPT_WRITEDATA, &resp_); + curl_easy_setopt(ch_, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(ch_, CURLOPT_CONNECTTIMEOUT, 10L); resp_.clear(); res = curl_easy_perform(ch_); diff --git a/src/gwarnt/tgbot.cpp b/src/gwarnt/tgbot.cpp new file mode 100644 index 0000000..2c39c37 --- /dev/null +++ b/src/gwarnt/tgbot.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include + +using json = nlohmann::json; + +namespace gwarnt { + +tgbot::tgbot(const std::string &token): + token_(token) +{ + init(); +} + +tgbot::tgbot(void) +{ + init(); +} + +inline void tgbot::init(void) +{ + net_.add_header("Content-Type", "application/json"); +} + +tgbot::~tgbot(void) = default; + +json tgbot::send_message(const std::string &chat_id, const std::string &text, + tgbot_send_msg_t parse_mode) +{ + json j; + + j["chat_id"] = chat_id; + j["text"] = text; + + switch (parse_mode) { + case SEND_MSG_HTML: + j["parse_mode"] = "HTML"; + break; + case SEND_MSG_MARKDOWN: + j["parse_mode"] = "Markdown"; + break; + default: + break; + } + + return exec_post("sendMessage", j); +} + +json tgbot::get_me(void) +{ + return exec_get("getMe"); +} + +json tgbot::exec_post(const std::string &method, const json &data) +{ + std::string url = "https://api.telegram.org/bot" + token_ + "/" + method; + + net_.set_url(url); + net_.set_data(data.dump()); + net_.set_method("POST"); + net_.exec(); + return json::parse(net_.get_resp()); +} + +json tgbot::exec_get(const std::string &method) +{ + std::string url = "https://api.telegram.org/bot" + token_ + "/" + method; + + net_.set_url(url); + net_.set_method("GET"); + net_.exec(); + return json::parse(net_.get_resp()); +} + +} /* namespace gwarnt */ diff --git a/src/gwarnt/tgbot.hpp b/src/gwarnt/tgbot.hpp new file mode 100644 index 0000000..73cae79 --- /dev/null +++ b/src/gwarnt/tgbot.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#ifndef GWARNT__TGBOT_HPP +#define GWARNT__TGBOT_HPP + +#include +#include + +#include + +namespace gwarnt { + +typedef enum tgbot_send_msg { + SEND_MSG_PLAIN, + SEND_MSG_HTML, + SEND_MSG_MARKDOWN, +} tgbot_send_msg_t; + +class tgbot { +public: + using json = nlohmann::json; + + tgbot(const std::string &token); + tgbot(void); + ~tgbot(void); + + json send_message(const std::string &chat_id, const std::string &text, + tgbot_send_msg_t parse_mode = SEND_MSG_PLAIN); + + json get_me(void); + + void set_token(const std::string &token) { token_ = token; } + +private: + json exec_post(const std::string &method, const json &data); + json exec_get(const std::string &method); + void init(void); + + net net_; + + std::string token_; +}; + +} /* namespace gwarnt */ + +#endif /* #ifndef GWARNT__TGBOT_HPP */ -- Alviro Iskandar Setiawan