Stage 07.4.3.11 — Risk Settings UI & UX

This commit is contained in:
2026-05-05 19:14:51 +03:00
parent 163e8efe82
commit 3c3f0e846a
13 changed files with 967 additions and 12 deletions

View File

@@ -0,0 +1,364 @@
# app/src/telegram/handlers/auto.py
from __future__ import annotations
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.telegram.ui.common import mode_line
from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService
from src.telegram.handlers.system import open_auto_settings
from src.integrations.exchange.service import ExchangeService
from src.telegram.ui.currency_ui import format_usd_amount
router = Router(name="auto")
# красивое отображение стратегии
def _strategy_label(strategy: str | None) -> str:
mapping = {
"TREND": "📈 Trend Following",
"GRID": "🧩 Grid Trading",
"SCALP": "⚡ Scalping",
}
return mapping.get(strategy or "", "")
# красивое отображение статуса
def _status_label(status: str) -> str:
mapping = {
"OFF": "⚪ Выключена",
"OBSERVING": "👀 Наблюдение",
"RUNNING": "🟢 Активна",
}
return mapping.get(status, status)
# красивое отображение сигнала
def _signal_label(signal: str | None) -> str:
mapping = {
"BUY": "🟢 BUY",
"SELL": "🔴 SELL",
"HOLD": "🟡 HOLD",
}
return mapping.get(signal or "", "")
# красивое отображение решения
def _decision_label(status: str) -> str:
mapping = {
"WAITING": "🟡 Ожидание",
"CONFIRMING": "🟠 Подтверждение",
"READY": "🟢 Готово к входу",
"BLOCKED": "🔴 Заблокировано",
}
return mapping.get(status, status)
# компактное значение или заглушка
def _value_or_dash(value: object) -> str:
if value is None:
return ""
return str(value)
# формат цены
def _price_or_dash(value: float | None) -> str:
if value is None:
return ""
return f"{value:.2f}"
# текущая цена инструмента
def _market_price_or_dash(symbol: str | None) -> str:
if not symbol:
return ""
try:
ticker = ExchangeService().get_price(symbol)
return f"$ {format_usd_amount(ticker.price)}"
except Exception:
return ""
# формат USD
def _usd_or_dash(value: float | None) -> str:
if value is None:
return ""
return f"{value:.2f} USD"
# формат размера позиции
def _size_or_dash(value: float | None) -> str:
if value is None:
return ""
return f"{value:.8f}".rstrip("0").rstrip(".")
# формат плеча
def _leverage_or_dash(value: float | None) -> str:
if value is None:
return ""
return f"{value:.1f}x"
# формат торгового инструмента для UI
def _format_symbol(symbol: str | None) -> str:
if not symbol:
return ""
base_symbol = symbol.split("_", 1)[0]
parts = base_symbol.split("/", 1)
if len(parts) == 2:
return f"{parts[0]} / {parts[1]}"
return base_symbol
# стратегия для компактного UI
def _compact_strategy(strategy: str | None) -> str:
if not strategy:
return ""
return strategy.upper()
# плечо для компактного UI
def _compact_leverage(value: float | None) -> str:
if value is None:
return ""
return f"x{value:g}"
# проверка, настроена ли автоторговля минимально
def _is_auto_configured(state) -> bool:
return bool(
state.symbol
and state.strategy
and state.risk_percent is not None
)
# строка инструмента / стратегии / плеча
def _context_line(state) -> str:
symbol = _format_symbol(state.symbol)
strategy = _compact_strategy(state.strategy)
leverage = _compact_leverage(state.leverage)
if leverage == "":
return f"{symbol} · {strategy}"
return f"{symbol} · {strategy} · {leverage}"
# клавиатура автоторговли
def _auto_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="▶️ Start", callback_data="auto:start")
builder.button(text="👀 Watch", callback_data="auto:observe")
builder.button(text="🛑 Stop", callback_data="auto:stop")
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
builder.adjust(3, 1)
return builder.as_markup()
# собрать текст экрана
def _build_auto_text() -> str:
service = AutoTradeService()
state = service.get_state()
account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE"
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else ""
configured = _is_auto_configured(state)
price = _market_price_or_dash(state.symbol)
status_line = {
"OFF": "⚪ Off",
"OBSERVING": "👀 Watch",
"RUNNING": "🟢 On",
}.get(state.status, state.status)
header = (
f"<b>🤖 Автоторговля · {status_line}</b>\n"
f"🔸 {account_mode} аккаунт\n\n"
)
if state.status == "OFF":
if not configured:
return (
f"{header}"
"⚠️ Не настроена\n"
"Настрой параметры"
)
return (
f"{header}"
f"{_context_line(state)}\n"
f"Price: {price}\n"
f"Risk: {risk}"
)
position_line = (
f"Pos: {_value_or_dash(state.position_side)} | "
f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}"
)
if state.position_side != "NONE" and state.entry_price is not None:
position_line = (
f"Pos: {_value_or_dash(state.position_side)} | "
f"Entry: $ {_price_or_dash(state.entry_price)} | "
f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}"
)
return (
f"{header}"
f"{_context_line(state)}\n"
f"Price: {price}\n\n"
f"{_signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
f"· {state.decision_status}\n\n"
f"{position_line}\n"
f"Risk: {risk}"
)
# отрисовать live-экран автоторговли
async def _render_auto_screen(
target_message: Message,
*,
edit_mode: bool,
) -> None:
text = _build_auto_text()
if edit_mode:
try:
await target_message.edit_text(text, reply_markup=_auto_keyboard())
except TelegramBadRequest as exc:
if "message is not modified" not in str(exc).lower():
raise
AutoTradeRunner.register_screen(
bot=target_message.bot,
chat_id=target_message.chat.id,
message_id=target_message.message_id,
render_text=_build_auto_text,
render_markup=_auto_keyboard,
)
return
sent_message = await target_message.answer(text, reply_markup=_auto_keyboard())
AutoTradeRunner.register_screen(
bot=sent_message.bot,
chat_id=sent_message.chat.id,
message_id=sent_message.message_id,
render_text=_build_auto_text,
render_markup=_auto_keyboard,
)
# открыть экран из главного меню
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
async def open_auto(message: Message, state: FSMContext) -> None:
await state.clear()
AutoTradeRunner.set_current_screen("auto")
current_state = AutoTradeService().get_state()
if current_state.status in {"RUNNING", "OBSERVING"}:
await AutoTradeRunner.delete_registered_screen(
bot=message.bot,
chat_id=message.chat.id,
)
await _render_auto_screen(message, edit_mode=False)
# открыть экран через callback
@router.callback_query(F.data == "auto:home")
async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True)
return
AutoTradeRunner.set_current_screen("auto")
await _render_auto_screen(callback.message, edit_mode=True)
await callback.answer()
# включить активную торговлю
@router.callback_query(F.data == "auto:start")
async def auto_start(callback: CallbackQuery) -> None:
service = AutoTradeService()
state = service.get_state()
if not _is_auto_configured(state):
await callback.answer(
"Сначала настрой параметры автоторговли",
show_alert=True,
)
if callback.message is not None:
await open_auto_settings(callback)
return
_, message = service.start()
AutoTradeRunner.set_current_screen("auto")
AutoTradeRunner.start()
if callback.message is not None:
await _render_auto_screen(callback.message, edit_mode=True)
await callback.answer(message)
# включить наблюдение
@router.callback_query(F.data == "auto:observe")
async def auto_observe(callback: CallbackQuery) -> None:
service = AutoTradeService()
state = service.get_state()
if not _is_auto_configured(state):
await callback.answer(
"Сначала настрой параметры автоторговли",
show_alert=True,
)
if callback.message is not None:
await open_auto_settings(callback)
return
_, message = service.observe()
AutoTradeRunner.set_current_screen("auto")
AutoTradeRunner.start()
if callback.message is not None:
await _render_auto_screen(callback.message, edit_mode=True)
await callback.answer(message)
# выключить автоторговлю
@router.callback_query(F.data == "auto:stop")
async def auto_stop(callback: CallbackQuery) -> None:
service = AutoTradeService()
_, message = service.stop()
AutoTradeRunner.stop()
if callback.message is not None:
await _render_auto_screen(callback.message, edit_mode=True)
await callback.answer(message)