import asyncio import logging.config from aiogram import F, Router 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_positions_by_symbol from app.bybit.open_positions import start_trading_cycle from app.helper_functions import safe_float from logger_helper.logger_helper import LOGGING_CONFIG logging.config.dictConfig(LOGGING_CONFIG) 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: """ Handles the "start_trading" callback query. Clears the FSM state and sends a message to the user to select the trading mode. :param callback_query: Message :param state: FSMContext :return: None """ try: await state.clear() additional_data = await rq.get_user_additional_settings( tg_id=callback_query.from_user.id ) trade_mode = additional_data.trade_mode symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) 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") 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", reply_markup=kbi.merged_start_trading, ) else: # trade_mode == "Both_Sides": await callback_query.message.edit_text( text="Выберите режим торговли:\n\n" "Лонг - все сделки открываются на покупку.\n" "Шорт - все сделки открываются на продажу.\n", reply_markup=kbi.both_start_trading, ) logger.debug( "Command start_trading processed successfully for user: %s", callback_query.from_user.id, ) except Exception as e: await callback_query.answer(text="Произошла ошибка при запуске торговли") logger.error( "Error processing command start_trading for user %s: %s", callback_query.from_user.id, e, ) @router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short") async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None: """ Handles the "long" or "short" callback query. Clears the FSM state and starts the trading cycle. :param callback_query: Message :param state: FSMContext :return: None """ try: if callback_query.data == "long": side = "Buy" elif callback_query.data == "short": side = "Sell" else: await callback_query.answer(text="Произошла ошибка при запуске торговли") return symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) 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") if position_idx == 0 and safe_float(size) > 0: await callback_query.answer( text="Торговля уже запущена в одностороннем режиме для данного инструмента" ) return conditional_data = await rq.get_user_conditional_settings( tg_id=callback_query.from_user.id ) 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] async def delay_start(): if timer_start > 0: await callback_query.message.edit_text( text=f"Торговля будет запущена с задержкой {timer_start} мин.", reply_markup=kbi.cancel_timer, ) await asyncio.sleep(timer_start * 60) res = await start_trading_cycle( tg_id=callback_query.from_user.id, side=side, switch_side_mode=False, ) error_messages = { "Limit price is out min price": "Цена лимитного ордера меньше минимального", "Limit price is out max price": "Цена лимитного ордера больше максимального", "Risk is too high for this trade": "Риск сделки превышает допустимый убыток", "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", "ab not enough for new order": "Недостаточно средств для создания нового ордера", "InvalidRequestError": "Произошла ошибка при запуске торговли.", "Order does not meet minimum order value": "Сумма ордера не sufficientдля запуска торговли", "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: # Получаем сообщение из таблицы, или дефолтный текст text = error_messages.get(res, "Произошла ошибка при запуске торговли") await callback_query.answer(text=text) task = asyncio.create_task(delay_start()) user_trade_tasks[callback_query.from_user.id] = task except Exception as e: await callback_query.answer(text="Произошла ошибка при запуске торговли") logger.error( "Error processing command long for user %s: %s", callback_query.from_user.id, e, ) except asyncio.CancelledError: logger.error("Cancelled timer for user %s", callback_query.from_user.id) @router_start_trading.callback_query(lambda c: c.data == "switch") async def start_trading_switch( callback_query: CallbackQuery, state: FSMContext ) -> None: """ Handles the "switch" callback query. Clears the FSM state and sends a message to the user to select the switch side. :param callback_query: Message :param state: FSMContext :return: None """ try: await state.clear() await callback_query.message.edit_text( text="Выберите направление первой сделки серии:\n\n" "Лонг - открывается первая сделка на покупку.\n" "Шорт - открывается первая сделка на продажу.\n" "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", reply_markup=kbi.switch_side, ) except Exception as e: await callback_query.answer(text="Произошла ошибка при запуске торговли") logger.error( "Error processing command start trading switch for user %s: %s", callback_query.from_user.id, e, ) @router_start_trading.callback_query( lambda c: c.data in {"switch_long", "switch_short", "switch_direction", "switch_opposite"} ) async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None: """ Starts the trading cycle with the selected side. :param callback_query: :param state: :return: """ try: symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) user_deals_data = await rq.get_user_deal_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) get_side = "Buy" if user_deals_data: get_side = user_deals_data.last_side or "Buy" if callback_query.data == "switch_long": side = "Buy" elif callback_query.data == "switch_short": side = "Sell" elif callback_query.data == "switch_direction": side = get_side elif callback_query.data == "switch_opposite": if get_side == "Buy": side = "Sell" else: side = "Buy" else: await callback_query.answer(text="Произошла ошибка при запуске торговли") return 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") if position_idx == 1 and safe_float(size) > 0 and side == "Buy": await callback_query.answer( text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента" ) return if position_idx == 2 and safe_float(size) > 0 and side == "Sell": await callback_query.answer( text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента" ) return conditional_data = await rq.get_user_conditional_settings( tg_id=callback_query.from_user.id ) 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] async def delay_start(): if timer_start > 0: await callback_query.message.edit_text( text=f"Торговля будет запущена с задержкой {timer_start} мин.", reply_markup=kbi.cancel_timer, ) await asyncio.sleep(timer_start * 60) res = await start_trading_cycle( tg_id=callback_query.from_user.id, side=side, switch_side_mode=True, ) error_messages = { "Limit price is out min price": "Цена лимитного ордера меньше минимального", "Limit price is out max price": "Цена лимитного ордера больше максимального", "Risk is too high for this trade": "Риск сделки превышает допустимый убыток", "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", "ab not enough for new order": "Недостаточно средств для создания нового ордера", "InvalidRequestError": "Произошла ошибка при запуске торговли.", "Order does not meet minimum order value": "Сумма ордера не sufficientдля запуска торговли", "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: text = error_messages.get(res, "Произошла ошибка при запуске торговли") await callback_query.answer(text=text) task = asyncio.create_task(delay_start()) user_trade_tasks[callback_query.from_user.id] = task except asyncio.CancelledError: logger.error("Cancelled timer for user %s", callback_query.from_user.id) except Exception as e: await callback_query.answer(text="Произошла ошибка при запуске торговли") logger.error( "Error processing command start switch for user %s: %s", callback_query.from_user.id, e, ) @router_start_trading.callback_query(F.data == "cancel_timer") async def cancel_start_trading( callback_query: CallbackQuery, state: FSMContext ) -> None: """ Handles the "cancel_timer" callback query. Clears the FSM state and sends a message to the user to cancel the start trading process. :param callback_query: Message :param state: FSMContext :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 ) except Exception as e: await callback_query.answer("Произошла ошибка при отмене запуска торговли") logger.error( "Error processing command cancel_timer for user %s: %s", callback_query.from_user.id, e, ) finally: await state.clear()