Files
portfolio-hub/docs/srs/Integration/req-041_wac-cost-basis-pnl-calculation.md
T
genichinandClaude Opus 4.8 f37ef87c8c docs(dcy-007): draft + approve REQ-039~042 (trade history + realized PnL API)
dcy-007 분해 완료: 체결내역 조회(REQ-039, GET /trades), 실현 PnL 집계(REQ-040, GET /pnl/realized with WAC), WAC 산출 로직(REQ-041, service/domain 격리), 테스트 커버리지(REQ-042).

팀장 confirm-req 완료:
- 4건 모두 Proposed → Approved (dcy-007 결정·발견사항 반영, R-1 경고필드 포함, blocker 없음)
- M1(6/22) fast-track, 단일 batch 예정
- docs/srs/index.md: Next ID REQ-043, Register 4건 Approved, Change Log 갱신
- docs/discovery/dcy-007: 생성된 REQ 참조 업데이트

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-18 16:07:33 +09:00

6.6 KiB
Raw Blame History

REQ-041: WAC 기반 cost_basis 산출 및 실현 PnL 계산 로직 (service/domain 계층 격리)

  • Status: Approved
  • 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.