feat(req-055): expose strategy & market meta fields in PortfolioItem response (additive)
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
This commit is contained in:
@@ -81,6 +81,11 @@ def _portfolio_to_item(portfolio: Portfolio) -> PortfolioItem:
|
||||
managed_by_daemon_id=portfolio.managed_by_daemon_id,
|
||||
created_at=portfolio.created_at,
|
||||
updated_at=portfolio.updated_at,
|
||||
# REQ-055: 전략·시장 메타데이터 additive 필드
|
||||
active_strategy_id=portfolio.active_strategy_id,
|
||||
strategy_name=portfolio.strategy_name,
|
||||
strategy_params=portfolio.strategy_params,
|
||||
markets=portfolio.markets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Protocol, runtime_checkable
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
|
||||
class MarketCode(str, Enum):
|
||||
@@ -116,6 +116,9 @@ class Portfolio:
|
||||
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
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import psycopg
|
||||
from psycopg import sql
|
||||
|
||||
@@ -64,9 +68,13 @@ _SELECT_PORTFOLIO = """
|
||||
p.managed_by_daemon_id,
|
||||
p.asset_id,
|
||||
COALESCE(p.tag_ids, '{}'::uuid[]),
|
||||
a.display_name
|
||||
a.display_name,
|
||||
ss.name,
|
||||
ss.config,
|
||||
a.markets
|
||||
FROM portfolios p
|
||||
JOIN assets a ON p.asset_id = a.asset_id
|
||||
LEFT JOIN strategy_sets ss ON p.active_strategy_id = ss.id
|
||||
"""
|
||||
|
||||
_SELECT_TAG = """
|
||||
@@ -495,6 +503,30 @@ class PostgresPortfolioRepository:
|
||||
else:
|
||||
avabile_strategies = [str(raw_available)]
|
||||
|
||||
# REQ-055: strategy_name (row[19] = ss.name from LEFT JOIN strategy_sets)
|
||||
strategy_name: str | None = row[19] if len(row) > 19 else None
|
||||
|
||||
# REQ-055: strategy_params — JSON 파싱 (파싱 불가 시 {} + WARNING, 500 금지)
|
||||
_config_raw = row[20] if len(row) > 20 else None
|
||||
if not _config_raw:
|
||||
strategy_params: dict[str, Any] = {}
|
||||
else:
|
||||
try:
|
||||
_parsed = json.loads(_config_raw)
|
||||
strategy_params = _parsed if isinstance(_parsed, dict) else {}
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logger.warning(
|
||||
"strategy_sets.config 파싱 실패 — portfolio_id=%s, active_strategy_id=%s, config=%r",
|
||||
row[0],
|
||||
row[9] if len(row) > 9 else None,
|
||||
_config_raw[:200] if isinstance(_config_raw, str) else _config_raw,
|
||||
)
|
||||
strategy_params = {}
|
||||
|
||||
# REQ-055: markets (row[21] = a.markets from assets JOIN)
|
||||
_markets_raw = row[21] if len(row) > 21 else None
|
||||
markets: list[str] = list(_markets_raw) if _markets_raw else []
|
||||
|
||||
return Portfolio(
|
||||
portfolio_id=row[0],
|
||||
broker=row[1] or "kis",
|
||||
@@ -516,6 +548,9 @@ class PostgresPortfolioRepository:
|
||||
asset_id=str(row[16]) if len(row) > 16 and row[16] is not None else None,
|
||||
tag_ids=[str(t) for t in row[17]] if len(row) > 17 and row[17] is not None else [],
|
||||
display_name=row[18] if len(row) > 18 else None,
|
||||
strategy_name=strategy_name,
|
||||
strategy_params=strategy_params,
|
||||
markets=markets,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -181,6 +181,11 @@ class PortfolioItem(BaseModel):
|
||||
managed_by_daemon_id: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
# REQ-055: 전략·시장 메타데이터 additive 필드
|
||||
active_strategy_id: str | None = None
|
||||
strategy_name: str | None = None
|
||||
strategy_params: dict[str, Any] = Field(default_factory=dict)
|
||||
markets: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PortfolioListResponse(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user