build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -1 +1,3 @@
"""Package marker."""
# app/src/telegram/handlers/__init__.py
"""Package marker."""

View File

@@ -10,6 +10,7 @@ from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
from src.integrations.exchange.runtime_ui import build_runtime_exchange_alert_lines
from src.telegram.ui.common import mode_line
from src.trading.auto.service import AutoTradeService
@@ -40,10 +41,10 @@ def build_auto_notification_text() -> str:
def _build_signal_notification_text(state, signal: str) -> str:
snapshot = _market_snapshot(getattr(state, "symbol", None))
quote = _market_quote(getattr(state, "symbol", None))
bid_price = _price_from_snapshot(snapshot, "bid_price")
ask_price = _price_from_snapshot(snapshot, "ask_price")
bid_price = _price_from_quote(quote, "bid_price")
ask_price = _price_from_quote(quote, "ask_price")
side = "Long" if signal == "BUY" else "Short"
side_icon = _signal_icon(signal)
@@ -85,28 +86,28 @@ def _build_signal_notification_text(state, signal: str) -> str:
return "\n".join(lines)
def _price_from_snapshot(
snapshot: dict[str, object] | None,
def _price_from_quote(
quote: Quote | None,
key: str,
) -> float | None:
if snapshot is None:
if quote is None:
return None
return safe_float(snapshot.get(key))
return safe_float(getattr(quote, key, None))
def _position_current_price(state) -> float | None:
snapshot = _market_snapshot(getattr(state, "symbol", None))
quote = _market_quote(getattr(state, "symbol", None))
if snapshot is not None:
if quote is not None:
side = str(getattr(state, "position_side", "") or "").upper()
if side == "LONG":
price = snapshot.get("bid_price") or snapshot.get("last_price")
price = quote.bid_price or quote.last_price
elif side == "SHORT":
price = snapshot.get("ask_price") or snapshot.get("last_price")
price = quote.ask_price or quote.last_price
else:
price = snapshot.get("last_price")
price = quote.last_price
parsed = safe_float(price)
if parsed is not None:
@@ -720,12 +721,15 @@ def _max_reserved_line(state, price: float | None = None) -> str:
return f"Маржа · {_format_usd_compact(own_funds_usd)}"
def _market_snapshot(symbol: str | None) -> dict[str, object] | None:
def _market_quote(symbol: str | None) -> Quote | None:
if not symbol:
return None
try:
return ExchangeService().get_market_snapshot(symbol, runtime_key="auto")
return ExchangeService().get_quote(
symbol,
runtime_key="auto",
)
except Exception:
return None
@@ -907,10 +911,10 @@ def _commission_lines_for_position(
def _current_price(symbol: str | None) -> float | None:
snapshot = _market_snapshot(symbol)
quote = _market_quote(symbol)
if snapshot is not None:
price = snapshot.get("last_price")
if quote is not None:
price = quote.last_price
if price is not None:
try:
parsed = safe_float(price)
@@ -922,25 +926,25 @@ def _current_price(symbol: str | None) -> float | None:
return None
try:
return float(ExchangeService().get_price(symbol).price)
return float(ExchangeService().get_quote(symbol).last_price)
except Exception:
return None
def _signal_entry_price(state) -> float | None:
snapshot = _market_snapshot(state.symbol)
quote = _market_quote(state.symbol)
if snapshot is None:
if quote is None:
return _current_price(state.symbol)
signal = (state.last_signal or "HOLD").upper()
if signal == "BUY":
price = snapshot.get("ask_price")
price = quote.ask_price
elif signal == "SELL":
price = snapshot.get("bid_price")
price = quote.bid_price
else:
price = snapshot.get("last_price")
price = quote.last_price
if price is None:
return None

View File

@@ -3,10 +3,15 @@
from __future__ import annotations
import time
from datetime import datetime, timezone
from decimal import Decimal
from zoneinfo import ZoneInfo
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.core.config import load_settings
from src.core.types import NumericLike
from src.integrations.exchange.service import ExchangeService
from src.trading.debug.service import DebugTradeService
@@ -113,6 +118,23 @@ def _format_updated_at(value: object) -> str:
if not value:
return ""
if isinstance(value, datetime):
current = value
if current.tzinfo is None:
current = current.replace(tzinfo=timezone.utc)
try:
settings = load_settings()
current = current.astimezone(
ZoneInfo(settings.tz),
)
except Exception:
current = current.astimezone()
return current.strftime("%H:%M:%S")
text = str(value)
if " " in text:
@@ -121,6 +143,23 @@ def _format_updated_at(value: object) -> str:
return text
def _quote_age_seconds(quote: object) -> float | None:
received_at = getattr(quote, "received_at", None)
if not isinstance(received_at, datetime):
return None
if received_at.tzinfo is None:
received_at = received_at.replace(tzinfo=timezone.utc)
return max(
0.0,
(
datetime.now(timezone.utc)
- received_at.astimezone(timezone.utc)
).total_seconds(),
)
def _market_snapshot_lines(symbol: str | None) -> list[str]:
if not symbol:
return [
@@ -141,7 +180,7 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]:
error = None
try:
market = ExchangeService().get_market_snapshot(
market = ExchangeService().get_quote(
symbol,
runtime_key="debug_auto",
)
@@ -167,11 +206,11 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]:
f"Error · {error or 'unknown'}",
]
last_price = market.get("last_price") if market else getattr(execution, "last_price", None)
bid_price = market.get("bid_price") if market else getattr(execution, "bid_price", None)
ask_price = market.get("ask_price") if market else getattr(execution, "ask_price", None)
market_source = market.get("source") if market else ""
market_age = market.get("age_seconds") if market else None
last_price = market.last_price if market else getattr(execution, "last_price", None)
bid_price = market.bid_price if market else getattr(execution, "bid_price", None)
ask_price = market.ask_price if market else getattr(execution, "ask_price", None)
market_source = market.source if market else ""
market_age = _quote_age_seconds(market) if market else None
execution_source = getattr(execution, "source", "") if execution else ""
execution_age = getattr(execution, "age_seconds", None) if execution else None
@@ -184,7 +223,7 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]:
f"Ask · {_format_usd_or_dash(ask_price)}",
f"Source · {market_source or ''}",
f"Quote age · {_format_age(market_age)}",
f"Exchange time · {_format_updated_at(market.get('updated_at') if market else None)}",
f"Exchange time · {_format_updated_at(market.exchange_timestamp if market else None)}",
"",
"<b>Execution</b>",
f"Source · {execution_source or ''}",
@@ -274,7 +313,9 @@ def _format_crypto_size(value: float | int | None) -> str:
return f"{float(value):.5f}".rstrip("0").rstrip(".")
def _format_money_compact(value: float | int | None) -> str:
def _format_money_compact(
value: float | int | Decimal | None,
) -> str:
if value is None:
return ""
@@ -286,21 +327,25 @@ def _format_money_compact(value: float | int | None) -> str:
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
def _format_usd_or_dash(value: float | int | None) -> str:
def _format_usd_or_dash(
value: float | int | Decimal | None,
) -> str:
if value is None:
return ""
return f"$ {_format_money_compact(value)}"
def _format_usd_or_off(value: float | int | None) -> str:
def _format_usd_or_off(
value: float | int | Decimal | None,
) -> str:
if value is None:
return "off"
return "Выкл."
return f"$ {_format_money_compact(value)}"
def _format_signed_usd(value: float | int | None) -> str:
def _format_signed_usd(value: float | int | Decimal | None) -> str:
if value is None:
return ""
@@ -315,7 +360,7 @@ def _format_signed_usd(value: float | int | None) -> str:
return "$ 0"
def _format_age(value: object) -> str:
def _format_age(value: NumericLike | None) -> str:
if value is None:
return ""

View File

@@ -1,505 +0,0 @@
# 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,
InaccessibleMessage,
InlineKeyboardMarkup,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.core.numbers import safe_float
from src.core.types import NumericLike
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.service import ExchangeService
from src.integrations.exchange.status import (
ExchangeRuntimeStatus,
ExchangeStatusCode,
build_exchange_error_status,
classify_exchange_error,
)
from src.telegram.live.active_screen import ActiveScreenManager
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
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 (
show_callback_exchange_error,
show_message_exchange_error,
)
from src.trading.journal.service import JournalService
router = Router(name="market")
_last_market_prices: dict[str, float] = {}
_last_market_directions: dict[str, str] = {}
def _require_message(callback: CallbackQuery) -> Message | None:
message = callback.message
if message is None or isinstance(message, InaccessibleMessage):
return None
return message
def _market_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
builder.adjust(1)
return builder.as_markup()
# собрать текст, когда рынок/биржа недоступны через unified status layer
def _build_market_status_text(status: ExchangeRuntimeStatus) -> str:
icon = "⏸️" if status.code == ExchangeStatusCode.BREAK else "⛔️"
return (
"<b>📈 Рынок</b>\n"
f"{mode_line()}"
f"{icon} {status.title}\n\n"
f"{status.message}\n\n"
f"{now_line()}"
)
def _build_market_text(
*,
ticker_price: NumericLike,
name: str,
market_type: str,
base_asset: str,
quote_asset: str,
) -> str:
price = safe_float(ticker_price)
if price is None:
price = 0.0
previous_price = _last_market_prices.get(name)
price_direction = _last_market_directions.get(name, "")
if previous_price is not None:
if price > previous_price:
price_direction = "🔺"
elif price < previous_price:
price_direction = "🔻"
_last_market_prices[name] = 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(price)}</b> {price_direction}\n\n"
f"{now_line()}"
)
# live-render должен сам уметь показать ошибку, иначе runner просто потеряет экран
def _build_market_live_text() -> str:
service = ExchangeService()
requested_symbol = service.settings.default_symbol
try:
runtime_status = service.get_symbol_runtime_status(requested_symbol)
except Exception as exc:
return _build_market_status_text(build_exchange_error_status(exc))
if runtime_status.code != ExchangeStatusCode.OPEN:
return _build_market_status_text(runtime_status)
symbol = runtime_status.symbol or requested_symbol
validation = service.validate_symbol(symbol)
if not validation.is_valid:
return _build_market_status_text(
service.get_symbol_runtime_status(requested_symbol)
)
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,
)
def _register_market_live_screen(message: Message) -> None:
bot = message.bot
if bot is None:
return
LiveScreenRunner.unregister_message(
chat_id=message.chat.id,
message_id=message.message_id,
)
ScreenRegistry.unregister_message(
chat_id=message.chat.id,
message_id=message.message_id,
)
LiveScreenRunner.register_screen(
LiveScreen(
screen="market",
bot=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 _prepare_market_from_message(message: Message) -> bool:
bot = message.bot
if bot is None:
return False
await ActiveScreenManager.prepare_new_screen(
screen="market",
bot=bot,
chat_id=message.chat.id,
)
return True
async def _prepare_market_from_callback(callback: CallbackQuery) -> bool:
message = _require_message(callback)
if message is None:
await callback.answer("Сообщение недоступно", show_alert=True)
return False
bot = message.bot
if bot is None:
await callback.answer("Bot недоступен", show_alert=True)
return False
await ActiveScreenManager.prepare_new_screen(
screen="market",
bot=bot,
chat_id=message.chat.id,
keep_message_id=message.message_id,
)
return True
async def _send_or_edit_market_screen(
target_message: Message,
*,
text: str,
edit_mode: bool,
) -> None:
if edit_mode:
await target_message.edit_text(text, reply_markup=_market_keyboard())
_register_market_live_screen(target_message)
ActiveScreenManager.register(screen="market", message=target_message)
return
sent_message = await target_message.answer(
text,
reply_markup=_market_keyboard(),
)
_register_market_live_screen(sent_message)
ActiveScreenManager.register(screen="market", message=sent_message)
async def _render_market_screen(
target_message: Message,
*,
user_id: int | None,
chat_id: int | None,
edit_mode: bool,
action: str,
) -> None:
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},
)
runtime_status = service.get_symbol_runtime_status(requested_symbol)
if runtime_status.code != ExchangeStatusCode.OPEN:
journal.log_ui_warning(
event_type="market_status_blocked",
message=runtime_status.message,
screen="market",
action=action,
user_id=user_id,
chat_id=chat_id,
payload=runtime_status.as_dict(),
)
await _send_or_edit_market_screen(
target_message,
text=_build_market_status_text(runtime_status),
edit_mode=edit_mode,
)
return
symbol = runtime_status.symbol or requested_symbol
validation = service.validate_symbol(symbol)
if not validation.is_valid:
invalid_status = service.get_symbol_runtime_status(requested_symbol)
journal.log_ui_warning(
event_type="market_symbol_invalid",
message=invalid_status.message,
screen="market",
action=action,
user_id=user_id,
chat_id=chat_id,
payload=invalid_status.as_dict(),
)
await _send_or_edit_market_screen(
target_message,
text=_build_market_status_text(invalid_status),
edit_mode=edit_mode,
)
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": safe_float(ticker.price),
"runtime_status": runtime_status.as_dict(),
},
)
await _send_or_edit_market_screen(
target_message,
text=text,
edit_mode=edit_mode,
)
@router.message(F.text == "📈 Рынок")
async def open_market(message: Message, state: FSMContext) -> None:
await state.clear()
if not await _prepare_market_from_message(message):
return
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 == "monitoring:market")
async def open_market_from_monitoring(
callback: CallbackQuery,
state: FSMContext,
) -> None:
await state.clear()
if not await _prepare_market_from_callback(callback):
return
message = _require_message(callback)
if message is None:
await callback.answer("Сообщение недоступно", show_alert=True)
return
user_id = callback.from_user.id if callback.from_user else None
chat_id = message.chat.id
try:
await _render_market_screen(
message,
user_id=user_id,
chat_id=chat_id,
edit_mode=True,
action="open_from_monitoring",
)
await callback.answer()
except ExchangeError as exc:
JournalService().log_ui_error(
event_type="market_open_error",
message="Не удалось загрузить экран рынка из мониторинга.",
screen="market",
action="open_from_monitoring",
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",
)
@router.callback_query(F.data == "market:retry")
async def retry_market(
callback: CallbackQuery,
state: FSMContext,
) -> None:
await state.clear()
if not await _prepare_market_from_callback(callback):
return
message = _require_message(callback)
if message is None:
await callback.answer("Сообщение недоступно", show_alert=True)
return
user_id = callback.from_user.id if callback.from_user else None
chat_id = message.chat.id
try:
await _render_market_screen(
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",
)

View File

@@ -3,8 +3,9 @@
from __future__ import annotations
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.models import BalanceSummary, ExchangeSymbol
from src.integrations.exchange.models import BalanceSummary
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.instrument import Instrument
FIAT_CURRENCIES = {"USD", "USDT", "EUR", "RUB", "BYN"}
@@ -31,7 +32,10 @@ def is_fiat_currency(currency: str) -> bool:
def get_currency_icon(currency: str) -> str:
return CURRENCY_ICONS.get(currency.upper(), currency.upper())
return CURRENCY_ICONS.get(
currency.upper(),
currency.upper(),
)
def get_currency_label(currency: str) -> str:
@@ -45,6 +49,7 @@ def render_currency_title(currency: str) -> str:
def format_amount(currency: str, value: float) -> str:
if is_fiat_currency(currency):
return f"{value:,.2f}".replace(",", " ")
return f"{value:,.8f}".replace(",", " ")
@@ -52,7 +57,9 @@ def format_usd_amount(value: float) -> str:
return f"{value:,.2f}".replace(",", " ")
def format_usd_price(value: float | int | str | None) -> str:
def format_usd_price(
value: float | int | str | None,
) -> str:
if value is None:
return ""
@@ -62,7 +69,9 @@ def format_usd_price(value: float | int | str | None) -> str:
return ""
def format_usd_pnl(value: float | int | str | None) -> str:
def format_usd_pnl(
value: float | int | str | None,
) -> str:
if value is None:
return ""
@@ -87,7 +96,10 @@ def render_currency_line(
show_code: bool = True,
) -> str:
icon = get_currency_icon(currency)
amount = format_amount(currency, value)
amount = format_amount(
currency,
value,
)
if show_code:
return f"{icon} {currency.upper()} · {amount}"
@@ -100,61 +112,79 @@ def balance_total(item: BalanceSummary) -> float:
def is_zero_balance(item: BalanceSummary) -> bool:
return abs(item.available) < 1e-12 and abs(item.locked) < 1e-12
return (
abs(item.available) < 1e-12
and abs(item.locked) < 1e-12
)
def _quote_priority(quote_asset: str) -> int:
value = (quote_asset or "").upper()
if value == "USD":
return 3
if value == "USDT":
return 2
return 0
def _status_priority(status: str) -> int:
value = (status or "").upper()
if value == "TRADING":
return 2
if value in {"HALT", "BREAK"}:
return 0
return 1
def _market_type_priority(market_type: str) -> int:
value = (market_type or "").upper()
if value == "SPOT":
return 3
if value == "LEVERAGE":
return 2
return 1
def _symbol_priority(symbol_info: ExchangeSymbol) -> tuple[int, int, int, str]:
def _instrument_priority(
instrument: Instrument,
) -> tuple[int, int, int, str]:
return (
_quote_priority(symbol_info.quote_asset),
_status_priority(symbol_info.status),
_market_type_priority(symbol_info.market_type),
symbol_info.symbol.upper(),
_quote_priority(instrument.quote_asset),
_status_priority(instrument.status),
_market_type_priority(instrument.market_type),
instrument.symbol.upper(),
)
def _resolve_asset_quote_symbol(
def _resolve_asset_quote_instrument(
exchange_service: ExchangeService,
asset: str,
) -> ExchangeSymbol | None:
) -> Instrument | None:
asset_upper = asset.upper()
try:
symbols = exchange_service.get_exchange_symbols()
instruments = exchange_service.get_instruments()
except ExchangeError:
return None
candidates: list[ExchangeSymbol] = []
candidates: list[Instrument] = []
for symbol_info in symbols:
base_asset = (symbol_info.base_asset or "").upper()
quote_asset = (symbol_info.quote_asset or "").upper()
for instrument in instruments:
base_asset = (
instrument.base_asset or ""
).upper()
quote_asset = (
instrument.quote_asset or ""
).upper()
if base_asset != asset_upper:
continue
@@ -162,12 +192,16 @@ def _resolve_asset_quote_symbol(
if quote_asset not in {"USD", "USDT"}:
continue
candidates.append(symbol_info)
candidates.append(instrument)
if not candidates:
return None
candidates.sort(key=_symbol_priority, reverse=True)
candidates.sort(
key=_instrument_priority,
reverse=True,
)
return candidates[0]
@@ -184,18 +218,26 @@ def get_asset_usd_rate(
if asset in price_cache:
return price_cache[asset]
symbol_info = _resolve_asset_quote_symbol(exchange_service, asset)
if symbol_info is None:
instrument = _resolve_asset_quote_instrument(
exchange_service,
asset,
)
if instrument is None:
price_cache[asset] = None
return None
try:
ticker = exchange_service.get_price(symbol_info.symbol)
rate = float(ticker.price)
quote = exchange_service.get_quote(
instrument.symbol
)
rate = float(quote.last_price)
# Пока считаем USDT ~= USD
# Пока считаем USDT ~= USD.
price_cache[asset] = rate
return rate
except ExchangeError:
price_cache[asset] = None
return None
@@ -207,10 +249,16 @@ def estimate_balance_usd(
price_cache: dict[str, float | None],
) -> float | None:
total = balance_total(item)
if total <= 0:
return None
rate = get_asset_usd_rate(exchange_service, item.currency, price_cache)
rate = get_asset_usd_rate(
exchange_service,
item.currency,
price_cache,
)
if rate is None:
return None