Exercise · m06b_etf.py
Index funds and ETFs: tracking difference and error, the premium band, creation-unit arbitrage
not run yet
Not saved: sign up to keep your code"""M06B · Index funds and ETFs: tracking difference and error, the premium band, creation-unit arbitrage.
Exercise stubs. Replace each ``raise NotImplementedError`` with your code, then run the tests in
../tests. See notes.md §6B.8 (tracking difference and tracking error (6B.24), Proposition 6B.8.1
decomposing tracking difference (6B.25), Worked example 6B.11) and §6B.9 (Proposition 6B.9.1, the
no-arbitrage band for the premium (6B.26), Worked example 6B.12).
Conventions
-----------
* ``price`` P: the ETF's mid price; ``inav`` I: the indicative NAV per unit at the same moment (₹).
* Route costs are fractions of value: κ the cost of trading the basket, φ the AMC's creation or
redemption fee, h the carry until settlement (financing and price risk), s/2 the ETF half-spread.
C_c = κ_b + φ_c + h + s/2 and C_r = κ_s + φ_r + h + s/2.
* Return series are simple daily returns; ``periods_per_year`` = 252.
* Tracking difference TD = R_I − R_F (index minus fund): a positive number is a shortfall.
"""
from __future__ import annotations
import math
import numpy as np
def premium(price: float, inav: float) -> float:
"""Premium (positive) or discount (negative) to iNAV: P/I − 1."""
raise NotImplementedError
def route_cost(kappa: float, fee: float, carry: float, half_spread: float) -> float:
"""First-order cost of one arbitrage route in (6B.26): C = κ + φ + h + s/2."""
raise NotImplementedError
def arbitrage_band(inav: float, c_create: float, c_redeem: float, creations_open: bool = True,
redemptions_open: bool = True) -> tuple[float, float]:
"""No-arbitrage band (lo, hi) = (I(1 − C_r), I(1 + C_c)) for the ETF price (6B.26). If creations are
impossible there is no upper bound (hi = math.inf); if redemptions are impossible there is no lower
bound (lo = 0, since a price cannot be negative)."""
raise NotImplementedError
def creation_profit(price: float, inav: float, units: float, kappa_b: float, fee_c: float, carry: float,
half_spread: float) -> float:
"""Exact profit of creating ``units`` ETF units and selling them (proof of Proposition 6B.9.1):
revenue units · P (1 − s/2) minus cost units · I (1 + κ_b + φ_c + h)."""
raise NotImplementedError
def redemption_profit(price: float, inav: float, units: float, kappa_s: float, fee_r: float, carry: float,
half_spread: float) -> float:
"""Exact profit of buying ``units`` ETF units at the ask and redeeming them for the basket:
units · [I (1 − κ_s − φ_r − h) − P (1 + s/2)]."""
raise NotImplementedError
def annualised_return(returns, periods_per_year: int = 252) -> float:
"""Compound annual return of periodic simple returns: (Π(1 + r))^(ppy/n) − 1."""
raise NotImplementedError
def tracking_difference(fund_returns, index_returns, periods_per_year: int = 252,
method: str = "compound") -> float:
"""Annualised tracking difference TD = R_I − R_F (6B.24).
* ``method="compound"``: annualised compound index return minus annualised compound fund return.
* ``method="arithmetic"``: mean daily difference (index − fund) times ``periods_per_year``.
Raise ValueError if the series differ in length or the method is unknown.
"""
raise NotImplementedError
def tracking_error(fund_returns, index_returns, periods_per_year: int = 252) -> float:
"""TE = sqrt(252) σ(r_F − r_I) (6B.24), with the sample standard deviation (ddof=1)."""
raise NotImplementedError
def td_decomposition(ter: float, cash_weight: float, index_return: float, cash_return: float,
trading_costs: float = 0.0, lending_income: float = 0.0, dividend_timing: float = 0.0,
sampling_return: float = 0.0) -> float:
"""Tracking difference to first order (6B.25):
TD ≈ c + w_c (R_I − r_c) + κ − ℓ + δ − Σ d_i R_i,
with ``sampling_return`` = Σ d_i R_i, the return of the deviations from index weights."""
raise NotImplementedError
def simulate_index_fund(index_returns, cash_weight: float, cash_return_daily: float, ter: float,
trading_costs: float = 0.0, lending_income: float = 0.0,
periods_per_year: int = 252) -> np.ndarray:
"""Daily returns of a fund rebalanced daily to (1 − w_c) in the index and w_c in cash, with the expense
ratio and trading costs accrued evenly and securities-lending income added:
r_F = (1 − w_c) r_I + w_c r_c − (c + κ − ℓ)/ppy."""
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.