322 lines
10 KiB
Python
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
|