diff --git a/CLAUDE.md b/CLAUDE.md index 5ad075b..1e8f2dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,12 +16,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co # 安装依赖 uv pip install -e . -# 运行命令 -evotraders backtest --start 2025-11-01 --end 2025-12-01 # 回测模式 -evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory # 带记忆回测 -evotraders live # 实盘交易 -evotraders live -t 22:30 # 定时每日交易 -evotraders frontend # 启动可视化界面 +# 运行回测 / 实盘 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 --enable-memory +python backend/main.py --mode live --config-name live +python backend/main.py --mode live --config-name live --trigger-time 22:30 + +# 启动前端 +cd frontend && npm run dev # 开发服务器 ./start-dev.sh # 启动全部 4 个微服务 (agent, runtime, trading, news) diff --git a/README.md b/README.md index c3d1357..9d5f48c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 大时代 is an open-source financial trading agent framework that combines multi-agent collaboration, run-scoped workspaces, and memory to support both backtests and live trading workflows. -The repository name and CLI entrypoints still use `evotraders` for compatibility, but the product-facing branding now follows the 大时代 naming used by the reference branch. +The repository name still uses `evotraders`, but the product-facing branding now follows the 大时代 naming used by the reference branch. --- @@ -218,7 +218,7 @@ This starts: Then start the frontend in another terminal: ```bash -evotraders frontend +cd frontend && npm run dev ``` Open `http://localhost:5173`. @@ -233,35 +233,29 @@ python -m uvicorn backend.apps.runtime_service:app --host 0.0.0.0 --port 8003 -- # compatibility gateway path, not the recommended primary dev entrypoint python -m backend.main --mode live --host 0.0.0.0 --port 8765 ``` - -### 4. Run backtest or live mode from CLI +### 4. Run backtest or live mode Backtest: ```bash -evotraders backtest --start 2025-11-01 --end 2025-12-01 -evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory -evotraders backtest --config-name smoke_fullstack --start 2025-11-01 --end 2025-12-01 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 --enable-memory ``` Live: ```bash -evotraders live -evotraders live --enable-memory -evotraders live --schedule-mode intraday --interval-minutes 60 -evotraders live --trigger-time 22:30 +python backend/main.py --mode live --config-name live +python backend/main.py --mode live --config-name live --enable-memory +python backend/main.py --mode live --config-name live --interval-minutes 60 +python backend/main.py --mode live --config-name live --trigger-time 22:30 ``` Help: ```bash -evotraders --help -evotraders backtest --help -evotraders live --help -evotraders frontend --help +python backend/main.py --help ``` - ### Offline backtest data If you want a quick backtest demo without external market APIs, download the offline bundle and unzip it into `backend/data`: @@ -381,11 +375,7 @@ trigger_time: "09:30" enable_memory: false ``` -Initialize run-scoped assets with: - -```bash -evotraders init-workspace --config-name my_run -``` +Run-scoped workspaces are created automatically at runtime. No manual initialization is required. --- @@ -399,8 +389,7 @@ evotraders/ │ ├── apps/ # split service surfaces │ ├── core/ # pipeline, scheduler, state sync │ ├── runtime/ # runtime manager and agent runtime state -│ ├── services/ # gateway, market/storage/db services -│ └── cli.py # Typer CLI entrypoint +│ └── services/ # gateway, market/storage/db services ├── frontend/ # React + Vite UI ├── shared/ # shared clients and schemas for split services ├── runs/ # run-scoped state and dashboards diff --git a/README_zh.md b/README_zh.md index c383773..dafd09c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -184,7 +184,7 @@ python3 scripts/smoke_evo_runtime.py --agent-id fundamentals_analyst 然后在另一个终端启动前端: ```bash -evotraders frontend +cd frontend && npm run dev ``` 访问 `http://localhost:5173`。 @@ -203,34 +203,29 @@ python -m backend.main --mode live --host 0.0.0.0 --port 8765 仓库里部署脚本使用的 `production` 只是一个示例 run label,不应再把它理解成 系统规定的根目录运行目录名。 -### 4. 使用 CLI 运行回测或实盘 +### 4. 运行回测或实盘 回测: ```bash -evotraders backtest --start 2025-11-01 --end 2025-12-01 -evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory -evotraders backtest --config-name smoke_fullstack --start 2025-11-01 --end 2025-12-01 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 +python backend/main.py --mode backtest --config-name smoke_fullstack --start-date 2025-11-01 --end-date 2025-12-01 --enable-memory ``` 实盘: ```bash -evotraders live -evotraders live --enable-memory -evotraders live --schedule-mode intraday --interval-minutes 60 -evotraders live --trigger-time 22:30 +python backend/main.py --mode live --config-name live +python backend/main.py --mode live --config-name live --enable-memory +python backend/main.py --mode live --config-name live --interval-minutes 60 +python backend/main.py --mode live --config-name live --trigger-time 22:30 ``` 帮助: ```bash -evotraders --help -evotraders backtest --help -evotraders live --help -evotraders frontend --help +python backend/main.py --help ``` - ### 离线回测数据 如果只是想快速体验回测,不依赖外部行情 API,可以下载离线数据包并解压到 `backend/data`: @@ -348,11 +343,7 @@ trigger_time: "09:30" enable_memory: false ``` -初始化一个 run 运行资产目录: - -```bash -evotraders init-workspace --config-name my_run -``` +运行时作用域工作区会在首次运行 pipeline 或服务时自动创建,无需手动初始化。 --- @@ -367,7 +358,6 @@ evotraders/ │ ├── core/ # pipeline、scheduler、state sync │ ├── runtime/ # runtime manager 和 agent runtime state │ ├── services/ # gateway、market/storage/db 服务 -│ └── cli.py # Typer CLI 入口 ├── frontend/ # React + Vite 前端 ├── shared/ # 拆分服务共用 client 和 schema ├── runs/ # run-scoped 状态和 dashboards diff --git a/backend/agents/base/evo_agent.py b/backend/agents/base/evo_agent.py index bbe8e30..eac1c0c 100644 --- a/backend/agents/base/evo_agent.py +++ b/backend/agents/base/evo_agent.py @@ -126,6 +126,8 @@ class EvoAgent(ToolGuardMixin, ReActAgent): self.agent_id = agent_id self.config_name = config_name self.workspace_dir = Path(workspace_dir) + self.workspace_id = config_name + self.config = {"config_name": config_name} self._skills_manager = skills_manager or SkillsManager() self._env_context = env_context self._prompt_files = prompt_files @@ -328,6 +330,17 @@ class EvoAgent(ToolGuardMixin, ReActAgent): # Call parent (which may be ToolGuardMixin's _reasoning) return await super()._reasoning(**kwargs) + def reload_runtime_assets(self, active_skill_dirs: Optional[List[Path]] = None) -> None: + """Reload toolkit and system prompt from current run assets. + + Refreshes prompt files from workspace config and rebuilds the toolkit. + """ + # Rebuild system prompt (also refreshes _agent_config and _prompt_files) + self.rebuild_sys_prompt() + + # Reload skills/toolkit + self.reload_skills(active_skill_dirs=active_skill_dirs) + def reload_skills(self, active_skill_dirs: Optional[List[Path]] = None) -> None: """Reload skills at runtime. @@ -497,6 +510,10 @@ class EvoAgent(ToolGuardMixin, ReActAgent): # Reload agent config in case it changed self._agent_config = self._load_agent_config() + # Refresh prompt_files from updated config + if "prompt_files" in self._agent_config: + self._prompt_files = list(self._agent_config["prompt_files"]) + # Rebuild prompt self._sys_prompt = self._build_system_prompt() diff --git a/backend/cli.py b/backend/cli.py deleted file mode 100644 index f846dc3..0000000 --- a/backend/cli.py +++ /dev/null @@ -1,1594 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -大时代 CLI - Command-line interface for the 大时代 trading system. - -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 -from pathlib import Path -from typing import Optional -from zoneinfo import ZoneInfo - -logger = logging.getLogger(__name__) - -import typer -import yaml -from rich.console import Console -from rich.panel import Panel -from rich.prompt import Confirm -from rich.table import Table -from dotenv import load_dotenv - -from backend.agents.agent_workspace import load_agent_workspace_config -from backend.agents.prompt_loader import get_prompt_loader -from backend.agents.skills_manager import SkillsManager -from backend.agents.team_pipeline_config import ( - ensure_team_pipeline_config, - load_team_pipeline_config, -) -from backend.agents.workspace_manager import WorkspaceManager -from backend.config.constants import ANALYST_TYPES -from backend.data.market_ingest import ingest_symbols -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="大时代:自进化多智能体交易系统", - add_completion=False, -) -ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.") -app.add_typer(ingest_app, name="ingest") -skills_app = typer.Typer(help="Inspect and manage per-agent skills.") -app.add_typer(skills_app, name="skills") -team_app = typer.Typer(help="Inspect and manage run-scoped team pipeline config.") -app.add_typer(team_app, name="team") - -console = Console() -_prompt_loader = get_prompt_loader() -load_dotenv() - - -def _normalize_typer_value(value, default): - """Allow CLI command functions to be called directly in tests/internal code.""" - if hasattr(value, "default"): - return value.default - return default if value is None else value - - -def get_project_root() -> Path: - """Get the project root directory.""" - # Assuming cli.py is in backend/ - 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 [/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. - - Args: - config_name: Configuration name for the run - auto_clean: If True, skip confirmation and clean automatically - """ - workspace_manager = WorkspaceManager(project_root=get_project_root()) - base_data_dir = workspace_manager.get_run_dir(config_name) - - # Check if historical data exists - if not base_data_dir.exists() or not any(base_data_dir.iterdir()): - console.print( - f"\n[dim]No historical data found for config '{config_name}'[/dim]", - ) - console.print("[dim] Will start from scratch[/dim]\n") - return - - console.print("\n[bold yellow]Detected existing run data:[/bold yellow]") - console.print(f" Data directory: [cyan]{base_data_dir}[/cyan]") - - # Show directory size - try: - total_size = sum( - f.stat().st_size for f in base_data_dir.rglob("*") if f.is_file() - ) - size_mb = total_size / (1024 * 1024) - if size_mb < 1: - console.print( - f" Directory size: [cyan]{total_size / 1024:.1f} KB[/cyan]", - ) - else: - console.print(f" Directory size: [cyan]{size_mb:.1f} MB[/cyan]") - except Exception as e: - logger.debug(f"Could not calculate directory size: {e}") - - # Show last modified time - state_dir = base_data_dir / "state" - if state_dir.exists(): - state_files = list(state_dir.glob("*.json")) - if state_files: - last_modified = max(f.stat().st_mtime for f in state_files) - last_modified_str = datetime.fromtimestamp(last_modified).strftime( - "%Y-%m-%d %H:%M:%S", - ) - console.print(f" Last updated: [cyan]{last_modified_str}[/cyan]") - - console.print() - - # Determine if we should clean - should_clean = auto_clean - if not auto_clean: - should_clean = Confirm.ask( - " ﹂ Clear historical data and start fresh?", - default=False, - ) - else: - console.print("[yellow]⚠️ Auto-clean enabled (--clean flag)[/yellow]") - should_clean = True - - if should_clean: - console.print("\n[yellow]▩ Cleaning historical data...[/yellow]") - - # Backup important config files if they exist - backup_files = [".env", "config.json"] - backed_up = [] - backup_dir = None - - for backup_file in backup_files: - file_path = base_data_dir / backup_file - if file_path.exists(): - if backup_dir is None: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backup_dir = ( - base_data_dir.parent - / f"{config_name}_backup_{timestamp}" - ) - backup_dir.mkdir(parents=True, exist_ok=True) - - shutil.copy(file_path, backup_dir / backup_file) - backed_up.append(backup_file) - - if backed_up: - console.print( - f" 💾 Backed up config files to: [cyan]{backup_dir}[/cyan]", - ) - console.print(f" Files: {', '.join(backed_up)}") - - # Remove the data directory - try: - shutil.rmtree(base_data_dir) - console.print(" ✔ Historical data cleared\n") - except Exception as e: - console.print(f" [red]✗ Error clearing data: {e}[/red]\n") - raise typer.Exit(1) - else: - console.print( - "\n[dim] Continuing with existing historical data[/dim]\n", - ) - - -def run_data_updater(project_root: Path) -> None: - """Run the historical data updater.""" - console.print("\n[bold]Checking historical data update...[/bold]") - try: - result = subprocess.run( - [sys.executable, "-m", "backend.data.ret_data_updater", "--help"], - capture_output=True, - timeout=5, - check=False, - ) - - if result.returncode == 0: - console.print("[cyan]Updating historical data...[/cyan]") - update_result = subprocess.run( - [sys.executable, "-m", "backend.data.ret_data_updater"], - cwd=project_root, - check=False, - ) - - if update_result.returncode == 0: - console.print( - "[green]✔ Historical data updated successfully[/green]\n", - ) - else: - console.print( - "[yellow] Data update failed (might be weekend/holiday)[/yellow]", - ) - console.print( - "[dim] Will continue with existing data[/dim]\n", - ) - else: - console.print( - "[yellow] Data updater module not available, skipping update[/yellow]\n", - ) - except Exception as e: - logger.debug(f"Data updater check failed: {e}") - console.print( - "[yellow] Data updater check failed, skipping update[/yellow]\n", - ) - - -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, - agent_ids=[ - "fundamentals_analyst", - "technical_analyst", - "sentiment_analyst", - "valuation_analyst", - "risk_manager", - "portfolio_manager", - ], - analyst_personas=_prompt_loader.load_yaml_config( - "analyst", - "personas", - ), - ) - return workspace_manager.get_run_dir(config_name) - - -def _require_agent_asset_dir(config_name: str, agent_id: str) -> Path: - manager = WorkspaceManager(project_root=get_project_root()) - manager.initialize_default_assets( - config_name=config_name, - agent_ids=[agent_id], - analyst_personas=_prompt_loader.load_yaml_config( - "analyst", - "personas", - ), - ) - return manager.skills_manager.get_agent_asset_dir(config_name, agent_id) - - -def _resolve_symbols(raw_tickers: Optional[str], config_name: Optional[str] = None) -> list[str]: - """Resolve symbols from explicit input or runtime bootstrap config.""" - if raw_tickers and raw_tickers.strip(): - return [ - item.strip().upper() - for item in raw_tickers.split(",") - if item.strip() - ] - - workspace_manager = WorkspaceManager(project_root=get_project_root()) - bootstrap_path = workspace_manager.get_run_dir(config_name or "default") / "BOOTSTRAP.md" - if bootstrap_path.exists(): - content = bootstrap_path.read_text(encoding="utf-8") - for line in content.splitlines(): - if line.strip().startswith("tickers:"): - raw = line.split(":", 1)[1] - return [ - item.strip().upper() - for item in raw.split(",") - if item.strip() - ] - return [] - - -def _filter_problematic_report_rows(rows: list[dict]) -> list[dict]: - """Keep tickers with incomplete coverage or without any LLM-enriched rows.""" - return [ - row - for row in rows - if float(row.get("coverage_pct") or 0.0) < 100.0 - or int(row.get("llm_count") or 0) == 0 - ] - - -def auto_update_market_store( - config_name: str, - *, - end_date: Optional[str] = None, -) -> None: - """Refresh the long-lived Polygon market store for the active watchlist.""" - api_key = os.getenv("POLYGON_API_KEY", "").strip() - if not api_key: - console.print( - "[dim]Skipping Polygon market store update: POLYGON_API_KEY not set[/dim]", - ) - return - - symbols = _resolve_symbols(None, config_name) - if not symbols: - console.print( - f"[dim]Skipping Polygon market store update: no tickers found for config '{config_name}'[/dim]", - ) - return - - target_end = end_date or datetime.now().date().isoformat() - console.print( - f"[cyan]Updating Polygon market store for {', '.join(symbols)} -> {target_end}[/cyan]", - ) - - try: - results = ingest_symbols( - symbols, - mode="incremental", - end_date=target_end, - ) - except Exception as exc: - console.print( - f"[yellow]Polygon market store update failed, continuing startup: {exc}[/yellow]", - ) - return - - for result in results: - console.print( - "[green]" - f"{result['symbol']}" - "[/green] " - f"prices={result['prices']} news={result['news']} aligned={result['aligned']}" - ) - - -def auto_prepare_backtest_market_store( - config_name: str, - *, - start_date: str, - end_date: str, -) -> None: - """Ensure the market store has the requested backtest window for the active watchlist.""" - api_key = os.getenv("POLYGON_API_KEY", "").strip() - if not api_key: - console.print( - "[dim]Skipping Polygon backtest preload: POLYGON_API_KEY not set[/dim]", - ) - return - - symbols = _resolve_symbols(None, config_name) - if not symbols: - console.print( - f"[dim]Skipping Polygon backtest preload: no tickers found for config '{config_name}'[/dim]", - ) - return - - console.print( - f"[cyan]Preparing Polygon market store for backtest {start_date} -> {end_date} " - f"({', '.join(symbols)})[/cyan]", - ) - - try: - results = ingest_symbols( - symbols, - mode="full", - start_date=start_date, - end_date=end_date, - ) - except Exception as exc: - console.print( - f"[yellow]Polygon backtest preload failed, continuing startup: {exc}[/yellow]", - ) - return - - for result in results: - console.print( - "[green]" - f"{result['symbol']}" - "[/green] " - f"prices={result['prices']} news={result['news']} aligned={result['aligned']}" - ) - - -def auto_enrich_market_store( - config_name: str, - *, - end_date: Optional[str] = None, - lookback_days: int = 120, - force: bool = False, -) -> None: - """Refresh explain-oriented enriched news for the active watchlist.""" - symbols = _resolve_symbols(None, config_name) - if not symbols: - console.print( - f"[dim]Skipping explain enrich: no tickers found for config '{config_name}'[/dim]", - ) - return - - target_end = end_date or datetime.now().date().isoformat() - try: - end_dt = datetime.strptime(target_end, "%Y-%m-%d") - except ValueError: - console.print( - f"[yellow]Skipping explain enrich: invalid end date {target_end}[/yellow]", - ) - return - - start_date = (end_dt - timedelta(days=max(1, lookback_days))).date().isoformat() - console.print( - f"[cyan]Refreshing explain enrich for {', '.join(symbols)} -> {target_end}[/cyan]", - ) - store = MarketStore() - try: - results = enrich_symbols( - store, - symbols, - start_date=start_date, - end_date=target_end, - limit=300, - skip_existing=not force, - ) - except Exception as exc: - console.print( - f"[yellow]Explain enrich failed, continuing startup: {exc}[/yellow]", - ) - return - - for result in results: - console.print( - "[green]" - f"{result['symbol']}" - "[/green] " - f"news={result['news_count']} queued={result['queued_count']} analyzed={result['analyzed']} " - f"skipped={result['skipped_existing_count']} deduped={result['deduped_count']} " - f"llm={result['llm_count']} local={result['local_count']}" - ) - - -@app.command("init-workspace") -def init_workspace( - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run label under runs/ for the initialized asset tree.", - ), -): - """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//`. - """ - run_dir = initialize_run_assets(config_name) - console.print( - Panel.fit( - f"[bold green]Run assets initialized[/bold green]\n[cyan]{run_dir}[/cyan]", - border_style="green", - ), - ) - - -@ingest_app.command("full") -def ingest_full( - tickers: Optional[str] = typer.Option( - None, - "--tickers", - "-t", - help="Comma-separated tickers to ingest", - ), - start: Optional[str] = typer.Option( - None, - "--start", - help="Start date for full ingestion (YYYY-MM-DD)", - ), - end: Optional[str] = typer.Option( - None, - "--end", - help="End date for ingestion (YYYY-MM-DD)", - ), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Fallback config to read tickers from BOOTSTRAP.md", - ), -): - """Run full Polygon ingestion for the specified symbols.""" - symbols = _resolve_symbols(tickers, config_name) - if not symbols: - console.print("[red]No tickers provided and none found in BOOTSTRAP.md[/red]") - raise typer.Exit(1) - - console.print(f"[cyan]Starting full Polygon ingest for {', '.join(symbols)}[/cyan]") - results = ingest_symbols(symbols, mode="full", start_date=start, end_date=end) - for result in results: - console.print( - f"[green]{result['symbol']}[/green] prices={result['prices']} news={result['news']} aligned={result['aligned']}" - ) - - -@ingest_app.command("update") -def ingest_update( - tickers: Optional[str] = typer.Option( - None, - "--tickers", - "-t", - help="Comma-separated tickers to update", - ), - end: Optional[str] = typer.Option( - None, - "--end", - help="Optional end date override (YYYY-MM-DD)", - ), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Fallback config to read tickers from BOOTSTRAP.md", - ), -): - """Run incremental Polygon ingestion using stored watermarks.""" - symbols = _resolve_symbols(tickers, config_name) - if not symbols: - console.print("[red]No tickers provided and none found in BOOTSTRAP.md[/red]") - raise typer.Exit(1) - - console.print(f"[cyan]Starting incremental Polygon ingest for {', '.join(symbols)}[/cyan]") - results = ingest_symbols(symbols, mode="incremental", end_date=end) - for result in results: - console.print( - f"[green]{result['symbol']}[/green] prices={result['prices']} news={result['news']} aligned={result['aligned']}" - ) - - -@ingest_app.command("enrich") -def ingest_enrich( - tickers: Optional[str] = typer.Option( - None, - "--tickers", - "-t", - help="Comma-separated tickers to enrich", - ), - start: Optional[str] = typer.Option( - None, - "--start", - help="Optional start date for enrichment window (YYYY-MM-DD)", - ), - end: Optional[str] = typer.Option( - None, - "--end", - help="Optional end date for enrichment window (YYYY-MM-DD)", - ), - limit: int = typer.Option( - 300, - "--limit", - help="Maximum raw news rows per ticker to analyze", - ), - force: bool = typer.Option( - False, - "--force", - help="Re-analyze already enriched news instead of only missing rows", - ), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Fallback config to read tickers from BOOTSTRAP.md", - ), -): - """Run explain-oriented news enrichment for symbols already in the market store.""" - symbols = _resolve_symbols(tickers, config_name) - if not symbols: - console.print("[red]No tickers provided and none found in BOOTSTRAP.md[/red]") - raise typer.Exit(1) - - console.print(f"[cyan]Starting explain enrich for {', '.join(symbols)}[/cyan]") - store = MarketStore() - results = enrich_symbols( - store, - symbols, - start_date=start, - end_date=end, - limit=max(10, limit), - skip_existing=not force, - ) - for result in results: - console.print( - f"[green]{result['symbol']}[/green] " - f"news={result['news_count']} queued={result['queued_count']} analyzed={result['analyzed']} " - f"skipped={result['skipped_existing_count']} deduped={result['deduped_count']} " - f"llm={result['llm_count']} local={result['local_count']}" - ) - - -@ingest_app.command("report") -def ingest_report( - tickers: Optional[str] = typer.Option( - None, - "--tickers", - "-t", - help="Optional comma-separated tickers to report", - ), - start: Optional[str] = typer.Option( - None, - "--start", - help="Optional start date for report window (YYYY-MM-DD)", - ), - end: Optional[str] = typer.Option( - None, - "--end", - help="Optional end date for report window (YYYY-MM-DD)", - ), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Fallback config to read tickers from BOOTSTRAP.md", - ), - only_problematic: bool = typer.Option( - False, - "--only-problematic", - help="Only show tickers with incomplete coverage or no LLM-enriched news", - ), -): - """Show explain enrichment coverage and freshness per ticker.""" - symbols = _resolve_symbols(tickers, config_name) - store = MarketStore() - report_rows = store.get_enrich_report( - symbols=symbols or None, - start_date=start, - end_date=end, - ) - if only_problematic: - report_rows = _filter_problematic_report_rows(report_rows) - if not report_rows: - if only_problematic: - console.print("[green]No problematic enrich report rows found for the requested scope[/green]") - else: - console.print("[yellow]No enrich report rows found for the requested scope[/yellow]") - raise typer.Exit(0) - - model_info = get_explain_model_info() - model_label = model_info["label"] if llm_enrichment_enabled() else "disabled" - table = Table(title="Explain Enrichment Report") - table.add_column("Ticker", style="cyan") - table.add_column("Raw News", justify="right") - table.add_column("Analyzed", justify="right") - table.add_column("Coverage", justify="right") - table.add_column("LLM", justify="right") - table.add_column("Local", justify="right") - table.add_column("Latest Trade Date") - table.add_column("Latest Analysis") - table.caption = f"Explain LLM: {model_label}" - - for row in report_rows: - table.add_row( - row["symbol"], - str(row["raw_news_count"]), - str(row["analyzed_news_count"]), - f'{row["coverage_pct"]:.1f}%', - str(row["llm_count"]), - str(row["local_count"]), - str(row["latest_trade_date"] or "-"), - str(row["latest_analysis_at"] or "-"), - ) - console.print(table) - - -@skills_app.command("list") -def skills_list( - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run config name.", - ), - agent_id: Optional[str] = typer.Option( - None, - "--agent-id", - "-a", - help="Optional agent id to show resolved status for.", - ), -): - """List available skills and optional agent-level enablement state.""" - project_root = get_project_root() - skills_manager = SkillsManager(project_root=project_root) - catalog = ( - skills_manager.list_agent_skill_catalog(config_name, agent_id) - if agent_id - else skills_manager.list_skill_catalog() - ) - if not catalog: - console.print("[yellow]No skills found[/yellow]") - raise typer.Exit(0) - - agent_config = None - resolved_skills = set() - if agent_id: - asset_dir = _require_agent_asset_dir(config_name, agent_id) - agent_config = load_agent_workspace_config(asset_dir / "agent.yaml") - resolved_skills = set( - skills_manager.resolve_agent_skill_names( - config_name=config_name, - agent_id=agent_id, - default_skills=[], - ), - ) - - table = Table(title="Skill Catalog") - table.add_column("Skill", style="cyan") - table.add_column("Source") - table.add_column("Description") - if agent_id: - table.add_column("Status") - - enabled = set(agent_config.enabled_skills) if agent_config else set() - disabled = set(agent_config.disabled_skills) if agent_config else set() - for skill in catalog: - row = [ - skill.skill_name, - skill.source, - skill.description or "-", - ] - if agent_id: - if skill.skill_name in disabled: - status = "disabled" - elif skill.skill_name in enabled: - status = "enabled" - elif skill.skill_name in resolved_skills: - status = "active" - else: - status = "-" - row.append(status) - table.add_row(*row) - console.print(table) - - -@skills_app.command("enable") -def skills_enable( - agent_id: str = typer.Option(..., "--agent-id", "-a", help="Agent id."), - skill: str = typer.Option(..., "--skill", "-s", help="Skill name."), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run config name.", - ), -): - """Enable a skill for one agent in agent.yaml.""" - asset_dir = _require_agent_asset_dir(config_name, agent_id) - skills_manager = SkillsManager(project_root=get_project_root()) - catalog = { - item.skill_name - for item in skills_manager.list_agent_skill_catalog(config_name, agent_id) - } - if skill not in catalog: - console.print(f"[red]Unknown skill: {skill}[/red]") - raise typer.Exit(1) - - result = skills_manager.update_agent_skill_overrides( - config_name=config_name, - agent_id=agent_id, - enable=[skill], - ) - console.print( - f"[green]Enabled[/green] `{skill}` for `{agent_id}` " - f"([{asset_dir / 'agent.yaml'}])", - ) - console.print(f"Enabled skills: {', '.join(result['enabled_skills']) or '-'}") - console.print(f"Disabled skills: {', '.join(result['disabled_skills']) or '-'}") - - -@skills_app.command("disable") -def skills_disable( - agent_id: str = typer.Option(..., "--agent-id", "-a", help="Agent id."), - skill: str = typer.Option(..., "--skill", "-s", help="Skill name."), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run config name.", - ), -): - """Disable a skill for one agent in agent.yaml.""" - asset_dir = _require_agent_asset_dir(config_name, agent_id) - skills_manager = SkillsManager(project_root=get_project_root()) - result = skills_manager.update_agent_skill_overrides( - config_name=config_name, - agent_id=agent_id, - disable=[skill], - ) - console.print( - f"[yellow]Disabled[/yellow] `{skill}` for `{agent_id}` " - f"([{asset_dir / 'agent.yaml'}])", - ) - console.print(f"Enabled skills: {', '.join(result['enabled_skills']) or '-'}") - console.print(f"Disabled skills: {', '.join(result['disabled_skills']) or '-'}") - - -@skills_app.command("install") -def skills_install( - agent_id: str = typer.Option(..., "--agent-id", "-a", help="Target agent id."), - source: str = typer.Option( - ..., - "--source", - "-s", - help="External skill source: directory path, zip path, or http(s) zip URL.", - ), - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run config name.", - ), - name: Optional[str] = typer.Option( - None, - "--name", - help="Optional override skill name.", - ), - activate: bool = typer.Option( - True, - "--activate/--no-activate", - help="Enable the skill for this agent immediately.", - ), -): - """Install an external skill into one agent's local skill directory.""" - _require_agent_asset_dir(config_name, agent_id) - skills_manager = SkillsManager(project_root=get_project_root()) - result = skills_manager.install_external_skill_for_agent( - config_name=config_name, - agent_id=agent_id, - source=source, - skill_name=name, - activate=activate, - ) - console.print( - f"[green]Installed[/green] `{result['skill_name']}` to `{agent_id}`", - ) - console.print(f"Path: {result['target_dir']}") - console.print(f"Activated: {result['activated']}") - warnings = result.get("warnings") or [] - if warnings: - console.print(f"Warnings: {'; '.join(warnings)}") - - -@team_app.command("show") -def team_show( - config_name: str = typer.Option( - "default", - "--config-name", - "-c", - help="Run config name.", - ), -): - """Show TEAM_PIPELINE.yaml for one run.""" - project_root = get_project_root() - ensure_team_pipeline_config( - project_root=project_root, - config_name=config_name, - default_analysts=list(ANALYST_TYPES.keys()), - ) - config = load_team_pipeline_config(project_root, config_name) - console.print( - Panel.fit( - yaml.safe_dump(config, allow_unicode=True, sort_keys=False), - title=f"TEAM_PIPELINE ({config_name})", - border_style="cyan", - ), - ) - - -# ============================================================================= -# 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( - None, - "--start", - "-s", - help="Start date for backtest (YYYY-MM-DD)", - ), - end: Optional[str] = typer.Option( - None, - "--end", - "-e", - help="End date for backtest (YYYY-MM-DD)", - ), - config_name: str = typer.Option( - "default_backtest_run", - "--config-name", - "-c", - help="Run label under runs/ for this backtest runtime.", - ), - host: str = typer.Option( - "0.0.0.0", - "--host", - help="WebSocket server host", - ), - port: int = typer.Option( - GATEWAY_PORT, - "--port", - "-p", - help="WebSocket server port", - ), - poll_interval: int = typer.Option( - 10, - "--poll-interval", - help="Price polling interval in seconds", - ), - clean: bool = typer.Option( - False, - "--clean", - help="Clear historical data before starting", - ), - enable_memory: bool = typer.Option( - False, - "--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 in STANDALONE mode (monolithic Gateway). - - 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 - """ - poll_interval = int(_normalize_typer_value(poll_interval, 10)) - - # Validate dates - required for backtest - if not start or not end: - console.print( - "[red]✗ Both --start and --end dates are required for backtest mode[/red]", - ) - raise typer.Exit(1) - - try: - datetime.strptime(start, "%Y-%m-%d") - except ValueError as exc: - console.print( - "[red]✗ Invalid start date format. Use YYYY-MM-DD[/red]", - ) - raise typer.Exit(1) from exc - - try: - datetime.strptime(end, "%Y-%m-%d") - except ValueError as exc: - console.print( - "[red]✗ Invalid end date format. Use YYYY-MM-DD[/red]", - ) - raise typer.Exit(1) from exc - - # 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(f" Period: {start} -> {end}") - console.print(f" Server: {host}:{port}") - console.print(f" Poll Interval: {poll_interval}s") - console.print( - f" Long-term Memory: {'enabled' if enable_memory else 'disabled'}", - ) - 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) - - # Run data updater - run_data_updater(project_root) - auto_prepare_backtest_market_store( - config_name, - start_date=start, - end_date=end, - ) - auto_enrich_market_store( - config_name, - end_date=end, - force=False, - ) - - # Build command using backend.main - cmd = [ - sys.executable, - "-u", - "-m", - "backend.main", - "--mode", - "backtest", - "--config-name", - config_name, - "--host", - host, - "--port", - str(port), - "--poll-interval", - str(poll_interval), - "--start-date", - start, - "--end-date", - end, - ] - - if enable_memory: - cmd.append("--enable-memory") - - try: - subprocess.run(cmd, check=True) - except KeyboardInterrupt: - console.print("\n\n[yellow]Backtest stopped by user[/yellow]") - except subprocess.CalledProcessError as e: - console.print( - f"\n[red]Backtest failed with exit code {e.returncode}[/red]", - ) - raise typer.Exit(1) - - -@app.command() -def live( - config_name: str = typer.Option( - "default_live_run", - "--config-name", - "-c", - help="Run label under runs/ for this live runtime.", - ), - host: str = typer.Option( - "0.0.0.0", - "--host", - help="WebSocket server host", - ), - port: int = typer.Option( - GATEWAY_PORT, - "--port", - "-p", - help="WebSocket server port", - ), - schedule_mode: str = typer.Option( - "daily", - "--schedule-mode", - help="Scheduler mode: 'daily' or 'intraday'", - ), - trigger_time: str = typer.Option( - "now", - "--trigger-time", - "-t", - help="Trigger time in LOCAL timezone (HH:MM), or 'now' to run immediately", - ), - interval_minutes: int = typer.Option( - 60, - "--interval-minutes", - help="When schedule-mode=intraday, run every N minutes", - ), - poll_interval: int = typer.Option( - 10, - "--poll-interval", - help="Price polling interval in seconds", - ), - clean: bool = typer.Option( - False, - "--clean", - help="Clear historical data before starting", - ), - enable_memory: bool = typer.Option( - False, - "--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 in STANDALONE mode (monolithic Gateway). - - 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 - evotraders live --trigger-time now # Run immediately - evotraders live --clean # Clear historical data before starting - """ - schedule_mode = str(_normalize_typer_value(schedule_mode, "daily")) - interval_minutes = int(_normalize_typer_value(interval_minutes, 60)) - - # 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" - if not env_file.exists(): - console.print("\n[yellow]Warning: .env file not found[/yellow]") - console.print("Creating from template...\n") - template = get_project_root() / "env.template" - if template.exists(): - shutil.copy(template, env_file) - console.print("[green].env file created[/green]") - console.print( - "\n[red]Error: Please edit .env and set FINNHUB_API_KEY[/red]", - ) - console.print( - "Get your free API key at: https://finnhub.io/register\n", - ) - else: - console.print("[red]Error: env.template not found[/red]") - raise typer.Exit(1) - - # Handle historical data cleanup - handle_history_cleanup(config_name, auto_clean=clean) - - if schedule_mode not in {"daily", "intraday"}: - console.print( - f"[red]Error: unsupported schedule mode '{schedule_mode}'[/red]", - ) - raise typer.Exit(1) - - if interval_minutes <= 0: - console.print("[red]Error: --interval-minutes must be > 0[/red]") - raise typer.Exit(1) - - # Convert local time to NYSE time - nyse_tz = ZoneInfo("America/New_York") - local_tz = datetime.now().astimezone().tzinfo - local_now = datetime.now() - nyse_now = datetime.now(nyse_tz) - - # Convert trigger time from local to NYSE - if schedule_mode == "intraday": - nyse_trigger_time = "now" - elif trigger_time.lower() == "now": - nyse_trigger_time = "now" - else: - local_trigger = datetime.strptime(trigger_time, "%H:%M") - local_trigger_dt = local_now.replace( - hour=local_trigger.hour, - minute=local_trigger.minute, - second=0, - microsecond=0, - ) - local_trigger_aware = local_trigger_dt.astimezone(local_tz) - nyse_trigger_dt = local_trigger_aware.astimezone(nyse_tz) - nyse_trigger_time = nyse_trigger_dt.strftime("%H:%M") - - # Display time info - console.print("\n[bold]Time Info:[/bold]") - console.print(f" Local Time: {local_now.strftime('%Y-%m-%d %H:%M:%S')}") - console.print( - f" NYSE Time: {nyse_now.strftime('%Y-%m-%d %H:%M:%S %Z')}", - ) - console.print(f" Schedule: {schedule_mode}") - if schedule_mode == "intraday": - console.print(f" Interval: every {interval_minutes} minute(s)") - elif nyse_trigger_time == "now": - console.print(" Trigger: [green]NOW (immediate)[/green]") - else: - console.print( - f" Trigger: {trigger_time} local = {nyse_trigger_time} NYSE", - ) - - # Display configuration - console.print("\n[bold]Configuration:[/bold]") - console.print( - " Data Mode: [green]LIVE[/green] (Real-time prices via Finnhub)", - ) - console.print(f" Server: {host}:{port}") - console.print(f" Poll Interval: {poll_interval}s") - console.print( - f" Long-term Memory: {'enabled' if enable_memory else 'disabled'}", - ) - - console.print("\nAccess frontend at: [cyan]http://localhost:5173[/cyan]") - console.print("Press Ctrl+C to stop\n") - - # Change to project root - project_root = get_project_root() - os.chdir(project_root) - - # Data update - run_data_updater(project_root) - auto_update_market_store( - config_name, - end_date=nyse_now.date().isoformat(), - ) - auto_enrich_market_store( - config_name, - end_date=nyse_now.date().isoformat(), - force=False, - ) - - # Build command using backend.main - cmd = [ - sys.executable, - "-u", - "-m", - "backend.main", - "--mode", - "live", - "--config-name", - config_name, - "--host", - host, - "--port", - str(port), - "--schedule-mode", - schedule_mode, - "--poll-interval", - str(poll_interval), - "--trigger-time", - nyse_trigger_time, - "--interval-minutes", - str(interval_minutes), - ] - - if enable_memory: - cmd.append("--enable-memory") - - try: - subprocess.run(cmd, check=True) - except KeyboardInterrupt: - console.print("\n\n[yellow]Live server stopped by user[/yellow]") - except subprocess.CalledProcessError as e: - console.print( - f"\n[red]Live server failed with exit code {e.returncode}[/red]", - ) - raise typer.Exit(1) from e - - -@app.command() -def frontend( - port: int = typer.Option( - GATEWAY_PORT, - "--ws-port", - "-p", - help="WebSocket server port to connect to", - ), - host_mode: bool = typer.Option( - False, - "--host", - help="Allow external access (default: localhost only)", - ), -): - """ - Start the frontend development server. - - Example: - evotraders frontend - evotraders frontend --ws-port 8765 - evotraders frontend --ws-port 8765 --host - """ - console.print( - Panel.fit( - "[bold cyan]大时代 Frontend[/bold cyan]", - border_style="cyan", - ), - ) - - project_root = get_project_root() - frontend_dir = project_root / "frontend" - - # Check if frontend directory exists - if not frontend_dir.exists(): - console.print( - f"\n[red]Error: Frontend directory not found: {frontend_dir}[/red]", - ) - raise typer.Exit(1) - - # Check if node_modules exists - node_modules = frontend_dir / "node_modules" - if not node_modules.exists(): - console.print("\n[yellow]Installing frontend dependencies...[/yellow]") - try: - subprocess.run( - ["npm", "install"], - cwd=frontend_dir, - check=True, - ) - console.print("[green]Dependencies installed[/green]\n") - except subprocess.CalledProcessError as exc: - console.print("\n[red]Error: Failed to install dependencies[/red]") - console.print("Make sure Node.js and npm are installed") - raise typer.Exit(1) from exc - - # Set WebSocket URL environment variable - ws_url = f"ws://localhost:{port}" - env = os.environ.copy() - env["VITE_WS_URL"] = ws_url - - # Display configuration - console.print("\n[bold]Configuration:[/bold]") - console.print(f" WebSocket URL: {ws_url}") - console.print(" Frontend Port: 5173 (Vite default)") - if host_mode: - console.print(" Access: External allowed") - else: - console.print(" Access: Localhost only") - console.print("\nAccess at: [cyan]http://localhost:5173[/cyan]") - console.print("Press Ctrl+C to stop\n") - - # Choose npm command - npm_cmd = ["npm", "run", "dev:host" if host_mode else "dev"] - - try: - subprocess.run( - npm_cmd, - cwd=frontend_dir, - env=env, - check=True, - ) - except KeyboardInterrupt: - console.print("\n\n[yellow]Frontend stopped by user[/yellow]") - except subprocess.CalledProcessError as e: - console.print( - f"\n[red]Frontend failed with exit code {e.returncode}[/red]", - ) - 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 大时代.""" - console.print( - "\n[bold cyan]大时代[/bold cyan] version [green]0.1.0[/green]\n", - ) - - -@app.callback() -def main(): - """ - 大时代:自进化多智能体交易系统 - - 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. - """ - - -if __name__ == "__main__": - app() diff --git a/backend/tests/test_agent_service_app.py b/backend/tests/test_agent_service_app.py index e096bb8..217f4eb 100644 --- a/backend/tests/test_agent_service_app.py +++ b/backend/tests/test_agent_service_app.py @@ -38,7 +38,7 @@ def test_agent_service_status_includes_scope_metadata(tmp_path): payload = response.json() assert payload["scope"]["design_time_registry"]["root"] == str(tmp_path / "workspaces") assert payload["scope"]["runtime_assets"]["root"] == str(tmp_path / "runs") - assert "runs/" in payload["scope"]["agent_route_note"] + assert "runs/{run_id}" in payload["scope"]["agent_route_note"] def test_agent_service_read_routes(monkeypatch, tmp_path): diff --git a/backend/tests/test_cli.py b/backend/tests/test_cli.py deleted file mode 100644 index c7d8fc8..0000000 --- a/backend/tests/test_cli.py +++ /dev/null @@ -1,317 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -from typer.testing import CliRunner - -from backend import cli - - -def test_live_runs_incremental_market_store_update_before_start(monkeypatch, tmp_path): - project_root = tmp_path - (project_root / ".env").write_text("FINNHUB_API_KEY=test\n", encoding="utf-8") - - calls = [] - - monkeypatch.setattr(cli, "get_project_root", lambda: project_root) - monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None) - monkeypatch.setattr(cli, "run_data_updater", lambda project_root: calls.append(("run_data_updater", project_root))) - monkeypatch.setattr( - cli, - "auto_update_market_store", - lambda config_name, end_date=None: calls.append(("auto_update_market_store", config_name, end_date)), - ) - monkeypatch.setattr( - cli, - "auto_enrich_market_store", - lambda config_name, end_date=None, lookback_days=120, force=False: calls.append( - ("auto_enrich_market_store", config_name, end_date, lookback_days, force) - ), - ) - monkeypatch.setattr(cli.os, "chdir", lambda path: calls.append(("chdir", Path(path)))) - - def fake_run(cmd, check=True, **kwargs): - calls.append(("subprocess.run", cmd, check)) - return 0 - - monkeypatch.setattr(cli.subprocess, "run", fake_run) - - cli.live( - config_name="smoke_fullstack", - host="0.0.0.0", - port=8765, - trigger_time="now", - poll_interval=10, - clean=False, - enable_memory=False, - ) - - assert any(item[0] == "run_data_updater" for item in calls) - assert any( - item[0] == "auto_update_market_store" and item[1] == "smoke_fullstack" - for item in calls - ) - assert any( - item[0] == "auto_enrich_market_store" and item[1] == "smoke_fullstack" - for item in calls - ) - run_call = next(item for item in calls if item[0] == "subprocess.run") - assert run_call[1][:6] == [ - cli.sys.executable, - "-u", - "-m", - "backend.main", - "--mode", - "live", - ] - - -def test_backtest_runs_full_market_store_prepare_before_start(monkeypatch, tmp_path): - project_root = tmp_path - calls = [] - - monkeypatch.setattr(cli, "get_project_root", lambda: project_root) - monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None) - monkeypatch.setattr(cli, "run_data_updater", lambda project_root: calls.append(("run_data_updater", project_root))) - monkeypatch.setattr( - cli, - "auto_prepare_backtest_market_store", - lambda config_name, start_date, end_date: calls.append( - ("auto_prepare_backtest_market_store", config_name, start_date, end_date) - ), - ) - monkeypatch.setattr( - cli, - "auto_enrich_market_store", - lambda config_name, end_date=None, lookback_days=120, force=False: calls.append( - ("auto_enrich_market_store", config_name, end_date, lookback_days, force) - ), - ) - monkeypatch.setattr(cli.os, "chdir", lambda path: calls.append(("chdir", Path(path)))) - - def fake_run(cmd, check=True, **kwargs): - calls.append(("subprocess.run", cmd, check)) - return 0 - - monkeypatch.setattr(cli.subprocess, "run", fake_run) - - cli.backtest( - start="2026-03-01", - end="2026-03-10", - config_name="smoke_fullstack", - host="0.0.0.0", - port=8765, - poll_interval=10, - clean=False, - enable_memory=False, - ) - - assert any(item[0] == "run_data_updater" for item in calls) - assert any( - item[0] == "auto_prepare_backtest_market_store" - and item[1:] == ("smoke_fullstack", "2026-03-01", "2026-03-10") - for item in calls - ) - assert any( - item[0] == "auto_enrich_market_store" - and item[1] == "smoke_fullstack" - and item[2] == "2026-03-10" - for item in calls - ) - run_call = next(item for item in calls if item[0] == "subprocess.run") - assert run_call[1][:6] == [ - cli.sys.executable, - "-u", - "-m", - "backend.main", - "--mode", - "backtest", - ] - - -def test_live_cli_defaults_to_generic_run_label(monkeypatch, tmp_path): - project_root = tmp_path - (project_root / ".env").write_text("FINNHUB_API_KEY=test\n", encoding="utf-8") - - calls = [] - runner = CliRunner() - - monkeypatch.setattr(cli, "get_project_root", lambda: project_root) - monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None) - monkeypatch.setattr(cli, "run_data_updater", lambda project_root: None) - monkeypatch.setattr(cli, "auto_update_market_store", lambda config_name, end_date=None: None) - monkeypatch.setattr( - cli, - "auto_enrich_market_store", - lambda config_name, end_date=None, lookback_days=120, force=False: None, - ) - monkeypatch.setattr(cli.os, "chdir", lambda path: None) - - def fake_run(cmd, check=True, **kwargs): - calls.append(cmd) - return 0 - - monkeypatch.setattr(cli.subprocess, "run", fake_run) - - result = runner.invoke(cli.app, ["live", "--trigger-time", "now"]) - - assert result.exit_code == 0 - assert calls - assert "--config-name" in calls[0] - config_index = calls[0].index("--config-name") - assert calls[0][config_index + 1] == "default_live_run" - - -def test_backtest_cli_defaults_to_generic_run_label(monkeypatch, tmp_path): - project_root = tmp_path - calls = [] - runner = CliRunner() - - monkeypatch.setattr(cli, "get_project_root", lambda: project_root) - monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None) - monkeypatch.setattr(cli, "run_data_updater", lambda project_root: None) - monkeypatch.setattr( - cli, - "auto_prepare_backtest_market_store", - lambda config_name, start_date, end_date: None, - ) - monkeypatch.setattr( - cli, - "auto_enrich_market_store", - lambda config_name, end_date=None, lookback_days=120, force=False: None, - ) - monkeypatch.setattr(cli.os, "chdir", lambda path: None) - - def fake_run(cmd, check=True, **kwargs): - calls.append(cmd) - return 0 - - monkeypatch.setattr(cli.subprocess, "run", fake_run) - - result = runner.invoke( - cli.app, - ["backtest", "--start", "2026-03-01", "--end", "2026-03-10"], - ) - - assert result.exit_code == 0 - assert calls - assert "--config-name" in calls[0] - config_index = calls[0].index("--config-name") - assert calls[0][config_index + 1] == "default_backtest_run" - - -def test_main_parser_defaults_to_generic_run_label(): - from backend.main import build_arg_parser - - parser = build_arg_parser() - args = parser.parse_args([]) - - assert args.config_name == "default_run" - - -def test_ingest_enrich_runs_batch_enrichment(monkeypatch): - calls = [] - - monkeypatch.setattr(cli, "_resolve_symbols", lambda raw_tickers, config_name=None: ["AAPL", "MSFT"]) - - class DummyStore: - pass - - monkeypatch.setattr(cli, "MarketStore", lambda: DummyStore()) - monkeypatch.setattr( - cli, - "enrich_symbols", - lambda store, symbols, start_date=None, end_date=None, limit=200, analysis_source="local", skip_existing=True: calls.append( - ("enrich_symbols", symbols, start_date, end_date, limit, analysis_source, skip_existing) - ) or [ - { - "symbol": symbol, - "news_count": 3, - "queued_count": 3, - "analyzed": 3, - "skipped_existing_count": 0, - "deduped_count": 0, - "llm_count": 0, - "local_count": 3, - } - for symbol in symbols - ], - ) - - cli.ingest_enrich( - tickers=None, - start="2026-03-01", - end="2026-03-10", - limit=150, - force=False, - config_name="smoke_fullstack", - ) - - assert calls == [ - ("enrich_symbols", ["AAPL", "MSFT"], "2026-03-01", "2026-03-10", 150, "local", True) - ] - - -def test_ingest_report_reads_market_store_report(monkeypatch): - calls = [] - printed = [] - - monkeypatch.setattr(cli, "_resolve_symbols", lambda raw_tickers, config_name=None: ["AAPL"]) - - class DummyStore: - def get_enrich_report(self, symbols=None, start_date=None, end_date=None): - calls.append(("get_enrich_report", symbols, start_date, end_date)) - return [ - { - "symbol": "AAPL", - "raw_news_count": 10, - "analyzed_news_count": 8, - "coverage_pct": 80.0, - "llm_count": 5, - "local_count": 3, - "latest_trade_date": "2026-03-16", - "latest_analysis_at": "2026-03-16T09:00:00", - } - ] - - monkeypatch.setattr(cli, "MarketStore", lambda: DummyStore()) - monkeypatch.setattr(cli, "get_explain_model_info", lambda: {"provider": "DASHSCOPE", "model_name": "qwen-max", "label": "DASHSCOPE:qwen-max"}) - monkeypatch.setattr(cli, "llm_enrichment_enabled", lambda: True) - monkeypatch.setattr(cli.console, "print", lambda value: printed.append(value)) - - cli.ingest_report( - tickers=None, - start="2026-03-01", - end="2026-03-16", - config_name="smoke_fullstack", - only_problematic=False, - ) - - assert calls == [ - ("get_enrich_report", ["AAPL"], "2026-03-01", "2026-03-16") - ] - assert printed - assert getattr(printed[0], "caption", "") == "Explain LLM: DASHSCOPE:qwen-max" - - -def test_filter_problematic_report_rows_keeps_low_coverage_and_no_llm(): - rows = [ - { - "symbol": "AAPL", - "coverage_pct": 100.0, - "llm_count": 2, - }, - { - "symbol": "MSFT", - "coverage_pct": 80.0, - "llm_count": 1, - }, - { - "symbol": "NVDA", - "coverage_pct": 100.0, - "llm_count": 0, - }, - ] - - filtered = cli._filter_problematic_report_rows(rows) - - assert [row["symbol"] for row in filtered] == ["MSFT", "NVDA"] diff --git a/backend/tests/test_evo_agent_selection.py b/backend/tests/test_evo_agent_selection.py index ecd0fab..b323a47 100644 --- a/backend/tests/test_evo_agent_selection.py +++ b/backend/tests/test_evo_agent_selection.py @@ -198,6 +198,7 @@ def test_evo_agent_reload_runtime_assets_refreshes_prompt_files(monkeypatch, tmp workspace_dir=workspace_dir, model=DummyModel(), formatter=DummyFormatter(), + prompt_files=["SOUL.md"], skills_manager=type( "SkillsManagerStub", (), @@ -248,11 +249,15 @@ def test_pipeline_create_runtime_analyst_uses_evo_agent_when_enabled(monkeypatch created = {} class DummyEvoAgent: + name = "test_analyst" + def __init__(self, **kwargs): created.update(kwargs) self.toolkit = None class DummyAnalystAgent: + name = "test_analyst" + def __init__(self, **kwargs): created.update(kwargs) self.toolkit = None @@ -308,11 +313,15 @@ def test_pipeline_create_runtime_analyst_uses_legacy_when_not_in_evo_ids(monkeyp created = {} class DummyEvoAgent: + name = "test_analyst" + def __init__(self, **kwargs): created.update(kwargs) self.toolkit = None class DummyAnalystAgent: + name = "test_analyst" + def __init__(self, **kwargs): created.update(kwargs) self.toolkit = None diff --git a/backend/tests/test_market_service.py b/backend/tests/test_market_service.py index 35289de..bf19420 100644 --- a/backend/tests/test_market_service.py +++ b/backend/tests/test_market_service.py @@ -115,7 +115,7 @@ class TestPollingPriceManager: {"c": 100.0, "o": 99.0, "h": 101.0, "l": 98.0, "pc": 99.5, "d": 0.5, "dp": 0.5, "t": 1}, ], ): - with caplog.at_level(logging.INFO): + with caplog.at_level(logging.INFO, logger="backend.data.polling_price_manager"): manager._fetch_prices() manager._fetch_prices() diff --git a/backend/tests/test_openclaw_cli_service.py b/backend/tests/test_openclaw_cli_service.py deleted file mode 100644 index 090c098..0000000 --- a/backend/tests/test_openclaw_cli_service.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests for the OpenClaw CLI service wrapper.""" - -from pathlib import Path - -import pytest - -from backend.services.openclaw_cli import OpenClawCliError, OpenClawCliService - - -class _Completed: - def __init__(self, *, returncode=0, stdout="", stderr=""): - self.returncode = returncode - self.stdout = stdout - self.stderr = stderr - - -def test_openclaw_cli_service_runs_json_command(monkeypatch, tmp_path): - captured = {} - - def _fake_run(command, **kwargs): - captured["command"] = command - captured["cwd"] = kwargs["cwd"] - return _Completed(stdout='{"sessions":[{"key":"main/session-1"}]}') - - monkeypatch.setattr("backend.services.openclaw_cli.subprocess.run", _fake_run) - - service = OpenClawCliService(base_command=["openclaw"], cwd=tmp_path, timeout_seconds=3) - payload = service.list_sessions() - - assert payload["sessions"][0]["key"] == "main/session-1" - assert captured["command"] == ["openclaw", "sessions", "--json"] - assert captured["cwd"] == tmp_path - - -def test_openclaw_cli_service_raises_on_failure(monkeypatch, tmp_path): - def _fake_run(command, **kwargs): - return _Completed(returncode=7, stdout="", stderr="boom") - - monkeypatch.setattr("backend.services.openclaw_cli.subprocess.run", _fake_run) - - service = OpenClawCliService(base_command=["openclaw"], cwd=tmp_path, timeout_seconds=3) - - with pytest.raises(OpenClawCliError) as exc_info: - service.list_cron_jobs() - - assert exc_info.value.exit_code == 7 - assert exc_info.value.stderr == "boom" - - -def test_openclaw_cli_service_can_extract_single_session(monkeypatch, tmp_path): - def _fake_run(command, **kwargs): - return _Completed(stdout='{"sessions":[{"key":"main/session-1","agentId":"main"}]}') - - monkeypatch.setattr("backend.services.openclaw_cli.subprocess.run", _fake_run) - - service = OpenClawCliService(base_command=["openclaw"], cwd=tmp_path, timeout_seconds=3) - session = service.get_session("main/session-1") - - assert session["agentId"] == "main" diff --git a/backend/tests/test_openclaw_service_app.py b/backend/tests/test_openclaw_service_app.py index 0f95627..db321e4 100644 --- a/backend/tests/test_openclaw_service_app.py +++ b/backend/tests/test_openclaw_service_app.py @@ -43,11 +43,49 @@ class _FakeOpenClawCliService: "items": [{"role": "assistant", "text": "hello"}], } + def status_model(self): + from shared.models.openclaw import OpenClawStatus + return OpenClawStatus(runtimeVersion="2026.3.24") + + def get_session_model(self, session_key: str): + from shared.models.openclaw import SessionEntry + for session in self.list_sessions()["sessions"]: + if session["key"] == session_key: + return SessionEntry.model_validate(session, strict=False) + raise KeyError(session_key) + + def list_sessions_model(self): + from shared.models.openclaw import SessionsList, SessionEntry + sessions = [ + SessionEntry.model_validate(s, strict=False) + for s in self.list_sessions()["sessions"] + ] + return SessionsList(sessions=sessions) + + def get_session_history_model(self, session_key: str, *, limit: int = 20): + from shared.models.openclaw import SessionHistory + raw = self.get_session_history(session_key, limit=limit) + return SessionHistory( + sessionKey=raw["sessionKey"], + session_id=None, + events=raw["items"], + history=raw["items"], + raw_text=None, + ) + def list_cron_jobs(self): return {"jobs": [{"id": "job-1", "name": "Daily sync"}]} + def list_cron_jobs_model(self): + from shared.models.openclaw import CronList + return CronList.from_raw(self.list_cron_jobs()) + def list_approvals(self): - return {"approvals": [{"id": "ap-1", "status": "pending"}]} + return {"approvals": [{"approvalId": "ap-1", "toolName": "test_tool", "status": "pending"}]} + + def list_approvals_model(self): + from shared.models.openclaw import ApprovalsList + return ApprovalsList.from_raw(self.list_approvals()) def test_openclaw_service_routes_are_exposed(): @@ -85,17 +123,17 @@ def test_openclaw_service_read_routes(): assert status.status_code == 200 assert status.json()["status"] == "operational" assert openclaw_status.status_code == 200 - assert openclaw_status.json()["runtimeVersion"] == "2026.3.24" + assert openclaw_status.json()["runtime_version"] == "2026.3.24" assert sessions.status_code == 200 assert len(sessions.json()["sessions"]) == 2 assert session.status_code == 200 - assert session.json()["session"]["agentId"] == "main" + assert session.json()["session"]["agent_id"] == "main" assert history.status_code == 200 - assert history.json()["limit"] == 5 + assert len(history.json()["events"]) == 1 assert cron.status_code == 200 assert cron.json()["jobs"][0]["id"] == "job-1" assert approvals.status_code == 200 - assert approvals.json()["approvals"][0]["id"] == "ap-1" + assert approvals.json()["approvals"][0]["approval_id"] == "ap-1" def test_openclaw_service_session_404(): diff --git a/backend/tests/test_runtime_service_app.py b/backend/tests/test_runtime_service_app.py index 029e603..f67f60d 100644 --- a/backend/tests/test_runtime_service_app.py +++ b/backend/tests/test_runtime_service_app.py @@ -38,8 +38,13 @@ def test_runtime_service_health_and_status(monkeypatch): assert health_response.json() == { "status": "healthy", "service": "runtime-service", - "gateway_running": False, - "gateway_port": 9876, + "gateway": { + "running": False, + "port": 9876, + "pid": None, + "process_status": "not_running", + "returncode": None, + }, } assert status_response.status_code == 200 assert status_response.json() == { @@ -48,6 +53,8 @@ def test_runtime_service_health_and_status(monkeypatch): "runtime": { "gateway_running": False, "gateway_port": 9876, + "gateway_pid": None, + "gateway_process_status": "not_running", "has_runtime_manager": True, }, } @@ -365,6 +372,8 @@ def test_runtime_service_start_stop_lifecycle_contract(monkeypatch, tmp_path): return self.context class _DummyProcess: + pid = 12345 + def poll(self): return None @@ -574,6 +583,8 @@ def test_start_runtime_restore_reuses_historical_run_id(monkeypatch, tmp_path): return self.context class _DummyProcess: + pid = 12345 + def poll(self): return None diff --git a/backend/tests/test_service_clients.py b/backend/tests/test_service_clients.py index 877d697..24e03bf 100644 --- a/backend/tests/test_service_clients.py +++ b/backend/tests/test_service_clients.py @@ -25,6 +25,8 @@ class _DummyAsyncClient: async def get(self, path, params=None): self.calls.append(("get", path, params)) + if path == "/sessions/main/session-1": + return _DummyResponse({"session": {"key": "main/session-1", "agentId": "main"}}) return _DummyResponse({"path": path, "params": params}) async def post(self, path, json=None): diff --git a/backend/tests/test_skills_cli.py b/backend/tests/test_skills_cli.py deleted file mode 100644 index 502aa3f..0000000 --- a/backend/tests/test_skills_cli.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -from backend import cli -from backend.agents.skill_metadata import parse_skill_metadata -from backend.agents.skills_manager import SkillsManager -from backend.agents.team_pipeline_config import ( - ensure_team_pipeline_config, - load_team_pipeline_config, - update_active_analysts, -) - - -def test_parse_skill_metadata_extended_frontmatter(tmp_path): - skill_dir = tmp_path / "demo_skill" - skill_dir.mkdir(parents=True, exist_ok=True) - (skill_dir / "SKILL.md").write_text( - "---\n" - "name: demo_skill\n" - "description: Demo description\n" - "tools:\n" - " - technical\n" - "---\n\n" - "# Demo Skill\n", - encoding="utf-8", - ) - - parsed = parse_skill_metadata(skill_dir, source="builtin") - - assert parsed.skill_name == "demo_skill" - assert parsed.description == "Demo description" - assert parsed.tools == ["technical"] - - -def test_update_agent_skill_overrides(tmp_path): - manager = SkillsManager(project_root=tmp_path) - asset_dir = manager.get_agent_asset_dir("demo", "risk_manager") - asset_dir.mkdir(parents=True, exist_ok=True) - (asset_dir / "agent.yaml").write_text( - "enabled_skills:\n" - " - risk_review\n" - "disabled_skills:\n" - " - old_skill\n", - encoding="utf-8", - ) - - result = manager.update_agent_skill_overrides( - config_name="demo", - agent_id="risk_manager", - enable=["extra_guard"], - disable=["risk_review"], - ) - - assert result["enabled_skills"] == ["extra_guard"] - assert result["disabled_skills"] == ["old_skill", "risk_review"] - - -def test_skills_enable_disable_and_list(monkeypatch, tmp_path): - builtin_root = tmp_path / "backend" / "skills" / "builtin" - for name in ("risk_review", "extra_guard"): - skill_dir = builtin_root / name - skill_dir.mkdir(parents=True, exist_ok=True) - (skill_dir / "SKILL.md").write_text( - f"---\nname: {name}\ndescription: {name} desc\n---\n", - encoding="utf-8", - ) - - printed = [] - monkeypatch.setattr(cli, "get_project_root", lambda: tmp_path) - monkeypatch.setattr(cli.console, "print", lambda value: printed.append(value)) - - cli.skills_enable(agent_id="risk_manager", skill="extra_guard", config_name="demo") - cli.skills_disable(agent_id="risk_manager", skill="risk_review", config_name="demo") - cli.skills_list(config_name="demo", agent_id="risk_manager") - - text_dump = "\n".join(str(item) for item in printed) - assert "Enabled" in text_dump - assert "Disabled" in text_dump - assert any(getattr(item, "title", None) == "Skill Catalog" for item in printed) - - -def test_install_external_skill_for_agent(tmp_path): - manager = SkillsManager(project_root=tmp_path) - skill_dir = tmp_path / "downloaded" / "new_skill" - skill_dir.mkdir(parents=True, exist_ok=True) - (skill_dir / "SKILL.md").write_text( - "---\n" - "name: new_skill\n" - "description: external skill\n" - "---\n\n" - "# New Skill\n", - encoding="utf-8", - ) - - result = manager.install_external_skill_for_agent( - config_name="demo", - agent_id="risk_manager", - source=str(skill_dir), - activate=True, - ) - - assert result["skill_name"] == "new_skill" - target = manager.get_agent_local_root("demo", "risk_manager") / "new_skill" - assert target.exists() - - -def test_team_pipeline_active_analyst_updates(tmp_path): - project_root = tmp_path - ensure_team_pipeline_config( - project_root=project_root, - config_name="demo", - default_analysts=["fundamentals_analyst", "technical_analyst"], - ) - update_active_analysts( - project_root=project_root, - config_name="demo", - available_analysts=["fundamentals_analyst", "technical_analyst"], - remove=["technical_analyst"], - ) - config = load_team_pipeline_config(project_root, "demo") - assert config["discussion"]["active_analysts"] == ["fundamentals_analyst"] diff --git a/pyproject.toml b/pyproject.toml index 677a9db..c408039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ docker-sandbox = [ ] dev = [ "pytest>=8.3.3", + "pytest-asyncio>=0.24.0", "ruff>=0.6.9", "black>=25.0.0" ] @@ -70,9 +71,6 @@ Repository = "https://github.com/agentscope-ai/agentscope-samples/evotraders" Documentation = "https://github.com/agentscope-ai/agentscope-samples/evotraders/README.md" "Bug Tracker" = "https://github.com/agentscope-ai/agentscope-samples/issues" -[project.scripts] -evotraders = "backend.cli:app" - [tool.setuptools.packages.find] include = ["backend*", "shared*"]