import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.utils.class_weight import compute_class_weight
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout, BatchNormalization
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.optimizers import Adam
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# ======================
# 1. Загрузка данных (без заголовков, 7 колонок)
# ======================
def load_data(filepath):
    df = pd.read_csv(filepath, header=None)
    if df.shape[1] != 7:
        raise ValueError(f"Ожидалось 7 колонок, получено {df.shape[1]}")
    df.columns = ['date', 'time', 'open', 'high', 'low', 'close', 'volume']
    df['datetime'] = pd.to_datetime(df['date'] + ' ' + df['time'],
                                    format='%Y.%m.%d %H:%M', errors='coerce')
    df.set_index('datetime', inplace=True)
    df.drop(['date', 'time'], axis=1, inplace=True)
    for col in ['open', 'high', 'low', 'close', 'volume']:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    df.dropna(inplace=True)
    df.sort_index(inplace=True)
    print(f"Загружено записей: {len(df)}")
    return df

# ======================
# 2. Добавление признаков
# ======================
def add_features(df):
    # EMA
    df['ema10'] = df['close'].ewm(span=10, adjust=False).mean()
    df['ema30'] = df['close'].ewm(span=30, adjust=False).mean()
    df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
    df['ema10_slope'] = df['ema10'].diff()
    df['ema30_slope'] = df['ema30'].diff()

    # Логарифмическая доходность
    df['log_ret'] = np.log(df['close'] / df['close'].shift(1))

    # RSI
    delta = df['close'].diff()
    gain = delta.where(delta > 0, 0).rolling(14).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
    rs = gain / loss
    df['rsi'] = 100 - 100 / (1 + rs)

    # MACD
    ema12 = df['close'].ewm(span=12).mean()
    ema26 = df['close'].ewm(span=26).mean()
    df['macd'] = ema12 - ema26
    df['macd_signal'] = df['macd'].ewm(span=9).mean()
    df['macd_hist'] = df['macd'] - df['macd_signal']

    # ATR
    high_low = df['high'] - df['low']
    high_close = (df['high'] - df['close'].shift()).abs()
    low_close = (df['low'] - df['close'].shift()).abs()
    tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
    df['atr'] = tr.rolling(14).mean()

    # Bollinger Bands
    ma20 = df['close'].rolling(20).mean()
    std20 = df['close'].rolling(20).std()
    df['bb_upper'] = ma20 + 2 * std20
    df['bb_lower'] = ma20 - 2 * std20
    df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / ma20
    df['bb_percent'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])

    # Волатильность и расстояния
    df['volatility'] = df['log_ret'].rolling(20).std()
    df['dist_from_high'] = (df['high'].rolling(20).max() - df['close']) / df['close']
    df['dist_from_low'] = (df['close'] - df['low'].rolling(20).min()) / df['close']

    # Отношения цен к EMA
    df['close_ema10'] = df['close'] / df['ema10'] - 1
    df['close_ema30'] = df['close'] / df['ema30'] - 1
    df['ema10_ema30'] = df['ema10'] / df['ema30'] - 1

    # Объёмные индикаторы
    df['volume_ma20'] = df['volume'].rolling(20).mean()
    df['volume_ratio'] = df['volume'] / df['volume_ma20']

    df.dropna(inplace=True)
    return df

# ======================
# 3. Разметка тренда (адаптивный порог + расширенный класс 3)
# ======================
def label_trend_adaptive(df, window=14, flat_percentile=20, break_window=5):
    """
    Определяет классы:
    0 - флэт (|slope| < threshold)
    1 - бычий (slope > threshold)
    2 - медвежий (slope < -threshold)
    3 - слом тренда (в окне break_window после пересечения EMA10 и EMA30)
    """
    prices = df['close'].values
    slopes = np.full(len(df), np.nan)
    for i in range(window, len(df)):
        y = prices[i-window:i]
        x = np.arange(window).reshape(-1, 1)
        model = LinearRegression().fit(x, y)
        slopes[i] = model.coef_[0]
    df['slope'] = slopes

    # Адаптивный порог для флэта
    abs_slope = np.abs(slopes[~np.isnan(slopes)])
    threshold = np.percentile(abs_slope, flat_percentile)
    print(f"Адаптивный порог флэта (abs(slope) < {threshold:.6f})")

    # Базовые классы 0,1,2
    df['trend'] = 0
    df.loc[df['slope'] > threshold, 'trend'] = 1
    df.loc[df['slope'] < -threshold, 'trend'] = 2

    # Пересечения EMA10 и EMA30 (сигнал слома тренда)
    ema10 = df['ema10']
    ema30 = df['ema30']
    cross_up = (ema10 > ema30) & (ema10.shift(1) <= ema30.shift(1))
    cross_down = (ema10 < ema30) & (ema10.shift(1) >= ema30.shift(1))
    cross = cross_up | cross_down

    # Расширяем окно на break_window дней после пересечения
    for i in range(break_window + 1):
        if i == 0:
            df.loc[cross, 'trend'] = 3
        else:
            df.loc[cross.shift(i).fillna(False), 'trend'] = 3

    df.dropna(subset=['slope'], inplace=True)
    return df

# ======================
# 4. Создание последовательностей
# ======================
def create_sequences(data, targets, lookback):
    X, y = [], []
    for i in range(len(data) - lookback):
        X.append(data[i:i+lookback])
        y.append(targets[i+lookback])
    return np.array(X), np.array(y)

# ======================
# 5. Аугментация (oversampling) для нескольких классов
# ======================
def oversample_classes(X_train, y_train, target_ratios, noise_scale=0.01):
    X_new, y_new = X_train.copy(), y_train.copy()
    for class_label, target_ratio in target_ratios.items():
        class_idx = np.where(y_new == class_label)[0]
        current_count = len(class_idx)
        total_count = len(y_new)
        current_ratio = current_count / total_count

        if current_ratio >= target_ratio:
            print(f"Класс {class_label}: {current_ratio:.2%} >= {target_ratio:.2%}, oversampling не требуется")
            continue

        target_count = int(total_count * target_ratio)
        n_to_add = target_count - current_count
        print(f"Класс {class_label}: {current_count} -> {target_count} (доля {target_ratio:.2%})")

        X_add = []
        for idx in np.random.choice(class_idx, n_to_add, replace=True):
            seq = X_new[idx].copy()
            noise = np.random.normal(0, noise_scale, seq.shape)
            seq += noise
            X_add.append(seq)

        X_new = np.vstack([X_new, np.array(X_add)])
        y_new = np.hstack([y_new, [class_label] * n_to_add])

    shuffle_idx = np.random.permutation(len(X_new))
    X_new = X_new[shuffle_idx]
    y_new = y_new[shuffle_idx]
    return X_new, y_new

# ======================
# 6. Модель LSTM
# ======================
def build_model(input_shape, num_classes):
    model = Sequential([
        LSTM(128, return_sequences=True, input_shape=input_shape),
        BatchNormalization(),
        Dropout(0.3),
        LSTM(64, return_sequences=False),
        BatchNormalization(),
        Dropout(0.3),
        Dense(32, activation='relu'),
        Dropout(0.2),
        Dense(num_classes, activation='softmax')
    ])
    optimizer = Adam(learning_rate=0.0005)
    model.compile(optimizer=optimizer,
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

# ======================
# 7. Основной блок
# ======================
def main():
    print("Загрузка данных...")
    df = load_data('BITCOIN5.csv')

    print("Добавление признаков...")
    df = add_features(df)

    print("Разметка тренда...")
    # Увеличиваем break_window до 5, чтобы получить больше примеров класса 3
    df = label_trend_adaptive(df, window=14, flat_percentile=20, break_window=5)
    print("Распределение классов (исходные метки):")
    print(df['trend'].value_counts().sort_index())

    # Отбор признаков
    feature_cols = ['ema10', 'ema30', 'ema50', 'ema10_slope', 'ema30_slope',
                    'log_ret', 'rsi', 'macd', 'macd_signal', 'macd_hist',
                    'atr', 'volatility', 'bb_width', 'bb_percent',
                    'dist_from_high', 'dist_from_low', 'close_ema10',
                    'close_ema30', 'ema10_ema30', 'volume_ratio']
    feature_cols = [c for c in feature_cols if c in df.columns]

    data_raw = df[feature_cols].values
    targets_raw = df['trend'].values.astype(int)

    # Перенумерация классов в 0..n-1
    unique_classes = np.unique(targets_raw)
    class_mapping = {old: new for new, old in enumerate(unique_classes)}
    targets = np.array([class_mapping[x] for x in targets_raw])
    print("Отображение исходных классов в новые:", class_mapping)
    print("Новое распределение классов:", pd.Series(targets).value_counts().sort_index().to_dict())

    # Нормализация
    scaler = StandardScaler()
    data_scaled = scaler.fit_transform(data_raw)

    # Создание последовательностей
    lookback = 60
    X, y = create_sequences(data_scaled, targets, lookback)
    print(f"Форма X: {X.shape}, y: {y.shape}")

    # Гарантируем наличие класса 3 в тестовой выборке
    class_3_new = class_mapping.get(3, None)
    if class_3_new is not None:
        indices_class3 = np.where(y == class_3_new)[0]
        if len(indices_class3) > 0:
            test_idx = [indices_class3[0]]
            all_indices = np.arange(len(y))
            train_indices = np.setdiff1d(all_indices, test_idx)
            np.random.shuffle(train_indices)
            test_size = int(0.2 * len(y))
            if test_size > 1:
                additional_test = np.random.choice(train_indices, size=test_size-1, replace=False)
                test_idx = np.concatenate([test_idx, additional_test])
            train_idx = np.setdiff1d(all_indices, test_idx)
            X_train, X_test = X[train_idx], X[test_idx]
            y_train, y_test = y[train_idx], y[test_idx]
            print(f"Ручное разделение: train {len(X_train)}, test {len(X_test)}")
            print(f"Класс 3 в тесте: {np.sum(y_test == class_3_new)}")
        else:
            split = int(len(X) * 0.8)
            X_train, X_test = X[:split], X[split:]
            y_train, y_test = y[:split], y[split:]
    else:
        split = int(len(X) * 0.8)
        X_train, X_test = X[:split], X[split:]
        y_train, y_test = y[:split], y[split:]

    # Oversampling для редких классов 0 и 3
    target_ratios = {}
    if 0 in class_mapping:
        target_ratios[class_mapping[0]] = 0.1   # 10% класса 0
    if 3 in class_mapping:
        target_ratios[class_mapping[3]] = 0.15  # 15% класса 3

    if target_ratios:
        X_train, y_train = oversample_classes(
            X_train, y_train,
            target_ratios=target_ratios,
            noise_scale=0.02
        )
        print("После oversampling распределение классов в train:")
        print(pd.Series(y_train).value_counts().sort_index().to_dict())

    # Веса классов
    classes_in_train = np.unique(y_train)
    class_weights = compute_class_weight('balanced', classes=classes_in_train, y=y_train)
    class_weight_dict = dict(zip(classes_in_train, class_weights))
    print("Веса классов (новые метки):", class_weight_dict)

    # Модель
    num_classes = len(unique_classes)
    model = build_model((X.shape[1], X.shape[2]), num_classes)
    model.summary()

    early_stop = EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True)

    history = model.fit(X_train, y_train,
                        validation_data=(X_test, y_test),
                        epochs=100,
                        batch_size=32,
                        class_weight=class_weight_dict,
                        callbacks=[early_stop],
                        verbose=1)

    # Оценка
    y_pred_prob = model.predict(X_test)
    y_pred_new = np.argmax(y_pred_prob, axis=1)

    inv_mapping = {v: k for k, v in class_mapping.items()}
    y_test_orig = np.array([inv_mapping[x] for x in y_test])
    y_pred_orig = np.array([inv_mapping[x] for x in y_pred_new])

    unique_orig_in_test = np.unique(y_test_orig)
    target_names = [f'Класс {u}' for u in unique_orig_in_test]
    print(classification_report(y_test_orig, y_pred_orig,
                                labels=unique_orig_in_test,
                                target_names=target_names))
    print("Confusion Matrix (исходные метки):")
    print(confusion_matrix(y_test_orig, y_pred_orig, labels=unique_orig_in_test))

    # Графики
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 2, 1)
    plt.plot(history.history['loss'], label='Train')
    plt.plot(history.history['val_loss'], label='Val')
    plt.legend()
    plt.title('Loss')

    plt.subplot(1, 2, 2)
    plt.plot(history.history['accuracy'], label='Train')
    plt.plot(history.history['val_accuracy'], label='Val')
    plt.legend()
    plt.title('Accuracy')
    plt.show()

    model.save('bitcoin_trend_improved.keras')
    print("Модель сохранена как 'bitcoin_trend_improved.keras'")

if __name__ == "__main__":
    main()