|
@@ -0,0 +1,703 @@
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
+"""
|
|
|
|
+Telegram Bot for Hyperliquid Trading
|
|
|
|
+
|
|
|
|
+This module provides a Telegram interface for manual Hyperliquid trading
|
|
|
|
+with comprehensive statistics tracking and phone-friendly controls.
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+import logging
|
|
|
|
+import asyncio
|
|
|
|
+import re
|
|
|
|
+from datetime import datetime
|
|
|
|
+from typing import Optional, Dict, Any
|
|
|
|
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
|
+from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
|
|
|
|
+from hyperliquid_client import HyperliquidClient
|
|
|
|
+from trading_stats import TradingStats
|
|
|
|
+from config import Config
|
|
|
|
+
|
|
|
|
+# Set up logging
|
|
|
|
+logging.basicConfig(
|
|
|
|
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
|
+ level=getattr(logging, Config.LOG_LEVEL)
|
|
|
|
+)
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
+
|
|
|
|
+class TelegramTradingBot:
|
|
|
|
+ """Telegram bot for manual trading with comprehensive statistics."""
|
|
|
|
+
|
|
|
|
+ def __init__(self):
|
|
|
|
+ """Initialize the Telegram trading bot."""
|
|
|
|
+ self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
|
|
|
|
+ self.stats = TradingStats()
|
|
|
|
+ self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
|
|
|
|
+ self.application = None
|
|
|
|
+
|
|
|
|
+ # Initialize stats with current balance
|
|
|
|
+ self._initialize_stats()
|
|
|
|
+
|
|
|
|
+ def _initialize_stats(self):
|
|
|
|
+ """Initialize stats with current balance."""
|
|
|
|
+ try:
|
|
|
|
+ balance = self.client.get_balance()
|
|
|
|
+ if balance and balance.get('total'):
|
|
|
|
+ # Get USDC balance as the main balance
|
|
|
|
+ usdc_balance = float(balance['total'].get('USDC', 0))
|
|
|
|
+ self.stats.set_initial_balance(usdc_balance)
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Could not initialize stats: {e}")
|
|
|
|
+
|
|
|
|
+ def is_authorized(self, chat_id: str) -> bool:
|
|
|
|
+ """Check if the chat ID is authorized to use the bot."""
|
|
|
|
+ return str(chat_id) == str(self.authorized_chat_id)
|
|
|
|
+
|
|
|
|
+ async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
|
|
|
|
+ """Send a message to the authorized chat."""
|
|
|
|
+ if self.application and self.authorized_chat_id:
|
|
|
|
+ try:
|
|
|
|
+ await self.application.bot.send_message(
|
|
|
|
+ chat_id=self.authorized_chat_id,
|
|
|
|
+ text=text,
|
|
|
|
+ parse_mode=parse_mode
|
|
|
|
+ )
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Failed to send message: {e}")
|
|
|
|
+
|
|
|
|
+ async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /start command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ welcome_text = """
|
|
|
|
+🤖 <b>Hyperliquid Manual Trading Bot</b>
|
|
|
|
+
|
|
|
|
+Welcome to your personal trading assistant! Control your Hyperliquid account directly from your phone.
|
|
|
|
+
|
|
|
|
+<b>📱 Quick Actions:</b>
|
|
|
|
+Tap the buttons below for instant access to key functions.
|
|
|
|
+
|
|
|
|
+<b>💼 Account Commands:</b>
|
|
|
|
+/balance - Account balance
|
|
|
|
+/positions - Open positions
|
|
|
|
+/orders - Open orders
|
|
|
|
+/stats - Trading statistics
|
|
|
|
+
|
|
|
|
+<b>📊 Market Commands:</b>
|
|
|
|
+/market - Market data
|
|
|
|
+/price - Current price
|
|
|
|
+
|
|
|
|
+<b>🔄 Trading Commands:</b>
|
|
|
|
+/buy [amount] [price] - Buy order
|
|
|
|
+/sell [amount] [price] - Sell order
|
|
|
|
+/trades - Recent trades
|
|
|
|
+/cancel [order_id] - Cancel order
|
|
|
|
+
|
|
|
|
+<b>📈 Statistics:</b>
|
|
|
|
+/stats - Full trading statistics
|
|
|
|
+/performance - Performance metrics
|
|
|
|
+/risk - Risk analysis
|
|
|
|
+
|
|
|
|
+Type /help for detailed command information.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ keyboard = [
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("💰 Balance", callback_data="balance"),
|
|
|
|
+ InlineKeyboardButton("📊 Stats", callback_data="stats")
|
|
|
|
+ ],
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("📈 Positions", callback_data="positions"),
|
|
|
|
+ InlineKeyboardButton("📋 Orders", callback_data="orders")
|
|
|
|
+ ],
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("💵 Price", callback_data="price"),
|
|
|
|
+ InlineKeyboardButton("📊 Market", callback_data="market")
|
|
|
|
+ ],
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("🔄 Recent Trades", callback_data="trades"),
|
|
|
|
+ InlineKeyboardButton("⚙️ Help", callback_data="help")
|
|
|
|
+ ]
|
|
|
|
+ ]
|
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
|
+
|
|
|
|
+ async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /help command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ help_text = """
|
|
|
|
+🔧 <b>Hyperliquid Trading Bot - Complete Guide</b>
|
|
|
|
+
|
|
|
|
+<b>💼 Account Management:</b>
|
|
|
|
+• /balance - Show account balance
|
|
|
|
+• /positions - Show open positions
|
|
|
|
+• /orders - Show open orders
|
|
|
|
+
|
|
|
|
+<b>📊 Market Data:</b>
|
|
|
|
+• /market - Detailed market data
|
|
|
|
+• /price - Quick price check
|
|
|
|
+
|
|
|
|
+<b>🔄 Manual Trading:</b>
|
|
|
|
+• /buy 0.001 50000 - Buy 0.001 BTC at $50,000
|
|
|
|
+• /sell 0.001 55000 - Sell 0.001 BTC at $55,000
|
|
|
|
+• /cancel ABC123 - Cancel order with ID ABC123
|
|
|
|
+
|
|
|
|
+<b>📈 Statistics & Analytics:</b>
|
|
|
|
+• /stats - Complete trading statistics
|
|
|
|
+• /performance - Win rate, profit factor, etc.
|
|
|
|
+• /risk - Sharpe ratio, drawdown, VaR
|
|
|
|
+• /trades - Recent trade history
|
|
|
|
+
|
|
|
|
+<b>⚙️ Configuration:</b>
|
|
|
|
+• Symbol: {symbol}
|
|
|
|
+• Default Amount: {amount}
|
|
|
|
+• Network: {network}
|
|
|
|
+
|
|
|
|
+<b>🛡️ Safety Features:</b>
|
|
|
|
+• All trades logged automatically
|
|
|
|
+• Comprehensive performance tracking
|
|
|
|
+• Real-time balance monitoring
|
|
|
|
+• Risk metrics calculation
|
|
|
|
+
|
|
|
|
+<b>📱 Mobile Optimized:</b>
|
|
|
|
+• Quick action buttons
|
|
|
|
+• Instant notifications
|
|
|
|
+• Clean, readable layout
|
|
|
|
+• One-tap commands
|
|
|
|
+
|
|
|
|
+For support, contact your bot administrator.
|
|
|
|
+ """.format(
|
|
|
|
+ symbol=Config.DEFAULT_TRADING_SYMBOL,
|
|
|
|
+ amount=Config.DEFAULT_TRADE_AMOUNT,
|
|
|
|
+ network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet"
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(help_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /stats command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Get current balance for stats
|
|
|
|
+ balance = self.client.get_balance()
|
|
|
|
+ current_balance = 0
|
|
|
|
+ if balance and balance.get('total'):
|
|
|
|
+ current_balance = float(balance['total'].get('USDC', 0))
|
|
|
|
+
|
|
|
|
+ stats_message = self.stats.format_stats_message(current_balance)
|
|
|
|
+ await update.message.reply_text(stats_message, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def buy_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /buy command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ if len(context.args) < 2:
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
+ "❌ Usage: /buy [amount] [price]\n"
|
|
|
|
+ f"Example: /buy {Config.DEFAULT_TRADE_AMOUNT} 50000"
|
|
|
|
+ )
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ amount = float(context.args[0])
|
|
|
|
+ price = float(context.args[1])
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+
|
|
|
|
+ # Confirmation message
|
|
|
|
+ confirmation_text = f"""
|
|
|
|
+🟢 <b>Buy Order Confirmation</b>
|
|
|
|
+
|
|
|
|
+📊 <b>Order Details:</b>
|
|
|
|
+• Symbol: {symbol}
|
|
|
|
+• Side: BUY
|
|
|
|
+• Amount: {amount}
|
|
|
|
+• Price: ${price:,.2f}
|
|
|
|
+• Total Value: ${amount * price:,.2f}
|
|
|
|
+
|
|
|
|
+⚠️ <b>Are you sure you want to place this order?</b>
|
|
|
|
+
|
|
|
|
+This will attempt to buy {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ keyboard = [
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("✅ Confirm Buy", callback_data=f"confirm_buy_{amount}_{price}"),
|
|
|
|
+ InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
|
+ ]
|
|
|
|
+ ]
|
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
|
+
|
|
|
|
+ except ValueError:
|
|
|
|
+ await update.message.reply_text("❌ Invalid amount or price. Please use numbers only.")
|
|
|
|
+ except Exception as e:
|
|
|
|
+ await update.message.reply_text(f"❌ Error processing buy command: {e}")
|
|
|
|
+
|
|
|
|
+ async def sell_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /sell command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ if len(context.args) < 2:
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
+ "❌ Usage: /sell [amount] [price]\n"
|
|
|
|
+ f"Example: /sell {Config.DEFAULT_TRADE_AMOUNT} 55000"
|
|
|
|
+ )
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ amount = float(context.args[0])
|
|
|
|
+ price = float(context.args[1])
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+
|
|
|
|
+ # Confirmation message
|
|
|
|
+ confirmation_text = f"""
|
|
|
|
+🔴 <b>Sell Order Confirmation</b>
|
|
|
|
+
|
|
|
|
+📊 <b>Order Details:</b>
|
|
|
|
+• Symbol: {symbol}
|
|
|
|
+• Side: SELL
|
|
|
|
+• Amount: {amount}
|
|
|
|
+• Price: ${price:,.2f}
|
|
|
|
+• Total Value: ${amount * price:,.2f}
|
|
|
|
+
|
|
|
|
+⚠️ <b>Are you sure you want to place this order?</b>
|
|
|
|
+
|
|
|
|
+This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ keyboard = [
|
|
|
|
+ [
|
|
|
|
+ InlineKeyboardButton("✅ Confirm Sell", callback_data=f"confirm_sell_{amount}_{price}"),
|
|
|
|
+ InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
|
+ ]
|
|
|
|
+ ]
|
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
|
+
|
|
|
|
+ except ValueError:
|
|
|
|
+ await update.message.reply_text("❌ Invalid amount or price. Please use numbers only.")
|
|
|
|
+ except Exception as e:
|
|
|
|
+ await update.message.reply_text(f"❌ Error processing sell command: {e}")
|
|
|
|
+
|
|
|
|
+ async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /trades command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ recent_trades = self.stats.get_recent_trades(10)
|
|
|
|
+
|
|
|
|
+ if not recent_trades:
|
|
|
|
+ await update.message.reply_text("📝 No trades recorded yet.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ trades_text = "🔄 <b>Recent Trades</b>\n\n"
|
|
|
|
+
|
|
|
|
+ for trade in reversed(recent_trades[-5:]): # Show last 5 trades
|
|
|
|
+ timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
|
|
|
|
+ side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
|
|
|
|
+
|
|
|
|
+ trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
|
|
|
|
+ trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
|
|
|
|
+ trades_text += f" 📅 {timestamp}\n\n"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(trades_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /balance command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ balance = self.client.get_balance()
|
|
|
|
+ if balance:
|
|
|
|
+ balance_text = "💰 <b>Account Balance</b>\n\n"
|
|
|
|
+ total_balance = balance.get('total', {})
|
|
|
|
+
|
|
|
|
+ if total_balance:
|
|
|
|
+ total_value = 0
|
|
|
|
+ for asset, amount in total_balance.items():
|
|
|
|
+ if float(amount) > 0:
|
|
|
|
+ balance_text += f"💵 <b>{asset}:</b> {amount}\n"
|
|
|
|
+ if asset == 'USDC':
|
|
|
|
+ total_value += float(amount)
|
|
|
|
+
|
|
|
|
+ balance_text += f"\n💼 <b>Total Value:</b> ${total_value:,.2f}"
|
|
|
|
+
|
|
|
|
+ # Add stats summary
|
|
|
|
+ basic_stats = self.stats.get_basic_stats()
|
|
|
|
+ if basic_stats['initial_balance'] > 0:
|
|
|
|
+ pnl = total_value - basic_stats['initial_balance']
|
|
|
|
+ pnl_percent = (pnl / basic_stats['initial_balance']) * 100
|
|
|
|
+
|
|
|
|
+ balance_text += f"\n📊 <b>P&L:</b> ${pnl:,.2f} ({pnl_percent:+.2f}%)"
|
|
|
|
+ else:
|
|
|
|
+ balance_text += "No balance data available"
|
|
|
|
+ else:
|
|
|
|
+ balance_text = "❌ Could not fetch balance data"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(balance_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /positions command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ positions = self.client.get_positions()
|
|
|
|
+ if positions:
|
|
|
|
+ positions_text = "📈 <b>Open Positions</b>\n\n"
|
|
|
|
+
|
|
|
|
+ open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
|
|
|
|
+
|
|
|
|
+ if open_positions:
|
|
|
|
+ total_unrealized = 0
|
|
|
|
+ for position in open_positions:
|
|
|
|
+ symbol = position.get('symbol', 'Unknown')
|
|
|
|
+ contracts = float(position.get('contracts', 0))
|
|
|
|
+ unrealized_pnl = float(position.get('unrealizedPnl', 0))
|
|
|
|
+ entry_price = float(position.get('entryPx', 0))
|
|
|
|
+
|
|
|
|
+ pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
|
+
|
|
|
|
+ positions_text += f"📊 <b>{symbol}</b>\n"
|
|
|
|
+ positions_text += f" 📏 Size: {contracts} contracts\n"
|
|
|
|
+ positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
|
|
|
|
+ positions_text += f" {pnl_emoji} PnL: ${unrealized_pnl:,.2f}\n\n"
|
|
|
|
+
|
|
|
|
+ total_unrealized += unrealized_pnl
|
|
|
|
+
|
|
|
|
+ positions_text += f"💼 <b>Total Unrealized P&L:</b> ${total_unrealized:,.2f}"
|
|
|
|
+ else:
|
|
|
|
+ positions_text += "No open positions"
|
|
|
|
+ else:
|
|
|
|
+ positions_text = "❌ Could not fetch positions data"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(positions_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /orders command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ orders = self.client.get_open_orders()
|
|
|
|
+ if orders:
|
|
|
|
+ orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
|
+
|
|
|
|
+ if orders and len(orders) > 0:
|
|
|
|
+ for order in orders:
|
|
|
|
+ symbol = order.get('symbol', 'Unknown')
|
|
|
|
+ side = order.get('side', 'Unknown')
|
|
|
|
+ amount = order.get('amount', 0)
|
|
|
|
+ price = order.get('price', 0)
|
|
|
|
+ order_id = order.get('id', 'Unknown')
|
|
|
|
+
|
|
|
|
+ side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
|
+
|
|
|
|
+ orders_text += f"{side_emoji} <b>{symbol}</b>\n"
|
|
|
|
+ orders_text += f" 📊 {side.upper()} {amount} @ ${price:,.2f}\n"
|
|
|
|
+ orders_text += f" 💵 Value: ${float(amount) * float(price):,.2f}\n"
|
|
|
|
+ orders_text += f" 🔑 ID: <code>{order_id}</code>\n\n"
|
|
|
|
+ else:
|
|
|
|
+ orders_text += "No open orders"
|
|
|
|
+ else:
|
|
|
|
+ orders_text = "❌ Could not fetch orders data"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(orders_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /market command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
|
+
|
|
|
|
+ if market_data:
|
|
|
|
+ ticker = market_data['ticker']
|
|
|
|
+ orderbook = market_data['orderbook']
|
|
|
|
+
|
|
|
|
+ # Calculate 24h change
|
|
|
|
+ current_price = float(ticker.get('last', 0))
|
|
|
|
+ high_24h = float(ticker.get('high', 0))
|
|
|
|
+ low_24h = float(ticker.get('low', 0))
|
|
|
|
+
|
|
|
|
+ market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
|
|
|
|
+ market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
|
|
|
|
+ market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
|
|
|
|
+ market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
|
|
|
|
+ market_text += f"📊 <b>24h Volume:</b> {ticker.get('baseVolume', 'N/A')}\n\n"
|
|
|
|
+
|
|
|
|
+ if orderbook.get('bids') and orderbook.get('asks'):
|
|
|
|
+ best_bid = float(orderbook['bids'][0][0]) if orderbook['bids'] else 0
|
|
|
|
+ best_ask = float(orderbook['asks'][0][0]) if orderbook['asks'] else 0
|
|
|
|
+ spread = best_ask - best_bid
|
|
|
|
+ spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
|
|
|
|
+
|
|
|
|
+ market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
|
|
|
|
+ market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
|
|
|
|
+ market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
|
|
|
|
+ else:
|
|
|
|
+ market_text = "❌ Could not fetch market data"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(market_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle the /price command."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
|
+
|
|
|
|
+ if market_data:
|
|
|
|
+ price = float(market_data['ticker'].get('last', 0))
|
|
|
|
+ price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
|
|
|
|
+
|
|
|
|
+ # Add timestamp
|
|
|
|
+ timestamp = datetime.now().strftime('%H:%M:%S')
|
|
|
|
+ price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
|
|
|
|
+ else:
|
|
|
|
+ price_text = f"❌ Could not fetch price for {symbol}"
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(price_text, parse_mode='HTML')
|
|
|
|
+
|
|
|
|
+ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle inline keyboard button presses."""
|
|
|
|
+ query = update.callback_query
|
|
|
|
+ await query.answer()
|
|
|
|
+
|
|
|
|
+ if not self.is_authorized(query.message.chat_id):
|
|
|
|
+ await query.edit_message_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ callback_data = query.data
|
|
|
|
+
|
|
|
|
+ # Handle trading confirmations
|
|
|
|
+ if callback_data.startswith('confirm_buy_'):
|
|
|
|
+ parts = callback_data.split('_')
|
|
|
|
+ amount = float(parts[2])
|
|
|
|
+ price = float(parts[3])
|
|
|
|
+ await self._execute_buy_order(query, amount, price)
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ elif callback_data.startswith('confirm_sell_'):
|
|
|
|
+ parts = callback_data.split('_')
|
|
|
|
+ amount = float(parts[2])
|
|
|
|
+ price = float(parts[3])
|
|
|
|
+ await self._execute_sell_order(query, amount, price)
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ elif callback_data == 'cancel_order':
|
|
|
|
+ await query.edit_message_text("❌ Order cancelled.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Create a fake update object for reusing command handlers
|
|
|
|
+ fake_update = Update(
|
|
|
|
+ update_id=update.update_id,
|
|
|
|
+ message=query.message,
|
|
|
|
+ callback_query=query
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ # Handle regular button callbacks
|
|
|
|
+ if callback_data == "balance":
|
|
|
|
+ await self.balance_command(fake_update, context)
|
|
|
|
+ elif callback_data == "stats":
|
|
|
|
+ await self.stats_command(fake_update, context)
|
|
|
|
+ elif callback_data == "positions":
|
|
|
|
+ await self.positions_command(fake_update, context)
|
|
|
|
+ elif callback_data == "orders":
|
|
|
|
+ await self.orders_command(fake_update, context)
|
|
|
|
+ elif callback_data == "market":
|
|
|
|
+ await self.market_command(fake_update, context)
|
|
|
|
+ elif callback_data == "price":
|
|
|
|
+ await self.price_command(fake_update, context)
|
|
|
|
+ elif callback_data == "trades":
|
|
|
|
+ await self.trades_command(fake_update, context)
|
|
|
|
+ elif callback_data == "help":
|
|
|
|
+ await self.help_command(fake_update, context)
|
|
|
|
+
|
|
|
|
+ async def _execute_buy_order(self, query, amount: float, price: float):
|
|
|
|
+ """Execute a buy order."""
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ await query.edit_message_text("⏳ Placing buy order...")
|
|
|
|
+
|
|
|
|
+ # Place the order
|
|
|
|
+ order = self.client.place_limit_order(symbol, 'buy', amount, price)
|
|
|
|
+
|
|
|
|
+ if order:
|
|
|
|
+ # Record the trade in stats
|
|
|
|
+ order_id = order.get('id', 'N/A')
|
|
|
|
+ self.stats.record_trade(symbol, 'buy', amount, price, order_id)
|
|
|
|
+
|
|
|
|
+ success_message = f"""
|
|
|
|
+✅ <b>Buy Order Placed Successfully!</b>
|
|
|
|
+
|
|
|
|
+📊 <b>Order Details:</b>
|
|
|
|
+• Symbol: {symbol}
|
|
|
|
+• Side: BUY
|
|
|
|
+• Amount: {amount}
|
|
|
|
+• Price: ${price:,.2f}
|
|
|
|
+• Order ID: <code>{order_id}</code>
|
|
|
|
+• Total Value: ${amount * price:,.2f}
|
|
|
|
+
|
|
|
|
+The order has been submitted to Hyperliquid.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
|
+ logger.info(f"Buy order placed: {amount} {symbol} @ ${price}")
|
|
|
|
+ else:
|
|
|
|
+ await query.edit_message_text("❌ Failed to place buy order. Please try again.")
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ error_message = f"❌ Error placing buy order: {str(e)}"
|
|
|
|
+ await query.edit_message_text(error_message)
|
|
|
|
+ logger.error(f"Error placing buy order: {e}")
|
|
|
|
+
|
|
|
|
+ async def _execute_sell_order(self, query, amount: float, price: float):
|
|
|
|
+ """Execute a sell order."""
|
|
|
|
+ symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ await query.edit_message_text("⏳ Placing sell order...")
|
|
|
|
+
|
|
|
|
+ # Place the order
|
|
|
|
+ order = self.client.place_limit_order(symbol, 'sell', amount, price)
|
|
|
|
+
|
|
|
|
+ if order:
|
|
|
|
+ # Record the trade in stats
|
|
|
|
+ order_id = order.get('id', 'N/A')
|
|
|
|
+ self.stats.record_trade(symbol, 'sell', amount, price, order_id)
|
|
|
|
+
|
|
|
|
+ success_message = f"""
|
|
|
|
+✅ <b>Sell Order Placed Successfully!</b>
|
|
|
|
+
|
|
|
|
+📊 <b>Order Details:</b>
|
|
|
|
+• Symbol: {symbol}
|
|
|
|
+• Side: SELL
|
|
|
|
+• Amount: {amount}
|
|
|
|
+• Price: ${price:,.2f}
|
|
|
|
+• Order ID: <code>{order_id}</code>
|
|
|
|
+• Total Value: ${amount * price:,.2f}
|
|
|
|
+
|
|
|
|
+The order has been submitted to Hyperliquid.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
|
+ logger.info(f"Sell order placed: {amount} {symbol} @ ${price}")
|
|
|
|
+ else:
|
|
|
|
+ await query.edit_message_text("❌ Failed to place sell order. Please try again.")
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ error_message = f"❌ Error placing sell order: {str(e)}"
|
|
|
|
+ await query.edit_message_text(error_message)
|
|
|
|
+ logger.error(f"Error placing sell order: {e}")
|
|
|
|
+
|
|
|
|
+ async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
|
+ """Handle unknown commands."""
|
|
|
|
+ if not self.is_authorized(update.effective_chat.id):
|
|
|
|
+ await update.message.reply_text("❌ Unauthorized access.")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
+ "❓ Unknown command. Use /help to see available commands or tap the buttons in /start."
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def setup_handlers(self):
|
|
|
|
+ """Set up command handlers for the bot."""
|
|
|
|
+ if not self.application:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Command handlers
|
|
|
|
+ self.application.add_handler(CommandHandler("start", self.start_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("help", self.help_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("balance", self.balance_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("positions", self.positions_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("orders", self.orders_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("market", self.market_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("price", self.price_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("stats", self.stats_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("trades", self.trades_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("buy", self.buy_command))
|
|
|
|
+ self.application.add_handler(CommandHandler("sell", self.sell_command))
|
|
|
|
+
|
|
|
|
+ # Callback query handler for inline keyboards
|
|
|
|
+ self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
|
|
|
+
|
|
|
|
+ # Handle unknown commands
|
|
|
|
+ self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
|
|
|
|
+
|
|
|
|
+ async def run(self):
|
|
|
|
+ """Run the Telegram bot."""
|
|
|
|
+ if not Config.TELEGRAM_BOT_TOKEN:
|
|
|
|
+ logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ if not Config.TELEGRAM_CHAT_ID:
|
|
|
|
+ logger.error("❌ TELEGRAM_CHAT_ID not configured")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Create application
|
|
|
|
+ self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
|
|
|
|
+
|
|
|
|
+ # Set up handlers
|
|
|
|
+ self.setup_handlers()
|
|
|
|
+
|
|
|
|
+ logger.info("🚀 Starting Telegram trading bot...")
|
|
|
|
+
|
|
|
|
+ # Send startup notification
|
|
|
|
+ await self.send_message(
|
|
|
|
+ "🤖 <b>Manual Trading Bot Started</b>\n\n"
|
|
|
|
+ f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
|
|
|
|
+ f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
|
|
|
|
+ f"📱 Manual trading ready!\n"
|
|
|
|
+ f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
|
|
+ "Use /start for quick actions or /help for all commands."
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ # Start the bot
|
|
|
|
+ await self.application.run_polling()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def main():
|
|
|
|
+ """Main entry point for the Telegram bot."""
|
|
|
|
+ try:
|
|
|
|
+ # Validate configuration
|
|
|
|
+ if not Config.validate():
|
|
|
|
+ logger.error("❌ Configuration validation failed!")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ if not Config.TELEGRAM_ENABLED:
|
|
|
|
+ logger.error("❌ Telegram is not enabled in configuration")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Create and run the bot
|
|
|
|
+ bot = TelegramTradingBot()
|
|
|
|
+ asyncio.run(bot.run())
|
|
|
|
+
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
+ logger.info("👋 Bot stopped by user")
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"❌ Unexpected error: {e}")
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
+ main()
|