Markets: A Rigorous Course

Exercise · m11_parity.py

Covered and uncovered interest parity

Read notes.md 11.8–11.9 first. Run: cd exercises && .venv/bin/python -m pytest modules/M11-macro-rates-fx/tests/test_m11_parity.py

Quote convention: S and F are DOMESTIC currency per unit of FOREIGN currency (e.g. USDINR = 94.5 means 94.5 INR per USD; domestic = INR, foreign = USD). Money-market rates are simple annual rates over a tenor of T years.

Tests (7)

  • test_cip_forward_simple_and_continuous
  • test_no_arbitrage_at_fair_forward
  • test_detects_rich_and_cheap_forward
  • test_band_and_never_two_way_arbitrage
  • test_cip_basis
  • test_uip_regression_recovers_beta_and_fama_puzzle
  • test_uip_test_size_under_null

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

not run yet

Not saved: sign up to keep your code
"""M11 exercise: covered and uncovered interest parity.

Read notes.md §11.8–11.9 first. Run:
    cd exercises && .venv/bin/python -m pytest modules/M11-macro-rates-fx/tests/test_m11_parity.py

Quote convention: S and F are DOMESTIC currency per unit of FOREIGN currency
(e.g. USDINR = 94.5 means 94.5 INR per USD; domestic = INR, foreign = USD).
Money-market rates are simple annual rates over a tenor of T years.
"""

from __future__ import annotations

import numpy as np

from toolkit.stats import ols as toolkit_ols


# ------------------------------------------------------------------ CIP

def cip_forward(spot: float, r_dom: float, r_for: float, T: float, compounding: str = "simple") -> float:
    """No-arbitrage forward, Proposition 11.8.
        compounding='simple':     F = S (1 + r_dom T) / (1 + r_for T)
        compounding='continuous': F = S exp((r_dom - r_for) T)
    Raise ValueError for another compounding string."""
    raise NotImplementedError


def forward_premium(spot: float, forward: float, T: float) -> float:
    """Annualised forward premium (F/S - 1)/T."""
    raise NotImplementedError


def cip_arbitrage(
    s_bid: float,
    s_ask: float,
    f_bid: float,
    f_ask: float,
    rd_borrow: float,
    rd_lend: float,
    rf_borrow: float,
    rf_lend: float,
    T: float,
) -> dict:
    """Detect a covered-interest arbitrage with bid/ask spreads (Proposition 11.9).

    Round trip A, per 1 unit of domestic currency borrowed (profit at T, domestic):
        profit_A = f_bid (1 + rf_lend T) / s_ask - (1 + rd_borrow T)
    Round trip B, per 1 unit of domestic currency raised by borrowing foreign:
        profit_B = (1 + rd_lend T) - f_ask (1 + rf_borrow T) / s_bid

    Return a dict with keys
        "profit_A", "profit_B",
        "direction": "A" if profit_A > 0 (and >= profit_B), else "B" if profit_B > 0, else None,
        "band": (lower, upper) with
            lower = s_bid (1 + rd_lend T)/(1 + rf_borrow T)
            upper = s_ask (1 + rd_borrow T)/(1 + rf_lend T).
    """
    raise NotImplementedError


def cip_basis_bp(spot: float, forward: float, r_dom: float, r_for: float, T: float) -> float:
    """Cross-currency basis x (in basis points) defined by
        (1 + r_dom T) = (F/S) (1 + (r_for + x) T).
    Solve for x and return 1e4 * x."""
    raise NotImplementedError


# ------------------------------------------------------------------ UIP

def simulate_uip_data(
    n: int = 600,
    beta: float = 1.0,
    alpha: float = 0.0,
    rho: float = 0.95,
    diff_vol: float = 0.002,
    fx_vol: float = 0.03,
    seed: int = 0,
) -> tuple[np.ndarray, np.ndarray]:
    """Monthly simulated data for the Fama regression, using
    rng = np.random.default_rng(seed).

    d_0 = 0.003; for t >= 1: d_t = 0.003 (1 - rho) + rho d_{t-1} + diff_vol * e_t
    ds_t = alpha + beta d_t + fx_vol * u_t   (the depreciation realised after d_t)
    Return (d, ds), both of length n."""
    raise NotImplementedError


def ols(y: np.ndarray, x: np.ndarray) -> dict:
    """OLS of y on [1, x] with classical (homoskedastic) standard errors,
    s^2 = RSS/(n-2). Returns {"alpha", "beta", "se_alpha", "se_beta", "sigma2", "n"}.
    (Given: a thin wrapper over toolkit.stats.ols, i.e. statsmodels OLS.)"""
    x = np.asarray(x, float)
    fit = toolkit_ols(np.asarray(y, float), np.column_stack([np.ones_like(x), x]))
    return {"alpha": fit.params[0], "beta": fit.params[1], "se_alpha": fit.bse[0],
            "se_beta": fit.bse[1], "sigma2": fit.sigma2, "n": fit.nobs}


def uip_regression(ds: np.ndarray, diff: np.ndarray) -> dict:
    """Fama (1984) regression ds_{t+1} = a + b (i_t - i*_t) + e (eq. 11.20).
    Return the ols() dict plus
        "t_beta_eq_1" = (b - 1)/se(b)
        "t_beta_eq_0" = b/se(b)."""
    raise NotImplementedError


def carry_excess_return(diff: np.ndarray, ds: np.ndarray) -> np.ndarray:
    """Log excess return of borrowing the low-rate (foreign) currency and lending
    the high-rate (domestic) currency for one period: xr = diff - ds (eq. 11.22)."""
    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.