|
@@ -393,34 +393,13 @@ class TradingStats:
|
|
|
def get_basic_stats(self, current_balance: Optional[float] = None) -> Dict[str, Any]:
|
|
|
"""Get basic trading statistics from DB, primarily using aggregated tables."""
|
|
|
|
|
|
- # Get counts of open positions (trades that are not yet migrated)
|
|
|
+ # Get counts of open positions
|
|
|
open_positions_count = self._get_open_positions_count_from_db()
|
|
|
|
|
|
- # Get overall aggregated stats from token_stats table
|
|
|
- query_token_stats_summary = """
|
|
|
- SELECT
|
|
|
- SUM(total_realized_pnl) as total_pnl_from_cycles,
|
|
|
- SUM(total_completed_cycles) as total_completed_cycles_sum,
|
|
|
- MIN(first_cycle_closed_at) as overall_first_cycle_closed,
|
|
|
- MAX(last_cycle_closed_at) as overall_last_cycle_closed,
|
|
|
- SUM(total_entry_volume) as total_entry_volume_sum,
|
|
|
- MAX(best_roe_percentage) as overall_best_roe,
|
|
|
- MIN(worst_roe_percentage) as overall_worst_roe
|
|
|
- FROM token_stats
|
|
|
- """
|
|
|
- token_stats_summary = self.db_manager._fetchone_query(query_token_stats_summary)
|
|
|
-
|
|
|
- # Find the specific trades for best/worst ROE
|
|
|
- best_roe_trade = self.db_manager._fetchone_query("SELECT token, best_roe_pnl, best_roe_percentage FROM token_stats ORDER BY best_roe_percentage DESC LIMIT 1")
|
|
|
- worst_roe_trade = self.db_manager._fetchone_query("SELECT token, worst_roe_pnl, worst_roe_percentage FROM token_stats ORDER BY worst_roe_percentage ASC LIMIT 1")
|
|
|
-
|
|
|
- total_pnl_from_cycles = token_stats_summary['total_pnl_from_cycles'] if token_stats_summary and token_stats_summary['total_pnl_from_cycles'] is not None else 0.0
|
|
|
- total_completed_cycles_sum = token_stats_summary['total_completed_cycles_sum'] if token_stats_summary and token_stats_summary['total_completed_cycles_sum'] is not None else 0
|
|
|
- total_entry_volume_sum = token_stats_summary['total_entry_volume_sum'] if token_stats_summary and token_stats_summary['total_entry_volume_sum'] is not None else 0.0
|
|
|
-
|
|
|
- # Total trades considered as sum of completed cycles and currently open positions
|
|
|
- total_trades_redefined = total_completed_cycles_sum + open_positions_count
|
|
|
+ # Get all performance metrics from the calculator
|
|
|
+ perf_metrics = self.get_performance_stats()
|
|
|
|
|
|
+ # Get additional metadata
|
|
|
initial_balance_str = self._get_metadata('initial_balance')
|
|
|
initial_balance = float(initial_balance_str) if initial_balance_str else 0.0
|
|
|
|
|
@@ -430,56 +409,32 @@ class TradingStats:
|
|
|
|
|
|
# Get last activity timestamp
|
|
|
last_activity_ts = None
|
|
|
- last_activity_query = """
|
|
|
- SELECT MAX(updated_at) as last_update
|
|
|
- FROM trades
|
|
|
- WHERE status IN ('position_opened', 'position_closed')
|
|
|
- """
|
|
|
- last_activity_row = self.db_manager._fetchone_query(last_activity_query)
|
|
|
+ last_activity_row = self.db_manager._fetchone_query(
|
|
|
+ "SELECT MAX(last_cycle_closed_at) as last_update FROM token_stats"
|
|
|
+ )
|
|
|
if last_activity_row and last_activity_row['last_update']:
|
|
|
- last_activity_ts = last_activity_row['last_update']
|
|
|
- # Ensure timezone-aware
|
|
|
- if isinstance(last_activity_ts, str):
|
|
|
- last_activity_ts = datetime.fromisoformat(last_activity_ts)
|
|
|
- if last_activity_ts.tzinfo is None:
|
|
|
- last_activity_ts = last_activity_ts.replace(tzinfo=timezone.utc)
|
|
|
-
|
|
|
- # Get last open trade timestamp
|
|
|
- last_open_trade_query = """
|
|
|
- SELECT MAX(updated_at) as last_update
|
|
|
- FROM trades
|
|
|
- WHERE status = 'position_opened'
|
|
|
- """
|
|
|
- last_open_trade_ts_row = self.db_manager._fetchone_query(last_open_trade_query)
|
|
|
- if last_open_trade_ts_row and last_open_trade_ts_row['last_update']:
|
|
|
- last_open_trade_ts = last_open_trade_ts_row['last_update']
|
|
|
- # Ensure timezone-aware
|
|
|
- if isinstance(last_open_trade_ts, str):
|
|
|
- last_open_trade_ts = datetime.fromisoformat(last_open_trade_ts)
|
|
|
- if last_open_trade_ts.tzinfo is None:
|
|
|
- last_open_trade_ts = last_open_trade_ts.replace(tzinfo=timezone.utc)
|
|
|
-
|
|
|
- # Now both datetimes are timezone-aware, we can compare them
|
|
|
- if not last_activity_ts or last_open_trade_ts > last_activity_ts:
|
|
|
- last_activity_ts = last_open_trade_ts
|
|
|
-
|
|
|
- # Get drawdown info
|
|
|
- _, max_drawdown_pct, drawdown_start_date = self.performance_calculator.get_live_max_drawdown()
|
|
|
+ last_activity_ts = datetime.fromisoformat(last_activity_row['last_update'])
|
|
|
+
|
|
|
+ # Best/Worst trades by ROE
|
|
|
+ best_roe_trade = self.db_manager._fetchone_query("SELECT token, best_roe_pnl, best_roe_percentage FROM token_stats ORDER BY best_roe_percentage DESC LIMIT 1")
|
|
|
+ worst_roe_trade = self.db_manager._fetchone_query("SELECT token, worst_roe_pnl, worst_roe_percentage FROM token_stats ORDER BY worst_roe_percentage ASC LIMIT 1")
|
|
|
+
|
|
|
+ total_pnl = perf_metrics.get('total_pnl', 0.0)
|
|
|
+ total_volume = perf_metrics.get('total_entry_volume', 0.0)
|
|
|
+ total_return_pct = (total_pnl / initial_balance * 100) if initial_balance > 0 else 0.0
|
|
|
|
|
|
return {
|
|
|
- 'total_trades': total_trades_redefined,
|
|
|
- 'completed_trades': total_completed_cycles_sum,
|
|
|
'initial_balance': initial_balance,
|
|
|
- 'total_pnl': total_pnl_from_cycles,
|
|
|
- 'total_volume': total_entry_volume_sum,
|
|
|
+ 'total_pnl': total_pnl,
|
|
|
+ 'total_volume': total_volume,
|
|
|
+ 'total_return_pct': total_return_pct,
|
|
|
'days_active': days_active,
|
|
|
'start_date': start_date_obj.strftime('%Y-%m-%d'),
|
|
|
'last_trade': last_activity_ts,
|
|
|
'open_positions_count': open_positions_count,
|
|
|
+ 'performance_metrics': perf_metrics,
|
|
|
'best_roe_trade': best_roe_trade,
|
|
|
- 'worst_roe_trade': worst_roe_trade,
|
|
|
- 'max_drawdown_percentage': max_drawdown_pct,
|
|
|
- 'drawdown_start_date': drawdown_start_date
|
|
|
+ 'worst_roe_trade': worst_roe_trade
|
|
|
}
|
|
|
|
|
|
def _get_open_positions_count_from_db(self) -> int:
|
|
@@ -656,68 +611,61 @@ class TradingStats:
|
|
|
|
|
|
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"• Current Balance: {await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val))}",
|
|
|
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}%)"
|
|
|
]
|
|
|
|
|
|
- # Drawdown
|
|
|
- max_drawdown_pct = basic_stats.get('max_drawdown_percentage', 0.0)
|
|
|
- drawdown_start_date_iso = basic_stats.get('drawdown_start_date')
|
|
|
- drawdown_date_str = ""
|
|
|
- if drawdown_start_date_iso:
|
|
|
- try:
|
|
|
- drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
|
|
|
- drawdown_date_str = f" (since {drawdown_date})"
|
|
|
- except (ValueError, TypeError):
|
|
|
- pass
|
|
|
- stats_text_parts.append(f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}")
|
|
|
-
|
|
|
# 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))}")
|
|
|
+
|
|
|
+ # Drawdown
|
|
|
+ max_drawdown_pct = perf.get('max_drawdown_pct', 0.0)
|
|
|
+ drawdown_start_date_iso = perf.get('drawdown_start_date')
|
|
|
+ drawdown_date_str = ""
|
|
|
+ if drawdown_start_date_iso:
|
|
|
+ try:
|
|
|
+ drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
|
|
|
+ drawdown_date_str = f" (since {drawdown_date})"
|
|
|
+ except (ValueError, TypeError):
|
|
|
+ pass
|
|
|
+ stats_text_parts.append(f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}")
|
|
|
+
|
|
|
+ stats_text_parts.append(f"• Trading Volume: {await formatter.format_price_with_symbol(perf.get('total_entry_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"• Win Rate: {perf.get('win_rate', 0.0):.2f}% ({perf.get('total_wins', 0)} wins)")
|
|
|
stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
|
|
|
|
|
|
# Largest Trades by P&L
|
|
|
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_roe = perf.get('largest_win_pct', 0.0)
|
|
|
largest_win_token = perf.get('largest_win_token', 'N/A')
|
|
|
- stats_text_parts.append(f"• Largest Win (P&L): {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 ""
|
|
|
+ stats_text_parts.append(f"• Largest Win (P&L): {await formatter.format_price_with_symbol(perf['largest_win'])} ({largest_win_roe:+.2f}% ROE) ({largest_win_token})")
|
|
|
+
|
|
|
+ if perf.get('largest_loss', 0.0) != 0:
|
|
|
+ largest_loss_roe = perf.get('largest_loss_pct', 0.0)
|
|
|
largest_loss_token = perf.get('largest_loss_token', 'N/A')
|
|
|
- stats_text_parts.append(f"• Largest Loss (P&L): {await formatter.format_price_with_symbol(perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
|
|
|
+ stats_text_parts.append(f"• Largest Loss (P&L): {await formatter.format_price_with_symbol(perf['largest_loss'])} ({largest_loss_roe:+.2f}% ROE) ({largest_loss_token})")
|
|
|
|
|
|
# Best/Worst by ROE from basic_stats
|
|
|
best_roe_trade = basic_stats.get('best_roe_trade')
|
|
|
worst_roe_trade = basic_stats.get('worst_roe_trade')
|
|
|
|
|
|
- if best_roe_trade:
|
|
|
+ if best_roe_trade and best_roe_trade.get('token') != perf.get('largest_win_token'):
|
|
|
best_roe_pnl = best_roe_trade.get('best_roe_pnl', 0.0)
|
|
|
best_roe_pct = best_roe_trade.get('best_roe_percentage', 0.0)
|
|
|
best_roe_token = best_roe_trade.get('token', 'N/A')
|
|
|
stats_text_parts.append(f"• Best Trade (ROE): {await formatter.format_price_with_symbol(best_roe_pnl)} ({best_roe_pct:+.2f}%) ({best_roe_token})")
|
|
|
|
|
|
- if worst_roe_trade:
|
|
|
+ if worst_roe_trade and worst_roe_trade.get('token') != perf.get('largest_loss_token'):
|
|
|
worst_roe_pnl = worst_roe_trade.get('worst_roe_pnl', 0.0)
|
|
|
worst_roe_pct = worst_roe_trade.get('worst_roe_percentage', 0.0)
|
|
|
worst_roe_token = worst_roe_trade.get('token', 'N/A')
|
|
|
stats_text_parts.append(f"• Worst Trade (ROE): {await formatter.format_price_with_symbol(worst_roe_pnl)} ({worst_roe_pct:+.2f}%) ({worst_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:
|