Files
evotraders/backend/utils/msg_adapter.py
2026-03-30 17:46:44 +08:00

322 lines
10 KiB
Python

# -*- coding: utf-8 -*-
"""
Message Adapter - Converts AgentScope Msg to frontend JSON format
Ensures compatibility with existing frontend without modifications
"""
import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from agentscope.message import Msg
logger = logging.getLogger(__name__)
class FrontendAdapter:
"""
Adapter to convert AgentScope messages to frontend-compatible format
Frontend expects specific message types:
- agent: Agent thinking/analysis messages
- team_summary: Portfolio summary with equity curves
- team_holdings: Current portfolio holdings
- team_stats: Portfolio statistics
- team_trades: Trade history
- team_leaderboard: Agent performance rankings
- price_update: Real-time price updates
- system: System notifications
"""
@staticmethod
def parse(msg: Msg) -> Optional[Dict[str, Any]]:
"""
Parse AgentScope Msg to frontend format
Args:
msg: AgentScope Msg object
Returns:
Dictionary in frontend format, or None if message should be skipped
"""
if msg is None:
return None
# Determine message type based on metadata or content
msg_type = FrontendAdapter._determine_type(msg)
if msg_type == "agent":
return FrontendAdapter._format_agent_msg(msg)
elif msg_type == "portfolio_update":
return FrontendAdapter._format_portfolio_msg(msg)
elif msg_type == "system":
return FrontendAdapter._format_system_msg(msg)
else:
# Default: treat as agent message
return FrontendAdapter._format_agent_msg(msg)
@staticmethod
def _determine_type(msg: Msg) -> str:
"""Determine frontend message type from Msg"""
# Check metadata for explicit type
if hasattr(msg, "metadata") and msg.metadata:
if "type" in msg.metadata:
return msg.metadata["type"]
# Check if message contains portfolio update
if "portfolio" in msg.metadata:
return "portfolio_update"
# Check message name/role
if msg.name == "system":
return "system"
# Default to agent message
return "agent"
@staticmethod
def _format_agent_msg(msg: object) -> Dict[str, Any]:
"""
Format agent message for frontend
Args:
msg: Either AgentScope Msg or dict from pipeline results
Frontend expects:
{
"type": "agent",
"role_key": "analyst_id",
"content": "message text",
"timestamp": "ISO timestamp"
}
"""
# Handle dict from pipeline results
if isinstance(msg, dict):
name = msg.get("agent", "unknown")
content = msg.get("content", "")
else:
# Handle Msg object
name = msg.name
content = msg.content
return {
"type": "agent",
"role_key": name,
"content": content
if isinstance(content, str)
else json.dumps(content),
"timestamp": datetime.now().isoformat(),
}
@staticmethod
def _format_portfolio_msg(msg: Msg) -> Dict[str, Any]:
"""
Format portfolio update message
This typically generates multiple frontend messages:
- team_summary
- team_holdings
- team_stats
- team_trades (if trades were executed)
"""
metadata = msg.metadata or {}
portfolio = metadata.get("portfolio", {})
messages: List[Dict[str, Any]] = []
# Generate holdings message
holdings = FrontendAdapter.build_holdings(portfolio)
if holdings:
messages.append(
{
"type": "team_holdings",
"data": holdings,
"timestamp": datetime.now().isoformat(),
},
)
# Generate stats message
stats = FrontendAdapter.build_stats(portfolio)
if stats:
messages.append(
{
"type": "team_stats",
"data": stats,
"timestamp": datetime.now().isoformat(),
},
)
# Generate trades message if execution logs exist
execution_logs = metadata.get("execution_logs", [])
if execution_logs:
trades = FrontendAdapter.build_trades(execution_logs)
messages.append(
{
"type": "team_trades",
"mode": "incremental",
"data": trades,
"timestamp": datetime.now().isoformat(),
},
)
# Return composite message
return {
"type": "composite",
"messages": messages,
}
@staticmethod
def _format_system_msg(msg: Msg) -> Dict[str, Any]:
"""Format system message"""
return {
"type": "system",
"content": msg.content
if isinstance(msg.content, str)
else json.dumps(msg.content),
"timestamp": datetime.now().isoformat(),
}
@staticmethod
def build_holdings(
portfolio: Dict[str, Any],
prices: Dict[str, float] = None,
) -> List[Dict[str, Any]]:
"""Build holdings array from portfolio state"""
holdings = []
prices = prices or {}
positions = portfolio.get("positions", {})
cash = portfolio.get("cash", 0.0)
# Calculate total value using current prices
total_value = cash
for ticker, position in positions.items():
long_shares = position.get("long", 0)
short_shares = position.get("short", 0)
price = prices.get(ticker) or position.get("avg_price", 0)
total_value += (long_shares - short_shares) * price
# Build holdings for each position
for ticker, position in positions.items():
long_shares = position.get("long", 0)
short_shares = position.get("short", 0)
avg_price = position.get("avg_price", 0)
current_price = prices.get(ticker) or avg_price
net_shares = long_shares - short_shares
if net_shares == 0:
continue
market_value = net_shares * current_price
weight = market_value / total_value if total_value > 0 else 0
holdings.append(
{
"ticker": ticker,
"quantity": net_shares,
"avg": avg_price,
"currentPrice": current_price,
"marketValue": market_value,
"weight": weight,
},
)
# Add cash as a holding
if cash > 0:
holdings.append(
{
"ticker": "CASH",
"quantity": 1,
"avg": cash,
"currentPrice": cash,
"marketValue": cash,
"weight": cash / total_value if total_value > 0 else 0,
},
)
return holdings
@staticmethod
def build_stats(
portfolio: Dict[str, Any],
prices: Dict[str, float] = None,
) -> Dict[str, Any]:
"""Build stats dictionary from portfolio"""
prices = prices or {}
positions = portfolio.get("positions", {})
cash = portfolio.get("cash", 0.0)
margin_used = portfolio.get("margin_used", 0.0)
# Calculate total value using current prices
total_value = cash
for ticker, position in positions.items():
long_shares = position.get("long", 0)
short_shares = position.get("short", 0)
price = prices.get(ticker) or position.get("avg_price", 0)
total_value += (long_shares - short_shares) * price
# Calculate ticker weights
ticker_weights = {}
for ticker, position in positions.items():
long_shares = position.get("long", 0)
short_shares = position.get("short", 0)
price = prices.get(ticker) or position.get("avg_price", 0)
market_value = (long_shares - short_shares) * price
if market_value != 0:
ticker_weights[ticker] = (
market_value / total_value if total_value > 0 else 0
)
# Calculate total return
initial_cash = portfolio.get("initial_cash", 100000.0)
total_return = (
((total_value - initial_cash) / initial_cash * 100)
if initial_cash > 0
else 0.0
)
return {
"totalAssetValue": round(total_value, 2),
"totalReturn": round(total_return, 2),
"cashPosition": round(cash, 2),
"tickerWeights": ticker_weights,
"marginUsed": round(margin_used, 2),
}
@staticmethod
def build_trades(execution_logs: List[str]) -> List[Dict[str, Any]]:
"""
Build trades array from execution logs
Frontend expects:
[{
"ts": 1234567890,
"ticker": "AAPL",
"side": "LONG",
"qty": 100,
"price": 150.0,
"reason": "Buy signal"
}, ...]
"""
trades = []
timestamp = int(datetime.now().timestamp() * 1000)
for log in execution_logs:
# Parse execution log (simplified - should use structured data)
if "Executed" in log:
# Extract trade details from log string
# in real implementation, pass structured data
trades.append(
{
"ts": timestamp,
"ticker": "UNKNOWN", # Should parse from log
"side": "LONG", # Should parse from log
"qty": 0, # Should parse from log
"price": 0.0, # Should parse from log
"reason": log,
},
)
return trades