|
@@ -10,8 +10,11 @@ from typing import Optional, Dict, Any, List
|
|
import os
|
|
import os
|
|
import json
|
|
import json
|
|
|
|
|
|
|
|
+from telegram.ext import CallbackContext
|
|
|
|
+
|
|
from src.config.config import Config
|
|
from src.config.config import Config
|
|
from src.monitoring.alarm_manager import AlarmManager
|
|
from src.monitoring.alarm_manager import AlarmManager
|
|
|
|
+from src.utils.token_display_formatter import get_formatter
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -387,6 +390,7 @@ class MarketMonitor:
|
|
"""Update position tracking and calculate P&L changes."""
|
|
"""Update position tracking and calculate P&L changes."""
|
|
try:
|
|
try:
|
|
new_position_map = {}
|
|
new_position_map = {}
|
|
|
|
+ formatter = get_formatter() # Get formatter
|
|
|
|
|
|
for position in current_positions:
|
|
for position in current_positions:
|
|
symbol = position.get('symbol')
|
|
symbol = position.get('symbol')
|
|
@@ -402,14 +406,18 @@ class MarketMonitor:
|
|
# Compare with previous positions to detect changes
|
|
# Compare with previous positions to detect changes
|
|
for symbol, new_data in new_position_map.items():
|
|
for symbol, new_data in new_position_map.items():
|
|
old_data = self.last_known_positions.get(symbol)
|
|
old_data = self.last_known_positions.get(symbol)
|
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol # Extract token
|
|
|
|
|
|
if not old_data:
|
|
if not old_data:
|
|
# New position opened
|
|
# New position opened
|
|
- logger.info(f"๐ New position detected (observed by MarketMonitor): {symbol} {new_data['contracts']} @ ${new_data['entry_price']:.4f}. TradingStats is the definitive source.")
|
|
|
|
|
|
+ amount_str = formatter.format_amount(new_data['contracts'], token)
|
|
|
|
+ price_str = formatter.format_price_with_symbol(new_data['entry_price'], token)
|
|
|
|
+ logger.info(f"๐ New position detected (observed by MarketMonitor): {symbol} {amount_str} @ {price_str}. TradingStats is the definitive source.")
|
|
elif abs(new_data['contracts'] - old_data['contracts']) > 0.000001:
|
|
elif abs(new_data['contracts'] - old_data['contracts']) > 0.000001:
|
|
# Position size changed
|
|
# Position size changed
|
|
change = new_data['contracts'] - old_data['contracts']
|
|
change = new_data['contracts'] - old_data['contracts']
|
|
- logger.info(f"๐ Position change detected (observed by MarketMonitor): {symbol} {change:+.6f} contracts. TradingStats is the definitive source.")
|
|
|
|
|
|
+ change_str = formatter.format_amount(change, token)
|
|
|
|
+ logger.info(f"๐ Position change detected (observed by MarketMonitor): {symbol} {change_str} contracts. TradingStats is the definitive source.")
|
|
|
|
|
|
# Check for closed positions
|
|
# Check for closed positions
|
|
for symbol in self.last_known_positions:
|
|
for symbol in self.last_known_positions:
|
|
@@ -760,7 +768,9 @@ class MarketMonitor:
|
|
)
|
|
)
|
|
if success:
|
|
if success:
|
|
pnl_emoji = "๐ข" if realized_pnl >= 0 else "๐ด"
|
|
pnl_emoji = "๐ข" if realized_pnl >= 0 else "๐ด"
|
|
- logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({action_type}). PNL for fill: ${realized_pnl:.2f}")
|
|
|
|
|
|
+ # Get formatter for this log line
|
|
|
|
+ formatter = get_formatter()
|
|
|
|
+ logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({action_type}). PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
|
|
symbols_with_fills.add(token)
|
|
symbols_with_fills.add(token)
|
|
if self.notification_manager:
|
|
if self.notification_manager:
|
|
await self.notification_manager.send_external_trade_notification(
|
|
await self.notification_manager.send_external_trade_notification(
|
|
@@ -777,7 +787,8 @@ class MarketMonitor:
|
|
if not fill_processed_this_iteration:
|
|
if not fill_processed_this_iteration:
|
|
if exchange_order_id_from_fill and exchange_order_id_from_fill in self.external_stop_losses:
|
|
if exchange_order_id_from_fill and exchange_order_id_from_fill in self.external_stop_losses:
|
|
stop_loss_info = self.external_stop_losses[exchange_order_id_from_fill]
|
|
stop_loss_info = self.external_stop_losses[exchange_order_id_from_fill]
|
|
- logger.info(f"๐ External SL (MM Tracking): {token} Order {exchange_order_id_from_fill} filled @ ${price_from_fill:.2f}")
|
|
|
|
|
|
+ formatter = get_formatter()
|
|
|
|
+ logger.info(f"๐ External SL (MM Tracking): {token} Order {exchange_order_id_from_fill} filled @ {formatter.format_price_with_symbol(price_from_fill, token)}")
|
|
|
|
|
|
sl_active_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
sl_active_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
if sl_active_lc:
|
|
if sl_active_lc:
|
|
@@ -789,7 +800,7 @@ class MarketMonitor:
|
|
success = stats.update_trade_position_closed(lc_id, price_from_fill, realized_pnl, trade_id)
|
|
success = stats.update_trade_position_closed(lc_id, price_from_fill, realized_pnl, trade_id)
|
|
if success:
|
|
if success:
|
|
pnl_emoji = "๐ข" if realized_pnl >= 0 else "๐ด"
|
|
pnl_emoji = "๐ข" if realized_pnl >= 0 else "๐ด"
|
|
- logger.info(f"{pnl_emoji} Lifecycle CLOSED by External SL (MM): {lc_id}. PNL: ${realized_pnl:.2f}")
|
|
|
|
|
|
+ logger.info(f"{pnl_emoji} Lifecycle CLOSED by External SL (MM): {lc_id}. PNL: {formatter.format_price_with_symbol(realized_pnl)}")
|
|
if self.notification_manager:
|
|
if self.notification_manager:
|
|
await self.notification_manager.send_stop_loss_execution_notification(
|
|
await self.notification_manager.send_stop_loss_execution_notification(
|
|
stop_loss_info, full_symbol, side_from_fill, amount_from_fill, price_from_fill,
|
|
stop_loss_info, full_symbol, side_from_fill, amount_from_fill, price_from_fill,
|
|
@@ -1163,7 +1174,7 @@ class MarketMonitor:
|
|
if not stats:
|
|
if not stats:
|
|
return
|
|
return
|
|
|
|
|
|
- # Get open positions that need stop loss activation
|
|
|
|
|
|
+ formatter = get_formatter() # Get formatter
|
|
trades_needing_sl = stats.get_pending_stop_loss_activations()
|
|
trades_needing_sl = stats.get_pending_stop_loss_activations()
|
|
|
|
|
|
if not trades_needing_sl:
|
|
if not trades_needing_sl:
|
|
@@ -1196,12 +1207,14 @@ class MarketMonitor:
|
|
trigger_reason = ""
|
|
trigger_reason = ""
|
|
|
|
|
|
if current_price and current_price > 0 and stop_loss_price and stop_loss_price > 0:
|
|
if current_price and current_price > 0 and stop_loss_price and stop_loss_price > 0:
|
|
|
|
+ current_price_str = formatter.format_price_with_symbol(current_price, token)
|
|
|
|
+ stop_loss_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
|
|
if sl_side == 'sell' and current_price <= stop_loss_price:
|
|
if sl_side == 'sell' and current_price <= stop_loss_price:
|
|
trigger_already_hit = True
|
|
trigger_already_hit = True
|
|
- trigger_reason = f"LONG SL: Current ${current_price:.4f} โค Stop ${stop_loss_price:.4f}"
|
|
|
|
|
|
+ trigger_reason = f"LONG SL: Current {current_price_str} โค Stop {stop_loss_price_str}"
|
|
elif sl_side == 'buy' and current_price >= stop_loss_price:
|
|
elif sl_side == 'buy' and current_price >= stop_loss_price:
|
|
trigger_already_hit = True
|
|
trigger_already_hit = True
|
|
- trigger_reason = f"SHORT SL: Current ${current_price:.4f} โฅ Stop ${stop_loss_price:.4f}"
|
|
|
|
|
|
+ trigger_reason = f"SHORT SL: Current {current_price_str} โฅ Stop {stop_loss_price_str}"
|
|
|
|
|
|
if trigger_already_hit:
|
|
if trigger_already_hit:
|
|
logger.warning(f"๐จ IMMEDIATE SL EXECUTION (Trades Table): {token} (Lifecycle: {lifecycle_id[:8]}) - {trigger_reason}. Executing market exit.")
|
|
logger.warning(f"๐จ IMMEDIATE SL EXECUTION (Trades Table): {token} (Lifecycle: {lifecycle_id[:8]}) - {trigger_reason}. Executing market exit.")
|
|
@@ -1219,14 +1232,17 @@ class MarketMonitor:
|
|
|
|
|
|
|
|
|
|
if self.notification_manager:
|
|
if self.notification_manager:
|
|
|
|
+ # Re-fetch formatted prices for notification if not already strings
|
|
|
|
+ current_price_str_notify = formatter.format_price_with_symbol(current_price, token) if current_price else "N/A"
|
|
|
|
+ stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token) if stop_loss_price else "N/A"
|
|
await self.notification_manager.send_generic_notification(
|
|
await self.notification_manager.send_generic_notification(
|
|
f"๐จ <b>Immediate Stop Loss Execution</b>\n\n"
|
|
f"๐จ <b>Immediate Stop Loss Execution</b>\n\n"
|
|
f"๐ <b>Source: Unified Trades Table</b>\n"
|
|
f"๐ <b>Source: Unified Trades Table</b>\n"
|
|
f"Token: {token}\n"
|
|
f"Token: {token}\n"
|
|
f"Lifecycle ID: {lifecycle_id[:8]}...\n"
|
|
f"Lifecycle ID: {lifecycle_id[:8]}...\n"
|
|
f"Position Type: {position_side.upper()}\n"
|
|
f"Position Type: {position_side.upper()}\n"
|
|
- f"SL Trigger Price: ${stop_loss_price:.4f}\n"
|
|
|
|
- f"Current Market Price: ${current_price:.4f}\n"
|
|
|
|
|
|
+ f"SL Trigger Price: {stop_loss_price_str_notify}\n"
|
|
|
|
+ f"Current Market Price: {current_price_str_notify}\n"
|
|
f"Trigger Logic: {trigger_reason}\n"
|
|
f"Trigger Logic: {trigger_reason}\n"
|
|
f"Action: Market close order placed immediately\n"
|
|
f"Action: Market close order placed immediately\n"
|
|
f"Exit Order ID: {exit_order_id}\n"
|
|
f"Exit Order ID: {exit_order_id}\n"
|
|
@@ -1284,16 +1300,19 @@ class MarketMonitor:
|
|
|
|
|
|
if sl_exchange_order_id: # If an actual exchange order ID was returned for the SL
|
|
if sl_exchange_order_id: # If an actual exchange order ID was returned for the SL
|
|
stats.link_stop_loss_to_trade(lifecycle_id, sl_exchange_order_id, stop_loss_price)
|
|
stats.link_stop_loss_to_trade(lifecycle_id, sl_exchange_order_id, stop_loss_price)
|
|
- logger.info(f"โ
Activated {position_side.upper()} stop loss for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price ${stop_loss_price:.4f}, Exchange SL Order ID: {sl_exchange_order_id}")
|
|
|
|
|
|
+ stop_loss_price_str_log = formatter.format_price_with_symbol(stop_loss_price, token)
|
|
|
|
+ logger.info(f"โ
Activated {position_side.upper()} stop loss for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price {stop_loss_price_str_log}, Exchange SL Order ID: {sl_exchange_order_id}")
|
|
if self.notification_manager:
|
|
if self.notification_manager:
|
|
|
|
+ current_price_str_notify = formatter.format_price_with_symbol(current_price, token) if current_price else 'Unknown'
|
|
|
|
+ stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token)
|
|
await self.notification_manager.send_generic_notification(
|
|
await self.notification_manager.send_generic_notification(
|
|
f"๐ <b>Stop Loss Activated</b>\n\n"
|
|
f"๐ <b>Stop Loss Activated</b>\n\n"
|
|
f"๐ <b>Source: Unified Trades Table</b>\n"
|
|
f"๐ <b>Source: Unified Trades Table</b>\n"
|
|
f"Token: {token}\n"
|
|
f"Token: {token}\n"
|
|
f"Lifecycle ID: {lifecycle_id[:8]}...\n"
|
|
f"Lifecycle ID: {lifecycle_id[:8]}...\n"
|
|
f"Position Type: {position_side.upper()}\n"
|
|
f"Position Type: {position_side.upper()}\n"
|
|
- f"Stop Loss Price: ${stop_loss_price:.4f}\n"
|
|
|
|
- f"Current Price: ${current_price:.4f if current_price else 'Unknown'}\n"
|
|
|
|
|
|
+ f"Stop Loss Price: {stop_loss_price_str_notify}\n"
|
|
|
|
+ f"Current Price: {current_price_str_notify}\n"
|
|
f"Exchange SL Order ID: {sl_exchange_order_id}\n" # Actual exchange order
|
|
f"Exchange SL Order ID: {sl_exchange_order_id}\n" # Actual exchange order
|
|
f"Time: {datetime.now().strftime('%H:%M:%S')}"
|
|
f"Time: {datetime.now().strftime('%H:%M:%S')}"
|
|
)
|
|
)
|
|
@@ -1642,35 +1661,35 @@ class MarketMonitor:
|
|
"""Estimate entry price for an orphaned position by checking recent fills and market data."""
|
|
"""Estimate entry price for an orphaned position by checking recent fills and market data."""
|
|
try:
|
|
try:
|
|
entry_fill_side = 'buy' if side == 'long' else 'sell'
|
|
entry_fill_side = 'buy' if side == 'long' else 'sell'
|
|
|
|
+ formatter = get_formatter()
|
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
recent_fills = self.trading_engine.get_recent_fills(symbol=symbol, limit=20)
|
|
recent_fills = self.trading_engine.get_recent_fills(symbol=symbol, limit=20)
|
|
if recent_fills:
|
|
if recent_fills:
|
|
symbol_side_fills = [
|
|
symbol_side_fills = [
|
|
fill for fill in recent_fills
|
|
fill for fill in recent_fills
|
|
if fill.get('symbol') == symbol and fill.get('side') == entry_fill_side and float(fill.get('amount',0)) > 0
|
|
if fill.get('symbol') == symbol and fill.get('side') == entry_fill_side and float(fill.get('amount',0)) > 0
|
|
]
|
|
]
|
|
- # Try to find a fill that closely matches quantity, prioritizing most recent of those
|
|
|
|
- # This is a heuristic. A perfect match is unlikely for externally opened positions.
|
|
|
|
if symbol_side_fills:
|
|
if symbol_side_fills:
|
|
- # Sort by timestamp (newest first) then by how close amount is to position size
|
|
|
|
symbol_side_fills.sort(key=lambda f: (
|
|
symbol_side_fills.sort(key=lambda f: (
|
|
datetime.fromtimestamp(f.get('timestamp') / 1000, tz=timezone.utc) if f.get('timestamp') else datetime.min.replace(tzinfo=timezone.utc),
|
|
datetime.fromtimestamp(f.get('timestamp') / 1000, tz=timezone.utc) if f.get('timestamp') else datetime.min.replace(tzinfo=timezone.utc),
|
|
abs(float(f.get('amount',0)) - contracts)
|
|
abs(float(f.get('amount',0)) - contracts)
|
|
- ), reverse=True) # Newest first
|
|
|
|
|
|
+ ), reverse=True)
|
|
|
|
|
|
- best_fill = symbol_side_fills[0] # Take the newest fill that matches side
|
|
|
|
|
|
+ best_fill = symbol_side_fills[0]
|
|
fill_price = float(best_fill.get('price', 0))
|
|
fill_price = float(best_fill.get('price', 0))
|
|
|
|
+ fill_amount = float(best_fill.get('amount', 0))
|
|
if fill_price > 0:
|
|
if fill_price > 0:
|
|
- logger.info(f"๐ก AUTO-SYNC: Estimated entry for {side} {symbol} via recent {entry_fill_side} fill: ${fill_price:.4f} (Amount: {best_fill.get('amount')})")
|
|
|
|
|
|
+ logger.info(f"๐ก AUTO-SYNC: Estimated entry for {side} {symbol} via recent {entry_fill_side} fill: {formatter.format_price_with_symbol(fill_price, token)} (Amount: {formatter.format_amount(fill_amount, token)})")
|
|
return fill_price
|
|
return fill_price
|
|
|
|
|
|
market_data = self.trading_engine.get_market_data(symbol)
|
|
market_data = self.trading_engine.get_market_data(symbol)
|
|
if market_data and market_data.get('ticker'):
|
|
if market_data and market_data.get('ticker'):
|
|
current_price = float(market_data['ticker'].get('last', 0))
|
|
current_price = float(market_data['ticker'].get('last', 0))
|
|
if current_price > 0:
|
|
if current_price > 0:
|
|
- logger.warning(f"โ ๏ธ AUTO-SYNC: Using current market price as entry estimate for {side} {symbol}: ${current_price:.4f}")
|
|
|
|
|
|
+ logger.warning(f"โ ๏ธ AUTO-SYNC: Using current market price as entry estimate for {side} {symbol}: {formatter.format_price_with_symbol(current_price, token)}")
|
|
return current_price
|
|
return current_price
|
|
|
|
|
|
- if market_data and market_data.get('ticker'): # Bid/Ask as last resort
|
|
|
|
|
|
+ if market_data and market_data.get('ticker'):
|
|
bid = float(market_data['ticker'].get('bid', 0))
|
|
bid = float(market_data['ticker'].get('bid', 0))
|
|
ask = float(market_data['ticker'].get('ask', 0))
|
|
ask = float(market_data['ticker'].get('ask', 0))
|
|
if bid > 0 and ask > 0: return (bid + ask) / 2
|
|
if bid > 0 and ask > 0: return (bid + ask) / 2
|
|
@@ -1690,6 +1709,7 @@ class MarketMonitor:
|
|
logger.warning("โ ๏ธ STARTUP: TradingStats not available for auto-sync.")
|
|
logger.warning("โ ๏ธ STARTUP: TradingStats not available for auto-sync.")
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ formatter = get_formatter() # Ensure formatter is available
|
|
exchange_positions = self.trading_engine.get_positions() or []
|
|
exchange_positions = self.trading_engine.get_positions() or []
|
|
if not exchange_positions:
|
|
if not exchange_positions:
|
|
logger.info("โ
STARTUP: No positions found on exchange.")
|
|
logger.info("โ
STARTUP: No positions found on exchange.")
|
|
@@ -1699,18 +1719,18 @@ class MarketMonitor:
|
|
for exchange_pos in exchange_positions:
|
|
for exchange_pos in exchange_positions:
|
|
symbol = exchange_pos.get('symbol')
|
|
symbol = exchange_pos.get('symbol')
|
|
contracts_abs = abs(float(exchange_pos.get('contracts', 0)))
|
|
contracts_abs = abs(float(exchange_pos.get('contracts', 0)))
|
|
|
|
+ token_for_log = symbol.split('/')[0] if symbol and '/' in symbol else symbol # Prepare token for logging
|
|
|
|
|
|
if not (symbol and contracts_abs > 1e-9): continue
|
|
if not (symbol and contracts_abs > 1e-9): continue
|
|
|
|
|
|
existing_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
|
|
existing_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
|
|
if not existing_trade_lc:
|
|
if not existing_trade_lc:
|
|
- # Determine position side and entry order side
|
|
|
|
position_side, order_side = '', ''
|
|
position_side, order_side = '', ''
|
|
ccxt_side = exchange_pos.get('side', '').lower()
|
|
ccxt_side = exchange_pos.get('side', '').lower()
|
|
if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
|
|
if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
|
|
elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
|
|
elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
|
|
|
|
|
|
- if not position_side: # Fallback to raw info
|
|
|
|
|
|
+ if not position_side:
|
|
raw_info = exchange_pos.get('info', {}).get('position', {})
|
|
raw_info = exchange_pos.get('info', {}).get('position', {})
|
|
if isinstance(raw_info, dict):
|
|
if isinstance(raw_info, dict):
|
|
szi_str = raw_info.get('szi')
|
|
szi_str = raw_info.get('szi')
|
|
@@ -1720,10 +1740,10 @@ class MarketMonitor:
|
|
if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
|
|
if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
|
|
elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
|
|
elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
|
|
|
|
|
|
- if not position_side: # Final fallback
|
|
|
|
|
|
+ if not position_side:
|
|
contracts_val = float(exchange_pos.get('contracts',0))
|
|
contracts_val = float(exchange_pos.get('contracts',0))
|
|
if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
|
|
if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
|
|
- elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell' # Assumes negative for short
|
|
|
|
|
|
+ elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell'
|
|
else:
|
|
else:
|
|
logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
|
|
logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
|
|
continue
|
|
continue
|
|
@@ -1743,7 +1763,7 @@ class MarketMonitor:
|
|
logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
|
|
logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
|
|
continue
|
|
continue
|
|
|
|
|
|
- logger.info(f"๐ STARTUP: Auto-syncing orphaned position: {symbol} {position_side.upper()} {contracts_abs} @ ${entry_price:.4f} {price_source_log}")
|
|
|
|
|
|
+ logger.info(f"๐ STARTUP: Auto-syncing orphaned position: {symbol} {position_side.upper()} {formatter.format_amount(contracts_abs, token_for_log)} @ {formatter.format_price_with_symbol(entry_price, token_for_log)} {price_source_log}")
|
|
|
|
|
|
lifecycle_id = stats.create_trade_lifecycle(
|
|
lifecycle_id = stats.create_trade_lifecycle(
|
|
symbol=symbol, side=order_side,
|
|
symbol=symbol, side=order_side,
|
|
@@ -1763,11 +1783,10 @@ class MarketMonitor:
|
|
else: logger.error(f"โ STARTUP: Failed to update lifecycle for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
|
|
else: logger.error(f"โ STARTUP: Failed to update lifecycle for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
|
|
else: logger.error(f"โ STARTUP: Failed to create lifecycle for {symbol}.")
|
|
else: logger.error(f"โ STARTUP: Failed to create lifecycle for {symbol}.")
|
|
|
|
|
|
- if synced_count == 0 and exchange_positions: # Positions existed but all were tracked
|
|
|
|
|
|
+ if synced_count == 0 and exchange_positions:
|
|
logger.info("โ
STARTUP: All existing exchange positions are already tracked.")
|
|
logger.info("โ
STARTUP: All existing exchange positions are already tracked.")
|
|
elif synced_count > 0:
|
|
elif synced_count > 0:
|
|
logger.info(f"๐ STARTUP: Auto-synced {synced_count} orphaned position(s).")
|
|
logger.info(f"๐ STARTUP: Auto-synced {synced_count} orphaned position(s).")
|
|
- # If no positions and no synced, it's already logged.
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
logger.error(f"โ Error in startup auto-sync: {e}", exc_info=True)
|
|
logger.error(f"โ Error in startup auto-sync: {e}", exc_info=True)
|
|
@@ -1777,34 +1796,39 @@ class MarketMonitor:
|
|
try:
|
|
try:
|
|
if not self.notification_manager: return
|
|
if not self.notification_manager: return
|
|
|
|
|
|
|
|
+ formatter = get_formatter()
|
|
token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
|
|
unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
|
|
- pnl_percentage = float(exchange_pos.get('percentage', 0)) # CCXT standard field for PNL %
|
|
|
|
pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
|
|
pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
|
|
|
|
|
|
- notification_text = (
|
|
|
|
- f"๐จ <b>Bot Startup: Position Auto-Synced</b>\n\n"
|
|
|
|
- f"Token: {token}\n"
|
|
|
|
- f"Lifecycle ID: {lifecycle_id[:8]}...\n"
|
|
|
|
- f"Direction: {position_side.upper()}\n"
|
|
|
|
- f"Size: {contracts:.6f} {token}\n"
|
|
|
|
- f"Entry Price: ${entry_price:,.4f} {price_source_log}\n"
|
|
|
|
- f"{pnl_emoji} P&L (Unrealized): ${unrealized_pnl:,.2f}\n"
|
|
|
|
- f"Reason: Position found on exchange without bot record.\n"
|
|
|
|
- f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
|
|
|
|
- f"โ
Position now tracked. Use /sl or /tp if needed."
|
|
|
|
- )
|
|
|
|
|
|
+ size_str = formatter.format_amount(contracts, token)
|
|
|
|
+ entry_price_str = formatter.format_price_with_symbol(entry_price, token)
|
|
|
|
+ pnl_str = formatter.format_price_with_symbol(unrealized_pnl)
|
|
|
|
+
|
|
|
|
+ notification_text_parts = [
|
|
|
|
+ f"๐จ <b>Bot Startup: Position Auto-Synced</b>\n",
|
|
|
|
+ f"Token: {token}",
|
|
|
|
+ f"Lifecycle ID: {lifecycle_id[:8]}...",
|
|
|
|
+ f"Direction: {position_side.upper()}",
|
|
|
|
+ f"Size: {size_str} {token}",
|
|
|
|
+ f"Entry Price: {entry_price_str} {price_source_log}",
|
|
|
|
+ f"{pnl_emoji} P&L (Unrealized): {pnl_str}",
|
|
|
|
+ f"Reason: Position found on exchange without bot record.",
|
|
|
|
+ # f"Time: {datetime.now().strftime('%H:%M:%S')}", # Time is in the main header of notification usually
|
|
|
|
+ "\nโ
Position now tracked. Use /sl or /tp if needed."
|
|
|
|
+ ]
|
|
|
|
|
|
liq_price = float(exchange_pos.get('liquidationPrice', 0))
|
|
liq_price = float(exchange_pos.get('liquidationPrice', 0))
|
|
- if liq_price > 0: notification_text += f"โ ๏ธ Liquidation: ${liq_price:,.2f}\n"
|
|
|
|
|
|
+ if liq_price > 0:
|
|
|
|
+ liq_price_str = formatter.format_price_with_symbol(liq_price, token)
|
|
|
|
+ notification_text_parts.append(f"โ ๏ธ Liquidation: {liq_price_str}")
|
|
|
|
|
|
- notification_text += (
|
|
|
|
- f"\n๐ <b>Discovered on bot startup</b>\n"
|
|
|
|
- f"โฐ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
|
|
|
|
- f"โ
Position now tracked. Use /sl or /tp if needed."
|
|
|
|
- )
|
|
|
|
|
|
+ # Combined details into the main block
|
|
|
|
+ # notification_text_parts.append("\n๐ <b>Discovered on bot startup</b>")
|
|
|
|
+ # notification_text_parts.append(f"โฐ Time: {datetime.now().strftime('%H:%M:%S')}")
|
|
|
|
+ # notification_text_parts.append("\nโ
Position now tracked. Use /sl or /tp if needed.")
|
|
|
|
|
|
- await self.notification_manager.send_generic_notification(notification_text)
|
|
|
|
|
|
+ await self.notification_manager.send_generic_notification("\n".join(notification_text_parts))
|
|
logger.info(f"๐ค STARTUP: Sent auto-sync notification for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
|
|
logger.info(f"๐ค STARTUP: Sent auto-sync notification for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|