Text Share Online

бот

import os
os.environ[‘TF_CPP_MIN_LOG_LEVEL’] = ‘2’

import logging
import sys
import time
import math
import random
import json
import asyncio
import websockets
import threading
import signal # Додаємо імпорт signal тут
from binance import AsyncClient, BinanceSocketManager
from datetime import datetime # Додаємо імпорт тут
from collections import defaultdict
from statistics import mean
from logging.handlers import RotatingFileHandler
import inspect
import numpy as np
import pandas as pd
import talib
import aiohttp
import pickle
import warnings
import tensorflow as tf
from tensorflow.keras.models import load_model
from tensorflow.keras.losses import MeanSquaredError
from xgboost import XGBRegressor
from binance.exceptions import BinanceAPIException
from ratelimit import limits, sleep_and_retry
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.metrics.pairwise import euclidean_distances
from sklearn.metrics import mean_absolute_error
import subprocess
client = None # Ініціалізація глобальної змінної
config = {}
global stats
stats = {}
active_positions = {}
market = {}
realtime_prices = {}
realtime_prices_lock = asyncio.Lock()
last_prices = {}
spot_holdings = {}
websocket_task = None
sync_task = None
display_task = None
report_task = None
reload_task = None
cleanup_task = None
xchange_info_cache = {
“futures”: None,
“spot”: None,
“last_updated”: 0,
“update_interval”: 6 * 3600, # 6 годин
}
exchange_info_lock = asyncio.Lock() # захист від конкурентного оновлення

bot_state_lock = asyncio.Lock()

# Функція оновлення exchange info (додати після init_client або де зручно)
async def update_exchange_info(market_type=”futures”):
“””
Оновлює кеш exchange info з Binance.
Використовує lock, щоб уникнути паралельних запитів.
“””
global exchange_info_cache

async with exchange_info_lock:
current_time = time.time()
key = “futures” if market_type == “futures” else “spot”

# Перевіряємо, чи потрібно оновлювати
if (
exchange_info_cache[key] is not None
and current_time – exchange_info_cache[“last_updated”] < exchange_info_cache[“update_interval”]
):
logging.debug(f”exchange_info_cache для {key} ще свіжий”)
return exchange_info_cache[key]

for attempt in range(3):
try:
logging.info(f”Оновлюємо exchangeInfo для {market_type} (спроба {attempt+1})…”)
if market_type == “futures”:
info = await client.futures_exchange_info()
else:
info = await client.get_exchange_info()

# Перетворюємо в зручний словник {symbol: {…}}
symbol_info = {}
for s in info[‘symbols’]:
symbol = s[‘symbol’]
symbol_info[symbol] = {
‘status’: s.get(‘status’),
‘baseAssetPrecision’: s.get(‘baseAssetPrecision’),
‘quotePrecision’: s.get(‘quotePrecision’),
‘pricePrecision’: s.get(‘pricePrecision’),
‘quantityPrecision’: s.get(‘quantityPrecision’),
‘filters’: {f[‘filterType’]: f for f in s.get(‘filters’, [])},
‘contractType’: s.get(‘contractType’), # для futures
‘deliveryDate’: s.get(‘deliveryDate’),
}

exchange_info_cache[key] = symbol_info
exchange_info_cache[“last_updated”] = current_time

logging.info(f”exchange_info_cache оновлено для {key}: {len(symbol_info)} символів”)
return symbol_info

except BinanceAPIException as e:
logging.error(f”BinanceAPIException при оновленні exchange info: {e}”)
if attempt == 2:
raise
await asyncio.sleep(3)
except Exception as e:
logging.error(f”Невідома помилка при оновленні exchange info: {e}”, exc_info=True)
if attempt == 2:
raise
await asyncio.sleep(5)

# Якщо дійшли сюди — невдача
logging.critical(“Не вдалося оновити exchange_info_cache після 3 спроб”)
return None

# Встановлюємо SelectorEventLoop для Windows
if sys.platform == “win32”:
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

def json_serializable(obj):
“””
Безпечна серіалізація для JSON: обробляє datetime, numpy типи, NaN/inf.
“””
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, (np.integer, np.floating)):
if math.isnan(obj) or math.isinf(obj):
return None # Або 0, залежно від логіки (None видалить ключ при dump)
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, (set, tuple)):
return list(obj)
return str(obj)

def load_config():
“””
Завантажує конфігурацію з config.json або створює нову з конфігурацією за замовчуванням.
“””
try:
with open(“config.json”, “r”, encoding=”utf-8″) as f:
config = json.load(f)
# Перевіряємо, чи всі ключі з default_config присутні
for key, value in default_config.items():
if key not in config:
config[key] = value
logging.info(f”Додано відсутній ключ до config: {key} = {value}”)
return config
except FileNotFoundError:
logging.warning(“Файл config.json не знайдено. Створюємо новий із конфігурацією за замовчуванням…”)
config = default_config.copy()
with open(“config.json”, “w”, encoding=”utf-8″) as f:
json.dump(config, f, indent=4)
return config
except json.JSONDecodeError:
logging.error(“Помилка: config.json містить невалідний JSON. Створюємо резервну копію та використовуємо конфігурацію за замовчуванням…”)
# Створюємо резервну копію пошкодженого файлу
if os.path.exists(“config.json”):
timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”)
os.rename(“config.json”, f”config_backup_{timestamp}.json”)
config = default_config.copy()
with open(“config.json”, “w”, encoding=”utf-8″) as f:
json.dump(config, f, indent=4)
return config
except Exception as e:
logging.error(f”Невідома помилка при завантаженні config.json: {e}. Використовуємо конфігурацію за замовчуванням…”)
config = default_config.copy()
with open(“config.json”, “w”, encoding=”utf-8″) as f:
json.dump(config, f, indent=4)
return config

warnings.filterwarnings(“ignore”, category=DeprecationWarning)

class SafeStreamHandler(logging.StreamHandler):
def emit(self, record):
try:
super().emit(record)
except ValueError:
pass # Ігноруємо I/O on closed file

logging.basicConfig(
level=logging.INFO,
format=”%(asctime)s – %(levelname)s – %(message)s”,
handlers=[
RotatingFileHandler(“trading_bot.log”, maxBytes=5*1024*1024, backupCount=5, encoding=’utf-8′),
logging.StreamHandler(sys.stdout)
]
)

if sys.platform == “win32”:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=’utf-8′)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=’utf-8′)

MODEL_FEATURES = [
“price”, “volume”, “rsi”, “macd”, “macd_signal”, “atr”, “obv”, “delta_volume”,
“bb_upper”, “bb_middle”, “bb_lower”, “stoch_k”, “stoch_d”, “ema”, “adx”,
“willr”, “cci”, “mfi”
]

knn_interval_weights = {symbol: {“1m”: 0.2, “5m”: 0.3, “15m”: 0.5} for symbol in [“BTCUSDT”, “ETHUSDT”, “SOLUSDT”, “XRPUSDT”]}
knn_interval_errors = {symbol: {“1m”: [], “5m”: [], “15m”: []} for symbol in [“BTCUSDT”, “ETHUSDT”, “SOLUSDT”, “XRPUSDT”]}
knn_selected_features = {symbol: {“1m”: MODEL_FEATURES[:], “5m”: MODEL_FEATURES[:], “15m”: MODEL_FEATURES[:]} for symbol in [“BTCUSDT”, “ETHUSDT”, “SOLUSDT”, “XRPUSDT”]}
last_feature_analysis_time = {symbol: 0 for symbol in [“BTCUSDT”, “ETHUSDT”, “SOLUSDT”, “XRPUSDT”]}

default_config = {
“API_KEY”: “qZSns67EgfxqTi0CQlOf4KWV5RXDgSioDQgGk29t4pvZ2vdi4Njd9CCsXT15z7En”,
“API_SECRET”: “4EVyYb2IL38IJ1O4FWLj9edUqD7XTvijReiNIvPKsqUpqVfhV9mYXIBO2LsFcncT”,
“market”: “futures”,
“symbols”: [“BTCUSDT”, “ETHUSDT”, “SOLUSDT”, “XRPUSDT”],
“intervals”: {“lstm”: “15m”, “transformer”: “15m”, “xgboost”: “5m”, “knn”: [“1m”, “5m”, “15m”]},
“interval”: “1m”,
“limit”: 1000,
“rsi_window”: 14,
“tp_2_multiplier_buy”: 1.03,
“tp_2_multiplier_sell”: 0.97,
“tp_3_multiplier_buy”: 1.05,
“tp_3_multiplier_sell”: 0.95,
“sl_multiplier_buy”: 0.98,
“sl_multiplier_sell”: 1.02,
“trailing_stop_activation_buy”: 1.01,
“trailing_stop_activation_sell”: 0.99,
“trailing_stop_distance”: 0.05,
“sleep_interval”: 5,
“threshold_mode”: “adaptive”, # fixed
“threshold_update_interval”: 1800,
“leverage”: {“BTCUSDT”: 3, “ETHUSDT”: 2, “SOLUSDT”: 2, “XRPUSDT”: 2},
“initial_balance”: 1000,
“commission_rate”: 0.0004,
“mode”: “real”, # paper
“trade_type”: “futures”,
“max_open_positions”: 5,
“capital_allocation”: {“BTCUSDT”: 0.25, “ETHUSDT”: 0.25, “SOLUSDT”: 0.25, “XRPUSDT”: 0.25},
“historical_limit”: 10000,
“max_loss_streak”: 5,
“min_win_rate”: 0.4,
“volatility_adjustment”: True,
“retrain_interval”: 24 * 60 * 60,
“atr_window”: 14,
“atr_history_length”: 60,
“volatility_threshold_high”: 0.03,
“volatility_threshold_low”: 0.005,
“threshold_update_fast”: 300,
“threshold_update_slow”: 7200,
“knn_feature_analysis_interval”: 3600,
“total_exposure_limit”: 2.5,
“lstm_window”: 40,
“transformer_window”: 40,
“loop_timeout”: 86400, # 24год.
“sync_delay”: 300, # Час між синхронізаціями в режимі real
“emergency_stop_loss”: -0.1,
“thresholds”: {
“BTCUSDT”: {“rsi_buy”: 30, “rsi_sell”: 70, “macd_buy”: -10, “macd_sell”: 10, “trading”: 0.001},
“ETHUSDT”: {“rsi_buy”: 30, “rsi_sell”: 70, “macd_buy”: -10, “macd_sell”: 10, “trading”: 0.001},
“SOLUSDT”: {“rsi_buy”: 30, “rsi_sell”: 70, “macd_buy”: -10, “macd_sell”: 10, “trading”: 0.001},
“XRPUSDT”: {“rsi_buy”: 55, “rsi_sell”: 70, “macd_buy”: -10, “macd_sell”: 10, “trading”: 0.001}
},
“model_weights”: {
“BTCUSDT”: {“lstm”: 0.3, “transformer”: 0.3, “xgboost”: 0.2, “knn”: 0.2},
“ETHUSDT”: {“lstm”: 0.3, “transformer”: 0.3, “xgboost”: 0.2, “knn”: 0.2},
“SOLUSDT”: {“lstm”: 0.3, “transformer”: 0.3, “xgboost”: 0.2, “knn”: 0.2},
“XRPUSDT”: {“lstm”: 0.3, “transformer”: 0.3, “xgboost”: 0.2, “knn”: 0.2}
},
“interval_update_delays”: {“1m”: 10, “5m”: 30, “15m”: 60},
“min_working_models”: 2,
“websocket_stale_threshold”: 180,
“api_retry_attempts”: 3,
“api_retry_delay”: 2,
“position_size”: 0.1, # Додаємо параметр для розміру позиції
“loss_adjustment_mode”: “manual”, # Додаємо параметр із значенням “manual” за замовчуванням
“position_sizing_mode”: “dynamic”, # Новий параметр: “dynamic” або “fixed”
“risk_management_mode”: “manual”, # Новий параметр
“pause_due_to_losses_timeout”: 3600, # Додаємо
“loss_streak_reset_interval”: 3600, # Додаємо
“telegram_token”: “7825181282:AAGzspC9DjesF0X89ZUJCEdARQPfAWgq0Ds”,
“telegram_chat_id”: “951025342”,
“default_values”: {
“sl_multiplier_buy”: 0.98,
“tp_2_multiplier_buy”: 1.03,
“tp_3_multiplier_buy”: 1.05,
“sl_multiplier_sell”: 1.02,
“tp_2_multiplier_sell”: 0.97,
“tp_3_multiplier_sell”: 0.95,
“trailing_stop_activation_buy”: 1.01,
“trailing_stop_activation_sell”: 0.99,
“trailing_stop_distance”: 0.05
}
}

async def adjust_thresholds_based_on_volatility(atr_value, close_price, symbol):
volatility_ratio = atr_value / close_price if close_price != 0 else 0.01
# Виправлено: rsi_buy не нижче 20, rsi_sell не вище 80 для логіки
config[“thresholds”][symbol][“rsi_buy”] = max(20, min(60, 60 – volatility_ratio * 100))
config[“thresholds”][symbol][“rsi_sell”] = max(40, min(80, 40 + volatility_ratio * 100))
config[“thresholds”][symbol][“macd_buy”] = max(-15, min(0, -volatility_ratio * 50)) # Додано lower bound
config[“thresholds”][symbol][“macd_sell”] = max(0, min(15, volatility_ratio * 50)) # Додано upper bound
config[“thresholds”][symbol][“trading”] = max(0.001, min(0.02, 0.005 + volatility_ratio * 0.1))
logging.info(f”⚙️ Оновлені пороги для {symbol}: RSI_buy={config[‘thresholds’][symbol][‘rsi_buy’]:.2f}, “
f”RSI_sell={config[‘thresholds’][symbol][‘rsi_sell’]:.2f}, “
f”MACD_buy={config[‘thresholds’][symbol][‘macd_buy’]:.2f}, “
f”MACD_sell={config[‘thresholds’][symbol][‘macd_sell’]:.2f}, “
f”Δціни={config[‘thresholds’][symbol][‘trading’]:.4f}”)

async def adjust_config_based_on_volatility(symbol, atr, current_price):
global config
async with config_lock:
try:
with open(“config.json”, “r”, encoding=”utf-8″) as f:
config_data = json.load(f)
logging.debug(f”Завантажено config.json для {symbol}: {json.dumps(config_data, indent=2)}”)
except Exception as e:
logging.error(f”Помилка читання config.json для {symbol}: {e}”)
config_data = config.copy()

default_values = {
“sl_multiplier_buy”: 0.98,
“sl_multiplier_sell”: 1.02,
“tp_2_multiplier_buy”: 1.03,
“tp_2_multiplier_sell”: 0.97,
“tp_3_multiplier_buy”: 1.05,
“tp_3_multiplier_sell”: 0.95,
“trailing_stop_activation_buy”: 1.01,
“trailing_stop_activation_sell”: 0.99,
“trailing_stop_distance”: 0.02,
“volatility_threshold_high”: 0.015,
“max_loss_streak”: 5,
“loss_streak_reset_interval”: 3600,
“pause_due_to_losses_timeout”: 7200
}
for key, value in default_values.items():
if key not in config_data:
config_data[key] = value
logging.info(f”Ініціалізовано {key} значенням за замовчуванням: {value} для {symbol}”)

if not isinstance(atr, (int, float)) or atr <= 0:
logging.warning(f”Невалідне значення ATR для {symbol}: {atr}. Використано значення за замовчуванням 0.01″)
atr = 0.01

if not isinstance(current_price, (int, float)) or current_price <= 0:
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol)
if not current_price:
logging.error(f”Невалідне значення current_price для {symbol}. Використання даних неможливе.”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка: неможливо отримати поточну ціну для {symbol}”
})
return None

reset_interval = config_data.get(“loss_streak_reset_interval”, 3600)
current_time = time.time()
last_loss_time = stats[symbol].get(“last_loss_time”, 0)
if last_loss_time > 0 and (current_time – last_loss_time) >= reset_interval:
stats[symbol][“loss_streak”] = 0
stats[symbol][“last_loss_time”] = 0
logging.info(f”Скинуто loss_streak для {symbol} через {reset_interval/60:.0f} хвилин після останньої збиткової угоди”)
await save_stats()

if symbol in bot_state[“paused_due_to_losses”]:
pause_time = bot_state[“paused_due_to_losses_time”].get(symbol, 0)
pause_timeout = config_data.get(“pause_due_to_losses_timeout”, 7200)
if current_time – pause_time >= pause_timeout:
bot_state[“paused_due_to_losses”].discard(symbol)
bot_state[“paused_due_to_losses_time”].pop(symbol, None)
stats[symbol][“loss_streak”] = 0
stats[symbol][“last_loss_time”] = 0
logging.info(f”Знято паузу через збитки для {symbol} після {pause_timeout/60:.0f} хвилин”)
await save_stats()
else:
logging.info(f”Пропущено коригування параметрів для {symbol}: символ на паузі через збитки”)
return config_data

# Виправлено: додано перевірку total_trades > 5 перед паузою
if stats[symbol][“total_trades”] > 5 and stats[symbol][“loss_streak”] >= config_data[“max_loss_streak”]:
logging.info(f”Пропущено коригування параметрів для {symbol}: loss_streak={stats[symbol][‘loss_streak’]} >= max_loss_streak={config_data[‘max_loss_streak’]}”)
bot_state[“paused_due_to_losses”].add(symbol)
bot_state[“paused_due_to_losses_time”][symbol] = current_time
logging.info(f”Символ {symbol} поставлено на паузу через досягнення max_loss_streak”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Торгівля призупинена через {config_data[‘max_loss_streak’]} послідовних збитків”
})
return config_data

volatility_factor = atr / current_price if current_price != 0 else 0.01
volatility_factor = max(0.5, min(2.0, volatility_factor / config_data[“volatility_threshold_high”]))

if config_data.get(“risk_management_mode”, “manual”) == “auto”:
# Виправлено: додано upper limit для position_size (max 0.2)
config_data[“position_size”] = max(0.05, min(0.2, 0.1 / volatility_factor))
config_data[“sl_multiplier_buy”] = max(0.95, 0.98 * volatility_factor)
config_data[“sl_multiplier_sell”] = min(1.05, 1.02 * volatility_factor)
config_data[“tp_2_multiplier_buy”] = min(1.05, 1.01 * volatility_factor)
config_data[“tp_2_multiplier_sell”] = max(0.95, 0.99 / volatility_factor)
config_data[“tp_3_multiplier_buy”] = min(1.07, 1.03 * volatility_factor)
config_data[“tp_3_multiplier_sell”] = max(0.93, 0.97 / volatility_factor)
config_data[“trailing_stop_activation_buy”] = min(1.04, 1.01 * volatility_factor)
config_data[“trailing_stop_activation_sell”] = max(0.96, 0.99 / volatility_factor)
config_data[“trailing_stop_distance”] = max(0.015, 0.02 * volatility_factor)
logging.info(
f”⚙️ Оновлено параметри ризику для {symbol}: “
f”position_size={config_data[‘position_size’]:.4f}, “
f”sl_multiplier_buy={config_data[‘sl_multiplier_buy’]:.4f}, “
f”sl_multiplier_sell={config_data[‘sl_multiplier_sell’]:.4f}, “
f”tp_2_multiplier_buy={config_data[‘tp_2_multiplier_buy’]:.4f}, “
f”tp_2_multiplier_sell={config_data[‘tp_2_multiplier_sell’]:.4f}, “
f”tp_3_multiplier_buy={config_data[‘tp_3_multiplier_buy’]:.4f}, “
f”tp_3_multiplier_sell={config_data[‘tp_3_multiplier_sell’]:.4f}, “
f”trailing_stop_activation_buy={config_data[‘trailing_stop_activation_buy’]:.4f}, “
f”trailing_stop_activation_sell={config_data[‘trailing_stop_activation_sell’]:.4f}, “
f”trailing_stop_distance={config_data[‘trailing_stop_distance’]:.4f}, “
f”volatility_factor={volatility_factor:.4f}”
)
else:
for key in default_values:
if not isinstance(config_data.get(key), (int, float)) or config_data.get(key) <= 0:
logging.warning(f”Невалідне значення {key} у config.json для {symbol}: {config_data.get(key)}. Використано значення за замовчуванням {default_values[key]}”)
config_data[key] = default_values[key]

config_changed = False
for key in default_values:
if config_data.get(key) != config.get(key):
config_changed = True
break
if config_changed:
try:
with open(“config.json”, “w”, encoding=”utf-8″) as f:
json.dump(config_data, f, indent=4)
config = config_data
logging.info(f”Оновлено config.json для {symbol}”)
except Exception as e:
logging.error(f”Помилка збереження config.json для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка збереження config.json: {str(e)}”
})

required_keys = [
“sl_multiplier_buy”, “sl_multiplier_sell”,
“tp_2_multiplier_buy”, “tp_2_multiplier_sell”,
“tp_3_multiplier_buy”, “tp_3_multiplier_sell”,
“trailing_stop_activation_buy”, “trailing_stop_activation_sell”,
“trailing_stop_distance”
]
for key in required_keys:
if not isinstance(config_data[key], (int, float)) or config_data[key] <= 0:
logging.error(f”Невалідне значення для {key} у config_data для {symbol}: {config_data[key]}. Замінено на {default_values[key]}”)
config_data[key] = default_values[key]

logging.debug(f”Повернуто config_data для {symbol}: {json.dumps({k: config_data[k] for k in required_keys}, indent=2)}”)
return config_data

# Додаємо функції save_stats і load_stats тут
async def save_stats():
async with stats_lock:
try:
logging.info(“Початок підготовки stats для збереження…”)
stats_to_save = {}
for symbol, data in stats.items():
stats_to_save[symbol] = data.copy()
stats_to_save[symbol][“balance_history”] = [
(t[0].isoformat() if isinstance(t[0], datetime) else t[0], float(t[1]))
for t in data[“balance_history”]
]
# Преобразуем все числовые поля в стандартные типы
for key in [“total_trades”, “winning_trades”, “losing_trades”, “total_profit”, “balance”, “win_rate”, “loss_streak”, “last_loss_time”]:
if key in stats_to_save[symbol]:
stats_to_save[symbol][key] = float(stats_to_save[symbol][key]) if isinstance(stats_to_save[symbol][key], (np.floating, np.integer)) else stats_to_save[symbol][key]
# Removed verbose logging of stats_to_save to avoid bloating logs
logging.info(“Stats prepared for saving (details omitted).”)
logging.info(“Початок запису stats у файл…”)
with open(“stats.json”, “w”, encoding=”utf-8″) as f:
json.dump(stats_to_save, f, indent=4, default=json_serializable)
logging.info(“Успішно збережено stats у файл.”)
except Exception as e:
logging.error(f”Помилка при збереженні stats: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка збереження stats: {str(e)}”,
“trade_id”: None
})
raise

async def load_stats():
try:
logging.info(“Спроба завантаження попередньої статистики…”)
if os.path.exists(“stats.json”):
with open(“stats.json”, “r”, encoding=”utf-8″) as f:
stats = json.load(f)
# Перевірка структури stats
for symbol in config[“symbols”]:
if symbol not in stats:
stats[symbol] = {
“balance”: config[“initial_balance”] * config[“capital_allocation”][symbol],
“total_trades”: 0,
“winning_trades”: 0,
“losing_trades”: 0,
“total_profit”: 0,
“win_rate”: 0.0,
“loss_streak”: 0,
“last_loss_time”: 0,
“balance_history”: [(datetime.now(), config[“initial_balance”] * config[“capital_allocation”][symbol])]
}
else:
# Очищаємо balance_history, якщо потрібно
stats[symbol][“balance_history”] = [(datetime.fromisoformat(t[0]), float(t[1])) for t in stats[symbol][“balance_history”][-100:]] # Обмежуємо до 100 записів
logging.info(“Попередня статистика завантажена.”)
return stats
else:
logging.info(“stats.json не знайдено, ініціалізація нової статистики…”)
return {
symbol: {
“balance”: config[“initial_balance”] * config[“capital_allocation”][symbol],
“total_trades”: 0,
“winning_trades”: 0,
“losing_trades”: 0,
“total_profit”: 0,
“win_rate”: 0.0,
“loss_streak”: 0,
“last_loss_time”: 0,
“balance_history”: [(datetime.now(), config[“initial_balance”] * config[“capital_allocation”][symbol])]
} for symbol in config[“symbols”]
}
except Exception as e:
logging.error(f”Помилка при завантаженні stats: {e}”, exc_info=True)
logging.info(“Ініціалізація нової статистики через помилку…”)
return {
symbol: {
“balance”: config[“initial_balance”] * config[“capital_allocation”][symbol],
“total_trades”: 0,
“winning_trades”: 0,
“losing_trades”: 0,
“total_profit”: 0,
“win_rate”: 0.0,
“loss_streak”: 0,
“last_loss_time”: 0,
“balance_history”: [(datetime.now(), config[“initial_balance”] * config[“capital_allocation”][symbol])]
} for symbol in config[“symbols”]
}

# Ініціалізація конфігурації
config = load_config()
MODEL_WEIGHTS = config[“model_weights”]
INTERVAL_UPDATE_DELAYS = config[“interval_update_delays”]
MIN_WORKING_MODELS = config[“min_working_models”]
WEBSOCKET_STALE_THRESHOLD = config[“websocket_stale_threshold”]
API_RETRY_ATTEMPTS = config[“api_retry_attempts”]
API_RETRY_DELAY = config[“api_retry_delay”]
market = config[“market”]

async def init_client():
global client
config = load_config()
logging.info(f”Спроба ініціалізації клієнта Binance. Режим: {config[‘mode’]}”)
max_retries = 3
for attempt in range(max_retries):
try:
if config[“mode”] == “real”:
api_key = config[“API_KEY”]
api_secret = config[“API_SECRET”]
logging.info(“Ініціалізація клієнта з автентифікацією…”)
client = await AsyncClient.create(api_key, api_secret, testnet=True)
await client.futures_ping() # Додано ping для futures
logging.info(“Підключено до Binance Futures Testnet з автентифікацією!”)
return
else:
logging.info(“Ініціалізація клієнта без автентифікації (режим ‘paper’)…”)
client = await AsyncClient.create(testnet=True)
await client.ping() # Додано ping для spot/paper
logging.info(“Підключено до Binance Futures Testnet у режимі ‘paper’!”)
return
except BinanceAPIException as e:
logging.error(f”Помилка підключення до Binance (спроба {attempt + 1}/{max_retries}): {e}”)
if attempt == max_retries – 1:
client = None
sys.exit(1)
await asyncio.sleep(5)
except Exception as e:
logging.error(f”Критична помилка підключення до Binance (спроба {attempt + 1}/{max_retries}): {e}”)
if attempt == max_retries – 1:
client = None
sys.exit(1)
await asyncio.sleep(5)

async def initialize():
stats = await load_stats()

active_positions = {symbol: [] for symbol in config[“symbols”]}
spot_holdings = {symbol: 0 for symbol in config[“symbols”]}
market_data_cache = {}
CACHE_DURATION = 300
prediction_history = {symbol: [] for symbol in config[“symbols”]}
historical_data = {symbol: [] for symbol in config[“symbols”]}
bot_state = {“paused”: False, “last_retrain”: time.time(), “last_retrain_lstm”: 0,
“last_retrain_transformer”: 0, “last_retrain_xgboost”: 0, “last_weight_update”: time.time(),
“last_threshold_update”: 0, “paused_symbols”: set(),
“paused_due_to_losses”: set(), # Додаємо для пауз через збитки
“paused_due_to_losses_time”: {}, # Додано
“paused_symbols_time”: {}, # Додаємо для сумісності
“last_stats_reset”: 0, # Новий словник для часу паузи
“last_sync”: 0
}
lstm_models = {}
transformer_models = {}
xgb_models = {}
scalers = {}
historical_datasets = {symbol: {“1m”: None, “5m”: None, “15m”: None} for symbol in config[“symbols”]}
model_errors = defaultdict(lambda: {“lstm”: [], “transformer”: [], “xgboost”: [], “knn”: []})
last_model_predictions = {symbol: {} for symbol in config[“symbols”]}

realtime_prices = {symbol: None for symbol in config[“symbols”]}
realtime_prices_lock = asyncio.Lock()
last_prices = defaultdict(lambda: None)
display_queue = asyncio.Queue()

exchange_info_cache = {“futures”: None, “spot”: None, “last_updated”: 0}

config_lock = asyncio.Lock() # Додаємо лок для синхронізації доступу до config.json

# Додаємо ініціалізацію last_feature_analysis_time і knn_selected_features тут
last_feature_analysis_time = {symbol: 0 for symbol in config[“symbols”]}

def safe_load_model(model_path):
try:
logging.info(f”Завантаження моделі: {model_path}”)
model = load_model(model_path, custom_objects={“mse”: MeanSquaredError()})
model.compile(optimizer=’adam’, loss=’mse’)
return model
except Exception as e:
logging.error(f”Помилка завантаження моделі {model_path}: {e}”)
return None

def safe_load_xgboost(model_path):
try:
logging.info(f”Завантаження XGBoost моделі: {model_path}”)
model = XGBRegressor()
model.load_model(model_path)
return model
except Exception as e:
logging.error(f”Помилка завантаження XGBoost моделі {model_path}: {e}”)
return None

async def load_models_and_scalers():
global lstm_models, transformer_models, xgb_models, scalers
tasks = []
for symbol in config[“symbols”]:
tasks.append(asyncio.create_task(safe_load_model_async(f”lstm_model_{symbol}.keras”, symbol, “lstm”)))
tasks.append(asyncio.create_task(safe_load_model_async(f”transformer_model_{symbol}.keras”, symbol, “transformer”)))
tasks.append(asyncio.create_task(safe_load_xgboost_async(f”xgboost_model_{symbol}.pkl”, symbol)))
tasks.append(asyncio.create_task(load_scalers_async(symbol)))
await asyncio.gather(*tasks)

for symbol in config[“symbols”]:
if all(model is None for model in [lstm_models.get(symbol), transformer_models.get(symbol), xgb_models.get(symbol)]):
logging.critical(f”Усі моделі для {symbol} не завантажилися. Торгівля для {symbol} неможлива.”)
bot_state[“paused_symbols”].add(symbol)

async def safe_load_model_async(model_path, symbol, model_type):
model = safe_load_model(model_path)
if model_type == “lstm”:
lstm_models[symbol] = model
elif model_type == “transformer”:
transformer_models[symbol] = model

async def safe_load_xgboost_async(model_path, symbol):
model = safe_load_xgboost(model_path)
xgb_models[symbol] = model

async def load_scalers_async(symbol):
scalers[symbol] = {}
try:
with open(f”price_scaler_{symbol}.pkl”, “rb”) as f:
scalers[symbol][“lstm_price”] = pickle.load(f)
with open(f”other_features_scaler_{symbol}.pkl”, “rb”) as f:
scalers[symbol][“lstm_features”] = pickle.load(f)
with open(f”transformer_scaler_{symbol}.pkl”, “rb”) as f:
scalers[symbol][“transformer”] = pickle.load(f)
with open(f”xgboost_scaler_{symbol}.pkl”, “rb”) as f:
scalers[symbol][“xgboost”] = pickle.load(f)
logging.info(f”Скейлери для {symbol} успішно завантажено.”)
except Exception as e:
logging.error(f”Помилка завантаження скейлерів для {symbol}: {e}”)
scalers[symbol] = None

global_data_cache = defaultdict(lambda: defaultdict(lambda: None))
data_cache_locks = defaultdict(lambda: defaultdict(lambda: None))

async def get_data_from_cache(symbol, interval):
“””
Асинхронно повертає кешований DataFrame або None.
Захищено asyncio.Lock() — створюємо лок лениво.
“””
# Ініціалізуємо lock при першому зверненні для (symbol, interval)
if data_cache_locks[symbol][interval] is None:
data_cache_locks[symbol][interval] = asyncio.Lock()

lock = data_cache_locks[symbol][interval]
async with lock:
return global_data_cache[symbol][interval]

semaphores = {“1m”: asyncio.Semaphore(3), “5m”: asyncio.Semaphore(2), “15m”: asyncio.Semaphore(1)}

async def safe_fetch(symbol, interval, fetch_func, last_ts=None):
async with semaphores[interval]:
try:
if last_ts:
df_new = await fetch_new_klines_since(symbol, interval, last_ts)
else:
df_new = await fetch_func(symbol, interval)
return df_new
except BinanceAPIException as e:
logging.error(f”Помилка Binance API для {symbol} [{interval}]: {e}”)
return None
except Exception as e:
logging.error(f”Помилка safe_fetch для {symbol} [{interval}]: {e}”)
return None

async def fetch_new_klines_since(symbol, interval, last_timestamp):
for attempt in range(2):
try:
start_time = int(pd.to_datetime(last_timestamp).timestamp() * 1000)
if market == “futures”:
klines = await client.futures_klines(symbol=symbol, interval=interval, startTime=start_time)
else:
klines = await client.get_klines(symbol=symbol, interval=interval, startTime=start_time)

if not klines:
logging.warning(f”Спроба {attempt + 1}: Отримано порожній результат для {symbol} [{interval}]”)
if attempt == 1:
logging.error(f”Не вдалося довантажити нові свічки {symbol} [{interval}] після 2 спроб: порожній результат”)
return None
else:
df = pd.DataFrame(klines, columns=[“timestamp”, “open”, “high”, “low”, “close”, “volume”,
“close_time”, “quote_asset_volume”, “number_of_trades”,
“taker_buy_base_volume”, “taker_buy_quote_volume”, “ignore”])
df[“timestamp”] = pd.to_datetime(df[“timestamp”], unit=”ms”)
df.set_index(“timestamp”, inplace=True)
df = df[[“open”, “high”, “low”, “close”, “volume”]].astype(float)
logging.info(f”✅ [{interval}] довантажено: {symbol} ({len(df)} нових)”)
return df
except BinanceAPIException as e:
logging.error(f”Спроба {attempt + 1}: Binance API помилка для {symbol} [{interval}]: {e}”)
if attempt == 1:
return None
await asyncio.sleep(2)
except Exception as e:
logging.error(f”Спроба {attempt + 1}: Помилка для {symbol} [{interval}]: {e}”)
if attempt == 1:
return None
await asyncio.sleep(2)
return None

async def get_historical_data(symbol, interval, limit=100):
try:
limit = min(limit, 1000)
logging.info(f”Запит до API: symbol={symbol}, interval={interval}, limit={limit}”)
if market == “futures”:
klines = await client.futures_klines(symbol=symbol, interval=interval, limit=limit)
else:
klines = await client.get_klines(symbol=symbol, interval=interval, limit=limit)
if not klines:
logging.warning(f”Отримано порожній результат для {symbol} [{interval}]”)
return None
df = pd.DataFrame(klines, columns=[“timestamp”, “open”, “high”, “low”, “close”, “volume”,
“close_time”, “quote_asset_volume”, “number_of_trades”,
“taker_buy_base_volume”, “taker_buy_quote_volume”, “ignore”])
df[“timestamp”] = pd.to_datetime(df[“timestamp”], unit=”ms”)
df.set_index(“timestamp”, inplace=True)
df = df[[“open”, “high”, “low”, “close”, “volume”]].astype(float)
return df
except BinanceAPIException as e:
logging.error(f”Помилка Binance API для {symbol} [{interval}]: {e}”)
return None
except Exception as e:
logging.error(f”Не вдалося отримати історію {symbol} [{interval}]: {e}”)
return None

async def update_cache_incremental(symbol, interval, new_df):
“””
Оновлює глобальний кеш інкрементально (асинхронно).
Зберігає максимум 1000 останніх рядків.
“””
if new_df is None or new_df.empty:
return

# переконаємось, що є lock
if data_cache_locks[symbol][interval] is None:
data_cache_locks[symbol][interval] = asyncio.Lock()

async with data_cache_locks[symbol][interval]:
old_df = global_data_cache[symbol][interval]
if old_df is None or old_df.empty:
global_data_cache[symbol][interval] = new_df.copy()
return

combined = pd.concat([old_df, new_df])
combined = combined[~combined.index.duplicated(keep=’last’)]
combined = combined.sort_index().tail(1000)
global_data_cache[symbol][interval] = combined

async def update_interval(symbols, interval, fetch_func, delay_seconds):
“””
Періодично довантажує нові клайни для заданого інтервалу і оновлює кеш.
fetch_func повинен бути асинхронною функцією (symbol, interval).
“””
while True:
for symbol in symbols:
try:
# беремо останній таймстамп з кешу (через async get_data_from_cache)
current_df = await get_data_from_cache(symbol, interval)
last_ts = current_df.index[-1] if (current_df is not None and not current_df.empty) else None

df_new = await safe_fetch(symbol, interval, fetch_func, last_ts)
if df_new is not None and not df_new.empty:
await update_cache_incremental(symbol, interval, df_new)
logging.info(f”✅ [{interval}] довантажено: {symbol} ({len(df_new)} нових)”)
# невелика пауза між символами
await asyncio.sleep(0.5)
except Exception as e:
logging.error(f”❌ [{interval}] помилка кешу {symbol}: {e}”, exc_info=True)
await asyncio.sleep(delay_seconds)

async def start_cache_tasks(symbols, fetch_func):
for interval, delay in INTERVAL_UPDATE_DELAYS.items():
asyncio.create_task(update_interval(symbols, interval, fetch_func, delay))

async def collect_historical_data_with_indicators(symbol, intervals=[“1m”, “5m”, “15m”], limit=config[“historical_limit”]):
result = {}
for interval in intervals:
try:
df = await get_historical_data(symbol, interval, limit=min(limit, 1000))
if df is None or df.empty:
result[interval] = None
continue

prices = df[“close”].values
volumes = df[“volume”].values
highs = df[“high”].values
lows = df[“low”].values

rsi = talib.RSI(prices, timeperiod=14)
macd, macd_signal, _ = talib.MACD(prices)
atr = talib.ATR(highs, lows, prices, timeperiod=14)
obv = talib.OBV(prices, volumes)
delta_volume = np.diff(volumes, prepend=volumes[0])
bb_upper, bb_middle, bb_lower = talib.BBANDS(prices, timeperiod=20)
stoch_k, stoch_d = talib.STOCH(highs, lows, prices, fastk_period=14, slowk_period=3, slowd_period=3)
ema = talib.EMA(prices, timeperiod=10)
adx = talib.ADX(highs, lows, prices, timeperiod=14)
willr = talib.WILLR(highs, lows, prices, timeperiod=14)
cci = talib.CCI(highs, lows, prices, timeperiod=20)
mfi = talib.MFI(highs, lows, prices, volumes, timeperiod=14)

historical_data_df = pd.DataFrame({
“price”: prices, “volume”: volumes, “rsi”: rsi, “macd”: macd, “macd_signal”: macd_signal,
“atr”: atr, “obv”: obv, “delta_volume”: delta_volume, “bb_upper”: bb_upper,
“bb_middle”: bb_middle, “bb_lower”: bb_lower, “stoch_k”: stoch_k, “stoch_d”: stoch_d,
“ema”: ema, “adx”: adx, “willr”: willr, “cci”: cci, “mfi”: mfi
}, index=df.index)
historical_data_df[“next_price”] = historical_data_df[“price”].shift(-1)
historical_data_df = historical_data_df.dropna()
result[interval] = historical_data_df if not historical_data_df.empty else None
except Exception as e:
logging.error(f”Помилка збору історичних даних для {symbol} ({interval}): {e}”)
result[interval] = None
return result

async def analyze_feature_importance(symbol, interval, df):
try:
if df is None or df.empty or len(df) < 50:
logging.warning(f”Недостатньо даних для аналізу важливості фіч для {symbol} [{interval}]”)
return MODEL_FEATURES

X = df[MODEL_FEATURES].iloc[:-1]
y = df[“next_price”].iloc[:-1]
X_test = df[MODEL_FEATURES].iloc[-1:]

distances = euclidean_distances(X_test, X)
nearest_idx = np.argmin(distances)
base_pred = y.iloc[nearest_idx]
base_mae = mean_absolute_error([y.iloc[-1]], [base_pred])

feature_importance = {}
for feature in MODEL_FEATURES:
X_reduced = X.drop(columns=[feature])
X_test_reduced = X_test.drop(columns=[feature])
distances_reduced = euclidean_distances(X_test_reduced, X_reduced)
nearest_idx_reduced = np.argmin(distances_reduced)
pred_reduced = y.iloc[nearest_idx_reduced]
mae_reduced = mean_absolute_error([y.iloc[-1]], [pred_reduced])
feature_importance[feature] = mae_reduced – base_mae

sorted_features = sorted(feature_importance.items(), key=lambda x: x[1], reverse=True)
top_features = [f[0] for f in sorted_features[:10]]
logging.info(f”Оновлені важливі фічі для {symbol} [{interval}]: {top_features}”)
return top_features
except Exception as e:
logging.error(f”Помилка аналізу важливості фіч для {symbol} [{interval}]: {e}”)
return MODEL_FEATURES

@tf.function(reduce_retracing=True)
def predict_lstm_tf(model, inputs):
return model(inputs)

@tf.function(reduce_retracing=True)
def predict_transformer_tf(model, inputs):
return model(inputs)

async def compute_indicators(df):
“””
Обчислює технічні індикатори для DataFrame, узгоджені з навчальними скриптами.
“””
try:
prices = talib.EMA(df[“close”].values, timeperiod=10)
prices = np.nan_to_num(prices, nan=np.nanmean(prices))
volumes = talib.EMA(df[“volume”].values, timeperiod=10)
volumes = np.nan_to_num(volumes, nan=np.nanmean(volumes))
highs = df[“high”].values
lows = df[“low”].values

delta_volume = np.diff(volumes, prepend=volumes[0])
rsi = talib.RSI(prices, timeperiod=14)
macd, macd_signal, _ = talib.MACD(prices, fastperiod=12, slowperiod=26, signalperiod=9)
atr = talib.ATR(highs, lows, prices, timeperiod=14)
obv = talib.OBV(prices, volumes)
bb_upper, bb_middle, bb_lower = talib.BBANDS(prices, timeperiod=20)
stoch_k, stoch_d = talib.STOCH(highs, lows, prices, fastk_period=14, slowk_period=3, slowd_period=3)
ema = talib.EMA(prices, timeperiod=10)
adx = talib.ADX(highs, lows, prices, timeperiod=14)
willr = talib.WILLR(highs, lows, prices, timeperiod=14)
cci = talib.CCI(highs, lows, prices, timeperiod=20)
mfi = talib.MFI(highs, lows, prices, volumes, timeperiod=14)

df_indicators = pd.DataFrame({
“price”: prices,
“volume”: volumes,
“rsi”: rsi,
“macd”: macd,
“macd_signal”: macd_signal,
“atr”: atr,
“obv”: obv,
“delta_volume”: delta_volume,
“bb_upper”: bb_upper,
“bb_middle”: bb_middle,
“bb_lower”: bb_lower,
“stoch_k”: stoch_k,
“stoch_d”: stoch_d,
“ema”: ema,
“adx”: adx,
“willr”: willr,
“cci”: cci,
“mfi”: mfi
}, index=df.index).interpolate(method=”linear”).dropna()
return df_indicators
except Exception as e:
logging.error(f”Помилка обчислення індикаторів: {e}”)
return None

async def predict_with_model_lstm(symbol):
“””
Прогнозує ціну за допомогою LSTM моделі.
“””
try:
if symbol not in lstm_models or lstm_models[symbol] is None:
logging.error(f”LSTM модель для {symbol} не завантажена”)
return None
if symbol not in scalers or scalers[symbol].get(“lstm_price”) is None or scalers[symbol].get(“lstm_features”) is None:
logging.error(f”Скейлери для LSTM {symbol} не завантажені”)
return None

df = await get_data_from_cache(symbol, “15m”)
if df is None or df.empty or len(df) < config[“lstm_window”]:
logging.warning(f”LSTM: недостатньо даних для {symbol} (доступно {len(df) if df is not None else 0}, потрібно {config[‘lstm_window’]})”)
await asyncio.sleep(5)
df = await get_historical_data(symbol, “15m”, limit=100)
if df is None or df.empty or len(df) < config[“lstm_window”]:
logging.error(f”LSTM: не вдалося отримати достатньо даних для {symbol}”)
return None
await update_cache_incremental(symbol, “15m”, df)

df_indicators = await compute_indicators(df)
if df_indicators is None or df_indicators.empty or len(df_indicators) < config[“lstm_window”]:
logging.error(f”LSTM: не вдалося обчислити індикатори або недостатньо даних для {symbol} (доступно {len(df_indicators) if df_indicators is not None else 0}, потрібно {config[‘lstm_window’]})”)
return None

X = df_indicators[-config[“lstm_window”]:][MODEL_FEATURES]
X[“price”] = scalers[symbol][“lstm_price”].transform(X[[“price”]])
X[MODEL_FEATURES[1:]] = scalers[symbol][“lstm_features”].transform(X[MODEL_FEATURES[1:]])
X = np.array([X.values])

prediction = predict_lstm_tf(lstm_models[symbol], X)[0][0]
prediction = scalers[symbol][“lstm_price”].inverse_transform([[prediction]])[0][0]
return float(prediction)
except Exception as e:
logging.error(f”LSTM помилка для {symbol}: {e}”)
return None

async def predict_with_model_transformer(symbol):
“””
Прогнозує ціну за допомогою Transformer моделі.
“””
try:
if symbol not in transformer_models or transformer_models[symbol] is None:
logging.error(f”Transformer модель для {symbol} не завантажена”)
return None
if symbol not in scalers or scalers[symbol].get(“transformer”) is None:
logging.error(f”Скейлер для Transformer {symbol} не завантажений”)
return None

df = await get_data_from_cache(symbol, “15m”)
if df is None or df.empty or len(df) < config[“transformer_window”]:
logging.warning(f”Transformer: недостатньо даних для {symbol} (доступно {len(df) if df is not None else 0}, потрібно {config[‘transformer_window’]})”)
await asyncio.sleep(5)
df = await get_historical_data(symbol, “15m”, limit=100)
if df is None or df.empty or len(df) < config[“transformer_window”]:
logging.error(f”Transformer: не вдалося отримати достатньо даних для {symbol}”)
return None
await update_cache_incremental(symbol, “15m”, df)

df_indicators = await compute_indicators(df)
if df_indicators is None or df_indicators.empty or len(df_indicators) < config[“transformer_window”]:
logging.error(f”Transformer: не вдалося обчислити індикатори або недостатньо даних для {symbol} (доступно {len(df_indicators) if df_indicators is not None else 0}, потрібно {config[‘transformer_window’]})”)
return None

X = df_indicators[-config[“transformer_window”]:][MODEL_FEATURES]
X_scaled = scalers[symbol][“transformer”].transform(X)
X_input = np.array([X_scaled]) # Додаємо батч-розмірність: (1, transformer_window, n_features)

prediction_scaled = transformer_models[symbol].predict(X_input, verbose=0)[:, -1, 0] # Беремо останній прогноз
last_scaled_row = X_scaled[-1:].copy()
last_scaled_row[0, 0] = prediction_scaled # Оновлюємо лише ціну
prediction_unscaled = scalers[symbol][“transformer”].inverse_transform(last_scaled_row)[0, 0]
return float(prediction_unscaled)
except Exception as e:
logging.error(f”Transformer помилка для {symbol}: {e}”)
return None

async def predict_with_model_xgboost(symbol):
“””
Прогнозує ціну за допомогою XGBoost моделі.
“””
try:
if symbol not in xgb_models or xgb_models[symbol] is None:
logging.error(f”XGBoost модель для {symbol} не завантажена”)
return None
if symbol not in scalers or scalers[symbol].get(“xgboost”) is None:
logging.error(f”Скейлер для XGBoost {symbol} не завантажений”)
return None

xgboost_window = config.get(“xgboost_window”, 20) # Використовуємо параметр із конфігурації або 20 за замовчуванням
df = await get_data_from_cache(symbol, “5m”)
if df is None or df.empty or len(df) < xgboost_window:
logging.warning(f”XGBoost: недостатньо даних для {symbol} (доступно {len(df) if df is not None else 0}, потрібно {xgboost_window})”)
await asyncio.sleep(5)
df = await get_historical_data(symbol, “5m”, limit=100)
if df is None or df.empty or len(df) < xgboost_window:
logging.error(f”XGBoost: не вдалося отримати достатньо даних для {symbol}”)
return None
update_cache_incremental(symbol, “5m”, df)

df_indicators = await compute_indicators(df)
if df_indicators is None or df_indicators.empty or len(df_indicators) < xgboost_window:
logging.error(f”XGBoost: не вдалося обчислити індикатори або недостатньо даних для {symbol} (доступно {len(df_indicators) if df_indicators is not None else 0}, потрібно {xgboost_window})”)
return None

X = df_indicators.iloc[-1:][MODEL_FEATURES]
X_scaled = scalers[symbol][“xgboost”].transform(X)
prediction = xgb_models[symbol].predict(X_scaled)[0]
return float(prediction)
except Exception as e:
logging.error(f”XGBoost помилка для {symbol}: {e}”)
return None

async def predict_with_model_knn(symbol):
“””
Прогнозує ціну за допомогою KNN, комбінуючи прогнози з різних інтервалів.
“””
try:
from sklearn.preprocessing import StandardScaler
intervals = config[“intervals”][“knn”]
predictions = {}
df_1m = await get_data_from_cache(symbol, “1m”)
if df_1m is None or df_1m.empty:
logging.warning(f”KNN: неможливо отримати дані 1m для {symbol}”)
df_1m = await get_historical_data(symbol, “1m”, limit=100)
if df_1m is None or df_1m.empty:
logging.error(f”KNN: не вдалося отримати поточну ціну для {symbol}”)
return None
update_cache_incremental(symbol, “1m”, df_1m)
current_price = float(df_1m[“close”].iloc[-1])
# Перевірка свіжості даних
last_timestamp = df_1m.index[-1].timestamp()
logging.info(f”KNN: затримка даних для {symbol}: {time.time() – last_timestamp:.2f} секунд”)
if time.time() – last_timestamp > WEBSOCKET_STALE_THRESHOLD:
logging.warning(f”KNN: дані для {symbol} застаріли (останній таймстамп: {last_timestamp})”)
return None

for interval in intervals:
if interval not in knn_selected_features[symbol]:
logging.warning(f”KNN: відсутні вибрані фічі для {symbol} [{interval}]”)
continue

df = await get_data_from_cache(symbol, interval)
if df is None or df.empty or len(df) < 50:
logging.warning(f”KNN: недостатньо даних для {symbol} на {interval} (доступно {len(df) if df is not None else 0}, потрібно 50)”)
await asyncio.sleep(5)
df = await get_historical_data(symbol, interval, limit=100)
if df is None or df.empty or len(df) < 50:
logging.error(f”KNN: не вдалося отримати дані для {symbol} [{interval}]”)
continue
update_cache_incremental(symbol, interval, df)

df_knn = await compute_indicators(df)
if df_knn is None or df_knn.empty or len(df_knn) < 50:
logging.error(f”KNN: не вдалося обчислити індикатори для {symbol} [{interval}]”)
continue

df_knn[“next_price”] = df_knn[“price”].shift(-1)
df_knn = df_knn.dropna()

selected_features = knn_selected_features[symbol][interval]
X_train = df_knn[selected_features].iloc[:-1]
y_train = df_knn[“next_price”].iloc[:-1]
X_test = df_knn[selected_features].iloc[-1:]

# Масштабування ознак
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
distances = euclidean_distances(X_test_scaled, X_train_scaled)
nearest_idx = np.argmin(distances)
prediction = y_train.iloc[nearest_idx]
predictions[interval] = float(prediction)

mae = abs(prediction – current_price)
knn_interval_errors[symbol][interval].append(mae)

if not predictions:
logging.warning(f”KNN: жоден таймфрейм не дав прогноз для {symbol}”)
return None

valid_intervals = [interval for interval in intervals if knn_interval_errors[symbol][interval]]
if not valid_intervals:
logging.warning(f”KNN: немає помилок для жодного інтервалу для {symbol}, використовуємо рівні ваги”)
for interval in predictions:
knn_interval_weights[symbol][interval] = 1 / len(predictions)
else:
interval_mae = {interval: mean(knn_interval_errors[symbol][interval]) for interval in valid_intervals}
inv_mae = {interval: 1 / max(interval_mae[interval], 1e-6) for interval in valid_intervals}
total_inv = sum(inv_mae.values())
if total_inv <= 0:
logging.error(f”KNN: total_inv дорівнює нулю для {symbol}, використовуємо рівні ваги”)
for interval in predictions:
knn_interval_weights[symbol][interval] = 1 / len(predictions)
else:
for interval in intervals:
knn_interval_weights[symbol][interval] = inv_mae.get(interval, 0) / total_inv if interval in inv_mae else 0

total_weight = sum(knn_interval_weights[symbol][interval] for interval in predictions)
if total_weight <= 0:
logging.error(f”KNN: total_weight дорівнює нулю для {symbol}”)
return None
combined_prediction = sum(knn_interval_weights[symbol][interval] * predictions[interval] for interval in predictions) / total_weight
logging.info(f”KNN прогноз для {symbol}: {predictions}, ваги: {knn_interval_weights[symbol]}, комбінований: {combined_prediction:.4f}”)
return combined_prediction
except Exception as e:
logging.error(f”KNN помилка для {symbol}: {e}”)
return None

async def predict_price(symbol):
“””
Комбінує прогнози від різних моделей для створення середнього прогнозу ціни.
“””
try:
if symbol in bot_state[“paused_symbols”]:
logging.info(f”Торгівля для {symbol} призупинена через попередні помилки моделей”)
return None

predictions = {}
df_1m = await get_data_from_cache(symbol, “1m”)
if df_1m is None or df_1m.empty:
logging.warning(f”Неможливо отримати поточну ціну з cache для {symbol}”)
df_1m = await get_historical_data(symbol, “1m”, limit=100)
if df_1m is None or df_1m.empty:
# Резервне джерело цін через realtime_prices
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol)
if current_price is None:
logging.error(f”Не вдалося отримати ціну для {symbol} з cache, history або realtime_prices”)
return None
else:
update_cache_incremental(symbol, “1m”, df_1m)
current_price = float(df_1m[“close”].iloc[-1]) if df_1m is not None and not df_1m.empty else current_price

predict_funcs = [
predict_with_model_lstm,
predict_with_model_transformer,
predict_with_model_xgboost,
predict_with_model_knn
]
model_names = [“lstm”, “transformer”, “xgboost”, “knn”]
results = await asyncio.gather(*[func(symbol) for func in predict_funcs], return_exceptions=True)

for model_name, result in zip(model_names, results):
if isinstance(result, Exception) or result is None:
logging.warning(f”Прогноз {model_name} для {symbol} не вдався”)
continue
if model_name not in MODEL_WEIGHTS[symbol] or not isinstance(MODEL_WEIGHTS[symbol][model_name], (int, float)) or MODEL_WEIGHTS[symbol][model_name] <= 0:
logging.warning(f”Невалідна вага для моделі {model_name} для {symbol}”)
continue
price_diff = abs((result – current_price) / current_price) if current_price != 0 else float(‘inf’)
if price_diff > 0.05: # Зменшено поріг до 5%
logging.warning(f”Прогноз {model_name} для {symbol} відхилено: {result:.4f} (поточна ціна: {current_price:.4f}, різниця: {price_diff:.2%})”)
continue
predictions[model_name] = result
mae = abs(result – current_price)
model_errors[symbol][model_name].append(mae)
last_model_predictions[symbol][model_name] = result

if len(predictions) < MIN_WORKING_MODELS:
logging.warning(f”Недостатньо працюючих моделей для {symbol} ({len(predictions)} < {MIN_WORKING_MODELS}). Призупиняємо торгівлю.”)
bot_state[“paused_symbols”].add(symbol)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Торгівля призупинена: недостатньо працюючих моделей ({len(predictions)} < {MIN_WORKING_MODELS})”
})
return None

total_weight = sum(MODEL_WEIGHTS[symbol][m] for m in predictions)
if total_weight <= 0:
logging.error(f”Сумарна вага моделей для {symbol} дорівнює нулю”)
return None
avg_prediction = sum(MODEL_WEIGHTS[symbol][m] * predictions[m] for m in predictions) / total_weight
logging.info(f”📊 Середній прогноз для {symbol}: {avg_prediction:.4f}”)
prediction_history[symbol].append({“timestamp”: datetime.now(), “predicted_price”: avg_prediction, “actual_price”: current_price})
if len(prediction_history[symbol]) > 1000: # Зменшено ліміт до 1000
prediction_history[symbol] = prediction_history[symbol][-1000:]
return avg_prediction
except Exception as e:
logging.error(f”predict_price помилка для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка прогнозування ціни: {str(e)}”
})
return None

async def analyze_market(df, symbol):
“””
Генерує торгові сигнали на основі технічних індикаторів (RSI, MACD) та прогнозованих цін.
“””
try:
# Перевірка на мінімальну довжину df перед обчисленням індикаторів
if len(df) < max(config[“atr_history_length”], config[“rsi_window”]):
logging.error(f”Недостатньо даних для {symbol}: {len(df)} < {max(config[‘atr_history_length’], config[‘rsi_window’])}”)
return None, None

prices = df[“close”].values
rsi = talib.RSI(prices, timeperiod=config[“rsi_window”])[-1]
macd, macd_signal, _ = talib.MACD(prices)
macd_value = macd[-1] – macd_signal[-1]
# Використовуємо atr_history_length замість atr_window
atr = talib.ATR(df[“high”].values, df[“low”].values, prices, timeperiod=config[“atr_history_length”])[-1]

# Перевірка на невалідні значення індикаторів
if any(np.isnan(x) or np.isinf(x) for x in [rsi, macd_value, atr]):
logging.error(f”Невалідні значення індикаторів для {symbol}: RSI={rsi}, MACD={macd_value}, ATR={atr}”)
return None, None

predicted_price = await predict_price(symbol)
if predicted_price is None:
return None, None

current_price = prices[-1]
price_change = (predicted_price – current_price) / current_price

rsi_buy_threshold = config[“thresholds”][symbol][“rsi_buy”]
rsi_sell_threshold = config[“thresholds”][symbol][“rsi_sell”]
macd_buy_threshold = config[“thresholds”][symbol][“macd_buy”]
macd_sell_threshold = config[“thresholds”][symbol][“macd_sell”]
threshold = config[“thresholds”][symbol][“trading”]

signal = “NO_TRADE”
if price_change > threshold and rsi < rsi_buy_threshold and macd_value < macd_buy_threshold:
signal = “BUY”
elif price_change < -threshold and rsi > rsi_sell_threshold and macd_value > macd_sell_threshold:
signal = “SELL”

logging.info(f”📢 Сигнал для {symbol}: {signal}, RSI={rsi:.2f}, MACD={macd_value:.2f}, ATR={atr:.2f}, Δціни={price_change:.4f}”)
return signal, atr
except ValueError as e:
logging.error(f”analyze_market помилка значення для {symbol}: {e}”)
return None, None
except Exception as e:
logging.error(f”analyze_market загальна помилка для {symbol}: {e}”)
return None, None

async def check_opposite_signal(symbol, current_price, df, side):
try:
# Викликаємо analyze_market для генерації сигналу
signal, _ = await analyze_market(df, symbol)

# Перевіряємо, чи є протилежний сигнал
if signal is None or signal == “NO_TRADE”:
logging.debug(f”Немає протилежного сигналу для {symbol}: поточний сигнал={signal}, сторона={side}”)
return False

if side == “BUY” and signal == “SELL”:
logging.info(f”Виявлено протилежний сигнал для {symbol}: SELL (поточна позиція BUY)”)
return True
elif side == “SELL” and signal == “BUY”:
logging.info(f”Виявлено протилежний сигнал для {symbol}: BUY (поточна позиція SELL)”)
return True
else:
logging.debug(f”Протилежного сигналу для {symbol} не виявлено: поточний сигнал={signal}, сторона={side}”)
return False
except Exception as e:
logging.error(f”Помилка в check_opposite_signal для {symbol}: {e}”)
return False

async def check_position_status(symbol, side, quantity):
if market == “futures”:
try:
positions = await client.futures_position_information(symbol=symbol)
for pos in positions:
if pos[“symbol”] == symbol:
position_amt = float(pos[“positionAmt”])
if position_amt == 0:
logging.info(f”Позиція для {symbol} вже закрита (positionAmt=0).”)
return False
if (side == “BUY” and position_amt > 0) or (side == “SELL” and position_amt < 0):
if abs(position_amt) >= quantity:
logging.info(f”Позиція для {symbol} відкрита: side={side}, positionAmt={position_amt}, quantity={quantity}”)
return True
else:
logging.warning(f”Позиція для {symbol} має недостатню кількість: positionAmt={position_amt}, очікувана quantity={quantity}”)
return False
logging.info(f”Позиція для {symbol} не знайдена в списку відкритих позицій.”)
return False
except BinanceAPIException as e:
logging.error(f”Помилка Binance API при перевірці позиції для {symbol}: {e}”)
return False
except Exception as e:
logging.error(f”Помилка перевірки стану позиції для {symbol}: {e}”)
return False
else:
try:
if side == “BUY”:
account = await client.get_account()
usdt_balance = float(next(a[“free”] for a in account[“balances”] if a[“asset”] == “USDT”))
current_price = realtime_prices[symbol] or (await get_data_from_cache(symbol, “1m”))[“close”].iloc[-1]
required_usdt = quantity * current_price
return usdt_balance >= required_usdt
elif side == “SELL”:
account = await client.get_account()
asset = symbol.replace(“USDT”, “”)
available = float(next((a[“free”] for a in account[“balances”] if a[“asset”] == asset), 0))
return available >= quantity
return False
except BinanceAPIException as e:
logging.error(f”Помилка Binance API при перевірці спотової позиції для {symbol}: {e}”)
return False
except Exception as e:
logging.error(f”Помилка перевірки спотової позиції для {symbol}: {e}”)
return False

async def update_exchange_info(market, max_retries=3):
global exchange_info_cache
if market not in [“futures”, “spot”]:
logging.error(f”Невалідне значення market: {market}”)
return False
current_time = time.time()
old_cache = exchange_info_cache.copy()
for attempt in range(max_retries):
try:
if market == “futures”:
exchange_info_cache[market] = await client.futures_exchange_info()
logging.info(f”Отримано futures exchange_info: {len(exchange_info_cache[market][‘symbols’])} символів”)
else:
exchange_info_cache[market] = await client.get_exchange_info()
logging.info(f”Отримано spot exchange_info: {len(exchange_info_cache[market][‘symbols’])} символів”)
exchange_info_cache[“last_updated”] = current_time
logging.debug(f”exchange_info_cache[{market}]: {exchange_info_cache[market]}”)
for symbol in config[“symbols”]:
symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
if symbol_info:
lot_size = next((f for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”), None)
price_filter = next((f for f in symbol_info[“filters”] if f[“filterType”] == “PRICE_FILTER”), None)
logging.info(f”exchange_info для {symbol}: lot_size={lot_size}, price_filter={price_filter}”)
else:
logging.warning(f”Символ {symbol} відсутній у exchange_info”)
logging.info(f”Успішно оновлено exchange_info для {market}”)
return True
except BinanceAPIException as e:
logging.error(f”Помилка Binance API при оновленні exchange_info (спроба {attempt + 1}/{max_retries}): {e}”)
if attempt < max_retries – 1:
await asyncio.sleep(2 ** attempt)
except Exception as e:
logging.error(f”Помилка оновлення exchange_info (спроба {attempt + 1}/{max_retries}): {e}”)
if attempt < max_retries – 1:
await asyncio.sleep(2 ** attempt)
logging.error(f”Не вдалося оновити exchange_info після {max_retries} спроб”)
exchange_info_cache = old_cache
return False

DEFAULT_LOT_SIZES = {
“BTCUSDT”: {“min_qty”: 0.0001, “step_size”: 0.0001, “max_qty”: 1000.0, “precision”: 4},
“ETHUSDT”: {“min_qty”: 0.001, “step_size”: 0.001, “max_qty”: 10000.0, “precision”: 3},
“SOLUSDT”: {“min_qty”: 0.01, “step_size”: 0.01, “max_qty”: 100000.0, “precision”: 2},
“XRPUSDT”: {“min_qty”: 1.0, “step_size”: 1.0, “max_qty”: 1000000.0, “precision”: 0}
}

async def round_quantity(symbol, quantity, market=None):
market = market or config[“market”]
if market not in [“futures”, “spot”]:
logging.error(f”Невалідне значення market: {market}”)
return None
if exchange_info_cache.get(market) is None or exchange_info_cache[“last_updated”] == 0 or time.time() – exchange_info_cache[“last_updated”] >= 300:
logging.debug(f”Оновлюємо exchange_info для {market}”)
await update_exchange_info(market)
try:
symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.warning(f”Символ {symbol} не знайдено, використовуємо резервні значення”)
lot_size = DEFAULT_LOT_SIZES.get(symbol, {“min_qty”: 0.001, “step_size”: 0.001, “max_qty”: 1000.0, “precision”: 4})
else:
quantity_precision = symbol_info[“quantityPrecision”]
lot_size_filter = next((f for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”), None)
if not lot_size_filter:
logging.warning(f”Фільтр LOT_SIZE не знайдено для {symbol}, використовуємо резервні значення”)
lot_size = DEFAULT_LOT_SIZES.get(symbol, {“min_qty”: 0.001, “step_size”: 0.001, “max_qty”: 1000.0, “precision”: 4})
else:
lot_size = {
“min_qty”: float(lot_size_filter[“minQty”]),
“max_qty”: float(lot_size_filter[“maxQty”]),
“step_size”: float(lot_size_filter[“stepSize”]),
“precision”: quantity_precision
}
if lot_size[“step_size”] == 0:
logging.error(f”step_size дорівнює 0 для {symbol}, використовуємо резервне значення”)
lot_size = DEFAULT_LOT_SIZES.get(symbol, {“min_qty”: 0.001, “step_size”: 0.001, “max_qty”: 1000.0, “precision”: 4})
quantity = round(quantity / lot_size[“step_size”]) * lot_size[“step_size”]
quantity = max(lot_size[“min_qty”], min(lot_size[“max_qty”], round(quantity, lot_size[“precision”])))
logging.debug(f”Округлена кількість для {symbol}: {quantity:.{lot_size[‘precision’]}f}”)
return quantity
except Exception as e:
logging.error(f”Помилка округлення кількості для {symbol}: {e}”)
lot_size = DEFAULT_LOT_SIZES.get(symbol, {“min_qty”: 0.001, “step_size”: 0.001, “max_qty”: 1000.0, “precision”: 4})
quantity = round(quantity / lot_size[“step_size”]) * lot_size[“step_size”]
quantity = max(lot_size[“min_qty”], min(lot_size[“max_qty”], round(quantity, lot_size[“precision”])))
logging.debug(f”Використано резервне округлення для {symbol}: {quantity:.{lot_size[‘precision’]}f}”)
return quantity

async def round_price(symbol, price):
“””
Округляє ціну відповідно до вимог Binance.
“””
# Повторні спроби для update_exchange_info
for attempt in range(config.get(“api_retry_attempts”, 3)):
try:
if exchange_info_cache[“last_updated”] == 0 or time.time() – exchange_info_cache[“last_updated”] >= 300:
await update_exchange_info(config[“market”])
market = config[“market”]
symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.error(f”Не вдалося знайти символ {symbol} у exchange_info_cache”)
await update_exchange_info(config[“market”])
symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.error(f”Не вдалося оновити exchange_info для {symbol}. Використовуємо стандартне округлення.”)
return round(price, 8)
price_filter = next(f for f in symbol_info[“filters”] if f[“filterType”] == “PRICE_FILTER”)
tick_size = float(price_filter[“tickSize”])
price = round(price / tick_size) * tick_size
price_precision = max(0, -int(np.floor(np.log10(tick_size))))
price = round(price, price_precision)
logging.debug(f”Округлена ціна для {symbol}: {price:.{price_precision}f}”)
return price
except Exception as e:
logging.warning(f”Спроба {attempt+1} оновлення exchange_info для {symbol} невдала: {e}”)
await asyncio.sleep(0.1)
logging.error(f”Не вдалося отримати symbol_info для {symbol}, використовуємо стандартне округлення”)
return round(price, 8)

async def get_current_price(symbol):
async with realtime_prices_lock:
price = realtime_prices.get(symbol)
if price and time.time() – last_heartbeat <= WEBSOCKET_STALE_THRESHOLD:
return price
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
return float(df[“close”].iloc[-1])
df = await get_historical_data(symbol, “1m”, limit=10)
if df is not None and not df.empty:
return float(df[“close”].iloc[-1])
logging.error(f”Не вдалося отримати ціну для {symbol}”)
return None

async def place_binance_order(symbol, side, quantity, price=None, order_type=”MARKET”, max_retries=5):
“””
Розміщує ордер на Binance.
“””
try:
quantity = await round_quantity(symbol, quantity)
if price is not None:
price = await round_price(symbol, price)

# Перевірка балансу перед розміщенням ордера
if config[“mode”] == “real” and order_type == “MARKET”:
account = await client.futures_account()
available_margin = float(account[“availableBalance”])
required_margin = (quantity * (price or await get_current_price(symbol))) / config[“leverage”][symbol]
if available_margin < required_margin:
logging.error(f”Недостатньо маржі для {symbol}: доступно={available_margin:.2f}, потрібно={required_margin:.2f}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Недостатньо маржі для розміщення ордера: потрібно {required_margin:.2f}, доступно {available_margin:.2f}”
})
return None

# Перевірка існуючих ордерів
if config[“mode”] == “real”:
open_orders = await client.futures_get_open_orders(symbol=symbol)
for order in open_orders:
if (order[“side”] == side.upper() and
order[“type”] == order_type and
abs(float(order[“origQty”]) – quantity) < 1e-6 and
(not price or abs(float(order.get(“stopPrice”, 0)) – price) < 1e-6)):
if order[“status”] in [“NEW”, “PARTIALLY_FILLED”, “PENDING”]: # Розширено перевірку статусів
logging.info(f”Ордер уже існує для {symbol}: {order[‘orderId’]}, статус={order[‘status’]}”)
return order
logging.info(f”Ордер для {symbol} вже виконаний або скасований: {order[‘orderId’]}, статус={order[‘status’]}”)

# Унікальний client_order_id з випадковим суфіксом
client_order_id = f”{symbol}_{side}_{int(time.time()*1000)}_{random.randint(1000, 9999)}”

for attempt in range(max_retries):
try:
if order_type == “MARKET”:
order = await client.futures_create_order(
symbol=symbol, side=side.upper(), type=”MARKET”, quantity=quantity,
newClientOrderId=client_order_id
)
elif order_type == “LIMIT”:
order = await client.futures_create_order(
symbol=symbol, side=side.upper(), type=”LIMIT”, price=price, quantity=quantity,
timeInForce=”GTC”, newClientOrderId=client_order_id
)
elif order_type == “STOP_MARKET”:
order = await client.futures_create_order(
symbol=symbol, side=side.upper(), type=”STOP_MARKET”, stopPrice=price,
quantity=quantity, reduceOnly=True, newClientOrderId=client_order_id
)
elif order_type == “TAKE_PROFIT_MARKET”:
order = await client.futures_create_order(
symbol=symbol, side=side.upper(), type=”TAKE_PROFIT_MARKET”, stopPrice=price,
quantity=quantity, reduceOnly=True, newClientOrderId=client_order_id
)
logging.info(f”Ордер виконано для {symbol}: {order}”)
return order
except BinanceAPIException as e:
logging.error(f”Помилка Binance API для {symbol} (код {e.code}): {e}”)
if e.code in [-2010, -1013]:
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка розміщення ордера: {order_type}, side={side}, qty={quantity}, price={price}, error={str(e)}”
})
return None
if attempt < max_retries – 1:
wait_time = 2 ** attempt
logging.warning(f”Спроба {attempt + 1}/{max_retries}: чекаємо {wait_time}с”)
await asyncio.sleep(wait_time)
else:
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Не вдалося розмістити ордер після {max_retries} спроб: {order_type}, side={side}, qty={quantity}, price={price}”
})
return None
except Exception as e:
logging.error(f”Невідома помилка для {symbol}: {e}”)
if attempt < max_retries – 1:
await asyncio.sleep(2 ** attempt)
else:
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Невідома помилка розміщення ордера після {max_retries} спроб: {order_type}, side={side}, qty={quantity}, price={price}, error={str(e)}”
})
return None
return None
except Exception as e:
logging.error(f”Помилка в place_binance_order для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка розміщення ордера: {str(e)}”
})
return None

def create_trade_template(trade_id, symbol, side, entry_price, quantity, leverage):
if not all(isinstance(x, (int, float)) and x > 0 for x in [entry_price, quantity, leverage]):
logging.error(f”Невалідні параметри для trade_id={trade_id}: entry_price={entry_price}, quantity={quantity}, leverage={leverage}”)
return None
if side not in [“BUY”, “SELL”]:
logging.error(f”Невалідний side для trade_id={trade_id}: {side}”)
return None
return {
“trade_id”: trade_id,
“symbol”: symbol,
“side”: side,
“entry_price”: float(entry_price),
“quantity”: float(quantity),
“leverage”: float(leverage),
“status”: “open”,
“open_time”: datetime.now(),
“profit”: 0,
“sl”: None,
“tp”: None,
“tp_3”: None,
“trailing_stop_activation”: None,
“trailing_stop”: None,
“sl_order_id”: None,
“tp_order_id”: None,
“tp_3_order_id”: None,
“order_id”: None,
“exit_price”: None,
“close_time”: None,
“tp1_triggered”: False
}

async def place_sl_tp_orders(trade, client_order_prefix=None):
“””
Розміщує SL і TP ордери для торгівлі.
Виправлено: Перевірка існуючих ордерів перед створенням (для ручних позицій), return True якщо знайдено активні.
“””
symbol = trade[“symbol”]
side = trade[“side”]
quantity = trade[“quantity”]
sl_price = trade[“sl”]
tp_price = trade[“tp”]
tp_3_price = trade[“tp_3”]
max_retries = config.get(“api_retry_attempts”, 3)
retry_delay = config.get(“api_retry_delay”, 1)
client_order_prefix = client_order_prefix or trade[“trade_id”]

async with config_lock:
try:
logging.info(f”INFO: Починаємо placement для {symbol}, trade_id={trade[‘trade_id’]}”) # Додано
if not all(isinstance(x, (int, float)) and x > 0 for x in [sl_price, tp_price, tp_3_price, quantity]):
logging.error(f”Невалідні значення для {symbol}: sl={sl_price}, tp={tp_price}, tp_3={tp_3_price}, quantity={quantity}, trade_id={trade[‘trade_id’]}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка: невалідні значення SL={sl_price}, TP={tp_price}, TP3={tp_3_price}, quantity={quantity}”,
“trade_id”: trade[“trade_id”],
“status”: “open”,
“open_time”: trade.get(“open_time”, datetime.now())
})
return False

if “tp1_triggered” not in trade:
trade[“tp1_triggered”] = False

if time.time() – exchange_info_cache[“last_updated”] >= 300:
await update_exchange_info(config[“market”])

symbol_info = next((s for s in exchange_info_cache[“futures”][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.error(f”Не знайдено symbol_info для {symbol}, використовуємо резервні значення”)
lot_size = {“minQty”: “0.001”, “stepSize”: “0.001”, “maxQty”: “1000.0”, “quantityPrecision”: 4}
tick_size = 0.0001
else:
price_filter = next((f for f in symbol_info[“filters”] if f[“filterType”] == “PRICE_FILTER”), {“tickSize”: “0.0001”})
lot_size_filter = next((f for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”), None)
tick_size = float(price_filter[“tickSize”])
lot_size = {
“min_qty”: float(lot_size_filter[“minQty”]) if lot_size_filter else 0.001,
“step_size”: float(lot_size_filter[“stepSize”]) if lot_size_filter else 0.001,
“max_qty”: float(lot_size_filter[“maxQty”]) if lot_size_filter else 1000.0,
“precision”: symbol_info[“quantityPrecision”]
}

trade[“sl”] = round(sl_price / tick_size) * tick_size
trade[“tp”] = round(tp_price / tick_size) * tick_size
trade[“tp_3”] = round(tp_3_price / tick_size) * tick_size
trade[“quantity”] = await round_quantity(symbol, quantity)
if trade[“quantity”] < lot_size[“min_qty”]:
logging.error(f”Кількість {trade[‘quantity’]} для {symbol} менша за min_qty {lot_size[‘min_qty’]}, trade_id={trade[‘trade_id’]}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка: кількість {trade[‘quantity’]} менша за min_qty {lot_size[‘min_qty’]}”,
“trade_id”: trade[“trade_id”],
“status”: “open”,
“open_time”: trade.get(“open_time”, datetime.now())
})
return False

tp_qty = round(trade[“quantity”] / 2, lot_size[“precision”]) if not trade[“tp1_triggered”] else 0
remaining_qty = trade[“quantity”] – tp_qty
if remaining_qty < lot_size[“min_qty”]:
trade[“quantity”] = max(trade[“quantity”], 2 * lot_size[“min_qty”])
trade[“quantity”] = await round_quantity(symbol, trade[“quantity”])
tp_qty = round(trade[“quantity”] / 2, lot_size[“precision”])
remaining_qty = trade[“quantity”] – tp_qty
logging.warning(f”Збільшено quantity до {trade[‘quantity’]} для {symbol}, trade_id={trade[‘trade_id’]} для відповідності min_qty”)

opposite_side = “SELL” if side == “BUY” else “BUY”

# Додано: Функція для перевірки існуючого ордера
async def check_existing_order(order_type, stop_price, expected_qty):
try:
open_orders = await client.futures_get_open_orders(symbol=symbol)
for order in open_orders:
if (order[“type”] == order_type and
float(order[“stopPrice”]) == stop_price and
float(order[“origQty”]) == expected_qty and
order[“status”] == “NEW”):
order_id = order[“orderId”]
logging.info(f”Ордер уже існує для {symbol}: {order_id}, статус=NEW”)
return order_id
return None
except Exception as e:
logging.warning(f”Помилка перевірки існуючого ордера для {symbol}: {e}”)
return None

async def try_place_order(order_type, price, order_id_suffix, qty, attempt=0):
if attempt >= max_retries:
logging.error(f”Досягнуто максимум спроб ({max_retries}) для {order_id_suffix} {symbol}, trade_id={trade[‘trade_id’]}”)
return None
try:
# Додано: Перевірка існуючого ордера перед створенням
existing_id = await check_existing_order(order_type, price, qty)
if existing_id:
return existing_id # Return True-еквівалент: ордер існує і активний

if config[“mode”] == “real”:
order = await place_binance_order(
symbol=symbol,
side=opposite_side,
quantity=qty,
price=price,
order_type=order_type,
max_retries=1
)
if order:
order_id = order[“orderId”]
# Перевірка статусу з retry
for check_attempt in range(2): # 2 спроби перевірки
try:
await asyncio.sleep(0.5)
order_details = await client.futures_get_order(symbol=symbol, orderId=order_id)
if order_details[“status”] in [“NEW”, “FILLED”]:
logging.info(f”Розміщено {order_id_suffix} ордер: price={price}, qty={qty}, id={order_id}, trade_id={trade[‘trade_id’]}”)
return order_id
else:
logging.warning(f”Ордер {order_id_suffix} для {symbol} не активний: status={order_details[‘status’]}, trade_id={trade[‘trade_id’]}”)
break
except Exception as check_e:
logging.warning(f”Помилка перевірки статусу {order_id_suffix} (спроба {check_attempt+1}): {check_e}”)
await asyncio.sleep(1)
# Якщо перевірка failed, retry create
await asyncio.sleep(retry_delay)
return await try_place_order(order_type, price, order_id_suffix, qty, attempt + 1)
else:
logging.warning(f”Не вдалося розмістити {order_id_suffix}, спроба {attempt + 1}/{max_retries}, trade_id={trade[‘trade_id’]}”)
await asyncio.sleep(retry_delay)
return await try_place_order(order_type, price, order_id_suffix, qty, attempt + 1)
else:
order_id = f”paper_{client_order_prefix}_{order_id_suffix}”
logging.info(f”Розміщено paper {order_id_suffix} ордер: price={price}, qty={qty}, id={order_id}, trade_id={trade[‘trade_id’]}”)
return order_id
except Exception as e:
logging.error(f”Помилка розміщення {order_id_suffix} для {symbol} (спроба {attempt + 1}/{max_retries}): {e}, trade_id={trade[‘trade_id’]}”)
await asyncio.sleep(retry_delay)
return await try_place_order(order_type, price, order_id_suffix, qty, attempt + 1)

success = True
sl_order_id = await try_place_order(“STOP_MARKET”, trade[“sl”], “SL”, trade[“quantity”])
if sl_order_id is None:
logging.error(f”Не вдалося розмістити SL для {symbol}, trade_id={trade[‘trade_id’]}”)
success = False
trade[“sl_order_id”] = sl_order_id

if not trade[“tp1_triggered”] and tp_qty > 0:
tp_order_id = await try_place_order(“TAKE_PROFIT_MARKET”, trade[“tp”], “TP”, tp_qty)
if tp_order_id is None:
logging.error(f”Не вдалося розмістити TP для {symbol}, trade_id={trade[‘trade_id’]}”)
success = False
trade[“tp_order_id”] = tp_order_id
else:
trade[“tp_order_id”] = None

if remaining_qty > 0:
tp_3_order_id = await try_place_order(“TAKE_PROFIT_MARKET”, trade[“tp_3”], “TP3”, remaining_qty)
if tp_3_order_id is None:
logging.error(f”Не вдалося розмістити TP3 для {symbol}, trade_id={trade[‘trade_id’]}”)
success = False
trade[“tp_3_order_id”] = tp_3_order_id
else:
trade[“tp_3_order_id”] = None

trade[“last_sl_tp_placed”] = datetime.now().isoformat()
trade[“open_time”] = trade.get(“open_time”, datetime.now())
if success:
await save_active_positions(active_positions) # Збереження після успіху
logging.info(f”Успішно розміщено/знайдено ордери для {symbol}: SL={trade[‘sl_order_id’]}, TP={trade[‘tp_order_id’]}, TP3={trade[‘tp_3_order_id’]}, trade_id={trade[‘trade_id’]}”)
else:
logging.warning(f”Частковий успіх розміщення ордерів для {symbol}, trade_id={trade[‘trade_id’]} (не закриваємо ручну позицію)”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Частковий успіх SL/TP/TP3 для trade_id={trade[‘trade_id’]} (ордери частково існують або помилка)”,
“trade_id”: trade[“trade_id”],
“status”: “open”,
“open_time”: trade[“open_time”]
})
# Виправлено: Не закриваємо позицію автоматично для ручних (тільки лог, без MARKET close)
logging.info(f”INFO: Placement завершено для {symbol}, success={success}, trade_id={trade[‘trade_id’]}”) # Додано
return success
except Exception as e:
logging.error(f”Помилка в place_sl_tp_orders для {symbol}: {e}, trade_id={trade[‘trade_id’]}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка розміщення SL/TP: {str(e)}”,
“trade_id”: trade[“trade_id”],
“status”: “open”,
“open_time”: trade.get(“open_time”, datetime.now())
})
return False

async def update_sl_order(trade, client, new_sl, config, stats):
“””
Оновлює SL-ордер для позиції з урахуванням динамічних режимів.
“””
try:
symbol = trade[“symbol”]
trade_id = trade[“trade_id”]
side = trade[“side”]

# Отримання поточної ціни для перевірки
df = await get_data_from_cache(symbol, “1m”)
current_price = float(df[“close”].iloc[-1]) if df is not None and not df.empty else 0
if current_price == 0:
logging.error(f”Неможливо отримати поточну ціну для {symbol}, trade_id={trade_id}”)
return

# Динамічний ризик-менеджмент
if config[“risk_management_mode”] == “dynamic”:
atr = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_window”])[-1] if not df.empty else 0.01
volatility_factor = max(1.0, atr / config[“volatility_threshold_high”])
base_sl_multiplier = config[“sl_multiplier_buy” if side == “BUY” else “sl_multiplier_sell”]
dynamic_sl_adjustment = base_sl_multiplier * (1 + (stats.get(symbol, {}).get(“loss_streak”, 0) * 0.05 * volatility_factor))
new_sl = new_sl * dynamic_sl_adjustment if side == “SELL” else new_sl / dynamic_sl_adjustment

# Динамічна корекція після збитків
if config[“loss_adjustment_mode”] == “dynamic” and stats.get(symbol, {}).get(“win_rate”, 0) < config[“min_win_rate”]:
adjustment_factor = 1.1 # Розширення SL на 10% при низькому win_rate
new_sl = new_sl * adjustment_factor if side == “SELL” else new_sl / adjustment_factor

# Перевірка розумності new_sl
price_diff = abs((new_sl – current_price) / current_price) if current_price != 0 else float(‘inf’)
if price_diff > 0.1: # SL не повинен відхилятися більше ніж на 10%
logging.warning(f”Некоректне значення new_sl для {symbol}: {new_sl:.4f}, поточна ціна: {current_price:.4f}, trade_id={trade_id}”)
return

# Округлення new_sl
new_sl = await round_price(symbol, new_sl)

if trade.get(“sl_order_id”):
try:
order = await client.futures_get_order(symbol=symbol, orderId=trade[“sl_order_id”])
if order[“status”] in [“NEW”, “PARTIALLY_FILLED”]:
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
logging.info(f”Скасовано старий SL-ордер для {symbol}, order_id={trade[‘sl_order_id’]}, trade_id={trade_id}”)
except BinanceAPIException as e:
logging.warning(f”Не вдалося перевірити або скасувати SL-ордер для {symbol}: {e}, trade_id={trade_id}”)

side_to_close = “SELL” if trade[“side”] == “BUY” else “BUY”
sl_order = await client.futures_create_order(
symbol=symbol,
side=side_to_close,
type=”STOP_MARKET”,
quantity=trade[“quantity”],
stopPrice=new_sl,
reduceOnly=True,
timeInForce=”GTC”,
clientOrderId=f”{symbol}_{side_to_close}_{trade_id}”
)
trade[“sl_order_id”] = sl_order[“orderId”]
trade[“sl”] = new_sl
logging.info(f”Оновлено SL-ордер для {symbol}: sl={new_sl}, order_id={trade[‘sl_order_id’]}, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка оновлення SL-ордеру для {symbol}: {e}, trade_id={trade_id}”)

async def close_binance_position(symbol, side, quantity, max_retries=5, market=None):
“””
Закриває позицію на Binance.
“””
try:
close_side = “SELL” if side == “BUY” else “BUY”

# Скасування всіх пов’язаних ордерів
for trade in active_positions[symbol]:
if trade[“side”] == side and trade[“status”] == “open”:
await cancel_all_related_orders(trade)

# Отримання поточної ціни з резервними джерелами
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol) or last_prices.get(symbol)
if current_price is None:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
df = await get_historical_data(symbol, “1m”, limit=10)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
logging.error(f”Неможливо отримати поточну ціну для {symbol}, використовуємо 0″)
current_price = 0

if not isinstance(quantity, (int, float)) or quantity <= 0:
logging.error(f”Недійсна кількість для закриття позиції {symbol}: {quantity}”)
return {“status”: “FAILED”, “avgPrice”: current_price}

if config[“mode”] == “real”:
if market == “futures”:
trades_to_close = [trade for trade in active_positions[symbol] if trade[“side”] == side and trade[“status”] == “open”]
for attempt in range(max_retries):
order = await place_binance_order(symbol, close_side, quantity, order_type=”MARKET”)
if order and order[“status”] == “FILLED”:
logging.info(f”Позицію закрито (futures): {order}”)
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = float(order.get(“avgPrice”, current_price))
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита: {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return order
logging.warning(f”Спроба {attempt + 1}/{max_retries} закриття позиції {symbol} не вдалася”)
await asyncio.sleep(2 ** attempt)
logging.error(f”Не вдалося закрити позицію через API після всіх спроб: {symbol}”)
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита (невдале закриття): {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return {“status”: “FAILED”, “avgPrice”: current_price}
else:
if close_side == “SELL” and spot_holdings.get(symbol, 0) >= quantity:
trades_to_close = [trade for trade in active_positions[symbol] if trade[“side”] == side and trade[“status”] == “open”]
for attempt in range(max_retries):
order = await place_binance_order(symbol, “SELL”, quantity, order_type=”MARKET”)
if order and order[“status”] == “FILLED”:
spot_holdings[symbol] -= quantity
logging.info(f”Позицію закрито (spot): {order}”)
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = float(order.get(“avgPrice”, current_price))
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита: {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return order
logging.warning(f”Спроба {attempt + 1}/{max_retries} закриття спотової позиції {symbol} не вдалося”)
await asyncio.sleep(2 ** attempt)
logging.error(f”Не вдалося закрити спотову позицію після всіх спроб: {symbol}”)
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита (невдале закриття): {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return {“status”: “FAILED”, “avgPrice”: current_price}
else:
logging.warning(f”Недостатньо активів для продажу: {symbol}, доступно={spot_holdings.get(symbol, 0)}”)
trades_to_close = [trade for trade in active_positions[symbol] if trade[“side”] == side and trade[“status”] == “open”]
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита (недостатньо активів): {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return {“status”: “FAILED”, “avgPrice”: current_price}
except BinanceAPIException as e:
logging.error(f”Помилка Binance API при закритті позиції {symbol} {side}: {str(e)}”)
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol) or last_prices.get(symbol)
if current_price is None:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
df = await get_historical_data(symbol, “1m”, limit=10)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
logging.error(f”Неможливо отримати поточну ціну для {symbol}, використовуємо 0″)
current_price = 0
trades_to_close = [trade for trade in active_positions[symbol] if trade[“side”] == side and trade[“status”] == “open”]
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита (помилка API): {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return {“status”: “FAILED”, “avgPrice”: current_price}
except Exception as e:
logging.error(f”Помилка закриття позиції {symbol} {side}: {str(e)}”)
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol) or last_prices.get(symbol)
if current_price is None:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
df = await get_historical_data(symbol, “1m”, limit=10)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
logging.error(f”Неможливо отримати поточну ціну для {symbol}, використовуємо 0″)
current_price = 0
trades_to_close = [trade for trade in active_positions[symbol] if trade[“side”] == side and trade[“status”] == “open”]
for trade in trades_to_close:
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“close_time”] = datetime.now()
logging.info(f”Позиція позначена як закрита (загальна помилка): {symbol}, trade_id={trade[‘trade_id’]}”)
active_positions[symbol] = [t for t in active_positions[symbol] if t[“status”] == “open”]
await save_active_positions(active_positions)
return {“status”: “FAILED”, “avgPrice”: current_price}

async def open_trade(signal, df, symbol, atr, market=None):
try:
market = market or config[“market”]
if market not in [“futures”, “spot”]:
logging.error(f”Невалідне значення market для {symbol}: {market}”)
return None, None
if exchange_info_cache.get(market) is None:
logging.error(f”exchange_info_cache для {market} не ініціалізовано”)
await update_exchange_info(market)
if exchange_info_cache.get(market) is None:
logging.error(f”Не вдалося ініціалізувати exchange_info_cache для {market}”)
return None, None

current_price = float(df[“close”].iloc[-1])
logging.info(f”Початок open_trade для {symbol}, signal={signal}, price={current_price}”)

atr_value = atr if atr is not None else talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_history_length”])[-1]
if pd.isna(atr_value) or not isinstance(atr_value, (int, float)) or atr_value <= 0:
logging.error(f”Невалідне значення ATR для {symbol}: {atr_value}”)
return None, None
volatility_ratio = atr_value / current_price if current_price > 0 else 0.01
logging.debug(f”volatility_ratio для {symbol}: {volatility_ratio:.4f}, threshold={config[‘volatility_threshold_high’]}”)
if volatility_ratio > config[“volatility_threshold_high”]:
logging.warning(f”📉 Висока волатильність для {symbol} (volatility_ratio={volatility_ratio:.4f}). Пропускаємо торгівлю.”)
return None, None

seen_trade_ids = set()
total_exposure = sum(
t[“quantity”] * t[“entry_price”] * t[“leverage”]
for s in config[“symbols”]
for t in active_positions.get(s, [])
if t.get(“status”) == “open” and t[“trade_id”] not in seen_trade_ids
and seen_trade_ids.add(t[“trade_id”])
)
leverage = config[“leverage”][symbol]
symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
max_leverage = symbol_info.get(“max_leverage”, 20) if symbol_info else 20
if leverage > max_leverage:
logging.warning(f”Леверидж {leverage} для {symbol} перевищує максимальний {max_leverage}, встановлюємо {max_leverage}”)
leverage = max_leverage
if market == “futures”:
await client.futures_change_leverage(symbol=symbol, leverage=leverage)

position_value = stats[symbol][“balance”] * config[“position_size”] * leverage
total_balance = sum(stats[s][“balance”] for s in config[“symbols”]) if config[“mode”] == “real” else config[“initial_balance”]
logging.debug(f”Експозиція для {symbol}: total_exposure={total_exposure:.2f}, position_value={position_value:.2f}, limit={total_balance * config[‘total_exposure_limit’]:.2f}”)
if total_exposure + position_value > total_balance * config[“total_exposure_limit”]:
logging.warning(f”📉 Перевищено ліміт експозиції для {symbol}: {total_exposure + position_value:.2f} > {total_balance * config[‘total_exposure_limit’]:.2f}”)
return None, None

if config.get(“position_sizing_mode”, “fixed”) == “dynamic”:
volatility_factor = atr_value / current_price if current_price > 0 else 0.01
volatility_factor = max(0.5, min(2.0, volatility_factor / config[“volatility_threshold_high”]))
position_size = config[“position_size”] / volatility_factor
else:
position_size = config[“position_size”]

allocation = stats[symbol][“balance”] * config[“capital_allocation”][symbol]
if current_price <= 0:
logging.error(f”Невалідна поточна ціна для {symbol}: {current_price}”)
return None, None
quantity = (allocation * position_size * leverage) / current_price
quantity = await round_quantity(symbol, quantity, market)
logging.info(f”Розрахунок позиції для {symbol}: balance={stats[symbol][‘balance’]:.2f}, allocation={allocation:.2f}, position_size={position_size:.4f}, leverage={leverage}, price={current_price:.2f}, quantity={quantity:.6f}”)

if not isinstance(quantity, (int, float)) or quantity <= 0:
logging.error(f”Невалідне значення quantity для {symbol}: {quantity}”)
return None, None

symbol_info = next((s for s in exchange_info_cache[market][“symbols”] if s[“symbol”] == symbol), None)
if symbol_info:
lot_size = next((f for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”), None)
if lot_size and float(lot_size[“minQty”]) > quantity:
logging.error(f”Кількість {quantity} для {symbol} менша за min_qty {lot_size[‘minQty’]}”)
return None, None

if config[“mode”] == “real”:
if market == “futures”:
account = await client.futures_account()
available_margin = float(account[“availableBalance”])
required_margin = (quantity * current_price) / leverage
logging.info(f”Перевірка маржі для {symbol}: доступно={available_margin:.2f}, потрібно={required_margin:.2f}”)
if available_margin < required_margin:
logging.warning(f”Недостатньо маржі для відкриття позиції {symbol}: доступно {available_margin:.2f}, потрібно {required_margin:.2f}”)
return None, None
elif market == “spot” and signal == “BUY”:
account = await client.get_account()
usdt_balance = float(next(a[“free”] for a in account[“balances”] if a[“asset”] == “USDT”))
required_usdt = quantity * current_price
if usdt_balance < required_usdt:
logging.warning(f”Недостатньо USDT для спотової покупки {symbol}: доступно {usdt_balance:.2f}, потрібно {required_usdt:.2f}”)
return None, None

position_exists = False
if market == “futures”:
positions = await client.futures_position_information(symbol=symbol)
for pos in positions:
if pos[“symbol”] == symbol and float(pos[“positionAmt”]) != 0:
position_amt = float(pos[“positionAmt”])
pos_side = “BUY” if position_amt > 0 else “SELL”
if pos_side == signal:
position_exists = True
logging.info(f”Позиція для {symbol} вже існує: side={pos_side}, amount={position_amt}, quantity={quantity}”)
break

if position_exists:
logging.info(f”Позиція для {symbol} вже існує, пропускаємо відкриття”)
return None, None

trade_id = f”{int(time.time())}_{random.randint(1000, 9999)}_{signal}_{symbol}”
trade = create_trade_template(trade_id, symbol, signal, current_price, quantity, leverage)
trade[“open_time”] = datetime.now()

order_id = None
if config[“mode”] == “real”:
try:
if market == “futures”:
side = “BUY” if signal == “BUY” else “SELL”
await client.futures_change_leverage(symbol=symbol, leverage=leverage)
order = await client.futures_create_order(
symbol=symbol, side=side, type=”MARKET”, quantity=quantity
)
order_id = order[“orderId”]
trade[“order_id”] = order_id
# Перевірка статусу ордера
await asyncio.sleep(2) # Затримка 2 секунди
order_details = await client.futures_get_order(symbol=symbol, orderId=order_id)
logging.debug(f”Статус ордера для {symbol}: {order_details[‘status’]}, trade_id={trade_id}”)
if order_details[“status”] not in [“NEW”, “FILLED”]:
logging.error(f”Помилка виконання ордера для {symbol}: status={order_details[‘status’]}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка виконання ордера: статус {order_details[‘status’]}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: trade[“open_time”]
})
return None, None
entry_price = float(order_details[“avgPrice”]) if order_details[“avgPrice”] else current_price
logging.info(f”Відкрито позицію для {symbol}: {signal}, qty={quantity:.6f}, leverage={leverage}, entry_price={entry_price:.2f}, order_id={order_id}”)
else:
side = “BUY” if signal == “BUY” else “SELL”
order = await client.create_order(
symbol=symbol, side=side, type=”MARKET”, quantity=quantity
)
order_id = order[“orderId”]
trade[“order_id”] = order_id
entry_price = float(order[“fills”][0][“price”]) if order[“fills”] else current_price
if signal == “BUY”:
spot_holdings[symbol] += quantity
else:
spot_holdings[symbol] -= quantity
logging.info(f”Відкрито спотову позицію для {symbol}: {signal}, qty={quantity:.6f}, entry_price={entry_price:.2f}, order_id={order_id}”)
except BinanceAPIException as e:
logging.error(f”Помилка Binance API при відкритті позиції для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка відкриття позиції: {str(e)}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: trade[“open_time”]
})
return None, None
except Exception as e:
logging.error(f”Помилка відкриття позиції для {symbol}: {type(e).__name__} – {str(e)}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Помилка відкриття позиції: {str(e)}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: trade[“open_time”]
})
return None, None
else:
entry_price = current_price
order_id = f”paper_{trade_id}”
trade[“order_id”] = order_id
logging.info(f”Відкрито paper позицію для {symbol}: {signal}, qty={quantity:.6f}, entry_price={entry_price:.2f}, order_id={order_id}”)

if not trade.get(“order_id”):
logging.warning(f”Позиція для {symbol} не відкрита, пропускаємо SL/TP, trade_id={trade_id}”)
return None, None

trade[“entry_price”] = entry_price

required_keys = [
“sl_multiplier_buy”, “tp_2_multiplier_buy”, “tp_3_multiplier_buy”,
“sl_multiplier_sell”, “tp_2_multiplier_sell”, “tp_3_multiplier_sell”,
“trailing_stop_activation_buy”, “trailing_stop_activation_sell”,
“trailing_stop_distance”
]
updated_config = await adjust_config_based_on_volatility(symbol, atr_value, entry_price)
if not updated_config or not isinstance(updated_config, dict):
logging.error(f”Невалідний updated_config для {symbol}, використовуємо значення з config.json, trade_id={trade_id}”)
updated_config = {key: config[key] for key in required_keys}

for key in required_keys:
if key not in updated_config or not isinstance(updated_config[key], (int, float)) or updated_config[key] <= 0:
logging.warning(f”Невалідне значення для {key} у updated_config для {symbol}: {updated_config.get(key, ‘None’)}, використовуємо config.json, trade_id={trade_id}”)
updated_config[key] = config[key]

if signal == “BUY”:
trade[“sl”] = entry_price * updated_config[“sl_multiplier_buy”]
trade[“tp”] = entry_price * updated_config[“tp_2_multiplier_buy”]
trade[“tp_3”] = entry_price * updated_config[“tp_3_multiplier_buy”]
trade[“trailing_stop_activation”] = entry_price * updated_config[“trailing_stop_activation_buy”]
trade[“trailing_stop_distance”] = updated_config[“trailing_stop_distance”]
else:
trade[“sl”] = entry_price * updated_config[“sl_multiplier_sell”]
trade[“tp”] = entry_price * updated_config[“tp_2_multiplier_sell”]
trade[“tp_3”] = entry_price * updated_config[“tp_3_multiplier_sell”]
trade[“trailing_stop_activation”] = entry_price * updated_config[“trailing_stop_activation_sell”]
trade[“trailing_stop_distance”] = updated_config[“trailing_stop_distance”]
trade[“trailing_stop”] = None

if not all(isinstance(x, (int, float)) and x > 0 for x in [trade[“sl”], trade[“tp”], trade[“tp_3”], trade[“trailing_stop_activation”]]):
logging.error(f”Невалідні або None значення для {symbol}: sl={trade[‘sl’]}, tp={trade[‘tp’]}, tp_3={trade[‘tp_3’]}, trailing_stop_activation={trade[‘trailing_stop_activation’]}, trade_id={trade_id}”)
if config[“mode”] == “real” and market == “futures”:
try:
await client.futures_create_order(
symbol=symbol, side=(“SELL” if signal == “BUY” else “BUY”), type=”MARKET”, quantity=quantity, reduceOnly=True
)
logging.info(f”Закрито позицію для {symbol} через невалідні SL/TP/TP3, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка закриття позиції для {symbol}: {type(e).__name__} – {str(e)}, trade_id={trade_id}”)
return None, None

trade[“sl”] = await round_price(symbol, trade[“sl”])
trade[“tp”] = await round_price(symbol, trade[“tp”])
trade[“tp_3”] = await round_price(symbol, trade[“tp_3”])
trade[“trailing_stop_activation”] = await round_price(symbol, trade[“trailing_stop_activation”])
logging.info(f”SL/TP після округлення: SL={trade[‘sl’]:.2f}, TP={trade[‘tp’]:.2f}, TP3={trade[‘tp_3’]:.2f}, trailing_stop_activation={trade[‘trailing_stop_activation’]:.2f}”)

if not all(isinstance(x, (int, float)) and x > 0 for x in [trade[“sl”], trade[“tp”], trade[“tp_3”]]):
logging.error(f”Невалідні SL/TP після округлення для {symbol}: sl={trade[‘sl’]}, tp={trade[‘tp’]}, tp_3={trade[‘tp_3’]}, trade_id={trade_id}”)
if config[“mode”] == “real” and market == “futures”:
try:
await client.futures_create_order(
symbol=symbol, side=(“SELL” if signal == “BUY” else “BUY”), type=”MARKET”, quantity=quantity, reduceOnly=True
)
logging.info(f”Закрито позицію для {symbol} через невалідне SL/TP/TP3 після округлення, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка закриття позиції для {symbol}: {type(e).__name__} – {str(e)}, trade_id={trade_id}”)
return None, None

success = await place_sl_tp_orders(trade, client_order_prefix=trade_id)

if not success or not all([trade.get(“sl_order_id”), trade.get(“tp_order_id”), trade.get(“tp_3_order_id”)]):
logging.warning(f”Не всі SL/TP ордери розміщено для {symbol}: sl={trade.get(‘sl_order_id’)}, tp={trade.get(‘tp_order_id’)}, tp3={trade.get(‘tp_3_order_id’)}, trade_id={trade_id}”)
if config[“mode”] == “real” and market == “futures”:
try:
await client.futures_create_order(
symbol=symbol, side=(“SELL” if signal == “BUY” else “BUY”), type=”MARKET”, quantity=quantity, reduceOnly=True
)
logging.info(f”Закрито позицію для {symbol} через невдале розміщення SL/TP/TP3, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка закриття позиції для {symbol}: {type(e).__name__} – {str(e)}, trade_id={trade_id}”)
return None, None

async with active_positions_lock:
active_positions.setdefault(symbol, []).append(trade)
logging.info(f”Додано позицію до active_positions: {trade}”)
await save_active_positions(active_positions)
async with stats_lock:
stats[symbol][“total_trades”] += 1
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
logging.info(f”Оновлено stats для {symbol}: total_trades={stats[symbol][‘total_trades’]}”)

logging.info(f”🟢 Відкрито угоду {trade_id}: {symbol}, {signal}, ціна={entry_price:.2f}, qty={quantity:.6f}, SL={trade[‘sl’]:.2f}, TP={trade[‘tp’]:.2f}, TP3={trade[‘tp_3’]:.2f}, trailing_stop_distance={trade[‘trailing_stop_distance’]:.4f}”)
logging.info(f”Надсилання повідомлення про відкриття угоди для {symbol}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: signal,
“entry_price”: entry_price,
“quantity”: quantity,
“leverage”: leverage,
“sl”: trade[“sl”],
“tp”: trade[“tp”],
“tp_3”: trade[“tp_3”],
“trade_id”: trade_id,
“status”: “open”,
“open_time”: trade[“open_time”]
})
return trade, order_id
except BinanceAPIException as e:
logging.error(f”Помилка Binance API в open_trade для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: signal,
“message”: f”Помилка відкриття позиції: {str(e)}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: datetime.now()
})
return None, None
except ValueError as e:
logging.error(f”Помилка значення в open_trade для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: signal,
“message”: f”Помилка значення: {str(e)}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: datetime.now()
})
return None, None
except Exception as e:
logging.error(f”Невідома помилка в open_trade для {symbol}: {type(e).__name__} – {str(e)}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: signal,
“message”: f”Невідома помилка: {str(e)}”,
“trade_id”: trade_id,
“status”: “open”,
“open_time”: datetime.now()
})
return None, None

async def adjust_config_based_on_loss_streak(symbol):
“””
Коригує конфігурацію на основі серії збитків.
“””
async with config_lock:
try:
with open(“config.json”, “r”, encoding=”utf-8″) as f:
config_data = json.load(f)

if config_data.get(“loss_adjustment_mode”, “manual”) == “auto” and config_data.get(“position_sizing_mode”, “dynamic”) == “fixed”:
async with stats_lock:
loss_streak = stats[symbol][“loss_streak”]
if loss_streak >= config_data[“max_loss_streak”]:
config_data[“position_size”] = max(config_data.get(“position_size”, 0.1) * 0.8, 0.01)
config_data[“sl_multiplier_buy”] = config_data[“sl_multiplier_buy”] * 1.1
config_data[“sl_multiplier_sell”] = config_data[“sl_multiplier_sell”] * 1.1
config_data[“tp_2_multiplier_buy”] = config_data[“tp_2_multiplier_buy”] * 1.05
config_data[“tp_2_multiplier_sell”] = config_data[“tp_2_multiplier_sell”] * 0.95
logging.info(f”Налаштовано config через серію збитків ({loss_streak} для {symbol})”)
elif loss_streak == 0:
config_data[“position_size”] = 0.1
config_data[“sl_multiplier_buy”] = 0.98
config_data[“sl_multiplier_sell”] = 1.02
config_data[“tp_2_multiplier_buy”] = 1.01
config_data[“tp_2_multiplier_sell”] = 0.97
logging.info(f”Скинуто config до стандартних значень для {symbol}”)

# Перевірка валідності
for key in [“position_size”, “sl_multiplier_buy”, “sl_multiplier_sell”, “tp_2_multiplier_buy”, “tp_2_multiplier_sell”]:
if not isinstance(config_data[key], (int, float)) or config_data[key] <= 0:
logging.error(f”Невалідне значення для {key}: {config_data[key]}, використовуємо стандартне”)
config_data[key] = {
“position_size”: 0.1,
“sl_multiplier_buy”: 0.98,
“sl_multiplier_sell”: 1.02,
“tp_2_multiplier_buy”: 1.01,
“tp_2_multiplier_sell”: 0.97
}[key]

# Спроба запису файлу
try:
with open(“config.json”, “w”, encoding=”utf-8″) as f:
json.dump(config_data, f, indent=4)
# Оновлення глобальної config
global config
config = config_data
logging.info(f”Оновлено глобальну config для {symbol}”)
except IOError as e:
logging.error(f”Не вдалося записати config.json для {symbol}: {e}”)
return config # Повертаємо старий config

return config_data
return config_data
except Exception as e:
logging.error(f”Помилка в adjust_config_based_on_loss_streak для {symbol}: {e}”)
return config

async def manage_trade(trade, df):
“””
Управляє відкритою позицією, включаючи трейлінг-стоп і закриття.
“””
try:
symbol = trade[“symbol”]
side = trade[“side”]
entry_price = trade[“entry_price”]
quantity = trade[“quantity”]
leverage = trade[“leverage”]
trade_id = trade[“trade_id”]

if “tp1_triggered” not in trade:
trade[“tp1_triggered”] = False

open_orders = await client.futures_get_open_orders(symbol=symbol)
active_order_ids = {order[“orderId”] for order in open_orders}
for order in open_orders:
order_id = order[“orderId”]
order_qty = float(order[“origQty”])
if order_id not in {trade.get(“sl_order_id”), trade.get(“tp_order_id”), trade.get(“tp_3_order_id”)} and order_qty != quantity:
try:
if order[“status”] in [“NEW”, “PARTIALLY_FILLED”]:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано невідповідний ордер для {symbol}: order_id={order_id}, qty={order_qty}, trade_id={trade_id}”)
except Exception as e:
logging.warning(f”Не вдалося скасувати ордер для {symbol}: order_id={order_id}, error={e}”)

async with realtime_prices_lock:
current_price = realtime_prices.get(symbol, df[“close”].iloc[-1])
if time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
logging.warning(f”Застарілі дані WebSocket для {symbol}, оновлюємо через HTTP”)
await update_prices_via_http([symbol])
current_price = realtime_prices.get(symbol, current_price) or current_price

if not trade[“tp1_triggered”] and not all(trade.get(key) for key in [“sl_order_id”, “tp_order_id”, “tp_3_order_id”]):
await place_sl_tp_orders(trade, client_order_prefix=trade_id)
async with active_positions_lock:
await save_active_positions(active_positions)
return trade

async with active_positions_lock:
for order_key, price_key in [(“sl_order_id”, “sl”), (“tp_order_id”, “tp”), (“tp_3_order_id”, “tp_3”)]:
order_id = trade.get(order_key)
if order_id and not str(order_id).startswith(“paper_”) and order_id not in active_order_ids:
logging.info(f”{order_key} виконано для {symbol}: trade_id={trade_id}”)
await cancel_all_related_orders(trade)
if order_key == “tp_order_id”:
trade[“tp1_triggered”] = True
trade[“tp_order_id”] = None
remaining_qty = await round_quantity(symbol, trade[“quantity”] / 2)
symbol_info = next((s for s in exchange_info_cache[“futures”][“symbols”] if s[“symbol”] == symbol), None)
lot_size = next((f for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”), None) if symbol_info else None
if lot_size and remaining_qty < float(lot_size[“minQty”]):
logging.error(f”remaining_qty {remaining_qty} для {symbol} менша за min_qty {lot_size[‘minQty’]}, закриваємо позицію”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (
(trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
if side == “BUY” else (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
)
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
trade[“quantity”] = remaining_qty
await save_active_positions(active_positions)

opposite_side = “SELL” if side == “BUY” else “BUY”
opposite_signal = await check_opposite_signal(symbol, current_price, df, side)
if opposite_signal:
try:
await client.futures_create_order(
symbol=symbol, side=opposite_side, type=”MARKET”, quantity=remaining_qty, reduceOnly=True
)
logging.info(f”Закрито залишок позиції для {symbol}: qty={remaining_qty}, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка закриття залишку позиції для {symbol}: {e}, trade_id={trade_id}”)
new_side = “SELL” if side == “BUY” else “BUY”
new_trade, _ = await open_trade(new_side, df, symbol, None)
if new_trade:
active_positions[symbol].append(new_trade)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (
(trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
if side == “BUY” else (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
)
stats[symbol][“winning_trades”] += 1
stats[symbol][“loss_streak”] = 0
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade

break_even_price = entry_price + (entry_price * config[“commission_rate”] + entry_price * 0.001 if side == “BUY” else -(entry_price * config[“commission_rate”] + entry_price * 0.001))
break_even_price = await round_price(symbol, break_even_price)
if trade.get(“sl_order_id”):
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
new_sl_order = None
for attempt in range(config.get(“api_retry_attempts”, 3)):
try:
new_sl_order = await place_binance_order(
symbol=symbol, side=opposite_side, quantity=remaining_qty, price=break_even_price, order_type=”STOP_MARKET”
)
if new_sl_order:
break
except Exception as e:
logging.warning(f”Спроба {attempt+1} встановлення SL у беззбиток для {symbol} невдала: {e}”)
await asyncio.sleep(0.1)
if new_sl_order:
trade[“sl_order_id”] = new_sl_order[“orderId”]
trade[“sl”] = break_even_price
trade[“trailing_stop_activation”] = break_even_price * (1.01 if side == “BUY” else 0.99)
logging.info(f”SL переставлено в беззбиток для {symbol}: sl={break_even_price}, qty={remaining_qty}, trade_id={trade_id}”)
else:
logging.error(f”Не вдалося встановити SL у беззбиток для {symbol}, закриваю позицію, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (
(trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
if side == “BUY” else (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
)
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
await save_active_positions(active_positions)
return trade
elif order_key in [“tp_3_order_id”, “sl_order_id”]:
trade[“status”] = “closed”
trade[“exit_price”] = trade[price_key]
trade[“profit”] = (
(trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
if side == “BUY” else (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
)
if order_key == “tp_3_order_id”:
stats[symbol][“winning_trades”] += 1
stats[symbol][“loss_streak”] = 0
else:
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade

opposite_side = “SELL” if side == “BUY” else “BUY”
async with active_positions_lock:
if side == “BUY”:
if current_price >= trade[“trailing_stop_activation”] and trade[“trailing_stop”] is None:
trade[“trailing_stop”] = current_price * (1 – trade[“trailing_stop_distance”])
trade[“trailing_stop”] = await round_price(symbol, trade[“trailing_stop”])
if config[“mode”] == “real” and trade.get(“sl_order_id”):
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
new_sl_order = await place_binance_order(
symbol=symbol, side=opposite_side, quantity=trade[“quantity”], price=trade[“trailing_stop”], order_type=”STOP_MARKET”
)
if new_sl_order:
trade[“sl_order_id”] = new_sl_order[“orderId”]
trade[“sl”] = trade[“trailing_stop”]
logging.info(f”Активовано trailing stop для {symbol}: sl={trade[‘sl’]}, trade_id={trade_id}”)
else:
logging.error(f”Не вдалося встановити trailing stop для {symbol}, закриваю позицію, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
elif trade[“trailing_stop”] is not None:
new_trailing_stop = current_price * (1 – trade[“trailing_stop_distance”])
new_trailing_stop = await round_price(symbol, new_trailing_stop)
if new_trailing_stop > trade[“trailing_stop”]:
trade[“trailing_stop”] = new_trailing_stop
if config[“mode”] == “real” and trade.get(“sl_order_id”):
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
new_sl_order = await place_binance_order(
symbol=symbol, side=opposite_side, quantity=trade[“quantity”], price=trade[“trailing_stop”], order_type=”STOP_MARKET”
)
if new_sl_order:
trade[“sl_order_id”] = new_sl_order[“orderId”]
trade[“sl”] = trade[“trailing_stop”]
logging.info(f”Оновлено trailing stop для {symbol}: sl={trade[‘sl’]}, trade_id={trade_id}”)
else:
logging.error(f”Не вдалося оновити trailing stop для {symbol}, закриваю позицію, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
if current_price <= trade[“trailing_stop”]:
await cancel_all_related_orders(trade)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (trade[“exit_price”] – entry_price) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
else: # SELL
if current_price <= trade[“trailing_stop_activation”] and trade[“trailing_stop”] is None:
trade[“trailing_stop”] = current_price * (1 + trade[“trailing_stop_distance”])
trade[“trailing_stop”] = await round_price(symbol, trade[“trailing_stop”])
if config[“mode”] == “real” and trade.get(“sl_order_id”):
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
new_sl_order = await place_binance_order(
symbol=symbol, side=opposite_side, quantity=trade[“quantity”], price=trade[“trailing_stop”], order_type=”STOP_MARKET”
)
if new_sl_order:
trade[“sl_order_id”] = new_sl_order[“orderId”]
trade[“sl”] = trade[“trailing_stop”]
logging.info(f”Активовано trailing stop для {symbol}: sl={trade[‘sl’]}, trade_id={trade_id}”)
else:
logging.error(f”Не вдалося встановити trailing stop для {symbol}, закриваю позицію, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
elif trade[“trailing_stop”] is not None:
new_trailing_stop = current_price * (1 + trade[“trailing_stop_distance”])
new_trailing_stop = await round_price(symbol, new_trailing_stop)
if new_trailing_stop < trade[“trailing_stop”]:
trade[“trailing_stop”] = new_trailing_stop
if config[“mode”] == “real” and trade.get(“sl_order_id”):
await client.futures_cancel_order(symbol=symbol, orderId=trade[“sl_order_id”])
new_sl_order = await place_binance_order(
symbol=symbol, side=opposite_side, quantity=trade[“quantity”], price=trade[“trailing_stop”], order_type=”STOP_MARKET”
)
if new_sl_order:
trade[“sl_order_id”] = new_sl_order[“orderId”]
trade[“sl”] = trade[“trailing_stop”]
logging.info(f”Оновлено trailing stop для {symbol}: sl={trade[‘sl’]}, trade_id={trade_id}”)
else:
logging.error(f”Не вдалося оновити trailing stop для {symbol}, закриваю позицію, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade
if current_price >= trade[“trailing_stop”]:
await cancel_all_related_orders(trade)
trade[“status”] = “closed”
trade[“exit_price”] = current_price
trade[“profit”] = (entry_price – trade[“exit_price”]) * trade[“quantity”] * leverage
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
trade = await finalize_trade(trade)
await adjust_config_based_on_loss_streak(symbol)
await save_active_positions(active_positions)
return trade

await save_active_positions(active_positions)
return trade
except Exception as e:
logging.error(f”Помилка в manage_trade для {symbol}: {e}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка управління позицією: {str(e)}”,
“trade_id”: trade_id
})
return trade

async def finalize_trade(trade):
“””
Завершує торгівлю, оновлює статистику та надсилає повідомлення.
“””
symbol = trade[“symbol”]
side = trade[“side”]
entry_price = trade[“entry_price”]
quantity = trade[“quantity”]
leverage = trade[“leverage”]
trade_id = trade[“trade_id”]

try:
async with stats_lock:
async with active_positions_lock:
if not isinstance(quantity, (int, float)) or quantity <= 0:
logging.error(f”Невалідна quantity для {symbol}: {quantity}, trade_id={trade_id}”)
trade[“status”] = “closed”
trade[“exit_price”] = entry_price
trade[“profit”] = 0
active_positions[symbol] = [t for t in active_positions[symbol] if t[“trade_id”] != trade_id]
await save_active_positions(active_positions)
return trade

if config[“mode”] == “real” and not trade.get(“exit_price”):
for attempt in range(3):
order = await close_binance_position(symbol, side, quantity)
if order and order[“status”] == “FILLED”:
trade[“exit_price”] = float(order.get(“avgPrice”, 0))
break
logging.warning(f”Спроба {attempt + 1}/3 закриття позиції {symbol} не вдалася, trade_id={trade_id}”)
await asyncio.sleep(1)
if not trade.get(“exit_price”) or trade[“exit_price”] == 0:
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol)
if time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
await update_prices_via_http([symbol])
current_price = realtime_prices.get(symbol)
if not current_price:
df = await get_data_from_cache(symbol, “1m”)
current_price = float(df[“close”].iloc[-1]) if df is not None and not df.empty else last_prices.get(symbol, 0)
order = await place_binance_order(symbol, “SELL” if side == “BUY” else “BUY”, trade[“quantity”], order_type=”MARKET”)
trade[“exit_price”] = float(order.get(“avgPrice”, current_price)) if order else current_price
elif not trade.get(“exit_price”):
async with realtime_prices_lock:
current_price = realtime_prices.get(symbol)
if time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
await update_prices_via_http([symbol])
current_price = realtime_prices.get(symbol)
if not current_price:
df = await get_data_from_cache(symbol, “1m”)
current_price = float(df[“close”].iloc[-1]) if df is not None and not df.empty else last_prices.get(symbol, 0)
trade[“exit_price”] = current_price
if market == “spot” and side == “SELL”:
spot_holdings[symbol] = max(0, spot_holdings.get(symbol, 0) – quantity)

if not isinstance(trade[“exit_price”], (int, float)) or trade[“exit_price”] <= 0:
logging.error(f”Невалідна exit_price для {symbol}: {trade[‘exit_price’]}, trade_id={trade_id}”)
trade[“exit_price”] = entry_price
trade[“profit”] = 0
else:
trade[“profit”] = (
(trade[“exit_price”] – entry_price) * quantity * leverage
if side == “BUY” else (entry_price – trade[“exit_price”]) * quantity * leverage
)
commission = (entry_price + trade[“exit_price”]) * quantity * config[“commission_rate”]
trade[“profit”] -= commission

open_orders = await client.futures_get_open_orders(symbol=symbol)
for order in open_orders:
order_id = order[“orderId”]
if order_id in [trade.get(“sl_order_id”), trade.get(“tp_order_id”), trade.get(“tp_3_order_id”)]:
try:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано ордер для {symbol}: order_id={order_id}, trade_id={trade_id}”)
except Exception as e:
logging.warning(f”Не вдалося скасувати ордер для {symbol}: order_id={order_id}, error={e}”)

stats[symbol][“total_trades”] += 1
if trade[“profit”] > 0:
stats[symbol][“winning_trades”] += 1
stats[symbol][“loss_streak”] = 0
stats[symbol][“last_loss_time”] = 0
else:
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = time.time()
stats[symbol][“total_profit”] += trade[“profit”]
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
stats[symbol][“win_rate”] = (
stats[symbol][“winning_trades”] / stats[symbol][“total_trades”] if stats[symbol][“total_trades”] > 0 else 0
)
await save_stats()

trade[“close_time”] = datetime.now()
save_trade(trade)
await send_telegram_notification(trade)

if symbol in active_positions:
active_positions[symbol] = [t for t in active_positions[symbol] if t[“trade_id”] != trade_id]
if not active_positions[symbol]:
del active_positions[symbol]
await save_active_positions(active_positions)

logging.info(f”🔒 Закрито угоду {symbol}: Профіт={trade[‘profit’]:.2f}, Баланс={stats[symbol][‘balance’]:.2f}, trade_id={trade_id}”)
return trade
except Exception as e:
logging.error(f”Помилка в finalize_trade для {symbol}: {e}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка в finalize_trade: {str(e)}”,
“trade_id”: trade_id
})
return trade

async def cancel_all_related_orders(trade):
“””
Скасовує всі пов’язані SL/TP ордери для угоди.
“””
symbol = trade[“symbol”]
trade_id = trade.get(“trade_id”, “невідомий”)

try:
for order_id_key in [“sl_order_id”, “tp_order_id”, “tp_3_order_id”]:
order_id = trade.get(order_id_key)
if not order_id:
continue
if str(order_id).startswith(“paper_”):
logging.debug(f”Пропущено скасування {order_id_key} для {symbol} у режимі paper: {order_id}, trade_id={trade_id}”)
trade[order_id_key] = None
continue

try:
# Перевіряємо статус ордера перед скасуванням
order_status = await client.futures_get_order(symbol=symbol, orderId=order_id)
if order_status[“status”] in [“NEW”, “PARTIALLY_FILLED”]:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано {order_id_key} для {symbol}: order_id={order_id}, trade_id={trade_id}”)
else:
logging.info(
f”{order_id_key} для {symbol} вже виконано або скасовано: “
f”status={order_status[‘status’]}, order_id={order_id}, trade_id={trade_id}”
)
trade[order_id_key] = None
except BinanceAPIException as e:
logging.info(
f”{order_id_key} для {symbol} вже скасовано або виконано: “
f”order_id={order_id}, error={e}, trade_id={trade_id}”
)
trade[order_id_key] = None
except Exception as e:
logging.error(
f”Помилка при скасуванні {order_id_key} для {symbol}: {e}, “
f”order_id={order_id}, trade_id={trade_id}”
)
trade[order_id_key] = None
await asyncio.sleep(0.1)
except Exception as e:
logging.error(f”Помилка в cancel_all_related_orders для {symbol}: {e}, trade_id={trade_id}”)

async def cleanup_stale_orders():
“””
Періодична перевірка і скасування застарілих ордерів.
“””
await asyncio.sleep(5)
for symbol in config[“symbols”]:
try:
if symbol not in active_positions or not active_positions[symbol]:
logging.debug(f”Немає активних позицій для {symbol}, пропускаємо очищення ордерів”)
continue

open_orders = await client.futures_get_open_orders(symbol=symbol)
# Перевірка на валідність open_orders
if not isinstance(open_orders, list):
logging.error(f”Невалідний формат open_orders для {symbol}: {open_orders}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Невалідний формат відкритих ордерів: {open_orders}”
})
continue
if not open_orders:
logging.debug(f”Немає відкритих ордерів для {symbol}, пропускаємо очищення”)
continue

active_quantities = {trade[“trade_id”]: trade[“quantity”] for trade in active_positions[symbol] if “trade_id” in trade}
active_order_ids = {
trade.get(key)
for trade in active_positions[symbol]
for key in [“sl_order_id”, “tp_order_id”, “tp_3_order_id”]
if trade.get(key) and not str(trade.get(key)).startswith(“paper_”)
}

if not active_order_ids:
logging.debug(f”Ордери для {symbol} ще не створені, пропускаємо очищення”)
continue

async with realtime_prices_lock:
current_price = realtime_prices.get(symbol)
if not current_price:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
current_price = float(df[“close”].iloc[-1])
else:
current_price = last_prices.get(symbol, 0)
logging.warning(f”Використано резервну ціну {current_price} для {symbol}”)

for order in open_orders:
order_id = order.get(“orderId”)
order_qty = float(order.get(“origQty”, 0))
if not order_id or not order_qty:
logging.warning(f”Невалідний ордер для {symbol}: {order}”)
continue
trade_id = next(
(t[“trade_id”] for t in active_positions[symbol] if order_id in [t.get(“sl_order_id”), t.get(“tp_order_id”), t.get(“tp_3_order_id”)]),
“невідомий”
)
if not trade_id or order_id not in active_order_ids or (trade_id != “невідомий” and order_qty > active_quantities.get(trade_id, 0)):
try:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано застарілий ордер для {symbol}: order_id={order_id}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Скасовано застарілий ордер: order_id={order_id}, trade_id={trade_id}”
})
except Exception as e:
logging.warning(f”Не вдалося скасувати застарілий ордер для {symbol}: order_id={order_id}, error={e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка скасування ордера: order_id={order_id}, error={str(e)}”
})
except Exception as e:
logging.error(f”Помилка в cleanup_stale_orders для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка очищення ордерів: {str(e)}”
})

# Виправлена функція sync_positions
async def sync_positions(client, config, market, active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats):
try:
if market != “futures”:
return
await update_exchange_info(config[“market”])
total_balance = sum(stats[s][“balance”] for s in config[“symbols”]) if config[“mode”] == “real” else config[“initial_balance”]

seen_trade_ids = set()

# Швидка синхронізація всіх позицій
async with active_positions_lock:
await asyncio.sleep(0.5) # Затримка для стабільності
positions = await asyncio.wait_for(client.futures_position_information(), timeout=120) # Збільшено таймаут
logging.info(“DEBUG: Починаємо цикл по positions в sync_positions…”) # Додано: лог початку циклу
for pos in positions:
symbol = pos.get(‘symbol’)
if symbol not in config[“symbols”]:
continue
qty = float(pos.get(‘positionAmt’, 0))
entry_price = float(pos.get(‘entryPrice’, 0))
logging.info(f”DEBUG: Перевіряємо позицію для {symbol}: qty={qty}, entry_price={entry_price}”) # Додано: лог перед if qty == 0
if qty == 0:
continue
side = ‘BUY’ if qty > 0 else ‘SELL’
quantity = abs(qty)
update_time = pos.get(“updateTime”, int(time.time() * 1000))
order_id = pos.get(“orderId”, f”binance_{update_time}_{side}”)

# Перевірка, чи позиція вже існує
found = any(t.get(“order_id”) == order_id and t.get(“status”) == “open” for t in active_positions.get(symbol, []))
if found:
logging.debug(f”Позиція {order_id} для {symbol} уже існує в active_positions, пропускаємо”)
continue

logging.warning(f”Виявлено нову позицію на Binance для {symbol}: order_id={order_id}. Додаємо до active_positions”)
trade_id = f”{int(time.time())}_{random.randint(1000, 9999)}_{side}_{symbol}”
trade = create_trade_template(trade_id, symbol, side, entry_price, quantity, config[“leverage”][symbol])
if trade is None:
logging.error(f”Не вдалося створити trade для {symbol}, order_id={order_id}”)
continue
trade[“order_id”] = order_id
trade[“status”] = “open” # Явно встановлюємо статус

# — Початок безпечного блоку для обробки ATR/SL/TP і розміщення ордерів —
# 1) Спроба отримати кешовані дані, і fallback на історичні
df = await get_data_from_cache(symbol, “1m”)
if df is None or df.empty:
logging.warning(f”Немає кешованих даних 1m для {symbol}, спроба підвантажити історію (limit=20).”)
try:
df = await get_historical_data(symbol, “1m”, limit=20)
if df is None or df.empty:
logging.warning(f”Не вдалося підвантажити історію для {symbol}. Продовжуємо з default-config (без ATR).”)
except Exception as e:
logging.error(f”Помилка підвантаження історії для {symbol}: {e}”)
df = None

# 2) Обчислюємо ATR без падіння якщо не вийшло
atr_value = None
if df is not None and not df.empty:
try:
atr_series = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config.get(“atr_history_length”, 14))
atr_value = float(atr_series[-1]) if len(atr_series) > 0 else None
if pd.isna(atr_value) or atr_value is None or atr_value <= 0:
logging.warning(f”ATR некоректний для {symbol}: {atr_value}”)
atr_value = None
except Exception as e:
logging.error(f”Помилка обчислення ATR для {symbol}: {e}”)
atr_value = None
else:
logging.debug(f”DF відсутній або порожній для {symbol}, ATR не буде обчислено.”)

# 3) Отримуємо конфіг, якщо ATR вдалий — коригуємо, інакше використовуємо default_values (без аварій)
updated_config = None
try:
if atr_value is not None:
updated_config = await adjust_config_based_on_volatility(symbol, atr_value, entry_price)
if not updated_config or not isinstance(updated_config, dict):
logging.warning(f”adjust_config_based_on_volatility повернув невалідні дані для {symbol}. Використовуємо default_values.”)
updated_config = None
except Exception as e:
logging.error(f”Помилка adjust_config_based_on_volatility для {symbol}: {e}”)
updated_config = None

if updated_config is None:
# Використовуємо конфіг за замовчуванням, але без зміни ризику global config (не жорстка заглушка ATR)
updated_config = config.get(“default_values”, {})
if not isinstance(updated_config, dict):
updated_config = {}
logging.info(f”Використовується default_values для {symbol} при формуванні SL/TP.”)

# 4) Забезпечити наявність потрібних ключів та розумні значення
required_keys = [
“sl_multiplier_buy”, “sl_multiplier_sell”,
“tp_2_multiplier_buy”, “tp_2_multiplier_sell”,
“tp_3_multiplier_buy”, “tp_3_multiplier_sell”,
“trailing_stop_activation_buy”, “trailing_stop_activation_sell”,
“trailing_stop_distance”
]
for key in required_keys:
if key not in updated_config or not isinstance(updated_config[key], (int, float)) or updated_config[key] <= 0:
updated_config[key] = config.get(“default_values”, {}).get(key, 1.0)
logging.debug(f”Встановлено запасне значення для {key} для {symbol}: {updated_config[key]}”)

# 5) Обчислюємо SL/TP/TP3/Trailing значення (з захистом від винятків)
try:
trade[“sl”] = float(entry_price) * float(updated_config[f”sl_multiplier_{side.lower()}”])
trade[“tp”] = float(entry_price) * float(updated_config[f”tp_2_multiplier_{side.lower()}”])
trade[“tp_3″] = float(entry_price) * float(updated_config[f”tp_3_multiplier_{side.lower()}”])
trade[“trailing_stop_activation”] = float(entry_price) * float(updated_config[f”trailing_stop_activation_{side.lower()}”])
trade[“trailing_stop_distance”] = float(updated_config.get(“trailing_stop_distance”, 0))
trade[“trailing_stop”] = None
except Exception as e:
logging.error(f”Помилка обчислення SL/TP для {symbol}: {e}. Позиція буде збережена як partial без SL/TP.”)
trade[“sl”] = None
trade[“tp”] = None
trade[“tp_3”] = None
trade[“trailing_stop_activation”] = None
trade[“trailing_stop_distance”] = None

# 6) Округлення чисел та перевірка quantity
try:
trade[“sl”] = await round_price(symbol, trade[“sl”]) if trade[“sl”] is not None else None
trade[“tp”] = await round_price(symbol, trade[“tp”]) if trade[“tp”] is not None else None
trade[“tp_3”] = await round_price(symbol, trade[“tp_3”]) if trade[“tp_3”] is not None else None
trade[“trailing_stop_activation”] = await round_price(symbol, trade[“trailing_stop_activation”]) if trade[“trailing_stop_activation”] is not None else None
trade[“quantity”] = await round_quantity(symbol, trade[“quantity”])
except Exception as e:
logging.error(f”Помилка округлення/quantity для {symbol}: {e}. Помічено як partial.”)
# не кидаємо, просто помічаємо як partial

# 7) Спроба розмістити SL/TP — якщо не вдається, НЕ падаємо, зберігаємо позицію як partial
try:
success = await place_sl_tp_orders(trade, client_order_prefix=trade_id)
if success:
trade[“status”] = “open”
else:
trade[“status”] = “partial”
logging.warning(f”SL/TP не було повністю розміщено для {symbol}, trade_id={trade_id}. Статус=partial”)
except Exception as e:
logging.error(f”Помилка при розміщенні SL/TP для {symbol}, trade_id={trade_id}: {e}”)
trade[“status”] = “partial”

# 8) Додаємо позицію у active_positions і гарантовано зберігаємо
active_positions.setdefault(symbol, []).append(trade)
try:
await save_active_positions(active_positions)
logging.info(f”Збережено active_positions після додавання позиції для {symbol}, trade_id={trade_id}, status={trade[‘status’]}, qty={trade[‘quantity’]}”)
except Exception as e:
logging.error(f”Не вдалося зберегти active_positions після додавання позиції для {symbol}: {e}”)
# — Кінець безпечного блоку —

updated_config = await adjust_config_based_on_volatility(symbol, atr_value, entry_price)
if not updated_config or not isinstance(updated_config, dict):
logging.error(f”Невалідний updated_config для {symbol}, використовуємо default_values, trade_id={trade_id}”)
updated_config = config[“default_values”]
required_keys = [“sl_multiplier_buy”, “sl_multiplier_sell”, “tp_2_multiplier_buy”, “tp_2_multiplier_sell”, “tp_3_multiplier_buy”, “tp_3_multiplier_sell”, “trailing_stop_activation_buy”, “trailing_stop_activation_sell”, “trailing_stop_distance”]
for key in required_keys:
if key not in updated_config or not isinstance(updated_config[key], (int, float)) or updated_config[key] <= 0:
logging.warning(f”Невалідне значення для {key} у updated_config для {symbol}: {updated_config.get(key, ‘None’)}”)
updated_config[key] = config[“default_values”][key]
trade[“sl”] = entry_price * updated_config[f”sl_multiplier_{side.lower()}”]
trade[“tp”] = entry_price * updated_config[f”tp_2_multiplier_{side.lower()}”]
trade[“tp_3″] = entry_price * updated_config[f”tp_3_multiplier_{side.lower()}”]
trade[“trailing_stop_activation”] = entry_price * updated_config[f”trailing_stop_activation_{side.lower()}”]
trade[“trailing_stop_distance”] = updated_config[“trailing_stop_distance”]
trade[“trailing_stop”] = None

trade[“sl”] = await round_price(symbol, trade[“sl”])
trade[“tp”] = await round_price(symbol, trade[“tp”])
trade[“tp_3”] = await round_price(symbol, trade[“tp_3”])
trade[“trailing_stop_activation”] = await round_price(symbol, trade[“trailing_stop_activation”])

if not all(isinstance(x, (int, float)) and x > 0 for x in [trade[“sl”], trade[“tp”], trade[“tp_3”]]):
logging.error(f”Невалідні SL/TP при додаванні нової позиції для {symbol}: sl={trade[‘sl’]}, tp={trade[‘tp’]}, tp_3={trade[‘tp_3’]}, trade_id={trade_id}”)
continue

success = await place_sl_tp_orders(trade, client_order_prefix=trade_id)
if success:
trade[“status”] = “open”
else:
trade[“status”] = “partial” # Додано: Завжди додаємо, з partial якщо !success
logging.warning(f”Частковий успіх для нової позиції {symbol}, trade_id={trade_id}: SL/TP розміщено неповністю”)

active_positions.setdefault(symbol, []).append(trade)
logging.info(f”Додано нову позицію до active_positions: {symbol}, trade_id={trade_id}, status={trade[‘status’]}, qty={trade[‘quantity’]}”)

await save_active_positions(active_positions) # Збереження одразу після додавання (завжди)
logging.info(f”Збережено active_positions після додавання нової позиції для {symbol}”)

if trade[“status”] == “open”: # Stats/Telegram тільки для open
async with stats_lock:
stats[symbol][“total_trades”] += 1
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
await save_stats()
logging.info(f”Надсилання повідомлення про нову позицію для {symbol}, trade_id={trade_id}”)
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“entry_price”: entry_price,
“quantity”: quantity,
“leverage”: config[“leverage”][symbol],
“sl”: trade[“sl”],
“tp”: trade[“tp”],
“tp_3”: trade[“tp_3”],
“trade_id”: trade_id,
“status”: “open”,
“open_time”: trade[“open_time”]
})
logging.info(f”Додано нову позицію для {symbol}: trade_id={trade_id}, side={side}, qty={quantity}, entry_price={entry_price}”)
else:
logging.warning(f”Позиція {symbol}, trade_id={trade_id} збережена як partial (без оновлення stats/Telegram)”)

logging.info(“DEBUG: Завершено цикл по positions в sync_positions”) # Додано

# Перевірка позицій на Binance
for symbol in config[“symbols”]:
symbol_info = next((s for s in exchange_info_cache[“futures”][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.error(f”Не знайдено symbol_info для {symbol}”)
continue
min_qty = float(next(f[“minQty”] for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”))

pos_info = await client.futures_position_information(symbol=symbol)
api_positions = {}
for pos in pos_info:
position_amt = float(pos[“positionAmt”])
if position_amt != 0:
side = “BUY” if position_amt > 0 else “SELL”
entry_price = float(pos[“entryPrice”])
quantity = abs(position_amt)
update_time = pos.get(“updateTime”, int(time.time() * 1000))
order_id = pos.get(“orderId”, f”binance_{update_time}_{side}”)
api_positions[order_id] = pos
logging.info(f”Позиція на Binance для {symbol}: order_id={order_id}, side={side}, qty={quantity}, entry_price={entry_price}”)

open_orders = await client.futures_get_open_orders(symbol=symbol)
current_order_ids = {order[“orderId”] for order in open_orders if order[“type”] in [“STOP_MARKET”, “TAKE_PROFIT_MARKET”]}

async with active_positions_lock:
if not isinstance(active_positions.get(symbol, []), list):
logging.warning(f”Некоректна структура active_positions для {symbol}: {active_positions.get(symbol)}. Ініціалізуємо як список.”)
active_positions[symbol] = []

for trade in active_positions.get(symbol, [])[:]:
if not isinstance(trade, dict) or “status” not in trade or trade[“status”] not in [“open”, “closed”]:
logging.error(f”Невалідний trade для {symbol}: {trade}, trade_id={trade.get(‘trade_id’, ‘N/A’)}”)
active_positions[symbol].remove(trade)
await save_active_positions(active_positions)
logging.info(f”Видалено невалідний trade для {symbol}, trade_id={trade.get(‘trade_id’, ‘N/A’)}”)
continue

if trade[“status”] != “open”:
continue
trade_order_id = trade.get(“order_id”, “”)
trade_found = False
for api_order_id, pos in api_positions.items():
if trade_order_id == api_order_id or (
trade[“side”] == (“BUY” if float(pos[“positionAmt”]) > 0 else “SELL”) and
abs(trade[“quantity”] – abs(float(pos[“positionAmt”]))) < 1e-6 and
abs(trade[“entry_price”] – float(pos[“entryPrice”])) < 1e-2
):
trade_found = True
if trade.get(“order_id”) and not trade[“order_id”].startswith(“paper_”):
try:
order_details = await client.futures_get_order(symbol=symbol, orderId=trade[“order_id”])
if order_details[“status”] in [“CANCELED”, “REJECTED”, “EXPIRED”]:
logging.info(f”Ордер {trade[‘order_id’]} для {symbol} скасовано або відхилено, закриваємо позицію, trade_id={trade[‘trade_id’]}”)
trade_found = False
except Exception as e:
logging.error(f”Помилка перевірки статусу ордера {trade[‘order_id’]} для {symbol}: {e}”)
break
if not trade_found and not trade_order_id.startswith(“paper_”):
logging.info(f”Позиція order_id={trade_order_id} відсутня на Binance, закриваємо, trade_id={trade[‘trade_id’]}”)
async with realtime_prices_lock:
exit_price = realtime_prices.get(symbol)
if exit_price is None or time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
await update_prices_via_http([symbol])
exit_price = realtime_prices.get(symbol, last_prices.get(symbol, trade[“entry_price”]))
trade[“exit_price”] = exit_price
trade[“profit”] = (
(trade[“exit_price”] – trade[“entry_price”]) * trade[“quantity”] * trade[“leverage”]
if trade[“side”] == “BUY”
else (trade[“entry_price”] – trade[“exit_price”]) * trade[“quantity”] * trade[“leverage”]
)
trade[“status”] = “closed”
trade[“close_time”] = datetime.now().isoformat()
trade = await finalize_trade(trade)
active_positions[symbol].remove(trade)
await save_active_positions(active_positions)
async with stats_lock:
stats[symbol][“total_trades”] += 1
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
if trade[“profit”] > 0:
stats[symbol][“winning_trades”] += 1
else:
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“total_profit”] += trade[“profit”]
stats[symbol][“win_rate”] = stats[symbol][“winning_trades”] / stats[symbol][“total_trades”] if stats[symbol][“total_trades”] > 0 else 0.0
await save_stats()
logging.info(f”Надсилання повідомлення про закриття позиції для {symbol}, trade_id={trade[‘trade_id’]}”)
await send_telegram_notification(trade)
continue

if trade[“quantity”] < min_qty:
logging.info(f”Закриття позиції {symbol}: trade_id={trade[‘trade_id’]}, qty={trade[‘quantity’]}, min_qty={min_qty}”)
await close_binance_position(symbol, trade[“side”], trade[“quantity”])
trade[“status”] = “closed”
async with realtime_prices_lock:
exit_price = realtime_prices.get(symbol)
if exit_price is None or time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
await update_prices_via_http([symbol])
exit_price = realtime_prices.get(symbol, last_prices.get(symbol, trade[“entry_price”]))
trade[“exit_price”] = exit_price
trade[“profit”] = (
(trade[“exit_price”] – trade[“entry_price”]) * trade[“quantity”] * trade[“leverage”]
if trade[“side”] == “BUY”
else (trade[“entry_price”] – trade[“exit_price”]) * trade[“quantity”] * trade[“leverage”]
)
trade[“close_time”] = datetime.now().isoformat()
trade = await finalize_trade(trade)
active_positions[symbol].remove(trade)
await save_active_positions(active_positions)
async with stats_lock:
stats[symbol][“total_trades”] += 1
stats[symbol][“balance”] += trade[“profit”]
stats[symbol][“balance_history”].append((datetime.now().isoformat(), stats[symbol][“balance”]))
if trade[“profit”] > 0:
stats[symbol][“winning_trades”] += 1
else:
stats[symbol][“losing_trades”] += 1
stats[symbol][“loss_streak”] += 1
stats[symbol][“last_loss_time”] = int(time.time())
stats[symbol][“total_profit”] += trade[“profit”]
stats[symbol][“win_rate”] = stats[symbol][“winning_trades”] / stats[symbol][“total_trades”] if stats[symbol][“total_trades”] > 0 else 0.0
await save_stats()
logging.info(f”Надсилання повідомлення про закриття позиції за мінімальною кількістю для {symbol}, trade_id={trade[‘trade_id’]}”)
await send_telegram_notification(trade)
continue

total_exposure = sum(
trade[“quantity”] * trade[“entry_price”] * trade[“leverage”]
for symbol in active_positions
for trade in active_positions[symbol]
if trade[“status”] == “open” and trade[“trade_id”] not in seen_trade_ids
and seen_trade_ids.add(trade[“trade_id”])
)

if total_exposure > total_balance * config[“total_exposure_limit”]:
logging.warning(f”Перевищено ліміт експозиції: {total_exposure} > {total_balance * config[‘total_exposure_limit’]}”)
bot_state[“paused”] = True
return

if bot_state.get(“paused”, False):
open_positions_exist = any(
any(t[“status”] == “open” for t in active_positions.get(sym, []))
for sym in config[“symbols”]
)
if not open_positions_exist:
bot_state[“paused”] = False
bot_state[“paused_due_to_losses”].clear()
bot_state[“paused_due_to_losses_time”].clear()
logging.info(“Всі позиції закриті, знято паузу (глобальну та через збитки).”)

await save_active_positions(active_positions)
logging.debug(f”Стан active_positions після синхронізації: {active_positions}”)
open_count = sum(len([p for p in pos_list if p.get(“status”) == “open”]) for pos_list in active_positions.values())
logging.info(f”Поточна кількість відкритих позицій: {open_count}”)
except BinanceAPIException as e:
logging.error(f”Помилка API в sync_positions: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “SYSTEM”,
“side”: “N/A”,
“message”: f”Помилка синхронізації позицій: {str(e)}”,
“trade_id”: None,
“status”: “open”,
“open_time”: datetime.now()
})
except Exception as e:
logging.error(f”Загальна помилка в sync_positions: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “SYSTEM”,
“side”: “N/A”,
“message”: f”Загальна помилка синхронізації: {str(e)}”,
“trade_id”: None,
“status”: “open”,
“open_time”: datetime.now()
})

async def clean_duplicate_positions_for_symbol(symbol):
if symbol not in active_positions:
return
seen_trade_ids = set()
unique_positions = []
duplicates_found = False
for trade in active_positions[symbol][:]:
trade_id = trade[“trade_id”]
if trade_id not in seen_trade_ids:
seen_trade_ids.add(trade_id)
unique_positions.append(trade)
else:
duplicates_found = True
logging.warning(f”Видалено дублікат позиції для {symbol}: trade_id={trade_id}”)
active_positions[symbol] = unique_positions
if duplicates_found:
logging.info(f”Очищено дублікати для {symbol}”)
else:
logging.debug(f”Дублікати для {symbol} не знайдені”)
await save_active_positions(active_positions)

# Виправлена функція resync_sl_tp_for_active_positions
async def resync_sl_tp_for_active_positions(client, market, config, active_positions, exchange_info_cache, realtime_prices):
semaphore = asyncio.Semaphore(3)
async def process_symbol(symbol):
async with semaphore:
try:
if market != “futures”:
return
await update_exchange_info(config[“market”])
symbol_info = next((s for s in exchange_info_cache[“futures”][“symbols”] if s[“symbol”] == symbol), None)
if not symbol_info:
logging.error(f”Не знайдено symbol_info для {symbol}”)
return
min_qty = float(next(f[“minQty”] for f in symbol_info[“filters”] if f[“filterType”] == “LOT_SIZE”))
tick_size = float(next(f[“tickSize”] for f in symbol_info[“filters”] if f[“filterType”] == “PRICE_FILTER”))

pos_info = await client.futures_position_information(symbol=symbol)
api_positions = {
f”{p[‘symbol’]}_{‘BUY’ if float(p[‘positionAmt’]) > 0 else ‘SELL’}_{float(p[‘entryPrice’]):.8f}_{abs(float(p[‘positionAmt’])):.8f}”: p
for p in pos_info if float(p[“positionAmt”]) != 0
}

open_orders = await client.futures_get_open_orders(symbol=symbol)
current_order_ids = {order[“orderId”] for order in open_orders if order[“type”] in [“STOP_MARKET”, “TAKE_PROFIT_MARKET”]}

# Скасування сторонніх ордерів (тільки тих, що не пов’язані з active_positions)
for order in open_orders:
if order[“type”] not in [“STOP_MARKET”, “TAKE_PROFIT_MARKET”]:
continue
order_id = order[“orderId”]
order_details = await client.futures_get_order(symbol=symbol, orderId=order_id)
if order_details[“status”] not in [“NEW”, “PARTIALLY_FILLED”] or order_id not in current_order_ids:
continue # Пропускаємо, якщо ордер виконаний або не в списку
is_related = any(
order_id in [trade.get(“sl_order_id”), trade.get(“tp_order_id”), trade.get(“tp_3_order_id”)]
for trade in active_positions.get(symbol, [])
if trade.get(“status”) == “open”
)
if not is_related:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано сторонній ордер для {symbol}: orderId={order_id}”)

trades_to_remove = []
for trade in active_positions[symbol][:]:
if not isinstance(trade, dict) or “trade_id” not in trade:
logging.error(f”Некоректна позиція для {symbol}: {trade}”)
trades_to_remove.append(trade)
continue
if trade[“status”] != “open”:
trades_to_remove.append(trade)
continue

# Ініціалізація tp1_triggered
if “tp1_triggered” not in trade:
trade[“tp1_triggered”] = False

# Перевірка валідності entry_price
if not isinstance(trade[“entry_price”], (int, float)) or trade[“entry_price”] <= 0:
logging.error(f”Невалідний entry_price для {symbol}: {trade[‘entry_price’]}, trade_id={trade[‘trade_id’]}”)
trades_to_remove.append(trade)
continue

trade_key = f”{symbol}_{trade[‘side’]}_{trade[‘entry_price’]:.8f}_{trade[‘quantity’]:.8f}”
pos = api_positions.get(trade_key)
if not pos:
logging.info(f”Позиція {trade_key} відсутня на Binance, закриваємо, trade_id={trade[‘trade_id’]}”)
trade[“status”] = “closed”
# Отримання exit_price з резервними джерелами
async with asyncio.Lock(): # Замінюємо realtime_prices_lock, якщо його немає
exit_price = realtime_prices.get(symbol)
if exit_price is None:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
exit_price = float(df[“close”].iloc[-1])
else:
exit_price = 0 # Резервне значення
logging.warning(f”Немає даних для exit_price для {symbol}”)
trade[“exit_price”] = exit_price
trade[“profit”] = (
(trade[“exit_price”] – trade[“entry_price”]) * trade[“quantity”] * trade[“leverage”]
if trade[“side”] == “BUY”
else (trade[“entry_price”] – trade[“exit_price”]) * trade[“quantity”] * trade[“leverage”]
)
await cancel_all_related_orders(trade)
save_trade(trade)
await send_telegram_notification(trade)
trades_to_remove.append(trade)
continue

# Порівняння entry_price з API з допустимою похибкою
api_entry_price = float(pos[“entryPrice”])
if abs(trade[“entry_price”] – api_entry_price) / api_entry_price > 0.01: # Похибка 1%
logging.warning(f”Розбіжність entry_price для {symbol}: trade={trade[‘entry_price’]}, api={api_entry_price}, trade_id={trade[‘trade_id’]}”)
trade[“entry_price”] = api_entry_price # Оновлюємо до значення з API

if trade[“quantity”] < min_qty:
logging.info(f”Закриття позиції {symbol}: trade_id={trade[‘trade_id’]}, qty={trade[‘quantity’]}, min_qty={min_qty}”)
await close_binance_position(symbol, trade[“side”], trade[“quantity”])
trade[“status”] = “closed”
# Отримання exit_price з резервними джерелами
async with asyncio.Lock(): # Замінюємо realtime_prices_lock, якщо його немає
exit_price = realtime_prices.get(symbol)
if exit_price is None:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
exit_price = float(df[“close”].iloc[-1])
else:
exit_price = 0 # Резервне значення
logging.warning(f”Немає даних для exit_price для {symbol}”)
trade[“exit_price”] = exit_price
trade[“profit”] = (
(trade[“exit_price”] – trade[“entry_price”]) * trade[“quantity”] * trade[“leverage”]
if trade[“side”] == “BUY”
else (trade[“entry_price”] – trade[“exit_price”]) * trade[“quantity”] * trade[“leverage”]
)
save_trade(trade)
await send_telegram_notification(trade)
trades_to_remove.append(trade)
continue

# Перевіряємо, чи потрібне розміщення ордерів
missing_orders = []
if not trade.get(“sl_order_id”) or trade[“sl_order_id”] not in current_order_ids:
missing_orders.append(“sl_order_id”)
if not trade[“tp1_triggered”] and (not trade.get(“tp_order_id”) or trade[“tp_order_id”] not in current_order_ids):
missing_orders.append(“tp_order_id”)
if not trade.get(“tp_3_order_id”) or trade[“tp_3_order_id”] not in current_order_ids:
missing_orders.append(“tp_3_order_id”)

if missing_orders:
logging.info(f”Розміщуємо/оновлюємо {missing_orders} для {symbol}, trade_id={trade[‘trade_id’]}”)
df = await get_data_from_cache(symbol, “1m”)
if df is None or df.empty:
logging.warning(f”Немає даних для {symbol} при ресинхронізації SL/TP”)
continue
atr_value = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_history_length”])[-1]
updated_config = await adjust_config_based_on_volatility(symbol, atr_value, trade[“entry_price”])

default_values = {
“sl_multiplier_buy”: 0.98,
“sl_multiplier_sell”: 1.02,
“tp_2_multiplier_buy”: 1.03,
“tp_2_multiplier_sell”: 0.97,
“tp_3_multiplier_buy”: 1.05,
“tp_3_multiplier_sell”: 0.95,
“trailing_stop_activation_buy”: 1.01,
“trailing_stop_activation_sell”: 0.99,
“trailing_stop_distance”: 0.02
}
if not updated_config or not all(key in updated_config for key in default_values):
logging.warning(f”Неповний updated_config для {symbol}, використовуємо резервні значення”)
updated_config = default_values.copy()

trade[“sl”] = trade[“entry_price”] * updated_config[f”sl_multiplier_{trade[‘side’].lower()}”]
trade[“tp”] = trade[“entry_price”] * updated_config[f”tp_2_multiplier_{trade[‘side’].lower()}”]
trade[“tp_3”] = trade[“entry_price”] * updated_config[f”tp_3_multiplier_{trade[‘side’].lower()}”]
trade[“trailing_stop_activation”] = trade[“entry_price”] * updated_config[f”trailing_stop_activation_{trade[‘side’].lower()}”]
trade[“trailing_stop_distance”] = updated_config[“trailing_stop_distance”]
trade[“trailing_stop”] = None

trade[“sl”] = await round_price(symbol, trade[“sl”])
trade[“tp”] = await round_price(symbol, trade[“tp”])
trade[“tp_3”] = await round_price(symbol, trade[“tp_3”])
trade[“trailing_stop_activation”] = await round_price(symbol, trade[“trailing_stop_activation”])

if not all(isinstance(x, (int, float)) and x > 0 for x in [trade[“sl”], trade[“tp”], trade[“tp_3”], trade[“trailing_stop_activation”]]):
logging.error(f”Невалідні SL/TP після округлення для {symbol}: sl={trade[‘sl’]}, tp={trade[‘tp’]}, tp_3={trade[‘tp_3’]}, trade_id={trade[‘trade_id’]}”)
trades_to_remove.append(trade)
continue

# Скасовуємо лише відсутні ордери з перевіркою статусу
for order_key in missing_orders:
order_id = trade.get(order_key)
if order_id and order_id in current_order_ids:
order_details = await client.futures_get_order(symbol=symbol, orderId=order_id)
if order_details[“status”] in [“NEW”, “PARTIALLY_FILLED”]:
await client.futures_cancel_order(symbol=symbol, orderId=order_id)
logging.info(f”Скасовано старий {order_key} для {symbol}: orderId={order_id}, trade_id={trade[‘trade_id’]}”)
trade[order_key] = None

success = await place_sl_tp_orders(trade, client_order_prefix=trade[“trade_id”])
if not success:
logging.error(f”Не вдалося розмістити SL/TP/TP3 для {symbol}, trade_id={trade[‘trade_id’]}”)
continue

trade[“last_sl_tp_placed”] = datetime.now().strftime(“%Y-%m-%dT%H:%M:%S.%f”)
await save_active_positions(active_positions) # Зберігаємо стан
logging.info(f”Оновлено SL/TP/TP3 для {symbol}: trade_id={trade[‘trade_id’]}, sl={trade[‘sl’]}, tp={trade[‘tp’]}, tp_3={trade[‘tp_3’]}”)

# Видаляємо позиції, позначені як закриті
for trade in trades_to_remove:
if trade in active_positions[symbol]:
active_positions[symbol].remove(trade)
logging.info(f”Видалено закриту позицію з active_positions для {symbol}: trade_id={trade.get(‘trade_id’, ‘невідомий’)}”)

except BinanceAPIException as e:
logging.error(f”Помилка API для {symbol} у resync_sl_tp: {e}”)
except Exception as e:
logging.error(f”Помилка для {symbol} у resync_sl_tp: {e}”)

try:
tasks = [process_symbol(symbol) for symbol in config[“symbols”]]
await asyncio.gather(*tasks)
await save_active_positions(active_positions)
except Exception as e:
logging.error(f”Помилка в resync_sl_tp_for_active_positions: {e}”)

def save_trade(trade):
try:
trade_data = trade.copy()
trade_data[“timestamp”] = datetime.now().isoformat()
for key in [“open_time”, “close_time”, “last_sl_tp_placed”]:
if key in trade_data and isinstance(trade_data[key], datetime):
trade_data[key] = trade_data[key].isoformat()
with open(f”trades_{trade[‘symbol’]}.json”, “a”) as f:
json.dump(trade_data, f, default=json_serializable)
f.write(“n”)
logging.info(f”Збережено трейд: {trade[‘trade_id’]}”)
except Exception as e:
logging.error(f”Помилка збереження трейду: {e}”)

# Ініціалізація локу для active_positions
stats_lock = asyncio.Lock()
active_positions_lock = asyncio.Lock()

# Виправлена функція save_active_positions
async def save_active_positions(active_positions):
“””
Зберігає активні позиції у active_positions.json з фільтрацією дублікатів та закритих.
Виправлено: Безпечна серіалізація, детальне логування, traceback в except.
“””
global active_positions_lock # Якщо lock глобальний, як у коді
async with active_positions_lock:
try:
logging.info(“TEST: Початок збереження active_positions…”) # Тестовий лог для діагностики
unique_positions = {}
skipped_count = 0
for symbol, trades in active_positions.items():
if not trades: # Пропустити порожні
continue
unique_positions[symbol] = []
seen_trade_ids = set()
for trade in trades:
if not isinstance(trade, dict):
logging.warning(f”Пропущено не-dict позицію для {symbol}: {type(trade)}”)
skipped_count += 1
continue
trade_id = trade.get(‘trade_id’)
if trade_id in seen_trade_ids:
logging.warning(f”Пропущено дублікат trade_id={trade_id} для {symbol}”)
skipped_count += 1
continue
status = trade.get(‘status’)
if status not in [‘open’, ‘partial’]:
logging.info(f”Пропущено закриту/partial позицію для {symbol}: trade_id={trade_id}, status={status}”)
skipped_count += 1
continue
unique_positions[symbol].append(trade)
seen_trade_ids.add(trade_id)
logging.debug(f”Збережено угоду для {symbol}: trade_id={trade_id}”)

logging.info(f”Підготовлено для збереження active_positions: символи={list(unique_positions.keys())}, “
f”всього позицій={sum(len(v) for v in unique_positions.values())}, пропущено={skipped_count}”)

# Виправлено: DEBUG зразок для перевірки (перша позиція кожного символу)
if unique_positions:
sample = {k: [v[0] if v else {} for v in list(unique_positions.values())[:1]]
for k in list(unique_positions.keys())[:1]}
logging.debug(f”Зразок unique_positions: {json.dumps(sample, default=json_serializable, indent=2)}”)

with open(“active_positions.json”, “w”, encoding=”utf-8″) as f:
json.dump(unique_positions, f, default=json_serializable, indent=4, ensure_ascii=False)

logging.info(f”TEST: Успішно збережено active_positions.json: {len(unique_positions)} символів, “
f”{sum(len(v) for v in unique_positions.values())} відкритих позицій”)

except Exception as e:
logging.error(f”Помилка збереження active_positions: {e}”)
logging.error(f”Повний traceback: {traceback.format_exc()}”)
try:
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка збереження active_positions: {str(e)}nTraceback: {traceback.format_exc()}”,
“trade_id”: None
})
except Exception as notify_exc:
logging.error(f”Помилка в send_telegram_notification: {notify_exc}”)

def load_active_positions(config):
try:
with open(“active_positions.json”, “r”) as f:
data = json.load(f)
# Конвертуємо рядки ISO назад у datetime, якщо потрібно
for symbol in data:
for trade in data[symbol]:
for key in [“open_time”, “close_time”, “last_sl_tp_placed”]:
if key in trade and isinstance(trade[key], str):
try:
trade[key] = datetime.fromisoformat(trade[key])
except ValueError:
trade[key] = None
# Фільтруємо тільки відкриті позиції
data[symbol] = [p for p in data[symbol] if str(p.get(“status”, “”)).lower() in [“open”, “partial”]]
logging.info(f”Завантажено active_positions: {data}”)
return data
except FileNotFoundError:
logging.warning(“active_positions.json не знайдено. Повертаємо порожній словник.”)
return {symbol: [] for symbol in config[“symbols”]}
except Exception as e:
logging.error(f”Помилка завантаження active_positions: {e}”, exc_info=True)
return {symbol: [] for symbol in config[“symbols”]}

def safe_format(value, fmt=’.2f’, default=0.0):
“””
Безпечне форматування значення, обробляє None або нечислові значення.
“””
try:
val = float(value) if value is not None else default
return f”{val:{fmt}}”
except (ValueError, TypeError):
return f”{default:{fmt}}”

async def send_telegram_notification(trade):
“””
Надсилає повідомлення в Telegram.
“””
try:
token = config.get(“telegram_token”, “”)
chat_id = config.get(“telegram_chat_id”, “”)
if not token or not chat_id:
logging.error(“Відсутні налаштування Telegram у config”)
return

# Перевірка на ключові поля з дефолтними значеннями
symbol = trade.get(“symbol”, “UNKNOWN”)
side = trade.get(“side”, “N/A”)
trade_id = trade.get(“trade_id”, “N/A”)
status = trade.get(“status”, “N/A”)
open_time = trade.get(“open_time”, datetime.now())
duration = datetime.now() – open_time
entry_price = trade.get(“entry_price”, 0)
logging.debug(f”Формування повідомлення для Telegram: symbol={symbol}, trade_id={trade_id}, status={status}”)

# Формування повідомлення залежно від типу
if symbol in [“SYSTEM”, “ALL”] or ‘message’ in trade: # Розширено для ALL/таймаутів
msg = (
f”⚠️ <b>Системне повідомлення</b>n”
f”🔸 Тип: <b>{side}</b>n”
f”📊 {trade.get(‘message’, ‘N/A’)}n” # Додано для кастомних повідомлень
f”📊 Баланс: <b>{safe_format(stats.get(‘BTCUSDT’, {‘balance’: 0})[‘balance’])}$</b>n”
)
elif status == “open”:
# Повідомлення для відкритої угоди
msg = (
f”🟢 <b>Угода відкрита</b>n”
f”🔸 Символ: <b>{symbol}</b>n”
f”📈 Тип: <b>{side}</b>n”
f”🚪 Вхід: <b>{safe_format(entry_price)}</b>n”
f”📏 Кількість: <b>{safe_format(trade.get(‘quantity’, 0), ‘.6f’)}</b>n”
f”🔧 Леверидж: <b>{safe_format(trade.get(‘leverage’, 1))}</b>n”
f”🛑 SL: <b>{safe_format(trade.get(‘sl’, 0))}</b>n”
f”🎯 TP: <b>{safe_format(trade.get(‘tp’, 0))}</b>n”
f”🎯 TP3: <b>{safe_format(trade.get(‘tp_3’, 0))}</b>n”
f”📊 Баланс: <b>{safe_format(stats.get(symbol, {‘balance’: 0})[‘balance’])}$</b>n”
f”⏱ Тривалість: <b>{duration}</b>n”
)
else:
# Повідомлення для закритої угоди
exit_price = trade.get(“exit_price”)
if exit_price is None:
logging.warning(f”Відсутня exit_price для закритої позиції {symbol}, trade_id={trade_id}”)
async with realtime_prices_lock:
exit_price = realtime_prices.get(symbol, entry_price)
trade[“exit_price”] = exit_price

# Обчислення прибутку
profit = trade.get(“profit”)
if profit is None and side != “N/A”:
profit = (
(exit_price – entry_price) * trade.get(“quantity”, 0) * trade.get(“leverage”, 1)
if side == “BUY”
else (entry_price – exit_price) * trade.get(“quantity”, 0) * trade.get(“leverage”, 1)
)
trade[“profit”] = profit

denominator = entry_price * trade.get(“quantity”, 0)
profit_pct = (profit / denominator * 100) if denominator > 0 else 0
msg = (
f”📊 <b>Угода закрита</b>n”
f”🔸 Символ: <b>{symbol}</b>n”
f”📈 Тип: <b>{side}</b>n”
f”🚪 Вхід: <b>{safe_format(entry_price)}</b>n”
f”🏁 Вихід: <b>{safe_format(exit_price)}</b>n”
f”💰 Прибуток: <b>{safe_format(profit)}$</b> ({safe_format(profit_pct)}%)n”
f”📊 Баланс: <b>{safe_format(stats.get(symbol, {‘balance’: 0})[‘balance’])}$</b>n”
f”⏱ Тривалість: <b>{duration}</b>n”
)

url = f”https://api.telegram.org/bot{token}/sendMessage”
payload = {“chat_id”: chat_id, “text”: msg, “parse_mode”: “HTML”}
logging.info(f”Надсилання повідомлення в Telegram для {symbol}, trade_id={trade_id}: {msg}”)
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as resp:
if resp.status != 200:
logging.warning(f”Telegram повідомлення не надіслано: {resp.status}, {await resp.text()}”)
else:
logging.info(f”Успішно надіслано повідомлення в Telegram для {symbol}, trade_id={trade_id}”)
except Exception as e:
logging.error(f”Помилка відправки Telegram повідомлення для {symbol}, trade_id={trade_id}: {e}”, exc_info=True)

async def send_periodic_report():
while True:
try:
async with active_positions_lock:
open_positions = {symbol: [t for t in positions if t[“status”] == “open”]
for symbol, positions in active_positions.items()}
msg = f”📊 <b>Періодичний звіт</b>n”
msg += f”⏰ Час: {datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)}n”
msg += f”💰 Загальний баланс: {sum(stats[s][‘balance’] for s in config[‘symbols’]):.2f}$n”
msg += f”📈 Відкриті позиції:n”
for symbol, positions in open_positions.items():
for trade in positions:
msg += (f”🔸 {symbol} ({trade[‘side’]}): Ціна входу={trade[‘entry_price’]:.2f}, “
f”Кількість={trade[‘quantity’]:.6f}, SL={trade[‘sl’]:.2f}, TP={trade[‘tp’]:.2f}n”)
if not any(open_positions.values()):
msg += “🔹 Немає відкритих позиційn”
logging.info(f”Надсилання періодичного звіту в Telegram”)
await send_telegram_notification({
“symbol”: “SYSTEM”,
“side”: “N/A”,
“message”: msg,
“trade_id”: None,
“status”: “open”
})
await asyncio.sleep(3600) # Щогодини
except Exception as e:
logging.error(f”Помилка періодичного звіту: {e}”)
await asyncio.sleep(60)

def load_last_trade(symbol):
try:
trades = []
with open(f”trades_{symbol}.json”, “r”, encoding=”utf-8″) as f:
for line in f:
trade = json.loads(line)
if trade[“status”] == “open”:
trade[“open_time”] = datetime.datetime.fromisoformat(trade[“open_time”])
trades.append(trade)
return trades
except FileNotFoundError:
return []
except Exception as e:
logging.error(f”Помилка завантаження угод для {symbol}: {e}”)
return []

# Виправлена функція process_symbol
async def process_symbol(symbol, bot_state, config, stats, historical_datasets, last_feature_analysis_time, knn_selected_features, active_positions):
async with active_positions_lock:
try:
logging.info(f”Початок обробки {symbol}”)
if bot_state[“paused”]:
logging.info(f”⏸ Глобальна пауза активована, пропускаємо обробку {symbol}”)
return

if symbol in bot_state.get(“paused_symbols”, set()):
pause_duration = time.time() – bot_state.get(“paused_due_to_losses_time”, {}).get(symbol, 0)
pause_timeout = config.get(“paused_symbols_timeout”, 3600)
if pause_duration > pause_timeout:
bot_state[“paused_symbols”].remove(symbol)
bot_state[“paused_due_to_losses_time”].pop(symbol, None)
logging.info(f”✅ Торгівля для {symbol} відновлена після паузи через помилки моделей”)
else:
logging.info(f”Торгівля для {symbol} призупинена через попередні помилки моделей (залишилося {(pause_timeout – pause_duration):.0f} сек)”)
return

reset_interval = config.get(“loss_streak_reset_interval”, 3600)
current_time = time.time()
last_loss_time = stats[symbol].get(“last_loss_time”, 0)
if last_loss_time > 0 and (current_time – last_loss_time) >= reset_interval:
stats[symbol][“loss_streak”] = 0
stats[symbol][“last_loss_time”] = 0
logging.info(f”Скинуто loss_streak для {symbol} через {reset_interval/60:.0f} хвилин після останньої збиткової угоди”)
await save_stats()

if symbol in bot_state.get(“paused_due_to_losses”, set()):
pause_duration = time.time() – bot_state[“paused_due_to_losses_time”].get(symbol, 0)
pause_timeout = config.get(“pause_due_to_losses_timeout”, 7200)
if pause_duration > pause_timeout:
bot_state[“paused_due_to_losses”].remove(symbol)
bot_state[“paused_due_to_losses_time”].pop(symbol, None)
logging.info(f”✅ Торгівля для {symbol} відновлена після паузи тривалістю {pause_duration:.0f} сек”)
elif stats[symbol][“loss_streak”] == 0 and stats[symbol][“win_rate”] >= config[“min_win_rate”]:
bot_state[“paused_due_to_losses”].remove(symbol)
bot_state[“paused_due_to_losses_time”].pop(symbol, None)
logging.info(f”✅ Торгівля для {symbol} відновлена: loss_streak={stats[symbol][‘loss_streak’]}, win_rate={stats[symbol][‘win_rate’]:.2f}”)
else:
logging.info(f”Торгівля для {symbol} залишається призупиненою (залишилося {(pause_timeout – pause_duration):.0f} сек до таймауту)”)
return

# Виправлено: додано stats[symbol][“total_trades”] > 5
if stats[symbol][“total_trades”] > 5 and (
stats[symbol][“loss_streak”] >= config[“max_loss_streak”] or
stats[symbol][“win_rate”] < config[“min_win_rate”]
):
bot_state[“paused_due_to_losses”].add(symbol)
bot_state[“paused_due_to_losses_time”][symbol] = time.time()
logging.warning(f”⏸ Торгівля призупинена для {symbol} через серію збитків ({stats[symbol][‘loss_streak’]}) або низький win_rate ({stats[symbol][‘win_rate’]:.2f})”)
await save_stats()
return

if all(historical_datasets[symbol][interval] is None for interval in [“1m”, “5m”, “15m”]):
historical_data_dict = await collect_historical_data_with_indicators(symbol)
historical_datasets[symbol] = historical_data_dict or {}
logging.info(f”Завантажено історичні дані для {symbol}”)

if current_time – last_feature_analysis_time[symbol] >= config[“knn_feature_analysis_interval”]:
for interval in [“1m”, “5m”, “15m”]:
df = historical_datasets[symbol][interval]
if df is not None and not df.empty:
top_features = await analyze_feature_importance(symbol, interval, df)
knn_selected_features[symbol][interval] = top_features
last_feature_analysis_time[symbol] = current_time
logging.info(f”Оновлено KNN фічі для {symbol}”)

df = await get_data_from_cache(symbol, “1m”)
if df is None or df.empty:
logging.warning(f”Немає даних для {symbol} у кеші”)
return

signal, atr = await analyze_market(df, symbol)
logging.info(f”Сигнал для {symbol}: {signal}, ATR={atr}”)
if signal is None or signal == “NO_TRADE”:
return

last_trade_close_time = max(
[t.get(“close_time”, 0) for t in active_positions.get(symbol, []) if t.get(“close_time”) is not None],
default=0
)
if (current_time – last_trade_close_time) < 10:
logging.info(f”Затримка відкриття нової позиції для {symbol}: не минуло 10 секунд після закриття останньої угоди”)
return

open_positions = len([t for t in active_positions.get(symbol, []) if t[“status”] == “open”])
if open_positions >= config[“max_open_positions”]:
logging.info(f”Досягнуто ліміт відкритих позицій ({open_positions}/{config[‘max_open_positions’]}) для {symbol}, пропускаємо”)
return

existing_trades = {t[“trade_id”] for t in active_positions.get(symbol, [])}
has_active_position = False
positions = await client.futures_position_information(symbol=symbol)
for pos in positions:
if float(pos[“positionAmt”]) != 0:
side_on_binance = “BUY” if float(pos[“positionAmt”]) > 0 else “SELL”
if side_on_binance == signal:
has_active_position = True
break
if signal != “NO_TRADE” and not has_active_position:
trade_id = f”{int(time.time())}_{random.randint(1000, 9999)}_{signal}_{symbol}”
if trade_id in existing_trades:
logging.info(f”Угода {trade_id} вже існує для {symbol}, пропускаємо”)
return
trade = await open_trade(signal, df, symbol, atr)
if trade and isinstance(trade, dict):
trade[“status”] = “open”
trade[“trade_id”] = trade_id
active_positions.setdefault(symbol, []).append(trade)
await save_active_positions(active_positions)
await sync_positions(client, config, market, active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats)
logging.info(f”Додано нову угоду для {symbol}, trade_id={trade_id}”)

for trade in active_positions.get(symbol, [])[:]:
if trade[“status”] == “open”:
updated = await manage_trade(trade, df)
if updated and updated[“status”] == “closed”:
active_positions[symbol].remove(trade)
await save_active_positions(active_positions)

logging.info(f”📊 [{symbol}] Баланс={stats[symbol][‘balance’]:.2f}, WinRate={stats[symbol][‘win_rate’]:.2f}”)
except Exception as e:
logging.error(f”process_symbol помилка для {symbol}: {e}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка в process_symbol: {str(e)}”,
“trade_id”: None
})

def update_model_weights():
for symbol in config[“symbols”]:
model_mae = {}
inv_mae = {}
for model in model_errors[symbol]:
errors = model_errors[symbol][model]
if errors:
mae = mean(errors)
model_mae[model] = mae
inv_mae[model] = 1 / max(mae, 1e-6)
total_inv = sum(inv_mae.values())
if total_inv > 0:
for model in inv_mae:
MODEL_WEIGHTS[symbol][model] = inv_mae[model] / total_inv
logging.info(f”🔁 Оновлені ваги для {symbol}: {MODEL_WEIGHTS[symbol]}”)
else:
logging.info(f”Не оновлено ваги для {symbol}: недостатньо даних”)
model_errors[symbol] = {m: [] for m in model_errors[symbol]}

def initialize_model_weights():
for symbol in config[“symbols”]:
historical_preds = prediction_history[symbol]
if not historical_preds or len(historical_preds) < 10:
continue
model_mae = {“lstm”: [], “transformer”: [], “xgboost”: [], “knn”: []}
for pred in historical_preds[-10:]:
actual = pred[“actual_price”]
for model in last_model_predictions[symbol]:
if model in pred:
mae = abs(pred[“predicted_price”] – actual)
model_mae[model].append(mae)
inv_mae = {}
for model, errors in model_mae.items():
if errors:
inv_mae[model] = 1 / max(mean(errors), 1e-6)
total_inv = sum(inv_mae.values())
if total_inv > 0:
for model in inv_mae:
MODEL_WEIGHTS[symbol][model] = inv_mae[model] / total_inv
logging.info(f”Ініціалізовані ваги для {symbol}: {MODEL_WEIGHTS[symbol]}”)

def retrain_and_reload(cmd, model_key):
try:
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logging.info(f”Запущено донавчання моделі {cmd[1]} в окремому процесі”)
bot_state[model_key] = time.time()
except Exception as e:
logging.error(f”Помилка запуску донавчання {cmd[1]}: {e}”)

def prune_model_errors():
for symbol in model_errors:
for model in model_errors[symbol]:
if len(model_errors[symbol][model]) > 100:
model_errors[symbol][model] = model_errors[symbol][model][-100:]

async def check_and_reload_models():
while True:
try:
for symbol in config[“symbols”]:
lstm_model_path = f”lstm_model_{symbol}.keras”
transformer_model_path = f”transformer_model_{symbol}.keras”
xgboost_model_path = f”xgboost_model_{symbol}.pkl”

# Перевіряємо оновлення моделей
if os.path.exists(lstm_model_path) and os.path.getmtime(lstm_model_path) > bot_state[“last_retrain_lstm”]:
lstm_models[symbol] = safe_load_model(lstm_model_path)
bot_state[“last_retrain_lstm”] = time.time()
logging.info(f”Перевантажено LSTM модель для {symbol}”)

if os.path.exists(transformer_model_path) and os.path.getmtime(transformer_model_path) > bot_state[“last_retrain_transformer”]:
transformer_models[symbol] = safe_load_model(transformer_model_path)
bot_state[“last_retrain_transformer”] = time.time()
logging.info(f”Перевантажено Transformer модель для {symbol}”)

if os.path.exists(xgboost_model_path) and os.path.getmtime(xgboost_model_path) > bot_state[“last_retrain_xgboost”]:
xgb_models[symbol] = safe_load_xgboost(xgboost_model_path)
bot_state[“last_retrain_xgboost”] = time.time()
logging.info(f”Перевантажено XGBoost модель для {symbol}”)

# Перевіряємо, чи достатньо моделей для відновлення торгівлі
working_models = sum(1 for model in [lstm_models.get(symbol), transformer_models.get(symbol), xgb_models.get(symbol)] if model is not None)
if symbol in bot_state[“paused_symbols”] and working_models >= MIN_WORKING_MODELS:
bot_state[“paused_symbols”].remove(symbol)
logging.info(f”✅ Відновлено торгівлю для {symbol}: доступно {working_models} моделей”)

await asyncio.sleep(300)
except Exception as e:
logging.error(f”Помилка в check_and_reload_models: {e}”)
await asyncio.sleep(60)

async def cleanup_open_orders():
“””
Періодично скасовує ордери для символів без відкритих позицій.
“””
while True:
try:
for symbol in config[“symbols”]:
async with realtime_prices_lock:
open_orders = await client.futures_get_open_orders(symbol=symbol)
open_positions = await client.futures_position_information(symbol=symbol)
active_position_amts = {pos[“symbol”]: float(pos[“positionAmt”]) for pos in open_positions}
has_active_position = any(
trade[“status”] == “open” for trade in active_positions.get(symbol, [])
)

if active_position_amts.get(symbol, 0) == 0 and not has_active_position:
for order in open_orders:
await client.futures_cancel_order(symbol=symbol, orderId=order[“orderId”])
logging.info(f”Скасовано ордер {order[‘orderId’]} для {symbol} (немає позиції)”)
await asyncio.sleep(300)
except Exception as e:
logging.error(f”Помилка в cleanup_open_orders: {e}”)
await asyncio.sleep(60)

async def fetch_historical_http(symbol: str, interval: str, limit: int = 500, max_retries: int = 5) -> pd.DataFrame:
“””
Отримує історичні дані через HTTP API Binance.
“””
url = f”https://api.binance.com/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}”
headers = {“Accept”: “application/json”}
timeout = aiohttp.ClientTimeout(total=15, connect=5, sock_read=10)

for attempt in range(max_retries):
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 429:
wait_time = 2 ** attempt
logging.warning(f”Rate limit для {symbol} [{interval}]. Чекаємо {wait_time}s”)
await asyncio.sleep(wait_time)
continue
elif resp.status != 200:
logging.error(f”HTTP помилка {resp.status} для {symbol} [{interval}]”)
break
data = await resp.json()
if not data:
logging.warning(f”Порожні дані для {symbol} [{interval}]”)
return pd.DataFrame()
df = pd.DataFrame(data, columns=[
“timestamp”, “open”, “high”, “low”, “close”, “volume”,
“close_time”, “quote_asset_volume”, “number_of_trades”,
“taker_buy_base_volume”, “taker_buy_quote_volume”, “ignore”
])
df[“timestamp”] = pd.to_datetime(df[“timestamp”], unit=”ms”)
df.set_index(“timestamp”, inplace=True)
df = df[[“open”, “high”, “low”, “close”, “volume”]].astype(float)
return df
except asyncio.TimeoutError as e:
logging.warning(f”Таймаут для {symbol} [{interval}]: {e}”)
await asyncio.sleep(2 ** attempt)
except Exception as e:
logging.error(f”Помилка для {symbol} [{interval}]: {e}”)
logging.error(f”Не вдалося отримати дані для {symbol} [{interval}] після {max_retries} спроб”)
return pd.DataFrame()

async def update_prices_via_http(symbols):
“””
Оновлює реальні ціни через HTTP.
“””
logging.info(f”Оновлення цін через HTTP для символів: {symbols}”)
updated = False
for symbol in symbols:
try:
df = await fetch_historical_http(symbol, “1m”, limit=1)
if not df.empty:
async with realtime_prices_lock:
price = float(df[“close”].iloc[-1])
if price > 0:
realtime_prices[symbol] = price
last_prices[symbol] = price
updated = True
logging.info(f”Оновлено ціну для {symbol}: {price}”)
else:
logging.warning(f”Невалідна ціна для {symbol}: {price}”)
else:
logging.warning(f”Порожні дані для {symbol}”)
except Exception as e:
logging.error(f”Помилка оновлення ціни для {symbol}: {e}”)
return updated

async def assess_market_state(symbol):
df = await get_data_from_cache(symbol, “1m”)
if df is None or df.empty or len(df) < config[“atr_history_length”]:
return config[“threshold_update_slow”]

# Замінено atr_window на atr_history_length для уніфікації
atr_values = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_history_length”])
current_price = df[“close”].iloc[-1]
atr_current = atr_values[-1]
atr_history = atr_values[-config[“atr_history_length”]:]
volatility_ratio = atr_current / current_price if current_price != 0 else 0.01
atr_std = np.std(atr_history) / np.mean(atr_history) if np.mean(atr_history) != 0 else 0
if volatility_ratio > config[“volatility_threshold_high”] or atr_std > 0.2:
return config[“threshold_update_fast”]
elif volatility_ratio < config[“volatility_threshold_low”] and atr_std < 0.1:
return config[“threshold_update_slow”]
else:
return (config[“threshold_update_fast”] + config[“threshold_update_slow”]) / 2

async def update_thresholds_fixed():
for symbol in config[“symbols”]:
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
atr = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_window”])[-1]
current_price = df[“close”].iloc[-1]
await adjust_thresholds_based_on_volatility(atr, current_price, symbol)

async def update_thresholds_adaptive(symbol):
df = await get_data_from_cache(symbol, “1m”)
if df is not None and not df.empty:
# Замінено atr_window на atr_history_length
atr = talib.ATR(df[“high”].values, df[“low”].values, df[“close”].values, timeperiod=config[“atr_history_length”])[-1]
current_price = df[“close”].iloc[-1]
await adjust_thresholds_based_on_volatility(atr, current_price, symbol)

async def sync_balance(client, config, stats):
try:
async with asyncio.timeout(30):
if config[“mode”] == “real”:
logging.debug(“Запит балансу з Binance…”)
account = await client.futures_account()
total_balance = float(account[“totalWalletBalance”])
logging.info(f”Отримано реальний баланс із Binance: {total_balance}”)
else:
total_balance = config[“initial_balance”]
logging.info(f”Використовується початковий баланс для paper mode: {total_balance}”)
# Якщо вже є balance_history >1, не перезаписувати balance
for symbol in config[“symbols”]:
if len(stats[symbol][“balance_history”]) > 1:
stats[symbol][“balance”] = stats[symbol][“balance_history”][-1][1] # Останній баланс
continue

logging.debug(f”Символи для обробки: {config[‘symbols’]}”)
for symbol in config[“symbols”]:
new_balance = total_balance * config[“capital_allocation”][symbol]
# Умовне додавання: тільки якщо баланс змінився
if not stats[symbol][“balance_history”] or stats[symbol][“balance_history”][-1][1] != new_balance:
stats[symbol][“balance”] = new_balance
stats[symbol][“balance_history”].append((datetime.now(), stats[symbol][“balance”]))
# Обмежуємо історію до 100 записів
stats[symbol][“balance_history”] = stats[symbol][“balance_history”][-100:]
logging.info(f”Оновлено баланс для {symbol}: {stats[symbol][‘balance’]}”)

logging.info(“Спроба збереження статистики…”)
await save_stats()
logging.info(“Статистика успішно збережена.”)
except asyncio.TimeoutError:
logging.error(“Таймаут при синхронізації балансу”)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: “Таймаут синхронізації балансу”,
“trade_id”: None
})
raise
except Exception as e:
logging.error(f”Помилка в sync_balance: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка в sync_balance: {str(e)}”,
“trade_id”: None
})
raise

async def keep_alive_websocket(symbols, interval=1800):
global last_heartbeat # Використовуємо глобальну змінну для відстеження останнього повідомлення
while True:
try:
current_time = time.time()
# Перевіряємо, чи минув час з останнього повідомлення більше порогу
if current_time – last_heartbeat > WEBSOCKET_STALE_THRESHOLD:
logging.warning(f”WebSocket не отримував даних протягом {WEBSOCKET_STALE_THRESHOLD} секунд. Перезапускаємо…”)
raise Exception(“No heartbeat detected – WebSocket stale”)
logging.info(“WebSocket keep-alive: з’єднання активне”)
except Exception as e:
logging.error(f”Помилка keep-alive: {e}. Перезапускаємо WebSocket…”)
await start_websocket_price_stream(symbols) # Перезапуск WebSocket
last_heartbeat = time.time() # Скидаємо heartbeat після перезапуску
await asyncio.sleep(interval)

async def get_websocket_url(client):
“””
Єдина функція для визначення WebSocket URL. Повертає testnet stream, якщо REST OK,
імена можна адаптувати під ваші налаштування.
“””
try:
# намагаємось отримати exchange info — якщо успішно, повертаємо testnet stream адрес
await client.get_exchange_info()
return “wss://testnet.binancefuture.com/stream”
except Exception:
# fallback на стандартний fstream endpoint
return “wss://fstream.binancefuture.com/stream”

async def start_websocket_price_stream(symbols, max_retries=5):
global last_heartbeat
attempt = 0
last_heartbeat = time.time()
heartbeat_interval = 30

symbol_streams = [f”{symbol.lower()}@ticker” for symbol in symbols]
streams = “/”.join(symbol_streams)
websocket_url = f”wss://fstream.binancefuture.com/ws/{streams}”
logging.info(f”Підключаємося до WebSocket: {websocket_url}”)

while attempt < max_retries:
try:
async with websockets.connect(websocket_url) as websocket:
logging.info(f”Запущено WebSocket для {symbols}”)
keep_alive_task = asyncio.create_task(keep_alive_websocket(symbols))
attempt = 0
while True:
try:
msg = await asyncio.wait_for(websocket.recv(), timeout=heartbeat_interval)
data = json.loads(msg)
if “s” not in data or “c” not in data:
continue
symbol = data[“s”]
price = float(data[“c”])
async with realtime_prices_lock:
realtime_prices[symbol] = price
if last_prices.get(symbol) != price:
last_prices[symbol] = price
await display_queue.put((symbol, price))
last_heartbeat = time.time()
except asyncio.TimeoutError:
if time.time() – last_heartbeat > WEBSOCKET_STALE_THRESHOLD:
logging.warning(f”WebSocket застарів. Оновлюємо ціни через HTTP.”)
await update_prices_via_http(symbols)
last_heartbeat = time.time()
# Виправлено: перезапускаємо WS на timeout
raise Exception(“Timeout – restarting WebSocket”)
continue
except Exception as e:
logging.error(f”Помилка в WebSocket потоці: {e}”)
await update_prices_via_http(symbols)
break
except Exception as e:
logging.error(f”Помилка WebSocket (спроба {attempt + 1}/{max_retries}): {e}”)
attempt += 1
if attempt >= max_retries:
logging.error(“Не вдалося відновити WebSocket. Синхронізуємо позиції.”)
await sync_positions(client, config, market, active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats)
attempt = 0
await asyncio.sleep(10)
else:
await asyncio.sleep(5)

if ‘keep_alive_task’ in locals():
keep_alive_task.cancel()
try:
await keep_alive_task
except asyncio.CancelledError:
logging.info(“Keep-alive задача скасована”)

async def display_realtime_prices(symbols):
price_display = {symbol: None for symbol in symbols}
while True:
symbol, price = await display_queue.get()
price_display[symbol] = price
price_str = ” | “.join(
f”{s}: {price_display[s]:.2f}” if price_display[s] is not None else f”{s}: N/A”
for s in symbols
)
print(f”rРеальні ціни: {price_str}”, end=””, flush=True)
display_queue.task_done()

async def wait_for_realtime_prices(symbols, timeout=30, check_interval=5):
start_time = time.time()
while True:
async with realtime_prices_lock:
all_initialized = all(realtime_prices[symbol] is not None for symbol in symbols)
if all_initialized:
logging.info(“✅ Усі ціни ініціалізовані”)
return True
if time.time() – start_time > timeout:
logging.warning(f”❌ WebSocket не ініціалізував ціни протягом {timeout} секунд. Використовуємо HTTP.”)
await update_prices_via_http(symbols)
async with realtime_prices_lock:
all_initialized = all(realtime_prices[symbol] is not None for symbol in symbols)
if all_initialized:
logging.info(“✅ Ціни успішно ініціалізовані через HTTP”)
return True
else:
logging.error(f”❌ Не вдалося ініціалізувати ціни навіть через HTTP”)
return False
await asyncio.sleep(check_interval)

async def periodic_sync():
global client, config, stats, realtime_prices_lock, last_prices, realtime_prices, market, active_positions, spot_holdings
max_retries = config.get(“api_retry_attempts”, 3)
retry_delay = config.get(“api_retry_delay”, 2)

try:
while True:
logging.info(“INFO: Періодична синхронізація розпочата…”) # Додано INFO-лог
sync_success = False
for attempt in range(max_retries):
try:
if config[“mode”] == “real”:
async with asyncio.timeout(300):
await sync_balance(client, config, stats)
logging.debug(“Баланс синхронізовано.”)

async with realtime_prices_lock:
all_prices_stale = all(
last_prices[symbol] == realtime_prices[symbol] for symbol in config[“symbols”]
)
if all_prices_stale:
logging.info(“INFO: Ціни застарілі, запускаємо sync_positions…”)
logging.info(“DEBUG: Викликаємо sync_positions”) # Додано
async with asyncio.timeout(60):
await sync_positions(client, config, market, active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats)
logging.info(“DEBUG: sync_positions завершено”) # Додано
logging.info(f”Позиції синхронізовано через застарілі ціни. Наступна синхронізація через 30 секунд”)
await asyncio.sleep(30)
else:
logging.info(f”Ціни актуальні. Наступна синхронізація через {config.get(‘sync_delay’, 300)} секунд”)
await asyncio.sleep(config.get(“sync_delay”, 300))
sync_success = True
break
else:
logging.info(f”Режим paper. Наступна синхронізація через 300 секунд”)
await asyncio.sleep(300)
sync_success = True
break
except asyncio.TimeoutError:
logging.error(f”Таймаут під час синхронізації (спроба {attempt + 1}/{max_retries})”)
backoff_delay = retry_delay * (2 ** attempt)
logging.info(f”Очікування {backoff_delay}s перед наступною спробою”)
await asyncio.sleep(backoff_delay)
except Exception as e:
logging.error(f”Помилка в синхронізації (спроба {attempt + 1}/{max_retries}): {e}”, exc_info=True)
backoff_delay = retry_delay * (2 ** attempt)
logging.info(f”Очікування {backoff_delay}s перед наступною спробою”)
await asyncio.sleep(backoff_delay)

if not sync_success:
logging.warning(f”Досягнуто максимум спроб синхронізації, надсилаємо повідомлення в Telegram”)
total_balance = sum(stats[s][“balance”] for s in config[“symbols”])
await send_telegram_notification({
“symbol”: “SYSTEM”,
“side”: “N/A”,
“message”: f”⚠️ <b>Системне повідомлення</b>n🔸 Тип: <b>N/A</b>n📊 Таймаут у periodic_sync після {max_retries} спробn📊 Баланс: <b>{total_balance:.2f}$</b>”,
“trade_id”: None,
“status”: “open”,
“open_time”: datetime.now()
})

logging.debug(f”Стан active_positions після синхронізації: {active_positions}”)
except Exception as e:
logging.error(f”Критична помилка в periodic_sync: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “SYSTEM”,
“side”: “N/A”,
“message”: f”Критична помилка в periodic_sync: {str(e)}”,
“trade_id”: None,
“status”: “open”,
“open_time”: datetime.now()
})
raise

async def main_loop():
await start_cache_tasks(config[“symbols”], get_historical_data)
await wait_for_realtime_prices(config[“symbols”])

try:
while True:
if bot_state[“paused”]:
open_positions_exist = any(
any(t[“status”] == “open” for t in active_positions.get(sym, []))
for sym in config[“symbols”]
)
total_balance = sum(stats[symbol][“balance”] for symbol in config[“symbols”])
seen_trade_ids = set()
total_exposure = sum(
trade[“quantity”] * trade[“entry_price”] * trade[“leverage”]
for symbol in active_positions
for trade in active_positions[symbol]
if trade[“status”] == “open” and trade[“trade_id”] not in seen_trade_ids
and seen_trade_ids.add(trade[“trade_id”])
)
if total_exposure <= total_balance * config[“total_exposure_limit”]:
bot_state[“paused”] = False
logging.info(“Експозиція нормалізована, знято паузу через ліміт експозиції.”)
elif not open_positions_exist:
bot_state[“paused”] = False
logging.info(“Всі позиції закриті, знято паузу.”)
else:
logging.info(“⏸ Бот на паузі”)
await asyncio.sleep(config[“sleep_interval”])
continue

current_time = time.time()

if time.time() – last_heartbeat > config[“websocket_stale_threshold”]:
await update_prices_via_http(config[“symbols”])
logging.info(“Оновлено ціни через HTTP через застарілий WebSocket”)

total_open_positions = sum(len([t for t in active_positions.get(s, []) if t[“status”] == “open”]) for s in config[“symbols”])
if total_open_positions >= config[“max_open_positions”]:
if not bot_state.get(“global_limit_reached”, False):
logging.info(f”Досягнуто глобальний ліміт відкритих позицій ({config[‘max_open_positions’]})”)
bot_state[“global_limit_reached”] = True
await asyncio.sleep(60)
continue
bot_state[“global_limit_reached”] = False
logging.info(f”Поточна кількість відкритих позицій: {total_open_positions}”)

total_balance = sum(stats[symbol][“balance”] for symbol in config[“symbols”])
seen_trade_ids = set()
total_exposure = sum(
trade[“quantity”] * trade[“entry_price”] * trade[“leverage”]
for symbol in active_positions
for trade in active_positions[symbol]
if trade[“status”] == “open” and trade[“trade_id”] not in seen_trade_ids
and seen_trade_ids.add(trade[“trade_id”])
)
if total_exposure > total_balance * config[“total_exposure_limit”]:
logging.warning(f”Перевищено ліміт експозиції: {total_exposure} > {total_balance * config[‘total_exposure_limit’]}”)
bot_state[“paused”] = True
continue

if current_time – bot_state.get(“last_stats_reset”, 0) >= 24 * 3600:
for symbol in config[“symbols”]:
if symbol in bot_state[“paused_due_to_losses”]:
stats[symbol][“loss_streak”] = 0
stats[symbol][“last_loss_time”] = 0
stats[symbol][“win_rate”] = 0.0
stats[symbol][“total_trades”] = 0
stats[symbol][“winning_trades”] = 0
stats[symbol][“losing_trades”] = 0
bot_state[“paused_due_to_losses”].remove(symbol)
logging.info(f”📊 Скинуто статистику для {symbol}, торгівля відновлена”)
bot_state[“last_stats_reset”] = current_time
await save_stats()
logging.info(“🔄 Скинуто статистику для всіх символів”)

if config[“mode”] == “real” and time.time() – bot_state.get(“last_sync”, 0) >= config[“sync_delay”]:
await sync_positions(client, config, market, active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats)
bot_state[“last_sync”] = time.time()
await save_active_positions(active_positions) # Додано збереження після синхронізації
logging.info(“🔄 Синхронізація завершена”)
logging.debug(f”active_positions після синхронізації в main_loop: {active_positions}”)

if current_time – bot_state[“last_threshold_update”] >= config[“threshold_update_interval”]:
if config[“threshold_mode”] == “fixed”:
await update_thresholds_fixed()
bot_state[“last_threshold_update”] = current_time

if config[“volatility_adjustment”]:
for symbol in config[“symbols”]:
update_interval = await assess_market_state(symbol)
if current_time – bot_state[“last_threshold_update”] >= update_interval:
await update_thresholds_adaptive(symbol)
bot_state[“last_threshold_update”] = current_time

if current_time – bot_state[“last_weight_update”] >= 3600:
update_model_weights()
bot_state[“last_weight_update”] = current_time

if current_time – bot_state[“last_retrain”] >= config[“retrain_interval”]:
for symbol in config[“symbols”]:
if current_time – bot_state[“last_retrain_lstm”] >= config[“retrain_interval”]:
retrain_and_reload([“python”, “retrain_lstm.py”, symbol], “last_retrain_lstm”)
if current_time – bot_state[“last_retrain_transformer”] >= config[“retrain_interval”]:
retrain_and_reload([“python”, “train_transformer.py”, symbol], “last_retrain_transformer”)
if current_time – bot_state[“last_retrain_xgboost”] >= config[“retrain_interval”]:
retrain_and_reload([“python”, “train_xgboost.py”, symbol], “last_retrain_xgboost”)
bot_state[“last_retrain”] = current_time

tasks = []
for symbol in config[“symbols”]:
logging.info(f”Створюємо завдання для обробки {symbol}”)
tasks.append(process_symbol(symbol, bot_state, config, stats, historical_datasets, last_feature_analysis_time, knn_selected_features, active_positions))
results = await asyncio.gather(*tasks, return_exceptions=True)
for symbol, result in zip(config[“symbols”], results):
if isinstance(result, Exception):
logging.error(f”Помилка обробки {symbol}: {result}”)
await send_telegram_notification({
“symbol”: symbol,
“message”: f”Помилка в process_symbol: {str(result)}”,
“trade_id”: None
})

total_balance = sum(stats[symbol][“balance”] for symbol in config[“symbols”])
if total_balance <= config[“initial_balance”] * (1 + config[“emergency_stop_loss”]):
logging.error(f”🚨 Аварійна зупинка: баланс {total_balance:.2f}”)
bot_state[“paused”] = True
trade_emergency = {
“symbol”: “SYSTEM”,
“side”: “EMERGENCY_STOP”,
“entry_price”: 0,
“profit”: 0,
“quantity”: 0,
“leverage”: 1,
“entry_time”: datetime.now(),
“status”: “closed”
}
await send_telegram_notification(trade_emergency)
await shutdown()

if int(time.time()) % 300 == 0:
await clean_duplicate_positions()
await cleanup_stale_orders()
logging.info(“🧹 Виконано очищення застарілих ордерів”)

await asyncio.sleep(config[“sleep_interval”])
except asyncio.CancelledError:
logging.info(“Отримано сигнал завершення, зберігаємо дані…”)
await shutdown()
raise
except Exception as e:
logging.error(f”Помилка в main_loop: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка в main_loop: {str(e)}”,
“trade_id”: None
})
await shutdown()
raise

websocket_task = None
sync_task = None
display_task = None
report_task = None

async def initialize_cache(symbols, intervals=[“1m”, “5m”, “15m”]):
tasks = []
for symbol in symbols:
for interval in intervals:
tasks.append(asyncio.create_task(get_historical_data(symbol, interval, limit=100)))
results = await asyncio.gather(*tasks, return_exceptions=True)
idx = 0
for symbol in symbols:
for interval in intervals:
df = results[idx]
if not isinstance(df, Exception) and df is not None and not df.empty:
update_cache_incremental(symbol, interval, df)
logging.info(f”Ініціалізовано кеш для {symbol} [{interval}] з {len(df)} свічок”)
else:
logging.warning(f”Не вдалося ініціалізувати кеш для {symbol} [{interval}]”)
idx += 1

async def clean_duplicate_positions():
for symbol in active_positions:
unique_positions = []
seen_trade_ids = set()
for position in active_positions[symbol]:
if position[‘trade_id’] not in seen_trade_ids and position[‘status’] == ‘open’:
unique_positions.append(position)
seen_trade_ids.add(position[‘trade_id’])
else:
logging.warning(f”Видалено дубльовану позицію для {symbol}: trade_id={position[‘trade_id’]}”)
active_positions[symbol] = unique_positions
await save_active_positions(active_positions)

async def full_run():
global websocket_task, sync_task, display_task, report_task, reload_task, cleanup_task
try:
logging.info(“Запуск full_run()…”)
logging.debug(“Ініціалізація exchange_info_cache…”)
global exchange_info_cache
exchange_info_cache = {}

logging.debug(“Оновлення exchange_info…”)
async with asyncio.timeout(10): # Таймаут 10 секунд
await update_exchange_info(config[“market”])
logging.debug(“exchange_info оновлено.”)

logging.debug(“Синхронізація балансу…”)
async with asyncio.timeout(30):
await sync_balance(client, config, stats)
logging.debug(“Баланс синхронізовано.”)

logging.debug(“Завантаження активних позицій…”)
active_positions.update(load_active_positions(config))
logging.debug(f”Активні позиції завантажено: {len(active_positions)} символів”)

logging.debug(“Ініціалізація кешу даних…”)
await initialize_cache(config[“symbols”])
logging.debug(“Кеш даних ініціалізовано.”)

logging.debug(“Перевірка глобальних змінних…”)
logging.debug(f”Тип client: {type(client)}”)
logging.debug(f”Тип config: {type(config)}”)
logging.debug(f”Тип market: {type(config[‘market’])}”)
logging.debug(f”Тип active_positions: {type(active_positions)}, кількість: {len(active_positions)}”)
logging.debug(f”Тип realtime_prices: {type(realtime_prices)}”)
logging.debug(f”Тип realtime_prices_lock: {type(realtime_prices_lock)}”)
logging.debug(f”Тип last_prices: {type(last_prices)}”)
logging.debug(f”Тип spot_holdings: {type(spot_holdings)}”)
logging.debug(f”Тип stats: {type(stats)}”)

logging.debug(“Синхронізація позицій…”)
async with asyncio.timeout(15):
await sync_positions(client, config, config[“market”], active_positions, realtime_prices, realtime_prices_lock, last_prices, spot_holdings, stats)
logging.debug(“Позиції синхронізовано.”)

logging.debug(“Очищення дублікатів позицій…”)
await clean_duplicate_positions()
logging.debug(“Дублікати позицій очищено.”)

logging.debug(“Перевірка SL/TP для активних позицій…”)
async with asyncio.timeout(15):
await resync_sl_tp_for_active_positions(client, config[“market”], config, active_positions, exchange_info_cache, realtime_prices)
logging.debug(“SL/TP перевірено.”)

logging.debug(“Збереження активних позицій…”)
await save_active_positions(active_positions)
logging.debug(“Активні позиції збережено.”)

logging.debug(“Ініціалізація ваг моделей…”)
initialize_model_weights()
logging.debug(“Ваги моделей ініціалізовано.”)

logging.debug(“Завантаження моделей і скейлерів…”)
async with asyncio.timeout(30):
await load_models_and_scalers()
logging.debug(“Моделі та скейлери завантажено.”)

logging.info(“Запуск асинхронних задач…”)
websocket_task = asyncio.create_task(start_websocket_price_stream(config[“symbols”]))
sync_task = asyncio.create_task(periodic_sync())
display_task = asyncio.create_task(display_realtime_prices(config[“symbols”]))
report_task = asyncio.create_task(send_periodic_report())
reload_task = asyncio.create_task(check_and_reload_models())
cleanup_task = asyncio.create_task(cleanup_open_orders())
logging.info(“Асинхронні задачі запущено.”)

logging.info(“Вхід у основний цикл обробки…”)
try:
async with asyncio.timeout(config.get(“loop_timeout”, 3600)): # Таймаут 1 година
await main_loop()
except asyncio.TimeoutError:
logging.error(“Таймаут у main_loop”)
await shutdown() # Викликаємо shutdown при таймауті
await send_telegram_notification({
“symbol”: “ALL”,
“message”: “Таймаут у main_loop”,
“trade_id”: None
})
except asyncio.CancelledError:
logging.info(“Main loop скасовано”)
except Exception as e:
logging.error(f”Помилка в main_loop: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка в main_loop: {str(e)}”,
“trade_id”: None
})
raise

logging.info(“Full_run завершено.”)
except asyncio.TimeoutError:
logging.error(“Таймаут у full_run”)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: “Таймаут у full_run”,
“trade_id”: None
})
raise
except Exception as e:
logging.error(f”Критична помилка в full_run: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Критична помилка в full_run: {str(e)}”,
“trade_id”: None
})
raise
finally:
logging.info(“Скасування асинхронних задач…”)
for task in (websocket_task, sync_task, display_task, report_task, reload_task, cleanup_task):
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
logging.debug(f”Задача {task.get_name()} скасована”)
logging.info(“Усі задачі скасовано.”)

# Додаємо функцію shutdown і обробники сигналів тут
async def shutdown():
“””
Зберігає всі дані перед завершенням роботи бота.
“””
try:
# Скасовуємо всі задачі
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True) # Чекаємо завершення
# Зберігаємо дані
await save_stats()
await save_active_positions(active_positions)
# Закриваємо сесії
if client:
await client.close_connection() # Виправлено: використовуємо close_connection()
logging.info(“Binance client закрито”)
if ‘client_session’ in globals() and client_session:
await client_session.close()
logging.info(“aiohttp сесію закрито”)
await asyncio.sleep(0.25) # Затримка для SSL конекторів
logging.info(“Бот завершено. Усі дані збережено.”)
except Exception as e:
logging.error(f”Помилка при завершенні роботи бота: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка при завершенні роботи бота: {str(e)}”,
“trade_id”: None
})

async def close_sessions():
“””
Закриває всі відкриті сесії та з’єднання.
“””
try:
# Закриваємо WebSocket, якщо він відкритий
if ‘websocket’ in globals() and websocket:
await websocket.close()
logging.info(“WebSocket закрито”)
# Закриваємо aiohttp сесію
if ‘client_session’ in globals() and client_session:
await client_session.close()
logging.info(“HTTP сесію закрито”)
except Exception as e:
logging.error(f”Помилка при закритті сесій: {e}”, exc_info=True)

def handle_shutdown(signum, frame):
“””
Обробник сигналів для коректного завершення.
“””
logging.info(f”Отримано сигнал {signum}. Завершуємо роботу…”)
loop = asyncio.get_event_loop()
try:
# Скасовуємо всі асинхронні задачі
tasks = [task for task in asyncio.all_tasks(loop) if task is not asyncio.current_task()]
for task in tasks:
task.cancel()
# Виконуємо shutdown асинхронно
loop.run_until_complete(shutdown())
# Завершуємо асинхронні генератори
loop.run_until_complete(loop.shutdown_asyncgens())
except Exception as e:
logging.error(f”Помилка при обробці завершення: {e}”, exc_info=True)
finally:
# Закриваємо цикл подій
loop.close()
logging.info(“Цикл подій закрито.”)
sys.exit(0)

async def close_all_positions():
“””
Закриває всі відкриті позиції для всіх символів.
“””
try:
for symbol in config[“symbols”]:
if symbol in active_positions and active_positions[symbol]:
for position in active_positions[symbol]:
if position[“status”] == “open”:
current_price = realtime_prices.get(symbol)
if current_price is None:
logging.warning(f”Не вдалося отримати поточну ціну для {symbol}. Пропускаємо закриття.”)
continue

side = position.get(“side”)
if side is None:
logging.warning(f”Відсутня ‘side’ для позиції {symbol}, trade_id={position[‘trade_id’]}. Пропускаємо.”)
continue

exit_price = current_price # Використовуємо поточну ціну як exit_price

# Оновлюємо позицію перед закриттям
position[“exit_price”] = exit_price
position[“status”] = “closed”

# Логіка закриття (замініть на вашу реальну функцію закриття ордера, якщо є)
# Наприклад: await client.futures_create_order(…) для закриття позиції
logging.info(f”Закрито позицію для {symbol}: side={side}, exit_price={exit_price}”)

# Надсилаємо Telegram-повідомлення
await send_telegram_notification({
“symbol”: symbol,
“side”: side,
“message”: f”Закрито позицію через команду ALL”,
“trade_id”: position[“trade_id”],
“exit_price”: exit_price,
“status”: “closed”,
“open_time”: position.get(“open_time”, datetime.now())
})
else:
logging.info(f”Немає відкритих позицій для {symbol}”)
await save_active_positions(active_positions)
except Exception as e:
logging.error(f”Помилка при закритті всіх позицій: {e}”, exc_info=True)
await send_telegram_notification({
“symbol”: “ALL”,
“message”: f”Помилка при закритті всіх позицій: {str(e)}”,
“trade_id”: None
})

# Реєструємо обробники сигналів
signal.signal(signal.SIGINT, handle_shutdown) # Ctrl+C
signal.signal(signal.SIGTERM, handle_shutdown) # Сигнал завершення процесу

async def main():
global config, client, stats, active_positions
logging.info(“Початок виконання main()…”)

try:
logging.info(“Завантаження конфігурації…”)
config = load_config()
logging.debug(f”Config завантажено: {config}”)

logging.info(“Спроба ініціалізації клієнта Binance…”)
await init_client()
if client is None:
logging.error(“Клієнт Binance не ініціалізований!”)
sys.exit(1)
logging.info(“Клієнт Binance успішно ініціалізований!”)

logging.info(“Завантаження статистики…”)
stats = await load_stats()
logging.debug(f”Stats завантажено: {stats}”)

logging.info(“Синхронізація балансу…”)
await sync_balance(client, config, stats)
logging.info(“Синхронізація балансу завершена успішно.”)
logging.debug(f”Баланс після синхронізації: {stats}”)

logging.info(“Перевірка глобальних змінних перед initialize()…”)
logging.debug(f”Тип active_positions: {type(active_positions)}, значення: {active_positions}”)

logging.info(“Виконання initialize()…”)
await initialize()
logging.info(“Initialize завершено успішно.”)

logging.info(“Запуск full_run()…”)
await full_run()
logging.info(“Full_run завершено успішно.”)
except Exception as e:
logging.error(f”Помилка в main(): {e}”, exc_info=True)
raise
finally:
# Виправлено: Збереження в finally, щоб виконалося завжди
logging.info(“Збереження даних перед завершенням…”)
await shutdown()

if __name__ == “__main__”:
try:
logging.info(“Початок роботи бота…”)
asyncio.run(main())
logging.info(“Основний цикл завершено.”)
except KeyboardInterrupt:
logging.info(“Бот зупинений користувачем”)
asyncio.run(shutdown()) # Виправлено: Виклик shutdown на interrupt
except Exception as e:
logging.error(f”Критична помилка: {e}”)
asyncio.run(shutdown()) # Виправлено: Виклик shutdown на помилці
sys.exit(1)

Share This: