import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')  # Бэкенд Agg для сохранения графиков
import matplotlib.pyplot as plt
import scipy.stats as stats
import seaborn as sns
import logging
import time
import os
from threading import Thread
import json
from datetime import datetime, timedelta

# Настройка логирования
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Директории для вывода
output_dir = "charts"
data_dir = "data"
for directory in [output_dir, data_dir]:
    if not os.path.exists(directory):
        os.makedirs(directory)

# --- Инициализация MT5 ---
def initialize_mt5(max_retries=3, retry_delay=5):
    """Подключение к открытому терминалу MT5"""
    try:
        for attempt in range(max_retries):
            if not mt5.initialize(timeout=10000):
                logger.error(f"Попытка {attempt + 1}/{max_retries} - Ошибка инициализации MT5: {mt5.last_error()}")
                if attempt < max_retries - 1:
                    logger.info(f"Ждем {retry_delay} секунд перед повторной попыткой...")
                    time.sleep(retry_delay)
                continue
            if mt5.account_info() is None:
                logger.error("Терминал не авторизован, выполните вход в MT5 вручную")
                mt5.shutdown()
                return False
            logger.info(f"MT5 подключен успешно, номер счета: {mt5.account_info().login}")
            return True
        logger.error(f"Не удалось инициализировать MT5 после {max_retries} попыток")
        return False
    except Exception as e:
        logger.error(f"Ошибка в initialize_mt5: {str(e)}")
        return False

# --- Получение данных ---
def fetch_historical_data(symbol, timeframe=mt5.TIMEFRAME_M5, start_date=None, end_date=None):
    """Загрузка исторических данных за указанный период"""
    try:
        if not mt5.symbol_select(symbol, True):
            logger.error(f"Символ {symbol} не найден в Market Watch")
            return None
        
        # Установка дат (по умолчанию последние 2 месяца от текущей даты)
        if start_date is None:
            start_date = datetime.now() - timedelta(days=60)  # 2 месяца назад
        if end_date is None:
            end_date = datetime.now()
        
        rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
        if rates is None or len(rates) == 0:
            logger.error(f"Ошибка загрузки данных для {symbol}: {mt5.last_error()}")
            return None
        
        df = pd.DataFrame(rates)
        df['time'] = pd.to_datetime(df['time'], unit='s')
        df.set_index('time', inplace=True)
        df.to_csv(os.path.join(data_dir, f"{symbol}_M5_2months.csv"))
        logger.info(f"Данные для {symbol} загружены и сохранены: {len(df)} записей (с {start_date} по {end_date})")
        return df[['open', 'high', 'low', 'close', 'spread']]
    except Exception as e:
        logger.error(f"Ошибка в fetch_historical_data для {symbol}: {str(e)}")
        return None

def fetch_tick_data(symbol, hours=24):
    """Загрузка тиковых данных за последние N часов"""
    try:
        from_time = datetime.now() - timedelta(hours=hours)
        ticks = mt5.copy_ticks_from(symbol, from_time, 100000, mt5.COPY_TICKS_ALL)
        if ticks is None or len(ticks) == 0:
            logger.error(f"Ошибка загрузки тиков для {symbol}: {mt5.last_error()}")
            return None
        df = pd.DataFrame(ticks)
        df['time'] = pd.to_datetime(df['time'], unit='s')
        df.set_index('time', inplace=True)
        logger.info(f"Тиковые данные для {symbol} загружены: {len(df)} записей")
        return df[['bid', 'ask']]
    except Exception as e:
        logger.error(f"Ошибка в fetch_tick_data для {symbol}: {str(e)}")
        return None

def sync_dataframes(dfs):
    """Синхронизация данных по времени"""
    try:
        common_index = dfs[0].index
        for df in dfs[1:]:
            common_index = common_index.intersection(df.index)
        synced_dfs = [df.loc[common_index] for df in dfs]
        logger.info(f"Данные синхронизированы, общий размер: {len(common_index)}")
        return synced_dfs
    except Exception as e:
        logger.error(f"Ошибка в sync_dataframes: {str(e)}")
        return None

# --- Расчет синтетических курсов и дисбалансов ---
def calculate_synthetic_rate(eurusd, gbpusd, eurgbp, normalize=False):
    """Расчет синтетической пары EURGBP"""
    try:
        synthetic = eurusd['close'] / gbpusd['close']
        if normalize:
            synthetic = (synthetic - synthetic.mean()) / synthetic.std()
        logger.info("Синтетическая EURGBP рассчитана")
        return synthetic
    except Exception as e:
        logger.error(f"Ошибка в calculate_synthetic_rate: {str(e)}")
        return None

def compute_imbalance(real_eurgbp, synthetic_eurgbp):
    """Расчет дисбаланса"""
    try:
        imbalance = real_eurgbp['close'] - synthetic_eurgbp
        logger.info("Дисбаланс рассчитан")
        return imbalance
    except Exception as e:
        logger.error(f"Ошибка в compute_imbalance: {str(e)}")
        return None

def monitor_imbalance_real_time(symbols, interval=5):
    """Мониторинг дисбалансов в реальном времени"""
    try:
        while True:
            bids = {symbol: mt5.symbol_info_tick(symbol).bid for symbol in symbols}
            if any(bid is None for bid in bids.values()):
                logger.error("Ошибка получения котировок в реальном времени")
                continue
            synthetic = bids['EURUSD'] / bids['GBPUSD']
            imbalance = bids['EURGBP'] - synthetic
            logger.info(f"Текущий дисбаланс: {imbalance:.6f}")
            time.sleep(interval)
    except Exception as e:
        logger.error(f"Ошибка в monitor_imbalance_real_time: {str(e)}")

# --- Визуализация ---
# Изменения в функции plot_pairs_and_imbalance
def plot_pairs_and_imbalance(eurusd, gbpusd, eurgbp, synthetic_eurgbp, imbalance):
    """График пар и дисбаланса"""
    try:
        # Фиксированная ширина 750px
        width_px = 750
        dpi = 100  # Стандартное значение DPI
        width_inches = width_px / dpi
        height_inches = width_inches * 0.7  # Соотношение сторон ~1.4
        
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(width_inches, height_inches), sharex=True, dpi=dpi)
        ax1.plot(eurgbp.index, eurgbp['close'], label='Real EURGBP', color='blue')
        ax1.plot(synthetic_eurgbp.index, synthetic_eurgbp, label='Synthetic EURGBP', color='orange', linestyle='--')
        ax1.set_title('Real vs Synthetic EURGBP')
        ax1.legend()
        
        ax2.plot(imbalance.index, imbalance, label='Imbalance', color='red')
        ax2.axhline(0, color='black', linestyle='--')
        ax2.set_title('Imbalance (Real - Synthetic)')
        ax2.legend()
        
        plt.tight_layout()
        output_file = os.path.join(output_dir, "pairs_and_imbalance_2months.png")
        plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
        plt.close(fig)
        logger.info(f"График сохранен: {output_file}")
    except Exception as e:
        logger.error(f"Ошибка в plot_pairs_and_imbalance: {str(e)}")

# Изменения в функции plot_imbalance_distribution
def plot_imbalance_distribution(imbalance):
    """Гистограмма и ящик с усами"""
    try:
        width_px = 750
        dpi = 100
        width_inches = width_px / dpi
        height_inches = width_inches * 0.5  # Соотношение сторон 2:1
        
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(width_inches, height_inches), dpi=dpi)
        sns.histplot(imbalance.dropna(), bins=50, kde=True, color='purple', ax=ax1)
        ax1.set_title('Imbalance Distribution (2 Months)')
        sns.boxplot(x=imbalance.dropna(), color='purple', ax=ax2)
        ax2.set_title('Imbalance Boxplot (2 Months)')
        
        plt.tight_layout()
        output_file = os.path.join(output_dir, "imbalance_distribution_2months.png")
        plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
        plt.close(fig)
        logger.info(f"Гистограмма и ящик сохранены: {output_file}")
    except Exception as e:
        logger.error(f"Ошибка в plot_imbalance_distribution: {str(e)}")

# Изменения в функции plot_heatmap
def plot_heatmap(imbalance, timeframe='H'):
    """Тепловая карта дисбалансов"""
    try:
        width_px = 750
        dpi = 100
        width_inches = width_px / dpi
        height_inches = width_inches * 0.5  # Соотношение сторон 2:1
        
        df = pd.DataFrame({'imbalance': imbalance})
        df['hour'] = df.index.hour
        df['day'] = df.index.dayofweek
        heatmap_data = df.pivot_table(values='imbalance', index='day', columns='hour', aggfunc='mean')
        
        plt.figure(figsize=(width_inches, height_inches), dpi=dpi)
        sns.heatmap(heatmap_data, cmap='RdBu', center=0)
        plt.title('Imbalance Heatmap by Hour and Day (2 Months)')
        
        plt.tight_layout()
        output_file = os.path.join(output_dir, "imbalance_heatmap_2months.png")
        plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
        plt.close()
        logger.info(f"Тепловая карта сохранена: {output_file}")
    except Exception as e:
        logger.error(f"Ошибка в plot_heatmap: {str(e)}")

# Изменения в функции backtest_strategy
def backtest_strategy(imbalance, eurgbp, threshold=0.000126, ema_period=20, session='all'):
    """Оптимизированная стратегия скальпинга с реальным спредом"""
    try:
        # Фильтр по сессии
        df = pd.DataFrame({'imbalance': imbalance})
        df['hour'] = df.index.hour
        if session == 'asian':
            df = df[(df['hour'] >= 0) & (df['hour'] < 8)]
        elif session == 'european':
            df = df[(df['hour'] >= 8) & (df['hour'] < 16)]
        elif session == 'american':
            df = df[(df['hour'] >= 16) & (df['hour'] <= 23)]
        imbalance = df['imbalance']
        
        ema = imbalance.ewm(span=ema_period, adjust=False).mean()
        signals = pd.Series(0, index=imbalance.index)
        signals[imbalance > ema + threshold] = -1  # Продажа
        signals[imbalance < ema - threshold] = 1   # Покупка
        
        # Выход при пересечении EMA
        exits = ((signals.shift(1) == 1) & (imbalance > ema)) | ((signals.shift(1) == -1) & (imbalance < ema))
        signals[exits] = 0
        
        # Реальный спред из данных
        spread = eurgbp['spread'].reindex(signals.index, method='ffill') / 10000  # Перевод в цену
        returns = signals.shift(1) * imbalance.diff() - spread * signals.abs()
        cumulative_returns = returns.cumsum()
        
        # Фиксированная ширина 750px
        width_px = 750
        dpi = 100
        width_inches = width_px / dpi
        height_inches = width_inches * 0.6  # Соотношение сторон ~1.7
        
        plt.figure(figsize=(width_inches, height_inches), dpi=dpi)
        plt.plot(cumulative_returns, label='Cumulative Returns')
        plt.title(f'Optimized Backtest Results ({session.capitalize()} Session, 2 Months)')
        plt.legend()
        
        plt.tight_layout()
        output_file = os.path.join(output_dir, f"optimized_backtest_{session}_2months.png")
        plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
        plt.close()
        
        logger.info(f"[{session.capitalize()}] Общая прибыль: {cumulative_returns.iloc[-1]:.6f}")
        logger.info(f"[{session.capitalize()}] Максимальная просадка: {-(cumulative_returns - cumulative_returns.cummax()).min():.6f}")
        logger.info(f"[{session.capitalize()}] Количество сделок: {signals.abs().sum()}")
        logger.info(f"[{session.capitalize()}] Средний спред: {spread.mean():.6f}")
    except Exception as e:
        logger.error(f"Ошибка в backtest_strategy: {str(e)}")

# Изменения в функции correlation_analysis
def correlation_analysis(imbalances):
    """Корреляционная матрица дисбалансов"""
    try:
        df = pd.DataFrame(imbalances)
        corr_matrix = df.corr()
        
        # Фиксированная ширина 750px
        width_px = 750
        dpi = 100
        width_inches = width_px / dpi
        height_inches = width_inches * 0.8  # Соотношение сторон 1.25:1
        
        plt.figure(figsize=(width_inches, height_inches), dpi=dpi)
        sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
        plt.title('Correlation of Imbalances (2 Months)')
        
        plt.tight_layout()
        output_file = os.path.join(output_dir, "correlation_matrix_2months.png")
        plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
        plt.close()
        logger.info(f"Корреляционная матрица сохранена: {output_file}")
    except Exception as e:
        logger.error(f"Ошибка в correlation_analysis: {str(e)}")

# --- Статистический анализ ---
def analyze_imbalance_stats(imbalance):
    """Полный статистический анализ"""
    try:
        stats_dict = {
            'Mean': imbalance.mean(),
            'Std Dev': imbalance.std(),
            'Min': imbalance.min(),
            'Max': imbalance.max(),
            'Skewness': stats.skew(imbalance.dropna()),
            'Kurtosis': stats.kurtosis(imbalance.dropna())
        }
        normality_p = stats.shapiro(imbalance.dropna())[1] if len(imbalance.dropna()) > 3 else float('nan')
        autocorr = pd.Series(imbalance).autocorr()
        
        logger.info("Статистика дисбаланса (2 месяца):")
        for key, value in stats_dict.items():
            logger.info(f"{key}: {value:.6f}")
        logger.info(f"Тест на нормальность (p-value): {normality_p:.6f}")
        logger.info(f"Автокорреляция (lag 1): {autocorr:.6f}")
        
        with open(os.path.join(data_dir, "imbalance_stats_2months.json"), 'w') as f:
            json.dump(stats_dict, f)
    except Exception as e:
        logger.error(f"Ошибка в analyze_imbalance_stats: {str(e)}")

def segment_by_time(imbalance):
    """Сегментация по времени"""
    try:
        df = pd.DataFrame({'imbalance': imbalance})
        df['hour'] = df.index.hour
        df['day'] = df.index.dayofweek
        
        sessions = {
            'Asian': df[(df['hour'] >= 0) & (df['hour'] < 8)]['imbalance'],
            'European': df[(df['hour'] >= 8) & (df['hour'] < 16)]['imbalance'],
            'American': df[(df['hour'] >= 16) & (df['hour'] <= 23)]['imbalance']
        }
        
        logger.info("Средний дисбаланс по сессиям (2 месяца):")
        for session, data in sessions.items():
            logger.info(f"{session}: {data.mean():.6f} (Std: {data.std():.6f})")
    except Exception as e:
        logger.error(f"Ошибка в segment_by_time: {str(e)}")

# --- Фильтрация шума ---
def filter_imbalance(imbalance, method='ema', window=20, threshold=2):
    """Фильтрация дисбалансов"""
    try:
        if method == 'ma':
            smoothed = imbalance.rolling(window=window, center=True).mean()
        elif method == 'ema':
            smoothed = imbalance.ewm(span=window, adjust=False).mean()
        std = imbalance.rolling(window=window, center=True).std()
        filtered = imbalance[(imbalance > smoothed + threshold * std) | (imbalance < smoothed - threshold * std)]
        logger.info(f"Фильтрация ({method}): {len(filtered.dropna())} значимых дисбалансов")
        return filtered
    except Exception as e:
        logger.error(f"Ошибка в filter_imbalance: {str(e)}")
        return None

# --- Анализ рыночных условий ---
def analyze_volatility_impact(eurusd, imbalance):
    """Связь волатильности и дисбаланса"""
    try:
        vol = eurusd['close'].pct_change().rolling(20).std() * np.sqrt(252)  # Годовая волатильность
        df = pd.DataFrame({'volatility': vol, 'imbalance': imbalance.abs()})
        correlation = df.corr().iloc[0, 1]
        logger.info(f"Корреляция волатильности и дисбаланса (2 месяца): {correlation:.6f}")
    except Exception as e:
        logger.error(f"Ошибка в analyze_volatility_impact: {str(e)}")

# --- Бэктестинг ---
def backtest_strategy(imbalance, eurgbp, threshold=0.000126, ema_period=20, session='all'):
    """Оптимизированная стратегия скальпинга с реальным спредом"""
    try:
        # Фильтр по сессии
        df = pd.DataFrame({'imbalance': imbalance})
        df['hour'] = df.index.hour
        if session == 'asian':
            df = df[(df['hour'] >= 0) & (df['hour'] < 8)]
        elif session == 'european':
            df = df[(df['hour'] >= 8) & (df['hour'] < 16)]
        elif session == 'american':
            df = df[(df['hour'] >= 16) & (df['hour'] <= 23)]
        imbalance = df['imbalance']
        
        ema = imbalance.ewm(span=ema_period, adjust=False).mean()
        signals = pd.Series(0, index=imbalance.index)
        signals[imbalance > ema + threshold] = -1  # Продажа
        signals[imbalance < ema - threshold] = 1   # Покупка
        
        # Выход при пересечении EMA
        exits = ((signals.shift(1) == 1) & (imbalance > ema)) | ((signals.shift(1) == -1) & (imbalance < ema))
        signals[exits] = 0
        
        # Реальный спред из данных
        spread = eurgbp['spread'].reindex(signals.index, method='ffill') / 10000  # Перевод в цену
        returns = signals.shift(1) * imbalance.diff() - spread * signals.abs()
        cumulative_returns = returns.cumsum()
        
        plt.figure(figsize=(10, 6))
        plt.plot(cumulative_returns, label='Cumulative Returns')
        plt.title(f'Optimized Backtest Results ({session.capitalize()} Session, 2 Months)')
        plt.legend()
        output_file = os.path.join(output_dir, f"optimized_backtest_{session}_2months.png")
        plt.savefig(output_file)
        plt.close()
        
        logger.info(f"[{session.capitalize()}] Общая прибыль: {cumulative_returns.iloc[-1]:.6f}")
        logger.info(f"[{session.capitalize()}] Максимальная просадка: {-(cumulative_returns - cumulative_returns.cummax()).min():.6f}")
        logger.info(f"[{session.capitalize()}] Количество сделок: {signals.abs().sum()}")
        logger.info(f"[{session.capitalize()}] Средний спред: {spread.mean():.6f}")
    except Exception as e:
        logger.error(f"Ошибка в backtest_strategy: {str(e)}")

# --- Расширенные треугольники ---
def analyze_extended_triangles(symbol_sets, start_date=None, end_date=None):
    """Анализ дополнительных треугольников"""
    try:
        for symbols in symbol_sets:
            dfs = [fetch_historical_data(symbol, start_date=start_date, end_date=end_date) for symbol in symbols]
            if any(df is None for df in dfs):
                continue
            synced_dfs = sync_dataframes(dfs)
            synthetic = synced_dfs[0]['close'] / synced_dfs[1]['close']
            imbalance = compute_imbalance(synced_dfs[2], synthetic)
            logger.info(f"Дисбаланс для {symbols} (2 месяца): {imbalance.mean():.6f}")
    except Exception as e:
        logger.error(f"Ошибка в analyze_extended_triangles: {str(e)}")



# --- Консольный интерфейс ---
def console_interface(imbalance, eurgbp):
    """Простой консольный интерфейс"""
    try:
        while True:
            print("\nКоманды: stats, plot, backtest, exit")
            cmd = input("Введите команду: ").strip().lower()
            if cmd == "stats":
                analyze_imbalance_stats(imbalance)
            elif cmd == "plot":
                plot_imbalance_distribution(imbalance)
            elif cmd == "backtest":
                session = input("Введите сессию (all, asian, european, american): ").strip().lower()
                backtest_strategy(imbalance, eurgbp, session=session)
            elif cmd == "exit":
                break
            else:
                print("Неизвестная команда")
    except Exception as e:
        logger.error(f"Ошибка в console_interface: {str(e)}")

# --- Основной цикл ---
def main():
    logger.info("Запуск анализа синтетических пар за 2 месяца")
    
    if not initialize_mt5():
        logger.error("Не удалось подключиться к MT5")
        return
    
    # Установка периода (2 месяца назад от текущей даты)
    end_date = datetime(2025, 3, 15)  # Текущая дата из ваших инструкций
    start_date = end_date - timedelta(days=60)  # 15 января 2025
    
    # Загрузка данных
    symbols = ['EURUSD', 'GBPUSD', 'EURGBP']
    dfs = [fetch_historical_data(symbol, start_date=start_date, end_date=end_date) for symbol in symbols]
    if any(df is None for df in dfs) or len(dfs) != 3:
        logger.error("Ошибка загрузки данных")
        mt5.shutdown()
        return
    eurusd, gbpusd, eurgbp = sync_dataframes(dfs)
    
    # Тиковые данные (последние 24 часа для сравнения)
    tick_data = fetch_tick_data('EURUSD', hours=24)
    
    # Расчет синтетической пары и дисбаланса
    synthetic_eurgbp = calculate_synthetic_rate(eurusd, gbpusd, eurgbp, normalize=False)
    imbalance = compute_imbalance(eurgbp, synthetic_eurgbp)
    
    # Визуализация
    plot_pairs_and_imbalance(eurusd, gbpusd, eurgbp, synthetic_eurgbp, imbalance)
    plot_imbalance_distribution(imbalance)
    plot_heatmap(imbalance)
    
    # Статистический анализ
    analyze_imbalance_stats(imbalance)
    segment_by_time(imbalance)
    
    # Фильтрация
    filtered_imbalance = filter_imbalance(imbalance, method='ema')
    
    # Анализ рыночных условий
    analyze_volatility_impact(eurusd, imbalance)
    
    # Бэктестинг по сессиям
    for session in ['all', 'asian', 'european', 'american']:
        backtest_strategy(imbalance, eurgbp, threshold=0.000126, session=session)
    
    # Реальное время (в отдельном потоке)
    Thread(target=monitor_imbalance_real_time, args=(symbols,), daemon=True).start()
    
    # Расширенные треугольники
    extended_sets = [['USDJPY', 'EURJPY', 'EURUSD'], ['AUDUSD', 'NZDUSD', 'AUDNZD']]
    analyze_extended_triangles(extended_sets, start_date=start_date, end_date=end_date)
    
    # Корреляции
    imbalances = {'EURGBP': imbalance}
    correlation_analysis(imbalances)
    
    # Консольный интерфейс
    console_interface(imbalance, eurgbp)
    
    mt5.shutdown()
    logger.info("Анализ за 2 месяца завершен")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        logger.error(f"Необработанная ошибка: {str(e)}")
        mt5.shutdown()
