|
@@ -1,4637 +0,0 @@
|
|
|
-#!/usr/bin/env python3
|
|
|
-"""
|
|
|
-Telegram Bot for Hyperliquid Trading
|
|
|
-
|
|
|
-This module provides a Telegram interface for manual Hyperliquid trading
|
|
|
-with comprehensive statistics tracking and phone-friendly controls.
|
|
|
-"""
|
|
|
-
|
|
|
-import asyncio
|
|
|
-import json
|
|
|
-import os
|
|
|
-from datetime import datetime, timedelta
|
|
|
-from typing import Optional, Dict, Any
|
|
|
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
|
|
|
-from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
|
|
|
-from hyperliquid_client import HyperliquidClient
|
|
|
-from src.stats import TradingStats
|
|
|
-from config import Config
|
|
|
-from alarm_manager import AlarmManager
|
|
|
-from logging_config import setup_logging, cleanup_logs, format_log_stats
|
|
|
-
|
|
|
-# Set up logging using the new configuration system
|
|
|
-logger = setup_logging().getChild(__name__)
|
|
|
-
|
|
|
-class TelegramTradingBot:
|
|
|
- """Telegram trading bot for manual trading operations."""
|
|
|
-
|
|
|
- def __init__(self):
|
|
|
- """Initialize the Telegram trading bot."""
|
|
|
- self.client = HyperliquidClient()
|
|
|
- self.application = None
|
|
|
- self.order_monitoring_task = None
|
|
|
- self.last_filled_orders = set()
|
|
|
- self.alarms = [] # List to store price alarms
|
|
|
- self.bot_heartbeat_seconds = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10)
|
|
|
- self.external_trade_timestamps = set() # Track external trade timestamps to avoid duplicates
|
|
|
- self.last_position_check = {} # Track last position state for comparison
|
|
|
- self.position_tracker = {} # For enhanced position tracking
|
|
|
- self.stats = None
|
|
|
- self.version = "Unknown" # Will be set by launcher
|
|
|
-
|
|
|
- # Bot state persistence file
|
|
|
- self.bot_state_file = "bot_state.json"
|
|
|
-
|
|
|
- # Order monitoring attributes
|
|
|
- self.monitoring_active = False
|
|
|
- self.last_known_orders = set() # Track order IDs we've seen
|
|
|
- self.last_known_positions = {} # Track position sizes for P&L calculation
|
|
|
-
|
|
|
- # External trade monitoring
|
|
|
- self.last_processed_trade_time = None # Track last processed external trade
|
|
|
-
|
|
|
- # Deposit/Withdrawal monitoring
|
|
|
- self.last_deposit_withdrawal_check = None # Track last deposit/withdrawal check
|
|
|
- self.deposit_withdrawal_check_interval = 3600 # Check every hour (3600 seconds)
|
|
|
-
|
|
|
- # Alarm management
|
|
|
- self.alarm_manager = AlarmManager()
|
|
|
-
|
|
|
- # Pending stop loss storage
|
|
|
- self.pending_stop_losses = {} # Format: {order_id: {'token': str, 'stop_price': float, 'side': str, 'amount': float, 'order_type': str}}
|
|
|
-
|
|
|
- # Track bot-generated trades to avoid double processing
|
|
|
- self.bot_trade_ids = set() # Track trade IDs generated by bot commands
|
|
|
-
|
|
|
- # Load bot state first, then initialize stats
|
|
|
- self._load_bot_state()
|
|
|
- self._initialize_stats()
|
|
|
-
|
|
|
- def _load_bot_state(self):
|
|
|
- """Load bot state from disk."""
|
|
|
- try:
|
|
|
- if os.path.exists(self.bot_state_file):
|
|
|
- with open(self.bot_state_file, 'r') as f:
|
|
|
- state_data = json.load(f)
|
|
|
-
|
|
|
- # Restore critical state
|
|
|
- self.pending_stop_losses = state_data.get('pending_stop_losses', {})
|
|
|
- self.last_known_orders = set(state_data.get('last_known_orders', []))
|
|
|
- self.last_known_positions = state_data.get('last_known_positions', {})
|
|
|
-
|
|
|
- # Restore bot trade IDs (prevent double processing)
|
|
|
- self.bot_trade_ids = set(state_data.get('bot_trade_ids', []))
|
|
|
-
|
|
|
- # Restore timestamps (convert from ISO string if present)
|
|
|
- last_trade_time = state_data.get('last_processed_trade_time')
|
|
|
- if last_trade_time:
|
|
|
- try:
|
|
|
- self.last_processed_trade_time = datetime.fromisoformat(last_trade_time)
|
|
|
- except (ValueError, TypeError):
|
|
|
- self.last_processed_trade_time = None
|
|
|
-
|
|
|
- last_deposit_check = state_data.get('last_deposit_withdrawal_check')
|
|
|
- if last_deposit_check:
|
|
|
- try:
|
|
|
- self.last_deposit_withdrawal_check = datetime.fromisoformat(last_deposit_check)
|
|
|
- except (ValueError, TypeError):
|
|
|
- self.last_deposit_withdrawal_check = None
|
|
|
-
|
|
|
- logger.info(f"🔄 Restored bot state: {len(self.pending_stop_losses)} pending stop losses, {len(self.last_known_orders)} tracked orders")
|
|
|
-
|
|
|
- # Log details about restored pending stop losses
|
|
|
- if self.pending_stop_losses:
|
|
|
- for order_id, stop_loss_info in self.pending_stop_losses.items():
|
|
|
- token = stop_loss_info.get('token', 'Unknown')
|
|
|
- stop_price = stop_loss_info.get('stop_price', 0)
|
|
|
- order_type = stop_loss_info.get('order_type', 'Unknown')
|
|
|
- logger.info(f"📋 Restored pending stop loss: {order_id} -> {token} {order_type} @ ${stop_price}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error loading bot state: {e}")
|
|
|
- # Initialize with defaults
|
|
|
- self.pending_stop_losses = {}
|
|
|
- self.last_known_orders = set()
|
|
|
- self.last_known_positions = {}
|
|
|
- self.bot_trade_ids = set()
|
|
|
- self.last_processed_trade_time = None
|
|
|
- self.last_deposit_withdrawal_check = None
|
|
|
-
|
|
|
- def _save_bot_state(self):
|
|
|
- """Save bot state to disk."""
|
|
|
- try:
|
|
|
- # Helper function to safely convert datetime to ISO string
|
|
|
- def safe_datetime_to_iso(dt):
|
|
|
- if dt is None:
|
|
|
- return None
|
|
|
- try:
|
|
|
- # Handle both datetime objects and strings
|
|
|
- if isinstance(dt, str):
|
|
|
- # If it's already a string, validate it's a valid ISO format
|
|
|
- datetime.fromisoformat(dt.replace('Z', '+00:00'))
|
|
|
- return dt
|
|
|
- else:
|
|
|
- # Convert datetime object to ISO string
|
|
|
- return dt.isoformat()
|
|
|
- except (ValueError, AttributeError) as e:
|
|
|
- logger.warning(f"⚠️ Invalid datetime value, using None: {dt} - {e}")
|
|
|
- return None
|
|
|
-
|
|
|
- state_data = {
|
|
|
- 'pending_stop_losses': self.pending_stop_losses,
|
|
|
- 'last_known_orders': list(self.last_known_orders), # Convert set to list for JSON
|
|
|
- 'last_known_positions': self.last_known_positions,
|
|
|
- 'bot_trade_ids': list(self.bot_trade_ids), # Track bot-generated trades
|
|
|
- 'last_processed_trade_time': safe_datetime_to_iso(self.last_processed_trade_time),
|
|
|
- 'last_deposit_withdrawal_check': safe_datetime_to_iso(self.last_deposit_withdrawal_check),
|
|
|
- 'last_updated': datetime.now().isoformat(),
|
|
|
- 'version': self.version
|
|
|
- }
|
|
|
-
|
|
|
- with open(self.bot_state_file, 'w') as f:
|
|
|
- json.dump(state_data, f, indent=2, default=str)
|
|
|
-
|
|
|
- logger.debug(f"💾 Saved bot state: {len(self.pending_stop_losses)} pending stop losses")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error saving bot state: {e}")
|
|
|
-
|
|
|
- def _initialize_stats(self):
|
|
|
- """Initialize stats with current balance."""
|
|
|
- try:
|
|
|
- # Initialize TradingStats object first
|
|
|
- self.stats = TradingStats()
|
|
|
-
|
|
|
- # Get current balance and set it as initial balance
|
|
|
- balance = self.client.get_balance()
|
|
|
- if balance and balance.get('total'):
|
|
|
- # Get USDC balance as the main balance
|
|
|
- usdc_balance = float(balance['total'].get('USDC', 0))
|
|
|
- self.stats.set_initial_balance(usdc_balance)
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Could not initialize stats: {e}")
|
|
|
-
|
|
|
- def is_authorized(self, chat_id: str) -> bool:
|
|
|
- """Check if the chat ID is authorized to use the bot."""
|
|
|
- return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
|
|
|
-
|
|
|
- async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
|
|
|
- """Send a message to the authorized chat."""
|
|
|
- if self.application and Config.TELEGRAM_CHAT_ID:
|
|
|
- try:
|
|
|
- await self.application.bot.send_message(
|
|
|
- chat_id=Config.TELEGRAM_CHAT_ID,
|
|
|
- text=text,
|
|
|
- parse_mode=parse_mode
|
|
|
- )
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Failed to send message: {e}")
|
|
|
-
|
|
|
- def _create_custom_keyboard(self) -> Optional[ReplyKeyboardMarkup]:
|
|
|
- """Create a custom keyboard from configuration."""
|
|
|
- if not Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED:
|
|
|
- return None
|
|
|
-
|
|
|
- try:
|
|
|
- layout = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT
|
|
|
- # Parse the layout: "cmd1,cmd2,cmd3|cmd4,cmd5|cmd6,cmd7,cmd8,cmd9"
|
|
|
- rows = layout.split('|')
|
|
|
- keyboard = []
|
|
|
-
|
|
|
- for row in rows:
|
|
|
- commands = [cmd.strip() for cmd in row.split(',') if cmd.strip()]
|
|
|
- if commands:
|
|
|
- keyboard.append([KeyboardButton(cmd.lstrip('/').capitalize()) for cmd in commands])
|
|
|
-
|
|
|
- if keyboard:
|
|
|
- return ReplyKeyboardMarkup(
|
|
|
- keyboard,
|
|
|
- resize_keyboard=True, # Resize to fit screen
|
|
|
- one_time_keyboard=False, # Keep keyboard persistent
|
|
|
- selective=True # Show only to authorized users
|
|
|
- )
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Failed to create custom keyboard: {e}")
|
|
|
-
|
|
|
- return None
|
|
|
-
|
|
|
- async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /start command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- welcome_text = """
|
|
|
-🤖 <b>Welcome to Hyperliquid Trading Bot</b>
|
|
|
-
|
|
|
-📱 <b>Quick Actions:</b>
|
|
|
-• Trading: /long BTC 100 or /short ETH 50
|
|
|
-• Exit: /exit BTC (closes position)
|
|
|
-• Info: /balance, /positions, /orders
|
|
|
-
|
|
|
-📊 <b>Market Data:</b>
|
|
|
-• /market - Detailed market overview
|
|
|
-• /price - Quick price check
|
|
|
-
|
|
|
-<b>⚡ Quick Commands:</b>
|
|
|
-• /balance - Account balance
|
|
|
-• /positions - Open positions
|
|
|
-• /orders - Active orders
|
|
|
-• /market - Market data & prices
|
|
|
-
|
|
|
-<b>🚀 Trading:</b>
|
|
|
-• /long BTC 100 - Long position
|
|
|
-• /long BTC 100 45000 - Limit order
|
|
|
-• /long BTC 100 sl:44000 - With stop loss
|
|
|
-• /short ETH 50 - Short position
|
|
|
-• /short ETH 50 3500 sl:3600 - With stop loss
|
|
|
-• /exit BTC - Close position
|
|
|
-• /coo BTC - Cancel open orders
|
|
|
-
|
|
|
-<b>🛡️ Risk Management:</b>
|
|
|
-• Enabled: {risk_enabled}
|
|
|
-• Auto Stop Loss: {stop_loss}%
|
|
|
-• Order Stop Loss: Use sl:price parameter
|
|
|
-• /sl BTC 44000 - Manual stop loss
|
|
|
-• /tp BTC 50000 - Take profit order
|
|
|
-
|
|
|
-<b>📈 Performance & Analytics:</b>
|
|
|
-• /stats - Complete trading statistics
|
|
|
-• /performance - Token performance ranking & detailed stats
|
|
|
-• /daily - Daily performance (last 10 days)
|
|
|
-• /weekly - Weekly performance (last 10 weeks)
|
|
|
-• /monthly - Monthly performance (last 10 months)
|
|
|
-• /risk - Sharpe ratio, drawdown, VaR
|
|
|
-• /version - Bot version & system information
|
|
|
-• /trades - Recent trade history
|
|
|
-
|
|
|
-<b>🔔 Price Alerts:</b>
|
|
|
-• /alarm - List all active alarms
|
|
|
-• /alarm BTC 50000 - Set alarm for BTC at $50,000
|
|
|
-• /alarm BTC - Show all BTC alarms
|
|
|
-• /alarm 3 - Remove alarm ID 3
|
|
|
-
|
|
|
-<b>🔄 Automatic Monitoring:</b>
|
|
|
-• Real-time order fill alerts
|
|
|
-• Position opened/closed notifications
|
|
|
-• P&L calculations on trade closure
|
|
|
-• Price alarm triggers
|
|
|
-• External trade detection & sync
|
|
|
-• Auto stats synchronization
|
|
|
-• Automatic stop loss placement
|
|
|
-• {heartbeat}-second monitoring interval
|
|
|
-
|
|
|
-<b>📊 Universal Trade Tracking:</b>
|
|
|
-• Bot trades: Full logging & notifications
|
|
|
-• Platform trades: Auto-detected & synced
|
|
|
-• Mobile app trades: Monitored & recorded
|
|
|
-• API trades: Tracked & included in stats
|
|
|
-
|
|
|
-Type /help for detailed command information.
|
|
|
-
|
|
|
-<b>🔄 Order Monitoring:</b>
|
|
|
-• /monitoring - View monitoring status
|
|
|
-• /logs - View log file statistics and cleanup
|
|
|
-
|
|
|
-<b>⚙️ Configuration:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Default Token: {symbol}
|
|
|
-• Network: {network}
|
|
|
-
|
|
|
-<b>🛡️ Safety Features:</b>
|
|
|
-• All trades logged automatically
|
|
|
-• Comprehensive performance tracking
|
|
|
-• Real-time balance monitoring
|
|
|
-• Risk metrics calculation
|
|
|
-• Automatic stop loss protection
|
|
|
-
|
|
|
-<b>📱 Mobile Optimized:</b>
|
|
|
-• Quick action buttons
|
|
|
-• Instant notifications
|
|
|
-• Clean, readable layout
|
|
|
-• One-tap commands
|
|
|
-
|
|
|
-<b>💡 Quick Access:</b>
|
|
|
-• /commands or /c - One-tap button menu for all commands
|
|
|
-• Buttons below for instant access to key functions
|
|
|
-
|
|
|
-For support, contact your bot administrator.
|
|
|
- """.format(
|
|
|
- symbol=Config.DEFAULT_TRADING_TOKEN,
|
|
|
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
|
|
|
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
|
|
|
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
|
|
|
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
|
|
|
- )
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("💰 Balance", callback_data="balance"),
|
|
|
- InlineKeyboardButton("📊 Stats", callback_data="stats")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("📈 Positions", callback_data="positions"),
|
|
|
- InlineKeyboardButton("📋 Orders", callback_data="orders")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("💵 Price", callback_data="price"),
|
|
|
- InlineKeyboardButton("📊 Market", callback_data="market")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("🔄 Recent Trades", callback_data="trades"),
|
|
|
- InlineKeyboardButton("⚙️ Help", callback_data="help")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- # Create custom keyboard for persistent buttons
|
|
|
- custom_keyboard = self._create_custom_keyboard()
|
|
|
-
|
|
|
- # Send message with inline keyboard
|
|
|
- await update.message.reply_text(
|
|
|
- welcome_text,
|
|
|
- parse_mode='HTML',
|
|
|
- reply_markup=reply_markup
|
|
|
- )
|
|
|
-
|
|
|
- # If custom keyboard is enabled, send a follow-up message to set the custom keyboard
|
|
|
- if custom_keyboard:
|
|
|
- await update.message.reply_text(
|
|
|
- "⌨️ <b>Custom keyboard enabled!</b>\n\nUse the buttons below for quick access to commands:",
|
|
|
- parse_mode='HTML',
|
|
|
- reply_markup=custom_keyboard
|
|
|
- )
|
|
|
-
|
|
|
- async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /help command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- help_text = """
|
|
|
-🔧 <b>Hyperliquid Trading Bot - Complete Guide</b>
|
|
|
-
|
|
|
-<b>💼 Account Management:</b>
|
|
|
-• /balance - Show account balance
|
|
|
-• /positions - Show open positions
|
|
|
-• /orders - Show open orders
|
|
|
-• /balance_adjustments - View deposit/withdrawal history
|
|
|
-
|
|
|
-<b>📊 Market Data:</b>
|
|
|
-• /market - Detailed market data (default token)
|
|
|
-• /market BTC - Market data for specific token
|
|
|
-• /price - Quick price check (default token)
|
|
|
-• /price SOL - Price for specific token
|
|
|
-
|
|
|
-<b>🚀 Perps Trading:</b>
|
|
|
-• /long BTC 100 - Long BTC with $100 USDC (Market Order)
|
|
|
-• /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
|
|
|
-• /long BTC 100 sl:44000 - Market order with automatic stop loss
|
|
|
-• /long BTC 100 45000 sl:44000 - Limit order with automatic stop loss
|
|
|
-• /short ETH 50 - Short ETH with $50 USDC (Market Order)
|
|
|
-• /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
|
|
|
-• /short ETH 50 sl:3600 - Market order with automatic stop loss
|
|
|
-• /short ETH 50 3500 sl:3600 - Limit order with automatic stop loss
|
|
|
-• /exit BTC - Close BTC position with Market Order
|
|
|
-
|
|
|
-<b>🛡️ Risk Management:</b>
|
|
|
-• /sl BTC 44000 - Set stop loss for BTC at $44,000
|
|
|
-• /tp BTC 50000 - Set take profit for BTC at $50,000
|
|
|
-
|
|
|
-<b>🚨 Automatic Stop Loss:</b>
|
|
|
-• Enabled: {risk_enabled}
|
|
|
-• Stop Loss: {stop_loss}% (automatic execution)
|
|
|
-• Monitoring: Every {heartbeat} seconds
|
|
|
-• Order-based: Use sl:price parameter for automatic placement
|
|
|
-
|
|
|
-<b>📋 Order Management:</b>
|
|
|
-• /orders - Show all open orders
|
|
|
-• /orders BTC - Show open orders for BTC only
|
|
|
-• /coo BTC - Cancel all open orders for BTC
|
|
|
-
|
|
|
-<b>📈 Statistics & Analytics:</b>
|
|
|
-• /stats - Complete trading statistics
|
|
|
-• /performance - Win rate, profit factor, etc.
|
|
|
-• /risk - Sharpe ratio, drawdown, VaR
|
|
|
-• /version - Bot version & system information
|
|
|
-• /trades - Recent trade history
|
|
|
-
|
|
|
-<b>🔔 Price Alerts:</b>
|
|
|
-• /alarm - List all active alarms
|
|
|
-• /alarm BTC 50000 - Set alarm for BTC at $50,000
|
|
|
-• /alarm BTC - Show all BTC alarms
|
|
|
-• /alarm 3 - Remove alarm ID 3
|
|
|
-
|
|
|
-<b>🔄 Order Monitoring:</b>
|
|
|
-• /monitoring - View monitoring status
|
|
|
-• /logs - View log file statistics and cleanup
|
|
|
-
|
|
|
-<b>⚙️ Configuration:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Default Token: {symbol}
|
|
|
-• Network: {network}
|
|
|
-
|
|
|
-<b>🛡️ Safety Features:</b>
|
|
|
-• All trades logged automatically
|
|
|
-• Comprehensive performance tracking
|
|
|
-• Real-time balance monitoring
|
|
|
-• Deposit/withdrawal tracking (hourly)
|
|
|
-• Risk metrics calculation
|
|
|
-• Automatic stop loss placement
|
|
|
-
|
|
|
-<b>📱 Mobile Optimized:</b>
|
|
|
-• Quick action buttons
|
|
|
-• Instant notifications
|
|
|
-• Clean, readable layout
|
|
|
-• One-tap commands
|
|
|
-
|
|
|
-For support, contact your bot administrator.
|
|
|
- """.format(
|
|
|
- symbol=Config.DEFAULT_TRADING_TOKEN,
|
|
|
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
|
|
|
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
|
|
|
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
|
|
|
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
|
|
|
- )
|
|
|
-
|
|
|
- await update.message.reply_text(help_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /commands and /c command with quick action buttons."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- commands_text = """
|
|
|
-📱 <b>Quick Commands</b>
|
|
|
-
|
|
|
-Tap any button below for instant access to bot functions:
|
|
|
-
|
|
|
-💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("💰 Balance", callback_data="balance"),
|
|
|
- InlineKeyboardButton("📈 Positions", callback_data="positions")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("📋 Orders", callback_data="orders"),
|
|
|
- InlineKeyboardButton("📊 Stats", callback_data="stats")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("💵 Price", callback_data="price"),
|
|
|
- InlineKeyboardButton("📊 Market", callback_data="market")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("🏆 Performance", callback_data="performance"),
|
|
|
- InlineKeyboardButton("🔔 Alarms", callback_data="alarm")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("📅 Daily", callback_data="daily"),
|
|
|
- InlineKeyboardButton("📊 Weekly", callback_data="weekly")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("📆 Monthly", callback_data="monthly"),
|
|
|
- InlineKeyboardButton("🔄 Trades", callback_data="trades")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("🔄 Monitoring", callback_data="monitoring"),
|
|
|
- InlineKeyboardButton("📝 Logs", callback_data="logs")
|
|
|
- ],
|
|
|
- [
|
|
|
- InlineKeyboardButton("⚙️ Help", callback_data="help")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /stats command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- # Get current balance for stats
|
|
|
- balance = self.client.get_balance()
|
|
|
- current_balance = 0
|
|
|
- if balance and balance.get('total'):
|
|
|
- current_balance = float(balance['total'].get('USDC', 0))
|
|
|
-
|
|
|
- stats_message = self.stats.format_stats_message(current_balance)
|
|
|
- await update.message.reply_text(stats_message, parse_mode='HTML')
|
|
|
-
|
|
|
- async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /trades command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- recent_trades = self.stats.get_recent_trades(10)
|
|
|
-
|
|
|
- if not recent_trades:
|
|
|
- await update.message.reply_text("📝 No trades recorded yet.")
|
|
|
- return
|
|
|
-
|
|
|
- trades_text = "🔄 <b>Recent Trades</b>\n\n"
|
|
|
-
|
|
|
- for trade in reversed(recent_trades[-5:]): # Show last 5 trades
|
|
|
- timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
|
|
|
- side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
|
|
|
-
|
|
|
- trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
|
|
|
- trades_text += f" 💰 ${trade['price']:,.2f} | 💵 ${trade['value']:,.2f}\n"
|
|
|
- trades_text += f" 📅 {timestamp}\n\n"
|
|
|
-
|
|
|
- await update.message.reply_text(trades_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /balance command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- balance = self.client.get_balance()
|
|
|
- if balance:
|
|
|
- balance_text = "💰 <b>Account Balance</b>\n\n"
|
|
|
-
|
|
|
- # Debug: Show raw balance structure (can be removed after debugging)
|
|
|
- logger.debug(f"Raw balance data: {balance}")
|
|
|
-
|
|
|
- # CCXT balance structure includes 'free', 'used', and 'total'
|
|
|
- total_balance = balance.get('total', {})
|
|
|
- free_balance = balance.get('free', {})
|
|
|
- used_balance = balance.get('used', {})
|
|
|
-
|
|
|
- if total_balance:
|
|
|
- total_value = 0
|
|
|
- available_value = 0
|
|
|
-
|
|
|
- # Display individual assets
|
|
|
- for asset, amount in total_balance.items():
|
|
|
- if float(amount) > 0:
|
|
|
- free_amount = float(free_balance.get(asset, 0))
|
|
|
- used_amount = float(used_balance.get(asset, 0))
|
|
|
-
|
|
|
- balance_text += f"💵 <b>{asset}:</b>\n"
|
|
|
- balance_text += f" 📊 Total: {amount}\n"
|
|
|
- balance_text += f" ✅ Available: {free_amount}\n"
|
|
|
-
|
|
|
- if used_amount > 0:
|
|
|
- balance_text += f" 🔒 In Use: {used_amount}\n"
|
|
|
-
|
|
|
- balance_text += "\n"
|
|
|
-
|
|
|
- # Calculate totals (convert all to USDC equivalent for summary)
|
|
|
- if asset == 'USDC':
|
|
|
- total_value += float(amount)
|
|
|
- available_value += free_amount
|
|
|
- else:
|
|
|
- # For non-USDC assets, add to totals (assuming 1:1 for now, could be enhanced with price conversion)
|
|
|
- total_value += float(amount)
|
|
|
- available_value += free_amount
|
|
|
-
|
|
|
- # Summary section
|
|
|
- balance_text += f"💼 <b>Portfolio Summary:</b>\n"
|
|
|
- balance_text += f" 💰 Total Value: ${total_value:,.2f}\n"
|
|
|
- balance_text += f" 🚀 Available for Trading: ${available_value:,.2f}\n"
|
|
|
-
|
|
|
- if total_value - available_value > 0:
|
|
|
- balance_text += f" 🔒 In Active Use: ${total_value - available_value:,.2f}\n"
|
|
|
-
|
|
|
- # Add P&L summary
|
|
|
- basic_stats = self.stats.get_basic_stats()
|
|
|
- if basic_stats['initial_balance'] > 0:
|
|
|
- # Use USDC balance for P&L calculation
|
|
|
- usdc_total = float(total_balance.get('USDC', 0))
|
|
|
- pnl = usdc_total - basic_stats['initial_balance']
|
|
|
- pnl_percent = (pnl / basic_stats['initial_balance']) * 100
|
|
|
-
|
|
|
- balance_text += f"\n📊 <b>Performance (USDC):</b>\n"
|
|
|
- balance_text += f" 💵 P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
|
|
|
- balance_text += f" 📈 Initial: ${basic_stats['initial_balance']:,.2f}"
|
|
|
- else:
|
|
|
- balance_text += "📭 No balance data available"
|
|
|
- else:
|
|
|
- balance_text = "❌ Could not fetch balance data"
|
|
|
-
|
|
|
- await update.message.reply_text(balance_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /positions command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- positions = self.client.get_positions()
|
|
|
-
|
|
|
- if positions is not None: # Successfully fetched (could be empty list)
|
|
|
- # Filter for actual open positions
|
|
|
- open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
|
|
|
-
|
|
|
- # Add position count to header
|
|
|
- position_count = len(open_positions)
|
|
|
- positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n"
|
|
|
-
|
|
|
- if open_positions:
|
|
|
- total_unrealized = 0
|
|
|
- total_position_value = 0
|
|
|
-
|
|
|
- for position in open_positions:
|
|
|
- symbol = position.get('symbol', 'Unknown')
|
|
|
- contracts = float(position.get('contracts', 0))
|
|
|
- unrealized_pnl = float(position.get('unrealizedPnl', 0))
|
|
|
-
|
|
|
- # Use the correct field name for entry price
|
|
|
- entry_price = float(position.get('entryPrice', 0)) # Changed from 'entryPx' to 'entryPrice'
|
|
|
-
|
|
|
- # Try to get position value from notional first, then calculate if not available
|
|
|
- notional = position.get('notional')
|
|
|
- if notional is not None:
|
|
|
- position_value = float(notional)
|
|
|
- else:
|
|
|
- # Fallback calculation
|
|
|
- position_value = abs(contracts) * entry_price
|
|
|
-
|
|
|
- # Calculate P&L percentage based on position value
|
|
|
- pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
|
|
|
-
|
|
|
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- # Extract token name for cleaner display
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
-
|
|
|
- # Use CCXT's side field if available, otherwise fall back to contracts sign
|
|
|
- side_field = position.get('side', '').lower()
|
|
|
- if side_field in ['long', 'short']:
|
|
|
- position_type = side_field.upper()
|
|
|
- else:
|
|
|
- # Fallback for exchanges that don't provide side field
|
|
|
- position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
-
|
|
|
- positions_text += f"📊 <b>{token}</b> ({position_type})\n"
|
|
|
- positions_text += f" 📏 Size: {abs(contracts):.6f} {token}\n"
|
|
|
- positions_text += f" 💰 Entry: ${entry_price:,.2f}\n"
|
|
|
- positions_text += f" 💵 Value: ${position_value:,.2f}\n"
|
|
|
- positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n"
|
|
|
-
|
|
|
- total_unrealized += unrealized_pnl
|
|
|
- total_position_value += position_value
|
|
|
-
|
|
|
- # Calculate overall P&L percentage
|
|
|
- total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
|
|
|
- total_pnl_emoji = "🟢" if total_unrealized >= 0 else "🔴"
|
|
|
-
|
|
|
- positions_text += f"💼 <b>Total Portfolio:</b>\n"
|
|
|
- positions_text += f" 💵 Total Value: ${total_position_value:,.2f}\n"
|
|
|
- positions_text += f" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)"
|
|
|
- else:
|
|
|
- positions_text += "📭 <b>No open positions currently</b>\n\n"
|
|
|
- positions_text += "🚀 Ready to start trading!\n"
|
|
|
- positions_text += "Use /long or /short commands to open positions."
|
|
|
- else:
|
|
|
- # Actual API error
|
|
|
- positions_text = "❌ <b>Could not fetch positions data</b>\n\n"
|
|
|
- positions_text += "🔄 Please try again in a moment.\n"
|
|
|
- positions_text += "If the issue persists, check your connection."
|
|
|
-
|
|
|
- await update.message.reply_text(positions_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /orders command with optional token filter."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- # Check if token filter is provided
|
|
|
- token_filter = None
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
- token_filter = context.args[0].upper()
|
|
|
-
|
|
|
- orders = self.client.get_open_orders()
|
|
|
-
|
|
|
- # Debug: Log what we got from orders
|
|
|
- logger.debug(f"Raw orders data: {orders}")
|
|
|
- logger.debug(f"Orders type: {type(orders)}, Length: {len(orders) if orders else 'None'}")
|
|
|
-
|
|
|
- if orders is not None: # Successfully fetched (could be empty list)
|
|
|
- if token_filter:
|
|
|
- # Filter orders for specific token
|
|
|
- target_symbol = f"{token_filter}/USDC:USDC"
|
|
|
- filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
|
|
|
- orders_text = f"📋 <b>Open Orders - {token_filter} ({len(filtered_orders)})</b>\n\n"
|
|
|
- else:
|
|
|
- filtered_orders = orders
|
|
|
- orders_text = f"📋 <b>All Open Orders ({len(orders)})</b>\n\n"
|
|
|
-
|
|
|
- if filtered_orders and len(filtered_orders) > 0:
|
|
|
- for order in filtered_orders:
|
|
|
- symbol = order.get('symbol', 'Unknown')
|
|
|
- side = order.get('side', 'Unknown')
|
|
|
- amount = order.get('amount', 0)
|
|
|
- price = order.get('price', 0)
|
|
|
- order_id = order.get('id', 'Unknown')
|
|
|
-
|
|
|
- # Extract token from symbol for display
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
-
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
-
|
|
|
- orders_text += f"{side_emoji} <b>{token}</b>\n"
|
|
|
- orders_text += f" 📊 {side.upper()} {amount} @ ${price:,.2f}\n"
|
|
|
- orders_text += f" 💵 Value: ${float(amount) * float(price):,.2f}\n"
|
|
|
- orders_text += f" 🔑 ID: <code>{order_id}</code>\n\n"
|
|
|
-
|
|
|
- # Add helpful commands
|
|
|
- if token_filter:
|
|
|
- orders_text += f"💡 <b>Quick Actions:</b>\n"
|
|
|
- orders_text += f"• <code>/coo {token_filter}</code> - Cancel all {token_filter} orders\n"
|
|
|
- orders_text += f"• <code>/orders</code> - View all orders"
|
|
|
- else:
|
|
|
- orders_text += f"💡 <b>Filter by token:</b> <code>/orders BTC</code>, <code>/orders ETH</code>"
|
|
|
- else:
|
|
|
- if token_filter:
|
|
|
- orders_text += f"📭 <b>No open orders for {token_filter}</b>\n\n"
|
|
|
- orders_text += f"💡 No pending {token_filter} orders found.\n"
|
|
|
- orders_text += f"Use <code>/long {token_filter} 100</code> or <code>/short {token_filter} 100</code> to create new orders."
|
|
|
- else:
|
|
|
- orders_text += "📭 <b>No open orders currently</b>\n\n"
|
|
|
- orders_text += "💡 All clear! No pending orders.\n"
|
|
|
- orders_text += "Use /long or /short commands to place new orders."
|
|
|
- else:
|
|
|
- # Actual API error
|
|
|
- orders_text = "❌ <b>Could not fetch orders data</b>\n\n"
|
|
|
- orders_text += "🔄 Please try again in a moment.\n"
|
|
|
- orders_text += "If the issue persists, check your connection."
|
|
|
-
|
|
|
- await update.message.reply_text(orders_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /market command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- # Check if token is provided as argument
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
- token = context.args[0].upper()
|
|
|
- else:
|
|
|
- token = Config.DEFAULT_TRADING_TOKEN
|
|
|
-
|
|
|
- # Convert token to full symbol format for API
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
-
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- try:
|
|
|
- ticker = market_data['ticker']
|
|
|
- orderbook = market_data.get('orderbook', {})
|
|
|
-
|
|
|
- # Safely extract ticker data with fallbacks
|
|
|
- current_price = float(ticker.get('last') or 0)
|
|
|
- high_24h = float(ticker.get('high') or 0)
|
|
|
- low_24h = float(ticker.get('low') or 0)
|
|
|
- volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
|
|
|
-
|
|
|
- market_text = f"📊 <b>Market Data - {token}</b>\n\n"
|
|
|
-
|
|
|
- if current_price > 0:
|
|
|
- market_text += f"💵 <b>Current Price:</b> ${current_price:,.2f}\n"
|
|
|
- else:
|
|
|
- market_text += f"💵 <b>Current Price:</b> N/A\n"
|
|
|
-
|
|
|
- if high_24h > 0:
|
|
|
- market_text += f"📈 <b>24h High:</b> ${high_24h:,.2f}\n"
|
|
|
- else:
|
|
|
- market_text += f"📈 <b>24h High:</b> N/A\n"
|
|
|
-
|
|
|
- if low_24h > 0:
|
|
|
- market_text += f"📉 <b>24h Low:</b> ${low_24h:,.2f}\n"
|
|
|
- else:
|
|
|
- market_text += f"📉 <b>24h Low:</b> N/A\n"
|
|
|
-
|
|
|
- market_text += f"📊 <b>24h Volume:</b> {volume_24h}\n\n"
|
|
|
-
|
|
|
- # Handle orderbook data safely
|
|
|
- if orderbook and orderbook.get('bids') and orderbook.get('asks'):
|
|
|
- try:
|
|
|
- bids = orderbook.get('bids', [])
|
|
|
- asks = orderbook.get('asks', [])
|
|
|
-
|
|
|
- if bids and asks and len(bids) > 0 and len(asks) > 0:
|
|
|
- best_bid = float(bids[0][0]) if bids[0][0] else 0
|
|
|
- best_ask = float(asks[0][0]) if asks[0][0] else 0
|
|
|
-
|
|
|
- if best_bid > 0 and best_ask > 0:
|
|
|
- spread = best_ask - best_bid
|
|
|
- spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
|
|
|
-
|
|
|
- market_text += f"🟢 <b>Best Bid:</b> ${best_bid:,.2f}\n"
|
|
|
- market_text += f"🔴 <b>Best Ask:</b> ${best_ask:,.2f}\n"
|
|
|
- market_text += f"📏 <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
|
|
|
- else:
|
|
|
- market_text += f"📋 <b>Orderbook:</b> Data unavailable\n"
|
|
|
- else:
|
|
|
- market_text += f"📋 <b>Orderbook:</b> No orders available\n"
|
|
|
- except (IndexError, ValueError, TypeError) as e:
|
|
|
- market_text += f"📋 <b>Orderbook:</b> Error parsing data\n"
|
|
|
- else:
|
|
|
- market_text += f"📋 <b>Orderbook:</b> Not available\n"
|
|
|
-
|
|
|
- # Add usage hint
|
|
|
- market_text += f"\n💡 <b>Usage:</b> <code>/market {token}</code> or <code>/market</code> for default"
|
|
|
-
|
|
|
- except (ValueError, TypeError) as e:
|
|
|
- market_text = f"❌ <b>Error parsing market data</b>\n\n"
|
|
|
- market_text += f"🔧 Raw data received but couldn't parse values.\n"
|
|
|
- market_text += f"📞 Please try again or contact support if this persists."
|
|
|
- else:
|
|
|
- market_text = f"❌ <b>Could not fetch market data for {token}</b>\n\n"
|
|
|
- market_text += f"🔄 Please try again in a moment.\n"
|
|
|
- market_text += f"🌐 Check your network connection.\n"
|
|
|
- market_text += f"📡 API may be temporarily unavailable.\n\n"
|
|
|
- market_text += f"💡 <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
|
|
|
-
|
|
|
- await update.message.reply_text(market_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /price command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- # Check if token is provided as argument
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
- token = context.args[0].upper()
|
|
|
- else:
|
|
|
- token = Config.DEFAULT_TRADING_TOKEN
|
|
|
-
|
|
|
- # Convert token to full symbol format for API
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
-
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- try:
|
|
|
- ticker = market_data['ticker']
|
|
|
- price_value = ticker.get('last')
|
|
|
-
|
|
|
- if price_value is not None:
|
|
|
- price = float(price_value)
|
|
|
- price_text = f"💵 <b>{token}</b>: ${price:,.2f}"
|
|
|
-
|
|
|
- # Add timestamp
|
|
|
- timestamp = datetime.now().strftime('%H:%M:%S')
|
|
|
- price_text += f"\n⏰ <i>Updated: {timestamp}</i>"
|
|
|
-
|
|
|
- # Add usage hint
|
|
|
- price_text += f"\n💡 <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
|
|
|
- else:
|
|
|
- price_text = f"💵 <b>{symbol}</b>: Price not available\n⚠️ <i>Data temporarily unavailable</i>"
|
|
|
-
|
|
|
- except (ValueError, TypeError) as e:
|
|
|
- price_text = f"❌ <b>Error parsing price for {symbol}</b>\n🔧 <i>Please try again</i>"
|
|
|
- else:
|
|
|
- price_text = f"❌ <b>Could not fetch price for {symbol}</b>\n🔄 <i>Please try again in a moment</i>\n\n"
|
|
|
- price_text += f"💡 <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
|
|
|
-
|
|
|
- await update.message.reply_text(price_text, parse_mode='HTML')
|
|
|
-
|
|
|
- async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle inline keyboard button presses."""
|
|
|
- query = update.callback_query
|
|
|
- await query.answer()
|
|
|
-
|
|
|
- if not self.is_authorized(query.message.chat_id):
|
|
|
- await query.edit_message_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- callback_data = query.data
|
|
|
-
|
|
|
- # Handle trading confirmations
|
|
|
- if callback_data.startswith('confirm_long_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- usdc_amount = float(parts[3])
|
|
|
- try:
|
|
|
- price = float(parts[4])
|
|
|
- except (ValueError, TypeError):
|
|
|
- price = None # Will be handled in execute_long_order
|
|
|
- is_limit = len(parts) > 5 and parts[5] == 'limit'
|
|
|
-
|
|
|
- # Parse stop loss if present
|
|
|
- stop_loss_price = None
|
|
|
- if len(parts) > 6 and parts[6] == 'sl':
|
|
|
- try:
|
|
|
- stop_loss_price = float(parts[7])
|
|
|
- except (ValueError, TypeError):
|
|
|
- stop_loss_price = None
|
|
|
- elif len(parts) > 5 and parts[5] == 'sl':
|
|
|
- try:
|
|
|
- stop_loss_price = float(parts[6])
|
|
|
- except (ValueError, TypeError):
|
|
|
- stop_loss_price = None
|
|
|
-
|
|
|
- await self._execute_long_order(query, token, usdc_amount, price, is_limit, stop_loss_price)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data.startswith('confirm_short_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- usdc_amount = float(parts[3])
|
|
|
- try:
|
|
|
- price = float(parts[4])
|
|
|
- except (ValueError, TypeError):
|
|
|
- price = None # Will be handled in execute_short_order
|
|
|
- is_limit = len(parts) > 5 and parts[5] == 'limit'
|
|
|
-
|
|
|
- # Parse stop loss if present
|
|
|
- stop_loss_price = None
|
|
|
- if len(parts) > 6 and parts[6] == 'sl':
|
|
|
- try:
|
|
|
- stop_loss_price = float(parts[7])
|
|
|
- except (ValueError, TypeError):
|
|
|
- stop_loss_price = None
|
|
|
- elif len(parts) > 5 and parts[5] == 'sl':
|
|
|
- try:
|
|
|
- stop_loss_price = float(parts[6])
|
|
|
- except (ValueError, TypeError):
|
|
|
- stop_loss_price = None
|
|
|
-
|
|
|
- await self._execute_short_order(query, token, usdc_amount, price, is_limit, stop_loss_price)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data.startswith('confirm_exit_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- exit_side = parts[3]
|
|
|
- contracts = float(parts[4])
|
|
|
- price = float(parts[5])
|
|
|
- await self._execute_exit_order(query, token, exit_side, contracts, price)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data.startswith('confirm_coo_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- await self._execute_coo(query, token)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data.startswith('confirm_sl_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- exit_side = parts[3]
|
|
|
- contracts = float(parts[4])
|
|
|
- price = float(parts[5])
|
|
|
- await self._execute_sl_order(query, token, exit_side, contracts, price)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data.startswith('confirm_tp_'):
|
|
|
- parts = callback_data.split('_')
|
|
|
- token = parts[2]
|
|
|
- exit_side = parts[3]
|
|
|
- contracts = float(parts[4])
|
|
|
- price = float(parts[5])
|
|
|
- await self._execute_tp_order(query, token, exit_side, contracts, price)
|
|
|
- return
|
|
|
-
|
|
|
- elif callback_data == 'cancel_order':
|
|
|
- await query.edit_message_text("❌ Order cancelled.")
|
|
|
- return
|
|
|
-
|
|
|
- # Create a fake update object for reusing command handlers
|
|
|
- fake_update = Update(
|
|
|
- update_id=update.update_id,
|
|
|
- message=query.message,
|
|
|
- callback_query=query
|
|
|
- )
|
|
|
-
|
|
|
- # Handle regular button callbacks
|
|
|
- if callback_data == "balance":
|
|
|
- await self.balance_command(fake_update, context)
|
|
|
- elif callback_data == "stats":
|
|
|
- await self.stats_command(fake_update, context)
|
|
|
- elif callback_data == "positions":
|
|
|
- await self.positions_command(fake_update, context)
|
|
|
- elif callback_data == "orders":
|
|
|
- await self.orders_command(fake_update, context)
|
|
|
- elif callback_data == "market":
|
|
|
- await self.market_command(fake_update, context)
|
|
|
- elif callback_data == "price":
|
|
|
- await self.price_command(fake_update, context)
|
|
|
- elif callback_data == "trades":
|
|
|
- await self.trades_command(fake_update, context)
|
|
|
- elif callback_data == "help":
|
|
|
- await self.help_command(fake_update, context)
|
|
|
- elif callback_data == "performance":
|
|
|
- await self.performance_command(fake_update, context)
|
|
|
- elif callback_data == "alarm":
|
|
|
- await self.alarm_command(fake_update, context)
|
|
|
- elif callback_data == "daily":
|
|
|
- await self.daily_command(fake_update, context)
|
|
|
- elif callback_data == "weekly":
|
|
|
- await self.weekly_command(fake_update, context)
|
|
|
- elif callback_data == "monthly":
|
|
|
- await self.monthly_command(fake_update, context)
|
|
|
- elif callback_data == "monitoring":
|
|
|
- await self.monitoring_command(fake_update, context)
|
|
|
- elif callback_data == "logs":
|
|
|
- await self.logs_command(fake_update, context)
|
|
|
-
|
|
|
- async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool, stop_loss_price: float = None):
|
|
|
- """Execute a long order."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Opening long position...")
|
|
|
-
|
|
|
- # Validate price
|
|
|
- if price is None or price <= 0:
|
|
|
- # Try to get current market price
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- price = float(market_data['ticker'].get('last', 0))
|
|
|
- if price <= 0:
|
|
|
- await query.edit_message_text("❌ Unable to get valid market price. Please try again.")
|
|
|
- return
|
|
|
- else:
|
|
|
- await query.edit_message_text("❌ Unable to fetch market price. Please try again.")
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate token amount based on USDC value and price
|
|
|
- token_amount = usdc_amount / price
|
|
|
-
|
|
|
- # Place order (limit or market)
|
|
|
- if is_limit:
|
|
|
- order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
|
|
|
- else:
|
|
|
- order = self.client.place_market_order(symbol, 'buy', token_amount)
|
|
|
-
|
|
|
- if order:
|
|
|
- # Record the trade in stats
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
- if actual_price is None or actual_price <= 0:
|
|
|
- # This should not happen due to our price validation above, but extra safety
|
|
|
- actual_price = price
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
|
|
|
-
|
|
|
- # Track this as a bot-generated trade to prevent double processing
|
|
|
- if order_id and order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
- self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
- logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
-
|
|
|
- # Save pending stop loss if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'symbol': symbol,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'sell', # For long position, stop loss is a sell order
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'long',
|
|
|
- 'original_order_id': order_id,
|
|
|
- 'is_limit': is_limit
|
|
|
- }
|
|
|
- self._save_bot_state() # Save state after adding pending stop loss
|
|
|
- logger.info(f"💾 Saved pending stop loss for order {order_id}: sell {token_amount:.6f} {token} @ ${stop_loss_price}")
|
|
|
-
|
|
|
- success_message = f"""
|
|
|
-✅ <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: LONG (Buy)
|
|
|
-• Amount: {token_amount:.6f} {token}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-• USDC Value: ~${usdc_amount:,.2f}
|
|
|
-• Order Type: {'Limit' if is_limit else 'Market'} Order
|
|
|
-• Order ID: <code>{order_id}</code>
|
|
|
-"""
|
|
|
-
|
|
|
- # Add stop loss confirmation if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- success_message += f"""
|
|
|
-🛑 <b>Stop Loss Saved:</b>
|
|
|
-• Stop Price: ${stop_loss_price:,.2f}
|
|
|
-• Will be placed automatically when order fills
|
|
|
-• Status: PENDING ⏳
|
|
|
-"""
|
|
|
-
|
|
|
- success_message += f"\n🚀 Your {'limit order has been placed' if is_limit else 'long position is now active'}!"
|
|
|
-
|
|
|
- await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
- logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
|
|
|
- else:
|
|
|
- await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error in long order: {e}")
|
|
|
-
|
|
|
- async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool, stop_loss_price: float = None):
|
|
|
- """Execute a short order."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Opening short position...")
|
|
|
-
|
|
|
- # Validate price
|
|
|
- if price is None or price <= 0:
|
|
|
- # Try to get current market price
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- price = float(market_data['ticker'].get('last', 0))
|
|
|
- if price <= 0:
|
|
|
- await query.edit_message_text("❌ Unable to get valid market price. Please try again.")
|
|
|
- return
|
|
|
- else:
|
|
|
- await query.edit_message_text("❌ Unable to fetch market price. Please try again.")
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate token amount based on USDC value and price
|
|
|
- token_amount = usdc_amount / price
|
|
|
-
|
|
|
- # Place order (limit or market)
|
|
|
- if is_limit:
|
|
|
- order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
|
|
|
- else:
|
|
|
- order = self.client.place_market_order(symbol, 'sell', token_amount)
|
|
|
-
|
|
|
- if order:
|
|
|
- # Record the trade in stats
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
- if actual_price is None or actual_price <= 0:
|
|
|
- # This should not happen due to our price validation above, but extra safety
|
|
|
- actual_price = price
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
|
|
|
-
|
|
|
- # Track this as a bot-generated trade to prevent double processing
|
|
|
- if order_id and order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
- self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
- logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
-
|
|
|
- # Save pending stop loss if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'symbol': symbol,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'buy', # For short position, stop loss is a buy order
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'short',
|
|
|
- 'original_order_id': order_id,
|
|
|
- 'is_limit': is_limit
|
|
|
- }
|
|
|
- self._save_bot_state() # Save state after adding pending stop loss
|
|
|
- logger.info(f"💾 Saved pending stop loss for order {order_id}: buy {token_amount:.6f} {token} @ ${stop_loss_price}")
|
|
|
-
|
|
|
- success_message = f"""
|
|
|
-✅ <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: SHORT (Sell)
|
|
|
-• Amount: {token_amount:.6f} {token}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-• USDC Value: ~${usdc_amount:,.2f}
|
|
|
-• Order Type: {'Limit' if is_limit else 'Market'} Order
|
|
|
-• Order ID: <code>{order_id}</code>
|
|
|
-"""
|
|
|
-
|
|
|
- # Add stop loss confirmation if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- success_message += f"""
|
|
|
-🛑 <b>Stop Loss Saved:</b>
|
|
|
-• Stop Price: ${stop_loss_price:,.2f}
|
|
|
-• Will be placed automatically when order fills
|
|
|
-• Status: PENDING ⏳
|
|
|
-"""
|
|
|
-
|
|
|
- success_message += f"\n📉 Your {'limit order has been placed' if is_limit else 'short position is now active'}!"
|
|
|
-
|
|
|
- await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
- logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
|
|
|
- else:
|
|
|
- await query.edit_message_text(f"❌ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error in short order: {e}")
|
|
|
-
|
|
|
- async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float):
|
|
|
- """Execute an exit order."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Closing position...")
|
|
|
-
|
|
|
- # Place market order to close position
|
|
|
- order = self.client.place_market_order(symbol, exit_side, contracts)
|
|
|
-
|
|
|
- if order:
|
|
|
- # Record the trade in stats
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
- if actual_price is None or actual_price <= 0:
|
|
|
- # Fallback to ensure we have a valid price
|
|
|
- actual_price = price
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
|
|
|
-
|
|
|
- # Track this as a bot-generated trade to prevent double processing
|
|
|
- if order_id and order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
- self._save_bot_state() # Save state to persist bot trade tracking
|
|
|
- logger.info(f"🤖 Tracked bot trade ID: {order_id}")
|
|
|
-
|
|
|
- position_type = "LONG" if exit_side == "sell" else "SHORT"
|
|
|
-
|
|
|
- success_message = f"""
|
|
|
-✅ <b>Position Closed Successfully!</b>
|
|
|
-
|
|
|
-📊 <b>Exit Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position Closed: {position_type}
|
|
|
-• Exit Side: {exit_side.upper()}
|
|
|
-• Amount: {contracts} {token}
|
|
|
-• Est. Price: ~${price:,.2f}
|
|
|
-• Order Type: Market Order
|
|
|
-• Order ID: <code>{order_id}</code>
|
|
|
-
|
|
|
-🎯 <b>Position Summary:</b>
|
|
|
-• Status: CLOSED
|
|
|
-• Exit Value: ~${contracts * price:,.2f}
|
|
|
-
|
|
|
-📊 Use /stats to see updated performance metrics.
|
|
|
- """
|
|
|
-
|
|
|
- await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
- logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}")
|
|
|
- else:
|
|
|
- await query.edit_message_text("❌ Failed to close position. Please try again.")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error closing position: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error closing position: {e}")
|
|
|
-
|
|
|
- async def _execute_coo(self, query, token: str):
|
|
|
- """Execute cancel open orders for a specific token."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Cancelling all orders...")
|
|
|
-
|
|
|
- # Get current orders for this token
|
|
|
- all_orders = self.client.get_open_orders()
|
|
|
- if all_orders is None:
|
|
|
- await query.edit_message_text(f"❌ Could not fetch orders to cancel {token} orders")
|
|
|
- return
|
|
|
-
|
|
|
- # Filter orders for the specific token
|
|
|
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
|
|
|
-
|
|
|
- if not token_orders:
|
|
|
- await query.edit_message_text(f"📭 No open orders found for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Cancel each order
|
|
|
- cancelled_orders = []
|
|
|
- failed_orders = []
|
|
|
-
|
|
|
- for order in token_orders:
|
|
|
- order_id = order.get('id')
|
|
|
- if order_id:
|
|
|
- try:
|
|
|
- success = self.client.cancel_order(order_id, symbol)
|
|
|
- if success:
|
|
|
- cancelled_orders.append(order)
|
|
|
- else:
|
|
|
- failed_orders.append(order)
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Failed to cancel order {order_id}: {e}")
|
|
|
- failed_orders.append(order)
|
|
|
-
|
|
|
- # Create result message
|
|
|
- result_message = f"""
|
|
|
-✅ <b>Cancel Orders Results</b>
|
|
|
-
|
|
|
-📊 <b>Summary:</b>
|
|
|
-• Token: {token}
|
|
|
-• Cancelled: {len(cancelled_orders)} orders
|
|
|
-• Failed: {len(failed_orders)} orders
|
|
|
-• Total Attempted: {len(token_orders)} orders
|
|
|
-"""
|
|
|
-
|
|
|
- if cancelled_orders:
|
|
|
- result_message += f"\n🗑️ <b>Successfully Cancelled:</b>\n"
|
|
|
- for order in cancelled_orders:
|
|
|
- side = order.get('side', 'Unknown')
|
|
|
- amount = order.get('amount', 0)
|
|
|
- price = order.get('price', 0)
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n"
|
|
|
-
|
|
|
- if failed_orders:
|
|
|
- result_message += f"\n❌ <b>Failed to Cancel:</b>\n"
|
|
|
- for order in failed_orders:
|
|
|
- side = order.get('side', 'Unknown')
|
|
|
- amount = order.get('amount', 0)
|
|
|
- price = order.get('price', 0)
|
|
|
- order_id = order.get('id', 'Unknown')
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n"
|
|
|
-
|
|
|
- if len(cancelled_orders) == len(token_orders):
|
|
|
- result_message += f"\n🎉 All {token} orders successfully cancelled!"
|
|
|
- elif len(cancelled_orders) > 0:
|
|
|
- result_message += f"\n⚠️ Some orders cancelled. Check failed orders above."
|
|
|
- else:
|
|
|
- result_message += f"\n❌ Could not cancel any {token} orders."
|
|
|
-
|
|
|
- await query.edit_message_text(result_message, parse_mode='HTML')
|
|
|
- logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error cancelling {token} orders: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error in COO execution: {e}")
|
|
|
-
|
|
|
- async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float):
|
|
|
- """Execute a stop loss order."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Setting stop loss...")
|
|
|
-
|
|
|
- # Place stop loss order
|
|
|
- order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price)
|
|
|
-
|
|
|
- if order:
|
|
|
- # Record the trade in stats
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
|
|
|
-
|
|
|
- position_type = "LONG" if exit_side == "sell" else "SHORT"
|
|
|
-
|
|
|
- success_message = f"""
|
|
|
-✅ <b>Stop Loss Order Set Successfully!</b>
|
|
|
-
|
|
|
-📊 <b>Stop Loss Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Size: {contracts} contracts
|
|
|
-• Stop Price: ${price:,.2f}
|
|
|
-• Action: {exit_side.upper()} (Close {position_type})
|
|
|
-• Amount: {contracts} {token}
|
|
|
-• Order Type: Limit Order
|
|
|
-• Order ID: <code>{order_id}</code>
|
|
|
-
|
|
|
-🎯 <b>Stop Loss Execution:</b>
|
|
|
-• Status: SET
|
|
|
-• Exit Value: ~${contracts * price:,.2f}
|
|
|
-
|
|
|
-📊 Use /stats to see updated performance metrics.
|
|
|
- """
|
|
|
-
|
|
|
- await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
- logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}")
|
|
|
- else:
|
|
|
- await query.edit_message_text("❌ Failed to set stop loss. Please try again.")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error setting stop loss: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error setting stop loss: {e}")
|
|
|
-
|
|
|
- async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float):
|
|
|
- """Execute a take profit order."""
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- try:
|
|
|
- await query.edit_message_text("⏳ Setting take profit...")
|
|
|
-
|
|
|
- # Place take profit order
|
|
|
- order = self.client.place_take_profit_order(symbol, exit_side, contracts, price)
|
|
|
-
|
|
|
- if order:
|
|
|
- # Record the trade in stats
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
- if actual_price is None or actual_price <= 0:
|
|
|
- # Fallback to ensure we have a valid price
|
|
|
- actual_price = price
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
|
|
|
-
|
|
|
- position_type = "LONG" if exit_side == "sell" else "SHORT"
|
|
|
-
|
|
|
- success_message = f"""
|
|
|
-✅ <b>Take Profit Order Set Successfully!</b>
|
|
|
-
|
|
|
-📊 <b>Take Profit Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Size: {contracts} contracts
|
|
|
-• Target Price: ${price:,.2f}
|
|
|
-• Action: {exit_side.upper()} (Close {position_type})
|
|
|
-• Amount: {contracts} {token}
|
|
|
-• Order Type: Limit Order
|
|
|
-• Order ID: <code>{order_id}</code>
|
|
|
-
|
|
|
-🎯 <b>Take Profit Execution:</b>
|
|
|
-• Status: SET
|
|
|
-• Exit Value: ~${contracts * price:,.2f}
|
|
|
-
|
|
|
-📊 Use /stats to see updated performance metrics.
|
|
|
- """
|
|
|
-
|
|
|
- await query.edit_message_text(success_message, parse_mode='HTML')
|
|
|
- logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
|
|
|
- else:
|
|
|
- await query.edit_message_text("❌ Failed to set take profit. Please try again.")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error setting take profit: {str(e)}"
|
|
|
- await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error setting take profit: {e}")
|
|
|
-
|
|
|
- async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle messages from custom keyboard buttons."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- message_text = update.message.text.lower()
|
|
|
-
|
|
|
- # Map keyboard button text to commands
|
|
|
- command_map = {
|
|
|
- 'balance': '/balance',
|
|
|
- 'positions': '/positions',
|
|
|
- 'orders': '/orders',
|
|
|
- 'stats': '/stats',
|
|
|
- 'trades': '/trades',
|
|
|
- 'market': '/market',
|
|
|
- 'price': '/price',
|
|
|
- 'help': '/help',
|
|
|
- 'commands': '/commands',
|
|
|
- 'monitoring': '/monitoring',
|
|
|
- 'logs': '/logs',
|
|
|
- 'performance': '/performance',
|
|
|
- 'daily': '/daily',
|
|
|
- 'weekly': '/weekly',
|
|
|
- 'monthly': '/monthly',
|
|
|
- 'risk': '/risk',
|
|
|
- 'alarm': '/alarm',
|
|
|
- 'keyboard': '/keyboard'
|
|
|
- }
|
|
|
-
|
|
|
- # Check if the message matches any keyboard command
|
|
|
- if message_text in command_map:
|
|
|
- # Create a fake update object with the corresponding command
|
|
|
- update.message.text = command_map[message_text]
|
|
|
- # Get the handler for this command and call it
|
|
|
- handlers = self.application.handlers[0] # Get default group handlers
|
|
|
- for handler in handlers:
|
|
|
- if hasattr(handler, 'callback') and hasattr(handler, 'filters'):
|
|
|
- if await handler.check_update(update):
|
|
|
- await handler.callback(update, context)
|
|
|
- return
|
|
|
-
|
|
|
- # If no keyboard command matched, show a help message
|
|
|
- await update.message.reply_text("❓ Unknown command. Use /help to see available commands.")
|
|
|
-
|
|
|
- async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle unknown commands."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- await update.message.reply_text(
|
|
|
- "❓ Unknown command. Use /help to see available commands or tap the buttons in /start."
|
|
|
- )
|
|
|
-
|
|
|
- def setup_handlers(self):
|
|
|
- """Set up command handlers for the bot."""
|
|
|
- if not self.application:
|
|
|
- return
|
|
|
-
|
|
|
- # Command handlers
|
|
|
- self.application.add_handler(CommandHandler("start", self.start_command))
|
|
|
- self.application.add_handler(CommandHandler("help", self.help_command))
|
|
|
- self.application.add_handler(CommandHandler("commands", self.commands_command))
|
|
|
- self.application.add_handler(CommandHandler("c", self.commands_command))
|
|
|
- self.application.add_handler(CommandHandler("balance", self.balance_command))
|
|
|
- self.application.add_handler(CommandHandler("positions", self.positions_command))
|
|
|
- self.application.add_handler(CommandHandler("orders", self.orders_command))
|
|
|
- self.application.add_handler(CommandHandler("market", self.market_command))
|
|
|
- self.application.add_handler(CommandHandler("price", self.price_command))
|
|
|
- self.application.add_handler(CommandHandler("stats", self.stats_command))
|
|
|
- self.application.add_handler(CommandHandler("trades", self.trades_command))
|
|
|
- self.application.add_handler(CommandHandler("long", self.long_command))
|
|
|
- self.application.add_handler(CommandHandler("short", self.short_command))
|
|
|
- self.application.add_handler(CommandHandler("exit", self.exit_command))
|
|
|
- self.application.add_handler(CommandHandler("coo", self.coo_command))
|
|
|
- self.application.add_handler(CommandHandler("sl", self.sl_command))
|
|
|
- self.application.add_handler(CommandHandler("tp", self.tp_command))
|
|
|
- self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
|
|
|
- self.application.add_handler(CommandHandler("alarm", self.alarm_command))
|
|
|
- self.application.add_handler(CommandHandler("logs", self.logs_command))
|
|
|
- self.application.add_handler(CommandHandler("performance", self.performance_command))
|
|
|
- self.application.add_handler(CommandHandler("daily", self.daily_command))
|
|
|
- self.application.add_handler(CommandHandler("weekly", self.weekly_command))
|
|
|
- self.application.add_handler(CommandHandler("monthly", self.monthly_command))
|
|
|
- self.application.add_handler(CommandHandler("risk", self.risk_command))
|
|
|
- self.application.add_handler(CommandHandler("version", self.version_command))
|
|
|
- self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_command))
|
|
|
- self.application.add_handler(CommandHandler("keyboard", self.keyboard_command))
|
|
|
- self.application.add_handler(CommandHandler("debug", self.debug_command))
|
|
|
-
|
|
|
- # Callback query handler for inline keyboards
|
|
|
- self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
|
|
-
|
|
|
- # Handle clean keyboard button messages (without /)
|
|
|
- self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_keyboard_message))
|
|
|
-
|
|
|
- # Handle unknown commands
|
|
|
- self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
|
|
|
-
|
|
|
- async def run(self):
|
|
|
- """Run the Telegram bot."""
|
|
|
- if not Config.TELEGRAM_BOT_TOKEN:
|
|
|
- logger.error("❌ TELEGRAM_BOT_TOKEN not configured")
|
|
|
- return
|
|
|
-
|
|
|
- if not Config.TELEGRAM_CHAT_ID:
|
|
|
- logger.error("❌ TELEGRAM_CHAT_ID not configured")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Create application
|
|
|
- self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
|
|
|
-
|
|
|
- # Set up handlers
|
|
|
- self.setup_handlers()
|
|
|
-
|
|
|
- logger.info("🚀 Starting Telegram trading bot...")
|
|
|
-
|
|
|
- # Initialize the application
|
|
|
- await self.application.initialize()
|
|
|
-
|
|
|
- # Send startup notification
|
|
|
- await self.send_message(
|
|
|
- f"🤖 <b>Manual Trading Bot v{self.version} Started</b>\n\n"
|
|
|
- f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
|
|
|
- f"📊 Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n"
|
|
|
- f"📱 Manual trading ready!\n"
|
|
|
- f"🔄 Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
|
|
|
- f"🔄 External trade monitoring: Active\n"
|
|
|
- f"🔔 Price alarms: Active\n"
|
|
|
- f"📊 Auto stats sync: Enabled\n"
|
|
|
- f"📝 Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
|
|
|
- f"⏰ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
|
- "Use /start for quick actions or /help for all commands."
|
|
|
- )
|
|
|
-
|
|
|
- # Perform initial log cleanup
|
|
|
- try:
|
|
|
- cleanup_logs(days_to_keep=30)
|
|
|
- logger.info("🧹 Initial log cleanup completed")
|
|
|
- except Exception as e:
|
|
|
- logger.warning(f"⚠️ Initial log cleanup failed: {e}")
|
|
|
-
|
|
|
- # Start the application
|
|
|
- await self.application.start()
|
|
|
-
|
|
|
- # Start order monitoring
|
|
|
- await self.start_order_monitoring()
|
|
|
-
|
|
|
- # Start polling for updates manually
|
|
|
- logger.info("🔄 Starting update polling...")
|
|
|
-
|
|
|
- # Get updates in a loop
|
|
|
- last_update_id = 0
|
|
|
- while True:
|
|
|
- try:
|
|
|
- # Get updates from Telegram
|
|
|
- updates = await self.application.bot.get_updates(
|
|
|
- offset=last_update_id + 1,
|
|
|
- timeout=30,
|
|
|
- allowed_updates=None
|
|
|
- )
|
|
|
-
|
|
|
- # Process each update
|
|
|
- for update in updates:
|
|
|
- last_update_id = update.update_id
|
|
|
-
|
|
|
- # Process the update through the application
|
|
|
- await self.application.process_update(update)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error processing updates: {e}")
|
|
|
- await asyncio.sleep(5) # Wait before retrying
|
|
|
-
|
|
|
- except asyncio.CancelledError:
|
|
|
- logger.info("🛑 Bot polling cancelled")
|
|
|
- raise
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error in telegram bot: {e}")
|
|
|
- raise
|
|
|
-
|
|
|
- finally:
|
|
|
- # Clean shutdown
|
|
|
- try:
|
|
|
- await self.stop_order_monitoring()
|
|
|
- if self.application:
|
|
|
- await self.application.stop()
|
|
|
- await self.application.shutdown()
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error during shutdown: {e}")
|
|
|
-
|
|
|
- async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /long command for opening long positions."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /long [token] [USDC amount] [price (optional)] [sl:price (optional)]\n"
|
|
|
- "Examples:\n"
|
|
|
- "• /long BTC 100 - Market order\n"
|
|
|
- "• /long BTC 100 45000 - Limit order at $45,000\n"
|
|
|
- "• /long BTC 100 sl:44000 - Market order with stop loss at $44,000\n"
|
|
|
- "• /long BTC 100 45000 sl:44000 - Limit order at $45,000 with stop loss at $44,000"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- usdc_amount = float(context.args[1])
|
|
|
-
|
|
|
- # Parse arguments for price and stop loss
|
|
|
- limit_price = None
|
|
|
- stop_loss_price = None
|
|
|
-
|
|
|
- # Parse remaining arguments
|
|
|
- for i, arg in enumerate(context.args[2:], 2):
|
|
|
- if arg.startswith('sl:'):
|
|
|
- # Stop loss parameter
|
|
|
- try:
|
|
|
- stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid stop loss price format. Use sl:price (e.g., sl:44000)")
|
|
|
- return
|
|
|
- elif limit_price is None:
|
|
|
- # First non-sl parameter is the limit price
|
|
|
- try:
|
|
|
- limit_price = float(arg)
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid limit price format. Please use numbers only.")
|
|
|
- return
|
|
|
-
|
|
|
- # Determine order type
|
|
|
- if limit_price:
|
|
|
- order_type = "Limit"
|
|
|
- order_description = f"at ${limit_price:,.2f}"
|
|
|
- else:
|
|
|
- order_type = "Market"
|
|
|
- order_description = "at current market price"
|
|
|
-
|
|
|
- # Convert token to full symbol format for Hyperliquid
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current market price to calculate amount and for display
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if not market_data:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
- if current_price <= 0:
|
|
|
- await update.message.reply_text(f"❌ Invalid price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Validate stop loss price for long positions
|
|
|
- if stop_loss_price is not None:
|
|
|
- entry_price = limit_price if limit_price else current_price
|
|
|
- if stop_loss_price >= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"❌ Stop loss price should be BELOW entry price for long positions\n\n"
|
|
|
- f"📊 Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"🛑 Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a lower price like: /long {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 0.95:.0f}"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate token amount based on price (market or limit)
|
|
|
- calculation_price = limit_price if limit_price else current_price
|
|
|
- token_amount = usdc_amount / calculation_price
|
|
|
-
|
|
|
- # Create confirmation message
|
|
|
- confirmation_text = f"""
|
|
|
-🟢 <b>Long Position Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: LONG (Buy)
|
|
|
-• USDC Value: ${usdc_amount:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-• Order Type: {order_type} Order
|
|
|
-• Token Amount: {token_amount:.6f} {token}
|
|
|
-
|
|
|
-🎯 <b>Execution:</b>
|
|
|
-• Will buy {token_amount:.6f} {token} {order_description}
|
|
|
-• Est. Value: ${token_amount * calculation_price:,.2f}
|
|
|
-"""
|
|
|
-
|
|
|
- # Add stop loss information if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- confirmation_text += f"""
|
|
|
-🛑 <b>Stop Loss:</b>
|
|
|
-• Stop Price: ${stop_loss_price:,.2f}
|
|
|
-• Will be placed automatically after order fills
|
|
|
-• Protection Level: {((calculation_price - stop_loss_price) / calculation_price * 100):.1f}% below entry
|
|
|
-"""
|
|
|
-
|
|
|
- confirmation_text += "\n⚠️ <b>Are you sure you want to open this long position?</b>"
|
|
|
-
|
|
|
- # Use limit_price for callback if provided, otherwise current_price
|
|
|
- callback_price = limit_price if limit_price else current_price
|
|
|
- callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}"
|
|
|
- if limit_price:
|
|
|
- callback_data += "_limit"
|
|
|
- if stop_loss_price is not None:
|
|
|
- callback_data += f"_sl_{stop_loss_price}"
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("✅ Confirm Long", callback_data=callback_data),
|
|
|
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing long command: {e}")
|
|
|
-
|
|
|
- async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /short command for opening short positions."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /short [token] [USDC amount] [price (optional)] [sl:price (optional)]\n"
|
|
|
- "Examples:\n"
|
|
|
- "• /short BTC 100 - Market order\n"
|
|
|
- "• /short BTC 100 46000 - Limit order at $46,000\n"
|
|
|
- "• /short BTC 100 sl:47000 - Market order with stop loss at $47,000\n"
|
|
|
- "• /short BTC 100 46000 sl:47000 - Limit order at $46,000 with stop loss at $47,000"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- usdc_amount = float(context.args[1])
|
|
|
-
|
|
|
- # Parse arguments for price and stop loss
|
|
|
- limit_price = None
|
|
|
- stop_loss_price = None
|
|
|
-
|
|
|
- # Parse remaining arguments
|
|
|
- for i, arg in enumerate(context.args[2:], 2):
|
|
|
- if arg.startswith('sl:'):
|
|
|
- # Stop loss parameter
|
|
|
- try:
|
|
|
- stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid stop loss price format. Use sl:price (e.g., sl:47000)")
|
|
|
- return
|
|
|
- elif limit_price is None:
|
|
|
- # First non-sl parameter is the limit price
|
|
|
- try:
|
|
|
- limit_price = float(arg)
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid limit price format. Please use numbers only.")
|
|
|
- return
|
|
|
-
|
|
|
- # Determine order type
|
|
|
- if limit_price:
|
|
|
- order_type = "Limit"
|
|
|
- order_description = f"at ${limit_price:,.2f}"
|
|
|
- else:
|
|
|
- order_type = "Market"
|
|
|
- order_description = "at current market price"
|
|
|
-
|
|
|
- # Convert token to full symbol format for Hyperliquid
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current market price to calculate amount and for display
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if not market_data:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
- if current_price <= 0:
|
|
|
- await update.message.reply_text(f"❌ Invalid price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Validate stop loss price for short positions
|
|
|
- if stop_loss_price is not None:
|
|
|
- entry_price = limit_price if limit_price else current_price
|
|
|
- if stop_loss_price <= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"❌ Stop loss price should be ABOVE entry price for short positions\n\n"
|
|
|
- f"📊 Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"🛑 Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a higher price like: /short {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 1.05:.0f}"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate token amount based on price (market or limit)
|
|
|
- calculation_price = limit_price if limit_price else current_price
|
|
|
- token_amount = usdc_amount / calculation_price
|
|
|
-
|
|
|
- # Create confirmation message
|
|
|
- confirmation_text = f"""
|
|
|
-🔴 <b>Short Position Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: SHORT (Sell)
|
|
|
-• USDC Value: ${usdc_amount:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-• Order Type: {order_type} Order
|
|
|
-• Token Amount: {token_amount:.6f} {token}
|
|
|
-
|
|
|
-🎯 <b>Execution:</b>
|
|
|
-• Will sell {token_amount:.6f} {token} {order_description}
|
|
|
-• Est. Value: ${token_amount * calculation_price:,.2f}
|
|
|
-"""
|
|
|
-
|
|
|
- # Add stop loss information if provided
|
|
|
- if stop_loss_price is not None:
|
|
|
- confirmation_text += f"""
|
|
|
-🛑 <b>Stop Loss:</b>
|
|
|
-• Stop Price: ${stop_loss_price:,.2f}
|
|
|
-• Will be placed automatically after order fills
|
|
|
-• Protection Level: {((stop_loss_price - calculation_price) / calculation_price * 100):.1f}% above entry
|
|
|
-"""
|
|
|
-
|
|
|
- confirmation_text += "\n⚠️ <b>Are you sure you want to open this short position?</b>"
|
|
|
-
|
|
|
- # Use limit_price for callback if provided, otherwise current_price
|
|
|
- callback_price = limit_price if limit_price else current_price
|
|
|
- callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}"
|
|
|
- if limit_price:
|
|
|
- callback_data += "_limit"
|
|
|
- if stop_loss_price is not None:
|
|
|
- callback_data += f"_sl_{stop_loss_price}"
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("✅ Confirm Short", callback_data=callback_data),
|
|
|
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid USDC amount or price. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing short command: {e}")
|
|
|
-
|
|
|
- async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /exit command for closing positions."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 1:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /exit [token]\n"
|
|
|
- "Example: /exit BTC"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current positions to find the position for this token
|
|
|
- positions = self.client.get_positions()
|
|
|
- if positions is None:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
|
|
|
- return
|
|
|
-
|
|
|
- # Find the position for this token
|
|
|
- current_position = None
|
|
|
- for position in positions:
|
|
|
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
|
|
|
- current_position = position
|
|
|
- break
|
|
|
-
|
|
|
- if not current_position:
|
|
|
- await update.message.reply_text(f"📭 No open position found for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Extract position details
|
|
|
- contracts = float(current_position.get('contracts', 0))
|
|
|
- entry_price = float(current_position.get('entryPx', 0))
|
|
|
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
|
|
|
-
|
|
|
- # Determine position direction and exit details
|
|
|
- if contracts > 0:
|
|
|
- position_type = "LONG"
|
|
|
- exit_side = "sell"
|
|
|
- exit_emoji = "🔴"
|
|
|
- else:
|
|
|
- position_type = "SHORT"
|
|
|
- exit_side = "buy"
|
|
|
- exit_emoji = "🟢"
|
|
|
- contracts = abs(contracts) # Make positive for display
|
|
|
-
|
|
|
- # Get current market price
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if not market_data:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch current price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
- if current_price <= 0:
|
|
|
- await update.message.reply_text(f"❌ Invalid current price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate estimated exit value
|
|
|
- exit_value = contracts * current_price
|
|
|
-
|
|
|
- # Create confirmation message
|
|
|
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- confirmation_text = f"""
|
|
|
-{exit_emoji} <b>Exit Position Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Position Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Size: {contracts} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-• {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
|
|
|
-
|
|
|
-🎯 <b>Exit Order:</b>
|
|
|
-• Action: {exit_side.upper()} (Close {position_type})
|
|
|
-• Amount: {contracts} {token}
|
|
|
-• Est. Value: ~${exit_value:,.2f}
|
|
|
-• Order Type: Market Order
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to close this {position_type} position?</b>
|
|
|
-
|
|
|
-This will place a market {exit_side} order to close your entire {token} position.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"),
|
|
|
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing exit command: {e}")
|
|
|
-
|
|
|
- async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /coo (cancel open orders) command for a specific token."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 1:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /coo [token]\n"
|
|
|
- "Example: /coo BTC\n\n"
|
|
|
- "This command cancels ALL open orders for the specified token."
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current orders for this token
|
|
|
- all_orders = self.client.get_open_orders()
|
|
|
- if all_orders is None:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch orders to cancel {token} orders")
|
|
|
- return
|
|
|
-
|
|
|
- # Filter orders for the specific token
|
|
|
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
|
|
|
-
|
|
|
- if not token_orders:
|
|
|
- await update.message.reply_text(f"📭 No open orders found for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Create confirmation message with order details
|
|
|
- confirmation_text = f"""
|
|
|
-⚠️ <b>Cancel All {token} Orders</b>
|
|
|
-
|
|
|
-📋 <b>Orders to Cancel:</b>
|
|
|
-"""
|
|
|
-
|
|
|
- total_value = 0
|
|
|
- for order in token_orders:
|
|
|
- side = order.get('side', 'Unknown')
|
|
|
- amount = order.get('amount', 0)
|
|
|
- price = order.get('price', 0)
|
|
|
- order_id = order.get('id', 'Unknown')
|
|
|
-
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- order_value = float(amount) * float(price)
|
|
|
- total_value += order_value
|
|
|
-
|
|
|
- confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n"
|
|
|
-
|
|
|
- confirmation_text += f"""
|
|
|
-💰 <b>Total Value:</b> ${total_value:,.2f}
|
|
|
-🔢 <b>Orders Count:</b> {len(token_orders)}
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to cancel ALL {token} orders?</b>
|
|
|
-
|
|
|
-This action cannot be undone.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton(f"✅ Cancel All {token}", callback_data=f"confirm_coo_{token}"),
|
|
|
- InlineKeyboardButton("❌ Keep Orders", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid token format. Please use token symbols like BTC, ETH, etc.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing cancel orders command: {e}")
|
|
|
-
|
|
|
- async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /sl (stop loss) command for setting stop loss orders."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /sl [token] [price]\n"
|
|
|
- "Example: /sl BTC 44000\n\n"
|
|
|
- "This creates a stop loss order at the specified price."
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- stop_price = float(context.args[1])
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current positions to find the position for this token
|
|
|
- positions = self.client.get_positions()
|
|
|
- if positions is None:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
|
|
|
- return
|
|
|
-
|
|
|
- # Find the position for this token
|
|
|
- current_position = None
|
|
|
- for position in positions:
|
|
|
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
|
|
|
- current_position = position
|
|
|
- break
|
|
|
-
|
|
|
- if not current_position:
|
|
|
- await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a stop loss.")
|
|
|
- return
|
|
|
-
|
|
|
- # Extract position details
|
|
|
- contracts = float(current_position.get('contracts', 0))
|
|
|
- entry_price = float(current_position.get('entryPx', 0))
|
|
|
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
|
|
|
-
|
|
|
- # Use CCXT's side field to determine position direction
|
|
|
- side_field = current_position.get('side', '').lower()
|
|
|
-
|
|
|
- if side_field == 'long':
|
|
|
- # Long position - stop loss should be below entry price
|
|
|
- position_type = "LONG"
|
|
|
- exit_side = "sell"
|
|
|
- exit_emoji = "🔴"
|
|
|
- contracts_abs = contracts
|
|
|
-
|
|
|
- if stop_price >= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
|
|
|
- f"📊 Your {token} LONG position:\n"
|
|
|
- f"• Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
|
|
|
- )
|
|
|
- return
|
|
|
- elif side_field == 'short':
|
|
|
- # Short position - stop loss should be above entry price
|
|
|
- position_type = "SHORT"
|
|
|
- exit_side = "buy"
|
|
|
- exit_emoji = "🟢"
|
|
|
- contracts_abs = contracts # Already positive from CCXT
|
|
|
-
|
|
|
- # Debug logging for short position validation
|
|
|
- logger.info(f"🛑 Stop loss validation for SHORT: entry_price=${entry_price}, stop_price=${stop_price}, contracts={contracts}")
|
|
|
-
|
|
|
- if stop_price <= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
|
|
|
- f"📊 Your {token} SHORT position:\n"
|
|
|
- f"• Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"• Stop Price: ${stop_price:,.2f} ❌\n"
|
|
|
- f"• Contracts: {contracts} (side: {side_field})\n\n"
|
|
|
- f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}\n\n"
|
|
|
- f"🔧 Debug: stop_price ({stop_price}) <= entry_price ({entry_price}) = {stop_price <= entry_price}"
|
|
|
- )
|
|
|
- return
|
|
|
- else:
|
|
|
- logger.info(f"✅ Stop loss validation PASSED for SHORT: ${stop_price} > ${entry_price}")
|
|
|
- else:
|
|
|
- await update.message.reply_text(f"❌ Could not determine position direction for {token}. Side field: '{side_field}'")
|
|
|
- return
|
|
|
-
|
|
|
- # Get current market price for reference
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- current_price = 0
|
|
|
- if market_data:
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
-
|
|
|
- # Calculate estimated P&L at stop loss
|
|
|
- if side_field == 'long':
|
|
|
- pnl_at_stop = (stop_price - entry_price) * contracts_abs
|
|
|
- else: # short
|
|
|
- pnl_at_stop = (entry_price - stop_price) * contracts_abs
|
|
|
-
|
|
|
- # Create confirmation message
|
|
|
- pnl_emoji = "🟢" if pnl_at_stop >= 0 else "🔴"
|
|
|
-
|
|
|
- confirmation_text = f"""
|
|
|
-🛑 <b>Stop Loss Order Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Position Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-
|
|
|
-🎯 <b>Stop Loss Order:</b>
|
|
|
-• Stop Price: ${stop_price:,.2f}
|
|
|
-• Action: {exit_side.upper()} (Close {position_type})
|
|
|
-• Amount: {contracts_abs} {token}
|
|
|
-• Order Type: Limit Order
|
|
|
-• {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to set this stop loss?</b>
|
|
|
-
|
|
|
-This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton(f"✅ Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"),
|
|
|
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing stop loss command: {e}")
|
|
|
-
|
|
|
- async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /tp (take profit) command for setting take profit orders."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /tp [token] [price]\n"
|
|
|
- "Example: /tp BTC 50000\n\n"
|
|
|
- "This creates a take profit order at the specified price."
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- token = context.args[0].upper()
|
|
|
- profit_price = float(context.args[1])
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
-
|
|
|
- # Get current positions to find the position for this token
|
|
|
- positions = self.client.get_positions()
|
|
|
- if positions is None:
|
|
|
- await update.message.reply_text(f"❌ Could not fetch positions to check {token} position")
|
|
|
- return
|
|
|
-
|
|
|
- # Find the position for this token
|
|
|
- current_position = None
|
|
|
- for position in positions:
|
|
|
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
|
|
|
- current_position = position
|
|
|
- break
|
|
|
-
|
|
|
- if not current_position:
|
|
|
- await update.message.reply_text(f"📭 No open position found for {token}\n\nYou need an open position to set a take profit.")
|
|
|
- return
|
|
|
-
|
|
|
- # Extract position details
|
|
|
- contracts = float(current_position.get('contracts', 0))
|
|
|
- entry_price = float(current_position.get('entryPx', 0))
|
|
|
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
|
|
|
-
|
|
|
- # Use CCXT's side field to determine position direction
|
|
|
- side_field = current_position.get('side', '').lower()
|
|
|
-
|
|
|
- if side_field == 'long':
|
|
|
- # Long position - take profit should be above entry price
|
|
|
- position_type = "LONG"
|
|
|
- exit_side = "sell"
|
|
|
- exit_emoji = "🔴"
|
|
|
- contracts_abs = contracts
|
|
|
-
|
|
|
- if profit_price <= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n"
|
|
|
- f"📊 Your {token} LONG position:\n"
|
|
|
- f"• Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
|
|
|
- )
|
|
|
- return
|
|
|
- elif side_field == 'short':
|
|
|
- # Short position - take profit should be below entry price
|
|
|
- position_type = "SHORT"
|
|
|
- exit_side = "buy"
|
|
|
- exit_emoji = "🟢"
|
|
|
- contracts_abs = contracts # Already positive from CCXT
|
|
|
-
|
|
|
- if profit_price >= entry_price:
|
|
|
- await update.message.reply_text(
|
|
|
- f"⚠️ Take profit price should be BELOW entry price for short positions\n\n"
|
|
|
- f"📊 Your {token} SHORT position:\n"
|
|
|
- f"• Entry Price: ${entry_price:,.2f}\n"
|
|
|
- f"• Take Profit: ${profit_price:,.2f} ❌\n\n"
|
|
|
- f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
|
|
|
- )
|
|
|
- return
|
|
|
- else:
|
|
|
- await update.message.reply_text(f"❌ Could not determine position direction for {token}. Side field: '{side_field}'")
|
|
|
- return
|
|
|
-
|
|
|
- # Get current market price for reference
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- current_price = 0
|
|
|
- if market_data:
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
-
|
|
|
- # Calculate estimated P&L at take profit
|
|
|
- if side_field == 'long':
|
|
|
- pnl_at_tp = (profit_price - entry_price) * contracts_abs
|
|
|
- else: # short
|
|
|
- pnl_at_tp = (entry_price - profit_price) * contracts_abs
|
|
|
-
|
|
|
- # Create confirmation message
|
|
|
- pnl_emoji = "🟢" if pnl_at_tp >= 0 else "🔴"
|
|
|
-
|
|
|
- confirmation_text = f"""
|
|
|
-🎯 <b>Take Profit Order Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Position Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-
|
|
|
-💰 <b>Take Profit Order:</b>
|
|
|
-• Target Price: ${profit_price:,.2f}
|
|
|
-• Action: {exit_side.upper()} (Close {position_type})
|
|
|
-• Amount: {contracts_abs} {token}
|
|
|
-• Order Type: Limit Order
|
|
|
-• {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to set this take profit?</b>
|
|
|
-
|
|
|
-This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton(f"✅ Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"),
|
|
|
- InlineKeyboardButton("❌ Cancel", callback_data="cancel_order")
|
|
|
- ]
|
|
|
- ]
|
|
|
- reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
-
|
|
|
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing take profit command: {e}")
|
|
|
-
|
|
|
- async def start_order_monitoring(self):
|
|
|
- """Start the order monitoring background task."""
|
|
|
- # Safety check in case this is called before initialization is complete
|
|
|
- if not hasattr(self, 'monitoring_active'):
|
|
|
- self.monitoring_active = False
|
|
|
-
|
|
|
- if self.monitoring_active:
|
|
|
- return
|
|
|
-
|
|
|
- self.monitoring_active = True
|
|
|
- logger.info("🔄 Starting order monitoring...")
|
|
|
-
|
|
|
- # Initialize tracking data
|
|
|
- await self._initialize_order_tracking()
|
|
|
-
|
|
|
- # Start monitoring loop
|
|
|
- asyncio.create_task(self._order_monitoring_loop())
|
|
|
-
|
|
|
- async def stop_order_monitoring(self):
|
|
|
- """Stop the order monitoring background task."""
|
|
|
- # Safety check in case this is called before initialization is complete
|
|
|
- if hasattr(self, 'monitoring_active'):
|
|
|
- self.monitoring_active = False
|
|
|
- logger.info("⏹️ Stopping order monitoring...")
|
|
|
-
|
|
|
- async def _initialize_order_tracking(self):
|
|
|
- """Initialize order and position tracking."""
|
|
|
- try:
|
|
|
- # Get current open orders to initialize tracking
|
|
|
- orders = self.client.get_open_orders()
|
|
|
- if orders:
|
|
|
- self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
|
|
|
- logger.info(f"📋 Initialized tracking with {len(self.last_known_orders)} open orders")
|
|
|
-
|
|
|
- # Get current positions for P&L tracking
|
|
|
- positions = self.client.get_positions()
|
|
|
- if positions:
|
|
|
- for position in positions:
|
|
|
- symbol = position.get('symbol')
|
|
|
- contracts = float(position.get('contracts', 0))
|
|
|
- entry_price = float(position.get('entryPx', 0))
|
|
|
-
|
|
|
- if symbol and contracts != 0:
|
|
|
- self.last_known_positions[symbol] = {
|
|
|
- 'contracts': contracts,
|
|
|
- 'entry_price': entry_price
|
|
|
- }
|
|
|
- logger.info(f"📊 Initialized tracking with {len(self.last_known_positions)} positions")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error initializing order tracking: {e}")
|
|
|
-
|
|
|
- async def _order_monitoring_loop(self):
|
|
|
- """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
|
|
|
- while getattr(self, 'monitoring_active', False):
|
|
|
- try:
|
|
|
- await self._check_order_fills()
|
|
|
- await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval
|
|
|
- except asyncio.CancelledError:
|
|
|
- logger.info("🛑 Order monitoring cancelled")
|
|
|
- break
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error in order monitoring loop: {e}")
|
|
|
- await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error
|
|
|
-
|
|
|
- async def _check_order_fills(self):
|
|
|
- """Check for filled orders and send notifications."""
|
|
|
- try:
|
|
|
- # Get current orders and positions
|
|
|
- current_orders = self.client.get_open_orders() or []
|
|
|
- current_positions = self.client.get_positions() or []
|
|
|
-
|
|
|
- # Get current order IDs
|
|
|
- current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
|
|
|
-
|
|
|
- # Find filled orders (orders that were in last_known_orders but not in current_orders)
|
|
|
- filled_order_ids = self.last_known_orders - current_order_ids
|
|
|
-
|
|
|
- # Add some debugging for filled order detection
|
|
|
- if filled_order_ids:
|
|
|
- logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders: {list(filled_order_ids)}")
|
|
|
-
|
|
|
- # Log pending stop losses before processing
|
|
|
- if self.pending_stop_losses:
|
|
|
- logger.info(f"📋 Current pending stop losses: {list(self.pending_stop_losses.keys())}")
|
|
|
- for order_id in filled_order_ids:
|
|
|
- if order_id in self.pending_stop_losses:
|
|
|
- stop_loss_info = self.pending_stop_losses[order_id]
|
|
|
- logger.info(f"🛑 Will process stop loss for filled order {order_id}: {stop_loss_info['token']} @ ${stop_loss_info['stop_price']}")
|
|
|
-
|
|
|
- await self._process_filled_orders(filled_order_ids, current_positions)
|
|
|
-
|
|
|
- # Process pending stop losses for filled orders
|
|
|
- await self._process_pending_stop_losses(filled_order_ids)
|
|
|
- else:
|
|
|
- # Log if we have pending stop losses but no filled orders
|
|
|
- if self.pending_stop_losses:
|
|
|
- logger.debug(f"📋 No filled orders detected, but {len(self.pending_stop_losses)} pending stop losses remain")
|
|
|
-
|
|
|
- # Update tracking data
|
|
|
- self.last_known_orders = current_order_ids
|
|
|
- await self._update_position_tracking(current_positions)
|
|
|
-
|
|
|
- # Save state after updating tracking data
|
|
|
- self._save_bot_state()
|
|
|
-
|
|
|
- # Check price alarms
|
|
|
- await self._check_price_alarms()
|
|
|
-
|
|
|
- # Check external trades (trades made outside the bot)
|
|
|
- await self._check_external_trades()
|
|
|
-
|
|
|
- # Check stop losses (if risk management is enabled)
|
|
|
- if Config.RISK_MANAGEMENT_ENABLED:
|
|
|
- await self._check_stop_losses(current_positions)
|
|
|
-
|
|
|
- # Check deposits/withdrawals (hourly)
|
|
|
- await self._check_deposits_withdrawals()
|
|
|
-
|
|
|
- # Clean up cancelled orders from pending stop losses
|
|
|
- await self._cleanup_cancelled_stop_losses(current_order_ids)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error checking order fills: {e}")
|
|
|
-
|
|
|
- async def _process_pending_stop_losses(self, filled_order_ids: set):
|
|
|
- """Process pending stop losses for filled orders."""
|
|
|
- try:
|
|
|
- processed_any = False
|
|
|
- for order_id in filled_order_ids:
|
|
|
- if order_id in self.pending_stop_losses:
|
|
|
- stop_loss_info = self.pending_stop_losses[order_id]
|
|
|
-
|
|
|
- # Place the stop loss order
|
|
|
- await self._place_pending_stop_loss(order_id, stop_loss_info)
|
|
|
-
|
|
|
- # Remove from pending after processing
|
|
|
- del self.pending_stop_losses[order_id]
|
|
|
- processed_any = True
|
|
|
- logger.info(f"🗑️ Removed processed stop loss for order {order_id}")
|
|
|
-
|
|
|
- # Save state if any stop losses were processed
|
|
|
- if processed_any:
|
|
|
- self._save_bot_state()
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error processing pending stop losses: {e}")
|
|
|
-
|
|
|
- async def _place_pending_stop_loss(self, original_order_id: str, stop_loss_info: Dict[str, Any]):
|
|
|
- """Place a pending stop loss order."""
|
|
|
- try:
|
|
|
- token = stop_loss_info['token']
|
|
|
- symbol = stop_loss_info['symbol']
|
|
|
- stop_price = stop_loss_info['stop_price']
|
|
|
- side = stop_loss_info['side']
|
|
|
- amount = stop_loss_info['amount']
|
|
|
- order_type = stop_loss_info['order_type']
|
|
|
-
|
|
|
- logger.info(f"🛑 Placing automatic stop loss: {side} {amount:.6f} {token} @ ${stop_price}")
|
|
|
-
|
|
|
- # Place the stop loss order as a limit order
|
|
|
- order = self.client.place_limit_order(symbol, side, amount, stop_price)
|
|
|
-
|
|
|
- if order:
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
-
|
|
|
- # Send notification
|
|
|
- await self._send_stop_loss_placed_notification(token, order_type, stop_price, amount, order_id, original_order_id)
|
|
|
-
|
|
|
- logger.info(f"✅ Successfully placed automatic stop loss for {token}: Order ID {order_id}")
|
|
|
- else:
|
|
|
- # Send failure notification
|
|
|
- await self._send_stop_loss_failed_notification(token, order_type, stop_price, amount, original_order_id)
|
|
|
- logger.error(f"❌ Failed to place automatic stop loss for {token}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error placing pending stop loss: {e}")
|
|
|
- await self._send_stop_loss_failed_notification(
|
|
|
- stop_loss_info.get('token', 'Unknown'),
|
|
|
- stop_loss_info.get('order_type', 'Unknown'),
|
|
|
- stop_loss_info.get('stop_price', 0),
|
|
|
- stop_loss_info.get('amount', 0),
|
|
|
- original_order_id,
|
|
|
- str(e)
|
|
|
- )
|
|
|
-
|
|
|
- async def _cleanup_cancelled_stop_losses(self, current_order_ids: set):
|
|
|
- """Remove pending stop losses for cancelled orders."""
|
|
|
- try:
|
|
|
- # Find orders that are no longer active but were not filled
|
|
|
- orders_to_remove = []
|
|
|
-
|
|
|
- for order_id, stop_loss_info in self.pending_stop_losses.items():
|
|
|
- if order_id not in current_order_ids:
|
|
|
- # Order is no longer in open orders, check if it was cancelled (not filled)
|
|
|
- # We assume if it's not in current_order_ids and we haven't processed it as filled,
|
|
|
- # then it was likely cancelled
|
|
|
- orders_to_remove.append(order_id)
|
|
|
-
|
|
|
- # Remove cancelled orders from pending stop losses
|
|
|
- for order_id in orders_to_remove:
|
|
|
- stop_loss_info = self.pending_stop_losses[order_id]
|
|
|
- token = stop_loss_info['token']
|
|
|
-
|
|
|
- # Send notification about cancelled stop loss
|
|
|
- await self._send_stop_loss_cancelled_notification(token, stop_loss_info, order_id)
|
|
|
-
|
|
|
- # Remove from pending
|
|
|
- del self.pending_stop_losses[order_id]
|
|
|
- logger.info(f"🗑️ Removed pending stop loss for cancelled order {order_id}")
|
|
|
-
|
|
|
- # Save state if any stop losses were removed
|
|
|
- if orders_to_remove:
|
|
|
- self._save_bot_state()
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error cleaning up cancelled stop losses: {e}")
|
|
|
-
|
|
|
- async def _send_stop_loss_placed_notification(self, token: str, order_type: str, stop_price: float, amount: float, stop_order_id: str, original_order_id: str):
|
|
|
- """Send notification when stop loss is successfully placed."""
|
|
|
- try:
|
|
|
- position_type = order_type.upper()
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🛑 <b>Stop Loss Placed Automatically</b>
|
|
|
-
|
|
|
-✅ <b>Stop Loss Active</b>
|
|
|
-
|
|
|
-📊 <b>Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Stop Price: ${stop_price:,.2f}
|
|
|
-• Amount: {amount:.6f} {token}
|
|
|
-• Stop Loss Order ID: <code>{stop_order_id}</code>
|
|
|
-• Original Order ID: <code>{original_order_id}</code>
|
|
|
-
|
|
|
-🎯 <b>Protection:</b>
|
|
|
-• Status: ACTIVE ✅
|
|
|
-• Will execute if price reaches ${stop_price:,.2f}
|
|
|
-• Order Type: Limit Order
|
|
|
-
|
|
|
-💡 Your {position_type} position is now protected with automatic stop loss!
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent stop loss placed notification: {token} @ ${stop_price}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending stop loss placed notification: {e}")
|
|
|
-
|
|
|
- async def _send_stop_loss_failed_notification(self, token: str, order_type: str, stop_price: float, amount: float, original_order_id: str, error: str = None):
|
|
|
- """Send notification when stop loss placement fails."""
|
|
|
- try:
|
|
|
- position_type = order_type.upper()
|
|
|
-
|
|
|
- message = f"""
|
|
|
-⚠️ <b>Stop Loss Placement Failed</b>
|
|
|
-
|
|
|
-❌ <b>Automatic Stop Loss Failed</b>
|
|
|
-
|
|
|
-📊 <b>Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Intended Stop Price: ${stop_price:,.2f}
|
|
|
-• Amount: {amount:.6f} {token}
|
|
|
-• Original Order ID: <code>{original_order_id}</code>
|
|
|
-
|
|
|
-🚨 <b>Action Required:</b>
|
|
|
-• Your position is NOT protected
|
|
|
-• Consider manually setting stop loss: <code>/sl {token} {stop_price:.0f}</code>
|
|
|
-• Monitor your position closely
|
|
|
-
|
|
|
-{f'🔧 Error: {error}' if error else ''}
|
|
|
-
|
|
|
-💡 Use /sl command to manually set stop loss protection.
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent stop loss failed notification: {token}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending stop loss failed notification: {e}")
|
|
|
-
|
|
|
- async def _send_stop_loss_cancelled_notification(self, token: str, stop_loss_info: Dict[str, Any], order_id: str):
|
|
|
- """Send notification when stop loss is cancelled due to order cancellation."""
|
|
|
- try:
|
|
|
- position_type = stop_loss_info['order_type'].upper()
|
|
|
- stop_price = stop_loss_info['stop_price']
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🚫 <b>Stop Loss Cancelled</b>
|
|
|
-
|
|
|
-📊 <b>Original Order Cancelled</b>
|
|
|
-
|
|
|
-• Token: {token}
|
|
|
-• Position: {position_type}
|
|
|
-• Cancelled Stop Price: ${stop_price:,.2f}
|
|
|
-• Original Order ID: <code>{order_id}</code>
|
|
|
-
|
|
|
-💡 <b>Status:</b>
|
|
|
-• Pending stop loss automatically cancelled
|
|
|
-• No position protection was placed
|
|
|
-• Order was cancelled before execution
|
|
|
-
|
|
|
-🔄 If you still want to trade {token}, place a new order with stop loss protection.
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent stop loss cancelled notification: {token}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending stop loss cancelled notification: {e}")
|
|
|
-
|
|
|
- async def _check_price_alarms(self):
|
|
|
- """Check all active price alarms."""
|
|
|
- try:
|
|
|
- # Get all active alarms
|
|
|
- active_alarms = self.alarm_manager.get_all_active_alarms()
|
|
|
- if not active_alarms:
|
|
|
- return
|
|
|
-
|
|
|
- # Get unique tokens from alarms
|
|
|
- tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
|
|
|
-
|
|
|
- # Fetch current prices for all tokens
|
|
|
- price_data = {}
|
|
|
- for token in tokens_to_check:
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
-
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- current_price = market_data['ticker'].get('last')
|
|
|
- if current_price is not None:
|
|
|
- price_data[token] = float(current_price)
|
|
|
-
|
|
|
- # Check alarms against current prices
|
|
|
- triggered_alarms = self.alarm_manager.check_alarms(price_data)
|
|
|
-
|
|
|
- # Send notifications for triggered alarms
|
|
|
- for alarm in triggered_alarms:
|
|
|
- await self._send_alarm_notification(alarm)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error checking price alarms: {e}")
|
|
|
-
|
|
|
- async def _send_alarm_notification(self, alarm: Dict[str, Any]):
|
|
|
- """Send notification for triggered alarm."""
|
|
|
- try:
|
|
|
- message = self.alarm_manager.format_triggered_alarm(alarm)
|
|
|
- await self.send_message(message)
|
|
|
- logger.info(f"📢 Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending alarm notification: {e}")
|
|
|
-
|
|
|
- async def _check_external_trades(self):
|
|
|
- """Check for trades made outside the Telegram bot and update stats."""
|
|
|
- try:
|
|
|
- # Get recent fills from Hyperliquid
|
|
|
- recent_fills = self.client.get_recent_fills()
|
|
|
-
|
|
|
- if not recent_fills:
|
|
|
- return
|
|
|
-
|
|
|
- # Initialize last processed time if first run
|
|
|
- if self.last_processed_trade_time is None:
|
|
|
- # Set to current time minus 1 hour to catch recent activity
|
|
|
- self.last_processed_trade_time = datetime.now() - timedelta(hours=1)
|
|
|
-
|
|
|
- # Filter for new trades since last check
|
|
|
- new_trades = []
|
|
|
- latest_trade_time = self.last_processed_trade_time
|
|
|
-
|
|
|
- for fill in recent_fills:
|
|
|
- fill_time = fill.get('timestamp')
|
|
|
- if fill_time:
|
|
|
- # Convert timestamps to comparable format
|
|
|
- try:
|
|
|
- # Convert fill_time to datetime object for comparison
|
|
|
- if isinstance(fill_time, (int, float)):
|
|
|
- # Assume it's a unix timestamp
|
|
|
- fill_datetime = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time)
|
|
|
- else:
|
|
|
- # Try to parse as ISO string
|
|
|
- fill_datetime = datetime.fromisoformat(str(fill_time).replace('Z', '+00:00'))
|
|
|
-
|
|
|
- # Compare datetime objects
|
|
|
- if fill_datetime > self.last_processed_trade_time:
|
|
|
- new_trades.append(fill)
|
|
|
- if fill_datetime > latest_trade_time:
|
|
|
- latest_trade_time = fill_datetime
|
|
|
- except Exception as timestamp_error:
|
|
|
- logger.warning(f"⚠️ Error processing timestamp {fill_time}: {timestamp_error}")
|
|
|
- continue
|
|
|
-
|
|
|
- if not new_trades:
|
|
|
- return
|
|
|
-
|
|
|
- # Process new trades
|
|
|
- for trade in new_trades:
|
|
|
- # Log trade processing for debugging
|
|
|
- trade_id = trade.get('id', 'external')
|
|
|
- symbol = trade.get('symbol', 'Unknown')
|
|
|
- side = trade.get('side', 'Unknown')
|
|
|
- amount = trade.get('amount', 0)
|
|
|
- price = trade.get('price', 0)
|
|
|
-
|
|
|
- logger.info(f"🔍 Processing trade: {trade_id} - {side} {amount} {symbol} @ ${price}")
|
|
|
-
|
|
|
- await self._process_external_trade(trade)
|
|
|
-
|
|
|
- # Update last processed time (keep as datetime object)
|
|
|
- self.last_processed_trade_time = latest_trade_time
|
|
|
-
|
|
|
- # Save state after updating last processed time
|
|
|
- self._save_bot_state()
|
|
|
-
|
|
|
- if new_trades:
|
|
|
- logger.info(f"📊 Processed {len(new_trades)} external trades")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error checking external trades: {e}")
|
|
|
-
|
|
|
- async def _check_deposits_withdrawals(self):
|
|
|
- """Check for deposits and withdrawals to maintain accurate P&L tracking."""
|
|
|
- try:
|
|
|
- # Check if it's time to run (hourly check)
|
|
|
- current_time = datetime.now()
|
|
|
-
|
|
|
- if self.last_deposit_withdrawal_check is not None:
|
|
|
- time_since_last_check = (current_time - self.last_deposit_withdrawal_check).total_seconds()
|
|
|
- if time_since_last_check < self.deposit_withdrawal_check_interval:
|
|
|
- return # Not time to check yet
|
|
|
-
|
|
|
- logger.info("🔍 Checking for deposits and withdrawals...")
|
|
|
-
|
|
|
- # Initialize last check time if first run
|
|
|
- if self.last_deposit_withdrawal_check is None:
|
|
|
- # Set to 24 hours ago to catch recent activity
|
|
|
- self.last_deposit_withdrawal_check = current_time - timedelta(hours=24)
|
|
|
-
|
|
|
- # Calculate timestamp for API calls (last check time)
|
|
|
- since_timestamp = int(self.last_deposit_withdrawal_check.timestamp() * 1000) # Hyperliquid expects milliseconds
|
|
|
-
|
|
|
- # Track new deposits/withdrawals
|
|
|
- new_deposits = 0
|
|
|
- new_withdrawals = 0
|
|
|
-
|
|
|
- # Check if sync_client is available
|
|
|
- if not hasattr(self.client, 'sync_client') or not self.client.sync_client:
|
|
|
- logger.warning("⚠️ CCXT sync_client not available for deposit/withdrawal checking")
|
|
|
- self.last_deposit_withdrawal_check = current_time
|
|
|
- return
|
|
|
-
|
|
|
- # Set up user parameter for Hyperliquid API calls
|
|
|
- params = {}
|
|
|
- if Config.HYPERLIQUID_WALLET_ADDRESS:
|
|
|
- wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
|
|
|
- params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
|
|
|
- else:
|
|
|
- logger.warning("⚠️ No wallet address configured for deposit/withdrawal checking")
|
|
|
- self.last_deposit_withdrawal_check = current_time
|
|
|
- return
|
|
|
-
|
|
|
- # Check for deposits
|
|
|
- try:
|
|
|
- deposits = self.client.sync_client.fetch_deposits(code='USDC', since=since_timestamp, params=params)
|
|
|
- if deposits:
|
|
|
- for deposit in deposits:
|
|
|
- amount = float(deposit.get('amount', 0))
|
|
|
- timestamp = deposit.get('datetime', datetime.now().isoformat())
|
|
|
- deposit_id = deposit.get('id', 'unknown')
|
|
|
-
|
|
|
- # Record in stats to adjust P&L calculations
|
|
|
- self.stats.record_deposit(amount, timestamp, deposit_id)
|
|
|
- new_deposits += 1
|
|
|
-
|
|
|
- # Send notification
|
|
|
- await self._send_deposit_notification(amount, timestamp)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.warning(f"⚠️ Error fetching deposits: {e}")
|
|
|
-
|
|
|
- # Check for withdrawals
|
|
|
- try:
|
|
|
- withdrawals = self.client.sync_client.fetch_withdrawals(code='USDC', since=since_timestamp, params=params)
|
|
|
- if withdrawals:
|
|
|
- for withdrawal in withdrawals:
|
|
|
- amount = float(withdrawal.get('amount', 0))
|
|
|
- timestamp = withdrawal.get('datetime', datetime.now().isoformat())
|
|
|
- withdrawal_id = withdrawal.get('id', 'unknown')
|
|
|
-
|
|
|
- # Record in stats to adjust P&L calculations
|
|
|
- self.stats.record_withdrawal(amount, timestamp, withdrawal_id)
|
|
|
- new_withdrawals += 1
|
|
|
-
|
|
|
- # Send notification
|
|
|
- await self._send_withdrawal_notification(amount, timestamp)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.warning(f"⚠️ Error fetching withdrawals: {e}")
|
|
|
-
|
|
|
- # Update last check time
|
|
|
- self.last_deposit_withdrawal_check = current_time
|
|
|
-
|
|
|
- # Save state after updating last check time
|
|
|
- self._save_bot_state()
|
|
|
-
|
|
|
- if new_deposits > 0 or new_withdrawals > 0:
|
|
|
- logger.info(f"💰 Processed {new_deposits} deposits and {new_withdrawals} withdrawals")
|
|
|
-
|
|
|
- # Get updated balance adjustments summary
|
|
|
- adjustments = self.stats.get_balance_adjustments_summary()
|
|
|
- logger.info(f"📊 Total adjustments: ${adjustments['net_adjustment']:,.2f} net ({adjustments['adjustment_count']} total)")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error checking deposits/withdrawals: {e}")
|
|
|
-
|
|
|
- async def _send_deposit_notification(self, amount: float, timestamp: str):
|
|
|
- """Send notification for detected deposit."""
|
|
|
- try:
|
|
|
- time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
|
|
|
-
|
|
|
- message = f"""
|
|
|
-💰 <b>Deposit Detected</b>
|
|
|
-
|
|
|
-💵 <b>Amount:</b> ${amount:,.2f} USDC
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-
|
|
|
-📊 <b>P&L Impact:</b>
|
|
|
-• Initial balance adjusted to maintain accurate P&L
|
|
|
-• Trading statistics unaffected by balance change
|
|
|
-• This deposit will not show as trading profit
|
|
|
-
|
|
|
-✅ <b>Balance tracking updated automatically</b>
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📱 Sent deposit notification: ${amount:,.2f}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending deposit notification: {e}")
|
|
|
-
|
|
|
- async def _send_withdrawal_notification(self, amount: float, timestamp: str):
|
|
|
- """Send notification for detected withdrawal."""
|
|
|
- try:
|
|
|
- time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
|
|
|
-
|
|
|
- message = f"""
|
|
|
-💸 <b>Withdrawal Detected</b>
|
|
|
-
|
|
|
-💵 <b>Amount:</b> ${amount:,.2f} USDC
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-
|
|
|
-📊 <b>P&L Impact:</b>
|
|
|
-• Initial balance adjusted to maintain accurate P&L
|
|
|
-• Trading statistics unaffected by balance change
|
|
|
-• This withdrawal will not show as trading loss
|
|
|
-
|
|
|
-✅ <b>Balance tracking updated automatically</b>
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📱 Sent withdrawal notification: ${amount:,.2f}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending withdrawal notification: {e}")
|
|
|
-
|
|
|
- async def _process_external_trade(self, trade: Dict[str, Any]):
|
|
|
- """Process an individual external trade and determine if it's opening or closing a position."""
|
|
|
- try:
|
|
|
- # Extract trade information
|
|
|
- symbol = trade.get('symbol', '')
|
|
|
- side = trade.get('side', '')
|
|
|
- amount = float(trade.get('amount', 0))
|
|
|
- price = float(trade.get('price', 0))
|
|
|
- trade_id = trade.get('id', 'external')
|
|
|
- timestamp = trade.get('timestamp', '')
|
|
|
-
|
|
|
- if not all([symbol, side, amount, price]):
|
|
|
- return
|
|
|
-
|
|
|
- # Skip bot-generated trades to prevent double processing
|
|
|
- if trade_id in self.bot_trade_ids:
|
|
|
- logger.debug(f"🤖 Skipping bot-generated trade: {trade_id}")
|
|
|
- return
|
|
|
-
|
|
|
- # Record trade in stats and get action type using enhanced tracking
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external")
|
|
|
-
|
|
|
- # Send enhanced notification based on action type
|
|
|
- await self._send_enhanced_trade_notification(symbol, side, amount, price, action_type, timestamp)
|
|
|
-
|
|
|
- logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price} ({action_type})")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error processing external trade: {e}")
|
|
|
-
|
|
|
- async def _send_enhanced_trade_notification(self, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str = None):
|
|
|
- """Send enhanced trade notification based on position action type."""
|
|
|
- try:
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- position = self.stats.get_enhanced_position_state(symbol)
|
|
|
-
|
|
|
- if timestamp is None:
|
|
|
- time_str = datetime.now().strftime('%H:%M:%S')
|
|
|
- else:
|
|
|
- try:
|
|
|
- time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
|
- time_str = time_obj.strftime('%H:%M:%S')
|
|
|
- except:
|
|
|
- time_str = "Unknown"
|
|
|
-
|
|
|
- # Handle different action types
|
|
|
- if action_type in ['long_opened', 'short_opened']:
|
|
|
- await self._send_position_opened_notification(token, side, amount, price, action_type, time_str)
|
|
|
-
|
|
|
- elif action_type in ['long_increased', 'short_increased']:
|
|
|
- await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str)
|
|
|
-
|
|
|
- elif action_type in ['long_reduced', 'short_reduced']:
|
|
|
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
|
|
|
- await self._send_position_reduced_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
|
|
|
-
|
|
|
- elif action_type in ['long_closed', 'short_closed']:
|
|
|
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
|
|
|
- await self._send_position_closed_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
|
|
|
-
|
|
|
- elif action_type in ['long_closed_and_short_opened', 'short_closed_and_long_opened']:
|
|
|
- await self._send_position_flipped_notification(token, side, amount, price, action_type, time_str)
|
|
|
-
|
|
|
- else:
|
|
|
- # Fallback to generic notification
|
|
|
- await self._send_external_trade_notification({
|
|
|
- 'symbol': symbol,
|
|
|
- 'side': side,
|
|
|
- 'amount': amount,
|
|
|
- 'price': price,
|
|
|
- 'timestamp': timestamp or datetime.now().isoformat()
|
|
|
- })
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending enhanced trade notification: {e}")
|
|
|
-
|
|
|
- async def _send_position_opened_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
|
|
|
- """Send notification for newly opened position."""
|
|
|
- position_type = "LONG" if action_type == 'long_opened' else "SHORT"
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- trade_value = amount * price
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🚀 <b>Position Opened</b>
|
|
|
-
|
|
|
-📊 <b>New {position_type} Position:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: {position_type}
|
|
|
-• Entry Size: {amount} {token}
|
|
|
-• Entry Price: ${price:,.2f}
|
|
|
-• Position Value: ${trade_value:,.2f}
|
|
|
-
|
|
|
-{side_emoji} <b>Trade Details:</b>
|
|
|
-• Side: {side.upper()}
|
|
|
-• Order Type: Market/Limit
|
|
|
-• Status: OPENED ✅
|
|
|
-
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-📈 <b>Note:</b> New {position_type} position established
|
|
|
-📊 Use /positions to view current holdings
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Position opened: {token} {position_type} {amount} @ ${price}")
|
|
|
-
|
|
|
- async def _send_position_increased_notification(self, token: str, side: str, amount: float, price: float, position: Dict, action_type: str, time_str: str):
|
|
|
- """Send notification for position increase (additional entry)."""
|
|
|
- position_type = "LONG" if action_type == 'long_increased' else "SHORT"
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
-
|
|
|
- total_size = abs(position['contracts'])
|
|
|
- avg_entry = position['avg_entry_price']
|
|
|
- entry_count = position['entry_count']
|
|
|
- total_value = total_size * avg_entry
|
|
|
-
|
|
|
- message = f"""
|
|
|
-📈 <b>Position Increased</b>
|
|
|
-
|
|
|
-📊 <b>{position_type} Position Updated:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: {position_type}
|
|
|
-• Added Size: {amount} {token} @ ${price:,.2f}
|
|
|
-• New Total Size: {total_size} {token}
|
|
|
-• Average Entry: ${avg_entry:,.2f}
|
|
|
-
|
|
|
-{side_emoji} <b>Position Summary:</b>
|
|
|
-• Total Value: ${total_value:,.2f}
|
|
|
-• Entry Points: {entry_count}
|
|
|
-• Last Entry: ${price:,.2f}
|
|
|
-• Status: INCREASED ⬆️
|
|
|
-
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-💡 <b>Strategy:</b> Multiple entry averaging
|
|
|
-📊 Use /positions for complete position details
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Position increased: {token} {position_type} +{amount} @ ${price} (total: {total_size})")
|
|
|
-
|
|
|
- async def _send_position_reduced_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
|
|
|
- """Send notification for partial position close."""
|
|
|
- position_type = "LONG" if action_type == 'long_reduced' else "SHORT"
|
|
|
-
|
|
|
- remaining_size = abs(position['contracts'])
|
|
|
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
|
|
|
- pnl = pnl_data['pnl']
|
|
|
- pnl_percent = pnl_data['pnl_percent']
|
|
|
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- # Calculate ROE (Return on Equity) for partial close
|
|
|
- cost_basis = amount * avg_entry
|
|
|
- roe = (pnl / cost_basis) * 100 if cost_basis > 0 else 0
|
|
|
-
|
|
|
- partial_value = amount * price
|
|
|
-
|
|
|
- message = f"""
|
|
|
-📉 <b>Position Partially Closed</b>
|
|
|
-
|
|
|
-📊 <b>{position_type} Partial Exit:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: {position_type}
|
|
|
-• Closed Size: {amount} {token}
|
|
|
-• Exit Price: ${price:,.2f}
|
|
|
-• Remaining Size: {remaining_size} {token}
|
|
|
-
|
|
|
-{pnl_emoji} <b>Partial P&L:</b>
|
|
|
-• Entry Price: ${avg_entry:,.2f}
|
|
|
-• Exit Value: ${partial_value:,.2f}
|
|
|
-• P&L: ${pnl:,.2f} ({roe:+.2f}% ROE)
|
|
|
-• Result: {"PROFIT" if pnl >= 0 else "LOSS"}
|
|
|
-
|
|
|
-💰 <b>Position Status:</b>
|
|
|
-• Status: PARTIALLY CLOSED 📉
|
|
|
-• Take Profit Strategy: Active
|
|
|
-
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-📊 Use /positions to view remaining position
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Position reduced: {token} {position_type} -{amount} @ ${price} P&L: ${pnl:.2f}")
|
|
|
-
|
|
|
- async def _send_position_closed_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
|
|
|
- """Send notification for fully closed position."""
|
|
|
- position_type = "LONG" if action_type == 'long_closed' else "SHORT"
|
|
|
-
|
|
|
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
|
|
|
- pnl = pnl_data['pnl']
|
|
|
- pnl_percent = pnl_data['pnl_percent']
|
|
|
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- # Calculate ROE (Return on Equity) for consistency
|
|
|
- cost_basis = amount * avg_entry
|
|
|
- roe = (pnl / cost_basis) * 100 if cost_basis > 0 else 0
|
|
|
-
|
|
|
- entry_count = position.get('entry_count', 1)
|
|
|
- exit_value = amount * price
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🎯 <b>Position Fully Closed</b>
|
|
|
-
|
|
|
-📊 <b>{position_type} Position Summary:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: {position_type}
|
|
|
-• Total Size: {amount} {token}
|
|
|
-• Average Entry: ${avg_entry:,.2f}
|
|
|
-• Exit Price: ${price:,.2f}
|
|
|
-• Exit Value: ${exit_value:,.2f}
|
|
|
-
|
|
|
-{pnl_emoji} <b>Total P&L:</b>
|
|
|
-• P&L: ${pnl:,.2f} ({roe:+.2f}% ROE)
|
|
|
-• Result: {"PROFIT" if pnl >= 0 else "LOSS"}
|
|
|
-• Entry Points Used: {entry_count}
|
|
|
-
|
|
|
-✅ <b>Trade Complete:</b>
|
|
|
-• Status: FULLY CLOSED 🎯
|
|
|
-• Position: FLAT
|
|
|
-
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-📊 Use /stats to view updated performance
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Position closed: {token} {position_type} {amount} @ ${price} Total P&L: ${pnl:.2f}")
|
|
|
-
|
|
|
- async def _send_position_flipped_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
|
|
|
- """Send notification for position flip (close and reverse)."""
|
|
|
- if action_type == 'long_closed_and_short_opened':
|
|
|
- old_type = "LONG"
|
|
|
- new_type = "SHORT"
|
|
|
- else:
|
|
|
- old_type = "SHORT"
|
|
|
- new_type = "LONG"
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🔄 <b>Position Flipped</b>
|
|
|
-
|
|
|
-📊 <b>Direction Change:</b>
|
|
|
-• Token: {token}
|
|
|
-• Previous: {old_type} position
|
|
|
-• New: {new_type} position
|
|
|
-• Size: {amount} {token}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-
|
|
|
-🎯 <b>Trade Summary:</b>
|
|
|
-• {old_type} position: CLOSED ✅
|
|
|
-• {new_type} position: OPENED 🚀
|
|
|
-• Flip Price: ${price:,.2f}
|
|
|
-• Status: POSITION REVERSED
|
|
|
-
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-💡 <b>Strategy:</b> Directional change
|
|
|
-📊 Use /positions to view new position
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Position flipped: {token} {old_type} -> {new_type} @ ${price}")
|
|
|
-
|
|
|
- async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /monitoring command to show monitoring status."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- # Get alarm statistics
|
|
|
- alarm_stats = self.alarm_manager.get_statistics()
|
|
|
-
|
|
|
- # Get balance adjustments info
|
|
|
- adjustments_summary = self.stats.get_balance_adjustments_summary()
|
|
|
- last_deposit_check = "Never"
|
|
|
- next_deposit_check = "Unknown"
|
|
|
-
|
|
|
- if hasattr(self, 'last_deposit_withdrawal_check') and self.last_deposit_withdrawal_check:
|
|
|
- last_deposit_check = self.last_deposit_withdrawal_check.strftime('%H:%M:%S')
|
|
|
- next_check_time = self.last_deposit_withdrawal_check + timedelta(seconds=self.deposit_withdrawal_check_interval)
|
|
|
- next_deposit_check = next_check_time.strftime('%H:%M:%S')
|
|
|
-
|
|
|
- # Safety checks for monitoring attributes
|
|
|
- monitoring_active = getattr(self, 'monitoring_active', False)
|
|
|
- last_known_orders = getattr(self, 'last_known_orders', set())
|
|
|
- last_known_positions = getattr(self, 'last_known_positions', {})
|
|
|
- deposit_withdrawal_check_interval = getattr(self, 'deposit_withdrawal_check_interval', 3600)
|
|
|
-
|
|
|
- status_text = f"""
|
|
|
-🔄 <b>System Monitoring Status</b>
|
|
|
-
|
|
|
-📊 <b>Order Monitoring:</b>
|
|
|
-• Active: {'✅ Yes' if monitoring_active else '❌ No'}
|
|
|
-• Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
|
|
|
-• Orders Tracked: {len(last_known_orders)}
|
|
|
-• Positions Tracked: {len(last_known_positions)}
|
|
|
-• Pending Stop Losses: {len(getattr(self, 'pending_stop_losses', {}))}
|
|
|
-
|
|
|
-💰 <b>Deposit/Withdrawal Monitoring:</b>
|
|
|
-• Check Interval: {deposit_withdrawal_check_interval // 3600} hour(s)
|
|
|
-• Last Check: {last_deposit_check}
|
|
|
-• Next Check: {next_deposit_check}
|
|
|
-• Total Adjustments: {adjustments_summary['adjustment_count']}
|
|
|
-• Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
|
|
|
-
|
|
|
-🔔 <b>Price Alarms:</b>
|
|
|
-• Active Alarms: {alarm_stats['total_active']}
|
|
|
-• Triggered Today: {alarm_stats['total_triggered']}
|
|
|
-• Tokens Monitored: {alarm_stats['tokens_tracked']}
|
|
|
-• Next Alarm ID: {alarm_stats['next_id']}
|
|
|
-
|
|
|
-🔄 <b>External Trade Monitoring:</b>
|
|
|
-• Last Check: {self.last_processed_trade_time or 'Not started'}
|
|
|
-• Auto Stats Update: ✅ Enabled
|
|
|
-• External Notifications: ✅ Enabled
|
|
|
-
|
|
|
-🛡️ <b>Risk Management:</b>
|
|
|
-• Automatic Stop Loss: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
|
|
|
-• Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
|
|
|
-• Position Monitoring: {'✅ Active' if Config.RISK_MANAGEMENT_ENABLED else '❌ Inactive'}
|
|
|
-• Order-based Stop Loss: ✅ Enabled
|
|
|
-
|
|
|
-📈 <b>Notifications:</b>
|
|
|
-• 🚀 Position Opened/Increased
|
|
|
-• 📉 Position Partially/Fully Closed
|
|
|
-• 🎯 P&L Calculations
|
|
|
-• 🔔 Price Alarm Triggers
|
|
|
-• 🔄 External Trade Detection
|
|
|
-• 💰 Deposit/Withdrawal Detection
|
|
|
-• 🛑 Automatic Stop Loss Triggers
|
|
|
-• 🛑 Order-based Stop Loss Placement
|
|
|
-
|
|
|
-💾 <b>Bot State Persistence:</b>
|
|
|
-• Pending Stop Losses: Saved to disk
|
|
|
-• Order Tracking: Saved to disk
|
|
|
-• External Trade Times: Saved to disk
|
|
|
-• Deposit Check Times: Saved to disk
|
|
|
-• State File: bot_state.json
|
|
|
-• Survives Bot Restarts: ✅ Yes
|
|
|
-
|
|
|
-⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
-
|
|
|
-💡 <b>Monitoring Features:</b>
|
|
|
-• Real-time order fill detection
|
|
|
-• Automatic P&L calculation
|
|
|
-• Position change tracking
|
|
|
-• Price alarm monitoring
|
|
|
-• External trade monitoring
|
|
|
-• Deposit/withdrawal tracking
|
|
|
-• Auto stats synchronization
|
|
|
-• Order-based stop loss placement
|
|
|
-• Instant Telegram notifications
|
|
|
- """
|
|
|
-
|
|
|
- if alarm_stats['token_breakdown']:
|
|
|
- status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
|
|
|
- for token, count in alarm_stats['token_breakdown'].items():
|
|
|
- status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
|
|
|
-
|
|
|
- await update.message.reply_text(status_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /alarm command for price alerts."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if not context.args or len(context.args) == 0:
|
|
|
- # No arguments - list all alarms
|
|
|
- alarms = self.alarm_manager.get_all_active_alarms()
|
|
|
- message = self.alarm_manager.format_alarm_list(alarms)
|
|
|
- await update.message.reply_text(message, parse_mode='HTML')
|
|
|
- return
|
|
|
-
|
|
|
- elif len(context.args) == 1:
|
|
|
- arg = context.args[0]
|
|
|
-
|
|
|
- # Check if argument is a number (alarm ID to remove)
|
|
|
- try:
|
|
|
- alarm_id = int(arg)
|
|
|
- # Remove alarm by ID
|
|
|
- if self.alarm_manager.remove_alarm(alarm_id):
|
|
|
- await update.message.reply_text(f"✅ Alarm ID {alarm_id} has been removed.")
|
|
|
- else:
|
|
|
- await update.message.reply_text(f"❌ Alarm ID {alarm_id} not found.")
|
|
|
- return
|
|
|
- except ValueError:
|
|
|
- # Not a number, treat as token
|
|
|
- token = arg.upper()
|
|
|
- alarms = self.alarm_manager.get_alarms_by_token(token)
|
|
|
- message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
|
|
|
- await update.message.reply_text(message, parse_mode='HTML')
|
|
|
- return
|
|
|
-
|
|
|
- elif len(context.args) == 2:
|
|
|
- # Set new alarm: /alarm TOKEN PRICE
|
|
|
- token = context.args[0].upper()
|
|
|
- target_price = float(context.args[1])
|
|
|
-
|
|
|
- # Get current market price
|
|
|
- symbol = f"{token}/USDC:USDC"
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
-
|
|
|
- if not market_data or not market_data.get('ticker'):
|
|
|
- await update.message.reply_text(f"❌ Could not fetch current price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
- if current_price <= 0:
|
|
|
- await update.message.reply_text(f"❌ Invalid current price for {token}")
|
|
|
- return
|
|
|
-
|
|
|
- # Create the alarm
|
|
|
- alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
|
|
|
-
|
|
|
- # Format confirmation message
|
|
|
- direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
|
|
|
- price_diff = abs(target_price - current_price)
|
|
|
- price_diff_percent = (price_diff / current_price) * 100
|
|
|
-
|
|
|
- message = f"""
|
|
|
-✅ <b>Price Alarm Created</b>
|
|
|
-
|
|
|
-📊 <b>Alarm Details:</b>
|
|
|
-• Alarm ID: {alarm['id']}
|
|
|
-• Token: {token}
|
|
|
-• Target Price: ${target_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-• Direction: {alarm['direction'].upper()}
|
|
|
-
|
|
|
-{direction_emoji} <b>Alert Condition:</b>
|
|
|
-Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
-
|
|
|
-💰 <b>Price Difference:</b>
|
|
|
-• Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
|
|
|
-• Status: ACTIVE ✅
|
|
|
-
|
|
|
-⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
-
|
|
|
-💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
|
|
|
- """
|
|
|
-
|
|
|
- await update.message.reply_text(message.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- else:
|
|
|
- # Too many arguments
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Invalid usage. Examples:\n\n"
|
|
|
- "• <code>/alarm</code> - List all alarms\n"
|
|
|
- "• <code>/alarm BTC</code> - List BTC alarms\n"
|
|
|
- "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
|
|
|
- "• <code>/alarm 3</code> - Remove alarm ID 3",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
-
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid price format. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing alarm command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in alarm command: {e}")
|
|
|
-
|
|
|
- async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /logs command to show log file statistics and cleanup options."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Check for cleanup argument
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
- if context.args[0].lower() == 'cleanup':
|
|
|
- # Get days parameter (default 30)
|
|
|
- days_to_keep = 30
|
|
|
- if len(context.args) >= 2:
|
|
|
- try:
|
|
|
- days_to_keep = int(context.args[1])
|
|
|
- except ValueError:
|
|
|
- await update.message.reply_text("❌ Invalid number of days. Using default (30).")
|
|
|
-
|
|
|
- # Perform cleanup
|
|
|
- await update.message.reply_text(f"🧹 Cleaning up log files older than {days_to_keep} days...")
|
|
|
- cleanup_logs(days_to_keep)
|
|
|
- await update.message.reply_text(f"✅ Log cleanup completed!")
|
|
|
- return
|
|
|
-
|
|
|
- # Show log statistics
|
|
|
- log_stats_text = format_log_stats()
|
|
|
-
|
|
|
- # Add additional info
|
|
|
- status_text = f"""
|
|
|
-📊 <b>System Logging Status</b>
|
|
|
-
|
|
|
-{log_stats_text}
|
|
|
-
|
|
|
-📈 <b>Log Configuration:</b>
|
|
|
-• Log Level: {Config.LOG_LEVEL}
|
|
|
-• Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
|
|
|
-• Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
-
|
|
|
-💡 <b>Log Management:</b>
|
|
|
-• <code>/logs cleanup</code> - Clean old logs (30 days)
|
|
|
-• <code>/logs cleanup 7</code> - Clean logs older than 7 days
|
|
|
-• Log rotation happens automatically
|
|
|
-• Old backups are removed automatically
|
|
|
-
|
|
|
-🔧 <b>Configuration:</b>
|
|
|
-• Rotation Type: {Config.LOG_ROTATION_TYPE}
|
|
|
-• Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
|
|
|
-• Backup Count: {Config.LOG_BACKUP_COUNT}
|
|
|
- """
|
|
|
-
|
|
|
- await update.message.reply_text(status_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing logs command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in logs command: {e}")
|
|
|
-
|
|
|
- async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /performance command to show token performance ranking or detailed stats."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Check if specific token is requested
|
|
|
- if context.args and len(context.args) >= 1:
|
|
|
- # Detailed performance for specific token
|
|
|
- token = context.args[0].upper()
|
|
|
- await self._show_token_performance(update, token)
|
|
|
- else:
|
|
|
- # Show token performance ranking
|
|
|
- await self._show_performance_ranking(update)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing performance command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in performance command: {e}")
|
|
|
-
|
|
|
- async def _show_performance_ranking(self, update: Update):
|
|
|
- """Show token performance ranking (compressed view)."""
|
|
|
- token_performance = self.stats.get_token_performance()
|
|
|
-
|
|
|
- if not token_performance:
|
|
|
- await update.message.reply_text(
|
|
|
- "📊 <b>Token Performance</b>\n\n"
|
|
|
- "📭 No trading data available yet.\n\n"
|
|
|
- "💡 Performance tracking starts after your first completed trades.\n"
|
|
|
- "Use /long or /short to start trading!",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Sort tokens by total P&L (best to worst)
|
|
|
- sorted_tokens = sorted(
|
|
|
- token_performance.items(),
|
|
|
- key=lambda x: x[1]['total_pnl'],
|
|
|
- reverse=True
|
|
|
- )
|
|
|
-
|
|
|
- performance_text = "🏆 <b>Token Performance Ranking</b>\n\n"
|
|
|
-
|
|
|
- # Add ranking with emojis
|
|
|
- for i, (token, stats) in enumerate(sorted_tokens, 1):
|
|
|
- # Ranking emoji
|
|
|
- if i == 1:
|
|
|
- rank_emoji = "🥇"
|
|
|
- elif i == 2:
|
|
|
- rank_emoji = "🥈"
|
|
|
- elif i == 3:
|
|
|
- rank_emoji = "🥉"
|
|
|
- else:
|
|
|
- rank_emoji = f"#{i}"
|
|
|
-
|
|
|
- # P&L emoji
|
|
|
- pnl_emoji = "🟢" if stats['total_pnl'] >= 0 else "🔴"
|
|
|
-
|
|
|
- # Format the line
|
|
|
- performance_text += f"{rank_emoji} <b>{token}</b>\n"
|
|
|
- performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n"
|
|
|
- performance_text += f" 📊 Trades: {stats['completed_trades']}"
|
|
|
-
|
|
|
- # Add win rate if there are completed trades
|
|
|
- if stats['completed_trades'] > 0:
|
|
|
- performance_text += f" | Win: {stats['win_rate']:.0f}%"
|
|
|
-
|
|
|
- performance_text += "\n\n"
|
|
|
-
|
|
|
- # Add summary
|
|
|
- total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
|
|
|
- total_trades = sum(stats['completed_trades'] for stats in token_performance.values())
|
|
|
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
-
|
|
|
- performance_text += f"💼 <b>Portfolio Summary:</b>\n"
|
|
|
- performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
- performance_text += f" 📈 Tokens Traded: {len(token_performance)}\n"
|
|
|
- performance_text += f" 🔄 Completed Trades: {total_trades}\n\n"
|
|
|
-
|
|
|
- performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
|
|
|
-
|
|
|
- await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- async def _show_token_performance(self, update: Update, token: str):
|
|
|
- """Show detailed performance for a specific token."""
|
|
|
- token_stats = self.stats.get_token_detailed_stats(token)
|
|
|
-
|
|
|
- # Check if token has any data
|
|
|
- if token_stats.get('total_trades', 0) == 0:
|
|
|
- await update.message.reply_text(
|
|
|
- f"📊 <b>{token} Performance</b>\n\n"
|
|
|
- f"📭 No trading history found for {token}.\n\n"
|
|
|
- f"💡 Start trading {token} with:\n"
|
|
|
- f"• <code>/long {token} 100</code>\n"
|
|
|
- f"• <code>/short {token} 100</code>\n\n"
|
|
|
- f"🔄 Use <code>/performance</code> to see all token rankings.",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Check if there's a message (no completed trades)
|
|
|
- if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
|
|
|
- await update.message.reply_text(
|
|
|
- f"📊 <b>{token} Performance</b>\n\n"
|
|
|
- f"{token_stats['message']}\n\n"
|
|
|
- f"📈 <b>Current Activity:</b>\n"
|
|
|
- f"• Total Trades: {token_stats['total_trades']}\n"
|
|
|
- f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
|
|
|
- f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
|
|
|
- f"• Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
|
|
|
- f"💡 Complete some trades to see P&L statistics!\n"
|
|
|
- f"🔄 Use <code>/performance</code> to see all token rankings.",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Detailed stats display
|
|
|
- pnl_emoji = "🟢" if token_stats['total_pnl'] >= 0 else "🔴"
|
|
|
-
|
|
|
- performance_text = f"""
|
|
|
-📊 <b>{token} Detailed Performance</b>
|
|
|
-
|
|
|
-💰 <b>P&L Summary:</b>
|
|
|
-• {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
|
|
|
-• 💵 Total Volume: ${token_stats['completed_volume']:,.2f}
|
|
|
-• 📈 Expectancy: ${token_stats['expectancy']:,.2f}
|
|
|
-
|
|
|
-📊 <b>Trading Activity:</b>
|
|
|
-• Total Trades: {token_stats['total_trades']}
|
|
|
-• Completed: {token_stats['completed_trades']}
|
|
|
-• Buy Orders: {token_stats['buy_trades']}
|
|
|
-• Sell Orders: {token_stats['sell_trades']}
|
|
|
-
|
|
|
-🏆 <b>Performance Metrics:</b>
|
|
|
-• Win Rate: {token_stats['win_rate']:.1f}%
|
|
|
-• Profit Factor: {token_stats['profit_factor']:.2f}
|
|
|
-• Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
|
|
|
-
|
|
|
-💡 <b>Best/Worst:</b>
|
|
|
-• Largest Win: ${token_stats['largest_win']:,.2f}
|
|
|
-• Largest Loss: ${token_stats['largest_loss']:,.2f}
|
|
|
-• Avg Win: ${token_stats['avg_win']:,.2f}
|
|
|
-• Avg Loss: ${token_stats['avg_loss']:,.2f}
|
|
|
- """
|
|
|
-
|
|
|
- # Add recent trades if available
|
|
|
- if token_stats.get('recent_trades'):
|
|
|
- performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
|
|
|
- for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
|
|
|
- trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
|
|
|
- side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
|
|
|
- pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
|
|
|
-
|
|
|
- performance_text += f"• {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
|
|
|
-
|
|
|
- performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
|
|
|
-
|
|
|
- await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /daily command to show daily performance stats."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- daily_stats = self.stats.get_daily_stats(10)
|
|
|
-
|
|
|
- if not daily_stats:
|
|
|
- await update.message.reply_text(
|
|
|
- "📅 <b>Daily Performance</b>\n\n"
|
|
|
- "📭 No daily performance data available yet.\n\n"
|
|
|
- "💡 Daily stats are calculated from completed trades.\n"
|
|
|
- "Start trading to see daily performance!",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- daily_text = "📅 <b>Daily Performance (Last 10 Days)</b>\n\n"
|
|
|
-
|
|
|
- total_pnl = 0
|
|
|
- total_trades = 0
|
|
|
- trading_days = 0
|
|
|
-
|
|
|
- for day_stats in daily_stats:
|
|
|
- if day_stats['has_trades']:
|
|
|
- # Day with completed trades
|
|
|
- pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
|
|
|
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
|
|
|
- daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
|
|
|
- daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
|
|
|
-
|
|
|
- total_pnl += day_stats['pnl']
|
|
|
- total_trades += day_stats['trades']
|
|
|
- trading_days += 1
|
|
|
- else:
|
|
|
- # Day with no trades
|
|
|
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
|
|
|
- daily_text += f" 📭 No completed trades\n\n"
|
|
|
-
|
|
|
- # Add summary
|
|
|
- if trading_days > 0:
|
|
|
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- daily_text += f"💼 <b>10-Day Summary:</b>\n"
|
|
|
- daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
- daily_text += f" 🔄 Total Trades: {total_trades}\n"
|
|
|
- daily_text += f" 📈 Trading Days: {trading_days}/10\n"
|
|
|
- daily_text += f" 📊 Avg per Trading Day: ${total_pnl/trading_days:,.2f}"
|
|
|
- else:
|
|
|
- daily_text += f"💼 <b>10-Day Summary:</b>\n"
|
|
|
- daily_text += f" 📭 No completed trades in the last 10 days\n"
|
|
|
- daily_text += f" 💡 Start trading to see daily performance!"
|
|
|
-
|
|
|
- await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing daily command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in daily command: {e}")
|
|
|
-
|
|
|
- async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /weekly command to show weekly performance stats."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- weekly_stats = self.stats.get_weekly_stats(10)
|
|
|
-
|
|
|
- if not weekly_stats:
|
|
|
- await update.message.reply_text(
|
|
|
- "📊 <b>Weekly Performance</b>\n\n"
|
|
|
- "📭 No weekly performance data available yet.\n\n"
|
|
|
- "💡 Weekly stats are calculated from completed trades.\n"
|
|
|
- "Start trading to see weekly performance!",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- weekly_text = "📊 <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
|
|
|
-
|
|
|
- total_pnl = 0
|
|
|
- total_trades = 0
|
|
|
- trading_weeks = 0
|
|
|
-
|
|
|
- for week_stats in weekly_stats:
|
|
|
- if week_stats['has_trades']:
|
|
|
- # Week with completed trades
|
|
|
- pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
|
|
|
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
|
|
|
- weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
|
|
|
- weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
|
|
|
-
|
|
|
- total_pnl += week_stats['pnl']
|
|
|
- total_trades += week_stats['trades']
|
|
|
- trading_weeks += 1
|
|
|
- else:
|
|
|
- # Week with no trades
|
|
|
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
|
|
|
- weekly_text += f" 📭 No completed trades\n\n"
|
|
|
-
|
|
|
- # Add summary
|
|
|
- if trading_weeks > 0:
|
|
|
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
|
|
|
- weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
- weekly_text += f" 🔄 Total Trades: {total_trades}\n"
|
|
|
- weekly_text += f" 📈 Trading Weeks: {trading_weeks}/10\n"
|
|
|
- weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
|
|
|
- else:
|
|
|
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
|
|
|
- weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
|
|
|
- weekly_text += f" 💡 Start trading to see weekly performance!"
|
|
|
-
|
|
|
- await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing weekly command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in weekly command: {e}")
|
|
|
-
|
|
|
- async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /monthly command to show monthly performance stats."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- monthly_stats = self.stats.get_monthly_stats(10)
|
|
|
-
|
|
|
- if not monthly_stats:
|
|
|
- await update.message.reply_text(
|
|
|
- "📆 <b>Monthly Performance</b>\n\n"
|
|
|
- "📭 No monthly performance data available yet.\n\n"
|
|
|
- "💡 Monthly stats are calculated from completed trades.\n"
|
|
|
- "Start trading to see monthly performance!",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- monthly_text = "📆 <b>Monthly Performance (Last 10 Months)</b>\n\n"
|
|
|
-
|
|
|
- total_pnl = 0
|
|
|
- total_trades = 0
|
|
|
- trading_months = 0
|
|
|
-
|
|
|
- for month_stats in monthly_stats:
|
|
|
- if month_stats['has_trades']:
|
|
|
- # Month with completed trades
|
|
|
- pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
|
|
|
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
|
|
|
- monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
|
|
|
- monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
|
|
|
-
|
|
|
- total_pnl += month_stats['pnl']
|
|
|
- total_trades += month_stats['trades']
|
|
|
- trading_months += 1
|
|
|
- else:
|
|
|
- # Month with no trades
|
|
|
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
|
|
|
- monthly_text += f" 📭 No completed trades\n\n"
|
|
|
-
|
|
|
- # Add summary
|
|
|
- if trading_months > 0:
|
|
|
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
|
|
|
- monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
- monthly_text += f" 🔄 Total Trades: {total_trades}\n"
|
|
|
- monthly_text += f" 📈 Trading Months: {trading_months}/10\n"
|
|
|
- monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
|
|
|
- else:
|
|
|
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
|
|
|
- monthly_text += f" 📭 No completed trades in the last 10 months\n"
|
|
|
- monthly_text += f" 💡 Start trading to see monthly performance!"
|
|
|
-
|
|
|
- await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing monthly command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in monthly command: {e}")
|
|
|
-
|
|
|
- async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /risk command to show advanced risk metrics."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Get current balance for context
|
|
|
- balance = self.client.get_balance()
|
|
|
- current_balance = 0
|
|
|
- if balance and balance.get('total'):
|
|
|
- current_balance = float(balance['total'].get('USDC', 0))
|
|
|
-
|
|
|
- # Get risk metrics and basic stats
|
|
|
- risk_metrics = self.stats.get_risk_metrics()
|
|
|
- basic_stats = self.stats.get_basic_stats()
|
|
|
-
|
|
|
- # Check if we have enough data for risk calculations
|
|
|
- if basic_stats['completed_trades'] < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "📊 <b>Risk Analysis</b>\n\n"
|
|
|
- "📭 <b>Insufficient Data</b>\n\n"
|
|
|
- f"• Current completed trades: {basic_stats['completed_trades']}\n"
|
|
|
- f"• Required for risk analysis: 2+ trades\n"
|
|
|
- f"• Daily balance snapshots: {len(self.stats.data.get('daily_balances', []))}\n\n"
|
|
|
- "💡 <b>To enable risk analysis:</b>\n"
|
|
|
- "• Complete more trades to generate returns data\n"
|
|
|
- "• Bot automatically records daily balance snapshots\n"
|
|
|
- "• Risk metrics will be available after sufficient trading history\n\n"
|
|
|
- "📈 Use /stats for current performance metrics",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Format the risk analysis message
|
|
|
- risk_text = f"""
|
|
|
-📊 <b>Risk Analysis & Advanced Metrics</b>
|
|
|
-
|
|
|
-🎯 <b>Risk-Adjusted Performance:</b>
|
|
|
-• Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
|
|
|
-• Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
|
|
|
-• Annual Volatility: {risk_metrics['volatility']:.2f}%
|
|
|
-
|
|
|
-📉 <b>Drawdown Analysis:</b>
|
|
|
-• Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
|
|
|
-• Value at Risk (95%): {risk_metrics['var_95']:.2f}%
|
|
|
-
|
|
|
-💰 <b>Portfolio Context:</b>
|
|
|
-• Current Balance: ${current_balance:,.2f}
|
|
|
-• Initial Balance: ${basic_stats['initial_balance']:,.2f}
|
|
|
-• Total P&L: ${basic_stats['total_pnl']:,.2f}
|
|
|
-• Days Active: {basic_stats['days_active']}
|
|
|
-
|
|
|
-📊 <b>Risk Interpretation:</b>
|
|
|
-"""
|
|
|
-
|
|
|
- # Add interpretive guidance
|
|
|
- sharpe = risk_metrics['sharpe_ratio']
|
|
|
- if sharpe > 2.0:
|
|
|
- risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
|
|
|
- elif sharpe > 1.0:
|
|
|
- risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
|
|
|
- elif sharpe > 0.5:
|
|
|
- risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
|
|
|
- elif sharpe > 0:
|
|
|
- risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
|
|
|
- else:
|
|
|
- risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
|
|
|
-
|
|
|
- max_dd = risk_metrics['max_drawdown']
|
|
|
- if max_dd < 5:
|
|
|
- risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
|
|
|
- elif max_dd < 15:
|
|
|
- risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
|
|
|
- elif max_dd < 30:
|
|
|
- risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
|
|
|
- else:
|
|
|
- risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
|
|
|
-
|
|
|
- volatility = risk_metrics['volatility']
|
|
|
- if volatility < 10:
|
|
|
- risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
|
|
|
- elif volatility < 25:
|
|
|
- risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
|
|
|
- elif volatility < 50:
|
|
|
- risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
|
|
|
- else:
|
|
|
- risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
|
|
|
-
|
|
|
- risk_text += f"""
|
|
|
-💡 <b>Risk Definitions:</b>
|
|
|
-• <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
|
|
|
-• <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
|
|
|
-• <b>Max Drawdown:</b> Largest peak-to-trough decline
|
|
|
-• <b>VaR 95%:</b> Maximum expected loss 95% of the time
|
|
|
-• <b>Volatility:</b> Annualized standard deviation of returns
|
|
|
-
|
|
|
-📈 <b>Data Based On:</b>
|
|
|
-• Completed Trades: {basic_stats['completed_trades']}
|
|
|
-• Daily Balance Records: {len(self.stats.data.get('daily_balances', []))}
|
|
|
-• Trading Period: {basic_stats['days_active']} days
|
|
|
-
|
|
|
-🔄 Use /stats for trading performance metrics
|
|
|
- """
|
|
|
-
|
|
|
- await update.message.reply_text(risk_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing risk command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in risk command: {e}")
|
|
|
-
|
|
|
- async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /version command to show bot version and system info."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Get system info
|
|
|
- import platform
|
|
|
- import sys
|
|
|
- from datetime import datetime
|
|
|
-
|
|
|
- uptime_info = "Unknown"
|
|
|
- try:
|
|
|
- # Try to get process uptime if available
|
|
|
- import psutil
|
|
|
- process = psutil.Process()
|
|
|
- create_time = datetime.fromtimestamp(process.create_time())
|
|
|
- uptime = datetime.now() - create_time
|
|
|
- days = uptime.days
|
|
|
- hours, remainder = divmod(uptime.seconds, 3600)
|
|
|
- minutes, _ = divmod(remainder, 60)
|
|
|
- uptime_info = f"{days}d {hours}h {minutes}m"
|
|
|
- except ImportError:
|
|
|
- # psutil not available, skip uptime
|
|
|
- pass
|
|
|
-
|
|
|
- # Get stats info
|
|
|
- basic_stats = self.stats.get_basic_stats()
|
|
|
-
|
|
|
- # Safety checks for monitoring attributes
|
|
|
- order_monitoring_task = getattr(self, 'order_monitoring_task', None)
|
|
|
- alarms = getattr(self, 'alarms', [])
|
|
|
-
|
|
|
- version_text = f"""
|
|
|
-🤖 <b>Trading Bot Version & System Info</b>
|
|
|
-
|
|
|
-📱 <b>Bot Information:</b>
|
|
|
-• Version: <code>{self.version}</code>
|
|
|
-• Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
|
|
|
-• Uptime: {uptime_info}
|
|
|
-• Default Token: {Config.DEFAULT_TRADING_TOKEN}
|
|
|
-
|
|
|
-💻 <b>System Information:</b>
|
|
|
-• Python: {sys.version.split()[0]}
|
|
|
-• Platform: {platform.system()} {platform.release()}
|
|
|
-• Architecture: {platform.machine()}
|
|
|
-
|
|
|
-📊 <b>Trading Stats:</b>
|
|
|
-• Total Orders: {basic_stats['total_trades']}
|
|
|
-• Completed Trades: {basic_stats['completed_trades']}
|
|
|
-• Days Active: {basic_stats['days_active']}
|
|
|
-• Start Date: {basic_stats['start_date']}
|
|
|
-
|
|
|
-🔄 <b>Monitoring Status:</b>
|
|
|
-• Order Monitoring: {'✅ Active' if order_monitoring_task and not order_monitoring_task.done() else '❌ Inactive'}
|
|
|
-• External Trades: ✅ Active
|
|
|
-• Price Alarms: ✅ Active ({len(alarms)} active)
|
|
|
-• Risk Management: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
|
|
|
-
|
|
|
-⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
- """
|
|
|
-
|
|
|
- await update.message.reply_text(version_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing version command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in version command: {e}")
|
|
|
-
|
|
|
- async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /balance_adjustments command to show deposit/withdrawal history."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Get balance adjustments summary
|
|
|
- adjustments_summary = self.stats.get_balance_adjustments_summary()
|
|
|
-
|
|
|
- # Get detailed adjustments
|
|
|
- all_adjustments = self.stats.data.get('balance_adjustments', [])
|
|
|
-
|
|
|
- if not all_adjustments:
|
|
|
- await update.message.reply_text(
|
|
|
- "💰 <b>Balance Adjustments</b>\n\n"
|
|
|
- "📭 No deposits or withdrawals detected yet.\n\n"
|
|
|
- "💡 The bot automatically monitors for deposits and withdrawals\n"
|
|
|
- "every hour to maintain accurate P&L calculations.",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- # Format the message
|
|
|
- adjustments_text = f"""
|
|
|
-💰 <b>Balance Adjustments History</b>
|
|
|
-
|
|
|
-📊 <b>Summary:</b>
|
|
|
-• Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
|
|
|
-• Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
|
|
|
-• Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
|
|
|
-• Total Transactions: {adjustments_summary['adjustment_count']}
|
|
|
-
|
|
|
-📅 <b>Recent Adjustments:</b>
|
|
|
-"""
|
|
|
-
|
|
|
- # Show last 10 adjustments
|
|
|
- recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
|
|
|
-
|
|
|
- for adj in recent_adjustments:
|
|
|
- try:
|
|
|
- # Format timestamp
|
|
|
- adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
|
|
|
-
|
|
|
- # Format type and amount
|
|
|
- if adj['type'] == 'deposit':
|
|
|
- emoji = "💰"
|
|
|
- amount_str = f"+${adj['amount']:,.2f}"
|
|
|
- else: # withdrawal
|
|
|
- emoji = "💸"
|
|
|
- amount_str = f"-${abs(adj['amount']):,.2f}"
|
|
|
-
|
|
|
- adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
|
|
|
-
|
|
|
- except Exception as adj_error:
|
|
|
- logger.warning(f"Error formatting adjustment: {adj_error}")
|
|
|
- continue
|
|
|
-
|
|
|
- adjustments_text += f"""
|
|
|
-
|
|
|
-💡 <b>How it Works:</b>
|
|
|
-• Bot checks for deposits/withdrawals every hour
|
|
|
-• Adjustments maintain accurate P&L calculations
|
|
|
-• Non-trading balance changes don't affect performance metrics
|
|
|
-• Trading statistics remain pure and accurate
|
|
|
-
|
|
|
-⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
|
|
|
- """
|
|
|
-
|
|
|
- await update.message.reply_text(adjustments_text.strip(), parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- error_message = f"❌ Error processing balance adjustments command: {str(e)}"
|
|
|
- await update.message.reply_text(error_message)
|
|
|
- logger.error(f"Error in balance_adjustments command: {e}")
|
|
|
-
|
|
|
- def _get_position_state(self, symbol: str) -> Dict[str, Any]:
|
|
|
- """Get current position state for a symbol."""
|
|
|
- if symbol not in self.position_tracker:
|
|
|
- self.position_tracker[symbol] = {
|
|
|
- 'contracts': 0.0,
|
|
|
- 'avg_entry_price': 0.0,
|
|
|
- 'total_cost_basis': 0.0,
|
|
|
- 'entry_count': 0,
|
|
|
- 'entry_history': [], # List of {price, amount, timestamp}
|
|
|
- 'last_update': datetime.now().isoformat()
|
|
|
- }
|
|
|
- return self.position_tracker[symbol]
|
|
|
-
|
|
|
- def _update_position_state(self, symbol: str, side: str, amount: float, price: float, timestamp: str = None):
|
|
|
- """Update position state with a new trade."""
|
|
|
- if timestamp is None:
|
|
|
- timestamp = datetime.now().isoformat()
|
|
|
-
|
|
|
- position = self._get_position_state(symbol)
|
|
|
-
|
|
|
- if side.lower() == 'buy':
|
|
|
- # Adding to long position or reducing short position
|
|
|
- if position['contracts'] >= 0:
|
|
|
- # Opening/adding to long position
|
|
|
- new_cost = amount * price
|
|
|
- old_cost = position['total_cost_basis']
|
|
|
- old_contracts = position['contracts']
|
|
|
-
|
|
|
- position['contracts'] += amount
|
|
|
- position['total_cost_basis'] += new_cost
|
|
|
- position['avg_entry_price'] = position['total_cost_basis'] / position['contracts'] if position['contracts'] > 0 else 0
|
|
|
- position['entry_count'] += 1
|
|
|
- position['entry_history'].append({
|
|
|
- 'price': price,
|
|
|
- 'amount': amount,
|
|
|
- 'timestamp': timestamp,
|
|
|
- 'side': 'buy'
|
|
|
- })
|
|
|
-
|
|
|
- logger.info(f"📈 Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}")
|
|
|
-
|
|
|
- # Check if this is truly a position open vs increase
|
|
|
- # For very small previous positions (< 0.001), consider it a new position open
|
|
|
- if old_contracts < 0.001:
|
|
|
- return 'long_opened'
|
|
|
- else:
|
|
|
- return 'long_increased'
|
|
|
- else:
|
|
|
- # Reducing short position
|
|
|
- reduction = min(amount, abs(position['contracts']))
|
|
|
- position['contracts'] += reduction
|
|
|
-
|
|
|
- if position['contracts'] >= 0:
|
|
|
- # Short position fully closed or flipped to long
|
|
|
- if position['contracts'] == 0:
|
|
|
- self._reset_position_state(symbol)
|
|
|
- return 'short_closed'
|
|
|
- else:
|
|
|
- # Flipped to long - need to track new long position
|
|
|
- remaining_amount = amount - reduction
|
|
|
- position['contracts'] = remaining_amount
|
|
|
- position['total_cost_basis'] = remaining_amount * price
|
|
|
- position['avg_entry_price'] = price
|
|
|
- return 'short_closed_and_long_opened'
|
|
|
- else:
|
|
|
- return 'short_reduced'
|
|
|
-
|
|
|
- elif side.lower() == 'sell':
|
|
|
- # Adding to short position or reducing long position
|
|
|
- if position['contracts'] <= 0:
|
|
|
- # Opening/adding to short position
|
|
|
- old_contracts = abs(position['contracts'])
|
|
|
- position['contracts'] -= amount
|
|
|
- position['entry_count'] += 1
|
|
|
- position['entry_history'].append({
|
|
|
- 'price': price,
|
|
|
- 'amount': amount,
|
|
|
- 'timestamp': timestamp,
|
|
|
- 'side': 'sell'
|
|
|
- })
|
|
|
-
|
|
|
- logger.info(f"📉 Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}")
|
|
|
-
|
|
|
- # Check if this is truly a position open vs increase
|
|
|
- # For very small previous positions (< 0.001), consider it a new position open
|
|
|
- if old_contracts < 0.001:
|
|
|
- return 'short_opened'
|
|
|
- else:
|
|
|
- return 'short_increased'
|
|
|
- else:
|
|
|
- # Reducing long position
|
|
|
- reduction = min(amount, position['contracts'])
|
|
|
- position['contracts'] -= reduction
|
|
|
-
|
|
|
- # Adjust cost basis proportionally
|
|
|
- if position['contracts'] > 0:
|
|
|
- reduction_ratio = reduction / (position['contracts'] + reduction)
|
|
|
- position['total_cost_basis'] *= (1 - reduction_ratio)
|
|
|
- return 'long_reduced'
|
|
|
- else:
|
|
|
- # Long position fully closed
|
|
|
- if position['contracts'] == 0:
|
|
|
- self._reset_position_state(symbol)
|
|
|
- return 'long_closed'
|
|
|
- else:
|
|
|
- # Flipped to short
|
|
|
- remaining_amount = amount - reduction
|
|
|
- position['contracts'] = -remaining_amount
|
|
|
- return 'long_closed_and_short_opened'
|
|
|
-
|
|
|
- position['last_update'] = timestamp
|
|
|
- return 'unknown'
|
|
|
-
|
|
|
- def _reset_position_state(self, symbol: str):
|
|
|
- """Reset position state when position is fully closed."""
|
|
|
- if symbol in self.position_tracker:
|
|
|
- del self.position_tracker[symbol]
|
|
|
-
|
|
|
- def _calculate_position_pnl(self, symbol: str, exit_amount: float, exit_price: float) -> Dict[str, float]:
|
|
|
- """Calculate P&L for a position exit."""
|
|
|
- position = self._get_position_state(symbol)
|
|
|
-
|
|
|
- if position['contracts'] == 0:
|
|
|
- return {'pnl': 0.0, 'pnl_percent': 0.0}
|
|
|
-
|
|
|
- avg_entry = position['avg_entry_price']
|
|
|
-
|
|
|
- if position['contracts'] > 0: # Long position
|
|
|
- pnl = exit_amount * (exit_price - avg_entry)
|
|
|
- else: # Short position
|
|
|
- pnl = exit_amount * (avg_entry - exit_price)
|
|
|
-
|
|
|
- cost_basis = exit_amount * avg_entry
|
|
|
- pnl_percent = (pnl / cost_basis * 100) if cost_basis > 0 else 0
|
|
|
-
|
|
|
- return {
|
|
|
- 'pnl': pnl,
|
|
|
- 'pnl_percent': pnl_percent,
|
|
|
- 'avg_entry_price': avg_entry
|
|
|
- }
|
|
|
-
|
|
|
- async def _send_external_trade_notification(self, trade: Dict[str, Any]):
|
|
|
- """Send generic notification for external trades (fallback)."""
|
|
|
- try:
|
|
|
- symbol = trade.get('symbol', '')
|
|
|
- side = trade.get('side', '')
|
|
|
- amount = float(trade.get('amount', 0))
|
|
|
- price = float(trade.get('price', 0))
|
|
|
- timestamp = trade.get('timestamp', '')
|
|
|
-
|
|
|
- # Extract token from symbol
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
-
|
|
|
- # Format timestamp
|
|
|
- try:
|
|
|
- trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
|
- time_str = trade_time.strftime('%H:%M:%S')
|
|
|
- except:
|
|
|
- time_str = "Unknown"
|
|
|
-
|
|
|
- # Determine trade type and emoji
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- trade_value = amount * price
|
|
|
-
|
|
|
- message = f"""
|
|
|
-🔄 <b>External Trade Detected</b>
|
|
|
-
|
|
|
-📊 <b>Trade Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Side: {side.upper()}
|
|
|
-• Amount: {amount} {token}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-• Value: ${trade_value:,.2f}
|
|
|
-
|
|
|
-{side_emoji} <b>Source:</b> External Platform Trade
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-
|
|
|
-📈 <b>Note:</b> This trade was executed outside the Telegram bot
|
|
|
-📊 Stats have been automatically updated
|
|
|
- """
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent generic external trade notification: {side} {amount} {token}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending external trade notification: {e}")
|
|
|
-
|
|
|
- async def _check_stop_losses(self, current_positions: list):
|
|
|
- """Check all positions for stop loss triggers and execute automatic exits."""
|
|
|
- try:
|
|
|
- if not current_positions:
|
|
|
- return
|
|
|
-
|
|
|
- stop_loss_triggers = []
|
|
|
-
|
|
|
- for position in current_positions:
|
|
|
- symbol = position.get('symbol')
|
|
|
- contracts = float(position.get('contracts', 0))
|
|
|
- entry_price = float(position.get('entryPx', 0))
|
|
|
-
|
|
|
- if not symbol or contracts == 0 or entry_price == 0:
|
|
|
- continue
|
|
|
-
|
|
|
- # Get current market price
|
|
|
- market_data = self.client.get_market_data(symbol)
|
|
|
- if not market_data or not market_data.get('ticker'):
|
|
|
- continue
|
|
|
-
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
- if current_price == 0:
|
|
|
- continue
|
|
|
-
|
|
|
- # Calculate current P&L percentage
|
|
|
- if contracts > 0: # Long position
|
|
|
- pnl_percent = ((current_price - entry_price) / entry_price) * 100
|
|
|
- else: # Short position
|
|
|
- pnl_percent = ((entry_price - current_price) / entry_price) * 100
|
|
|
-
|
|
|
- # Check if stop loss should trigger
|
|
|
- if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE:
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- stop_loss_triggers.append({
|
|
|
- 'symbol': symbol,
|
|
|
- 'token': token,
|
|
|
- 'contracts': contracts,
|
|
|
- 'entry_price': entry_price,
|
|
|
- 'current_price': current_price,
|
|
|
- 'pnl_percent': pnl_percent
|
|
|
- })
|
|
|
-
|
|
|
- # Execute stop losses
|
|
|
- for trigger in stop_loss_triggers:
|
|
|
- await self._execute_automatic_stop_loss(trigger)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error checking stop losses: {e}")
|
|
|
-
|
|
|
- async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]):
|
|
|
- """Execute an automatic stop loss order."""
|
|
|
- try:
|
|
|
- symbol = trigger['symbol']
|
|
|
- token = trigger['token']
|
|
|
- contracts = trigger['contracts']
|
|
|
- entry_price = trigger['entry_price']
|
|
|
- current_price = trigger['current_price']
|
|
|
- pnl_percent = trigger['pnl_percent']
|
|
|
-
|
|
|
- # Determine the exit side (opposite of position)
|
|
|
- exit_side = 'sell' if contracts > 0 else 'buy'
|
|
|
- contracts_abs = abs(contracts)
|
|
|
-
|
|
|
- # Send notification before executing
|
|
|
- await self._send_stop_loss_notification(trigger, "triggered")
|
|
|
-
|
|
|
- # Execute the stop loss order (market order for immediate execution)
|
|
|
- try:
|
|
|
- if exit_side == 'sell':
|
|
|
- order = self.client.create_market_sell_order(symbol, contracts_abs)
|
|
|
- else:
|
|
|
- order = self.client.create_market_buy_order(symbol, contracts_abs)
|
|
|
-
|
|
|
- if order:
|
|
|
- logger.info(f"🛑 Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}")
|
|
|
-
|
|
|
- # Record the trade in stats and update position tracking
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'), "auto_stop_loss")
|
|
|
-
|
|
|
- # Send success notification
|
|
|
- await self._send_stop_loss_notification(trigger, "executed", order)
|
|
|
- else:
|
|
|
- logger.error(f"❌ Stop loss order failed for {token}")
|
|
|
- await self._send_stop_loss_notification(trigger, "failed")
|
|
|
-
|
|
|
- except Exception as order_error:
|
|
|
- logger.error(f"❌ Stop loss order execution failed for {token}: {order_error}")
|
|
|
- await self._send_stop_loss_notification(trigger, "failed", error=str(order_error))
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error executing automatic stop loss: {e}")
|
|
|
-
|
|
|
- async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None):
|
|
|
- """Send notification for stop loss events."""
|
|
|
- try:
|
|
|
- token = trigger['token']
|
|
|
- contracts = trigger['contracts']
|
|
|
- entry_price = trigger['entry_price']
|
|
|
- current_price = trigger['current_price']
|
|
|
- pnl_percent = trigger['pnl_percent']
|
|
|
-
|
|
|
- position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
- contracts_abs = abs(contracts)
|
|
|
-
|
|
|
- if status == "triggered":
|
|
|
- title = "🛑 Stop Loss Triggered"
|
|
|
- status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss"
|
|
|
- emoji = "🚨"
|
|
|
- elif status == "executed":
|
|
|
- title = "✅ Stop Loss Executed"
|
|
|
- status_text = "Position closed automatically"
|
|
|
- emoji = "🛑"
|
|
|
- elif status == "failed":
|
|
|
- title = "❌ Stop Loss Failed"
|
|
|
- status_text = f"Stop loss execution failed{': ' + error if error else ''}"
|
|
|
- emoji = "⚠️"
|
|
|
- else:
|
|
|
- return
|
|
|
-
|
|
|
- # Calculate loss
|
|
|
- loss_value = contracts_abs * abs(current_price - entry_price)
|
|
|
-
|
|
|
- message = f"""
|
|
|
-{title}
|
|
|
-
|
|
|
-{emoji} <b>Risk Management Alert</b>
|
|
|
-
|
|
|
-📊 <b>Position Details:</b>
|
|
|
-• Token: {token}
|
|
|
-• Direction: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
-
|
|
|
-🔴 <b>Loss Details:</b>
|
|
|
-• Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
|
|
|
-• Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
|
|
|
-
|
|
|
-📋 <b>Action:</b> {status_text}
|
|
|
-⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
- """
|
|
|
-
|
|
|
- if order and status == "executed":
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- message += f"\n🆔 <b>Order ID:</b> {order_id}"
|
|
|
-
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent stop loss notification: {token} {status}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error sending stop loss notification: {e}")
|
|
|
-
|
|
|
- async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
|
|
|
- """Process filled orders using enhanced position tracking."""
|
|
|
- try:
|
|
|
- # For bot-initiated orders, we'll detect changes in position size
|
|
|
- # and send appropriate notifications using the enhanced system
|
|
|
-
|
|
|
- # This method will be triggered when orders placed through the bot are filled
|
|
|
- # The external trade monitoring will handle trades made outside the bot
|
|
|
-
|
|
|
- # Update position tracking based on current positions
|
|
|
- await self._update_position_tracking(current_positions)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error processing filled orders: {e}")
|
|
|
-
|
|
|
- async def _update_position_tracking(self, current_positions: list):
|
|
|
- """Update the legacy position tracking data for compatibility."""
|
|
|
- new_position_map = {}
|
|
|
-
|
|
|
- for position in current_positions:
|
|
|
- symbol = position.get('symbol')
|
|
|
- contracts = float(position.get('contracts', 0))
|
|
|
- entry_price = float(position.get('entryPx', 0))
|
|
|
-
|
|
|
- if symbol and contracts != 0:
|
|
|
- new_position_map[symbol] = {
|
|
|
- 'contracts': contracts,
|
|
|
- 'entry_price': entry_price
|
|
|
- }
|
|
|
-
|
|
|
- # Also update our enhanced position tracker if not already present
|
|
|
- if symbol not in self.position_tracker:
|
|
|
- self._get_position_state(symbol)
|
|
|
- self.position_tracker[symbol]['contracts'] = contracts
|
|
|
- self.position_tracker[symbol]['avg_entry_price'] = entry_price
|
|
|
- self.position_tracker[symbol]['total_cost_basis'] = contracts * entry_price
|
|
|
-
|
|
|
- self.last_known_positions = new_position_map
|
|
|
-
|
|
|
- async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /keyboard command to enable/show custom keyboard."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- custom_keyboard = self._create_custom_keyboard()
|
|
|
-
|
|
|
- if custom_keyboard:
|
|
|
- await update.message.reply_text(
|
|
|
- "⌨️ <b>Custom Keyboard Activated!</b>\n\n"
|
|
|
- "🎯 <b>Your quick buttons are now ready:</b>\n"
|
|
|
- "• Daily - Daily performance\n"
|
|
|
- "• Performance - Performance stats\n"
|
|
|
- "• Balance - Account balance\n"
|
|
|
- "• Stats - Trading statistics\n"
|
|
|
- "• Positions - Open positions\n"
|
|
|
- "• Orders - Active orders\n"
|
|
|
- "• Price - Quick price check\n"
|
|
|
- "• Market - Market overview\n"
|
|
|
- "• Help - Help guide\n"
|
|
|
- "• Commands - Command menu\n\n"
|
|
|
- "💡 <b>How to use:</b>\n"
|
|
|
- "Tap any button below instead of typing the command manually!\n\n"
|
|
|
- "🔧 These buttons will stay at the bottom of your chat.",
|
|
|
- parse_mode='HTML',
|
|
|
- reply_markup=custom_keyboard
|
|
|
- )
|
|
|
- else:
|
|
|
- await update.message.reply_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"
|
|
|
- f"📋 <b>Current config:</b>\n"
|
|
|
- f"• Enabled: {Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED}\n"
|
|
|
- f"• Layout: {Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT}",
|
|
|
- parse_mode='HTML'
|
|
|
- )
|
|
|
-
|
|
|
- async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle messages from custom keyboard buttons."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- message_text = update.message.text.lower()
|
|
|
-
|
|
|
- # Map keyboard button text to commands
|
|
|
- command_map = {
|
|
|
- 'balance': '/balance',
|
|
|
- 'positions': '/positions',
|
|
|
- 'orders': '/orders',
|
|
|
- 'stats': '/stats',
|
|
|
- 'trades': '/trades',
|
|
|
- 'market': '/market',
|
|
|
- 'price': '/price',
|
|
|
- 'help': '/help',
|
|
|
- 'commands': '/commands',
|
|
|
- 'monitoring': '/monitoring',
|
|
|
- 'logs': '/logs',
|
|
|
- 'performance': '/performance',
|
|
|
- 'daily': '/daily',
|
|
|
- 'weekly': '/weekly',
|
|
|
- 'monthly': '/monthly',
|
|
|
- 'risk': '/risk',
|
|
|
- 'alarm': '/alarm',
|
|
|
- 'keyboard': '/keyboard'
|
|
|
- }
|
|
|
-
|
|
|
- # Check if the message matches any keyboard command
|
|
|
- if message_text in command_map:
|
|
|
- # Create a fake update object with the corresponding command
|
|
|
- update.message.text = command_map[message_text]
|
|
|
- # Get the handler for this command and call it
|
|
|
- handlers = self.application.handlers[0] # Get default group handlers
|
|
|
- for handler in handlers:
|
|
|
- if hasattr(handler, 'callback') and hasattr(handler, 'filters'):
|
|
|
- if await handler.check_update(update):
|
|
|
- await handler.callback(update, context)
|
|
|
- return
|
|
|
-
|
|
|
- # If no keyboard command matched, show a help message
|
|
|
- await update.message.reply_text("❓ Unknown command. Use /help to see available commands.")
|
|
|
-
|
|
|
- async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Debug command to show internal bot state."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- # Get bot state information
|
|
|
- debug_info = f"🔧 <b>Bot Debug Information</b>\\n\\n"
|
|
|
-
|
|
|
- # Bot version and status
|
|
|
- debug_info += f"🤖 <b>Version:</b> {self.version}\\n"
|
|
|
- debug_info += f"⚡ <b>Status:</b> Running\\n"
|
|
|
-
|
|
|
- # Position tracking information
|
|
|
- debug_info += f"\\n📊 <b>Position State Tracking:</b>\\n"
|
|
|
- if hasattr(self, '_internal_positions') and self._internal_positions:
|
|
|
- for symbol, pos_state in self._internal_positions.items():
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- contracts = pos_state.get('contracts', 0)
|
|
|
- avg_price = pos_state.get('average_price', 0)
|
|
|
- debug_info += f" • {token}: {contracts:.6f} @ ${avg_price:.2f}\\n"
|
|
|
- else:
|
|
|
- debug_info += f" No internal position tracking data\\n"
|
|
|
-
|
|
|
- # Get raw Hyperliquid position data for comparison
|
|
|
- debug_info += f"\\n🔍 <b>Raw Hyperliquid Position Data:</b>\\n"
|
|
|
- raw_positions = self.client.get_positions()
|
|
|
- if raw_positions:
|
|
|
- for pos in raw_positions:
|
|
|
- if float(pos.get('contracts', 0)) != 0:
|
|
|
- symbol = pos.get('symbol', 'Unknown')
|
|
|
- contracts = pos.get('contracts', 0)
|
|
|
- side = pos.get('side', 'Unknown')
|
|
|
- entry_price = pos.get('entryPrice', 0)
|
|
|
- unrealized_pnl = pos.get('unrealizedPnl', 0)
|
|
|
- notional = pos.get('notional', 0)
|
|
|
-
|
|
|
- # Get raw Hyperliquid data from info field
|
|
|
- raw_info = pos.get('info', {})
|
|
|
- raw_position = raw_info.get('position', {}) if raw_info else {}
|
|
|
- raw_szi = raw_position.get('szi', 'N/A')
|
|
|
-
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- debug_info += f" • <b>{token}:</b>\\n"
|
|
|
- debug_info += f" - CCXT contracts: {contracts} (always positive)\\n"
|
|
|
- debug_info += f" - CCXT side: {side}\\n"
|
|
|
- debug_info += f" - Raw Hyperliquid szi: {raw_szi} (negative = short)\\n"
|
|
|
- debug_info += f" - Entry price: ${entry_price}\\n"
|
|
|
- debug_info += f" - Unrealized P&L: ${unrealized_pnl}\\n"
|
|
|
- debug_info += f" - Notional: ${notional}\\n"
|
|
|
-
|
|
|
- # Show what the bot would interpret this as
|
|
|
- side_field = pos.get('side', '').lower()
|
|
|
- if side_field in ['long', 'short']:
|
|
|
- interpreted_direction = side_field.upper()
|
|
|
- else:
|
|
|
- interpreted_direction = "LONG" if float(contracts) > 0 else "SHORT"
|
|
|
- debug_info += f" - Bot interprets as: {interpreted_direction} (using CCXT side field)\\n\\n"
|
|
|
- else:
|
|
|
- debug_info += f" No positions found or API error\\n"
|
|
|
-
|
|
|
- # Show monitoring status
|
|
|
- debug_info += f"\\n🔍 <b>Monitoring:</b> {'Active' if self.monitoring_active else 'Inactive'}\\n"
|
|
|
- debug_info += f"📋 <b>Tracked Orders:</b> {len(self.last_known_orders)}\\n"
|
|
|
- debug_info += f"🤖 <b>Bot Trade IDs:</b> {len(self.bot_trade_ids)}\\n"
|
|
|
- if self.bot_trade_ids:
|
|
|
- debug_info += " Recent bot trades: " + ", ".join(list(self.bot_trade_ids)[-5:]) + "\\n"
|
|
|
-
|
|
|
- await update.message.reply_text(debug_info, parse_mode='HTML')
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error in debug command: {e}")
|
|
|
- await update.message.reply_text(f"❌ Debug error: {e}")
|
|
|
-
|
|
|
-
|
|
|
-async def main_async():
|
|
|
- """Async main entry point for the Telegram bot."""
|
|
|
- try:
|
|
|
- # Validate configuration
|
|
|
- if not Config.validate():
|
|
|
- logger.error("❌ Configuration validation failed!")
|
|
|
- return
|
|
|
-
|
|
|
- if not Config.TELEGRAM_ENABLED:
|
|
|
- logger.error("❌ Telegram is not enabled in configuration")
|
|
|
- return
|
|
|
-
|
|
|
- # Create and run the bot
|
|
|
- bot = TelegramTradingBot()
|
|
|
- await bot.run()
|
|
|
-
|
|
|
- except KeyboardInterrupt:
|
|
|
- logger.info("👋 Bot stopped by user")
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Unexpected error: {e}")
|
|
|
- raise
|
|
|
-
|
|
|
-
|
|
|
-def main():
|
|
|
- """Main entry point for the Telegram bot."""
|
|
|
- try:
|
|
|
- # Check if we're already in an asyncio context
|
|
|
- try:
|
|
|
- loop = asyncio.get_running_loop()
|
|
|
- # If we get here, we're already in an asyncio context
|
|
|
- logger.error("❌ Cannot run main() from within an asyncio context. Use main_async() instead.")
|
|
|
- return
|
|
|
- except RuntimeError:
|
|
|
- # No running loop, safe to use asyncio.run()
|
|
|
- pass
|
|
|
-
|
|
|
- # Run the async main function
|
|
|
- asyncio.run(main_async())
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Failed to start telegram bot: {e}")
|
|
|
- raise
|
|
|
-
|
|
|
-
|
|
|
-if __name__ == "__main__":
|
|
|
- main()
|