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