- 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
97 lines
6.9 KiB
Markdown
97 lines
6.9 KiB
Markdown
# REQ-041: WAC 기반 cost_basis 산출 및 실현 PnL 계산 로직 (service/domain 계층 격리)
|
||
|
||
- Status: Implemented
|
||
- Type: Integration
|
||
- Priority: High
|
||
- Owner: Justin Ko
|
||
|
||
## Intent
|
||
- portfolio_ledger의 buy/sell 기록에서 weighted average cost(WAC, 가중평균 취득단가) 방식으로 cost_basis를 계산하고, 매도 실현 손익 = (매도가 − 평균취득가) × 수량 − 수수료 − 세금 공식을 service 또는 domain 계층에 격리 구현한다(dcy-007 FR-3, OQ-1=WAC 확정).
|
||
- repository(SQL) 계층과 PnL 계산 로직을 분리하여 단위 테스트가 DB 없이 가능하도록 한다(dcy-007 NFR-3).
|
||
- R-1(이력 완전성) 완화를 위해 cost_basis 산출 시작 시점과 이력 불완전 감지 조건을 이 계층에서 판단하고 경고 신호를 반환한다.
|
||
|
||
## Requirement
|
||
- WAC cost_basis 계산 함수는 `app/services/` 또는 `app/domain/` 계층에 위치해야 하며, repository(psycopg/SQL) 직접 의존성 없이 순수 Python으로 구현해야 한다.
|
||
- 입력: portfolio_ledger 행 목록(side, quantity, price, fee_amount, tax_amount, executed_at, order_id, execution_id 포함) — 시간 순 정렬.
|
||
- 출력: 건별 매도에 대한 실현 PnL 계산 결과 목록, cost_basis_from(첫 buy 기록 시점), data_completeness_warning(이력 불완전 감지 시 경고 문자열, 없으면 None).
|
||
- WAC 계산 규칙:
|
||
- buy 시: `running_wac = (running_wac × running_qty + price × qty) / (running_qty + qty)`, `running_qty += qty`.
|
||
- sell 시: `realized_pnl = (sell_price − running_wac) × qty − fee − tax`, `running_qty -= qty`.
|
||
- 매도 수량이 보유 수량을 초과(running_qty < qty)하는 경우: 이력 불완전으로 판단하고 `data_completeness_warning` 반환 + 최선 추정으로 계속 계산(중단하지 않음).
|
||
- cost_basis_from은 입력 행 목록에서 가장 이른 buy 기록의 executed_at으로 설정. buy 기록이 없으면 None.
|
||
- 이 함수는 repository를 통해 얻은 원시 행 목록을 받으므로, repository는 관련 buy/sell 이력을 충분한 범위로 조회해 전달해야 한다(repository와 계층 경계 명시).
|
||
|
||
## Acceptance Criteria
|
||
1. Given buy 100주@10,000 → buy 100주@12,000 → sell 50주@15,000 fee=500 tax=200 순 행 목록 입력, When WAC 계산 함수 호출, Then cost_basis_per_unit=11,000(WAC = (100×10,000 + 100×12,000) / 200) + realized_pnl=199,300((15,000-11,000)×50 - 500 - 200) 반환.
|
||
2. Given buy 100주@10,000만 이력이 있고 sell 없음, When 함수 호출, Then items=[] + total_realized_pnl=0 + cost_basis_from=buy 시각 + data_completeness_warning=None 반환.
|
||
3. Given 이력이 sell부터 시작(buy 이력 없음 = running_qty=0에서 sell 발생), When 함수 호출, Then data_completeness_warning에 비어 있지 않은 경고 문자열 반환 + 계산은 중단하지 않고 최선 추정 계속.
|
||
4. Given 매도 수량이 현재 running_qty를 초과하는 경우, When 함수 호출, Then data_completeness_warning에 비어 있지 않은 경고 문자열 반환.
|
||
5. Given 동일 포트폴리오에 여러 번 buy/sell 반복(시간 순 정렬 입력 10건), When 함수 호출, Then 각 sell 시점 WAC가 해당 시점까지의 buy 누적 가중평균을 정확히 반영.
|
||
6. Given 입력 행 목록이 빈 배열, When 함수 호출, Then items=[] + total_realized_pnl=0 + cost_basis_from=None + data_completeness_warning=None 반환.
|
||
7. Given 이 계산 함수가 `app/services/` 또는 `app/domain/` 계층에 위치, When `grep -rn "psycopg\|asyncpg\|psycopg2" app/services/ app/domain/` 실행, Then WAC 계산 함수 파일에서 DB 라이브러리 직접 import가 0건(순수 Python 격리 확인, NFR-3 반영).
|
||
8. Given 계산 함수가 구현된 모듈, When 단위 테스트(`tests/test_wac_calculation.py`)를 pytest로 실행, Then DB 컨테이너 없이 통과(--no-db 마커 또는 DB 의존성 없는 pytest 실행).
|
||
|
||
## Impacted Area
|
||
- Docs:
|
||
- `docs/project-structure.md` — app/services/ 또는 app/domain/ 책임 갱신(WAC PnL 산출 로직 추가 반영)
|
||
- Modules:
|
||
- `app/services/portfolio_service.py` 또는 `app/domain/pnl.py`(신규) — WAC cost_basis 계산 함수 구현
|
||
- `app/repositories/postgres.py` — WAC 산출을 위한 buy/sell 이력 조회 메서드(`get_all_trades_for_pnl(portfolio_id, from_, to_, symbol)`) 추가 — service 계층이 consume
|
||
- Tests:
|
||
- `tests/test_wac_calculation.py` 신규 — AC-1~8 순수 단위 테스트(DB 불필요)
|
||
|
||
## Notes
|
||
- 구현 위치 결정(app/services/ vs app/domain/): 도메인 규칙으로서 domain 계층이 적절하나, 현행 코드 패턴(app/domain/services.py)을 따르는 것이 권장. 구현 단계에서 팀장 확인 필요.
|
||
- repository는 PnL 집계 기간(`from`~`to`) 이전의 buy 이력도 cost_basis 산출을 위해 조회해야 한다. 즉, 집계 기간 내 매도가 있을 경우 그 이전 buy 이력 전부를 시간 순으로 함께 전달해야 WAC가 정확하다. 이 쿼리 범위 설계는 구현 단계에서 확인.
|
||
- 이력 불완전 감지 조건은 현재 AC-3/AC-4의 2가지로 정의. 추가 조건은 구현 단계에서 확인.
|
||
- FIFO 방식은 OQ-1에서 명시적으로 제외(WAC 확정). 이 REQ는 WAC 전용이다.
|
||
- Source: dcy-007 FR-3, NFR-3, R-1, OQ-1(WAC 확정).
|
||
|
||
## Change Log
|
||
|
||
### CHG-20260618-01
|
||
- Date: 2026-06-18
|
||
- Author: Justin Ko
|
||
|
||
#### Change Summary
|
||
- 최초 생성 — dcy-007 confirm-discovery 후 draft-req 실행. WAC PnL 산출 로직 격리 산출물.
|
||
|
||
#### Intent
|
||
- dcy-007 FR-3(WAC cost_basis 계산) + NFR-3(service/domain 계층 격리, 단위 테스트 가능)을 Integration REQ로 정규화. R-1 완화(경고 필드 조건)를 AC-3/AC-4에 반영.
|
||
|
||
#### Acceptance Criteria Delta
|
||
- Added:
|
||
- AC-1~8 최초 작성
|
||
|
||
#### Impacted Area Delta
|
||
- Docs: docs/project-structure.md — 최초 지정
|
||
- Modules: app/services/portfolio_service.py 또는 app/domain/pnl.py, app/repositories/postgres.py — 최초 지정
|
||
- Tests: tests/test_wac_calculation.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-040이 이 REQ에 의존하므로 동일 batch 편입 권장
|
||
|
||
#### Validation Plan
|
||
- pytest tests/test_wac_calculation.py (DB 불필요 단위 테스트) + AC-7 grep 검증
|
||
|
||
### CHG-20260618-02
|
||
- Date: 2026-06-18
|
||
- Author: 팀장 (Claude)
|
||
|
||
#### Change Summary
|
||
- confirm-req: `Proposed` → `Approved`. WAC 공식 확정(FIFO 제외), NFR-3 격리 AC-7/AC-8·R-1 완화 AC-3/AC-4 확인, blocker 없음. 구현 위치(domain vs service)는 구현 단계 결정. M1 fast-track.
|
||
|
||
### CHG-20260618-03
|
||
- Date: 2026-06-18
|
||
- Author: 팀장 (Claude)
|
||
|
||
#### Change Summary
|
||
- confirm-req-implemented: `Approved` → `Implemented`. Evidence: app/domain/pnl.py 구현, WAC 누적 정확성+199,300 검증, NFR-3 격리(psycopg import 0) QC 확인, R-1 경고 동작, test_wac_calculation.py 20 passed.
|