"""Verify the Settings → Rates fixed-mode arithmetic. For 100% stocks with growth=6% and dividend=2.5%, inflation=3%, the expected real return per year is ``(1 + 0.06 + 0.025) / 1.03 - 1`` ≈ 0.0534. We assert the simulator's portfolio compounds at this rate in the absence of withdrawals (spending=0, no strategy draw). """ from __future__ import annotations from decimal import Decimal import numpy as np import pytest from fire_planner.api.schemas import SimulateRequest from fire_planner.api.simulate import _build_paths, _project @pytest.mark.asyncio async def test_fixed_rates_real_return_arithmetic() -> None: req = SimulateRequest( jurisdiction="uae", # 0% tax to isolate compounding strategy="trinity", leave_uk_year=0, spending_gbp=Decimal("1"), nw_seed_gbp=Decimal("100000"), horizon_years=30, n_paths=100, rates_mode="fixed", inflation_pct=Decimal("0.03"), stocks_growth_pct=Decimal("0.06"), stocks_dividend_pct=Decimal("0.025"), bonds_growth_pct=Decimal("0.015"), bonds_dividend_pct=Decimal("0.035"), stocks_allocation=Decimal("1"), ) paths = await _build_paths(req) assert paths.shape == (100, 30, 3) # nominal stock return embedded should be growth + dividend = 0.085 assert paths[0, 0, 0] == pytest.approx(0.085) assert paths[0, 0, 1] == pytest.approx(0.05) assert paths[0, 0, 2] == pytest.approx(0.03) expected_real = (1 + 0.06 + 0.025) / (1 + 0.03) - 1 assert expected_real == pytest.approx(0.0534, abs=1e-3) result, _ = _project(req, paths) # All paths identical → median == any single path. After 30 years of # compounding 0.0534 with the trinity 4% draw, ending NW lies in a # well-defined window. end_real = float(np.median(result.portfolio_real[:, -1])) assert end_real > 100_000 # grew despite the £1/y withdrawal growth_factor = end_real / 100_000.0 expected_factor = (1 + expected_real)**30 # Loose because trinity strategy still draws something each year. assert growth_factor == pytest.approx(expected_factor, rel=0.05)