Stage 03.5 - private account balance and portfolio UI
This commit is contained in:
@@ -1,12 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.telegram.menus import PORTFOLIO_TEXT
|
||||
from src.integrations.exchange.exceptions import ExchangeError
|
||||
from src.integrations.exchange.models import BalanceSummary
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
|
||||
|
||||
router = Router(name="portfolio")
|
||||
|
||||
|
||||
FIAT_CURRENCIES = {"USD", "USDT", "EUR", "RUB", "BYN"}
|
||||
|
||||
CURRENCY_ICONS = {
|
||||
"USD": "💵",
|
||||
"USDT": "💵",
|
||||
"EUR": "💶",
|
||||
"RUB": "₽",
|
||||
"BYN": "Br",
|
||||
"BTC": "₿",
|
||||
"ETH": "Ξ",
|
||||
"BNB": "🟡",
|
||||
"SOL": "◎",
|
||||
"ADA": "🔵",
|
||||
"XRP": "✕",
|
||||
"DOGE": "🐶",
|
||||
}
|
||||
|
||||
PINNED_ORDER = {
|
||||
"USD": 1,
|
||||
"USDT": 2,
|
||||
"BTC": 3,
|
||||
"ETH": 4,
|
||||
}
|
||||
|
||||
|
||||
def format_amount(currency: str, value: float) -> str:
|
||||
if currency.upper() in FIAT_CURRENCIES:
|
||||
return f"{value:,.2f}".replace(",", " ")
|
||||
return f"{value:,.8f}".replace(",", " ")
|
||||
|
||||
|
||||
def get_currency_label(currency: str) -> str:
|
||||
icon = CURRENCY_ICONS.get(currency.upper(), "💰")
|
||||
return f"{icon} {currency.upper()}"
|
||||
|
||||
|
||||
def is_zero_balance(item: BalanceSummary) -> bool:
|
||||
return abs(item.available) < 1e-12 and abs(item.locked) < 1e-12
|
||||
|
||||
|
||||
def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
|
||||
def sort_key(item: BalanceSummary) -> tuple[int, str]:
|
||||
currency = item.currency.upper()
|
||||
priority = PINNED_ORDER.get(currency, 999)
|
||||
return (priority, currency)
|
||||
|
||||
return sorted(items, key=sort_key)
|
||||
|
||||
|
||||
def split_balances(items: list[BalanceSummary]) -> tuple[list[BalanceSummary], list[BalanceSummary]]:
|
||||
major: list[BalanceSummary] = []
|
||||
other: list[BalanceSummary] = []
|
||||
|
||||
for item in items:
|
||||
if item.currency.upper() in PINNED_ORDER:
|
||||
major.append(item)
|
||||
else:
|
||||
other.append(item)
|
||||
|
||||
return major, other
|
||||
|
||||
|
||||
def render_balance_block(item: BalanceSummary) -> list[str]:
|
||||
total = item.available + item.locked
|
||||
|
||||
return [
|
||||
f"<b>{get_currency_label(item.currency)}</b>",
|
||||
f"• доступно: {format_amount(item.currency, item.available)}",
|
||||
f"• заблокировано: {format_amount(item.currency, item.locked)}",
|
||||
f"• всего: {format_amount(item.currency, total)}",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
@router.message(F.text == "💼 Портфель")
|
||||
async def open_portfolio(message: Message) -> None:
|
||||
await message.answer(PORTFOLIO_TEXT)
|
||||
service = ExchangeService()
|
||||
|
||||
try:
|
||||
balances = service.get_balance_summary()
|
||||
except ExchangeError as exc:
|
||||
await message.answer(
|
||||
"<b>💼 Портфель</b>\n\n"
|
||||
"Не удалось получить баланс с private API.\n"
|
||||
f"Ошибка: {exc}"
|
||||
)
|
||||
return
|
||||
|
||||
if not balances:
|
||||
await message.answer(
|
||||
"<b>💼 Портфель</b>\n\n"
|
||||
"Баланс пуст."
|
||||
)
|
||||
return
|
||||
|
||||
visible_balances = [item for item in balances if not is_zero_balance(item)]
|
||||
visible_balances = sort_balances(visible_balances)
|
||||
|
||||
if not visible_balances:
|
||||
await message.answer(
|
||||
"<b>💼 Портфель</b>\n\n"
|
||||
"Все балансы нулевые."
|
||||
)
|
||||
return
|
||||
|
||||
major_balances, other_balances = split_balances(visible_balances)
|
||||
|
||||
lines: list[str] = ["<b>💼 Портфель</b>", "", "<b>Баланс аккаунта</b>", ""]
|
||||
|
||||
if major_balances:
|
||||
lines.append("<b>Основные активы</b>")
|
||||
lines.append("")
|
||||
for item in major_balances:
|
||||
lines.extend(render_balance_block(item))
|
||||
|
||||
if other_balances:
|
||||
lines.append("<b>Прочие активы</b>")
|
||||
lines.append("")
|
||||
for item in other_balances:
|
||||
lines.extend(render_balance_block(item))
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"<b>Итого</b>",
|
||||
f"• активов с ненулевым балансом: {len(visible_balances)}",
|
||||
]
|
||||
)
|
||||
|
||||
text = "\n".join(lines).rstrip()
|
||||
await message.answer(text)
|
||||
|
||||
Reference in New Issue
Block a user