|
@@ -0,0 +1,334 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""
|
|
|
+Copy Trading Commands - Handles copy trading related Telegram commands.
|
|
|
+"""
|
|
|
+
|
|
|
+import logging
|
|
|
+from typing import Optional
|
|
|
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
+from telegram.ext import ContextTypes
|
|
|
+
|
|
|
+from ..config.config import Config
|
|
|
+from ..monitoring.monitoring_coordinator import MonitoringCoordinator
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+class CopyTradingCommands:
|
|
|
+ """Handles copy trading related Telegram commands."""
|
|
|
+
|
|
|
+ def __init__(self, monitoring_coordinator: MonitoringCoordinator):
|
|
|
+ """Initialize copy trading commands."""
|
|
|
+ self.monitoring_coordinator = monitoring_coordinator
|
|
|
+
|
|
|
+ def _is_authorized(self, chat_id: str) -> bool:
|
|
|
+ """Check if the chat ID is authorized."""
|
|
|
+ return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
|
|
|
+
|
|
|
+ async def copy_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /copy_status command."""
|
|
|
+ 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:
|
|
|
+ copy_monitor = self.monitoring_coordinator.copy_trading_monitor
|
|
|
+
|
|
|
+ if not copy_monitor:
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="❌ Copy trading monitor not available.",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ status = copy_monitor.get_status()
|
|
|
+
|
|
|
+ # Format status message
|
|
|
+ status_emoji = "🟢" if status['enabled'] else "🔴"
|
|
|
+ status_text = "ACTIVE" if status['enabled'] else "STOPPED"
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+🔄 <b>Copy Trading Status: {status_emoji} {status_text}</b>
|
|
|
+
|
|
|
+📊 <b>Configuration:</b>
|
|
|
+• Target Address: <code>{status['target_address'][:10] + '...' if status['target_address'] else 'Not Set'}</code>
|
|
|
+• Portfolio Allocation: <b>{status['portfolio_percentage']:.1%}</b>
|
|
|
+• Copy Mode: <b>{status['copy_mode']}</b>
|
|
|
+• Max Leverage: <b>{status['max_leverage']}x</b>
|
|
|
+
|
|
|
+📈 <b>Current State:</b>
|
|
|
+• Target Positions: <b>{status['target_positions']}</b>
|
|
|
+• Our Positions: <b>{status['our_positions']}</b>
|
|
|
+• Tracked Positions: <b>{status['tracked_positions']}</b>
|
|
|
+• Copied Trades: <b>{status['copied_trades']}</b>
|
|
|
+
|
|
|
+⏰ <b>Session Info:</b>
|
|
|
+• Start Time: {status['session_start_time'].strftime('%Y-%m-%d %H:%M:%S') if status['session_start_time'] else 'N/A'}
|
|
|
+• Duration: {f"{status['session_duration_hours']:.1f} hours" if status['session_duration_hours'] else 'N/A'}
|
|
|
+• Last Check: {status['last_check'].strftime('%H:%M:%S') if status['last_check'] else 'N/A'}
|
|
|
+ """
|
|
|
+
|
|
|
+ # Add control buttons
|
|
|
+ keyboard = []
|
|
|
+ if status['enabled']:
|
|
|
+ keyboard.append([InlineKeyboardButton("🛑 Stop Copy Trading", callback_data="copy_stop")])
|
|
|
+ else:
|
|
|
+ keyboard.append([InlineKeyboardButton("🚀 Start Copy Trading", callback_data="copy_start_prompt")])
|
|
|
+
|
|
|
+ keyboard.append([InlineKeyboardButton("🔄 Refresh Status", callback_data="copy_status")])
|
|
|
+
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
+
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=message,
|
|
|
+ parse_mode='HTML',
|
|
|
+ reply_markup=reply_markup
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in copy_status command: {e}")
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=f"❌ Error retrieving copy trading status: {e}"
|
|
|
+ )
|
|
|
+
|
|
|
+ async def copy_start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /copy_start command."""
|
|
|
+ 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:
|
|
|
+ copy_monitor = self.monitoring_coordinator.copy_trading_monitor
|
|
|
+
|
|
|
+ if not copy_monitor:
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="❌ Copy trading monitor not available."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Check if already running
|
|
|
+ if copy_monitor.enabled and copy_monitor.state_manager.is_enabled():
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="⚠️ Copy trading is already running. Use /copy_stop to stop it first."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Check if target address is provided
|
|
|
+ if context.args and len(context.args) > 0:
|
|
|
+ target_address = context.args[0]
|
|
|
+
|
|
|
+ # Validate Ethereum address format
|
|
|
+ if not target_address.startswith('0x') or len(target_address) != 42:
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="❌ Invalid Ethereum address format. Address must start with '0x' and be 42 characters long."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Update target address
|
|
|
+ copy_monitor.target_address = target_address
|
|
|
+ copy_monitor.config.COPY_TRADING_TARGET_ADDRESS = target_address
|
|
|
+
|
|
|
+ # Check if target address is set
|
|
|
+ if not copy_monitor.target_address:
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=(
|
|
|
+ "❌ No target address configured.\n\n"
|
|
|
+ "Usage: /copy_start [target_address]\n"
|
|
|
+ "Example: /copy_start 0x1234567890abcdef1234567890abcdef12345678"
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Create confirmation message
|
|
|
+ message = f"""
|
|
|
+🚀 <b>Start Copy Trading Confirmation</b>
|
|
|
+
|
|
|
+🎯 <b>Target Trader:</b> <code>{copy_monitor.target_address}</code>
|
|
|
+💰 <b>Portfolio Allocation:</b> {copy_monitor.portfolio_percentage:.1%}
|
|
|
+📊 <b>Copy Mode:</b> {copy_monitor.copy_mode}
|
|
|
+⚡ <b>Max Leverage:</b> {copy_monitor.max_leverage}x
|
|
|
+💵 <b>Min Position Size:</b> ${copy_monitor.min_position_size}
|
|
|
+⏱️ <b>Execution Delay:</b> {copy_monitor.execution_delay}s
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to start copy trading?</b>
|
|
|
+
|
|
|
+This will:
|
|
|
+• Monitor the target trader's positions
|
|
|
+• Automatically copy NEW trades (existing positions ignored)
|
|
|
+• Allocate {copy_monitor.portfolio_percentage:.1%} of your portfolio per trade
|
|
|
+• Send notifications for each copied trade
|
|
|
+ """
|
|
|
+
|
|
|
+ keyboard = [
|
|
|
+ [
|
|
|
+ InlineKeyboardButton("✅ Start Copy Trading", callback_data=f"copy_start_confirm_{copy_monitor.target_address}"),
|
|
|
+ InlineKeyboardButton("❌ Cancel", callback_data="copy_cancel")
|
|
|
+ ]
|
|
|
+ ]
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
+
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=message,
|
|
|
+ parse_mode='HTML',
|
|
|
+ reply_markup=reply_markup
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in copy_start command: {e}")
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=f"❌ Error starting copy trading: {e}"
|
|
|
+ )
|
|
|
+
|
|
|
+ async def copy_stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle the /copy_stop command."""
|
|
|
+ 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:
|
|
|
+ copy_monitor = self.monitoring_coordinator.copy_trading_monitor
|
|
|
+
|
|
|
+ if not copy_monitor:
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="❌ Copy trading monitor not available."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Check if already stopped
|
|
|
+ if not copy_monitor.enabled or not copy_monitor.state_manager.is_enabled():
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text="⚠️ Copy trading is already stopped."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ # Get session info before stopping
|
|
|
+ session_info = copy_monitor.state_manager.get_session_info()
|
|
|
+
|
|
|
+ # Create confirmation message
|
|
|
+ message = f"""
|
|
|
+🛑 <b>Stop Copy Trading Confirmation</b>
|
|
|
+
|
|
|
+🎯 <b>Current Target:</b> <code>{copy_monitor.target_address[:10]}...</code>
|
|
|
+📊 <b>Session Stats:</b>
|
|
|
+• Tracked Positions: {session_info['tracked_positions_count']}
|
|
|
+• Copied Trades: {session_info['copied_trades_count']}
|
|
|
+• Duration: {f"{session_info['session_duration_seconds'] / 3600:.1f} hours" if session_info['session_duration_seconds'] else 'N/A'}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to stop copy trading?</b>
|
|
|
+
|
|
|
+This will:
|
|
|
+• Stop monitoring the target trader
|
|
|
+• Preserve session data for later resumption
|
|
|
+• Keep existing positions (won't auto-close)
|
|
|
+• You can restart later with /copy_start
|
|
|
+ """
|
|
|
+
|
|
|
+ keyboard = [
|
|
|
+ [
|
|
|
+ InlineKeyboardButton("✅ Stop Copy Trading", callback_data="copy_stop_confirm"),
|
|
|
+ InlineKeyboardButton("❌ Keep Running", callback_data="copy_cancel")
|
|
|
+ ]
|
|
|
+ ]
|
|
|
+ reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
+
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=message,
|
|
|
+ parse_mode='HTML',
|
|
|
+ reply_markup=reply_markup
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in copy_stop command: {e}")
|
|
|
+ await context.bot.send_message(
|
|
|
+ chat_id=chat_id,
|
|
|
+ text=f"❌ Error stopping copy trading: {e}"
|
|
|
+ )
|
|
|
+
|
|
|
+ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
+ """Handle button callbacks for copy trading commands."""
|
|
|
+ query = update.callback_query
|
|
|
+ await query.answer()
|
|
|
+
|
|
|
+ callback_data = query.data
|
|
|
+
|
|
|
+ try:
|
|
|
+ copy_monitor = self.monitoring_coordinator.copy_trading_monitor
|
|
|
+
|
|
|
+ if callback_data == "copy_status":
|
|
|
+ # Refresh status
|
|
|
+ await self.copy_status_command(update, context)
|
|
|
+
|
|
|
+ elif callback_data == "copy_start_prompt":
|
|
|
+ await query.edit_message_text(
|
|
|
+ text=(
|
|
|
+ "🚀 <b>Start Copy Trading</b>\n\n"
|
|
|
+ "Please provide the target trader's address:\n\n"
|
|
|
+ "Usage: /copy_start [target_address]\n"
|
|
|
+ "Example: /copy_start 0x1234567890abcdef1234567890abcdef12345678"
|
|
|
+ ),
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+
|
|
|
+ elif callback_data.startswith("copy_start_confirm_"):
|
|
|
+ target_address = callback_data.replace("copy_start_confirm_", "")
|
|
|
+
|
|
|
+ await query.edit_message_text("🚀 Starting copy trading...")
|
|
|
+
|
|
|
+ if copy_monitor:
|
|
|
+ # Enable copy trading
|
|
|
+ copy_monitor.enabled = True
|
|
|
+ copy_monitor.target_address = target_address
|
|
|
+
|
|
|
+ # Start monitoring in background
|
|
|
+ import asyncio
|
|
|
+ asyncio.create_task(copy_monitor.start_monitoring())
|
|
|
+
|
|
|
+ await query.edit_message_text(
|
|
|
+ f"✅ Copy trading started!\n\n"
|
|
|
+ f"🎯 Target: {target_address[:10]}...\n"
|
|
|
+ f"📊 Monitoring active positions and new trades\n\n"
|
|
|
+ f"Use /copy_status to check progress.",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ await query.edit_message_text("❌ Copy trading monitor not available.")
|
|
|
+
|
|
|
+ elif callback_data == "copy_stop_confirm":
|
|
|
+ await query.edit_message_text("🛑 Stopping copy trading...")
|
|
|
+
|
|
|
+ if copy_monitor:
|
|
|
+ await copy_monitor.stop_monitoring()
|
|
|
+
|
|
|
+ await query.edit_message_text(
|
|
|
+ "✅ Copy trading stopped!\n\n"
|
|
|
+ "📊 Session data preserved\n"
|
|
|
+ "🔄 Use /copy_start to resume later",
|
|
|
+ parse_mode='HTML'
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ await query.edit_message_text("❌ Copy trading monitor not available.")
|
|
|
+
|
|
|
+ elif callback_data == "copy_stop":
|
|
|
+ # Show stop confirmation
|
|
|
+ await self.copy_stop_command(update, context)
|
|
|
+
|
|
|
+ elif callback_data == "copy_cancel":
|
|
|
+ await query.edit_message_text("❌ Copy trading action cancelled.")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error in copy trading button callback: {e}")
|
|
|
+ await query.edit_message_text(f"❌ Error: {e}")
|