quant-ai / src /labels /excess_returns.py
Ypeng12's picture
feat: 10/10 HRT style point-in-time alpha research platform, purged walk-forward CV, risk parity & test suite
b54319d
Raw
History Blame Contribute Delete
1.7 kB
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