├── bot ├── __init__.py ├── telegram_bot │ ├── __init__.py │ ├── common.py │ ├── remove_alert.py │ ├── inactivate_alert.py │ ├── activate_alert.py │ ├── show_alerts.py │ ├── prices.py │ ├── add_alert.py │ └── main.py ├── alerts │ ├── __init__.py │ ├── alert.py │ └── alert_manager.py ├── database.py ├── utils.py └── binance_api.py ├── run.py ├── docker-compose.yml ├── requirements.txt ├── Dockerfile ├── LICENSE ├── README.md └── .gitignore /bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/telegram_bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/alerts/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert import Alert 2 | from .alert_manager import AlertManager 3 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from bot.telegram_bot.main import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bot: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - .:/usr/src/app 10 | environment: 11 | - PYTHONUNBUFFERED=1 12 | - PYTHONIOENCODING=utf-8 13 | restart: always 14 | -------------------------------------------------------------------------------- /bot/telegram_bot/common.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import ConversationHandler 2 | from telegram import Update 3 | 4 | 5 | async def cancel(update: Update, context) -> int: 6 | await update.message.reply_text('Conversation canceled.') 7 | return ConversationHandler.END 8 | 9 | 10 | async def error(update: Update, context): 11 | print(f'Update {update} caused error {context.error}') 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.6 2 | aiosignal==1.2.0 3 | aiosqlite==0.19.0 4 | anyio==4.0.0 5 | async-timeout==4.0.3 6 | attrs==23.1.0 7 | certifi==2023.7.22 8 | cffi==1.16.0 9 | charset-normalizer==3.3.2 10 | cryptography==41.0.5 11 | exceptiongroup==1.1.3 12 | frozenlist==1.4.0 13 | future==0.18.3 14 | h11==0.14.0 15 | httpcore==1.0.1 16 | httpx==0.25.1 17 | idna==3.4 18 | multidict==6.0.4 19 | pycparser==2.21 20 | python-telegram-bot==20.6 21 | requests==2.31.0 22 | sniffio==1.3.0 23 | tornado==6.3.3 24 | urllib3==2.0.7 25 | yarl==1.9.2 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9.6 3 | 4 | # Set the working directory in the container 5 | WORKDIR /usr/src/app 6 | 7 | # Copy the dependencies file to the working directory 8 | COPY requirements.txt ./ 9 | 10 | # Update pip 11 | RUN pip install --upgrade pip 12 | 13 | # Install any dependencies 14 | RUN pip install --no-cache-dir -r requirements.txt 15 | 16 | # Copy the rest of your application's code 17 | COPY . . 18 | 19 | # Command to run the application 20 | ENTRYPOINT ["python", "run.py"] 21 | -------------------------------------------------------------------------------- /bot/telegram_bot/remove_alert.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CallbackContext 3 | from bot.alerts.alert_manager import AlertManager 4 | 5 | alert_manager = AlertManager() 6 | 7 | 8 | async def remove_alert(update: Update, context: CallbackContext) -> None: 9 | user_id = update.effective_user.id 10 | alert_id = int(context.args[0]) if context.args else None 11 | 12 | if alert_id is None: 13 | await update.message.reply_text('Please provide the ID of the alert you wish to remove.\nUsage: /remove_alert ') 14 | return 15 | 16 | alert = alert_manager.alerts.get(alert_id) 17 | if alert: 18 | if alert.user_id == user_id: 19 | await alert_manager.remove_alert(alert_id) 20 | message = f"Alert {alert_id} has been removed." 21 | else: 22 | message = f"Alert {alert_id} does not belong to user {user_id}." 23 | else: 24 | message = "Alert does not exist!" 25 | 26 | await update.message.reply_text(message) 27 | -------------------------------------------------------------------------------- /bot/telegram_bot/inactivate_alert.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CallbackContext 3 | from bot.alerts.alert_manager import AlertManager 4 | 5 | alert_manager = AlertManager() 6 | 7 | 8 | async def inactivate_alert(update: Update, context: CallbackContext) -> None: 9 | user_id = update.effective_user.id 10 | alert_id = int(context.args[0]) if context.args else None 11 | 12 | if alert_id is None: 13 | await update.message.reply_text('Please provide the ID of the alert you wish to inactivate.\nUsage: /inactivate_alert ') 14 | return 15 | 16 | alert = alert_manager.alerts.get(alert_id) 17 | if alert: 18 | if alert.user_id == user_id: 19 | await alert_manager.inactivate_alert(alert_id) 20 | message = f"Alert {alert_id} has been inactivated." 21 | else: 22 | message = f"Alert {alert_id} does not belong to user {user_id}." 23 | else: 24 | message = "Alert does not exist!" 25 | 26 | await update.message.reply_text(message) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fadi Younes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot/telegram_bot/activate_alert.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CallbackContext 3 | from bot.alerts.alert_manager import AlertManager 4 | 5 | alert_manager = AlertManager() 6 | 7 | 8 | async def activate_alert(update: Update, context: CallbackContext) -> None: 9 | user_id = update.effective_user.id 10 | alert_id = int(context.args[0]) if context.args else None 11 | 12 | if alert_id is None: 13 | await update.message.reply_text('Please provide the ID of the alert you wish to activate.\nUsage: /activate_alert ') 14 | return 15 | 16 | alert = alert_manager.alerts.get(alert_id) 17 | if alert: 18 | print(alert.user_id, user_id, alert.user_id == user_id, type(alert.user_id), type(user_id)) 19 | if alert.user_id == user_id: 20 | await alert_manager.activate_alert(alert_id) 21 | message = f"Alert {alert_id} has been activated." 22 | else: 23 | message = f"Alert {alert_id} does not belong to user {user_id}." 24 | else: 25 | message = "Alert does not exist!" 26 | 27 | await update.message.reply_text(message) 28 | -------------------------------------------------------------------------------- /bot/telegram_bot/show_alerts.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CallbackContext 3 | from bot.alerts.alert_manager import AlertManager 4 | 5 | alert_manager = AlertManager() 6 | 7 | 8 | async def show_alerts(update: Update, context: CallbackContext) -> None: 9 | user_id = update.effective_user.id 10 | user_alerts = [alert for alert in alert_manager.alerts.values() if alert.user_id == user_id] 11 | 12 | if not user_alerts: 13 | await update.message.reply_text('You have no alerts set.') 14 | return 15 | 16 | message = 'Your alerts:\n\n' 17 | for i, alert in enumerate(user_alerts): 18 | status = 'Active' if alert.active else 'Inactive' 19 | message += (f"ID: {alert.alert_id}\n\n" 20 | f"{alert.asset}\{alert.fiat}\n" 21 | f"Type: {alert.trade_type}\n" 22 | f"Threshold: {alert.threshold_price}\n" 23 | f"Payment Type: {alert.payment_method}\n" 24 | f"Status: {status}\n") 25 | if i < len(user_alerts) - 1: 26 | message += '\n-------\n\n' 27 | 28 | await update.message.reply_text(message, parse_mode='HTML') 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binance P2P alerts Telegram Bot 2 | ![Python 3.9.6](https://img.shields.io/badge/python-3.9.6-blue.svg) ![MIT License](https://img.shields.io/badge/license-MIT-green.svg) 3 | 4 | This repository contains a Telegram bot, [@binance_p2p_alertsbot](https://t.me/binance_p2p_alertsbot), that interacts with the Binance P2P platform. It allows users to fetch the latest prices and set price alerts directly through Telegram. 5 | 6 | ## Features 7 | 8 | - `/prices`: Retrieves the top 5 offers for a specified crypto and fiat asset pair on Binance P2P. It prompts the user to enter the details of the crypto asset, fiat currency, and order type (buy/sell), then displays the best available offers. 9 | 10 | - `/show_alerts`: Lists all active price alerts set by the user, showing details such as asset type, fiat currency, price threshold, and whether each alert is active or inactive. 11 | 12 | - `/add_alert`: Sets up a new price alert for Binance P2P. The bot will ask for the crypto asset, fiat asset, order type, threshold price, and payment type, then monitors the prices and notifies the user when conditions are met. 13 | 14 | - `/remove_alert`: Deletes a specified price alert. The user provides the ID of the alert they wish to remove. 15 | 16 | - `/inactivate_alert`: Temporarily disables a specified alert without deleting it. This can be used to pause alerts during times of low trading activity. 17 | 18 | - `/activate_alert`: Reactivates a specified price alert that was previously inactivated. 19 | 20 | - `/cancel`: Cancels the current conversation or command input, allowing the user to start over or choose a different action. 21 | 22 | ## Setup 23 | 24 | With Docker, setting up the bot is straightforward: 25 | 26 | 1. Clone the repository. 27 | 2. Run `docker-compose build` to build the Docker container. 28 | 3. Run `docker-compose up` to start the bot. 29 | 30 | Ensure you have a `secret.py` file with your Telegram Bot Token `TELEGRAM_TOKEN = ` before starting the bot. 31 | 32 | ## Contributing 33 | 34 | Contributions are welcome! Please feel free to submit a pull request or open an issue for bugs, feature requests, or other concerns. 35 | 36 | ## License 37 | 38 | This project is open-sourced under the [MIT License](LICENSE). 39 | -------------------------------------------------------------------------------- /bot/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from bot.alerts import Alert 3 | 4 | 5 | class Database: 6 | def __init__(self, db_name): 7 | self.db_name = db_name 8 | 9 | def init_db(self): 10 | with sqlite3.connect(self.db_name) as db: 11 | db.execute(''' 12 | CREATE TABLE IF NOT EXISTS alerts ( 13 | alert_id INTEGER PRIMARY KEY, 14 | user_id INTEGER NOT NULL, 15 | asset TEXT NOT NULL, 16 | fiat TEXT NOT NULL, 17 | trade_type TEXT NOT NULL, 18 | threshold_price REAL NOT NULL, 19 | payment_method TEXT NOT NULL, 20 | active BOOLEAN NOT NULL, 21 | last_triggered TIMESTAMP 22 | ) 23 | ''') 24 | db.commit() 25 | 26 | def insert_alert(self, alert): 27 | with sqlite3.connect(self.db_name) as db: 28 | db.execute(''' 29 | INSERT INTO alerts (alert_id, user_id, asset, fiat, trade_type, threshold_price, payment_method, active) 30 | VALUES (?, ?, ?, ?, ?, ?, ?, ?) 31 | ''', (alert.alert_id, alert.user_id, alert.asset, alert.fiat, alert.trade_type, alert.threshold_price, alert.payment_method, 1)) 32 | db.commit() 33 | 34 | def delete_alert(self, alert_id): 35 | with sqlite3.connect(self.db_name) as db: 36 | db.execute('DELETE FROM alerts WHERE alert_id = ?', (alert_id,)) 37 | db.commit() 38 | 39 | def update_last_triggered(self, alert_id, last_triggered): 40 | with sqlite3.connect(self.db_name) as db: 41 | db.execute('UPDATE alerts SET last_triggered = ? WHERE alert_id = ?', (last_triggered, alert_id)) 42 | db.commit() 43 | 44 | def load_alerts(self): 45 | alerts = {} 46 | with sqlite3.connect(self.db_name) as db: 47 | cursor = db.execute('SELECT * FROM alerts') 48 | for row in cursor.fetchall(): 49 | alert = Alert(*row[:-2]) 50 | alert.active = row[-2] 51 | alert.last_triggered = row[-1] 52 | alerts[alert.alert_id] = alert 53 | return alerts 54 | -------------------------------------------------------------------------------- /bot/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from secret import TELEGRAM_TOKEN 3 | 4 | 5 | def send_telegram_message(user_id, message): 6 | """ 7 | Send a message to a user from a Telegram bot. 8 | 9 | Parameters: 10 | user_id (str): Unique identifier for the target user or username of the target channel. 11 | message (str): Text of the message to be sent. 12 | 13 | Returns: 14 | dict: Response from the Telegram API. 15 | """ 16 | # Telegram API endpoint for sending messages 17 | send_message_url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage" 18 | 19 | # Parameters for the API request 20 | params = { 21 | 'chat_id': user_id, 22 | 'text': message, 23 | 'parse_mode': 'HTML' 24 | } 25 | 26 | # Making the request to the Telegram API 27 | response = requests.post(send_message_url, params=params) 28 | 29 | # Returning the response as a Python dictionary 30 | return response.json() 31 | 32 | 33 | def to_float(s): 34 | if s is None: 35 | return None 36 | else: 37 | return float(s) 38 | 39 | 40 | def format_table(data, columns, html=False): 41 | """ 42 | Formats a 2D list into a string representation of a table with fixed column widths. 43 | Optionally formats the output as HTML with bold column names. 44 | 45 | :param data: A 2D list of data. 46 | :param columns: A list of column names. 47 | :param html: A boolean indicating whether to format output as HTML. 48 | :return: A string representing the data in table format. 49 | """ 50 | # Determine the maximum width for each column 51 | col_widths = [max(len(str(item)) for item in column_data) for column_data in zip(*data)] 52 | 53 | formatted_rows = [] 54 | for row in data: 55 | formatted_row = [] 56 | for i, col in enumerate(columns): 57 | # Wrap column names in if html is True 58 | col_name = f"{col}" if html else col 59 | # Format each item to have a fixed width, left aligned 60 | formatted_item = f"{col_name}: {str(row[i]).ljust(col_widths[i])}" 61 | formatted_row.append(formatted_item) 62 | formatted_rows.append(", ".join(formatted_row)) 63 | return "\n".join(formatted_rows) 64 | -------------------------------------------------------------------------------- /bot/alerts/alert.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from bot.binance_api import get_offers, get_link 3 | from bot.utils import send_telegram_message 4 | 5 | 6 | class Alert: 7 | def __init__(self, alert_id, user_id, asset, fiat, trade_type, threshold_price, payment_method): 8 | self.alert_id = alert_id 9 | self.user_id = user_id 10 | self.asset = asset 11 | self.fiat = fiat 12 | self.trade_type = trade_type 13 | self.threshold_price = threshold_price 14 | self.payment_method = payment_method 15 | self.active = True 16 | self.last_triggered = None # Track when the alert was last triggered 17 | self.trigger_interval = 15 # in minutes 18 | self.link = get_link(self.fiat, self.asset, self.payment_method, self.trade_type) 19 | 20 | async def check_alert(self): 21 | """ 22 | Check if the current offers meet the alert condition. 23 | """ 24 | if self.active and (self.last_triggered is None or datetime.now() >= self.last_triggered + timedelta(minutes=self.trigger_interval)): 25 | offers = await get_offers(self.asset, self.fiat, self.trade_type, payment_method=self.payment_method) 26 | for offer in offers: 27 | price = float(offer['price']) 28 | if (self.trade_type == 'Sell' and price >= self.threshold_price) or \ 29 | (self.trade_type == 'Buy' and price <= self.threshold_price): 30 | await self.trigger_alert(price, offer['min_amount'], offer['max_amount']) 31 | break 32 | 33 | async def trigger_alert(self, price, min_amount, max_amount): 34 | """ 35 | Trigger the alert. This should notify the user. 36 | """ 37 | message = (f"Alert {self.alert_id}:\n\n" 38 | f"{self.payment_method} {self.asset}/{self.fiat} {self.trade_type} at {price}\n" 39 | f"Min amount: {min_amount}, Max amount: {max_amount}\n\n" 40 | f"{get_link(self.fiat, self.asset, self.payment_method, self.trade_type)}") 41 | send_telegram_message(self.user_id, message) 42 | # Send a message to the user using the Telegram Bot API 43 | self.last_triggered = datetime.now() 44 | -------------------------------------------------------------------------------- /bot/alerts/alert_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from itertools import count 3 | from . import Alert 4 | from bot.database import Database 5 | 6 | 7 | class Singleton(type): 8 | _instances = {} 9 | 10 | def __call__(cls, *args, **kwargs): 11 | if cls not in cls._instances: 12 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 13 | return cls._instances[cls] 14 | 15 | 16 | class AlertManager(metaclass=Singleton): 17 | def __init__(self): 18 | self.lock = asyncio.Lock() # Use an asyncio Lock instead of threading.Lock 19 | self.loop = asyncio.get_event_loop() 20 | self.loop.create_task(self.start_checking()) 21 | self.db = Database('alerts.db') # Initialize the Database class 22 | self.db.init_db() # Ensure the database is set up 23 | self.alerts = self.db.load_alerts() 24 | start_id = 1 + max([0] + [alert.alert_id for alert in self.alerts.values()]) 25 | self._id_generator = count(start=start_id) # Unique ID generator 26 | 27 | async def add_alert(self, user_id, asset, fiat, trade_type, threshold_price, payment_method): 28 | async with self.lock: 29 | alert_id = next(self._id_generator) 30 | alert = Alert(alert_id, user_id, asset, fiat, trade_type, threshold_price, payment_method) 31 | self.db.insert_alert(alert) 32 | self.alerts[alert_id] = alert 33 | return alert_id, alert.link 34 | 35 | async def remove_alert(self, alert_id): 36 | async with self.lock: 37 | del self.alerts[alert_id] 38 | self.db.delete_alert(alert_id) 39 | 40 | async def inactivate_alert(self, alert_id): 41 | async with self.lock: 42 | alert = self.alerts.get(alert_id) 43 | alert.active = False 44 | 45 | async def activate_alert(self, alert_id): 46 | async with self.lock: 47 | alert = self.alerts.get(alert_id) 48 | alert.active = True 49 | 50 | async def check_alerts(self): 51 | """ 52 | Check all active alerts concurrently against current offers. 53 | """ 54 | tasks = [] 55 | for alert_id, alert in list(self.alerts.items()): 56 | if alert.active: 57 | # Schedule the alert check for concurrent execution 58 | tasks.append(alert.check_alert()) 59 | 60 | # Use asyncio.gather to run tasks concurrently 61 | await asyncio.gather(*tasks) 62 | 63 | async def start_checking(self, interval=1): 64 | """ 65 | Start the asynchronous loop that checks alerts. 66 | """ 67 | while True: 68 | await self.check_alerts() 69 | await asyncio.sleep(interval) 70 | -------------------------------------------------------------------------------- /bot/telegram_bot/prices.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import ConversationHandler 3 | from bot.binance_api import get_offers, get_link 4 | from bot.utils import format_table 5 | 6 | # Define the states used for the /prices conversation 7 | (GET_CRYPTO, GET_FIAT, GET_ORDER_TYPE, GET_PAYMENT_METHOD) = range(4) 8 | 9 | 10 | def format_offers_message(offers, context): 11 | message = "Top 5 offers::\n\n" 12 | 13 | data = [] 14 | for offer in offers: 15 | data.append([f"{offer['price']} {context.user_data['fiat']}", offer['min_amount'], offer['max_amount']]) 16 | message += format_table(data, columns=['Price', 'Min', 'Max'], html=True) + '\n' 17 | 18 | link = get_link(context.user_data['fiat'], context.user_data['asset'], 19 | context.user_data['payment_method'], context.user_data['order_type']) 20 | message += (f"\n{link}\n" 21 | f"Don't forget to uncheck 'Only show Merchant Ads'") 22 | 23 | return message 24 | 25 | 26 | async def start_prices(update: Update, context) -> int: 27 | await update.message.reply_text("Please enter the crypto asset (e.g., USDT, BTC):") 28 | return GET_CRYPTO 29 | 30 | 31 | async def get_crypto(update: Update, context) -> int: 32 | context.user_data['asset'] = update.message.text.upper() 33 | await update.message.reply_text("Please enter the fiat asset (e.g., USD, EUR):") 34 | return GET_FIAT 35 | 36 | 37 | async def get_fiat(update: Update, context) -> int: 38 | context.user_data['fiat'] = update.message.text.upper() 39 | await update.message.reply_text("Please enter the order type 'Buy' or 'Sell':") 40 | return GET_ORDER_TYPE 41 | 42 | 43 | async def get_order_type(update: Update, context) -> int: 44 | order_type = update.message.text.capitalize() 45 | 46 | if order_type not in ['Buy', 'Sell']: 47 | await update.message.reply_text("Invalid order type. Please type 'Buy' or 'Sell':") 48 | return GET_ORDER_TYPE # This will ask for the order type again 49 | 50 | context.user_data['order_type'] = order_type 51 | 52 | await update.message.reply_text("Please enter the payment type for the alert (e.g., Wise, Bank):") 53 | return GET_PAYMENT_METHOD 54 | 55 | 56 | async def get_payment_method(update: Update, context) -> int: 57 | context.user_data['payment_method'] = update.message.text.capitalize() 58 | 59 | offers = await get_offers( 60 | context.user_data['asset'], 61 | context.user_data['fiat'], 62 | context.user_data['order_type'], 63 | context.user_data['payment_method'] 64 | ) 65 | offers_message = format_offers_message(offers, context) 66 | 67 | await update.message.reply_text(offers_message, parse_mode='HTML') 68 | return ConversationHandler.END 69 | -------------------------------------------------------------------------------- /bot/telegram_bot/add_alert.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CallbackContext, ConversationHandler 3 | from bot.alerts import AlertManager 4 | 5 | # Define the states used for the /add_alert conversation 6 | (GET_CRYPTO, GET_FIAT, GET_ORDER_TYPE, GET_PAYMENT_METHOD, GET_THRESHOLD) = range(5) 7 | 8 | alert_manager = AlertManager() 9 | 10 | 11 | async def start_add_alert(update: Update, context: CallbackContext) -> int: 12 | await update.message.reply_text("Please enter the crypto asset for the alert (e.g., USDT, BTC):") 13 | return GET_CRYPTO 14 | 15 | 16 | async def get_crypto(update: Update, context: CallbackContext) -> int: 17 | context.user_data['alert_asset'] = update.message.text.upper() 18 | await update.message.reply_text("Please enter the fiat asset for the alert (e.g., USD, EUR):") 19 | return GET_FIAT 20 | 21 | 22 | async def get_fiat(update: Update, context: CallbackContext) -> int: 23 | context.user_data['alert_fiat'] = update.message.text.upper() 24 | await update.message.reply_text("Is this alert for a 'Buy' or 'Sell' order?") 25 | return GET_ORDER_TYPE 26 | 27 | 28 | async def get_order_type(update: Update, context: CallbackContext) -> int: 29 | order_type = update.message.text.capitalize() 30 | if order_type not in ['Buy', 'Sell']: 31 | await update.message.reply_text("Invalid order type. Please type 'Buy' or 'Sell':") 32 | return GET_ORDER_TYPE # This will ask for the order type again 33 | 34 | context.user_data['alert_order_type'] = order_type 35 | 36 | await update.message.reply_text("Please enter the payment type for the alert (e.g., Wise, Bank):") 37 | return GET_PAYMENT_METHOD 38 | 39 | 40 | async def get_payment_method(update: Update, context: CallbackContext) -> int: 41 | context.user_data['alert_payment_method'] = update.message.text.capitalize() 42 | 43 | await update.message.reply_text("Please enter the threshold price for the alert:") 44 | return GET_THRESHOLD 45 | 46 | 47 | async def get_threshold(update: Update, context: CallbackContext) -> int: 48 | # Validate the threshold price input 49 | try: 50 | threshold_price = float(update.message.text) 51 | context.user_data['alert_threshold'] = threshold_price 52 | except ValueError: 53 | await update.message.reply_text("Invalid input. Please enter a valid number for the threshold price.") 54 | return GET_THRESHOLD 55 | 56 | # Add the alert to the AlertManager 57 | user_id = update.effective_user.id 58 | alert_id, link = await alert_manager.add_alert( 59 | user_id, 60 | context.user_data['alert_asset'], 61 | context.user_data['alert_fiat'], 62 | context.user_data['alert_order_type'], 63 | context.user_data['alert_threshold'], 64 | context.user_data['alert_payment_method'] 65 | ) 66 | message = f"Alert {alert_id} set successfully!\n" \ 67 | f"Watching offers at {link}" 68 | 69 | await update.message.reply_text(message) 70 | return ConversationHandler.END 71 | -------------------------------------------------------------------------------- /bot/binance_api.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import aiohttp 3 | import asyncio 4 | from bot.utils import to_float 5 | from typing import List 6 | 7 | # Binance P2P API endpoint 8 | BINANCE_P2P_API_URL = 'https://p2p.binance.com/bapi/c2c/v2/friendly/c2c/adv/search' 9 | 10 | 11 | def get_link(fiat: str, asset: str, payment_method: str, order_type: str): 12 | """ 13 | Get the link to the offers from Binance P2P. 14 | 15 | :param asset: Cryptocurrency asset, e.g., 'USDT', 'BTC'. 16 | :param fiat: Fiat currency, e.g., 'USD', 'EUR'. 17 | :param payment_method: payment type, default is "Wise". 18 | :param order_type: Order type, either 'Buy' or 'Sell'. 19 | :return: str, link to the offers from Binance P2P. 20 | """ 21 | url = f"https://p2p.binance.com/en/trade/{order_type}/{payment_method}/{asset}?fiat={fiat}" 22 | return url 23 | 24 | 25 | async def get_offers(asset: str, fiat: str, trade_type: str, payment_method: str, 26 | rows: int = 5, page: int = 1, trans_amount: str = None) -> List[dict]: 27 | """ 28 | Fetch the best offers from Binance P2P. 29 | 30 | :param asset: Cryptocurrency asset, e.g., 'USDT', 'BTC'. 31 | :param fiat: Fiat currency, e.g., 'USD', 'EUR'. 32 | :param trade_type: Trade type, either 'Buy' or 'Sell'. 33 | :param rows: Number of offers to retrieve, default is 5. 34 | :param page: Page number for pagination, default is 1. 35 | :param trans_amount: Transaction amount for filtering offers. 36 | :param payment_method: payment type, default is "Wise". 37 | :return: List of offers from Binance P2P. 38 | """ 39 | data = { 40 | "asset": asset, 41 | "fiat": fiat, 42 | "merchantCheck": 'true', # Assuming this should always be true for more reliable offers. 43 | "page": page, 44 | "payTypes": [payment_method], 45 | "publisherType": None, # Assuming we don't filter by publisher type. 46 | "rows": rows, 47 | "tradeType": trade_type, 48 | "transAmount": trans_amount 49 | } 50 | headers = { 51 | "Content-Type": "application/json" 52 | } 53 | 54 | async with aiohttp.ClientSession() as session: 55 | async with session.post(BINANCE_P2P_API_URL, json=data, headers=headers) as response: 56 | if response.status == 200: 57 | response_json = await response.json() 58 | offers_data = response_json.get('data', []) 59 | offers = [{ 60 | 'price': to_float(adv.get('price')), 61 | 'min_amount': to_float(adv.get('minSingleTransAmount')), 62 | 'max_amount': to_float(adv.get('maxSingleTransAmount')) 63 | } for item in offers_data for adv in [item.get('adv', {})]] 64 | return offers 65 | else: 66 | raise Exception(f"Error fetching offers from Binance P2P: {response.status} - {await response.text()}") 67 | 68 | 69 | async def main(): 70 | # Fetch best offers for USDT/USD for a Buy trade 71 | offers = await get_offers('USDT', 'USD', 'Buy') 72 | pprint.pprint(offers, indent=2) 73 | 74 | 75 | if __name__ == "__main__": 76 | asyncio.run(main()) 77 | -------------------------------------------------------------------------------- /bot/telegram_bot/main.py: -------------------------------------------------------------------------------- 1 | from secret import TELEGRAM_TOKEN 2 | from telegram.ext import CommandHandler, MessageHandler, filters 3 | from telegram.ext import Application 4 | from bot.telegram_bot import add_alert, prices 5 | from .common import * 6 | from .show_alerts import * 7 | from .remove_alert import * 8 | from .inactivate_alert import * 9 | from .activate_alert import * 10 | 11 | # Define conversation states 12 | (CRYPTO, FIAT, ORDER_TYPE, PRICE, REMOVE_ALERT) = range(5) 13 | 14 | 15 | def main(): 16 | # Create the Application and pass it your bot's token. 17 | application = Application.builder().token(TELEGRAM_TOKEN).build() 18 | 19 | application.add_error_handler(error) 20 | 21 | # Define and add conversation handler for the /prices command 22 | prices_conv_handler = ConversationHandler( 23 | entry_points=[CommandHandler('prices', prices.start_prices)], 24 | states={ 25 | prices.GET_CRYPTO: [MessageHandler(filters.TEXT & ~filters.COMMAND, prices.get_crypto)], 26 | prices.GET_FIAT: [MessageHandler(filters.TEXT & ~filters.COMMAND, prices.get_fiat)], 27 | prices.GET_ORDER_TYPE: [MessageHandler(filters.TEXT & ~filters.COMMAND, prices.get_order_type)], 28 | prices.GET_PAYMENT_METHOD: [MessageHandler(filters.TEXT & ~filters.COMMAND, prices.get_payment_method)], 29 | }, 30 | fallbacks=[CommandHandler('cancel', cancel)] 31 | ) 32 | application.add_handler(prices_conv_handler) 33 | 34 | # Define and add conversation handler for the /add_alert command 35 | add_alert_conv_handler = ConversationHandler( 36 | entry_points=[CommandHandler('add_alert', add_alert.start_add_alert)], 37 | states={ 38 | add_alert.GET_CRYPTO: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_alert.get_crypto)], 39 | add_alert.GET_FIAT: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_alert.get_fiat)], 40 | add_alert.GET_ORDER_TYPE: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_alert.get_order_type)], 41 | add_alert.GET_PAYMENT_METHOD: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_alert.get_payment_method)], 42 | add_alert.GET_THRESHOLD: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_alert.get_threshold)], 43 | }, 44 | fallbacks=[CommandHandler('cancel', cancel)] 45 | ) 46 | application.add_handler(add_alert_conv_handler) 47 | 48 | # Define and add command handler for the /show_alerts command 49 | show_alerts_handler = CommandHandler('show_alerts', show_alerts) 50 | application.add_handler(show_alerts_handler) 51 | 52 | # Define and add command handler for the /remove_alert command 53 | remove_alert_handler = CommandHandler('remove_alert', remove_alert) 54 | application.add_handler(remove_alert_handler) 55 | 56 | # Define and add command handler for the /inactivate_alert command 57 | inactivate_alert_handler = CommandHandler('inactivate_alert', inactivate_alert) 58 | application.add_handler(inactivate_alert_handler) 59 | 60 | # Define and add command handler for the /inactivate_alert command 61 | inactivate_alert_handler = CommandHandler('inactivate_alert', inactivate_alert) 62 | application.add_handler(inactivate_alert_handler) 63 | 64 | # Define and add command handler for the /activate_alert command 65 | activate_alert_handler = CommandHandler('activate_alert', activate_alert) 66 | application.add_handler(activate_alert_handler) 67 | 68 | # Run the bot indefinitely 69 | print('Running bot...') 70 | application.run_polling() 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | 163 | # databases 164 | *.db 165 | 166 | # secret variables 167 | secret.py --------------------------------------------------------------------------------