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