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
+71 -9
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
from contextlib import ExitStack
from datetime import UTC, datetime, timezone
from decimal import Decimal
from unittest.mock import MagicMock, patch
import pytest
@@ -286,12 +287,12 @@ class TestAC2PartialSellWacUnchanged:
assert resp.json()["total_cost_basis"] == pytest.approx(500_000.0)
def test_corporate_action_row_keeps_holdings_cost_basis_http_200() -> None:
def test_corporate_action_row_returns_authoritative_holdings_cost_basis() -> None:
buy = _make_entry(
ledger_entry_id=1,
side="buy",
quantity=20,
price=100,
quantity=1452,
price=1086.2341459366393,
executed_at=datetime(2026, 9, 1, tzinfo=UTC),
)
corporate_action = LedgerEntry(
@@ -299,13 +300,21 @@ def test_corporate_action_row_keeps_holdings_cost_basis_http_200() -> None:
portfolio_id=_PORTFOLIO_ID,
source="corporate_action",
side="stock_consolidation",
quantity=10,
price=200,
quantity=726,
price=2172.46829187,
recorded_at=datetime(2026, 9, 2, tzinfo=UTC),
corporate_action={"action_type": "stock_consolidation"},
corporate_action={
"corporate_action_id": "ca-019685",
"action_type": "stock_consolidation",
"quantity_before": Decimal(1452),
"quantity_after": Decimal(726),
"average_price_after": Decimal("2172.46829187"),
"total_cost_basis": Decimal("1577211.97990000"),
},
)
repo = _make_service_repo(
portfolio=_make_portfolio(asset_quantity=10),
# Corporate-action replay is authoritative for the returned quantity.
portfolio=_make_portfolio(symbol="019685", asset_quantity=999),
trades=[buy, corporate_action],
)
@@ -316,8 +325,61 @@ def test_corporate_action_row_keeps_holdings_cost_basis_http_200() -> None:
)
assert response.status_code == 200
assert response.json()["cost_basis_per_unit"] == pytest.approx(100)
assert response.json()["total_cost_basis"] == pytest.approx(1000)
body = response.json()
assert body["current_quantity"] == pytest.approx(726)
assert body["cost_basis_per_unit"] == 1577211.9799 / 726
assert body["total_cost_basis"] == 1577211.9799
assert body["data_completeness_warning"] is None
@pytest.mark.parametrize(
"corporate_action",
[
{"action_type": "stock_consolidation", "total_cost_basis": Decimal(1000)},
{
"action_type": "stock_consolidation",
"quantity_after": Decimal(0),
"total_cost_basis": Decimal(1000),
},
{"action_type": "stock_consolidation", "quantity_after": Decimal(10)},
],
)
def test_corporate_action_missing_required_metadata_returns_null_costs(
corporate_action: dict[str, object],
) -> None:
buy = _make_entry(
ledger_entry_id=1,
side="buy",
quantity=20,
price=100,
executed_at=datetime(2026, 9, 1, tzinfo=UTC),
)
action = LedgerEntry(
ledger_entry_id=2,
portfolio_id=_PORTFOLIO_ID,
source="corporate_action",
side="stock_consolidation",
quantity=10,
price=200,
recorded_at=datetime(2026, 9, 2, tzinfo=UTC),
corporate_action=corporate_action,
)
repo = _make_service_repo(
portfolio=_make_portfolio(asset_quantity=10),
trades=[buy, action],
)
with _with_repo(repo):
response = _make_client(_make_settings()).get(
_PATH,
headers={"Authorization": "Bearer tok"},
)
assert response.status_code == 200
body = response.json()
assert body["cost_basis_per_unit"] is None
assert body["total_cost_basis"] is None
assert "기업행동 metadata" in body["data_completeness_warning"]
# ---------------------------------------------------------------------------