import asyncio import functools 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") active_start_tasks = {} active_close_tasks = {} def safe_float(val): try: if val is None or val == '': return 0.0 return float(val) except (ValueError, TypeError): return 0.0 async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty, tp, sl, entry_price, limit_price, order_type): human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross' text = f'''{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}! Торговая пара: {symbol} Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)} Движение: {trade_mode} Тип-маржи: {human_margin_mode} Кредитное плечо: {leverage}x Количество: {qty} Тейк-профит: {round(tp, 5)} Стоп-лосс: {round(sl, 5)} ''' await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.create_close_deal_markup(symbol)) async def error_max_step(message): logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.', reply_markup=inline_markup.back_to_main) async def error_max_risk(message): 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'): 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')) commission_fee = safe_float(data_risk_stgs.get('commission_fee', 0)) 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"Пожалуйста, увеличьте количество позиций.") 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, # Market или Limit 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 info_access_open_deal(message, symbol, data_main_stgs.get('trading_mode', ''), bybit_margin_mode, data_main_stgs.get('size_leverage', 1), next_quantity, take_profit_price, stop_loss_price, entry_price, limit_price, order_type=order_type) 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 fetch_positions_async(client, symbol): loop = asyncio.get_running_loop() # запускаем блокирующий вызов get_positions в отдельном потоке return await loop.run_in_executor(None, functools.partial(client.get_positions, category='linear', symbol=symbol)) async def get_active_positions(message, api_key, secret_key): client = HTTP( api_key=api_key, api_secret=secret_key ) instruments_resp = client.get_instruments_info(category='linear') if instruments_resp.get('retCode') != 0: return [] symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])] active_positions = [] for sym in symbols: try: resp = await fetch_positions_async(client, sym) if resp.get('retCode') == 0: positions = resp.get('result', {}).get('list', []) for pos in positions: if pos.get('size') and safe_float(pos['size']) > 0: active_positions.append(pos) except Exception as e: logger.error(f"Ошибка при получении позиций: {e}") await message.answer('⚠️ Ошибка при получении позиций', reply_markup=inline_markup.back_to_main) return active_positions async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool: try: 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) # Получаем текущие открытые позиции 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 # Получаем настройки пользователя data_main_stgs = await rq.get_user_main_settings(tg_id) order_type = data_main_stgs.get('entry_order_type') limit_price = await rq.get_limit_price(tg_id) # Получаем открытые ордера orders = client.get_open_orders(category='linear', symbol=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', {}) # поддержать оба варианта: result это dict с key 'list', или list 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)) if order_type == 'Limit': # Если есть открытый лимитный ордер – отменяем его if order_id: cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id) if cancel_resp.get('retCode') != 0: if message: await message.answer("Ошибка при отмене лимитного ордера.", reply_markup=inline_markup.back_to_main) return False # Можно здесь добавить логику выставления лимитного ордера на закрытие, если нужно # В текущем коде отсутствует if message: await message.answer(f"Лимитный ордер отменён, позиция не закрыта автоматически.", reply_markup=inline_markup.back_to_main) return False else: # Рыночный ордер для закрытия позиции 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: if message: pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0 text = (f"Сделка {symbol} успешно закрыта.\n" f"Цена входа: {entry_price if entry_price else limit_price}\n" f"Цена закрытия: {current_price}\n" f"Результат: {pnl:.4f} USDT ({pnl_percent:.2f}%)") await message.answer(text, reply_markup=inline_markup.back_to_main) 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): try: await asyncio.sleep(delay_sec) result = await close_user_trade(tg_id, symbol) 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) finally: active_close_tasks.pop(tg_id, None) def get_positive_percent(negative_percent: float, manual_positive_percent: float | None) -> float: if manual_positive_percent and manual_positive_percent > 0: return manual_positive_percent return abs(negative_percent)