Spaces:
Sleeping
Sleeping
Update 2_trading_bot_logic.py
Browse files- 2_trading_bot_logic.py +224 -222
2_trading_bot_logic.py
CHANGED
|
@@ -6,82 +6,78 @@ from binance.exceptions import BinanceAPIException
|
|
| 6 |
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
|
| 7 |
from peft import PeftModel
|
| 8 |
import torch
|
| 9 |
-
import config as cfg
|
| 10 |
-
# from dotenv import load_dotenv # Если используете .env
|
| 11 |
-
# import os
|
| 12 |
-
|
| 13 |
-
# load_dotenv() # Загрузка переменных из .env, если используется
|
| 14 |
-
# API_KEY = os.getenv("BINANCE_API_KEY")
|
| 15 |
-
# API_SECRET = os.getenv("BINANCE_API_SECRET")
|
| 16 |
-
# Если не .env, то берем из config.py (НЕБЕЗОПАСНО ДЛЯ ПРОДА)
|
| 17 |
-
API_KEY = cfg.BINANCE_API_KEY
|
| 18 |
-
API_SECRET = cfg.BINANCE_API_SECRET
|
| 19 |
|
| 20 |
# --- Инициализация клиента Binance ---
|
| 21 |
if cfg.USE_TESTNET:
|
| 22 |
-
client = Client(
|
| 23 |
print("Используется ТЕСТОВАЯ СЕТЬ Binance.")
|
| 24 |
else:
|
| 25 |
-
client = Client(
|
| 26 |
print("ВНИМАНИЕ: Используется РЕАЛЬНАЯ СЕТЬ Binance!")
|
| 27 |
|
| 28 |
-
|
| 29 |
# --- Загрузка обученной модели (адаптера) ---
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
bnb_4bit_quant_type="nf4",
|
| 34 |
-
bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
base_model = AutoModelForCausalLM.from_pretrained(
|
| 38 |
-
cfg.BASE_MODEL_NAME,
|
| 39 |
-
quantization_config=bnb_config_inf,
|
| 40 |
-
torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
|
| 41 |
-
device_map="auto",
|
| 42 |
-
trust_remote_code=True
|
| 43 |
-
)
|
| 44 |
-
tokenizer = AutoTokenizer.from_pretrained(cfg.BASE_MODEL_NAME, trust_remote_code=True)
|
| 45 |
-
tokenizer.pad_token = tokenizer.eos_token
|
| 46 |
|
| 47 |
try:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
print(f"Ошибка загрузки адаптера модели из {cfg.FINETUNED_ADAPTER_PATH}: {e}")
|
| 53 |
-
print("Убедитесь, что модель была обучена и адаптер сохранен.")
|
| 54 |
-
model = None # Чтобы последующий код не упал сразу
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# Или, если вы слили модель:
|
| 58 |
-
# model = AutoModelForCausalLM.from_pretrained(cfg.MERGED_MODEL_PATH, device_map="auto", torch_dtype=torch.bfloat16)
|
| 59 |
-
# tokenizer = AutoTokenizer.from_pretrained(cfg.MERGED_MODEL_PATH)
|
| 60 |
-
# model.eval()
|
| 61 |
-
|
| 62 |
-
# Создаем pipeline для удобства, если модель и токенизатор загружены
|
| 63 |
-
if model and tokenizer:
|
| 64 |
-
text_generator = pipeline(
|
| 65 |
-
"text-generation",
|
| 66 |
-
model=model,
|
| 67 |
-
tokenizer=tokenizer,
|
| 68 |
-
device_map="auto" # или model.device
|
| 69 |
)
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
def get_market_data_binance(symbol, interval, limit):
|
| 75 |
-
"""Получает исторические данные (свечи) с Binance."""
|
| 76 |
try:
|
| 77 |
klines = client.get_klines(symbol=symbol, interval=interval, limit=limit)
|
| 78 |
-
df = pd.DataFrame(klines, columns=['
|
| 79 |
-
'
|
| 80 |
-
'
|
| 81 |
-
df['
|
| 82 |
-
df.set_index('
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return df
|
| 86 |
except BinanceAPIException as e:
|
| 87 |
print(f"Ошибка API Binance при получении данных: {e}")
|
|
@@ -89,160 +85,162 @@ def get_market_data_binance(symbol, interval, limit):
|
|
| 89 |
print(f"Другая ошибка при получении данных: {e}")
|
| 90 |
return pd.DataFrame()
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
return df
|
| 103 |
|
| 104 |
|
| 105 |
-
def format_live_data_for_llm(current_data_row
|
| 106 |
-
"""Форматирует живые данные для LLM,
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
if
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
# Полный промпт для модели (должен совпадать с форматом обучения)
|
| 117 |
-
|
| 118 |
-
return
|
| 119 |
|
| 120 |
|
| 121 |
def get_llm_prediction(prompt_text):
|
| 122 |
-
"""Получает предсказание от LLM."""
|
| 123 |
if not text_generator:
|
| 124 |
print("Генератор текста (LLM) не инициализирован.")
|
| 125 |
-
return "HOLD"
|
| 126 |
-
|
| 127 |
-
# Параметры генерации
|
| 128 |
-
# max_new_tokens должно быть маленьким, т.к. мы ожидаем одно слово: BUY, SELL, или HOLD
|
| 129 |
try:
|
| 130 |
-
outputs = text_generator(
|
| 131 |
-
prompt_text,
|
| 132 |
-
max_new_tokens=5, # Ожидаем короткий ответ
|
| 133 |
-
do_sample=False, # Для более детерминированного ответа
|
| 134 |
-
pad_token_id=tokenizer.eos_token_id
|
| 135 |
-
)
|
| 136 |
-
# Ответ будет в outputs[0]['generated_text']
|
| 137 |
-
# Нужно извлечь сам сигнал из сгенерированного текста
|
| 138 |
full_response = outputs[0]['generated_text']
|
| 139 |
-
# Извлекаем то, что после [/INST]
|
| 140 |
signal_text = full_response.split("[/INST]")[-1].strip().upper()
|
| 141 |
|
| 142 |
-
|
| 143 |
-
if
|
| 144 |
-
|
| 145 |
-
if signal_text.startswith("HOLD"): return "HOLD"
|
| 146 |
-
|
| 147 |
-
print(f"Нераспознанный сигнал от LLM: '{signal_text}' из ответа '{full_response}'. Возвращаем HOLD.")
|
| 148 |
-
return "HOLD"
|
| 149 |
except Exception as e:
|
| 150 |
print(f"Ошибка при генерации текста LLM: {e}")
|
| 151 |
return "HOLD"
|
| 152 |
|
| 153 |
-
def
|
| 154 |
-
"""
|
| 155 |
if signal == "HOLD":
|
| 156 |
print("Сигнал: HOLD. Нет действий.")
|
| 157 |
return
|
| 158 |
|
| 159 |
-
# Получаем текущую цену для расчета количества актива
|
| 160 |
try:
|
| 161 |
ticker = client.get_symbol_ticker(symbol=symbol)
|
| 162 |
current_price = float(ticker['price'])
|
| 163 |
-
|
| 164 |
-
print(f"Не удалось получить текущую цену для {symbol}: {e}")
|
| 165 |
-
return
|
| 166 |
-
|
| 167 |
-
if current_price == 0:
|
| 168 |
-
print(f"Текущая цена для {symbol} равна 0. Невозможно рассчитать количество.")
|
| 169 |
-
return
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
# Получаем информацию о символе для форматирования
|
| 175 |
-
try:
|
| 176 |
info = client.get_symbol_info(symbol)
|
| 177 |
step_size = 0.0
|
| 178 |
-
|
|
|
|
| 179 |
for f in info['filters']:
|
| 180 |
if f['filterType'] == 'LOT_SIZE':
|
| 181 |
step_size = float(f['stepSize'])
|
| 182 |
-
if f['filterType'] == '
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
# Форматируем количество
|
| 186 |
if step_size > 0:
|
| 187 |
-
|
| 188 |
-
#
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
return
|
| 193 |
|
| 194 |
-
except Exception as e:
|
| 195 |
-
print(f"Ошибка получения информации о символе {symbol} для форматирования ордера: {e}")
|
| 196 |
-
# Продолжаем без точного форматирования, может вызвать ошибку при создании ордера
|
| 197 |
-
|
| 198 |
-
if quantity_asset == 0:
|
| 199 |
-
print("Рассчитанное количество актива равно 0. Сделка не выполняется.")
|
| 200 |
-
return
|
| 201 |
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
-
# Этот пример очень наивен: он просто покупает или продает.
|
| 207 |
-
# В реальной системе нужно отслеживать открытые позиции,
|
| 208 |
-
# решать, когда закрывать существующую позицию перед открытием новой и т.д.
|
| 209 |
-
# Например, если уже есть LONG позиция, а сигнал SELL, то нужно сначала продать (закрыть LONG),
|
| 210 |
-
# а потом, возможно, открыть SHORT (если это фьючерсы) или просто продать и ждать.
|
| 211 |
-
# Для спота: если есть BTC и сигнал SELL -> продаем BTC. Если нет BTC и сигнал BUY -> покупаем.
|
| 212 |
|
| 213 |
-
try:
|
| 214 |
if signal == "BUY":
|
| 215 |
-
#
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
usdt_balance = 100000 # Предположим большой баланс в тестнете
|
| 219 |
-
|
| 220 |
-
if usdt_balance < quantity_usd:
|
| 221 |
-
print(f"Недостаточно USDT для покупки. Доступно: {usdt_balance}, нужно: {quantity_usd}")
|
| 222 |
-
return
|
| 223 |
-
|
| 224 |
-
print(f"Размещение MARKET BUY ордера на {quantity_asset:.8f} {symbol.replace('USDT','')}...")
|
| 225 |
-
order = client.order_market_buy(symbol=symbol, quantity=round(quantity_asset, 5)) # Округление для BTC
|
| 226 |
print("BUY ордер размещен:", order)
|
| 227 |
-
|
| 228 |
elif signal == "SELL":
|
| 229 |
-
#
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
if asset_balance_data:
|
| 233 |
-
asset_balance = float(asset_balance_data['free'])
|
| 234 |
-
else:
|
| 235 |
-
asset_balance = 0.0
|
| 236 |
-
|
| 237 |
-
if asset_balance < quantity_asset:
|
| 238 |
-
print(f"Недостаточно {base_asset} для продажи. Доступно: {asset_balance}, нужно: {quantity_asset}")
|
| 239 |
-
# Можно продать все, что есть, если это часть стратегии
|
| 240 |
-
# quantity_asset = asset_balance
|
| 241 |
-
# if quantity_asset == 0: return
|
| 242 |
-
return
|
| 243 |
-
|
| 244 |
-
print(f"Размещение MARKET SELL ордера на {quantity_asset:.8f} {symbol.replace('USDT','')}...")
|
| 245 |
-
order = client.order_market_sell(symbol=symbol, quantity=round(quantity_asset, 5)) # Округление для BTC
|
| 246 |
print("SELL ордер размещен:", order)
|
| 247 |
|
| 248 |
except BinanceAPIException as e:
|
|
@@ -256,52 +254,60 @@ def trading_loop():
|
|
| 256 |
print("Модель не загружена. Торговый цикл не может быть запущен.")
|
| 257 |
return
|
| 258 |
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
while True:
|
| 262 |
print(f"\n--- {time.ctime()} ---")
|
| 263 |
# 1. Получение свежих данных
|
| 264 |
-
|
| 265 |
-
# Если обучались на дневных, а тут минутные - будет плохо.
|
| 266 |
-
# Для примера возьмем часовые свечи (Client.KLINE_INTERVAL_1HOUR)
|
| 267 |
-
# cfg.LOOKBACK_PERIODS должно быть достаточным для расчета всех индикаторов.
|
| 268 |
-
df_live = get_market_data_binance(cfg.TRADING_PAIR, Client.KLINE_INTERVAL_1HOUR, cfg.LOOKBACK_PERIODS + 5)
|
| 269 |
|
| 270 |
-
if df_live.empty or len(df_live) <
|
| 271 |
-
print("Недостаточно данных для анализа. Пропускаем цикл.")
|
| 272 |
-
time.sleep(60)
|
| 273 |
continue
|
| 274 |
|
| 275 |
# 2. Расчет индикаторов
|
| 276 |
-
|
| 277 |
|
| 278 |
-
# Берем последнюю ПОЛНУЮ свечу для анализа
|
| 279 |
-
#
|
| 280 |
-
# Для
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
current_market_state_row = df_live.iloc[-2]
|
| 288 |
-
if current_market_state_row[indicators_to_use_live].isnull().any():
|
| 289 |
-
print("Предпоследняя строка также содержит NaN. Пропускаем цикл.")
|
| 290 |
-
time.sleep(60)
|
| 291 |
-
continue
|
| 292 |
-
else:
|
| 293 |
-
print("Недостаточно строк для анализа после расчета индикаторов. Пропускаем цикл.")
|
| 294 |
-
time.sleep(60)
|
| 295 |
-
continue
|
| 296 |
-
|
| 297 |
-
# 3. Форматирование для LLM
|
| 298 |
-
llm_prompt = format_live_data_for_llm(current_market_state_row, indicators_to_use_live)
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
continue
|
| 304 |
|
|
|
|
|
|
|
| 305 |
print("Промпт для LLM:", llm_prompt)
|
| 306 |
|
| 307 |
# 4. Получение предсказания от LLM
|
|
@@ -309,24 +315,20 @@ def trading_loop():
|
|
| 309 |
print(f"Предсказанный сигнал: {predicted_signal}")
|
| 310 |
|
| 311 |
# 5. Исполнение сделки (С ОСТОРОЖНОСТЬЮ!)
|
| 312 |
-
# ВАЖНО: Здесь должна быть серьезная логика управления рисками, проверки баланса и т.д.
|
| 313 |
if not cfg.USE_TESTNET:
|
| 314 |
-
|
| 315 |
-
if
|
| 316 |
-
print("Торговля на реальном счете отменена
|
| 317 |
-
return
|
| 318 |
|
| 319 |
-
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
# Если часовые свечи, то ждать около часа.
|
| 324 |
-
print("Ожидание следующего цикла (1 час)...")
|
| 325 |
-
time.sleep(3600) # 1 час
|
| 326 |
|
| 327 |
if __name__ == "__main__":
|
| 328 |
-
# Перед запуском убедитесь, что у вас есть обученный адаптер модели!
|
| 329 |
if model and text_generator:
|
| 330 |
trading_loop()
|
| 331 |
else:
|
| 332 |
-
print("Модель не была корректно загружена. Запустите скрипт обучения 1_finetune_mixtral.py
|
|
|
|
|
|
| 6 |
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
|
| 7 |
from peft import PeftModel
|
| 8 |
import torch
|
| 9 |
+
import config as cfg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# --- Инициализация клиента Binance ---
|
| 12 |
if cfg.USE_TESTNET:
|
| 13 |
+
client = Client(cfg.BINANCE_API_KEY, cfg.BINANCE_API_SECRET, testnet=True)
|
| 14 |
print("Используется ТЕСТОВАЯ СЕТЬ Binance.")
|
| 15 |
else:
|
| 16 |
+
client = Client(cfg.BINANCE_API_KEY, cfg.BINANCE_API_SECRET)
|
| 17 |
print("ВНИМАНИЕ: Используется РЕАЛЬНАЯ СЕТЬ Binance!")
|
| 18 |
|
|
|
|
| 19 |
# --- Загрузка обученной модели (адаптера) ---
|
| 20 |
+
model = None
|
| 21 |
+
tokenizer = None
|
| 22 |
+
text_generator = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
try:
|
| 25 |
+
bnb_config_inf = BitsAndBytesConfig(
|
| 26 |
+
load_in_4bit=True,
|
| 27 |
+
bnb_4bit_quant_type="nf4",
|
| 28 |
+
bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
)
|
| 30 |
+
base_model_for_inf = AutoModelForCausalLM.from_pretrained(
|
| 31 |
+
cfg.BASE_MODEL_NAME,
|
| 32 |
+
quantization_config=bnb_config_inf,
|
| 33 |
+
torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
|
| 34 |
+
device_map="auto",
|
| 35 |
+
trust_remote_code=True
|
| 36 |
+
)
|
| 37 |
+
tokenizer = AutoTokenizer.from_pretrained(cfg.BASE_MODEL_NAME, trust_remote_code=True)
|
| 38 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 39 |
|
| 40 |
+
model = PeftModel.from_pretrained(base_model_for_inf, cfg.FINETUNED_ADAPTER_PATH)
|
| 41 |
+
model.eval()
|
| 42 |
+
print(f"Адаптер {cfg.FINETUNED_ADAPTER_PATH} успешно загружен.")
|
| 43 |
+
|
| 44 |
+
text_generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device_map="auto")
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"Ошибка загрузки модели или адаптера: {e}")
|
| 47 |
+
print("Убедитесь, что модель была обучена и адаптер сохранен в FINETUNED_ADAPTER_PATH.")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def get_binance_klines_df(symbol, interval_str, limit):
|
| 51 |
+
"""Получает klines и преобразует в pandas DataFrame с правильными типами."""
|
| 52 |
+
# Преобразование строки интервала в константу Binance Client
|
| 53 |
+
interval_map = {
|
| 54 |
+
'1m': Client.KLINE_INTERVAL_1MINUTE, '3m': Client.KLINE_INTERVAL_3MINUTE,
|
| 55 |
+
'5m': Client.KLINE_INTERVAL_5MINUTE, '15m': Client.KLINE_INTERVAL_15MINUTE,
|
| 56 |
+
'30m': Client.KLINE_INTERVAL_30MINUTE, '1h': Client.KLINE_INTERVAL_1HOUR,
|
| 57 |
+
'2h': Client.KLINE_INTERVAL_2HOUR, '4h': Client.KLINE_INTERVAL_4HOUR,
|
| 58 |
+
'6h': Client.KLINE_INTERVAL_6HOUR, '8h': Client.KLINE_INTERVAL_8HOUR,
|
| 59 |
+
'12h': Client.KLINE_INTERVAL_12HOUR, '1d': Client.KLINE_INTERVAL_1DAY,
|
| 60 |
+
'3d': Client.KLINE_INTERVAL_3DAY, '1w': Client.KLINE_INTERVAL_1WEEK,
|
| 61 |
+
'1M': Client.KLINE_INTERVAL_1MONTH
|
| 62 |
+
}
|
| 63 |
+
if interval_str not in interval_map:
|
| 64 |
+
raise ValueError(f"Неподдерживаемый интервал: {interval_str}")
|
| 65 |
+
interval = interval_map[interval_str]
|
| 66 |
|
|
|
|
|
|
|
| 67 |
try:
|
| 68 |
klines = client.get_klines(symbol=symbol, interval=interval, limit=limit)
|
| 69 |
+
df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume',
|
| 70 |
+
'close_time', 'quote_asset_volume', 'number_trades',
|
| 71 |
+
'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume', 'ignore'])
|
| 72 |
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
|
| 73 |
+
df.set_index('timestamp', inplace=True) # Устанавливаем индекс для pandas_ta
|
| 74 |
+
|
| 75 |
+
# Преобразование колонок в числовой тип
|
| 76 |
+
cols_to_numeric = ['open', 'high', 'low', 'close', 'volume', 'quote_asset_volume',
|
| 77 |
+
'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume']
|
| 78 |
+
for col in cols_to_numeric:
|
| 79 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 80 |
+
df.rename(columns={'timestamp':'date'}, inplace=False) # Для pandas_ta нужны стандартные имена
|
| 81 |
return df
|
| 82 |
except BinanceAPIException as e:
|
| 83 |
print(f"Ошибка API Binance при получении данных: {e}")
|
|
|
|
| 85 |
print(f"Другая ошибка при получении данных: {e}")
|
| 86 |
return pd.DataFrame()
|
| 87 |
|
| 88 |
+
def calculate_live_indicators_from_df(df):
|
| 89 |
+
"""Рассчитывает индикаторы для DataFrame с последними данными Binance."""
|
| 90 |
+
if df.empty: return df
|
| 91 |
+
|
| 92 |
+
# Убедимся, что колонки OHLCV называются так, как ожидает pandas_ta
|
| 93 |
+
# (get_binance_klines_df уже должен это делать)
|
| 94 |
+
|
| 95 |
+
# RSI
|
| 96 |
+
df.ta.rsi(length=7, append=True, col_names=('rsi_7',))
|
| 97 |
+
df.ta.rsi(length=14, append=True, col_names=('rsi_14',))
|
| 98 |
+
# CCI
|
| 99 |
+
df.ta.cci(length=7, append=True, col_names=('cci_7',))
|
| 100 |
+
df.ta.cci(length=14, append=True, col_names=('cci_14',))
|
| 101 |
+
# SMA
|
| 102 |
+
df.ta.sma(length=50, append=True, col_names=('sma_50',))
|
| 103 |
+
df.ta.sma(length=100, append=True, col_names=('sma_100',))
|
| 104 |
+
# EMA
|
| 105 |
+
df.ta.ema(length=50, append=True, col_names=('ema_50',))
|
| 106 |
+
df.ta.ema(length=100, append=True, col_names=('ema_100',))
|
| 107 |
+
# MACD - возвращает macd, macd_histogram, macd_signal
|
| 108 |
+
macd_df = df.ta.macd()
|
| 109 |
+
if macd_df is not None and not macd_df.empty:
|
| 110 |
+
df['macd'] = macd_df.iloc[:,0] # Берем первую колонку (обычно это и есть линия MACD)
|
| 111 |
+
else:
|
| 112 |
+
df['macd'] = pd.NA
|
| 113 |
|
| 114 |
+
# Bollinger Bands - возвращает BBL, BBM, BBU, BBB, BBP
|
| 115 |
+
bbands_df = df.ta.bbands(length=20) # Стандартная длина 20 для BB
|
| 116 |
+
if bbands_df is not None and not bbands_df.empty:
|
| 117 |
+
# Предполагаем, что 'bollinger' в вашем CSV - это средняя линия
|
| 118 |
+
df['bollinger'] = bbands_df.iloc[:,1] # BBM_20_2.0 (средняя линия)
|
| 119 |
+
else:
|
| 120 |
+
df['bollinger'] = pd.NA
|
| 121 |
+
|
| 122 |
+
# TrueRange - pandas_ta имеет atr, который использует TrueRange внутри.
|
| 123 |
+
# Если нужен сам TrueRange, можно рассчитать его отдельно или взять из ATR расчета.
|
| 124 |
+
# df.ta.true_range(append=True, col_names=('TrueRange',)) # Если pandas_ta такой имеет
|
| 125 |
+
# Вручную True Range:
|
| 126 |
+
df['prev_close'] = df['close'].shift(1)
|
| 127 |
+
df['hl'] = df['high'] - df['low']
|
| 128 |
+
df['h_pc'] = abs(df['high'] - df['prev_close'])
|
| 129 |
+
df['l_pc'] = abs(df['low'] - df['prev_close'])
|
| 130 |
+
df['TrueRange'] = df[['hl', 'h_pc', 'l_pc']].max(axis=1)
|
| 131 |
+
df.drop(columns=['prev_close', 'hl', 'h_pc', 'l_pc'], inplace=True)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ATR
|
| 135 |
+
df.ta.atr(length=7, append=True, col_names=('atr_7',))
|
| 136 |
+
df.ta.atr(length=14, append=True, col_names=('atr_14',))
|
| 137 |
+
|
| 138 |
return df
|
| 139 |
|
| 140 |
|
| 141 |
+
def format_live_data_for_llm(current_data_row):
|
| 142 |
+
"""Форматирует живые данные для LLM, используя ВСЕ колонки, как в обучении."""
|
| 143 |
+
# Собираем все колонки, которые были в обучающем датасете (кроме next_day_close)
|
| 144 |
+
# На основе вашего CSV:
|
| 145 |
+
expected_cols_for_prompt = [
|
| 146 |
+
'open', 'high', 'low', 'close', 'volume', 'rsi_7', 'rsi_14',
|
| 147 |
+
'cci_7', 'cci_14', 'sma_50', 'ema_50', 'sma_100', 'ema_100',
|
| 148 |
+
'macd', 'bollinger', 'TrueRange', 'atr_7', 'atr_14'
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
prompt_parts = []
|
| 152 |
+
# Базовая информация OHLCV первой
|
| 153 |
+
for col in ['open', 'high', 'low', 'close', 'volume']:
|
| 154 |
+
if col in current_data_row and pd.notna(current_data_row[col]):
|
| 155 |
+
value = current_data_row[col]
|
| 156 |
+
prompt_parts.append(f"{col}: {value:.2f}" if col != 'volume' else f"{col}: {value:.0f}")
|
| 157 |
+
|
| 158 |
+
base_prompt = ", ".join(prompt_parts) + "."
|
| 159 |
+
|
| 160 |
+
# Технические индикаторы
|
| 161 |
+
indicator_descs = []
|
| 162 |
+
for col in expected_cols_for_prompt:
|
| 163 |
+
if col not in ['open', 'high', 'low', 'close', 'volume'] and \
|
| 164 |
+
col in current_data_row and pd.notna(current_data_row[col]):
|
| 165 |
+
indicator_descs.append(f"{col.replace('_', ' ')}: {current_data_row[col]:.2f}")
|
| 166 |
|
| 167 |
+
if indicator_descs:
|
| 168 |
+
tech_prompt = "Technical indicators: " + ", ".join(indicator_descs) + "."
|
| 169 |
+
full_description = base_prompt + " " + tech_prompt
|
| 170 |
+
else:
|
| 171 |
+
full_description = base_prompt
|
| 172 |
+
|
| 173 |
# Полный промпт для модели (должен совпадать с форматом обучения)
|
| 174 |
+
llm_prompt = f"<s>[INST] Анализ рынка BTC/USDT на основе следующих данных: {full_description} Какое торговое действие (BUY, SELL, или HOLD) следует предпринять? [/INST]"
|
| 175 |
+
return llm_prompt
|
| 176 |
|
| 177 |
|
| 178 |
def get_llm_prediction(prompt_text):
|
|
|
|
| 179 |
if not text_generator:
|
| 180 |
print("Генератор текста (LLM) не инициализирован.")
|
| 181 |
+
return "HOLD"
|
|
|
|
|
|
|
|
|
|
| 182 |
try:
|
| 183 |
+
outputs = text_generator(prompt_text, max_new_tokens=10, do_sample=False, pad_token_id=tokenizer.eos_token_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
full_response = outputs[0]['generated_text']
|
|
|
|
| 185 |
signal_text = full_response.split("[/INST]")[-1].strip().upper()
|
| 186 |
|
| 187 |
+
if "BUY" in signal_text: return "BUY"
|
| 188 |
+
if "SELL" in signal_text: return "SELL"
|
| 189 |
+
return "HOLD" # По умолчанию HOLD, если нечеткий сигнал
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
except Exception as e:
|
| 191 |
print(f"Ошибка при генерации текста LLM: {e}")
|
| 192 |
return "HOLD"
|
| 193 |
|
| 194 |
+
def execute_trade_logic(signal, symbol, quantity_usd):
|
| 195 |
+
"""Очень упрощенная логика торговли (см. предыдущий ответ для деталей и предупреждений)."""
|
| 196 |
if signal == "HOLD":
|
| 197 |
print("Сигнал: HOLD. Нет действий.")
|
| 198 |
return
|
| 199 |
|
|
|
|
| 200 |
try:
|
| 201 |
ticker = client.get_symbol_ticker(symbol=symbol)
|
| 202 |
current_price = float(ticker['price'])
|
| 203 |
+
if current_price == 0: return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
+
quantity_asset_precise = quantity_usd / current_price
|
| 206 |
+
|
| 207 |
+
# Получение информации о символе для форматирования количества
|
|
|
|
|
|
|
| 208 |
info = client.get_symbol_info(symbol)
|
| 209 |
step_size = 0.0
|
| 210 |
+
min_notional_val = 5.0 # Обычно около 5-10 USD для BTCUSDT
|
| 211 |
+
|
| 212 |
for f in info['filters']:
|
| 213 |
if f['filterType'] == 'LOT_SIZE':
|
| 214 |
step_size = float(f['stepSize'])
|
| 215 |
+
if f['filterType'] == 'MIN_NOTIONAL':
|
| 216 |
+
min_notional_val = float(f['minNotional'])
|
| 217 |
+
|
|
|
|
| 218 |
if step_size > 0:
|
| 219 |
+
quantity_asset = (quantity_asset_precise // step_size) * step_size
|
| 220 |
+
else: # Если step_size не найден или 0, пробуем округление (менее надежно)
|
| 221 |
+
# Для BTC обычно 5-6 знаков после запятой для количества
|
| 222 |
+
if symbol == "BTCUSDT": quantity_asset = round(quantity_asset_precise, 5)
|
| 223 |
+
else: quantity_asset = round(quantity_asset_precise, 3)
|
|
|
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
+
if quantity_asset == 0:
|
| 227 |
+
print(f"Рассчитанное количество актива {quantity_asset_precise:.8f} после округления стало 0. Сделка не выполняется.")
|
| 228 |
+
return
|
| 229 |
+
if quantity_asset * current_price < min_notional_val:
|
| 230 |
+
print(f"Стоимость ордера {quantity_asset * current_price:.2f} USD меньше минимальной ({min_notional_val} USD). Сделка не выполняется.")
|
| 231 |
+
return
|
| 232 |
|
| 233 |
+
print(f"Попытка исполнить {signal} ордер для {symbol}, количество: {quantity_asset} (~{quantity_usd}$)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
|
|
|
| 235 |
if signal == "BUY":
|
| 236 |
+
# Тут должна быть проверка баланса USDT
|
| 237 |
+
print(f"Размещение MARKET BUY ордера на {quantity_asset} {symbol.replace('USDT','')}...")
|
| 238 |
+
order = client.order_market_buy(symbol=symbol, quantity=quantity_asset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
print("BUY ордер размещен:", order)
|
|
|
|
| 240 |
elif signal == "SELL":
|
| 241 |
+
# Тут должна быть проверка баланса базового актива (напр. BTC)
|
| 242 |
+
print(f"Размещение MARKET SELL ордера на {quantity_asset} {symbol.replace('USDT','')}...")
|
| 243 |
+
order = client.order_market_sell(symbol=symbol, quantity=quantity_asset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
print("SELL ордер размещен:", order)
|
| 245 |
|
| 246 |
except BinanceAPIException as e:
|
|
|
|
| 254 |
print("Модель не загружена. Торговый цикл не может быть запущен.")
|
| 255 |
return
|
| 256 |
|
| 257 |
+
# Определяем интервал и время ожидания
|
| 258 |
+
kline_interval_str = cfg.KLINE_INTERVAL_TO_TRADE
|
| 259 |
+
sleep_duration_seconds = 0
|
| 260 |
+
if 'm' in kline_interval_str:
|
| 261 |
+
sleep_duration_seconds = int(kline_interval_str.replace('m', '')) * 60
|
| 262 |
+
elif 'h' in kline_interval_str:
|
| 263 |
+
sleep_duration_seconds = int(kline_interval_str.replace('h', '')) * 3600
|
| 264 |
+
elif 'd' in kline_interval_str:
|
| 265 |
+
sleep_duration_seconds = int(kline_interval_str.replace('d', '')) * 86400
|
| 266 |
+
else: # По умолчанию 1 час, если н�� распознано
|
| 267 |
+
sleep_duration_seconds = 3600
|
| 268 |
+
print(f"Не удалось определить время сна для интервала {kline_interval_str}, используется {sleep_duration_seconds} сек.")
|
| 269 |
+
|
| 270 |
|
| 271 |
while True:
|
| 272 |
print(f"\n--- {time.ctime()} ---")
|
| 273 |
# 1. Получение свежих данных
|
| 274 |
+
df_live = get_binance_klines_df(cfg.TRADING_PAIR, kline_interval_str, cfg.LOOKBACK_PERIODS_LIVE)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
+
if df_live.empty or len(df_live) < 100: # Нужно достаточно данных для SMA100/EMA100 и ATR14
|
| 277 |
+
print(f"Недостаточно данных для анализа ({len(df_live)} строк). Пропускаем цикл.")
|
| 278 |
+
time.sleep(60)
|
| 279 |
continue
|
| 280 |
|
| 281 |
# 2. Расчет индикаторов
|
| 282 |
+
df_with_indicators = calculate_live_indicators_from_df(df_live.copy())
|
| 283 |
|
| 284 |
+
# Берем последнюю ПОЛНУЮ свечу для анализа
|
| 285 |
+
# get_klines обычно возвращает последнюю свечу как частично сформированную, если limit > 1
|
| 286 |
+
# Для анализа лучше брать предпоследнюю, если это так.
|
| 287 |
+
# Или, если уверены, что последняя закрыта (например, если limit=1 и время свечи прошло)
|
| 288 |
+
# Для простоты, если используем lookback > 1, берем предпоследнюю.
|
| 289 |
+
if len(df_with_indicators) < 2:
|
| 290 |
+
print("Недостаточно строк после расчета индикаторов. Пропускаем.")
|
| 291 |
+
time.sleep(sleep_duration_seconds)
|
| 292 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
+
current_market_state_row = df_with_indicators.iloc[-2] # Предпоследняя строка, более вероятно закрытая свеча
|
| 295 |
+
|
| 296 |
+
# Проверка на NaN в нужных колонках для current_market_state_row
|
| 297 |
+
# Список колонок, которые должны быть в промпте (из format_live_data_for_llm)
|
| 298 |
+
required_cols_for_prompt = [
|
| 299 |
+
'open', 'high', 'low', 'close', 'volume', 'rsi_7', 'rsi_14',
|
| 300 |
+
'cci_7', 'cci_14', 'sma_50', 'ema_50', 'sma_100', 'ema_100',
|
| 301 |
+
'macd', 'bollinger', 'TrueRange', 'atr_7', 'atr_14'
|
| 302 |
+
]
|
| 303 |
+
if current_market_state_row[required_cols_for_prompt].isnull().any():
|
| 304 |
+
print("В данных для анализа (предпоследняя свеча) есть NaN значения в ключевых индикаторах. Пропускаем цикл.")
|
| 305 |
+
print(current_market_state_row[current_market_state_row[required_cols_for_prompt].isnull()])
|
| 306 |
+
time.sleep(sleep_duration_seconds)
|
| 307 |
continue
|
| 308 |
|
| 309 |
+
# 3. Форматирование для LLM
|
| 310 |
+
llm_prompt = format_live_data_for_llm(current_market_state_row)
|
| 311 |
print("Промпт для LLM:", llm_prompt)
|
| 312 |
|
| 313 |
# 4. Получение предсказания от LLM
|
|
|
|
| 315 |
print(f"Предсказанный сигнал: {predicted_signal}")
|
| 316 |
|
| 317 |
# 5. Исполнение сделки (С ОСТОРОЖНОСТЬЮ!)
|
|
|
|
| 318 |
if not cfg.USE_TESTNET:
|
| 319 |
+
user_confirm = input("ВЫ УВЕРЕНЫ, ЧТО ХОТИТЕ ТОРГОВАТЬ НА РЕАЛЬНОМ СЧЕТЕ? (yes/NO):")
|
| 320 |
+
if user_confirm.lower() != "yes":
|
| 321 |
+
print("Торговля на реальном счете отменена.")
|
| 322 |
+
return
|
| 323 |
|
| 324 |
+
execute_trade_logic(predicted_signal, cfg.TRADING_PAIR, cfg.TRADE_AMOUNT_USD)
|
| 325 |
|
| 326 |
+
print(f"Ожидание следующего цикла ({sleep_duration_seconds // 60} минут)...")
|
| 327 |
+
time.sleep(sleep_duration_seconds)
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
if __name__ == "__main__":
|
|
|
|
| 330 |
if model and text_generator:
|
| 331 |
trading_loop()
|
| 332 |
else:
|
| 333 |
+
print("Модель не была корректно загружена. Запустите скрипт обучения (1_finetune_mixtral.py) "
|
| 334 |
+
"или проверьте путь к адаптеру в config.py (FINETUNED_ADAPTER_PATH).")
|