- Add acceptance criteria documentation (AC-0013 through AC-0023) - Add Line-0003 discovery documentation - Implement Strategy Catalog v1 with new database migration - Update portfolio API endpoints and schemas - Refactor strategy and portfolio services - Add strategy management CLI commands - Update domain models with strategy catalog support - Add comprehensive tests for v1 strategies - Update design documentation (data model, interfaces, runtime flows)
114 lines
4.2 KiB
Python
114 lines
4.2 KiB
Python
"""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)
|