diff --git a/app/src/telegram/handlers/auto.py b/app/src/telegram/handlers/_auto.py
similarity index 100%
rename from app/src/telegram/handlers/auto.py
rename to app/src/telegram/handlers/_auto.py
diff --git a/app/src/telegram/handlers/auto/__init__.py b/app/src/telegram/handlers/auto/__init__.py
new file mode 100644
index 0000000..04c251a
--- /dev/null
+++ b/app/src/telegram/handlers/auto/__init__.py
@@ -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"]
\ No newline at end of file
diff --git a/app/src/telegram/handlers/auto/debug.py b/app/src/telegram/handlers/auto/debug.py
new file mode 100644
index 0000000..9cae47d
--- /dev/null
+++ b/app/src/telegram/handlers/auto/debug.py
@@ -0,0 +1,2 @@
+# app/src/telegram/handlers/auto/debug.py
+
diff --git a/app/src/telegram/handlers/auto/main.py b/app/src/telegram/handlers/auto/main.py
new file mode 100644
index 0000000..fdc60cd
--- /dev/null
+++ b/app/src/telegram/handlers/auto/main.py
@@ -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)
\ No newline at end of file
diff --git a/app/src/telegram/handlers/auto/risk.py b/app/src/telegram/handlers/auto/risk.py
new file mode 100644
index 0000000..0b7a853
--- /dev/null
+++ b/app/src/telegram/handlers/auto/risk.py
@@ -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 = (
+ "⚠️ Risk Settings\n\n"
+ "СИСТЕМА · Настройки · Автоторговля\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"
+ "Подсказка:\n"
+ "Пример: 0.5, 1\n"
+ "Введите 0, чтобы отключить параметр."
+ )
+
+ 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(
+ "🛑 Stop Loss\n\n"
+ "СИСТЕМА · Настройки · Автоторговля\n\n"
+ "Введите Stop Loss в процентах.\n"
+ "Например: 2\n\n"
+ "Введите 0, чтобы отключить."
+ )
+
+ 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(
+ "🎯 Take Profit\n\n"
+ "СИСТЕМА · Настройки · Автоторговля\n\n"
+ "Введите Take Profit в процентах.\n"
+ "Например: 3\n\n"
+ "Введите 0, чтобы отключить."
+ )
+
+ 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(
+ "💸 Max Loss\n\n"
+ "СИСТЕМА · Настройки · Автоторговля\n\n"
+ "Введите максимальный paper-убыток в USD.\n"
+ "Например: 10\n\n"
+ "Введите 0, чтобы отключить."
+ )
+
+ 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()
\ No newline at end of file
diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py
new file mode 100644
index 0000000..33bc87d
--- /dev/null
+++ b/app/src/telegram/handlers/auto/ui.py
@@ -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"🤖 Автоторговля · {status_line}\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)}"
+ )
\ No newline at end of file
diff --git a/app/src/telegram/handlers/system.py b/app/src/telegram/handlers/system.py
index a1cd08b..636fb5f 100644
--- a/app/src/telegram/handlers/system.py
+++ b/app/src/telegram/handlers/system.py
@@ -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:
"СИСТЕМА · Настройки\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()
diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py
index 70f643d..2671c33 100644
--- a/app/src/trading/auto/runner.py
+++ b/app/src/trading/auto/runner.py
@@ -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:
diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py
index d6697ef..8db3aa4 100644
--- a/app/src/trading/auto/service.py
+++ b/app/src/trading/auto/service.py
@@ -220,6 +220,24 @@ class AutoTradeService:
state = self.get_state()
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:
diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py
index 384fd08..1dff8d9 100644
--- a/app/src/trading/auto/state.py
+++ b/app/src/trading/auto/state.py
@@ -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
\ No newline at end of file
+ max_loss_usd: float | None = None
\ No newline at end of file
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 294b17c..7e3d204 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -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
diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md
index 9fe0cb1..f8f4c70 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -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
diff --git a/docs/stages/stage-07_4_3_11-risk_settings_ui_ux.md b/docs/stages/stage-07_4_3_11-risk_settings_ui_ux.md
new file mode 100644
index 0000000..a715289
--- /dev/null
+++ b/docs/stages/stage-07_4_3_11-risk_settings_ui_ux.md
@@ -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