From d224d716f6b438c9ef16fa4d67ef9426eff062e1 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 26 Jun 2026 20:25:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(holdings):=20/holdings/cost-basis=20WAC=20r?= =?UTF-8?q?ow=EC=97=90=20fee=5Famount=20=ED=8E=B8=EC=9E=85=20(QC=EB=B3=B4?= =?UTF-8?q?=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_holdings_cost_basis 핸들러의 row dict에 fee_amount 누락으로 /holdings/cost-basis의 cost_basis_per_unit이 매수 수수료 미편입값을 반환하던 버그 수정. /pnl/realized 핸들러와 동일하게 e.fee_amount 포함. 테스트: buy fee>0 케이스(1000.5 단언) + fee=0 회귀 케이스 추가. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5 --- app/api/v1_portfolios.py | 1 + tests/test_v1_portfolios_holdings.py | 68 +++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/app/api/v1_portfolios.py b/app/api/v1_portfolios.py index 32d9049..dc8d886 100644 --- a/app/api/v1_portfolios.py +++ b/app/api/v1_portfolios.py @@ -859,6 +859,7 @@ def get_holdings_cost_basis( "side": e.side, "quantity": e.quantity, "price": e.price, + "fee_amount": e.fee_amount, "executed_at": e.executed_at, } for e in ledger_entries diff --git a/tests/test_v1_portfolios_holdings.py b/tests/test_v1_portfolios_holdings.py index 4eafdc4..8ee0aec 100644 --- a/tests/test_v1_portfolios_holdings.py +++ b/tests/test_v1_portfolios_holdings.py @@ -78,6 +78,7 @@ def _make_entry( quantity: float, price: float, executed_at: datetime, + fee_amount: float = 0.0, ) -> LedgerEntry: return LedgerEntry( ledger_entry_id=ledger_entry_id, @@ -87,7 +88,7 @@ def _make_entry( side=side, quantity=quantity, price=price, - fee_amount=0.0, + fee_amount=fee_amount, tax_amount=0.0, executed_at=executed_at, recorded_at=executed_at, @@ -535,3 +536,68 @@ def test_portfolio_id_in_response_matches_request() -> None: resp = client.get(_PATH, headers={"Authorization": "Bearer tok"}) assert resp.status_code == 200 assert resp.json()["portfolio_id"] == _PORTFOLIO_ID + + +# --------------------------------------------------------------------------- +# QC보정: buy fee>0 케이스 — cost_basis_per_unit이 fee 편입값인지 확인 +# buy 10@1000 fee=5 → cost_basis_per_unit = (10*1000 + 5) / 10 = 1000.5 +# --------------------------------------------------------------------------- + +def test_cost_basis_per_unit_includes_buy_fee() -> None: + """buy fee>0 케이스: cost_basis_per_unit이 매수 fee 편입값을 반환한다. + + buy 10주 @ 1,000원, fee=5원 → WAC = (10*1000 + 5) / 10 = 1000.5 + REQ-043 Notes: 매수 fee를 cost basis에 포함. + """ + settings = _make_settings() + trades = [ + _make_entry( + ledger_entry_id=1, + side="buy", + quantity=10, + price=1_000, + fee_amount=5.0, + executed_at=datetime(2026, 6, 1, 9, 0, tzinfo=timezone.utc), + ), + ] + repo = _make_service_repo( + portfolio=_make_portfolio(asset_quantity=10.0), + trades=trades, + ) + with _with_repo(repo): + client = _make_client(settings) + resp = client.get(_PATH, headers={"Authorization": "Bearer tok"}) + assert resp.status_code == 200 + body = resp.json() + # (10 * 1000 + 5) / 10 = 1000.5 + assert body["cost_basis_per_unit"] == pytest.approx(1000.5) + assert body["total_cost_basis"] == pytest.approx(10_005.0) + + +def test_cost_basis_per_unit_zero_fee_unchanged() -> None: + """buy fee=0 케이스: fee 편입 추가 후에도 fee=0이면 기존 값 그대로. + + buy 10주 @ 1,000원, fee=0 → WAC = 1000.0 (회귀 확인) + """ + settings = _make_settings() + trades = [ + _make_entry( + ledger_entry_id=1, + side="buy", + quantity=10, + price=1_000, + fee_amount=0.0, + executed_at=datetime(2026, 6, 1, 9, 0, tzinfo=timezone.utc), + ), + ] + repo = _make_service_repo( + portfolio=_make_portfolio(asset_quantity=10.0), + trades=trades, + ) + with _with_repo(repo): + client = _make_client(settings) + resp = client.get(_PATH, headers={"Authorization": "Bearer tok"}) + assert resp.status_code == 200 + body = resp.json() + assert body["cost_basis_per_unit"] == pytest.approx(1000.0) + assert body["total_cost_basis"] == pytest.approx(10_000.0)