Dmitriy-Egorov commited on
Commit
444f3df
·
verified ·
1 Parent(s): 654f591

Update 2_trading_bot_logic.py

Browse files
Files changed (1) hide show
  1. 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(API_KEY, API_SECRET, testnet=True) # Для тестовой сети
23
  print("Используется ТЕСТОВАЯ СЕТЬ Binance.")
24
  else:
25
- client = Client(API_KEY, API_SECRET)
26
  print("ВНИМАНИЕ: Используется РЕАЛЬНАЯ СЕТЬ Binance!")
27
 
28
-
29
  # --- Загрузка обученной модели (адаптера) ---
30
- # Для инференса можно также использовать квантизацию
31
- bnb_config_inf = BitsAndBytesConfig(
32
- load_in_4bit=True,
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
- model = PeftModel.from_pretrained(base_model, cfg.FINETUNED_ADAPTER_PATH)
49
- model.eval() # Перевод в режим инференса
50
- print(f"Адаптер {cfg.FINETUNED_ADAPTER_PATH} успешно загружен.")
51
- except Exception as e:
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
- else:
71
- text_generator = None
 
 
 
 
 
 
 
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=['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume',
79
- 'CloseTime', 'QuoteAssetVolume', 'NumberTrades',
80
- 'TakerBuyBaseAssetVolume', 'TakerBuyQuoteAssetVolume', 'Ignore'])
81
- df['Timestamp'] = pd.to_datetime(df['Timestamp'], unit='ms')
82
- df.set_index('Timestamp', inplace=True)
83
- for col in ['Open', 'High', 'Low', 'Close', 'Volume']: # И другие числовые, если нужны
84
- df[col] = pd.to_numeric(df[col])
 
 
 
 
 
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
- def calculate_live_indicators(df):
94
- """Рассчитывает индикаторы для DataFrame с последними данными."""
95
- # Используйте те же индикаторы и параметры, что и при обучении
96
- df.ta.rsi(length=14, append=True, col_names=('RSI',))
97
- df.ta.macd(append=True, col_names=('MACD', 'MACD_hist', 'MACD_signal'))
98
- df.ta.sma(length=20, append=True, col_names=('SMA_20',))
99
- df.ta.sma(length=50, append=True, col_names=('SMA_50',))
100
- df.ta.bbands(length=20, append=True, col_names=('BB_lower', 'BB_mid', 'BB_upper', 'BB_bandwidth', 'BB_percent'))
101
- # df.dropna(inplace=True) # Обычно последняя строка может иметь NaN, если индикаторы требуют больше данных
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  return df
103
 
104
 
105
- def format_live_data_for_llm(current_data_row, indicators_to_use):
106
- """Форматирует живые данные для LLM, так же, как в 0_prepare_data.py."""
107
- prompt = f"Текущая цена закрытия BTC/USDT: {current_data_row['Close']:.2f}. "
108
- desc_parts = []
109
- for indicator in indicators_to_use:
110
- if indicator in current_data_row and pd.notna(current_data_row[indicator]):
111
- desc_parts.append(f"{indicator}: {current_data_row[indicator]:.2f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- if not desc_parts: return None
114
- prompt += "Технические индикаторы: " + ", ".join(desc_parts) + "."
115
- # prompt += " Какое действие предпринять (BUY, SELL, HOLD)?" # Этот вопрос будет частью [INST] промпта
 
 
 
116
  # Полный промпт для модели (должен совпадать с форматом обучения)
117
- full_prompt = f"<s>[INST] Анализ рынка BTC/USDT. {prompt} Какое действие предпринять: BUY, SELL, или HOLD? [/INST]"
118
- return full_prompt
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 signal_text.startswith("BUY"): return "BUY"
144
- if signal_text.startswith("SELL"): return "SELL"
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 execute_trade(signal, symbol, quantity_usd):
154
- """Исполняет сделку на Binance."""
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
- except Exception as e:
164
- print(f"Не удалось получить текущую цену для {symbol}: {e}")
165
- return
166
-
167
- if current_price == 0:
168
- print(f"Текущая цена для {symbol} равна 0. Невозможно рассчитать количество.")
169
- return
170
 
171
- quantity_asset = quantity_usd / current_price
172
-
173
- # Binance требует определенной точности для количества и цены
174
- # Получаем информацию о символе для форматирования
175
- try:
176
  info = client.get_symbol_info(symbol)
177
  step_size = 0.0
178
- tick_size = 0.0
 
179
  for f in info['filters']:
180
  if f['filterType'] == 'LOT_SIZE':
181
  step_size = float(f['stepSize'])
182
- if f['filterType'] == 'PRICE_FILTER':
183
- tick_size = float(f['tickSize'])
184
-
185
- # Форматируем количество
186
  if step_size > 0:
187
- quantity_asset = (quantity_asset // step_size) * step_size
188
- # Убедимся, что количество не слишком маленькое (minNotional)
189
- # Этот код упрощен, в реальности проверка minNotional сложнее
190
- if quantity_asset * current_price < 10: # Примерный порог minNotional (для BTCUSDT около 5-10 USD)
191
- print(f"Рассчитанное количество {quantity_asset} для {symbol} слишком мало (меньше порога minNotional). Сделка не выполняется.")
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
- order_type = Client.ORDER_TYPE_MARKET # Рыночный ордер
203
- print(f"Попытка исполнить {signal} ордер для {symbol}, количество: {quantity_asset:.8f} (на сумму ~{quantity_usd}$)")
 
 
 
 
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
- # Проверяем баланс USDT
216
- usdt_balance = float(client.get_asset_balance(asset='USDT')['free'])
217
- if cfg.USE_TESTNET: # В тестнете балансы обычно большие, для реала эта проверка важна
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
- # Проверяем баланс BTC (или другого базового актива)
230
- base_asset = symbol.replace("USDT", "")
231
- asset_balance_data = client.get_asset_balance(asset=base_asset)
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
- indicators_to_use_live = ['RSI', 'MACD', 'MACD_signal', 'SMA_50', 'SMA_200', 'Upper_BB', 'Lower_BB', 'Close', 'Volume']
 
 
 
 
 
 
 
 
 
 
 
 
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) < 20: # Минимум для некоторых индикаторов
271
- print("Недостаточно данных для анализа. Пропускаем цикл.")
272
- time.sleep(60) # Ждем минуту
273
  continue
274
 
275
  # 2. Расчет индикаторов
276
- df_live = calculate_live_indicators(df_live.copy())
277
 
278
- # Берем последнюю ПОЛНУЮ свечу для анализа (предпоследнюю строку, т.к. последняя может быть не закрыта)
279
- # Или последнюю, если уверены, что get_klines возвращает только закрытые свечи для `limit`
280
- # Для простоты возьмем последнюю доступную строку с рассчитанными индикаторами
281
- current_market_state_row = df_live.iloc[-1] # Последняя строка с данными
282
-
283
- # Проверяем, есть ли NaN в ключевых индикаторах на последней строке
284
- if current_market_state_row[indicators_to_use_live].isnull().any():
285
- print("Последняя строка содержит NaN в индикаторах после расчета. Используем предпоследнюю, если есть.")
286
- if len(df_live) > 1:
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
- if not llm_prompt:
301
- print("Не удалось сформировать промпт для LLM. Пропускаем цикл.")
302
- time.sleep(60)
 
 
 
 
 
 
 
 
 
 
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
- print_danger = input("ВЫ УВЕРЕНЫ, ЧТО ХОТИТЕ ТОРГОВАТЬ НА РЕАЛЬНОМ СЧЕТЕ? (yes/NO):")
315
- if print_danger.lower() != "yes":
316
- print("Торговля на реальном счете отменена пользователем.")
317
- return # Выход из цикла и программы
318
 
319
- execute_trade(predicted_signal, cfg.TRADING_PAIR, cfg.TRADE_AMOUNT_USD)
320
 
321
- # 6. Пауза перед следующим циклом
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).")