#!/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 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 backend.agents.prompt_loader import PromptLoader from backend.agents.workspace_manager import WorkspaceManager app = typer.Typer( name="evotraders", help="EvoTraders: A self-evolving multi-agent trading system", add_completion=False, ) console = Console() _prompt_loader = PromptLoader() 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 """ # logs_dir = get_project_root() / "logs" logs_dir = get_project_root() base_data_dir = logs_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) @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", ), ) @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) # 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) 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()