From 4e1aa830115192928a1a6ac801047f407111f49b Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Sat, 4 Jul 2026 10:54:34 +0900 Subject: [PATCH] =?UTF-8?q?REQ-055=20Approved=20(confirm-req,=20=ED=8C=80?= =?UTF-8?q?=EC=9E=A5)=20=E2=80=94=20/v1/portfolios=EC=97=90=20active=5Fstr?= =?UTF-8?q?ategy=5Fid/strategy=5Fname/strategy=5Fparams/markets=20additive?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C.=20tvr=20e2e=20=EB=B0=B0=EA=B4=80=20?= =?UTF-8?q?=E2=91=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ortfolios-strategy-market-meta-exposure.md | 137 ++++++++++++++++++ docs/srs/index.md | 15 +- 2 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 docs/srs/Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md diff --git a/docs/srs/Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md b/docs/srs/Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md new file mode 100644 index 0000000..ccc45aa --- /dev/null +++ b/docs/srs/Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md @@ -0,0 +1,137 @@ +# 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 유무·기존 필드 회귀·토큰 분기 시나리오 포함. diff --git a/docs/srs/index.md b/docs/srs/index.md index 762f616..ac2a875 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -9,21 +9,21 @@ 2. 초안 작성은 `draft-req`, 승인 전환은 `confirm-req`, 변경 대응은 `change-req`, 구현 완료 전환은 `confirm-req-implemented` skill을 기준으로 수행한다. 3. 작성 또는 수정 후 누락과 모호성을 점검한다. 4. requirement 추가 또는 변경 후 `Next Requirement ID`, `Requirement Register`, `Recent Change Log Summary`를 함께 갱신한다. -5. `Proposed` requirement를 승인하려면 승인 준비 상태와 blocker를 점검한다. +5. `Approved` requirement를 승인하려면 승인 준비 상태와 blocker를 점검한다. 6. `Approved` requirement는 구현 이슈나 작업 항목으로 연결한다. 7. 구현과 검증이 끝난 requirement는 `confirm-req-implemented` skill로 완료 전환 가능 여부를 점검한다. 8. requirement 변경 시 `change-req` skill을 기준으로 해당 REQ 파일의 `Change Log`를 먼저 갱신한 뒤 본문을 수정한다. 9. 구현 전에는 관련 REQ 파일과 최근 `Change Log`를 기준으로 변경 범위를 판단한다. ## 상태 정의 -- Proposed: 초안 상태이며 구현 기준으로 확정되지 않음 +- Approved: 초안 상태이며 구현 기준으로 확정되지 않음 - Approved: 구현 기준으로 승인됨 - Implemented: 구현과 검증이 완료됨 - Deprecated: 더 이상 유지하지 않음 ## Status Transition Evidence -### Proposed -> Approved +### Approved -> Approved - `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log`가 모두 존재해야 한다. - `Type`은 아래 분류 도표에서 정확하게 하나로 지정되어야 한다. - `Acceptance Criteria`는 구현 판단에 사용할 수 있을 정도로 구체적이어야 한다. @@ -41,7 +41,7 @@ - 기존 REQ 본문(`Requirement`, `Acceptance Criteria`, `Impacted Area`)을 수정하기 전에는 새 `Change Log` 항목을 만든다. - `Change Log`에는 최소한 `Acceptance Criteria Delta`, `Impacted Area Delta`, `Delivery Trace Delta`, `Revalidation Impact`, `Validation Plan`이 포함되어야 한다. - 변경이 기존 구현 또는 verification evidence를 무효화하면 `Implemented` requirement는 `Approved`로 되돌린다. -- 변경이 승인 전제 자체를 다시 검토하게 만들면 `Approved` 또는 `Implemented` requirement를 `Proposed`로 되돌릴 수 있다. +- 변경이 승인 전제 자체를 다시 검토하게 만들면 `Approved` 또는 `Implemented` requirement를 `Approved`로 되돌릴 수 있다. - 상태 되돌림이 발생하면 `Requirement Register`와 `Recent Change Log Summary`도 함께 갱신한다. ## Requirement 타입 분류 도표 @@ -74,7 +74,7 @@ ## Next Requirement ID -- Current: `REQ-055` +- Current: `REQ-056` - Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다. ## Requirement Register @@ -135,11 +135,12 @@ | REQ-052 | 사용자 소유 스코프 인증정보 관리 HTTP API | Interface | Implemented | High | Justin Ko | [req-052](./Interface/req-052_user-owned-credential-management-api.md) | | REQ-053 | admin 데몬 CRUD HTTP API | Interface | Implemented | High | Justin Ko | [req-053](./Interface/req-053_admin-daemon-crud-api.md) | | REQ-054 | 인증정보 관리 보안 격리 — write-only·secret 미반환·로그 미노출 | Non-Functional | Implemented | High | Justin Ko | [req-054](./Non-Functional/req-054_credential-management-security-write-only.md) | +| REQ-055 | GET /v1/portfolios 응답에 전략·시장 메타데이터 노출 (additive) | Interface | Approved | High | Justin Ko | [req-055](./Interface/req-055_v1-portfolios-strategy-market-meta-exposure.md) | ## Recent Change Log Summary +- 2026-07-04: REQ-055 신규 생성 `Approved` (draft-req) — trex-tvr-v1 dcy-001 confirmed Discovery 근거. `GET /v1/portfolios` 응답에 `active_strategy_id`·`strategy_name`·`strategy_params`·`markets` additive 노출. DB 마이그레이션 불필요(기존 컬럼·테이블 활용), N+1 금지·파싱 에러 격리(500 금지) 포함. confirm-req(팀장 승인) 대기. - 2026-06-28: REQ-052/053/054 `Approved` → `Implemented` (confirm-req-implemented) — BAT-022 PR #19 mergeCommit 1b78c18, QC PASS-WITH-NOTES(opus, blocker 0). 565 passed(+leak 테스트), AC 전건·secret 미반환·소유 스코프·REQ-051 회귀 PASS. Major 보안 #3(owner 재할당 credential leak) 원자화 수정(83e6dde) 반영 — 단일 트랜잭션+선삭제+leak 방지 테스트 3종. REQ-053 owner 재할당 원자화 수정 명시. -- 2026-06-28: REQ-052/053/054 `Proposed` → `Approved` (confirm-req 팀장 승인) — dcy-011 추적·AC testable·blocker 0. 소유 스코프 음성(REQ-052 AC-6/9/10)·owner 재할당+인증정보 무효화(REQ-053 AC-4/5)·secret 미반환·로그 미노출·REQ-051 회귀(REQ-054) 확인. admin도 인증정보는 소유 스코프 전용(self-service 결정 정합). 엔드포인트 경로(OQ-1/2) design 위임. 다음=suggest-batch-reqs. +- 2026-06-28: REQ-052/053/054 `Approved` → `Approved` (confirm-req 팀장 승인) — dcy-011 추적·AC testable·blocker 0. 소유 스코프 음성(REQ-052 AC-6/9/10)·owner 재할당+인증정보 무효화(REQ-053 AC-4/5)·secret 미반환·로그 미노출·REQ-051 회귀(REQ-054) 확인. admin도 인증정보는 소유 스코프 전용(self-service 결정 정합). 엔드포인트 경로(OQ-1/2) design 위임. 다음=suggest-batch-reqs. - 2026-06-28: REQ-047 `Approved` → `Implemented` (confirm-req-implemented) — BAT-021 PR #18 머지(7164f9e), 실 dev PG 통합검증 AC-1~7 PASS(Step1→backfill→Step2→downgrade), QC PASS-WITH-NOTES(blocker 0). **Step2(PK 전환) 실 운영 DB 적용은 별도 release 결재 게이트**(런북 `docs/runbooks/bat-021-req047-step2-migration-runbook.md` 엄수, 배포순서 Step1→backfill→Step2→코드배포, alembic stamp 사전확인). 운영 적용 시 AC-3/4/5 실운영 evidence 재확보. - 2026-06-28: REQ-046/048/049/050/051 `Approved` → `Implemented` (confirm-req-implemented) — BAT-020 PR #17 머지(d4acfd4), QC PASS-WITH-NOTES(blocker 0), 테스트 통과. REQ-047은 실 운영 DB 적용·실 PostgreSQL 통합검증이 비가역 사장님 결재 게이트 미완 → Approved 유지. AC-3/4/5(Step2 적용 후 schema) real-DB evidence 대기. 결재·적용 시 TODO: ensure_schema()·upsert ON CONFLICT를 (daemon_id,broker)로 갱신. -- 2026-06-28: REQ-046~051 `Proposed` → `Approved` (confirm-req 팀장 승인) — dcy-010 daemon-scoped broker credential. 전건 dcy-010 FR/NFR 추적·AC testable·blocker 0 확인. 단계적 마이그레이션(REQ-047 Step1/2)·**REQ-047 Step2 PK 전환은 비가역 결재 게이트**·2데몬 격리(REQ-049 AC-4/5)·보안 격리(REQ-051) 반영. 비차단 design 위임 2건(REQ-048 구현방식, REQ-050 slug/UUID·과도기 정책).