Markets: A Rigorous Course

Exercise · m12_event_study.py

Event studies (MacKinlay 1997; Patell 1976; Boehmer-Musumeci-Poulsen 1991)

Read notes.md 12.1–12.3 first. Run: cd exercises && .venv/bin/python -m pytest modules/M12-geopolitics-events/tests/test_m12_event_study.py

Index conventions

  • Returns are 1-d arrays (one asset) or 2-d arrays of shape (T, N) (N assets, one column each), all aligned with a market return array r_m of length T.
  • event_idx is the integer row of event day 0 ("tau = 0").
  • Windows are inclusive offsets relative to event_idx: est = (-250, -11) means rows event_idx-250 .. event_idx-11 (L1 = 240 days) win = (-1, 1) means rows event_idx-1 .. event_idx+1 (L2 = 3 days) Vectorise over firms: the tests run thousands of firms at once.

Tests (7)

  • test_market_model_recovers_parameters
  • test_car_variance_formula_matches_monte_carlo
  • test_single_firm_size_and_power
  • test_cross_sectional_tests_size_under_null
  • test_cross_sectional_power
  • test_event_induced_variance_breaks_patell_not_bmp
  • test_window_validation

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

not run yet

Not saved: sign up to keep your code
"""M12 exercise: event studies (MacKinlay 1997; Patell 1976; Boehmer-Musumeci-Poulsen 1991).

Read notes.md §12.1–12.3 first. Run:
    cd exercises && .venv/bin/python -m pytest modules/M12-geopolitics-events/tests/test_m12_event_study.py

Index conventions
  * Returns are 1-d arrays (one asset) or 2-d arrays of shape (T, N) (N assets,
    one column each), all aligned with a market return array r_m of length T.
  * event_idx is the integer row of event day 0 ("tau = 0").
  * Windows are inclusive offsets relative to event_idx:
      est = (-250, -11) means rows event_idx-250 .. event_idx-11 (L1 = 240 days)
      win = (-1, 1)     means rows event_idx-1 .. event_idx+1   (L2 = 3 days)
Vectorise over firms: the tests run thousands of firms at once.
"""

from __future__ import annotations

import numpy as np


def market_model_fit(r_i: np.ndarray, r_m: np.ndarray) -> dict:
    """OLS market model R_it = alpha_i + beta_i R_mt + eps_it on the estimation window
    (eq. 12.1). r_i is (L1,) or (L1, N); r_m is (L1,).

    Return {"alpha", "beta", "sigma2", "L1", "xtx_inv"} where
      alpha, beta: scalars or (N,) arrays,
      sigma2 = RSS/(L1 - 2) per asset (eq. 12.3),
      xtx_inv = (X'X)^{-1} for X = [1, r_m] (2x2)."""
    raise NotImplementedError


def abnormal_returns(r_i: np.ndarray, r_m: np.ndarray, alpha, beta) -> np.ndarray:
    """AR_t = R_it - alpha - beta R_mt (eq. 12.4). Broadcast over columns when r_i is 2-d."""
    raise NotImplementedError


def ar_covariance_factor(fit: dict, r_m_event: np.ndarray) -> np.ndarray:
    """Return the (L2 x L2) matrix M = I + X*(X'X)^{-1}X*' with X* = [1, r_m_event],
    so that Var(AR* | X) = sigma^2 M (eq. 12.5)."""
    raise NotImplementedError


def car_variance(fit: dict, r_m_event: np.ndarray):
    """Var(CAR) = sigma2 * 1'M1 (eq. 12.8). Scalar or (N,) array."""
    raise NotImplementedError


def event_study(
    r_i: np.ndarray,
    r_m: np.ndarray,
    event_idx: int,
    est: tuple[int, int] = (-250, -11),
    win: tuple[int, int] = (-1, 1),
) -> dict:
    """Market-model event study for one asset or N assets.

    Raise ValueError if a window falls outside the data or if est[1] >= win[0]
    (the estimation window must end before the event window starts).

    Return dict with keys
        "alpha", "beta", "sigma2", "L1", "L2",
        "ar"      : abnormal returns over the event window, (L2,) or (L2, N)
        "car"     : sum of ar over the window, scalar or (N,)
        "var_car" : eq. (12.8)
        "scar"    : car / sqrt(var_car)   (Proposition 12.2: t with L1-2 df under H0)"""
    raise NotImplementedError


def j1_test(car: np.ndarray, var_car: np.ndarray) -> float:
    """J1 = mean(CAR) / sqrt(sum_i var_car_i / N^2)  (eq. 12.10)."""
    raise NotImplementedError


def patell_z(scar: np.ndarray, L1: int) -> float:
    """Z = sum_i SCAR_i / sqrt(N (L1-2)/(L1-4))  (eq. 12.12)."""
    raise NotImplementedError


def bmp_t(scar: np.ndarray) -> float:
    """BMP t = mean(SCAR) / (sd(SCAR, ddof=1)/sqrt(N))  (eq. 12.13)."""
    raise NotImplementedError


def cross_sectional_t(car: np.ndarray) -> float:
    """t = mean(CAR) / (sd(CAR, ddof=1)/sqrt(N))  (eq. 12.11)."""
    raise NotImplementedError


def simulate_event_panel(
    r_m: np.ndarray,
    n_firms: int,
    event_idx: int,
    abnormal: float = 0.0,
    sigma: float = 0.015,
    var_mult: float = 1.0,
    var_window: tuple[int, int] = (0, 0),
    seed: int = 0,
) -> np.ndarray:
    """Simulate R_it = a_i + b_i R_mt + eps_it, shape (T, n_firms), with
    rng = np.random.default_rng(seed) drawing, in this order:
        a = rng.normal(0, 0.0003, n_firms)
        b = rng.uniform(0.5, 1.5, n_firms)
        eps = sigma * rng.standard_normal((T, n_firms))
    Multiply eps on rows event_idx+var_window[0] .. event_idx+var_window[1]
    (inclusive) by sqrt(var_mult). Add `abnormal` to every firm on row event_idx."""
    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.