feat(agent): complete EvoAgent integration for all 6 agent roles

Migrate all agent roles from Legacy to EvoAgent architecture:
- fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst
- risk_manager, portfolio_manager

Key changes:
- EvoAgent now supports Portfolio Manager compatibility methods (_make_decision,
  get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio)
- Add UnifiedAgentFactory for centralized agent creation
- ToolGuard with batch approval API and WebSocket broadcast
- Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent)
- Remove backend/agents/compat.py migration shim
- Add run_id alongside workspace_id for semantic clarity
- Complete integration test coverage (13 tests)
- All smoke tests passing for 6 agent roles

Constraint: Must maintain backward compatibility with existing run configs
Constraint: Memory support must work with EvoAgent (no fallback to Legacy)
Rejected: Separate PM implementation for EvoAgent | unified approach cleaner
Confidence: high
Scope-risk: broad
Directive: EVO_AGENT_IDS env var still respected but defaults to all roles
Not-tested: Kubernetes sandbox mode for skill execution
This commit is contained in:
2026-04-02 00:55:08 +08:00
parent 0fa413380c
commit 16b54d5ccc
73 changed files with 9454 additions and 904 deletions

View File

@@ -5,12 +5,36 @@
This module provides easy-to-use commands for running backtest, live trading,
and frontend development server.
ARCHITECTURE NOTE:
==================
This CLI supports TWO distinct runtime modes:
1. STANDALONE MODE (default):
- Uses `evotraders backtest` or `evotraders live` commands
- Starts a self-contained monolithic Gateway process with all agents
- Suitable for: quick testing, single-machine deployment, development
- WebSocket server runs on port 8765 (default)
- No external service dependencies
2. MICROSERVICE MODE (production):
- Uses `./start-dev.sh` or manual service orchestration
- Runs 4 separate FastAPI services (agent, runtime, trading, news)
- Gateway runs as a subprocess of runtime_service
- Suitable for: production scaling, distributed deployment
- Services communicate via REST APIs
When microservices are already running, standalone mode will warn you about
port conflicts and potential confusion. Use `--force` to override.
For more details, see: docs/current-architecture.md
"""
# flake8: noqa: E501
# pylint: disable=R0912, R0915
import logging
import os
import shutil
import socket
import subprocess
import sys
from datetime import datetime, timedelta
@@ -42,6 +66,17 @@ from backend.data.market_store import MarketStore
from backend.enrich.llm_enricher import get_explain_model_info, llm_enrichment_enabled
from backend.enrich.news_enricher import enrich_symbols
# Microservice port definitions (for conflict detection)
MICROSERVICE_PORTS = {
"agent_service": 8000,
"trading_service": 8001,
"news_service": 8002,
"runtime_service": 8003,
}
# Gateway default port
GATEWAY_PORT = 8765
app = typer.Typer(
name="evotraders",
help="大时代:自进化多智能体交易系统",
@@ -72,6 +107,101 @@ def get_project_root() -> Path:
return Path(__file__).parent.parent
def _is_port_in_use(port: int, host: str = "127.0.0.1") -> bool:
"""Check if a port is already in use."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1.0)
result = sock.connect_ex((host, port))
return result == 0
except Exception:
return False
def _detect_running_microservices() -> dict[str, int]:
"""Detect which microservices are already running."""
running = {}
for service_name, port in MICROSERVICE_PORTS.items():
if _is_port_in_use(port):
running[service_name] = port
return running
def _check_gateway_port_conflict(port: int) -> bool:
"""Check if the Gateway port is already in use."""
return _is_port_in_use(port)
def _display_mode_warning(
running_services: dict[str, int],
gateway_port: int,
force: bool = False,
) -> bool:
"""
Display warning when microservices are detected.
Returns:
True if should proceed, False if should abort
"""
if not running_services and not _check_gateway_port_conflict(gateway_port):
return True
console.print()
console.print(
Panel.fit(
"[bold yellow]⚠️ MICROSERVICE MODE DETECTED[/bold yellow]\n\n"
"You are attempting to start in STANDALONE mode, but microservices "
"appear to already be running. This can cause confusion and port conflicts.",
border_style="yellow",
)
)
if running_services:
console.print("\n[bold]Detected running services:[/bold]")
for service, port in running_services.items():
console.print(f"{service}: [cyan]http://localhost:{port}[/cyan]")
if _check_gateway_port_conflict(gateway_port):
console.print(
f"\n[bold red]Port {gateway_port} is already in use![/bold red] "
"Another Gateway instance may be running."
)
console.print("\n[bold]Options:[/bold]")
console.print(" 1. Stop microservices first: [cyan]pkill -f 'uvicorn|backend.main'[/cyan]")
console.print(" 2. Use microservice mode instead: [cyan]./start-dev.sh[/cyan]")
console.print(" 3. Use a different port: [cyan]--port <other_port>[/cyan]")
if force:
console.print(
"\n[yellow]⚠️ --force flag used. Proceeding despite conflicts...[/yellow]"
)
return True
console.print()
should_proceed = Confirm.ask(
"Do you want to proceed anyway?",
default=False,
)
return should_proceed
def _display_standalone_banner(mode: str, config_name: str) -> None:
"""Display standalone mode startup banner."""
console.print(
Panel.fit(
f"[bold cyan]大时代 {mode.upper()} Mode[/bold cyan]\n"
"[dim]Standalone Mode (Monolithic Gateway)[/dim]",
border_style="cyan",
)
)
console.print("\n[dim]Architecture:[/dim]")
console.print(" Mode: [yellow]Standalone (Single Process)[/yellow]")
console.print(f" Config: [cyan]{config_name}[/cyan]")
console.print("\n[dim]Note: This is NOT microservice mode. For distributed deployment,")
console.print(" use ./start-dev.sh instead.[/dim]\n")
def handle_history_cleanup(config_name: str, auto_clean: bool = False) -> None:
"""
Handle cleanup of historical data for a given config.
@@ -215,8 +345,8 @@ def run_data_updater(project_root: Path) -> None:
)
def initialize_workspace(config_name: str) -> Path:
"""Create run-scoped workspace files for a config."""
def initialize_run_assets(config_name: str) -> Path:
"""Create run-scoped agent assets and bootstrap files for a config."""
workspace_manager = WorkspaceManager(project_root=get_project_root())
workspace_manager.initialize_default_assets(
config_name=config_name,
@@ -438,14 +568,18 @@ def init_workspace(
"default",
"--config-name",
"-c",
help="Configuration name for the workspace",
help="Run label under runs/<config_name> for the initialized asset tree.",
),
):
"""Initialize run-scoped BOOTSTRAP and agent prompt asset files."""
run_dir = initialize_workspace(config_name)
"""Initialize run-scoped BOOTSTRAP and agent asset files.
The command name is retained for compatibility even though the target is
the run-scoped asset tree under `runs/<config_name>/`.
"""
run_dir = initialize_run_assets(config_name)
console.print(
Panel.fit(
f"[bold green]Workspace initialized[/bold green]\n[cyan]{run_dir}[/cyan]",
f"[bold green]Run assets initialized[/bold green]\n[cyan]{run_dir}[/cyan]",
border_style="green",
),
)
@@ -861,6 +995,13 @@ def team_show(
)
# =============================================================================
# STANDALONE MODE COMMANDS (backtest/live)
# =============================================================================
# These commands start a self-contained monolithic Gateway process.
# For microservice mode, use ./start-dev.sh instead.
# =============================================================================
@app.command()
def backtest(
start: Optional[str] = typer.Option(
@@ -876,10 +1017,10 @@ def backtest(
help="End date for backtest (YYYY-MM-DD)",
),
config_name: str = typer.Option(
"backtest",
"default_backtest_run",
"--config-name",
"-c",
help="Configuration name for this backtest run",
help="Run label under runs/<config_name> for this backtest runtime.",
),
host: str = typer.Option(
"0.0.0.0",
@@ -887,7 +1028,7 @@ def backtest(
help="WebSocket server host",
),
port: int = typer.Option(
8765,
GATEWAY_PORT,
"--port",
"-p",
help="WebSocket server port",
@@ -907,22 +1048,24 @@ def backtest(
"--enable-memory",
help="Enable ReMeTaskLongTermMemory for agents (requires MEMORY_API_KEY)",
),
force: bool = typer.Option(
False,
"--force",
help="Force start even if microservices are detected (may cause conflicts)",
),
):
"""
Run backtest mode with historical data.
Run backtest mode in STANDALONE mode (monolithic Gateway).
Example:
This starts a self-contained process with all agents. For microservice
mode (distributed services), use ./start-dev.sh instead.
Examples:
evotraders backtest --start 2025-11-01 --end 2025-12-01
evotraders backtest --config-name my_strategy --port 9000
evotraders backtest --clean # Clear historical data before starting
evotraders backtest --enable-memory # Enable long-term memory
"""
console.print(
Panel.fit(
"[bold cyan]大时代 Backtest Mode[/bold cyan]",
border_style="cyan",
),
)
poll_interval = int(_normalize_typer_value(poll_interval, 10))
# Validate dates - required for backtest
@@ -948,13 +1091,18 @@ def backtest(
)
raise typer.Exit(1) from exc
# Handle historical data cleanup
handle_history_cleanup(config_name, auto_clean=clean)
# Check for microservice conflicts
running_services = _detect_running_microservices()
if running_services or _check_gateway_port_conflict(port):
if not _display_mode_warning(running_services, port, force=force):
console.print("\n[yellow]Startup aborted.[/yellow]")
raise typer.Exit(0)
# Display standalone mode banner
_display_standalone_banner("backtest", config_name)
# Display configuration
console.print("\n[bold]Configuration:[/bold]")
console.print(" Mode: Backtest")
console.print(f" Config: {config_name}")
console.print(f" Period: {start} -> {end}")
console.print(f" Server: {host}:{port}")
console.print(f" Poll Interval: {poll_interval}s")
@@ -964,6 +1112,9 @@ def backtest(
console.print("\nAccess frontend at: [cyan]http://localhost:5173[/cyan]")
console.print("Press Ctrl+C to stop\n")
# Handle historical data cleanup
handle_history_cleanup(config_name, auto_clean=clean)
# Change to project root
project_root = get_project_root()
os.chdir(project_root)
@@ -1020,10 +1171,10 @@ def backtest(
@app.command()
def live(
config_name: str = typer.Option(
"live",
"default_live_run",
"--config-name",
"-c",
help="Configuration name for this live run",
help="Run label under runs/<config_name> for this live runtime.",
),
host: str = typer.Option(
"0.0.0.0",
@@ -1031,7 +1182,7 @@ def live(
help="WebSocket server host",
),
port: int = typer.Option(
8765,
GATEWAY_PORT,
"--port",
"-p",
help="WebSocket server port",
@@ -1067,11 +1218,19 @@ def live(
"--enable-memory",
help="Enable ReMeTaskLongTermMemory for agents (requires MEMORY_API_KEY)",
),
force: bool = typer.Option(
False,
"--force",
help="Force start even if microservices are detected (may cause conflicts)",
),
):
"""
Run live trading mode with real-time data.
Run live trading mode in STANDALONE mode (monolithic Gateway).
Example:
This starts a self-contained process with all agents. For microservice
mode (distributed services), use ./start-dev.sh instead.
Examples:
evotraders live # Run immediately (default)
evotraders live -t 22:30 # Run at 22:30 local time daily
evotraders live --schedule-mode intraday --interval-minutes 60
@@ -1080,12 +1239,16 @@ def live(
"""
schedule_mode = str(_normalize_typer_value(schedule_mode, "daily"))
interval_minutes = int(_normalize_typer_value(interval_minutes, 60))
console.print(
Panel.fit(
"[bold cyan]大时代 LIVE Mode[/bold cyan]",
border_style="cyan",
),
)
# Check for microservice conflicts
running_services = _detect_running_microservices()
if running_services or _check_gateway_port_conflict(port):
if not _display_mode_warning(running_services, port, force=force):
console.print("\n[yellow]Startup aborted.[/yellow]")
raise typer.Exit(0)
# Display standalone mode banner
_display_standalone_banner("live", config_name)
# Check for required API key in live mode
env_file = get_project_root() / ".env"
@@ -1161,9 +1324,8 @@ def live(
# Display configuration
console.print("\n[bold]Configuration:[/bold]")
console.print(
" Mode: [green]LIVE[/green] (Real-time prices via Finnhub)",
" Data Mode: [green]LIVE[/green] (Real-time prices via Finnhub)",
)
console.print(f" Config: {config_name}")
console.print(f" Server: {host}:{port}")
console.print(f" Poll Interval: {poll_interval}s")
console.print(
@@ -1230,7 +1392,7 @@ def live(
@app.command()
def frontend(
port: int = typer.Option(
8765,
GATEWAY_PORT,
"--ws-port",
"-p",
help="WebSocket server port to connect to",
@@ -1317,6 +1479,90 @@ def frontend(
raise typer.Exit(1)
@app.command()
def status(
detailed: bool = typer.Option(
False,
"--detailed",
"-d",
help="Show detailed service information",
),
):
"""
Check the status of running services (microservice or standalone mode).
Detects whether microservices are running and shows their health status.
"""
console.print(
Panel.fit(
"[bold cyan]大时代 Service Status[/bold cyan]",
border_style="cyan",
)
)
running_services = _detect_running_microservices()
gateway_running = _check_gateway_port_conflict(GATEWAY_PORT)
# Determine mode
if running_services:
mode = "microservice"
console.print(f"\n[bold]Mode:[/bold] [green]{mode.upper()}[/green]")
console.print("[dim]Microservices are running on the following ports:[/dim]\n")
table = Table(title="Running Microservices")
table.add_column("Service", style="cyan")
table.add_column("Port", justify="right")
table.add_column("URL")
for service, port in running_services.items():
url = f"http://localhost:{port}"
table.add_row(service, str(port), url)
if gateway_running:
table.add_row(
"gateway (WebSocket)",
str(GATEWAY_PORT),
f"ws://localhost:{GATEWAY_PORT}",
)
console.print(table)
elif gateway_running:
mode = "standalone"
console.print(f"\n[bold]Mode:[/bold] [yellow]{mode.upper()}[/yellow]")
console.print("[dim]Standalone Gateway is running (monolithic mode)[/dim]")
console.print(f"\n Gateway: [cyan]ws://localhost:{GATEWAY_PORT}[/cyan]")
else:
console.print(f"\n[bold]Mode:[/bold] [red]NOT RUNNING[/red]")
console.print("\n[dim]No services detected. Start with:[/dim]")
console.print(" • Standalone: [cyan]evotraders backtest[/cyan] or [cyan]evotraders live[/cyan]")
console.print(" • Microservice: [cyan]./start-dev.sh[/cyan]")
if detailed and running_services:
console.print("\n[bold]Health Checks:[/bold]")
import urllib.request
import json
for service, port in running_services.items():
try:
req = urllib.request.Request(
f"http://localhost:{port}/health",
method="GET",
headers={"Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=2) as response:
if response.status == 200:
data = json.loads(response.read().decode())
status_text = data.get("status", "unknown")
color = "green" if status_text == "healthy" else "yellow"
console.print(f" {service}: [{color}]{status_text}[/{color}]")
else:
console.print(f" {service}: [yellow]HTTP {response.status}[/yellow]")
except Exception as e:
console.print(f" {service}: [red]unreachable ({type(e).__name__})[/red]")
console.print()
@app.command()
def version():
"""Show the version of 大时代."""
@@ -1330,7 +1576,17 @@ def main():
"""
大时代:自进化多智能体交易系统
Use 'evotraders --help' to see available commands.
RUNTIME MODES:
--------------
• STANDALONE (default): Use 'evotraders backtest' or 'evotraders live'
Starts a self-contained monolithic Gateway with all agents.
Best for: quick testing, single-machine deployment
• MICROSERVICE: Use './start-dev.sh'
Starts 4 separate FastAPI services + Gateway subprocess.
Best for: production scaling, distributed deployment
Use 'evotraders status' to check which mode is currently running.
"""