11 KiB
REQ-055: GET /v1/portfolios 응답에 전략·시장 메타데이터 노출 (additive)
- Status: Implemented
- Type: Interface
- Priority: High
- Owner: Justin Ko
Intent
GET /v1/portfolios(및 단건GET /v1/portfolios/{id}존재 시 동일)의PortfolioItem응답에 전략 셋 바인딩 정보(active_strategy_id,strategy_name,strategy_params)와 거래 가능 시장 목록(markets)을 additive 필드로 노출한다.- trex-core
hub_read.get_portfolios()가 응답 dict에서active_strategy_id·strategy_params를 파싱 준비(hub_read.py:168-169) 중이나,PortfolioItem스키마에 해당 필드가 미노출되어 항상None/KeyError위험 상태다. 본 REQ가 이 갭을 닫는다. - tvr-v1(dcy-001) 전략 설정의 단일 진실원천이
portfolio-hub.strategy_sets로 확정(사장님 확정)된 데 따라, 소비자가 전략 이름·파라미터를 별도 조회 없이 획득할 수 있도록 한다. - 기존 필드·정렬·인증·페이지네이션은 불변(하위호환 additive 확장).
Requirement
-
additive 필드 4개를
PortfolioItem스키마와GET /v1/portfolios응답(및 단건 조회 엔드포인트 응답)에 추가한다.필드 타입 설명 active_strategy_idstr | nullportfolios.active_strategy_id참조 ID. 미바인딩 시null.strategy_namestr | nullstrategy_sets.name(JOIN 해석). 미바인딩 또는 strategy_set row 없으면null.strategy_paramsdictstrategy_sets.config(JSON 문자열) 파싱 결과. 미바인딩·빈 config·파싱 불가 시{}. 파싱 불가 시 서버 WARNING 로그 1건 출력(500 금지).marketslist[str]portfolios → assets.markets조인 결과. asset 미연결이거나 markets 빈 배열이면[]. -
N+1 금지: strategy_sets는 list 조회 시 일괄 조인(LEFT JOIN strategy_sets on active_strategy_id) 또는 일괄 IN-fetch 방식으로 처리한다. 단건 조회도 동일 접근 적용.
-
파싱 에러 격리:
strategy_sets.config가 유효하지 않은 JSON이면strategy_params = {}를 반환하고, 서버 측에서 WARNING 수준 로그를 출력한다. HTTP 응답은 200을 유지한다(500 금지). -
기존 필드 불변:
portfolio_id,asset_id,symbol,display_name,owner_user_id,broker,status,name,cash_balance,asset_quantity,price,profit_buffer,tag_ids,managed_by_daemon_id,created_at,updated_at필드는 변경 없이 유지된다. -
권한 모델 불변: 토큰 분기(service/session/admin) 및 필터(
?market,?include_disabled,?user_id) 동작은 REQ-028 계약을 그대로 따른다.
Acceptance Criteria
-
Given
active_strategy_id가 바인딩된 포트폴리오, WhenGET /v1/portfolios, Then 해당 item에active_strategy_id(비null),strategy_name(strategy_sets.name 값),strategy_params(config 파싱 dict) 세 필드가 정상 값으로 반환된다. -
Given
active_strategy_id가null인 포트폴리오, WhenGET /v1/portfolios, Thenactive_strategy_id=null,strategy_name=null,strategy_params={}로 반환된다. -
Given
active_strategy_id가 존재하나 해당 strategy_set row가 삭제된 포트폴리오(dangling FK), WhenGET /v1/portfolios, Thenactive_strategy_id는 원래 값,strategy_name=null,strategy_params={}로 반환되며 500이 발생하지 않는다. -
Given strategy_sets.config가 유효하지 않은 JSON 문자열인 포트폴리오, When
GET /v1/portfolios, Thenstrategy_params={}로 반환되고, 서버 로그에 WARNING 1건이 기록되며 HTTP 응답은 200이다. -
Given
assets.markets = ['KRX', 'NXT']인 asset에 연결된 포트폴리오, WhenGET /v1/portfolios, Thenmarkets=['KRX', 'NXT']로 반환된다. -
Given
assets.markets = [](빈 배열)인 asset에 연결된 포트폴리오, WhenGET /v1/portfolios, Thenmarkets=[]로 반환된다. -
Given 임의 유효 토큰으로
GET /v1/portfolios, When 응답의 기존 필드(portfolio_id,broker,status,name,symbol,display_name,owner_user_id,cash_balance,asset_quantity,price,profit_buffer,tag_ids,managed_by_daemon_id,created_at,updated_at)를 확인, Then 기존 값이 변경 없이 그대로 반환된다(회귀 없음). -
Given service token(daemon-bound) 또는 session/admin token, When
GET /v1/portfolios, Then REQ-028의 토큰 분기 필터(daemon_id·owner_user_id·admin?user_id)가 4개 신규 필드 추가 이후에도 동일하게 동작한다. -
Given strategy_name="tvr_v1", config=
{"target_weight": 0.5, "rebalance_band_pct": 0.05, "min_trade_krw": 10000}인 strategy_set이 바인딩된 포트폴리오, WhenGET /v1/portfolios, Thenstrategy_params응답 dict가 원본 JSON과 동일 키·값을 갖는다. -
Given 복수의 포트폴리오를 가진 사용자(일부는 strategy 바인딩, 일부는 미바인딩), When
GET /v1/portfolios, Then 각 포트폴리오의active_strategy_id·strategy_name·strategy_params·markets가 개별적으로 정확하게 반환된다(리스트 조회 정합성).
Impacted Area
-
Docs:
docs/interface-contract.md—GET /v1/portfoliosPortfolioItem응답 필드 정의에 4개 신규 필드 추가 명세
-
Modules:
app/schemas/v1.py—PortfolioItem에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)추가app/domain/models.py—Portfoliodataclass에strategy_name: str | None = None,strategy_params: dict[str, Any] = Field(default_factory=dict),markets: list[str]필드 추가 (strategy_sets JOIN 결과 수용)app/repositories/postgres.py—_SELECT_PORTFOLIO쿼리에LEFT JOIN strategy_sets ss ON p.active_strategy_id = ss.id추가 및ss.name,ss.config,a.markets컬럼 포함._row_to_portfolio파싱 보강(config JSON 파싱 + 오류 격리). 단건 get/list 경로 모두 반영.app/api/v1_portfolios.py—_portfolio_to_item매핑에 4개 신규 필드 포함
-
Tests:
tests/test_v1_portfolios_list.py— AC-1~10 검증 케이스 추가(전략 바인딩/미바인딩/dangling/파싱불가/markets/기존필드 회귀)
Notes
-
Discovery 참조:
trex-tvr-v1 docs/discovery/dcy-001_20260702_target-value-rebalancing.md(confirmed): 전략 설정 단일 진실원천 =portfolio-hub.strategy_sets(name="tvr_v1", config=JSON 문자열). 사장님 확정.trex-tvr-v1 docs/discovery/dcy-002v3 FR-8:assets.markets를 거래 가능 시장 목록으로 정의(["KRX","NXT"];[]=KRX only 보수 해석).
-
소비자 정합 (필드명 고정):
- trex-core
hub_read.py:168-169가 응답 dict에서active_strategy_id·strategy_params키를 파싱 준비 중. 본 REQ에서 정의하는 필드명은 이 키 이름과 정확히 일치해야 한다. 이름 변경은 소비자 코드 변경을 유발하므로 본 REQ 범위 밖.
- trex-core
-
배포 순서: 본 REQ(①포트폴리오 응답에 전략·시장 메타 실어주기) → trex-core slots 소비 확장(②흘려주기) → tvr e2e 검증. 본 REQ가 배포되지 않으면 trex-core가 읽을 데이터 없음.
-
레거시(OQ-1, 비차단): 기존
strategy_sets에 name="target_value", config={}레코드 및 이를 참조하는 포트폴리오 2개가 존재. name="tvr_v1" 확정에 따른 레거시 마이그레이션(rename/backfill) 여부는 사장님 확인 후 별도 처리. 본 REQ 노출 로직은 레거시 존재와 무관하게 동작(config{}→strategy_params={}정상 처리). -
DB 마이그레이션 불필요:
strategy_sets테이블과portfolios.active_strategy_id컬럼은 이미 존재.assets.markets컬럼도 존재. 본 REQ는 쿼리 JOIN 확장과 응답 스키마 변경만 필요하며 alembic 마이그레이션은 발생하지 않는다. -
OQ-1(미결):
strategy_sets.config컬럼 타입이TEXT(JSON 문자열). 응답strategy_params는 서버 측에서 파싱해 dict로 직렬화. 향후JSONB타입 전환 고려 가능하나 본 REQ 범위 밖.
Change Log
CHG-20260704-03
- Date: 2026-07-04
- Author: 팀장(Claude) — confirm-req-implemented
Change Summary
- Approved → Implemented: bat-023 구현 완료. 독립 QC PASS-WITH-NOTES(blocker 0) — AC 10/10, 필드명 잠금 정확, N+1 없음, 기존 16필드·토큰분기 회귀 0. PR #20 사장님 결재·머지(
522a10f, 583 passed) = confirm-batch-verification 완료. - 잔여(비차단): dangling FK LEFT JOIN NULL 실DB 확인은 배포 후 통합검증.
Delivery Trace Delta
- Batches: bat-023_20260704_portfolios-strategy-market-meta (merged, PR #20)
Revalidation Impact
- Reverification Needed: no / Existing Implementation Invalidated: no
- Status Recommendation: Implemented
CHG-20260704-02
- Date: 2026-07-04
- Author: 팀장(Claude) — confirm-req
Change Summary
- Proposed → Approved: 팀장 검토·승인. tvr e2e 배관 ①(실어주기). 배포 순서 ①→trex-core②. OQ-1(필드명 고정)·OQ-2(레거시 target_value, 비차단) 인지.
Acceptance Criteria Delta
- Added/Updated/Removed: 없음
Revalidation Impact
- Reverification Needed: no / Existing Implementation Invalidated: no
- Status Recommendation: Approved
CHG-20260704-01
- Date: 2026-07-04
- Author: portfolio-hub-dev (Claude)
Change Summary
- 최초 생성 — trex-tvr-v1 dcy-001 confirmed Discovery 근거로 draft-req 실행. 상태 Proposed.
Intent
GET /v1/portfolios응답에 전략·시장 메타데이터를 additive 노출해 trex-core 소비자의 전략 파라미터 단일 조회 경로를 확보한다.
Acceptance Criteria Delta
- Added: AC-1~10 최초 작성
Impacted Area Delta
- Docs: docs/interface-contract.md — PortfolioItem 필드 추가 명세
- Modules: app/schemas/v1.py, app/domain/models.py, app/repositories/postgres.py, app/api/v1_portfolios.py
- Tests: tests/test_v1_portfolios_list.py
Delivery Trace Delta
- Batches: none (미정, confirm-req 이후 suggest-batch-reqs)
- Releases: none (미정)
Revalidation Impact
- Reverification Needed: no (신규 REQ, 기존 구현 없음)
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Proposed)
- Follow-up Action: confirm-req(팀장 승인) → suggest-batch-reqs → batch 계획 편입
Validation Plan
- pytest TestClient로 AC-1~10 검증. 전략 바인딩·미바인딩·dangling FK·파싱불가·markets 유무·기존 필드 회귀·토큰 분기 시나리오 포함.