Stage 07.3.4 - monitoring screen and journal migration

This commit is contained in:
2026-04-29 15:15:07 +03:00
parent 51659037bb
commit 41c332d9cb
15 changed files with 455 additions and 70 deletions

View File

@@ -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()

View File

@@ -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 (
"<b>📤 Экспорт</b>\n\n"
"<b>СИСТЕМА · Журнал</b>\n\n"
"<b>МОНИТОРИНГ · Журнал</b>\n\n"
"Выберите формат:"
)
@@ -193,7 +193,7 @@ def render(events, page, total_pages):
lines = [
"<b>📒 Журнал</b>",
"",
"<b>СИСТЕМА</b>",
"<b>МОНИТОРИНГ</b>",
"",
"<b>Последние события:</b>",
"",

View File

@@ -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="<b>📈 Рынок</b>",
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:

View File

@@ -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 (
"<b>📊 Мониторинг</b>\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()
# переход к портфелю из мониторинга
# переход к рынку из мониторинга

View File

@@ -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="<b>💼 Портфель</b>",
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:

View File

@@ -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 = (
"<b>📊 Торговля</b>\n\n"
"<b>💹 Торговля</b>\n\n"
"<b>СИСТЕМА</b> · Настройки\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())

View File

@@ -19,7 +19,7 @@ router = Router(name="trade_main")
def _trade_screen(title: str) -> str:
return (
f"<b>📊 Торговля — {title}</b>\n"
f"<b>💹 Торговля — {title}</b>\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(
"<b>📊 Торговля — История</b>\n\n"
"<b>💹 Торговля — История</b>\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(
"<b>📊 Торговля — История</b>\n\n"
"<b>💹 Торговля — История</b>\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(
"<b>📊 Торговля — Настройки</b>\n\n"
"<b>💹 Торговля — Настройки</b>\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(
"<b>📊 Торговля — Настройки</b>\n\n"
"<b>💹 Торговля — Настройки</b>\n\n"
"Шаг 1/1: Режим работы\n"
"Текущий режим: <b>demo</b>",
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(
"<b>📊 Торговля — Справка</b>\n\n"
"<b>💹 Торговля — Справка</b>\n\n"
"Шаг 1/1: Информация\n"
"Раздел в разработке.",
reply_markup=_trade_home_button(),

View File

@@ -51,13 +51,10 @@ MAIN_MENU_BUTTONS = {
"🏠 Главная",
"📈 Рынок",
"💼 Портфель",
"📊 Торговля",
"⚡ Торговля",
"Торговля",
"💹 Торговля",
"🤖 Авто",
"📒 Журнал",
" Система",
"⚙ Система",
"🖥 Система",
"Меню",
}
@@ -275,7 +272,7 @@ async def cancel_order_builder(message: Message, state: FSMContext) -> None:
)
await message.answer(
"<b>📊 Торговля — Новый ордер</b>\n"
"<b>💹 Торговля — Новый ордер</b>\n"
f"{mode_line()}"
"<b>⛔ Создание черновика отменено</b>",
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 = (
"<b>📊 Торговля — Новый ордер</b>\n"
"<b>💹 Торговля — Новый ордер</b>\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="<b>📊 Торговля — Новый ордер</b>",
title="<b>💹 Торговля — Новый ордер</b>",
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 = (
"<b>📊 Торговля — Новый ордер</b>\n"
"<b>💹 Торговля — Новый ордер</b>\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="<b>📊 Торговля — Новый ордер</b>",
title="<b>💹 Торговля — Новый ордер</b>",
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="<b>📊 Торговля — Подтверждение черновика</b>",
title="<b>💹 Торговля — Подтверждение черновика</b>",
exc=exc,
retry_callback_data=callback.data,
drafts_page=data.get("draft_edit_page"),

View File

@@ -38,7 +38,7 @@ async def _return_to_draft_detail(
if not draft:
await callback.message.edit_text(
"<b>📊 Торговля</b>\n\n"
"<b>💹 Торговля</b>\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 = (
"<b>📊 Торговля — Новый ордер</b>\n"
"<b>💹 Торговля — Новый ордер</b>\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="<b>📊 Торговля — Новый ордер</b>",
title="<b>💹 Торговля — Новый ордер</b>",
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 = (
"<b>📊 Торговля — Новый ордер</b>\n"
"<b>💹 Торговля — Новый ордер</b>\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="<b>📊 Торговля — Новый ордер</b>",
title="<b>💹 Торговля — Новый ордер</b>",
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(
"<b>📊 Торговля</b>\n\n"
"<b>💹 Торговля</b>\n\n"
"Не удалось восстановить шаг подтверждения.",
reply_markup=_trade_back_home_keyboard(),
)

View File

@@ -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 = "✅ <b>Черновик изменён</b>" if is_edit_mode else "✅ <b>Черновик создан</b>"
lines = [
"<b>📊 Торговля — Черновик ордера</b>",
"<b>💹 Торговля — Черновик ордера</b>",
mode_line().rstrip(),
"",
f"{symbol}",
@@ -468,7 +468,7 @@ def _render_confirm(
def _render_validation_error(errors: list[str]) -> str:
lines = [
"<b>📊 Торговля — Ошибка валидации</b>",
"<b>💹 Торговля — Ошибка валидации</b>",
mode_line().rstrip(),
"Шаг 4/4. Проверь параметры черновика",
"",
@@ -603,7 +603,7 @@ def _render_draft_detail(
order_type = str(draft["order_type"]).upper()
lines = [
"<b>📊 Торговля — Черновик</b>",
"<b>💹 Торговля — Черновик</b>",
mode_line().rstrip(),
"",
f"<b>{draft['symbol']}</b>",
@@ -642,8 +642,8 @@ def _format_draft_quantity(value: str) -> str:
def _screen_title(is_edit_mode: bool) -> str:
if is_edit_mode:
return "<b>📊 Торговля — Редактирование черновика</b>"
return "<b>📊 Торговля — Новый ордер</b>"
return "<b>💹 Торговля — Редактирование черновика</b>"
return "<b>💹 Торговля — Новый ордер</b>"
# Рендерит экран выбора количества.
@@ -908,7 +908,7 @@ async def show_recent_drafts(
if not drafts:
text = (
"<b>📊 Торговля — Черновики</b>\n"
"<b>💹 Торговля — Черновики</b>\n"
f"{mode_line()}"
"<b>Список пуст</b>\n\n"
"Черновиков пока нет."
@@ -920,7 +920,7 @@ async def show_recent_drafts(
return
lines = [
"<b>📊 Торговля — Черновики</b>",
"<b>💹 Торговля — Черновики</b>",
mode_line().rstrip(),
"",
]

View File

@@ -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="🖥️ Система"),
],
],

View File

@@ -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

View File

@@ -17,7 +17,7 @@ HOME_TEXT = (
)
SYSTEM_TEXT = (
"<b> Система</b>\n\n"
"<b>🖥 Система</b>\n\n"
"Системный экран.\n\n"
"<b>Справка</b>\n"
"/start — запуск\n"
@@ -27,6 +27,6 @@ SYSTEM_TEXT = (
MARKET_TEXT = "<b>📈 Рынок</b>\n\nРаздел пока в разработке."
PORTFOLIO_TEXT = "<b>💼 Портфель</b>\n\nРаздел пока в разработке."
TRADE_TEXT = "<b>📊 Торговля</b>\n\nВыберите действие:\n<i>DRAFT режим</i>"
TRADE_TEXT = "<b>💹 Торговля</b>\n\nВыберите действие:\n<i>DRAFT режим</i>"
AUTO_TEXT = "<b>🤖 Авто</b>\n\nРаздел пока в разработке."
JOURNAL_TEXT = "<b>📒 Журнал</b>\n\nРаздел пока в разработке."

View File

@@ -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,6 +17,7 @@ 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)

View File

@@ -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.