|
@@ -1,703 +0,0 @@
|
|
|
-#!/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()
|