|
@@ -64,18 +64,18 @@ class TradingStats:
|
|
|
# DATABASE MANAGEMENT DELEGATION
|
|
|
# =============================================================================
|
|
|
|
|
|
- def set_initial_balance(self, balance: float):
|
|
|
+ async def set_initial_balance(self, balance: float):
|
|
|
"""Set initial balance."""
|
|
|
- return self.db_manager.set_initial_balance(balance)
|
|
|
+ return await self.db_manager.set_initial_balance(balance)
|
|
|
|
|
|
def get_initial_balance(self) -> float:
|
|
|
"""Get initial balance."""
|
|
|
return self.db_manager.get_initial_balance()
|
|
|
|
|
|
- def record_balance_snapshot(self, balance: float, unrealized_pnl: float = 0.0,
|
|
|
+ async def record_balance_snapshot(self, balance: float, unrealized_pnl: float = 0.0,
|
|
|
timestamp: Optional[str] = None, notes: Optional[str] = None):
|
|
|
"""Record balance snapshot."""
|
|
|
- return self.db_manager.record_balance_snapshot(balance, unrealized_pnl, timestamp, notes)
|
|
|
+ return await self.db_manager.record_balance_snapshot(balance, unrealized_pnl, timestamp, notes)
|
|
|
|
|
|
def purge_old_balance_history(self, days_to_keep: int = 30) -> int:
|
|
|
"""Purge old balance history."""
|
|
@@ -194,17 +194,17 @@ class TradingStats:
|
|
|
stop_loss_price, take_profit_price, trade_type
|
|
|
)
|
|
|
|
|
|
- def update_trade_position_opened(self, lifecycle_id: str, entry_price: float,
|
|
|
+ async def update_trade_position_opened(self, lifecycle_id: str, entry_price: float,
|
|
|
entry_amount: float, exchange_fill_id: str) -> bool:
|
|
|
"""Update trade position opened."""
|
|
|
- return self.trade_manager.update_trade_position_opened(
|
|
|
+ return await self.trade_manager.update_trade_position_opened(
|
|
|
lifecycle_id, entry_price, entry_amount, exchange_fill_id
|
|
|
)
|
|
|
|
|
|
- def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
|
|
|
+ async def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
|
|
|
realized_pnl: float, exchange_fill_id: str) -> bool:
|
|
|
"""Update trade position closed."""
|
|
|
- return self.trade_manager.update_trade_position_closed(
|
|
|
+ return await self.trade_manager.update_trade_position_closed(
|
|
|
lifecycle_id, exit_price, realized_pnl, exchange_fill_id
|
|
|
)
|
|
|
|
|
@@ -212,17 +212,17 @@ class TradingStats:
|
|
|
"""Update trade cancelled."""
|
|
|
return self.trade_manager.update_trade_cancelled(lifecycle_id, reason)
|
|
|
|
|
|
- def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
|
|
|
+ async def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
|
|
|
stop_loss_price: float) -> bool:
|
|
|
"""Link stop loss to trade."""
|
|
|
- return self.trade_manager.link_stop_loss_to_trade(
|
|
|
+ return await self.trade_manager.link_stop_loss_to_trade(
|
|
|
lifecycle_id, stop_loss_order_id, stop_loss_price
|
|
|
)
|
|
|
|
|
|
- def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
|
|
|
+ async def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
|
|
|
take_profit_price: float) -> bool:
|
|
|
"""Link take profit to trade."""
|
|
|
- return self.trade_manager.link_take_profit_to_trade(
|
|
|
+ return await self.trade_manager.link_take_profit_to_trade(
|
|
|
lifecycle_id, take_profit_order_id, take_profit_price
|
|
|
)
|
|
|
|
|
@@ -282,29 +282,29 @@ class TradingStats:
|
|
|
return self.trade_manager.get_all_trades()
|
|
|
|
|
|
def cancel_linked_orders(self, parent_bot_order_ref_id: str, new_status: str = 'cancelled_parent_filled') -> int:
|
|
|
- """Cancel all orders linked to a parent order. Returns count of cancelled orders."""
|
|
|
- return self.order_manager.cancel_linked_orders(parent_bot_order_ref_id, new_status)
|
|
|
+ """Cancel linked SL/TP orders when a parent order is filled or cancelled."""
|
|
|
+ return self.trade_manager.cancel_linked_orders(parent_bot_order_ref_id, new_status)
|
|
|
|
|
|
# =============================================================================
|
|
|
# AGGREGATION MANAGEMENT DELEGATION
|
|
|
# =============================================================================
|
|
|
|
|
|
def migrate_trade_to_aggregated_stats(self, trade_lifecycle_id: str):
|
|
|
- """Migrate trade to aggregated stats."""
|
|
|
+ """Migrate completed trade to aggregated stats."""
|
|
|
return self.aggregation_manager.migrate_trade_to_aggregated_stats(trade_lifecycle_id)
|
|
|
|
|
|
- def record_deposit(self, amount: float, timestamp: Optional[str] = None,
|
|
|
+ async def record_deposit(self, amount: float, timestamp: Optional[str] = None,
|
|
|
deposit_id: Optional[str] = None, description: Optional[str] = None):
|
|
|
- """Record deposit."""
|
|
|
- return self.aggregation_manager.record_deposit(amount, timestamp, deposit_id, description)
|
|
|
+ """Record a deposit."""
|
|
|
+ return await self.aggregation_manager.record_deposit(amount, timestamp, deposit_id, description)
|
|
|
|
|
|
- def record_withdrawal(self, amount: float, timestamp: Optional[str] = None,
|
|
|
+ async def record_withdrawal(self, amount: float, timestamp: Optional[str] = None,
|
|
|
withdrawal_id: Optional[str] = None, description: Optional[str] = None):
|
|
|
- """Record withdrawal."""
|
|
|
- return self.aggregation_manager.record_withdrawal(amount, timestamp, withdrawal_id, description)
|
|
|
+ """Record a withdrawal."""
|
|
|
+ return await self.aggregation_manager.record_withdrawal(amount, timestamp, withdrawal_id, description)
|
|
|
|
|
|
def get_balance_adjustments_summary(self) -> Dict[str, Any]:
|
|
|
- """Get balance adjustments summary."""
|
|
|
+ """Get summary of balance adjustments."""
|
|
|
return self.aggregation_manager.get_balance_adjustments_summary()
|
|
|
|
|
|
def get_daily_stats(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
@@ -702,171 +702,144 @@ class TradingStats:
|
|
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
- def format_stats_message(self, current_balance: Optional[float] = None) -> str:
|
|
|
- """Format stats for Telegram display using data from DB."""
|
|
|
- try:
|
|
|
- basic = self.get_basic_stats(current_balance)
|
|
|
- perf = self.get_performance_stats()
|
|
|
- risk = self.get_risk_metrics()
|
|
|
-
|
|
|
- formatter = get_formatter()
|
|
|
-
|
|
|
- effective_current_balance = current_balance if current_balance is not None else (basic['initial_balance'] + basic['total_pnl'])
|
|
|
- initial_bal = basic['initial_balance']
|
|
|
-
|
|
|
- total_pnl_val = effective_current_balance - initial_bal if initial_bal > 0 and current_balance is not None else basic['total_pnl']
|
|
|
- total_return_pct = (total_pnl_val / initial_bal * 100) if initial_bal > 0 else 0.0
|
|
|
- pnl_emoji = "🟢" if total_pnl_val >= 0 else "🔴"
|
|
|
- open_positions_count = basic['open_positions_count']
|
|
|
-
|
|
|
- stats_text_parts = []
|
|
|
- stats_text_parts.append(f"📊 <b>Trading Statistics</b>\n")
|
|
|
-
|
|
|
- # Account Overview
|
|
|
- stats_text_parts.append(f"\n💰 <b>Account Overview:</b>")
|
|
|
- stats_text_parts.append(f"• Current Balance: {formatter.format_price_with_symbol(effective_current_balance)}")
|
|
|
- stats_text_parts.append(f"• Initial Balance: {formatter.format_price_with_symbol(initial_bal)}")
|
|
|
- stats_text_parts.append(f"• Open Positions: {open_positions_count}")
|
|
|
- stats_text_parts.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)")
|
|
|
- stats_text_parts.append(f"• Days Active: {basic['days_active']}\n")
|
|
|
-
|
|
|
- # Performance Metrics
|
|
|
- stats_text_parts.append(f"\n🏆 <b>Performance Metrics:</b>")
|
|
|
- stats_text_parts.append(f"• Total Completed Trades: {basic['completed_trades']}")
|
|
|
- stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_wins']}/{basic['completed_trades']})")
|
|
|
- stats_text_parts.append(f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
|
|
|
- stats_text_parts.append(f"• Profit Factor: {perf['profit_factor']:.2f}")
|
|
|
- stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf['expectancy'])}")
|
|
|
-
|
|
|
- largest_win_pct_str = f" ({perf.get('largest_winning_percentage', 0):+.2f}%)" if perf.get('largest_winning_percentage', 0) != 0 else ""
|
|
|
- largest_loss_pct_str = f" ({perf.get('largest_losing_percentage', 0):+.2f}%)" if perf.get('largest_losing_percentage', 0) != 0 else ""
|
|
|
-
|
|
|
- # Show token names for largest trades
|
|
|
- largest_win_token = perf.get('largest_winning_token', 'N/A')
|
|
|
- largest_loss_token = perf.get('largest_losing_token', 'N/A')
|
|
|
-
|
|
|
- stats_text_parts.append(f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str} ({largest_win_token})")
|
|
|
- stats_text_parts.append(f"• Largest Losing Trade: {formatter.format_price_with_symbol(-perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
|
|
|
-
|
|
|
- # Add ROE-based largest trades if different from dollar-based
|
|
|
- largest_win_roe_token = perf.get('largest_winning_roe_token', 'N/A')
|
|
|
- largest_loss_roe_token = perf.get('largest_losing_roe_token', 'N/A')
|
|
|
- largest_win_roe = perf.get('largest_winning_roe', 0)
|
|
|
- largest_loss_roe = perf.get('largest_losing_roe', 0)
|
|
|
-
|
|
|
- if largest_win_roe_token != largest_win_token and largest_win_roe > 0:
|
|
|
- largest_win_roe_pnl = perf.get('largest_winning_roe_pnl', 0)
|
|
|
- stats_text_parts.append(f"• Best ROE Trade: {formatter.format_price_with_symbol(largest_win_roe_pnl)} (+{largest_win_roe:.2f}%) ({largest_win_roe_token})")
|
|
|
-
|
|
|
- if largest_loss_roe_token != largest_loss_token and largest_loss_roe > 0:
|
|
|
- largest_loss_roe_pnl = perf.get('largest_losing_roe_pnl', 0)
|
|
|
- stats_text_parts.append(f"• Worst ROE Trade: {formatter.format_price_with_symbol(-largest_loss_roe_pnl)} (-{largest_loss_roe:.2f}%) ({largest_loss_roe_token})")
|
|
|
-
|
|
|
- best_token_stats = perf.get('best_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
|
|
|
- worst_token_stats = perf.get('worst_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
|
|
|
-
|
|
|
- stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
- stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({worst_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
-
|
|
|
- stats_text_parts.append(f"• Average Trade Duration: {perf.get('avg_trade_duration', 'N/A')}")
|
|
|
- stats_text_parts.append(f"• Portfolio Max Drawdown: {risk.get('max_drawdown_live_percentage', 0.0):.2f}% <i>(Live)</i>")
|
|
|
-
|
|
|
- # Session Info
|
|
|
- stats_text_parts.append(f"\n\n⏰ <b>Session Info:</b>")
|
|
|
- stats_text_parts.append(f"• Bot Started: {basic['start_date']}")
|
|
|
- stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
|
-
|
|
|
- return "\n".join(stats_text_parts).strip()
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error formatting stats message: {e}", exc_info=True)
|
|
|
- return f"""📊 <b>Trading Statistics</b>\n\n❌ <b>Error loading statistics</b>\n\n🔧 <b>Debug info:</b> {str(e)[:100]}"""
|
|
|
+ async def format_stats_message(self, current_balance: Optional[float] = None) -> str:
|
|
|
+ """Formats a comprehensive statistics message."""
|
|
|
+ formatter = get_formatter()
|
|
|
+ basic_stats = self.get_basic_stats(current_balance)
|
|
|
+ initial_bal = basic_stats.get('initial_balance', 0.0)
|
|
|
+ total_pnl_val = basic_stats.get('total_pnl', 0.0)
|
|
|
+ total_return_pct = basic_stats.get('total_return_pct', 0.0)
|
|
|
+ pnl_emoji = "✅" if total_pnl_val >= 0 else "🔻"
|
|
|
+
|
|
|
+ stats_text_parts = [
|
|
|
+ f"📊 <b>Trading Performance Summary</b>",
|
|
|
+ f"• Current Balance: {await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val))} ({await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val) - initial_bal) if initial_bal > 0 else 'N/A'})",
|
|
|
+ f"• Initial Balance: {await formatter.format_price_with_symbol(initial_bal)}",
|
|
|
+ f"• Balance Change: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)",
|
|
|
+ f"• {pnl_emoji} Total P&L: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)"
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Performance Metrics
|
|
|
+ perf = basic_stats.get('performance_metrics', {})
|
|
|
+ if perf:
|
|
|
+ stats_text_parts.append("\n<b>Key Metrics:</b>")
|
|
|
+ stats_text_parts.append(f"• Trading Volume (Entry Vol.): {await formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
|
|
|
+ if perf.get('expectancy') is not None:
|
|
|
+ stats_text_parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(perf['expectancy'])}")
|
|
|
+ stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.2f}% ({perf.get('num_wins', 0)} wins)")
|
|
|
+ stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
|
|
|
+
|
|
|
+ # Largest Trades
|
|
|
+ if perf.get('largest_win') is not None:
|
|
|
+ largest_win_pct_str = f" ({perf.get('largest_win_entry_pct', 0):.2f}%)" if perf.get('largest_win_entry_pct') is not None else ""
|
|
|
+ largest_win_token = perf.get('largest_win_token', 'N/A')
|
|
|
+ stats_text_parts.append(f"• Largest Winning Trade: {await formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str} ({largest_win_token})")
|
|
|
+ if perf.get('largest_loss') is not None:
|
|
|
+ largest_loss_pct_str = f" ({perf.get('largest_loss_entry_pct', 0):.2f}%)" if perf.get('largest_loss_entry_pct') is not None else ""
|
|
|
+ largest_loss_token = perf.get('largest_loss_token', 'N/A')
|
|
|
+ stats_text_parts.append(f"• Largest Losing Trade: {await formatter.format_price_with_symbol(-perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
|
|
|
+
|
|
|
+ # ROE-based metrics if available
|
|
|
+ largest_win_roe = perf.get('largest_win_roe')
|
|
|
+ largest_loss_roe = perf.get('largest_loss_roe')
|
|
|
+ if largest_win_roe is not None:
|
|
|
+ largest_win_roe_pnl = perf.get('largest_win_roe_pnl', 0.0)
|
|
|
+ largest_win_roe_token = perf.get('largest_win_roe_token', 'N/A')
|
|
|
+ stats_text_parts.append(f"• Best ROE Trade: {await formatter.format_price_with_symbol(largest_win_roe_pnl)} (+{largest_win_roe:.2f}%) ({largest_win_roe_token})")
|
|
|
+ if largest_loss_roe is not None:
|
|
|
+ largest_loss_roe_pnl = perf.get('largest_loss_roe_pnl', 0.0)
|
|
|
+ largest_loss_roe_token = perf.get('largest_loss_roe_token', 'N/A')
|
|
|
+ stats_text_parts.append(f"• Worst ROE Trade: {await formatter.format_price_with_symbol(-largest_loss_roe_pnl)} (-{largest_loss_roe:.2f}%) ({largest_loss_roe_token})")
|
|
|
+
|
|
|
+ # Best/Worst Tokens
|
|
|
+ best_token_stats = basic_stats.get('best_token')
|
|
|
+ worst_token_stats = basic_stats.get('worst_token')
|
|
|
+ if best_token_stats:
|
|
|
+ stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {await formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
+ if worst_token_stats:
|
|
|
+ stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {await formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({worst_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
+
|
|
|
+ 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())
|
|
|
+
|
|
|
+ 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>"
|
|
|
+ )
|
|
|
|
|
|
- def format_token_stats_message(self, token: str) -> str:
|
|
|
- """Format detailed statistics for a specific token."""
|
|
|
- try:
|
|
|
- from src.utils.token_display_formatter import get_formatter
|
|
|
- formatter = get_formatter()
|
|
|
-
|
|
|
- token_stats_data = self.get_token_detailed_stats(token)
|
|
|
- token_name = token_stats_data.get('token', token.upper())
|
|
|
-
|
|
|
- if not token_stats_data or token_stats_data.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>"
|
|
|
- )
|
|
|
-
|
|
|
- perf_summary = token_stats_data.get('performance_summary', {})
|
|
|
- open_positions = token_stats_data.get('open_positions', [])
|
|
|
-
|
|
|
- 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
|
|
|
-
|
|
|
- parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
|
|
|
- parts.append(f"• {pnl_emoji} Realized P&L: {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: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
|
|
|
- parts.append(f"• Avg Win: {formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
|
|
|
-
|
|
|
- # Format largest trades with percentages
|
|
|
- largest_win_pct_str = f" ({perf_summary.get('largest_win_percentage', 0):+.2f}%)" if perf_summary.get('largest_win_percentage', 0) != 0 else ""
|
|
|
- largest_loss_pct_str = f" ({perf_summary.get('largest_loss_percentage', 0):+.2f}%)" if perf_summary.get('largest_loss_percentage', 0) != 0 else ""
|
|
|
-
|
|
|
- parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
|
|
|
- parts.append(f"• Entry Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
|
|
|
- parts.append(f"• Exit Volume: {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)}")
|
|
|
- else:
|
|
|
- parts.append("• No completed trades for this token yet.")
|
|
|
- parts.append("")
|
|
|
-
|
|
|
- # Open Positions
|
|
|
- parts.append("📉 <b>Current Open Positions:</b>")
|
|
|
- if open_positions:
|
|
|
- total_open_unrealized_pnl = token_stats_data.get('summary_total_unrealized_pnl', 0.0)
|
|
|
- open_pnl_emoji = "🟢" if total_open_unrealized_pnl >= 0 else "🔴"
|
|
|
+ perf_summary = token_stats.get('performance_summary', {})
|
|
|
+ open_positions = token_stats.get('open_positions', [])
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ 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))}")
|
|
|
+
|
|
|
+ # 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 ""
|
|
|
+
|
|
|
+ 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)}")
|
|
|
+ else:
|
|
|
+ parts.append("• No completed trades for this token yet.")
|
|
|
+ parts.append("")
|
|
|
+
|
|
|
+ # Open Positions
|
|
|
+ parts.append("📉 <b>Current Open Positions:</b>")
|
|
|
+ if 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:
|
|
|
+ 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"
|
|
|
+ if pos.get('opened_at'):
|
|
|
+ try:
|
|
|
+ from datetime import datetime
|
|
|
+ opened_at_dt = datetime.fromisoformat(pos['opened_at'])
|
|
|
+ opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
|
|
|
+ except:
|
|
|
+ pass
|
|
|
|
|
|
- for pos in open_positions:
|
|
|
- pos_side_emoji = "🟢" if pos.get('side') == 'long' else "🔴"
|
|
|
- pos_pnl_emoji = "🟢" if pos.get('unrealized_pnl', 0) >= 0 else "🔴"
|
|
|
- opened_at_str = "N/A"
|
|
|
- if pos.get('opened_at'):
|
|
|
- try:
|
|
|
- from datetime import datetime
|
|
|
- opened_at_dt = datetime.fromisoformat(pos['opened_at'])
|
|
|
- opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
|
|
|
- except:
|
|
|
- pass
|
|
|
-
|
|
|
- parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
|
|
|
- parts.append(f" Entry: {formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
|
|
|
- parts.append(f" {pos_pnl_emoji} Unrealized P&L: {formatter.format_price_with_symbol(pos.get('unrealized_pnl',0))}")
|
|
|
- parts.append(f" Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
|
|
|
- parts.append(f" {open_pnl_emoji} <b>Total Open P&L: {formatter.format_price_with_symbol(total_open_unrealized_pnl)}</b>")
|
|
|
- else:
|
|
|
- parts.append("• No open positions for this token.")
|
|
|
- parts.append("")
|
|
|
-
|
|
|
- parts.append(f"📋 Open Orders (Exchange): {token_stats_data.get('current_open_orders_count', 0)}")
|
|
|
- parts.append(f"💡 Use <code>/performance {token_name}</code> for another view including recent trades.")
|
|
|
-
|
|
|
- return "\n".join(parts)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"❌ Error formatting token stats message for {token}: {e}", exc_info=True)
|
|
|
- return f"❌ Error generating statistics for {token}: {str(e)[:100]}"
|
|
|
+ parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {await formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
|
|
|
+ parts.append(f" Entry: {await formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {await formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
|
|
|
+ parts.append(f" {pos_pnl_emoji} Unrealized P&L: {await formatter.format_price_with_symbol(pos.get('unrealized_pnl',0))}")
|
|
|
+ parts.append(f" Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
|
|
|
+ parts.append(f" {open_pnl_emoji} <b>Total Open P&L: {await formatter.format_price_with_symbol(total_open_unrealized_pnl)}</b>")
|
|
|
+ else:
|
|
|
+ parts.append("• No open positions for this token.")
|
|
|
+ parts.append("")
|
|
|
+
|
|
|
+ parts.append(f"📋 Open Orders (Exchange): {token_stats.get('current_open_orders_count', 0)}")
|
|
|
+ parts.append(f"💡 Use <code>/performance {token_name}</code> for another view including recent trades.")
|
|
|
+
|
|
|
+ return "\n".join(parts)
|
|
|
|
|
|
# =============================================================================
|
|
|
# CONVENIENCE METHODS & HIGH-LEVEL OPERATIONS
|
|
@@ -930,28 +903,25 @@ class TradingStats:
|
|
|
logger.error(f"❌ Error generating summary report: {e}")
|
|
|
return {'error': str(e)}
|
|
|
|
|
|
- def record_trade(self, symbol: str, side: str, amount: float, price: float,
|
|
|
+ async def record_trade(self, symbol: str, side: str, amount: float, price: float,
|
|
|
exchange_fill_id: Optional[str] = None, trade_type: str = "manual",
|
|
|
pnl: Optional[float] = None, timestamp: Optional[str] = None,
|
|
|
linked_order_table_id_to_link: Optional[int] = None):
|
|
|
- """Record a trade directly in the database (used for unmatched external fills)."""
|
|
|
+ """DEPRECATED - use trade lifecycle methods instead."""
|
|
|
if timestamp is None:
|
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
value = amount * price
|
|
|
+ formatter = get_formatter()
|
|
|
+ ts = timestamp or datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
- try:
|
|
|
- self.db_manager._execute_query(
|
|
|
- "INSERT OR IGNORE INTO trades (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl, linked_order_table_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
|
- (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl or 0.0, linked_order_table_id_to_link)
|
|
|
- )
|
|
|
-
|
|
|
- formatter = get_formatter()
|
|
|
- base_asset_for_amount = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
- logger.info(f"📈 Trade recorded: {side.upper()} {formatter.format_amount(amount, base_asset_for_amount)} {symbol} @ {formatter.format_price(price, symbol)} ({formatter.format_price(value, symbol)}) [{trade_type}]")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Failed to record trade: {e}")
|
|
|
+ base_asset_for_amount = symbol.split('/')[0]
|
|
|
+ logger.info(f"📈 Trade recorded: {side.upper()} {await formatter.format_amount(amount, base_asset_for_amount)} {symbol} @ {await formatter.format_price(price, symbol)} ({await formatter.format_price(value, symbol)}) [{trade_type}]")
|
|
|
+
|
|
|
+ self.db_manager._execute_query(
|
|
|
+ "INSERT OR IGNORE INTO trades (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl, linked_order_table_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
|
+ (symbol, side, amount, price, value, trade_type, ts, exchange_fill_id, pnl or 0.0, linked_order_table_id_to_link)
|
|
|
+ )
|
|
|
|
|
|
def health_check(self) -> Dict[str, Any]:
|
|
|
"""Perform health check on all components."""
|