Exercise · m11_parity.py
Covered and uncovered interest parity
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.