diff --git a/app/src/telegram/handlers/market.py b/app/src/telegram/handlers/market.py
index 5516c11..a7e7d20 100644
--- a/app/src/telegram/handlers/market.py
+++ b/app/src/telegram/handlers/market.py
@@ -11,6 +11,7 @@ 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,
@@ -21,6 +22,8 @@ from src.trading.journal.service import JournalService
router = Router(name="market")
+_last_market_prices: dict[str, float] = {}
+_last_market_directions: dict[str, str] = {}
# клавиатура экрана рынка
@@ -36,20 +39,21 @@ 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:
- from src.telegram.ui.common import now_line
+ previous_price = _last_market_prices.get(name)
+ price_direction = _last_market_directions.get(name, "▲")
- status_map = {
- "TRADING": "доступен для торговли",
- "HALT": "торги остановлены",
- "BREAK": "перерыв",
- }
- status_ru = status_map.get(symbol_status.upper(), symbol_status.lower())
+ 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",
@@ -60,13 +64,9 @@ def _build_market_text(
return (
"📈 Рынок\n"
f"{mode_line()}"
- f"Пара: {name}\n"
- f"Цена: {ticker_price:.2f} {quote_asset}\n"
- f"Статус: {status_ru}\n"
- f"Тип инструмента: {market_type_ru}\n"
- f"Базовый актив: {base_asset}\n"
- f"Валюта котировки: {quote_asset}\n"
- f"Шаг цены: {tick_size} {quote_asset}\n\n"
+ "\n"
+ f"{base_asset} / {quote_asset} ({market_type_ru})\n\n"
+ f"$ {format_usd_amount(ticker_price)} {price_direction}\n\n"
f"{now_line()}"
)
@@ -89,13 +89,7 @@ def _build_market_live_text() -> str:
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"
- tick_size = (
- f"{symbol_info.tick_size}"
- if symbol_info and symbol_info.tick_size is not None
- 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
@@ -103,11 +97,9 @@ def _build_market_live_text() -> str:
return _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,
)
@@ -137,7 +129,6 @@ async def _render_market_screen(
action: str,
) -> None:
AutoTradeRunner.set_current_screen("market")
- LiveScreenRunner.set_current_screen("market")
service = ExchangeService()
journal = JournalService()
@@ -188,13 +179,7 @@ async def _render_market_screen(
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"
- tick_size = (
- f"{symbol_info.tick_size}"
- if symbol_info and symbol_info.tick_size is not None
- 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
@@ -202,11 +187,9 @@ async def _render_market_screen(
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,
)
journal.log_ui_info(
diff --git a/app/src/telegram/handlers/portfolio.py b/app/src/telegram/handlers/portfolio.py
index 6d405a1..d929aae 100644
--- a/app/src/telegram/handlers/portfolio.py
+++ b/app/src/telegram/handlers/portfolio.py
@@ -12,6 +12,7 @@ from src.integrations.exchange.models import BalanceSummary
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.currency_ui import (
balance_total,
estimate_balance_usd,
@@ -39,6 +40,30 @@ PINNED_ORDER = {
"ETH": 4,
}
+# компактное форматирование количества
+def _compact_amount(currency: str, value: float) -> str:
+ currency = currency.upper()
+
+ if currency in {"USD", "USDT", "EUR"}:
+ return format_usd_amount(value)
+
+ text = f"{value:.8f}"
+
+ if "." in text:
+ integer, frac = text.split(".")
+ integer = f"{int(integer):,}".replace(",", " ")
+
+ stripped = frac.rstrip("0")
+
+ if stripped == "":
+ return f"{integer}.00"
+
+ if len(stripped) <= 2:
+ return f"{integer}.{stripped.ljust(2, '0')}"
+
+ return f"{integer}.{stripped}"
+
+ return f"{int(text):,}".replace(",", " ")
# клавиатура портфеля
def _portfolio_keyboard() -> InlineKeyboardMarkup:
@@ -104,7 +129,6 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
"💼 Портфель",
mode_line().rstrip(),
"",
- f"БАЛАНС · АКТИВЫ · {len(visible_balances)}",
]
asset_blocks: list[list[str]] = []
@@ -120,18 +144,18 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
elif total > 0:
missing_estimate_assets.append(currency)
- block = [
- "",
- f"{currency}",
- f"• доступно: {format_amount(currency, item.available)}",
- f"• заблокировано: {format_amount(currency, item.locked)}",
- f"• всего: {format_amount(currency, total)}",
- ]
+ line = f"{currency}: {_compact_amount(currency, total)}"
- if estimated_usd is not None and currency not in {"USD", "USDT"}:
- block.append(f"≈ {format_usd_amount(estimated_usd)} USD")
+ if item.locked > 0:
+ line += f" · locked {_compact_amount(currency, item.locked)}"
- asset_blocks.append(block)
+ if estimated_usd is not None and currency not in {'USD', 'USDT'}:
+ line += f" ≈ $ {format_usd_amount(estimated_usd)}"
+
+ if currency == "BTC" and any("USD:" in x or "USDT:" in x for x in lines):
+ lines.append("")
+
+ lines.append(line)
has_partial_data = len(missing_estimate_assets) > 0
@@ -139,7 +163,8 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
lines.append("🟡 Данные загружены частично")
if has_any_estimate:
- lines.append(f"Оценка: ~${format_usd_amount(total_estimated_usd)}")
+ lines.insert(3, "")
+ lines.insert(3, f"Оценка: ≈ $ {format_usd_amount(total_estimated_usd)}")
if missing_estimate_assets:
lines.append(f"Нет оценки: {', '.join(missing_estimate_assets)}")
@@ -196,7 +221,6 @@ async def _render_portfolio_screen(
action: str,
) -> None:
AutoTradeRunner.set_current_screen("portfolio")
- LiveScreenRunner.set_current_screen("portfolio")
journal = JournalService()
diff --git a/app/src/telegram/live/runner.py b/app/src/telegram/live/runner.py
index ba14e84..32dac8d 100644
--- a/app/src/telegram/live/runner.py
+++ b/app/src/telegram/live/runner.py
@@ -34,21 +34,26 @@ class LiveScreen:
class LiveScreenRunner:
- _screens: dict[str, LiveScreen] = {}
+ _screens: dict[str, list[LiveScreen]] = {}
_tasks: dict[str, asyncio.Task] = {}
- _current_screen: str | None = None
-
- # переключить активный экран
- @classmethod
- def set_current_screen(cls, screen: str) -> None:
- cls._current_screen = screen
# зарегистрировать live-экран
@classmethod
def register_screen(cls, live_screen: LiveScreen) -> None:
- cls._screens[live_screen.screen] = live_screen
+ screens = cls._screens.setdefault(live_screen.screen, [])
- # удалить старый live-экран из Telegram
+ screens[:] = [
+ item
+ for item in screens
+ if not (
+ item.chat_id == live_screen.chat_id
+ and item.message_id == live_screen.message_id
+ )
+ ]
+
+ screens.append(live_screen)
+
+ # удалить все live-экраны указанного типа из Telegram
@classmethod
async def delete_screen(
cls,
@@ -57,25 +62,29 @@ class LiveScreenRunner:
bot: Bot,
chat_id: int,
) -> None:
- live_screen = cls._screens.get(screen)
+ screens = cls._screens.get(screen, [])
- if live_screen is None:
- return
+ remaining: list[LiveScreen] = []
- if live_screen.chat_id != chat_id:
- return
+ for live_screen in screens:
+ if live_screen.chat_id != chat_id:
+ remaining.append(live_screen)
+ continue
- try:
- await bot.delete_message(
- chat_id=live_screen.chat_id,
- message_id=live_screen.message_id,
- )
- except Exception:
- pass
+ try:
+ await bot.delete_message(
+ chat_id=live_screen.chat_id,
+ message_id=live_screen.message_id,
+ )
+ except Exception:
+ pass
- cls._screens.pop(screen, None)
+ if remaining:
+ cls._screens[screen] = remaining
+ else:
+ cls._screens.pop(screen, None)
- # запустить автообновление экрана
+ # запустить автообновление группы экранов
@classmethod
def start(cls, screen: str) -> None:
task = cls._tasks.get(screen)
@@ -85,7 +94,7 @@ class LiveScreenRunner:
cls._tasks[screen] = asyncio.create_task(cls._worker(screen))
- # остановить автообновление экрана
+ # остановить автообновление группы экранов
@classmethod
def stop(cls, screen: str) -> None:
task = cls._tasks.get(screen)
@@ -96,38 +105,44 @@ class LiveScreenRunner:
task.cancel()
cls._tasks.pop(screen, None)
- # фоновый цикл обновления одного экрана
+ # фоновый цикл обновления группы экранов
@classmethod
async def _worker(cls, screen: str) -> None:
while True:
await cls._refresh_screen(screen)
await asyncio.sleep(cls._screen_interval(screen))
- # получить интервал обновления экрана
+ # получить интервал обновления группы экранов
@classmethod
def _screen_interval(cls, screen: str) -> int:
- live_screen = cls._screens.get(screen)
- if live_screen is None:
+ screens = cls._screens.get(screen, [])
+ if not screens:
return 5
- return live_screen.interval_seconds
+ return screens[0].interval_seconds
- # обновить Telegram-сообщение live-экрана
+ # обновить все Telegram-сообщения live-экрана
@classmethod
async def _refresh_screen(cls, screen: str) -> None:
- if cls._current_screen != screen:
+ screens = cls._screens.get(screen, [])
+ if not screens:
return
- live_screen = cls._screens.get(screen)
- if live_screen is None:
- return
+ alive_screens: list[LiveScreen] = []
- try:
- await live_screen.bot.edit_message_text(
- chat_id=live_screen.chat_id,
- message_id=live_screen.message_id,
- text=live_screen.render_text(),
- reply_markup=live_screen.render_markup(),
- )
- except Exception:
- pass
\ No newline at end of file
+ for live_screen in screens:
+ try:
+ await live_screen.bot.edit_message_text(
+ chat_id=live_screen.chat_id,
+ message_id=live_screen.message_id,
+ text=live_screen.render_text(),
+ reply_markup=live_screen.render_markup(),
+ )
+ alive_screens.append(live_screen)
+ except Exception:
+ pass
+
+ if alive_screens:
+ cls._screens[screen] = alive_screens
+ else:
+ cls._screens.pop(screen, None)
\ No newline at end of file