|
@@ -34,6 +34,11 @@ class TelegramTradingBot:
|
|
|
self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
|
|
|
self.application = None
|
|
|
|
|
|
+ # Order monitoring
|
|
|
+ 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
|
|
|
+
|
|
|
# Initialize stats with current balance
|
|
|
self._initialize_stats()
|
|
|
|
|
@@ -88,19 +93,61 @@ Tap the buttons below for instant access to key functions.
|
|
|
/market - Market data
|
|
|
/price - Current price
|
|
|
|
|
|
-<b>🔄 Trading Commands:</b>
|
|
|
-/buy [amount] [price] - Buy order
|
|
|
-/sell [amount] [price] - Sell order
|
|
|
-/trades - Recent trades
|
|
|
-/cancel [order_id] - Cancel order
|
|
|
+<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)
|
|
|
+• /short ETH 50 - Short ETH with $50 USDC (Market Order)
|
|
|
+• /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
|
|
|
+• /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>📈 Statistics:</b>
|
|
|
+<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 - Full trading statistics
|
|
|
/performance - Performance metrics
|
|
|
/risk - Risk analysis
|
|
|
|
|
|
+<b>🔄 Automatic Notifications:</b>
|
|
|
+• Real-time order fill alerts
|
|
|
+• Position opened/closed notifications
|
|
|
+• P&L calculations on trade closure
|
|
|
+• 30-second monitoring interval
|
|
|
+
|
|
|
Type /help for detailed command information.
|
|
|
- """
|
|
|
+
|
|
|
+<b>🔄 Order Monitoring:</b>
|
|
|
+• /monitoring - View monitoring status
|
|
|
+
|
|
|
+<b>⚙️ Configuration:</b>
|
|
|
+• Symbol: {symbol}
|
|
|
+• Default Amount: {amount}
|
|
|
+• Network: {network}
|
|
|
+
|
|
|
+<b>🛡️ Safety Features:</b>
|
|
|
+• All trades logged automatically
|
|
|
+• Comprehensive performance tracking
|
|
|
+• Real-time balance monitoring
|
|
|
+• Risk metrics calculation
|
|
|
+
|
|
|
+<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_SYMBOL,
|
|
|
+ amount=Config.DEFAULT_TRADE_AMOUNT,
|
|
|
+ network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet"
|
|
|
+ )
|
|
|
|
|
|
keyboard = [
|
|
|
[
|
|
@@ -142,10 +189,21 @@ Type /help for detailed command information.
|
|
|
• /market - Detailed market data
|
|
|
• /price - Quick price check
|
|
|
|
|
|
-<b>🔄 Manual Trading:</b>
|
|
|
-• /buy 0.001 50000 - Buy 0.001 BTC at $50,000
|
|
|
-• /sell 0.001 55000 - Sell 0.001 BTC at $55,000
|
|
|
-• /cancel ABC123 - Cancel order with ID ABC123
|
|
|
+<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)
|
|
|
+• /short ETH 50 - Short ETH with $50 USDC (Market Order)
|
|
|
+• /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
|
|
|
+• /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>📋 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
|
|
@@ -153,6 +211,9 @@ Type /help for detailed command information.
|
|
|
• /risk - Sharpe ratio, drawdown, VaR
|
|
|
• /trades - Recent trade history
|
|
|
|
|
|
+<b>🔄 Order Monitoring:</b>
|
|
|
+• /monitoring - View monitoring status
|
|
|
+
|
|
|
<b>⚙️ Configuration:</b>
|
|
|
• Symbol: {symbol}
|
|
|
• Default Amount: {amount}
|
|
@@ -194,104 +255,6 @@ For support, contact your bot administrator.
|
|
|
stats_message = self.stats.format_stats_message(current_balance)
|
|
|
await update.message.reply_text(stats_message, parse_mode='HTML')
|
|
|
|
|
|
- async def buy_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /buy command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /buy [amount] [price]\n"
|
|
|
- f"Example: /buy {Config.DEFAULT_TRADE_AMOUNT} 50000"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- amount = float(context.args[0])
|
|
|
- price = float(context.args[1])
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
-
|
|
|
- # Confirmation message
|
|
|
- confirmation_text = f"""
|
|
|
-🟢 <b>Buy Order Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Side: BUY
|
|
|
-• Amount: {amount}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-• Total Value: ${amount * price:,.2f}
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to place this order?</b>
|
|
|
-
|
|
|
-This will attempt to buy {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("✅ Confirm Buy", callback_data=f"confirm_buy_{amount}_{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 amount or price. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing buy command: {e}")
|
|
|
-
|
|
|
- async def sell_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /sell command."""
|
|
|
- if not self.is_authorized(update.effective_chat.id):
|
|
|
- await update.message.reply_text("❌ Unauthorized access.")
|
|
|
- return
|
|
|
-
|
|
|
- try:
|
|
|
- if len(context.args) < 2:
|
|
|
- await update.message.reply_text(
|
|
|
- "❌ Usage: /sell [amount] [price]\n"
|
|
|
- f"Example: /sell {Config.DEFAULT_TRADE_AMOUNT} 55000"
|
|
|
- )
|
|
|
- return
|
|
|
-
|
|
|
- amount = float(context.args[0])
|
|
|
- price = float(context.args[1])
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
-
|
|
|
- # Confirmation message
|
|
|
- confirmation_text = f"""
|
|
|
-🔴 <b>Sell Order Confirmation</b>
|
|
|
-
|
|
|
-📊 <b>Order Details:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Side: SELL
|
|
|
-• Amount: {amount}
|
|
|
-• Price: ${price:,.2f}
|
|
|
-• Total Value: ${amount * price:,.2f}
|
|
|
-
|
|
|
-⚠️ <b>Are you sure you want to place this order?</b>
|
|
|
-
|
|
|
-This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
- """
|
|
|
-
|
|
|
- keyboard = [
|
|
|
- [
|
|
|
- InlineKeyboardButton("✅ Confirm Sell", callback_data=f"confirm_sell_{amount}_{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 amount or price. Please use numbers only.")
|
|
|
- except Exception as e:
|
|
|
- await update.message.reply_text(f"❌ Error processing sell command: {e}")
|
|
|
-
|
|
|
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):
|
|
@@ -325,27 +288,55 @@ This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
balance = self.client.get_balance()
|
|
|
if balance:
|
|
|
balance_text = "💰 <b>Account Balance</b>\n\n"
|
|
|
+
|
|
|
+ # 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:
|
|
|
- balance_text += f"💵 <b>{asset}:</b> {amount}\n"
|
|
|
+ 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 for USDC (main trading currency)
|
|
|
if asset == 'USDC':
|
|
|
total_value += float(amount)
|
|
|
+ available_value += free_amount
|
|
|
|
|
|
- balance_text += f"\n💼 <b>Total Value:</b> ${total_value:,.2f}"
|
|
|
+ # 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"
|
|
|
|
|
|
- # Add stats summary
|
|
|
+ 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:
|
|
|
pnl = total_value - basic_stats['initial_balance']
|
|
|
pnl_percent = (pnl / basic_stats['initial_balance']) * 100
|
|
|
|
|
|
- balance_text += f"\n📊 <b>P&L:</b> ${pnl:,.2f} ({pnl_percent:+.2f}%)"
|
|
|
+ balance_text += f"\n📊 <b>Performance:</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"
|
|
|
+ balance_text += "📭 No balance data available"
|
|
|
else:
|
|
|
balance_text = "❌ Could not fetch balance data"
|
|
|
|
|
@@ -358,9 +349,11 @@ This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
return
|
|
|
|
|
|
positions = self.client.get_positions()
|
|
|
- if positions:
|
|
|
+
|
|
|
+ if positions is not None: # Successfully fetched (could be empty list)
|
|
|
positions_text = "📈 <b>Open Positions</b>\n\n"
|
|
|
|
|
|
+ # Filter for actual open positions
|
|
|
open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
|
|
|
|
|
|
if open_positions:
|
|
@@ -382,40 +375,79 @@ This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
|
|
|
positions_text += f"💼 <b>Total Unrealized P&L:</b> ${total_unrealized:,.2f}"
|
|
|
else:
|
|
|
- positions_text += "No open positions"
|
|
|
+ positions_text += "📭 <b>No open positions currently</b>\n\n"
|
|
|
+ positions_text += "🚀 Ready to start trading!\n"
|
|
|
+ positions_text += "Use /buy or /sell commands to open positions."
|
|
|
else:
|
|
|
- positions_text = "❌ Could not fetch positions data"
|
|
|
+ # 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."""
|
|
|
+ """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 len(context.args) >= 1:
|
|
|
+ token_filter = context.args[0].upper()
|
|
|
+
|
|
|
orders = self.client.get_open_orders()
|
|
|
- if orders:
|
|
|
- orders_text = "📋 <b>Open Orders</b>\n\n"
|
|
|
+
|
|
|
+ if orders is not None: # Successfully fetched (could be empty list)
|
|
|
+ if token_filter:
|
|
|
+ orders_text = f"📋 <b>Open Orders - {token_filter}</b>\n\n"
|
|
|
+ # 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]
|
|
|
+ else:
|
|
|
+ orders_text = "📋 <b>All Open Orders</b>\n\n"
|
|
|
+ filtered_orders = orders
|
|
|
|
|
|
- if orders and len(orders) > 0:
|
|
|
- for order in orders:
|
|
|
+ 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>{symbol}</b>\n"
|
|
|
+ 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:
|
|
|
- orders_text += "No open orders"
|
|
|
+ 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:
|
|
|
- orders_text = "❌ Could not fetch orders data"
|
|
|
+ # 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')
|
|
|
|
|
@@ -490,18 +522,55 @@ This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
callback_data = query.data
|
|
|
|
|
|
# Handle trading confirmations
|
|
|
- if callback_data.startswith('confirm_buy_'):
|
|
|
+ if callback_data.startswith('confirm_long_'):
|
|
|
+ parts = callback_data.split('_')
|
|
|
+ token = parts[2]
|
|
|
+ usdc_amount = float(parts[3])
|
|
|
+ price = float(parts[4])
|
|
|
+ is_limit = len(parts) > 5 and parts[5] == 'limit'
|
|
|
+ await self._execute_long_order(query, token, usdc_amount, price, is_limit)
|
|
|
+ return
|
|
|
+
|
|
|
+ elif callback_data.startswith('confirm_short_'):
|
|
|
+ parts = callback_data.split('_')
|
|
|
+ token = parts[2]
|
|
|
+ usdc_amount = float(parts[3])
|
|
|
+ price = float(parts[4])
|
|
|
+ is_limit = len(parts) > 5 and parts[5] == 'limit'
|
|
|
+ await self._execute_short_order(query, token, usdc_amount, price, is_limit)
|
|
|
+ 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('_')
|
|
|
- amount = float(parts[2])
|
|
|
- price = float(parts[3])
|
|
|
- await self._execute_buy_order(query, amount, price)
|
|
|
+ token = parts[2]
|
|
|
+ await self._execute_coo(query, token)
|
|
|
return
|
|
|
|
|
|
- elif callback_data.startswith('confirm_sell_'):
|
|
|
+ elif callback_data.startswith('confirm_sl_'):
|
|
|
parts = callback_data.split('_')
|
|
|
- amount = float(parts[2])
|
|
|
- price = float(parts[3])
|
|
|
- await self._execute_sell_order(query, amount, price)
|
|
|
+ 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':
|
|
@@ -533,83 +602,324 @@ This will attempt to sell {amount} {symbol} at ${price:,.2f} per unit.
|
|
|
elif callback_data == "help":
|
|
|
await self.help_command(fake_update, context)
|
|
|
|
|
|
- async def _execute_buy_order(self, query, amount: float, price: float):
|
|
|
- """Execute a buy order."""
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+ async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
|
|
|
+ """Execute a long order."""
|
|
|
+ symbol = f"{token}/USDC:USDC"
|
|
|
|
|
|
try:
|
|
|
- await query.edit_message_text("⏳ Placing buy order...")
|
|
|
+ await query.edit_message_text("⏳ Opening long position...")
|
|
|
|
|
|
- # Place the order
|
|
|
- order = self.client.place_limit_order(symbol, 'buy', amount, price)
|
|
|
+ # 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')
|
|
|
- self.stats.record_trade(symbol, 'buy', amount, price, order_id)
|
|
|
+ actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
+ self.stats.record_trade(symbol, 'buy', token_amount, actual_price, order_id)
|
|
|
|
|
|
success_message = f"""
|
|
|
-✅ <b>Buy Order Placed Successfully!</b>
|
|
|
+✅ <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
|
|
|
|
📊 <b>Order Details:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Side: BUY
|
|
|
-• Amount: {amount}
|
|
|
+• 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>
|
|
|
-• Total Value: ${amount * price:,.2f}
|
|
|
|
|
|
-The order has been submitted to Hyperliquid.
|
|
|
+🚀 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"Buy order placed: {amount} {symbol} @ ${price}")
|
|
|
+ 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("❌ Failed to place buy order. Please try again.")
|
|
|
+ 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 buy order: {str(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 placing buy order: {e}")
|
|
|
+ logger.error(f"Error in long order: {e}")
|
|
|
|
|
|
- async def _execute_sell_order(self, query, amount: float, price: float):
|
|
|
- """Execute a sell order."""
|
|
|
- symbol = Config.DEFAULT_TRADING_SYMBOL
|
|
|
+ async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
|
|
|
+ """Execute a short order."""
|
|
|
+ symbol = f"{token}/USDC:USDC"
|
|
|
|
|
|
try:
|
|
|
- await query.edit_message_text("⏳ Placing sell order...")
|
|
|
+ await query.edit_message_text("⏳ Opening short position...")
|
|
|
|
|
|
- # Place the order
|
|
|
- order = self.client.place_limit_order(symbol, 'sell', amount, price)
|
|
|
+ # 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')
|
|
|
- self.stats.record_trade(symbol, 'sell', amount, price, order_id)
|
|
|
+ actual_price = order.get('average', price) # Use actual fill price if available
|
|
|
+ self.stats.record_trade(symbol, 'sell', token_amount, actual_price, order_id)
|
|
|
|
|
|
success_message = f"""
|
|
|
-✅ <b>Sell Order Placed Successfully!</b>
|
|
|
+✅ <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
|
|
|
|
📊 <b>Order Details:</b>
|
|
|
-• Symbol: {symbol}
|
|
|
-• Side: SELL
|
|
|
-• Amount: {amount}
|
|
|
+• 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>
|
|
|
+
|
|
|
+📉 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
|
|
|
+ self.stats.record_trade(symbol, exit_side, contracts, actual_price, 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
|
|
|
+ self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
|
|
|
+
|
|
|
+ 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
|
|
|
+ self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
|
|
|
+
|
|
|
+ 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>
|
|
|
-• Total Value: ${amount * price:,.2f}
|
|
|
|
|
|
-The order has been submitted to Hyperliquid.
|
|
|
+🎯 <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"Sell order placed: {amount} {symbol} @ ${price}")
|
|
|
+ logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
|
|
|
else:
|
|
|
- await query.edit_message_text("❌ Failed to place sell order. Please try again.")
|
|
|
+ await query.edit_message_text("❌ Failed to set take profit. Please try again.")
|
|
|
|
|
|
except Exception as e:
|
|
|
- error_message = f"❌ Error placing sell order: {str(e)}"
|
|
|
+ error_message = f"❌ Error setting take profit: {str(e)}"
|
|
|
await query.edit_message_text(error_message)
|
|
|
- logger.error(f"Error placing sell order: {e}")
|
|
|
+ logger.error(f"Error setting take profit: {e}")
|
|
|
|
|
|
async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle unknown commands."""
|
|
@@ -636,8 +946,13 @@ The order has been submitted to Hyperliquid.
|
|
|
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("buy", self.buy_command))
|
|
|
- self.application.add_handler(CommandHandler("sell", self.sell_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))
|
|
|
|
|
|
# Callback query handler for inline keyboards
|
|
|
self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
|
@@ -673,6 +988,7 @@ The order has been submitted to Hyperliquid.
|
|
|
f"✅ Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
|
|
|
f"📊 Default Symbol: {Config.DEFAULT_TRADING_SYMBOL}\n"
|
|
|
f"📱 Manual trading ready!\n"
|
|
|
+ f"🔄 Order monitoring: Active\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."
|
|
|
)
|
|
@@ -680,6 +996,9 @@ The order has been submitted to Hyperliquid.
|
|
|
# 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...")
|
|
|
|
|
@@ -716,12 +1035,925 @@ The order has been submitted to Hyperliquid.
|
|
|
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 len(context.args) < 2:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "❌ Usage: /long [token] [USDC amount] [price (optional)]\n"
|
|
|
+ "Examples:\n"
|
|
|
+ "• /long BTC 100 - Market order\n"
|
|
|
+ "• /long BTC 100 45000 - Limit order at $45,000"
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ token = context.args[0].upper()
|
|
|
+ usdc_amount = float(context.args[1])
|
|
|
+
|
|
|
+ # Check if price is provided for limit order
|
|
|
+ limit_price = None
|
|
|
+ if len(context.args) >= 3:
|
|
|
+ limit_price = float(context.args[2])
|
|
|
+ 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
|
|
|
+
|
|
|
+ # 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}
|
|
|
+
|
|
|
+⚠️ <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"
|
|
|
+
|
|
|
+ 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 len(context.args) < 2:
|
|
|
+ await update.message.reply_text(
|
|
|
+ "❌ Usage: /short [token] [USDC amount] [price (optional)]\n"
|
|
|
+ "Examples:\n"
|
|
|
+ "• /short BTC 100 - Market order\n"
|
|
|
+ "• /short BTC 100 46000 - Limit order at $46,000"
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ token = context.args[0].upper()
|
|
|
+ usdc_amount = float(context.args[1])
|
|
|
+
|
|
|
+ # Check if price is provided for limit order
|
|
|
+ limit_price = None
|
|
|
+ if len(context.args) >= 3:
|
|
|
+ limit_price = float(context.args[2])
|
|
|
+ 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
|
|
|
+
|
|
|
+ # 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}
|
|
|
+
|
|
|
+⚠️ <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"
|
|
|
+
|
|
|
+ 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 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 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 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))
|
|
|
+
|
|
|
+ # Determine position direction and validate stop loss price
|
|
|
+ if contracts > 0:
|
|
|
+ # 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
|
|
|
+ else:
|
|
|
+ # Short position - stop loss should be above entry price
|
|
|
+ position_type = "SHORT"
|
|
|
+ exit_side = "buy"
|
|
|
+ exit_emoji = "🟢"
|
|
|
+ contracts_abs = abs(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\n"
|
|
|
+ f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
|
|
|
+ )
|
|
|
+ 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 contracts > 0: # Long position
|
|
|
+ pnl_at_stop = (stop_price - entry_price) * contracts_abs
|
|
|
+ else: # Short position
|
|
|
+ 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 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))
|
|
|
+
|
|
|
+ # Determine position direction and validate take profit price
|
|
|
+ if contracts > 0:
|
|
|
+ # 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
|
|
|
+ else:
|
|
|
+ # Short position - take profit should be below entry price
|
|
|
+ position_type = "SHORT"
|
|
|
+ exit_side = "buy"
|
|
|
+ exit_emoji = "🟢"
|
|
|
+ contracts_abs = abs(contracts)
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ # 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 contracts > 0: # Long position
|
|
|
+ pnl_at_tp = (profit_price - entry_price) * contracts_abs
|
|
|
+ else: # Short position
|
|
|
+ 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."""
|
|
|
+ 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."""
|
|
|
+ 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 30 seconds."""
|
|
|
+ while self.monitoring_active:
|
|
|
+ try:
|
|
|
+ await self._check_order_fills()
|
|
|
+ await asyncio.sleep(30) # Wait 30 seconds
|
|
|
+ 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(30) # 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
|
|
|
+
|
|
|
+ if filled_order_ids:
|
|
|
+ logger.info(f"🎯 Detected {len(filled_order_ids)} filled orders")
|
|
|
+ await self._process_filled_orders(filled_order_ids, current_positions)
|
|
|
+
|
|
|
+ # Update tracking data
|
|
|
+ self.last_known_orders = current_order_ids
|
|
|
+ await self._update_position_tracking(current_positions)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error checking order fills: {e}")
|
|
|
+
|
|
|
+ async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
|
|
|
+ """Process filled orders and determine if they opened or closed positions."""
|
|
|
+ try:
|
|
|
+ # Create a map of current positions
|
|
|
+ current_position_map = {}
|
|
|
+ for position in current_positions:
|
|
|
+ symbol = position.get('symbol')
|
|
|
+ contracts = float(position.get('contracts', 0))
|
|
|
+ if symbol:
|
|
|
+ current_position_map[symbol] = contracts
|
|
|
+
|
|
|
+ # For each symbol, check if position size changed
|
|
|
+ for symbol, old_position_data in self.last_known_positions.items():
|
|
|
+ old_contracts = old_position_data['contracts']
|
|
|
+ current_contracts = current_position_map.get(symbol, 0)
|
|
|
+
|
|
|
+ if old_contracts != current_contracts:
|
|
|
+ # Position changed - determine if it's open or close
|
|
|
+ await self._handle_position_change(symbol, old_position_data, current_contracts)
|
|
|
+
|
|
|
+ # Check for new positions (symbols not in last_known_positions)
|
|
|
+ for symbol, current_contracts in current_position_map.items():
|
|
|
+ if symbol not in self.last_known_positions and current_contracts != 0:
|
|
|
+ # New position opened
|
|
|
+ await self._handle_new_position(symbol, current_contracts)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error processing filled orders: {e}")
|
|
|
+
|
|
|
+ async def _handle_position_change(self, symbol: str, old_position_data: dict, current_contracts: float):
|
|
|
+ """Handle when an existing position changes size."""
|
|
|
+ old_contracts = old_position_data['contracts']
|
|
|
+ old_entry_price = old_position_data['entry_price']
|
|
|
+
|
|
|
+ # Get current market price
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
+ current_price = 0
|
|
|
+ if market_data:
|
|
|
+ current_price = float(market_data['ticker'].get('last', 0))
|
|
|
+
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+
|
|
|
+ if current_contracts == 0 and old_contracts != 0:
|
|
|
+ # Position closed
|
|
|
+ await self._send_close_trade_notification(token, old_contracts, old_entry_price, current_price)
|
|
|
+ elif abs(current_contracts) > abs(old_contracts):
|
|
|
+ # Position increased
|
|
|
+ added_contracts = current_contracts - old_contracts
|
|
|
+ await self._send_open_trade_notification(token, added_contracts, current_price, "increased")
|
|
|
+ elif abs(current_contracts) < abs(old_contracts):
|
|
|
+ # Position decreased (partial close)
|
|
|
+ closed_contracts = old_contracts - current_contracts
|
|
|
+ await self._send_partial_close_notification(token, closed_contracts, old_entry_price, current_price)
|
|
|
+
|
|
|
+ async def _handle_new_position(self, symbol: str, contracts: float):
|
|
|
+ """Handle when a new position is opened."""
|
|
|
+ # Get current market price
|
|
|
+ market_data = self.client.get_market_data(symbol)
|
|
|
+ current_price = 0
|
|
|
+ if market_data:
|
|
|
+ current_price = float(market_data['ticker'].get('last', 0))
|
|
|
+
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ await self._send_open_trade_notification(token, contracts, current_price, "opened")
|
|
|
+
|
|
|
+ async def _update_position_tracking(self, current_positions: list):
|
|
|
+ """Update the position tracking data."""
|
|
|
+ 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
|
|
|
+ }
|
|
|
+
|
|
|
+ self.last_known_positions = new_position_map
|
|
|
+
|
|
|
+ async def _send_open_trade_notification(self, token: str, contracts: float, price: float, action: str):
|
|
|
+ """Send notification for opened/increased position."""
|
|
|
+ position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
+ contracts_abs = abs(contracts)
|
|
|
+ value = contracts_abs * price
|
|
|
+
|
|
|
+ if action == "opened":
|
|
|
+ title = "🚀 Position Opened"
|
|
|
+ action_text = f"New {position_type} position opened"
|
|
|
+ else:
|
|
|
+ title = "📈 Position Increased"
|
|
|
+ action_text = f"{position_type} position increased"
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+{title}
|
|
|
+
|
|
|
+📊 <b>Trade Details:</b>
|
|
|
+• Token: {token}
|
|
|
+• Direction: {position_type}
|
|
|
+• Size: {contracts_abs} contracts
|
|
|
+• Entry Price: ${price:,.2f}
|
|
|
+• Value: ${value:,.2f}
|
|
|
+
|
|
|
+✅ <b>Status:</b> {action_text}
|
|
|
+⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+
|
|
|
+📱 Use /positions to view all positions
|
|
|
+ """
|
|
|
+
|
|
|
+ await self.send_message(message.strip())
|
|
|
+ logger.info(f"📢 Sent open trade notification: {token} {position_type} {contracts_abs} @ ${price}")
|
|
|
+
|
|
|
+ async def _send_close_trade_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
|
|
|
+ """Send notification for closed position with P&L."""
|
|
|
+ position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
+ contracts_abs = abs(contracts)
|
|
|
+
|
|
|
+ # Calculate P&L
|
|
|
+ if contracts > 0: # Long position
|
|
|
+ pnl = (exit_price - entry_price) * contracts_abs
|
|
|
+ else: # Short position
|
|
|
+ pnl = (entry_price - exit_price) * contracts_abs
|
|
|
+
|
|
|
+ pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
|
|
|
+ pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
+
|
|
|
+ exit_value = contracts_abs * exit_price
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+🎯 <b>Position Closed</b>
|
|
|
+
|
|
|
+📊 <b>Trade Summary:</b>
|
|
|
+• Token: {token}
|
|
|
+• Direction: {position_type}
|
|
|
+• Size: {contracts_abs} contracts
|
|
|
+• Entry Price: ${entry_price:,.2f}
|
|
|
+• Exit Price: ${exit_price:,.2f}
|
|
|
+• Exit Value: ${exit_value:,.2f}
|
|
|
+
|
|
|
+{pnl_emoji} <b>Profit & Loss:</b>
|
|
|
+• P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
|
|
|
+• Result: {"PROFIT" if pnl >= 0 else "LOSS"}
|
|
|
+
|
|
|
+✅ <b>Status:</b> Position fully closed
|
|
|
+⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+
|
|
|
+📊 Use /stats to view updated performance
|
|
|
+ """
|
|
|
+
|
|
|
+ await self.send_message(message.strip())
|
|
|
+ logger.info(f"📢 Sent close trade notification: {token} {position_type} P&L: ${pnl:.2f}")
|
|
|
+
|
|
|
+ async def _send_partial_close_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
|
|
|
+ """Send notification for partially closed position."""
|
|
|
+ position_type = "LONG" if contracts > 0 else "SHORT"
|
|
|
+ contracts_abs = abs(contracts)
|
|
|
+
|
|
|
+ # Calculate P&L for closed portion
|
|
|
+ if contracts > 0: # Long position
|
|
|
+ pnl = (exit_price - entry_price) * contracts_abs
|
|
|
+ else: # Short position
|
|
|
+ pnl = (entry_price - exit_price) * contracts_abs
|
|
|
+
|
|
|
+ pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
|
|
|
+ pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
+
|
|
|
+ message = f"""
|
|
|
+📉 <b>Position Partially Closed</b>
|
|
|
+
|
|
|
+📊 <b>Partial Close Details:</b>
|
|
|
+• Token: {token}
|
|
|
+• Direction: {position_type}
|
|
|
+• Closed Size: {contracts_abs} contracts
|
|
|
+• Entry Price: ${entry_price:,.2f}
|
|
|
+• Exit Price: ${exit_price:,.2f}
|
|
|
+
|
|
|
+{pnl_emoji} <b>Partial P&L:</b>
|
|
|
+• P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
|
|
|
+
|
|
|
+✅ <b>Status:</b> Partial position closed
|
|
|
+⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+
|
|
|
+📈 Use /positions to view remaining position
|
|
|
+ """
|
|
|
+
|
|
|
+ await self.send_message(message.strip())
|
|
|
+ logger.info(f"📢 Sent partial close notification: {token} {position_type} Partial P&L: ${pnl:.2f}")
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ status_text = f"""
|
|
|
+🔄 <b>Order Monitoring Status</b>
|
|
|
+
|
|
|
+📊 <b>Current Status:</b>
|
|
|
+• Active: {'✅ Yes' if self.monitoring_active else '❌ No'}
|
|
|
+• Check Interval: 30 seconds
|
|
|
+• Orders Tracked: {len(self.last_known_orders)}
|
|
|
+• Positions Tracked: {len(self.last_known_positions)}
|
|
|
+
|
|
|
+📈 <b>Notifications:</b>
|
|
|
+• 🚀 Position Opened
|
|
|
+• 📈 Position Increased
|
|
|
+• 📉 Position Partially Closed
|
|
|
+• 🎯 Position Closed (with P&L)
|
|
|
+
|
|
|
+⏰ <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
|
|
|
+• Instant Telegram notifications
|
|
|
+ """
|
|
|
+
|
|
|
+ await update.message.reply_text(status_text.strip(), parse_mode='HTML')
|
|
|
+
|
|
|
|
|
|
async def main_async():
|
|
|
"""Async main entry point for the Telegram bot."""
|