Stage 07.4.3.1 — trend strategy stabilization

This commit is contained in:
2026-05-01 18:25:27 +03:00
parent ec8e53c416
commit 38c8686a9b
8 changed files with 735 additions and 49 deletions

View File

@@ -46,6 +46,101 @@ def _signal_label(signal: str | None) -> str:
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}"
# формат USD
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"
# формат торгового инструмента для UI
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
# стратегия для компактного UI
def _compact_strategy(strategy: str | None) -> str:
if not strategy:
return ""
return strategy.upper()
# плечо для компактного UI
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()
@@ -64,21 +159,46 @@ def _build_auto_text() -> str:
service = AutoTradeService()
state = service.get_state()
strategy = _strategy_label(state.strategy)
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)
header = (
"<b>🤖 Автоторговля</b>\n"
f"🔸 {account_mode} аккаунт\n\n"
)
if state.status == "OFF":
if not configured:
return (
f"{header}"
"⚪ Выключена\n\n"
"⚠️ Не настроена\n"
"Настрой параметры"
)
return (
f"{header}"
"⚪ Выключена\n\n"
f"{_context_line(state)}\n"
f"Risk: {risk}"
)
status_line = (
"🟢 Активна"
if state.status == "RUNNING"
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\n"
f"Последний анализ: {state.last_check_at or ''}\n"
f"Сигнал: {_signal_label(state.last_signal)} · {state.last_signal_repeat_count} подряд\n"
f"Уверенность: {state.last_signal_confidence:.2f}\n"
f"Причина: {state.last_signal_reason or ''}"
f"{header}"
f"{status_line}\n\n"
f"{_context_line(state)}\n\n"
f"{_signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
f"· {state.decision_status}\n\n"
f"Pos: {_value_or_dash(state.position_side)} | "
f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}\n"
f"Risk: {risk}"
)

View File

@@ -168,10 +168,16 @@ async def open_system_management(callback: CallbackQuery) -> None:
@router.callback_query(F.data == "settings:auto")
async def open_auto_settings(callback: CallbackQuery) -> None:
AutoTradeRunner.set_current_screen("settings_auto")
if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True)
return
AutoTradeRunner.unregister_screen(
chat_id=callback.message.chat.id,
message_id=callback.message.message_id,
)
state = AutoTradeService().get_state()
strategy_map = {
@@ -181,13 +187,15 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
}
strategy = strategy_map.get(state.strategy 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 ""
text = (
"<b>🤖 Автоторговля</b>\n\n"
"<b>СИСТЕМА</b> · Настройки\n\n"
f"Стратегия: {strategy}\n"
f"Инструмент: {state.symbol}\n"
f"Риск: {risk}\n\n"
f"Риск: {risk}\n"
f"Плечо: {leverage}\n\n"
"Выберите настройку:"
)
@@ -195,9 +203,10 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
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_leverage")
builder.button(text="⬅️ Назад", callback_data="system:management")
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
builder.adjust(2, 1, 2)
builder.adjust(2, 2, 2)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
await callback.answer()
@@ -313,6 +322,46 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
await callback.answer("Риск обновлён")
@router.callback_query(F.data == "settings:auto_leverage")
async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
AutoTradeRunner.set_current_screen("settings_auto")
if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True)
return
text = (
"<b>⚙️ Плечо</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Выберите плечо:"
)
builder = InlineKeyboardBuilder()
builder.button(text="x1", callback_data="settings:auto_leverage:1")
builder.button(text="x2", callback_data="settings:auto_leverage:2")
builder.button(text="x3", callback_data="settings:auto_leverage:3")
builder.button(text="x5", callback_data="settings:auto_leverage:5")
builder.button(text="x10", callback_data="settings:auto_leverage:10")
builder.button(text="x20", callback_data="settings:auto_leverage:20")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.adjust(3, 3, 1)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
await callback.answer()
@router.callback_query(F.data.startswith("settings:auto_leverage:"))
async def set_auto_leverage(callback: CallbackQuery) -> None:
leverage = float(callback.data.split(":", 2)[2])
AutoTradeService().set_leverage(leverage)
if callback.message is not None:
await open_auto_settings(callback)
AutoTradeRunner.set_current_screen("settings_auto")
await callback.answer("Плечо обновлено")
@router.callback_query(F.data == "settings:trade")
async def open_trade_settings(callback: CallbackQuery) -> None:
if callback.message is None:

View File

@@ -3,9 +3,11 @@
from __future__ import annotations
import asyncio
import time
from typing import Callable
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
from src.trading.auto.service import AutoTradeService
@@ -18,9 +20,11 @@ class AutoTradeRunner:
_render_text: Callable[[], str] | None = None
_render_markup: Callable[[], object] | None = None
_current_screen: str | None = None
_interval_seconds = 5
_interval_seconds = 15
_last_text: str | None = None
_retry_after_until: float = 0.0
# зарегистрировать live-экран для автообновления
@classmethod
def register_screen(
cls,
@@ -36,8 +40,8 @@ class AutoTradeRunner:
cls._message_id = message_id
cls._render_text = render_text
cls._render_markup = render_markup
cls._last_text = None
# удалить ранее зарегистрированный live-экран
@classmethod
async def delete_registered_screen(
cls,
@@ -62,13 +66,27 @@ class AutoTradeRunner:
cls._message_id = None
cls._render_text = None
cls._render_markup = None
cls._last_text = None
@classmethod
def unregister_screen(
cls,
*,
chat_id: int,
message_id: int,
) -> None:
if cls._chat_id != chat_id or cls._message_id != message_id:
return
cls._message_id = None
cls._render_text = None
cls._render_markup = None
cls._last_text = None
# переключить активный экран
@classmethod
def set_current_screen(cls, screen: str) -> None:
cls._current_screen = screen
# запустить background runner
@classmethod
def start(cls) -> None:
if cls._task is not None and not cls._task.done():
@@ -76,7 +94,6 @@ class AutoTradeRunner:
cls._task = asyncio.create_task(cls._worker())
# остановить background runner
@classmethod
def stop(cls) -> None:
if cls._task is None:
@@ -85,7 +102,6 @@ class AutoTradeRunner:
cls._task.cancel()
cls._task = None
# background loop автоторговли
@classmethod
async def _worker(cls) -> None:
service = AutoTradeService()
@@ -102,9 +118,11 @@ class AutoTradeRunner:
await cls._refresh_screen()
await asyncio.sleep(cls._interval_seconds)
# обновить live-экран Telegram
@classmethod
async def _refresh_screen(cls) -> None:
if time.monotonic() < cls._retry_after_until:
return
if not all(
[
cls._bot,
@@ -116,12 +134,36 @@ class AutoTradeRunner:
):
return
text = cls._render_text()
if text == cls._last_text:
return
try:
await cls._bot.edit_message_text(
chat_id=cls._chat_id,
message_id=cls._message_id,
text=cls._render_text(),
text=text,
reply_markup=cls._render_markup(),
)
cls._last_text = text
except TelegramRetryAfter as exc:
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
except TelegramBadRequest as exc:
error_text = str(exc).lower()
if "message is not modified" in error_text:
cls._last_text = text
return
if "message to edit not found" in error_text:
cls._message_id = None
cls._render_text = None
cls._render_markup = None
cls._last_text = None
return
except Exception:
pass

View File

@@ -16,8 +16,18 @@ class AutoTradeService:
_state = AutoTradeState()
_loop_task: asyncio.Task | None = None
_loop_interval_seconds = 5
# минимальное количество повторов BUY / SELL для подтверждения сигнала
_confirm_repeats = 3
# минимальная уверенность для готовности к будущему execution
_ready_confidence = 0.7
_last_signal_key: str | None = None
_last_signal_value: str | None = None
_last_signal_reason: str = ""
_last_signal_confidence: float = 0.0
_last_signal_payload: dict | None = None
_same_signal_count = 0
# получить текущее состояние автоторговли
@@ -106,15 +116,14 @@ class AutoTradeService:
def set_symbol(self, symbol: str) -> AutoTradeState:
state = self.get_state()
state.symbol = symbol
self._reset_signal_tracking()
return state
# установить стратегию
def set_strategy(self, strategy: str) -> AutoTradeState:
state = self.get_state()
state.strategy = strategy.strip().upper()
self._last_signal_key = None
self._last_signal_value = None
self._same_signal_count = 0
self._reset_signal_tracking()
return state
# установить риск
@@ -122,6 +131,30 @@ class AutoTradeService:
state = self.get_state()
state.risk_percent = risk_percent
return state
# установить плечо
def set_leverage(self, leverage: float) -> AutoTradeState:
state = self.get_state()
state.leverage = leverage
return state
# сбросить внутренний трекинг сигналов
def _reset_signal_tracking(self) -> None:
self._last_signal_key = None
self._last_signal_value = None
self._last_signal_reason = ""
self._last_signal_confidence = 0.0
self._last_signal_payload = None
self._same_signal_count = 0
state = self.get_state()
state.last_signal_repeat_count = 0
state.last_signal_confidence = 0.0
state.last_signal_reason = None
state.decision_status = "WAITING"
state.decision_reason = None
state.is_signal_confirmed = False
state.is_signal_ready = False
# собрать контекст для стратегии
def _build_strategy_context(self) -> StrategyContext:
@@ -138,6 +171,46 @@ class AutoTradeService:
state = self.get_state()
return StrategyRegistry.get(state.strategy)
# обновить статус решения по текущему сигналу
def _update_decision_state(
self,
*,
state: AutoTradeState,
signal: str,
confidence: float,
) -> None:
state.is_signal_confirmed = False
state.is_signal_ready = False
if signal == "HOLD":
state.decision_status = "WAITING"
state.decision_reason = "Нет торгового направления."
return
if self._same_signal_count < self._confirm_repeats:
state.decision_status = "CONFIRMING"
state.decision_reason = (
f"Сигнал {signal} подтверждается: "
f"{self._same_signal_count}/{self._confirm_repeats} повторов."
)
return
state.is_signal_confirmed = True
if confidence < self._ready_confidence:
state.decision_status = "BLOCKED"
state.decision_reason = (
f"Сигнал {signal} подтверждён, но уверенность низкая: "
f"{confidence:.2f} < {self._ready_confidence:.2f}."
)
return
state.is_signal_ready = True
state.decision_status = "READY"
state.decision_reason = (
f"Сигнал {signal} подтверждён и готов к будущему execution."
)
# записать новый сигнал и итог предыдущей серии при смене сигнала
def _log_signal_if_changed(
self,
@@ -156,6 +229,12 @@ class AutoTradeService:
if is_same_signal:
self._same_signal_count += 1
self._update_signal_state_fields(
state=state,
signal=signal,
reason=reason,
confidence=confidence,
)
return
if previous_signal is not None:
@@ -166,6 +245,9 @@ class AutoTradeService:
previous_signal=previous_signal,
previous_count=previous_count,
next_signal=signal,
reason=self._last_signal_reason,
confidence=self._last_signal_confidence,
payload=self._last_signal_payload,
)
else:
self._log_signal_event(
@@ -173,7 +255,7 @@ class AutoTradeService:
state=state,
signal=previous_signal,
reason=f"{previous_signal} завершился без серии.",
confidence=0.0,
confidence=self._last_signal_confidence,
payload={
"previous_signal": previous_signal,
"next_signal": signal,
@@ -182,13 +264,39 @@ class AutoTradeService:
self._last_signal_key = signal_key
self._last_signal_value = signal
self._last_signal_reason = reason
self._last_signal_confidence = confidence
self._last_signal_payload = payload
self._same_signal_count = 1
# Новый сигнал не пишем сразу.
# Он попадёт в журнал при следующей смене сигнала:
# либо как одиночный сигнал, либо как серия.
# записать сам сигнал в журнал
self._update_signal_state_fields(
state=state,
signal=signal,
reason=reason,
confidence=confidence,
)
# обновить поля state для экрана автоторговли
def _update_signal_state_fields(
self,
*,
state: AutoTradeState,
signal: str,
reason: str,
confidence: float,
) -> None:
state.last_signal = signal
state.last_signal_repeat_count = self._same_signal_count
state.last_signal_confidence = confidence
state.last_signal_reason = reason
self._update_decision_state(
state=state,
signal=signal,
confidence=confidence,
)
# записать одиночный сигнал в журнал
def _log_signal_event(
self,
*,
@@ -220,7 +328,7 @@ class AutoTradeService:
"confidence": confidence,
"reason": reason,
"repeat_count": 1,
"is_strong_signal": confidence > 0.7,
"is_strong_signal": confidence > self._ready_confidence,
"is_aggregated": False,
"payload": payload or {},
},
@@ -237,6 +345,9 @@ class AutoTradeService:
previous_signal: str,
previous_count: int,
next_signal: str,
reason: str,
confidence: float,
payload: dict | None,
) -> None:
emoji_map = {
"BUY": "🟢",
@@ -261,7 +372,11 @@ class AutoTradeService:
"signal": previous_signal,
"next_signal": next_signal,
"repeat_count": previous_count,
"confidence": confidence,
"reason": reason,
"is_strong_signal": confidence > self._ready_confidence,
"is_aggregated": True,
"payload": payload or {},
},
)
except Exception:
@@ -279,7 +394,6 @@ class AutoTradeService:
result = strategy.analyze(context)
state.last_check_at = datetime.now().strftime("%H:%M:%S")
state.last_signal = result.signal.value
self._log_signal_if_changed(
strategy_name=strategy.name,
@@ -290,8 +404,4 @@ class AutoTradeService:
payload=result.payload,
)
state.last_signal_repeat_count = self._same_signal_count
state.last_signal_confidence = result.confidence
state.last_signal_reason = result.reason
return state

View File

@@ -25,7 +25,7 @@ class AutoTradeState:
# время последней проверки
last_check_at: str | None = None
# последний сигнал стратегии
# последний сырой сигнал стратегии
last_signal: str | None = None
# количество одинаковых сигналов подряд
@@ -35,4 +35,34 @@ class AutoTradeState:
last_signal_confidence: float = 0.0
# причина последнего сигнала
last_signal_reason: str | None = None
last_signal_reason: str | None = None
# статус торгового решения: WAITING / CONFIRMING / READY / BLOCKED
decision_status: str = "WAITING"
# человекочитаемое объяснение решения
decision_reason: str | None = None
# сигнал подтверждён по количеству повторов
is_signal_confirmed: bool = False
# сигнал готов к будущему execution
is_signal_ready: bool = False
# текущая позиция: NONE / LONG / SHORT
position_side: str = "NONE"
# цена входа
entry_price: float | None = None
# размер позиции
position_size: float | None = None
# нереализованный PnL
unrealized_pnl_usd: float | None = None
# максимальная просадка
max_drawdown_usd: float | None = None
# плечо
leverage: float | None = 2.0

View File

@@ -88,6 +88,7 @@
✔ стратегия
✔ риск
✔ символ
✔ leverage (default x2)
## 07.3 — Analysis Cycle
✔ run_cycle()
@@ -122,8 +123,23 @@
### 07.4.2
✔ Strategy Registry
### 07.4.3
⏳ Trend Strategy
### 07.4.3 — Trend Strategy
✔ signal generation
✔ repeat confirmation logic
✔ confidence scoring
✔ UI integration
### 07.4.3.1 — UI Optimization
✔ compact auto screen
✔ state-based rendering (OFF / RUNNING / OBSERVING)
✔ minimal trading layout
✔ duplicate info removal
### 07.4.3.2 — Engine Decoupling (NEXT)
⏳ split analysis / UI refresh
⏳ fast price polling (1s)
⏳ slow UI updates (event-driven / 60s)
⏳ anti-flood protection
### 07.4.4
⏳ Grid Strategy
@@ -166,5 +182,5 @@
## Текущий статус проекта
👉 Завершён: 07.4.1
👉 Следующий шаг: 07.4.2 Strategy Registry
👉 Завершён: 07.4.3.1
👉 Следующий шаг: 07.4.3.2 — Engine Decoupling + Price Polling

View File

@@ -16,7 +16,7 @@
✔ стратегия
✔ риск
✔ символ
presets UI
leverage (default x2)
---
@@ -86,8 +86,38 @@
### 07.4.2
✔ registry стратегий
### 07.4.3
⏳ Trend strategy
---
### 07.4.3 — Trend Strategy
✔ генерация сигналов
✔ repeat tracking
✔ confidence logic
✔ decision state (WAITING / CONFIRMING / READY / BLOCKED)
---
### 07.4.3.1 — UI Optimization
✔ компактный экран автоторговли
✔ разделение OFF / ACTIVE / OBSERVING
✔ убраны дубли (WAITING / HOLD и т.д.)
✔ оптимизация под mobile
---
### 07.4.3.2 — Engine Decoupling (NEXT)
⏳ разделение:
- analysis loop (частый)
- UI loop (редкий)
⏳ price polling:
- быстрый (1s)
- независимый от UI
⏳ Telegram:
- обновление только при изменении состояния
- защита от flood control
---
### 07.4.4
⏳ Grid strategy
@@ -99,5 +129,5 @@
## Текущий статус
👉 Завершён: 07.4.1
👉 Следующий шаг: 07.4.2
👉 Завершён: 07.4.3.1
👉 Следующий шаг: 07.4.3.2 — Decoupling + Price Polling

View File

@@ -0,0 +1,289 @@
# 07.4.3.1 — Trend Strategy Stabilization
## Цель этапа
Стабилизировать сигналы автоторговли перед будущим execution-слоем.
На этапе 07.4.3 бот уже начал генерировать реальные BUY / SELL / HOLD сигналы. На этапе 07.4.3.1 добавлена логика, которая отделяет сырой сигнал стратегии от торгового решения.
Главная цель:
- не реагировать на одиночные шумовые сигналы;
- подтверждать BUY / SELL через серию повторов;
- учитывать confidence;
- показывать пользователю не только сигнал, но и статус решения;
- подготовить систему к безопасному execution.
---
## Что реализовано
### 1. Decision state
В состояние автоторговли добавлены поля торгового решения:
- decision_status;
- decision_reason;
- is_signal_confirmed;
- is_signal_ready.
Статусы решения:
- WAITING — нет торгового направления;
- CONFIRMING — сигнал есть, но ещё подтверждается;
- READY — сигнал подтверждён и готов к будущему execution;
- BLOCKED — сигнал подтверждён, но заблокирован условиями фильтра.
---
### 2. Подтверждение сигнала через повторы
BUY / SELL больше не считаются готовыми сразу после первого появления.
Текущая логика:
- BUY / SELL 1 раз → CONFIRMING;
- BUY / SELL 2 раза → CONFIRMING;
- BUY / SELL 3 раза → потенциально READY;
- HOLD → WAITING.
Текущий порог подтверждения:
- 3 одинаковых сигнала подряд.
---
### 3. Фильтр confidence
Даже подтверждённый сигнал не становится READY, если confidence ниже порога.
Текущий порог:
- confidence >= 0.70.
Если сигнал повторился нужное количество раз, но confidence ниже 0.70, решение получает статус BLOCKED.
---
### 4. Разделение raw signal и decision
Сигнал стратегии и торговое решение теперь разделены.
Пример:
- raw signal: BUY;
- repeat count: 2;
- confidence: 0.85;
- decision: CONFIRMING.
Это важно, потому что стратегия может видеть направление, но execution ещё не должен открывать сделку.
---
### 5. Улучшение экрана автоторговли
Экран автоторговли был подготовлен под реальную торговлю и стал компактнее.
Согласованный формат для выключенной автоторговли без полной настройки:
```text
🤖 Автоторговля
🔸 DEMO аккаунт
⚪ Выключена
⚠️ Не настроена
Настрой параметры
```
Согласованный формат для выключенной автоторговли с настроенными параметрами:
```text
🤖 Автоторговля
🔸 DEMO аккаунт
⚪ Выключена
BTC / USD · TREND · x2
Risk: 0.5%
```
Согласованный формат для включенной автоторговли:
```text
🤖 Автоторговля
🔸 DEMO аккаунт
🟢 Активна
BTC / USD · TREND · x2
🟡 HOLD ×12 · WAITING
Pos: — | PnL: —
Risk: 0.5%
```
---
### 6. Форматирование инструмента для UI
Биржа возвращает инструмент в формате:
```text
BTC/USD_LEVERAGE
```
В UI отображается человекочитаемый формат:
```text
BTC / USD
```
---
### 7. Плечо в настройках автоторговли
Добавлено поле leverage в состояние автоторговли.
Значение по умолчанию:
- x2.
Плечо вынесено в настройки автоторговли.
Доступные варианты:
- x1;
- x2;
- x3;
- x5;
- x10;
- x20.
В UI плечо отображается компактно:
```text
x2
x5
x20
```
---
### 8. Подготовка execution-полей
В AutoTradeState добавлены поля под будущий execution:
- position_side;
- entry_price;
- position_size;
- unrealized_pnl_usd;
- max_drawdown_usd;
- leverage.
Пока execution не подключён, эти поля отображаются как заглушки.
---
### 9. Защита от лишних UI-обновлений
Выявлена проблема Telegram flood control при частых edit_message_text.
Причина:
- анализ и обновление Telegram UI пока работают слишком близко друг к другу;
- live-обновления могут слишком часто редактировать одно и то же сообщение.
Зафиксировано решение на следующий этап:
- развязать частоту анализа и частоту Telegram UI;
- обновлять UI редко или при важном изменении;
- вынести быстрый price polling в отдельный слой.
---
## Изменённые файлы
```text
app/src/trading/auto/state.py
app/src/trading/auto/service.py
app/src/telegram/handlers/auto.py
app/src/telegram/handlers/system.py
app/src/trading/auto/runner.py
```
---
## Поведение после этапа
### OFF, не настроено
Пользователь видит, что автоторговля выключена и параметры ещё не заданы.
### OFF, настроено
Пользователь видит текущий торговый контекст:
- инструмент;
- стратегию;
- плечо;
- риск.
### OBSERVING
Бот анализирует рынок, считает повторы сигналов, confidence и decision_status, но не открывает сделки.
### RUNNING
Бот анализирует рынок и показывает готовность сигнала к будущему execution. Реальное исполнение сделок пока не подключено.
---
## Ограничения этапа
Пока не реализовано:
- отдельный быстрый price polling;
- отдельная частота анализа;
- отдельная частота Telegram UI;
- execution;
- реальные позиции;
- реальные заявки;
- риск-менеджмент исполнения.
---
## Следующий этап
### 07.4.3.2 — Analysis/UI Decoupling + Fast Price Polling
План следующего этапа:
- разделить частоту анализа и частоту Telegram UI;
- получать цену чаще, например раз в 1 секунду;
- запускать стратегию отдельно от UI;
- обновлять Telegram не чаще заданного интервала;
- делать force update только при важных изменениях:
- смена signal;
- смена decision_status;
- CONFIRMING → READY;
- READY → BLOCKED;
- RUNNING → OFF;
- ошибка / восстановление.
---
## Итог
На этапе 07.4.3.1 автоторговля получила слой стабилизации сигналов.
Система теперь различает:
- сырой сигнал стратегии;
- подтверждённый сигнал;
- готовность к будущему исполнению;
- заблокированный сигнал;
- отсутствие торгового направления.
Это ключевой шаг перед подключением execution.