telegram_bot.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. #!/usr/bin/env python3
  2. """
  3. Telegram Bot for Hyperliquid Trading
  4. This module provides a Telegram interface for manual Hyperliquid trading
  5. with comprehensive statistics tracking and phone-friendly controls.
  6. """
  7. import logging
  8. import asyncio
  9. import re
  10. from datetime import datetime
  11. from typing import Optional, Dict, Any
  12. from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
  13. from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
  14. from hyperliquid_client import HyperliquidClient
  15. from trading_stats import TradingStats
  16. from config import Config
  17. # Set up logging
  18. logging.basicConfig(
  19. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  20. level=getattr(logging, Config.LOG_LEVEL)
  21. )
  22. logger = logging.getLogger(__name__)
  23. class TelegramTradingBot:
  24. """Telegram bot for manual trading with comprehensive statistics."""
  25. def __init__(self):
  26. """Initialize the Telegram trading bot."""
  27. self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
  28. self.stats = TradingStats()
  29. self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
  30. self.application = None
  31. # Initialize stats with current balance
  32. self._initialize_stats()
  33. def _initialize_stats(self):
  34. """Initialize stats with current balance."""
  35. try:
  36. balance = self.client.get_balance()
  37. if balance and balance.get('total'):
  38. # Get USDC balance as the main balance
  39. usdc_balance = float(balance['total'].get('USDC', 0))
  40. self.stats.set_initial_balance(usdc_balance)
  41. except Exception as e:
  42. logger.error(f"Could not initialize stats: {e}")
  43. def is_authorized(self, chat_id: str) -> bool:
  44. """Check if the chat ID is authorized to use the bot."""
  45. return str(chat_id) == str(self.authorized_chat_id)
  46. async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
  47. """Send a message to the authorized chat."""
  48. if self.application and self.authorized_chat_id:
  49. try:
  50. await self.application.bot.send_message(
  51. chat_id=self.authorized_chat_id,
  52. text=text,
  53. parse_mode=parse_mode
  54. )
  55. except Exception as e:
  56. logger.error(f"Failed to send message: {e}")
  57. async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  58. """Handle the /start command."""
  59. if not self.is_authorized(update.effective_chat.id):
  60. await update.message.reply_text("❌ Unauthorized access.")
  61. return
  62. welcome_text = """
  63. 🤖 <b>Hyperliquid Manual Trading Bot</b>
  64. Welcome to your personal trading assistant! Control your Hyperliquid account directly from your phone.
  65. <b>📱 Quick Actions:</b>
  66. Tap the buttons below for instant access to key functions.
  67. <b>💼 Account Commands:</b>
  68. /balance - Account balance
  69. /positions - Open positions
  70. /orders - Open orders
  71. /stats - Trading statistics
  72. <b>📊 Market Commands:</b>
  73. /market - Market data
  74. /price - Current price
  75. <b>🔄 Trading Commands:</b>
  76. /buy [amount] [price] - Buy order
  77. /sell [amount] [price] - Sell order
  78. /trades - Recent trades
  79. /cancel [order_id] - Cancel order
  80. <b>📈 Statistics:</b>
  81. /stats - Full trading statistics
  82. /performance - Performance metrics
  83. /risk - Risk analysis
  84. Type /help for detailed command information.
  85. """
  86. keyboard = [
  87. [
  88. InlineKeyboardButton("💰 Balance", callback_data="balance"),
  89. InlineKeyboardButton("📊 Stats", callback_data="stats")
  90. ],
  91. [
  92. InlineKeyboardButton("📈 Positions", callback_data="positions"),
  93. InlineKeyboardButton("📋 Orders", callback_data="orders")
  94. ],
  95. [
  96. InlineKeyboardButton("💵 Price", callback_data="price"),
  97. InlineKeyboardButton("📊 Market", callback_data="market")
  98. ],
  99. [
  100. InlineKeyboardButton("🔄 Recent Trades", callback_data="trades"),
  101. InlineKeyboardButton("⚙️ Help", callback_data="help")
  102. ]
  103. ]
  104. reply_markup = InlineKeyboardMarkup(keyboard)
  105. await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
  106. async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  107. """Handle the /help command."""
  108. if not self.is_authorized(update.effective_chat.id):
  109. await update.message.reply_text("❌ Unauthorized access.")
  110. return
  111. help_text = """
  112. 🔧 <b>Hyperliquid Trading Bot - Complete Guide</b>
  113. <b>💼 Account Management:</b>
  114. • /balance - Show account balance
  115. • /positions - Show open positions
  116. • /orders - Show open orders
  117. <b>📊 Market Data:</b>
  118. • /market - Detailed market data
  119. • /price - Quick price check
  120. <b>🔄 Manual Trading:</b>
  121. • /buy 0.001 50000 - Buy 0.001 BTC at $50,000
  122. • /sell 0.001 55000 - Sell 0.001 BTC at $55,000
  123. • /cancel ABC123 - Cancel order with ID ABC123
  124. <b>📈 Statistics & Analytics:</b>
  125. • /stats - Complete trading statistics
  126. • /performance - Win rate, profit factor, etc.
  127. • /risk - Sharpe ratio, drawdown, VaR
  128. • /trades - Recent trade history
  129. <b>⚙️ Configuration:</b>
  130. • Symbol: {symbol}
  131. • Default Amount: {amount}
  132. • Network: {network}
  133. <b>🛡️ Safety Features:</b>
  134. • All trades logged automatically
  135. • Comprehensive performance tracking
  136. • Real-time balance monitoring
  137. • Risk metrics calculation
  138. <b>📱 Mobile Optimized:</b>
  139. • Quick action buttons
  140. • Instant notifications
  141. • Clean, readable layout
  142. • One-tap commands
  143. For support, contact your bot administrator.
  144. """.format(
  145. symbol=Config.DEFAULT_TRADING_SYMBOL,
  146. amount=Config.DEFAULT_TRADE_AMOUNT,
  147. network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet"
  148. )
  149. await update.message.reply_text(help_text, parse_mode='HTML')
  150. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  151. """Handle the /stats command."""
  152. if not self.is_authorized(update.effective_chat.id):
  153. await update.message.reply_text("❌ Unauthorized access.")
  154. return
  155. # Get current balance for stats
  156. balance = self.client.get_balance()
  157. current_balance = 0
  158. if balance and balance.get('total'):
  159. current_balance = float(balance['total'].get('USDC', 0))
  160. stats_message = self.stats.format_stats_message(current_balance)
  161. await update.message.reply_text(stats_message, parse_mode='HTML')
  162. async def buy_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  163. """Handle the /buy command."""
  164. if not self.is_authorized(update.effective_chat.id):
  165. await update.message.reply_text("❌ Unauthorized access.")
  166. return
  167. try:
  168. if len(context.args) < 2:
  169. await update.message.reply_text(
  170. "❌ Usage: /buy [amount] [price]\n"
  171. f"Example: /buy {Config.DEFAULT_TRADE_AMOUNT} 50000"
  172. )
  173. return
  174. amount = float(context.args[0])
  175. price = float(context.args[1])
  176. symbol = Config.DEFAULT_TRADING_SYMBOL
  177. # Confirmation message
  178. confirmation_text = f"""
  179. 🟢 <b>Buy Order Confirmation</b>
  180. 📊 <b>Order Details:</b>
  181. • Symbol: {symbol}
  182. • Side: BUY
  183. • Amount: {amount}
  184. • Price: ${price:,.2f}
  185. • Total Value: ${amount * price:,.2f}
  186. ⚠️ <b>Are you sure you want to place this order?</b>
  187. This will attempt to buy {amount} {symbol} at ${price:,.2f} per unit.
  188. """
  189. keyboard = [
  190. [
  191. InlineKeyboardButton("✅ Confirm Buy", callback_data=f"confirm_buy_{amount}_{price}"),
  192. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  193. ]
  194. ]
  195. reply_markup = InlineKeyboardMarkup(keyboard)
  196. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  197. except ValueError:
  198. await update.message.reply_text("❌ Invalid amount or price. Please use numbers only.")
  199. except Exception as e:
  200. await update.message.reply_text(f"❌ Error processing buy command: {e}")
  201. async def sell_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  202. """Handle the /sell command."""
  203. if not self.is_authorized(update.effective_chat.id):
  204. await update.message.reply_text("❌ Unauthorized access.")
  205. return
  206. try:
  207. if len(context.args) < 2:
  208. await update.message.reply_text(
  209. "❌ Usage: /sell [amount] [price]\n"
  210. f"Example: /sell {Config.DEFAULT_TRADE_AMOUNT} 55000"
  211. )
  212. return
  213. amount = float(context.args[0])
  214. price = float(context.args[1])
  215. symbol = Config.DEFAULT_TRADING_SYMBOL
  216. # Confirmation message
  217. confirmation_text = f"""
  218. 🔴 <b>Sell Order Confirmation</b>
  219. 📊 <b>Order Details:</b>
  220. • Symbol: {symbol}
  221. • Side: SELL
  222. • Amount: {amount}
  223. • Price: ${price:,.2f}
  224. • Total Value: ${amount * price:,.2f}
  225. ⚠️ <b>Are you sure you want to place this order?</b>
  226. This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
  227. """
  228. keyboard = [
  229. [
  230. InlineKeyboardButton("✅ Confirm Sell", callback_data=f"confirm_sell_{amount}_{price}"),
  231. InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
  232. ]
  233. ]
  234. reply_markup = InlineKeyboardMarkup(keyboard)
  235. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  236. except ValueError:
  237. await update.message.reply_text("❌ Invalid amount or price. Please use numbers only.")
  238. except Exception as e:
  239. await update.message.reply_text(f"❌ Error processing sell command: {e}")
  240. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  241. """Handle the /trades command."""
  242. if not self.is_authorized(update.effective_chat.id):
  243. await update.message.reply_text("❌ Unauthorized access.")
  244. return
  245. recent_trades = self.stats.get_recent_trades(10)
  246. if not recent_trades:
  247. await update.message.reply_text("📝 No trades recorded yet.")
  248. return
  249. trades_text = "🔄 <b>Recent Trades</b>\n\n"
  250. for trade in reversed(recent_trades[-5:]): # Show last 5 trades
  251. timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  252. side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
  253. trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
  254. trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
  255. trades_text += f" 📅 {timestamp}\n\n"
  256. await update.message.reply_text(trades_text, parse_mode='HTML')
  257. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  258. """Handle the /balance command."""
  259. if not self.is_authorized(update.effective_chat.id):
  260. await update.message.reply_text("❌ Unauthorized access.")
  261. return
  262. balance = self.client.get_balance()
  263. if balance:
  264. balance_text = "💰 <b>Account Balance</b>\n\n"
  265. total_balance = balance.get('total', {})
  266. if total_balance:
  267. total_value = 0
  268. for asset, amount in total_balance.items():
  269. if float(amount) > 0:
  270. balance_text += f"💵 <b>{asset}:</b> {amount}\n"
  271. if asset == 'USDC':
  272. total_value += float(amount)
  273. balance_text += f"\n💼 <b>Total Value:</b> ${total_value:,.2f}"
  274. # Add stats summary
  275. basic_stats = self.stats.get_basic_stats()
  276. if basic_stats['initial_balance'] > 0:
  277. pnl = total_value - basic_stats['initial_balance']
  278. pnl_percent = (pnl / basic_stats['initial_balance']) * 100
  279. balance_text += f"\n📊 <b>P&L:</b> ${pnl:,.2f} ({pnl_percent:+.2f}%)"
  280. else:
  281. balance_text += "No balance data available"
  282. else:
  283. balance_text = "❌ Could not fetch balance data"
  284. await update.message.reply_text(balance_text, parse_mode='HTML')
  285. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  286. """Handle the /positions command."""
  287. if not self.is_authorized(update.effective_chat.id):
  288. await update.message.reply_text("❌ Unauthorized access.")
  289. return
  290. positions = self.client.get_positions()
  291. if positions:
  292. positions_text = "📈 <b>Open Positions</b>\n\n"
  293. open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
  294. if open_positions:
  295. total_unrealized = 0
  296. for position in open_positions:
  297. symbol = position.get('symbol', 'Unknown')
  298. contracts = float(position.get('contracts', 0))
  299. unrealized_pnl = float(position.get('unrealizedPnl', 0))
  300. entry_price = float(position.get('entryPx', 0))
  301. pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
  302. positions_text += f"📊 <b>{symbol}</b>\n"
  303. positions_text += f" 📏 Size: {contracts} contracts\n"
  304. positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
  305. positions_text += f" {pnl_emoji} PnL: ${unrealized_pnl:,.2f}\n\n"
  306. total_unrealized += unrealized_pnl
  307. positions_text += f"💼 <b>Total Unrealized P&L:</b> ${total_unrealized:,.2f}"
  308. else:
  309. positions_text += "No open positions"
  310. else:
  311. positions_text = "❌ Could not fetch positions data"
  312. await update.message.reply_text(positions_text, parse_mode='HTML')
  313. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  314. """Handle the /orders command."""
  315. if not self.is_authorized(update.effective_chat.id):
  316. await update.message.reply_text("❌ Unauthorized access.")
  317. return
  318. orders = self.client.get_open_orders()
  319. if orders:
  320. orders_text = "📋 <b>Open Orders</b>\n\n"
  321. if orders and len(orders) > 0:
  322. for order in orders:
  323. symbol = order.get('symbol', 'Unknown')
  324. side = order.get('side', 'Unknown')
  325. amount = order.get('amount', 0)
  326. price = order.get('price', 0)
  327. order_id = order.get('id', 'Unknown')
  328. side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
  329. orders_text += f"{side_emoji} <b>{symbol}</b>\n"
  330. orders_text += f" 📊 {side.upper()} {amount} @ ${price:,.2f}\n"
  331. orders_text += f" 💵 Value: ${float(amount) * float(price):,.2f}\n"
  332. orders_text += f" 🔑 ID: <code>{order_id}</code>\n\n"
  333. else:
  334. orders_text += "No open orders"
  335. else:
  336. orders_text = "❌ Could not fetch orders data"
  337. await update.message.reply_text(orders_text, parse_mode='HTML')
  338. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  339. """Handle the /market command."""
  340. if not self.is_authorized(update.effective_chat.id):
  341. await update.message.reply_text("❌ Unauthorized access.")
  342. return
  343. symbol = Config.DEFAULT_TRADING_SYMBOL
  344. market_data = self.client.get_market_data(symbol)
  345. if market_data:
  346. ticker = market_data['ticker']
  347. orderbook = market_data['orderbook']
  348. # Calculate 24h change
  349. current_price = float(ticker.get('last', 0))
  350. high_24h = float(ticker.get('high', 0))
  351. low_24h = float(ticker.get('low', 0))
  352. market_text = f"📊 <b>Market Data - {symbol}</b>\n\n"
  353. market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
  354. market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
  355. market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
  356. market_text += f"📊 <b>24h Volume:</b> {ticker.get('baseVolume', 'N/A')}\n\n"
  357. if orderbook.get('bids') and orderbook.get('asks'):
  358. best_bid = float(orderbook['bids'][0][0]) if orderbook['bids'] else 0
  359. best_ask = float(orderbook['asks'][0][0]) if orderbook['asks'] else 0
  360. spread = best_ask - best_bid
  361. spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
  362. market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
  363. market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
  364. market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
  365. else:
  366. market_text = "❌ Could not fetch market data"
  367. await update.message.reply_text(market_text, parse_mode='HTML')
  368. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  369. """Handle the /price command."""
  370. if not self.is_authorized(update.effective_chat.id):
  371. await update.message.reply_text("❌ Unauthorized access.")
  372. return
  373. symbol = Config.DEFAULT_TRADING_SYMBOL
  374. market_data = self.client.get_market_data(symbol)
  375. if market_data:
  376. price = float(market_data['ticker'].get('last', 0))
  377. price_text = f"💵 <b>{symbol}</b>: ${price:,.2f}"
  378. # Add timestamp
  379. timestamp = datetime.now().strftime('%H:%M:%S')
  380. price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
  381. else:
  382. price_text = f"❌ Could not fetch price for {symbol}"
  383. await update.message.reply_text(price_text, parse_mode='HTML')
  384. async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  385. """Handle inline keyboard button presses."""
  386. query = update.callback_query
  387. await query.answer()
  388. if not self.is_authorized(query.message.chat_id):
  389. await query.edit_message_text("❌ Unauthorized access.")
  390. return
  391. callback_data = query.data
  392. # Handle trading confirmations
  393. if callback_data.startswith('confirm_buy_'):
  394. parts = callback_data.split('_')
  395. amount = float(parts[2])
  396. price = float(parts[3])
  397. await self._execute_buy_order(query, amount, price)
  398. return
  399. elif callback_data.startswith('confirm_sell_'):
  400. parts = callback_data.split('_')
  401. amount = float(parts[2])
  402. price = float(parts[3])
  403. await self._execute_sell_order(query, amount, price)
  404. return
  405. elif callback_data == 'cancel_order':
  406. await query.edit_message_text("❌ Order cancelled.")
  407. return
  408. # Create a fake update object for reusing command handlers
  409. fake_update = Update(
  410. update_id=update.update_id,
  411. message=query.message,
  412. callback_query=query
  413. )
  414. # Handle regular button callbacks
  415. if callback_data == "balance":
  416. await self.balance_command(fake_update, context)
  417. elif callback_data == "stats":
  418. await self.stats_command(fake_update, context)
  419. elif callback_data == "positions":
  420. await self.positions_command(fake_update, context)
  421. elif callback_data == "orders":
  422. await self.orders_command(fake_update, context)
  423. elif callback_data == "market":
  424. await self.market_command(fake_update, context)
  425. elif callback_data == "price":
  426. await self.price_command(fake_update, context)
  427. elif callback_data == "trades":
  428. await self.trades_command(fake_update, context)
  429. elif callback_data == "help":
  430. await self.help_command(fake_update, context)
  431. async def _execute_buy_order(self, query, amount: float, price: float):
  432. """Execute a buy order."""
  433. symbol = Config.DEFAULT_TRADING_SYMBOL
  434. try:
  435. await query.edit_message_text("⏳ Placing buy order...")
  436. # Place the order
  437. order = self.client.place_limit_order(symbol, 'buy', amount, price)
  438. if order:
  439. # Record the trade in stats
  440. order_id = order.get('id', 'N/A')
  441. self.stats.record_trade(symbol, 'buy', amount, price, order_id)
  442. success_message = f"""
  443. ✅ <b>Buy Order Placed Successfully!</b>
  444. 📊 <b>Order Details:</b>
  445. • Symbol: {symbol}
  446. • Side: BUY
  447. • Amount: {amount}
  448. • Price: ${price:,.2f}
  449. • Order ID: <code>{order_id}</code>
  450. • Total Value: ${amount * price:,.2f}
  451. The order has been submitted to Hyperliquid.
  452. """
  453. await query.edit_message_text(success_message, parse_mode='HTML')
  454. logger.info(f"Buy order placed: {amount} {symbol} @ ${price}")
  455. else:
  456. await query.edit_message_text("❌ Failed to place buy order. Please try again.")
  457. except Exception as e:
  458. error_message = f"❌ Error placing buy order: {str(e)}"
  459. await query.edit_message_text(error_message)
  460. logger.error(f"Error placing buy order: {e}")
  461. async def _execute_sell_order(self, query, amount: float, price: float):
  462. """Execute a sell order."""
  463. symbol = Config.DEFAULT_TRADING_SYMBOL
  464. try:
  465. await query.edit_message_text("⏳ Placing sell order...")
  466. # Place the order
  467. order = self.client.place_limit_order(symbol, 'sell', amount, price)
  468. if order:
  469. # Record the trade in stats
  470. order_id = order.get('id', 'N/A')
  471. self.stats.record_trade(symbol, 'sell', amount, price, order_id)
  472. success_message = f"""
  473. ✅ <b>Sell Order Placed Successfully!</b>
  474. 📊 <b>Order Details:</b>
  475. • Symbol: {symbol}
  476. • Side: SELL
  477. • Amount: {amount}
  478. • Price: ${price:,.2f}
  479. • Order ID: <code>{order_id}</code>
  480. • Total Value: ${amount * price:,.2f}
  481. The order has been submitted to Hyperliquid.
  482. """
  483. await query.edit_message_text(success_message, parse_mode='HTML')
  484. logger.info(f"Sell order placed: {amount} {symbol} @ ${price}")
  485. else:
  486. await query.edit_message_text("❌ Failed to place sell order. Please try again.")
  487. except Exception as e:
  488. error_message = f"❌ Error placing sell order: {str(e)}"
  489. await query.edit_message_text(error_message)
  490. logger.error(f"Error placing sell order: {e}")
  491. async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  492. """Handle unknown commands."""
  493. if not self.is_authorized(update.effective_chat.id):
  494. await update.message.reply_text("❌ Unauthorized access.")
  495. return
  496. await update.message.reply_text(
  497. "❓ Unknown command. Use /help to see available commands or tap the buttons in /start."
  498. )
  499. def setup_handlers(self):
  500. """Set up command handlers for the bot."""
  501. if not self.application:
  502. return
  503. # Command handlers
  504. self.application.add_handler(CommandHandler("start", self.start_command))
  505. self.application.add_handler(CommandHandler("help", self.help_command))
  506. self.application.add_handler(CommandHandler("balance", self.balance_command))
  507. self.application.add_handler(CommandHandler("positions", self.positions_command))
  508. self.application.add_handler(CommandHandler("orders", self.orders_command))
  509. self.application.add_handler(CommandHandler("market", self.market_command))
  510. self.application.add_handler(CommandHandler("price", self.price_command))
  511. self.application.add_handler(CommandHandler("stats", self.stats_command))
  512. self.application.add_handler(CommandHandler("trades", self.trades_command))
  513. self.application.add_handler(CommandHandler("buy", self.buy_command))
  514. self.application.add_handler(CommandHandler("sell", self.sell_command))
  515. # Callback query handler for inline keyboards
  516. self.application.add_handler(CallbackQueryHandler(self.button_callback))
  517. # Handle unknown commands
  518. self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
  519. async def run(self):
  520. """Run the Telegram bot."""
  521. if not Config.TELEGRAM_BOT_TOKEN:
  522. logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
  523. return
  524. if not Config.TELEGRAM_CHAT_ID:
  525. logger.error("❌ TELEGRAM_CHAT_ID not configured")
  526. return
  527. # Create application
  528. self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
  529. # Set up handlers
  530. self.setup_handlers()
  531. logger.info("🚀 Starting Telegram trading bot...")
  532. # Send startup notification
  533. await self.send_message(
  534. "🤖 <b>Manual Trading Bot Started</b>\n\n"
  535. f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
  536. f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
  537. f"📱 Manual trading ready!\n"
  538. f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
  539. "Use /start for quick actions or /help for all commands."
  540. )
  541. # Start the bot
  542. await self.application.run_polling()
  543. def main():
  544. """Main entry point for the Telegram bot."""
  545. try:
  546. # Validate configuration
  547. if not Config.validate():
  548. logger.error("❌ Configuration validation failed!")
  549. return
  550. if not Config.TELEGRAM_ENABLED:
  551. logger.error("❌ Telegram is not enabled in configuration")
  552. return
  553. # Create and run the bot
  554. bot = TelegramTradingBot()
  555. asyncio.run(bot.run())
  556. except KeyboardInterrupt:
  557. logger.info("👋 Bot stopped by user")
  558. except Exception as e:
  559. logger.error(f"❌ Unexpected error: {e}")
  560. if __name__ == "__main__":
  561. main()