management_commands.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  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, timezone
  10. from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup
  11. from telegram.ext import ContextTypes
  12. import json
  13. from typing import Dict, Any, List, Optional
  14. from src.config.config import Config
  15. from src.monitoring.alarm_manager import AlarmManager
  16. from src.utils.token_display_formatter import get_formatter
  17. from src.stats import TradingStats
  18. from src.config.logging_config import LoggingManager
  19. from .info.base import InfoCommandsBase
  20. logger = logging.getLogger(__name__)
  21. def _normalize_token_case(token: str) -> str:
  22. """
  23. Normalize token case: if any characters are already uppercase, keep as-is.
  24. Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
  25. """
  26. # Check if any character is already uppercase
  27. if any(c.isupper() for c in token):
  28. return token # Keep original case for mixed-case tokens
  29. else:
  30. return token.upper() # Convert to uppercase for all-lowercase input
  31. class ManagementCommands(InfoCommandsBase):
  32. """Handles all management-related Telegram commands."""
  33. def __init__(self, trading_engine, monitoring_coordinator):
  34. """Initialize with trading engine and monitoring coordinator."""
  35. super().__init__(trading_engine)
  36. self.trading_engine = trading_engine
  37. self.monitoring_coordinator = monitoring_coordinator
  38. self.alarm_manager = AlarmManager()
  39. async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  40. """Handle the /monitoring command."""
  41. if not self._is_authorized(update):
  42. await self._reply(update, "❌ Unauthorized access.")
  43. return
  44. chat_id = update.effective_chat.id
  45. # Get alarm statistics
  46. alarm_stats = self.alarm_manager.get_statistics()
  47. # Get balance adjustments info
  48. stats = self.trading_engine.get_stats()
  49. adjustments_summary = stats.get_balance_adjustments_summary() if stats else {
  50. 'total_deposits': 0, 'total_withdrawals': 0, 'net_adjustment': 0, 'adjustment_count': 0
  51. }
  52. formatter = get_formatter()
  53. # Get monitoring status from coordinator
  54. monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
  55. monitoring_active = monitoring_status.get('is_running', False)
  56. status_text = f"""
  57. 🔄 <b>System Monitoring Status</b>
  58. 📊 <b>Monitoring System:</b>
  59. • Status: {'✅ Active' if monitoring_active else '❌ Inactive'}
  60. • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
  61. • Position Tracker: {'✅' if monitoring_status.get('components', {}).get('position_tracker', False) else '❌'}
  62. • Risk Manager: {'✅' if monitoring_status.get('components', {}).get('risk_manager', False) else '❌'}
  63. • Pending Orders: {'✅' if monitoring_status.get('components', {}).get('pending_orders_manager', False) else '❌'}
  64. 💰 <b>Balance Tracking:</b>
  65. • Total Adjustments: {adjustments_summary['adjustment_count']}
  66. • Net Adjustment: {await formatter.format_price_with_symbol(adjustments_summary['net_adjustment'])}
  67. 🔔 <b>Price Alarms:</b>
  68. • Active Alarms: {alarm_stats['total_active']}
  69. • Triggered Today: {alarm_stats['total_triggered']}
  70. • Tokens Monitored: {alarm_stats['tokens_tracked']}
  71. • Next Alarm ID: {alarm_stats['next_id']}
  72. 🔄 <b>External Trade Monitoring:</b>
  73. • Auto Stats Update: ✅ Enabled
  74. • External Notifications: ✅ Enabled
  75. 🛡️ <b>Risk Management:</b>
  76. • Automatic Stop Loss: {'✅ Enabled' if hasattr(Config, 'RISK_MANAGEMENT_ENABLED') and Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
  77. • Order-based Stop Loss: ✅ Enabled
  78. 📈 <b>Notifications:</b>
  79. • 🚀 Position Opened/Increased
  80. • 📉 Position Partially/Fully Closed
  81. • 🎯 P&L Calculations
  82. • 🔔 Price Alarm Triggers
  83. • 🔄 External Trade Detection
  84. • 🛑 Order-based Stop Loss Placement
  85. 💾 <b>Bot State Persistence:</b>
  86. • Trading Engine State: ✅ Saved
  87. • Order Tracking: ✅ Saved
  88. • State Survives Restarts: ✅ Yes
  89. ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
  90. 💡 <b>Monitoring Features:</b>
  91. • Real-time order fill detection
  92. • Automatic P&L calculation
  93. • Position change tracking
  94. • Price alarm monitoring
  95. • External trade monitoring
  96. • Auto stats synchronization
  97. • Order-based stop loss placement
  98. • Instant Telegram notifications
  99. """
  100. if alarm_stats['token_breakdown']:
  101. status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
  102. for token, count in alarm_stats['token_breakdown'].items():
  103. status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
  104. await context.bot.send_message(chat_id=chat_id, text=status_text.strip(), parse_mode='HTML')
  105. async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  106. """Handle the /alarm command."""
  107. if not self._is_authorized(update):
  108. await self._reply(update, "❌ Unauthorized access.")
  109. return
  110. chat_id = update.effective_chat.id
  111. try:
  112. if not context.args or len(context.args) == 0:
  113. # No arguments - list all alarms
  114. alarms = self.alarm_manager.get_all_active_alarms()
  115. message = self.alarm_manager.format_alarm_list(alarms)
  116. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  117. return
  118. elif len(context.args) == 1:
  119. arg = context.args[0]
  120. # Check if argument is a number (alarm ID to remove)
  121. try:
  122. alarm_id = int(arg)
  123. # Remove alarm by ID
  124. if self.alarm_manager.remove_alarm(alarm_id):
  125. await context.bot.send_message(chat_id=chat_id, text=f"✅ Alarm ID {alarm_id} has been removed.")
  126. else:
  127. await context.bot.send_message(chat_id=chat_id, text=f"❌ Alarm ID {alarm_id} not found.")
  128. return
  129. except ValueError:
  130. # Not a number, treat as token
  131. token = _normalize_token_case(arg)
  132. alarms = self.alarm_manager.get_alarms_by_token(token)
  133. message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
  134. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  135. return
  136. elif len(context.args) == 2:
  137. # Set new alarm: /alarm TOKEN PRICE
  138. token = _normalize_token_case(context.args[0])
  139. target_price = float(context.args[1])
  140. # Get current market price
  141. symbol = f"{token}/USDC:USDC"
  142. market_data = await self.trading_engine.get_market_data(symbol)
  143. if not market_data or not market_data.get('ticker'):
  144. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch current price for {token}")
  145. return
  146. current_price = float(market_data['ticker'].get('last', 0))
  147. if current_price <= 0:
  148. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}")
  149. return
  150. # Create the alarm
  151. alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
  152. formatter = get_formatter()
  153. # Format confirmation message
  154. direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
  155. price_diff = abs(target_price - current_price)
  156. price_diff_percent = (price_diff / current_price) * 100 if current_price != 0 else 0
  157. target_price_str = await formatter.format_price_with_symbol(target_price, token)
  158. current_price_str = await formatter.format_price_with_symbol(current_price, token)
  159. price_diff_str = await formatter.format_price_with_symbol(price_diff, token)
  160. message = f"""
  161. ✅ <b>Price Alarm Created</b>
  162. 📊 <b>Alarm Details:</b>
  163. • Alarm ID: {alarm['id']}
  164. • Token: {token}
  165. • Target Price: {target_price_str}
  166. • Current Price: {current_price_str}
  167. • Direction: {alarm['direction'].upper()}
  168. {direction_emoji} <b>Alert Condition:</b>
  169. Will trigger when {token} price moves {alarm['direction']} {target_price_str}
  170. 💰 <b>Price Difference:</b>
  171. • Distance: {price_diff_str} ({price_diff_percent:.2f}%)
  172. • Status: ACTIVE ✅
  173. ⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
  174. 💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
  175. """
  176. await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML')
  177. else:
  178. # Too many arguments
  179. await context.bot.send_message(chat_id=chat_id, text=(
  180. "❌ Invalid usage. Examples:\n\n"
  181. "• <code>/alarm</code> - List all alarms\n"
  182. "• <code>/alarm BTC</code> - List BTC alarms\n"
  183. "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
  184. "• <code>/alarm 3</code> - Remove alarm ID 3"
  185. ), parse_mode='HTML')
  186. except ValueError:
  187. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.")
  188. except Exception as e:
  189. error_message = f"❌ Error processing alarm command: {str(e)}"
  190. await context.bot.send_message(chat_id=chat_id, text=error_message)
  191. logger.error(f"Error in alarm command: {e}")
  192. async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  193. """Handle the /logs command."""
  194. if not self._is_authorized(update):
  195. await self._reply(update, "❌ Unauthorized access.")
  196. return
  197. chat_id = update.effective_chat.id
  198. try:
  199. logs_dir = "logs"
  200. if not os.path.exists(logs_dir):
  201. await context.bot.send_message(chat_id=chat_id, text="📜 No logs directory found.")
  202. return
  203. # Get log files
  204. log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
  205. if not log_files:
  206. await context.bot.send_message(chat_id=chat_id, text="📜 No log files found.")
  207. return
  208. # Handle cleanup command
  209. if context.args and context.args[0].lower() == 'cleanup':
  210. days_to_keep = 30 # Default
  211. if len(context.args) > 1:
  212. try:
  213. days_to_keep = int(context.args[1])
  214. except ValueError:
  215. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid number of days. Using default (30 days).")
  216. # Clean up old log files
  217. cutoff_date = datetime.now() - timedelta(days=days_to_keep)
  218. cleaned_files = 0
  219. total_size_cleaned = 0
  220. for log_file in log_files:
  221. file_path = os.path.join(logs_dir, log_file)
  222. file_stat = os.stat(file_path)
  223. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  224. if file_date < cutoff_date:
  225. file_size = file_stat.st_size
  226. os.remove(file_path)
  227. cleaned_files += 1
  228. total_size_cleaned += file_size
  229. size_cleaned_mb = total_size_cleaned / (1024 * 1024)
  230. await context.bot.send_message(chat_id=chat_id, text=
  231. f"🧹 Log cleanup complete.\n"
  232. f"• Files older than {days_to_keep} days removed.\n"
  233. f"• Total files deleted: {cleaned_files}\n"
  234. f"• Total size cleaned: {size_cleaned_mb:.2f} MB"
  235. )
  236. return
  237. # Show log statistics
  238. total_size = 0
  239. oldest_file = None
  240. newest_file = None
  241. recent_files = []
  242. for log_file in sorted(log_files):
  243. file_path = os.path.join(logs_dir, log_file)
  244. file_stat = os.stat(file_path)
  245. file_size = file_stat.st_size
  246. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  247. total_size += file_size
  248. if oldest_file is None or file_date < oldest_file[1]:
  249. oldest_file = (log_file, file_date)
  250. if newest_file is None or file_date > newest_file[1]:
  251. newest_file = (log_file, file_date)
  252. # Keep track of recent files
  253. if len(recent_files) < 5:
  254. recent_files.append((log_file, file_size, file_date))
  255. logs_message = f"""
  256. 📜 <b>System Logging Status</b>
  257. 📁 <b>Log Directory:</b> {logs_dir}/
  258. • Total Files: {len(log_files)}
  259. • Total Size: {total_size / 1024 / 1024:.2f} MB
  260. • Oldest File: {oldest_file[0]} ({oldest_file[1].strftime('%m/%d/%Y')})
  261. • Newest File: {newest_file[0]} ({newest_file[1].strftime('%m/%d/%Y')})
  262. 📋 <b>Recent Log Files:</b>
  263. """
  264. for log_file, file_size, file_date in reversed(recent_files):
  265. size_mb = file_size / 1024 / 1024
  266. logs_message += f"• {log_file} ({size_mb:.2f} MB) - {file_date.strftime('%m/%d %H:%M')}\n"
  267. logs_message += f"""
  268. 📊 <b>Log Management:</b>
  269. • Location: ./logs/
  270. • Rotation: Daily
  271. • Retention: Manual cleanup available
  272. • Format: timestamp - module - level - message
  273. 🧹 <b>Cleanup Commands:</b>
  274. • <code>/logs cleanup</code> - Remove logs older than 30 days
  275. • <code>/logs cleanup 7</code> - Remove logs older than 7 days
  276. 💡 <b>Log Levels:</b>
  277. • INFO: Normal operations
  278. • ERROR: Error conditions
  279. • DEBUG: Detailed debugging (if enabled)
  280. """
  281. await context.bot.send_message(chat_id=chat_id, text=logs_message.strip(), parse_mode='HTML')
  282. except Exception as e:
  283. error_message = f"❌ Error processing logs command: {str(e)}"
  284. await context.bot.send_message(chat_id=chat_id, text=error_message)
  285. logger.error(f"Error in logs command: {e}")
  286. async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  287. """Handle the /debug command."""
  288. if not self._is_authorized(update):
  289. await self._reply(update, "❌ Unauthorized access.")
  290. return
  291. chat_id = update.effective_chat.id
  292. try:
  293. # Get monitoring status
  294. monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
  295. monitoring_active = monitoring_status.get('is_running', False)
  296. # Get system information
  297. debug_info = f"""
  298. 🐛 <b>Debug Information</b>
  299. 💻 <b>System Info:</b>
  300. • Python: {sys.version.split()[0]}
  301. • Platform: {platform.system()} {platform.release()}
  302. • Architecture: {platform.machine()}
  303. 📊 <b>Trading Engine:</b>
  304. • Stats Available: {'✅ Yes' if self.trading_engine.get_stats() else '❌ No'}
  305. • Client Connected: {'✅ Yes' if self.trading_engine.client else '❌ No'}
  306. 🔄 <b>Monitoring System:</b>
  307. • Running: {'✅ Yes' if monitoring_active else '❌ No'}
  308. 📁 <b>State Files:</b>
  309. • Price Alarms: {'✅ Exists' if os.path.exists('data/price_alarms.json') else '❌ Missing'}
  310. • Trading Stats: {'✅ Exists' if os.path.exists('data/trading_stats.sqlite') else '❌ Missing'}
  311. 🔔 <b>Alarm Manager:</b>
  312. • Active Alarms: {self.alarm_manager.get_statistics()['total_active']}
  313. • Triggered Alarms: {self.alarm_manager.get_statistics()['total_triggered']}
  314. ⏰ <b>Timestamps:</b>
  315. • Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  316. • Debug Generated: {datetime.now().isoformat()}
  317. """
  318. # Get current positions for debugging
  319. try:
  320. positions = self.trading_engine.get_positions()
  321. if positions:
  322. debug_info += f"\n📈 <b>Current Positions:</b> {len(positions)} found\n"
  323. for pos in positions[:3]: # Show first 3 positions
  324. symbol = pos.get('symbol', 'Unknown').replace('/USDC:USDC', '')
  325. contracts = pos.get('contracts', 0)
  326. if float(contracts) != 0:
  327. debug_info += f" • {symbol}: {contracts} contracts\n"
  328. else:
  329. debug_info += "\n📈 <b>Positions:</b> No positions found\n"
  330. except Exception as e:
  331. debug_info += f"\n📈 <b>Positions:</b> Error fetching ({str(e)})\n"
  332. # Get balance for debugging
  333. try:
  334. balance = self.trading_engine.get_balance()
  335. if balance and balance.get('total'):
  336. usdc_balance = float(balance['total'].get('USDC', 0))
  337. debug_info += f"\n💰 <b>USDC Balance:</b> ${usdc_balance:,.2f}\n"
  338. else:
  339. debug_info += "\n💰 <b>Balance:</b> No balance data\n"
  340. except Exception as e:
  341. debug_info += f"\n💰 <b>Balance:</b> Error fetching ({str(e)})\n"
  342. await context.bot.send_message(chat_id=chat_id, text=debug_info.strip(), parse_mode='HTML')
  343. except Exception as e:
  344. logger.error(f"❌ Error in debug command: {e}")
  345. await context.bot.send_message(chat_id=chat_id, text=f"❌ Debug error: {e}")
  346. async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  347. """Handle the /version command."""
  348. if not self._is_authorized(update):
  349. await self._reply(update, "❌ Unauthorized access.")
  350. return
  351. chat_id = update.effective_chat.id
  352. try:
  353. # Get monitoring status
  354. monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
  355. monitoring_active = monitoring_status.get('is_running', False)
  356. # Get uptime info
  357. uptime_info = "Unknown"
  358. try:
  359. import psutil
  360. process = psutil.Process()
  361. create_time = datetime.fromtimestamp(process.create_time())
  362. uptime = datetime.now() - create_time
  363. days = uptime.days
  364. hours, remainder = divmod(uptime.seconds, 3600)
  365. minutes, _ = divmod(remainder, 60)
  366. uptime_info = f"{days}d {hours}h {minutes}m"
  367. except ImportError:
  368. pass
  369. # Get stats info
  370. stats = self.trading_engine.get_stats()
  371. if stats:
  372. basic_stats = stats.get_basic_stats()
  373. # Ensure all required keys exist with safe defaults
  374. total_trades = basic_stats.get('total_trades', 0)
  375. completed_trades = basic_stats.get('completed_trades', 0)
  376. days_active = basic_stats.get('days_active', 0)
  377. start_date = basic_stats.get('start_date', 'Unknown')
  378. else:
  379. total_trades = 0
  380. completed_trades = 0
  381. days_active = 0
  382. start_date = 'Unknown'
  383. version_text = f"""
  384. 🤖 <b>Trading Bot Version & System Info</b>
  385. 📱 <b>Bot Information:</b>
  386. • Version: <code>3.0.316</code>
  387. • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
  388. • Uptime: {uptime_info}
  389. • Default Token: {Config.DEFAULT_TRADING_TOKEN}
  390. 💻 <b>System Information:</b>
  391. • Python: {sys.version.split()[0]}
  392. • Platform: {platform.system()} {platform.release()}
  393. • Architecture: {platform.machine()}
  394. 📊 <b>Trading Stats:</b>
  395. • Total Orders: {total_trades}
  396. • Completed Trades: {completed_trades}
  397. • Days Active: {days_active}
  398. • Start Date: {start_date}
  399. 🔄 <b>Monitoring Status:</b>
  400. • Monitoring System: {'✅ Active' if monitoring_active else '❌ Inactive'}
  401. • External Trades: ✅ Active
  402. • Price Alarms: ✅ Active ({self.alarm_manager.get_statistics()['total_active']} active)
  403. ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  404. """
  405. await context.bot.send_message(chat_id=chat_id, text=version_text.strip(), parse_mode='HTML')
  406. except Exception as e:
  407. error_message = f"❌ Error processing version command: {str(e)}"
  408. await context.bot.send_message(chat_id=chat_id, text=error_message)
  409. logger.error(f"Error in version command: {e}")
  410. async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  411. """Handle the /keyboard command to show the main keyboard."""
  412. if not self._is_authorized(update):
  413. await self._reply(update, "❌ Unauthorized access.")
  414. return
  415. chat_id = update.effective_chat.id
  416. # Define default keyboard layout
  417. default_keyboard = [
  418. [KeyboardButton("LONG"), KeyboardButton("SHORT"), KeyboardButton("EXIT")],
  419. [KeyboardButton("BALANCE"), KeyboardButton("POSITIONS"), KeyboardButton("ORDERS")],
  420. [KeyboardButton("STATS"), KeyboardButton("MARKET"), KeyboardButton("PERFORMANCE")],
  421. [KeyboardButton("DAILY"), KeyboardButton("WEEKLY"), KeyboardButton("MONTHLY")],
  422. [KeyboardButton("RISK"), KeyboardButton("ALARM"), KeyboardButton("MONITORING")],
  423. [KeyboardButton("LOGS"), KeyboardButton("DEBUG"), KeyboardButton("VERSION")],
  424. [KeyboardButton("COMMANDS"), KeyboardButton("KEYBOARD"), KeyboardButton("COO"), KeyboardButton("COPY")]
  425. ]
  426. # Try to use custom keyboard from config if enabled
  427. if Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED and Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT:
  428. try:
  429. # Parse layout from config: "cmd1,cmd2|cmd3,cmd4|cmd5,cmd6"
  430. rows = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT.split('|')
  431. keyboard = []
  432. for row in rows:
  433. buttons = []
  434. for cmd in row.split(','):
  435. cmd = cmd.strip()
  436. # Remove leading slash if present and convert to button text
  437. if cmd.startswith('/'):
  438. cmd = cmd[1:]
  439. buttons.append(KeyboardButton(cmd.upper()))
  440. if buttons: # Only add non-empty rows
  441. keyboard.append(buttons)
  442. except Exception as e:
  443. logger.warning(f"Error parsing custom keyboard layout: {e}, falling back to default")
  444. keyboard = default_keyboard
  445. else:
  446. # Use default keyboard when custom keyboard is disabled
  447. keyboard = default_keyboard
  448. reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
  449. await context.bot.send_message(
  450. chat_id=chat_id,
  451. text="🎹 <b>Trading Bot Keyboard</b>\n\nUse the buttons below for quick access to commands:",
  452. reply_markup=reply_markup,
  453. parse_mode='HTML'
  454. )
  455. async def sync_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  456. """Handle the /sync command to synchronize exchange orders with database."""
  457. try:
  458. if not self._is_authorized(update):
  459. await self._reply(update, "❌ Unauthorized access.")
  460. return
  461. # Get force parameter
  462. force = False
  463. if context.args and context.args[0].lower() == 'force':
  464. force = True
  465. await self._reply(update, "🔄 Starting order synchronization...")
  466. # Get monitoring coordinator
  467. monitoring_coordinator = self.monitoring_coordinator
  468. if not monitoring_coordinator:
  469. await self._reply(update, "❌ Monitoring coordinator not available. Please restart the bot.")
  470. return
  471. # Check if exchange order sync is available, if not try to initialize it
  472. if not monitoring_coordinator.exchange_order_sync:
  473. await self._reply(update, "⚠️ Exchange order sync not initialized. Attempting to initialize...")
  474. # Try to initialize the sync manually
  475. try:
  476. await self._initialize_exchange_order_sync(monitoring_coordinator)
  477. if not monitoring_coordinator.exchange_order_sync:
  478. await self._reply(update, "❌ Failed to initialize exchange order sync. Please restart the bot.")
  479. return
  480. await self._reply(update, "✅ Exchange order sync initialized successfully.")
  481. except Exception as e:
  482. logger.error(f"Failed to initialize exchange order sync: {e}")
  483. await self._reply(update, f"❌ Failed to initialize exchange order sync: {str(e)}")
  484. return
  485. # Run synchronization
  486. sync_results = monitoring_coordinator.exchange_order_sync.sync_exchange_orders_to_database()
  487. # Format results message
  488. message = await self._format_sync_results(sync_results, force)
  489. await self._reply(update, message)
  490. except Exception as e:
  491. logger.error(f"Error in sync command: {e}", exc_info=True)
  492. await self._reply(update, "❌ Error during synchronization.")
  493. async def _initialize_exchange_order_sync(self, monitoring_coordinator):
  494. """Try to manually initialize exchange order sync."""
  495. try:
  496. from src.monitoring.exchange_order_sync import ExchangeOrderSync
  497. # Get trading stats from trading engine
  498. stats = self.trading_engine.get_stats()
  499. if not stats:
  500. raise Exception("Trading stats not available from trading engine")
  501. # Get hyperliquid client
  502. hl_client = self.trading_engine.client
  503. if not hl_client:
  504. raise Exception("Hyperliquid client not available")
  505. # Initialize the exchange order sync
  506. monitoring_coordinator.exchange_order_sync = ExchangeOrderSync(hl_client, stats)
  507. logger.info("✅ Manually initialized exchange order sync")
  508. except Exception as e:
  509. logger.error(f"Error manually initializing exchange order sync: {e}")
  510. raise
  511. async def _format_sync_results(self, sync_results: Dict[str, Any], force: bool) -> str:
  512. """Format synchronization results for display."""
  513. if 'error' in sync_results:
  514. return f"❌ Synchronization failed: {sync_results['error']}"
  515. new_orders = sync_results.get('new_orders_added', 0)
  516. updated_orders = sync_results.get('orders_updated', 0)
  517. cancelled_orders = sync_results.get('orphaned_orders_cancelled', 0)
  518. errors = sync_results.get('errors', [])
  519. message_parts = ["✅ <b>Order Synchronization Complete</b>\n"]
  520. # Results summary
  521. message_parts.append("📊 <b>Sync Results:</b>")
  522. message_parts.append(f" • {new_orders} new orders added to database")
  523. message_parts.append(f" • {updated_orders} existing orders updated")
  524. message_parts.append(f" • {cancelled_orders} orphaned orders cancelled")
  525. if errors:
  526. message_parts.append(f" • {len(errors)} errors encountered")
  527. message_parts.append("")
  528. message_parts.append("⚠️ <b>Errors:</b>")
  529. for error in errors[:3]: # Show first 3 errors
  530. message_parts.append(f" • {error}")
  531. if len(errors) > 3:
  532. message_parts.append(f" • ... and {len(errors) - 3} more errors")
  533. message_parts.append("")
  534. # Status messages
  535. if new_orders == 0 and updated_orders == 0 and cancelled_orders == 0:
  536. message_parts.append("✅ All orders are already synchronized")
  537. else:
  538. message_parts.append("🔄 Database now matches exchange state")
  539. # Help text
  540. message_parts.append("")
  541. message_parts.append("💡 <b>About Sync:</b>")
  542. message_parts.append("• Orders placed directly on exchange are now tracked")
  543. message_parts.append("• Bot-placed orders remain tracked as before")
  544. message_parts.append("• Sync runs automatically every 30 seconds")
  545. message_parts.append("• Use /orders to see all synchronized orders")
  546. return "\n".join(message_parts)
  547. async def sync_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  548. """Handle the /sync_status command to show synchronization diagnostics."""
  549. try:
  550. if not self._is_authorized(update):
  551. await self._reply(update, "❌ Unauthorized access.")
  552. return
  553. # Check monitoring coordinator
  554. monitoring_coordinator = self.monitoring_coordinator
  555. diagnostic_parts = ["🔍 <b>Synchronization Diagnostics</b>\n"]
  556. # Check monitoring coordinator
  557. if monitoring_coordinator:
  558. diagnostic_parts.append("✅ <b>Monitoring Coordinator:</b> Available")
  559. diagnostic_parts.append(f" • Running: {'Yes' if monitoring_coordinator.is_running else 'No'}")
  560. # Check exchange order sync
  561. if monitoring_coordinator.exchange_order_sync:
  562. diagnostic_parts.append("✅ <b>Exchange Order Sync:</b> Available")
  563. # Try to get sync stats
  564. try:
  565. sync_stats = self._get_sync_diagnostics(monitoring_coordinator)
  566. diagnostic_parts.extend(sync_stats)
  567. except Exception as e:
  568. diagnostic_parts.append(f"⚠️ <b>Sync Stats Error:</b> {str(e)}")
  569. else:
  570. diagnostic_parts.append("❌ <b>Exchange Order Sync:</b> Not Available")
  571. # Try to diagnose why
  572. diagnostic_parts.append("\n🔍 <b>Troubleshooting:</b>")
  573. # Check position tracker
  574. if hasattr(monitoring_coordinator, 'position_tracker'):
  575. diagnostic_parts.append("✅ Position Tracker: Available")
  576. if hasattr(monitoring_coordinator.position_tracker, 'trading_stats'):
  577. if monitoring_coordinator.position_tracker.trading_stats:
  578. diagnostic_parts.append("✅ Position Tracker Trading Stats: Available")
  579. else:
  580. diagnostic_parts.append("❌ Position Tracker Trading Stats: None")
  581. else:
  582. diagnostic_parts.append("❌ Position Tracker Trading Stats: Not Found")
  583. else:
  584. diagnostic_parts.append("❌ Position Tracker: Not Available")
  585. # Check trading engine stats
  586. stats = self.trading_engine.get_stats()
  587. if stats:
  588. diagnostic_parts.append("✅ Trading Engine Stats: Available")
  589. else:
  590. diagnostic_parts.append("❌ Trading Engine Stats: Not Available")
  591. # Check hyperliquid client
  592. client = self.trading_engine.client
  593. if client:
  594. diagnostic_parts.append("✅ Hyperliquid Client: Available")
  595. else:
  596. diagnostic_parts.append("❌ Hyperliquid Client: Not Available")
  597. else:
  598. diagnostic_parts.append("❌ <b>Monitoring Coordinator:</b> Not Available")
  599. diagnostic_parts.append("\n💡 <b>Solutions:</b>")
  600. diagnostic_parts.append("• Try: /sync force")
  601. diagnostic_parts.append("• If that fails, restart the bot")
  602. diagnostic_parts.append("• Check logs for detailed error messages")
  603. await self._reply(update, "\n".join(diagnostic_parts))
  604. except Exception as e:
  605. logger.error(f"Error in sync_status command: {e}", exc_info=True)
  606. await self._reply(update, f"❌ Error getting sync status: {str(e)}")
  607. def _get_sync_diagnostics(self, monitoring_coordinator):
  608. """Get detailed sync diagnostics."""
  609. diagnostic_parts = []
  610. try:
  611. # Test if we can access the sync components
  612. sync_manager = monitoring_coordinator.exchange_order_sync
  613. if hasattr(sync_manager, 'hl_client'):
  614. diagnostic_parts.append("✅ Hyperliquid Client: Connected")
  615. else:
  616. diagnostic_parts.append("❌ Hyperliquid Client: Missing")
  617. if hasattr(sync_manager, 'trading_stats'):
  618. diagnostic_parts.append("✅ Trading Stats: Connected")
  619. # Try to get a count of database orders
  620. try:
  621. db_orders = []
  622. db_orders.extend(sync_manager.trading_stats.get_orders_by_status('open', limit=10))
  623. db_orders.extend(sync_manager.trading_stats.get_orders_by_status('submitted', limit=10))
  624. diagnostic_parts.append(f"✅ Database Orders: {len(db_orders)} found")
  625. except Exception as e:
  626. diagnostic_parts.append(f"⚠️ Database Orders: Error ({str(e)})")
  627. else:
  628. diagnostic_parts.append("❌ Trading Stats: Missing")
  629. except Exception as e:
  630. diagnostic_parts.append(f"❌ Sync Components: Error ({str(e)})")
  631. return diagnostic_parts
  632. async def deposit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  633. """Handle the /deposit command to record a deposit."""
  634. if not self._is_authorized(update):
  635. await self._reply(update, "❌ Unauthorized access.")
  636. return
  637. chat_id = update.effective_chat.id
  638. try:
  639. # Parse arguments
  640. if not context.args or len(context.args) != 1:
  641. await context.bot.send_message(
  642. chat_id=chat_id,
  643. text="❌ Usage: /deposit <amount>\n\nExample: /deposit 500.00"
  644. )
  645. return
  646. amount = float(context.args[0])
  647. if amount <= 0:
  648. await context.bot.send_message(chat_id=chat_id, text="❌ Deposit amount must be positive.")
  649. return
  650. # Record the deposit
  651. stats = self.trading_engine.get_stats()
  652. if not stats:
  653. await context.bot.send_message(chat_id=chat_id, text="❌ Trading stats not available.")
  654. return
  655. await stats.record_deposit(
  656. amount=amount,
  657. description=f"Manual deposit via Telegram command"
  658. )
  659. # Get updated stats
  660. basic_stats = stats.get_basic_stats()
  661. formatter = get_formatter()
  662. message = f"""
  663. ✅ <b>Deposit Recorded</b>
  664. 💰 <b>Deposit Amount:</b> {await formatter.format_price_with_symbol(amount)}
  665. 📊 <b>Updated Stats:</b>
  666. • Effective Initial Balance: {await formatter.format_price_with_symbol(basic_stats['initial_balance'])}
  667. • Current P&L: {await formatter.format_price_with_symbol(basic_stats['total_pnl'])}
  668. • Total Return: {basic_stats['total_return_pct']:.2f}%
  669. 💡 Your P&L calculations are now updated with the deposit.
  670. """
  671. await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML')
  672. logger.info(f"Recorded deposit of ${amount:.2f} via Telegram command")
  673. except ValueError:
  674. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid amount. Please enter a valid number.")
  675. except Exception as e:
  676. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error recording deposit: {str(e)}")
  677. logger.error(f"Error in deposit command: {e}")
  678. async def withdrawal_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  679. """Handle the /withdrawal command to record a withdrawal."""
  680. if not self._is_authorized(update):
  681. await self._reply(update, "❌ Unauthorized access.")
  682. return
  683. chat_id = update.effective_chat.id
  684. try:
  685. # Parse arguments
  686. if not context.args or len(context.args) != 1:
  687. await context.bot.send_message(
  688. chat_id=chat_id,
  689. text="❌ Usage: /withdrawal <amount>\n\nExample: /withdrawal 200.00"
  690. )
  691. return
  692. amount = float(context.args[0])
  693. if amount <= 0:
  694. await context.bot.send_message(chat_id=chat_id, text="❌ Withdrawal amount must be positive.")
  695. return
  696. # Record the withdrawal
  697. stats = self.trading_engine.get_stats()
  698. if not stats:
  699. await context.bot.send_message(chat_id=chat_id, text="❌ Trading stats not available.")
  700. return
  701. await stats.record_withdrawal(
  702. amount=amount,
  703. description=f"Manual withdrawal via Telegram command"
  704. )
  705. # Get updated stats
  706. basic_stats = stats.get_basic_stats()
  707. formatter = get_formatter()
  708. message = f"""
  709. ✅ <b>Withdrawal Recorded</b>
  710. 💸 <b>Withdrawal Amount:</b> {await formatter.format_price_with_symbol(amount)}
  711. 📊 <b>Updated Stats:</b>
  712. • Effective Initial Balance: {await formatter.format_price_with_symbol(basic_stats['initial_balance'])}
  713. • Current P&L: {await formatter.format_price_with_symbol(basic_stats['total_pnl'])}
  714. • Total Return: {basic_stats['total_return_pct']:.2f}%
  715. 💡 Your P&L calculations are now updated with the withdrawal.
  716. """
  717. await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML')
  718. logger.info(f"Recorded withdrawal of ${amount:.2f} via Telegram command")
  719. except ValueError:
  720. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid amount. Please enter a valid number.")
  721. except Exception as e:
  722. await context.bot.send_message(chat_id=chat_id, text=f"❌ Error recording withdrawal: {str(e)}")
  723. logger.error(f"Error in withdrawal command: {e}")