lora / 2_trading_bot_logic.py
Dmitriy-Egorov's picture
Update 2_trading_bot_logic.py
444f3df verified
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).")