# app/src/telegram/handlers/market.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.integrations.exchange.exceptions import ExchangeError from src.integrations.exchange.service import ExchangeService from src.telegram.live.runner import LiveScreen, LiveScreenRunner from src.telegram.ui.common import mode_line, now_line from src.telegram.ui.currency_ui import format_usd_amount from src.telegram.ui.exchange_error import ( classify_exchange_error, show_callback_exchange_error, show_message_exchange_error, ) from src.trading.auto.runner import AutoTradeRunner from src.trading.journal.service import JournalService router = Router(name="market") _last_market_prices: dict[str, float] = {} _last_market_directions: dict[str, str] = {} # клавиатура экрана рынка 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, market_type: str, base_asset: str, quote_asset: str, ) -> str: previous_price = _last_market_prices.get(name) price_direction = _last_market_directions.get(name, "▲") if previous_price is not None: if ticker_price > previous_price: price_direction = "▲" elif ticker_price < previous_price: price_direction = "▼" _last_market_prices[name] = ticker_price _last_market_directions[name] = price_direction type_map = { "LEVERAGE": "leverage", "SPOT": "spot", } market_type_ru = type_map.get(market_type.upper(), market_type.lower()) return ( "📈 Рынок\n" f"{mode_line()}" "\n" f"{base_asset} / {quote_asset} ({market_type_ru})\n\n" f"$ {format_usd_amount(ticker_price)} {price_direction}\n\n" f"{now_line()}" ) # собрать актуальный live-текст рынка def _build_market_live_text() -> str: service = ExchangeService() requested_symbol = service.settings.default_symbol validation = service.validate_symbol(requested_symbol) if not validation.is_valid: return ( "📈 Рынок\n" f"{mode_line()}" "⚠️ Ошибка инструмента\n\n" "Инструмент недоступен." ) ticker = service.get_price(validation.normalized_symbol) symbol_info = validation.symbol_info market_type = symbol_info.market_type if symbol_info else "n/a" base_asset = symbol_info.base_asset if symbol_info and symbol_info.base_asset else "n/a" 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 return _build_market_text( ticker_price=ticker.price, name=name, market_type=market_type, base_asset=base_asset, quote_asset=quote_asset, ) # зарегистрировать сообщение как live-экран рынка def _register_market_live_screen(message: Message) -> None: LiveScreenRunner.register_screen( LiveScreen( screen="market", bot=message.bot, chat_id=message.chat.id, message_id=message.message_id, render_text=_build_market_live_text, render_markup=_market_keyboard, interval_seconds=5, ) ) LiveScreenRunner.start("market") # отрисовать экран рынка async def _render_market_screen( target_message: Message, *, user_id: int | None, chat_id: int | None, edit_mode: bool, action: str, ) -> None: AutoTradeRunner.set_current_screen("market") service = ExchangeService() journal = JournalService() requested_symbol = service.settings.default_symbol journal.log_ui_info( event_type="market_open_requested", message="Запрошено открытие экрана рынка.", screen="market", action=action, user_id=user_id, chat_id=chat_id, payload={"symbol": requested_symbol}, ) validation = service.validate_symbol(requested_symbol) if not validation.is_valid: journal.log_ui_warning( event_type="market_symbol_invalid", message="Инструмент недоступен.", screen="market", action=action, user_id=user_id, chat_id=chat_id, payload={ "symbol": requested_symbol, "validation_message": validation.message, }, ) text = ( "📈 Рынок\n" f"{mode_line()}" "⚠️ Ошибка инструмента\n\n" "Инструмент недоступен." ) if edit_mode: await target_message.edit_text(text, reply_markup=_market_keyboard()) _register_market_live_screen(target_message) else: sent_message = await target_message.answer(text, reply_markup=_market_keyboard()) _register_market_live_screen(sent_message) return ticker = service.get_price(validation.normalized_symbol) symbol_info = validation.symbol_info market_type = symbol_info.market_type if symbol_info else "n/a" base_asset = symbol_info.base_asset if symbol_info and symbol_info.base_asset else "n/a" 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 = _build_market_text( ticker_price=ticker.price, name=name, market_type=market_type, base_asset=base_asset, quote_asset=quote_asset, ) journal.log_ui_info( event_type="market_open_success", message="Экран рынка загружен.", screen="market", action=action, user_id=user_id, chat_id=chat_id, payload={ "symbol": ticker.symbol, "price": ticker.price, }, ) if edit_mode: await target_message.edit_text(text, reply_markup=_market_keyboard()) _register_market_live_screen(target_message) else: sent_message = await target_message.answer(text, reply_markup=_market_keyboard()) _register_market_live_screen(sent_message) # открыть рынок из главного меню @router.message(F.text == "📈 Рынок") async def open_market(message: Message, state: FSMContext) -> None: await state.clear() await LiveScreenRunner.delete_screen( screen="market", bot=message.bot, chat_id=message.chat.id, ) 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, action="open", ) except ExchangeError as exc: JournalService().log_ui_error( event_type="market_open_error", message="Не удалось загрузить экран рынка.", screen="market", action="open", user_id=user_id, chat_id=chat_id, error_type=classify_exchange_error(exc), raw_error=str(exc), ) await show_message_exchange_error( message, 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: 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, action="retry", ) await callback.answer() except ExchangeError as exc: JournalService().log_ui_error( event_type="market_retry_error", message="Не удалось обновить экран рынка.", screen="market", action="retry", 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", )