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