# 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.