diff --git a/python/valuecell/agents/auto_trading_agent/agent.py b/python/valuecell/agents/auto_trading_agent/agent.py index 79663dba1..aa46892e4 100644 --- a/python/valuecell/agents/auto_trading_agent/agent.py +++ b/python/valuecell/agents/auto_trading_agent/agent.py @@ -267,7 +267,7 @@ async def _process_trading_instance( ) trade_message = FilteredCardPushNotificationComponentData( title=f"{config.agent_model} Trade", - data=f"šŸ’° **Trade Executed:**\n{trade_message_text}\n", + data=f"šŸ’° **Trade Executed**\n\n{trade_message_text}\n", filters=[config.agent_model], table_title="Trade Detail", create_time=datetime.now(timezone.utc).strftime( @@ -548,39 +548,33 @@ def _get_instance_status_component_data( output = [] # Header - output.append(f"šŸ“Š **Trading Portfolio Status** - {instance_id}") - output.append("\n**Instance Configuration**") + output.append("šŸ“Š **Instance Configuration**\n") output.append(f"- Model: `{config.agent_model}`") output.append(f"- Symbols: {', '.join(config.crypto_symbols)}") output.append( - f"- Status: {'🟢 Active' if instance['active'] else 'šŸ”“ Stopped'}" + f"- Status: {'🟢 Active' if instance['active'] else 'šŸ”“ Stopped'}\n" ) # Portfolio Summary Section - output.append("\nšŸ’° **Portfolio Summary**") - output.append("\n**Overall Performance**") - output.append(f"- Initial Capital: `${config.initial_capital:,.2f}`") - output.append(f"- Current Value: `${portfolio_value:,.2f}`") + output.append("šŸ’° **Portfolio Summary**\n") + output.append("**Overall Performance**\n") + output.append(f"- Current Value: `${portfolio_value:,.2f}`\n") pnl_emoji = "🟢" if total_pnl >= 0 else "šŸ”“" pnl_sign = "+" if total_pnl >= 0 else "" output.append( - f"- Total P&L: {pnl_emoji} **{pnl_sign}${total_pnl:,.2f}** ({pnl_sign}{pnl_pct:.2f}%)" + f"- Total P&L: {pnl_emoji} **{pnl_sign}${total_pnl:,.2f}** ({pnl_sign}{pnl_pct:.2f}%)\n" ) - - output.append("\n**Cash Position**") - output.append(f"- Available Cash: `${available_cash:,.2f}`") + output.append(f"- Available Cash: `${available_cash:,.2f}`\n") # Current Positions Section - output.append(f"\nšŸ“ˆ **Current Positions ({len(executor.positions)})**") + output.append(f"šŸ“ˆ **Current Positions ({len(executor.positions)})**") if executor.positions: output.append( - "\n| Symbol | Type | Quantity | Avg Price | Current Price | Position Value | Unrealized P&L |" - ) - output.append( - "|--------|------|----------|-----------|---------------|----------------|----------------|" + "\n| Symbol | Type | **Position**/Quantity | **Current**/Avg | **P&L** |" ) + output.append("|--------|------|---------|--------|--------|") for symbol, pos in executor.positions.items(): try: @@ -603,15 +597,14 @@ def _get_instance_status_component_data( ) position_value = pos.notional + unrealized_pnl - # Format row - pnl_emoji = "🟢" if unrealized_pnl >= 0 else "šŸ”“" + # Format row with merged columns pnl_sign = "+" if unrealized_pnl >= 0 else "" output.append( f"| **{symbol}** | {pos.trade_type.value.upper()} | " - f"{abs(pos.quantity):.4f} | ${pos.entry_price:,.2f} | " - f"${current_price:,.2f} | ${position_value:,.2f} | " - f"{pnl_emoji} {pnl_sign}${unrealized_pnl:,.2f} |" + f"**${position_value:,.2f}**
{abs(pos.quantity):.4f} | " + f"**${current_price:,.2f}**
${pos.entry_price:,.2f} | " + f"**{pnl_sign}${unrealized_pnl:,.2f}** |" ) except Exception as e: @@ -619,8 +612,9 @@ def _get_instance_status_component_data( # Fallback display with entry price only output.append( f"| **{symbol}** | {pos.trade_type.value.upper()} | " - f"{abs(pos.quantity):.4f} | ${pos.entry_price:,.2f} | " - f"N/A | ${pos.notional:,.2f} | N/A |" + f"**${pos.notional:,.2f}**
{abs(pos.quantity):.4f} | " + f"**${pos.entry_price:,.2f}**
${pos.entry_price:,.2f} | " + f"**N/A** |" ) else: output.append("\n*No open positions*") diff --git a/python/valuecell/agents/auto_trading_agent/formatters.py b/python/valuecell/agents/auto_trading_agent/formatters.py index 9e2e9a8eb..12cbed843 100644 --- a/python/valuecell/agents/auto_trading_agent/formatters.py +++ b/python/valuecell/agents/auto_trading_agent/formatters.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone from typing import Any, Dict, Optional +from ...utils.i18n_utils import convert_timezone, get_current_timezone from .models import Position, TechnicalIndicators, TradeAction, TradeType logger = logging.getLogger(__name__) @@ -13,6 +14,63 @@ class MessageFormatter: """Formats various messages and notifications""" + @staticmethod + def _convert_and_format_timestamp( + dt: datetime, format_str: str = "%m/%d, %I:%M %p", include_tz: bool = False + ) -> str: + """ + Convert timestamp to user's timezone and format it. + + Args: + dt: DateTime to convert and format + format_str: Format string for strftime + include_tz: Whether to include timezone abbreviation + + Returns: + Formatted timestamp string + """ + try: + # Get user's configured timezone + user_tz = get_current_timezone() + + # Convert from UTC to user's timezone + # Assume input is UTC if no timezone info + if dt.tzinfo is None: + dt = datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + tzinfo=timezone.utc, + ) + + converted_dt = convert_timezone(dt, "UTC", user_tz) + + # Format the datetime + formatted = converted_dt.strftime(format_str) + + # Optionally append timezone info + if include_tz: + formatted += f" ({user_tz})" + + return formatted + except Exception as e: + logger.warning(f"Failed to convert timezone: {e}, using UTC") + # Fallback to UTC format + if dt.tzinfo is None: + dt = datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + tzinfo=timezone.utc, + ) + return dt.strftime(format_str) + @staticmethod def format_trade_notification( trade_details: Dict[str, Any], agent_name: str = "AutoTrading" @@ -33,13 +91,16 @@ def format_trade_notification( trade_type = trade_details["trade_type"] timestamp = trade_details["timestamp"] + # Convert timestamp to user's timezone + formatted_time = MessageFormatter._convert_and_format_timestamp(timestamp) + if action == "opened": message = ( - f"{agent_name} opened a {trade_type} position on {symbol}!\n" - f"{timestamp.strftime('%m/%d, %I:%M %p')}\n" - f"Price: ${trade_details['entry_price']:,.2f}\n" - f"Quantity: {trade_details['quantity']:.4f}\n" - f"Notional: ${trade_details['notional']:,.2f}" + f"**{agent_name}** opened a **{trade_type}** position on **{symbol}**!\n\n" + f"šŸ“… {formatted_time}\n\n" + f"**Price:** `${trade_details['entry_price']:,.2f}`\n\n" + f"**Quantity:** `{trade_details['quantity']:.4f}`\n\n" + f"**Notional:** `${trade_details['notional']:,.2f}`" ) else: # closed hours = int(trade_details["holding_time"].total_seconds() // 3600) @@ -48,15 +109,16 @@ def format_trade_notification( ) pnl = trade_details["pnl"] pnl_sign = "+" if pnl >= 0 else "" + pnl_emoji = "🟢" if pnl >= 0 else "šŸ”“" message = ( - f"{agent_name} completed a {trade_type} trade on {symbol}!\n" - f"{timestamp.strftime('%m/%d, %I:%M %p')}\n" - f"Price: ${trade_details['entry_price']:,.2f} → ${trade_details['exit_price']:,.2f}\n" - f"Quantity: {trade_details['quantity']:.4f}\n" - f"Notional: ${trade_details['entry_notional']:,.2f} → ${trade_details['exit_notional']:,.2f}\n" - f"Holding time: {hours}H {minutes}M\n" - f"Net P&L: {pnl_sign}${pnl:,.2f}" + f"**{agent_name}** completed a **{trade_type}** trade on **{symbol}**!\n\n" + f"šŸ“… {formatted_time}\n\n" + f"**Price:** `${trade_details['entry_price']:,.2f}` → `${trade_details['exit_price']:,.2f}`\n\n" + f"**Quantity:** `{trade_details['quantity']:.4f}`\n\n" + f"**Notional:** `${trade_details['entry_notional']:,.2f}` → `${trade_details['exit_notional']:,.2f}`\n\n" + f"**Holding time:** `{hours}H {minutes}M`\n\n" + f"**Net P&L:** {pnl_emoji} **{pnl_sign}${pnl:,.2f}**" ) return message @@ -106,9 +168,14 @@ def format_portfolio_notification( "data": {"Portfolio": portfolio_value}, } + # Convert timestamp to user's timezone for display + formatted_time = MessageFormatter._convert_and_format_timestamp( + timestamp, format_str="%m/%d, %I:%M %p", include_tz=True + ) + display_message = ( f"šŸ’° Portfolio Update\n" - f"Time: {timestamp.strftime('%m/%d, %I:%M %p UTC')}\n" + f"Time: {formatted_time}\n" f"Total Value: ${portfolio_value:,.2f}\n" f"Open Positions: {positions_count}\n" f"Available Capital: ${current_capital:,.2f}" @@ -146,6 +213,11 @@ def format_market_analysis_notification( try: timestamp = datetime.now(timezone.utc) + # Convert timestamp to user's timezone for display + formatted_time = MessageFormatter._convert_and_format_timestamp( + timestamp, format_str="%m/%d, %I:%M %p", include_tz=True + ) + # Format action with emoji action_emoji = { TradeAction.BUY: "🟢", @@ -155,7 +227,7 @@ def format_market_analysis_notification( message = ( f"šŸ“Š **Market Analysis - {symbol}**\n" - f"Time: {timestamp.strftime('%m/%d, %I:%M %p UTC')}\n\n" + f"Time: {formatted_time}\n\n" f"**Current Price:** ${indicators.close_price:,.2f}\n" f"**Decision:** {action_emoji.get(action, '')} {action.value.upper()}" )