"""Strategy catalog CLI commands.""" from __future__ import annotations import typer from typing import Literal from app.cli.core.utils import execute_command from app.schemas.portfolio import CreateStrategySetRequest, UpdateStrategySetRequest from app.services.strategy_set_service import StrategySetService strategy_app = typer.Typer(help="Strategy catalog management commands.") def _with_service(ctx: typer.Context, callback): app_context = ctx.obj repository = app_context.repository_factory(app_context.settings) service = StrategySetService(repository) try: return execute_command(app_context.console, callback=lambda: callback(service)) finally: if callable(getattr(type(repository), "close", None)): repository.close() def _print_strategy_set_table(strategy_sets) -> None: if not strategy_sets: print("No strategies found.") return print(f"{'ID':<40} {'Name':<20} {'Status':<10} {'Revision':<8} {'Nick':<28} {'Updated At':<26}") print("-" * 138) for strategy in strategy_sets: updated_at = strategy.updated_at.isoformat() if strategy.updated_at else "N/A" print( f"{strategy.id:<40} {strategy.name:<20} {strategy.status:<10} " f"{strategy.revision:<8} {strategy.nick:<28} {updated_at:<26}" ) def _print_strategy(strategy, *, action: str) -> None: print(f"Strategy {action}:") print(f" ID: {strategy.id}") print(f" Name: {strategy.name}") print(f" Nick: {strategy.nick}") print(f" Status: {strategy.status}") print(f" Revision: {strategy.revision}") print(f" Created At: {strategy.created_at.isoformat() if strategy.created_at else 'N/A'}") print(f" Updated At: {strategy.updated_at.isoformat() if strategy.updated_at else 'N/A'}") @strategy_app.command("list") def list_strategy_sets(ctx: typer.Context) -> None: """List active and inactive catalog strategies.""" _with_service(ctx, lambda service: _print_strategy_set_table(service.list(include_inactive=True))) @strategy_app.command("create") def create_strategy_set( ctx: typer.Context, name: str = typer.Option(..., help="Strategy name (e.g., 'target_value')."), config: str = typer.Option(..., help="Strategy config as JSON string."), status: Literal["active", "inactive"] = typer.Option("active", help="Strategy status: active or inactive."), ) -> None: """Create a strategy or return the existing matching catalog entry.""" def cmd(service: StrategySetService) -> None: strategy = service.create(CreateStrategySetRequest(name=name, config=config, status=status)) _print_strategy(strategy, action="created/retrieved") _with_service(ctx, cmd) @strategy_app.command("update") def update_strategy_set( ctx: typer.Context, strategy_set_id: str = typer.Argument(..., help="Strategy ID to update."), name: str | None = typer.Option(None, help="New strategy name."), config: str | None = typer.Option(None, help="New strategy config as JSON string."), status: Literal["active", "inactive"] | None = typer.Option(None, help="New strategy status: active or inactive."), ) -> None: """Update catalog fields while preserving the strategy ID.""" def cmd(service: StrategySetService) -> None: if name is None and config is None and status is None: raise ValueError("at least one of --name, --config, or --status is required") strategy = service.update( strategy_set_id, UpdateStrategySetRequest(name=name, config=config, status=status), ) _print_strategy(strategy, action="updated") _with_service(ctx, cmd) @strategy_app.command("delete") def delete_strategy_set( ctx: typer.Context, strategy_set_id: str = typer.Argument(..., help="Strategy ID to delete."), force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation."), ) -> None: """Delete an unreferenced strategy.""" def cmd(service: StrategySetService) -> None: if not force and not typer.confirm(f"Delete strategy {strategy_set_id}?"): print("Cancelled.") return service.delete(strategy_set_id) print(f"Strategy {strategy_set_id} deleted.") _with_service(ctx, cmd)