feat: unify market/portfolio/system UI, improve exchange errors and asset valuation

This commit is contained in:
2026-04-22 18:21:34 +03:00
parent 2a9ef16524
commit 1fb72ced58
13 changed files with 2034 additions and 822 deletions

View File

@@ -4,10 +4,16 @@ from __future__ import annotations
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.service import ExchangeService
from src.telegram.ui.common import mode_line
from src.telegram.ui.exchange_error import (
show_callback_exchange_error,
show_message_exchange_error,
)
from src.trading.journal.service import JournalService
@@ -50,16 +56,58 @@ def _safe_log_error(
pass
@router.message(F.text == "📈 Рынок")
async def open_market(message: Message, state: FSMContext) -> None:
# Глобальный экран: всегда выходим из текущего FSM-сценария.
await state.clear()
def _market_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="🏠 К торговле", callback_data="trade:home")
builder.adjust(1)
return builder.as_markup()
def _build_market_text(
*,
ticker_price: float,
name: str,
symbol_status: str,
market_type: str,
base_asset: str,
quote_asset: str,
tick_size: str,
) -> str:
status_map = {
"TRADING": "доступен для торговли",
"HALT": "торги остановлены",
"BREAK": "перерыв",
}
status_ru = status_map.get(symbol_status.upper(), symbol_status.lower())
type_map = {
"LEVERAGE": "leverage",
"SPOT": "spot",
}
market_type_ru = type_map.get(market_type.upper(), market_type.lower())
return (
"<b>📈 Рынок</b>\n"
f"{mode_line()}"
f"Пара: <b>{name}</b>\n"
f"Цена: <b>{ticker_price:.2f} {quote_asset}</b>\n"
f"Статус: {status_ru}\n"
f"Тип инструмента: {market_type_ru}\n"
f"Базовый актив: {base_asset}\n"
f"Валюта котировки: {quote_asset}\n"
f"Шаг цены: {tick_size} {quote_asset}"
)
async def _render_market_screen(
target_message: Message,
*,
user_id: int | None,
chat_id: int | None,
edit_mode: bool,
) -> 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(
@@ -73,52 +121,38 @@ async def open_market(message: Message, state: FSMContext) -> None:
},
)
try:
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}"
)
return
validation = service.validate_symbol(requested_symbol)
ticker = service.get_price(validation.normalized_symbol)
except ExchangeError as exc:
_safe_log_error(
if not validation.is_valid:
_safe_log_warning(
journal,
"market_open_error",
f"Не удалось открыть экран рынка: {exc}",
"market_symbol_invalid",
f"Символ не прошел проверку: {validation.message}",
{
"user_id": user_id,
"chat_id": chat_id,
"symbol": requested_symbol,
},
)
await message.answer(
"<b>📈 Рынок</b>\n\n"
"Не удалось получить цену с биржи.\n"
f"Ошибка: {exc}"
text = (
"<b>📈 Рынок</b>\n"
f"{mode_line()}"
"⚠️ Ошибка инструмента\n\n"
"Инструмент недоступен."
)
if edit_mode:
await target_message.edit_text(text, reply_markup=_market_keyboard())
else:
await target_message.answer(text, reply_markup=_market_keyboard())
return
ticker = service.get_price(validation.normalized_symbol)
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
@@ -128,19 +162,14 @@ async def open_market(message: Message, state: FSMContext) -> None:
quote_asset = symbol_info.quote_asset if symbol_info and symbol_info.quote_asset else "n/a"
name = symbol_info.name if symbol_info and symbol_info.name else ticker.symbol
text = (
"<b>📈 Рынок</b>\n\n"
f"Символ: <b>{ticker.symbol}</b>\n"
f"Название: {name}\n"
f"Цена: <b>{ticker.price:.2f}</b>\n"
f"Статус: {symbol_status}\n"
f"Тип рынка: {market_type}\n"
f"Режимы: {market_modes}\n"
f"Base asset: {base_asset}\n"
f"Quote asset: {quote_asset}\n"
f"Tick size: {tick_size}\n"
f"Источник: {ticker.source}\n"
f"Обновлено: {ticker.updated_at}"
text = _build_market_text(
ticker_price=ticker.price,
name=name,
symbol_status=symbol_status,
market_type=market_type,
base_asset=base_asset,
quote_asset=quote_asset,
tick_size=tick_size,
)
_safe_log_info(
@@ -152,9 +181,79 @@ async def open_market(message: Message, state: FSMContext) -> None:
"chat_id": chat_id,
"symbol": ticker.symbol,
"price": ticker.price,
"base_asset": base_asset,
"quote_asset": quote_asset,
},
)
await message.answer(text)
if edit_mode:
await target_message.edit_text(text, reply_markup=_market_keyboard())
else:
await target_message.answer(text, reply_markup=_market_keyboard())
@router.message(F.text == "📈 Рынок")
async def open_market(message: Message, state: FSMContext) -> None:
await state.clear()
user_id = message.from_user.id if message.from_user else None
chat_id = message.chat.id if message.chat else None
try:
await _render_market_screen(
message,
user_id=user_id,
chat_id=chat_id,
edit_mode=False,
)
except ExchangeError as exc:
_safe_log_error(
JournalService(),
"market_open_error",
f"Не удалось открыть экран рынка: {exc}",
{"user_id": user_id, "chat_id": chat_id},
)
await show_message_exchange_error(
message,
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:
await state.clear()
if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True)
return
user_id = callback.from_user.id if callback.from_user else None
chat_id = callback.message.chat.id if callback.message.chat else None
try:
await _render_market_screen(
callback.message,
user_id=user_id,
chat_id=chat_id,
edit_mode=True,
)
await callback.answer()
except ExchangeError as exc:
_safe_log_error(
JournalService(),
"market_retry_error",
f"Не удалось повторно открыть рынок: {exc}",
{"user_id": user_id, "chat_id": chat_id},
)
await show_callback_exchange_error(
callback,
title="<b>📈 Рынок</b>",
exc=exc,
network_details="Рыночные данные недоступны.\nОбнови экран.",
auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.",
retry_callback_data="market:retry",
)