From cd5f9823e3fe4147c854ed114677954ade976120 Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 4 May 2026 14:47:50 +0300 Subject: [PATCH] =?UTF-8?q?Stage=2007.4.3.7=20=E2=80=94=20Alert=20Priority?= =?UTF-8?q?=20&=20UX=20Improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/telegram/handlers/debug.py | 92 +++++++++++++++++++++---- app/src/trading/auto/runner.py | 107 +++++++++++++++++++++++++---- 2 files changed, 174 insertions(+), 25 deletions(-) diff --git a/app/src/telegram/handlers/debug.py b/app/src/telegram/handlers/debug.py index ff7a1ae..a4863b8 100644 --- a/app/src/telegram/handlers/debug.py +++ b/app/src/telegram/handlers/debug.py @@ -8,8 +8,8 @@ from aiogram.types import Message from src.core.config import load_settings from src.trading.auto.runner import AutoTradeRunner from src.trading.auto.service import AutoTradeService -from src.trading.journal.service import JournalService from src.trading.execution.engine import ExecutionEngine +from src.trading.journal.service import JournalService router = Router(name="debug") @@ -19,21 +19,81 @@ def _debug_enabled() -> bool: return load_settings().debug_enabled +def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]: + parts = (raw_text or "").split() + + signal = parts[1].upper() if len(parts) > 1 else "BUY" + if signal not in {"BUY", "SELL", "HOLD"}: + return "BUY", 0.9, 2, "SIGNAL должен быть BUY, SELL или HOLD." + + try: + confidence = float(parts[2]) if len(parts) > 2 else 0.9 + except ValueError: + return "BUY", 0.9, 2, "CONFIDENCE должен быть числом от 0.00 до 1.00." + + if confidence < 0 or confidence > 1: + return "BUY", 0.9, 2, "CONFIDENCE должен быть от 0.00 до 1.00." + + try: + repeat_count = int(parts[3]) if len(parts) > 3 else 2 + except ValueError: + return "BUY", 0.9, 2, "REPEATS должен быть целым числом." + + if repeat_count < 1: + return "BUY", 0.9, 2, "REPEATS должен быть больше или равен 1." + + return signal, confidence, repeat_count, None + + +def _debug_help_text() -> str: + return ( + "🧪 Debug commands\n\n" + "Основная команда:\n" + "/debug_signal BUY 0.95 3\n" + "/debug_signal SELL 0.70 2\n" + "/debug_signal HOLD 0.00 1\n\n" + "Быстрые команды:\n" + "/debug_signal — BUY 0.90 2\n" + "/debug_ready — READY BUY\n" + "/debug_state — текущее состояние\n" + "/debug_help — список команд\n\n" + "Priority тест:\n" + "HIGH: confidence >= 0.80 и repeats >= 3\n" + "MEDIUM: confidence >= 0.60 или repeats >= 2\n" + "LOW: всё остальное" + ) + + +@router.message(F.text == "/debug_help") +async def debug_help(message: Message) -> None: + if not _debug_enabled(): + await message.answer("Debug mode выключен.") + return + + await message.answer(_debug_help_text()) + + @router.message(F.text.startswith("/debug_signal")) async def debug_signal(message: Message) -> None: if not _debug_enabled(): await message.answer("Debug mode выключен.") return - parts = (message.text or "").split() - signal = parts[1].upper() if len(parts) > 1 else "BUY" + signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text) + + if error is not None: + await message.answer( + f"⛔️ {error}\n\n" + f"{_debug_help_text()}" + ) + return service = AutoTradeService() state = service.debug_force_signal( signal=signal, - confidence=0.9, - repeat_count=2, - reason=f"DEBUG FORCE {signal}", + confidence=confidence, + repeat_count=repeat_count, + reason=f"DEBUG FORCE {signal} {confidence:.2f} ×{repeat_count}", ) if state.status == "OFF": @@ -41,7 +101,7 @@ async def debug_signal(message: Message) -> None: await AutoTradeRunner._handle_important_event(state) - ExecutionEngine().process(state) + execution_result = ExecutionEngine().process(state) AutoTradeRunner.start() @@ -57,6 +117,9 @@ async def debug_signal(message: Message) -> None: "decision_status": state.decision_status, "confidence": state.last_signal_confidence, "repeat_count": state.last_signal_repeat_count, + "execution_action": execution_result.action, + "execution_can_execute": execution_result.can_execute, + "execution_reason": execution_result.reason, }, ) @@ -65,7 +128,10 @@ async def debug_signal(message: Message) -> None: f"Signal: {state.last_signal}\n" f"Decision: {state.decision_status}\n" f"Confidence: {state.last_signal_confidence:.2f}\n" - f"Repeats: {state.last_signal_repeat_count}" + f"Repeats: {state.last_signal_repeat_count}\n\n" + f"Execution: {execution_result.action}\n" + f"Can execute: {execution_result.can_execute}\n" + f"Reason: {execution_result.reason}" ) @@ -79,8 +145,8 @@ async def debug_ready(message: Message) -> None: state = service.debug_force_signal( signal="BUY", confidence=0.95, - repeat_count=2, - reason="DEBUG READY BUY", + repeat_count=3, + reason="DEBUG READY BUY 0.95 ×3", ) if state.status == "OFF": @@ -88,14 +154,16 @@ async def debug_ready(message: Message) -> None: await AutoTradeRunner._handle_important_event(state) - ExecutionEngine().process(state) + execution_result = ExecutionEngine().process(state) AutoTradeRunner.start() await message.answer( "✅ Debug READY создан\n\n" f"Signal: {state.last_signal}\n" - f"Decision: {state.decision_status}" + f"Decision: {state.decision_status}\n" + f"Execution: {execution_result.action}\n" + f"Can execute: {execution_result.can_execute}" ) diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py index 6b559eb..37769b2 100644 --- a/app/src/trading/auto/runner.py +++ b/app/src/trading/auto/runner.py @@ -185,6 +185,11 @@ class AutoTradeRunner: leverage = payload.get("leverage") if payload.get("leverage") is not None else state.leverage reason = str(payload.get("reason") or state.last_signal_reason or "—") + priority = cls._alert_priority( + confidence=confidence, + repeat_count=repeat_count, + ) + alert_key = ( f"{symbol}:{strategy}:{signal}:" f"{repeat_count}:{confidence:.2f}:" @@ -213,19 +218,15 @@ class AutoTradeRunner: cls._last_strong_alert_key = alert_key cls._last_strong_alert_at_by_key[alert_key] = now - signal_icon = { - "BUY": "🟢", - "SELL": "🔴", - }.get(signal, "🚨") - - leverage_text = f"x{leverage:g}" if isinstance(leverage, (int, float)) else "—" - - text = ( - f"🚨 Сильный сигнал {signal_icon} {signal}\n\n" - f"{symbol} · {strategy} · {leverage_text}\n" - f"Confidence: {confidence:.2f}\n" - f"Repeats: {repeat_count}\n\n" - f"Причина: {reason}" + text = cls._build_strong_signal_alert_text( + signal=signal, + symbol=symbol, + strategy=strategy, + repeat_count=repeat_count, + confidence=confidence, + leverage=leverage, + reason=reason, + priority=priority, ) try: @@ -247,6 +248,7 @@ class AutoTradeRunner: "confidence": confidence, "leverage": leverage, "reason": reason, + "priority": priority, }, ) @@ -289,6 +291,85 @@ class AutoTradeRunner: except Exception: pass + @classmethod + def _alert_priority( + cls, + *, + confidence: float, + repeat_count: int, + ) -> str: + if confidence >= 0.8 and repeat_count >= 3: + return "HIGH" + + if confidence >= 0.6 or repeat_count >= 2: + return "MEDIUM" + + return "LOW" + + @classmethod + def _priority_label(cls, priority: str) -> str: + mapping = { + "HIGH": "🚨 HIGH", + "MEDIUM": "⚡ MEDIUM", + "LOW": "ℹ️ LOW", + } + return mapping.get(priority, priority) + + @classmethod + def _format_alert_symbol(cls, symbol: str) -> str: + if not symbol or 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 + + @classmethod + def _format_alert_leverage(cls, leverage: object) -> str: + if isinstance(leverage, (int, float)): + return f"x{leverage:g}" + + return "—" + + @classmethod + def _signal_icon(cls, signal: str) -> str: + mapping = { + "BUY": "🟢", + "SELL": "🔴", + } + return mapping.get(signal, "⚪") + + @classmethod + def _build_strong_signal_alert_text( + cls, + *, + signal: str, + symbol: str, + strategy: str, + repeat_count: int, + confidence: float, + leverage: object, + reason: str, + priority: str, + ) -> str: + icon = cls._signal_icon(signal) + symbol_text = cls._format_alert_symbol(symbol) + leverage_text = cls._format_alert_leverage(leverage) + priority_text = cls._priority_label(priority) + + return ( + f"{priority_text} · {icon} {signal}\n\n" + f"{symbol_text} · {strategy} · {leverage_text}\n\n" + f"🧠 Confidence: {confidence:.2f}\n" + f"🔁 Repeats: {repeat_count}\n\n" + f"💡 Причина:\n" + f"{reason}" + ) + @classmethod async def _refresh_screen(cls, *, force: bool = False) -> None: now = time.monotonic()