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:
+11
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user