fix(pnl): 매수 수수료를 WAC cost basis에 편입 (REQ-041 CHG-20260626-01)

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
This commit is contained in:
2026-06-26 20:34:03 +09:00
co-authored by Claude Sonnet 4.6
parent bd8ff1bf45
commit 73414817e0
6 changed files with 213 additions and 15 deletions
+11 -7
View File
@@ -84,9 +84,10 @@ def calculate_realized_pnl(
WacPnlResult with per-sell PnL items, total, cost_basis_from,
and data_completeness_warning when history is incomplete.
WAC Rules (REQ-041):
buy: running_wac = (running_wac * running_qty + price * qty) / (running_qty + qty)
WAC Rules (REQ-041, buy-fee-in-cost-basis):
buy: running_wac = (running_wac * running_qty + price * qty + buy_fee) / (running_qty + qty)
running_qty += qty
(buy_fee = fee_amount of the buy row, incorporated into cost basis)
sell: realized_pnl = (sell_price - running_wac) * qty - fee - tax
running_qty -= qty
Incomplete history (running_qty < sell_qty or sell with running_qty=0):
@@ -114,10 +115,10 @@ def calculate_realized_pnl(
execution_id: str | None = row.get("execution_id")
if side == "buy":
# Update running WAC
# Update running WAC — include buy fee in cost basis (매수 수수료 편입)
new_total_qty = running_qty + qty
if new_total_qty > 0:
running_wac = (running_wac * running_qty + price * qty) / new_total_qty
running_wac = (running_wac * running_qty + price * qty + fee) / new_total_qty
running_qty = new_total_qty
# Track earliest buy for cost_basis_from
if cost_basis_from is None and executed_at is not None:
@@ -204,9 +205,10 @@ def calculate_current_wac(rows: list[dict[str, Any]]) -> CurrentWacResult:
- cost_basis_from: earliest buy date used
- data_completeness_warning: non-None when history is incomplete
WAC Rules (same as calculate_realized_pnl, REQ-041):
buy: running_wac = (running_wac * running_qty + price * qty) / (running_qty + qty)
WAC Rules (same as calculate_realized_pnl, REQ-041, buy-fee-in-cost-basis):
buy: running_wac = (running_wac * running_qty + price * qty + buy_fee) / (running_qty + qty)
running_qty += qty
(buy_fee = fee_amount of the buy row, incorporated into cost basis)
sell: running_qty -= qty (WAC per unit unchanged on sell)
Incomplete history (sell with running_qty=0, or sell qty > running_qty):
data_completeness_warning is set; calculation continues with best estimate.
@@ -228,12 +230,14 @@ def calculate_current_wac(rows: list[dict[str, Any]]) -> CurrentWacResult:
side = str(row.get("side", "")).lower()
qty = float(row.get("quantity", 0.0))
price = float(row.get("price", 0.0))
fee = float(row.get("fee_amount", 0.0))
executed_at: datetime | None = row.get("executed_at")
if side == "buy":
# Include buy fee in cost basis (매수 수수료 편입)
new_total_qty = running_qty + qty
if new_total_qty > 0:
running_wac = (running_wac * running_qty + price * qty) / new_total_qty
running_wac = (running_wac * running_qty + price * qty + fee) / new_total_qty
running_qty = new_total_qty
if cost_basis_from is None and executed_at is not None:
cost_basis_from = executed_at