541 lines
25 KiB
Python
541 lines
25 KiB
Python
import asyncio
|
||
import logging.config
|
||
from aiogram import F, Router
|
||
|
||
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
|
||
from app.telegram.functions.main_settings.settings import main_settings_message
|
||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||
|
||
from app.services.Bybit.functions.Futures import (close_user_trade, 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,
|
||
open_position, close_trade_after_delay, safe_float,
|
||
)
|
||
from app.services.Bybit.functions.balance import get_balance
|
||
import app.telegram.Keyboards.inline_keyboards as inline_markup
|
||
|
||
import app.telegram.database.requests as rq
|
||
from aiogram.types import Message, CallbackQuery
|
||
from app.services.Bybit.functions.price_symbol import get_price
|
||
# import app.services.Bybit.functions.balance as balance_g
|
||
|
||
from app.states.States import (state_update_symbol,
|
||
SetTP_SL_State, CloseTradeTimerState)
|
||
from aiogram.fsm.context import FSMContext
|
||
|
||
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
|
||
|
||
logging.config.dictConfig(LOGGING_CONFIG)
|
||
logger = logging.getLogger("functions")
|
||
|
||
router_functions_bybit_trade = Router()
|
||
|
||
user_trade_tasks = {}
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
|
||
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
|
||
"""
|
||
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
|
||
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
|
||
"""
|
||
user_id = callback.from_user.id
|
||
balance = await get_balance(user_id, callback.message)
|
||
|
||
if balance:
|
||
symbol = await rq.get_symbol(user_id)
|
||
price = await get_price(user_id, symbol=symbol)
|
||
|
||
text = (
|
||
f"💎 Торговля на Bybit\n\n"
|
||
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
|
||
f"📊 Текущая торговая пара: {symbol}\n"
|
||
f"$$$ Цена: {price}\n\n"
|
||
"Как начать торговлю?\n\n"
|
||
"1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
|
||
"2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
|
||
"3️⃣ Нажмите кнопку 'Начать торговать'.\n"
|
||
)
|
||
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
||
|
||
|
||
async def start_bybit_trade_message(message: Message) -> None:
|
||
"""
|
||
Отправляет пользователю информацию о балансе, символе и текущей цене,
|
||
вместе с инструкциями по началу торговли.
|
||
"""
|
||
balance = await get_balance(message.from_user.id, message)
|
||
tg_id = message.from_user.id
|
||
|
||
if balance:
|
||
await run_ws_for_user(tg_id, message)
|
||
symbol = await rq.get_symbol(message.from_user.id)
|
||
price = await get_price(message.from_user.id, symbol=symbol)
|
||
|
||
text = (
|
||
f"💎 Торговля на Bybit\n\n"
|
||
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
|
||
f"📊 Текущая торговая пара: {symbol}\n"
|
||
f"$$$ Цена: {price}\n\n"
|
||
"Как начать торговлю?\n\n"
|
||
"1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
|
||
"2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
|
||
"3️⃣ Нажмите кнопку 'Начать торговать'.\n"
|
||
)
|
||
|
||
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
|
||
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
|
||
"""
|
||
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
|
||
"""
|
||
await state.set_state(state_update_symbol.symbol)
|
||
await callback.answer()
|
||
|
||
await callback.message.answer(
|
||
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
|
||
reply_markup=inline_markup.cancel)
|
||
|
||
|
||
@router_functions_bybit_trade.message(state_update_symbol.symbol)
|
||
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
|
||
"""
|
||
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
|
||
При успешном обновлении сохранит пару и отправит обновлённую информацию.
|
||
"""
|
||
user_input = message.text.strip().upper()
|
||
exists = await get_valid_symbols(message.from_user.id, user_input)
|
||
|
||
if not exists:
|
||
await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
|
||
return
|
||
|
||
await state.update_data(symbol=message.text)
|
||
await message.answer('Пара была успешно обновлена')
|
||
await rq.update_symbol(message.from_user.id, user_input)
|
||
await start_bybit_trade_message(message)
|
||
|
||
await state.clear()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
|
||
async def start_trading_process(callback: CallbackQuery) -> None:
|
||
"""
|
||
Запускает торговый цикл в выбранном режиме Long/Short.
|
||
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
|
||
затем запускает торговый цикл с задержкой или без неё.
|
||
"""
|
||
await callback.answer()
|
||
tg_id = callback.from_user.id
|
||
message = callback.message
|
||
data_main_stgs = await rq.get_user_main_settings(tg_id)
|
||
# data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
|
||
# client = await get_bybit_client(tg_id)
|
||
symbol = await rq.get_symbol(tg_id)
|
||
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
|
||
trading_mode = data_main_stgs.get('trading_mode')
|
||
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
|
||
switch_state = data_main_stgs.get("switch_state", "По направлению")
|
||
# martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
|
||
# max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0))
|
||
# commission_fee = data_risk_stgs.get("commission_fee")
|
||
# fee_info = client.get_fee_rates(category='linear', symbol=symbol)
|
||
|
||
|
||
if trading_mode == 'Switch':
|
||
if switch_state == "По направлению":
|
||
side = data_main_stgs.get("last_side")
|
||
else:
|
||
side = data_main_stgs.get("last_side")
|
||
if side.lower() == "buy":
|
||
side = "Sell"
|
||
else:
|
||
side = "Buy"
|
||
else:
|
||
if trading_mode == 'Long':
|
||
side = 'Buy'
|
||
elif trading_mode == 'Short':
|
||
side = 'Sell'
|
||
else:
|
||
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
|
||
reply_markup=inline_markup.back_to_main)
|
||
return
|
||
|
||
# if commission_fee == "Да":
|
||
# commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
|
||
# else:
|
||
# commission_fee_percent = 0.0
|
||
|
||
# total_budget = await calculate_total_budget(
|
||
# starting_quantity=starting_quantity,
|
||
# martingale_factor=martingale_factor,
|
||
# max_steps=max_martingale_steps,
|
||
# commission_fee_percent=commission_fee_percent,
|
||
# )
|
||
|
||
# balance = await balance_g.get_balance(tg_id, message)
|
||
# if safe_float(balance) < total_budget:
|
||
# logger.error(
|
||
# f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
|
||
# f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT."
|
||
# )
|
||
# await message.answer(
|
||
# f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
|
||
# f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.",
|
||
# reply_markup=inline_markup.back_to_main,
|
||
# )
|
||
# return
|
||
|
||
await message.answer("Начинаю торговлю с использованием текущих настроек...")
|
||
await rq.update_trigger(tg_id=tg_id, trigger="Автоматический")
|
||
|
||
timer_data = await rq.get_user_timer(tg_id)
|
||
if isinstance(timer_data, dict):
|
||
timer_minute = timer_data.get('timer_minutes', 0)
|
||
else:
|
||
timer_minute = timer_data or 0
|
||
|
||
if timer_minute > 0:
|
||
await message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start)
|
||
|
||
async def delay_start():
|
||
try:
|
||
await asyncio.sleep(timer_minute * 60)
|
||
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
|
||
await rq.update_user_timer(tg_id, minutes=0)
|
||
except asyncio.exceptions.CancelledError:
|
||
logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.")
|
||
raise
|
||
|
||
task = asyncio.create_task(delay_start())
|
||
user_trade_tasks[tg_id] = task
|
||
else:
|
||
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start")
|
||
async def cancel_start_trading(callback: CallbackQuery):
|
||
tg_id = callback.from_user.id
|
||
task = user_trade_tasks.get(tg_id)
|
||
if task and not task.done():
|
||
task.cancel()
|
||
try:
|
||
await task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
user_trade_tasks.pop(tg_id, None)
|
||
await rq.update_user_timer(tg_id, minutes=0)
|
||
await rq.update_trigger(tg_id, "Ручной")
|
||
await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main)
|
||
await callback.message.edit_reply_markup(reply_markup=None)
|
||
else:
|
||
await callback.answer("Нет запланированной задачи запуска.", show_alert=True)
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
|
||
async def show_my_trades(callback: CallbackQuery) -> None:
|
||
"""
|
||
Отображает пользователю выбор типа сделки по текущей торговой паре.
|
||
"""
|
||
await callback.answer()
|
||
try:
|
||
await callback.message.answer("Выберите тип сделки:",
|
||
reply_markup=inline_markup.my_deals_select_markup)
|
||
except Exception as e:
|
||
logger.error("Произошла ошибка при выборе типа сделки: %s", e)
|
||
|
||
|
||
@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(callback.from_user.id, message=callback.message)
|
||
except Exception as e:
|
||
logger.error("Произошла ошибка при выборе сделки: %s", 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_"):]
|
||
await rq.update_symbol(callback_query.from_user.id, symbol)
|
||
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("Произошла ошибка при выборе сделки: %s", 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:
|
||
"""
|
||
Показывает открытые позиции пользователя по символу.
|
||
"""
|
||
await callback.answer()
|
||
|
||
try:
|
||
await get_active_orders(callback.from_user.id, message=callback.message)
|
||
except Exception as e:
|
||
logger.error("Произошла ошибка при выборе ордера: %s", 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_"):]
|
||
await rq.update_symbol(callback_query.from_user.id, symbol)
|
||
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("Произошла ошибка при выборе сделки: %s", 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)
|
||
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
|
||
reply_markup=inline_markup.cancel)
|
||
|
||
|
||
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
|
||
async def process_take_profit(message: Message, state: FSMContext) -> None:
|
||
"""
|
||
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
|
||
"""
|
||
try:
|
||
tp = float(message.text.strip())
|
||
if tp <= 0:
|
||
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
|
||
reply_markup=inline_markup.cancel)
|
||
return
|
||
except ValueError:
|
||
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
|
||
reply_markup=inline_markup.cancel)
|
||
return
|
||
|
||
await state.update_data(take_profit=tp)
|
||
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
|
||
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
|
||
|
||
|
||
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
|
||
async def process_stop_loss(message: Message, state: FSMContext) -> None:
|
||
"""
|
||
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
|
||
"""
|
||
try:
|
||
sl = float(message.text.strip())
|
||
if sl <= 0:
|
||
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
|
||
reply_markup=inline_markup.cancel)
|
||
return
|
||
except ValueError:
|
||
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
|
||
reply_markup=inline_markup.cancel)
|
||
return
|
||
|
||
data = await state.get_data()
|
||
tp = data.get("take_profit")
|
||
|
||
if tp is None:
|
||
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
|
||
await state.clear()
|
||
return
|
||
|
||
tg_id = message.from_user.id
|
||
|
||
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
|
||
|
||
await state.clear()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
|
||
async def close_trade_callback(callback: CallbackQuery) -> None:
|
||
"""
|
||
Закрывает сделку пользователя по символу.
|
||
"""
|
||
symbol = callback.data.split(':')[1]
|
||
tg_id = callback.from_user.id
|
||
|
||
result = await close_user_trade(tg_id, symbol)
|
||
|
||
if result:
|
||
logger.info(f"Сделка {symbol} успешно закрыта.")
|
||
else:
|
||
logger.error(f"Не удалось закрыть сделку {symbol}.")
|
||
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
|
||
|
||
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:
|
||
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:
|
||
"""
|
||
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
|
||
"""
|
||
symbol = callback.data.split(":")[1]
|
||
await state.update_data(symbol=symbol)
|
||
await state.set_state(CloseTradeTimerState.waiting_for_delay)
|
||
await callback.message.answer("Введите задержку в минутах до закрытия сделки:",
|
||
reply_markup=inline_markup.cancel)
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
|
||
async def process_close_delay(message: Message, state: FSMContext) -> None:
|
||
"""
|
||
Обрабатывает ввод закрытия сделки с задержкой.
|
||
"""
|
||
try:
|
||
delay_minutes = int(message.text.strip())
|
||
if delay_minutes <= 0:
|
||
await message.answer("Введите положительное число.")
|
||
return
|
||
except ValueError:
|
||
await message.answer("Некорректный ввод. Введите число в минутах.")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
symbol = data.get("symbol")
|
||
|
||
delay = delay_minutes * 60
|
||
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.",
|
||
reply_markup=inline_markup.back_to_main)
|
||
await close_trade_after_delay(message.from_user.id, message, symbol, delay)
|
||
await state.clear()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
|
||
async def reset_martingale(callback: CallbackQuery) -> None:
|
||
"""
|
||
Сбрасывает шаги мартингейла пользователя.
|
||
"""
|
||
tg_id = callback.from_user.id
|
||
await rq.update_martingale_step(tg_id, 1)
|
||
await callback.answer("Сброс шагов выполнен.")
|
||
await main_settings_message(tg_id, callback.message)
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
|
||
async def confirm_stop_trading(callback: CallbackQuery):
|
||
"""
|
||
Предлагает пользователю выбрать вариант подтверждение остановки торговли.
|
||
"""
|
||
await callback.message.answer(
|
||
"Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
|
||
async def stop_immediately(callback: CallbackQuery):
|
||
"""
|
||
Останавливает торговлю немедленно.
|
||
"""
|
||
tg_id = callback.from_user.id
|
||
symbol = await rq.get_symbol(tg_id)
|
||
|
||
await close_user_trade(tg_id, symbol)
|
||
await rq.update_trigger(tg_id, "Ручной")
|
||
await rq.update_martingale_step(tg_id, 1)
|
||
|
||
await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
|
||
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
|
||
"""
|
||
Запускает диалог с пользователем для задания задержки до остановки торговли.
|
||
"""
|
||
|
||
await state.set_state(CloseTradeTimerState.waiting_for_trade)
|
||
await callback.message.answer("Введите задержку в минутах до остановки торговли:",
|
||
reply_markup=inline_markup.cancel)
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade)
|
||
async def process_stop_delay(message: Message, state: FSMContext):
|
||
"""
|
||
Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
|
||
"""
|
||
try:
|
||
delay_minutes = int(message.text.strip())
|
||
if delay_minutes <= 0:
|
||
await message.answer("Введите положительное число минут.")
|
||
return
|
||
except ValueError:
|
||
await message.answer("Некорректный формат. Введите число в минутах.")
|
||
return
|
||
|
||
tg_id = message.from_user.id
|
||
delay_seconds = delay_minutes * 60
|
||
|
||
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.",
|
||
reply_markup=inline_markup.back_to_main)
|
||
await asyncio.sleep(delay_seconds)
|
||
|
||
symbol = await rq.get_symbol(tg_id)
|
||
|
||
await close_user_trade(tg_id, symbol)
|
||
await rq.update_trigger(tg_id, "Ручной")
|
||
await rq.update_martingale_step(tg_id, 1)
|
||
await message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
|
||
|
||
await state.clear()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
|
||
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
|
||
"""
|
||
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
|
||
"""
|
||
try:
|
||
await state.clear()
|
||
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
|
||
await callback.answer()
|
||
except Exception as e:
|
||
logger.error("Ошибка при обработке отмены: %s", e)
|