# app/src/telegram/handlers/system.py from __future__ import annotations from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from src.core.config import load_settings from src.core.constants import APP_NAME, APP_VERSION from src.core.system_status import build_system_text, get_system_snapshot, has_system_alerts from src.telegram.live.active_screen import ActiveScreenManager from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen from src.trading.auto.service import AutoTradeService from src.trading.journal.service import JournalService router = Router(name="system") def _system_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="🛠️ Настройки", callback_data="system:management") builder.button(text="ℹ️ Информация", callback_data="system:about") builder.adjust(2) return builder.as_markup() def _system_alert_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="🔁 Обновить", callback_data="system:retry") builder.button(text="🛠️ Настройки", callback_data="system:management") builder.button(text="ℹ️ Информация", callback_data="system:about") builder.adjust(1, 2) return builder.as_markup() def _register_system_screen(message: Message, screen: str = "system") -> None: LiveScreenRunner.unregister_message( chat_id=message.chat.id, message_id=message.message_id, ) ScreenRegistry.unregister_message( chat_id=message.chat.id, message_id=message.message_id, ) ScreenRegistry.register_screen( StaticScreen( screen=screen, bot=message.bot, chat_id=message.chat.id, message_id=message.message_id, ) ) ActiveScreenManager.register( screen=screen, message=message, ) async def _prepare_system_from_message(message: Message, screen: str = "system") -> None: await ActiveScreenManager.prepare_new_screen( screen=screen, bot=message.bot, chat_id=message.chat.id, ) async def _prepare_system_from_callback( callback: CallbackQuery, screen: str = "system", ) -> bool: if callback.message is None: await callback.answer("Сообщение не найдено", show_alert=True) return False await ActiveScreenManager.prepare_new_screen( screen=screen, bot=callback.message.bot, chat_id=callback.message.chat.id, keep_message_id=callback.message.message_id, ) return True async def _render_system_screen( target_message: Message, *, edit_mode: bool, user_id: int | None, chat_id: int | None, action: str, ) -> None: journal = JournalService() journal.log_ui_info( event_type="system_open_requested", message="Запрошено открытие экрана системы.", screen="system", action=action, user_id=user_id, chat_id=chat_id, ) snapshot = get_system_snapshot() is_alert = has_system_alerts(snapshot) if is_alert: journal.log_ui_warning( event_type="system_open_alert", message="Система загружена с предупреждениями.", screen="system", action=action, user_id=user_id, chat_id=chat_id, payload={ "has_alerts": True, "components": [ { "name": component.name, "state": component.state, "details": component.details, } for component in snapshot.components if component.state != "🟢" ], }, ) else: journal.log_ui_info( event_type="system_open_success", message="Экран системы загружен.", screen="system", action=action, user_id=user_id, chat_id=chat_id, payload={"has_alerts": False}, ) text = build_system_text(include_updated_at=is_alert) reply_markup = _system_alert_keyboard() if is_alert else _system_keyboard() if edit_mode: await target_message.edit_text(text, reply_markup=reply_markup) _register_system_screen(target_message, screen="system") return sent_message = await target_message.answer(text, reply_markup=reply_markup) _register_system_screen(sent_message, screen="system") @router.message(F.text.in_({"🖥️ Система"})) async def open_system(message: Message, state: FSMContext) -> None: await state.clear() await _prepare_system_from_message(message, screen="system") user_id = message.from_user.id if message.from_user else None chat_id = message.chat.id if message.chat else None await _render_system_screen( message, edit_mode=False, user_id=user_id, chat_id=chat_id, action="open", ) @router.callback_query(F.data == "system:retry") async def retry_system(callback: CallbackQuery, state: FSMContext) -> None: await state.clear() if not await _prepare_system_from_callback(callback, screen="system"): return user_id = callback.from_user.id if callback.from_user else None chat_id = callback.message.chat.id if callback.message.chat else None await _render_system_screen( callback.message, edit_mode=True, user_id=user_id, chat_id=chat_id, action="retry", ) await callback.answer() @router.callback_query(F.data == "system:management") async def open_system_management(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="system"): return text = ( "🛠️ Настройки\n\n" "СИСТЕМА\n\n" "Выберите раздел:" ) builder = InlineKeyboardBuilder() builder.button(text="🤖 Автоторговля", callback_data="settings:auto") builder.button(text="💹 Торговля", callback_data="settings:trade") builder.button(text="🌍 Общие", callback_data="settings:general") builder.button(text="📒 Журнал", callback_data="settings:journal") builder.button(text="⬅️ Назад", callback_data="system:back") builder.adjust(2, 2, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="system") await callback.answer() @router.callback_query(F.data == "settings:auto") async def open_auto_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return state = AutoTradeService().get_state() strategy_map = { "TREND": "TREND FOLLOWING", "GRID": "GRID TRADING", "SCALP": "SCALPING", } strategy_ready = state.strategy is not None symbol_ready = bool(state.symbol) risk_ready = state.risk_percent is not None leverage_ready = state.leverage is not None is_trend_strategy = (state.strategy or "").upper() == "TREND" sl_ready = state.stop_loss_percent is not None and state.stop_loss_percent > 0 is_configured = ( strategy_ready and symbol_ready and risk_ready and leverage_ready and (not is_trend_strategy or sl_ready) ) strategy = strategy_map.get(state.strategy or "", "—") symbol = "—" if state.symbol: base = state.symbol.split("_", 1)[0].upper() if "/" in base: symbol = base.split("/", 1)[0] else: for suffix in ("USDT", "USD", "EUR", "BTC"): if base.endswith(suffix) and len(base) > len(suffix): base = base[: -len(suffix)] break symbol = base risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—" leverage = f"x{state.leverage:g}" if state.leverage is not None else "—" max_reserved = ( f"{state.max_reserved_balance_percent:g}%" if state.max_reserved_balance_percent is not None else "off" ) sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off" tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off" ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off" strategy_icon = "✅" if strategy_ready else "⚠️" symbol_icon = "✅" if symbol_ready else "⚠️" risk_icon = "✅" if risk_ready else "⚠️" leverage_icon = "✅" if leverage_ready else "⚠️" sl_icon = "✅" if sl_ready else "⚠️" if is_trend_strategy: risk_controls_block = ( "Защита позиции:\n" f"{sl_icon} Stop Loss · {'required' if not sl_ready else sl}\n" f"✅ Take Profit · {tp}\n" f"✅ Max Loss · {ml}" ) else: risk_controls_block = ( "Защита позиции:\n" f"✅ Stop Loss · {sl}\n" f"✅ Take Profit · {tp}\n" f"✅ Max Loss · {ml}" ) config_status = "✅ Все параметры настроены" if is_configured else "⚠️ Настрой все параметры" text = ( "🤖 Автоторговля\n\n" "СИСТЕМА · Настройки\n\n" f"{strategy_icon} Стратегия: {strategy}\n" f"{symbol_icon} Актив: {symbol}\n" f"{risk_icon} Риск на сделку: {risk}\n" f"{leverage_icon} Плечо: {leverage}\n\n" f"✅ Лимит на сделку: {max_reserved}\n\n" f"{risk_controls_block}\n\n" f"{config_status}" ) builder = InlineKeyboardBuilder() builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy") builder.button(text="💱 Актив", callback_data="settings:auto_symbol") builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage") builder.button(text="🏦 Лимит", callback_data="settings:auto_max_reserved") builder.button(text="🛡️ Риск", callback_data="settings:auto_risk") builder.button(text="🧯 Защита", callback_data="auto:risk") builder.button(text="🤖 Автоторговля", callback_data="auto:home") builder.button(text="⬅️ Назад", callback_data="system:management") builder.adjust(2, 2, 2, 2) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data == "settings:auto_strategy") async def open_auto_strategy_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return text = ( "🧠 Стратегия\n\n" "СИСТЕМА · Настройки · Автоторговля\n\n" "Выберите стратегию:" ) builder = InlineKeyboardBuilder() builder.button(text="📈 Trend", callback_data="settings:auto_strategy:trend") builder.button(text="🧩 Grid", callback_data="settings:auto_strategy:grid") builder.button(text="⚡ Scalp", callback_data="settings:auto_strategy:scalp") builder.button(text="⬅️ Назад", callback_data="settings:auto") builder.adjust(3, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data.startswith("settings:auto_strategy:")) async def set_auto_strategy(callback: CallbackQuery) -> None: strategy = callback.data.split(":", 2)[2] AutoTradeService().set_strategy(strategy.upper()) await open_auto_settings(callback) await callback.answer("Стратегия обновлена") @router.callback_query(F.data == "settings:auto_symbol") async def open_auto_symbol_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return text = ( "💱 Актив\n\n" "СИСТЕМА · Настройки · Автоторговля\n\n" "Выберите актив:" ) builder = InlineKeyboardBuilder() builder.button(text="BTC", callback_data="settings:auto_symbol:BTC/USD_LEVERAGE") builder.button(text="ETH", callback_data="settings:auto_symbol:ETH/USD_LEVERAGE") builder.button(text="LTC", callback_data="settings:auto_symbol:LTC/USD_LEVERAGE") builder.button(text="XRP", callback_data="settings:auto_symbol:XRP/USD_LEVERAGE") builder.button(text="⬅️ Назад", callback_data="settings:auto") builder.adjust(2, 2, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data.startswith("settings:auto_symbol:")) async def set_auto_symbol(callback: CallbackQuery) -> None: symbol = callback.data.split(":", 2)[2] AutoTradeService().set_symbol(symbol) await open_auto_settings(callback) await callback.answer("Актив обновлён") @router.callback_query(F.data == "settings:auto_risk") async def open_auto_risk_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return text = ( "🛡️ Риск на сделку\n\n" "СИСТЕМА · Настройки · Автоторговля\n\n" "Выберите риск на сделку:" ) builder = InlineKeyboardBuilder() builder.button(text="0.5%", callback_data="settings:auto_risk:0.5") builder.button(text="1.0%", callback_data="settings:auto_risk:1.0") builder.button(text="2.0%", callback_data="settings:auto_risk:2.0") builder.button(text="⬅️ Назад", callback_data="settings:auto") builder.adjust(3, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data.startswith("settings:auto_risk:")) async def set_auto_risk(callback: CallbackQuery) -> None: risk = float(callback.data.split(":", 2)[2]) AutoTradeService().set_risk_percent(risk) await open_auto_settings(callback) await callback.answer("Риск обновлён") @router.callback_query(F.data == "settings:auto_leverage") async def open_auto_leverage_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return text = ( "⚙️ Плечо\n\n" "СИСТЕМА · Настройки · Автоторговля\n\n" "Выберите плечо:" ) builder = InlineKeyboardBuilder() builder.button(text="x1", callback_data="settings:auto_leverage:1") builder.button(text="x2", callback_data="settings:auto_leverage:2") builder.button(text="x3", callback_data="settings:auto_leverage:3") builder.button(text="x5", callback_data="settings:auto_leverage:5") builder.button(text="x10", callback_data="settings:auto_leverage:10") builder.button(text="x20", callback_data="settings:auto_leverage:20") builder.button(text="⬅️ Назад", callback_data="settings:auto") builder.adjust(3, 3, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data.startswith("settings:auto_leverage:")) async def set_auto_leverage(callback: CallbackQuery) -> None: leverage = float(callback.data.split(":", 2)[2]) AutoTradeService().set_leverage(leverage) await open_auto_settings(callback) await callback.answer("Плечо обновлено") @router.callback_query(F.data == "settings:trade") async def open_trade_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_trade"): return text = ( "💹 Торговля\n\n" "СИСТЕМА · Настройки\n\n" "Актив: —\n" "Тип ордера по умолчанию: —\n" "Пресеты количества: —\n\n" "В разработке." ) builder = InlineKeyboardBuilder() builder.button(text="⬅️ Назад", callback_data="system:management") builder.button(text="💹 Торговля", callback_data="trade:home") builder.adjust(2) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_trade") await callback.answer() @router.callback_query(F.data == "settings:general") async def open_general_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_general"): return text = ( "🌍 Общие\n\n" "СИСТЕМА · Настройки\n\n" "Режим аккаунта: —\n" "Часовой пояс: —\n" "Язык интерфейса: ru\n\n" "В разработке." ) builder = InlineKeyboardBuilder() builder.button(text="⬅️ Назад", callback_data="system:management") builder.adjust(1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_general") await callback.answer() @router.callback_query(F.data == "settings:journal") async def open_journal_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_journal"): return service = JournalService() total = service.get_total_count() text = ( "📒 Журнал\n\n" "СИСТЕМА · Настройки\n\n" f"📄 Записей: {total}\n" "📦 Лимит: —\n" "⏳ Хранение: —\n" "🗄 Архив: —\n\n" ) builder = InlineKeyboardBuilder() builder.button(text="🗑 Очистка", callback_data="journal:clear_confirm") builder.button(text="🗄 Архив", callback_data="settings:journal_archive") builder.button(text="📦 Лимит", callback_data="settings:journal_limit") builder.button(text="⏳ Хранение", callback_data="settings:journal_retention") builder.button(text="⬅️ Назад", callback_data="system:management") builder.button(text="📒 Журнал", callback_data="journal:1") builder.adjust(2, 2, 2) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_journal") await callback.answer() @router.callback_query(F.data == "settings:journal_archive") async def open_journal_archive_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_journal"): return text = ( "🗄 Архив\n\n" "СИСТЕМА · Настройки · Журнал\n\n" "Автоматический архив журнала: —\n" "Формат архива: —\n" "Периодичность: —\n\n" "В разработке." ) builder = InlineKeyboardBuilder() builder.button(text="⬅️ Назад", callback_data="settings:journal") builder.button(text="📒 Журнал", callback_data="journal:1") builder.adjust(2) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_journal") await callback.answer() @router.callback_query(F.data == "settings:journal_limit") async def open_journal_limit_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_journal"): return text = ( "📦 Лимит\n\n" "СИСТЕМА · Настройки · Журнал\n\n" "Текущий лимит: —\n\n" "Доступные варианты:" ) builder = InlineKeyboardBuilder() builder.button(text="1 000", callback_data="settings:journal_limit_stub") builder.button(text="5 000", callback_data="settings:journal_limit_stub") builder.button(text="10 000", callback_data="settings:journal_limit_stub") builder.button(text="∞", callback_data="settings:journal_limit_stub") builder.button(text="⬅️ Назад", callback_data="settings:journal") builder.adjust(2, 2, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_journal") await callback.answer() @router.callback_query(F.data == "settings:journal_retention") async def open_journal_retention_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_journal"): return text = ( "⏳ Хранение\n\n" "СИСТЕМА · Настройки · Журнал\n\n" "Текущий срок хранения: —\n\n" "Доступные варианты:" ) builder = InlineKeyboardBuilder() builder.button(text="7 дней", callback_data="settings:journal_retention_stub") builder.button(text="30 дней", callback_data="settings:journal_retention_stub") builder.button(text="90 дней", callback_data="settings:journal_retention_stub") builder.button(text="∞", callback_data="settings:journal_retention_stub") builder.button(text="⬅️ Назад", callback_data="settings:journal") builder.adjust(2, 2, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_journal") await callback.answer() @router.callback_query(F.data.in_({ "settings:journal_limit_stub", "settings:journal_retention_stub", })) async def journal_settings_stub(callback: CallbackQuery) -> None: await callback.answer("Настройка скоро появится", show_alert=True) @router.callback_query(F.data == "system:back") async def back_to_system(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="system"): return user_id = callback.from_user.id if callback.from_user else None chat_id = callback.message.chat.id if callback.message.chat else None await _render_system_screen( callback.message, edit_mode=True, user_id=user_id, chat_id=chat_id, action="back", ) await callback.answer() @router.callback_query(F.data == "system:about") async def open_system_about(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="system_about"): return settings = load_settings() journal = JournalService() journal.log_ui_info( event_type="system_about_opened", message="Открыта информация о продукте.", screen="system", action="about", user_id=callback.from_user.id if callback.from_user else None, chat_id=callback.message.chat.id if callback.message.chat else None, ) text = ( "ℹ️ Информация\n\n" "СИСТЕМА\n\n" f"{APP_NAME}\n" f"Версия: {APP_VERSION}\n" f"Режим: {'DEMO' if 'demo' in settings.exchange_base_url.lower() else 'LIVE'}\n" f"Часовой пояс: {settings.tz}\n\n" "Торговый Telegram-бот для контроля рынка, портфеля, журнала событий " "и будущей автоторговли." ) builder = InlineKeyboardBuilder() builder.button(text="⬅️ Назад", callback_data="system:back") builder.adjust(1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="system_about") await callback.answer() @router.callback_query(F.data == "settings:auto_max_reserved") async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_auto"): return text = ( "🏦 Лимит на сделку\n\n" "СИСТЕМА · Настройки · Автоторговля\n\n" "Максимальная доля баланса, которую можно зарезервировать под позицию:" ) builder = InlineKeyboardBuilder() builder.button(text="25%", callback_data="settings:auto_max_reserved:25") builder.button(text="50%", callback_data="settings:auto_max_reserved:50") builder.button(text="75%", callback_data="settings:auto_max_reserved:75") builder.button(text="100%", callback_data="settings:auto_max_reserved:100") builder.button(text="off", callback_data="settings:auto_max_reserved:off") builder.button(text="⬅️ Назад", callback_data="settings:auto") builder.adjust(2, 2, 1, 1) await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="settings_auto") await callback.answer() @router.callback_query(F.data.startswith("settings:auto_max_reserved:")) async def set_auto_max_reserved(callback: CallbackQuery) -> None: raw_value = callback.data.split(":", 2)[2] value = None if raw_value == "off" else float(raw_value) AutoTradeService().set_max_reserved_balance_percent(value) await open_auto_settings(callback) await callback.answer("Max Reserved обновлён")