""" Feature engineering for time-series forecasting. Technical indicators + lag features. """ import pandas as pd import numpy as np def log_returns(df: pd.DataFrame, col: str = 'close') -> pd.Series: """Log returns: log(price_t / price_{t-1})""" return np.log(df[col] / df[col].shift(1)) def rolling_volatility(df: pd.DataFrame, window: int = 20, col: str = 'close') -> pd.Series: """Rolling standard deviation of log returns""" returns = log_returns(df, col) return returns.rolling(window).std() def rsi(df: pd.DataFrame, window: int = 14, col: str = 'close') -> pd.Series: """Relative Strength Index""" delta = df[col].diff() gain = (delta.where(delta > 0, 0)).rolling(window).mean() loss = (-delta.where(delta < 0, 0)).rolling(window).mean() rs = gain / loss return 100 - (100 / (1 + rs)) def macd(df: pd.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9, col: str = 'close') -> pd.DataFrame: """MACD indicator: returns DataFrame with macd, signal, histogram""" ema_fast = df[col].ewm(span=fast, adjust=False).mean() ema_slow = df[col].ewm(span=slow, adjust=False).mean() macd_line = ema_fast - ema_slow signal_line = macd_line.ewm(span=signal, adjust=False).mean() histogram = macd_line - signal_line return pd.DataFrame({ 'macd': macd_line, 'macd_signal': signal_line, 'macd_hist': histogram }) def volume_zscore(df: pd.DataFrame, window: int = 20) -> pd.Series: """Volume z-score relative to rolling window""" if 'volume' not in df.columns: return pd.Series(0, index=df.index) mean = df['volume'].rolling(window).mean() std = df['volume'].rolling(window).std() return (df['volume'] - mean) / (std + 1e-8) def lag_returns(df: pd.DataFrame, lags: list[int], col: str = 'close') -> pd.DataFrame: """Lagged returns for multiple periods""" returns = log_returns(df, col) return pd.DataFrame({ f'return_lag_{lag}': returns.shift(lag) for lag in lags }) def build_features(df: pd.DataFrame) -> pd.DataFrame: """ Build complete feature set from OHLCV data. Returns: DataFrame with all engineered features """ features = pd.DataFrame(index=df.index) # Price-based features features['log_return'] = log_returns(df) features['volatility_20'] = rolling_volatility(df, window=20) features['rsi_14'] = rsi(df, window=14) # MACD macd_df = macd(df) features = pd.concat([features, macd_df], axis=1) # Volume features['volume_zscore'] = volume_zscore(df) # Lagged returns lag_df = lag_returns(df, lags=[1, 2, 3, 5, 10]) features = pd.concat([features, lag_df], axis=1) # Price momentum features['momentum_5'] = df['close'].pct_change(5) features['momentum_20'] = df['close'].pct_change(20) return features def make_target(df: pd.DataFrame, horizon: int = 1, col: str = 'close') -> pd.Series: """ Create regression target: future log return. Args: df: OHLCV dataframe horizon: Prediction horizon in periods col: Column to predict Returns: Series of future log returns """ return np.log(df[col].shift(-horizon) / df[col]) def make_direction_target(df: pd.DataFrame, horizon: int = 1, col: str = 'close') -> pd.Series: """ Create classification target: future direction (up=1, down=0). Args: df: OHLCV dataframe horizon: Prediction horizon in periods col: Column to predict Returns: Series of binary direction labels """ future_return = make_target(df, horizon, col) return (future_return > 0).astype(int)