Stage 07.1 - auto trading skeleton UI, state machine and mock controls

This commit is contained in:
2026-04-28 11:17:22 +03:00
parent cea74da4c4
commit b48d9c7f35
7 changed files with 336 additions and 9 deletions

View File

@@ -6,29 +6,72 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder 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") router = Router(name="auto")
def _status_label(status: str) -> str:
mapping = {
"OFF": "⚪ Выключена",
"OBSERVING": "👀 Наблюдение",
"RUNNING": "🟢 Активна",
}
return mapping.get(status, status)
def _auto_keyboard() -> InlineKeyboardMarkup: def _auto_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder() 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.button(text="🛠️ Настройки", callback_data="settings:auto")
builder.adjust(1)
builder.adjust(3, 1)
return builder.as_markup() 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 (
"<b>🤖 Автоторговля</b>\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( async def _render_auto_screen(
target_message: Message, target_message: Message,
*, *,
edit_mode: bool, edit_mode: bool,
) -> None: ) -> None:
text = _build_auto_text()
if edit_mode: 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: 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_({"🤖 Автоторговля", "🤖 Авто"})) @router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
@@ -47,3 +90,36 @@ async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) ->
await _render_auto_screen(callback.message, edit_mode=True) await _render_auto_screen(callback.message, edit_mode=True)
await callback.answer() 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)

View File

@@ -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, "Автоторговля выключена."

View File

@@ -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

View File

@@ -0,0 +1,35 @@
# 0015 — Auto Trading State Machine
## Решение
Для автоторговли вводится state-machine из трёх состояний:
- OFF
- OBSERVING
- RUNNING
## Причины
OFF:
полное отключение loop.
OBSERVING:
анализ рынка без открытия новых сделок.
RUNNING:
анализ + торговля.
## Последствия
Позволяет:
- быстро строить background loop;
- безопасно включать наблюдение;
- расширять стратегический движок.

View File

@@ -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
--- ---

View File

@@ -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

View File

@@ -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. Навигация
Добавлен переход:
Автоторговля → Настройки
Настройки → Автоторговля