feat(req-002): implement portfolio tagging and tag filters

This commit is contained in:
2026-04-14 01:06:10 +09:00
parent 2dab17cbb1
commit a776ea3b1d
20 changed files with 1033 additions and 32 deletions
+57 -2
View File
@@ -14,6 +14,7 @@ from app.schemas.portfolio import (
CommentUpdateRequest,
CreatePortfolioRequest,
InvestRequest,
PortfolioTagMutationRequest,
ResetPortfolioRequest,
SellRequest,
TransferRequest,
@@ -24,7 +25,9 @@ from app.services.portfolio_service import PortfolioService
portfolio_app = typer.Typer(help="Portfolio lifecycle and trade commands.")
comment_app = typer.Typer(help="Portfolio comment history commands.")
tag_app = typer.Typer(help="Portfolio tag commands.")
portfolio_app.add_typer(comment_app, name="comment")
portfolio_app.add_typer(tag_app, name="tag")
def _parse_strategy_params(value: str) -> dict[str, Any]:
@@ -61,8 +64,20 @@ def list_portfolios(
ctx: typer.Context,
broker: str | None = typer.Option(None, help="Filter by broker."),
status: str | None = typer.Option(None, help="Filter by status."),
include_tags: list[str] | None = typer.Option(None, "--include-tag", help="Include tag filter. Repeatable."),
exclude_tags: list[str] | None = typer.Option(None, "--exclude-tag", help="Exclude tag filter. Repeatable."),
match_mode: str | None = typer.Option(None, "--match-mode", help="AND or OR for include tags."),
) -> None:
detail = _with_service(ctx, lambda service, app_context: service.list_portfolios(broker=broker, status=status))
detail = _with_service(
ctx,
lambda service, app_context: service.list_portfolios(
broker=broker,
status=status,
include_tags=include_tags,
exclude_tags=exclude_tags,
match_mode=match_mode,
),
)
render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No portfolios found.")
@@ -333,4 +348,44 @@ def comment_delete(
if ctx.obj.output_format == "json":
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": comment_id, "portfolio_id": portfolio_id})
else:
ctx.obj.console.print(f"Deleted comment: {comment_id}")
ctx.obj.console.print(f"Deleted comment: {comment_id}")
@tag_app.command("add")
def tag_add(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
tag: str = typer.Option(..., "--tag", help="Tag value."),
) -> None:
detail = _with_service(
ctx,
lambda service, app_context: service.add_tag(portfolio_id, PortfolioTagMutationRequest(tag=tag)),
)
render_output(ctx.obj.console, ctx.obj.output_format, detail)
@tag_app.command("list")
def tag_list(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
) -> None:
detail = _with_service(ctx, lambda service, app_context: service.list_tags(portfolio_id))
render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No tags found.")
@tag_app.command("remove")
def tag_remove(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
tag: str = typer.Option(..., "--tag", help="Tag value."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Remove tag {tag!r} from portfolio {portfolio_id}?")
_with_service(
ctx,
lambda service, app_context: service.remove_tag(portfolio_id, PortfolioTagMutationRequest(tag=tag)),
)
if ctx.obj.output_format == "json":
render_output(ctx.obj.console, ctx.obj.output_format, {"portfolio_id": portfolio_id, "deleted": tag.strip()})
else:
ctx.obj.console.print(f"Removed tag: {tag.strip()}")