feat: add portfolio daemon assignment and improve list rendering
- Add --daemon-id option to portfolio update command for daemon assignment - Update managed_by_daemon_id field in portfolio updates - Refactor print_portfolio_table() to print_portfolio_list() for better readability - Display daemon_id in portfolio list output when assigned - Show portfolio tags and managed daemon information in CLI list view - Add daemon_id support to portfolio repository and service layers - Update schemas to include managed_by_daemon_id field in responses
This commit is contained in:
@@ -195,6 +195,7 @@ def update_portfolio(
|
|||||||
strategy_name: str | None = typer.Option(None, help="Strategy name."),
|
strategy_name: str | None = typer.Option(None, help="Strategy name."),
|
||||||
strategy_params: str | None = typer.Option(None, help="Strategy params as JSON object."),
|
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."),
|
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."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||||
) -> None:
|
) -> None:
|
||||||
_validate_portfolio_name(name, ctx.obj.console)
|
_validate_portfolio_name(name, ctx.obj.console)
|
||||||
@@ -218,6 +219,7 @@ def update_portfolio(
|
|||||||
strategy_name=strategy_name,
|
strategy_name=strategy_name,
|
||||||
strategy_params=parsed_strategy_params,
|
strategy_params=parsed_strategy_params,
|
||||||
asset_price=asset_price,
|
asset_price=asset_price,
|
||||||
|
managed_by_daemon_id=daemon_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+16
-24
@@ -39,30 +39,22 @@ def to_plain_data(value: Any) -> Any:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def print_portfolio_table(console: Console, portfolios: list[PortfolioListItem]) -> None:
|
def print_portfolio_list(console: Console, portfolios: list[PortfolioListItem]) -> None:
|
||||||
table = Table(title="Portfolios", expand=True)
|
for i, item in enumerate(portfolios):
|
||||||
table.add_column("ID", style="cyan", no_wrap=True)
|
if i:
|
||||||
table.add_column("Broker")
|
console.print()
|
||||||
table.add_column("Status", no_wrap=True)
|
tags_str = f" [{', '.join(item.tags)}]" if item.tags else ""
|
||||||
table.add_column("Symbol", no_wrap=True)
|
console.print(f"[bold]{item.name}[/bold] {item.symbol} [dim]{item.status}[/dim] {item.broker}{tags_str}")
|
||||||
table.add_column("Cash", justify="right")
|
console.print(
|
||||||
table.add_column("Qty", justify="right")
|
f" cash {format_currency(item.cash_balance)}"
|
||||||
table.add_column("Price", justify="right")
|
f" qty {item.asset_quantity:g}"
|
||||||
table.add_column("Buffer", justify="right")
|
f" price {format_currency(item.price)}"
|
||||||
table.add_column("Total", justify="right")
|
f" buffer {format_currency(item.profit_buffer)}"
|
||||||
for item in portfolios:
|
f" total {format_currency(item.total_value)}"
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
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:
|
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)
|
print_daemon_table(console, payload)
|
||||||
return
|
return
|
||||||
if all(isinstance(item, PortfolioListItem) for item in payload):
|
if all(isinstance(item, PortfolioListItem) for item in payload):
|
||||||
print_portfolio_table(console, payload)
|
print_portfolio_list(console, payload)
|
||||||
return
|
return
|
||||||
if all(isinstance(item, UserResponse) for item in payload):
|
if all(isinstance(item, UserResponse) for item in payload):
|
||||||
print_user_table(console, payload)
|
print_user_table(console, payload)
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class PortfolioRepository(Protocol):
|
|||||||
|
|
||||||
def get(self, portfolio_id: str) -> Portfolio: ...
|
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(
|
def get_or_create(
|
||||||
self,
|
self,
|
||||||
portfolio_id: str,
|
portfolio_id: str,
|
||||||
|
|||||||
@@ -743,6 +743,20 @@ class PostgresPortfolioRepository:
|
|||||||
self._attach_tags(cur, [portfolio])
|
self._attach_tags(cur, [portfolio])
|
||||||
return 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(
|
def get_or_create(
|
||||||
self,
|
self,
|
||||||
portfolio_id: str,
|
portfolio_id: str,
|
||||||
@@ -850,6 +864,7 @@ class PostgresPortfolioRepository:
|
|||||||
"asset_price": "asset_price",
|
"asset_price": "asset_price",
|
||||||
"active_strategy_id": "active_strategy_id",
|
"active_strategy_id": "active_strategy_id",
|
||||||
"avabile_strategies": "avabile_strategies",
|
"avabile_strategies": "avabile_strategies",
|
||||||
|
"managed_by_daemon_id": "managed_by_daemon_id",
|
||||||
}
|
}
|
||||||
assignments: list[str] = []
|
assignments: list[str] = []
|
||||||
params: list[object] = []
|
params: list[object] = []
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class PortfolioSummary(BaseModel):
|
|||||||
profit_buffer: float = 0.0
|
profit_buffer: float = 0.0
|
||||||
active_strategy_id: str | None = None
|
active_strategy_id: str | None = None
|
||||||
avabile_strategies: list[str] | None = None
|
avabile_strategies: list[str] | None = None
|
||||||
|
managed_by_daemon_id: str | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
last_trade_at: datetime | None = None
|
last_trade_at: datetime | None = None
|
||||||
@@ -104,6 +105,7 @@ class UpdatePortfolioRequest(BaseModel):
|
|||||||
active_strategy_id: str | None = None
|
active_strategy_id: str | None = None
|
||||||
avabile_strategies: list[str] | None = None
|
avabile_strategies: list[str] | None = None
|
||||||
asset_price: float | None = Field(default=None, ge=0.0)
|
asset_price: float | None = Field(default=None, ge=0.0)
|
||||||
|
managed_by_daemon_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class BuyRequest(BaseModel):
|
class BuyRequest(BaseModel):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ def _to_portfolio_summary(portfolio: Portfolio):
|
|||||||
profit_buffer=portfolio.profit_buffer,
|
profit_buffer=portfolio.profit_buffer,
|
||||||
active_strategy_id=portfolio.active_strategy_id,
|
active_strategy_id=portfolio.active_strategy_id,
|
||||||
avabile_strategies=portfolio.avabile_strategies,
|
avabile_strategies=portfolio.avabile_strategies,
|
||||||
|
managed_by_daemon_id=portfolio.managed_by_daemon_id,
|
||||||
created_at=portfolio.created_at,
|
created_at=portfolio.created_at,
|
||||||
updated_at=portfolio.updated_at,
|
updated_at=portfolio.updated_at,
|
||||||
last_trade_at=portfolio.last_trade_at,
|
last_trade_at=portfolio.last_trade_at,
|
||||||
@@ -214,7 +216,8 @@ class PortfolioService:
|
|||||||
active_strategy_id = strategy_set.id
|
active_strategy_id = strategy_set.id
|
||||||
avabile_strategies = _merge_strategy_ids(active_strategy_id, payload.avabile_strategies)
|
avabile_strategies = _merge_strategy_ids(active_strategy_id, payload.avabile_strategies)
|
||||||
portfolio = self._repository.create_portfolio(
|
portfolio = self._repository.create_portfolio(
|
||||||
payload.name,
|
str(uuid.uuid4()),
|
||||||
|
name=payload.name,
|
||||||
owner_user_id=owner_user_id,
|
owner_user_id=owner_user_id,
|
||||||
broker=payload.broker,
|
broker=payload.broker,
|
||||||
status=payload.status,
|
status=payload.status,
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ class FakeRepo:
|
|||||||
|
|
||||||
def create_portfolio(
|
def create_portfolio(
|
||||||
self,
|
self,
|
||||||
name: str,
|
portfolio_id: str,
|
||||||
*,
|
*,
|
||||||
|
name: str,
|
||||||
owner_user_id: str,
|
owner_user_id: str,
|
||||||
broker: str,
|
broker: str,
|
||||||
status: str,
|
status: str,
|
||||||
@@ -90,7 +91,6 @@ class FakeRepo:
|
|||||||
for p in self._portfolios
|
for p in self._portfolios
|
||||||
):
|
):
|
||||||
raise ValueError(f"이미 사용 중인 이름입니다: {name}")
|
raise ValueError(f"이미 사용 중인 이름입니다: {name}")
|
||||||
portfolio_id = str(uuid.uuid4())
|
|
||||||
portfolio = Portfolio(
|
portfolio = Portfolio(
|
||||||
portfolio_id=portfolio_id,
|
portfolio_id=portfolio_id,
|
||||||
broker=broker,
|
broker=broker,
|
||||||
|
|||||||
Reference in New Issue
Block a user