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,224 @@
# app/src/telegram/handlers/auto/ui.py
from __future__ import annotations
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.integrations.exchange.service import ExchangeService
from src.telegram.ui.common import mode_line
from src.telegram.ui.currency_ui import format_usd_amount
from src.trading.auto.service import AutoTradeService
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 ""
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"
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
def compact_strategy(strategy: str | None) -> str:
if not strategy:
return ""
return strategy.upper()
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.button(text="⚠️ Risk", callback_data="auto:risk")
builder.adjust(3, 2)
return builder.as_markup()
def risk_settings_line(state) -> str:
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
max_loss = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
return f"Controls: SL {sl} · TP {tp} · ML {max_loss}"
def estimated_size_line(state) -> str:
if state.risk_percent is None or state.leverage is None:
return ""
size = round((state.risk_percent * state.leverage) / 100, 8)
return f"Est. Size: {size}"
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"Position Risk: {risk}\n"
f"{estimated_size_line(state)}\n"
f"{risk_settings_line(state)}"
)
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"Position Risk: {risk}\n"
f"{estimated_size_line(state)}\n"
f"{risk_settings_line(state)}"
)