|
@@ -243,7 +243,12 @@ class PositionTracker:
|
|
|
|
|
|
if not positions:
|
|
|
logger.warning("📊 No positions returned from exchange - this might be wrong if you have open positions!")
|
|
|
- self.current_positions = {}
|
|
|
+ # Don't clear positions during API failures - keep last known state to avoid false "position opened" notifications
|
|
|
+ if not self.current_positions:
|
|
|
+ # Only clear if we truly have no tracked positions (e.g., first startup)
|
|
|
+ self.current_positions = {}
|
|
|
+ else:
|
|
|
+ logger.info(f"📊 Keeping last known positions during API failure: {list(self.current_positions.keys())}")
|
|
|
return
|
|
|
|
|
|
logger.info(f"📊 Raw positions data from exchange: {len(positions)} positions")
|
|
@@ -287,11 +292,25 @@ class PositionTracker:
|
|
|
'return_on_equity': float(position_data.get('returnOnEquity', '0'))
|
|
|
}
|
|
|
|
|
|
+ # Check if we're recovering from API failure
|
|
|
+ had_positions_before = len(self.current_positions) > 0
|
|
|
+ getting_positions_now = len(new_positions) > 0
|
|
|
+
|
|
|
+ if had_positions_before and not getting_positions_now:
|
|
|
+ logger.info("📊 All positions appear to have been closed")
|
|
|
+ elif not had_positions_before and getting_positions_now:
|
|
|
+ logger.info(f"📊 New positions detected: {list(new_positions.keys())}")
|
|
|
+ elif had_positions_before and getting_positions_now:
|
|
|
+ logger.debug(f"✅ Updated current positions: {len(new_positions)} open positions ({list(new_positions.keys())})")
|
|
|
+ else:
|
|
|
+ logger.debug(f"✅ Updated current positions: {len(new_positions)} open positions ({list(new_positions.keys()) if new_positions else 'none'})")
|
|
|
+
|
|
|
self.current_positions = new_positions
|
|
|
- logger.debug(f"✅ Updated current positions: {len(new_positions)} open positions ({list(new_positions.keys()) if new_positions else 'none'})")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Error updating current positions: {e}", exc_info=True)
|
|
|
+ # Don't clear positions on exception - keep last known state
|
|
|
+ logger.info(f"📊 Keeping last known positions during error: {list(self.current_positions.keys()) if self.current_positions else 'none'}")
|
|
|
|
|
|
async def _process_position_changes(self, previous: Dict, current: Dict):
|
|
|
"""Process changes between previous and current positions"""
|
|
@@ -437,22 +456,89 @@ class PositionTracker:
|
|
|
|
|
|
# Check if position size changed significantly
|
|
|
size_change = abs(curr_size) - abs(prev_size)
|
|
|
- if abs(size_change) > 0.001: # Threshold to avoid noise
|
|
|
+
|
|
|
+ # Get current market price for more accurate value calculation
|
|
|
+ try:
|
|
|
+ full_symbol = f"{symbol}/USDC:USDC"
|
|
|
+ market_data = self.hl_client.get_market_data(full_symbol)
|
|
|
+ current_market_price = float(market_data.get('ticker', {}).get('last', current['entry_px'])) if market_data else current['entry_px']
|
|
|
+ except Exception:
|
|
|
+ current_market_price = current['entry_px'] # Fallback to entry price
|
|
|
+
|
|
|
+ # Calculate change value using current market price
|
|
|
+ change_value = abs(size_change) * current_market_price
|
|
|
+
|
|
|
+ # Get formatter to determine token category and appropriate thresholds
|
|
|
+ try:
|
|
|
+ from src.utils.token_display_formatter import get_formatter
|
|
|
+ formatter = get_formatter()
|
|
|
+
|
|
|
+ # Use the existing token classification system to determine threshold
|
|
|
+ price_decimals = await formatter.get_token_price_decimal_places(symbol)
|
|
|
+ amount_decimals = await formatter.get_token_amount_decimal_places(symbol)
|
|
|
+
|
|
|
+ # Determine quantity threshold based on token characteristics
|
|
|
+ # Higher precision tokens (like BTC, ETH) need smaller quantity thresholds
|
|
|
+ if price_decimals <= 2: # Major tokens like BTC, ETH (high value)
|
|
|
+ quantity_threshold = 0.0001
|
|
|
+ elif price_decimals <= 4: # Mid-tier tokens
|
|
|
+ quantity_threshold = 0.001
|
|
|
+ else: # Lower-value tokens (meme coins, etc.)
|
|
|
+ quantity_threshold = 0.01
|
|
|
+
|
|
|
+ # Also set minimum value threshold based on token category
|
|
|
+ min_value_threshold = 1.0 # Minimum $1 change for any token
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.debug(f"Could not get token formatting info for {symbol}, using defaults: {e}")
|
|
|
+ quantity_threshold = 0.001
|
|
|
+ min_value_threshold = 1.0
|
|
|
+ price_decimals = 4 # Default for fallback logging
|
|
|
+
|
|
|
+ # Trigger notification if either:
|
|
|
+ # 1. Quantity change exceeds token-specific threshold, OR
|
|
|
+ # 2. Value change exceeds minimum value threshold
|
|
|
+ should_notify = (abs(size_change) > quantity_threshold or
|
|
|
+ change_value > min_value_threshold)
|
|
|
+
|
|
|
+ if should_notify:
|
|
|
|
|
|
change_type = "Increased" if size_change > 0 else "Decreased"
|
|
|
side = "Long" if curr_size > 0 else "Short"
|
|
|
|
|
|
+ # Use formatter for consistent display
|
|
|
+ try:
|
|
|
+ formatted_new_size = await formatter.format_amount(abs(curr_size), symbol)
|
|
|
+ formatted_change = await formatter.format_amount(abs(size_change), symbol)
|
|
|
+ formatted_value_change = await formatter.format_price_with_symbol(change_value)
|
|
|
+ formatted_current_price = await formatter.format_price_with_symbol(current_market_price, symbol)
|
|
|
+ except Exception:
|
|
|
+ # Fallback formatting
|
|
|
+ formatted_new_size = f"{abs(curr_size):.4f}"
|
|
|
+ formatted_change = f"{abs(size_change):.4f}"
|
|
|
+ formatted_value_change = f"${change_value:.2f}"
|
|
|
+ formatted_current_price = f"${current_market_price:.4f}"
|
|
|
+
|
|
|
message = (
|
|
|
f"🔄 Position {change_type}\n"
|
|
|
f"Token: {symbol}\n"
|
|
|
f"Side: {side}\n"
|
|
|
- f"New Size: {abs(curr_size):.4f}\n"
|
|
|
- f"Change: {'+' if size_change > 0 else ''}{size_change:.4f}\n\n"
|
|
|
+ f"New Size: {formatted_new_size}\n"
|
|
|
+ f"Change: {'+' if size_change > 0 else ''}{formatted_change}\n"
|
|
|
+ f"Value Change: {formatted_value_change}\n"
|
|
|
+ f"Current Price: {formatted_current_price}\n\n"
|
|
|
f"💡 Use /positions to see current positions"
|
|
|
)
|
|
|
|
|
|
await self.notification_manager.send_generic_notification(message)
|
|
|
- logger.info(f"Position changed: {symbol} {change_type} by {size_change:.4f}")
|
|
|
+ logger.info(f"Position changed: {symbol} {change_type} by {size_change:.6f} (${change_value:.2f}) "
|
|
|
+ f"threshold: {quantity_threshold} qty or ${min_value_threshold} value")
|
|
|
+ else:
|
|
|
+ # Log when changes don't meet threshold (debug level to avoid spam)
|
|
|
+ logger.debug(f"Position size changed for {symbol} but below notification threshold: "
|
|
|
+ f"{size_change:.6f} quantity (${change_value:.2f} value), "
|
|
|
+ f"thresholds: {quantity_threshold} qty or ${min_value_threshold} value "
|
|
|
+ f"(price_decimals: {price_decimals if 'price_decimals' in locals() else 'unknown'})")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error handling position change for {symbol}: {e}")
|