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:
2026-07-04 11:05:51 +09:00
co-authored by Claude Fable 5
parent b2261c4428
commit 8a07742049
4 changed files with 50 additions and 2 deletions
+5
View File
@@ -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,
)
+4 -1
View File
@@ -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
+36 -1
View File
@@ -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
+5
View File
@@ -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):