300 lines
9.7 KiB
Python
300 lines
9.7 KiB
Python
# 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 (
|
||
"<b>📈 Рынок</b>\n"
|
||
f"{mode_line()}"
|
||
"\n"
|
||
f"<b>{base_asset} / {quote_asset}</b> ({market_type_ru})\n\n"
|
||
f"<b>$ {format_usd_amount(ticker_price)}</b> {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 (
|
||
"<b>📈 Рынок</b>\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 = (
|
||
"<b>📈 Рынок</b>\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="<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,
|
||
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="<b>📈 Рынок</b>",
|
||
exc=exc,
|
||
network_details="Рыночные данные недоступны.\nОбнови экран.",
|
||
auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.",
|
||
retry_callback_data="market:retry",
|
||
) |