WAC 공식에 buy row의 fee_amount를 분자에 포함해 실현 PnL이 당일 net과 정확히 일치하도록 수정. calculate_realized_pnl + calculate_current_wac 양 함수 동일 적용. 신규 단위 테스트 9개(TestBuyFeeInCostBasis 5 + TestCurrentWacBuyFeeInCostBasis 4) 추가. change-req: REQ-041 AC-9/AC-10 추가, REQ-040/REQ-043 Notes 갱신, index 갱신. 전체 456 passed / 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
11 KiB
11 KiB
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 계산 규칙 (매수 수수료 cost basis 편입 포함, 2026-06-26 확정):
- buy 시:
running_wac = (running_wac × running_qty + price × qty + buy_fee) / (running_qty + qty),running_qty += qty.buy_fee는 해당 buy 행의fee_amount. 매수 수수료를 cost basis에 포함함으로써 실현 PnL이 "당일 net"과 정확히 일치한다.
- sell 시:
realized_pnl = (sell_price − running_wac) × qty − sell_fee − tax,running_qty -= qty.- sell fee는 running_wac에 편입하지 않고 sell 시 차감한다(기존 방식 유지).
- 매도 수량이 보유 수량을 초과(running_qty < qty)하는 경우: 이력 불완전으로 판단하고
data_completeness_warning반환 + 최선 추정으로 계속 계산(중단하지 않음).
- buy 시:
- cost_basis_from은 입력 행 목록에서 가장 이른 buy 기록의 executed_at으로 설정. buy 기록이 없으면 None.
- 이 함수는 repository를 통해 얻은 원시 행 목록을 받으므로, repository는 관련 buy/sell 이력을 충분한 범위로 조회해 전달해야 한다(repository와 계층 경계 명시).
Acceptance Criteria
- Given buy 100주@10,000 fee=0 → buy 100주@12,000 fee=0 → sell 50주@15,000 fee=500 tax=200 순 행 목록 입력(buy fee=0이므로 기존 수치 유지), When WAC 계산 함수 호출, Then cost_basis_per_unit=11,000(WAC = (100×10,000 + 0 + 100×12,000 + 0) / 200) + realized_pnl=199,300((15,000-11,000)×50 - 500 - 200) 반환.
- Given buy 100주@10,000만 이력이 있고 sell 없음, When 함수 호출, Then items=[] + total_realized_pnl=0 + cost_basis_from=buy 시각 + data_completeness_warning=None 반환.
- Given 이력이 sell부터 시작(buy 이력 없음 = running_qty=0에서 sell 발생), When 함수 호출, Then data_completeness_warning에 비어 있지 않은 경고 문자열 반환 + 계산은 중단하지 않고 최선 추정 계속.
- Given 매도 수량이 현재 running_qty를 초과하는 경우, When 함수 호출, Then data_completeness_warning에 비어 있지 않은 경고 문자열 반환.
- Given 동일 포트폴리오에 여러 번 buy/sell 반복(시간 순 정렬 입력 10건), When 함수 호출, Then 각 sell 시점 WAC가 해당 시점까지의 buy 누적 가중평균(buy fee 포함)을 정확히 반영.
- Given 입력 행 목록이 빈 배열, When 함수 호출, Then items=[] + total_realized_pnl=0 + cost_basis_from=None + data_completeness_warning=None 반환.
- Given 이 계산 함수가
app/services/또는app/domain/계층에 위치, Whengrep -rn "psycopg\|asyncpg\|psycopg2" app/services/ app/domain/실행, Then WAC 계산 함수 파일에서 DB 라이브러리 직접 import가 0건(순수 Python 격리 확인, NFR-3 반영). - Given 계산 함수가 구현된 모듈, When 단위 테스트(
tests/test_wac_calculation.py)를 pytest로 실행, Then DB 컨테이너 없이 통과(--no-db 마커 또는 DB 의존성 없는 pytest 실행). - Given buy 10주@1,000 fee=5, sell 10주@1,100 fee=5 tax=33(일중 닫힌 거래), When WAC 계산 함수 호출, Then cost_basis_per_unit=1,000.5(WAC = (1000×10+5)/10) + realized_pnl=957((1100-1000.5)×10-5-33). 이 값이 "당일 net"(= 1100×10 - 1000×10 - 5 - 5 - 33 = 957)과 정확히 일치.
- Given calculate_current_wac 함수에 buy 10주@1,000 fee=5 입력, When 호출, Then current_wac=1,000.5(buy fee 편입) + current_quantity=10.
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-20260626-01
- Date: 2026-06-26
- Author: Justin Ko (portfolio-hub-dev)
Change Summary
- WAC 공식에 매수 수수료 cost basis 편입 적용. 사장님 결정(2026-06-26): 매수 fee를 WAC에 포함해 실현 PnL이 "당일 net"과 정확히 일치하도록.
Intent
- 기존 공식
running_wac = (running_wac*qty + price*qty) / new_total_qty에서 buy row의fee_amount를 분자에 추가. 신 공식:running_wac = (running_wac*qty + price*qty + buy_fee) / new_total_qty calculate_realized_pnl과calculate_current_wac양 함수 모두 동일하게 적용해 WAC 정의 일관성 유지.- trex-core dcy-035 멀티데이 운영리포트가 이 API를 소비하며, 일중 닫힌 거래에서 실현 PnL = 당일 net이 정확히 성립한다.
- sell 분기의
realized_pnl = (price - running_wac)*qty - sell_fee - tax공식 자체는 변경 없음. sell fee/tax는 그대로 차감 유지.
Acceptance Criteria Delta
- Changed:
- AC-1: WAC 계산에 buy fee=0 명시 (기존 buy fee 0 가정을 명문화해 기댓값 수치 199,300 유지)
- AC-5: "buy 누적 가중평균(buy fee 포함)을 정확히 반영"으로 명시
- Added:
- AC-9: 일중 닫힌 거래 기준 케이스 — buy 10@1000 fee=5, sell 10@1100 fee=5 tax=33 → realized=957, 당일 net=957 일치
- AC-10: calculate_current_wac buy fee 편입 — buy 10@1000 fee=5 → current_wac=1000.5
Impacted Area Delta
- Modules:
app/domain/pnl.py—calculate_realized_pnlbuy 분기 WAC 공식 갱신(fee 편입).calculate_current_wacbuy 분기에 fee_amount 읽기 추가 및 동일 편입.
- Tests:
tests/test_wac_calculation.py—TestBuyFeeInCostBasis(5개) +TestCurrentWacBuyFeeInCostBasis(4개) 신규 클래스 추가. 기존 테스트 기댓값 변경 없음(기존 케이스 모두 buy fee=0).
Delivery Trace Delta
- Batches: 직접 batch 없음 — 사장님 지시 핫픽스 적용
- Releases: none (미배포)
Revalidation Impact
- Reverification Needed: yes (기존 구현 공식이 바뀌므로 재검증)
- Existing Implementation Invalidated: yes (매수 fee>0인 실제 거래에서 기존 realized_pnl 값이 부정확했음)
- Status Recommendation:
Implemented유지 — 기존 테스트 기댓값은 모두 buy fee=0 케이스라 기존 테스트 PASS 그대로 유지. 신규 AC-9/AC-10 테스트 통과 확인됨(pytest 456 passed / 0 failed, 2026-06-26 실행). - Follow-up Action: REQ-040 Notes 갱신(WAC 정의 변경 참조 추가), REQ-043 Notes 갱신(동일). docs/srs/index.md Recent Change Log 갱신.
Validation Plan
- pytest tests/test_wac_calculation.py — 29 passed (신규 TestBuyFeeInCostBasis 5개 + TestCurrentWacBuyFeeInCostBasis 4개 포함). DB 불필요.
- pytest 전체 스위트 — 456 passed / 0 failed (2026-06-26 실행 확인).
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.