fix(analytics): нереалистичная годовая доходность не валит пересчёт метрик

XIRR за несколько дней истории даёт артефакт солвера за ±100 000 000 %, который переполнял NUMERIC(24,10) и ронял весь refresh. Такое значение теперь считается «нет ответа».
This commit is contained in:
Dmitry
2026-09-19 21:54:36 +03:00
parent 3b3ee4d682
commit 1b521be35e
2 changed files with 20 additions and 1 deletions
+8 -1
View File
@@ -58,6 +58,10 @@ ZERO = Decimal(0)
ONE = Decimal(1) ONE = Decimal(1)
DAYS_IN_YEAR = Decimal(365) DAYS_IN_YEAR = Decimal(365)
RATE_PLACES = Decimal("0.000001") RATE_PLACES = Decimal("0.000001")
MAX_ABS_RATE = Decimal(10**6)
"""An annualised rate beyond ±100 000 000 % is a solver artefact (a few days of history, or
flows with several sign changes), not a return. It is reported as «no answer»: stored, it would
also overflow the NUMERIC(24,10) column and take the whole metrics refresh down with it."""
PERIODS: tuple[str, ...] = ("1m", "3m", "6m", "ytd", "1y", "3y", "all") PERIODS: tuple[str, ...] = ("1m", "3m", "6m", "ytd", "1y", "3y", "all")
@@ -205,7 +209,10 @@ def annualize(cumulative: Decimal | None, days: int) -> Decimal | None:
def _as_rate(value: float) -> Decimal | None: def _as_rate(value: float) -> Decimal | None:
try: try:
return _quantize(Decimal(repr(value))) rate = Decimal(repr(value))
if not rate.is_finite() or abs(rate) > MAX_ABS_RATE:
return None
return _quantize(rate)
except (InvalidOperation, ValueError): except (InvalidOperation, ValueError):
return None return None
+12
View File
@@ -119,3 +119,15 @@ def test_period_start(period: str, expected: date | None):
def test_months_back_clamps_to_a_shorter_month(): def test_months_back_clamps_to_a_shorter_month():
assert months_back(date(2026, 3, 31), 1) == date(2026, 2, 28) assert months_back(date(2026, 3, 31), 1) == date(2026, 2, 28)
assert months_back(date(2026, 1, 15), 13) == date(2024, 12, 15) assert months_back(date(2026, 1, 15), 13) == date(2024, 12, 15)
def test_a_rate_no_column_could_hold_is_no_answer():
# doubling in one day annualises to 2**365 - 1: arithmetically right, and not a return
assert xirr([day(0), day(1)], [D("-1000"), D("2000")]) is None
def test_a_large_but_plausible_rate_is_kept():
# +10 % in a month is roughly +214 % a year
rate = xirr([day(0), day(30)], [D("-1000"), D("1100")])
assert rate is not None
assert D("2.0") < rate < D("2.3")