Stage 04.2 - journal and event log

This commit is contained in:
2026-04-16 13:13:03 +03:00
parent c35deeaefa
commit 2c49bb70c0
11 changed files with 780 additions and 28 deletions

View File

@@ -1,12 +1,85 @@
from aiogram import F, Router
from aiogram.types import Message
from __future__ import annotations
from src.telegram.menus import JOURNAL_TEXT
from aiogram import F, Router
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.trading.journal.service import JournalService
router = Router(name="journal")
PAGE_SIZE = 3
def build_keyboard(page: int, total_pages: int):
kb = InlineKeyboardBuilder()
# кнопка "в начало"
if page > 1:
kb.button(text="⏮️", callback_data="journal:1")
# назад
if page > 1:
kb.button(text="⬅️", callback_data=f"journal:{page-1}")
# текущая страница
kb.button(text=f"{page}/{total_pages}", callback_data="noop")
# вперед
if page < total_pages:
kb.button(text="➡️", callback_data=f"journal:{page+1}")
return kb.as_markup()
def render(events, page, total_pages):
lines = ["<b>📒 Журнал</b>", "", "<b>Последние события</b>", ""]
for e in events:
lines.extend(
[
f" <b>{e['event_type']}</b>",
f"• уровень: {e['level']}",
f"• время: {e['created_at']}",
f"• сообщение: {e['message']}",
"",
]
)
return "\n".join(lines)
@router.message(F.text == "📒 Журнал")
async def open_journal(message: Message) -> None:
await message.answer(JOURNAL_TEXT)
async def open_journal(message: Message):
service = JournalService()
total = service.get_total_count()
total_pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
events = service.get_page(1, PAGE_SIZE)
text = render(events, 1, total_pages)
kb = build_keyboard(1, total_pages)
await message.answer(text, reply_markup=kb)
@router.callback_query(F.data.startswith("journal:"))
async def paginate(callback: CallbackQuery):
page = int(callback.data.split(":")[1])
service = JournalService()
total = service.get_total_count()
total_pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
page = max(1, min(page, total_pages))
events = service.get_page(page, PAGE_SIZE)
text = render(events, page, total_pages)
kb = build_keyboard(page, total_pages)
await callback.message.edit_text(text, reply_markup=kb)
await callback.answer()

View File

@@ -5,26 +5,99 @@ from aiogram.types import Message
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.service import ExchangeService
from src.trading.journal.service import JournalService
router = Router(name="market")
def _safe_log_info(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_info(event_type, message, payload)
except Exception:
pass
def _safe_log_warning(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_warning(event_type, message, payload)
except Exception:
pass
def _safe_log_error(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_error(event_type, message, payload)
except Exception:
pass
@router.message(F.text == "📈 Рынок")
async def open_market(message: Message) -> None:
service = ExchangeService()
journal = JournalService()
user_id = message.from_user.id if message.from_user else None
chat_id = message.chat.id if message.chat else None
requested_symbol = service.settings.default_symbol
_safe_log_info(
journal,
"user_open_market",
"Пользователь открыл экран рынка.",
{
"user_id": user_id,
"chat_id": chat_id,
"symbol": requested_symbol,
},
)
try:
validation = service.validate_symbol(service.settings.default_symbol)
validation = service.validate_symbol(requested_symbol)
if not validation.is_valid:
_safe_log_warning(
journal,
"market_symbol_invalid",
f"Символ не прошел проверку: {validation.message}",
{
"user_id": user_id,
"chat_id": chat_id,
"symbol": requested_symbol,
},
)
await message.answer(
"<b>📈 Рынок</b>\n\n"
f"Ошибка символа: {validation.message}"
f"Ошибка инструмента: {validation.message}"
)
return
ticker = service.get_price(validation.normalized_symbol)
except ExchangeError as exc:
_safe_log_error(
journal,
"market_open_error",
f"Не удалось открыть экран рынка: {exc}",
{
"user_id": user_id,
"chat_id": chat_id,
"symbol": requested_symbol,
},
)
await message.answer(
"<b>📈 Рынок</b>\n\n"
"Не удалось получить цену с биржи.\n"
@@ -35,8 +108,16 @@ async def open_market(message: Message) -> None:
symbol_info = validation.symbol_info
symbol_status = symbol_info.status if symbol_info else "n/a"
market_type = symbol_info.market_type if symbol_info else "n/a"
market_modes = ", ".join(symbol_info.market_modes) if symbol_info and symbol_info.market_modes else "n/a"
tick_size = f"{symbol_info.tick_size}" if symbol_info and symbol_info.tick_size is not None else "n/a"
market_modes = (
", ".join(symbol_info.market_modes)
if symbol_info and symbol_info.market_modes
else "n/a"
)
tick_size = (
f"{symbol_info.tick_size}"
if symbol_info and symbol_info.tick_size is not None
else "n/a"
)
name = symbol_info.name if symbol_info and symbol_info.name else ticker.symbol
text = (
@@ -52,4 +133,16 @@ async def open_market(message: Message) -> None:
f"Обновлено: {ticker.updated_at}"
)
await message.answer(text)
_safe_log_info(
journal,
"market_open_success",
"Экран рынка успешно показан пользователю.",
{
"user_id": user_id,
"chat_id": chat_id,
"symbol": ticker.symbol,
"price": ticker.price,
},
)
await message.answer(text)

View File

@@ -6,6 +6,7 @@ from aiogram.types import Message
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.models import BalanceSummary
from src.integrations.exchange.service import ExchangeService
from src.trading.journal.service import JournalService
router = Router(name="portfolio")
@@ -60,7 +61,9 @@ def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
return sorted(items, key=sort_key)
def split_balances(items: list[BalanceSummary]) -> tuple[list[BalanceSummary], list[BalanceSummary]]:
def split_balances(
items: list[BalanceSummary],
) -> tuple[list[BalanceSummary], list[BalanceSummary]]:
major: list[BalanceSummary] = []
other: list[BalanceSummary] = []
@@ -85,13 +88,72 @@ def render_balance_block(item: BalanceSummary) -> list[str]:
]
def _safe_log_info(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_info(event_type, message, payload)
except Exception:
pass
def _safe_log_warning(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_warning(event_type, message, payload)
except Exception:
pass
def _safe_log_error(
journal: JournalService,
event_type: str,
message: str,
payload: dict | None = None,
) -> None:
try:
journal.log_error(event_type, message, payload)
except Exception:
pass
@router.message(F.text == "💼 Портфель")
async def open_portfolio(message: Message) -> None:
service = ExchangeService()
journal = JournalService()
user_id = message.from_user.id if message.from_user else None
chat_id = message.chat.id if message.chat else None
_safe_log_info(
journal,
"user_open_portfolio",
"Пользователь открыл экран портфеля.",
{
"user_id": user_id,
"chat_id": chat_id,
},
)
try:
balances = service.get_balance_summary()
except ExchangeError as exc:
_safe_log_error(
journal,
"portfolio_open_error",
f"Не удалось открыть портфель: {exc}",
{
"user_id": user_id,
"chat_id": chat_id,
},
)
await message.answer(
"<b>💼 Портфель</b>\n\n"
"Не удалось получить баланс с private API.\n"
@@ -100,6 +162,15 @@ async def open_portfolio(message: Message) -> None:
return
if not balances:
_safe_log_warning(
journal,
"portfolio_empty",
"Портфель открыт, но баланс пуст.",
{
"user_id": user_id,
"chat_id": chat_id,
},
)
await message.answer(
"<b>💼 Портфель</b>\n\n"
"Баланс пуст."
@@ -110,6 +181,16 @@ async def open_portfolio(message: Message) -> None:
visible_balances = sort_balances(visible_balances)
if not visible_balances:
_safe_log_warning(
journal,
"portfolio_zero_balances",
"Портфель открыт, но все балансы нулевые.",
{
"user_id": user_id,
"chat_id": chat_id,
"assets_count": len(balances),
},
)
await message.answer(
"<b>💼 Портфель</b>\n\n"
"Все балансы нулевые."
@@ -139,5 +220,16 @@ async def open_portfolio(message: Message) -> None:
]
)
_safe_log_info(
journal,
"portfolio_open_success",
"Портфель успешно показан пользователю.",
{
"user_id": user_id,
"chat_id": chat_id,
"assets_count": len(visible_balances),
},
)
text = "\n".join(lines).rstrip()
await message.answer(text)
await message.answer(text)