diff --git a/app/src/telegram/handlers/auto.py b/app/src/telegram/handlers/auto.py index 0d1e5d7..43f1799 100644 --- a/app/src/telegram/handlers/auto.py +++ b/app/src/telegram/handlers/auto.py @@ -6,29 +6,72 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.exceptions import TelegramBadRequest -from src.telegram.menus import AUTO_TEXT +from src.telegram.ui.common import mode_line +from src.trading.auto.service import AutoTradeService router = Router(name="auto") +def _status_label(status: str) -> str: + mapping = { + "OFF": "⚪ Выключена", + "OBSERVING": "👀 Наблюдение", + "RUNNING": "🟢 Активна", + } + return mapping.get(status, status) + + def _auto_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() + + # 1 ряд + builder.button(text="▶️ Start", callback_data="auto:start") + builder.button(text="👀 Watch", callback_data="auto:observe") + builder.button(text="🛑 Stop", callback_data="auto:stop") + + # 2 ряд builder.button(text="🛠️ Настройки", callback_data="settings:auto") - builder.adjust(1) + + builder.adjust(3, 1) return builder.as_markup() +def _build_auto_text() -> str: + state = AutoTradeService().get_state() + + strategy = state.strategy or "—" + risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—" + + return ( + "🤖 Автоторговля\n" + f"{mode_line()}" + f"Статус: {_status_label(state.status)}\n" + f"Стратегия: {strategy}\n" + f"Инструмент: {state.symbol}\n" + f"Риск: {risk}\n" + f"PnL: {state.pnl_usd:.2f} USD" + ) + + async def _render_auto_screen( target_message: Message, *, edit_mode: bool, ) -> None: + text = _build_auto_text() + if edit_mode: - await target_message.edit_text(AUTO_TEXT, reply_markup=_auto_keyboard()) + try: + await target_message.edit_text(text, reply_markup=_auto_keyboard()) + except TelegramBadRequest as exc: + if "message is not modified" in str(exc).lower(): + return + raise else: - await target_message.answer(AUTO_TEXT, reply_markup=_auto_keyboard()) + await target_message.answer(text, reply_markup=_auto_keyboard()) @router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"})) @@ -46,4 +89,37 @@ async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> return await _render_auto_screen(callback.message, edit_mode=True) - await callback.answer() \ No newline at end of file + await callback.answer() + + +@router.callback_query(F.data == "auto:start") +async def auto_start(callback: CallbackQuery) -> None: + service = AutoTradeService() + _, message = service.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() + _, message = service.observe() + + 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() + + 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/trading/auto/service.py b/app/src/trading/auto/service.py new file mode 100644 index 0000000..c94bda5 --- /dev/null +++ b/app/src/trading/auto/service.py @@ -0,0 +1,52 @@ +# app/src/trading/auto/service.py + +from __future__ import annotations + +from src.core.config import load_settings +from src.trading.auto.state import AutoTradeState + + +class AutoTradeService: + _state = AutoTradeState() + + def get_state(self) -> AutoTradeState: + if not self._state.symbol: + self._state.symbol = load_settings().default_symbol + + return self._state + + def start(self) -> tuple[AutoTradeState, str]: + state = self.get_state() + + if state.status == "RUNNING": + return state, "Автоторговля уже активна." + + if state.status == "OBSERVING": + state.status = "RUNNING" + return state, "Автоторговля активирована." + + state.status = "RUNNING" + return state, "Автоторговля запущена." + + def observe(self) -> tuple[AutoTradeState, str]: + state = self.get_state() + previous_status = state.status + + if previous_status == "OBSERVING": + return state, "Режим наблюдения уже включён." + + state.status = "OBSERVING" + + if previous_status == "OFF": + return state, "Включён режим наблюдения." + + return state, "Автоторговля переведена в режим наблюдения." + + def stop(self) -> tuple[AutoTradeState, str]: + state = self.get_state() + + if state.status == "OFF": + return state, "Автоторговля уже выключена." + + state.status = "OFF" + return state, "Автоторговля выключена." \ No newline at end of file diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py new file mode 100644 index 0000000..c857c83 --- /dev/null +++ b/app/src/trading/auto/state.py @@ -0,0 +1,14 @@ +# app/src/trading/auto/state.py + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(slots=True) +class AutoTradeState: + status: str = "OFF" + strategy: str | None = None + symbol: str = "" + risk_percent: float | None = None + pnl_usd: float = 0.0 \ No newline at end of file diff --git a/docs/decisions/0015-auto-trading-state-machine.md b/docs/decisions/0015-auto-trading-state-machine.md new file mode 100644 index 0000000..25e77db --- /dev/null +++ b/docs/decisions/0015-auto-trading-state-machine.md @@ -0,0 +1,35 @@ +# 0015 — Auto Trading State Machine + +## Решение + +Для автоторговли вводится state-machine из трёх состояний: + +- OFF + +- OBSERVING + +- RUNNING + +## Причины + +OFF: + +полное отключение loop. + +OBSERVING: + +анализ рынка без открытия новых сделок. + +RUNNING: + +анализ + торговля. + +## Последствия + +Позволяет: + +- быстро строить background loop; + +- безопасно включать наблюдение; + +- расширять стратегический движок. \ No newline at end of file diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index 309b727..b5134bd 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -85,10 +85,21 @@ --- -## Stage 07 — Observability -⏳ логирование -⏳ алерты -⏳ метрики +## Stage 07 — Auto Trading + +### 07.1 +✔ auto trading skeleton UI +✔ state machine +✔ mock controls + +### 07.2 +⏳ real settings + +### 07.3 +⏳ background loop + +### 07.4 +⏳ strategy plugin architecture --- diff --git a/docs/roadmap/stage-07-roadmap.md b/docs/roadmap/stage-07-roadmap.md new file mode 100644 index 0000000..545ebc5 --- /dev/null +++ b/docs/roadmap/stage-07-roadmap.md @@ -0,0 +1,37 @@ +# Stage 07 — Auto Trading Roadmap + +## Цель + +Добавить автоторговлю. + +--- + +## 07.1 — Skeleton UI + +✔ экран автоторговли +✔ state machine +✔ mock controls + +--- + +## 07.2 — Real settings + +⏳ стратегия +⏳ риск +⏳ символ + +--- + +## 07.3 — Background loop + +⏳ scheduler +⏳ market polling +⏳ signal loop + +--- + +## 07.4 — Strategy plugins + +⏳ plugin architecture +⏳ strategy registry +⏳ signal execution \ No newline at end of file diff --git a/docs/stages/stage-07_1-auto-trading-skeleton-ui.md b/docs/stages/stage-07_1-auto-trading-skeleton-ui.md new file mode 100644 index 0000000..c03cad8 --- /dev/null +++ b/docs/stages/stage-07_1-auto-trading-skeleton-ui.md @@ -0,0 +1,102 @@ +# Stage 07.1 — Auto Trading Skeleton UI + +## Что сделано + +Реализован базовый skeleton автоторговли. + +--- + +## 1. Экран 🤖 Автоторговля + +Добавлен новый экран: + +Показывает: + +- режим аккаунта +- статус автоторговли +- стратегию +- инструмент +- риск +- PnL + +--- + +## 2. State machine + +Добавлены состояния: + +- OFF → выключена +- OBSERVING → наблюдение +- RUNNING → активна + +Логика: + +### OFF +бот полностью выключен + +### OBSERVING +бот следит за рынком, но не торгует + +### RUNNING +бот следит за рынком и торгует + +--- + +## 3. Mock controls + +Добавлены кнопки управления: + +- ▶️ Start +- 👀 Watch +- 🛑 Stop + +Поведение: + +### Start +OFF / OBSERVING → RUNNING + +### Watch +OFF / RUNNING → OBSERVING + +### Stop +OBSERVING / RUNNING → OFF + +--- + +## 4. Service layer + +Добавлены файлы: + +``` +src/trading/auto/state.py +src/trading/auto/service.py +``` + +### AutoTradeState + +Хранит: + +* status +* strategy +* symbol +* risk_percent +* pnl_usd + +### AutoTradeService + +Методы: + +* get_state() +* start() +* observe() +* stop() + +--- + +## 5. Навигация + +Добавлен переход: + +Автоторговля → Настройки + +Настройки → Автоторговля \ No newline at end of file