Stage 07.4.3.7 — Alert Priority & UX Improvements
This commit is contained in:
@@ -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,9 @@ 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)
|
||||||
|
|
||||||
|
await AutoTradeRunner.process_last_event_now()
|
||||||
|
|
||||||
AutoTradeRunner.start()
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
@@ -57,6 +119,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 +130,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 +147,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 +156,18 @@ 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)
|
||||||
|
|
||||||
|
await AutoTradeRunner.process_last_event_now()
|
||||||
|
|
||||||
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}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,22 +24,20 @@ class AutoTradeRunner:
|
|||||||
_render_markup: Callable[[], object] | None = None
|
_render_markup: Callable[[], object] | None = None
|
||||||
_current_screen: str | None = None
|
_current_screen: str | None = None
|
||||||
|
|
||||||
# анализ стратегии — часто
|
|
||||||
_analysis_interval_seconds = 5
|
_analysis_interval_seconds = 5
|
||||||
|
|
||||||
# Telegram UI — редко
|
|
||||||
_ui_interval_seconds = 60
|
_ui_interval_seconds = 60
|
||||||
|
|
||||||
_last_text: str | None = None
|
_last_text: str | None = None
|
||||||
_last_ui_refresh_at: float = 0.0
|
_last_ui_refresh_at: float = 0.0
|
||||||
_last_event_version: int = 0
|
_last_event_version: int = 0
|
||||||
_retry_after_until: float = 0.0
|
_retry_after_until: float = 0.0
|
||||||
_last_strong_alert_key: str | None = None
|
|
||||||
|
|
||||||
# защита от частых одинаковых alert-ов
|
_last_strong_alert_key: str | None = None
|
||||||
_strong_alert_cooldown_seconds = 120
|
_strong_alert_cooldown_seconds = 120
|
||||||
_last_strong_alert_at_by_key: dict[str, float] = {}
|
_last_strong_alert_at_by_key: dict[str, float] = {}
|
||||||
|
|
||||||
|
_last_execution_alert_key: str | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_screen(
|
def register_screen(
|
||||||
cls,
|
cls,
|
||||||
@@ -160,18 +158,25 @@ class AutoTradeRunner:
|
|||||||
async def _handle_important_event(cls, state) -> None:
|
async def _handle_important_event(cls, state) -> None:
|
||||||
event_type, payload = EventBus.last_event()
|
event_type, payload = EventBus.last_event()
|
||||||
|
|
||||||
if event_type != "auto_decision_changed":
|
if event_type == "auto_decision_changed":
|
||||||
|
if payload.get("decision_status") != "READY":
|
||||||
|
return
|
||||||
|
|
||||||
|
signal = str(payload.get("signal", "")).upper()
|
||||||
|
if signal not in {"BUY", "SELL"}:
|
||||||
|
return
|
||||||
|
|
||||||
|
await cls._send_strong_signal_alert(state=state, payload=payload)
|
||||||
return
|
return
|
||||||
|
|
||||||
if payload.get("decision_status") != "READY":
|
if event_type in {"paper_position_opened", "paper_position_closed"}:
|
||||||
|
await cls._send_execution_alert(
|
||||||
|
state=state,
|
||||||
|
event_type=event_type,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
signal = str(payload.get("signal", "")).upper()
|
|
||||||
if signal not in {"BUY", "SELL"}:
|
|
||||||
return
|
|
||||||
|
|
||||||
await cls._send_strong_signal_alert(state=state, payload=payload)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _send_strong_signal_alert(cls, *, state, payload: dict) -> None:
|
async def _send_strong_signal_alert(cls, *, state, payload: dict) -> None:
|
||||||
if cls._bot is None or cls._chat_id is None:
|
if cls._bot is None or cls._chat_id is None:
|
||||||
@@ -185,8 +190,15 @@ 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 "—")
|
||||||
|
|
||||||
|
position_context = str(getattr(state, "position_side", "NONE") or "NONE")
|
||||||
|
|
||||||
|
priority = cls._alert_priority(
|
||||||
|
confidence=confidence,
|
||||||
|
repeat_count=repeat_count,
|
||||||
|
)
|
||||||
|
|
||||||
alert_key = (
|
alert_key = (
|
||||||
f"{symbol}:{strategy}:{signal}:"
|
f"signal:{position_context}:{symbol}:{strategy}:{signal}:"
|
||||||
f"{repeat_count}:{confidence:.2f}:"
|
f"{repeat_count}:{confidence:.2f}:"
|
||||||
f"{state.decision_status}:{reason}"
|
f"{state.decision_status}:{reason}"
|
||||||
)
|
)
|
||||||
@@ -207,25 +219,23 @@ class AutoTradeRunner:
|
|||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
cooldown_left=round(cls._strong_alert_cooldown_seconds - elapsed, 2),
|
cooldown_left=round(cls._strong_alert_cooldown_seconds - elapsed, 2),
|
||||||
|
position_context=position_context,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
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"
|
position_context=position_context,
|
||||||
f"Confidence: {confidence:.2f}\n"
|
|
||||||
f"Repeats: {repeat_count}\n\n"
|
|
||||||
f"Причина: {reason}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -247,6 +257,8 @@ class AutoTradeRunner:
|
|||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
"leverage": leverage,
|
"leverage": leverage,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
|
"priority": priority,
|
||||||
|
"position_context": position_context,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -256,6 +268,117 @@ class AutoTradeRunner:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _send_execution_alert(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
state,
|
||||||
|
event_type: str,
|
||||||
|
payload: dict,
|
||||||
|
) -> None:
|
||||||
|
if cls._bot is None or cls._chat_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
alert_key = cls._execution_alert_key(
|
||||||
|
event_type=event_type,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if alert_key == cls._last_execution_alert_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
cls._last_execution_alert_key = alert_key
|
||||||
|
|
||||||
|
text = cls._build_execution_alert_text(
|
||||||
|
state=state,
|
||||||
|
event_type=event_type,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cls._bot.send_message(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
text=text,
|
||||||
|
)
|
||||||
|
|
||||||
|
JournalService().log_ui_info(
|
||||||
|
event_type="auto_execution_alert_sent",
|
||||||
|
message="Отправлено Telegram-уведомление по paper execution.",
|
||||||
|
screen="auto",
|
||||||
|
action="execution_alert",
|
||||||
|
payload={
|
||||||
|
"source_event_type": event_type,
|
||||||
|
**payload,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
except TelegramRetryAfter as exc:
|
||||||
|
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _execution_alert_key(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
event_type: str,
|
||||||
|
payload: dict,
|
||||||
|
) -> str:
|
||||||
|
return (
|
||||||
|
f"{event_type}:"
|
||||||
|
f"{payload.get('symbol')}:"
|
||||||
|
f"{payload.get('side')}:"
|
||||||
|
f"{payload.get('entry_price')}:"
|
||||||
|
f"{payload.get('exit_price')}:"
|
||||||
|
f"{payload.get('size')}:"
|
||||||
|
f"{payload.get('pnl')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_execution_alert_text(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
state,
|
||||||
|
event_type: str,
|
||||||
|
payload: dict,
|
||||||
|
) -> str:
|
||||||
|
symbol = str(payload.get("symbol") or state.symbol or "—")
|
||||||
|
side = str(payload.get("side") or state.position_side or "—")
|
||||||
|
leverage = payload.get("leverage") if payload.get("leverage") is not None else state.leverage
|
||||||
|
|
||||||
|
symbol_text = cls._format_alert_symbol(symbol)
|
||||||
|
leverage_text = cls._format_alert_leverage(leverage)
|
||||||
|
|
||||||
|
if event_type == "paper_position_opened":
|
||||||
|
entry_price = cls._format_price(payload.get("entry_price"))
|
||||||
|
size = cls._format_size(payload.get("size"))
|
||||||
|
side_icon = "🟢" if side == "LONG" else "🔴"
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<b>📄 Paper position opened {side_icon} {side}</b>\n\n"
|
||||||
|
f"{symbol_text} · {leverage_text}\n"
|
||||||
|
f"Entry: $ {entry_price}\n"
|
||||||
|
f"Size: {size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_type == "paper_position_closed":
|
||||||
|
entry_price = cls._format_price(payload.get("entry_price"))
|
||||||
|
exit_price = cls._format_price(payload.get("exit_price"))
|
||||||
|
size = cls._format_size(payload.get("size"))
|
||||||
|
pnl = cls._format_pnl(payload.get("pnl"))
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<b>✅ Paper position closed</b>\n\n"
|
||||||
|
f"{side} · {symbol_text} · {leverage_text}\n"
|
||||||
|
f"Entry: $ {entry_price}\n"
|
||||||
|
f"Exit: $ {exit_price}\n"
|
||||||
|
f"Size: {size}\n\n"
|
||||||
|
f"PnL: {pnl}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "<b>📄 Paper execution event</b>"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _log_suppressed_strong_alert(
|
def _log_suppressed_strong_alert(
|
||||||
cls,
|
cls,
|
||||||
@@ -268,6 +391,7 @@ class AutoTradeRunner:
|
|||||||
leverage: object,
|
leverage: object,
|
||||||
reason: str,
|
reason: str,
|
||||||
cooldown_left: float,
|
cooldown_left: float,
|
||||||
|
position_context: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
@@ -284,11 +408,117 @@ class AutoTradeRunner:
|
|||||||
"leverage": leverage,
|
"leverage": leverage,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"cooldown_left": cooldown_left,
|
"cooldown_left": cooldown_left,
|
||||||
|
"position_context": position_context,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
position_context: 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"
|
||||||
|
f"Position: {position_context}\n\n"
|
||||||
|
f"🧠 Confidence: {confidence:.2f}\n"
|
||||||
|
f"🔁 Repeats: {repeat_count}\n\n"
|
||||||
|
f"💡 Причина:\n"
|
||||||
|
f"{reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_price(cls, value: object) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_size(cls, value: object) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.8f}".rstrip("0").rstrip(".")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_pnl(cls, value: object) -> str:
|
||||||
|
try:
|
||||||
|
pnl = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
prefix = "+" if pnl > 0 else ""
|
||||||
|
return f"{prefix}{pnl:.4f} USD"
|
||||||
|
|
||||||
@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()
|
||||||
|
|||||||
@@ -172,6 +172,14 @@
|
|||||||
- journal logging suppressed событий
|
- journal logging suppressed событий
|
||||||
- не влияет на execution pipeline
|
- не влияет на execution pipeline
|
||||||
|
|
||||||
|
#### 07.4.3.7 — Alert priority & UX improvements ✅
|
||||||
|
- priority levels: HIGH / MEDIUM / LOW
|
||||||
|
- improved Telegram alert layout
|
||||||
|
- normalized symbol & leverage formatting
|
||||||
|
- compatible with cooldown & suppression
|
||||||
|
- extended debug_signal parameters
|
||||||
|
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,6 @@
|
|||||||
✔ убраны дубли (WAITING / HOLD и т.д.)
|
✔ убраны дубли (WAITING / HOLD и т.д.)
|
||||||
✔ оптимизация под mobile
|
✔ оптимизация под mobile
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 07.4.3.2 — Engine Decoupling (NEXT)
|
#### 07.4.3.2 — Engine Decoupling (NEXT)
|
||||||
⏳ разделение:
|
⏳ разделение:
|
||||||
- analysis loop (частый)
|
- analysis loop (частый)
|
||||||
@@ -117,8 +115,6 @@
|
|||||||
- обновление только при изменении состояния
|
- обновление только при изменении состояния
|
||||||
- защита от flood control
|
- защита от flood control
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Stage 07.4.3.3 — Paper Position & Execution Engine
|
#### Stage 07.4.3.3 — Paper Position & Execution Engine
|
||||||
- добавлен ExecutionEngine
|
- добавлен ExecutionEngine
|
||||||
- реализованы paper-позиции (LONG / SHORT)
|
- реализованы paper-позиции (LONG / SHORT)
|
||||||
@@ -127,8 +123,6 @@
|
|||||||
- логирование paper execution
|
- логирование paper execution
|
||||||
- EventBus события (paper_position_opened)
|
- EventBus события (paper_position_opened)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Stage 07.4.3.4 — Telegram Strong Signal Alerts
|
#### Stage 07.4.3.4 — Telegram Strong Signal Alerts
|
||||||
- EventBus-driven уведомления
|
- EventBus-driven уведомления
|
||||||
- Фильтрация READY сигналов
|
- Фильтрация READY сигналов
|
||||||
@@ -142,8 +136,6 @@
|
|||||||
- Execution lifecycle
|
- Execution lifecycle
|
||||||
- Real trading notifications
|
- Real trading notifications
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Stage 07.4.3.5 — Debug Commands & Test Mode ✅
|
#### Stage 07.4.3.5 — Debug Commands & Test Mode ✅
|
||||||
|
|
||||||
- DEBUG_ENABLED env flag
|
- DEBUG_ENABLED env flag
|
||||||
@@ -161,6 +153,14 @@
|
|||||||
- journal logging suppressed событий
|
- journal logging suppressed событий
|
||||||
- не влияет на execution pipeline
|
- не влияет на execution pipeline
|
||||||
|
|
||||||
|
#### 07.4.3.7 — Alert priority & UX improvements ✅
|
||||||
|
|
||||||
|
- priority levels: HIGH / MEDIUM / LOW
|
||||||
|
- improved Telegram alert layout
|
||||||
|
- normalized symbol & leverage formatting
|
||||||
|
- compatible with cooldown & suppression
|
||||||
|
- extended debug_signal parameters
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
|
|||||||
265
docs/stages/stage-07_4_3_7-alert-priority-ux.md
Normal file
265
docs/stages/stage-07_4_3_7-alert-priority-ux.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# Stage 07.4.3.7 — Alert Priority & UX Improvements
|
||||||
|
|
||||||
|
## Цель этапа
|
||||||
|
|
||||||
|
Улучшить Telegram-уведомления о сильных сигналах BUY / SELL:
|
||||||
|
|
||||||
|
- сделать alert более читаемым;
|
||||||
|
- добавить визуальный приоритет сигнала;
|
||||||
|
- унифицировать формат symbol / leverage;
|
||||||
|
- сохранить совместимость с cooldown / suppression;
|
||||||
|
- не влиять на ExecutionEngine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что изменено
|
||||||
|
|
||||||
|
### 1. Новый формат Telegram alert
|
||||||
|
|
||||||
|
Было:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 Сильный сигнал 🟢 BUY
|
||||||
|
|
||||||
|
BTC/USD_LEVERAGE · TREND · x2
|
||||||
|
Confidence: 0.90
|
||||||
|
Repeats: 2
|
||||||
|
|
||||||
|
Причина: DEBUG FORCE BUY
|
||||||
|
```
|
||||||
|
|
||||||
|
Стало:
|
||||||
|
|
||||||
|
```text
|
||||||
|
⚡ MEDIUM · 🟢 BUY
|
||||||
|
|
||||||
|
BTC / USD · TREND · x2
|
||||||
|
|
||||||
|
🧠 Confidence: 0.90
|
||||||
|
🔁 Repeats: 2
|
||||||
|
|
||||||
|
💡 Причина:
|
||||||
|
DEBUG FORCE BUY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Priority layer
|
||||||
|
|
||||||
|
Добавлена логика определения приоритета:
|
||||||
|
|
||||||
|
```python
|
||||||
|
HIGH = confidence >= 0.80 and repeat_count >= 3
|
||||||
|
MEDIUM = confidence >= 0.60 or repeat_count >= 2
|
||||||
|
LOW = everything else
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority labels
|
||||||
|
|
||||||
|
```text
|
||||||
|
HIGH → 🚨 HIGH
|
||||||
|
MEDIUM → ⚡ MEDIUM
|
||||||
|
LOW → ℹ️ LOW
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Форматирование symbol
|
||||||
|
|
||||||
|
Для UI alert-ов добавлено компактное отображение:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BTC/USD_LEVERAGE → BTC / USD
|
||||||
|
```
|
||||||
|
|
||||||
|
Это делает alert визуально единым с экраном автоторговли.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Форматирование leverage
|
||||||
|
|
||||||
|
Плечо выводится компактно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2.0 → x2
|
||||||
|
5.0 → x5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Совместимость с throttling
|
||||||
|
|
||||||
|
Существующая логика cooldown / suppression сохранена:
|
||||||
|
|
||||||
|
- одинаковый alert не отправляется чаще заданного cooldown;
|
||||||
|
- suppressed alert логируется в Journal;
|
||||||
|
- priority не ломает alert key;
|
||||||
|
- debug и normal режимы используют один и тот же alert pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменённые файлы
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/auto/runner.py
|
||||||
|
app/src/telegram/handlers/debug.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
### 1. MEDIUM alert
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.90 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
⚡ MEDIUM · 🟢 BUY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. HIGH alert
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.95 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 HIGH · 🟢 BUY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. SELL alert
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal SELL 0.70 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
⚡ MEDIUM · 🔴 SELL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. LOW alert
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.40 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ℹ️ LOW · 🟢 BUY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. HOLD
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal HOLD 0.00 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
- decision = WAITING;
|
||||||
|
- alert не отправляется;
|
||||||
|
- execution не выполняется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Cooldown
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.95 3
|
||||||
|
/debug_signal BUY 0.95 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
- первый alert отправляется;
|
||||||
|
- второй alert подавляется;
|
||||||
|
- в Journal появляется `auto_strong_signal_alert_suppressed`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурный результат
|
||||||
|
|
||||||
|
Alert layer стал отдельным читаемым UX-слоем:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EventBus
|
||||||
|
↓
|
||||||
|
AutoTradeRunner
|
||||||
|
↓
|
||||||
|
Priority / Formatting / Cooldown
|
||||||
|
↓
|
||||||
|
Telegram alert
|
||||||
|
```
|
||||||
|
|
||||||
|
Execution layer не изменён:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Signal alert ≠ Execution alert
|
||||||
|
```
|
||||||
|
|
||||||
|
Это важно для будущего этапа `07.4.3.8 — Telegram execution alerts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap update
|
||||||
|
|
||||||
|
### Stage 07 roadmap
|
||||||
|
|
||||||
|
```text
|
||||||
|
07.4.3.7 — Alert Priority & UX Improvements ✅
|
||||||
|
- priority labels HIGH / MEDIUM / LOW
|
||||||
|
- improved alert layout
|
||||||
|
- normalized symbol display
|
||||||
|
- normalized leverage display
|
||||||
|
- compatible with cooldown / suppression
|
||||||
|
- parametrized debug signal testing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Master roadmap
|
||||||
|
|
||||||
|
```text
|
||||||
|
Stage 07 — Auto Trading
|
||||||
|
✔ 07.4.3.7 — Alert Priority & UX Improvements
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/telegram/handlers/debug.py
|
||||||
|
git add app/src/trading/auto/runner.py
|
||||||
|
git commit -m "Stage 07.4.3.7 — Alert priority and UX improvements"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
```text
|
||||||
|
07.4.3.8 — Telegram execution alerts
|
||||||
|
```
|
||||||
|
|
||||||
|
План:
|
||||||
|
|
||||||
|
- отдельные сообщения OPEN_LONG / OPEN_SHORT;
|
||||||
|
- отдельные сообщения CLOSE;
|
||||||
|
- отображение Entry / Exit / PnL;
|
||||||
|
- journal + Telegram consistency.
|
||||||
Reference in New Issue
Block a user