Exercise · m00_sip_xirr.py
SIP future value and XIRR
not run yet
Not saved: sign up to keep your code"""Exercise: SIP future value and XIRR (notes §0.4).
Implement every function, then run from the repo root:
cd exercises && .venv/bin/python -m pytest modules/M00-market-on-ramp/tests/test_m00_sip_xirr.py
Conventions. A SIP invests a fixed amount P at the start of each of n months
(an annuity due) or at the end (an ordinary annuity). i is the monthly rate.
XIRR uses actual days between dates and a 365-day year, as spreadsheet XIRR does.
numpy and scipy.optimize.brentq are allowed (or write your own bisection/Newton).
"""
from __future__ import annotations
import datetime as dt
from typing import Sequence
import numpy as np
from scipy.optimize import brentq # noqa: F401 (use it, or write your own root-finder)
def monthly_rate(annual: float, convention: str = "effective") -> float:
"""Monthly rate equivalent to an annual rate.
convention="effective": (1 + annual)^(1/12) - 1, so 12 months compound to `annual`.
convention="nominal": annual / 12 (a quoted rate compounded monthly).
Raise ValueError for any other convention.
"""
raise NotImplementedError
def sip_future_value(P: float, i: float, n: int, due: bool = True) -> float:
"""Value after n months of n instalments of P at monthly rate i (notes eq. 0.2).
FV = P * ((1 + i)^n - 1) / i * (1 + i)^[due]. When i == 0, FV = n * P.
"""
raise NotImplementedError
def sip_required_instalment(target: float, i: float, n: int, due: bool = True) -> float:
"""Monthly instalment that reaches `target` after n months (inverse of sip_future_value)."""
raise NotImplementedError
def npv_times(rate: float, cashflows: Sequence[float], t_years: Sequence[float]) -> float:
"""sum_k c_k (1 + rate)^(-t_k): the net present value at times t_k (years)."""
raise NotImplementedError
def irr_times(cashflows: Sequence[float], t_years: Sequence[float], lo: float = -0.9999, hi: float = 10.0) -> float:
"""Root x > -1 of npv_times(x, cashflows, t_years) = 0, by Brent's method.
Expand `hi` (doubling, up to 1e6) until the NPV changes sign on [lo, hi].
Raise ValueError if the cash flows do not contain both signs or no sign
change is found. With all outflows before all inflows the root is unique
(notes Proposition 0.2). Solve to about 1e-12.
"""
raise NotImplementedError
def xirr(cashflows: Sequence[float], dates: Sequence[dt.date]) -> float:
"""XIRR: irr_times with t_k = (date_k - date_0).days / 365. Raise ValueError on length mismatch."""
raise NotImplementedError
def sip_cashflows(P: float, start: dt.date, n: int, value: float, value_date: dt.date):
"""Cash flows of a monthly SIP from the investor's side: -P on the same
day-of-month for n months from `start`, and +value on value_date.
Return (cashflows, dates) as two lists of length n + 1.
Raise ValueError if start.day > 28.
"""
raise NotImplementedError
No runs yet
The stub's functions raise
NotImplementedError until you write them. Fill in one, press
Run tests, and watch its tests turn green.Reference solution
Available after your first run, with an account approved for code runs.