management_commands.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. #!/usr/bin/env python3
  2. """
  3. Management Commands - Handles management and monitoring Telegram commands.
  4. """
  5. import logging
  6. import os
  7. import platform
  8. import sys
  9. from datetime import datetime, timedelta
  10. from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup
  11. from telegram.ext import ContextTypes
  12. import json
  13. from src.config.config import Config
  14. from src.monitoring.alarm_manager import AlarmManager
  15. logger = logging.getLogger(__name__)
  16. class ManagementCommands:
  17. """Handles all management-related Telegram commands."""
  18. def __init__(self, trading_engine, market_monitor):
  19. """Initialize with trading engine and market monitor."""
  20. self.trading_engine = trading_engine
  21. self.market_monitor = market_monitor
  22. self.alarm_manager = AlarmManager()
  23. def _is_authorized(self, chat_id: str) -> bool:
  24. """Check if the chat ID is authorized."""
  25. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  26. async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  27. """Handle the /monitoring command."""
  28. chat_id = update.effective_chat.id
  29. if not self._is_authorized(chat_id):
  30. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  31. return
  32. # Get alarm statistics
  33. alarm_stats = self.alarm_manager.get_statistics()
  34. # Get balance adjustments info
  35. stats = self.trading_engine.get_stats()
  36. adjustments_summary = stats.get_balance_adjustments_summary() if stats else {
  37. 'total_deposits': 0, 'total_withdrawals': 0, 'net_adjustment': 0, 'adjustment_count': 0
  38. }
  39. # Safety checks for monitoring attributes
  40. monitoring_active = self.market_monitor.is_running
  41. status_text = f"""
  42. 🔄 <b>System Monitoring Status</b>
  43. 📊 <b>Order Monitoring:</b>
  44. • Active: {'✅ Yes' if monitoring_active else '❌ No'}
  45. • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
  46. • Market Monitor: {'✅ Running' if monitoring_active else '❌ Stopped'}
  47. 💰 <b>Balance Tracking:</b>
  48. • Total Adjustments: {adjustments_summary['adjustment_count']}
  49. • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  50. 🔔 <b>Price Alarms:</b>
  51. • Active Alarms: {alarm_stats['total_active']}
  52. • Triggered Today: {alarm_stats['total_triggered']}
  53. • Tokens Monitored: {alarm_stats['tokens_tracked']}
  54. • Next Alarm ID: {alarm_stats['next_id']}
  55. 🔄 <b>External Trade Monitoring:</b>
  56. • Auto Stats Update: ✅ Enabled
  57. • External Notifications: ✅ Enabled
  58. 🛡️ <b>Risk Management:</b>
  59. • Automatic Stop Loss: {'✅ Enabled' if hasattr(Config, 'RISK_MANAGEMENT_ENABLED') and Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
  60. • Order-based Stop Loss: ✅ Enabled
  61. 📈 <b>Notifications:</b>
  62. • 🚀 Position Opened/Increased
  63. • 📉 Position Partially/Fully Closed
  64. • 🎯 P&L Calculations
  65. • 🔔 Price Alarm Triggers
  66. • 🔄 External Trade Detection
  67. • 🛑 Order-based Stop Loss Placement
  68. 💾 <b>Bot State Persistence:</b>
  69. • Trading Engine State: ✅ Saved
  70. • Order Tracking: ✅ Saved
  71. • State Survives Restarts: ✅ Yes
  72. ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
  73. 💡 <b>Monitoring Features:</b>
  74. • Real-time order fill detection
  75. • Automatic P&L calculation
  76. • Position change tracking
  77. • Price alarm monitoring
  78. • External trade monitoring
  79. • Auto stats synchronization
  80. • Order-based stop loss placement
  81. • Instant Telegram notifications
  82. """
  83. if alarm_stats['token_breakdown']:
  84. status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
  85. for token, count in alarm_stats['token_breakdown'].items():
  86. status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
  87. await context.bot.send_message(chat_id=chat_id, text=status_text.strip(), parse_mode='HTML')
  88. async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  89. """Handle the /alarm command."""
  90. chat_id = update.effective_chat.id
  91. if not self._is_authorized(chat_id):
  92. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  93. return
  94. try:
  95. if not context.args or len(context.args) == 0:
  96. # No arguments - list all alarms
  97. alarms = self.alarm_manager.get_all_active_alarms()
  98. message = self.alarm_manager.format_alarm_list(alarms)
  99. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  100. return
  101. elif len(context.args) == 1:
  102. arg = context.args[0]
  103. # Check if argument is a number (alarm ID to remove)
  104. try:
  105. alarm_id = int(arg)
  106. # Remove alarm by ID
  107. if self.alarm_manager.remove_alarm(alarm_id):
  108. await context.bot.send_message(chat_id=chat_id, text=f"✅ Alarm ID {alarm_id} has been removed.")
  109. else:
  110. await context.bot.send_message(chat_id=chat_id, text=f"❌ Alarm ID {alarm_id} not found.")
  111. return
  112. except ValueError:
  113. # Not a number, treat as token
  114. token = arg.upper()
  115. alarms = self.alarm_manager.get_alarms_by_token(token)
  116. message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
  117. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  118. return
  119. elif len(context.args) == 2:
  120. # Set new alarm: /alarm TOKEN PRICE
  121. token = context.args[0].upper()
  122. target_price = float(context.args[1])
  123. # Get current market price
  124. symbol = f"{token}/USDC:USDC"
  125. market_data = self.trading_engine.get_market_data(symbol)
  126. if not market_data or not market_data.get('ticker'):
  127. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch current price for {token}")
  128. return
  129. current_price = float(market_data['ticker'].get('last', 0))
  130. if current_price <= 0:
  131. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}")
  132. return
  133. # Create the alarm
  134. alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
  135. # Format confirmation message
  136. direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
  137. price_diff = abs(target_price - current_price)
  138. price_diff_percent = (price_diff / current_price) * 100
  139. message = f"""
  140. ✅ <b>Price Alarm Created</b>
  141. 📊 <b>Alarm Details:</b>
  142. • Alarm ID: {alarm['id']}
  143. • Token: {token}
  144. • Target Price: ${target_price:,.2f}
  145. • Current Price: ${current_price:,.2f}
  146. • Direction: {alarm['direction'].upper()}
  147. {direction_emoji} <b>Alert Condition:</b>
  148. Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
  149. 💰 <b>Price Difference:</b>
  150. • Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
  151. • Status: ACTIVE ✅
  152. ⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
  153. 💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
  154. """
  155. await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML')
  156. else:
  157. # Too many arguments
  158. await context.bot.send_message(chat_id=chat_id, text=(
  159. "❌ Invalid usage. Examples:\n\n"
  160. "• <code>/alarm</code> - List all alarms\n"
  161. "• <code>/alarm BTC</code> - List BTC alarms\n"
  162. "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
  163. "• <code>/alarm 3</code> - Remove alarm ID 3"
  164. ), parse_mode='HTML')
  165. except ValueError:
  166. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.")
  167. except Exception as e:
  168. error_message = f"❌ Error processing alarm command: {str(e)}"
  169. await context.bot.send_message(chat_id=chat_id, text=error_message)
  170. logger.error(f"Error in alarm command: {e}")
  171. async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  172. """Handle the /logs command."""
  173. chat_id = update.effective_chat.id
  174. if not self._is_authorized(chat_id):
  175. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  176. return
  177. try:
  178. logs_dir = "logs"
  179. if not os.path.exists(logs_dir):
  180. await context.bot.send_message(chat_id=chat_id, text="📜 No logs directory found.")
  181. return
  182. # Get log files
  183. log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
  184. if not log_files:
  185. await context.bot.send_message(chat_id=chat_id, text="📜 No log files found.")
  186. return
  187. # Handle cleanup command
  188. if context.args and context.args[0].lower() == 'cleanup':
  189. days_to_keep = 30 # Default
  190. if len(context.args) > 1:
  191. try:
  192. days_to_keep = int(context.args[1])
  193. except ValueError:
  194. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid number of days. Using default (30 days).")
  195. # Clean up old log files
  196. cutoff_date = datetime.now() - timedelta(days=days_to_keep)
  197. cleaned_files = 0
  198. total_size_cleaned = 0
  199. for log_file in log_files:
  200. file_path = os.path.join(logs_dir, log_file)
  201. file_stat = os.stat(file_path)
  202. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  203. if file_date < cutoff_date:
  204. file_size = file_stat.st_size
  205. os.remove(file_path)
  206. cleaned_files += 1
  207. total_size_cleaned += file_size
  208. size_cleaned_mb = total_size_cleaned / (1024 * 1024)
  209. await context.bot.send_message(chat_id=chat_id, text=
  210. f"🧹 Log cleanup complete.\n"
  211. f"• Files older than {days_to_keep} days removed.\n"
  212. f"• Total files deleted: {cleaned_files}\n"
  213. f"• Total size cleaned: {size_cleaned_mb:.2f} MB"
  214. )
  215. return
  216. # Show log statistics
  217. total_size = 0
  218. oldest_file = None
  219. newest_file = None
  220. recent_files = []
  221. for log_file in sorted(log_files):
  222. file_path = os.path.join(logs_dir, log_file)
  223. file_stat = os.stat(file_path)
  224. file_size = file_stat.st_size
  225. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  226. total_size += file_size
  227. if oldest_file is None or file_date < oldest_file[1]:
  228. oldest_file = (log_file, file_date)
  229. if newest_file is None or file_date > newest_file[1]:
  230. newest_file = (log_file, file_date)
  231. # Keep track of recent files
  232. if len(recent_files) < 5:
  233. recent_files.append((log_file, file_size, file_date))
  234. logs_message = f"""
  235. 📜 <b>System Logging Status</b>
  236. 📁 <b>Log Directory:</b> {logs_dir}/
  237. • Total Files: {len(log_files)}
  238. • Total Size: {total_size / 1024 / 1024:.2f} MB
  239. • Oldest File: {oldest_file[0]} ({oldest_file[1].strftime('%m/%d/%Y')})
  240. • Newest File: {newest_file[0]} ({newest_file[1].strftime('%m/%d/%Y')})
  241. 📋 <b>Recent Log Files:</b>
  242. """
  243. for log_file, file_size, file_date in reversed(recent_files):
  244. size_mb = file_size / 1024 / 1024
  245. logs_message += f"• {log_file} ({size_mb:.2f} MB) - {file_date.strftime('%m/%d %H:%M')}\n"
  246. logs_message += f"""
  247. 📊 <b>Log Management:</b>
  248. • Location: ./logs/
  249. • Rotation: Daily
  250. • Retention: Manual cleanup available
  251. • Format: timestamp - module - level - message
  252. 🧹 <b>Cleanup Commands:</b>
  253. • <code>/logs cleanup</code> - Remove logs older than 30 days
  254. • <code>/logs cleanup 7</code> - Remove logs older than 7 days
  255. 💡 <b>Log Levels:</b>
  256. • INFO: Normal operations
  257. • ERROR: Error conditions
  258. • DEBUG: Detailed debugging (if enabled)
  259. """
  260. await context.bot.send_message(chat_id=chat_id, text=logs_message.strip(), parse_mode='HTML')
  261. except Exception as e:
  262. error_message = f"❌ Error processing logs command: {str(e)}"
  263. await context.bot.send_message(chat_id=chat_id, text=error_message)
  264. logger.error(f"Error in logs command: {e}")
  265. async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  266. """Handle the /debug command."""
  267. chat_id = update.effective_chat.id
  268. if not self._is_authorized(chat_id):
  269. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  270. return
  271. try:
  272. # Get system information
  273. debug_info = f"""
  274. 🐛 <b>Debug Information</b>
  275. 💻 <b>System Info:</b>
  276. • Python: {sys.version.split()[0]}
  277. • Platform: {platform.system()} {platform.release()}
  278. • Architecture: {platform.machine()}
  279. 📊 <b>Trading Engine:</b>
  280. • Stats Available: {'✅ Yes' if self.trading_engine.get_stats() else '❌ No'}
  281. • Client Connected: {'✅ Yes' if self.trading_engine.client else '❌ No'}
  282. 🔄 <b>Market Monitor:</b>
  283. • Running: {'✅ Yes' if self.market_monitor.is_running else '❌ No'}
  284. 📁 <b>State Files:</b>
  285. • Trading Engine State: {'✅ Exists' if os.path.exists('data/trading_engine_state.json') else '❌ Missing'}
  286. • Price Alarms: {'✅ Exists' if os.path.exists('data/price_alarms.json') else '❌ Missing'}
  287. • Trading Stats: {'✅ Exists' if os.path.exists('data/trading_stats.json') else '❌ Missing'}
  288. 🔔 <b>Alarm Manager:</b>
  289. • Active Alarms: {self.alarm_manager.get_statistics()['total_active']}
  290. • Triggered Alarms: {self.alarm_manager.get_statistics()['total_triggered']}
  291. ⏰ <b>Timestamps:</b>
  292. • Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  293. • Debug Generated: {datetime.now().isoformat()}
  294. """
  295. # Get current positions for debugging
  296. try:
  297. positions = self.trading_engine.get_positions()
  298. if positions:
  299. debug_info += f"\n📈 <b>Current Positions:</b> {len(positions)} found\n"
  300. for pos in positions[:3]: # Show first 3 positions
  301. symbol = pos.get('symbol', 'Unknown').replace('/USDC:USDC', '')
  302. contracts = pos.get('contracts', 0)
  303. if float(contracts) != 0:
  304. debug_info += f" • {symbol}: {contracts} contracts\n"
  305. else:
  306. debug_info += "\n📈 <b>Positions:</b> No positions found\n"
  307. except Exception as e:
  308. debug_info += f"\n📈 <b>Positions:</b> Error fetching ({str(e)})\n"
  309. # Get balance for debugging
  310. try:
  311. balance = self.trading_engine.get_balance()
  312. if balance and balance.get('total'):
  313. usdc_balance = float(balance['total'].get('USDC', 0))
  314. debug_info += f"\n💰 <b>USDC Balance:</b> ${usdc_balance:,.2f}\n"
  315. else:
  316. debug_info += "\n💰 <b>Balance:</b> No balance data\n"
  317. except Exception as e:
  318. debug_info += f"\n💰 <b>Balance:</b> Error fetching ({str(e)})\n"
  319. await context.bot.send_message(chat_id=chat_id, text=debug_info.strip(), parse_mode='HTML')
  320. except Exception as e:
  321. logger.error(f"❌ Error in debug command: {e}")
  322. await context.bot.send_message(chat_id=chat_id, text=f"❌ Debug error: {e}")
  323. async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  324. """Handle the /version command."""
  325. chat_id = update.effective_chat.id
  326. if not self._is_authorized(chat_id):
  327. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  328. return
  329. try:
  330. # Get uptime info
  331. uptime_info = "Unknown"
  332. try:
  333. import psutil
  334. process = psutil.Process()
  335. create_time = datetime.fromtimestamp(process.create_time())
  336. uptime = datetime.now() - create_time
  337. days = uptime.days
  338. hours, remainder = divmod(uptime.seconds, 3600)
  339. minutes, _ = divmod(remainder, 60)
  340. uptime_info = f"{days}d {hours}h {minutes}m"
  341. except ImportError:
  342. pass
  343. # Get stats info
  344. stats = self.trading_engine.get_stats()
  345. if stats:
  346. basic_stats = stats.get_basic_stats()
  347. else:
  348. basic_stats = {'total_trades': 0, 'completed_trades': 0, 'days_active': 0, 'start_date': 'Unknown'}
  349. version_text = f"""
  350. 🤖 <b>Trading Bot Version & System Info</b>
  351. 📱 <b>Bot Information:</b>
  352. • Version: <code>2.1.2</code>
  353. • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
  354. • Uptime: {uptime_info}
  355. • Default Token: {Config.DEFAULT_TRADING_TOKEN}
  356. 💻 <b>System Information:</b>
  357. • Python: {sys.version.split()[0]}
  358. • Platform: {platform.system()} {platform.release()}
  359. • Architecture: {platform.machine()}
  360. 📊 <b>Trading Stats:</b>
  361. • Total Orders: {basic_stats['total_trades']}
  362. • Completed Trades: {basic_stats['completed_trades']}
  363. • Days Active: {basic_stats['days_active']}
  364. • Start Date: {basic_stats['start_date']}
  365. 🔄 <b>Monitoring Status:</b>
  366. • Market Monitor: {'✅ Active' if self.market_monitor.is_running else '❌ Inactive'}
  367. • External Trades: ✅ Active
  368. • Price Alarms: ✅ Active ({self.alarm_manager.get_statistics()['total_active']} active)
  369. ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  370. """
  371. await context.bot.send_message(chat_id=chat_id, text=version_text.strip(), parse_mode='HTML')
  372. except Exception as e:
  373. error_message = f"❌ Error processing version command: {str(e)}"
  374. await context.bot.send_message(chat_id=chat_id, text=error_message)
  375. logger.error(f"Error in version command: {e}")
  376. async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  377. """Handle the /keyboard command to enable/show custom keyboard."""
  378. chat_id = update.effective_chat.id
  379. if not self._is_authorized(chat_id):
  380. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  381. return
  382. # Check if custom keyboard is enabled in config
  383. keyboard_enabled = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_ENABLED', False)
  384. if keyboard_enabled:
  385. # Create a simple reply keyboard with common commands
  386. keyboard = [
  387. [KeyboardButton("/balance"), KeyboardButton("/positions")],
  388. [KeyboardButton("/orders"), KeyboardButton("/stats")],
  389. [KeyboardButton("/daily"), KeyboardButton("/performance")],
  390. [KeyboardButton("/help"), KeyboardButton("/commands")]
  391. ]
  392. reply_markup = ReplyKeyboardMarkup(
  393. keyboard,
  394. resize_keyboard=True,
  395. one_time_keyboard=False,
  396. selective=True
  397. )
  398. await context.bot.send_message(chat_id=chat_id, text="⌨️ <b>Custom Keyboard Activated!</b>\n\n🎯 <b>Your quick buttons are now ready:</b>\n• /balance - Account balance\n• /positions - Open positions\n• /orders - Active orders\n• /stats - Trading statistics\n• /daily - Daily performance\n• /performance - Performance stats\n• /help - Help guide\n• /commands - Command menu\n\n💡 <b>How to use:</b>\nTap any button below to send the command instantly!\n\n🔧 These buttons will stay at the bottom of your chat.", reply_markup=reply_markup, parse_mode='HTML')
  399. else:
  400. await context.bot.send_message(chat_id=chat_id, text="❌ <b>Custom Keyboard Disabled</b>\n\n🔧 <b>To enable:</b>\n• Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n• Restart the bot\n• Run /keyboard again\n\n📋 <b>Current config:</b>\n• Enabled: {keyboard_enabled}\n• Layout: {getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', 'default')}", parse_mode='HTML')