SPB Git forge

spb/wp8_uqo

Public
3commits 1branches 1releases
4.2 MBsize
maindefault branch
2 h agolast push
Python 56.2% TeX 43.7%
6.6 KB · 212 lines python
Raw Blame History
1"""High-frequency event study around macro events.23Implements the event-study methodology of MacKinlay (1997) with4Patell (1976) and Boehmer-Musumeci-Poulsen (1991) test statistics.5"""67from __future__ import annotations89import pandas as pd10import numpy as np11from scipy import stats12from dataclasses import dataclass131415# ---------------------------------------------------------------------------16# Data classes17# ---------------------------------------------------------------------------1819@dataclass20class EventStudyResult:21    """Container for event-study output."""22    cars: pd.DataFrame           # [event_date, window, CAR]23    test_stats: pd.DataFrame     # [window, mean_CAR, t_stat, p_value]24    event_type: str25    n_events: int262728# ---------------------------------------------------------------------------29# Core event study30# ---------------------------------------------------------------------------3132def run_event_study(33    prices: pd.DataFrame,34    event_dates: list,35    event_type: str = "FOMC",36    window: tuple[int, int] = (-5, 5),37    estimation_window: int = 60,38) -> EventStudyResult:39    """Event study methodology following MacKinlay (1997).4041    1. For each event date, find the row in *prices* closest to that date.42    2. Estimation window: ``[t - estimation_window, t + window[0] - 1]``43       for the normal-return (constant-mean) model.44    3. Event window: ``[t + window[0], t + window[1]]``.45    4. Abnormal return = actual return - expected return.46    5. CAR = cumulative sum of abnormal returns in the event window.47    6. Test statistics: cross-sectional *t*-test of CARs.4849    Parameters50    ----------51    prices : pd.DataFrame52        Each column is a market's price series with a DatetimeIndex.53    event_dates : list54        Dates (or date-like objects) of the events.55    event_type : str56        Label for the event category (e.g. ``"FOMC"``).57    window : tuple[int, int]58        (pre, post) trading-day offsets around each event.59    estimation_window : int60        Number of trading days used to estimate the normal-return model.6162    Returns63    -------64    EventStudyResult65    """66    # Compute simple returns67    returns = prices.pct_change().dropna(how="all")6869    # Ensure DatetimeIndex70    if not isinstance(returns.index, pd.DatetimeIndex):71        returns.index = pd.to_datetime(returns.index)7273    event_dates_dt = pd.to_datetime(event_dates)7475    all_cars: list[dict] = []76    all_ars: list[pd.DataFrame] = []7778    for ev_date in event_dates_dt:79        # Find closest available trading day80        idx = returns.index.searchsorted(ev_date)81        if idx >= len(returns.index):82            idx = len(returns.index) - 183        t0 = idx  # position index8485        est_start = t0 - estimation_window + window[0] - 186        est_end = t0 + window[0] - 187        ev_start = t0 + window[0]88        ev_end = t0 + window[1]8990        # Boundary checks91        if est_start < 0 or ev_end >= len(returns):92            continue9394        # Estimation-window returns → constant-mean model95        est_returns = returns.iloc[est_start:est_end + 1]96        expected = est_returns.mean()  # per column97        est_std = est_returns.std()9899        # Event-window abnormal returns100        ev_returns = returns.iloc[ev_start:ev_end + 1]101        ar = ev_returns.subtract(expected, axis=1)102        car = ar.cumsum()103104        # Store CAR at end of window (last row)105        final_car = car.iloc[-1]106        for col in final_car.index:107            all_cars.append({108                "event_date": ev_date,109                "market": col,110                "window": f"[{window[0]},{window[1]}]",111                "CAR": final_car[col],112            })113114        all_ars.append(ar)115116    cars_df = pd.DataFrame(all_cars)117    n_events = int(cars_df["event_date"].nunique()) if len(cars_df) > 0 else 0118119    # Cross-sectional test statistics per market120    test_rows: list[dict] = []121    if len(cars_df) > 0:122        for market, grp in cars_df.groupby("market"):123            car_vals = grp["CAR"].dropna()124            if len(car_vals) < 2:125                continue126            t_stat, p_val = stats.ttest_1samp(car_vals, 0)127            test_rows.append({128                "market": market,129                "window": f"[{window[0]},{window[1]}]",130                "mean_CAR": float(car_vals.mean()),131                "t_stat": float(t_stat),132                "p_value": float(p_val),133                "n_events": len(car_vals),134            })135136    test_df = pd.DataFrame(test_rows)137138    return EventStudyResult(139        cars=cars_df,140        test_stats=test_df,141        event_type=event_type,142        n_events=n_events,143    )144145146# ---------------------------------------------------------------------------147# Patell (1976) test148# ---------------------------------------------------------------------------149150def compute_patell_test(151    abnormal_returns: pd.DataFrame,152    std_errors: pd.Series,153) -> float:154    """Patell (1976) standardised-residual test.155156    Parameters157    ----------158    abnormal_returns : pd.DataFrame159        Abnormal returns (events x time).160    std_errors : pd.Series161        Estimation-window standard errors for each event.162163    Returns164    -------165    float166        Patell *z*-statistic.167    """168    # Standardise each event's AR by its estimation-window std error169    sar = abnormal_returns.div(std_errors, axis=0)170    # Cumulative standardised AR171    csar = sar.sum(axis=1)  # sum across event window per event172    n = len(csar)173    if n == 0:174        return np.nan175    z = csar.mean() / (csar.std() / np.sqrt(n))176    return float(z)177178179# ---------------------------------------------------------------------------180# BMP (1991) test181# ---------------------------------------------------------------------------182183def compute_bmp_test(184    standardized_ars: pd.DataFrame,185) -> tuple[float, float]:186    """Boehmer, Musumeci, Poulsen (1991) test for event-induced variance.187188    Uses cross-sectional variance of standardised abnormal returns to189    produce a *t*-statistic robust to event-induced variance changes.190191    Parameters192    ----------193    standardized_ars : pd.DataFrame194        Standardised abnormal returns (events x time-steps in window).195196    Returns197    -------198    tuple[float, float]199        (BMP *t*-statistic, two-sided *p*-value).200    """201    # Average SAR across the event window for each event202    avg_sar = standardized_ars.mean(axis=1)203    n = len(avg_sar)204    if n < 2:205        return (np.nan, np.nan)206207    mean_sar = avg_sar.mean()208    se = avg_sar.std(ddof=1) / np.sqrt(n)209    t_stat = mean_sar / se if se > 0 else np.nan210    p_value = float(2 * stats.t.sf(abs(t_stat), df=n - 1)) if not np.isnan(t_stat) else np.nan211    return (float(t_stat), p_value)212