Spaces:
Sleeping
Sleeping
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"<s>[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).") |