Trad / ml_engine /indicators.py
Riy777's picture
Update ml_engine/indicators.py
e173390
# ml_engine/indicators.py (V10.2 - Anti-Fragile FIX)
import pandas as pd
import pandas_ta as ta
import numpy as np
from typing import Dict
try:
from hurst import compute_Hc
HURST_AVAILABLE = True
except ImportError:
print("⚠️ مكتبة 'hurst' غير موجودة. ميزة 'مفتاح النظام' ستكون معطلة.")
print(" -> قم بتثبيتها: pip install hurst")
HURST_AVAILABLE = False
class AdvancedTechnicalAnalyzer:
def __init__(self):
# (هذا الكونفيغ سيبقى للاستخدامات القديمة مثل الحارس 1m)
self.indicators_config = {
'trend': ['ema_9', 'ema_21', 'ema_50', 'ema_200', 'ichimoku', 'adx', 'parabolic_sar', 'dmi'],
'momentum': ['rsi', 'stoch_rsi', 'macd', 'williams_r', 'cci', 'awesome_oscillator', 'momentum'],
'volatility': ['bbands', 'atr', 'keltner', 'donchian', 'rvi'],
'volume': ['vwap', 'obv', 'mfi', 'volume_profile', 'ad', 'volume_oscillator'],
'cycle': ['hull_ma', 'supertrend', 'zigzag', 'fisher_transform']
}
# 🔴 --- START OF UPDATED FUNCTION (V10.2 - Anti-Fragile) --- 🔴
def calculate_v9_smart_features(self, dataframe: pd.DataFrame) -> Dict[str, float]:
"""
(محدث V10.2) - (إصلاح خطأ "الميزات المفقودة" بشكل نهائي)
- استخدام "قالب الميزات" لضمان إرجاع جميع الميزات دائماً.
"""
if dataframe.empty or dataframe is None or len(dataframe) < 100:
return {}
# --- (V10.2) الخطوة 1: تعريف "قالب الميزات" بالقيم الافتراضية ---
# (هذه هي جميع الميزات التي تدرب عليها نموذج V9.8)
features = {
'price_to_ema_50': 0.0, 'price_to_ema_200': 0.0, 'price_to_min_100': 0.0,
'price_to_max_100': 0.0, 'slope_14_50': 0.0, 'adx_slope': 0.0,
'volume_zscore_50': 0.0, 'vwap_gap': 0.0, 'cmf_20': 0.0, 'vroc_12': 0.0,
'obv_slope': 0.0, 'rsi_14': 50.0, 'rsi_mean_10': 50.0, 'rsi_std_10': 0.0,
'mfi_14': 50.0, 'mfi_mean_10': 50.0, 'adx_14': 20.0, 'atr_percent': 0.0,
'vol_of_vol': 0.0, 'atr_normalized_return': 0.0, 'hurst': 0.5,
'ppo_hist': 0.0, 'ppo_line': 0.0
# (ميزات مونت كارلو ستضاف لاحقاً في data_manager)
}
try:
# --- جلب البيانات الأساسية (Series) ---
close = dataframe['close']
high = dataframe['high']
low = dataframe['low']
volume = dataframe['volume']
current_price = close.iloc[-1]
# --- 1. حساب مؤشرات السلسلة الكاملة (Series) ---
rsi_series = ta.rsi(close, length=14)
mfi_series = ta.mfi(high, low, close, volume, length=14)
atr_series = ta.atr(high, low, close, length=14)
adx_data = ta.adx(high, low, close, length=14)
obv_series = ta.obv(close, volume)
# --- 2. ميزات "نسب السعر" (Price Ratios) ---
try:
ema_50 = ta.ema(close, length=50).iloc[-1]
ema_200 = ta.ema(close, length=200).iloc[-1]
if ema_50 and ema_50 > 0: features['price_to_ema_50'] = (current_price / ema_50) - 1
if ema_200 and ema_200 > 0: features['price_to_ema_200'] = (current_price / ema_200) - 1
min_100 = low.tail(100).min(); max_100 = high.tail(100).max()
if min_100 and min_100 > 0: features['price_to_min_100'] = (current_price / min_100) - 1
if max_100 and max_100 > 0: features['price_to_max_100'] = (current_price / max_100) - 1
ema_14 = ta.ema(close, length=14).iloc[-1]
if ema_14 and ema_50: features['slope_14_50'] = (ema_14 - ema_50) / 14
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
# --- 3. ميزات "الميل" (Slope) ---
try:
if adx_data is not None and not adx_data.empty:
adx_series = adx_data['ADX_14']
if adx_series is not None and not adx_series.empty:
adx_ema_5 = ta.ema(adx_series, length=5).iloc[-1]; adx_ema_15 = ta.ema(adx_series, length=15).iloc[-1]
if adx_ema_5 and adx_ema_15: features['adx_slope'] = (adx_ema_5 - adx_ema_15) / 5
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
# --- 4. ميزات "الحجم" (Volume) و "السيولة" ---
try:
vol_ma_50 = volume.tail(50).mean(); vol_std_50 = volume.tail(50).std()
if vol_std_50 and vol_std_50 > 0: features['volume_zscore_50'] = (volume.iloc[-1] - vol_ma_50) / vol_std_50
vwap = ta.vwap(high, low, close, volume).iloc[-1]
if vwap and vwap > 0: features['vwap_gap'] = (current_price - vwap) / vwap
cmf = ta.cmf(high, low, close, volume, length=20)
if cmf is not None and not cmf.empty: features['cmf_20'] = cmf.iloc[-1]
vroc = ta.roc(volume, length=12)
if vroc is not None and not vroc.empty: features['vroc_12'] = vroc.iloc[-1]
if obv_series is not None and not obv_series.empty:
obv_ema_10 = ta.ema(obv_series, length=10).iloc[-1]; obv_ema_30 = ta.ema(obv_series, length=30).iloc[-1]
if obv_ema_10 and obv_ema_30: features['obv_slope'] = (obv_ema_10 - obv_ema_30) / 10
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
# --- 5. ميزات "تجميعية" (Aggregative) ---
try:
if rsi_series is not None and not rsi_series.empty:
features['rsi_14'] = rsi_series.iloc[-1]; features['rsi_mean_10'] = rsi_series.tail(10).mean(); features['rsi_std_10'] = rsi_series.tail(10).std()
if mfi_series is not None and not mfi_series.empty:
features['mfi_14'] = mfi_series.iloc[-1]; features['mfi_mean_10'] = mfi_series.tail(10).mean()
if adx_data is not None and not adx_data.empty:
adx_val = adx_data['ADX_14'].iloc[-1]
if adx_val is not None: features['adx_14'] = adx_val
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
# --- 6. ميزات "التقلب" (Volatility) ---
try:
atr_val = None
if atr_series is not None and not atr_series.empty:
atr_val = atr_series.iloc[-1]
if atr_val and current_price > 0: features['atr_percent'] = (atr_val / current_price) * 100
vol_of_vol_series = ta.atr(atr_series, length=10) # (Vol-of-Vol)
if vol_of_vol_series is not None and not vol_of_vol_series.empty: features['vol_of_vol'] = vol_of_vol_series.iloc[-1]
last_return = close.pct_change().iloc[-1]
if atr_val and atr_val > 0:
features['atr_normalized_return'] = last_return / atr_val
# (لا نحتاج else، القيمة الافتراضية 0.0 موجودة)
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
# --- 7. ميزات النظام (Regime Features) ---
try:
if HURST_AVAILABLE:
hurst_series = close.tail(100).to_numpy()
H, c, data = compute_Hc(hurst_series, kind='price', simplified=True)
features['hurst'] = H
# (لا نحتاج else، القيمة الافتراضية 0.5 موجودة)
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
try:
ppo_data = ta.ppo(close, fast=12, slow=26, signal=9)
if ppo_data is not None and not ppo_data.empty:
features['ppo_hist'] = ppo_data['PPOh_12_26_9'].iloc[-1]
features['ppo_line'] = ppo_data['PPO_12_26_9'].iloc[-1]
except Exception: pass # (فشل هذا الجزء، ستبقى القيم الافتراضية)
except Exception as e:
# (فشل كبير، سنعيد القالب الافتراضي)
# print(f"⚠️ خطأ كارثي في حساب ميزات V9.8: {e}");
pass
# (تنظيف نهائي للتأكد من عدم وجود NaN/Inf)
for key, value in features.items():
if not np.isfinite(value):
features[key] = 0.0 # (إعادة التعيين إلى 0.0 إذا كان الحساب NaN)
return features
# 🔴 --- END OF UPDATED FUNCTION (V10.2) --- 🔴
# -----------------------------------------------------------------
# --- (الدوال القديمة تبقى كما هي للاستخدامات الأخرى مثل Sentry 1m) ---
# -----------------------------------------------------------------
def calculate_all_indicators(self, dataframe, timeframe):
if dataframe.empty or dataframe is None: return {}
indicators = {}
try:
indicators.update(self._calculate_trend_indicators(dataframe))
indicators.update(self._calculate_momentum_indicators(dataframe))
indicators.update(self._calculate_volatility_indicators(dataframe))
indicators.update(self._calculate_volume_indicators(dataframe, timeframe))
indicators.update(self._calculate_cycle_indicators(dataframe))
except Exception as e:
print(f"⚠️ خطأ في حساب المؤشرات لـ {timeframe}: {e}")
return indicators
def _calculate_trend_indicators(self, dataframe):
trend = {};
try:
if dataframe is None or dataframe.empty or 'close' not in dataframe.columns: return {};
if len(dataframe) >= 9:
ema_9 = ta.ema(dataframe['close'], length=9);
if ema_9 is not None and not ema_9.empty and not pd.isna(ema_9.iloc[-1]): trend['ema_9'] = float(ema_9.iloc[-1]);
if len(dataframe) >= 21:
ema_21 = ta.ema(dataframe['close'], length=21);
if ema_21 is not None and not ema_21.empty and not pd.isna(ema_21.iloc[-1]): trend['ema_21'] = float(ema_21.iloc[-1]);
if len(dataframe) >= 50:
ema_50 = ta.ema(dataframe['close'], length=50);
if ema_50 is not None and not ema_50.empty and not pd.isna(ema_50.iloc[-1]): trend['ema_50'] = float(ema_50.iloc[-1]);
if len(dataframe) >= 200:
ema_200 = ta.ema(dataframe['close'], length=200);
if ema_200 is not None and not ema_200.empty and not pd.isna(ema_200.iloc[-1]): trend['ema_200'] = float(ema_200.iloc[-1]);
if len(dataframe) >= 26:
try:
ichimoku = ta.ichimoku(dataframe['high'], dataframe['low'], dataframe['close']);
if ichimoku is not None and len(ichimoku) > 0:
conversion_line = ichimoku[0].get('ITS_9') if ichimoku[0] is not None else None;
base_line = ichimoku[0].get('IKS_26') if ichimoku[0] is not None else None;
if conversion_line is not None and not conversion_line.empty and not pd.isna(conversion_line.iloc[-1]): trend['ichimoku_conversion'] = float(conversion_line.iloc[-1]);
if base_line is not None and not base_line.empty and not pd.isna(base_line.iloc[-1]): trend['ichimoku_base'] = float(base_line.iloc[-1]);
except Exception as ichimoku_error: pass;
if len(dataframe) >= 14:
try:
adx_result = ta.adx(dataframe['high'], dataframe['low'], dataframe['close'], length=14);
if adx_result is not None and not adx_result.empty:
adx_value = adx_result.get('ADX_14');
if adx_value is not None and not adx_value.empty and not pd.isna(adx_value.iloc[-1]): trend['adx'] = float(adx_value.iloc[-1]);
except Exception as adx_error: pass;
except Exception as e: pass;
return {key: value for key, value in trend.items() if value is not None and not np.isnan(value)};
def _calculate_momentum_indicators(self, dataframe):
momentum = {};
try:
if dataframe is None or dataframe.empty or 'close' not in dataframe.columns: return {};
if len(dataframe) >= 14:
rsi = ta.rsi(dataframe['close'], length=14);
if rsi is not None and not rsi.empty and not pd.isna(rsi.iloc[-1]): momentum['rsi'] = float(rsi.iloc[-1]);
if len(dataframe) >= 26:
macd = ta.macd(dataframe['close']);
if macd is not None and not macd.empty:
macd_hist = macd.get('MACDh_12_26_9');
macd_line = macd.get('MACD_12_26_9');
if macd_hist is not None and not macd_hist.empty and not pd.isna(macd_hist.iloc[-1]): momentum['macd_hist'] = float(macd_hist.iloc[-1]);
if macd_line is not None and not macd_line.empty and not pd.isna(macd_line.iloc[-1]): momentum['macd_line'] = float(macd_line.iloc[-1]);
if len(dataframe) >= 14:
stoch_rsi = ta.stochrsi(dataframe['close'], length=14);
if stoch_rsi is not None and not stoch_rsi.empty:
stoch_k = stoch_rsi.get('STOCHRSIk_14_14_3_3');
if stoch_k is not None and not stoch_k.empty and not pd.isna(stoch_k.iloc[-1]): momentum['stoch_rsi_k'] = float(stoch_k.iloc[-1]);
if len(dataframe) >= 14:
williams = ta.willr(dataframe['high'], dataframe['low'], dataframe['close'], length=14);
if williams is not None and not williams.empty and not pd.isna(williams.iloc[-1]): momentum['williams_r'] = float(williams.iloc[-1]);
except Exception as e: pass;
return {key: value for key, value in momentum.items() if value is not None and not np.isnan(value)};
def _calculate_volatility_indicators(self, dataframe):
volatility = {};
try:
if dataframe is None or dataframe.empty or 'close' not in dataframe.columns: return {};
if len(dataframe) >= 20:
bollinger_bands = ta.bbands(dataframe['close'], length=20, std=2);
if bollinger_bands is not None and not bollinger_bands.empty:
bb_lower = bollinger_bands.get('BBL_20_2.0'); bb_upper = bollinger_bands.get('BBU_20_2.0'); bb_middle = bollinger_bands.get('BBM_20_2.0');
if bb_lower is not None and not bb_lower.empty and not pd.isna(bb_lower.iloc[-1]): volatility['bb_lower'] = float(bb_lower.iloc[-1]);
if bb_upper is not None and not bb_upper.empty and not pd.isna(bb_upper.iloc[-1]): volatility['bb_upper'] = float(bb_upper.iloc[-1]);
if bb_middle is not None and not bb_middle.empty and not pd.isna(bb_middle.iloc[-1]): volatility['bb_middle'] = float(bb_middle.iloc[-1]);
if len(dataframe) >= 14:
average_true_range = ta.atr(dataframe['high'], dataframe['low'], dataframe['close'], length=14);
if average_true_range is not None and not average_true_range.empty and not pd.isna(average_true_range.iloc[-1]):
atr_value = float(average_true_range.iloc[-1]); volatility['atr'] = atr_value;
current_close = dataframe['close'].iloc[-1] if not dataframe['close'].empty else 0;
if atr_value and current_close > 0: volatility['atr_percent'] = (atr_value / current_close) * 100;
except Exception as e: pass;
return {key: value for key, value in volatility.items() if value is not None and not np.isnan(value)};
def _calculate_volume_indicators(self, dataframe, timeframe):
volume = {};
try:
if dataframe is None or dataframe.empty or 'close' not in dataframe.columns or 'volume' not in dataframe.columns: return {};
if len(dataframe) >= 1:
try:
df_vwap = dataframe.copy();
if not isinstance(df_vwap.index, pd.DatetimeIndex):
if 'timestamp' in df_vwap.columns:
df_vwap['timestamp'] = pd.to_datetime(df_vwap['timestamp'], unit='ms'); df_vwap.set_index('timestamp', inplace=True);
elif not df_vwap.index.is_numeric():
df_vwap.index = pd.to_datetime(df_vwap.index, unit='ms');
else:
raise ValueError("DataFrame needs 'timestamp' column or DatetimeIndex");
df_vwap.sort_index(inplace=True);
volume_weighted_average_price = ta.vwap(high=df_vwap['high'], low=df_vwap['low'], close=df_vwap['close'], volume=df_vwap['volume']);
if volume_weighted_average_price is not None and not volume_weighted_average_price.empty and not pd.isna(volume_weighted_average_price.iloc[-1]): volume['vwap'] = float(volume_weighted_average_price.iloc[-1]);
except Exception as vwap_error:
if "VWAP requires an ordered DatetimeIndex" not in str(vwap_error) and "Index" not in str(vwap_error): pass;
if len(dataframe) >= 20:
try:
typical_price = (dataframe['high'] + dataframe['low'] + dataframe['close']) / 3;
vwap_simple = (typical_price * dataframe['volume']).sum() / dataframe['volume'].sum();
if not np.isnan(vwap_simple): volume['vwap'] = float(vwap_simple);
except Exception as simple_vwap_error: pass;
try:
on_balance_volume = ta.obv(dataframe['close'], dataframe['volume']);
if on_balance_volume is not None and not on_balance_volume.empty and not pd.isna(on_balance_volume.iloc[-1]): volume['obv'] = float(on_balance_volume.iloc[-1]);
except Exception as obv_error: pass;
if len(dataframe) >= 14:
try:
money_flow_index = ta.mfi(dataframe['high'], dataframe['low'], dataframe['close'], dataframe['volume'], length=14);
if money_flow_index is not None and not money_flow_index.empty and not pd.isna(money_flow_index.iloc[-1]): volume['mfi'] = float(money_flow_index.iloc[-1]);
except Exception as mfi_error: pass;
if len(dataframe) >= 20:
try:
volume_avg_20 = float(dataframe['volume'].tail(20).mean());
current_volume = float(dataframe['volume'].iloc[-1]) if not dataframe['volume'].empty else 0;
if volume_avg_20 and volume_avg_20 > 0 and current_volume > 0:
volume_ratio = current_volume / volume_avg_20;
if not np.isnan(volume_ratio): volume['volume_ratio'] = volume_ratio;
except Exception as volume_error: pass;
except Exception as e: pass;
return {key: value for key, value in volume.items() if value is not None and not np.isnan(value)};
def _calculate_cycle_indicators(self, dataframe):
cycle = {};
try:
if dataframe is None or dataframe.empty or 'close' not in dataframe.columns: return {};
if len(dataframe) >= 9:
hull_moving_average = ta.hma(dataframe['close'], length=9);
if hull_moving_average is not None and not hull_moving_average.empty and not pd.isna(hull_moving_average.iloc[-1]): cycle['hull_ma'] = float(hull_moving_average.iloc[-1]);
if len(dataframe) >= 10:
supertrend = ta.supertrend(dataframe['high'], dataframe['low'], dataframe['close'], length=10, multiplier=3);
if supertrend is not None and not supertrend.empty:
supertrend_value = supertrend.get('SUPERT_10_3.0');
if supertrend_value is not None and not supertrend_value.empty and not pd.isna(supertrend_value.iloc[-1]): cycle['supertrend'] = float(supertrend_value.iloc[-1]);
except Exception as e: pass;
return {key: value for key, value in cycle.items() if value is not None and not np.isnan(value)};
print("✅ ML Module: Technical Indicators loaded (V10.2 - Anti-Fragile FIX)")