Stage 07.4.3.7 — Alert Priority & UX Improvements

This commit is contained in:
2026-05-04 14:47:50 +03:00
parent 75ba87c6d1
commit cd5f9823e3
2 changed files with 174 additions and 25 deletions

View File

@@ -8,8 +8,8 @@ from aiogram.types import Message
from src.core.config import load_settings from src.core.config import load_settings
from src.trading.auto.runner import AutoTradeRunner from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
from src.trading.execution.engine import ExecutionEngine from src.trading.execution.engine import ExecutionEngine
from src.trading.journal.service import JournalService
router = Router(name="debug") router = Router(name="debug")
@@ -19,21 +19,81 @@ def _debug_enabled() -> bool:
return load_settings().debug_enabled 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 (
"<b>🧪 Debug commands</b>\n\n"
"<b>Основная команда:</b>\n"
"/debug_signal BUY 0.95 3\n"
"/debug_signal SELL 0.70 2\n"
"/debug_signal HOLD 0.00 1\n\n"
"<b>Быстрые команды:</b>\n"
"/debug_signal — BUY 0.90 2\n"
"/debug_ready — READY BUY\n"
"/debug_state — текущее состояние\n"
"/debug_help — список команд\n\n"
"<b>Priority тест:</b>\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")) @router.message(F.text.startswith("/debug_signal"))
async def debug_signal(message: Message) -> None: async def debug_signal(message: Message) -> None:
if not _debug_enabled(): if not _debug_enabled():
await message.answer("Debug mode выключен.") await message.answer("Debug mode выключен.")
return return
parts = (message.text or "").split() signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
signal = parts[1].upper() if len(parts) > 1 else "BUY"
if error is not None:
await message.answer(
f"⛔️ {error}\n\n"
f"{_debug_help_text()}"
)
return
service = AutoTradeService() service = AutoTradeService()
state = service.debug_force_signal( state = service.debug_force_signal(
signal=signal, signal=signal,
confidence=0.9, confidence=confidence,
repeat_count=2, repeat_count=repeat_count,
reason=f"DEBUG FORCE {signal}", reason=f"DEBUG FORCE {signal} {confidence:.2f} ×{repeat_count}",
) )
if state.status == "OFF": if state.status == "OFF":
@@ -41,7 +101,7 @@ async def debug_signal(message: Message) -> None:
await AutoTradeRunner._handle_important_event(state) await AutoTradeRunner._handle_important_event(state)
ExecutionEngine().process(state) execution_result = ExecutionEngine().process(state)
AutoTradeRunner.start() AutoTradeRunner.start()
@@ -57,6 +117,9 @@ async def debug_signal(message: Message) -> None:
"decision_status": state.decision_status, "decision_status": state.decision_status,
"confidence": state.last_signal_confidence, "confidence": state.last_signal_confidence,
"repeat_count": state.last_signal_repeat_count, "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"Signal: {state.last_signal}\n"
f"Decision: {state.decision_status}\n" f"Decision: {state.decision_status}\n"
f"Confidence: {state.last_signal_confidence:.2f}\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( state = service.debug_force_signal(
signal="BUY", signal="BUY",
confidence=0.95, confidence=0.95,
repeat_count=2, repeat_count=3,
reason="DEBUG READY BUY", reason="DEBUG READY BUY 0.95 ×3",
) )
if state.status == "OFF": if state.status == "OFF":
@@ -88,14 +154,16 @@ async def debug_ready(message: Message) -> None:
await AutoTradeRunner._handle_important_event(state) await AutoTradeRunner._handle_important_event(state)
ExecutionEngine().process(state) execution_result = ExecutionEngine().process(state)
AutoTradeRunner.start() AutoTradeRunner.start()
await message.answer( await message.answer(
"✅ Debug READY создан\n\n" "✅ Debug READY создан\n\n"
f"Signal: {state.last_signal}\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}"
) )

View File

@@ -185,6 +185,11 @@ class AutoTradeRunner:
leverage = payload.get("leverage") if payload.get("leverage") is not None else state.leverage 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 "") reason = str(payload.get("reason") or state.last_signal_reason or "")
priority = cls._alert_priority(
confidence=confidence,
repeat_count=repeat_count,
)
alert_key = ( alert_key = (
f"{symbol}:{strategy}:{signal}:" f"{symbol}:{strategy}:{signal}:"
f"{repeat_count}:{confidence:.2f}:" f"{repeat_count}:{confidence:.2f}:"
@@ -213,19 +218,15 @@ class AutoTradeRunner:
cls._last_strong_alert_key = alert_key cls._last_strong_alert_key = alert_key
cls._last_strong_alert_at_by_key[alert_key] = now cls._last_strong_alert_at_by_key[alert_key] = now
signal_icon = { text = cls._build_strong_signal_alert_text(
"BUY": "🟢", signal=signal,
"SELL": "🔴", symbol=symbol,
}.get(signal, "🚨") strategy=strategy,
repeat_count=repeat_count,
leverage_text = f"x{leverage:g}" if isinstance(leverage, (int, float)) else "" confidence=confidence,
leverage=leverage,
text = ( reason=reason,
f"<b>🚨 Сильный сигнал {signal_icon} {signal}</b>\n\n" priority=priority,
f"{symbol} · {strategy} · {leverage_text}\n"
f"Confidence: {confidence:.2f}\n"
f"Repeats: {repeat_count}\n\n"
f"Причина: {reason}"
) )
try: try:
@@ -247,6 +248,7 @@ class AutoTradeRunner:
"confidence": confidence, "confidence": confidence,
"leverage": leverage, "leverage": leverage,
"reason": reason, "reason": reason,
"priority": priority,
}, },
) )
@@ -289,6 +291,85 @@ class AutoTradeRunner:
except Exception: except Exception:
pass 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"<b>{priority_text} · {icon} {signal}</b>\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 @classmethod
async def _refresh_screen(cls, *, force: bool = False) -> None: async def _refresh_screen(cls, *, force: bool = False) -> None:
now = time.monotonic() now = time.monotonic()