Stage 07.4.3.11 — Risk Settings UI & UX
This commit is contained in:
224
app/src/telegram/handlers/auto/ui.py
Normal file
224
app/src/telegram/handlers/auto/ui.py
Normal 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)}"
|
||||
)
|
||||
Reference in New Issue
Block a user