Adds 4 additive fields to GET /v1/portfolios (and single-portfolio) response:
- active_strategy_id (str|null): portfolios.active_strategy_id raw value
- strategy_name (str|null): strategy_sets.name via LEFT JOIN
- strategy_params (dict): strategy_sets.config JSON parsed; {} on missing/invalid
- markets (list[str]): assets.markets; [] on missing/empty
_SELECT_PORTFOLIO extended with LEFT JOIN strategy_sets; _row_to_portfolio
parses new columns (row[19-21]) with JSON error isolation (WARNING log, no 500).
_portfolio_to_item maps all 4 new fields. Existing 16 fields unchanged.
N+1 prevented: single SQL JOIN for both list and single-item paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
190 lines
4.4 KiB
Python
190 lines
4.4 KiB
Python
"""Portfolio domain models."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
|
|
class MarketCode(str, Enum):
|
|
KRX = "KRX"
|
|
NXT = "NXT"
|
|
SOR = "SOR"
|
|
NASDAQ = "NASDAQ"
|
|
NYSE = "NYSE"
|
|
UPBIT = "UPBIT"
|
|
BITHUMB = "BITHUMB"
|
|
|
|
|
|
@dataclass
|
|
class Tag:
|
|
tag_id: str
|
|
name: str
|
|
created_by_user_id: str
|
|
is_admin_tag: bool = False
|
|
is_active: bool = True
|
|
created_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class Asset:
|
|
asset_id: str
|
|
symbol: str
|
|
markets: list[str]
|
|
display_name: str | None = None
|
|
status: str = "active"
|
|
tag_ids: list[str] = field(default_factory=list)
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class StrategySet:
|
|
id: str
|
|
name: str
|
|
config: str
|
|
nick: str
|
|
created_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class User:
|
|
id: str
|
|
username: str
|
|
email: str | None = None
|
|
permission: str = "read"
|
|
is_active: bool = True
|
|
password_hash: str | None = None
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
last_login_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class Daemon:
|
|
id: str
|
|
owner_user_id: str
|
|
daemon_slug: str
|
|
display_name: str | None = None
|
|
is_active: bool = True
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class AuthToken:
|
|
id: str
|
|
user_id: str
|
|
name: str
|
|
token_prefix: str
|
|
permission: str
|
|
token_type: str
|
|
expires_at: datetime | None = None
|
|
last_used_at: datetime | None = None
|
|
revoked_at: datetime | None = None
|
|
created_at: datetime | None = None
|
|
runtime_id: str | None = None
|
|
daemon_id: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class TokenPrincipal:
|
|
token_id: str
|
|
user_id: str
|
|
permission: str
|
|
token_type: str
|
|
runtime_id: str | None = None
|
|
daemon_id: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class Portfolio:
|
|
portfolio_id: str
|
|
broker: str
|
|
status: str
|
|
name: str
|
|
symbol: str
|
|
asset_id: str | None = None
|
|
display_name: str | None = None
|
|
tags: list[str] | None = None
|
|
tag_ids: list[str] | None = None
|
|
owner_user_id: str | None = None
|
|
cash_balance: float = 0.0
|
|
asset_quantity: float = 0.0
|
|
price: float = 0.0
|
|
profit_buffer: float = 0.0
|
|
active_strategy_id: str | None = None
|
|
strategy_name: str | None = None # REQ-055: strategy_sets.name JOIN 결과
|
|
strategy_params: dict[str, Any] = field(default_factory=dict) # REQ-055: strategy_sets.config 파싱 결과
|
|
markets: list[str] = field(default_factory=list) # REQ-055: assets.markets JOIN 결과
|
|
avabile_strategies: list[str] | None = None
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
last_trade_at: datetime | None = None
|
|
managed_by_daemon_id: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class LedgerEntry:
|
|
portfolio_id: str
|
|
quantity: float
|
|
price: float
|
|
ledger_entry_id: int = 0
|
|
order_id: str | None = None
|
|
execution_id: str | None = None
|
|
side: str | None = None
|
|
fee_amount: float = 0.0
|
|
tax_amount: float = 0.0
|
|
executed_at: datetime | None = None
|
|
recorded_at: datetime | None = None
|
|
cash_balance_after: float = 0.0
|
|
asset_quantity_after: float = 0.0
|
|
total_value_after: float = 0.0
|
|
profit_buffer_after: float = 0.0
|
|
strategy_set_id: str = "legacy"
|
|
note: str = ""
|
|
action: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.side is None and self.action:
|
|
self.side = str(self.action).lower()
|
|
if self.action is None and self.side:
|
|
self.action = str(self.side).upper()
|
|
if self.executed_at is None:
|
|
self.executed_at = self.recorded_at
|
|
|
|
|
|
@dataclass
|
|
class PortfolioComment:
|
|
id: str
|
|
portfolio_id: str
|
|
content: str
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
|
|
|
|
@dataclass
|
|
class PortfolioTag:
|
|
portfolio_id: str
|
|
tag: str
|
|
created_at: datetime | None = None
|
|
|
|
|
|
@runtime_checkable
|
|
class LiveAssetSource(Protocol):
|
|
def get_asset_quantity(self, symbol: str) -> float: ...
|
|
|
|
|
|
@dataclass
|
|
class BrokerCredential:
|
|
user_id: str
|
|
broker: str
|
|
account_ref: str
|
|
encrypted_payload: str
|
|
credential_version: int = 1
|
|
last_rotated_at: datetime | None = None
|
|
daemon_id: str | None = None
|
|
|