Stage 07.4.3.1 — trend strategy stabilization
This commit is contained in:
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
289
docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md
Normal file
289
docs/stages/stage-07_4_3_1-trend-strategy-stabilization.md
Normal 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.
|
||||
Reference in New Issue
Block a user