diff --git a/app/cli/commands/portfolio.py b/app/cli/commands/portfolio.py index 8835f78..cc620fc 100644 --- a/app/cli/commands/portfolio.py +++ b/app/cli/commands/portfolio.py @@ -195,6 +195,7 @@ def update_portfolio( strategy_name: str | None = typer.Option(None, help="Strategy name."), strategy_params: str | None = typer.Option(None, help="Strategy params as JSON object."), asset_price: float | None = typer.Option(None, min=0.0, help="Current asset price."), + daemon_id: str | None = typer.Option(None, "--daemon-id", help="Assign portfolio to this daemon (managed_by_daemon_id)."), force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."), ) -> None: _validate_portfolio_name(name, ctx.obj.console) @@ -218,6 +219,7 @@ def update_portfolio( strategy_name=strategy_name, strategy_params=parsed_strategy_params, asset_price=asset_price, + managed_by_daemon_id=daemon_id, ), ) diff --git a/app/cli/core/utils.py b/app/cli/core/utils.py index a0d013b..5e2d698 100644 --- a/app/cli/core/utils.py +++ b/app/cli/core/utils.py @@ -39,30 +39,22 @@ def to_plain_data(value: Any) -> Any: return value -def print_portfolio_table(console: Console, portfolios: list[PortfolioListItem]) -> None: - table = Table(title="Portfolios", expand=True) - table.add_column("ID", style="cyan", no_wrap=True) - table.add_column("Broker") - table.add_column("Status", no_wrap=True) - table.add_column("Symbol", no_wrap=True) - table.add_column("Cash", justify="right") - table.add_column("Qty", justify="right") - table.add_column("Price", justify="right") - table.add_column("Buffer", justify="right") - table.add_column("Total", justify="right") - for item in portfolios: - table.add_row( - item.portfolio_id, - item.broker, - item.status, - item.symbol, - format_currency(item.cash_balance), - f"{item.asset_quantity:g}", - format_currency(item.price), - format_currency(item.profit_buffer), - format_currency(item.total_value), +def print_portfolio_list(console: Console, portfolios: list[PortfolioListItem]) -> None: + for i, item in enumerate(portfolios): + if i: + console.print() + tags_str = f" [{', '.join(item.tags)}]" if item.tags else "" + console.print(f"[bold]{item.name}[/bold] {item.symbol} [dim]{item.status}[/dim] {item.broker}{tags_str}") + console.print( + f" cash {format_currency(item.cash_balance)}" + f" qty {item.asset_quantity:g}" + f" price {format_currency(item.price)}" + f" buffer {format_currency(item.profit_buffer)}" + f" total {format_currency(item.total_value)}" ) - console.print(table) + console.print(f" [dim]{item.portfolio_id}[/dim]") + if item.managed_by_daemon_id: + console.print(f" daemon {item.managed_by_daemon_id}") def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) -> None: @@ -279,7 +271,7 @@ def render_output(console: Console, output_format: str, payload: Any, *, empty_m print_daemon_table(console, payload) return if all(isinstance(item, PortfolioListItem) for item in payload): - print_portfolio_table(console, payload) + print_portfolio_list(console, payload) return if all(isinstance(item, UserResponse) for item in payload): print_user_table(console, payload) diff --git a/app/repositories/portfolio_repository.py b/app/repositories/portfolio_repository.py index 02cd203..e49eba9 100644 --- a/app/repositories/portfolio_repository.py +++ b/app/repositories/portfolio_repository.py @@ -112,6 +112,8 @@ class PortfolioRepository(Protocol): def get(self, portfolio_id: str) -> Portfolio: ... + def get_portfolio_by_name_and_user(self, owner_user_id: str, name: str) -> Portfolio | None: ... + def get_or_create( self, portfolio_id: str, diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index 4e7f3fa..eae614f 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -743,6 +743,20 @@ class PostgresPortfolioRepository: self._attach_tags(cur, [portfolio]) return portfolio + def get_portfolio_by_name_and_user(self, owner_user_id: str, name: str) -> Portfolio | None: + self.ensure_schema() + with self._connect().cursor() as cur: + cur.execute( + _SELECT_PORTFOLIO + "WHERE owner_user_id = %s AND name = %s", + (owner_user_id, name), + ) + row = cur.fetchone() + if row is None: + return None + portfolio = self._row_to_portfolio(row) + self._attach_tags(cur, [portfolio]) + return portfolio + def get_or_create( self, portfolio_id: str, @@ -850,6 +864,7 @@ class PostgresPortfolioRepository: "asset_price": "asset_price", "active_strategy_id": "active_strategy_id", "avabile_strategies": "avabile_strategies", + "managed_by_daemon_id": "managed_by_daemon_id", } assignments: list[str] = [] params: list[object] = [] diff --git a/app/schemas/portfolio.py b/app/schemas/portfolio.py index d92414f..503681d 100644 --- a/app/schemas/portfolio.py +++ b/app/schemas/portfolio.py @@ -57,6 +57,7 @@ class PortfolioSummary(BaseModel): profit_buffer: float = 0.0 active_strategy_id: str | None = None avabile_strategies: list[str] | None = None + managed_by_daemon_id: str | None = None created_at: datetime | None = None updated_at: datetime | None = None last_trade_at: datetime | None = None @@ -104,6 +105,7 @@ class UpdatePortfolioRequest(BaseModel): active_strategy_id: str | None = None avabile_strategies: list[str] | None = None asset_price: float | None = Field(default=None, ge=0.0) + managed_by_daemon_id: str | None = None class BuyRequest(BaseModel): diff --git a/app/services/portfolio_service.py b/app/services/portfolio_service.py index e51fa76..2148fa2 100644 --- a/app/services/portfolio_service.py +++ b/app/services/portfolio_service.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import uuid from fastapi import Depends @@ -59,6 +60,7 @@ def _to_portfolio_summary(portfolio: Portfolio): profit_buffer=portfolio.profit_buffer, active_strategy_id=portfolio.active_strategy_id, avabile_strategies=portfolio.avabile_strategies, + managed_by_daemon_id=portfolio.managed_by_daemon_id, created_at=portfolio.created_at, updated_at=portfolio.updated_at, last_trade_at=portfolio.last_trade_at, @@ -214,7 +216,8 @@ class PortfolioService: active_strategy_id = strategy_set.id avabile_strategies = _merge_strategy_ids(active_strategy_id, payload.avabile_strategies) portfolio = self._repository.create_portfolio( - payload.name, + str(uuid.uuid4()), + name=payload.name, owner_user_id=owner_user_id, broker=payload.broker, status=payload.status, diff --git a/tests/test_portfolio_uuid.py b/tests/test_portfolio_uuid.py index e6477b6..f6e479e 100644 --- a/tests/test_portfolio_uuid.py +++ b/tests/test_portfolio_uuid.py @@ -72,8 +72,9 @@ class FakeRepo: def create_portfolio( self, - name: str, + portfolio_id: str, *, + name: str, owner_user_id: str, broker: str, status: str, @@ -90,7 +91,6 @@ class FakeRepo: for p in self._portfolios ): raise ValueError(f"이미 사용 중인 이름입니다: {name}") - portfolio_id = str(uuid.uuid4()) portfolio = Portfolio( portfolio_id=portfolio_id, broker=broker,