From 38c8686a9bb9a8fce8bee1daf60e6f57e36219b1 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 1 May 2026 18:25:27 +0300 Subject: [PATCH] =?UTF-8?q?Stage=2007.4.3.1=20=E2=80=94=20trend=20strategy?= =?UTF-8?q?=20stabilization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/telegram/handlers/auto.py | 144 ++++++++- app/src/telegram/handlers/system.py | 53 +++- app/src/trading/auto/runner.py | 60 +++- app/src/trading/auto/service.py | 140 ++++++++- app/src/trading/auto/state.py | 34 ++- docs/roadmap/master-roadmap.md | 24 +- docs/roadmap/stage-07-auto-trading-roadmap.md | 40 ++- ...e-07_4_3_1-trend-strategy-stabilization.md | 289 ++++++++++++++++++ 8 files changed, 735 insertions(+), 49 deletions(-) create mode 100644 docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md 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.