|
@@ -22,45 +22,18 @@ class PositionsCommands(InfoCommandsBase):
|
|
await self._reply(update, "❌ Trading stats not available.")
|
|
await self._reply(update, "❌ Trading stats not available.")
|
|
return
|
|
return
|
|
|
|
|
|
- # Get open positions from DB
|
|
|
|
|
|
+ # Get open positions from DB (updated every heartbeat by PositionTracker)
|
|
open_positions = stats.get_open_positions()
|
|
open_positions = stats.get_open_positions()
|
|
if not open_positions:
|
|
if not open_positions:
|
|
await self._reply(update, "📭 No open positions\n\n💡 Use /long or /short to open a position")
|
|
await self._reply(update, "📭 No open positions\n\n💡 Use /long or /short to open a position")
|
|
return
|
|
return
|
|
|
|
|
|
- # Get current exchange data for live ROE and mark prices
|
|
|
|
- exchange_positions = self.trading_engine.get_positions() or []
|
|
|
|
- exchange_orders = self.trading_engine.get_orders() or []
|
|
|
|
-
|
|
|
|
- # Debug: Log the exchange positions data structure
|
|
|
|
- logger.info(f"🔍 Exchange positions data: {[{k: v for k, v in pos.items() if k in ['symbol']} for pos in exchange_positions[:2]]}")
|
|
|
|
-
|
|
|
|
- # Create lookup for exchange data by symbol
|
|
|
|
- exchange_data_by_symbol = {}
|
|
|
|
- for ex_pos in exchange_positions:
|
|
|
|
- # Access the nested position data in info.position
|
|
|
|
- position_data = ex_pos.get('info', {}).get('position', {})
|
|
|
|
- if position_data:
|
|
|
|
- # Get the coin symbol from the nested data
|
|
|
|
- coin = position_data.get('coin', '')
|
|
|
|
- if coin:
|
|
|
|
- exchange_data_by_symbol[coin] = position_data
|
|
|
|
-
|
|
|
|
- # Also map by the CCXT symbol for backup
|
|
|
|
- symbol = ex_pos.get('symbol', '')
|
|
|
|
- if symbol and '/' in symbol:
|
|
|
|
- base_token = symbol.split('/')[0]
|
|
|
|
- if base_token not in exchange_data_by_symbol and position_data:
|
|
|
|
- exchange_data_by_symbol[base_token] = position_data
|
|
|
|
-
|
|
|
|
- logger.info(f"🔍 Exchange data keys: {list(exchange_data_by_symbol.keys())}")
|
|
|
|
-
|
|
|
|
# Initialize totals
|
|
# Initialize totals
|
|
total_position_value = 0.0
|
|
total_position_value = 0.0
|
|
total_unrealized = 0.0
|
|
total_unrealized = 0.0
|
|
total_margin_used = 0.0
|
|
total_margin_used = 0.0
|
|
|
|
|
|
- # Build position details
|
|
|
|
|
|
+ # Build position details using only database data
|
|
positions_text = "📊 <b>Open Positions</b>\n\n"
|
|
positions_text = "📊 <b>Open Positions</b>\n\n"
|
|
|
|
|
|
for position_trade in open_positions:
|
|
for position_trade in open_positions:
|
|
@@ -70,7 +43,7 @@ class PositionsCommands(InfoCommandsBase):
|
|
base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
position_side = position_trade.get('position_side', 'unknown')
|
|
position_side = position_trade.get('position_side', 'unknown')
|
|
|
|
|
|
- # Safely convert numeric values with proper null checks
|
|
|
|
|
|
+ # Get all data from database (updated every heartbeat)
|
|
entry_price = 0.0
|
|
entry_price = 0.0
|
|
if position_trade.get('entry_price') is not None:
|
|
if position_trade.get('entry_price') is not None:
|
|
try:
|
|
try:
|
|
@@ -115,56 +88,25 @@ class PositionsCommands(InfoCommandsBase):
|
|
logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
|
|
logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
|
|
duration_str = "Error"
|
|
duration_str = "Error"
|
|
|
|
|
|
- # Get exchange data for this position (used for ROE and mark price)
|
|
|
|
- exchange_data = exchange_data_by_symbol.get(base_asset)
|
|
|
|
- logger.info(f"🔍 Looking for '{base_asset}' in exchange data. Found: {exchange_data is not None}")
|
|
|
|
- if exchange_data:
|
|
|
|
- logger.info(f"🔍 Exchange data for {base_asset}: ROE={exchange_data.get('returnOnEquity')}, entryPx={exchange_data.get('entryPx')}")
|
|
|
|
-
|
|
|
|
- # Get price data with defaults - prioritize live exchange data
|
|
|
|
- mark_price = entry_price # Default to entry price
|
|
|
|
-
|
|
|
|
- # For Hyperliquid, we need to calculate current mark price from position value and size
|
|
|
|
- if exchange_data:
|
|
|
|
- try:
|
|
|
|
- position_value = float(exchange_data.get('positionValue', 0))
|
|
|
|
- position_size = float(exchange_data.get('szi', 0))
|
|
|
|
- if position_size != 0:
|
|
|
|
- mark_price = position_value / abs(position_size)
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- logger.warning(f"Could not calculate mark price from exchange data for {symbol}")
|
|
|
|
-
|
|
|
|
- # Fallback to database mark price if calculation fails
|
|
|
|
- if mark_price == entry_price and position_trade.get('mark_price') is not None:
|
|
|
|
|
|
+ # Get mark price from database (updated every heartbeat)
|
|
|
|
+ mark_price = entry_price
|
|
|
|
+ if position_trade.get('mark_price') is not None:
|
|
try:
|
|
try:
|
|
mark_price = float(position_trade['mark_price'])
|
|
mark_price = float(position_trade['mark_price'])
|
|
except (ValueError, TypeError):
|
|
except (ValueError, TypeError):
|
|
logger.warning(f"Could not convert database mark_price for {symbol}")
|
|
logger.warning(f"Could not convert database mark_price for {symbol}")
|
|
|
|
|
|
- # Get unrealized PnL from exchange data first, then database
|
|
|
|
|
|
+ # Get unrealized PnL from database (updated every heartbeat)
|
|
unrealized_pnl = 0.0
|
|
unrealized_pnl = 0.0
|
|
- if exchange_data and exchange_data.get('unrealizedPnl') is not None:
|
|
|
|
- try:
|
|
|
|
- unrealized_pnl = float(exchange_data['unrealizedPnl'])
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- logger.warning(f"Could not convert exchange unrealizedPnl for {symbol}")
|
|
|
|
- elif position_trade.get('unrealized_pnl') is not None:
|
|
|
|
|
|
+ if position_trade.get('unrealized_pnl') is not None:
|
|
try:
|
|
try:
|
|
unrealized_pnl = float(position_trade['unrealized_pnl'])
|
|
unrealized_pnl = float(position_trade['unrealized_pnl'])
|
|
except (ValueError, TypeError):
|
|
except (ValueError, TypeError):
|
|
logger.warning(f"Could not convert database unrealized_pnl for {symbol}")
|
|
logger.warning(f"Could not convert database unrealized_pnl for {symbol}")
|
|
|
|
|
|
- # Get ROE from live exchange data (much more accurate)
|
|
|
|
|
|
+ # Get ROE from database (updated every heartbeat)
|
|
roe_percentage = 0.0
|
|
roe_percentage = 0.0
|
|
- if exchange_data and exchange_data.get('returnOnEquity') is not None:
|
|
|
|
- try:
|
|
|
|
- # Convert from decimal (0.118) to percentage (11.8%)
|
|
|
|
- roe_percentage = float(exchange_data['returnOnEquity']) * 100
|
|
|
|
- except (ValueError, TypeError):
|
|
|
|
- logger.warning(f"Could not convert exchange ROE for {symbol}")
|
|
|
|
-
|
|
|
|
- # Fallback to database ROE if exchange data not available
|
|
|
|
- if roe_percentage == 0.0 and position_trade.get('roe_percentage') is not None:
|
|
|
|
|
|
+ if position_trade.get('roe_percentage') is not None:
|
|
try:
|
|
try:
|
|
roe_percentage = float(position_trade['roe_percentage'])
|
|
roe_percentage = float(position_trade['roe_percentage'])
|
|
except (ValueError, TypeError):
|
|
except (ValueError, TypeError):
|
|
@@ -199,14 +141,20 @@ class PositionsCommands(InfoCommandsBase):
|
|
pos_emoji = "🟢" if position_side == 'long' else "🔴"
|
|
pos_emoji = "🟢" if position_side == 'long' else "🔴"
|
|
direction_text = position_side.upper()
|
|
direction_text = position_side.upper()
|
|
|
|
|
|
- leverage = position_trade.get('leverage')
|
|
|
|
|
|
+ # Get leverage from database (updated every heartbeat)
|
|
|
|
+ leverage = None
|
|
|
|
+ if position_trade.get('leverage') is not None:
|
|
|
|
+ try:
|
|
|
|
+ leverage = float(position_trade['leverage'])
|
|
|
|
+ except (ValueError, TypeError):
|
|
|
|
+ logger.warning(f"Could not convert database leverage for {symbol}")
|
|
|
|
+
|
|
if leverage is not None:
|
|
if leverage is not None:
|
|
try:
|
|
try:
|
|
- leverage_val = float(leverage)
|
|
|
|
- leverage_str = f"x{leverage_val:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage_val:.1f}" else f"x{int(leverage_val)}"
|
|
|
|
|
|
+ leverage_str = f"x{leverage:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage:.1f}" else f"x{int(leverage)}"
|
|
direction_text = f"{direction_text} {leverage_str}"
|
|
direction_text = f"{direction_text} {leverage_str}"
|
|
except ValueError:
|
|
except ValueError:
|
|
- logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
|
|
|
|
|
|
+ logger.warning(f"Could not format leverage value: {leverage} for {symbol}")
|
|
|
|
|
|
# --- Format Output String ---
|
|
# --- Format Output String ---
|
|
formatter = self._get_formatter()
|
|
formatter = self._get_formatter()
|
|
@@ -266,29 +214,6 @@ class PositionsCommands(InfoCommandsBase):
|
|
logger.error(f"Error processing position {position_trade.get('symbol', 'unknown')}: {e}")
|
|
logger.error(f"Error processing position {position_trade.get('symbol', 'unknown')}: {e}")
|
|
continue
|
|
continue
|
|
|
|
|
|
- # Calculate total unrealized P&L and total ROE
|
|
|
|
- total_unrealized_pnl = 0.0
|
|
|
|
- total_roe = 0.0
|
|
|
|
- for pos in open_positions:
|
|
|
|
- try:
|
|
|
|
- size = float(pos.get('size', 0)) if pos.get('size') is not None else 0.0
|
|
|
|
- entry_price = float(pos.get('entryPrice', 0)) if pos.get('entryPrice') is not None else 0.0
|
|
|
|
- # Handle None markPrice safely
|
|
|
|
- mark_price_raw = pos.get('markPrice')
|
|
|
|
- mark_price = float(mark_price_raw) if mark_price_raw is not None else entry_price
|
|
|
|
- roe = float(pos.get('roe_percentage', 0)) if pos.get('roe_percentage') is not None else 0.0
|
|
|
|
- if size != 0 and entry_price != 0:
|
|
|
|
- position_value = abs(size * entry_price)
|
|
|
|
- total_unrealized_pnl += size * (mark_price - entry_price)
|
|
|
|
- total_roe += roe * position_value
|
|
|
|
- total_position_value += position_value
|
|
|
|
- except (ValueError, TypeError) as e:
|
|
|
|
- logger.warning(f"Error calculating portfolio totals for position: {e}")
|
|
|
|
- continue
|
|
|
|
- # Weighted average ROE
|
|
|
|
- avg_roe = (total_roe / total_position_value) if total_position_value > 0 else 0.0
|
|
|
|
- roe_emoji = "🟢" if avg_roe >= 0 else "🔴"
|
|
|
|
-
|
|
|
|
# Add portfolio summary
|
|
# Add portfolio summary
|
|
portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
|
|
portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
|
|
positions_text += f"💼 <b>Total Portfolio:</b>\n"
|
|
positions_text += f"💼 <b>Total Portfolio:</b>\n"
|
|
@@ -302,8 +227,10 @@ class PositionsCommands(InfoCommandsBase):
|
|
margin_pnl_percentage = (total_unrealized / total_margin_used) * 100
|
|
margin_pnl_percentage = (total_unrealized / total_margin_used) * 100
|
|
positions_text += f" 📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)\n"
|
|
positions_text += f" 📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)\n"
|
|
positions_text += "\n"
|
|
positions_text += "\n"
|
|
- positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced • 🛡️ External SL\n"
|
|
|
|
- positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
|
|
|
|
|
|
+ positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
|
+ positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management\n"
|
|
|
|
+ from src.config.config import Config
|
|
|
|
+ positions_text += f"🔄 Data updated every {Config.BOT_HEARTBEAT_SECONDS}s via monitoring system"
|
|
|
|
|
|
await self._reply(update, positions_text.strip())
|
|
await self._reply(update, positions_text.strip())
|
|
|
|
|