import asyncio import logging.config import time import app.services.Bybit.functions.balance as balance_g import app.services.Bybit.functions.price_symbol as price_symbol import app.telegram.database.requests as rq import app.telegram.Keyboards.inline_keyboards as inline_markup from logger_helper.logger_helper import LOGGING_CONFIG from pybit import exceptions from pybit.unified_trading import HTTP logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("futures") processed_trade_ids = set() async def get_bybit_client(tg_id): """ Асинхронно получает экземпляр клиента Bybit. :param tg_id: int - ID пользователя Telegram :return: HTTP - экземпляр клиента Bybit """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) return HTTP(api_key=api_key, api_secret=secret_key) def safe_float(val) -> float: """ Безопасное преобразование значения в float. Возвращает 0.0, если значение None, пустое или некорректное. """ try: if val is None or val == "": return 0.0 return float(val) except (ValueError, TypeError): logger.error("Некорректное значение для преобразования в float", exc_info=True) return 0.0 def format_trade_details_position(data, commission_fee): """ Форматирует информацию о сделке в виде строки. """ msg = data.get("data", [{}])[0] closed_size = safe_float(msg.get("closedSize", 0)) symbol = msg.get("symbol", "N/A") entry_price = safe_float(msg.get("execPrice", 0)) qty = safe_float(msg.get("execQty", 0)) order_type = msg.get("orderType", "N/A") side = msg.get("side", "") commission = safe_float(msg.get("execFee", 0)) pnl = safe_float(msg.get("execPnl", 0)) if commission_fee == "Да": pnl -= commission movement = "" if side.lower() == "buy": movement = "Покупка" elif side.lower() == "sell": movement = "Продажа" else: movement = side if closed_size > 0: return ( f"Сделка закрыта:\n" f"Торговая пара: {symbol}\n" f"Цена исполнения: {entry_price:.6f}\n" f"Количество: {qty}\n" f"Закрыто позиций: {closed_size}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Комиссия за сделку: {commission:.6f}\n" f"Реализованная прибыль: {pnl:.6f} USDT" ) if order_type == "Market": return ( f"Сделка открыта:\n" f"Торговая пара: {symbol}\n" f"Цена исполнения: {entry_price:.6f}\n" f"Количество: {qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Комиссия за сделку: {commission:.6f}" ) return None def format_order_details_position(data): """ Форматирует информацию об ордере в виде строки. """ msg = data.get("data", [{}])[0] price = safe_float(msg.get("price", 0)) qty = safe_float(msg.get("qty", 0)) cum_exec_qty = safe_float(msg.get("cumExecQty", 0)) cum_exec_fee = safe_float(msg.get("cumExecFee", 0)) take_profit = safe_float(msg.get("takeProfit", 0)) stop_loss = safe_float(msg.get("stopLoss", 0)) order_status = msg.get("orderStatus", "N/A") symbol = msg.get("symbol", "N/A") order_type = msg.get("orderType", "N/A") side = msg.get("side", "") movement = "" if side.lower() == "buy": movement = "Покупка" elif side.lower() == "sell": movement = "Продажа" else: movement = side if order_status.lower() == "filled" and order_type.lower() == "limit": text = ( f"Ордер исполнен:\n" f"Торговая пара: {symbol}\n" f"Цена исполнения: {price:.6f}\n" f"Количество: {qty}\n" f"Исполнено позиций: {cum_exec_qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Тейк-профит: {take_profit:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n" f"Комиссия за сделку: {cum_exec_fee:.6f}\n" ) return text elif order_status.lower() == "new": text = ( f"Ордер создан:\n" f"Торговая пара: {symbol}\n" f"Цена: {price:.6f}\n" f"Количество: {qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Тейк-профит: {take_profit:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n" ) return text elif order_status.lower() == "cancelled": text = ( f"Ордер отменен:\n" f"Торговая пара: {symbol}\n" f"Цена: {price:.6f}\n" f"Количество: {qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Тейк-профит: {take_profit:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n" ) return text return None def parse_pnl_from_msg(msg) -> float: """ Извлекает реализованную прибыль/убыток из сообщения. """ try: data = msg.get("data", [{}])[0] return float(data.get("execPnl", 0)) except Exception as e: logger.error("Ошибка при извлечении реализованной прибыли: %s", e) return 0.0 async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price): """ Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча. Параметры: - starting_quantity_usdt: стартовый размер ставки в долларах (USD) - martingale_factor: множитель увеличения ставки при каждом проигрыше - max_steps: максимальное количество шагов удвоения ставки - commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие) - leverage: кредитное плечо - current_price: текущая цена актива (например BTCUSDT) Возвращает: - общий бюджет в долларах, который необходимо иметь на счету """ total = 0 for step in range(max_steps): quantity = starting_quantity * (martingale_factor ** step) # размер ставки на текущем шаге в USDT # Переводим ставку из USDT в количество актива по текущей цене quantity_in_asset = quantity / current_price # Учитываем комиссию за вход и выход (умножаем на 2) quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100) # Учитываем кредитное плечо - реальные собственные вложения меньше effective_quantity = quantity_with_fee / leverage total += effective_quantity # Возвращаем бюджет в USDT total_usdt = total * current_price return total_usdt async def handle_execution_message(message, msg): """ Обработчик сообщений об исполнении сделки. Логирует событие и проверяет условия для мартингейла и TP. """ tg_id = message.from_user.id data = msg.get("data", [{}])[0] data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) commission_fee = data_main_risk_stgs.get("commission_fee", "ДА") pnl = parse_pnl_from_msg(msg) data_main_stgs = await rq.get_user_main_settings(tg_id) symbol = data.get("symbol") trading_mode = data_main_stgs.get("trading_mode", "Long") trigger = await rq.get_for_registration_trigger(tg_id) margin_mode = data_main_stgs.get("margin_type", "Isolated") starting_quantity = safe_float(data_main_stgs.get("starting_quantity")) martingale_factor = safe_float(data_main_stgs.get("martingale_factor")) closed_size = safe_float(data.get("closedSize", 0)) commission = safe_float(data.get("execFee", 0)) if commission_fee == "Да": pnl -= commission trade_info = format_trade_details_position( data=msg, commission_fee=commission_fee ) if trade_info: await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) if closed_size == 0: side = data.get("side", "") if side.lower() == "buy": await rq.set_last_series_info(tg_id, last_side="Buy") elif side.lower() == "sell": await rq.set_last_series_info(tg_id, last_side="Sell") if trigger == "Автоматический" and closed_size > 0: if pnl < 0: if trading_mode == 'Switch': side = data_main_stgs.get("last_side") else: side = "Buy" if trading_mode == "Long" else "Sell" current_martingale = await rq.get_martingale_step(tg_id) current_martingale_step = int(current_martingale) current_martingale += 1 next_quantity = float(starting_quantity) * ( float(martingale_factor) ** current_martingale_step ) await rq.update_martingale_step(tg_id, current_martingale) await open_position( tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, quantity=next_quantity, ) elif pnl > 0: await rq.update_martingale_step(tg_id, 0) await message.answer( "❗️ Прибыль достигнута, шаг мартингейла сброшен." ) async def handle_order_message(message, msg: dict) -> None: """ Обработчик сообщений об исполнении ордера. Логирует событие и проверяет условия для мартингейла и TP. """ # logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") trade_info = format_order_details_position(msg) if trade_info: await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) async def error_max_step(message) -> None: """ Сообщение об ошибке превышения максимального количества шагов мартингейла. """ logger.error( "Сделка не была совершена, превышен лимит максимального количества ставок в серии." ) await message.answer( "Сделка не была совершена, превышен лимит максимального количества ставок в серии.", reply_markup=inline_markup.back_to_main, ) async def error_max_risk(message) -> None: """ Сообщение об ошибке превышения риск-лимита сделки. """ logger.error("Сделка не была совершена, риск убытка превышает допустимый лимит.") await message.answer( "Сделка не была совершена, риск убытка превышает допустимый лимит.", reply_markup=inline_markup.back_to_main, ) async def open_position( tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode="Full" ): """ Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях. """ try: client = await get_bybit_client(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id) order_type = data_main_stgs.get("entry_order_type") bybit_margin_mode = ( "ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN" ) limit_price = None if order_type == "Limit": limit_price = await rq.get_limit_price(tg_id) data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) price = await price_symbol.get_price(tg_id, symbol=symbol) entry_price = safe_float(price) leverage = safe_float(data_main_stgs.get("size_leverage", 1)) max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0)) current_martingale = await rq.get_martingale_step(tg_id) max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal")) loss_profit = safe_float(data_risk_stgs.get("price_loss")) commission_fee = data_risk_stgs.get("commission_fee") starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) fee_info = client.get_fee_rates(category='linear', symbol=symbol) instruments_resp = client.get_instruments_info(category="linear", symbol=symbol) instrument = instruments_resp.get("result", {}).get("list", []) if commission_fee == "Да": commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate']) else: commission_fee_percent = 0.0 total_budget = await calculate_total_budget( starting_quantity=starting_quantity, martingale_factor=martingale_factor, max_steps=max_martingale_steps, commission_fee_percent=commission_fee_percent, leverage=leverage, current_price=entry_price, ) balance = await balance_g.get_balance(tg_id, message) if safe_float(balance) < total_budget: await message.answer( f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.", reply_markup=inline_markup.back_to_main, ) return if order_type == "Limit" and limit_price: price_for_calc = limit_price else: price_for_calc = entry_price potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) adjusted_loss = potential_loss / leverage allowed_loss = safe_float(balance) * (max_risk_percent / 100) if adjusted_loss > allowed_loss: await error_max_risk(message) return if max_martingale_steps == current_martingale: await error_max_step(message) return client.set_margin_mode(setMarginMode=bybit_margin_mode) max_leverage = safe_float(instrument[0].get("leverageFilter", {}).get("maxLeverage", 0)) if safe_float(leverage) > max_leverage: await message.answer( f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. " f"Устанавливаю максимальное.", reply_markup=inline_markup.back_to_main, ) logger.info( f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.") leverage_to_set = max_leverage else: leverage_to_set = safe_float(leverage) try: client.set_leverage( category="linear", symbol=symbol, buyLeverage=str(leverage_to_set), sellLeverage=str(leverage_to_set), ) logger.info(f"Set leverage to {leverage_to_set} for {symbol}") except exceptions.InvalidRequestError as e: if "110043" in str(e): logger.info(f"Leverage already set to {leverage} for {symbol}") else: raise e if instruments_resp.get("retCode") == 0: instrument_info = instruments_resp.get("result", {}).get("list", []) if instrument_info: instrument_info = instrument_info[0] min_notional_value = float(instrument_info.get("lotSizeFilter", {}).get("minNotionalValue", 0)) min_order_value = min_notional_value else: min_order_value = 5.0 order_value = float(quantity) * price_for_calc if order_value < min_order_value: await message.answer( f"Сумма ордера слишком мала: {order_value:.2f} USDT. " f"Минимум для торговли — {min_order_value} USDT. " f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main, ) return False if bybit_margin_mode == "ISOLATED_MARGIN": # Открываем позицию response = client.place_order( category="linear", symbol=symbol, side=side, orderType=order_type, qty=str(quantity), price=( str(limit_price) if order_type == "Limit" and limit_price else None ), timeInForce="GTC", orderLinkId=f"deal_{symbol}_{int(time.time())}", ) if response.get("retCode", -1) != 0: logger.error(f"Ошибка открытия ордера: {response}") await message.answer( f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main ) return False # Получаем цену ликвидации positions = client.get_positions(category="linear", symbol=symbol) pos = positions.get("result", {}).get("list", [{}])[0] avg_price = float(pos.get("avgPrice", 0)) liq_price = safe_float(pos.get("liqPrice", 0)) if liq_price > 0 and avg_price > 0: if side.lower() == "buy": take_profit_price = avg_price + (avg_price - liq_price) else: take_profit_price = avg_price - (liq_price - avg_price) take_profit_price = max(take_profit_price, 0) try: try: client.set_tp_sl_mode( symbol=symbol, category="linear", tpSlMode="Full" ) except exceptions.InvalidRequestError as e: if "same tp sl mode" in str(e): logger.info("Режим TP/SL уже установлен - пропускаем") else: raise resp = client.set_trading_stop( category="linear", symbol=symbol, takeProfit=str(round(take_profit_price, 5)), tpTriggerBy="LastPrice", slTriggerBy="LastPrice", positionIdx=0, reduceOnly=False, tpslMode=tpsl_mode, ) except Exception as e: logger.error(f"Ошибка установки TP/SL: {e}") await message.answer( "Ошибка при установке Take Profit и Stop Loss.", reply_markup=inline_markup.back_to_main, ) return False else: logger.warning("Не удалось получить цену ликвидации для позиции") else: # REGULAR_MARGIN try: client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode="Full") except exceptions.InvalidRequestError as e: if "same tp sl mode" in str(e): logger.info("Режим TP/SL уже установлен - пропускаем") else: raise if order_type == "Market": base_price = entry_price else: base_price = limit_price if side.lower() == "buy": take_profit_price = base_price * (1 + loss_profit / 100) stop_loss_price = base_price * (1 - loss_profit / 100) else: take_profit_price = base_price * (1 - loss_profit / 100) stop_loss_price = base_price * (1 + loss_profit / 100) take_profit_price = max(take_profit_price, 0) stop_loss_price = max(stop_loss_price, 0) if tpsl_mode == "Full": tp_order_type = "Market" sl_order_type = "Market" tp_limit_price = None sl_limit_price = None else: # Partial tp_order_type = "Limit" sl_order_type = "Limit" tp_limit_price = take_profit_price sl_limit_price = stop_loss_price response = client.place_order( category="linear", symbol=symbol, side=side, orderType=order_type, qty=str(quantity), price=( str(limit_price) if order_type == "Limit" and limit_price else None ), takeProfit=str(take_profit_price), tpOrderType=tp_order_type, tpLimitPrice=str(tp_limit_price) if tp_limit_price else None, stopLoss=str(stop_loss_price), slOrderType=sl_order_type, slLimitPrice=str(sl_limit_price) if sl_limit_price else None, tpslMode=tpsl_mode, timeInForce="GTC", orderLinkId=f"deal_{symbol}_{int(time.time())}", ) if response.get("retCode", -1) == 0: return True else: logger.error(f"Ошибка открытия ордера: {response}") await message.answer( f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main ) return False return None except exceptions.InvalidRequestError as e: logger.error("InvalidRequestError: %s", e) error_text = str(e) if "estimated will trigger liq" in error_text: await message.answer( "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", reply_markup=inline_markup.back_to_main, ) elif "ab not enough for new order" in error_text: await message.answer("Недостаточно средств для нового ордера", reply_markup=inline_markup.back_to_main) else: await message.answer( "Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", reply_markup=inline_markup.back_to_main, ) except Exception as e: logger.error("Ошибка при совершении сделки: %s", e) await message.answer( "Возникла ошибка при попытке открыть позицию.", reply_markup=inline_markup.back_to_main, ) async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quantity): """ Цикл торговой логики с учётом таймера пользователя. """ try: timer_data = await rq.get_user_timer(tg_id) timer_min = 0 if isinstance(timer_data, dict): timer_min = timer_data.get("timer_minutes") or timer_data.get("timer") or 0 else: timer_min = timer_data or 0 timer_sec = timer_min * 60 if timer_sec > 0: await asyncio.sleep(timer_sec) await open_position( tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, quantity=starting_quantity, ) except asyncio.CancelledError: logger.info( f"Торговый цикл для пользователя {tg_id} был отменён.", exc_info=True ) async def set_take_profit_stop_loss( tg_id: int, message, take_profit_price: float, stop_loss_price: float, tpsl_mode="Full", ): """ Устанавливает уровни Take Profit и Stop Loss для открытой позиции. """ symbol = await rq.get_symbol(tg_id) client = await get_bybit_client(tg_id) await cancel_all_tp_sl_orders(tg_id, symbol) try: try: client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode=tpsl_mode) except exceptions.InvalidRequestError as e: if "same tp sl mode" in str(e).lower(): logger.info(f"Режим TP/SL уже установлен для {symbol}") else: raise resp = client.set_trading_stop( category="linear", symbol=symbol, takeProfit=str(round(take_profit_price, 5)), stopLoss=str(round(stop_loss_price, 5)), tpTriggerBy="LastPrice", slTriggerBy="LastPrice", positionIdx=0, reduceOnly=False, tpslMode=tpsl_mode, ) if resp.get("retCode") != 0: await message.answer( f"Ошибка обновления TP/SL: {resp.get('retMsg')}", reply_markup=inline_markup.back_to_main, ) return await message.answer( f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}", reply_markup=inline_markup.back_to_main, ) except Exception as e: logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True) await message.answer( "Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main, ) async def cancel_all_tp_sl_orders(tg_id, symbol): """ Отменяет лимитные ордера для указанного символа. """ client = await get_bybit_client(tg_id) last_response = None try: orders_resp = client.get_open_orders(category="linear", symbol=symbol) orders = orders_resp.get("result", {}).get("list", []) for order in orders: order_id = order.get("orderId") order_symbol = order.get("symbol") cancel_resp = client.cancel_order( category="linear", symbol=symbol, orderId=order_id ) if cancel_resp.get("retCode") != 0: logger.warning( f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}" ) else: last_response = order_symbol except Exception as e: logger.error(f"Ошибка при отмене ордера: {e}") return last_response async def get_active_positions(tg_id, message): """ Показывает активные позиции пользователя. """ client = await get_bybit_client(tg_id) active_positions = client.get_positions(category="linear", settleCoin="USDT") positions = active_positions.get("result", {}).get("list", []) active_symbols = [ pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0 ] if active_symbols: await message.answer( "📈 Ваши активные позиции:", reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols), ) else: await message.answer( "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main ) return async def get_active_positions_by_symbol(tg_id, symbol, message): """ Показывает активные позиции пользователя по символу. """ client = await get_bybit_client(tg_id) active_positions = client.get_positions(category="linear", symbol=symbol) positions = active_positions.get("result", {}).get("list", []) pos = positions[0] if positions else None if float(pos.get("size", 0)) == 0: await message.answer( "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main ) return text = ( f"Торговая пара: {pos.get('symbol')}\n" f"Цена входа: {pos.get('avgPrice')}\n" f"Движение: {pos.get('side')}\n" f"Кредитное плечо: {pos.get('leverage')}x\n" f"Количество: {pos.get('size')}\n" f"Тейк-профит: {pos.get('takeProfit')}\n" f"Стоп-лосс: {pos.get('stopLoss')}\n" ) await message.answer( text, reply_markup=inline_markup.create_close_deal_markup(symbol) ) async def get_active_orders(tg_id, message): """ Показывает активные лимитные ордера пользователя. """ client = await get_bybit_client(tg_id) response = client.get_open_orders( category="linear", settleCoin="USDT", orderType="Limit" ) orders = response.get("result", {}).get("list", []) limit_orders = [order for order in orders if order.get("orderType") == "Limit"] if limit_orders: symbols = [order["symbol"] for order in limit_orders] await message.answer( "📈 Ваши активные лимитные ордера:", reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols), ) else: await message.answer( "❗️ У вас нет активных лимитных ордеров.", reply_markup=inline_markup.back_to_main, ) return async def get_active_orders_by_symbol(tg_id, symbol, message): """ Показывает активные лимитные ордера пользователя по символу. """ client = await get_bybit_client(tg_id) active_orders = client.get_open_orders(category="linear", symbol=symbol) limit_orders = [ order for order in active_orders.get("result", {}).get("list", []) if order.get("orderType") == "Limit" ] if not limit_orders: await message.answer( "Нет активных лимитных ордеров по данной торговой паре.", reply_markup=inline_markup.back_to_main, ) return texts = [] for order in limit_orders: text = ( f"Торговая пара: {order.get('symbol')}\n" f"Тип ордера: {order.get('orderType')}\n" f"Сторона: {order.get('side')}\n" f"Цена: {order.get('price')}\n" f"Количество: {order.get('qty')}\n" f"Тейк-профит: {order.get('takeProfit')}\n" f"Стоп-лосс: {order.get('stopLoss')}\n" ) texts.append(text) await message.answer( "\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol) ) async def close_user_trade(tg_id: int, symbol: str): """ Закрывает открытые позиции пользователя по символу рыночным ордером. Возвращает True при успехе, False при ошибках. """ try: client = await get_bybit_client(tg_id) positions_resp = client.get_positions(category="linear", symbol=symbol) if positions_resp.get("retCode") != 0: return False positions_list = positions_resp.get("result", {}).get("list", []) if not positions_list: return False position = positions_list[0] qty = abs(safe_float(position.get("size"))) side = position.get("side") if qty == 0: return False close_side = "Sell" if side == "Buy" else "Buy" place_resp = client.place_order( category="linear", symbol=symbol, side=close_side, orderType="Market", qty=str(qty), timeInForce="GTC", reduceOnly=True, ) if place_resp.get("retCode") == 0: return True else: return False except Exception as e: logger.error( f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True, ) return False async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int): """ Закрывает сделку пользователя после задержки delay_sec секунд. """ try: await asyncio.sleep(delay_sec) result = await close_user_trade(tg_id, symbol) if result: await message.answer( f"Сделка {symbol} успешно закрыта по таймеру." ) logger.info(f"Сделка {symbol} успешно закрыта по таймеру.") else: await message.answer( f"Не удалось закрыть сделку {symbol} по таймеру.", reply_markup=inline_markup.back_to_main, ) logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.") except asyncio.CancelledError: await message.answer( f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main, ) logger.info(f"Закрытие сделки {symbol} по таймеру отменено.")