├── LICENSE ├── README.md ├── config_example.env └── tri_arb_bot.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 guldo111 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | """ 2 | # Triangular Arbitrage Bot 3 | 4 | This Python script implements a Triangular Arbitrage Bot using the CCXT library to interact with various cryptocurrency exchanges. The bot scans the markets for arbitrage opportunities across different trading pairs and automatically executes trades to take advantage of price discrepancies. 5 | 6 | The bot supports exchanges such as Binance, Kucoin, Okex, and Huobi. It utilizes the CCXT library to fetch market data, place orders, and calculate price impacts. The bot considers fees and tick sizes when executing trades. Additionally, the bot takes into account the impact of its orders on the current order book to avoid profitable trades only theoretically. 7 | 8 | The bot searches through all possible triangular opportunities on each exchange that start with USDT as the quote currency (e.g., BTC/USDT, ETH/USDT, BTC/ETH) and looks for opportunities where the cumulative profits of the trades are above a specified threshold. 9 | 10 | The bot sends notifications to a Telegram chat with details of profitable trades, including the trading pairs, profit percentage, and executed trades. It logs all activities in the 'arbitrage.log' file. 11 | 12 | To use the bot, you need to set up API keys for the supported exchanges and provide them in a 'config.env' file. You also need to specify the initial amount of USDT to trade. 13 | 14 | 15 | 16 | ## Disclaimer 17 | 18 | - The code provided is for educational and informational purposes only. Use it at your own risk. 19 | - Triangular arbitrage can be risky and complex. Make sure you understand the risks involved and do thorough testing before using the bot with real funds. 20 | - The bot's performance and profitability may vary depending on market conditions and exchange limitations. 21 | 22 | ## Prerequisites 23 | 24 | - Python 3.7 or higher 25 | - CCXT library (`ccxt` package) 26 | - `dotenv` library (`python-dotenv` package) 27 | - `pandas` library (`pandas` package) 28 | - `numpy` library (`numpy` package) 29 | - `telegram` library (`python-telegram-bot` package) 30 | 31 | ## Installation 32 | 33 | 1. Clone the repository: -git clone https://github.com/your-username/triangular-arbitrage-bot.git 34 | 35 | 2. Install the required dependencies using pip 36 | 37 | 3. Set up API keys and other configuration parameters: 38 | 39 | 4. Rename the `config.env.example` file to `config.env`. 40 | 41 | 5. Open the `config.env` file and replace the placeholders with your actual API keys and telegram bot token. 42 | 43 | ## Usage 44 | 45 | 1. Run the script: python tri_arb_bot.py 46 | 2. The bot will start scanning for triangular arbitrage opportunities across the configured exchanges. 47 | 3. Once an arbitrage opportunity is found and meets the specified criteria, the bot will execute the trades automatically. 48 | 4. The bot will send notifications to a Telegram chat with the details of profitable trades. 49 | 5. The bot will log all activities in the `arbitrage.log` file. 50 | 51 | ## Configuration 52 | 53 | - Adjust the `min_message_interval` variable to set the minimum time between messages sent to the Telegram Bot for each trading pair. 54 | - Modify the fees for each exchange by updating the respective fee variables (`binance_fee`, `kucoin_fee`, `okx_fee`, `huobi_fee`). 55 | - Customize the profit percentage threshold in the `find_triangular_arbitrage_opportunities` function to filter opportunities. 56 | 57 | ## Contributing 58 | 59 | Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. 60 | 61 | ## License 62 | 63 | This project is licensed under the MIT License. If you use or modify this code, please provide attribution by citing the original work. 64 | 65 | 66 | ## Contact 67 | 68 | Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on GitHub. 69 | """ 70 | -------------------------------------------------------------------------------- /config_example.env: -------------------------------------------------------------------------------- 1 | binance_api_key=YOUR_BINANCE_API_KEY 2 | binance_api_secret=YOUR_BINANCE_API_SECRET 3 | huobi_api_key=YOUR_HUOBI_API_KEY 4 | huobi_api_secret=YOUR_HUOBI_API_SECRET 5 | okx_api_key=YOUR_OKX_API_KEY 6 | okx_api_secret=YOUR_OKX_API_SECRET 7 | kucoin_api_key=YOUR_KUCOIN_API_KEY 8 | kucoin_api_secret=YOUR_KUCOIN_API_SECRET 9 | kucoin_password=YOUR_KUCOIN_TRADING_PASSWORD 10 | bot_token=TELEGRAM_BOT_TOKEN 11 | chat_id=TELEGRAM_BOT_CHAT_ID -------------------------------------------------------------------------------- /tri_arb_bot.py: -------------------------------------------------------------------------------- 1 | import ccxt.async_support as ccxt 2 | import os 3 | from dotenv import load_dotenv 4 | import asyncio 5 | from telegram import Bot 6 | import pandas as pd 7 | import math 8 | from telegram import Update 9 | from telegram.ext import Updater, MessageHandler, Filters, CallbackContext 10 | import time 11 | from datetime import datetime 12 | import os.path 13 | import traceback 14 | from decimal import Decimal 15 | import logging 16 | from decimal import ROUND_DOWN,ROUND_UP 17 | import asyncio 18 | from decimal import Decimal, InvalidOperation 19 | import numpy as np 20 | 21 | 22 | logging.basicConfig(filename='arbitrage.log', level=logging.INFO, format='%(asctime)s %(message)s') 23 | start_time = time.time() 24 | 25 | # Load API keys from config.env file 26 | load_dotenv('config.env') 27 | 28 | binance_api_key = os.environ.get('binance_api_key') 29 | binance_api_secret = os.environ.get('binance_api_secret') 30 | 31 | okx_api_key = os.environ.get('okx_api_key') 32 | okx_api_secret = os.environ.get('okx_api_secret') 33 | 34 | huobi_api_key = os.environ.get('huobi_api_key') 35 | huobi_api_secret = os.environ.get('huobi_api_secret') 36 | 37 | kucoin_api_key = os.environ.get('kucoin_api_key') 38 | kucoin_api_secret = os.environ.get('kucoin_api_secret') 39 | kucoin_password = os.environ.get('kucoin_password') 40 | 41 | # Load bot token and chat ID 42 | bot_token = os.environ.get('bot_token') 43 | chat_id = os.environ.get('chat_id') 44 | 45 | # Set the minimum time between messages of the Telegram Bot for each trading pair (in seconds) 46 | min_message_interval = 60 # 1 minute 47 | 48 | # Create a dictionary to keep track of the last time a message was sent for each trading pair 49 | last_message_times = {} 50 | 51 | #Load exchanges 52 | 53 | huobi = ccxt.huobi({ 54 | 'apiKey': huobi_api_key, 55 | 'secret': huobi_api_secret, 56 | 'enableRateLimit': True 57 | }) 58 | 59 | kucoin = ccxt.kucoin({ 60 | 'apiKey': kucoin_api_key, 61 | 'secret': kucoin_api_secret, 62 | 'password': kucoin_password, 63 | 'enableRateLimit': True 64 | }) 65 | 66 | binance = ccxt.binance({ 67 | 'apiKey': binance_api_key, 68 | 'secret': binance_api_secret, 69 | 'enableRateLimit': True 70 | }) 71 | 72 | okx = ccxt.okx({ 73 | 'apiKey': okx_api_key, 74 | 'secret': okx_api_secret, 75 | 'enableRateLimit': True 76 | }) 77 | 78 | 79 | # Defining function for the telegram Bot, the first is sending message, the second is to stop the script with by sending a message to the bot 80 | async def send_message(bot_token, chat_id, text): 81 | bot = Bot(bot_token) 82 | bot.send_message(chat_id=chat_id, text=text) 83 | 84 | def stop_command(update: Update, context: CallbackContext): 85 | global running 86 | running = False 87 | update.message.reply_text('Stopping script') 88 | 89 | 90 | # Function for executing trades 91 | async def execute_trade(exchange, first_symbol, second_symbol, third_symbol, tickers, initial_amount, fee, first_tick_size, second_tick_size, third_tick_size): 92 | 93 | # Use adjusted trades (including fee) 94 | first_price = Decimal(tickers[first_symbol]['ask']) 95 | first_trade = (initial_amount / first_price) * (1 - Decimal(fee)) 96 | first_trade = first_trade.quantize(Decimal(str(first_tick_size)), rounding=ROUND_DOWN) 97 | 98 | # Place first order 99 | print(f'\nPlacing first order: {first_trade} {first_symbol}') 100 | order = await exchange.create_order(first_symbol, 'market', 'buy', float(first_trade)) 101 | order_id = order['id'] 102 | 103 | # Wait for first order to be filled 104 | while True: 105 | order = await exchange.fetch_order(order_id, first_symbol) 106 | if order['status'] == 'closed': 107 | break 108 | await asyncio.sleep(1) 109 | 110 | # Retrieve actual amount of first trading pair bought 111 | first_trade = Decimal(order['filled']) 112 | 113 | # Use the entire amount of first trade for the second order 114 | second_trade = first_trade 115 | 116 | # Place second order 117 | print(f'Placing second order: {second_trade} {second_symbol}') 118 | order = await exchange.create_order(second_symbol, 'market', 'sell', float(second_trade)) 119 | order_id = order['id'] 120 | 121 | # Wait for second order to be filled 122 | while True: 123 | order = await exchange.fetch_order(order_id, second_symbol) 124 | if order['status'] == 'closed': 125 | break 126 | await asyncio.sleep(1) 127 | 128 | # Retrieve actual cost of second trading pair 129 | second_trade = Decimal(order['cost']) 130 | 131 | # Use the entire cost of second trade for the third order 132 | third_trade = second_trade * (1 - Decimal(fee)) 133 | 134 | # Place third order 135 | print(f'Placing third order: {third_trade} {third_symbol}') 136 | order = await exchange.create_order(third_symbol, 'market', 'sell', float(third_trade)) 137 | order_id = order['id'] 138 | 139 | while True: 140 | order = await exchange.fetch_order(order_id, third_symbol) 141 | if order['status'] == 'closed': 142 | break 143 | await asyncio.sleep(1) 144 | 145 | # Fetch final balance 146 | balance = await exchange.fetch_balance() 147 | final_amount = balance['free']['USDT'] 148 | 149 | # Calculate profit/loss 150 | profit = final_amount - initial_amount 151 | 152 | print(f'Trade completed: Initial amount: {initial_amount}, Final amount: {final_amount}, Profit: {profit}') 153 | 154 | # return profit and final amount if needed for further calculations or logging 155 | return profit, final_amount 156 | 157 | 158 | # Function for calculating the price impact of the order based on the orderbook asks, bids, and volumes 159 | async def calculate_price_impact(exchange, symbols, order_sizes, sides): 160 | logging.info(f'Calculating price impact ') 161 | 162 | # Fetch order books concurrently 163 | order_books = await asyncio.gather(*[exchange.fetch_order_book(symbol) for symbol in symbols]) 164 | logging.info(f'Order books fetched on {exchange}') 165 | price_impacts = [] 166 | 167 | for i in range(len(symbols)): 168 | symbol = symbols[i] 169 | side = sides[i] 170 | order_size = float(order_sizes[i]) 171 | order_book = order_books[i] 172 | 173 | # If we're buying, we need to look at the asks. If we're selling, we need to look at the bids. 174 | orders = np.array(order_book['asks']) if side == 'buy' else np.array(order_book['bids']) 175 | 176 | # Slice orders into prices and volumes 177 | prices, volumes = orders[:,0], orders[:,1] 178 | 179 | logging.info(f'Processing order book for {symbol} with side {side} and order size {order_size}') 180 | logging.info(f'Order book prices: {prices}') 181 | logging.info(f'Order book volumes: {volumes}') 182 | 183 | total_value = 0 184 | total_volume = 0 185 | 186 | for j in range(len(prices)): 187 | if order_size > 0: 188 | volume_for_this_order = min(volumes[j], order_size) 189 | value_for_this_order = volume_for_this_order * prices[j] 190 | 191 | logging.info(f'At price level {prices[j]}: volume_for_this_order={volume_for_this_order}, value_for_this_order={value_for_this_order}') 192 | 193 | total_value += value_for_this_order 194 | total_volume += volume_for_this_order 195 | order_size -= volume_for_this_order 196 | 197 | if order_size <= 0: 198 | # Calculate price impact 199 | price_impact = total_value / total_volume if total_volume != 0 else None 200 | logging.info(f'Price impact for {symbol}: {price_impact}') 201 | price_impacts.append(price_impact) 202 | else: 203 | # If order size was not completely filled, price impact can't be calculated 204 | price_impacts.append(None) 205 | 206 | return price_impacts 207 | 208 | #Function for finding triangular arbitrage opportunities for each exchange 209 | async def find_triangular_arbitrage_opportunities(exchange, markets, tickers, exchange_name, fee, initial_amount ): 210 | 211 | logging.info('Finding arbitrage opportunities.') 212 | # Read existing trades from CSV file 213 | csv_file = 'tri_arb_opportunities.csv' 214 | 215 | if os.path.exists(csv_file) and os.path.getsize(csv_file) > 0: 216 | df = pd.read_csv(csv_file) 217 | tri_arb_opportunities = df.to_dict('records') 218 | else: 219 | tri_arb_opportunities = [] 220 | 221 | # Add a new variable to keep track of the last time a trade was added to the CSV file for each trading pair 222 | last_trade_time = {} 223 | 224 | #load markets data 225 | markets = await exchange.load_markets(True) 226 | symbols = list(markets.keys()) 227 | tickers = await exchange.fetch_tickers() 228 | 229 | # Create a dictionary with all the USDT symbols 230 | usdt_symbols = {symbol for symbol in markets.keys() if symbol.endswith('/USDT')} 231 | symbols_by_base = {} 232 | 233 | for symbol in markets.keys(): 234 | base, quote = symbol.split('/') 235 | if base not in symbols_by_base: 236 | symbols_by_base[base] = set() 237 | symbols_by_base[base].add(symbol) 238 | 239 | # Split the first symbol in base and quote 240 | for usdt_symbol in usdt_symbols: 241 | first_symbol = usdt_symbol 242 | base, quote = usdt_symbol.split('/') 243 | second_base = base 244 | second_symbols = symbols_by_base.get(second_base, set()) 245 | 246 | # Loop to find all the possible second symbols 247 | for second_symbol in second_symbols: 248 | unavailable_pairs = {'YGG/BNB', 'RAD/BNB', 'VOXEL/BNB', 'GLMR/BNB', 'UNI/EUR'} 249 | if second_symbol == first_symbol or second_symbol in unavailable_pairs: 250 | continue 251 | base, quote = second_symbol.split('/') 252 | if base == second_base: 253 | third_base = quote 254 | else: 255 | third_base = base 256 | # Third symbol 257 | third_symbol = f'{third_base}/USDT' 258 | 259 | # Check if trading pairs are valid on the exchange 260 | if third_symbol in markets and first_symbol in markets and second_symbol in markets: 261 | 262 | # Retrieve tick size for all trading pairs 263 | market = exchange.markets 264 | 265 | first_market = market[first_symbol] 266 | first_tick_size = first_market['precision']['price'] 267 | 268 | second_market = market[second_symbol] 269 | second_tick_size = second_market['precision']['price'] 270 | 271 | third_market = market[third_symbol] 272 | third_tick_size = third_market['precision']['price'] 273 | 274 | if any(symbol not in tickers for symbol in [first_symbol, second_symbol, third_symbol]): 275 | continue 276 | 277 | if all(tickers[symbol].get('ask') is not None for symbol in [first_symbol]) and all(tickers[symbol].get('bid') is not None for symbol in [second_symbol, third_symbol]): 278 | first_price = Decimal(tickers[first_symbol]['ask']) 279 | second_price = Decimal(tickers[second_symbol]['bid']) 280 | third_price = Decimal(tickers[third_symbol]['bid']) 281 | else: 282 | continue 283 | 284 | # Quantize the prices 285 | first_price = first_price.quantize(Decimal(str(first_tick_size)), rounding=ROUND_DOWN) 286 | second_price = second_price.quantize(Decimal(str(second_tick_size)), rounding=ROUND_DOWN) 287 | third_price = third_price.quantize(Decimal(str(third_tick_size)), rounding=ROUND_DOWN) 288 | 289 | if not first_price or not second_price or not third_price: 290 | continue 291 | 292 | # Check for zero prices to avoid division by zero 293 | if first_price == 0 or second_price == 0 or third_price == 0: 294 | continue 295 | 296 | #Trades calculation 297 | first_trade = initial_amount / first_price 298 | first_trade = first_trade.quantize(Decimal(str(first_tick_size)), rounding=ROUND_DOWN) 299 | 300 | second_trade = first_trade * second_price 301 | second_trade = second_trade.quantize(Decimal(str(second_tick_size)), rounding=ROUND_DOWN) 302 | 303 | third_trade = second_trade * third_price 304 | third_trade = third_trade.quantize(Decimal(str(third_tick_size)), rounding=ROUND_DOWN) 305 | 306 | # Check for negative trades 307 | if first_trade < 0 or second_trade < 0 or third_trade < 0: 308 | continue 309 | 310 | # Calculate profits 311 | profit = third_trade - initial_amount 312 | profit_percentage = (profit / initial_amount) * 100 313 | 314 | opportunities = [] 315 | 316 | 317 | if profit_percentage > 0.3: 318 | logging.info(f'Arbitrage opportunity found. Checking liquidity on {exchange_name}...') 319 | print(f'\rArbitrage opportunities found, checking liquidity', end='\r') 320 | 321 | opportunities.append({ 322 | 'first_symbol': first_symbol, 323 | 'second_symbol': second_symbol, 324 | 'third_symbol': third_symbol, 325 | 'first_trade': first_trade, 326 | 'second_trade': second_trade, 327 | 'third_trade': third_trade, 328 | 'profit': profit, 329 | 'profit_percentage': profit_percentage 330 | }) 331 | 332 | # Sort opportunities by profit percentage in descending order 333 | opportunities.sort(key=lambda x: -x['profit_percentage']) 334 | 335 | # Take the top 1 or 2 opportunities 336 | top_opportunities = opportunities[:1] # Change this to the number of opportunities you want to process 337 | 338 | # Calculate price impacts for top opportunities 339 | for opportunity in top_opportunities: 340 | # Log prices before checking opportunity 341 | logging.info(f'Before opportunity check on {exchange_name}: first_symbol= {first_symbol}, first_price = {first_price}, second_symbol = {second_symbol} second_price = {second_price}, third_symbol = {third_symbol}, third_price = {third_price}, profit percentage: {profit_percentage}') 342 | 343 | price_impacts = await calculate_price_impact( 344 | exchange, 345 | [opportunity['first_symbol'], opportunity['second_symbol'], opportunity['third_symbol']], 346 | [initial_amount, opportunity['first_trade'], opportunity['second_trade']], 347 | ['buy', 'sell', 'sell'] 348 | ) 349 | 350 | # Unpack the results 351 | first_price_impact, second_price_impact, third_price_impact = price_impacts 352 | 353 | # Quantize the price impacts 354 | first_price_impact = Decimal(first_price_impact).quantize(Decimal(str(first_tick_size)), rounding=ROUND_UP) 355 | second_price_impact = Decimal(second_price_impact).quantize(Decimal(str(second_tick_size)), rounding=ROUND_DOWN) 356 | third_price_impact = Decimal(third_price_impact).quantize(Decimal(str(third_tick_size)), rounding=ROUND_DOWN) 357 | 358 | # Calculate trades considering price impact and including fees 359 | first_trade_before_fees = initial_amount / first_price_impact 360 | first_trade_amount = first_trade_before_fees * ( 1- Decimal(fee)) 361 | first_trade_amount = first_trade_amount.quantize(Decimal(str(first_tick_size)), rounding=ROUND_DOWN) 362 | 363 | second_trade_before_fees = first_trade_amount * second_price_impact 364 | second_trade_amount = second_trade_before_fees * (1 - Decimal(fee)) 365 | second_trade_amount = second_trade_amount.quantize(Decimal(str(second_tick_size)), rounding=ROUND_DOWN) 366 | 367 | third_trade_before_fees = second_trade_amount * third_price_impact 368 | third_trade_amount = third_trade_before_fees * (1 - Decimal(fee)) 369 | third_trade_amount = third_trade_amount.quantize(Decimal(str(third_tick_size)), rounding=ROUND_DOWN) 370 | 371 | # Check real profit after price impact calculation and fees 372 | real_profit = third_trade_amount - initial_amount 373 | real_profit_percentage = (real_profit / initial_amount) * 100 374 | 375 | logging.info(f'After liquidity check on {exchange_name}: first_symbol= {first_symbol}, first_price = {first_price_impact}, second_symbol = {second_symbol} second_price = {second_price_impact}, third_symbol = {third_symbol}, third_price = {third_price_impact}, profit percentage: {real_profit_percentage}') 376 | if real_profit_percentage > 0.1: 377 | 378 | logging.info(f'Arbitrage opportunity confirmed on {exchange_name}.') 379 | 380 | # call execute trades 381 | profit, final_amount = await execute_trade( 382 | exchange, 383 | first_symbol, 384 | second_symbol, 385 | third_symbol, 386 | tickers, 387 | initial_amount , 388 | fee, 389 | first_tick_size, 390 | second_tick_size, 391 | third_tick_size 392 | ) 393 | 394 | print(f'Profitable trade found on {exchange_name}: {first_symbol} -> {second_symbol} -> {third_symbol}. Profit percentage: {real_profit_percentage:.2f}%', 'Profit change after checks: ', real_profit-profit, ' USDT') 395 | 396 | trade_key = f'{exchange_name}-{first_symbol}-{second_symbol}-{third_symbol}' 397 | current_time = time.time() 398 | last_message_time = last_message_times.get(trade_key, 0) 399 | time_since_last_message = current_time - last_message_time 400 | 401 | if time_since_last_message > min_message_interval: 402 | message_text = f'Profitable trade found on {exchange_name}: {first_symbol} -> {second_symbol} -> {third_symbol}. Profit: {profit:.2f}. Profit percentage: {profit_percentage:.2f}%' 403 | await send_message(bot_token, chat_id, message_text) 404 | last_message_times[trade_key] = current_time 405 | 406 | # Check if a trade for the same trading pair has been added to the CSV file within the last minute 407 | last_trade_time_for_pair= last_trade_time.get(trade_key, 0) 408 | time_since_last_trade= current_time - last_trade_time_for_pair 409 | 410 | # If a trade for the same trading pair has not been added to the CSV file within the last 5 minute, 411 | # append trade_data to trades list and update last_trade_time[trade_key] with current time 412 | if time_since_last_trade> 300: 413 | trade_data= { 414 | 'exchange': exchange_name, 415 | 'order size (USDT)': initial_amount, 416 | 'first_symbol': first_symbol, 417 | 'second_symbol': second_symbol, 418 | 'third_symbol': third_symbol, 419 | 'first_price': first_price_impact, 420 | 'second_price': second_price_impact, 421 | 'third_price': third_price_impact, 422 | 'profit_percentage': real_profit_percentage, 423 | 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 424 | } 425 | tri_arb_opportunities.append(trade_data) 426 | last_trade_time[trade_key]= current_time 427 | else: 428 | logging.info(f'Arbitrage opportunity not confirmed on {exchange_name}.') 429 | # Print arbitrage opportunity message 430 | print(f'\rArbitrage opportunities found, checking liquidity | Opportunities not confirmed', end='\r') 431 | 432 | # Write updated trades to CSV and Excell file 433 | df= pd.DataFrame(tri_arb_opportunities) 434 | df.to_csv(csv_file, index=False) 435 | 436 | 437 | async def main(): 438 | 439 | # Get user input on USDT initial amount 440 | while True: 441 | initial_amount_input = input("How many USDT do you want to trade? | Only numbers are accepted (in the form 1, 10, 20.1) \nUSDT amount: ") 442 | try: 443 | # Try to convert the input to a Decimal 444 | initial_amount = Decimal(initial_amount_input) 445 | break # If the conversion succeeds, break out of the loop 446 | except InvalidOperation: 447 | print("Please enter a valid number.") 448 | 449 | # Set up the updater and dispatcher 450 | updater = Updater(bot_token) 451 | dispatcher = updater.dispatcher 452 | 453 | # Add a command handler for the /stop command 454 | dispatcher.add_handler(MessageHandler(Filters.regex('^/stop$'), stop_command)) 455 | 456 | # Start the updater 457 | updater.start_polling() 458 | 459 | # Message from the Telegram Bot 460 | await send_message(bot_token, chat_id, "Finding arbitrage opportunities...") 461 | global running 462 | running = True 463 | 464 | print('\nFinding arbitrage opportunities...') 465 | 466 | iteration_count = 1 # initialize iteration counter 467 | while running: 468 | try: 469 | # Load markets and tickers for all exchanges concurrently 470 | binance_markets, binance_tickers, kucoin_markets, kucoin_tickers, okx_markets, okx_tickers, huobi_markets, huobi_tickers = await asyncio.gather( 471 | binance.load_markets(True), 472 | binance.fetch_tickers(), 473 | kucoin.load_markets(True), 474 | kucoin.fetch_tickers(), 475 | okx.load_markets(True), 476 | okx.fetch_tickers(), 477 | huobi.load_markets(True), 478 | huobi.fetch_tickers() 479 | ) 480 | 481 | # Set fees for all exchanges 482 | binance_fee = 0.001 483 | kucoin_fee = 0.001 484 | okx_fee = 0.001 485 | huobi_fee = 0.002 486 | 487 | # Search for arbitrage opportunities on all exchanges concurrently 488 | await asyncio.gather( 489 | find_triangular_arbitrage_opportunities(binance, binance_markets, binance_tickers, 'Binance', binance_fee, initial_amount), 490 | find_triangular_arbitrage_opportunities(kucoin, kucoin_markets, kucoin_tickers, 'Kucoin', kucoin_fee, initial_amount), 491 | find_triangular_arbitrage_opportunities(okx, okx_markets, okx_tickers, 'Okx', okx_fee, initial_amount ), 492 | find_triangular_arbitrage_opportunities(huobi, huobi_markets, huobi_tickers, 'Huobi', huobi_fee, initial_amount ) 493 | ) 494 | end_time = time.time() 495 | elapsed_time = end_time - start_time 496 | # Print elapsed time and number of iterations 497 | print(f'\n\rElapsed time: {elapsed_time:.2f} seconds | Number of iterations: {iteration_count}', end='\r') 498 | 499 | iteration_count += 1 # increment iteration counter 500 | 501 | await asyncio.sleep(10) # sleep for 10 seconds before starting next iteration 502 | 503 | except Exception as e: 504 | print(f'An error occurred: {e}') 505 | traceback.print_exc() 506 | 507 | # Stop the updater when the script is stopped 508 | updater.stop() 509 | 510 | # Release resources used by the exchange instances 511 | await binance.close() 512 | await kucoin.close() 513 | await okx.close() 514 | await huobi.close() 515 | 516 | if __name__ == "__main__": 517 | asyncio.run(main()) 518 | --------------------------------------------------------------------------------