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,
|
managed_by_daemon_id=portfolio.managed_by_daemon_id,
|
||||||
created_at=portfolio.created_at,
|
created_at=portfolio.created_at,
|
||||||
updated_at=portfolio.updated_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 dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Any, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
class MarketCode(str, Enum):
|
class MarketCode(str, Enum):
|
||||||
@@ -116,6 +116,9 @@ class Portfolio:
|
|||||||
price: float = 0.0
|
price: float = 0.0
|
||||||
profit_buffer: float = 0.0
|
profit_buffer: float = 0.0
|
||||||
active_strategy_id: str | None = None
|
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
|
avabile_strategies: list[str] | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import psycopg
|
import psycopg
|
||||||
from psycopg import sql
|
from psycopg import sql
|
||||||
|
|
||||||
@@ -64,9 +68,13 @@ _SELECT_PORTFOLIO = """
|
|||||||
p.managed_by_daemon_id,
|
p.managed_by_daemon_id,
|
||||||
p.asset_id,
|
p.asset_id,
|
||||||
COALESCE(p.tag_ids, '{}'::uuid[]),
|
COALESCE(p.tag_ids, '{}'::uuid[]),
|
||||||
a.display_name
|
a.display_name,
|
||||||
|
ss.name,
|
||||||
|
ss.config,
|
||||||
|
a.markets
|
||||||
FROM portfolios p
|
FROM portfolios p
|
||||||
JOIN assets a ON p.asset_id = a.asset_id
|
JOIN assets a ON p.asset_id = a.asset_id
|
||||||
|
LEFT JOIN strategy_sets ss ON p.active_strategy_id = ss.id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_SELECT_TAG = """
|
_SELECT_TAG = """
|
||||||
@@ -495,6 +503,30 @@ class PostgresPortfolioRepository:
|
|||||||
else:
|
else:
|
||||||
avabile_strategies = [str(raw_available)]
|
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(
|
return Portfolio(
|
||||||
portfolio_id=row[0],
|
portfolio_id=row[0],
|
||||||
broker=row[1] or "kis",
|
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,
|
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 [],
|
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,
|
display_name=row[18] if len(row) > 18 else None,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
strategy_params=strategy_params,
|
||||||
|
markets=markets,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ class PortfolioItem(BaseModel):
|
|||||||
managed_by_daemon_id: str | None = None
|
managed_by_daemon_id: str | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_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):
|
class PortfolioListResponse(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user