- GET /v1/portfolios/{id}/trades and GET /v1/portfolios/{id}/pnl/realized endpoints
- Weighted Average Cost (WAC) realized PnL calculation in app/domain/pnl.py
- cost_basis and data_completeness_warning returned in pnl API response
- Test coverage: 50 passed (WAC 20 + trades 12 + pnl 18), full 427 passed regression ✓
- WAC validated against 199,300 unit e2e test cases
- DB migration: none required
- QC: qc-reviewer PASS-WITH-NOTES; team lead confirm-batch-verification → release-candidate
- Remaining non-blocking: symbol filter edge case testing, HTTP_422 deprecation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
7.3 KiB
7.3 KiB
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
- 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). - Given 동일 포트폴리오, When
GET /v1/portfolios/{id}/pnl/realized?from=2026-06-01&to=2026-06-30&symbol=005930.KS, Then symbol 필터가 적용되어 해당 symbol의 매도 건만 집계. - Given
from파라미터 누락 또는to파라미터 누락, When 호출, Then HTTP 422 반환. - Given 지정 기간에 매도 체결 0건, When 호출, Then HTTP 200 +
total_realized_pnl=0+items=[]반환(404 아님). - Given portfolio_ledger 이력이 포트폴리오 생성 초기부터 불완전한 경우(buy 이력 일부 누락 감지), When 호출, Then 응답
data_completeness_warning필드에 경고 메시지 포함 +cost_basis_from에 집계 시작 시점 명시(R-1 완화 반영). - Given portfolio_ledger 이력이 완전한 경우, When 호출, Then 응답
data_completeness_warning필드가 null. - Given 비소유자 사용자 token(admin 아님), When 타인 portfolio_id 조회, Then HTTP 403 반환.
- Given 존재하지 않는 portfolio_id, When 호출, Then HTTP 404 반환.
- Given 엔드포인트 구현 완료, When OpenAPI
/docs또는openapi.json확인, ThenGET /v1/portfolios/{portfolio_id}/pnl/realized경로가 파라미터 설명 및 응답 스키마(data_completeness_warning포함)와 함께 문서화되어 있음(NFR-2 반영). - 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 + 100×12,000) / 200 = 11,000. 실현 PnL = (15,000 - 11,000) × 50 - 500 - 200 = 200,000 - 700 = 199,300.
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 테스트 공백.