Files
portfolio-hub/docs/srs/Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md
T

10 KiB

REQ-055: GET /v1/portfolios 응답에 전략·시장 메타데이터 노출 (additive)

  • Status: Approved
  • 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_id str | null portfolios.active_strategy_id 참조 ID. 미바인딩 시 null.
    strategy_name str | null strategy_sets.name(JOIN 해석). 미바인딩 또는 strategy_set row 없으면 null.
    strategy_params dict strategy_sets.config(JSON 문자열) 파싱 결과. 미바인딩·빈 config·파싱 불가 시 {}. 파싱 불가 시 서버 WARNING 로그 1건 출력(500 금지).
    markets list[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

  1. Given active_strategy_id가 바인딩된 포트폴리오, When GET /v1/portfolios, Then 해당 item에 active_strategy_id(비null), strategy_name(strategy_sets.name 값), strategy_params(config 파싱 dict) 세 필드가 정상 값으로 반환된다.

  2. Given active_strategy_id가 null인 포트폴리오, When GET /v1/portfolios, Then active_strategy_id=null, strategy_name=null, strategy_params={} 로 반환된다.

  3. Given active_strategy_id가 존재하나 해당 strategy_set row가 삭제된 포트폴리오(dangling FK), When GET /v1/portfolios, Then active_strategy_id는 원래 값, strategy_name=null, strategy_params={} 로 반환되며 500이 발생하지 않는다.

  4. Given strategy_sets.config가 유효하지 않은 JSON 문자열인 포트폴리오, When GET /v1/portfolios, Then strategy_params={} 로 반환되고, 서버 로그에 WARNING 1건이 기록되며 HTTP 응답은 200이다.

  5. Given assets.markets = ['KRX', 'NXT']인 asset에 연결된 포트폴리오, When GET /v1/portfolios, Then markets=['KRX', 'NXT'] 로 반환된다.

  6. Given assets.markets = [](빈 배열)인 asset에 연결된 포트폴리오, When GET /v1/portfolios, Then markets=[] 로 반환된다.

  7. 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 기존 값이 변경 없이 그대로 반환된다(회귀 없음).

  8. Given service token(daemon-bound) 또는 session/admin token, When GET /v1/portfolios, Then REQ-028의 토큰 분기 필터(daemon_id·owner_user_id·admin ?user_id)가 4개 신규 필드 추가 이후에도 동일하게 동작한다.

  9. Given strategy_name="tvr_v1", config={"target_weight": 0.5, "rebalance_band_pct": 0.05, "min_trade_krw": 10000}인 strategy_set이 바인딩된 포트폴리오, When GET /v1/portfolios, Then strategy_params 응답 dict가 원본 JSON과 동일 키·값을 갖는다.

  10. Given 복수의 포트폴리오를 가진 사용자(일부는 strategy 바인딩, 일부는 미바인딩), When GET /v1/portfolios, Then 각 포트폴리오의 active_strategy_id·strategy_name·strategy_params·markets가 개별적으로 정확하게 반환된다(리스트 조회 정합성).

Impacted Area

  • Docs:

    • docs/interface-contract.md — GET /v1/portfolios PortfolioItem 응답 필드 정의에 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 — Portfolio dataclass에 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-002 v3 FR-8: assets.markets를 거래 가능 시장 목록으로 정의(["KRX","NXT"]; []=KRX only 보수 해석).
  • 소비자 정합 (필드명 고정):

    • trex-core hub_read.py:168-169가 응답 dict에서 active_strategy_id · strategy_params 키를 파싱 준비 중. 본 REQ에서 정의하는 필드명은 이 키 이름과 정확히 일치해야 한다. 이름 변경은 소비자 코드 변경을 유발하므로 본 REQ 범위 밖.
  • 배포 순서: 본 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-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 유무·기존 필드 회귀·토큰 분기 시나리오 포함.