File size: 1,695 Bytes
b54319d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import pandas as pd
import numpy as np
def calculate_forward_excess_returns(
df: pd.DataFrame,
horizons: list = [1, 5],
quantile_top: float = 0.20
) -> pd.DataFrame:
"""
Computes Forward Excess Return Labels starting strictly at t+1:
1. Forward Return R_{i, t+1 -> t+h} = P_{t+h} / P_{t+1} - 1
2. Excess Return = R_{i, t+1 -> t+h} - Mean_universe(R_{t+1 -> t+h})
3. Classification Label = 1 if Excess Return in Top Quantile (Top 20%), else 0
"""
df = df.copy()
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values(["symbol", "date"]).reset_index(drop=True)
price_col = "adjusted_close" if "adjusted_close" in df.columns else "close"
for h in horizons:
# Shift forward to get t+1 price and t+h price
p_t1 = df.groupby("symbol")[price_col].shift(-1)
p_th = df.groupby("symbol")[price_col].shift(-(1 + h))
# Raw forward return from t+1 to t+1+h
fwd_ret = (p_th / (p_t1 + 1e-8)) - 1.0
df[f"fwd_ret_{h}d"] = fwd_ret
# Excess return relative to cross-sectional mean at date t
cs_mean_fwd_ret = df.groupby("date")[f"fwd_ret_{h}d"].transform("mean")
df[f"label_excess_ret_{h}d"] = df[f"fwd_ret_{h}d"] - cs_mean_fwd_ret
# Binary classification label: Top quantile
def _top_quantile_label(series):
if series.dropna().empty:
return pd.Series(np.nan, index=series.index)
cutoff = series.quantile(1.0 - quantile_top)
return (series >= cutoff).astype(float)
df[f"label_top_quantile_{h}d"] = df.groupby("date")[f"label_excess_ret_{h}d"].transform(_top_quantile_label)
return df
|