fix(portfolio): tolerate consolidation WAC residue
Publish verified PortfolioHub image / verify (push) Canceled after 0s
Publish verified PortfolioHub image / publish (push) Canceled after 0s

This commit is contained in:
2026-09-13 18:36:00 +09:00
parent 4520fbbfbd
commit 2b470c735f
14 changed files with 146 additions and 34 deletions
+14 -5
View File
@@ -6,6 +6,13 @@ import json
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal
STORAGE_QUANTUM = Decimal("0.00000001")
def quantize_stored_decimal(value: Decimal) -> Decimal:
"""Round a monetary value to the NUMERIC(*, 8) persistence contract."""
return value.quantize(STORAGE_QUANTUM, rounding=ROUND_HALF_UP)
def canonical_payload_hash(payload: object) -> str:
if hasattr(payload, "model_dump"):
@@ -36,11 +43,13 @@ def calculate_stock_consolidation(quantity_before: Decimal, quantity_after: Deci
raise CorporateActionError("stock consolidation must reduce quantity")
if q1 != q1.to_integral_value() or q0 != q0.to_integral_value():
raise CorporateActionError("fractional result unsupported", "FRACTIONAL_RESULT_UNSUPPORTED")
total = (q0 * wac).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
after = (total / q1).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
if after * q1 != total:
raise CorporateActionError("fractional result unsupported", "FRACTIONAL_RESULT_UNSUPPORTED")
return StockConsolidation(q0, q1, wac, after, total, ratio)
# Preserve the authoritative pre-action total independently. A per-unit WAC
# rounded to the storage scale cannot always multiply back to that total
# exactly, and that harmless decimal residue is not a fractional share.
total = quantize_stored_decimal(q0 * wac)
before = quantize_stored_decimal(wac)
after = quantize_stored_decimal(total / q1)
return StockConsolidation(q0, q1, before, after, total, ratio)
def apply_corporate_action_to_wac(quantity: Decimal, wac: Decimal, quantity_after: Decimal) -> tuple[Decimal, Decimal]:
result = calculate_stock_consolidation(quantity, quantity_after, wac)
+10 -2
View File
@@ -179,7 +179,11 @@ def calculate_realized_pnl(
if abs(running_qty - before) > 1e-9:
warning_triggered = True
running_qty = after
running_wac = float(row.get("average_price_after", running_wac * before / after))
total_cost_basis = row.get("total_cost_basis")
if total_cost_basis is not None:
running_wac = float(total_cost_basis) / after
else:
running_wac = float(row.get("average_price_after", running_wac * before / after))
warning: str | None = None
if warning_triggered:
@@ -272,7 +276,11 @@ def calculate_current_wac(rows: list[dict[str, Any]]) -> CurrentWacResult:
if abs(running_qty - before) > 1e-9:
warning_triggered = True
running_qty = after
running_wac = float(row.get("average_price_after", running_wac * before / after))
total_cost_basis = row.get("total_cost_basis")
if total_cost_basis is not None:
running_wac = float(total_cost_basis) / after
else:
running_wac = float(row.get("average_price_after", running_wac * before / after))
warning: str | None = None
if warning_triggered: