|
|
@@ -0,0 +1,320 @@
|
|
|
+import asyncio
|
|
|
+import requests
|
|
|
+from datetime import datetime
|
|
|
+from telegram import Bot
|
|
|
+import platform
|
|
|
+
|
|
|
+# Telegram bot configuration (replace with your own bot token and chat ID)
|
|
|
+TELEGRAM_BOT_TOKEN = "YourBotToken" # Replace with your Telegram bot token
|
|
|
+TELEGRAM_CHAT_ID = "YourID" # Replace with your Telegram chat ID
|
|
|
+
|
|
|
+# API endpoints
|
|
|
+NOBITEX_API = "https://apiv2.nobitex.ir/v3/orderbook/USDTIRT"
|
|
|
+BITPIN_API = "https://api.bitpin.ir/v4/mth/matches/USDT_IRT/"
|
|
|
+
|
|
|
+# Minimum volume for Nobitex sell orders (in USDT)
|
|
|
+MIN_SELL_VOLUME = 1000.0
|
|
|
+
|
|
|
+def log_error(message):
|
|
|
+ """Log errors with timestamp."""
|
|
|
+ print(f"Error at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: {message}")
|
|
|
+
|
|
|
+async def send_telegram_message(message):
|
|
|
+ """Send Telegram message with error handling."""
|
|
|
+ try:
|
|
|
+ bot = Bot(token=TELEGRAM_BOT_TOKEN)
|
|
|
+ await bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=message)
|
|
|
+ print("Sent message:", message)
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Failed to send Telegram message: {e}")
|
|
|
+
|
|
|
+def fetch_nobitex_data():
|
|
|
+ """Fetch Nobitex orderbook data with error handling."""
|
|
|
+ try:
|
|
|
+ response = requests.get(NOBITEX_API, timeout=10)
|
|
|
+ response.raise_for_status()
|
|
|
+ data = response.json()
|
|
|
+ if data.get("status") == "ok":
|
|
|
+ print("Nobitex API read successfully at", response.headers.get('Date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
|
+ return data.get("asks", []), data.get("bids", [])
|
|
|
+ else:
|
|
|
+ log_error(f"Nobitex API error: {data.get('status')}")
|
|
|
+ return [], []
|
|
|
+ except (requests.RequestException, ValueError) as e:
|
|
|
+ log_error(f"Error fetching Nobitex data: {e}")
|
|
|
+ return [], []
|
|
|
+
|
|
|
+def fetch_bitpin_data():
|
|
|
+ """Fetch Bitpin trade data with error handling."""
|
|
|
+ try:
|
|
|
+ response = requests.get(BITPIN_API, timeout=10)
|
|
|
+ response.raise_for_status()
|
|
|
+ data = response.json()
|
|
|
+ print("Bitpin API read successfully at", response.headers.get('Date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
|
+ return data
|
|
|
+ except (requests.RequestException, ValueError) as e:
|
|
|
+ log_error(f"Error fetching Bitpin data: {e}")
|
|
|
+ return []
|
|
|
+
|
|
|
+def calculate_weighted_average(orders, target_volume, start_index=0):
|
|
|
+ """Calculate weighted average price for orders starting from start_index until target_volume is reached."""
|
|
|
+ try:
|
|
|
+ total_volume = 0.0
|
|
|
+ weighted_price_sum = 0.0
|
|
|
+ used_orders = []
|
|
|
+ for i in range(start_index, len(orders)):
|
|
|
+ price, volume = orders[i]
|
|
|
+ volume_to_use = min(volume, target_volume - total_volume)
|
|
|
+ weighted_price_sum += price * volume_to_use
|
|
|
+ total_volume += volume_to_use
|
|
|
+ used_orders.append((price, volume_to_use))
|
|
|
+ if total_volume >= target_volume:
|
|
|
+ break
|
|
|
+ if total_volume < target_volume:
|
|
|
+ print(f"Debug: Insufficient volume, got {total_volume:.2f}, needed {target_volume:.2f}")
|
|
|
+ return None, 0.0
|
|
|
+ avg_price = weighted_price_sum / total_volume
|
|
|
+ print(f"Debug: Aggregated {len(used_orders)} orders: {[(p, v) for p, v in used_orders]}, Total Volume: {total_volume:.2f}, Avg Price: {avg_price:.1f}")
|
|
|
+ return avg_price, total_volume
|
|
|
+ except (ZeroDivisionError, TypeError) as e:
|
|
|
+ log_error(f"Error in calculate_weighted_average: {e}")
|
|
|
+ return None, 0.0
|
|
|
+
|
|
|
+async def check_arbitrage():
|
|
|
+ """Check for arbitrage opportunities with error handling."""
|
|
|
+ try:
|
|
|
+ # Fetch data from both APIs
|
|
|
+ nobitex_asks, nobitex_bids = fetch_nobitex_data()
|
|
|
+ bitpin_trades = fetch_bitpin_data()
|
|
|
+
|
|
|
+ if not nobitex_asks or not nobitex_bids or not bitpin_trades:
|
|
|
+ print("No data to compare")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Convert Bitpin prices to IRR (toman to rial) and sort by price
|
|
|
+ bitpin_buys = []
|
|
|
+ bitpin_sells = []
|
|
|
+ try:
|
|
|
+ for trade in bitpin_trades:
|
|
|
+ price = float(trade["price"]) * 10
|
|
|
+ volume = float(trade["base_amount"])
|
|
|
+ if trade["side"] == "buy":
|
|
|
+ bitpin_buys.append((price, volume))
|
|
|
+ elif trade["side"] == "sell":
|
|
|
+ bitpin_sells.append((price, volume))
|
|
|
+ bitpin_buys = sorted(bitpin_buys, key=lambda x: x[0], reverse=True)
|
|
|
+ bitpin_sells = sorted(bitpin_sells, key=lambda x: x[0])
|
|
|
+ except (KeyError, ValueError, TypeError) as e:
|
|
|
+ log_error(f"Error parsing Bitpin trades: {e}")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Sort Nobitex asks by price ascending and bids by price descending
|
|
|
+ try:
|
|
|
+ nobitex_asks = sorted(
|
|
|
+ [(float(ask[0]), float(ask[1])) for ask in nobitex_asks],
|
|
|
+ key=lambda x: x[0]
|
|
|
+ )
|
|
|
+ nobitex_bids = sorted(
|
|
|
+ [(float(bid[0]), float(bid[1])) for bid in nobitex_bids],
|
|
|
+ key=lambda x: x[0], reverse=True
|
|
|
+ )
|
|
|
+ except (ValueError, TypeError) as e:
|
|
|
+ log_error(f"Error parsing Nobitex asks/bids: {e}")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Track best arbitrage opportunity
|
|
|
+ best_ask_sell_diff = float('-inf')
|
|
|
+ best_bid_buy_diff = float('-inf')
|
|
|
+ best_ask_sell_details = None
|
|
|
+ best_bid_buy_details = None
|
|
|
+
|
|
|
+ # Process first valid Nobitex ask
|
|
|
+ if nobitex_asks:
|
|
|
+ try:
|
|
|
+ ask_price, ask_volume = nobitex_asks[0]
|
|
|
+ orig_ask_price, orig_ask_volume = ask_price, ask_volume
|
|
|
+ print(f"Debug: First Nobitex ask - Price: {ask_price:.1f}, Volume: {ask_volume:.2f}")
|
|
|
+ # Enforce minimum sell volume
|
|
|
+ if ask_volume < MIN_SELL_VOLUME:
|
|
|
+ print(f"Debug: Aggregating Nobitex asks to reach {MIN_SELL_VOLUME} USDT")
|
|
|
+ avg_price, total_volume = calculate_weighted_average(nobitex_asks, MIN_SELL_VOLUME)
|
|
|
+ if avg_price is None or total_volume < MIN_SELL_VOLUME:
|
|
|
+ print(f"Debug: Failed to aggregate Nobitex asks to {MIN_SELL_VOLUME} USDT")
|
|
|
+ else:
|
|
|
+ ask_price, ask_volume = avg_price, total_volume
|
|
|
+ print(f"Debug: Aggregated Nobitex ask - Price: {ask_price:.1f}, Volume: {ask_volume:.2f}")
|
|
|
+ # Compare with Bitpin sells
|
|
|
+ for i, (sell_price, sell_volume) in enumerate(bitpin_sells):
|
|
|
+ try:
|
|
|
+ print(f"Debug: Comparing with Bitpin sell - Price: {sell_price:.1f}, Volume: {sell_volume:.2f}")
|
|
|
+ volume_diff_ratio = abs(ask_volume - sell_volume) / max(ask_volume, sell_volume)
|
|
|
+ if volume_diff_ratio > 0.05:
|
|
|
+ print(f"Debug: Volume difference > 5% ({volume_diff_ratio:.2%}), aggregating Bitpin sells")
|
|
|
+ avg_sell_price, total_sell_volume = calculate_weighted_average(bitpin_sells, ask_volume, i)
|
|
|
+ if avg_sell_price is None or total_sell_volume < MIN_SELL_VOLUME:
|
|
|
+ print(f"Debug: Failed to aggregate Bitpin sells to {ask_volume:.2f} USDT")
|
|
|
+ continue
|
|
|
+ price_diff = avg_sell_price - ask_price
|
|
|
+ compare_volume = total_sell_volume
|
|
|
+ compare_price = avg_sell_price
|
|
|
+ print(f"Debug: Bitpin sell aggregated - Price: {avg_sell_price:.1f}, Volume: {total_sell_volume:.2f}")
|
|
|
+ else:
|
|
|
+ price_diff = sell_price - ask_price
|
|
|
+ compare_volume = sell_volume
|
|
|
+ compare_price = sell_price
|
|
|
+ print(f"Debug: Using single Bitpin sell - Price: {sell_price:.1f}, Volume: {sell_volume:.2f}")
|
|
|
+
|
|
|
+ if price_diff > best_ask_sell_diff:
|
|
|
+ best_ask_sell_diff = price_diff
|
|
|
+ best_ask_sell_details = (ask_price, ask_volume, compare_price, compare_volume, orig_ask_price, orig_ask_volume, sell_price, sell_volume)
|
|
|
+ print(f"Debug: New best ask-sell diff: {price_diff:.1f}")
|
|
|
+ except (ZeroDivisionError, TypeError) as e:
|
|
|
+ log_error(f"Error comparing Bitpin sell: {e}")
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ # Use first ask directly if volume is sufficient
|
|
|
+ print(f"Debug: First Nobitex ask volume sufficient, comparing with Bitpin sells")
|
|
|
+ for i, (sell_price, sell_volume) in enumerate(bitpin_sells):
|
|
|
+ try:
|
|
|
+ print(f"Debug: Comparing with Bitpin sell - Price: {sell_price:.1f}, Volume: {sell_volume:.2f}")
|
|
|
+ volume_diff_ratio = abs(ask_volume - sell_volume) / max(ask_volume, sell_volume)
|
|
|
+ if volume_diff_ratio > 0.05:
|
|
|
+ print(f"Debug: Volume difference > 5% ({volume_diff_ratio:.2%}), aggregating Bitpin sells")
|
|
|
+ avg_sell_price, total_sell_volume = calculate_weighted_average(bitpin_sells, ask_volume, i)
|
|
|
+ if avg_sell_price is None or total_sell_volume < MIN_SELL_VOLUME:
|
|
|
+ print(f"Debug: Failed to aggregate Bitpin sells to {ask_volume:.2f} USDT")
|
|
|
+ continue
|
|
|
+ price_diff = avg_sell_price - ask_price
|
|
|
+ compare_volume = total_sell_volume
|
|
|
+ compare_price = avg_sell_price
|
|
|
+ print(f"Debug: Bitpin sell aggregated - Price: {avg_sell_price:.1f}, Volume: {total_sell_volume:.2f}")
|
|
|
+ else:
|
|
|
+ price_diff = sell_price - ask_price
|
|
|
+ compare_volume = sell_volume
|
|
|
+ compare_price = sell_price
|
|
|
+ print(f"Debug: Using single Bitpin sell - Price: {sell_price:.1f}, Volume: {sell_volume:.2f}")
|
|
|
+
|
|
|
+ if price_diff > best_ask_sell_diff:
|
|
|
+ best_ask_sell_diff = price_diff
|
|
|
+ best_ask_sell_details = (ask_price, ask_volume, compare_price, compare_volume, orig_ask_price, orig_ask_volume, sell_price, sell_volume)
|
|
|
+ print(f"Debug: New best ask-sell diff: {price_diff:.1f}")
|
|
|
+ except (ZeroDivisionError, TypeError) as e:
|
|
|
+ log_error(f"Error comparing Bitpin sell: {e}")
|
|
|
+ continue
|
|
|
+ except (IndexError, TypeError) as e:
|
|
|
+ log_error(f"Error processing Nobitex ask: {e}")
|
|
|
+
|
|
|
+ # Process first valid Nobitex bid
|
|
|
+ if nobitex_bids:
|
|
|
+ try:
|
|
|
+ bid_price, bid_volume = nobitex_bids[0]
|
|
|
+ orig_bid_price, orig_bid_volume = bid_price, bid_volume
|
|
|
+ print(f"Debug: First Nobitex bid - Price: {bid_price:.1f}, Volume: {bid_volume:.2f}")
|
|
|
+ for i, (buy_price, buy_volume) in enumerate(bitpin_buys):
|
|
|
+ try:
|
|
|
+ print(f"Debug: Comparing with Bitpin buy - Price: {buy_price:.1f}, Volume: {buy_volume:.2f}")
|
|
|
+ volume_diff_ratio = abs(bid_volume - buy_volume) / max(bid_volume, buy_volume)
|
|
|
+ if volume_diff_ratio > 0.05:
|
|
|
+ print(f"Debug: Volume difference > 5% ({volume_diff_ratio:.2%}), aggregating Bitpin buys")
|
|
|
+ avg_buy_price, total_buy_volume = calculate_weighted_average(bitpin_buys, bid_volume, i)
|
|
|
+ if avg_buy_price is None:
|
|
|
+ print(f"Debug: Failed to aggregate Bitpin buys to {bid_volume:.2f} USDT")
|
|
|
+ continue
|
|
|
+ price_diff = bid_price - avg_buy_price
|
|
|
+ compare_volume = total_buy_volume
|
|
|
+ compare_price = avg_buy_price
|
|
|
+ print(f"Debug: Bitpin buy aggregated - Price: {avg_buy_price:.1f}, Volume: {total_buy_volume:.2f}")
|
|
|
+ else:
|
|
|
+ price_diff = bid_price - buy_price
|
|
|
+ compare_volume = buy_volume
|
|
|
+ compare_price = buy_price
|
|
|
+ print(f"Debug: Using single Bitpin buy - Price: {buy_price:.1f}, Volume: {buy_volume:.2f}")
|
|
|
+
|
|
|
+ if price_diff > best_bid_buy_diff:
|
|
|
+ best_bid_buy_diff = price_diff
|
|
|
+ best_bid_buy_details = (bid_price, bid_volume, compare_price, compare_volume, orig_bid_price, orig_bid_volume, buy_price, buy_volume)
|
|
|
+ print(f"Debug: New best bid-buy diff: {price_diff:.1f}")
|
|
|
+ except (ZeroDivisionError, TypeError) as e:
|
|
|
+ log_error(f"Error comparing Bitpin buy: {e}")
|
|
|
+ continue
|
|
|
+ except (IndexError, TypeError) as e:
|
|
|
+ log_error(f"Error processing Nobitex bid: {e}")
|
|
|
+
|
|
|
+ # Send single Telegram message for the best opportunity
|
|
|
+ try:
|
|
|
+ best_message = None
|
|
|
+ if best_ask_sell_diff > best_bid_buy_diff and best_ask_sell_diff > 8000 and best_ask_sell_details:
|
|
|
+ ask_price, ask_volume, compare_price, compare_volume, orig_ask_price, orig_ask_volume, sell_price, sell_volume = best_ask_sell_details
|
|
|
+ best_message = (
|
|
|
+ f"✅ موقعیت آربیتراژ یافت شد 📈\n"
|
|
|
+ f"💸 خرید از نوبیتکس: {int(orig_ask_price)} ریال با حجم : {orig_ask_volume:.2f} دلار\n"
|
|
|
+ f"💰 فروش در بیت پین: {int(sell_price)} ریال با حجم: {sell_volume:.2f} دلار\n"
|
|
|
+ f"📊 قیمت میانگین خرید: {int(ask_price)} ریال\n"
|
|
|
+ f"📊 قیمت میانگین فروش: {int(compare_price)} ریال\n"
|
|
|
+ f"📏 حجم قیمت میانگین: {compare_volume:.2f} دلار\n"
|
|
|
+ f"📈 تفاوت قیمت: {int(best_ask_sell_diff)} ریال"
|
|
|
+ )
|
|
|
+ elif best_bid_buy_diff > 8000 and best_bid_buy_details:
|
|
|
+ bid_price, bid_volume, compare_price, compare_volume, orig_bid_price, orig_bid_volume, buy_price, buy_volume = best_bid_buy_details
|
|
|
+ best_message = (
|
|
|
+ f"✅ موقعیت آربیتراژ یافت شد 📈\n"
|
|
|
+ f"💸 خرید از بیت پین: {int(orig_bid_price)} ریال با حجم : {orig_bid_volume:.2f} دلار\n"
|
|
|
+ f"💰 فروش در نوبیتکس: {int(buy_price)} ریال با حجم: {buy_volume:.2f} دلار\n"
|
|
|
+ f"📊 قیمت میانگین خرید: {int(bid_price)} ریال\n"
|
|
|
+ f"📊 قیمت میانگین فروش: {int(compare_price)} ریال\n"
|
|
|
+ f"📏 حجم قیمت میانگین: {compare_volume:.2f} دلار\n"
|
|
|
+ f"📈 تفاوت قیمت: {int(best_bid_buy_diff)} ریال"
|
|
|
+ )
|
|
|
+
|
|
|
+ if best_message:
|
|
|
+ await send_telegram_message(best_message)
|
|
|
+ else:
|
|
|
+ print("No arbitrage opportunity found with price difference > 8000 IRR")
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Error preparing Telegram message: {e}")
|
|
|
+
|
|
|
+ # Print maximum price differences
|
|
|
+ try:
|
|
|
+ if best_ask_sell_details:
|
|
|
+ print(
|
|
|
+ f"Max Nobitex Ask - Bitpin Sell Difference: {best_ask_sell_diff:.1f} IRR\n"
|
|
|
+ f"Nobitex Sell: {best_ask_sell_details[0]:.1f} IRR, Volume: {best_ask_sell_details[1]:.2f}\n"
|
|
|
+ f"Bitpin Sell: {best_ask_sell_details[2]:.1f} IRR, Volume: {best_ask_sell_details[3]:.2f}\n"
|
|
|
+ f"Original Nobitex Sell: {best_ask_sell_details[4]:.1f} IRR, Volume: {best_ask_sell_details[5]:.2f}"
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ print("No valid Nobitex Ask - Bitpin Sell comparisons")
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Error printing ask-sell differences: {e}")
|
|
|
+
|
|
|
+ try:
|
|
|
+ if best_bid_buy_details:
|
|
|
+ print(
|
|
|
+ f"Max Nobitex Bid - Bitpin Buy Difference: {best_bid_buy_diff:.1f} IRR\n"
|
|
|
+ f"Nobitex Buy: {best_bid_buy_details[0]:.1f} IRR, Volume: {best_bid_buy_details[1]:.2f}\n"
|
|
|
+ f"Bitpin Buy: {best_bid_buy_details[2]:.1f} IRR, Volume: {best_bid_buy_details[3]:.2f}\n"
|
|
|
+ f"Original Nobitex Buy: {best_bid_buy_details[4]:.1f} IRR, Volume: {best_bid_buy_details[5]:.2f}"
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ print("No valid Nobitex Bid - Bitpin Buy comparisons")
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Error printing bid-buy differences: {e}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Error in check_arbitrage: {e}")
|
|
|
+
|
|
|
+async def main():
|
|
|
+ FPS = 1 / 30 # Check every 60 seconds
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ await check_arbitrage()
|
|
|
+ await asyncio.sleep(1.0 / FPS)
|
|
|
+ except Exception as e:
|
|
|
+ log_error(f"Error in main loop: {e}")
|
|
|
+ await asyncio.sleep(1.0 / FPS) # Continue after error
|
|
|
+
|
|
|
+if platform.system() == "Emscripten":
|
|
|
+ asyncio.ensure_future(main())
|
|
|
+else:
|
|
|
+ if __name__ == "__main__":
|
|
|
+ asyncio.run(main())
|