diff --git a/app/src/telegram/handlers/journal.py b/app/src/telegram/handlers/journal.py index c7b8078..0ac1ca1 100644 --- a/app/src/telegram/handlers/journal.py +++ b/app/src/telegram/handlers/journal.py @@ -15,6 +15,7 @@ from src.telegram.handlers.journal_ui import ( build_actions_keyboard, render_actions, ) +from src.telegram.live.runner import ScreenRegistry, StaticScreen from src.trading.journal.service import JournalService from src.trading.auto.runner import AutoTradeRunner @@ -60,8 +61,24 @@ async def _show_journal_page( if edit_mode: await target_message.edit_text(text, reply_markup=kb) + ScreenRegistry.register_screen( + StaticScreen( + screen="journal", + bot=target_message.bot, + chat_id=target_message.chat.id, + message_id=target_message.message_id, + ) + ) else: - await target_message.answer(text, reply_markup=kb) + sent_message = await target_message.answer(text, reply_markup=kb) + ScreenRegistry.register_screen( + StaticScreen( + screen="journal", + bot=sent_message.bot, + chat_id=sent_message.chat.id, + message_id=sent_message.message_id, + ) + ) @router.callback_query(F.data == "journal:actions") @@ -82,6 +99,12 @@ async def journal_actions(callback: CallbackQuery) -> None: async def open_journal(message: Message, state: FSMContext) -> None: await state.clear() + await ScreenRegistry.delete_screen( + screen="journal", + bot=message.bot, + chat_id=message.chat.id, + ) + JournalService().log_ui_info( event_type="journal_open_requested", message="Запрошено открытие журнала.", @@ -98,6 +121,37 @@ async def open_journal(message: Message, state: FSMContext) -> None: ) +@router.callback_query(F.data == "monitoring:journal") +async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None: + await state.clear() + + if callback.message is None: + await callback.answer("Сообщение не найдено", show_alert=True) + return + + await ScreenRegistry.delete_screen( + screen="journal", + bot=callback.message.bot, + chat_id=callback.message.chat.id, + ) + + JournalService().log_ui_info( + event_type="journal_open_requested", + message="Запрошено открытие журнала из мониторинга.", + screen="journal", + action="open_from_monitoring", + user_id=_user_id_from_callback(callback), + chat_id=_chat_id_from_callback(callback), + ) + + await _show_journal_page( + callback.message, + page=1, + edit_mode=True, + ) + await callback.answer() + + @router.callback_query(F.data == "journal:noop") async def journal_noop(callback: CallbackQuery) -> None: await callback.answer() diff --git a/app/src/telegram/handlers/journal_ui.py b/app/src/telegram/handlers/journal_ui.py index bba7f5b..ef9380d 100644 --- a/app/src/telegram/handlers/journal_ui.py +++ b/app/src/telegram/handlers/journal_ui.py @@ -103,7 +103,7 @@ def build_keyboard(page: int, total_pages: int) -> InlineKeyboardMarkup: kb.button(text="📤 Экспорт", callback_data="journal:actions") kb.button(text="🛠️ Настройки", callback_data="settings:journal") - kb.button(text="⬅️ Назад", callback_data="system:back") + kb.button(text="📊 К мониторингу", callback_data="monitoring:home") nav_count = 1 if page > 1: @@ -127,7 +127,7 @@ def build_actions_keyboard() -> InlineKeyboardMarkup: def render_actions() -> str: return ( "📤 Экспорт\n\n" - "СИСТЕМА · Журнал\n\n" + "МОНИТОРИНГ · Журнал\n\n" "Выберите формат:" ) @@ -193,7 +193,7 @@ def render(events, page, total_pages): lines = [ "📒 Журнал", "", - "СИСТЕМА", + "МОНИТОРИНГ", "", "Последние события:", "", diff --git a/app/src/telegram/handlers/market.py b/app/src/telegram/handlers/market.py index a7e7d20..f45d7ab 100644 --- a/app/src/telegram/handlers/market.py +++ b/app/src/telegram/handlers/market.py @@ -29,7 +29,7 @@ _last_market_directions: dict[str, str] = {} # клавиатура экрана рынка def _market_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="📊 К мониторингу", callback_data="monitoring:home") builder.adjust(1) return builder.as_markup() @@ -257,6 +257,55 @@ async def open_market(message: Message, state: FSMContext) -> None: ) +# открыть рынок из экрана мониторинга +@router.callback_query(F.data == "monitoring:market") +async def open_market_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None: + await state.clear() + + if callback.message is None: + await callback.answer("Сообщение не найдено", show_alert=True) + return + + await LiveScreenRunner.delete_screen( + screen="market", + bot=callback.message.bot, + chat_id=callback.message.chat.id, + ) + + user_id = callback.from_user.id if callback.from_user else None + chat_id = callback.message.chat.id if callback.message.chat else None + + try: + await _render_market_screen( + callback.message, + user_id=user_id, + chat_id=chat_id, + edit_mode=True, + action="open_from_monitoring", + ) + await callback.answer() + except ExchangeError as exc: + JournalService().log_ui_error( + event_type="market_open_error", + message="Не удалось загрузить экран рынка из мониторинга.", + screen="market", + action="open_from_monitoring", + user_id=user_id, + chat_id=chat_id, + error_type=classify_exchange_error(exc), + raw_error=str(exc), + ) + + await show_callback_exchange_error( + callback, + title="📈 Рынок", + exc=exc, + network_details="Рыночные данные недоступны.\nОбнови экран.", + auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.", + retry_callback_data="market:retry", + ) + + # обновить рынок вручную @router.callback_query(F.data == "market:retry") async def retry_market(callback: CallbackQuery, state: FSMContext) -> None: diff --git a/app/src/telegram/handlers/monitoring.py b/app/src/telegram/handlers/monitoring.py new file mode 100644 index 0000000..784465d --- /dev/null +++ b/app/src/telegram/handlers/monitoring.py @@ -0,0 +1,66 @@ +# app/src/telegram/handlers/monitoring.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.trading.auto.runner import AutoTradeRunner + + +router = Router(name="monitoring") + + +# клавиатура экрана мониторинга +def _monitoring_keyboard() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button(text="💼 Портфель", callback_data="monitoring:portfolio") + builder.button(text="📈 Рынок", callback_data="monitoring:market") + builder.button(text="📒 Журнал", callback_data="monitoring:journal") + builder.adjust(2, 1) + return builder.as_markup() + + +# текст экрана мониторинга +def _monitoring_text() -> str: + return ( + "📊 Мониторинг\n\n" + "Выберите раздел:" + ) + + +# открыть мониторинг из главного меню +@router.message(F.text == "📊 Мониторинг") +async def open_monitoring(message: Message, state: FSMContext) -> None: + await state.clear() + AutoTradeRunner.set_current_screen("monitoring") + + await message.answer( + _monitoring_text(), + reply_markup=_monitoring_keyboard(), + ) + + +# вернуться на экран мониторинга из callback +@router.callback_query(F.data == "monitoring:home") +async def open_monitoring_callback(callback: CallbackQuery, state: FSMContext) -> None: + await state.clear() + AutoTradeRunner.set_current_screen("monitoring") + + if callback.message is None: + await callback.answer("Сообщение не найдено", show_alert=True) + return + + await callback.message.edit_text( + _monitoring_text(), + reply_markup=_monitoring_keyboard(), + ) + await callback.answer() + + +# переход к портфелю из мониторинга + + +# переход к рынку из мониторинга \ No newline at end of file diff --git a/app/src/telegram/handlers/portfolio.py b/app/src/telegram/handlers/portfolio.py index d929aae..e52bb3b 100644 --- a/app/src/telegram/handlers/portfolio.py +++ b/app/src/telegram/handlers/portfolio.py @@ -68,7 +68,7 @@ def _compact_amount(currency: str, value: float) -> str: # клавиатура портфеля def _portfolio_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="📊 К мониторингу", callback_data="monitoring:home") builder.adjust(1) return builder.as_markup() @@ -77,7 +77,7 @@ def _portfolio_keyboard() -> InlineKeyboardMarkup: def _portfolio_warning_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="🔁 Обновить", callback_data="portfolio:retry") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="📊 К мониторингу", callback_data="monitoring:home") builder.adjust(1, 1) return builder.as_markup() @@ -296,6 +296,55 @@ async def open_portfolio(message: Message, state: FSMContext) -> None: ) +# открыть портфель из экрана мониторинга +@router.callback_query(F.data == "monitoring:portfolio") +async def open_portfolio_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None: + await state.clear() + + if callback.message is None: + await callback.answer("Сообщение не найдено", show_alert=True) + return + + await LiveScreenRunner.delete_screen( + screen="portfolio", + bot=callback.message.bot, + chat_id=callback.message.chat.id, + ) + + user_id = callback.from_user.id if callback.from_user else None + chat_id = callback.message.chat.id if callback.message.chat else None + + try: + await _render_portfolio_screen( + callback.message, + user_id=user_id, + chat_id=chat_id, + edit_mode=True, + action="open_from_monitoring", + ) + await callback.answer() + except ExchangeError as exc: + JournalService().log_ui_error( + event_type="portfolio_open_error", + message="Не удалось загрузить портфель из мониторинга.", + screen="portfolio", + action="open_from_monitoring", + user_id=user_id, + chat_id=chat_id, + error_type=classify_exchange_error(exc), + raw_error=str(exc), + ) + + await show_callback_exchange_error( + callback, + title="💼 Портфель", + exc=exc, + network_details="Не загружен баланс аккаунта.\nОбнови экран.", + auth_details="Не загружен баланс аккаунта.\nПроверь API ключи.", + retry_callback_data="portfolio:retry", + ) + + # обновить портфель вручную @router.callback_query(F.data == "portfolio:retry") async def retry_portfolio(callback: CallbackQuery, state: FSMContext) -> None: diff --git a/app/src/telegram/handlers/system.py b/app/src/telegram/handlers/system.py index 18b560b..daac544 100644 --- a/app/src/telegram/handlers/system.py +++ b/app/src/telegram/handlers/system.py @@ -20,20 +20,18 @@ router = Router(name="system") def _system_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="📒 Журнал", callback_data="journal:1") builder.button(text="🛠️ Настройки", callback_data="system:management") builder.button(text="ℹ️ Информация", callback_data="system:about") - builder.adjust(2, 1) + 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="journal:1") builder.button(text="🛠️ Настройки", callback_data="system:management") builder.button(text="ℹ️ Информация", callback_data="system:about") - builder.adjust(1, 2, 1) + builder.adjust(1, 2) return builder.as_markup() @@ -102,7 +100,7 @@ async def _render_system_screen( await target_message.answer(text, reply_markup=reply_markup) -@router.message(F.text.in_({"🖥️ Система", "⚙️ Система", "⚙ Система"})) +@router.message(F.text.in_({"🖥️ Система"})) async def open_system(message: Message, state: FSMContext) -> None: await state.clear() @@ -153,7 +151,7 @@ async def open_system_management(callback: CallbackQuery) -> None: builder = InlineKeyboardBuilder() builder.button(text="🤖 Автоторговля", callback_data="settings:auto") - builder.button(text="📊 Торговля", callback_data="settings:trade") + 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") @@ -322,7 +320,7 @@ async def open_trade_settings(callback: CallbackQuery) -> None: return text = ( - "📊 Торговля\n\n" + "💹 Торговля\n\n" "СИСТЕМА · Настройки\n\n" "Инструмент: —\n" "Тип ордера по умолчанию: —\n" @@ -332,7 +330,7 @@ async def open_trade_settings(callback: CallbackQuery) -> None: builder = InlineKeyboardBuilder() builder.button(text="⬅️ Назад", callback_data="system:management") - builder.button(text="📊 Торговля", callback_data="trade:home") + builder.button(text="💹 Торговля", callback_data="trade:home") builder.adjust(2) await callback.message.edit_text(text, reply_markup=builder.as_markup()) diff --git a/app/src/telegram/handlers/trade/main.py b/app/src/telegram/handlers/trade/main.py index c5dd444..016b67a 100644 --- a/app/src/telegram/handlers/trade/main.py +++ b/app/src/telegram/handlers/trade/main.py @@ -19,7 +19,7 @@ router = Router(name="trade_main") def _trade_screen(title: str) -> str: return ( - f"📊 Торговля — {title}\n" + f"💹 Торговля — {title}\n" f"{mode_line()}" "Выбери раздел" ) @@ -41,14 +41,14 @@ def _trade_home_keyboard() -> InlineKeyboardMarkup: def _trade_home_button() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") return builder.as_markup() def _orders_menu_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="📂 Черновики", callback_data="trade:orders:drafts") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2) return builder.as_markup() @@ -57,7 +57,7 @@ def _history_menu_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="✅ Исполненные", callback_data="trade:history:filled") builder.button(text="🚫 Отменённые", callback_data="trade:history:canceled") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2, 1) return builder.as_markup() @@ -67,7 +67,7 @@ def _settings_menu_keyboard() -> InlineKeyboardMarkup: builder.button(text="⚙️ Параметры", callback_data="trade:settings:params") builder.button(text="🔁 Режим", callback_data="trade:settings:mode") builder.button(text="ℹ️ Справка", callback_data="trade:settings:help") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2, 2) return builder.as_markup() @@ -95,7 +95,7 @@ def _trade_settings_text() -> str: # ENTRY # ========================= -@router.message(F.text.in_({"📊 Торговля", "⚡ Торговля", "Торговля"})) +@router.message(F.text.in_({"💹 Торговля"})) async def open_trade(message: Message) -> None: AutoTradeRunner.set_current_screen("trade") @@ -180,7 +180,7 @@ async def open_filled_history(callback: CallbackQuery) -> None: await callback.answer() if callback.message is not None: await callback.message.edit_text( - "📊 Торговля — История\n\n" + "💹 Торговля — История\n\n" "Шаг 1/1: Исполненные\n" "Раздел в разработке.", reply_markup=_trade_home_button(), @@ -192,7 +192,7 @@ async def open_canceled_history(callback: CallbackQuery) -> None: await callback.answer() if callback.message is not None: await callback.message.edit_text( - "📊 Торговля — История\n\n" + "💹 Торговля — История\n\n" "Шаг 1/1: Отменённые\n" "Раздел в разработке.", reply_markup=_trade_home_button(), @@ -220,7 +220,7 @@ async def open_trade_settings_params(callback: CallbackQuery) -> None: await callback.answer() if callback.message is not None: await callback.message.edit_text( - "📊 Торговля — Настройки\n\n" + "💹 Торговля — Настройки\n\n" "Шаг 1/1: Параметры ордера\n" "Раздел в разработке.", reply_markup=_trade_home_button(), @@ -232,7 +232,7 @@ async def open_trade_settings_mode(callback: CallbackQuery) -> None: await callback.answer() if callback.message is not None: await callback.message.edit_text( - "📊 Торговля — Настройки\n\n" + "💹 Торговля — Настройки\n\n" "Шаг 1/1: Режим работы\n" "Текущий режим: demo", reply_markup=_trade_home_button(), @@ -244,7 +244,7 @@ async def open_trade_settings_help(callback: CallbackQuery) -> None: await callback.answer() if callback.message is not None: await callback.message.edit_text( - "📊 Торговля — Справка\n\n" + "💹 Торговля — Справка\n\n" "Шаг 1/1: Информация\n" "Раздел в разработке.", reply_markup=_trade_home_button(), diff --git a/app/src/telegram/handlers/trade/new_order_flow.py b/app/src/telegram/handlers/trade/new_order_flow.py index f11e3da..36bd03f 100644 --- a/app/src/telegram/handlers/trade/new_order_flow.py +++ b/app/src/telegram/handlers/trade/new_order_flow.py @@ -51,13 +51,10 @@ MAIN_MENU_BUTTONS = { "🏠 Главная", "📈 Рынок", "💼 Портфель", - "📊 Торговля", - "⚡ Торговля", - "Торговля", + "💹 Торговля", "🤖 Авто", "📒 Журнал", - "⚙️ Система", - "⚙ Система", + "🖥️ Система", "Меню", } @@ -275,7 +272,7 @@ async def cancel_order_builder(message: Message, state: FSMContext) -> None: ) await message.answer( - "📊 Торговля — Новый ордер\n" + "💹 Торговля — Новый ордер\n" f"{mode_line()}" "⛔ Создание черновика отменено", reply_markup=_trade_back_home_keyboard(), @@ -298,7 +295,7 @@ async def start_new_order_draft( context = service.get_entry_context(side="BUY", order_type="MARKET") text = ( - "📊 Торговля — Новый ордер\n" + "💹 Торговля — Новый ордер\n" f"{mode_line()}" f"{context.symbol}\n\n" "Шаг 1/4. Выбери сторону" @@ -331,7 +328,7 @@ async def start_new_order_draft( ) await show_message_exchange_error( message, - title="📊 Торговля — Новый ордер", + title="💹 Торговля — Новый ордер", exc=exc, retry_callback_data="trade:new_order_retry", ) @@ -357,7 +354,7 @@ async def process_order_side_callback( context = service.get_entry_context(side=side, order_type="MARKET") text = ( - "📊 Торговля — Новый ордер\n" + "💹 Торговля — Новый ордер\n" f"{mode_line()}" f"{context.symbol}\n\n" f"{path}\n\n" @@ -390,7 +387,7 @@ async def process_order_side_callback( ) await show_callback_exchange_error( callback, - title="📊 Торговля — Новый ордер", + title="💹 Торговля — Новый ордер", exc=exc, retry_callback_data=callback.data, ) @@ -1251,7 +1248,7 @@ async def confirm_order(callback: CallbackQuery, state: FSMContext) -> None: await show_callback_exchange_error( callback, - title="📊 Торговля — Подтверждение черновика", + title="💹 Торговля — Подтверждение черновика", exc=exc, retry_callback_data=callback.data, drafts_page=data.get("draft_edit_page"), diff --git a/app/src/telegram/handlers/trade/new_order_navigation.py b/app/src/telegram/handlers/trade/new_order_navigation.py index 9f0bd14..1adf374 100644 --- a/app/src/telegram/handlers/trade/new_order_navigation.py +++ b/app/src/telegram/handlers/trade/new_order_navigation.py @@ -38,7 +38,7 @@ async def _return_to_draft_detail( if not draft: await callback.message.edit_text( - "📊 Торговля\n\n" + "💹 Торговля\n\n" "Черновик не найден.", reply_markup=_trade_back_home_keyboard(), ) @@ -94,7 +94,7 @@ async def go_back_to_side(callback: CallbackQuery, state: FSMContext) -> None: await state.set_state(NewOrderDraftStates.waiting_side) text = ( - "📊 Торговля — Новый ордер\n" + "💹 Торговля — Новый ордер\n" f"{mode_line()}" f"{context.symbol}\n\n" "Шаг 1/4. Выбери сторону" @@ -104,7 +104,7 @@ async def go_back_to_side(callback: CallbackQuery, state: FSMContext) -> None: except ExchangeError as exc: await _show_navigation_exchange_error( callback, - title="📊 Торговля — Новый ордер", + title="💹 Торговля — Новый ордер", exc=exc, ) @@ -133,7 +133,7 @@ async def go_back_to_type(callback: CallbackQuery, state: FSMContext) -> None: await state.set_state(NewOrderDraftStates.waiting_type) text = ( - "📊 Торговля — Новый ордер\n" + "💹 Торговля — Новый ордер\n" f"{mode_line()}" f"{context.symbol}\n\n" f"{path}\n\n" @@ -144,7 +144,7 @@ async def go_back_to_type(callback: CallbackQuery, state: FSMContext) -> None: except ExchangeError as exc: await _show_navigation_exchange_error( callback, - title="📊 Торговля — Новый ордер", + title="💹 Торговля — Новый ордер", exc=exc, ) @@ -213,7 +213,7 @@ async def go_back_from_confirm(callback: CallbackQuery, state: FSMContext) -> No if not confirm_draft: await state.clear() await callback.message.edit_text( - "📊 Торговля\n\n" + "💹 Торговля\n\n" "Не удалось восстановить шаг подтверждения.", reply_markup=_trade_back_home_keyboard(), ) diff --git a/app/src/telegram/handlers/trade/new_order_ui.py b/app/src/telegram/handlers/trade/new_order_ui.py index 0445cdc..1e0e833 100644 --- a/app/src/telegram/handlers/trade/new_order_ui.py +++ b/app/src/telegram/handlers/trade/new_order_ui.py @@ -159,7 +159,7 @@ def _side_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="🟢 BUY", callback_data="order_side:BUY") builder.button(text="🔴 SELL", callback_data="order_side:SELL") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2, 1) return builder.as_markup() @@ -169,7 +169,7 @@ def _type_keyboard() -> InlineKeyboardMarkup: builder.button(text="⚡ MARKET", callback_data="order_type:MARKET") builder.button(text="🎯 LIMIT", callback_data="order_type:LIMIT") builder.button(text="⬅️ Назад", callback_data="order_back:side") - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2, 2) return builder.as_markup() @@ -192,7 +192,7 @@ def _quantity_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") if len(presets) == 0: builder.adjust(1, 2) @@ -215,7 +215,7 @@ def _quantity_manual_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2) return builder.as_markup() @@ -237,7 +237,7 @@ def _price_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2, 2, 2) return builder.as_markup() @@ -252,7 +252,7 @@ def _price_manual_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2) return builder.as_markup() @@ -268,7 +268,7 @@ def _confirm_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(1, 2) return builder.as_markup() @@ -276,7 +276,7 @@ def _confirm_keyboard( def _trade_back_home_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") return builder.as_markup() @@ -304,7 +304,7 @@ def _drafts_pagination_keyboard(page: int, total_pages: int) -> InlineKeyboardMa if page < total_pages: first_row_count += 1 - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(first_row_count, 1) return builder.as_markup() @@ -334,7 +334,7 @@ def _exchange_error_keyboard( if drafts_page is not None: builder.button(text="📚 К черновикам", callback_data=f"drafts:{drafts_page}") else: - builder.button(text="🏠 К торговле", callback_data="trade:home") + builder.button(text="💹 К торговле", callback_data="trade:home") builder.adjust(2 if back_callback_data else 1) return builder.as_markup() @@ -386,7 +386,7 @@ def _render_draft_summary( success_text = "✅ Черновик изменён" if is_edit_mode else "✅ Черновик создан" lines = [ - "📊 Торговля — Черновик ордера", + "💹 Торговля — Черновик ордера", mode_line().rstrip(), "", f"{symbol}", @@ -468,7 +468,7 @@ def _render_confirm( def _render_validation_error(errors: list[str]) -> str: lines = [ - "📊 Торговля — Ошибка валидации", + "💹 Торговля — Ошибка валидации", mode_line().rstrip(), "Шаг 4/4. Проверь параметры черновика", "", @@ -603,7 +603,7 @@ def _render_draft_detail( order_type = str(draft["order_type"]).upper() lines = [ - "📊 Торговля — Черновик", + "💹 Торговля — Черновик", mode_line().rstrip(), "", f"{draft['symbol']}", @@ -642,8 +642,8 @@ def _format_draft_quantity(value: str) -> str: def _screen_title(is_edit_mode: bool) -> str: if is_edit_mode: - return "📊 Торговля — Редактирование черновика" - return "📊 Торговля — Новый ордер" + return "💹 Торговля — Редактирование черновика" + return "💹 Торговля — Новый ордер" # Рендерит экран выбора количества. @@ -908,7 +908,7 @@ async def show_recent_drafts( if not drafts: text = ( - "📊 Торговля — Черновики\n" + "💹 Торговля — Черновики\n" f"{mode_line()}" "Список пуст\n\n" "Черновиков пока нет." @@ -920,7 +920,7 @@ async def show_recent_drafts( return lines = [ - "📊 Торговля — Черновики", + "💹 Торговля — Черновики", mode_line().rstrip(), "", ] diff --git a/app/src/telegram/keyboards/reply.py b/app/src/telegram/keyboards/reply.py index 22ba154..837ac62 100644 --- a/app/src/telegram/keyboards/reply.py +++ b/app/src/telegram/keyboards/reply.py @@ -8,13 +8,10 @@ def build_main_menu_keyboard() -> ReplyKeyboardMarkup: keyboard=[ [ KeyboardButton(text="🤖 Автоторговля"), - KeyboardButton(text="📊 Торговля"), - ], - [ - KeyboardButton(text="💼 Портфель"), - KeyboardButton(text="📈 Рынок"), + KeyboardButton(text="💹 Торговля"), ], [ + KeyboardButton(text="📊 Мониторинг"), KeyboardButton(text="🖥️ Система"), ], ], diff --git a/app/src/telegram/live/runner.py b/app/src/telegram/live/runner.py index 32dac8d..d0814bb 100644 --- a/app/src/telegram/live/runner.py +++ b/app/src/telegram/live/runner.py @@ -7,11 +7,12 @@ from dataclasses import dataclass from typing import Callable from aiogram import Bot +from aiogram.exceptions import TelegramBadRequest @dataclass(slots=True) class LiveScreen: - # имя live-экрана: market / portfolio / journal + # имя live-экрана: market / portfolio screen: str # Telegram bot instance @@ -33,6 +34,71 @@ class LiveScreen: interval_seconds: int = 5 +@dataclass(slots=True) +class StaticScreen: + # имя статичного экрана: journal / monitoring / etc + screen: str + + # Telegram bot instance + bot: Bot + + # чат, где находится экран + chat_id: int + + # сообщение экрана + message_id: int + + +class ScreenRegistry: + _screens: dict[str, list[StaticScreen]] = {} + + # зарегистрировать статичный экран + @classmethod + def register_screen(cls, static_screen: StaticScreen) -> None: + screens = cls._screens.setdefault(static_screen.screen, []) + + screens[:] = [ + item + for item in screens + if not ( + item.chat_id == static_screen.chat_id + and item.message_id == static_screen.message_id + ) + ] + + screens.append(static_screen) + + # удалить старые статичные экраны указанного типа + @classmethod + async def delete_screen( + cls, + *, + screen: str, + bot: Bot, + chat_id: int, + ) -> None: + screens = cls._screens.get(screen, []) + remaining: list[StaticScreen] = [] + + for static_screen in screens: + if static_screen.chat_id != chat_id: + remaining.append(static_screen) + continue + + try: + await bot.delete_message( + chat_id=static_screen.chat_id, + message_id=static_screen.message_id, + ) + except Exception: + pass + + if remaining: + cls._screens[screen] = remaining + else: + cls._screens.pop(screen, None) + + class LiveScreenRunner: _screens: dict[str, list[LiveScreen]] = {} _tasks: dict[str, asyncio.Task] = {} @@ -63,7 +129,6 @@ class LiveScreenRunner: chat_id: int, ) -> None: screens = cls._screens.get(screen, []) - remaining: list[LiveScreen] = [] for live_screen in screens: @@ -139,6 +204,12 @@ class LiveScreenRunner: reply_markup=live_screen.render_markup(), ) alive_screens.append(live_screen) + + except TelegramBadRequest as exc: + if "message is not modified" in str(exc).lower(): + alive_screens.append(live_screen) + continue + except Exception: pass diff --git a/app/src/telegram/menus.py b/app/src/telegram/menus.py index b099b85..1220ced 100644 --- a/app/src/telegram/menus.py +++ b/app/src/telegram/menus.py @@ -17,7 +17,7 @@ HOME_TEXT = ( ) SYSTEM_TEXT = ( - "⚙️ Система\n\n" + "🖥️ Система\n\n" "Системный экран.\n\n" "Справка\n" "/start — запуск\n" @@ -27,6 +27,6 @@ SYSTEM_TEXT = ( MARKET_TEXT = "📈 Рынок\n\nРаздел пока в разработке." PORTFOLIO_TEXT = "💼 Портфель\n\nРаздел пока в разработке." -TRADE_TEXT = "📊 Торговля\n\nВыберите действие:\nDRAFT режим" +TRADE_TEXT = "💹 Торговля\n\nВыберите действие:\nDRAFT режим" AUTO_TEXT = "🤖 Авто\n\nРаздел пока в разработке." JOURNAL_TEXT = "📒 Журнал\n\nРаздел пока в разработке." \ No newline at end of file diff --git a/app/src/telegram/routers.py b/app/src/telegram/routers.py index 9425b98..03db76b 100644 --- a/app/src/telegram/routers.py +++ b/app/src/telegram/routers.py @@ -1,9 +1,12 @@ +# app/src/telegram/routers.py + from aiogram import Dispatcher from src.telegram.handlers.auto import router as auto_router from src.telegram.handlers.home import router as home_router from src.telegram.handlers.journal import router as journal_router from src.telegram.handlers.market import router as market_router +from src.telegram.handlers.monitoring import router as monitoring_router from src.telegram.handlers.portfolio import router as portfolio_router from src.telegram.handlers.start import router as start_router from src.telegram.handlers.system import router as system_router @@ -14,10 +17,11 @@ from src.telegram.handlers.trade.new_order import router as trade_new_order_rout def setup_routers(dispatcher: Dispatcher) -> None: dispatcher.include_router(start_router) dispatcher.include_router(home_router) + dispatcher.include_router(monitoring_router) dispatcher.include_router(market_router) dispatcher.include_router(portfolio_router) dispatcher.include_router(trade_main_router) dispatcher.include_router(trade_new_order_router) dispatcher.include_router(auto_router) dispatcher.include_router(journal_router) - dispatcher.include_router(system_router) + dispatcher.include_router(system_router) \ No newline at end of file diff --git a/docs/stages/stage-07_3_4-monitoring-screen-and-journal-migration.md b/docs/stages/stage-07_3_4-monitoring-screen-and-journal-migration.md new file mode 100644 index 0000000..97e0014 --- /dev/null +++ b/docs/stages/stage-07_3_4-monitoring-screen-and-journal-migration.md @@ -0,0 +1,100 @@ +# Stage 07.3.4 — Monitoring Screen and Journal Migration + +## Цель + +Добавить единый экран мониторинга и перенести Журнал из раздела Система. + +--- + +## Что реализовано + +### Экран Мониторинг + +Добавлен новый раздел: + +- 📊 Мониторинг + +Экран содержит быстрые переходы: + +- 💼 Портфель +- 📈 Рынок +- 📒 Журнал + +--- + +### Навигация + +Обновлена структура главного меню: + +``` +🤖 Автоторговля 📊 Торговля +📊 Мониторинг 🖥️ Система +``` + +--- + +### Интеграция live-экранов + +Портфель и Рынок теперь работают как дочерние экраны Мониторинга: + +* кнопка «📊 К мониторингу» +* возврат через callback +* сохраняется live-refresh + +--- + +### Журнал перенесён + +Журнал убран из: + +* экрана Система +* клавиатуры Система + +Журнал теперь относится к Monitoring / Observability. + +--- + +### LiveScreenRunner improvements + +Улучшена логика live-экранов: + +* поддержка нескольких live-сообщений одного типа; +* обновление всех активных экземпляров; +* обработка TelegramBadRequest: + * message is not modified больше не убивает экран. + +--- + +## Архитектурный результат + +Теперь разделы выглядят логично: + +🤖 Автоторговля + +управление стратегией + +📊 Торговля + +ручное создание ордеров + +📊 Мониторинг + +наблюдение за системой торговли: + +* рынок +* портфель +* журнал + +🖥️ Система + +администрирование и настройки + +--- + +## Следующий этап + +Stage 07.3.5 — WebSocket Market Stream + +Цель: + +убрать polling REST для рынка и перейти на realtime stream. \ No newline at end of file