diff --git a/app/src/telegram/handlers/auto.py b/app/src/telegram/handlers/auto.py
index 774f6c7..98ded26 100644
--- a/app/src/telegram/handlers/auto.py
+++ b/app/src/telegram/handlers/auto.py
@@ -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 = (
+ "🤖 Автоторговля\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 (
- "🤖 Автоторговля\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}"
)
diff --git a/app/src/telegram/handlers/system.py b/app/src/telegram/handlers/system.py
index daac544..cd7ae87 100644
--- a/app/src/telegram/handlers/system.py
+++ b/app/src/telegram/handlers/system.py
@@ -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 = (
"🤖 Автоторговля\n\n"
"СИСТЕМА · Настройки\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 = (
+ "⚙️ Плечо\n\n"
+ "СИСТЕМА · Настройки · Автоторговля\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:
diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py
index f98ef94..2efc102 100644
--- a/app/src/trading/auto/runner.py
+++ b/app/src/trading/auto/runner.py
@@ -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
\ No newline at end of file
diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py
index 8a6ca52..5461db4 100644
--- a/app/src/trading/auto/service.py
+++ b/app/src/trading/auto/service.py
@@ -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
\ No newline at end of file
diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py
index e9c52b3..ef071f0 100644
--- a/app/src/trading/auto/state.py
+++ b/app/src/trading/auto/state.py
@@ -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
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 5a6a3d8..a3caef5 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -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
\ No newline at end of file
+👉 Завершён: 07.4.3.1
+👉 Следующий шаг: 07.4.3.2 — Engine Decoupling + Price Polling
\ No newline at end of file
diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md
index 39793b4..3c78f2b 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -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
\ No newline at end of file
+👉 Завершён: 07.4.3.1
+👉 Следующий шаг: 07.4.3.2 — Decoupling + Price Polling
\ No newline at end of file
diff --git a/docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md b/docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md
new file mode 100644
index 0000000..0bfc93c
--- /dev/null
+++ b/docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md
@@ -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.