Files
portfolio-hub/app/domain/models.py
T
genichinandClaude Sonnet 4.6 51b7cbee35 feat(bat-020): daemon-scoped 브로커 인증정보 (REQ-046~051)
- REQ-046: BrokerCredential.daemon_id 필드 추가, repository 시그니처 갱신
- REQ-047: alembic revision Step1(daemon_id NULL 컬럼 추가), Step2(PK 전환, dev/test DB 한정)
- REQ-048: backfill-daemon-id CLI subcommand (--dry-run/--force/--broker 지원)
- REQ-049: GET /v1/user_info daemon-scoped 단일 broker 전달 (brokers→broker, daemon_id 없으면 403)
- REQ-050: credential set/list/remove --daemon-id 옵션 추가 (daemon_slug 해석)
- REQ-051: 보안 격리 - service-token 전용, web 토큰 차단, 로그 app_secret 비노출, graceful degrade

비가역 결재 게이트: REQ-047 Step2(PK 전환)는 사장님 결재 후 실DB 적용 예정(이 batch는 revision 코드만)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-28 00:45:46 +09:00

187 lines
4.1 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 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
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