|
@@ -17,17 +17,10 @@ from .order_manager import OrderManager
|
|
|
from .trade_lifecycle_manager import TradeLifecycleManager
|
|
|
from .aggregation_manager import AggregationManager
|
|
|
from .performance_calculator import PerformanceCalculator
|
|
|
-from src.utils.token_display_formatter import get_formatter
|
|
|
+from src.utils.token_display_formatter import get_formatter, normalize_token_case
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
-def _normalize_token_case(token: str) -> str:
|
|
|
- """Normalize token case for consistency."""
|
|
|
- if any(c.isupper() for c in token):
|
|
|
- return token # Keep original case for mixed-case tokens
|
|
|
- else:
|
|
|
- return token.upper() # Convert to uppercase for all-lowercase
|
|
|
-
|
|
|
class TradingStats:
|
|
|
"""Refactored trading statistics tracker using modular components."""
|
|
|
|
|
@@ -340,100 +333,14 @@ class TradingStats:
|
|
|
return self.performance_calculator.get_performance_stats()
|
|
|
|
|
|
def get_token_performance(self, token: Optional[str] = None) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
|
- """Get performance data for a specific token or all tokens."""
|
|
|
- try:
|
|
|
- if token:
|
|
|
- # Get performance for specific token
|
|
|
- query = """
|
|
|
- SELECT
|
|
|
- symbol,
|
|
|
- COUNT(*) as total_trades,
|
|
|
- SUM(CASE WHEN realized_pnl > 0 THEN 1 ELSE 0 END) as winning_trades,
|
|
|
- SUM(realized_pnl) as total_pnl,
|
|
|
- AVG(realized_pnl) as avg_trade,
|
|
|
- MAX(realized_pnl) as largest_win,
|
|
|
- MIN(realized_pnl) as largest_loss,
|
|
|
- AVG(CASE WHEN realized_pnl > 0 THEN realized_pnl ELSE NULL END) as avg_win,
|
|
|
- AVG(CASE WHEN realized_pnl < 0 THEN realized_pnl ELSE NULL END) as avg_loss
|
|
|
- FROM trades
|
|
|
- WHERE symbol = ? AND status = 'position_closed'
|
|
|
- GROUP BY symbol
|
|
|
- """
|
|
|
- result = self.db_manager._fetchone_query(query, (token,))
|
|
|
-
|
|
|
- if not result:
|
|
|
- return {}
|
|
|
-
|
|
|
- # Calculate win rate
|
|
|
- total_trades = result['total_trades']
|
|
|
- winning_trades = result['winning_trades']
|
|
|
- win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
-
|
|
|
- # Get recent trades
|
|
|
- recent_trades_query = """
|
|
|
- SELECT
|
|
|
- side,
|
|
|
- entry_price,
|
|
|
- exit_price,
|
|
|
- realized_pnl as pnl,
|
|
|
- position_opened_at,
|
|
|
- position_closed_at
|
|
|
- FROM trades
|
|
|
- WHERE symbol = ? AND status = 'position_closed'
|
|
|
- ORDER BY position_closed_at DESC
|
|
|
- LIMIT 5
|
|
|
- """
|
|
|
- recent_trades = self.db_manager._fetch_query(recent_trades_query, (token,))
|
|
|
-
|
|
|
- return {
|
|
|
- 'token': token,
|
|
|
- 'total_trades': total_trades,
|
|
|
- 'winning_trades': winning_trades,
|
|
|
- 'win_rate': win_rate,
|
|
|
- 'total_pnl': result['total_pnl'],
|
|
|
- 'avg_trade': result['avg_trade'],
|
|
|
- 'largest_win': result['largest_win'],
|
|
|
- 'largest_loss': result['largest_loss'],
|
|
|
- 'avg_win': result['avg_win'],
|
|
|
- 'avg_loss': result['avg_loss'],
|
|
|
- 'recent_trades': recent_trades
|
|
|
- }
|
|
|
- else:
|
|
|
- # Get performance for all tokens
|
|
|
- query = """
|
|
|
- SELECT
|
|
|
- symbol,
|
|
|
- COUNT(*) as total_trades,
|
|
|
- SUM(CASE WHEN realized_pnl > 0 THEN 1 ELSE 0 END) as winning_trades,
|
|
|
- SUM(realized_pnl) as total_pnl,
|
|
|
- AVG(realized_pnl) as avg_trade
|
|
|
- FROM trades
|
|
|
- WHERE status = 'position_closed'
|
|
|
- GROUP BY symbol
|
|
|
- ORDER BY total_pnl DESC
|
|
|
- """
|
|
|
- results = self.db_manager._fetch_query(query)
|
|
|
-
|
|
|
- performance_data = []
|
|
|
- for result in results:
|
|
|
- total_trades = result['total_trades']
|
|
|
- winning_trades = result['winning_trades']
|
|
|
- win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
-
|
|
|
- performance_data.append({
|
|
|
- 'token': result['symbol'],
|
|
|
- 'total_trades': total_trades,
|
|
|
- 'winning_trades': winning_trades,
|
|
|
- 'win_rate': win_rate,
|
|
|
- 'total_pnl': result['total_pnl'],
|
|
|
- 'avg_trade': result['avg_trade']
|
|
|
- })
|
|
|
-
|
|
|
- return performance_data
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error getting token performance: {e}")
|
|
|
- return [] if token is None else {}
|
|
|
+ """
|
|
|
+ Get performance statistics for a specific token or all tokens.
|
|
|
+ """
|
|
|
+ if token:
|
|
|
+ normalized_token = normalize_token_case(token)
|
|
|
+ return self.performance_calculator.get_single_token_performance(normalized_token)
|
|
|
+ else:
|
|
|
+ return self.performance_calculator.get_all_token_performance()
|
|
|
|
|
|
def get_balance_history(self, days: int = 30) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
|
"""Get balance history."""
|
|
@@ -565,132 +472,129 @@ class TradingStats:
|
|
|
return row['count'] if row else 0
|
|
|
|
|
|
def get_token_detailed_stats(self, token: str) -> Dict[str, Any]:
|
|
|
- """Get detailed statistics for a specific token."""
|
|
|
- try:
|
|
|
- # Normalize token case
|
|
|
- upper_token = _normalize_token_case(token)
|
|
|
-
|
|
|
- # Get aggregated stats from token_stats table
|
|
|
- token_agg_stats = self.db_manager._fetchone_query(
|
|
|
- "SELECT * FROM token_stats WHERE token = ?", (upper_token,)
|
|
|
- )
|
|
|
-
|
|
|
- # Get open trades for this token
|
|
|
- open_trades_for_token = self.db_manager._fetch_query(
|
|
|
- "SELECT * FROM trades WHERE status = 'position_opened' AND symbol LIKE ? ORDER BY position_opened_at DESC",
|
|
|
- (f"{upper_token}/%",)
|
|
|
- )
|
|
|
-
|
|
|
- # Initialize performance stats
|
|
|
- perf_stats = {
|
|
|
- 'completed_trades': 0,
|
|
|
- 'total_pnl': 0.0,
|
|
|
- 'pnl_percentage': 0.0,
|
|
|
- 'win_rate': 0.0,
|
|
|
- 'profit_factor': 0.0,
|
|
|
- 'avg_win': 0.0,
|
|
|
- 'avg_loss': 0.0,
|
|
|
- 'largest_win': 0.0,
|
|
|
- 'largest_loss': 0.0,
|
|
|
- 'expectancy': 0.0,
|
|
|
- 'total_wins': 0,
|
|
|
- 'total_losses': 0,
|
|
|
- 'completed_entry_volume': 0.0,
|
|
|
- 'completed_exit_volume': 0.0,
|
|
|
- 'total_cancelled': 0,
|
|
|
- 'total_duration_seconds': 0,
|
|
|
- 'avg_trade_duration': "N/A"
|
|
|
- }
|
|
|
-
|
|
|
- if token_agg_stats:
|
|
|
- total_cycles = token_agg_stats.get('total_completed_cycles', 0)
|
|
|
- winning_cycles = token_agg_stats.get('winning_cycles', 0)
|
|
|
- losing_cycles = token_agg_stats.get('losing_cycles', 0)
|
|
|
- sum_winning_pnl = token_agg_stats.get('sum_of_winning_pnl', 0.0)
|
|
|
- sum_losing_pnl = token_agg_stats.get('sum_of_losing_pnl', 0.0)
|
|
|
-
|
|
|
- # Calculate percentages for largest trades
|
|
|
- largest_win_pnl = token_agg_stats.get('largest_winning_cycle_pnl', 0.0)
|
|
|
- largest_loss_pnl = token_agg_stats.get('largest_losing_cycle_pnl', 0.0)
|
|
|
- largest_win_entry_volume = token_agg_stats.get('largest_winning_cycle_entry_volume', 0.0)
|
|
|
- largest_loss_entry_volume = token_agg_stats.get('largest_losing_cycle_entry_volume', 0.0)
|
|
|
-
|
|
|
- largest_win_percentage = (largest_win_pnl / largest_win_entry_volume * 100) if largest_win_entry_volume > 0 else 0.0
|
|
|
- largest_loss_percentage = (largest_loss_pnl / largest_loss_entry_volume * 100) if largest_loss_entry_volume > 0 else 0.0
|
|
|
-
|
|
|
- perf_stats.update({
|
|
|
- 'completed_trades': total_cycles,
|
|
|
- 'total_pnl': token_agg_stats.get('total_realized_pnl', 0.0),
|
|
|
- 'win_rate': (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0.0,
|
|
|
- 'profit_factor': (sum_winning_pnl / sum_losing_pnl) if sum_losing_pnl > 0 else float('inf') if sum_winning_pnl > 0 else 0.0,
|
|
|
- 'avg_win': (sum_winning_pnl / winning_cycles) if winning_cycles > 0 else 0.0,
|
|
|
- 'avg_loss': (sum_losing_pnl / losing_cycles) if losing_cycles > 0 else 0.0,
|
|
|
- 'largest_win': largest_win_pnl,
|
|
|
- 'largest_loss': largest_loss_pnl,
|
|
|
- 'largest_win_percentage': largest_win_percentage,
|
|
|
- 'largest_loss_percentage': largest_loss_percentage,
|
|
|
- 'total_wins': winning_cycles,
|
|
|
- 'total_losses': losing_cycles,
|
|
|
- 'completed_entry_volume': token_agg_stats.get('total_entry_volume', 0.0),
|
|
|
- 'completed_exit_volume': token_agg_stats.get('total_exit_volume', 0.0),
|
|
|
- 'total_cancelled': token_agg_stats.get('total_cancelled_cycles', 0),
|
|
|
- 'total_duration_seconds': token_agg_stats.get('total_duration_seconds', 0)
|
|
|
- })
|
|
|
-
|
|
|
- # Calculate expectancy
|
|
|
- win_rate_decimal = perf_stats['win_rate'] / 100
|
|
|
- perf_stats['expectancy'] = (perf_stats['avg_win'] * win_rate_decimal) - (perf_stats['avg_loss'] * (1 - win_rate_decimal))
|
|
|
-
|
|
|
- # Format average trade duration
|
|
|
- if total_cycles > 0:
|
|
|
- avg_duration_seconds = token_agg_stats.get('total_duration_seconds', 0) / total_cycles
|
|
|
- perf_stats['avg_trade_duration'] = self._format_duration(avg_duration_seconds)
|
|
|
+ """
|
|
|
+ Retrieves detailed trading statistics for a single token, including calculations
|
|
|
+ not directly stored in the database.
|
|
|
+ """
|
|
|
+ normalized_token = normalize_token_case(token)
|
|
|
+
|
|
|
+ # Get raw stats from DB
|
|
|
+ token_stats = self.db_manager._fetchone_query(
|
|
|
+ "SELECT * FROM token_stats WHERE token = ?", (normalized_token,)
|
|
|
+ )
|
|
|
+
|
|
|
+ # Get open trades for this token
|
|
|
+ open_trades_for_token = self.db_manager._fetch_query(
|
|
|
+ "SELECT * FROM trades WHERE status = 'position_opened' AND symbol LIKE ? ORDER BY position_opened_at DESC",
|
|
|
+ (f"{normalized_token}/%",)
|
|
|
+ )
|
|
|
+
|
|
|
+ # Initialize performance stats
|
|
|
+ perf_stats = {
|
|
|
+ 'completed_trades': 0,
|
|
|
+ 'total_pnl': 0.0,
|
|
|
+ 'pnl_percentage': 0.0,
|
|
|
+ 'win_rate': 0.0,
|
|
|
+ 'profit_factor': 0.0,
|
|
|
+ 'avg_win': 0.0,
|
|
|
+ 'avg_loss': 0.0,
|
|
|
+ 'largest_win': 0.0,
|
|
|
+ 'largest_loss': 0.0,
|
|
|
+ 'expectancy': 0.0,
|
|
|
+ 'total_wins': 0,
|
|
|
+ 'total_losses': 0,
|
|
|
+ 'completed_entry_volume': 0.0,
|
|
|
+ 'completed_exit_volume': 0.0,
|
|
|
+ 'total_cancelled': 0,
|
|
|
+ 'total_duration_seconds': 0,
|
|
|
+ 'avg_trade_duration': "N/A"
|
|
|
+ }
|
|
|
+
|
|
|
+ if token_stats:
|
|
|
+ total_cycles = token_stats.get('total_completed_cycles', 0)
|
|
|
+ winning_cycles = token_stats.get('winning_cycles', 0)
|
|
|
+ losing_cycles = token_stats.get('losing_cycles', 0)
|
|
|
+ sum_winning_pnl = token_stats.get('sum_of_winning_pnl', 0.0)
|
|
|
+ sum_losing_pnl = token_stats.get('sum_of_losing_pnl', 0.0)
|
|
|
|
|
|
- # Calculate open positions summary
|
|
|
- open_positions_summary = []
|
|
|
- total_open_value = 0.0
|
|
|
- total_open_unrealized_pnl = 0.0
|
|
|
+ # Calculate percentages for largest trades
|
|
|
+ largest_win_pnl = token_stats.get('largest_winning_cycle_pnl', 0.0)
|
|
|
+ largest_loss_pnl = token_stats.get('largest_losing_cycle_pnl', 0.0)
|
|
|
+ largest_win_entry_volume = token_stats.get('largest_winning_cycle_entry_volume', 0.0)
|
|
|
+ largest_loss_entry_volume = token_stats.get('largest_losing_cycle_entry_volume', 0.0)
|
|
|
|
|
|
- for op_trade in open_trades_for_token:
|
|
|
- open_positions_summary.append({
|
|
|
- 'lifecycle_id': op_trade.get('trade_lifecycle_id'),
|
|
|
- 'side': op_trade.get('position_side'),
|
|
|
- 'amount': op_trade.get('current_position_size'),
|
|
|
- 'entry_price': op_trade.get('entry_price'),
|
|
|
- 'mark_price': op_trade.get('mark_price'),
|
|
|
- 'unrealized_pnl': op_trade.get('unrealized_pnl'),
|
|
|
- 'opened_at': op_trade.get('position_opened_at')
|
|
|
- })
|
|
|
- total_open_value += op_trade.get('value', 0.0)
|
|
|
- total_open_unrealized_pnl += op_trade.get('unrealized_pnl', 0.0)
|
|
|
+ largest_win_percentage = (largest_win_pnl / largest_win_entry_volume * 100) if largest_win_entry_volume > 0 else 0.0
|
|
|
+ largest_loss_percentage = (largest_loss_pnl / largest_loss_entry_volume * 100) if largest_loss_entry_volume > 0 else 0.0
|
|
|
|
|
|
- # Get open orders count for this token
|
|
|
- open_orders_count_row = self.db_manager._fetchone_query(
|
|
|
- "SELECT COUNT(*) as count FROM orders WHERE symbol LIKE ? AND status IN ('open', 'submitted', 'pending_trigger')",
|
|
|
- (f"{upper_token}/%",)
|
|
|
- )
|
|
|
- current_open_orders_for_token = open_orders_count_row['count'] if open_orders_count_row else 0
|
|
|
+ perf_stats.update({
|
|
|
+ 'completed_trades': total_cycles,
|
|
|
+ 'total_pnl': token_stats.get('total_realized_pnl', 0.0),
|
|
|
+ 'win_rate': (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0.0,
|
|
|
+ 'profit_factor': (sum_winning_pnl / sum_losing_pnl) if sum_losing_pnl > 0 else float('inf') if sum_winning_pnl > 0 else 0.0,
|
|
|
+ 'avg_win': (sum_winning_pnl / winning_cycles) if winning_cycles > 0 else 0.0,
|
|
|
+ 'avg_loss': (sum_losing_pnl / losing_cycles) if losing_cycles > 0 else 0.0,
|
|
|
+ 'largest_win': largest_win_pnl,
|
|
|
+ 'largest_loss': largest_loss_pnl,
|
|
|
+ 'largest_win_percentage': largest_win_percentage,
|
|
|
+ 'largest_loss_percentage': largest_loss_percentage,
|
|
|
+ 'total_wins': winning_cycles,
|
|
|
+ 'total_losses': losing_cycles,
|
|
|
+ 'completed_entry_volume': token_stats.get('total_entry_volume', 0.0),
|
|
|
+ 'completed_exit_volume': token_stats.get('total_exit_volume', 0.0),
|
|
|
+ 'total_cancelled': token_stats.get('total_cancelled_cycles', 0),
|
|
|
+ 'total_duration_seconds': token_stats.get('total_duration_seconds', 0)
|
|
|
+ })
|
|
|
|
|
|
- effective_total_trades = perf_stats['completed_trades'] + len(open_trades_for_token)
|
|
|
+ # Calculate expectancy
|
|
|
+ win_rate_decimal = perf_stats['win_rate'] / 100
|
|
|
+ perf_stats['expectancy'] = (perf_stats['avg_win'] * win_rate_decimal) - (perf_stats['avg_loss'] * (1 - win_rate_decimal))
|
|
|
|
|
|
- return {
|
|
|
- 'token': upper_token,
|
|
|
- 'message': f"Statistics for {upper_token}",
|
|
|
- 'performance_summary': perf_stats, # Expected key by formatting method
|
|
|
- 'performance': perf_stats, # Legacy compatibility
|
|
|
- 'open_positions': open_positions_summary, # Direct list as expected
|
|
|
- 'summary_total_trades': effective_total_trades, # Expected by formatting method
|
|
|
- 'summary_total_unrealized_pnl': total_open_unrealized_pnl, # Expected by formatting method
|
|
|
- 'current_open_orders_count': current_open_orders_for_token, # Expected by formatting method
|
|
|
- 'summary': {
|
|
|
- 'total_trades': effective_total_trades,
|
|
|
- 'open_orders': current_open_orders_for_token,
|
|
|
- }
|
|
|
+ # Format average trade duration
|
|
|
+ if total_cycles > 0:
|
|
|
+ avg_duration_seconds = token_stats.get('total_duration_seconds', 0) / total_cycles
|
|
|
+ perf_stats['avg_trade_duration'] = self._format_duration(avg_duration_seconds)
|
|
|
+
|
|
|
+ # Calculate open positions summary
|
|
|
+ open_positions_summary = []
|
|
|
+ total_open_value = 0.0
|
|
|
+ total_open_unrealized_pnl = 0.0
|
|
|
+
|
|
|
+ for op_trade in open_trades_for_token:
|
|
|
+ open_positions_summary.append({
|
|
|
+ 'lifecycle_id': op_trade.get('trade_lifecycle_id'),
|
|
|
+ 'side': op_trade.get('position_side'),
|
|
|
+ 'amount': op_trade.get('current_position_size'),
|
|
|
+ 'entry_price': op_trade.get('entry_price'),
|
|
|
+ 'mark_price': op_trade.get('mark_price'),
|
|
|
+ 'unrealized_pnl': op_trade.get('unrealized_pnl'),
|
|
|
+ 'opened_at': op_trade.get('position_opened_at')
|
|
|
+ })
|
|
|
+ total_open_value += op_trade.get('value', 0.0)
|
|
|
+ total_open_unrealized_pnl += op_trade.get('unrealized_pnl', 0.0)
|
|
|
+
|
|
|
+ # Get open orders count for this token
|
|
|
+ open_orders_count_row = self.db_manager._fetchone_query(
|
|
|
+ "SELECT COUNT(*) as count FROM orders WHERE symbol LIKE ? AND status IN ('open', 'submitted', 'pending_trigger')",
|
|
|
+ (f"{normalized_token}/%",)
|
|
|
+ )
|
|
|
+ current_open_orders_for_token = open_orders_count_row['count'] if open_orders_count_row else 0
|
|
|
+
|
|
|
+ effective_total_trades = perf_stats['completed_trades'] + len(open_trades_for_token)
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'token': normalized_token,
|
|
|
+ 'message': f"Statistics for {normalized_token}",
|
|
|
+ 'performance_summary': perf_stats, # Expected key by formatting method
|
|
|
+ 'performance': perf_stats, # Legacy compatibility
|
|
|
+ 'open_positions': open_positions_summary, # Direct list as expected
|
|
|
+ 'summary_total_trades': effective_total_trades, # Expected by formatting method
|
|
|
+ 'summary_total_unrealized_pnl': total_open_unrealized_pnl, # Expected by formatting method
|
|
|
+ 'current_open_orders_count': current_open_orders_for_token, # Expected by formatting method
|
|
|
+ 'summary': {
|
|
|
+ 'total_trades': effective_total_trades,
|
|
|
+ 'open_orders': current_open_orders_for_token,
|
|
|
}
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error getting detailed stats for {token}: {e}")
|
|
|
- return {}
|
|
|
+ }
|
|
|
|
|
|
def _format_duration(self, seconds: float) -> str:
|
|
|
"""Format duration in seconds to a human-readable string."""
|
|
@@ -774,60 +678,52 @@ class TradingStats:
|
|
|
return "\n".join(stats_text_parts)
|
|
|
|
|
|
async def format_token_stats_message(self, token: str) -> str:
|
|
|
- """Formats a statistics message for a specific token."""
|
|
|
- formatter = get_formatter()
|
|
|
- token_stats = self.get_token_detailed_stats(token)
|
|
|
- normalized_token = _normalize_token_case(token)
|
|
|
- token_name = token_stats.get('token', normalized_token.upper())
|
|
|
+ """Formats the statistics message for a single token."""
|
|
|
+ normalized_token = normalize_token_case(token)
|
|
|
+ token_stats = self.get_token_detailed_stats(normalized_token)
|
|
|
|
|
|
- if not token_stats or token_stats.get('summary_total_trades', 0) == 0:
|
|
|
- return (
|
|
|
- f"📊 <b>{token_name} Statistics</b>\n\n"
|
|
|
- f"📭 No trading data found for {token_name}.\n\n"
|
|
|
- f"💡 To trade this token, try commands like:\n"
|
|
|
- f" <code>/long {token_name} 100</code>\n"
|
|
|
- f" <code>/short {token_name} 100</code>"
|
|
|
- )
|
|
|
+ if not token_stats or not token_stats.get('total_trades', 0):
|
|
|
+ return f"📊 No trading data found for {normalized_token}."
|
|
|
|
|
|
- perf_summary = token_stats.get('performance_summary', {})
|
|
|
- open_positions = token_stats.get('open_positions', [])
|
|
|
+ formatter = get_formatter()
|
|
|
+ token_name = token_stats.get('token', normalized_token.upper())
|
|
|
|
|
|
parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
|
|
|
|
|
|
# Completed Trades Summary
|
|
|
parts.append("📈 <b>Completed Trades Summary:</b>")
|
|
|
- if perf_summary.get('completed_trades', 0) > 0:
|
|
|
- pnl_emoji = "✅" if perf_summary.get('total_pnl', 0) >= 0 else "🔻"
|
|
|
- entry_vol = perf_summary.get('completed_entry_volume', 0.0)
|
|
|
- pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
|
|
|
+ if token_stats.get('completed_trades', 0) > 0:
|
|
|
+ pnl_emoji = "✅" if token_stats.get('total_pnl', 0) >= 0 else "🔻"
|
|
|
+ entry_vol = token_stats.get('completed_entry_volume', 0.0)
|
|
|
+ pnl_pct = (token_stats.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
|
|
|
|
|
|
- parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
|
|
|
- parts.append(f"• {pnl_emoji} Realized P&L: {await formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({pnl_pct:+.2f}%)")
|
|
|
- parts.append(f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}W / {perf_summary.get('total_losses', 0)}L)")
|
|
|
- parts.append(f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}")
|
|
|
- parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
|
|
|
- parts.append(f"• Avg Win: {await formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {await formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
|
|
|
+ parts.append(f"• Total Completed: {token_stats.get('completed_trades', 0)}")
|
|
|
+ parts.append(f"• {pnl_emoji} Realized P&L: {await formatter.format_price_with_symbol(token_stats.get('total_pnl', 0.0))} ({pnl_pct:+.2f}%)")
|
|
|
+ parts.append(f"• Win Rate: {token_stats.get('win_rate', 0.0):.1f}% ({token_stats.get('total_wins', 0)}W / {token_stats.get('total_losses', 0)}L)")
|
|
|
+ parts.append(f"• Profit Factor: {token_stats.get('profit_factor', 0.0):.2f}")
|
|
|
+ parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(token_stats.get('expectancy', 0.0))}")
|
|
|
+ parts.append(f"• Avg Win: {await formatter.format_price_with_symbol(token_stats.get('avg_win', 0.0))} | Avg Loss: {await formatter.format_price_with_symbol(token_stats.get('avg_loss', 0.0))}")
|
|
|
|
|
|
# Format largest trades with percentages
|
|
|
- largest_win_pct_str = f" ({perf_summary.get('largest_win_entry_pct', 0):.2f}%)" if perf_summary.get('largest_win_entry_pct') is not None else ""
|
|
|
- largest_loss_pct_str = f" ({perf_summary.get('largest_loss_entry_pct', 0):.2f}%)" if perf_summary.get('largest_loss_entry_pct') is not None else ""
|
|
|
+ largest_win_pct_str = f" ({token_stats.get('largest_win_entry_pct', 0):.2f}%)" if token_stats.get('largest_win_entry_pct') is not None else ""
|
|
|
+ largest_loss_pct_str = f" ({token_stats.get('largest_loss_entry_pct', 0):.2f}%)" if token_stats.get('largest_loss_entry_pct') is not None else ""
|
|
|
|
|
|
- parts.append(f"• Largest Win: {await formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {await formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
|
|
|
- parts.append(f"• Entry Volume: {await formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
|
|
|
- parts.append(f"• Exit Volume: {await formatter.format_price_with_symbol(perf_summary.get('completed_exit_volume', 0.0))}")
|
|
|
- parts.append(f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}")
|
|
|
- parts.append(f"• Cancelled Cycles: {perf_summary.get('total_cancelled', 0)}")
|
|
|
+ parts.append(f"• Largest Win: {await formatter.format_price_with_symbol(token_stats.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {await formatter.format_price_with_symbol(token_stats.get('largest_loss', 0.0))}{largest_loss_pct_str}")
|
|
|
+ parts.append(f"• Entry Volume: {await formatter.format_price_with_symbol(token_stats.get('completed_entry_volume', 0.0))}")
|
|
|
+ parts.append(f"• Exit Volume: {await formatter.format_price_with_symbol(token_stats.get('completed_exit_volume', 0.0))}")
|
|
|
+ parts.append(f"• Average Trade Duration: {token_stats.get('avg_trade_duration', 'N/A')}")
|
|
|
+ parts.append(f"• Cancelled Cycles: {token_stats.get('total_cancelled', 0)}")
|
|
|
else:
|
|
|
parts.append("• No completed trades for this token yet.")
|
|
|
parts.append("")
|
|
|
|
|
|
# Open Positions
|
|
|
parts.append("📉 <b>Current Open Positions:</b>")
|
|
|
- if open_positions:
|
|
|
+ if token_stats.get('open_positions', []):
|
|
|
total_open_unrealized_pnl = token_stats.get('summary_total_unrealized_pnl', 0.0)
|
|
|
open_pnl_emoji = "✅" if total_open_unrealized_pnl >= 0 else "🔻"
|
|
|
|
|
|
- for pos in open_positions:
|
|
|
+ for pos in token_stats.get('open_positions', []):
|
|
|
pos_side_emoji = "🔼" if pos.get('side', 'buy').lower() == 'buy' else "🔽"
|
|
|
pos_pnl_emoji = "✅" if pos.get('unrealized_pnl', 0) >= 0 else "🔻"
|
|
|
opened_at_str = "N/A"
|