import asyncio import logging.config from aiogram import F, Router 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, trading_cycle, open_position, close_trade_after_delay, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup from pybit.unified_trading import HTTP import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery from app.services.Bybit.functions.price_symbol import get_price from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price, 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() @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) price = await get_price(user_id) if balance: symbol = await rq.get_symbol(user_id) 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) price = await get_price(message.from_user.id) if balance: symbol = await rq.get_symbol(message.from_user.id) text = ( f"💎 Торговля на Bybit\n\n" f"⚖️ Ваш баланс (USDT): {balance}\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.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_update_entry_type') async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None: """ Запрашивает у пользователя тип входа в позицию (Market или Limit). """ await state.set_state(state_update_entry_type.entry_type) await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) await callback.answer() @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:')) async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None: """ Обработка выбора типа входа в позицию. Если Limit, запрашивает цену лимитного ордера. Если Market — обновляет настройки. """ order_type = callback.data.split(':')[1] if order_type not in ['Market', 'Limit']: await callback.answer("Ошибка выбора", show_alert=True) return if order_type == 'Limit': await state.set_state(state_limit_price.price) await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel) await callback.answer() return try: await state.update_data(entry_order_type=order_type) await rq.update_entry_order_type(callback.from_user.id, order_type) await callback.message.answer(f"Выбран тип входа в позицию: {order_type}", reply_markup=inline_markup.start_trading_markup) await callback.answer() except Exception as e: logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}") await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию", reply_markup=inline_markup.back_to_main) await state.clear() @router_functions_bybit_trade.message(state_limit_price.price) async def set_limit_price(message: Message, state: FSMContext) -> None: """ Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки. """ try: price = float(message.text) if price <= 0: await message.answer("Цена должна быть положительным числом. Попробуйте снова.", reply_markup=inline_markup.cancel) return except ValueError: await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel) return await state.update_data(entry_order_type='Limit', limit_price=price) await rq.update_entry_order_type(message.from_user.id, 'Limit') await rq.update_limit_price(message.from_user.id, price) await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup) 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-ключи, режим торговли, маржинальный режим и открытые позиции, затем запускает торговый цикл с задержкой или без неё. """ tg_id = callback.from_user.id message = callback.message data_main_stgs = await rq.get_user_main_settings(tg_id) 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) margin_mode = data_main_stgs.get('margin_type', 'Isolated') trading_mode = data_main_stgs.get('trading_mode') switch_mode = data_main_stgs.get('switch_mode_enabled') if not api_key or not secret_key: await message.answer("❗️ У вас не настроены API ключи для Bybit.") await callback.answer() return if trading_mode not in ['Long', 'Short', 'Smart']: await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") await callback.answer() return if margin_mode not in ['Isolated', 'Cross']: margin_mode = 'Isolated' client = HTTP(api_key=api_key, api_secret=secret_key) try: positions_resp = client.get_positions(category='linear', symbol=symbol) positions = positions_resp.get('result', {}).get('list', []) except Exception as e: logger.error(f"Ошибка при получении позиций: {e}") positions = [] for pos in positions: size = pos.get('size') existing_margin_mode = pos.get('margin_mode') if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode: await callback.answer( f"⚠️ Маржинальный режим нельзя менять при открытой позиции " f"(текущий режим: {existing_margin_mode})", show_alert=True) return side = None if switch_mode == 'Включено': switch_state = data_main_stgs.get('switch_state', 'Long') side = 'Buy' if switch_state == 'Long' else 'Sell' 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) await callback.answer() return await message.answer("Начинаю торговлю с использованием текущих настроек...") 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 trading_cycle(tg_id, message) await message.answer(f"Торговля начнётся через {timer_minute} мин.") await rq.update_user_timer(tg_id, minutes=0) else: await open_position(tg_id, message, side, margin_mode) await callback.answer() @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(f"Выберите тип сделки:", reply_markup=inline_markup.my_deals_select_markup) except Exception as e: logger.error(f"Произошла ошибка при выборе типа сделки: {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(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_"):] 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(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: """ Показывает открытые позиции пользователя по символу. """ await callback.answer() try: 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_"):] 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(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) 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, message=callback.message) 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} мин.") 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, 0) await callback.answer("Сброс шагов выполнен.") @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 await rq.update_trigger(tg_id, "Ручной") 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_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_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) await rq.update_trigger(tg_id, "Ручной") await message.answer("Торговля по таймеру остановлена.") await state.clear() @router_functions_bybit_trade.callback_query(F.data == "clb_cancel") async def cancel(callback: CallbackQuery, state: FSMContext) -> None: """ Отменяет текущее состояние FSM и сообщает пользователю об отмене. """ await state.clear() await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) await callback.answer()