|
@@ -827,7 +827,7 @@ Tap any button below for instant access to bot functions:
|
|
|
# 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, 'buy', token_amount, actual_price, order_id)
|
|
|
+ action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
|
|
|
|
|
|
success_message = f"""
|
|
|
✅ <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
@@ -874,7 +874,7 @@ Tap any button below for instant access to bot functions:
|
|
|
# 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, 'sell', token_amount, actual_price, order_id)
|
|
|
+ action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
|
|
|
|
|
|
success_message = f"""
|
|
|
✅ <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
|
|
@@ -915,7 +915,7 @@ Tap any button below for instant access to bot functions:
|
|
|
# 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)
|
|
|
+ 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"
|
|
|
|
|
@@ -1044,7 +1044,7 @@ Tap any button below for instant access to bot functions:
|
|
|
# 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)
|
|
|
+ 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"
|
|
|
|
|
@@ -1092,7 +1092,7 @@ Tap any button below for instant access to bot functions:
|
|
|
# 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)
|
|
|
+ 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"
|
|
|
|
|
@@ -2063,7 +2063,7 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
logger.error(f"❌ Error checking external trades: {e}")
|
|
|
|
|
|
async def _process_external_trade(self, trade: Dict[str, Any]):
|
|
|
- """Process an individual external trade."""
|
|
|
+ """Process an individual external trade and determine if it's opening or closing a position."""
|
|
|
try:
|
|
|
# Extract trade information
|
|
|
symbol = trade.get('symbol', '')
|
|
@@ -2076,405 +2076,236 @@ This will place a limit {exit_side} order at ${profit_price:,.2f} to capture pro
|
|
|
if not all([symbol, side, amount, price]):
|
|
|
return
|
|
|
|
|
|
- # Record trade in stats
|
|
|
- self.stats.record_trade(symbol, side, amount, price, trade_id)
|
|
|
+ # 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 notification for significant trades
|
|
|
- await self._send_external_trade_notification(trade)
|
|
|
+ # 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}")
|
|
|
+ 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_external_trade_notification(self, trade: Dict[str, Any]):
|
|
|
- """Send notification for external trades."""
|
|
|
+ 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:
|
|
|
- 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
|
|
|
+ position = self.stats.get_enhanced_position_state(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"
|
|
|
+ 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"
|
|
|
|
|
|
- # Determine trade type and emoji
|
|
|
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
|
|
|
- trade_value = amount * price
|
|
|
+ # 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)
|
|
|
|
|
|
- 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> Direct Platform Trade
|
|
|
-⏰ <b>Time:</b> {time_str}
|
|
|
-
|
|
|
-📈 <b>Note:</b> This trade was executed outside the Telegram bot
|
|
|
-📊 Stats have been automatically updated
|
|
|
- """
|
|
|
+ elif action_type in ['long_increased', 'short_increased']:
|
|
|
+ await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str)
|
|
|
|
|
|
- await self.send_message(message.strip())
|
|
|
- logger.info(f"📢 Sent external trade notification: {side} {amount} {token}")
|
|
|
+ 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)
|
|
|
|
|
|
- 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
|
|
|
+ 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)
|
|
|
|
|
|
- stop_loss_triggers = []
|
|
|
+ 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)
|
|
|
|
|
|
- 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
|
|
|
- self.stats.record_trade(symbol, exit_side, contracts_abs, current_price, order.get('id', '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))
|
|
|
+ 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 executing automatic stop loss: {e}")
|
|
|
+ logger.error(f"❌ Error sending enhanced trade notification: {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>
|
|
|
+ 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>Position Details:</b>
|
|
|
+📊 <b>New {position_type} Position:</b>
|
|
|
• Token: {token}
|
|
|
• Direction: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Current Price: ${current_price:,.2f}
|
|
|
+• Entry Size: {amount} {token}
|
|
|
+• Entry Price: ${price:,.2f}
|
|
|
+• Position Value: ${trade_value:,.2f}
|
|
|
|
|
|
-🔴 <b>Loss Details:</b>
|
|
|
-• Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
|
|
|
-• Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
|
|
|
+{side_emoji} <b>Trade Details:</b>
|
|
|
+• Side: {side.upper()}
|
|
|
+• Order Type: Market/Limit
|
|
|
+• Status: OPENED ✅
|
|
|
|
|
|
-📋 <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}")
|
|
|
+⏰ <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 _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']
|
|
|
+ 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 "🔴"
|
|
|
|
|
|
- # 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))
|
|
|
+ total_size = abs(position['contracts'])
|
|
|
+ avg_entry = position['avg_entry_price']
|
|
|
+ entry_count = position['entry_count']
|
|
|
+ total_value = total_size * avg_entry
|
|
|
|
|
|
- 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))
|
|
|
+ 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
|
|
|
+ """
|
|
|
|
|
|
- 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 = {}
|
|
|
+ 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"
|
|
|
|
|
|
- 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
|
|
|
- }
|
|
|
+ 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 "🔴"
|
|
|
|
|
|
- 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"
|
|
|
+ partial_value = amount * price
|
|
|
|
|
|
message = f"""
|
|
|
-{title}
|
|
|
+📉 <b>Position Partially Closed</b>
|
|
|
|
|
|
-📊 <b>Trade Details:</b>
|
|
|
+📊 <b>{position_type} Partial Exit:</b>
|
|
|
• Token: {token}
|
|
|
• Direction: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${price:,.2f}
|
|
|
-• Value: ${value:,.2f}
|
|
|
+• Closed Size: {amount} {token}
|
|
|
+• Exit Price: ${price:,.2f}
|
|
|
+• Remaining Size: {remaining_size} {token}
|
|
|
|
|
|
-✅ <b>Status:</b> {action_text}
|
|
|
-⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+{pnl_emoji} <b>Partial P&L:</b>
|
|
|
+• Entry Price: ${avg_entry:,.2f}
|
|
|
+• Exit Value: ${partial_value:,.2f}
|
|
|
+• P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
|
|
|
+• Result: {"PROFIT" if pnl >= 0 else "LOSS"}
|
|
|
+
|
|
|
+💰 <b>Position Status:</b>
|
|
|
+• Status: PARTIALLY CLOSED 📉
|
|
|
+• Take Profit Strategy: Active
|
|
|
|
|
|
-📱 Use /positions to view all positions
|
|
|
+⏰ <b>Time:</b> {time_str}
|
|
|
+📊 Use /positions to view remaining position
|
|
|
"""
|
|
|
|
|
|
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
|
|
|
+ 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"
|
|
|
|
|
|
- pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
|
|
|
+ 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 "🔴"
|
|
|
|
|
|
- exit_value = contracts_abs * exit_price
|
|
|
+ entry_count = position.get('entry_count', 1)
|
|
|
+ exit_value = amount * price
|
|
|
|
|
|
message = f"""
|
|
|
-🎯 <b>Position Closed</b>
|
|
|
+🎯 <b>Position Fully Closed</b>
|
|
|
|
|
|
-📊 <b>Trade Summary:</b>
|
|
|
+📊 <b>{position_type} Position Summary:</b>
|
|
|
• Token: {token}
|
|
|
• Direction: {position_type}
|
|
|
-• Size: {contracts_abs} contracts
|
|
|
-• Entry Price: ${entry_price:,.2f}
|
|
|
-• Exit Price: ${exit_price:,.2f}
|
|
|
+• Total Size: {amount} {token}
|
|
|
+• Average Entry: ${avg_entry:,.2f}
|
|
|
+• Exit Price: ${price:,.2f}
|
|
|
• Exit Value: ${exit_value:,.2f}
|
|
|
|
|
|
-{pnl_emoji} <b>Profit & Loss:</b>
|
|
|
+{pnl_emoji} <b>Total P&L:</b>
|
|
|
• P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
|
|
|
• Result: {"PROFIT" if pnl >= 0 else "LOSS"}
|
|
|
+• Entry Points Used: {entry_count}
|
|
|
|
|
|
-✅ <b>Status:</b> Position fully closed
|
|
|
-⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+✅ <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"📢 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 "🔴"
|
|
|
+ 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 Partially Closed</b>
|
|
|
+🔄 <b>Position Flipped</b>
|
|
|
|
|
|
-📊 <b>Partial Close Details:</b>
|
|
|
+📊 <b>Direction Change:</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}%)
|
|
|
+• Previous: {old_type} position
|
|
|
+• New: {new_type} position
|
|
|
+• Size: {amount} {token}
|
|
|
+• Price: ${price:,.2f}
|
|
|
|
|
|
-✅ <b>Status:</b> Partial position closed
|
|
|
-⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
|
|
|
+🎯 <b>Trade Summary:</b>
|
|
|
+• {old_type} position: CLOSED ✅
|
|
|
+• {new_type} position: OPENED 🚀
|
|
|
+• Flip Price: ${price:,.2f}
|
|
|
+• Status: POSITION REVERSED
|
|
|
|
|
|
-📈 Use /positions to view remaining position
|
|
|
+⏰ <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"📢 Sent partial close notification: {token} {position_type} Partial P&L: ${pnl:.2f}")
|
|
|
+ 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."""
|
|
@@ -3000,6 +2831,372 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
|
|
|
await update.message.reply_text(error_message)
|
|
|
logger.error(f"Error in monthly 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}")
|
|
|
+ return 'long_opened' if old_contracts == 0 else '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
|
|
|
+ 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}")
|
|
|
+ return 'short_opened' if position['contracts'] == -amount else '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 main_async():
|
|
|
"""Async main entry point for the Telegram bot."""
|