#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ EvoTraders CLI - Command-line interface for the EvoTraders trading system. This module provides easy-to-use commands for running backtest, live trading, and frontend development server. """ # flake8: noqa: E501 # pylint: disable=R0912, R0915 import os import shutil import subprocess import sys from datetime import datetime, timedelta from pathlib import Path from typing import Optional from zoneinfo import ZoneInfo import typer 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.prompt_loader import PromptLoader from backend.agents.workspace_manager import WorkspaceManager 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 app = typer.Typer( name="evotraders", help="EvoTraders: A self-evolving multi-agent trading system", add_completion=False, ) ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.") app.add_typer(ingest_app, name="ingest") console = Console() _prompt_loader = PromptLoader() load_dotenv() def get_project_root() -> Path: """Get the project root directory.""" # Assuming cli.py is in backend/ return Path(__file__).parent.parent 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: pass # 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: console.print( "[yellow] Data updater check failed, skipping update[/yellow]\n", ) def initialize_workspace(config_name: str) -> Path: """Create run-scoped workspace 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 _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="Configuration name for the workspace", ), ): """Initialize run-scoped BOOTSTRAP and agent prompt asset files.""" run_dir = initialize_workspace(config_name) console.print( Panel.fit( f"[bold green]Workspace 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) @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( "backtest", "--config-name", "-c", help="Configuration name for this backtest run", ), host: str = typer.Option( "0.0.0.0", "--host", help="WebSocket server host", ), port: int = typer.Option( 8765, "--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)", ), ): """ Run backtest mode with historical data. Example: 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]EvoTraders Backtest Mode[/bold cyan]", border_style="cyan", ), ) # 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 # Handle historical data cleanup handle_history_cleanup(config_name, auto_clean=clean) # 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") 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) # 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( mock: bool = typer.Option( False, "--mock", help="Use mock mode with simulated prices (for testing)", ), config_name: str = typer.Option( "live", "--config-name", "-c", help="Configuration name for this live run", ), host: str = typer.Option( "0.0.0.0", "--host", help="WebSocket server host", ), port: int = typer.Option( 8765, "--port", "-p", help="WebSocket server port", ), trigger_time: str = typer.Option( "now", "--trigger-time", "-t", help="Trigger time in LOCAL timezone (HH:MM), or 'now' to run immediately", ), 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)", ), ): """ Run live trading mode with real-time data. Example: evotraders live # Run immediately (default) evotraders live --mock # Mock mode evotraders live -t 22:30 # Run at 22:30 local time daily evotraders live --trigger-time now # Run immediately evotraders live --clean # Clear historical data before starting """ mode_name = "MOCK" if mock else "LIVE" console.print( Panel.fit( f"[bold cyan]EvoTraders {mode_name} Mode[/bold cyan]", border_style="cyan", ), ) # Check for required API key in live mode if not mock: 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) # 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 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')}", ) if 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]") if mock: console.print(" Mode: [yellow]MOCK[/yellow] (Simulated prices)") else: console.print( " 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( 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 (if not mock mode) if not mock: 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, ) else: console.print( "\n[dim]Mock mode enabled - skipping data update[/dim]\n", ) # Build command using backend.main cmd = [ sys.executable, "-u", "-m", "backend.main", "--mode", "live", "--config-name", config_name, "--host", host, "--port", str(port), "--poll-interval", str(poll_interval), "--trigger-time", nyse_trigger_time, ] if mock: cmd.append("--mock") 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( 8765, "--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]EvoTraders 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 version(): """Show the version of EvoTraders.""" console.print( "\n[bold cyan]EvoTraders[/bold cyan] version [green]0.1.0[/green]\n", ) @app.callback() def main(): """ EvoTraders: A self-evolving multi-agent trading system Use 'evotraders --help' to see available commands. """ if __name__ == "__main__": app()