Markets: A Rigorous Course

Exercise · m06b_etf.py

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.

Tests (7)

  • test_premium
  • test_worked_example_6b12_band
  • test_exact_profits_match_the_first_order_band
  • test_annualised_return
  • test_tracking_difference_and_error_definitions
  • test_worked_example_6b11
  • test_td_decomposition_against_simulation

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

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.

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.