import time import pandas as pd import pandas_ta as ta from binance.client import Client from binance.exceptions import BinanceAPIException from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline from peft import PeftModel import torch import config as cfg # --- Инициализация клиента Binance --- if cfg.USE_TESTNET: client = Client(cfg.BINANCE_API_KEY, cfg.BINANCE_API_SECRET, testnet=True) print("Используется ТЕСТОВАЯ СЕТЬ Binance.") else: client = Client(cfg.BINANCE_API_KEY, cfg.BINANCE_API_SECRET) print("ВНИМАНИЕ: Используется РЕАЛЬНАЯ СЕТЬ Binance!") # --- Загрузка обученной модели (адаптера) --- model = None tokenizer = None text_generator = None try: bnb_config_inf = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16, ) base_model_for_inf = AutoModelForCausalLM.from_pretrained( cfg.BASE_MODEL_NAME, quantization_config=bnb_config_inf, torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16, device_map="auto", trust_remote_code=True ) tokenizer = AutoTokenizer.from_pretrained(cfg.BASE_MODEL_NAME, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token model = PeftModel.from_pretrained(base_model_for_inf, cfg.FINETUNED_ADAPTER_PATH) model.eval() print(f"Адаптер {cfg.FINETUNED_ADAPTER_PATH} успешно загружен.") text_generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device_map="auto") except Exception as e: print(f"Ошибка загрузки модели или адаптера: {e}") print("Убедитесь, что модель была обучена и адаптер сохранен в FINETUNED_ADAPTER_PATH.") def get_binance_klines_df(symbol, interval_str, limit): """Получает klines и преобразует в pandas DataFrame с правильными типами.""" # Преобразование строки интервала в константу Binance Client interval_map = { '1m': Client.KLINE_INTERVAL_1MINUTE, '3m': Client.KLINE_INTERVAL_3MINUTE, '5m': Client.KLINE_INTERVAL_5MINUTE, '15m': Client.KLINE_INTERVAL_15MINUTE, '30m': Client.KLINE_INTERVAL_30MINUTE, '1h': Client.KLINE_INTERVAL_1HOUR, '2h': Client.KLINE_INTERVAL_2HOUR, '4h': Client.KLINE_INTERVAL_4HOUR, '6h': Client.KLINE_INTERVAL_6HOUR, '8h': Client.KLINE_INTERVAL_8HOUR, '12h': Client.KLINE_INTERVAL_12HOUR, '1d': Client.KLINE_INTERVAL_1DAY, '3d': Client.KLINE_INTERVAL_3DAY, '1w': Client.KLINE_INTERVAL_1WEEK, '1M': Client.KLINE_INTERVAL_1MONTH } if interval_str not in interval_map: raise ValueError(f"Неподдерживаемый интервал: {interval_str}") interval = interval_map[interval_str] try: klines = client.get_klines(symbol=symbol, interval=interval, limit=limit) df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_asset_volume', 'number_trades', 'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume', 'ignore']) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') df.set_index('timestamp', inplace=True) # Устанавливаем индекс для pandas_ta # Преобразование колонок в числовой тип cols_to_numeric = ['open', 'high', 'low', 'close', 'volume', 'quote_asset_volume', 'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume'] for col in cols_to_numeric: df[col] = pd.to_numeric(df[col], errors='coerce') df.rename(columns={'timestamp':'date'}, inplace=False) # Для pandas_ta нужны стандартные имена return df except BinanceAPIException as e: print(f"Ошибка API Binance при получении данных: {e}") except Exception as e: print(f"Другая ошибка при получении данных: {e}") return pd.DataFrame() def calculate_live_indicators_from_df(df): """Рассчитывает индикаторы для DataFrame с последними данными Binance.""" if df.empty: return df # Убедимся, что колонки OHLCV называются так, как ожидает pandas_ta # (get_binance_klines_df уже должен это делать) # RSI df.ta.rsi(length=7, append=True, col_names=('rsi_7',)) df.ta.rsi(length=14, append=True, col_names=('rsi_14',)) # CCI df.ta.cci(length=7, append=True, col_names=('cci_7',)) df.ta.cci(length=14, append=True, col_names=('cci_14',)) # SMA df.ta.sma(length=50, append=True, col_names=('sma_50',)) df.ta.sma(length=100, append=True, col_names=('sma_100',)) # EMA df.ta.ema(length=50, append=True, col_names=('ema_50',)) df.ta.ema(length=100, append=True, col_names=('ema_100',)) # MACD - возвращает macd, macd_histogram, macd_signal macd_df = df.ta.macd() if macd_df is not None and not macd_df.empty: df['macd'] = macd_df.iloc[:,0] # Берем первую колонку (обычно это и есть линия MACD) else: df['macd'] = pd.NA # Bollinger Bands - возвращает BBL, BBM, BBU, BBB, BBP bbands_df = df.ta.bbands(length=20) # Стандартная длина 20 для BB if bbands_df is not None and not bbands_df.empty: # Предполагаем, что 'bollinger' в вашем CSV - это средняя линия df['bollinger'] = bbands_df.iloc[:,1] # BBM_20_2.0 (средняя линия) else: df['bollinger'] = pd.NA # TrueRange - pandas_ta имеет atr, который использует TrueRange внутри. # Если нужен сам TrueRange, можно рассчитать его отдельно или взять из ATR расчета. # df.ta.true_range(append=True, col_names=('TrueRange',)) # Если pandas_ta такой имеет # Вручную True Range: df['prev_close'] = df['close'].shift(1) df['hl'] = df['high'] - df['low'] df['h_pc'] = abs(df['high'] - df['prev_close']) df['l_pc'] = abs(df['low'] - df['prev_close']) df['TrueRange'] = df[['hl', 'h_pc', 'l_pc']].max(axis=1) df.drop(columns=['prev_close', 'hl', 'h_pc', 'l_pc'], inplace=True) # ATR df.ta.atr(length=7, append=True, col_names=('atr_7',)) df.ta.atr(length=14, append=True, col_names=('atr_14',)) return df def format_live_data_for_llm(current_data_row): """Форматирует живые данные для LLM, используя ВСЕ колонки, как в обучении.""" # Собираем все колонки, которые были в обучающем датасете (кроме next_day_close) # На основе вашего CSV: expected_cols_for_prompt = [ 'open', 'high', 'low', 'close', 'volume', 'rsi_7', 'rsi_14', 'cci_7', 'cci_14', 'sma_50', 'ema_50', 'sma_100', 'ema_100', 'macd', 'bollinger', 'TrueRange', 'atr_7', 'atr_14' ] prompt_parts = [] # Базовая информация OHLCV первой for col in ['open', 'high', 'low', 'close', 'volume']: if col in current_data_row and pd.notna(current_data_row[col]): value = current_data_row[col] prompt_parts.append(f"{col}: {value:.2f}" if col != 'volume' else f"{col}: {value:.0f}") base_prompt = ", ".join(prompt_parts) + "." # Технические индикаторы indicator_descs = [] for col in expected_cols_for_prompt: if col not in ['open', 'high', 'low', 'close', 'volume'] and \ col in current_data_row and pd.notna(current_data_row[col]): indicator_descs.append(f"{col.replace('_', ' ')}: {current_data_row[col]:.2f}") if indicator_descs: tech_prompt = "Technical indicators: " + ", ".join(indicator_descs) + "." full_description = base_prompt + " " + tech_prompt else: full_description = base_prompt # Полный промпт для модели (должен совпадать с форматом обучения) llm_prompt = f"[INST] Анализ рынка BTC/USDT на основе следующих данных: {full_description} Какое торговое действие (BUY, SELL, или HOLD) следует предпринять? [/INST]" return llm_prompt def get_llm_prediction(prompt_text): if not text_generator: print("Генератор текста (LLM) не инициализирован.") return "HOLD" try: outputs = text_generator(prompt_text, max_new_tokens=10, do_sample=False, pad_token_id=tokenizer.eos_token_id) full_response = outputs[0]['generated_text'] signal_text = full_response.split("[/INST]")[-1].strip().upper() if "BUY" in signal_text: return "BUY" if "SELL" in signal_text: return "SELL" return "HOLD" # По умолчанию HOLD, если нечеткий сигнал except Exception as e: print(f"Ошибка при генерации текста LLM: {e}") return "HOLD" def execute_trade_logic(signal, symbol, quantity_usd): """Очень упрощенная логика торговли (см. предыдущий ответ для деталей и предупреждений).""" if signal == "HOLD": print("Сигнал: HOLD. Нет действий.") return try: ticker = client.get_symbol_ticker(symbol=symbol) current_price = float(ticker['price']) if current_price == 0: return quantity_asset_precise = quantity_usd / current_price # Получение информации о символе для форматирования количества info = client.get_symbol_info(symbol) step_size = 0.0 min_notional_val = 5.0 # Обычно около 5-10 USD для BTCUSDT for f in info['filters']: if f['filterType'] == 'LOT_SIZE': step_size = float(f['stepSize']) if f['filterType'] == 'MIN_NOTIONAL': min_notional_val = float(f['minNotional']) if step_size > 0: quantity_asset = (quantity_asset_precise // step_size) * step_size else: # Если step_size не найден или 0, пробуем округление (менее надежно) # Для BTC обычно 5-6 знаков после запятой для количества if symbol == "BTCUSDT": quantity_asset = round(quantity_asset_precise, 5) else: quantity_asset = round(quantity_asset_precise, 3) if quantity_asset == 0: print(f"Рассчитанное количество актива {quantity_asset_precise:.8f} после округления стало 0. Сделка не выполняется.") return if quantity_asset * current_price < min_notional_val: print(f"Стоимость ордера {quantity_asset * current_price:.2f} USD меньше минимальной ({min_notional_val} USD). Сделка не выполняется.") return print(f"Попытка исполнить {signal} ордер для {symbol}, количество: {quantity_asset} (~{quantity_usd}$)") if signal == "BUY": # Тут должна быть проверка баланса USDT print(f"Размещение MARKET BUY ордера на {quantity_asset} {symbol.replace('USDT','')}...") order = client.order_market_buy(symbol=symbol, quantity=quantity_asset) print("BUY ордер размещен:", order) elif signal == "SELL": # Тут должна быть проверка баланса базового актива (напр. BTC) print(f"Размещение MARKET SELL ордера на {quantity_asset} {symbol.replace('USDT','')}...") order = client.order_market_sell(symbol=symbol, quantity=quantity_asset) print("SELL ордер размещен:", order) except BinanceAPIException as e: print(f"Ошибка API Binance при исполнении ордера: {e}") except Exception as e: print(f"Другая ошибка при исполнении ордера: {e}") def trading_loop(): if not model or not text_generator: print("Модель не загружена. Торговый цикл не может быть запущен.") return # Определяем интервал и время ожидания kline_interval_str = cfg.KLINE_INTERVAL_TO_TRADE sleep_duration_seconds = 0 if 'm' in kline_interval_str: sleep_duration_seconds = int(kline_interval_str.replace('m', '')) * 60 elif 'h' in kline_interval_str: sleep_duration_seconds = int(kline_interval_str.replace('h', '')) * 3600 elif 'd' in kline_interval_str: sleep_duration_seconds = int(kline_interval_str.replace('d', '')) * 86400 else: # По умолчанию 1 час, если не распознано sleep_duration_seconds = 3600 print(f"Не удалось определить время сна для интервала {kline_interval_str}, используется {sleep_duration_seconds} сек.") while True: print(f"\n--- {time.ctime()} ---") # 1. Получение свежих данных df_live = get_binance_klines_df(cfg.TRADING_PAIR, kline_interval_str, cfg.LOOKBACK_PERIODS_LIVE) if df_live.empty or len(df_live) < 100: # Нужно достаточно данных для SMA100/EMA100 и ATR14 print(f"Недостаточно данных для анализа ({len(df_live)} строк). Пропускаем цикл.") time.sleep(60) continue # 2. Расчет индикаторов df_with_indicators = calculate_live_indicators_from_df(df_live.copy()) # Берем последнюю ПОЛНУЮ свечу для анализа # get_klines обычно возвращает последнюю свечу как частично сформированную, если limit > 1 # Для анализа лучше брать предпоследнюю, если это так. # Или, если уверены, что последняя закрыта (например, если limit=1 и время свечи прошло) # Для простоты, если используем lookback > 1, берем предпоследнюю. if len(df_with_indicators) < 2: print("Недостаточно строк после расчета индикаторов. Пропускаем.") time.sleep(sleep_duration_seconds) continue current_market_state_row = df_with_indicators.iloc[-2] # Предпоследняя строка, более вероятно закрытая свеча # Проверка на NaN в нужных колонках для current_market_state_row # Список колонок, которые должны быть в промпте (из format_live_data_for_llm) required_cols_for_prompt = [ 'open', 'high', 'low', 'close', 'volume', 'rsi_7', 'rsi_14', 'cci_7', 'cci_14', 'sma_50', 'ema_50', 'sma_100', 'ema_100', 'macd', 'bollinger', 'TrueRange', 'atr_7', 'atr_14' ] if current_market_state_row[required_cols_for_prompt].isnull().any(): print("В данных для анализа (предпоследняя свеча) есть NaN значения в ключевых индикаторах. Пропускаем цикл.") print(current_market_state_row[current_market_state_row[required_cols_for_prompt].isnull()]) time.sleep(sleep_duration_seconds) continue # 3. Форматирование для LLM llm_prompt = format_live_data_for_llm(current_market_state_row) print("Промпт для LLM:", llm_prompt) # 4. Получение предсказания от LLM predicted_signal = get_llm_prediction(llm_prompt) print(f"Предсказанный сигнал: {predicted_signal}") # 5. Исполнение сделки (С ОСТОРОЖНОСТЬЮ!) if not cfg.USE_TESTNET: user_confirm = input("ВЫ УВЕРЕНЫ, ЧТО ХОТИТЕ ТОРГОВАТЬ НА РЕАЛЬНОМ СЧЕТЕ? (yes/NO):") if user_confirm.lower() != "yes": print("Торговля на реальном счете отменена.") return execute_trade_logic(predicted_signal, cfg.TRADING_PAIR, cfg.TRADE_AMOUNT_USD) print(f"Ожидание следующего цикла ({sleep_duration_seconds // 60} минут)...") time.sleep(sleep_duration_seconds) if __name__ == "__main__": if model and text_generator: trading_loop() else: print("Модель не была корректно загружена. Запустите скрипт обучения (1_finetune_mixtral.py) " "или проверьте путь к адаптеру в config.py (FINETUNED_ADAPTER_PATH).")