161 lines
6.5 KiB
Python
161 lines
6.5 KiB
Python
import pandas as pd
|
|
import numpy as np
|
|
import ta
|
|
import talib
|
|
from sklearn.ensemble import RandomForestRegressor
|
|
from sklearn.feature_selection import SelectFromModel
|
|
from sklearn.preprocessing import LabelEncoder
|
|
from scipy.stats import ks_2samp
|
|
from collections import deque
|
|
|
|
from utils import log_and_print
|
|
|
|
# Global variable to track drift history
|
|
drift_window = deque(maxlen=10)
|
|
|
|
def identify_market_structure(data):
|
|
"""Identifies swing highs/lows and market structure (HH, HL, LH, LL)."""
|
|
data['swing_high'] = (data['high'].shift(1) < data['high']) & (data['high'].shift(-1) < data['high'])
|
|
data['swing_low'] = (data['low'].shift(1) > data['low']) & (data['low'].shift(-1) > data['low'])
|
|
|
|
swing_highs = data[data['swing_high']]
|
|
swing_lows = data[data['swing_low']]
|
|
|
|
data['HH'] = (data['swing_high']) & (data['high'] > swing_highs['high'].shift(1))
|
|
data['HL'] = (data['swing_low']) & (data['low'] > swing_lows['low'].shift(1))
|
|
data['LH'] = (data['swing_high']) & (data['high'] < swing_highs['high'].shift(1))
|
|
data['LL'] = (data['swing_low']) & (data['low'] < swing_lows['low'].shift(1))
|
|
|
|
# Fill NaN values
|
|
for col in ['swing_high', 'swing_low', 'HH', 'HL', 'LH', 'LL']:
|
|
data[col] = data[col].fillna(False)
|
|
return data
|
|
|
|
def identify_candlestick_patterns(data):
|
|
"""Identifies common candlestick patterns."""
|
|
patterns = {
|
|
'engulfing': talib.CDLENGULFING,
|
|
'doji': talib.CDLDOJI,
|
|
'hammer': talib.CDLHAMMER,
|
|
'shooting_star': talib.CDLSHOOTINGSTAR
|
|
}
|
|
for name, pattern_func in patterns.items():
|
|
data[name] = pattern_func(data['open'], data['high'], data['low'], data['close'])
|
|
return data
|
|
|
|
def identify_trends(data):
|
|
"""Identifies the current trend based on market structure."""
|
|
if 'HH' not in data.columns or 'HL' not in data.columns or 'LH' not in data.columns or 'LL' not in data.columns:
|
|
log_and_print("Warning: Market structure columns (HH, HL, etc.) are missing. Cannot identify trends.", is_error=True)
|
|
data['trend'] = 'sideways'
|
|
else:
|
|
data['trend'] = np.where(data['HH'] & data['HL'], 'uptrend',
|
|
np.where(data['LH'] & data['LL'], 'downtrend', 'sideways'))
|
|
return data
|
|
|
|
def detect_breakouts(data, window=20):
|
|
"""Detects breakouts from recent highs and lows."""
|
|
data['breakout'] = np.logical_or(
|
|
np.greater(data['close'], data['high'].rolling(window=window).max().shift(1)),
|
|
np.less(data['close'], data['low'].rolling(window=window).min().shift(1))
|
|
)
|
|
return data
|
|
|
|
def add_indicators(data):
|
|
"""Adds a set of technical indicators to the dataframe."""
|
|
# Add basic indicators
|
|
data['RSI'] = ta.momentum.rsi(data['close'], window=14)
|
|
data['MACD'] = ta.trend.macd(data['close'])
|
|
data['MACD_signal'] = ta.trend.macd_signal(data['close'])
|
|
data['Bollinger_high'] = ta.volatility.bollinger_hband(data['close'])
|
|
data['Bollinger_low'] = ta.volatility.bollinger_lband(data['close'])
|
|
data['MA20'] = data['close'].rolling(window=20).mean()
|
|
data['MA50'] = data['close'].rolling(window=50).mean()
|
|
data['MA200'] = data['close'].rolling(window=200).mean()
|
|
data['volatility_atr'] = ta.volatility.average_true_range(data['high'], data['low'], data['close'])
|
|
|
|
# Add advanced indicators
|
|
data['ichimoku_a'] = ta.trend.ichimoku_a(data['high'], data['low'])
|
|
data['ichimoku_b'] = ta.trend.ichimoku_b(data['high'], data['low'])
|
|
data['adx'] = ta.trend.adx(data['high'], data['low'], data['close'])
|
|
if 'tick_volume' in data.columns:
|
|
data['vwap'] = ta.volume.volume_weighted_average_price(data['high'], data['low'], data['close'], data['tick_volume'])
|
|
|
|
return data
|
|
|
|
def detect_market_regime(data):
|
|
"""Detects market regime (trending or ranging) based on ADX."""
|
|
try:
|
|
adx = data.get('adx', ta.trend.adx(data['high'], data['low'], data['close']))
|
|
data['regime'] = np.where(adx > 25, 'trending', 'ranging')
|
|
except Exception as e:
|
|
log_and_print(f"Error in detect_market_regime: {e}", is_error=True)
|
|
data['regime'] = 'ranging' # Default to 'ranging'
|
|
return data
|
|
|
|
def add_all_features(df):
|
|
"""A master function to add all features to the dataframe."""
|
|
df = add_indicators(df)
|
|
df = identify_market_structure(df)
|
|
df = identify_candlestick_patterns(df)
|
|
df = identify_trends(df)
|
|
df = detect_breakouts(df)
|
|
df = detect_market_regime(df)
|
|
df = df.fillna(0)
|
|
return df
|
|
|
|
def select_features(X, y, max_features=20):
|
|
"""Selects the best features using a RandomForestRegressor."""
|
|
estimator = RandomForestRegressor(n_estimators=100, random_state=42)
|
|
selector = SelectFromModel(estimator=estimator, threshold=-np.inf, max_features=max_features).fit(X, y)
|
|
selected_features = X.columns[selector.get_support()]
|
|
log_and_print(f"Selected Features: {list(selected_features)}")
|
|
return selected_features
|
|
|
|
def encode_categorical(df):
|
|
"""Encodes categorical columns like 'trend' and 'regime'."""
|
|
le = LabelEncoder()
|
|
for col in ['trend', 'regime']:
|
|
if col in df.columns:
|
|
df[col] = df[col].astype(str)
|
|
df[col] = le.fit_transform(df[col])
|
|
return df
|
|
|
|
def calculate_future_returns(df):
|
|
"""Calculates future returns for training the model."""
|
|
df["future_returns"] = df["close"].pct_change().shift(-1)
|
|
return df.dropna()
|
|
|
|
def ensure_same_columns(df1, df2):
|
|
"""Ensures two DataFrames have the same columns in the same order."""
|
|
missing_in_df1 = set(df2.columns) - set(df1.columns)
|
|
for c in missing_in_df1:
|
|
df1[c] = 0
|
|
missing_in_df2 = set(df1.columns) - set(df2.columns)
|
|
for c in missing_in_df2:
|
|
df2[c] = 0
|
|
return df1[df2.columns], df2
|
|
|
|
def detect_feature_drift(current_df, reference_df):
|
|
"""Detects drift between current data and a reference dataset."""
|
|
current_df, reference_df = ensure_same_columns(current_df.copy(), reference_df.copy())
|
|
|
|
drift_features = []
|
|
for column in current_df.columns:
|
|
if column not in reference_df.columns:
|
|
continue
|
|
stat, p_value = ks_2samp(current_df[column].dropna(), reference_df[column].dropna())
|
|
if p_value < 0.05:
|
|
drift_features.append(column)
|
|
|
|
drift_detected = 1 if drift_features else 0
|
|
drift_window.append(drift_detected)
|
|
|
|
drift_ratio = sum(drift_window) / len(drift_window) if drift_window else 0
|
|
log_and_print(f"Rolling drift ratio: {drift_ratio:.2f}")
|
|
|
|
if drift_ratio > 0.5:
|
|
log_and_print(f"Significant feature drift detected in: {drift_features}")
|
|
return drift_features
|
|
else:
|
|
return [] |