# app/src/telegram/handlers/auto.py from __future__ import annotations from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from src.telegram.ui.common import mode_line from src.trading.auto.runner import AutoTradeRunner from src.trading.auto.service import AutoTradeService from src.telegram.handlers.system import open_auto_settings from src.integrations.exchange.service import ExchangeService from src.telegram.ui.currency_ui import format_usd_amount router = Router(name="auto") # красивое отображение стратегии def _strategy_label(strategy: str | None) -> str: mapping = { "TREND": "📈 Trend Following", "GRID": "🧩 Grid Trading", "SCALP": "⚡ Scalping", } return mapping.get(strategy or "", "—") # красивое отображение статуса def _status_label(status: str) -> str: mapping = { "OFF": "⚪ Выключена", "OBSERVING": "👀 Наблюдение", "RUNNING": "🟢 Активна", } return mapping.get(status, status) # красивое отображение сигнала def _signal_label(signal: str | None) -> str: mapping = { "BUY": "🟢 BUY", "SELL": "🔴 SELL", "HOLD": "🟡 HOLD", } return mapping.get(signal or "", "—") # красивое отображение решения def _decision_label(status: str) -> str: mapping = { "WAITING": "🟡 Ожидание", "CONFIRMING": "🟠 Подтверждение", "READY": "🟢 Готово к входу", "BLOCKED": "🔴 Заблокировано", } return mapping.get(status, status) # компактное значение или заглушка def _value_or_dash(value: object) -> str: if value is None: return "—" return str(value) # формат цены def _price_or_dash(value: float | None) -> str: if value is None: return "—" return f"{value:.2f}" # текущая цена инструмента def _market_price_or_dash(symbol: str | None) -> str: if not symbol: return "—" try: ticker = ExchangeService().get_price(symbol) return f"$ {format_usd_amount(ticker.price)}" except Exception: return "—" # формат USD def _usd_or_dash(value: float | None) -> str: if value is None: return "—" return f"{value:.2f} USD" # формат размера позиции def _size_or_dash(value: float | None) -> str: if value is None: return "—" return f"{value:.8f}".rstrip("0").rstrip(".") # формат плеча def _leverage_or_dash(value: float | None) -> str: if value is None: return "—" return f"{value:.1f}x" # формат торгового инструмента для UI def _format_symbol(symbol: str | None) -> str: if not symbol: return "—" base_symbol = symbol.split("_", 1)[0] parts = base_symbol.split("/", 1) if len(parts) == 2: return f"{parts[0]} / {parts[1]}" return base_symbol # стратегия для компактного UI def _compact_strategy(strategy: str | None) -> str: if not strategy: return "—" return strategy.upper() # плечо для компактного UI def _compact_leverage(value: float | None) -> str: if value is None: return "—" return f"x{value:g}" # проверка, настроена ли автоторговля минимально def _is_auto_configured(state) -> bool: return bool( state.symbol and state.strategy and state.risk_percent is not None ) # строка инструмента / стратегии / плеча def _context_line(state) -> str: symbol = _format_symbol(state.symbol) strategy = _compact_strategy(state.strategy) leverage = _compact_leverage(state.leverage) if leverage == "—": return f"{symbol} · {strategy}" return f"{symbol} · {strategy} · {leverage}" # клавиатура автоторговли def _auto_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="▶️ Start", callback_data="auto:start") builder.button(text="👀 Watch", callback_data="auto:observe") builder.button(text="🛑 Stop", callback_data="auto:stop") builder.button(text="🛠️ Настройки", callback_data="settings:auto") builder.adjust(3, 1) return builder.as_markup() # собрать текст экрана def _build_auto_text() -> str: service = AutoTradeService() state = service.get_state() account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE" risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—" configured = _is_auto_configured(state) price = _market_price_or_dash(state.symbol) status_line = { "OFF": "⚪ Off", "OBSERVING": "👀 Watch", "RUNNING": "🟢 On", }.get(state.status, state.status) header = ( f"🤖 Автоторговля · {status_line}\n" f"🔸 {account_mode} аккаунт\n\n" ) if state.status == "OFF": if not configured: return ( f"{header}" "⚠️ Не настроена\n" "Настрой параметры" ) return ( f"{header}" f"{_context_line(state)}\n" f"Price: {price}\n" f"Risk: {risk}" ) position_line = ( f"Pos: {_value_or_dash(state.position_side)} | " f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}" ) if state.position_side != "NONE" and state.entry_price is not None: position_line = ( f"Pos: {_value_or_dash(state.position_side)} | " f"Entry: $ {_price_or_dash(state.entry_price)} | " f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}" ) return ( f"{header}" f"{_context_line(state)}\n" f"Price: {price}\n\n" f"{_signal_label(state.last_signal)} ×{state.last_signal_repeat_count} " f"· {state.decision_status}\n\n" f"{position_line}\n" f"Risk: {risk}" ) # отрисовать live-экран автоторговли async def _render_auto_screen( target_message: Message, *, edit_mode: bool, ) -> None: text = _build_auto_text() if edit_mode: try: await target_message.edit_text(text, reply_markup=_auto_keyboard()) except TelegramBadRequest as exc: if "message is not modified" not in str(exc).lower(): raise AutoTradeRunner.register_screen( bot=target_message.bot, chat_id=target_message.chat.id, message_id=target_message.message_id, render_text=_build_auto_text, render_markup=_auto_keyboard, ) return sent_message = await target_message.answer(text, reply_markup=_auto_keyboard()) AutoTradeRunner.register_screen( bot=sent_message.bot, chat_id=sent_message.chat.id, message_id=sent_message.message_id, render_text=_build_auto_text, render_markup=_auto_keyboard, ) # открыть экран из главного меню @router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"})) async def open_auto(message: Message, state: FSMContext) -> None: await state.clear() AutoTradeRunner.set_current_screen("auto") current_state = AutoTradeService().get_state() if current_state.status in {"RUNNING", "OBSERVING"}: await AutoTradeRunner.delete_registered_screen( bot=message.bot, chat_id=message.chat.id, ) await _render_auto_screen(message, edit_mode=False) # открыть экран через callback @router.callback_query(F.data == "auto:home") async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None: await state.clear() if callback.message is None: await callback.answer("Сообщение не найдено", show_alert=True) return AutoTradeRunner.set_current_screen("auto") await _render_auto_screen(callback.message, edit_mode=True) await callback.answer() # включить активную торговлю @router.callback_query(F.data == "auto:start") async def auto_start(callback: CallbackQuery) -> None: service = AutoTradeService() state = service.get_state() if not _is_auto_configured(state): await callback.answer( "Сначала настрой параметры автоторговли", show_alert=True, ) if callback.message is not None: await open_auto_settings(callback) return _, message = service.start() AutoTradeRunner.set_current_screen("auto") AutoTradeRunner.start() if callback.message is not None: await _render_auto_screen(callback.message, edit_mode=True) await callback.answer(message) # включить наблюдение @router.callback_query(F.data == "auto:observe") async def auto_observe(callback: CallbackQuery) -> None: service = AutoTradeService() state = service.get_state() if not _is_auto_configured(state): await callback.answer( "Сначала настрой параметры автоторговли", show_alert=True, ) if callback.message is not None: await open_auto_settings(callback) return _, message = service.observe() AutoTradeRunner.set_current_screen("auto") AutoTradeRunner.start() if callback.message is not None: await _render_auto_screen(callback.message, edit_mode=True) await callback.answer(message) # выключить автоторговлю @router.callback_query(F.data == "auto:stop") async def auto_stop(callback: CallbackQuery) -> None: service = AutoTradeService() _, message = service.stop() AutoTradeRunner.stop() if callback.message is not None: await _render_auto_screen(callback.message, edit_mode=True) await callback.answer(message)