| """ |
| Advanced Feature Engineering |
| Multi-timeframe, regime detection, and sophisticated market features. |
| """ |
|
|
| import numpy as np |
| import pandas as pd |
| from typing import Dict, List, Tuple, Optional |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class AdvancedFeatureEngine: |
| """ |
| Advanced feature engineering for crypto trading. |
| |
| Features: |
| - Multi-timeframe momentum |
| - Volatility regime detection |
| - Price action patterns |
| - Market microstructure |
| - Trend strength indicators |
| """ |
| |
| def __init__(self, lookback: int = 200): |
| self.lookback = lookback |
| |
| def compute_all(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Compute all advanced features.""" |
| df = df.copy() |
| |
| |
| df = self._add_returns(df) |
| df = self._add_volatility_features(df) |
| df = self._add_trend_features(df) |
| |
| |
| df = self._add_momentum_features(df) |
| |
| |
| df = self._add_candlestick_patterns(df) |
| df = self._add_support_resistance(df) |
| |
| |
| df = self._add_regime_features(df) |
| |
| |
| df = self._add_volume_features(df) |
| |
| |
| df = self._add_multi_timeframe_features(df) |
| |
| return df |
| |
| def _add_returns(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add return-based features.""" |
| |
| for period in [1, 4, 12, 24, 48]: |
| df[f'return_{period}h'] = np.log(df['close'] / df['close'].shift(period)) |
| |
| |
| df['cumret_24h'] = df['return_1h'].rolling(24).sum() |
| df['cumret_7d'] = df['return_1h'].rolling(168).sum() |
| |
| return df |
| |
| def _add_volatility_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add volatility regime features.""" |
| |
| for window in [12, 24, 72, 168]: |
| df[f'volatility_{window}h'] = df['return_1h'].rolling(window).std() * np.sqrt(window) |
| |
| |
| df['vol_ratio'] = df['volatility_24h'] / (df['volatility_168h'] + 1e-8) |
| |
| |
| high_low = df['high'] - df['low'] |
| high_close = np.abs(df['high'] - df['close'].shift()) |
| low_close = np.abs(df['low'] - df['close'].shift()) |
| true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) |
| |
| df['atr_14'] = true_range.rolling(14).mean() |
| df['atr_percent'] = df['atr_14'] / df['close'] |
| |
| |
| df['vol_regime'] = (df['volatility_24h'] > df['volatility_24h'].rolling(168).mean()).astype(float) |
| |
| return df |
| |
| def _add_trend_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add trend detection features.""" |
| |
| for period in [8, 21, 55, 100, 200]: |
| df[f'ema_{period}'] = df['close'].ewm(span=period, adjust=False).mean() |
| df[f'ema_{period}_dist'] = (df['close'] - df[f'ema_{period}']) / df[f'ema_{period}'] |
| |
| |
| df['trend_strength'] = ( |
| (df['close'] > df['ema_21']).astype(float) + |
| (df['ema_21'] > df['ema_55']).astype(float) + |
| (df['ema_55'] > df['ema_100']).astype(float) + |
| (df['ema_100'] > df['ema_200']).astype(float) |
| ) / 4 |
| |
| |
| for window in [20, 50, 100]: |
| df[f'slope_{window}'] = self._rolling_slope(df['close'], window) |
| |
| |
| df['dx'] = self._compute_dx(df) |
| df['adx'] = df['dx'].rolling(14).mean() |
| |
| return df |
| |
| def _add_momentum_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add momentum indicators.""" |
| |
| for period in [7, 14, 21]: |
| df[f'rsi_{period}'] = self._compute_rsi(df['close'], period) |
| |
| |
| df['rsi_divergence'] = ( |
| df['return_24h'].rolling(24).corr(df['rsi_14'].diff(24)) |
| ) |
| |
| |
| for period in [14, 21]: |
| low_min = df['low'].rolling(period).min() |
| high_max = df['high'].rolling(period).max() |
| df[f'stoch_{period}'] = 100 * (df['close'] - low_min) / (high_max - low_min + 1e-8) |
| |
| |
| ema12 = df['close'].ewm(span=12, adjust=False).mean() |
| ema26 = df['close'].ewm(span=26, adjust=False).mean() |
| df['macd'] = ema12 - ema26 |
| df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean() |
| df['macd_hist'] = df['macd'] - df['macd_signal'] |
| df['macd_hist_change'] = df['macd_hist'].diff() |
| |
| |
| for period in [12, 24, 48]: |
| df[f'roc_{period}'] = (df['close'] / df['close'].shift(period) - 1) * 100 |
| |
| return df |
| |
| def _add_candlestick_patterns(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add candlestick pattern features.""" |
| |
| body = abs(df['close'] - df['open']) |
| range_ = df['high'] - df['low'] |
| df['body_ratio'] = body / (range_ + 1e-8) |
| |
| |
| upper_wick = df['high'] - df[['open', 'close']].max(axis=1) |
| lower_wick = df[['open', 'close']].min(axis=1) - df['low'] |
| df['upper_wick_ratio'] = upper_wick / (range_ + 1e-8) |
| df['lower_wick_ratio'] = lower_wick / (range_ + 1e-8) |
| |
| |
| df['bullish'] = (df['close'] > df['open']).astype(float) |
| |
| |
| df['consec_bullish'] = df['bullish'].rolling(5).sum() |
| df['consec_bearish'] = (1 - df['bullish']).rolling(5).sum() |
| |
| |
| prev_body = abs(df['close'].shift() - df['open'].shift()) |
| df['engulfing'] = ((body > prev_body * 1.5) & (df['bullish'] != df['bullish'].shift())).astype(float) |
| |
| return df |
| |
| def _add_support_resistance(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add support/resistance features.""" |
| |
| for period in [24, 72, 168]: |
| df[f'dist_high_{period}h'] = (df['high'].rolling(period).max() - df['close']) / df['close'] |
| df[f'dist_low_{period}h'] = (df['close'] - df['low'].rolling(period).min()) / df['close'] |
| |
| |
| df['breakout_high'] = (df['close'] > df['high'].rolling(48).max().shift()).astype(float) |
| df['breakout_low'] = (df['close'] < df['low'].rolling(48).min().shift()).astype(float) |
| |
| |
| high_72 = df['high'].rolling(72).max() |
| low_72 = df['low'].rolling(72).min() |
| df['position_in_range'] = (df['close'] - low_72) / (high_72 - low_72 + 1e-8) |
| |
| return df |
| |
| def _add_regime_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add market regime detection features.""" |
| |
| |
| df['trending_regime'] = (df['adx'] > 25).astype(float) |
| |
| |
| df['vol_expanding'] = (df['volatility_24h'] > df['volatility_24h'].shift(24)).astype(float) |
| |
| |
| mean_100 = df['close'].rolling(100).mean() |
| std_100 = df['close'].rolling(100).std() |
| df['zscore_100'] = (df['close'] - mean_100) / (std_100 + 1e-8) |
| |
| |
| short_trend = np.sign(df['return_4h']) |
| medium_trend = np.sign(df['return_24h']) |
| long_trend = np.sign(df['return_48h']) |
| df['trend_alignment'] = (short_trend + medium_trend + long_trend) / 3 |
| |
| return df |
| |
| def _add_volume_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add volume analysis features.""" |
| |
| df['vol_ma_24h'] = df['volume'].rolling(24).mean() |
| df['vol_ma_72h'] = df['volume'].rolling(72).mean() |
| df['vol_ma_168h'] = df['volume'].rolling(168).mean() |
| |
| |
| df['vol_ratio_24h'] = df['volume'] / (df['vol_ma_24h'] + 1e-8) |
| |
| |
| df['vol_trend'] = df['vol_ma_24h'] / (df['vol_ma_72h'] + 1e-8) |
| |
| |
| |
| df['pv_confirm'] = np.sign(df['return_1h']) * df['vol_ratio_24h'] |
| |
| |
| money_flow_mult = ((df['close'] - df['low']) - (df['high'] - df['close'])) / (df['high'] - df['low'] + 1e-8) |
| money_flow_vol = money_flow_mult * df['volume'] |
| df['ad_line'] = money_flow_vol.cumsum() |
| df['ad_line_norm'] = (df['ad_line'] - df['ad_line'].rolling(168).mean()) / (df['ad_line'].rolling(168).std() + 1e-8) |
| |
| return df |
| |
| def _add_multi_timeframe_features(self, df: pd.DataFrame) -> pd.DataFrame: |
| """Add higher timeframe context (simulated from hourly).""" |
| |
| df['close_4h'] = df['close'].rolling(4).apply(lambda x: x.iloc[-1] if len(x) == 4 else np.nan) |
| df['high_4h'] = df['high'].rolling(4).max() |
| df['low_4h'] = df['low'].rolling(4).min() |
| |
| |
| df['close_1d'] = df['close'].rolling(24).apply(lambda x: x.iloc[-1] if len(x) == 24 else np.nan) |
| df['high_1d'] = df['high'].rolling(24).max() |
| df['low_1d'] = df['low'].rolling(24).min() |
| |
| |
| df['rsi_4h'] = self._compute_rsi(df['close'].rolling(4).mean(), 14) |
| df['rsi_1d'] = self._compute_rsi(df['close'].rolling(24).mean(), 14) |
| |
| |
| ema_20_4h = df['close'].rolling(4).mean().ewm(span=20, adjust=False).mean() |
| df['trend_4h'] = ((df['close'].rolling(4).mean() > ema_20_4h) * 2 - 1).astype(float) |
| |
| return df |
| |
| def _rolling_slope(self, series: pd.Series, window: int) -> pd.Series: |
| """Compute rolling linear regression slope.""" |
| def calc_slope(x): |
| if len(x) < window: |
| return np.nan |
| y = np.array(x) |
| x_arr = np.arange(len(y)) |
| slope = np.polyfit(x_arr, y, 1)[0] |
| return slope / y.mean() |
| |
| return series.rolling(window).apply(calc_slope, raw=False) |
| |
| def _compute_rsi(self, series: pd.Series, period: int) -> pd.Series: |
| """Compute RSI indicator.""" |
| delta = series.diff() |
| gain = delta.where(delta > 0, 0).rolling(period).mean() |
| loss = (-delta.where(delta < 0, 0)).rolling(period).mean() |
| rs = gain / (loss + 1e-8) |
| return 100 - (100 / (1 + rs)) |
| |
| def _compute_dx(self, df: pd.DataFrame) -> pd.Series: |
| """Compute Directional Index for ADX.""" |
| high = df['high'] |
| low = df['low'] |
| close = df['close'] |
| |
| plus_dm = high.diff() |
| minus_dm = -low.diff() |
| |
| plus_dm[plus_dm < 0] = 0 |
| minus_dm[minus_dm < 0] = 0 |
| |
| tr = pd.concat([ |
| high - low, |
| abs(high - close.shift()), |
| abs(low - close.shift()) |
| ], axis=1).max(axis=1) |
| |
| atr = tr.rolling(14).mean() |
| plus_di = 100 * (plus_dm.rolling(14).mean() / (atr + 1e-8)) |
| minus_di = 100 * (minus_dm.rolling(14).mean() / (atr + 1e-8)) |
| |
| dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di + 1e-8) |
| return dx |
| |
| def get_feature_columns(self) -> List[str]: |
| """Return list of feature column names.""" |
| return [ |
| |
| 'return_1h', 'return_4h', 'return_12h', 'return_24h', 'return_48h', |
| 'cumret_24h', 'cumret_7d', |
| |
| 'volatility_24h', 'volatility_168h', 'vol_ratio', 'atr_percent', 'vol_regime', |
| |
| 'ema_21_dist', 'ema_55_dist', 'ema_100_dist', 'ema_200_dist', |
| 'trend_strength', 'slope_20', 'slope_50', 'adx', |
| |
| 'rsi_7', 'rsi_14', 'rsi_21', 'rsi_divergence', |
| 'stoch_14', 'stoch_21', |
| 'macd_hist', 'macd_hist_change', |
| 'roc_12', 'roc_24', 'roc_48', |
| |
| 'body_ratio', 'upper_wick_ratio', 'lower_wick_ratio', |
| 'consec_bullish', 'consec_bearish', 'engulfing', |
| |
| 'dist_high_24h', 'dist_low_24h', 'dist_high_72h', 'dist_low_72h', |
| 'breakout_high', 'breakout_low', 'position_in_range', |
| |
| 'trending_regime', 'vol_expanding', 'zscore_100', 'trend_alignment', |
| |
| 'vol_ratio_24h', 'vol_trend', 'pv_confirm', 'ad_line_norm', |
| |
| 'rsi_4h', 'rsi_1d', 'trend_4h', |
| ] |
|
|