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)