public inbox for [email protected]
 help / color / mirror / Atom feed
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


  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