fix(portfolios): replay corporate-action cost basis (#38)

This commit is contained in:
2026-09-19 17:07:14 +09:00
committed by GitHub
parent f79f3c79b5
commit 567d662913
12 changed files with 352 additions and 61 deletions
+56 -1
View File
@@ -2,6 +2,8 @@ from datetime import UTC, date, datetime
from decimal import Decimal
from pathlib import Path
import pytest
from app.domain.corporate_actions import canonical_payload_hash
from app.domain.pnl import calculate_current_wac, calculate_realized_pnl
from app.schemas.corporate_actions import CorporateActionRequest
@@ -10,7 +12,7 @@ from app.schemas.corporate_actions import CorporateActionRequest
def test_buy_consolidation_sell_replays_wac_and_realized_pnl():
rows = [
{"side": "buy", "quantity": 10, "price": 100, "fee_amount": 0, "tax_amount": 0, "executed_at": datetime(2026, 1, 1, tzinfo=UTC)},
{"side": "stock_consolidation", "quantity": 0, "price": 0, "quantity_before": 10, "quantity_after": 5, "average_price_after": 200, "executed_at": datetime(2026, 1, 2, tzinfo=UTC)},
{"side": "stock_consolidation", "quantity": 0, "price": 0, "quantity_before": 10, "quantity_after": 5, "average_price_after": 200, "total_cost_basis": 1000, "executed_at": datetime(2026, 1, 2, tzinfo=UTC)},
{"side": "sell", "quantity": 2, "price": 250, "fee_amount": 0, "tax_amount": 0, "executed_at": datetime(2026, 1, 3, tzinfo=UTC)},
]
assert calculate_current_wac(rows).current_wac == 200
@@ -35,6 +37,59 @@ def test_consolidation_replay_uses_preserved_total_cost_basis():
result = calculate_current_wac(rows)
assert result.current_quantity == 726
assert result.current_wac == float(Decimal("1577211.97990000") / Decimal(726))
assert result.total_cost_basis == float(Decimal("1577211.97990000"))
assert result.data_completeness_warning is None
def test_consolidation_replay_ignores_rounded_average_price_after():
rows = [
{"side": "buy", "quantity": 1452, "price": 1086.2341459366393},
{
"side": "stock_consolidation",
"quantity_after": 726,
"average_price_after": Decimal("1.00"),
"total_cost_basis": Decimal("1577211.97990000"),
},
]
result = calculate_current_wac(rows)
assert result.current_wac == float(Decimal("1577211.97990000") / Decimal(726))
assert result.total_cost_basis == float(Decimal("1577211.97990000"))
@pytest.mark.parametrize(
"metadata",
[
{"total_cost_basis": Decimal(1000)},
{"quantity_after": 0, "total_cost_basis": Decimal(1000)},
{"quantity_after": 5},
],
)
def test_consolidation_replay_fails_closed_when_required_metadata_is_missing(metadata):
rows = [
{"side": "buy", "quantity": 10, "price": 100},
{"side": "stock_consolidation", **metadata},
]
result = calculate_current_wac(rows)
assert result.current_wac is None
assert result.total_cost_basis is None
assert result.data_completeness_warning is not None
assert "기업행동 metadata" in result.data_completeness_warning
def test_consolidation_authoritative_total_continues_through_later_trades():
rows = [
{"side": "buy", "quantity": 10, "price": 100},
{"side": "stock_consolidation", "quantity_after": 5, "total_cost_basis": Decimal(1000)},
{"side": "buy", "quantity": 1, "price": 300, "fee_amount": 2},
{"side": "sell", "quantity": 2, "price": 400},
]
result = calculate_current_wac(rows)
assert result.current_quantity == 4
assert result.current_wac == pytest.approx(1302 / 6)
assert result.total_cost_basis == pytest.approx((1302 / 6) * 4)
def test_canonical_hash_same_payload_and_changed_payload_differ():