Files
calminer/tests/test_financial.py
zwitschi 795a9f99f4 feat: Enhance currency handling and validation across scenarios
- Updated form template to prefill currency input with default value and added help text for clarity.
- Modified integration tests to assert more descriptive error messages for invalid currency codes.
- Introduced new tests for currency normalization and validation in various scenarios, including imports and exports.
- Added comprehensive tests for pricing calculations, ensuring defaults are respected and overrides function correctly.
- Implemented unit tests for pricing settings repository, ensuring CRUD operations and default settings are handled properly.
- Enhanced scenario pricing evaluation tests to validate currency handling and metadata defaults.
- Added simulation tests to ensure Monte Carlo runs are accurate and handle various distribution scenarios.
2025-11-11 18:29:59 +01:00

163 lines
4.5 KiB
Python

from __future__ import annotations
from datetime import date
import pytest
from services.financial import (
CashFlow,
PaybackNotReachedError,
internal_rate_of_return,
net_present_value,
normalize_cash_flows,
payback_period,
)
def test_normalize_cash_flows_with_dates() -> None:
base = date(2025, 1, 1)
period_length = 365.0 / 4
flows = [
CashFlow(amount=-1_000_000, date=base),
CashFlow(amount=350_000, date=date(2025, 4, 1)),
CashFlow(amount=420_000, date=date(2025, 7, 1)),
]
normalised = normalize_cash_flows(flows, compounds_per_year=4)
assert normalised[0] == (-1_000_000.0, 0.0)
expected_second = (date(2025, 4, 1) - base).days / period_length
expected_third = (date(2025, 7, 1) - base).days / period_length
assert normalised[1][1] == pytest.approx(expected_second, rel=1e-6)
assert normalised[2][1] == pytest.approx(expected_third, rel=1e-6)
def test_net_present_value_with_period_indices() -> None:
rate = 0.10
flows = [
CashFlow(amount=-1_000, period_index=0),
CashFlow(amount=500, period_index=1),
CashFlow(amount=500, period_index=2),
CashFlow(amount=500, period_index=3),
]
expected = -1_000 + sum(500 / (1 + rate) **
period for period in range(1, 4))
result = net_present_value(rate, flows)
assert result == pytest.approx(expected, rel=1e-9)
def test_net_present_value_with_residual_value() -> None:
rate = 0.08
flows = [
CashFlow(amount=-100_000, period_index=0),
CashFlow(amount=30_000, period_index=1),
CashFlow(amount=35_000, period_index=2),
]
expected = (
-100_000
+ 30_000 / (1 + rate)
+ 35_000 / (1 + rate) ** 2
+ 25_000 / (1 + rate) ** 3
)
result = net_present_value(rate, flows, residual_value=25_000)
assert result == pytest.approx(expected, rel=1e-9)
def test_internal_rate_of_return_simple_case() -> None:
flows = [
CashFlow(amount=-1_000, period_index=0),
CashFlow(amount=1_210, period_index=1),
]
irr = internal_rate_of_return(flows, guess=0.05)
assert irr == pytest.approx(0.21, rel=1e-9)
def test_internal_rate_of_return_multiple_sign_changes() -> None:
flows = [
CashFlow(amount=-500_000, period_index=0),
CashFlow(amount=250_000, period_index=1),
CashFlow(amount=-100_000, period_index=2),
CashFlow(amount=425_000, period_index=3),
]
irr = internal_rate_of_return(flows, guess=0.2)
npv = net_present_value(irr, flows)
assert npv == pytest.approx(0.0, abs=1e-6)
def test_internal_rate_of_return_requires_mixed_signs() -> None:
flows = [
CashFlow(amount=100_000, period_index=0),
CashFlow(amount=150_000, period_index=1),
]
with pytest.raises(ValueError):
internal_rate_of_return(flows)
def test_payback_period_exact_period() -> None:
flows = [
CashFlow(amount=-120_000, period_index=0),
CashFlow(amount=40_000, period_index=1),
CashFlow(amount=40_000, period_index=2),
CashFlow(amount=40_000, period_index=3),
]
period = payback_period(flows, allow_fractional=False)
assert period == pytest.approx(3.0)
def test_payback_period_fractional_period() -> None:
flows = [
CashFlow(amount=-100_000, period_index=0),
CashFlow(amount=80_000, period_index=1),
CashFlow(amount=30_000, period_index=2),
]
fractional = payback_period(flows)
whole = payback_period(flows, allow_fractional=False)
assert fractional == pytest.approx(1 + 20_000 / 30_000, rel=1e-9)
assert whole == pytest.approx(2.0)
def test_payback_period_raises_when_never_recovered() -> None:
flows = [
CashFlow(amount=-250_000, period_index=0),
CashFlow(amount=50_000, period_index=1),
CashFlow(amount=60_000, period_index=2),
CashFlow(amount=70_000, period_index=3),
]
with pytest.raises(PaybackNotReachedError):
payback_period(flows)
def test_payback_period_with_quarterly_compounding() -> None:
base = date(2025, 1, 1)
flows = [
CashFlow(amount=-120_000, date=base),
CashFlow(amount=35_000, date=date(2025, 4, 1)),
CashFlow(amount=35_000, date=date(2025, 7, 1)),
CashFlow(amount=50_000, date=date(2025, 10, 1)),
]
period = payback_period(flows, compounds_per_year=4)
period_length = 365.0 / 4
expected_period = (date(2025, 10, 1) - base).days / period_length
assert period == pytest.approx(expected_period, abs=1e-6)