Stage 07.4.3.11 — Risk Settings UI & UX
This commit is contained in:
11
app/src/telegram/handlers/auto/__init__.py
Normal file
11
app/src/telegram/handlers/auto/__init__.py
Normal 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"]
|
||||||
2
app/src/telegram/handlers/auto/debug.py
Normal file
2
app/src/telegram/handlers/auto/debug.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# app/src/telegram/handlers/auto/debug.py
|
||||||
|
|
||||||
150
app/src/telegram/handlers/auto/main.py
Normal file
150
app/src/telegram/handlers/auto/main.py
Normal 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)
|
||||||
376
app/src/telegram/handlers/auto/risk.py
Normal file
376
app/src/telegram/handlers/auto/risk.py
Normal 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()
|
||||||
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)}"
|
||||||
|
)
|
||||||
@@ -197,6 +197,10 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
symbol = state.symbol or "—"
|
symbol = state.symbol or "—"
|
||||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
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 "—"
|
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 "👉"
|
strategy_icon = "✅" if strategy_ready else "👉"
|
||||||
symbol_icon = "✅" if symbol_ready else "👉"
|
symbol_icon = "✅" if symbol_ready else "👉"
|
||||||
@@ -214,8 +218,9 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||||
f"{strategy_icon} Стратегия: {strategy}\n"
|
f"{strategy_icon} Стратегия: {strategy}\n"
|
||||||
f"{symbol_icon} Инструмент: {symbol}\n"
|
f"{symbol_icon} Инструмент: {symbol}\n"
|
||||||
f"{risk_icon} Риск: {risk}\n"
|
f"{risk_icon} Риск на сделку: {risk}\n"
|
||||||
f"{leverage_icon} Плечо: {leverage}\n\n"
|
f"{leverage_icon} Плечо: {leverage}\n\n"
|
||||||
|
f"✅ Risk Controls: {risk_controls}\n\n"
|
||||||
f"{config_status}"
|
f"{config_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,11 +230,12 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
|
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
|
||||||
builder.button(text="📈 Инструмент", callback_data="settings:auto_symbol")
|
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="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.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.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|||||||
@@ -550,6 +550,9 @@ class AutoTradeRunner:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
||||||
|
if cls._current_screen != "auto":
|
||||||
|
return
|
||||||
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|
||||||
if now < cls._retry_after_until:
|
if now < cls._retry_after_until:
|
||||||
|
|||||||
@@ -220,6 +220,24 @@ class AutoTradeService:
|
|||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
state.leverage = leverage
|
state.leverage = leverage
|
||||||
return state
|
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:
|
def _reset_signal_tracking(self) -> None:
|
||||||
|
|||||||
@@ -68,15 +68,10 @@ class AutoTradeState:
|
|||||||
leverage: float | None = 2.0
|
leverage: float | None = 2.0
|
||||||
|
|
||||||
# stop loss по движению цены в %
|
# stop loss по движению цены в %
|
||||||
#stop_loss_percent: float | None = 2.0
|
stop_loss_percent: float | None = None
|
||||||
|
|
||||||
# take profit по движению цены в %
|
# take profit по движению цены в %
|
||||||
#take_profit_percent: float | None = 3.0
|
take_profit_percent: float | None = None
|
||||||
|
|
||||||
# максимальный допустимый paper-убыток в USD
|
# максимальный допустимый paper-убыток в USD
|
||||||
#max_loss_usd: float | None = None
|
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
|
|
||||||
@@ -192,6 +192,29 @@
|
|||||||
- unified execution alert for flip
|
- unified execution alert for flip
|
||||||
- improved execution realism (no idle gap)
|
- 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
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|||||||
@@ -176,6 +176,29 @@
|
|||||||
- unified execution alert for flip
|
- unified execution alert for flip
|
||||||
- improved execution realism (no idle gap)
|
- 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
|
### 07.4.4
|
||||||
|
|||||||
124
docs/stages/stage-07_4_3_11-risk_settings_ui_ux.md
Normal file
124
docs/stages/stage-07_4_3_11-risk_settings_ui_ux.md
Normal 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
|
||||||
Reference in New Issue
Block a user