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()