import os
# جلوگیری از کرش‌های خاموش سطح پردازنده
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'

import time
import warnings
import gc
import traceback
import requests
from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import pandas_ta_classic as ta

import tensorflow as tf
from tensorflow.keras import layers, models

warnings.filterwarnings('ignore')

# =========================================================
# 1. تنظیمات اصلی
# =========================================================
# مسیر اصلاح شد: آدرس پوشه روی سرور شما
MODELS_DIR = r"C:\Users\Administrator\Desktop\forcasting"
SYMBOLS = ['XAUUSD', 'EURUSD', 'BTCUSD', 'GBPUSD', 'USDJPY', 'USDCNH', 'AUDUSD', 'WTI', 'DXY']
TARGET_SYMBOL = 'XAUUSD'
TIMEFRAMES = ['m1', 'm5', 'm15']
SEQ_LENGTH = 40

# افزایش طول دیتای خام برای محاسبه صحیح اندیکاتور Hurst
WARMUP_CANDLES = 450 
NUM_FEATURES = 13 

# --- TELEGRAM SETTINGS ---
TELEGRAM_TOKEN = "8831148493:AAHAbFGYBiGoouxX6OPw2Rj036QX6jxcJbk"

TELEGRAM_CHATS = {
    'm1': "-1004336538647",
    'm5': "-1003933147540",
    'm15': "-1004338034306"
}

def send_telegram_message(text, chat_id):
    if not TELEGRAM_TOKEN or not chat_id:
        return
    url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
    payload = {
        "chat_id": chat_id,
        "text": text,
        "parse_mode": "HTML"
    }
    try:
        response = requests.post(url, json=payload, timeout=5)
        if response.status_code != 200:
            print(f"[WARNING] Telegram Error: {response.text}")
    except Exception as e:
        print(f"[WARNING] Telegram Connection Error: {e}")

# =========================================================
# 2. لایه‌های سفارشی
# =========================================================
@tf.keras.utils.register_keras_serializable(package="Custom", name="AdaptiveLassoFeatureGate")
class AdaptiveLassoFeatureGate(layers.Layer):
    def __init__(self, l1_penalty=1e-5, **kwargs):
        super().__init__(**kwargs)
        self.l1_penalty = l1_penalty

    def build(self, input_shape):
        self.gate_weights = self.add_weight(
            name='dynamic_feature_gate', shape=(1, 1, 1, input_shape[-1]), 
            initializer=tf.keras.initializers.Constant(2.0), trainable=True
        )

    def call(self, inputs):
        activation_mask = tf.nn.sigmoid(self.gate_weights)
        return inputs * activation_mask

@tf.keras.utils.register_keras_serializable(package="Custom", name="ResidualSpatioTemporalGCN")
class ResidualSpatioTemporalGCN(layers.Layer):
    def __init__(self, units, dropout_rate=0.15, **kwargs):
        super().__init__(**kwargs)
        self.units = units
        self.dropout_rate = dropout_rate

    def build(self, input_shape):
        node_feat_dim = input_shape[0][-1]
        self.W = self.add_weight(
            shape=(node_feat_dim, self.units), initializer='glorot_uniform',
            name='gcn_kernel', trainable=True
        )
        self.res_linear = layers.Dense(self.units) if node_feat_dim != self.units else lambda x: x
        self.layer_norm = layers.LayerNormalization(epsilon=1e-6)
        self.dropout = layers.Dropout(self.dropout_rate)

    def call(self, inputs, training=False):
        X, A = inputs 
        X_W = tf.tensordot(X, self.W, axes=[[3], [0]]) 
        H = tf.einsum('bsij,bsjk->bsik', A, X_W)
        residual = self.res_linear(X)
        activated_out = tf.nn.swish(H + residual)
        normalized_out = self.layer_norm(activated_out)
        return self.dropout(normalized_out, training=training)

# =========================================================
# 3. ساختار شبکه عصبی
# =========================================================
def build_upgraded_gnn(seq_len, num_nodes, num_features):
    input_x = layers.Input(shape=(seq_len, num_nodes, num_features), name="Node_Features")
    input_e = layers.Input(shape=(seq_len, num_nodes, num_nodes), name="Edge_Matrix")
    
    x_filtered = AdaptiveLassoFeatureGate(l1_penalty=1e-5)(input_x)
    global_market_context = layers.Lambda(lambda x: tf.reduce_mean(x, axis=2), name="Global_Context")(x_filtered)
   
    gcn_out = ResidualSpatioTemporalGCN(units=32, dropout_rate=0.15)([x_filtered, input_e])
    
    gold_target_node = layers.Lambda(lambda x: x[:, :, 0, :], name="Gold_Target_GCN")(gcn_out)
    gold_raw_features = layers.Lambda(lambda x: x[:, :, 0, :], name="Gold_Target_Raw")(x_filtered)
    
    merged_representation = layers.Concatenate(axis=-1)([gold_target_node, gold_raw_features, global_market_context])
    
    attn_out = layers.MultiHeadAttention(num_heads=4, key_dim=24)(merged_representation, merged_representation)
    merged_representation = layers.LayerNormalization(epsilon=1e-6)(merged_representation + attn_out)
    
    conv_1 = layers.Conv1D(filters=64, kernel_size=3, padding='causal', activation='swish')(merged_representation)
    conv_2 = layers.Conv1D(filters=64, kernel_size=3, dilation_rate=2, padding='causal', activation='swish')(conv_1)
    
    temporal_vector = layers.Bidirectional(layers.GRU(48, return_sequences=False))(conv_2)
    
    dense_layer = layers.Dense(64, activation='swish')(temporal_vector) 
    dense_layer = layers.BatchNormalization()(dense_layer)
    dense_layer = layers.Dropout(0.2)(dense_layer)
    
    outputs = layers.Dense(1, activation='linear', name="Slope_Output")(dense_layer)
    
    model = models.Model(inputs=[input_x, input_e], outputs=outputs)
    return model

# =========================================================
# 4. بارگذاری ایمن مدل‌ها
# =========================================================
def load_all_models_weights():
    loaded_models = {'m1': [], 'm5': [], 'm15': []}
    stages = ['40pct', '60pct', '80pct', '100pct']
    seeds = [42, 123, 777]
    
    print(f"\n[INFO] Building Graphs and Injecting Weights from {MODELS_DIR}...")
    
    for tf_name in TIMEFRAMES:
        print(f"\n---> Loading weights for {tf_name.upper()}:")
        for st in stages:
            for sd in seeds:
                file_name = f"gnn_{tf_name}_{st}_seed{sd}.keras"
                full_path = os.path.join(MODELS_DIR, file_name)
                
                if os.path.exists(full_path):
                    try:
                        # اول معماری ساخته می‌شود
                        model = build_upgraded_gnn(SEQ_LENGTH, len(SYMBOLS), NUM_FEATURES)
                        # سپس فقط وزن‌ها لود می‌شوند
                        model.load_weights(full_path)
                        loaded_models[tf_name].append(model)
                        print(f"     [+] Loaded {file_name} successfully.")
                    except Exception as e:
                        print(f"     [-] FAILED on {file_name}: {e}")
                        
        print(f" -> Active models for {tf_name.upper()}: {len(loaded_models[tf_name])}")
        
    return loaded_models

# =========================================================
# 5. استخراج دیتا و فیچرها
# =========================================================
# متغیر سراسری برای ذخیره زمان آخرین تیک دریافتی
_LAST_TICK_MSC = 0

def check_is_market_open(symbol=TARGET_SYMBOL, max_delay_seconds=10):
    global _LAST_TICK_MSC
    
    tick = mt5.symbol_info_tick(symbol)
    if tick is None:
        return False
        
    # اگر زمان تیک فعلی با آخرین باری که چک کردیم یکی بود (هیچ دیتای جدیدی نیامده) یعنی بازار بسته است
    if tick.time_msc <= _LAST_TICK_MSC:
        return False
        
    # آپدیت کردن زمان آخرین تیک برای بررسی‌های بعدی
    _LAST_TICK_MSC = tick.time_msc
    
    # استخراج روز هفته مستقیماً از تایم‌استمپ خود بروکر (UTC)
    tick_time_utc = datetime.utcfromtimestamp(tick.time)
    if tick_time_utc.weekday() in [5, 6]:  # 5 = شنبه، 6 = یکشنبه
        return False
        
    return True

def get_mt5_data(symbol, timeframe, num_candles):
    rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, num_candles)
    if rates is None or len(rates) == 0: return None
    df = pd.DataFrame(rates)
    df['time'] = pd.to_datetime(df['time'], unit='s')
    df.set_index('time', inplace=True)
    df = df[~df.index.duplicated(keep='last')]
    return df

def compute_vectorized_hurst(series, window=50):
    lags = np.array([12, 24, 72, 144, 288]) 
    tau = np.zeros((len(series), len(lags)), dtype=np.float32)
    for i, lag in enumerate(lags):
        tau[:, i] = np.log(series / series.shift(lag)).rolling(window).std()
    with np.errstate(divide='ignore', invalid='ignore'): y = np.log(tau)
    x = np.log(lags)
    x_mean, x_var = np.mean(x), np.var(x) * len(x)
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        y_mean = np.nanmean(y, axis=1, keepdims=True)
    with np.errstate(invalid='ignore'):
        covar = np.nansum((y - y_mean) * (x - x_mean), axis=1)
        slopes = covar / x_var
    return pd.Series(slopes, index=series.index).ffill().fillna(0.5)

def compute_asset_features(df):
    df = df.copy()
    log_ret = np.log((df['close'] + 1e-8) / (df['close'].shift(1) + 1e-8))
    temp_candle_range_pct = (df['high'] - df['low']) / (df['close'] + 1e-8)
    
    df['rolling_volatility'] = log_ret.rolling(20).std()
    
    df.ta.rsi(length=14, append=True)
    if 'RSI_14' in df.columns: df['RSI_14'] = df['RSI_14'] / 100.0
    
    df.ta.macd(fast=12, slow=26, signal=9, append=True)
    df.drop(columns=[c for c in df.columns if c.startswith('MACDs')], inplace=True, errors='ignore')
    macd_cols = [c for c in df.columns if c.startswith('MACD') or c.startswith('MACDh')]
    for c in macd_cols:
        df[c + '_pct'] = df[c] / (df['close'] + 1e-8)
        df.drop(columns=[c], inplace=True)
    macd_pct_cols = [c + '_pct' for c in macd_cols]
        
    bb_mid = df['close'].rolling(20).mean()
    bb_std = df['close'].rolling(20).std()
    df['bb_upper_dist_pct'] = ((bb_mid + (2 * bb_std)) - df['close']) / (df['close'] + 1e-8)
    df['bb_width_pct'] = (4 * bb_std) / (bb_mid + 1e-8)

    sma50 = df['close'].rolling(50).mean()
    sma200 = df['close'].rolling(200).mean()
    df['dist_SMA200_pct'] = ((df['close'] - sma200) / (df['close'] + 1e-8)).diff(10).fillna(0) * 100.0
    df['diff_SMA50_SMA200_pct'] = ((sma50 - sma200) / (df['close'] + 1e-8)).diff(10).fillna(0) * 100.0

    df['hurst_exponent'] = compute_vectorized_hurst(df['close'], window=50)
    df['spread_ma20'] = df['spread'].rolling(20).mean()
    df['spread_anomaly_pct'] = np.log((df['spread'] + 1e-8) / (df['spread_ma20'] + 1e-8))
    
    rolling_range_ma = temp_candle_range_pct.rolling(20).mean()
    df['range_expansion_ratio'] = temp_candle_range_pct / (rolling_range_ma + 1e-8)
    df['roc_3_pct'] = (df['close'] - df['close'].shift(3)) / (df['close'].shift(3) + 1e-8)

    gnn_cols = [
        'rolling_volatility', 'RSI_14',
        'bb_upper_dist_pct', 'bb_width_pct', 
        'dist_SMA200_pct', 'diff_SMA50_SMA200_pct', 'hurst_exponent',
        'spread_anomaly_pct', 'range_expansion_ratio', 'roc_3_pct'
    ] + macd_pct_cols

    pct_columns = [c for c in df.columns if c.endswith('_pct') and c not in ['dist_SMA200_pct', 'diff_SMA50_SMA200_pct']]
    for c in pct_columns: df[c] = df[c] * 100.0

    if 'bb_width_pct' in df.columns: df['bb_width_pct'] /= 2.0
    if 'spread_anomaly_pct' in df.columns: df['spread_anomaly_pct'] /= 20.0
    if 'range_expansion_ratio' in df.columns: df['range_expansion_ratio'] /= 5.0
        
    cols_to_exclude = ['open', 'high', 'low', 'close', 'tick_volume', 'spread', 'real_volume']
    numeric_cols = [c for c in df.columns if df[c].dtype in [np.float32, np.float64, float] and c not in cols_to_exclude]
    for c in numeric_cols: df[c] = df[c].clip(lower=-15.0, upper=15.0)

    df.attrs['gnn_columns'] = gnn_cols
    return df

def generate_live_gnn_input(mt5_tf):
    raw_data = {}
    for symbol in SYMBOLS:
        df = get_mt5_data(symbol, mt5_tf, num_candles=WARMUP_CANDLES)
        if df is None: return None, None
        raw_data[symbol] = df

    processed_dfs = {}
    for sym in SYMBOLS:
        processed_dfs[sym] = compute_asset_features(raw_data[sym])
        
    gnn_feature_names = processed_dfs['XAUUSD'].attrs['gnn_columns']
            
    master_index = processed_dfs['XAUUSD'].index.drop_duplicates()
    for sym in SYMBOLS: processed_dfs[sym] = processed_dfs[sym].reindex(master_index).ffill(limit=12)
            
    valid_mask = pd.Series(True, index=master_index)
    for sym in SYMBOLS: valid_mask = valid_mask & ~processed_dfs[sym].isna().any(axis=1)
    common_index = master_index[valid_mask]
    
    for sym in SYMBOLS: processed_dfs[sym] = processed_dfs[sym].loc[common_index]

    aligned_log_rets = pd.DataFrame(index=common_index)
    for sym in SYMBOLS:
        close_series = processed_dfs[sym]['close']
        aligned_log_rets[sym] = np.log((close_series + 1e-8) / (close_series.shift(1) + 1e-8))
    aligned_log_rets = aligned_log_rets.fillna(0)
    
    cs_mean = aligned_log_rets.mean(axis=1)
    cs_std = aligned_log_rets.std(axis=1)
    
    for sym in SYMBOLS:
        processed_dfs[sym]['cross_sectional_zscore'] = ((aligned_log_rets[sym] - cs_mean) / (cs_std + 1e-8)).fillna(0) / 3.0
        processed_dfs[sym]['cross_sectional_zscore'] = processed_dfs[sym]['cross_sectional_zscore'].clip(lower=-15.0, upper=15.0)
        
    if 'cross_sectional_zscore' not in gnn_feature_names: gnn_feature_names.append('cross_sectional_zscore')

    rolling_corr_df = aligned_log_rets.rolling(window=20, min_periods=2).corr().fillna(0)
    edge_attr_matrix = rolling_corr_df.values.reshape(len(common_index), len(SYMBOLS), len(SYMBOLS)).astype(np.float32)

    scaled_gnn_dict = {}
    for sym in SYMBOLS:
        scaled_gnn_dict[sym] = processed_dfs[sym][gnn_feature_names].copy().values.astype(np.float32)

    gnn_matrices = np.stack([scaled_gnn_dict[sym] for sym in SYMBOLS], axis=1)
    
    X_live = gnn_matrices[-SEQ_LENGTH:][np.newaxis, ...] 
    E_live = edge_attr_matrix[-SEQ_LENGTH:][np.newaxis, ...] 
    
    if np.isnan(X_live).any() or np.isinf(X_live).any():
        X_live = np.nan_to_num(X_live, nan=0.0, posinf=15.0, neginf=-15.0)
        
    if np.isnan(E_live).any() or np.isinf(E_live).any():
        E_live = np.nan_to_num(E_live, nan=0.0, posinf=1.0, neginf=-1.0)

    return X_live, E_live

# =========================================================
# 6. حلقه اصلی 
# =========================================================
def main_live_loop():
    if not mt5.initialize():
        print("[ERROR] MT5 Initialization failed!")
        return

    models_dict = load_all_models_weights()
    mt5_tf_map = {'m1': mt5.TIMEFRAME_M1, 'm5': mt5.TIMEFRAME_M5, 'm15': mt5.TIMEFRAME_M15}

    latest_signals = {
        'm1': {'val': None, 'time': None},
        'm5': {'val': None, 'time': None},
        'm15': {'val': None, 'time': None}
    }

    print("\n[START] System Armed. Models loaded via 'load_weights'.")
    print("Running timeframe updates 2s before their respective candle closes...")
    
    last_executed_minute = -1
    
    try:
        while True:
            current_time = datetime.now()
            minute = current_time.minute
            second = current_time.second
            
            if second == 58 and minute != last_executed_minute:
                last_executed_minute = minute
                
                if not check_is_market_open(symbol=TARGET_SYMBOL, max_delay_seconds=10):
                    print(f"\n[MARKET CLOSED @ {current_time.strftime('%H:%M:%S')}] No new ticks received. Skipping signal calculations...")
                    time.sleep(0.2)
                    continue

                active_timeframes = ['m1']
                if (minute + 1) % 5 == 0:
                    active_timeframes.append('m5')
                if (minute + 1) % 15 == 0:
                    active_timeframes.append('m15')

                for tf_name in active_timeframes:
                    if not models_dict[tf_name]:
                        continue
                        
                    X_live, E_live = generate_live_gnn_input(mt5_tf_map[tf_name])
                    if X_live is None:
                        continue

                    all_predictions = []
                    for model in models_dict[tf_name]:
                        try:
                            pred = model([X_live, E_live], training=False)
                            all_predictions.append(pred.numpy())
                        except Exception as e:
                            print(f"[{tf_name.upper()}] Predict Error: {e}")

                    if all_predictions:
                        all_predictions = np.array(all_predictions).squeeze()
                        if all_predictions.ndim == 0: all_predictions = all_predictions[np.newaxis]
                        
                        ensemble_mean = np.mean(all_predictions, axis=0) * 2.0
                        
                        latest_signals[tf_name]['val'] = ensemble_mean
                        latest_signals[tf_name]['time'] = current_time.strftime('%H:%M:%S')

                print(f"\n==================== [STATUS @ {current_time.strftime('%H:%M:%S')}] ====================")
                
                for tf_name in ['m1', 'm5', 'm15']:
                    sig = latest_signals[tf_name]
                    is_new = tf_name in active_timeframes
                    tag = "(NEW UPDATE)" if is_new else "(PREVIOUS)"
                    
                    if sig['val'] is not None:
                        direction = "BULL (Long)" if sig['val'] > 0 else "BEAR (Short)"
                        print(f"[{tf_name.upper()}] {sig['val']:+.6f} -> {direction:<12} | Last Update: {sig['time']} {tag}")
                    else:
                        print(f"[{tf_name.upper()}] Waiting for first calculation...")

                print("==================================================================")
                
                # --- ارسال پیام به تلگرام به تفکیک کانال ---
                for tf_name in active_timeframes:
                    val = latest_signals[tf_name]['val']
                    if val is not None:
                        target_chat_id = TELEGRAM_CHATS.get(tf_name)
                        
                        # فرمت ساده و مستقیم طبق درخواست (تغییر یافته به دقیقاً 4 رقم اعشار)
                        tg_msg = f"{tf_name} =  {float(val):.4f}"
                        
                        send_telegram_message(tg_msg, target_chat_id)
                
                gc.collect()
            
            time.sleep(0.2)
            
    except KeyboardInterrupt:
        print("\n[STOP] Program interrupted by user.")
    finally:
        mt5.shutdown()
        print("[INFO] MT5 Shutdown complete.")

if __name__ == "__main__":
    main_live_loop()