Exercise · m06b_costs.py
What a fund costs: fee drag, direct versus regular SIPs, transaction charges, ETF versus index fund
not run yet
Not saved: sign up to keep your code"""M06B · What a fund costs: fee drag, direct versus regular SIPs, transaction charges, ETF versus index fund.
Exercise stubs. Replace each ``raise NotImplementedError`` with your code, then run the tests in
../tests. See notes.md §6B.3: Proposition 6B.3.1 on fee drag for a lump sum (6B.7) with its bounds
(6B.8), Proposition 6B.3.2 for a SIP (6B.9), Worked examples 6B.3 (direct versus regular over 20 years)
and 6B.4 (stamp duty, exit load and STT on one transaction), and costs outside the TER; and §6B.9.4,
Proposition 6B.9.2 (the ETF-versus-index-fund crossover horizon, (6B.27)).
Conventions
-----------
* ``r`` : gross annual return of the portfolio (before the expense ratio), a fraction.
* ``c`` : annual cost (expense ratio), a fraction. One year multiplies wealth by 1 + r − c.
* Monthly SIPs: the monthly growth factor is g = (1 + r − c)^(1/12); an instalment is invested at the
start of each month and the corpus is valued at the end of the last month.
* One-off costs ``k`` (exit load, stamp duty, the bid–ask spread and brokerage on an ETF round trip)
are fractions of the amount invested.
"""
from __future__ import annotations
import math
def lump_sum_value(amount: float, r: float, c: float, years: float) -> float:
"""Terminal value of a lump sum: amount · (1 + r − c)^T."""
raise NotImplementedError
def terminal_wealth_ratio(r: float, c1: float, c2: float, years: float) -> float:
"""ρ_T = ((1 + r − c1)/(1 + r − c2))^T (6B.7): terminal wealth with cost c1 over wealth with cost c2."""
raise NotImplementedError
def fee_drag_bounds(r: float, c1: float, c2: float, years: float) -> tuple[float, float]:
"""Bounds (6B.8) for c1 > c2, Δc = c1 − c2:
(exp(−T Δc/(1 + r − c1)), exp(−T Δc/(1 + r − c2)))."""
raise NotImplementedError
def fee_drag_continuous(c1: float, c2: float, years: float) -> float:
"""The continuous-time ratio exp(−Δc T), exact when wealth is W0 e^{(μ − c)T}. Applied to annual
compounding it overstates the drag, because it ignores the 1 + r in (6B.8)."""
raise NotImplementedError
def sip_corpus(monthly: float, r: float, c: float, years: int) -> float:
"""Corpus after ``years`` of instalments ``monthly`` at the start of each month:
FV = P Σ_{m=1}^{n} g^m = P g (g^n − 1)/(g − 1), g = (1 + r − c)^(1/12), n = 12 · years (P n if g = 1)."""
raise NotImplementedError
def sip_wealth_ratio(r: float, c1: float, c2: float, years: int) -> float:
"""FV_1/FV_2 for the same SIP under costs c1 and c2 (6B.9). For c1 > c2 it lies strictly between
γ^n and γ, with γ = g_1/g_2 the monthly ratio and n = 12 · years (Proposition 6B.3.2)."""
raise NotImplementedError
def sip_cost_in_rupees(monthly: float, r: float, c_direct: float, c_regular: float, years: int) -> float:
"""What the regular plan costs a SIP investor in rupees: FV(direct) − FV(regular)."""
raise NotImplementedError
def units_allotted(amount: float, nav: float, stamp_duty: float = 0.00005) -> float:
"""Units allotted on a purchase after stamp duty: amount (1 − stamp_duty)/NAV (§6B.3.3)."""
raise NotImplementedError
def redemption_proceeds(units: float, nav: float, exit_load: float = 0.0, stt: float = 0.0) -> float:
"""Proceeds of a redemption (§6B.3.3, Worked example 6B.4): gross value G = units · NAV, exit load
exit_load · G (credited to the scheme), STT on the net redemption value G (1 − exit_load):
proceeds = G (1 − exit_load)(1 − stt)."""
raise NotImplementedError
def portfolio_trading_cost(turnover: float, one_way_cost: float) -> float:
"""Annual trading cost outside the TER: about 2 τ κ for turnover τ and one-way cost κ (§6B.3.3)."""
raise NotImplementedError
def crossover_horizon(k_etf: float, c_etf: float, k_fund: float, c_fund: float, r: float | None = None) -> float:
"""Crossover horizon of Proposition 6B.9.2 (6B.27), with k_E = k_etf, c_E = c_etf, k_I = k_fund,
c_I = c_fund:
* with ``r``: T* = ln[(1 − k_I)/(1 − k_E)] / ln[(1 + r − c_E)/(1 + r − c_I)];
* ``r is None``: the approximation T* ≈ (k_E − k_I)/(c_I − c_E).
Return 0.0 if the ETF is no dearer up front and no dearer to hold (k_E <= k_I and c_E <= c_I);
otherwise math.inf if c_E >= c_I (it never catches up). Never negative.
"""
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.