|
@@ -494,47 +494,296 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
|
|
|
logger.error(f"Error in version command: {e}")
|
|
|
|
|
|
async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /keyboard command to enable/show custom keyboard."""
|
|
|
+ """Handle the /keyboard command to show the main keyboard."""
|
|
|
chat_id = update.effective_chat.id
|
|
|
if not self._is_authorized(chat_id):
|
|
|
await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
return
|
|
|
+
|
|
|
+ # Define default keyboard layout
|
|
|
+ default_keyboard = [
|
|
|
+ [KeyboardButton("LONG"), KeyboardButton("SHORT"), KeyboardButton("EXIT")],
|
|
|
+ [KeyboardButton("BALANCE"), KeyboardButton("POSITIONS"), KeyboardButton("ORDERS")],
|
|
|
+ [KeyboardButton("STATS"), KeyboardButton("MARKET"), KeyboardButton("PERFORMANCE")],
|
|
|
+ [KeyboardButton("DAILY"), KeyboardButton("WEEKLY"), KeyboardButton("MONTHLY")],
|
|
|
+ [KeyboardButton("RISK"), KeyboardButton("ALARM"), KeyboardButton("MONITORING")],
|
|
|
+ [KeyboardButton("LOGS"), KeyboardButton("DEBUG"), KeyboardButton("VERSION")],
|
|
|
+ [KeyboardButton("COMMANDS"), KeyboardButton("KEYBOARD"), KeyboardButton("COO")]
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Try to use custom keyboard from config if enabled
|
|
|
+ if Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED and Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT:
|
|
|
+ try:
|
|
|
+ # Parse layout from config: "cmd1,cmd2|cmd3,cmd4|cmd5,cmd6"
|
|
|
+ rows = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT.split('|')
|
|
|
+ keyboard = []
|
|
|
+ for row in rows:
|
|
|
+ buttons = []
|
|
|
+ for cmd in row.split(','):
|
|
|
+ cmd = cmd.strip()
|
|
|
+ # Remove leading slash if present and convert to button text
|
|
|
+ if cmd.startswith('/'):
|
|
|
+ cmd = cmd[1:]
|
|
|
+ buttons.append(KeyboardButton(cmd.upper()))
|
|
|
+ if buttons: # Only add non-empty rows
|
|
|
+ keyboard.append(buttons)
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"Error parsing custom keyboard layout: {e}, falling back to default")
|
|
|
+ keyboard = default_keyboard
|
|
|
+ else:
|
|
|
+ # Use default keyboard when custom keyboard is disabled
|
|
|
+ keyboard = default_keyboard
|
|
|
+
|
|
|
+ reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
|
|
|
|
|
|
- keyboard_enabled = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_ENABLED', False)
|
|
|
- keyboard_layout_str = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', '')
|
|
|
-
|
|
|
- if keyboard_enabled and keyboard_layout_str:
|
|
|
- rows_str = keyboard_layout_str.split('|')
|
|
|
- keyboard = []
|
|
|
- button_details_message = ""
|
|
|
- for row_str in rows_str:
|
|
|
- button_commands = row_str.split(',')
|
|
|
- row_buttons = []
|
|
|
- for cmd_text in button_commands:
|
|
|
- cmd_text = cmd_text.strip()
|
|
|
- if cmd_text: # Ensure not empty after strip
|
|
|
- row_buttons.append(KeyboardButton(cmd_text))
|
|
|
- # For the message, show the command and a brief description if possible
|
|
|
- # This part would require a mapping from command to description
|
|
|
- # For now, just list the command text.
|
|
|
- button_details_message += f"• {cmd_text}\n"
|
|
|
- if row_buttons:
|
|
|
- keyboard.append(row_buttons)
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="🎹 <b>Trading Bot Keyboard</b>\n\nUse the buttons below for quick access to commands:",
|
|
|
+ reply_markup=reply_markup,
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ async def sync_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /sync command to clear local data and resync with exchange."""
|
|
|
+ chat_id = update.effective_chat.id
|
|
|
+ if not self._is_authorized(chat_id):
|
|
|
+ await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Get confirmation from user unless force flag is provided
|
|
|
+ force_sync = len(context.args) > 0 and context.args[0].lower() == "force"
|
|
|
|
|
|
- if not keyboard:
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard layout is empty or invalid in config.", parse_mode='HTML')
|
|
|
+ if not force_sync:
|
|
|
+ # Send confirmation message
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=(
|
|
|
+ "⚠️ <b>Data Synchronization Warning</b>\n\n"
|
|
|
+ "This will:\n"
|
|
|
+ "• Clear ALL local trades, orders, and pending stop losses\n"
|
|
|
+ "• Reset bot tracking state\n"
|
|
|
+ "• Sync with current exchange positions/orders\n"
|
|
|
+ "• Preserve balance and performance history\n\n"
|
|
|
+ "🔄 Use <code>/sync force</code> to proceed\n"
|
|
|
+ "❌ Use any other command to cancel"
|
|
|
+ ),
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
return
|
|
|
|
|
|
- reply_markup = ReplyKeyboardMarkup(
|
|
|
- keyboard,
|
|
|
- resize_keyboard=True,
|
|
|
- one_time_keyboard=False, # Persistent keyboard
|
|
|
- selective=True
|
|
|
+ # Send processing message
|
|
|
+ processing_msg = await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="🔄 <b>Synchronizing with Exchange...</b>\n\n⏳ Step 1/5: Clearing local data...",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ # Step 1: Clear local trading data
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if stats:
|
|
|
+ # Clear trades table (keep only position_closed for history)
|
|
|
+ stats.db_manager._execute_query(
|
|
|
+ "DELETE FROM trades WHERE status IN ('pending', 'executed', 'position_opened', 'cancelled')"
|
|
|
+ )
|
|
|
+
|
|
|
+ # Clear orders table (keep only filled/cancelled for history)
|
|
|
+ stats.db_manager._execute_query(
|
|
|
+ "DELETE FROM orders WHERE status IN ('pending_submission', 'open', 'submitted', 'pending_trigger')"
|
|
|
+ )
|
|
|
+
|
|
|
+ logger.info("🧹 Cleared local trading state from database")
|
|
|
+
|
|
|
+ # Update status
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n⏳ Step 2/5: Clearing pending stop losses...",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ # Step 2: Clear pending stop loss orders
|
|
|
+ try:
|
|
|
+ if hasattr(self.monitoring_coordinator, 'pending_orders_manager'):
|
|
|
+ pending_manager = self.monitoring_coordinator.pending_orders_manager
|
|
|
+ if pending_manager and hasattr(pending_manager, 'db_path'):
|
|
|
+ import sqlite3
|
|
|
+ with sqlite3.connect(pending_manager.db_path) as conn:
|
|
|
+ conn.execute("DELETE FROM pending_stop_loss WHERE status IN ('pending', 'placed')")
|
|
|
+ conn.commit()
|
|
|
+ logger.info("🧹 Cleared pending stop loss orders")
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"Could not clear pending orders: {e}")
|
|
|
+
|
|
|
+ # Update status
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n⏳ Step 3/5: Fetching exchange state...",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ # Step 3: Fetch current exchange state
|
|
|
+ exchange_positions = self.trading_engine.get_positions() or []
|
|
|
+ exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
+ # Update status
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n⏳ Step 4/5: Recreating position tracking...",
|
|
|
+ parse_mode='HTML'
|
|
|
)
|
|
|
+
|
|
|
+ # Step 4: Recreate position tracking for open positions
|
|
|
+ positions_synced = 0
|
|
|
+ orders_synced = 0
|
|
|
+
|
|
|
+ if stats:
|
|
|
+ # Create trade lifecycles for open positions
|
|
|
+ for position in exchange_positions:
|
|
|
+ try:
|
|
|
+ size = float(position.get('contracts', 0))
|
|
|
+ if abs(size) > 1e-8: # Position exists
|
|
|
+ symbol = position.get('symbol', '')
|
|
|
+ if symbol:
|
|
|
+ # Extract token from symbol (e.g., "BTC/USDC:USDC" -> "BTC")
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
+
|
|
|
+ # Create a new trade lifecycle for this existing position
|
|
|
+ side = 'buy' if size > 0 else 'sell'
|
|
|
+ position_side = 'long' if size > 0 else 'short'
|
|
|
+ entry_price = float(position.get('entryPrice', 0))
|
|
|
+
|
|
|
+ if entry_price > 0: # Valid position data
|
|
|
+ # Create the lifecycle entry (this generates and returns lifecycle_id)
|
|
|
+ lifecycle_id = stats.create_trade_lifecycle(
|
|
|
+ symbol=symbol,
|
|
|
+ side=side,
|
|
|
+ stop_loss_price=None, # Will be detected from orders
|
|
|
+ trade_type='sync'
|
|
|
+ )
|
|
|
+
|
|
|
+ if lifecycle_id:
|
|
|
+ # Update to position opened status
|
|
|
+ await stats.update_trade_position_opened(
|
|
|
+ lifecycle_id=lifecycle_id,
|
|
|
+ entry_price=entry_price,
|
|
|
+ entry_amount=abs(size),
|
|
|
+ exchange_fill_id=f"sync_{lifecycle_id[:8]}"
|
|
|
+ )
|
|
|
+
|
|
|
+ # Update with current market data
|
|
|
+ unrealized_pnl = float(position.get('unrealizedPnl', 0))
|
|
|
+ mark_price = float(position.get('markPrice', entry_price))
|
|
|
+
|
|
|
+ stats.update_trade_market_data(
|
|
|
+ trade_lifecycle_id=lifecycle_id,
|
|
|
+ current_position_size=abs(size),
|
|
|
+ unrealized_pnl=unrealized_pnl,
|
|
|
+ mark_price=mark_price,
|
|
|
+ position_value=abs(size) * mark_price
|
|
|
+ )
|
|
|
+
|
|
|
+ positions_synced += 1
|
|
|
+ logger.info(f"🔄 Recreated position tracking for {token}: {position_side} {abs(size)} @ {entry_price}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error recreating position for {position}: {e}")
|
|
|
+
|
|
|
+ # Link existing orders to positions
|
|
|
+ for order in exchange_orders:
|
|
|
+ try:
|
|
|
+ order_symbol = order.get('symbol', '')
|
|
|
+ order_id = order.get('id', '')
|
|
|
+ order_type = order.get('type', '').lower()
|
|
|
+ is_reduce_only = order.get('reduceOnly', False)
|
|
|
+
|
|
|
+ if order_symbol and order_id and is_reduce_only:
|
|
|
+ # This might be a stop loss or take profit order
|
|
|
+ # Find the corresponding position
|
|
|
+ matching_trade = stats.get_trade_by_symbol_and_status(order_symbol, 'position_opened')
|
|
|
+ if matching_trade:
|
|
|
+ lifecycle_id = matching_trade.get('trade_lifecycle_id')
|
|
|
+ if lifecycle_id:
|
|
|
+ order_price = float(order.get('price', 0))
|
|
|
+ stop_price = float(order.get('stopPrice', 0)) or order_price
|
|
|
+
|
|
|
+ if 'stop' in order_type and stop_price > 0:
|
|
|
+ # Link as stop loss
|
|
|
+ await stats.link_stop_loss_to_trade(lifecycle_id, order_id, stop_price)
|
|
|
+ orders_synced += 1
|
|
|
+ logger.info(f"🔗 Linked stop loss order {order_id} to position {lifecycle_id[:8]}")
|
|
|
+ elif order_type in ['limit', 'take_profit'] and order_price > 0:
|
|
|
+ # Link as take profit
|
|
|
+ await stats.link_take_profit_to_trade(lifecycle_id, order_id, order_price)
|
|
|
+ orders_synced += 1
|
|
|
+ logger.info(f"🔗 Linked take profit order {order_id} to position {lifecycle_id[:8]}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error linking order {order}: {e}")
|
|
|
+
|
|
|
+ # Update status
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n✅ Step 4/5: Position tracking recreated\n⏳ Step 5/5: Updating balance...",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ # Step 5: Update current balance
|
|
|
+ current_balance = 0.0
|
|
|
+ try:
|
|
|
+ balance_data = self.trading_engine.get_balance()
|
|
|
+ if balance_data and balance_data.get('total'):
|
|
|
+ current_balance = float(balance_data['total'].get('USDC', 0))
|
|
|
+ if stats:
|
|
|
+ await stats.record_balance_snapshot(current_balance, notes="Post-sync balance")
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"Could not update balance after sync: {e}")
|
|
|
+
|
|
|
+ # Final success message
|
|
|
+ formatter = get_formatter()
|
|
|
+ success_message = f"""
|
|
|
+✅ <b>Synchronization Complete!</b>
|
|
|
+
|
|
|
+📊 <b>Sync Results:</b>
|
|
|
+• Positions Recreated: {positions_synced}
|
|
|
+• Orders Linked: {orders_synced}
|
|
|
+• Current Balance: {await formatter.format_price_with_symbol(current_balance)}
|
|
|
+
|
|
|
+🔄 <b>What was reset:</b>
|
|
|
+• Local trade tracking
|
|
|
+• Pending orders database
|
|
|
+• Order monitoring state
|
|
|
+
|
|
|
+🎯 <b>What was preserved:</b>
|
|
|
+• Historical performance data
|
|
|
+• Balance adjustment history
|
|
|
+• Completed trade statistics
|
|
|
+
|
|
|
+💡 The bot is now synchronized with your exchange state and ready for trading!
|
|
|
+ """
|
|
|
+
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text=success_message.strip(),
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ logger.info(f"🎉 Sync completed: {positions_synced} positions, {orders_synced} orders linked")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_message = f"❌ <b>Sync Failed</b>\n\nError: {str(e)}\n\n💡 Check logs for details."
|
|
|
+ try:
|
|
|
+ await context.bot.edit_message_text(
|
|
|
+ chat_id=chat_id,
|
|
|
+ message_id=processing_msg.message_id,
|
|
|
+ text=error_message,
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+ except:
|
|
|
+ await context.bot.send_message(chat_id=chat_id, text=error_message, parse_mode='HTML')
|
|
|
|
|
|
- message_text = f"⌨️ <b>Custom Keyboard Activated!</b>\n\n🎯 <b>Your quick buttons based on config are ready:</b>\n{button_details_message}\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."
|
|
|
- await context.bot.send_message(chat_id=chat_id, text=message_text, reply_markup=reply_markup, parse_mode='HTML')
|
|
|
- elif not keyboard_enabled:
|
|
|
- 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}", parse_mode='HTML')
|
|
|
- else: # keyboard_enabled is true, but layout_str is empty
|
|
|
- await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard is enabled but the layout string (TELEGRAM_CUSTOM_KEYBOARD_LAYOUT) is empty in your configuration. Please define a layout.", parse_mode='HTML')
|
|
|
+ logger.error(f"Error in sync command: {e}", exc_info=True)
|