From 15086297270b67ea1a7700aa7c7b569f5d615b26 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 3 Oct 2025 14:19:18 +0500 Subject: [PATCH] Fixed --- app/bybit/get_functions/get_positions.py | 8 +- app/bybit/open_positions.py | 26 ++-- .../set_functions/set_switch_position_mode.py | 6 + app/bybit/set_functions/set_tp_sl.py | 7 +- app/bybit/telegram_message_handler.py | 40 +++-- app/bybit/web_socket.py | 1 - .../handlers/get_positions_handlers.py | 133 +++++++++++----- .../main_settings/additional_settings.py | 7 + app/telegram/handlers/start_trading.py | 142 +++++++++--------- app/telegram/handlers/stop_trading.py | 59 +++++--- app/telegram/handlers/tp_sl_handlers.py | 9 +- app/telegram/keyboards/inline.py | 54 ++++--- database/models.py | 9 +- database/request.py | 111 +++++++++++--- logger_helper/logger_helper.py | 10 ++ 15 files changed, 412 insertions(+), 210 deletions(-) diff --git a/app/bybit/get_functions/get_positions.py b/app/bybit/get_functions/get_positions.py index 83e808c..1f6606a 100644 --- a/app/bybit/get_functions/get_positions.py +++ b/app/bybit/get_functions/get_positions.py @@ -22,7 +22,7 @@ async def get_active_positions(tg_id: int) -> list | None: ] if active_symbols: logger.info("Active positions for user: %s", tg_id) - return active_symbols + return positions else: logger.warning("No active positions found for user: %s", tg_id) return ["No active positions found"] @@ -50,7 +50,7 @@ async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None positions = response.get("result", {}).get("list", []) if positions: logger.info("Active positions for user: %s", tg_id) - return positions[0] + return positions else: logger.warning("No active positions found for user: %s", tg_id) return None @@ -85,7 +85,7 @@ async def get_active_orders(tg_id: int) -> list | None: ] if active_orders: logger.info("Active orders for user: %s", tg_id) - return active_orders + return orders else: logger.warning("No active orders found for user: %s", tg_id) return ["No active orders found"] @@ -115,7 +115,7 @@ async def get_active_orders_by_symbol(tg_id: int, symbol: str) -> dict | None: orders = response.get("result", {}).get("list", []) if orders: logger.info("Active orders for user: %s", tg_id) - return orders[0] + return orders else: logger.warning("No active orders found for user: %s", tg_id) return None diff --git a/app/bybit/open_positions.py b/app/bybit/open_positions.py index 0fa3972..f3bd6d4 100644 --- a/app/bybit/open_positions.py +++ b/app/bybit/open_positions.py @@ -169,6 +169,7 @@ async def trading_cycle(tg_id: int, symbol: str, reverse_side: str, size: str) - leverage=leverage, ) + if reverse_side == "Buy": real_side = "Sell" else: @@ -219,7 +220,7 @@ async def trading_cycle(tg_id: int, symbol: str, reverse_side: str, size: str) - leverage_to_sell=leverage_to_sell, order_type=order_type, conditional_order_type=conditional_order_type, - order_quantity=current_step, + order_quantity=next_quantity, limit_price=limit_price, trigger_price=trigger_price, martingale_factor=martingale_factor, @@ -258,9 +259,9 @@ async def open_positions( trigger_price: float, trade_mode: str, margin_type: str, - leverage: float, - leverage_to_buy: float, - leverage_to_sell: float, + leverage: str, + leverage_to_buy: str, + leverage_to_sell: str, take_profit_percent: float, stop_loss_percent: float, max_risk_percent: float, @@ -317,19 +318,22 @@ async def open_positions( if trade_mode == "Both_Sides": po_position_idx = 1 if side == "Buy" else 2 - leverage = safe_float( + if margin_type == "ISOLATED_MARGIN": + get_leverage = safe_float( leverage_to_buy if side == "Buy" else leverage_to_sell ) + else: + get_leverage = safe_float(leverage) else: po_position_idx = 0 - leverage = safe_float(leverage) + get_leverage = safe_float(leverage) potential_loss = ( safe_float(order_quantity) * safe_float(price_for_calc) * (stop_loss_percent / 100) ) - adjusted_loss = potential_loss / leverage + adjusted_loss = potential_loss / get_leverage allowed_loss = safe_float(user_balance) * (max_risk_percent / 100) if adjusted_loss > allowed_loss: @@ -356,10 +360,11 @@ async def open_positions( entry_price=price_for_calc, symbol=symbol, order_quantity=order_quantity, + leverage=get_leverage, ) - if liq_long > 0 or liq_short > 0 and price_for_calc > 0: - if side.lower() == "buy": + if (liq_long > 0 or liq_short > 0) and price_for_calc > 0: + if side == "Buy": base_tp = price_for_calc + (price_for_calc - liq_long) take_profit_price = base_tp + total_commission else: @@ -371,7 +376,7 @@ async def open_positions( stop_loss_price = None else: - if side.lower() == "buy": + if side == "Buy": take_profit_price = price_for_calc * tp_multiplier stop_loss_price = price_for_calc * (1 - stop_loss_percent / 100) else: @@ -383,6 +388,7 @@ async def open_positions( take_profit_price = max(take_profit_price, 0) stop_loss_price = max(stop_loss_price, 0) + # Place order order_params = { "category": "linear", diff --git a/app/bybit/set_functions/set_switch_position_mode.py b/app/bybit/set_functions/set_switch_position_mode.py index aa9abba..046bedb 100644 --- a/app/bybit/set_functions/set_switch_position_mode.py +++ b/app/bybit/set_functions/set_switch_position_mode.py @@ -43,6 +43,12 @@ async def set_switch_position_mode(tg_id: int, symbol: str, mode: int) -> str | tg_id, ) return "You have an existing position, so position mode cannot be switched" + if str(e).startswith("Open orders exist, so you cannot change position mode"): + logger.debug( + "Open orders exist, so you cannot change position mode for user: %s", + tg_id, + ) + return "Open orders exist, so you cannot change position mode" else: logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) return False diff --git a/app/bybit/set_functions/set_tp_sl.py b/app/bybit/set_functions/set_tp_sl.py index 011beaf..eded48f 100644 --- a/app/bybit/set_functions/set_tp_sl.py +++ b/app/bybit/set_functions/set_tp_sl.py @@ -1,7 +1,6 @@ import logging.config from app.bybit import get_bybit_client -from app.bybit.get_functions.get_positions import get_active_positions_by_symbol from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG logging.config.dictConfig(LOGGING_CONFIG) @@ -13,6 +12,7 @@ async def set_tp_sl_for_position( symbol: str, take_profit_price: float, stop_loss_price: float, + position_idx: int, ) -> bool: """ Set take profit and stop loss for a symbol. @@ -20,14 +20,11 @@ async def set_tp_sl_for_position( :param symbol: Symbol to set take profit and stop loss for :param take_profit_price: Take profit price :param stop_loss_price: Stop loss price + :param position_idx: Position index :return: bool """ try: client = await get_bybit_client(tg_id) - user_positions = await get_active_positions_by_symbol( - tg_id=tg_id, symbol=symbol - ) - position_idx = user_positions.get("positionIdx") resp = client.set_trading_stop( category="linear", symbol=symbol, diff --git a/app/bybit/telegram_message_handler.py b/app/bybit/telegram_message_handler.py index 433ed38..6335567 100644 --- a/app/bybit/telegram_message_handler.py +++ b/app/bybit/telegram_message_handler.py @@ -106,12 +106,27 @@ class TelegramMessageHandler: if side == "Buy" else "Продажа" if side == "Sell" else "Нет данных" ) + + if safe_float(closed_size) == 0: + await rq.set_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, side=side, fee=safe_float(exec_fee) + ) + + user_auto_trading = await rq.get_user_auto_trading( + tg_id=tg_id, symbol=symbol + ) + + if user_auto_trading is not None and user_auto_trading.fee is not None: + fee = user_auto_trading.fee + else: + fee = 0 + exec_pnl = format_value(execution.get("execPnl")) risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) commission_fee = risk_management_data.commission_fee if commission_fee == "Yes_commission_fee": - total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) + total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee else: total_pnl = safe_float(exec_pnl) @@ -138,13 +153,12 @@ class TelegramMessageHandler: chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit ) - user_auto_trading = await rq.get_user_auto_trading( - tg_id=tg_id, symbol=symbol - ) auto_trading = ( user_auto_trading.auto_trading if user_auto_trading else False ) - user_symbols = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) + user_symbols = ( + user_auto_trading.symbol if user_auto_trading else None + ) if ( auto_trading @@ -152,20 +166,22 @@ class TelegramMessageHandler: and user_symbols is not None ): if safe_float(total_pnl) > 0: - await rq.set_auto_trading( - tg_id=tg_id, symbol=symbol, auto_trading=False - ) profit_text = "📈 Прибыль достигнута\n" await self.telegram_bot.send_message( chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_auto_trading(tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side) else: open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" await self.telegram_bot.send_message( chat_id=tg_id, text=open_order_text ) res = await trading_cycle( - tg_id=tg_id, symbol=symbol, reverse_side=side + tg_id=tg_id, symbol=symbol, reverse_side=side, size=closed_size ) if res == "OK": @@ -180,8 +196,12 @@ class TelegramMessageHandler: error_text = errors.get( res, "❗️ Не удалось открыть новую сделку" ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" await rq.set_auto_trading( - tg_id=tg_id, symbol=symbol, auto_trading=False + tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side ) await self.telegram_bot.send_message( chat_id=tg_id, diff --git a/app/bybit/web_socket.py b/app/bybit/web_socket.py index d344a61..a0ecc02 100644 --- a/app/bybit/web_socket.py +++ b/app/bybit/web_socket.py @@ -64,7 +64,6 @@ class WebSocketBot: channel_type="private", api_key=api_key, api_secret=api_secret, - restart_on_error=True, ) self.user_sockets[tg_id] = self.ws_private diff --git a/app/telegram/handlers/get_positions_handlers.py b/app/telegram/handlers/get_positions_handlers.py index b1ad99c..c0f1c9d 100644 --- a/app/telegram/handlers/get_positions_handlers.py +++ b/app/telegram/handlers/get_positions_handlers.py @@ -5,7 +5,7 @@ from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery import app.telegram.keyboards.inline as kbi -import database.request as rq + from app.bybit.get_functions.get_positions import ( get_active_orders, get_active_orders_by_symbol, @@ -22,7 +22,7 @@ router_get_positions_handlers = Router(name="get_positions_handlers") @router_get_positions_handlers.callback_query(F.data == "my_deals") async def get_positions_handlers( - callback_query: CallbackQuery, state: FSMContext + callback_query: CallbackQuery, state: FSMContext ) -> None: """ Gets the user's active positions. @@ -43,7 +43,7 @@ async def get_positions_handlers( @router_get_positions_handlers.callback_query(F.data == "change_position") async def get_positions_handler( - callback_query: CallbackQuery, state: FSMContext + callback_query: CallbackQuery, state: FSMContext ) -> None: """ Gets the user's active positions. @@ -64,9 +64,17 @@ async def get_positions_handler( await callback_query.answer(text="Нет активных позиций.") return + active_positions = [pos for pos in res if float(pos.get("size", 0)) > 0] + + if not active_positions: + await callback_query.answer(text="Нет активных позиций.") + return + + active_symbols_sides = [(pos.get("symbol"), pos.get("side")) for pos in active_positions] + await callback_query.message.edit_text( text="Ваши активные позиции:", - reply_markup=kbi.create_active_positions_keyboard(res), + reply_markup=kbi.create_active_positions_keyboard(symbols=active_symbols_sides), ) except Exception as e: logger.error("Error in get_positions_handler: %s", e) @@ -82,7 +90,10 @@ async def get_positions_handler( ) async def get_position_handler(callback_query: CallbackQuery, state: FSMContext): try: - symbol = callback_query.data.split("_", 2)[2] + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + get_side = parts[3] res = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) @@ -93,26 +104,48 @@ async def get_position_handler(callback_query: CallbackQuery, state: FSMContext) ) return - symbol = res.get("symbol") or "Нет данных" - avg_price = res.get("avgPrice") or "Нет данных" - size = res.get("size") or "Нет данных" - side = res.get("side") or "" + position = next((pos for pos in res if pos.get("side") == get_side), None) + + if position: + side = position.get("side") + symbol = position.get("symbol") or "Нет данных" + avg_price = position.get("avgPrice") or "Нет данных" + size = position.get("size") or "Нет данных" + take_profit = position.get("takeProfit") or "Нет данных" + stop_loss = position.get("stopLoss") or "Нет данных" + position_idx = position.get("positionIdx") + else: + side = "Нет данных" + symbol = "Нет данных" + avg_price = "Нет данных" + size = "Нет данных" + take_profit = "Нет данных" + stop_loss = "Нет данных" + position_idx = "Нет данных" + side_rus = ( "Покупка" if side == "Buy" else "Продажа" if side == "Sell" else "Нет данных" ) - take_profit = res.get("takeProfit") or "Нет данных" - stop_loss = res.get("stopLoss") or "Нет данных" + + position_idx_rus = ("Односторонний" if position_idx == 0 + else "Покупка в режиме хеджирования" if position_idx == 1 + else "Продажа в режиме хеджирования" if position_idx == 2 + else "Нет данных") await callback_query.message.edit_text( text=f"Торговая пара: {symbol}\n" - f"Цена входа: {avg_price}\n" - f"Количество: {size}\n" - f"Движение: {side_rus}\n" - f"Тейк-профит: {take_profit}\n" - f"Стоп-лосс: {stop_loss}\n", - reply_markup=kbi.make_close_position_keyboard(symbol_pos=symbol), + f"Режим позиции: {position_idx_rus}\n" + f"Цена входа: {avg_price}\n" + f"Количество: {size}\n" + f"Движение: {side_rus}\n" + f"Тейк-профит: {take_profit}\n" + f"Стоп-лосс: {stop_loss}\n", + reply_markup=kbi.make_close_position_keyboard(symbol_pos=symbol, + side=side, + position_idx=position_idx, + qty=size), ) except Exception as e: @@ -126,7 +159,7 @@ async def get_position_handler(callback_query: CallbackQuery, state: FSMContext) @router_get_positions_handlers.callback_query(F.data == "open_orders") async def get_open_orders_handler( - callback_query: CallbackQuery, state: FSMContext + callback_query: CallbackQuery, state: FSMContext ) -> None: """ Gets the user's open orders. @@ -147,9 +180,17 @@ async def get_open_orders_handler( await callback_query.answer(text="Нет активных ордеров.") return + active_positions = [pos for pos in res if pos.get("orderStatus", 0) == "New"] + + if not active_positions: + await callback_query.answer(text="Нет активных ордеров.") + return + + active_orders_sides = [(pos.get("symbol"), pos.get("side")) for pos in active_positions] + await callback_query.message.edit_text( text="Ваши активные ордера:", - reply_markup=kbi.create_active_orders_keyboard(res), + reply_markup=kbi.create_active_orders_keyboard(orders=active_orders_sides), ) except Exception as e: logger.error("Error in get_open_orders_handler: %s", e) @@ -163,7 +204,10 @@ async def get_open_orders_handler( @router_get_positions_handlers.callback_query(lambda c: c.data.startswith("get_order_")) async def get_order_handler(callback_query: CallbackQuery, state: FSMContext): try: - symbol = callback_query.data.split("_", 2)[2] + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + get_side = parts[3] res = await get_active_orders_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) @@ -174,37 +218,52 @@ async def get_order_handler(callback_query: CallbackQuery, state: FSMContext): ) return - symbol = res.get("symbol") or "Нет данных" - price = res.get("price") or "Нет данных" - qty = res.get("qty") or "Нет данных" - side = res.get("side") or "" + orders = next((pos for pos in res if pos.get("side") == get_side), None) + + if orders: + side = orders.get("side") + symbol = orders.get("symbol") or "Нет данных" + price = orders.get("price") or "Нет данных" + qty = orders.get("qty") or "Нет данных" + order_type = orders.get("orderType") or "" + trigger_price = orders.get("triggerPrice") or "Нет данных" + take_profit = orders.get("takeProfit") or "Нет данных" + stop_loss = orders.get("stopLoss") or "Нет данных" + order_id = orders.get("orderId") + else: + side = "Нет данных" + symbol = "Нет данных" + price = "Нет данных" + qty = "Нет данных" + order_type = "Нет данных" + trigger_price = "Нет данных" + take_profit = "Нет данных" + stop_loss = "Нет данных" + order_id = "Нет данных" + side_rus = ( "Покупка" if side == "Buy" else "Продажа" if side == "Sell" else "Нет данных" ) - order_type = res.get("orderType") or "" + order_type_rus = ( "Рыночный" if order_type == "Market" else "Лимитный" if order_type == "Limit" else "Нет данных" ) - trigger_price = res.get("triggerPrice") or "Нет данных" - take_profit = res.get("takeProfit") or "Нет данных" - stop_loss = res.get("stopLoss") or "Нет данных" await callback_query.message.edit_text( text=f"Торговая пара: {symbol}\n" - f"Цена: {price}\n" - f"Количество: {qty}\n" - f"Движение: {side_rus}\n" - f"Тип ордера: {order_type_rus}\n" - f"Триггер цена: {trigger_price}\n" - f"Тейк-профит: {take_profit}\n" - f"Стоп-лосс: {stop_loss}\n", - reply_markup=kbi.make_close_orders_keyboard(symbol_order=symbol), + f"Цена: {price}\n" + f"Количество: {qty}\n" + f"Движение: {side_rus}\n" + f"Тип ордера: {order_type_rus}\n" + f"Триггер цена: {trigger_price}\n" + f"Тейк-профит: {take_profit}\n" + f"Стоп-лосс: {stop_loss}\n", + reply_markup=kbi.make_close_orders_keyboard(symbol_order=symbol, order_id=order_id), ) - await rq.set_user_symbol(tg_id=callback_query.from_user.id, symbol=symbol) except Exception as e: logger.error("Error in get_order_handler: %s", e) await callback_query.answer( diff --git a/app/telegram/handlers/main_settings/additional_settings.py b/app/telegram/handlers/main_settings/additional_settings.py index d3da981..3e763b8 100644 --- a/app/telegram/handlers/main_settings/additional_settings.py +++ b/app/telegram/handlers/main_settings/additional_settings.py @@ -124,6 +124,13 @@ async def trade_mode(callback_query: CallbackQuery, state: FSMContext) -> None: ) return + if response == "Open orders exist, so you cannot change position mode": + await callback_query.answer( + text="У вас есть открытые ордера, " + "поэтому режим позиции не может быть изменен." + ) + return + if callback_query.data.startswith("Merged_Single"): await callback_query.answer(text="Выбран режим позиции: Односторонний") await set_leverage( diff --git a/app/telegram/handlers/start_trading.py b/app/telegram/handlers/start_trading.py index 5c8db9b..cc05813 100644 --- a/app/telegram/handlers/start_trading.py +++ b/app/telegram/handlers/start_trading.py @@ -5,6 +5,8 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery +from app.telegram.tasks.tasks import add_start_task_merged, cancel_start_task_merged, add_start_task_switch, \ + cancel_start_task_switch import app.telegram.keyboards.inline as kbi import database.request as rq from app.bybit.get_functions.get_positions import get_active_positions_by_symbol @@ -17,8 +19,6 @@ logger = logging.getLogger("start_trading") router_start_trading = Router(name="start_trading") -user_trade_tasks = {} - @router_start_trading.callback_query(F.data == "start_trading") async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> None: @@ -39,36 +39,41 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non deals = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) - size = deals.get("size") or 0 - position_idx = deals.get("positionIdx") + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None if position_idx != 0 and safe_float(size) > 0 and trade_mode == "Merged_Single": await callback_query.answer( text="У вас есть активная позиция в режиме хеджирования. " - "Открытие сделки в одностороннем режиме невозможно.", + "Открытие сделки в одностороннем режиме невозможно.", ) return if position_idx == 0 and safe_float(size) > 0 and trade_mode == "Both_Sides": await callback_query.answer( text="У вас есть активная позиция в одностороннем режиме. " - "Открытие сделки в режиме хеджирования невозможно.", + "Открытие сделки в режиме хеджирования невозможно.", ) return if trade_mode == "Merged_Single": await callback_query.message.edit_text( text="Выберите режим торговли:\n\n" - "Лонг - все сделки серии открываются на покупку.\n" - "Шорт - все сделки серии открываются на продажу.\n" - "Свитч - направление каждой сделки серии меняется по переменно.\n", + "Лонг - все сделки серии открываются на покупку.\n" + "Шорт - все сделки серии открываются на продажу.\n" + "Свитч - направление каждой сделки серии меняется по переменно.\n", reply_markup=kbi.merged_start_trading, ) else: # trade_mode == "Both_Sides": await callback_query.message.edit_text( text="Выберите режим торговли:\n\n" - "Лонг - все сделки открываются на покупку.\n" - "Шорт - все сделки открываются на продажу.\n", + "Лонг - все сделки открываются на покупку.\n" + "Шорт - все сделки открываются на продажу.\n", reply_markup=kbi.both_start_trading, ) logger.debug( @@ -106,8 +111,13 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) - deals = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) - size = deals.get("size") or 0 - position_idx = deals.get("positionIdx") + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None if position_idx == 0 and safe_float(size) > 0: await callback_query.answer( @@ -120,19 +130,22 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) - ) timer_start = conditional_data.timer_start - if callback_query.from_user.id in user_trade_tasks: - task = user_trade_tasks[callback_query.from_user.id] - if not task.done(): - task.cancel() - del user_trade_tasks[callback_query.from_user.id] + cancel_start_task_merged(user_id=callback_query.from_user.id) async def delay_start(): if timer_start > 0: await callback_query.message.edit_text( text=f"Торговля будет запущена с задержкой {timer_start} мин.", - reply_markup=kbi.cancel_timer, + reply_markup=kbi.cancel_timer_merged, + ) + await rq.set_start_timer( + tg_id=callback_query.from_user.id, timer_start=0 ) await asyncio.sleep(timer_start * 60) + + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True, side=side + ) res = await start_trading_cycle( tg_id=callback_query.from_user.id, side=side, @@ -146,27 +159,24 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) - "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", "ab not enough for new order": "Недостаточно средств для создания нового ордера", "InvalidRequestError": "Произошла ошибка при запуске торговли.", - "Order does not meet minimum order value": "Сумма ордера не sufficientдля запуска торговли", + "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли", "position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента", "Qty invalid": "Некорректное значение ордера для данного инструмента", } if res == "OK": - await rq.set_start_timer( - tg_id=callback_query.from_user.id, timer_start=0 - ) - await rq.set_auto_trading( - tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True - ) await callback_query.answer(text="Торговля запущена") await state.clear() else: - # Получаем сообщение из таблицы, или дефолтный текст + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=False, side=side + ) text = error_messages.get(res, "Произошла ошибка при запуске торговли") await callback_query.answer(text=text) + await callback_query.message.edit_text("Запуск торговли...") task = asyncio.create_task(delay_start()) - user_trade_tasks[callback_query.from_user.id] = task + await add_start_task_merged(user_id=callback_query.from_user.id, task=task) except Exception as e: await callback_query.answer(text="Произошла ошибка при запуске торговли") @@ -181,7 +191,7 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) - @router_start_trading.callback_query(lambda c: c.data == "switch") async def start_trading_switch( - callback_query: CallbackQuery, state: FSMContext + callback_query: CallbackQuery, state: FSMContext ) -> None: """ Handles the "switch" callback query. @@ -194,10 +204,10 @@ async def start_trading_switch( await state.clear() await callback_query.message.edit_text( text="Выберите направление первой сделки серии:\n\n" - "Лонг - открывается первая сделка на покупку.\n" - "Шорт - открывается первая сделка на продажу.\n" - "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" - "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", + "Лонг - открывается первая сделка на покупку.\n" + "Шорт - открывается первая сделка на продажу.\n" + "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" + "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", reply_markup=kbi.switch_side, ) except Exception as e: @@ -211,7 +221,7 @@ async def start_trading_switch( @router_start_trading.callback_query( lambda c: c.data - in {"switch_long", "switch_short", "switch_direction", "switch_opposite"} + in {"switch_long", "switch_short", "switch_direction", "switch_opposite"} ) async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None: """ @@ -249,8 +259,13 @@ async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None deals = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) - size = deals.get("size") or 0 - position_idx = deals.get("positionIdx") + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None if position_idx == 1 and safe_float(size) > 0 and side == "Buy": await callback_query.answer( @@ -269,19 +284,21 @@ async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None ) timer_start = conditional_data.timer_start - if callback_query.from_user.id in user_trade_tasks: - task = user_trade_tasks[callback_query.from_user.id] - if not task.done(): - task.cancel() - del user_trade_tasks[callback_query.from_user.id] + cancel_start_task_switch(user_id=callback_query.from_user.id) async def delay_start(): if timer_start > 0: await callback_query.message.edit_text( text=f"Торговля будет запущена с задержкой {timer_start} мин.", - reply_markup=kbi.cancel_timer, + reply_markup=kbi.cancel_timer_switch, + ) + await rq.set_start_timer( + tg_id=callback_query.from_user.id, timer_start=0 ) await asyncio.sleep(timer_start * 60) + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True, side=side + ) res = await start_trading_cycle( tg_id=callback_query.from_user.id, side=side, @@ -295,26 +312,24 @@ async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", "ab not enough for new order": "Недостаточно средств для создания нового ордера", "InvalidRequestError": "Произошла ошибка при запуске торговли.", - "Order does not meet minimum order value": "Сумма ордера не sufficientдля запуска торговли", + "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли", "position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента", "Qty invalid": "Некорректное значение ордера для данного инструмента", } if res == "OK": - await rq.set_start_timer( - tg_id=callback_query.from_user.id, timer_start=0 - ) - await rq.set_auto_trading( - tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True - ) await callback_query.answer(text="Торговля запущена") await state.clear() else: + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=False, side=side + ) text = error_messages.get(res, "Произошла ошибка при запуске торговли") await callback_query.answer(text=text) + await callback_query.message.edit_text("Запуск торговли...") task = asyncio.create_task(delay_start()) - user_trade_tasks[callback_query.from_user.id] = task + await add_start_task_switch(user_id=callback_query.from_user.id, task=task) except asyncio.CancelledError: logger.error("Cancelled timer for user %s", callback_query.from_user.id) except Exception as e: @@ -326,9 +341,9 @@ async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None ) -@router_start_trading.callback_query(F.data == "cancel_timer") +@router_start_trading.callback_query(lambda c: c.data == "cancel_timer_merged" or c.data == "cancel_timer_switch") async def cancel_start_trading( - callback_query: CallbackQuery, state: FSMContext + callback_query: CallbackQuery, state: FSMContext ) -> None: """ Handles the "cancel_timer" callback query. @@ -338,21 +353,12 @@ async def cancel_start_trading( :return: None """ try: - task = user_trade_tasks.get(callback_query.from_user.id) - if task and not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - user_trade_tasks.pop(callback_query.from_user.id, None) - await callback_query.message.edit_text( - "Таймер отменён.", reply_markup=kbi.profile_bybit - ) - else: - await callback_query.message.edit_text( - "Таймер не запущен.", reply_markup=kbi.profile_bybit - ) + await state.clear() + if callback_query.data == "cancel_timer_merged": + cancel_start_task_merged(user_id=callback_query.from_user.id) + elif callback_query.data == "cancel_timer_switch": + cancel_start_task_switch(user_id=callback_query.from_user.id) + await callback_query.message.edit_text(text="Запуск торговли отменен", reply_markup=kbi.profile_bybit) except Exception as e: await callback_query.answer("Произошла ошибка при отмене запуска торговли") logger.error( @@ -360,5 +366,3 @@ async def cancel_start_trading( callback_query.from_user.id, e, ) - finally: - await state.clear() diff --git a/app/telegram/handlers/stop_trading.py b/app/telegram/handlers/stop_trading.py index 0effea7..6d83cb0 100644 --- a/app/telegram/handlers/stop_trading.py +++ b/app/telegram/handlers/stop_trading.py @@ -7,7 +7,7 @@ from aiogram.types import CallbackQuery import app.telegram.keyboards.inline as kbi import database.request as rq -from app.bybit.close_positions import cancel_order, close_position +from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task from logger_helper.logger_helper import LOGGING_CONFIG logging.config.dictConfig(LOGGING_CONFIG) @@ -15,50 +15,46 @@ logger = logging.getLogger("stop_trading") router_stop_trading = Router(name="stop_trading") -user_trade_tasks = {} - @router_stop_trading.callback_query(F.data == "stop_trading") -async def stop_trading(callback_query: CallbackQuery, state: FSMContext): +async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext): try: await state.clear() - if callback_query.from_user.id in user_trade_tasks: - task = user_trade_tasks[callback_query.from_user.id] - if not task.done(): - task.cancel() - del user_trade_tasks[callback_query.from_user.id] + cancel_stop_task(callback_query.from_user.id) conditional_data = await rq.get_user_conditional_settings( tg_id=callback_query.from_user.id ) timer_end = conditional_data.timer_end - symbols = await rq.get_all_symbols(tg_id=callback_query.from_user.id) async def delay_start(): if timer_end > 0: await callback_query.message.edit_text( text=f"Торговля будет остановлена с задержкой {timer_end} мин.", - reply_markup=kbi.cancel_timer, + reply_markup=kbi.cancel_timer_stop, ) + await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0) await asyncio.sleep(timer_end * 60) - for symbol in symbols: - auto_trading_data = await rq.get_user_auto_trading( - tg_id=callback_query.from_user.id, symbol=symbol - ) - if auto_trading_data is not None and auto_trading_data.auto_trading: - await close_position(tg_id=callback_query.from_user.id, symbol=symbol) - await cancel_order(tg_id=callback_query.from_user.id, symbol=symbol) - await rq.set_auto_trading( - tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=False - ) + user_auto_trading_list = await rq.get_all_user_auto_trading(tg_id=callback_query.from_user.id) - await callback_query.answer(text="Торговля остановлена") - await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0) + for active_auto_trading in user_auto_trading_list: + if active_auto_trading.auto_trading: + symbol = active_auto_trading.symbol + get_side = active_auto_trading.side + req = await rq.set_auto_trading( + tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=False, side=get_side + ) + if not req: + await callback_query.edit_text(text="Произошла ошибка при остановке торговли", + reply_markup=kbi.profile_bybit) + return + + await callback_query.message.edit_text(text="Торговля остановлена", reply_markup=kbi.profile_bybit) task = asyncio.create_task(delay_start()) - user_trade_tasks[callback_query.from_user.id] = task + await add_stop_task(user_id=callback_query.from_user.id, task=task) logger.debug( "Command stop_trading processed successfully for user: %s", @@ -71,3 +67,18 @@ async def stop_trading(callback_query: CallbackQuery, state: FSMContext): callback_query.from_user.id, e, ) + + +@router_stop_trading.callback_query(F.data == "cancel_timer_stop") +async def cancel_stop_trading(callback_query: CallbackQuery, state: FSMContext): + try: + await state.clear() + cancel_stop_task(callback_query.from_user.id) + await callback_query.message.edit_text(text="Таймер отменён.", reply_markup=kbi.profile_bybit) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при отмене остановки торговли") + logger.error( + "Error processing command cancel_timer_stop for user %s: %s", + callback_query.from_user.id, + e, + ) \ No newline at end of file diff --git a/app/telegram/handlers/tp_sl_handlers.py b/app/telegram/handlers/tp_sl_handlers.py index 1ef38b9..8bc9c24 100644 --- a/app/telegram/handlers/tp_sl_handlers.py +++ b/app/telegram/handlers/tp_sl_handlers.py @@ -29,9 +29,14 @@ async def set_tp_sl_handler(callback_query: CallbackQuery, state: FSMContext) -> """ try: await state.clear() - symbol = callback_query.data.split("_", 3)[3] + data = callback_query.data + parts = data.split("_") + symbol = parts[3] + position_idx = int(parts[4]) + await state.set_state(SetTradingStopState.take_profit_state) await state.update_data(symbol=symbol) + await state.update_data(position_idx=position_idx) msg = await callback_query.message.answer( text="Введите тейк-профит:", reply_markup=kbi.cancel ) @@ -142,11 +147,13 @@ async def set_stop_loss_handler(message: Message, state: FSMContext) -> None: data = await state.get_data() symbol = data["symbol"] take_profit = data["take_profit"] + position_idx = data["position_idx"] res = await set_tp_sl_for_position( tg_id=message.from_user.id, symbol=symbol, take_profit_price=float(take_profit), stop_loss_price=float(stop_loss), + position_idx=position_idx, ) if res: diff --git a/app/telegram/keyboards/inline.py b/app/telegram/keyboards/inline.py index 1e04913..13069cd 100644 --- a/app/telegram/keyboards/inline.py +++ b/app/telegram/keyboards/inline.py @@ -39,7 +39,7 @@ main_menu = InlineKeyboardMarkup( [InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")], [ InlineKeyboardButton( - text="Остановить торговлю", callback_data="stop_trading" + text="Остановить торговлю", callback_data="trading_stop" ) ], ] @@ -327,58 +327,57 @@ change_position = InlineKeyboardMarkup( ) -def create_active_positions_keyboard(symbols): +def create_active_positions_keyboard(symbols: list): builder = InlineKeyboardBuilder() - for sym in symbols: - builder.button(text=f"{sym}", callback_data=f"get_position_{sym}") + for sym, side in symbols: + builder.button(text=f"{sym}:{side}", callback_data=f"get_position_{sym}_{side}") builder.button(text="Назад", callback_data="my_deals") builder.button(text="На главную", callback_data="profile_bybit") builder.adjust(2) return builder.as_markup() -def make_close_position_keyboard(symbol_pos: str): +def make_close_position_keyboard(symbol_pos: str, side: str, position_idx: int, qty: int): return InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( - text="Закрыть позицию", callback_data=f"close_position_{symbol_pos}" + text="Закрыть позицию", callback_data=f"close_position_{symbol_pos}_{side}_{position_idx}_{qty}" ) ], [ InlineKeyboardButton( - text="Установить TP/SL", callback_data=f"pos_tp_sl_{symbol_pos}" + text="Установить TP/SL", callback_data=f"pos_tp_sl_{symbol_pos}_{position_idx}" ) ], [ - InlineKeyboardButton(text="Назад", callback_data="my_deals"), + InlineKeyboardButton(text="Назад", callback_data="change_position"), InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), ], ] ) -def create_active_orders_keyboard(orders): +def create_active_orders_keyboard(orders:list): builder = InlineKeyboardBuilder() - for order in orders: - builder.button(text=f"{order}", callback_data=f"get_order_{order}") - builder.button(text="Закрыть все ордера", callback_data="cancel_all_orders") + for order, side in orders: + builder.button(text=f"{order}", callback_data=f"get_order_{order}_{side}") builder.button(text="Назад", callback_data="my_deals") builder.button(text="На главную", callback_data="profile_bybit") builder.adjust(2) return builder.as_markup() -def make_close_orders_keyboard(symbol_order: str): +def make_close_orders_keyboard(symbol_order: str, order_id: str): return InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( - text="Закрыть ордер", callback_data=f"close_order_{symbol_order}" + text="Закрыть ордер", callback_data=f"close_order_{symbol_order}_{order_id}" ) ], [ - InlineKeyboardButton(text="Назад", callback_data="my_deals"), + InlineKeyboardButton(text="Назад", callback_data="open_orders"), InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), ], ] @@ -438,12 +437,31 @@ back_to_start_trading = InlineKeyboardMarkup( ] ) -cancel_timer = InlineKeyboardMarkup( +cancel_timer_merged = InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="Отменить таймер", callback_data="cancel_timer")], + [InlineKeyboardButton(text="Отменить таймер", callback_data="cancel_timer_merged")], [ - InlineKeyboardButton(text="Назад", callback_data="conditions"), InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), ], ] ) + +cancel_timer_switch = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Отменить таймер", callback_data="cancel_timer_switch")], + [ + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +# STOP TRADING + +cancel_timer_stop = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Отменить таймер", callback_data="cancel_timer_stop")], + [ + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) \ No newline at end of file diff --git a/database/models.py b/database/models.py index 0fc5305..af5e5cd 100644 --- a/database/models.py +++ b/database/models.py @@ -166,7 +166,6 @@ class UserDeals(Base): switch_side_mode = Column(Boolean, nullable=True) limit_price = Column(Float, nullable=True) trigger_price = Column(Float, nullable=True) - fee = Column(Float, nullable=True) user = relationship("User", back_populates="user_deals") @@ -185,9 +184,7 @@ class UserAutoTrading(Base): nullable=False) symbol = Column(String, nullable=True) auto_trading = Column(Boolean, nullable=True) + side = Column(String, nullable=True) + fee = Column(Float, nullable=True) - user = relationship("User", back_populates="user_auto_trading") - - __table_args__ = ( - UniqueConstraint('user_id', 'symbol', name='uq_user_auto_trading_symbol'), - ) + user = relationship("User", back_populates="user_auto_trading") \ No newline at end of file diff --git a/database/request.py b/database/request.py index a617daf..f6402c9 100644 --- a/database/request.py +++ b/database/request.py @@ -446,7 +446,7 @@ async def set_leverage(tg_id: int, leverage: str) -> bool: async def set_leverage_to_buy_and_sell( - tg_id: int, leverage_to_buy: str, leverage_to_sell: str + tg_id: int, leverage_to_buy: str, leverage_to_sell: str ) -> bool: """ Set leverage for a user in the database. @@ -1066,26 +1066,26 @@ async def set_stop_timer(tg_id: int, timer_end: int) -> bool: # USER DEALS async def set_user_deal( - tg_id: int, - symbol: str, - last_side: str, - current_step: int, - trade_mode: str, - margin_type: str, - leverage: str, - leverage_to_buy: str, - leverage_to_sell: str, - order_type: str, - conditional_order_type: str, - order_quantity: float, - limit_price: float, - trigger_price: float, - martingale_factor: float, - max_bets_in_series: int, - take_profit_percent: int, - stop_loss_percent: int, - max_risk_percent: int, - switch_side_mode: bool, + tg_id: int, + symbol: str, + last_side: str, + current_step: int, + trade_mode: str, + margin_type: str, + leverage: str, + leverage_to_buy: str, + leverage_to_sell: str, + order_type: str, + conditional_order_type: str, + order_quantity: float, + limit_price: float, + trigger_price: float, + martingale_factor: float, + max_bets_in_series: int, + take_profit_percent: int, + stop_loss_percent: int, + max_risk_percent: int, + switch_side_mode: bool, ): """ Set the user deal in the database. @@ -1249,6 +1249,25 @@ async def set_fee_user_deal_by_symbol(tg_id: int, symbol: str, fee: float): # USER AUTO TRADING +async def get_all_user_auto_trading(tg_id: int): + """Get all user auto trading from the database asynchronously.""" + try: + async with async_session() as session: + result_user = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result_user.scalars().first() + if not user: + return [] + + result_auto_trading = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id) + ) + auto_trading = result_auto_trading.scalars().all() + return auto_trading + except Exception as e: + logger.error("Error getting auto trading for user %s: %s", tg_id, e) + return [] + + async def get_user_auto_trading(tg_id: int, symbol: str): """Get user auto trading from the database asynchronously.""" try: @@ -1268,12 +1287,13 @@ async def get_user_auto_trading(tg_id: int, symbol: str): return None -async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool: +async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: str) -> bool: """ Set the auto trading for a user in the database. :param tg_id: Telegram user ID :param symbol: Symbol :param auto_trading: Auto trading + :param side: Side :return: bool """ try: @@ -1285,7 +1305,7 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool: return False result = await session.execute( - select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol) + select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side) ) record = result.scalars().first() if record: @@ -1294,7 +1314,8 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool: new_record = UserAutoTrading( user_id=user.id, symbol=symbol, - auto_trading=auto_trading + auto_trading=auto_trading, + side=side ) session.add(new_record) await session.commit() @@ -1302,4 +1323,44 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool: return True except Exception as e: logger.error("Error setting auto_trading for user %s and symbol %s: %s", tg_id, symbol, e) - return False \ No newline at end of file + return False + + +async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: float) -> bool: + """ + Set the fee for a user auto trading in the database. + :param tg_id: + :param symbol: + :param side: + :param fee: + :return: + """ + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + if user is None: + logger.error(f"User with tg_id={tg_id} not found") + return False + + result = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side) + ) + record = result.scalars().first() + + if record: + record.fee = fee + else: + user_fee = UserAutoTrading( + user_id=user.id, + symbol=symbol, + fee=fee, + side=side + ) + session.add(user_fee) + await session.commit() + logger.info("Set fee for user %s and symbol %s", tg_id, symbol) + return True + except Exception as e: + logger.error("Error setting user auto trading fee for user %s and symbol %s: %s", tg_id, symbol, e) + return False diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 553a0f4..d47e11f 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -110,6 +110,11 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "stop_trading": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, "changing_the_symbol": { "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", @@ -135,5 +140,10 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "tasks": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, }, }