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 "", "") 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: def _auto_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
@@ -64,21 +159,46 @@ def _build_auto_text() -> str:
service = AutoTradeService() service = AutoTradeService()
state = service.get_state() 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 "" 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 ( return (
"<b>🤖 Автоторговля</b>\n" f"{header}"
f"{mode_line()}" f"{status_line}\n\n"
f"Статус: {_status_label(state.status)}\n" f"{_context_line(state)}\n\n"
f"Стратегия: {strategy}\n" f"{_signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
f"Инструмент: {state.symbol}\n" f"· {state.decision_status}\n\n"
f"Риск: {risk}\n" f"Pos: {_value_or_dash(state.position_side)} | "
f"PnL: {state.pnl_usd:.2f} USD\n" f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}\n"
f"Последний анализ: {state.last_check_at or ''}\n" f"Risk: {risk}"
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 ''}"
) )

View File

@@ -168,10 +168,16 @@ async def open_system_management(callback: CallbackQuery) -> None:
@router.callback_query(F.data == "settings:auto") @router.callback_query(F.data == "settings:auto")
async def open_auto_settings(callback: CallbackQuery) -> None: async def open_auto_settings(callback: CallbackQuery) -> None:
AutoTradeRunner.set_current_screen("settings_auto") AutoTradeRunner.set_current_screen("settings_auto")
if callback.message is None: if callback.message is None:
await callback.answer("Сообщение не найдено", show_alert=True) await callback.answer("Сообщение не найдено", show_alert=True)
return return
AutoTradeRunner.unregister_screen(
chat_id=callback.message.chat.id,
message_id=callback.message.message_id,
)
state = AutoTradeService().get_state() state = AutoTradeService().get_state()
strategy_map = { strategy_map = {
@@ -181,13 +187,15 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
} }
strategy = strategy_map.get(state.strategy or "", "") strategy = strategy_map.get(state.strategy or "", "")
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "" 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 = ( text = (
"<b>🤖 Автоторговля</b>\n\n" "<b>🤖 Автоторговля</b>\n\n"
"<b>СИСТЕМА</b> · Настройки\n\n" "<b>СИСТЕМА</b> · Настройки\n\n"
f"Стратегия: {strategy}\n" f"Стратегия: {strategy}\n"
f"Инструмент: {state.symbol}\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_strategy")
builder.button(text="📈 Инструмент", callback_data="settings:auto_symbol") builder.button(text="📈 Инструмент", callback_data="settings:auto_symbol")
builder.button(text="🛡️ Риск", callback_data="settings:auto_risk") 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="system:management")
builder.button(text="🤖 Автоторговля", callback_data="auto:home") 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.message.edit_text(text, reply_markup=builder.as_markup())
await callback.answer() await callback.answer()
@@ -313,6 +322,46 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
await callback.answer("Риск обновлён") 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") @router.callback_query(F.data == "settings:trade")
async def open_trade_settings(callback: CallbackQuery) -> None: async def open_trade_settings(callback: CallbackQuery) -> None:
if callback.message is None: if callback.message is None:

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ class AutoTradeState:
# время последней проверки # время последней проверки
last_check_at: str | None = None last_check_at: str | None = None
# последний сигнал стратегии # последний сырой сигнал стратегии
last_signal: str | None = None last_signal: str | None = None
# количество одинаковых сигналов подряд # количество одинаковых сигналов подряд
@@ -35,4 +35,34 @@ class AutoTradeState:
last_signal_confidence: float = 0.0 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 ## 07.3 — Analysis Cycle
✔ run_cycle() ✔ run_cycle()
@@ -122,8 +123,23 @@
### 07.4.2 ### 07.4.2
✔ Strategy Registry ✔ Strategy Registry
### 07.4.3 ### 07.4.3 — Trend Strategy
⏳ 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 ### 07.4.4
⏳ Grid Strategy ⏳ Grid Strategy
@@ -166,5 +182,5 @@
## Текущий статус проекта ## Текущий статус проекта
👉 Завершён: 07.4.1 👉 Завершён: 07.4.3.1
👉 Следующий шаг: 07.4.2 Strategy Registry 👉 Следующий шаг: 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 ### 07.4.2
✔ registry стратегий ✔ 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 ### 07.4.4
⏳ Grid strategy ⏳ Grid strategy
@@ -99,5 +129,5 @@
## Текущий статус ## Текущий статус
👉 Завершён: 07.4.1 👉 Завершён: 07.4.3.1
👉 Следующий шаг: 07.4.2 👉 Следующий шаг: 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.