diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 4412bf4..4a46099 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -14,6 +14,8 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("futures") +processed_trade_ids = set() + def safe_float(val) -> float: """ @@ -100,6 +102,13 @@ async def handle_execution_message(message, msg: dict) -> None: """ # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + trade_id = msg.get('data', [{}])[0].get('orderId') + if trade_id in processed_trade_ids: + logger.info(f"Уже обработана сделка {trade_id}, дублирующее уведомление игнорируется") + return + + processed_trade_ids.add(trade_id) + pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id @@ -475,7 +484,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa 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) @@ -487,26 +496,49 @@ async def cancel_all_tp_sl_orders(tg_id, symbol): 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) - last_response = cancel_resp 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"Ошибка при отмене ордеров TP/SL: {e}") + logger.error(f"Ошибка при отмене ордера: {e}") return last_response -async def get_active_positions_by_symbol(tg_id, message): +async def get_active_positions(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) + + 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): """ Показывает активные позиции пользователя по символу. """ 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 @@ -526,14 +558,33 @@ async def get_active_positions_by_symbol(tg_id, message): await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) +async def get_active_orders(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) -async def get_active_orders_by_symbol(tg_id, message): + 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): """ Показывает активные лимитные ордера пользователя по символу. """ 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) @@ -561,7 +612,7 @@ async def get_active_orders_by_symbol(tg_id, message): ) texts.append(text) - await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_markup(symbol)) + 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, message): @@ -574,7 +625,6 @@ async def close_user_trade(tg_id: int, symbol: str, message): 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) @@ -589,66 +639,29 @@ async def close_user_trade(tg_id: int, symbol: str, message): 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 + 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: + await message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) + return True + else: + 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) + await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main) return False diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 4ac7ee0..f4f7c1a 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -7,6 +7,7 @@ from logger_helper.logger_helper import LOGGING_CONFIG from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \ get_active_positions_by_symbol, get_active_orders_by_symbol, + get_active_positions, get_active_orders, cancel_all_tp_sl_orders, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -264,8 +265,7 @@ async def show_my_trades(callback: CallbackQuery) -> None: """ await callback.answer() try: - symbol = await rq.get_symbol(callback.from_user.id) - await callback.message.answer(f"Выберите тип сделки для пары {symbol}:", + await callback.message.answer(f"Выберите тип сделки:", reply_markup=inline_markup.my_deals_select_markup) except Exception as e: logger.error(f"Произошла ошибка при выборе типа сделки: {e}") @@ -274,16 +274,30 @@ async def show_my_trades(callback: CallbackQuery) -> None: @router_functions_bybit_trade.callback_query(F.data == "clb_open_deals") async def show_my_trades_callback(callback: CallbackQuery): """ - Показывает открытые позиции пользователя по символу. + Показывает открытые позиции пользователя. """ await callback.answer() try: - await get_active_positions_by_symbol(callback.from_user.id, message=callback.message) + await get_active_positions(callback.from_user.id, message=callback.message) except Exception as e: logger.error(f"Произошла ошибка при выборе сделки: {e}") await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) +@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_")) +async def show_deal_callback(callback_query: CallbackQuery) -> None: + """ + Показывает сделку пользователя по символу. + """ + await callback_query.answer() + try: + symbol = callback_query.data[len("show_deal_"):] + tg_id = callback_query.from_user.id + await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message) + except Exception as e: + logger.error(f"Произошла ошибка при выборе сделки: {e}") + await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + @router_functions_bybit_trade.callback_query(F.data == "clb_open_orders") async def show_my_orders_callback(callback: CallbackQuery) -> None: @@ -293,16 +307,31 @@ async def show_my_orders_callback(callback: CallbackQuery) -> None: await callback.answer() try: - await get_active_orders_by_symbol(callback.from_user.id, message=callback.message) + await get_active_orders(callback.from_user.id, message=callback.message) except Exception as e: logger.error(f"Произошла ошибка при выборе ордера: {e}") await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main) +@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_limit_")) +async def show_limit_callback(callback_query: CallbackQuery) -> None: + """ + Показывает сделку пользователя по символу. + """ + await callback_query.answer() + try: + symbol = callback_query.data[len("show_limit_"):] + tg_id = callback_query.from_user.id + await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message) + except Exception as e: + logger.error(f"Произошла ошибка при выборе сделки: {e}") + await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + + @router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl") async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None: """ - Показывает активные ордера пользователя. + Запускает процесс установки Take Profit и Stop Loss. """ await callback.answer() await state.set_state(SetTP_SL_State.waiting_for_take_profit) @@ -394,6 +423,25 @@ async def close_trade_callback(callback: CallbackQuery) -> None: await callback.answer() +@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:")) +async def close_trade_callback(callback: CallbackQuery) -> None: + """ + Закрывает ордера пользователя по символу. + """ + symbol = callback.data.split(':')[1] + tg_id = callback.from_user.id + + result = await cancel_all_tp_sl_orders(tg_id, symbol) + + if result: + await callback.message.answer(f"Ордер {result} успешно закрыт.", reply_markup=inline_markup.back_to_main) + logger.info(f"Ордер {result} успешно закрыт.") + else: + await callback.message.answer(f"Не удалось закрыть ордер {result}.") + + await callback.answer() + + @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:")) async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None: """ diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 19de990..140c05e 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -155,15 +155,21 @@ buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТ my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"), - InlineKeyboardButton(text='Открытые ордера', callback_data="clb_open_orders")], + InlineKeyboardButton(text='Лимитные ордера', callback_data="clb_open_orders")], back_btn_to_main ]) def create_trades_inline_keyboard(trades): builder = InlineKeyboardBuilder() for trade in trades: - symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol - builder.button(text=symbol, callback_data=f"show_deal_{symbol}") + builder.button(text=trade, callback_data=f"show_deal_{trade}") + builder.adjust(2) + return builder.as_markup() + +def create_trades_inline_keyboard_limits(trades): + builder = InlineKeyboardBuilder() + for trade in trades: + builder.button(text=trade, callback_data=f"show_limit_{trade}") builder.adjust(2) return builder.as_markup() @@ -176,6 +182,12 @@ def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: back_btn_to_main ]) +def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")], + [InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl_")], + back_btn_to_main + ]) timer_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index 53d699d..f600403 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -33,7 +33,7 @@ async def price_profit_message(message, state): text = 'Введите число изменения цены для фиксации прибыли: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.price_profit) @@ -62,7 +62,7 @@ async def price_loss_message(message, state): text = 'Введите число изменения цены для фиксации убытков: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.price_loss) @@ -111,7 +111,7 @@ async def max_risk_deal_message(message, state): text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)