AI Trading Bot: Разрабатываем алгоритм для long-сделок на Python с использованием CCXT (Машинное обучение для трейдинга: Предсказываем разворот тренда на крипторынке)

import logging  # Для логирования сообщений
import configparser  # Для чтения конфигурационного файла
import os  # Для работы с файловой системой
import pandas as pd  # Для работы с данными в табличном формате
import numpy as np  # Для численных операций
import sys  # Для системных параметров
import time  # Для задержек

from joblib import dump, load  # Для сохранения/загрузки моделей
from collections import Counter  # Для подсчёта классов
from imblearn.over_sampling import SMOTE  # Для oversampling классов
from sklearn.model_selection import train_test_split  # Для разделения данных
from sklearn.ensemble import RandomForestClassifier  # Модель случайного леса
from sklearn.metrics import accuracy_score, classification_report  # Метрики оценки

import ccxt  # Библиотека для взаимодействия с биржами 

# Настройка логирования для вывода сообщений в консоль и файл
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def get_application_path():
    """Определяет путь к исполняемому файлу или скрипту."""
    if getattr(sys, 'frozen', False):  # Если запущен как .exe
        return os.path.dirname(sys.executable)
    elif '__file__' in globals():  # Если запущен как .py-скрипт
        return os.path.dirname(os.path.abspath(__file__))
    else:  # Если запущен в интерпретаторе (REPL, Jupyter)
        return os.getcwd()

application_path = get_application_path()
print("Путь к приложению:", application_path)

# Чтение конфигурационного файла settings.ini
config = configparser.ConfigParser()
config.read(os.path.join(application_path, "settings.ini"), encoding="utf-8")

try:
    # Инициализация биржи Binance через ccxt (добавлено, предполагая API ключи в config, если нужно)
    binance = ccxt.binance({
        'apiKey': config.get('BINANCE', 'api_key', fallback=''),  # Если нужны ключи
        'secret': config.get('BINANCE', 'secret', fallback=''),
    })

    # Пример пары, взять из config или аргумента (добавлено, замените на реальное)
    pair = config.get('SETTINGS_PAIR', 'pair', fallback='BTC-USDT')  # Например, 'BTC-USDT'

    if binance.has['fetchOHLCV']:
        logger.debug(f"binance 1h {pair}")
        logger.debug(f"ms {int(binance.milliseconds())}")

        # Путь к модели
        model_path = os.path.join(application_path, f"models/crypto_model_1h_{pair.replace('-', '')}.joblib")

        if os.path.exists(model_path):
            count_day = int(config['SETTINGS_PAIR']['count_day'])
        else:
            count_day = 0
            logger.info(f"Модель для пары {pair.replace('-', '')} не найдена! Запрашиваем свечи с начала торгов.")

        # Расчёт начальной даты для загрузки данных
        if count_day != 0:
            since = int(binance.milliseconds()) - 86400000 * count_day  # -N дней от текущего времени
        else:
            since = 0  # С начала торгов

        klines = []  # Список для хранения свечей

        # Цикл загрузки OHLCV данных порциями по 1000 свечей
        while since < int(binance.milliseconds()):
            orders = binance.fetch_ohlcv(
                symbol=pair.replace('-', '/'),
                since=since,
                limit=1000,
                timeframe='1h'
            )
            time.sleep(binance.rateLimit / 1000)  # Задержка по rate limit биржи (улучшено)

            if len(orders):
                since = orders[-1][0] + 1  # Обновление since на следующую свечу
                klines += orders
            else:
                break

        # Функция для преобразования сырых данных в DataFrame
        def fetch_ohlcv(klines):
            df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
            return df

        data_1h = fetch_ohlcv(klines)
        data_1h['timestamp'] = pd.to_datetime(data_1h['timestamp'], unit='ms')  # Преобразование timestamp в datetime
        data_1h.set_index('timestamp', inplace=True)  # Установка timestamp как индекса
        logger.debug(f"data_df {data_1h}")

        # Функция для добавления технических индикаторов и признаков
        def add_features(data_1h):
            if data_1h.empty:
                logger.debug("DataFrame пуст. Проверьте источник данных.")
                return data_1h

            # Расчёт свечных паттернов в процентах
            data_1h['lower_shadow'] = (data_1h['open'] - data_1h['low']) / data_1h['open'] * 100
            data_1h['upper_shadow'] = (data_1h['high'] - data_1h['close']) / data_1h['open'] * 100
            data_1h['body_size'] = abs(data_1h['close'] - data_1h['open']) / data_1h['open'] * 100
            data_1h['volatility'] = (data_1h['high'] - data_1h['low']) / data_1h['open'] * 100
            data_1h['volume_change'] = data_1h['volume'].pct_change().replace([np.inf, -np.inf], np.nan).fillna(0)

            # Добавление лаговых признаков для теней и волатильности
            for lag in range(1, 6):
                data_1h[f'lag_{lag}_shadow'] = data_1h['lower_shadow'].shift(lag)
                data_1h[f'lag_{lag}_volatility'] = data_1h['volatility'].shift(lag)

            # Rate of Change (ROC) за 3 периода
            data_1h['roc_3'] = data_1h['close'].pct_change(periods=3) * 100

            # Функция расчёта RSI
            def calculate_rsi(series, period=14):
                delta = series.diff()
                gain = np.where(delta > 0, delta, 0)
                loss = np.where(delta < 0, -delta, 0)
                avg_gain = pd.Series(gain).rolling(window=period).mean()
                avg_loss = pd.Series(loss).rolling(window=period).mean()
                rs = avg_gain / avg_loss
                rsi = 100 - (100 / (1 + rs))
                return rsi

            data_1h['rsi_3'] = calculate_rsi(data_1h['close'], period=3)

            # Лаговые разницы цен
            for lag in range(1, 6):
                data_1h[f'diff_lag_{lag}'] = data_1h['close'] - data_1h['close'].shift(lag)

            # Кластеры объёма
            data_1h['volume_cluster'] = data_1h['volume'] / data_1h['volume'].rolling(window=3).mean()

            # Скользящие статистики
            data_1h['rolling_max'] = data_1h['high'].rolling(window=3).max()
            data_1h['rolling_min'] = data_1h['low'].rolling(window=3).min()
            data_1h['rolling_std'] = data_1h['close'].rolling(window=3).std()

            # Индикаторы тренда (SMA, EMA)
            data_1h['sma_50'] = data_1h['close'].rolling(window=50).mean()
            data_1h['sma_200'] = data_1h['close'].rolling(window=200).mean()
            data_1h['ema_14'] = data_1h['close'].ewm(span=14, adjust=False).mean()
            data_1h['price_sma50_diff'] = data_1h['close'] - data_1h['sma_50']
            data_1h['price_sma200_diff'] = data_1h['close'] - data_1h['sma_200']
            data_1h['price_ema_ratio'] = data_1h['close'] / data_1h['ema_14']

            # Индикаторы волатильности (ATR)
            data_1h['atr'] = (data_1h['high'] - data_1h['low']).rolling(window=6).mean()
            data_1h['volatility_ratio'] = data_1h['volatility'] / data_1h['atr']  # Улучшено: отношение к ATR вместо close

            # Объёмные признаки
            data_1h['volume_sma'] = data_1h['volume'].rolling(window=3).mean()
            data_1h['volume_ratio'] = data_1h['volume'] / data_1h['volume_sma']

            # Лаговые разности
            data_1h['price_diff_1'] = data_1h['close'] - data_1h['close'].shift(1)
            data_1h['volume_diff_1'] = data_1h['volume'] - data_1h['volume'].shift(1)

            # Логирование колонок и данных
            column_names = data_1h.columns.tolist()
            logger.debug(f"Заголовки data_1h 1h: {column_names}")
            logger.debug(f"Признаки data_1h 1h: {data_1h}")

            # Обработка NaN: forward fill, затем нули (улучшено)
            data_1h.fillna(method='ffill', inplace=True)
            data_1h.fillna(0, inplace=True)

            return data_1h

        # Функция обучения модели
        def train_model(data_1h, model_path):
            data_1h = add_features(data_1h)

            # Определение таргета: рост цены на 3 свечи вперёд (устойчивый тренд)
            data_1h['target'] = ((data_1h['close'] > data_1h['close'].shift(1)) &
                                 (data_1h['close'].shift(-1) > data_1h['close']) &
                                 (data_1h['close'].shift(-2) > data_1h['close'])).astype(int)

            # Удаление строк с NaN в таргете (из-за shift(-1/-2))
            data_1h.dropna(subset=['target'], inplace=True)

            features = [col for col in data_1h.columns if col not in ['target']]
            X = data_1h[features]
            y = data_1h['target']
            logger.debug(f"Распределение классов: {Counter(y)}")

            # Проверка на достаточность данных
            if len(Counter(y)) < 2 or Counter(y).get(1, 0) < 5:
                logger.warning("Недостаточно данных для обучения.")
                return None

            # Разделение на train/test
            X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

            # Oversampling с SMOTE для баланса классов
            smote = SMOTE(random_state=42)
            X_train_res, y_train_res = smote.fit_resample(X_train, y_train)

            # Обучение модели
            model = RandomForestClassifier(n_estimators=10000, random_state=42)
            model.fit(X_train_res, y_train_res)

            # Предсказания и метрики
            y_pred = model.predict(X_test)
            logger.debug(f"Accuracy: {accuracy_score(y_test, y_pred)}")
            logger.debug(f"Classification Report:\n{classification_report(y_test, y_pred)}")

            # Сохранение модели
            dump(model, model_path)
            logger.info(f"Модель сохранена в {model_path}")
            return model

        # Функция предсказания вероятности входа
        def predict_entry_point(model, data_1h):
            data_1h = add_features(data_1h)  # Добавление признаков, если не добавлены
            features = [col for col in data_1h.columns if col not in ['target']]
            latest_data = data_1h[features].iloc[-1:]
            probability = model.predict_proba(latest_data)[:, 1][0]
            logger.debug(f"Вероятность точки входа в лонг: {probability:.2%}")
            return probability

        # Основная логика
        if data_1h.empty:
            logger.debug("DataFrame пуст, обучение пропущено")
        else:
            model_path = os.path.join(application_path, f"models/crypto_model_1h_{pair.replace('-', '')}.joblib")
            if os.path.exists(model_path):
                model = load(model_path)
                logger.info(f"Модель для пары: {pair.replace('-', '')} загружена.")
            else:
                model = train_model(data_1h, model_path)

            if model:
                prob = predict_entry_point(model, data_1h)
                if prob > 0.7:
                    logger.info(f"Найдена точка входа в лонг с вероятностью: {round(prob * 100, 2)}%.")
                else:
                    logger.info(f"Точка входа в лонг для пары: {pair.replace('-', '')} не найдена. Вероятность: {round(prob * 100, 2)}%")

except Exception as error:
    logger.exception(error)