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
+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