Markets: A Rigorous Course

Exercise · m00_sip_xirr.py

SIP future value and XIRR

Implement every function, then press Run tests.

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).

Run locally

From the repository root, with the exercises environment installed:

cd exercises && .venv/bin/python -m pytest modules/M00-market-on-ramp/tests/test_m00_sip_xirr.py

Tests (8)

  • test_monthly_rate
  • test_sip_future_value_closed_form_and_loop
  • test_required_instalment_inverts
  • test_xirr_spreadsheet_reference_example
  • test_xirr_simple_one_year
  • test_irr_of_sip_recovers_growth_rate
  • test_sip_cashflows_and_npv_zero_at_root
  • test_irr_needs_both_signs

Theory: M00 notes. Run with Ctrl/⌘+Enter.

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.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.