feat: unify market/portfolio/system UI, improve exchange errors and asset valuation
This commit is contained in:
@@ -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",
|
||||
)
|
||||
Reference in New Issue
Block a user