import asyncio import json import time import logging.config from pybit import exceptions from pybit.unified_trading import HTTP from logger_helper.logger_helper import LOGGING_CONFIG import app.services.Bybit.functions.price_symbol as price_symbol import app.services.Bybit.functions.balance as balance_g import app.telegram.database.requests as rq import app.telegram.Keyboards.inline_keyboards as inline_markup logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("futures") 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") return 0.0 def format_trade_details_position(data, commission_fee): """ Форматирует информацию о сделке в виде строки. """ msg = data.get('data', [{}])[0] closed_size = float(msg.get('closedSize', 0)) symbol = msg.get('symbol', 'N/A') entry_price = float(msg.get('execPrice', 0)) qty = float(msg.get('execQty', 0)) order_type = msg.get('orderType', 'N/A') side = msg.get('side', '') commission = float(msg.get('execFee', 0)) pnl = float(msg.get('execPnl', 0)) if commission_fee == "Да": if pnl >= 0: pnl -= commission else: 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" ) else: return ( f"Сделка открыта:\n" f"Торговая пара: {symbol}\n" f"Цена исполнения: {entry_price:.6f}\n" f"Количество: {qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" f"Комиссия за сделку: {commission:.6f}" ) def parse_pnl_from_msg(msg) -> float: """ Извлекает реализованную прибыль/убыток из сообщения. """ try: return float(msg.get('realisedPnl', 0)) except Exception as e: logger.error(f"Ошибка при извлечении реализованной прибыли: {e}") return 0.0 async def handle_execution_message(message, msg: dict) -> None: """ Обработчик сообщений об исполнении сделки. Логирует событие и проверяет условия для мартингейла и TP. """ logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) take_profit_percent = safe_float(data_main_stgs.get('take_profit_percent', 2)) commission_fee = data_main_risk_stgs.get('commission_fee', "ДА") symbol = await rq.get_symbol(tg_id) api_key = await rq.get_bybit_api_key(tg_id) api_secret = await rq.get_bybit_secret_key(tg_id) client = HTTP(api_key=api_key, api_secret=api_secret) positions_resp = client.get_positions(category='linear', symbol=symbol) positions_list = positions_resp.get('result', {}).get('list', []) position = positions_list[0] if positions_list else None trade_info = format_trade_details_position(msg, commission_fee=commission_fee) await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) liquidation_threshold = -100 if pnl <= liquidation_threshold: current_step = int(await rq.get_martingale_step(tg_id)) current_step += 1 await rq.update_martingale_step(tg_id, current_step) side = 'Buy' if position and position.get('side', '').lower() == 'long' else 'Sell' margin_mode = data_main_stgs.get('margin_type', 'Isolated') await open_position(tg_id, message, side=side, margin_mode=margin_mode) elif position: entry_price = safe_float(position.get('avgPrice')) side = position.get('side', '') current_price = float(position.get('markPrice', 0)) if side.lower() == 'long': take_profit_trigger_price = entry_price * (1 + take_profit_percent / 100) if current_price >= take_profit_trigger_price: await close_user_trade(tg_id, symbol, message) await rq.update_martingale_step(tg_id, 0) elif side.lower() == 'short': take_profit_trigger_price = entry_price * (1 - take_profit_percent / 100) if current_price <= take_profit_trigger_price: await close_user_trade(tg_id, symbol, message) await rq.update_martingale_step(tg_id, 0) 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, tpsl_mode='Full'): """ Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях. """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) symbol = await rq.get_symbol(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id) order_type = data_main_stgs.get('entry_order_type') 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) bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN' client = HTTP(api_key=api_key, api_secret=secret_key) 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 try: balance = await balance_g.get_balance(tg_id, message) price = await price_symbol.get_price(tg_id) client.set_margin_mode(setMarginMode=bybit_margin_mode) martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0)) starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal')) loss_profit = safe_float(data_risk_stgs.get('price_loss')) take_profit = safe_float(data_risk_stgs.get('price_profit')) positions_resp = client.get_positions(category='linear', symbol=symbol) positions_list = positions_resp.get('result', {}).get('list', []) if positions_list: position = positions_list[0] size = safe_float(position.get('size', 0)) side_pos = position.get('side', '') if size > 0 and side_pos: entry_price = safe_float(position.get('avgPrice', price)) else: entry_price = price else: entry_price = price if order_type == 'Market': base_price = entry_price else: base_price = limit_price if side.lower() == 'buy': take_profit_price = base_price * (1 + take_profit / 100) stop_loss_price = base_price * (1 - loss_profit / 100) else: take_profit_price = base_price * (1 - take_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) current_martingale_step = 0 next_quantity = starting_quantity realised_pnl = 0.0 current_martingale = await rq.get_martingale_step(tg_id) current_martingale_step = int(current_martingale) if positions_list: if realised_pnl > 0: current_martingale_step = 0 next_quantity = starting_quantity else: current_martingale_step += 1 if current_martingale_step > max_martingale_steps: await error_max_step(message) return next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) else: next_quantity = starting_quantity current_martingale_step = 0 potential_loss = safe_float(next_quantity) * safe_float(price) * (loss_profit / 100) allowed_loss = safe_float(balance) * (max_risk_percent / 100) if potential_loss > allowed_loss: await error_max_risk(message) return instruments_resp = client.get_instruments_info(category='linear', symbol=symbol) if instruments_resp.get('retCode') == 0: instrument_info = instruments_resp.get('result', {}).get('list', []) if instrument_info: instrument = instrument_info[0] min_order_qty = float(instrument.get('minOrderQty', 0)) min_order_value_api = float(instrument.get('minOrderValue', 0)) if min_order_value_api == 0: min_order_value_api = 5.0 # Рассчитываем по формуле: min_order_value_calc = min_order_qty * price if min_order_qty > 0 else 0 # Минимальное значение из значений параметров на бирже min_order_value = max(min_order_value_calc, min_order_value_api) else: min_order_value = 5.0 order_value = float(next_quantity) * float(price) 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 leverage = int(data_main_stgs.get('size_leverage', 1)) try: resp = client.set_leverage( category='linear', symbol=symbol, buyLeverage=str(leverage), sellLeverage=str(leverage) ) except exceptions.InvalidRequestError as e: if "110043" in str(e): logger.info(f"Leverage already set to {leverage} for {symbol}") else: raise e 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(next_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: await rq.update_martingale_step(tg_id, current_martingale_step) return True else: logger.error(f"Ошибка открытия ордера: {response}") await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) return False except exceptions.InvalidRequestError as e: logger.error(f"InvalidRequestError: {e}") await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.', reply_markup=inline_markup.back_to_main) except Exception as e: logger.error(f"Ошибка при совершении сделки: {e}") await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main) async def trading_cycle(tg_id, message): """ Цикл торговой логики с учётом таймера пользователя. """ 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) data_main_stgs = await rq.get_user_main_settings(tg_id) side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell' margin_mode = data_main_stgs.get('margin_type', 'Isolated') await open_position(tg_id, message, side=side, margin_mode=margin_mode) except asyncio.CancelledError: logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.") 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 для открытой позиции. """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) symbol = await rq.get_symbol(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id) order_type = data_main_stgs.get('entry_order_type') starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) 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) trading_mode = data_main_stgs.get('trading_mode') side = None if trading_mode == 'Long': side = 'Buy' elif trading_mode == 'Short': side = 'Sell' if side is None: await message.answer("Не удалось определить сторону сделки.") return client = HTTP(api_key=api_key, api_secret=secret_key) 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 positions_resp = client.get_positions(category='linear', symbol=symbol) positions = positions_resp.get('result', {}).get('list', []) if not positions or abs(float(positions[0].get('size', 0))) == 0: params = dict( category='linear', symbol=symbol, side=side, orderType=order_type, qty=str(starting_quantity), timeInForce='GTC', orderLinkId=f"deal_{symbol}_{int(time.time())}", takeProfit=str(take_profit_price), stopLoss=str(stop_loss_price), tpOrderType='Limit' if tpsl_mode == 'Partial' else 'Market', slOrderType='Limit' if tpsl_mode == 'Partial' else 'Market', tpslMode=tpsl_mode ) if order_type == 'Limit' and limit_price is not None: params['price'] = str(limit_price) if tpsl_mode == 'Partial': params['tpLimitPrice'] = str(take_profit_price) params['slLimitPrice'] = str(stop_loss_price) response = client.place_order(**params) if response.get('retCode') != 0: await message.answer(f"Ошибка создания ордера с TP/SL: {response.get('retMsg')}", reply_markup=inline_markup.back_to_main) return else: 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', reduceOnly=False ) 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): """ Отменяет все открытые ордера TP/SL для указанного символа. """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) client = HTTP(api_key=api_key, api_secret=secret_key) 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') cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id) last_response = cancel_resp if cancel_resp.get('retCode') != 0: logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}") except Exception as e: logger.error(f"Ошибка при отмене ордеров TP/SL: {e}") return last_response async def get_active_positions_by_symbol(tg_id, message): """ Показывает активные позиции пользователя по символу. """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) client = HTTP(api_key=api_key, api_secret=secret_key) symbol = await rq.get_symbol(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_by_symbol(tg_id, message): """ Показывает активные лимитные ордера пользователя по символу. """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) symbol = await rq.get_symbol(tg_id) client = HTTP(api_key=api_key, api_secret=secret_key) 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" f"Кредитное плечо: {order.get('leverage')}\n" ) texts.append(text) await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_markup(symbol)) async def close_user_trade(tg_id: int, symbol: str, message): """ Закрывает открытые позиции пользователя по символу рыночным ордером. Возвращает True при успехе, False при ошибках. """ try: api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) limit_price = await rq.get_limit_price(tg_id) include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да' client = HTTP(api_key=api_key, api_secret=secret_key) 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') entry_price = safe_float(position.get('avgPrice')) if qty == 0: return False orders = client.get_open_orders(category='linear', symbol=symbol) cancel_resp = await cancel_all_tp_sl_orders(tg_id, symbol) open_orders_list = orders.get('result', {}).get('list', []) order_id = open_orders_list[0].get('orderId') if open_orders_list else None close_side = "Sell" if side == "Buy" else "Buy" ticker_resp = client.get_tickers(category="linear", symbol=symbol) current_price = 0.0 if ticker_resp.get('retCode') == 0: result = ticker_resp.get('result', {}) ticker_list = [] if isinstance(result, dict): ticker_list = result.get('list', []) elif isinstance(result, list): ticker_list = result if ticker_list: current_price = float(ticker_list[0].get('lastPrice', 0.0)) 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', -1) == 0: trade_fee = 0 try: trades_resp = client.get_closed_pnl(category="linear", symbol=symbol) if trades_resp.get('retCode') == 0: trades = trades_resp.get('result', {}).get('list', []) for trade in trades: if trade.get('orderId') == order_id: trade_fee += float(trade.get('execFee', 0)) except Exception as e: logger.error(f"Ошибка при получении сделок: {e}") trade_fee = 0 pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty if include_fee: pnl -= trade_fee pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0 return True else: if message: await message.answer(f"Ошибка закрытия сделки {symbol}.", reply_markup=inline_markup.back_to_main) return False except Exception as e: logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True) if message: await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main) 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, message) if result: await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", reply_markup=inline_markup.back_to_main) else: await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.", reply_markup=inline_markup.back_to_main) except asyncio.CancelledError: await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main)