From: Alviro Iskandar Setiawan <[email protected]>
To: Ammar Faizi <[email protected]>,
Michael William Jonathan <[email protected]>
Cc: Alviro Iskandar Setiawan <[email protected]>,
Ravel Kevin Ethan <[email protected]>,
GNU/Weeb Mailing List <[email protected]>
Subject: [RFC PATCH 9/9] gwarnt: Add Telegram bot
Date: Tue, 10 Sep 2024 23:44:14 +0200 [thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
Send a notification to Telegram using bot if arbitrage opportunities
detected.
Signed-off-by: Alviro Iskandar Setiawan <[email protected]>
---
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 <unistd.h>
+#include <ctime>
+#include <cstdlib>
+#include <sstream>
#include <iostream>
+#include <unordered_map>
+
+#include <gwarnt/tgbot.hpp>
#include <gwarnt/p2p/okx.hpp>
#include <gwarnt/p2p/binance.hpp>
#include <gwarnt/arbitrage.hpp>
+#include <gwarnt/helpers.hpp>
-int main(void)
+template <typename T>
+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<std::string, time_t> 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<std::string>();
+
+ 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<std::string> &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<gwarnt::arb_opp> &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<gwarnt::p2p_ad> buy_bnc, sell_bnc;
std::vector<gwarnt::p2p_ad> buy_okx, sell_okx;
- std::vector<gwarnt::arb_opp> opportunities;
+ std::vector<gwarnt::arb_opp> 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 <gwarnt/tgbot.hpp>
+
+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 <gwarnt/net.hpp>
+#include <gwarnt/json.hpp>
+
+#include <string>
+
+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
next prev parent reply other threads:[~2024-09-10 21:44 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-09-10 21:44 [RFC PATCH 0/9] Introducing GNU/Weeb Arbitrage Opportunity Notification Bot Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 2/9] gwarnt: Create initial P2P ad data structure Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 3/9] gwarnt: p2p: Add P2P Binance Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 4/9] gwarnt: p2p: Add P2P OKX Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 5/9] gwarnt: Create function to find arbitrage opportunities Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 6/9] gwarnt: p2p/binance: Fix invalid page Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 7/9] gwarnt: Create the initial example Alviro Iskandar Setiawan
2024-09-10 21:44 ` [RFC PATCH 8/9] gwarnt: Add README file Alviro Iskandar Setiawan
2024-09-10 21:44 ` Alviro Iskandar Setiawan [this message]
2024-09-10 22:21 ` [RFC PATCH 0/9] Introducing GNU/Weeb Arbitrage Opportunity Notification Bot Ammar Faizi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240910214414.3401712-10-alviro.iskandar@gnuweeb.org \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox