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,11 @@
from aiogram import Router
from src.telegram.handlers.auto.main import router as main_router
from src.telegram.handlers.auto.risk import router as risk_router
router = Router(name="auto")
router.include_router(main_router)
router.include_router(risk_router)
__all__ = ["router"]

View File

@@ -0,0 +1,2 @@
# app/src/telegram/handlers/auto/debug.py

View File

@@ -0,0 +1,150 @@
# app/src/telegram/handlers/auto/main.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, Message
from src.telegram.handlers.auto.ui import (
auto_keyboard,
build_auto_text,
is_auto_configured,
)
from src.telegram.handlers.system import open_auto_settings
from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService
router = Router(name="auto")
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)
@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)

View File

@@ -0,0 +1,376 @@
# app/src/telegram/handlers/auto/risk.py
from __future__ import annotations
import asyncio
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
router = Router(name="auto_risk")
class AutoRiskStates(StatesGroup):
waiting_stop_loss = State()
waiting_take_profit = State()
waiting_max_loss = State()
def _format_percent(value: float | None) -> str:
if value is None:
return "⚪ off"
return f"🟢 {value:g}%"
def _format_usd(value: float | None) -> str:
if value is None:
return "⚪ off"
return f"🟢 {value:g} USD"
def _risk_keyboard() -> InlineKeyboardMarkup:
state = AutoTradeService().get_state()
builder = InlineKeyboardBuilder()
builder.button(text=f"🛑 Stop Loss", callback_data="auto:risk:set_sl")
builder.button(text=f"🎯 Take Profit", callback_data="auto:risk:set_tp")
builder.button(text=f"💸 Max Loss", callback_data="auto:risk:set_ml")
builder.button(text="♻️ Reset", callback_data="auto:risk:reset")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
builder.adjust(2, 2, 2)
return builder.as_markup()
def _risk_text(status_message: str | None = None) -> str:
state = AutoTradeService().get_state()
active_count = sum(
value is not None
for value in (
state.stop_loss_percent,
state.take_profit_percent,
state.max_loss_usd,
)
)
status = "🟢 Активна" if active_count else "⚪ Выключена"
text = (
"<b>⚠️ Risk Settings</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
f"Статус защиты: {status}\n"
f"Активных правил: {active_count}/3\n\n"
f"🛑 Stop Loss: {_format_percent(state.stop_loss_percent)}\n"
f"🎯 Take Profit: {_format_percent(state.take_profit_percent)}\n"
f"💸 Max Loss: {_format_usd(state.max_loss_usd)}\n\n"
"<b>Подсказка:</b>\n"
"Пример: <code>0.5</code>, <code>1</code>\n"
"Введите <code>0</code>, чтобы отключить параметр."
)
if status_message:
text += f"\n\n{status_message}"
return text
async def _render_risk_screen(callback: CallbackQuery) -> None:
AutoTradeRunner.set_current_screen("auto_risk")
if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True)
return
await callback.message.edit_text(
_risk_text(),
reply_markup=_risk_keyboard(),
)
await callback.answer()
async def _render_risk_screen_by_message(
message: Message,
*,
state: FSMContext,
status_message: str | None = None,
auto_clear: bool = False,
) -> None:
AutoTradeRunner.set_current_screen("auto_risk")
data = await state.get_data()
chat_id = data.get("risk_chat_id")
message_id = data.get("risk_message_id")
if chat_id is None or message_id is None:
await message.answer(
_risk_text(status_message=status_message),
reply_markup=_risk_keyboard(),
)
return
await message.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=_risk_text(status_message=status_message),
reply_markup=_risk_keyboard(),
)
if status_message and auto_clear:
await asyncio.sleep(2.5)
if getattr(AutoTradeRunner, "_current_screen", None) != "auto_risk":
return
try:
await message.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=_risk_text(),
reply_markup=_risk_keyboard(),
)
except Exception:
pass
async def _remember_risk_screen(callback: CallbackQuery, state: FSMContext) -> None:
if callback.message is None:
return
await state.update_data(
risk_chat_id=callback.message.chat.id,
risk_message_id=callback.message.message_id,
)
def _parse_positive_or_none(raw_text: str | None) -> float | None:
value_text = (raw_text or "").strip().replace(",", ".")
if value_text in {"0", "0.0", "off", "OFF", "-"}:
return None
value = float(value_text)
if value <= 0:
return None
return value
def _validate_percent(value: float | None) -> bool:
if value is None:
return True
return 0 < value <= 100
def _validate_max_loss(value: float | None) -> bool:
if value is None:
return True
return 0 < value <= 10000
def _log_risk_updated(action: str) -> None:
state = AutoTradeService().get_state()
try:
JournalService().log_ui_info(
event_type="auto_risk_settings_updated",
message=(
"Risk settings updated: "
f"SL={state.stop_loss_percent}, "
f"TP={state.take_profit_percent}, "
f"ML={state.max_loss_usd}"
),
screen="auto",
action=action,
payload={
"stop_loss_percent": state.stop_loss_percent,
"take_profit_percent": state.take_profit_percent,
"max_loss_usd": state.max_loss_usd,
},
)
except Exception:
pass
@router.callback_query(F.data == "auto:risk")
async def open_auto_risk(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
await _render_risk_screen(callback)
@router.callback_query(F.data == "settings:auto_risk_controls")
async def open_auto_risk_from_settings(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
await _render_risk_screen(callback)
@router.callback_query(F.data == "auto:risk:set_sl")
async def ask_stop_loss(callback: CallbackQuery, state: FSMContext) -> None:
AutoTradeRunner.set_current_screen("auto_risk")
await state.set_state(AutoRiskStates.waiting_stop_loss)
await _remember_risk_screen(callback, state)
if callback.message is not None:
await callback.message.edit_text(
"<b>🛑 Stop Loss</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите Stop Loss в процентах.\n"
"Например: <code>2</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
)
await callback.answer()
@router.callback_query(F.data == "auto:risk:set_tp")
async def ask_take_profit(callback: CallbackQuery, state: FSMContext) -> None:
AutoTradeRunner.set_current_screen("auto_risk")
await state.set_state(AutoRiskStates.waiting_take_profit)
await _remember_risk_screen(callback, state)
if callback.message is not None:
await callback.message.edit_text(
"<b>🎯 Take Profit</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите Take Profit в процентах.\n"
"Например: <code>3</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
)
await callback.answer()
@router.callback_query(F.data == "auto:risk:set_ml")
async def ask_max_loss(callback: CallbackQuery, state: FSMContext) -> None:
AutoTradeRunner.set_current_screen("auto_risk")
await state.set_state(AutoRiskStates.waiting_max_loss)
await _remember_risk_screen(callback, state)
if callback.message is not None:
await callback.message.edit_text(
"<b>💸 Max Loss</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите максимальный paper-убыток в USD.\n"
"Например: <code>10</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
)
await callback.answer()
@router.callback_query(F.data == "auto:risk:reset")
async def reset_risk(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
AutoTradeRunner.set_current_screen("auto_risk")
service = AutoTradeService()
service.set_stop_loss_percent(None)
service.set_take_profit_percent(None)
service.set_max_loss_usd(None)
_log_risk_updated("risk_reset")
if callback.message is not None:
await callback.message.edit_text(
_risk_text(status_message="✅ Risk Controls сброшены"),
reply_markup=_risk_keyboard(),
)
await callback.answer()
await asyncio.sleep(5.5)
if getattr(AutoTradeRunner, "_current_screen", None) != "auto_risk":
return
try:
await callback.message.edit_text(
_risk_text(),
reply_markup=_risk_keyboard(),
)
except Exception:
pass
return
await callback.answer()
@router.message(AutoRiskStates.waiting_stop_loss)
async def set_stop_loss(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 2 или 0 для отключения.")
return
if not _validate_percent(value):
await message.answer("Stop Loss должен быть от 0 до 100%.")
return
AutoTradeService().set_stop_loss_percent(value)
_log_risk_updated("set_stop_loss")
await _render_risk_screen_by_message(
message,
state=state,
status_message=f"✅ Stop Loss обновлён: {_format_percent(value)}",
auto_clear=True,
)
await state.clear()
@router.message(AutoRiskStates.waiting_take_profit)
async def set_take_profit(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 3 или 0 для отключения.")
return
if not _validate_percent(value):
await message.answer("Take Profit должен быть от 0 до 100%.")
return
AutoTradeService().set_take_profit_percent(value)
_log_risk_updated("set_take_profit")
await _render_risk_screen_by_message(
message,
state=state,
status_message=f"✅ Take Profit обновлён: {_format_percent(value)}",
auto_clear=True,
)
await state.clear()
@router.message(AutoRiskStates.waiting_max_loss)
async def set_max_loss(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 10 или 0 для отключения.")
return
if not _validate_max_loss(value):
await message.answer("Max Loss должен быть от 0 до 10000 USD.")
return
AutoTradeService().set_max_loss_usd(value)
_log_risk_updated("set_max_loss")
await _render_risk_screen_by_message(
message,
state=state,
status_message=f"✅ Max Loss обновлён: {_format_usd(value)}",
auto_clear=True,
)
await state.clear()

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)}"
)

View File

@@ -197,6 +197,10 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
symbol = state.symbol or ""
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else ""
leverage = f"x{state.leverage:g}" if state.leverage is not None else ""
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"
ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
risk_controls = f"SL {sl} · TP {tp} · ML {ml}"
strategy_icon = "" if strategy_ready else "👉"
symbol_icon = "" if symbol_ready else "👉"
@@ -214,8 +218,9 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
"<b>СИСТЕМА</b> · Настройки\n\n"
f"{strategy_icon} Стратегия: {strategy}\n"
f"{symbol_icon} Инструмент: {symbol}\n"
f"{risk_icon} Риск: {risk}\n"
f"{risk_icon} Риск на сделку: {risk}\n"
f"{leverage_icon} Плечо: {leverage}\n\n"
f"✅ Risk Controls: {risk_controls}\n\n"
f"{config_status}"
)
@@ -225,11 +230,12 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
builder = InlineKeyboardBuilder()
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
builder.button(text="📈 Инструмент", callback_data="settings:auto_symbol")
builder.button(text="🛡️ Риск", callback_data="settings:auto_risk")
builder.button(text="🛡️ Риск на сделку", callback_data="settings:auto_risk")
builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage")
builder.button(text=" Назад", callback_data="system:management")
builder.button(text=" Risk Controls", callback_data="auto:risk")
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
builder.adjust(2, 2, 2)
builder.button(text="⬅️ Назад", callback_data="system:management")
builder.adjust(2, 2, 1, 2)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
await callback.answer()

View File

@@ -550,6 +550,9 @@ class AutoTradeRunner:
@classmethod
async def _refresh_screen(cls, *, force: bool = False) -> None:
if cls._current_screen != "auto":
return
now = time.monotonic()
if now < cls._retry_after_until:

View File

@@ -221,6 +221,24 @@ class AutoTradeService:
state.leverage = leverage
return state
# установить stop loss в %
def set_stop_loss_percent(self, value: float | None) -> AutoTradeState:
state = self.get_state()
state.stop_loss_percent = value
return state
# установить take profit в %
def set_take_profit_percent(self, value: float | None) -> AutoTradeState:
state = self.get_state()
state.take_profit_percent = value
return state
# установить max loss в USD
def set_max_loss_usd(self, value: float | None) -> AutoTradeState:
state = self.get_state()
state.max_loss_usd = value
return state
# сбросить внутренний трекинг сигналов
def _reset_signal_tracking(self) -> None:
self._last_signal_key = None

View File

@@ -68,15 +68,10 @@ class AutoTradeState:
leverage: float | None = 2.0
# stop loss по движению цены в %
#stop_loss_percent: float | None = 2.0
stop_loss_percent: float | None = None
# take profit по движению цены в %
#take_profit_percent: float | None = 3.0
take_profit_percent: float | None = None
# максимальный допустимый paper-убыток в USD
#max_loss_usd: float | None = None
# для демонстрации рисков: стоп-лосс и тейк-профит по риску в % от капитала
stop_loss_percent: float | None = None
take_profit_percent: float | None = None
max_loss_usd: float | None = 0.01
max_loss_usd: float | None = None

View File

@@ -192,6 +192,29 @@
- unified execution alert for flip
- improved execution realism (no idle gap)
#### 07.4.3.10 — Auto UI Refactor & Live Screen ✅
- разделение auto.py → main.py + ui.py
- единый render-пайплайн через AutoTradeRunner
- live-обновление экрана без дублирования сообщений
- компактный UI: Signal / Decision / Position / PnL
- отображение Position Risk и Est. Size
- унификация форматирования (USD / price / leverage)
- защита от лишних edit (message is not modified)
#### 07.4.3.11 — Risk Settings UI & UX ✅
- отдельный экран Risk Settings (SL / TP / Max Loss)
- FSM-ввод значений (проценты и USD)
- inline-редактирование (без новых сообщений)
- временные статусы (auto-clear через ~2.5 сек)
- защита от race condition (убран “скачок” экранов)
- reset risk controls (все параметры → off)
- интеграция в Auto screen (Controls строка)
- интеграция в Settings (Risk Controls summary)
- единая навигация: Auto ↔ Settings ↔ Risk
- UX-подсказки и валидация ввода
### 07.4.4
⏳ Grid Strategy

View File

@@ -176,6 +176,29 @@
- unified execution alert for flip
- improved execution realism (no idle gap)
#### 07.4.3.10 — Auto UI Refactor & Live Screen ✅
- разделение auto.py → main.py + ui.py
- единый render-пайплайн через AutoTradeRunner
- live-обновление экрана без дублирования сообщений
- компактный UI: Signal / Decision / Position / PnL
- отображение Position Risk и Est. Size
- унификация форматирования (USD / price / leverage)
- защита от лишних edit (message is not modified)
#### 07.4.3.11 — Risk Settings UI & UX ✅
- отдельный экран Risk Settings (SL / TP / Max Loss)
- FSM-ввод значений (проценты и USD)
- inline-редактирование (без новых сообщений)
- временные статусы (auto-clear через ~2.5 сек)
- защита от race condition (убран “скачок” экранов)
- reset risk controls (все параметры → off)
- интеграция в Auto screen (Controls строка)
- интеграция в Settings (Risk Controls summary)
- единая навигация: Auto ↔ Settings ↔ Risk
- UX-подсказки и валидация ввода
---
### 07.4.4

View File

@@ -0,0 +1,124 @@
# Stage 07.4.3.11 — Risk Settings UI & UX
## 📌 Обзор
Реализован полноценный Telegram UI для управления risk-настройками:
- Stop Loss (%)
- Take Profit (%)
- Max Loss (USD)
- FSM-ввод
- Временные статусы
- Защита от "скачков" экранов
- Интеграция с Auto и Settings
---
## 🎯 Цель
Сделать risk controls управляемыми из Telegram без изменения кода.
---
## ⚙️ Настройки
- SL (% от цены)
- TP (% от цены)
- ML (USD лимит)
Отключение через: 0 / off
---
## 🖥 Экран
⚠️ Risk Settings
СИСТЕМА · Настройки · Автоторговля
Статус защиты: 🟢 Активна
Активных правил: 2/3
🛑 Stop Loss: ⚪ off
🎯 Take Profit: 🟢 0.5%
💸 Max Loss: 🟢 10 USD
---
## 🎛 Кнопки
🛑 Stop Loss | 🎯 Take Profit
💸 Max Loss | ♻️ Reset
⬅️ Назад | 🤖 Автоторговля
---
## 🧠 FSM
Состояния:
- waiting_stop_loss
- waiting_take_profit
- waiting_max_loss
Flow:
клик → ввод → валидация → update state → edit_message
---
## 🔢 Парсинг
0 → None
0.5 → 0.5
"0,5" → 0.5
---
## ✅ Валидация
Percent: 0 < x ≤ 100
Max Loss: 0 < x ≤ 10000
---
## 🔔 UX
Статус:
✅ Take Profit обновлён: 🟢 0.5%
Автоочистка через ~2.5 сек
---
## ⚠️ Fix скачков
Добавлена защита:
if current_screen != "auto_risk": return
---
## 🔄 Inline UI
Используется:
edit_message_text()
---
## 🔗 Интеграция
Auto screen:
Controls: SL · TP · ML
Settings:
Risk Controls summary
---
## 🚀 Результат
✔ UI
✔ FSM
✔ UX
✔ Навигация
✔ Стабильность
---
## 🗺 Roadmap
07.4.3.11 — Risk Settings UI & UX ✅
07.4.3.12 — Risk Engine (execution)
07.4.3.13 — Position sizing
07.4.3.14 — Analytics
---
## 💬 Commit
Stage 07.4.3.11 — Risk Settings UI & UX