Stage 07.1 - auto trading skeleton UI, state machine and mock controls
This commit is contained in:
@@ -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)
|
||||||
52
app/src/trading/auto/service.py
Normal file
52
app/src/trading/auto/service.py
Normal 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, "Автоторговля выключена."
|
||||||
14
app/src/trading/auto/state.py
Normal file
14
app/src/trading/auto/state.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
37
docs/roadmap/stage-07-roadmap.md
Normal file
37
docs/roadmap/stage-07-roadmap.md
Normal 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
|
||||||
102
docs/stages/stage-07_1-auto-trading-skeleton-ui.md
Normal file
102
docs/stages/stage-07_1-auto-trading-skeleton-ui.md
Normal 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. Навигация
|
||||||
|
|
||||||
|
Добавлен переход:
|
||||||
|
|
||||||
|
Автоторговля → Настройки
|
||||||
|
|
||||||
|
Настройки → Автоторговля
|
||||||
Reference in New Issue
Block a user