# REQ-040: GET /v1/portfolios/{id}/pnl/realized 실현 PnL 집계 엔드포인트 신설 - Status: Implemented - Type: Interface - Priority: High - Owner: Justin Ko ## Intent - 지정 기간 내 매도(side='sell') 체결에 대해 수수료·세금을 반영한 실현 PnL 합계 및 건별 내역을 반환하는 GET 엔드포인트를 신설한다. - WAC(weighted average cost, 가중평균 취득단가) 방식으로 산출된 cost_basis 기반 실현 손익을 API 단일 호출로 제공하여 trex_core ledger_outbox 수기 계산 의존을 제거한다(dcy-007 FR-2, S-2). - R-1(이력 완전성) 리스크를 완화하기 위해, cost_basis 산출 시작 시점을 응답에 포함하고 이력이 불완전할 경우 경고 필드(`data_completeness_warning`)를 응답에 포함한다(dcy-007 R-1 완화 방안). ## Requirement - 시스템은 `GET /v1/portfolios/{portfolio_id}/pnl/realized` 엔드포인트를 제공해야 한다. - `from`/`to` 날짜 범위 파라미터(YYYY-MM-DD)로 집계 기간을 지정하며, 두 파라미터 모두 필수다. - 선택적 `symbol` 파라미터로 특정 종목 필터링을 지원한다. - 응답은 다음을 포함해야 한다: 기간 합계 실현 PnL(`total_realized_pnl`), 건별 매도 내역 배열(`items`), cost_basis 산출 시작 시점(`cost_basis_from`), 이력 불완전 경고 필드(`data_completeness_warning: str | null`). - 건별 항목은 symbol(또는 portfolio 식별 가능 필드), executed_at, quantity, sell_price, cost_basis_per_unit, fee_amount, tax_amount, realized_pnl을 포함해야 한다. - 인증은 기존 read_token 이상 권한(DB 토큰)으로 접근 가능해야 하며, 별도 권한 추가는 없다(dcy-007 FR-4, A-3). - 호출자 소유권 확인: 비소유자 비관리자 호출은 HTTP 403을 반환해야 한다. - PnL 산출은 REQ-041(WAC 산출 로직)에 위임하며, 이 엔드포인트는 service 계층을 통해 산출 결과를 수신하고 HTTP 응답으로 직렬화한다. ## Acceptance Criteria 1. Given read_token 이상 권한, portfolio에 buy 100주@10,000 + buy 100주@12,000 이력 존재 후 sell 50주@15,000 fee=500 tax=200 실행, When `GET /v1/portfolios/{id}/pnl/realized?from=2026-06-01&to=2026-06-30`, Then HTTP 200 + `total_realized_pnl`이 WAC 공식((15,000 - 11,000) × 50 - 500 - 200 = 199,300)과 일치 + `items` 배열에 해당 매도 1건 포함 + 각 항목에 cost_basis_per_unit=11,000 포함(WAC = (100×10,000 + 100×12,000) / 200 = 11,000). 2. Given 동일 포트폴리오, When `GET /v1/portfolios/{id}/pnl/realized?from=2026-06-01&to=2026-06-30&symbol=005930.KS`, Then symbol 필터가 적용되어 해당 symbol의 매도 건만 집계. 3. Given `from` 파라미터 누락 또는 `to` 파라미터 누락, When 호출, Then HTTP 422 반환. 4. Given 지정 기간에 매도 체결 0건, When 호출, Then HTTP 200 + `total_realized_pnl=0` + `items=[]` 반환(404 아님). 5. Given portfolio_ledger 이력이 포트폴리오 생성 초기부터 불완전한 경우(buy 이력 일부 누락 감지), When 호출, Then 응답 `data_completeness_warning` 필드에 경고 메시지 포함 + `cost_basis_from`에 집계 시작 시점 명시(R-1 완화 반영). 6. Given portfolio_ledger 이력이 완전한 경우, When 호출, Then 응답 `data_completeness_warning` 필드가 null. 7. Given 비소유자 사용자 token(admin 아님), When 타인 portfolio_id 조회, Then HTTP 403 반환. 8. Given 존재하지 않는 portfolio_id, When 호출, Then HTTP 404 반환. 9. Given 엔드포인트 구현 완료, When OpenAPI `/docs` 또는 `openapi.json` 확인, Then `GET /v1/portfolios/{portfolio_id}/pnl/realized` 경로가 파라미터 설명 및 응답 스키마(`data_completeness_warning` 포함)와 함께 문서화되어 있음(NFR-2 반영). 10. Given 30일 범위 쿼리, When portfolio_ledger 인덱스 활용 환경에서 호출, Then p95 응답 시간 500ms 이하(NFR-1 반영 — 통합/성능 테스트 환경에서 검증). ## Impacted Area - Docs: - `docs/runtime-flows.md` — `GET /v1/portfolios/{id}/pnl/realized` 흐름 추가 필요 - `docs/project-structure.md` — app/api/v1_portfolios.py 책임 갱신, app/schemas/v1.py RealizedPnlResponse 추가 - Modules: - `app/api/v1_portfolios.py` — GET 엔드포인트 추가(날짜/symbol 파라미터 파싱, 소유권 확인, service 계층 위임) - `app/services/portfolio_service.py` 또는 신규 — REQ-041 WAC 산출 로직 호출 + data_completeness_warning 판단 - `app/repositories/postgres.py` — `aggregate_realized_pnl(portfolio_id, from_, to_, symbol)` 메서드 추가 (매도 이력 + WAC 산출용 buy 이력 조회) - `app/repositories/portfolio_repository.py` — abstract method 추가 - `app/schemas/v1.py` — `RealizedPnlItem`, `RealizedPnlResponse` 스키마 추가(`data_completeness_warning: str | None`, `cost_basis_from: date | None` 포함) - Tests: - `tests/test_v1_portfolios_pnl.py` 신규 — AC-1~10 커버 ## Notes - AC-1의 수치 근거: WAC = (100×10,000 + 0 + 100×12,000 + 0) / 200 = 11,000(buy fee=0이므로 기존과 동일). 실현 PnL = (15,000 - 11,000) × 50 - 500 - 200 = 200,000 - 700 = 199,300. - **WAC 정의 변경(2026-06-26)**: REQ-041 CHG-20260626-01에 따라 WAC 공식에 매수 수수료가 편입됨. `running_wac = (running_wac*qty + price*qty + buy_fee) / new_total_qty`. 이 엔드포인트가 반환하는 `cost_basis_per_unit` 및 `realized_pnl`은 해당 정의를 따른다. AC-1 수치는 buy fee=0인 케이스라 기댓값 불변. - `data_completeness_warning` 트리거 조건(이력 불완전 감지 기준)은 REQ-041 구현 단계에서 구체화. 현재 가정: portfolio 생성 시점보다 이른 buy 이력이 없거나, cost_basis 산출 대상 buy 합산 수량이 매도 수량보다 적을 경우. - 미실현 PnL(현재가 연동)은 Out of Scope(dcy-007 범위 경계 확인). - 전체 portfolio 집계 PnL 뷰(OQ-2 결정: Out of Scope) — 단일 portfolio 단위만. - Source: dcy-007 FR-2, FR-3, FR-4, NFR-1, NFR-2, R-1, S-2. ## Change Log ### CHG-20260618-01 - Date: 2026-06-18 - Author: Justin Ko #### Change Summary - 최초 생성 — dcy-007 confirm-discovery 후 draft-req 실행. 실현 PnL 집계 API 산출물. #### Intent - dcy-007 FR-2/FR-4(실현 PnL 엔드포인트 신설 + read_token 인증)를 구현 가능한 Interface REQ로 정규화. R-1 완화 방안(경고 필드 + cost_basis_from)을 AC-5/AC-6에 반드시 반영. #### Acceptance Criteria Delta - Added: - AC-1~10 최초 작성 #### Impacted Area Delta - Docs: docs/runtime-flows.md, docs/project-structure.md — 최초 지정 - Modules: app/api/v1_portfolios.py, app/services/portfolio_service.py, app/repositories/postgres.py, app/repositories/portfolio_repository.py, app/schemas/v1.py — 최초 지정 - Tests: tests/test_v1_portfolios_pnl.py — 최초 지정 #### Delivery Trace Delta - Batches: none (미정) - Releases: none (미정) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: keep-current - Follow-up Action: confirm-req 후 batch 계획 편입; REQ-041 의존성 확인 #### Validation Plan - pytest TestClient로 AC-1~9 검증; AC-10 성능은 통합/성능 테스트 환경에서 검증 ### CHG-20260618-02 - Date: 2026-06-18 - Author: 팀장 (Claude) #### Change Summary - confirm-req: `Proposed` → `Approved`. WAC 수치 AC-1(199,300)·R-1 경고 필드 AC-5/AC-6 확인, OQ-2 OoS 반영, blocker 없음. REQ-041 의존(동일 batch). M1 fast-track. ### CHG-20260618-03 - Date: 2026-06-18 - Author: 팀장 (Claude) #### Change Summary - confirm-req-implemented: `Approved` → `Implemented`. Evidence: BAT-017 release-candidate, 독립 QC PASS-WITH-NOTES, WAC 199,300 단위+e2e 검증, R-1 경고필드 정상, AC-1~9 PASS(AC-10 환경제약). 잔여(비차단): symbol 필터 edge 테스트 공백.