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:
328
backend/cli.py
328
backend/cli.py
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user