07.4.4.1.10.3 — Telegram Diagnostic Screen
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
# app/src/core/event_titles.py
|
||||
|
||||
# app/src/core/event_titles.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ def build_execution_notification(event: RuntimeEvent) -> NotificationMessage | N
|
||||
|
||||
if event.event_type == RuntimeEventType.POSITION_FLIPPED:
|
||||
return _build_position_flipped(event)
|
||||
|
||||
if event.event_type == RuntimeEventType.POSITION_FLIP_BLOCKED:
|
||||
return _build_flip_blocked(event)
|
||||
|
||||
return None
|
||||
|
||||
@@ -24,23 +27,41 @@ def _build_position_opened(event: RuntimeEvent) -> NotificationMessage:
|
||||
payload = event.payload
|
||||
|
||||
symbol = _format_symbol(payload.get("symbol"))
|
||||
side = str(payload.get("side") or "—")
|
||||
strategy = str(payload.get("strategy") or "—")
|
||||
side = str(payload.get("side") or "—").upper()
|
||||
leverage = _format_leverage(payload.get("leverage"))
|
||||
entry_price = _format_price(payload.get("entry_price"))
|
||||
size = _format_size(payload.get("size"))
|
||||
confidence = float(payload.get("confidence") or 0.0)
|
||||
priority = _alert_priority(
|
||||
confidence=confidence,
|
||||
repeat_count=int(payload.get("repeat_count") or 0),
|
||||
)
|
||||
semantic_lines = payload.get("semantic_lines") or []
|
||||
|
||||
side_icon = "🟢" if side == "LONG" else "🔴"
|
||||
|
||||
text = (
|
||||
f"<b>📄 Paper position opened {side_icon} {side}</b>\n\n"
|
||||
f"{symbol} · {leverage}\n"
|
||||
f"Entry: $ {entry_price}\n"
|
||||
f"Size: {size}"
|
||||
)
|
||||
lines = [
|
||||
"<b>🧾 Позиция открыта</b>",
|
||||
"",
|
||||
f"{side_icon} {symbol} · {strategy} · {side} {leverage}",
|
||||
f"Вход: {entry_price}",
|
||||
f"Размер: {size}",
|
||||
f"Объём: {_format_notional(entry_price=payload.get('entry_price'), size=payload.get('size'))}",
|
||||
"",
|
||||
f"{_strength_bar(priority)} Сигнал {_strength_label(priority).lower()} · {confidence:.2f}",
|
||||
]
|
||||
|
||||
if semantic_lines:
|
||||
lines.extend(
|
||||
str(line).strip().rstrip(".")
|
||||
for line in semantic_lines
|
||||
if str(line).strip()
|
||||
)
|
||||
|
||||
return NotificationMessage(
|
||||
title=event.title,
|
||||
text=text,
|
||||
text="\n".join(lines),
|
||||
priority=event.priority,
|
||||
dedupe_key=event.dedupe_key,
|
||||
)
|
||||
@@ -50,63 +71,162 @@ def _build_position_closed(event: RuntimeEvent) -> NotificationMessage:
|
||||
payload = event.payload
|
||||
|
||||
symbol = _format_symbol(payload.get("symbol"))
|
||||
side = str(payload.get("side") or "—")
|
||||
side = str(payload.get("side") or "—").upper()
|
||||
leverage = _format_leverage(payload.get("leverage"))
|
||||
|
||||
entry_price = _format_price(payload.get("entry_price"))
|
||||
exit_price = _format_price(payload.get("exit_price"))
|
||||
size = _format_size(payload.get("size"))
|
||||
pnl = _format_pnl(payload.get("pnl"))
|
||||
risk_reason = payload.get("risk_reason")
|
||||
|
||||
risk_line = f"\nRisk: {risk_reason}" if risk_reason else ""
|
||||
pnl_value = float(payload.get("pnl") or 0.0)
|
||||
pnl_text = _format_pnl_amount(pnl_value)
|
||||
|
||||
text = (
|
||||
f"<b>✅ Paper position closed</b>\n\n"
|
||||
f"{side} · {symbol} · {leverage}\n"
|
||||
f"Entry: $ {entry_price}\n"
|
||||
f"Exit: $ {exit_price}\n"
|
||||
f"Size: {size}\n\n"
|
||||
f"PnL: {pnl}"
|
||||
f"{risk_line}"
|
||||
)
|
||||
risk_reason = _human_close_reason(payload.get("risk_reason"))
|
||||
|
||||
pnl_icon = "🟢" if pnl_value >= 0 else "🔴"
|
||||
pnl_label = "Прибыль" if pnl_value >= 0 else "Убыток"
|
||||
|
||||
lines = [
|
||||
"<b>🧾 Сделка закрыта</b>",
|
||||
f"{pnl_icon} {pnl_label} · {pnl_text}",
|
||||
"",
|
||||
f"{symbol} · {side} {leverage}",
|
||||
f"Вход: $ {entry_price}",
|
||||
f"Выход: $ {exit_price}",
|
||||
f"Размер: {size}",
|
||||
]
|
||||
|
||||
if risk_reason:
|
||||
lines.extend([
|
||||
"",
|
||||
f"Закрытие по {risk_reason}",
|
||||
])
|
||||
|
||||
return NotificationMessage(
|
||||
title=event.title,
|
||||
text=text,
|
||||
text="\n".join(lines),
|
||||
priority=event.priority,
|
||||
dedupe_key=event.dedupe_key,
|
||||
)
|
||||
|
||||
|
||||
def _format_pnl_amount(value: float) -> str:
|
||||
amount = f"$ {abs(value):,.2f}".replace(",", " ").rstrip("0").rstrip(".")
|
||||
|
||||
if value > 0:
|
||||
return f"+{amount}"
|
||||
|
||||
if value < 0:
|
||||
return f"−{amount}"
|
||||
|
||||
return "$ 0"
|
||||
|
||||
|
||||
def _human_close_reason(value: object) -> str:
|
||||
mapping = {
|
||||
"STOP_LOSS": "Stop Loss",
|
||||
"TAKE_PROFIT": "Take Profit",
|
||||
"MAX_LOSS": "Max Loss",
|
||||
}
|
||||
|
||||
return mapping.get(str(value or ""), "")
|
||||
|
||||
|
||||
def _build_position_flipped(event: RuntimeEvent) -> NotificationMessage:
|
||||
payload = event.payload
|
||||
|
||||
symbol = _format_symbol(payload.get("symbol"))
|
||||
leverage = _format_leverage(payload.get("leverage"))
|
||||
strategy = str(payload.get("strategy") or "—").title()
|
||||
|
||||
old_side = str(payload.get("old_side") or "—")
|
||||
new_side = str(payload.get("new_side") or payload.get("side") or "—")
|
||||
old_side = str(payload.get("old_side") or "—").upper()
|
||||
new_side = str(payload.get("new_side") or payload.get("side") or "—").upper()
|
||||
|
||||
old_leverage = _format_leverage(
|
||||
payload.get("old_leverage")
|
||||
if payload.get("old_leverage") is not None
|
||||
else payload.get("leverage")
|
||||
)
|
||||
new_leverage = _format_leverage(payload.get("leverage"))
|
||||
|
||||
entry_price = _format_price(payload.get("entry_price"))
|
||||
exit_price = _format_price(payload.get("exit_price"))
|
||||
new_entry_price = _format_price(payload.get("new_entry_price"))
|
||||
|
||||
old_size = _format_size(payload.get("old_size"))
|
||||
new_size = _format_size(payload.get("new_size"))
|
||||
pnl = _format_pnl(payload.get("pnl"))
|
||||
|
||||
pnl_value = float(payload.get("pnl") or 0.0)
|
||||
pnl_text = _format_pnl_amount(pnl_value)
|
||||
|
||||
pnl_icon = "🟢" if pnl_value >= 0 else "🔴"
|
||||
pnl_label = "Прибыль" if pnl_value >= 0 else "Убыток"
|
||||
|
||||
old_icon = "🟢" if old_side == "LONG" else "🔴"
|
||||
new_icon = "🟢" if new_side == "LONG" else "🔴"
|
||||
|
||||
confidence = float(payload.get("confidence") or 0.0)
|
||||
repeat_count = int(payload.get("repeat_count") or 0)
|
||||
priority = _alert_priority(
|
||||
confidence=confidence,
|
||||
repeat_count=repeat_count,
|
||||
)
|
||||
semantic_lines = payload.get("semantic_lines") or []
|
||||
|
||||
lines = [
|
||||
"<b>🧾 Сделка развернута</b>",
|
||||
f"{pnl_label} {pnl_icon} {pnl_text}",
|
||||
f"{symbol} · {strategy} {old_icon} {old_side} → {new_icon} {new_side}",
|
||||
"",
|
||||
f"Закрыта {old_side} {old_leverage}",
|
||||
f"Вход: $ {entry_price}",
|
||||
f"Выход: $ {exit_price}",
|
||||
f"Размер: {old_size}",
|
||||
"",
|
||||
f"Открыта {new_side} {new_leverage}",
|
||||
f"Вход: $ {new_entry_price}",
|
||||
f"Размер: {new_size}",
|
||||
(
|
||||
"Объём: "
|
||||
f"{_format_notional(entry_price=payload.get('new_entry_price'), size=payload.get('new_size'))}"
|
||||
),
|
||||
"",
|
||||
f"{_strength_bar(priority)} Сигнал {_strength_label(priority).lower()} · {confidence:.2f}",
|
||||
]
|
||||
|
||||
if semantic_lines:
|
||||
lines.extend(
|
||||
str(line).strip().rstrip(".")
|
||||
for line in semantic_lines
|
||||
if str(line).strip()
|
||||
)
|
||||
|
||||
return NotificationMessage(
|
||||
title=event.title,
|
||||
text="\n".join(lines),
|
||||
priority=event.priority,
|
||||
dedupe_key=event.dedupe_key,
|
||||
)
|
||||
|
||||
|
||||
def _build_flip_blocked(event: RuntimeEvent) -> NotificationMessage:
|
||||
payload = event.payload
|
||||
|
||||
symbol = _format_symbol(payload.get("symbol"))
|
||||
signal = str(payload.get("signal") or "").upper()
|
||||
confidence = float(payload.get("confidence") or 0.0)
|
||||
reason = str(payload.get("reason") or "Flip заблокирован")
|
||||
position_side = str(payload.get("position_side") or "—").upper()
|
||||
|
||||
target_side = "LONG" if signal == "BUY" else "SHORT" if signal == "SELL" else "—"
|
||||
icon = "🟢" if target_side == "LONG" else "🔴" if target_side == "SHORT" else ""
|
||||
|
||||
text = (
|
||||
f"<b>🔁 Paper position flipped {old_icon} {old_side} → "
|
||||
f"{new_icon} {new_side}</b>\n\n"
|
||||
f"{symbol} · {leverage}\n\n"
|
||||
f"Old entry: $ {entry_price}\n"
|
||||
f"Exit: $ {exit_price}\n"
|
||||
f"Old size: {old_size}\n\n"
|
||||
f"New entry: $ {new_entry_price}\n"
|
||||
f"New size: {new_size}\n\n"
|
||||
f"PnL: {pnl}"
|
||||
f"<b>⚠️ Flip отменён</b>\n\n"
|
||||
f"{icon} {symbol} · {target_side}\n"
|
||||
f"Текущая позиция: {position_side}\n\n"
|
||||
f"Недостаточно условий для разворота\n"
|
||||
f"{reason}\n"
|
||||
f"Сила сигнала: {confidence:.2f}"
|
||||
)
|
||||
|
||||
return NotificationMessage(
|
||||
@@ -123,13 +243,7 @@ def _format_symbol(value: object) -> 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
|
||||
return symbol.split("_", 1)[0].split("/", 1)[0].upper()
|
||||
|
||||
|
||||
def _format_leverage(value: object) -> str:
|
||||
@@ -169,4 +283,45 @@ def _format_pnl(value: object) -> str:
|
||||
if number < 0:
|
||||
return f"🔴 −{amount}"
|
||||
|
||||
return "$ 0"
|
||||
return "$ 0"
|
||||
|
||||
|
||||
def _alert_priority(*, 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"
|
||||
|
||||
|
||||
def _strength_label(priority: str) -> str:
|
||||
mapping = {
|
||||
"HIGH": "Сильный",
|
||||
"MEDIUM": "Средний",
|
||||
"LOW": "Слабый",
|
||||
}
|
||||
return mapping.get(priority.upper(), priority)
|
||||
|
||||
|
||||
def _strength_bar(priority: str) -> str:
|
||||
mapping = {
|
||||
"HIGH": "●●●",
|
||||
"MEDIUM": "●●○",
|
||||
"LOW": "●○○",
|
||||
}
|
||||
return mapping.get(priority.upper(), "●○○")
|
||||
|
||||
|
||||
def _format_notional(
|
||||
*,
|
||||
entry_price: object,
|
||||
size: object,
|
||||
) -> str:
|
||||
try:
|
||||
value = float(entry_price) * float(size)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
return f"$ {value:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
|
||||
@@ -14,37 +14,44 @@ def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None
|
||||
payload = event.payload
|
||||
|
||||
signal = str(payload.get("signal") or "—").upper()
|
||||
symbol = str(payload.get("symbol") or "—")
|
||||
strategy = str(payload.get("strategy") or "—")
|
||||
symbol = _format_symbol(str(payload.get("symbol") or "—"))
|
||||
confidence = float(payload.get("confidence") or 0.0)
|
||||
repeat_count = int(payload.get("repeat_count") or 0)
|
||||
leverage = payload.get("leverage")
|
||||
reason = str(payload.get("reason") or "—")
|
||||
position_context = str(payload.get("position_context") or "NONE")
|
||||
position_context = str(payload.get("position_context") or "NONE").upper()
|
||||
semantic_lines = payload.get("semantic_lines") or []
|
||||
|
||||
priority = str(event.priority or _alert_priority(
|
||||
confidence=confidence,
|
||||
repeat_count=repeat_count,
|
||||
repeat_count=int(payload.get("repeat_count") or 0),
|
||||
)).upper()
|
||||
|
||||
icon = _signal_icon(signal)
|
||||
symbol_text = _format_symbol(symbol)
|
||||
leverage_text = _format_leverage(leverage)
|
||||
priority_text = _priority_label(priority)
|
||||
direction = _signal_direction(signal)
|
||||
icon = _direction_icon(direction)
|
||||
strength = _strength_label(priority)
|
||||
strength_bar = _strength_bar(priority)
|
||||
|
||||
text = (
|
||||
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}"
|
||||
)
|
||||
lines = [
|
||||
f"<b>Сигнал {icon} {symbol} · {direction}</b>",
|
||||
"",
|
||||
]
|
||||
|
||||
if position_context not in {"NONE", "—", ""} and position_context != direction:
|
||||
lines.extend([
|
||||
"⚠️ ПРОТИВ ПОЗИЦИИ",
|
||||
"",
|
||||
])
|
||||
|
||||
lines.append(f"{strength_bar} {strength} · {confidence:.2f}")
|
||||
|
||||
if semantic_lines:
|
||||
lines.extend(
|
||||
str(line).strip().rstrip(".")
|
||||
for line in semantic_lines
|
||||
if str(line).strip()
|
||||
)
|
||||
|
||||
return NotificationMessage(
|
||||
title=event.title,
|
||||
text=text,
|
||||
text="\n".join(lines),
|
||||
priority=priority.lower(),
|
||||
dedupe_key=event.dedupe_key or _dedupe_key(payload),
|
||||
)
|
||||
@@ -60,34 +67,49 @@ def _alert_priority(*, confidence: float, repeat_count: int) -> str:
|
||||
return "LOW"
|
||||
|
||||
|
||||
def _priority_label(priority: str) -> str:
|
||||
def _strength_label(priority: str) -> str:
|
||||
mapping = {
|
||||
"HIGH": "🚨 HIGH",
|
||||
"MEDIUM": "⚡ MEDIUM",
|
||||
"LOW": "ℹ️ LOW",
|
||||
"HIGH": "Сильный",
|
||||
"MEDIUM": "Средний",
|
||||
"LOW": "Слабый",
|
||||
}
|
||||
return mapping.get(priority.upper(), priority)
|
||||
|
||||
|
||||
def _signal_icon(signal: str) -> str:
|
||||
def _strength_bar(priority: str) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢",
|
||||
"SELL": "🔴",
|
||||
"HIGH": "●●●",
|
||||
"MEDIUM": "●●○",
|
||||
"LOW": "●○○",
|
||||
}
|
||||
return mapping.get(signal, "⚪")
|
||||
return mapping.get(priority.upper(), "●○○")
|
||||
|
||||
|
||||
def _signal_direction(signal: str) -> str:
|
||||
if signal == "BUY":
|
||||
return "LONG"
|
||||
|
||||
if signal == "SELL":
|
||||
return "SHORT"
|
||||
|
||||
return "—"
|
||||
|
||||
|
||||
def _direction_icon(direction: str) -> str:
|
||||
if direction == "LONG":
|
||||
return "🟢"
|
||||
|
||||
if direction == "SHORT":
|
||||
return "🔴"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def _format_symbol(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
|
||||
return symbol.split("_", 1)[0].split("/", 1)[0].upper()
|
||||
|
||||
|
||||
def _format_leverage(leverage: object) -> str:
|
||||
|
||||
@@ -11,6 +11,7 @@ class RuntimeEventType(str, Enum):
|
||||
POSITION_OPENED = "position_opened"
|
||||
POSITION_CLOSED = "position_closed"
|
||||
POSITION_FLIPPED = "position_flipped"
|
||||
POSITION_FLIP_BLOCKED = "position_flip_blocked"
|
||||
|
||||
EXECUTION_BLOCKED = "execution_blocked"
|
||||
RISK_ALERT = "risk_alert"
|
||||
|
||||
@@ -8,6 +8,7 @@ from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from src.telegram.handlers.auto.ui import (
|
||||
auto_diagnostics_keyboard,
|
||||
auto_keyboard,
|
||||
build_auto_text,
|
||||
is_auto_configured,
|
||||
@@ -16,6 +17,8 @@ from src.telegram.handlers.system import open_auto_settings
|
||||
from src.telegram.live.active_screen import ActiveScreenManager
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
from src.trading.diagnostics.formatter import SemanticDiagnosticFormatter
|
||||
from src.trading.diagnostics.snapshot import SemanticDiagnosticSnapshotBuilder
|
||||
|
||||
|
||||
router = Router(name="auto")
|
||||
@@ -88,6 +91,58 @@ async def _prepare_auto_from_callback(callback: CallbackQuery) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def build_auto_diagnostics_text() -> str:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
|
||||
snapshot = SemanticDiagnosticSnapshotBuilder().build(
|
||||
state,
|
||||
is_configured=is_auto_configured(state),
|
||||
)
|
||||
|
||||
return SemanticDiagnosticFormatter().format(snapshot)
|
||||
|
||||
|
||||
async def render_auto_diagnostics_screen(
|
||||
target_message: Message,
|
||||
) -> None:
|
||||
text = build_auto_diagnostics_text()
|
||||
|
||||
try:
|
||||
await target_message.edit_text(
|
||||
text,
|
||||
reply_markup=auto_diagnostics_keyboard(),
|
||||
)
|
||||
except TelegramBadRequest as exc:
|
||||
error_text = str(exc).lower()
|
||||
|
||||
if "message to edit not found" in error_text:
|
||||
await target_message.answer(
|
||||
text,
|
||||
reply_markup=auto_diagnostics_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
if "message is not modified" in error_text:
|
||||
return
|
||||
|
||||
raise
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=target_message.bot,
|
||||
chat_id=target_message.chat.id,
|
||||
message_id=target_message.message_id,
|
||||
render_text=build_auto_diagnostics_text,
|
||||
render_markup=auto_diagnostics_keyboard,
|
||||
)
|
||||
|
||||
ActiveScreenManager.register(
|
||||
screen="auto_diagnostics",
|
||||
message=target_message,
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
||||
async def open_auto(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
@@ -173,4 +228,21 @@ async def auto_stop(callback: CallbackQuery) -> None:
|
||||
await _prepare_auto_from_callback(callback)
|
||||
await render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
await callback.answer(message)
|
||||
await callback.answer(message)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "auto:diagnostics")
|
||||
async def open_auto_diagnostics(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="auto_diagnostics",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
|
||||
await render_auto_diagnostics_screen(callback.message)
|
||||
await callback.answer()
|
||||
@@ -15,15 +15,45 @@ from src.trading.auto.service import AutoTradeService
|
||||
|
||||
|
||||
def auto_keyboard() -> InlineKeyboardMarkup:
|
||||
state = AutoTradeService().get_state()
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
status = (state.status or "").upper()
|
||||
|
||||
if status == "OFF":
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
|
||||
elif status == "RUNNING":
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
|
||||
elif status == "OBSERVING":
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
|
||||
else:
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
|
||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
||||
builder.button(text="🧯 Защита", callback_data="auto:risk")
|
||||
builder.button(text="🔬 Диагностика", callback_data="auto:diagnostics")
|
||||
|
||||
builder.adjust(3, 2)
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def auto_diagnostics_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="🔄 Обновить", callback_data="auto:diagnostics")
|
||||
builder.button(text="⬅️ Назад", callback_data="auto:home")
|
||||
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -281,7 +311,6 @@ def _build_waiting_text(state) -> str:
|
||||
_order_header_line(state),
|
||||
f"{_price_label_for_signal(state)} · {_format_plain_or_dash(price)}",
|
||||
_estimated_size_text(state, price),
|
||||
_adaptive_size_line(state),
|
||||
_max_reserved_line(state, price),
|
||||
_effective_risk_line(state),
|
||||
]
|
||||
@@ -346,7 +375,6 @@ def _build_active_position_text(state) -> str:
|
||||
"",
|
||||
f"Размер · {_format_crypto_size(size)}",
|
||||
f"Позиция · {_format_money_compact(notional)}",
|
||||
_adaptive_size_line(state),
|
||||
f"Вход · {_format_plain_or_dash(state.entry_price)}",
|
||||
f"Цена · {_format_plain_or_dash(price_for_calc)}",
|
||||
"",
|
||||
|
||||
@@ -18,6 +18,8 @@ from src.runtime_events.publisher import RuntimeEventPublisher
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
from src.trading.journal.service import JournalService
|
||||
from src.telegram.handlers.auto.ui import build_auto_semantic_text
|
||||
from src.trading.diagnostics.formatter import SemanticDiagnosticFormatter
|
||||
from src.trading.diagnostics.snapshot import SemanticDiagnosticSnapshotBuilder
|
||||
|
||||
|
||||
class AutoTradeRunner:
|
||||
@@ -269,13 +271,15 @@ class AutoTradeRunner:
|
||||
if signal not in {"BUY", "SELL"}:
|
||||
return
|
||||
|
||||
if cls._is_position_aligned_signal(state=state, signal=signal):
|
||||
cls._log_position_aligned_signal_suppressed(
|
||||
state=state,
|
||||
payload=payload,
|
||||
signal=signal,
|
||||
)
|
||||
return
|
||||
# Если сигнал совпадает с открытой позицией, не публикуем событие,
|
||||
# чтобы не создавать избыточные уведомления
|
||||
#if cls._is_position_aligned_signal(state=state, signal=signal):
|
||||
# cls._log_position_aligned_signal_suppressed(
|
||||
# state=state,
|
||||
# payload=payload,
|
||||
# signal=signal,
|
||||
# )
|
||||
# return
|
||||
|
||||
cls._publish_strong_signal_event(state=state, payload=payload)
|
||||
return
|
||||
@@ -284,6 +288,7 @@ class AutoTradeRunner:
|
||||
"paper_position_opened",
|
||||
"paper_position_closed",
|
||||
"paper_position_flipped",
|
||||
"paper_flip_blocked",
|
||||
}:
|
||||
cls._publish_execution_event(
|
||||
state=state,
|
||||
@@ -292,6 +297,18 @@ class AutoTradeRunner:
|
||||
)
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def _notification_reason_lines(cls, state) -> list[str]:
|
||||
snapshot = SemanticDiagnosticSnapshotBuilder().build(
|
||||
state,
|
||||
is_configured=True,
|
||||
)
|
||||
|
||||
return SemanticDiagnosticFormatter().build_notification_reason_lines(
|
||||
snapshot,
|
||||
limit=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _is_position_aligned_signal(cls, *, state, signal: str) -> bool:
|
||||
position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
|
||||
@@ -390,6 +407,8 @@ class AutoTradeRunner:
|
||||
"reason": reason,
|
||||
"position_context": position_context,
|
||||
"decision_status": state.decision_status,
|
||||
"semantic_lines": cls._notification_reason_lines(state),
|
||||
"position_side": position_context,
|
||||
},
|
||||
priority=priority.lower(),
|
||||
dedupe_key=(
|
||||
@@ -423,6 +442,8 @@ class AutoTradeRunner:
|
||||
old_side = str(payload.get("old_side") or "—")
|
||||
new_side = str(payload.get("new_side") or side or "—")
|
||||
|
||||
semantic_lines = cls._notification_reason_lines(state)
|
||||
|
||||
RuntimeEventPublisher.publish(
|
||||
RuntimeEvent(
|
||||
event_type=runtime_event_type,
|
||||
@@ -436,6 +457,8 @@ class AutoTradeRunner:
|
||||
"new_side": new_side,
|
||||
"leverage": payload.get("leverage") if payload.get("leverage") is not None else state.leverage,
|
||||
**payload,
|
||||
"strategy": state.strategy,
|
||||
"semantic_lines": semantic_lines,
|
||||
},
|
||||
priority="normal",
|
||||
dedupe_key=cls._execution_dedupe_key(
|
||||
@@ -451,6 +474,7 @@ class AutoTradeRunner:
|
||||
"paper_position_opened": RuntimeEventType.POSITION_OPENED,
|
||||
"paper_position_closed": RuntimeEventType.POSITION_CLOSED,
|
||||
"paper_position_flipped": RuntimeEventType.POSITION_FLIPPED,
|
||||
"paper_flip_blocked": RuntimeEventType.POSITION_FLIP_BLOCKED,
|
||||
}
|
||||
return mapping.get(event_type)
|
||||
|
||||
@@ -460,6 +484,7 @@ class AutoTradeRunner:
|
||||
RuntimeEventType.POSITION_OPENED: "Paper position opened",
|
||||
RuntimeEventType.POSITION_CLOSED: "Paper position closed",
|
||||
RuntimeEventType.POSITION_FLIPPED: "Paper position flipped",
|
||||
RuntimeEventType.POSITION_FLIP_BLOCKED: "Flip blocked",
|
||||
}
|
||||
return mapping.get(event_type, "Paper execution event")
|
||||
|
||||
|
||||
@@ -230,6 +230,12 @@ class AutoTradeService:
|
||||
|
||||
state.status = "RUNNING"
|
||||
self._reset_signal_tracking()
|
||||
state.cycle_realized_pnl_usd = 0.0
|
||||
state.last_flip_old_side = None
|
||||
state.last_flip_new_side = None
|
||||
state.last_flip_pnl_usd = None
|
||||
state.last_flip_reason = None
|
||||
state.last_flip_monotonic_at = None
|
||||
state.last_signal = "HOLD"
|
||||
state.signal_started_at = time.monotonic()
|
||||
|
||||
@@ -261,6 +267,12 @@ class AutoTradeService:
|
||||
)
|
||||
|
||||
if previous_status == "OFF":
|
||||
state.cycle_realized_pnl_usd = 0.0
|
||||
state.last_flip_old_side = None
|
||||
state.last_flip_new_side = None
|
||||
state.last_flip_pnl_usd = None
|
||||
state.last_flip_reason = None
|
||||
state.last_flip_monotonic_at = None
|
||||
return state, "Включён режим наблюдения."
|
||||
|
||||
return state, "Автоторговля переведена в режим наблюдения."
|
||||
@@ -275,6 +287,12 @@ class AutoTradeService:
|
||||
return state, "Автоторговля уже выключена."
|
||||
|
||||
state.status = "OFF"
|
||||
state.cycle_realized_pnl_usd = 0.0
|
||||
state.last_flip_old_side = None
|
||||
state.last_flip_new_side = None
|
||||
state.last_flip_pnl_usd = None
|
||||
state.last_flip_reason = None
|
||||
state.last_flip_monotonic_at = None
|
||||
self.stop_loop()
|
||||
|
||||
EventBus.emit(
|
||||
|
||||
@@ -94,6 +94,18 @@ class AutoTradeState:
|
||||
# зафиксированный результат закрытых paper-сделок
|
||||
realized_pnl_usd: float = 0.0
|
||||
|
||||
# cumulative realized pnl за текущий цикл автоторговли
|
||||
cycle_realized_pnl_usd: float = 0.0
|
||||
|
||||
# данные последнего flip
|
||||
last_flip_old_side: str | None = None
|
||||
last_flip_new_side: str | None = None
|
||||
last_flip_pnl_usd: float | None = None
|
||||
last_flip_reason: str | None = None
|
||||
|
||||
# monotonic timestamp последнего flip
|
||||
last_flip_monotonic_at: float | None = None
|
||||
|
||||
# последнее execution-действие
|
||||
last_execution_action: str | None = None
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
366
app/src/trading/diagnostics/snapshot.py
Normal file
366
app/src/trading/diagnostics/snapshot.py
Normal file
@@ -0,0 +1,366 @@
|
||||
# app/src/trading/diagnostics/snapshot.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from src.trading.auto.state import AutoTradeState
|
||||
|
||||
|
||||
class SemanticDiagnosticSnapshotBuilder:
|
||||
def build(self, state: AutoTradeState, *, is_configured: bool) -> dict[str, Any]:
|
||||
now = time.monotonic()
|
||||
|
||||
signal_age_seconds = self._age_seconds(
|
||||
now=now,
|
||||
started_at=state.signal_started_at,
|
||||
)
|
||||
|
||||
market_age_seconds = self._age_seconds(
|
||||
now=now,
|
||||
started_at=state.market_analysis_updated_at,
|
||||
)
|
||||
|
||||
blockers = self._blockers(state)
|
||||
health_score = self._health_score(state=state, blockers=blockers)
|
||||
severity = self._severity(
|
||||
state=state,
|
||||
health_score=health_score,
|
||||
blockers=blockers,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": {
|
||||
"status": state.status,
|
||||
"symbol": state.symbol,
|
||||
"strategy": state.strategy,
|
||||
"is_configured": is_configured,
|
||||
},
|
||||
"signal": {
|
||||
"signal": state.last_signal,
|
||||
"confidence": state.last_signal_confidence,
|
||||
"decision_status": state.decision_status,
|
||||
"is_confirmed": state.is_signal_confirmed,
|
||||
"is_ready": state.is_signal_ready,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"confirmation_progress": state.signal_confirmation_progress,
|
||||
"age_seconds": signal_age_seconds,
|
||||
"reason": state.last_signal_reason,
|
||||
},
|
||||
"market": {
|
||||
"state": state.market_state,
|
||||
"trend": state.market_trend,
|
||||
"volatility": state.market_volatility,
|
||||
"trend_strength": state.market_trend_strength,
|
||||
"trend_quality": state.market_trend_quality,
|
||||
"phase": state.market_phase,
|
||||
"phase_direction": state.market_phase_direction,
|
||||
"entry_block_reason": state.entry_block_reason,
|
||||
"entry_block_message": state.entry_block_message,
|
||||
"age_seconds": market_age_seconds,
|
||||
},
|
||||
"momentum": {
|
||||
"state": getattr(state, "momentum_state", None),
|
||||
"direction": getattr(state, "momentum_direction", None),
|
||||
"strength": getattr(state, "momentum_strength", None),
|
||||
"change_percent": getattr(state, "momentum_change_percent", None),
|
||||
"breakout_level": getattr(state, "breakout_level", None),
|
||||
"breakout_distance_percent": getattr(
|
||||
state,
|
||||
"breakout_distance_percent",
|
||||
None,
|
||||
),
|
||||
"is_breakout": getattr(state, "momentum_state", None)
|
||||
in {"BREAKOUT_UP", "BREAKOUT_DOWN"},
|
||||
"breakout_reason": getattr(state, "breakout_reason", None),
|
||||
},
|
||||
"execution": {
|
||||
"quality": state.execution_quality,
|
||||
"quality_reason": state.execution_quality_reason,
|
||||
"quality_message": state.execution_quality_message,
|
||||
"semantic_status": state.execution_semantic_status,
|
||||
"semantic_message": state.execution_semantic_message,
|
||||
"semantic_reason": state.execution_semantic_reason,
|
||||
"confidence_score": state.execution_confidence_score,
|
||||
"confidence_level": state.execution_confidence_level,
|
||||
"confidence_reason": state.execution_confidence_reason,
|
||||
"spread_percent": state.spread_percent,
|
||||
"snapshot_age_seconds": state.snapshot_age_seconds,
|
||||
"market_runtime_degraded": state.market_runtime_degraded,
|
||||
},
|
||||
"adaptive_size": {
|
||||
"base": state.adaptive_size_base,
|
||||
"final": state.adaptive_size_final,
|
||||
"multiplier": state.adaptive_size_multiplier,
|
||||
"effective_risk_percent": state.effective_risk_percent,
|
||||
"effective_target_risk_usd": state.effective_target_risk_usd,
|
||||
"reason": state.adaptive_size_reason,
|
||||
"factors": state.adaptive_size_factors,
|
||||
},
|
||||
"position": {
|
||||
"side": state.position_side,
|
||||
"entry_price": state.entry_price,
|
||||
"size": state.position_size,
|
||||
"leverage": state.leverage,
|
||||
"unrealized_pnl_usd": state.unrealized_pnl_usd,
|
||||
"realized_pnl_usd": state.realized_pnl_usd,
|
||||
"cycle_realized_pnl_usd": state.cycle_realized_pnl_usd,
|
||||
"last_execution_action": state.last_execution_action,
|
||||
"last_execution_reason": state.last_execution_reason,
|
||||
"last_flip_old_side": state.last_flip_old_side,
|
||||
"last_flip_new_side": state.last_flip_new_side,
|
||||
"last_flip_pnl_usd": state.last_flip_pnl_usd,
|
||||
"last_flip_reason": state.last_flip_reason,
|
||||
"last_flip_monotonic_at": state.last_flip_monotonic_at,
|
||||
},
|
||||
"runtime_health": {
|
||||
"health_score": health_score,
|
||||
"severity": severity,
|
||||
"is_runtime_degraded": self._is_runtime_degraded(state),
|
||||
"signal_age_seconds": signal_age_seconds,
|
||||
"market_age_seconds": market_age_seconds,
|
||||
"runtime_expired_reason": state.runtime_expired_reason,
|
||||
"runtime_expired_message": state.runtime_expired_message,
|
||||
"has_market_data": state.market_state is not None,
|
||||
"has_momentum_data": getattr(state, "momentum_state", None) is not None,
|
||||
},
|
||||
"summary": {
|
||||
"health_score": health_score,
|
||||
"severity": severity,
|
||||
"assessment": self._assessment(severity),
|
||||
"mode": self._display_mode(
|
||||
severity=severity,
|
||||
blockers=blockers,
|
||||
state=state,
|
||||
),
|
||||
|
||||
"headline_mode": (
|
||||
"POSITION"
|
||||
if state.position_side != "NONE"
|
||||
else "ENTRY"
|
||||
),
|
||||
|
||||
"main_message": self._main_message(state=state, blockers=blockers),
|
||||
|
||||
"market": state.market_state,
|
||||
"phase": state.market_phase,
|
||||
"momentum": getattr(state, "momentum_state", None),
|
||||
"execution": state.execution_semantic_status,
|
||||
"position": state.position_side,
|
||||
"is_ready": state.is_signal_ready,
|
||||
"is_blocked": bool(blockers),
|
||||
"blockers": blockers,
|
||||
},
|
||||
}
|
||||
|
||||
def _age_seconds(
|
||||
self,
|
||||
*,
|
||||
now: float,
|
||||
started_at: float | None,
|
||||
) -> int | None:
|
||||
if started_at is None:
|
||||
return None
|
||||
|
||||
return max(0, int(now - float(started_at)))
|
||||
|
||||
def _is_runtime_degraded(self, state: AutoTradeState) -> bool:
|
||||
return bool(
|
||||
state.market_runtime_degraded
|
||||
or state.execution_quality == "BLOCKED"
|
||||
or state.runtime_expired_reason
|
||||
)
|
||||
|
||||
def _health_score(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
blockers: list[str],
|
||||
) -> int:
|
||||
score = 100
|
||||
|
||||
if state.status != "RUNNING":
|
||||
score -= 10
|
||||
|
||||
if blockers:
|
||||
score -= min(35, len(blockers) * 12)
|
||||
|
||||
if state.execution_quality == "BLOCKED":
|
||||
score -= 30
|
||||
elif state.execution_quality == "WARNING":
|
||||
score -= 15
|
||||
|
||||
if state.market_state in {"RANGE", "HIGH_VOLATILITY", "LOW_VOLATILITY"}:
|
||||
score -= 15
|
||||
|
||||
if state.market_trend_strength == "WEAK":
|
||||
score -= 10
|
||||
|
||||
if state.market_trend_quality == "NOISY":
|
||||
score -= 10
|
||||
|
||||
if state.market_phase in {"RANGE", "SQUEEZE", "PULLBACK"}:
|
||||
score -= 10
|
||||
|
||||
if state.market_runtime_degraded:
|
||||
score -= 15
|
||||
|
||||
if state.runtime_expired_reason:
|
||||
score -= 20
|
||||
|
||||
if state.is_signal_ready:
|
||||
score += 10
|
||||
|
||||
return max(0, min(100, score))
|
||||
|
||||
def _severity(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
health_score: int,
|
||||
blockers: list[str],
|
||||
) -> str:
|
||||
signal = str(state.last_signal or "HOLD").upper()
|
||||
has_ready_signal = bool(state.is_signal_ready)
|
||||
has_position = state.position_side != "NONE"
|
||||
|
||||
has_waiting_data_blocker = any(
|
||||
str(item).strip().lower()
|
||||
in {
|
||||
"мало данных",
|
||||
"мало live-данных",
|
||||
"недостаточно live-данных",
|
||||
}
|
||||
for item in blockers
|
||||
)
|
||||
|
||||
if has_waiting_data_blocker:
|
||||
return "WAITING"
|
||||
|
||||
if (
|
||||
state.execution_quality == "BLOCKED"
|
||||
or state.decision_status == "BLOCKED"
|
||||
or state.runtime_expired_reason
|
||||
):
|
||||
return "RED"
|
||||
|
||||
if has_position:
|
||||
if health_score < 45:
|
||||
return "RED"
|
||||
|
||||
if blockers or state.execution_quality == "WARNING" or health_score < 75:
|
||||
return "YELLOW"
|
||||
|
||||
return "GREEN"
|
||||
|
||||
if signal == "HOLD" and not has_ready_signal:
|
||||
return "WAITING"
|
||||
|
||||
if state.entry_block_reason == "MARKET_FILTER_BLOCKED":
|
||||
return "YELLOW"
|
||||
|
||||
if health_score < 45:
|
||||
return "YELLOW"
|
||||
|
||||
if blockers or state.execution_quality == "WARNING" or health_score < 75:
|
||||
return "YELLOW"
|
||||
|
||||
return "GREEN"
|
||||
|
||||
def _assessment(self, severity: str) -> str:
|
||||
if severity == "GREEN":
|
||||
return "стабильно"
|
||||
|
||||
if severity == "WAITING":
|
||||
return "ожидание"
|
||||
|
||||
if severity == "YELLOW":
|
||||
return "осторожно"
|
||||
|
||||
return "вход нежелателен"
|
||||
|
||||
def _display_mode(
|
||||
self,
|
||||
*,
|
||||
severity: str,
|
||||
blockers: list[str],
|
||||
state: AutoTradeState | None = None,
|
||||
) -> str:
|
||||
if state is not None and state.position_side != "NONE":
|
||||
return "EXPANDED"
|
||||
|
||||
if severity == "GREEN" and not blockers:
|
||||
return "COMPACT"
|
||||
|
||||
return "EXPANDED"
|
||||
|
||||
def _main_message(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
blockers: list[str],
|
||||
) -> str:
|
||||
if state.entry_block_reason == "MARKET_FILTER_BLOCKED":
|
||||
if state.market_state == "RANGE" or state.market_phase == "RANGE":
|
||||
return "Ожидание: рынок без направления."
|
||||
|
||||
return "Осторожно: рынок не подходит."
|
||||
|
||||
if state.execution_quality == "BLOCKED":
|
||||
reason = str(state.execution_quality_reason or "")
|
||||
|
||||
if reason == "HIGH_SPREAD":
|
||||
return "Вход нежелателен: спред мешает входу."
|
||||
|
||||
if reason == "STALE_SNAPSHOT":
|
||||
return "Вход нежелателен: данные рынка устарели."
|
||||
|
||||
if reason in {"SNAPSHOT_ERROR", "SNAPSHOT_UNAVAILABLE"}:
|
||||
return "Вход нежелателен: нет надёжных данных рынка."
|
||||
|
||||
return "Вход нежелателен: исполнение заблокировано."
|
||||
|
||||
if state.entry_block_message:
|
||||
return f"Рынок не готов: {state.entry_block_message}."
|
||||
|
||||
if state.execution_quality == "WARNING":
|
||||
return "Вход рискованный: качество исполнения снижено."
|
||||
|
||||
if state.is_signal_ready:
|
||||
return "Сигнал готов, вход разрешён."
|
||||
|
||||
if state.last_signal in {"BUY", "SELL"}:
|
||||
return "Сигнал есть, идёт подтверждение."
|
||||
|
||||
if blockers:
|
||||
return f"Есть ограничения: {', '.join(blockers)}."
|
||||
|
||||
return "Критичных ограничений нет."
|
||||
|
||||
def _blockers(self, state: AutoTradeState) -> list[str]:
|
||||
blockers: list[str] = []
|
||||
|
||||
if state.entry_block_reason == "MARKET_FILTER_BLOCKED":
|
||||
if state.market_state == "RANGE" or state.market_phase == "RANGE":
|
||||
blockers.append("рынок без направления")
|
||||
elif state.entry_block_message:
|
||||
blockers.append(str(state.entry_block_message))
|
||||
else:
|
||||
blockers.append("рынок не подходит")
|
||||
|
||||
return blockers
|
||||
|
||||
if state.entry_block_message:
|
||||
blockers.append(str(state.entry_block_message))
|
||||
|
||||
if state.execution_quality == "BLOCKED":
|
||||
blockers.append(str(state.execution_quality_message or "исполнение заблокировано"))
|
||||
|
||||
if state.decision_status == "BLOCKED":
|
||||
blockers.append(str(state.decision_reason or "решение заблокировано"))
|
||||
|
||||
if state.runtime_expired_message:
|
||||
blockers.append(str(state.runtime_expired_message))
|
||||
|
||||
return blockers
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
@@ -227,6 +228,13 @@ class ExecutionEngine:
|
||||
)
|
||||
|
||||
state.realized_pnl_usd += pnl
|
||||
state.cycle_realized_pnl_usd += pnl
|
||||
|
||||
state.last_flip_old_side = old_side
|
||||
state.last_flip_new_side = new_side
|
||||
state.last_flip_pnl_usd = pnl
|
||||
state.last_flip_reason = state.last_signal_reason
|
||||
state.last_flip_monotonic_at = time.monotonic()
|
||||
|
||||
old_side = position.side
|
||||
old_entry_price = position.entry_price
|
||||
@@ -341,6 +349,7 @@ class ExecutionEngine:
|
||||
pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
|
||||
|
||||
state.realized_pnl_usd += pnl
|
||||
state.cycle_realized_pnl_usd += pnl
|
||||
|
||||
now = self._now_time()
|
||||
|
||||
@@ -404,6 +413,7 @@ class ExecutionEngine:
|
||||
f"Позиция закрыта по правилу защиты: {forced_reason}.",
|
||||
)
|
||||
|
||||
|
||||
return ExecutionDecision("CLOSE", True, "Позиция закрыта.")
|
||||
|
||||
def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:
|
||||
|
||||
@@ -110,6 +110,9 @@ class TrendStrategy:
|
||||
if len(prices) > self._window_size:
|
||||
prices.pop(0)
|
||||
|
||||
market_phase = self._normalized_market_phase(market)
|
||||
market_phase_direction = self._normalized_market_phase_direction(market)
|
||||
|
||||
base_payload = {
|
||||
"strategy": self.name,
|
||||
"symbol": symbol,
|
||||
@@ -125,8 +128,8 @@ class TrendStrategy:
|
||||
"market_analysis": market.payload,
|
||||
"market_trend_strength": market.trend_strength.value,
|
||||
"market_trend_quality": market.trend_quality.value,
|
||||
"market_phase": market.market_phase.value,
|
||||
"market_phase_direction": market.phase_direction.value,
|
||||
"market_phase": market_phase,
|
||||
"market_phase_direction": market_phase_direction,
|
||||
"market_phase_change_percent": market.phase_change_percent,
|
||||
"market_phase_direction_consistency": market.payload.get("market_phase_direction_consistency"),
|
||||
"market_phase_reason": market.phase_reason,
|
||||
@@ -305,7 +308,10 @@ class TrendStrategy:
|
||||
momentum_direction = getattr(market, "momentum_direction", TrendDirection.UNKNOWN)
|
||||
momentum_strength = float(getattr(market, "momentum_strength", 0.0) or 0.0)
|
||||
|
||||
if momentum_state == MomentumState.BREAKOUT_UP:
|
||||
if (
|
||||
momentum_state == MomentumState.BREAKOUT_UP
|
||||
and market.state == MarketState.TREND_UP
|
||||
):
|
||||
return SignalResult(
|
||||
signal=SignalType.BUY,
|
||||
reason="BREAKOUT_UP подтверждён momentum/breakout semantic layer.",
|
||||
@@ -319,7 +325,10 @@ class TrendStrategy:
|
||||
},
|
||||
)
|
||||
|
||||
if momentum_state == MomentumState.BREAKOUT_DOWN:
|
||||
if (
|
||||
momentum_state == MomentumState.BREAKOUT_DOWN
|
||||
and market.state == MarketState.TREND_DOWN
|
||||
):
|
||||
return SignalResult(
|
||||
signal=SignalType.SELL,
|
||||
reason="BREAKOUT_DOWN подтверждён momentum/breakout semantic layer.",
|
||||
@@ -333,6 +342,37 @@ class TrendStrategy:
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
momentum_state == MomentumState.BREAKOUT_DOWN
|
||||
and market.state == MarketState.TREND_UP
|
||||
):
|
||||
return SignalResult(
|
||||
signal=SignalType.HOLD,
|
||||
reason="Пробой вниз против TREND_UP считается коррекцией, вход в SHORT запрещён.",
|
||||
confidence=0.0,
|
||||
payload={
|
||||
**base_payload,
|
||||
"entry_block_reason": "COUNTER_TREND_BREAKOUT",
|
||||
"entry_block_message": "пробой против тренда",
|
||||
"expected_direction": "BUY",
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
momentum_state == MomentumState.BREAKOUT_UP
|
||||
and market.state == MarketState.TREND_DOWN
|
||||
):
|
||||
return SignalResult(
|
||||
signal=SignalType.HOLD,
|
||||
reason="Пробой вверх против TREND_DOWN считается откатом, вход в LONG запрещён.",
|
||||
confidence=0.0,
|
||||
payload={
|
||||
**base_payload,
|
||||
"entry_block_reason": "COUNTER_TREND_BREAKOUT",
|
||||
"entry_block_message": "пробой против тренда",
|
||||
"expected_direction": "SELL",
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
def _calculate_breakout_confidence(self, momentum_strength: float) -> float:
|
||||
@@ -384,6 +424,31 @@ class TrendStrategy:
|
||||
|
||||
return down_moves / total_moves
|
||||
|
||||
def _normalized_market_phase(self, market) -> str:
|
||||
phase = market.market_phase.value
|
||||
momentum_state = market.momentum_state.value
|
||||
|
||||
active_momentum_states = {
|
||||
"MOMENTUM_UP",
|
||||
"MOMENTUM_DOWN",
|
||||
"BREAKOUT_UP",
|
||||
"BREAKOUT_DOWN",
|
||||
}
|
||||
|
||||
if phase == "IMPULSE" and momentum_state not in active_momentum_states:
|
||||
return "UNKNOWN"
|
||||
|
||||
return phase
|
||||
|
||||
|
||||
def _normalized_market_phase_direction(self, market) -> str:
|
||||
phase = self._normalized_market_phase(market)
|
||||
|
||||
if phase == "UNKNOWN":
|
||||
return "UNKNOWN"
|
||||
|
||||
return market.phase_direction.value
|
||||
|
||||
def _calculate_confidence(
|
||||
self,
|
||||
change_percent: float,
|
||||
|
||||
@@ -1205,6 +1205,58 @@
|
||||
- diagnostic layer подготовлен к Diagnostic Journal Layer
|
||||
- diagnostic layer подготовлен к Auto-refresh Diagnostic UI
|
||||
|
||||
#### 07.4.4.1.10.3 ✅ Telegram Diagnostic Screen
|
||||
- реализирован полноценный Telegram Diagnostic Screen
|
||||
- реализован отдельный diagnostics Telegram UI layer
|
||||
- реализован auto-refresh diagnostics screen
|
||||
- реализована интеграция diagnostics screen с AutoTradeRunner
|
||||
- реализована интеграция diagnostics screen с ActiveScreenManager
|
||||
- реализован отдельный diagnostic navigation flow
|
||||
- реализована отдельная diagnostics keyboard
|
||||
- реализовано безопасное обновление diagnostic messages
|
||||
- реализована защита Telegram diagnostics UI от TelegramBadRequest
|
||||
- реализован explainable runtime diagnostic screen
|
||||
- реализован explainable semantic diagnostic UI
|
||||
- реализован explainable market diagnostics UI
|
||||
- реализован explainable momentum diagnostics UI
|
||||
- реализован explainable breakout diagnostics UI
|
||||
- реализован explainable execution diagnostics UI
|
||||
- реализован explainable adaptive sizing diagnostics UI
|
||||
- реализован explainable runtime health diagnostics UI
|
||||
- реализован explainable position diagnostics UI
|
||||
- реализован explainable severity system
|
||||
- реализована semantic severity hierarchy
|
||||
- реализовано разделение WAITING / YELLOW / RED runtime states
|
||||
- реализована логика semantic waiting state
|
||||
- реализована логика runtime freshness interpretation
|
||||
- реализована логика execution readiness interpretation
|
||||
- реализована логика signal confirmation interpretation
|
||||
- реализована логика market noise interpretation
|
||||
- реализована логика market phase interpretation
|
||||
- реализована логика breakout explanation
|
||||
- реализована логика execution quality explanation
|
||||
- реализована логика adaptive sizing explanation
|
||||
- реализована логика runtime degradation explanation
|
||||
- исключены ложные warning состояния при HOLD signal
|
||||
- исключены ложные yellow состояния без momentum
|
||||
- реализован semantic OFF diagnostics mode
|
||||
- реализован lightweight diagnostics режим для OFF состояния
|
||||
- реализована корректная diagnostics логика без RUNNING state
|
||||
- реализована подготовка cycle pnl diagnostics
|
||||
- реализована подготовка flip diagnostics
|
||||
- реализована подготовка cumulative realized pnl diagnostics
|
||||
- реализована подготовка old/new side flip diagnostics
|
||||
- реализована подготовка flip pnl diagnostics
|
||||
- реализована подготовка position cycle analytics
|
||||
- semantic analytics layer стал explainable
|
||||
- semantic analytics layer стал user-readable
|
||||
- semantic analytics layer стал Telegram-ready
|
||||
- diagnostics layer подготовлен к Diagnostic Journal Layer
|
||||
- diagnostics layer подготовлен к persistent runtime analytics
|
||||
- diagnostics layer подготовлен к advanced cycle analytics
|
||||
- diagnostics layer подготовлен к semantic trade analytics
|
||||
- diagnostics layer подготовлен к auto-refresh runtime dashboard
|
||||
|
||||
---
|
||||
|
||||
### 07.4.5
|
||||
|
||||
@@ -1131,6 +1131,58 @@
|
||||
- execution runtime подготовлен к semantic breakout routing
|
||||
- execution runtime подготовлен к AI-driven momentum interpretation
|
||||
|
||||
#### 07.4.4.1.10.3 ✅ Telegram Diagnostic Screen
|
||||
- реализирован полноценный Telegram Diagnostic Screen
|
||||
- реализован отдельный diagnostics Telegram UI layer
|
||||
- реализован auto-refresh diagnostics screen
|
||||
- реализована интеграция diagnostics screen с AutoTradeRunner
|
||||
- реализована интеграция diagnostics screen с ActiveScreenManager
|
||||
- реализован отдельный diagnostic navigation flow
|
||||
- реализована отдельная diagnostics keyboard
|
||||
- реализовано безопасное обновление diagnostic messages
|
||||
- реализована защита Telegram diagnostics UI от TelegramBadRequest
|
||||
- реализован explainable runtime diagnostic screen
|
||||
- реализован explainable semantic diagnostic UI
|
||||
- реализован explainable market diagnostics UI
|
||||
- реализован explainable momentum diagnostics UI
|
||||
- реализован explainable breakout diagnostics UI
|
||||
- реализован explainable execution diagnostics UI
|
||||
- реализован explainable adaptive sizing diagnostics UI
|
||||
- реализован explainable runtime health diagnostics UI
|
||||
- реализован explainable position diagnostics UI
|
||||
- реализован explainable severity system
|
||||
- реализована semantic severity hierarchy
|
||||
- реализовано разделение WAITING / YELLOW / RED runtime states
|
||||
- реализована логика semantic waiting state
|
||||
- реализована логика runtime freshness interpretation
|
||||
- реализована логика execution readiness interpretation
|
||||
- реализована логика signal confirmation interpretation
|
||||
- реализована логика market noise interpretation
|
||||
- реализована логика market phase interpretation
|
||||
- реализована логика breakout explanation
|
||||
- реализована логика execution quality explanation
|
||||
- реализована логика adaptive sizing explanation
|
||||
- реализована логика runtime degradation explanation
|
||||
- исключены ложные warning состояния при HOLD signal
|
||||
- исключены ложные yellow состояния без momentum
|
||||
- реализован semantic OFF diagnostics mode
|
||||
- реализован lightweight diagnostics режим для OFF состояния
|
||||
- реализована корректная diagnostics логика без RUNNING state
|
||||
- реализована подготовка cycle pnl diagnostics
|
||||
- реализована подготовка flip diagnostics
|
||||
- реализована подготовка cumulative realized pnl diagnostics
|
||||
- реализована подготовка old/new side flip diagnostics
|
||||
- реализована подготовка flip pnl diagnostics
|
||||
- реализована подготовка position cycle analytics
|
||||
- semantic analytics layer стал explainable
|
||||
- semantic analytics layer стал user-readable
|
||||
- semantic analytics layer стал Telegram-ready
|
||||
- diagnostics layer подготовлен к Diagnostic Journal Layer
|
||||
- diagnostics layer подготовлен к persistent runtime analytics
|
||||
- diagnostics layer подготовлен к advanced cycle analytics
|
||||
- diagnostics layer подготовлен к semantic trade analytics
|
||||
- diagnostics layer подготовлен к auto-refresh runtime dashboard
|
||||
|
||||
---
|
||||
|
||||
### 07.4.4.1.10 Semantic Runtime Diagnostics & Observability
|
||||
|
||||
388
docs/stages/stage-07_4_4_1_10_3-telegram_diagnostic_screen.md
Normal file
388
docs/stages/stage-07_4_4_1_10_3-telegram_diagnostic_screen.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# 07.4.4.1.10.3 — Telegram Diagnostic Screen
|
||||
|
||||
## Статус
|
||||
|
||||
✅ Этап реализован.
|
||||
|
||||
---
|
||||
|
||||
## Назначение этапа
|
||||
|
||||
Этап **07.4.4.1.10.3 Telegram Diagnostic Screen** добавляет полноценный Telegram diagnostic interface поверх ранее реализованных:
|
||||
|
||||
- semantic diagnostic snapshot builder
|
||||
- human-readable formatter
|
||||
- runtime semantic analytics layer
|
||||
|
||||
Главная цель этапа — превратить внутреннюю runtime-аналитику автоторговли в explainable Telegram UI.
|
||||
|
||||
Теперь бот умеет не только анализировать рынок, но и подробно объяснять:
|
||||
|
||||
- почему вход разрешён или запрещён
|
||||
- почему execution считается рискованным
|
||||
- почему рынок считается шумным
|
||||
- почему signal находится в HOLD
|
||||
- почему flip разрешён или заблокирован
|
||||
- почему adaptive sizing изменил размер позиции
|
||||
- почему runtime считается degraded
|
||||
- почему severity имеет WAITING / YELLOW / RED состояние
|
||||
|
||||
---
|
||||
|
||||
## Что было реализовано
|
||||
|
||||
### Telegram Diagnostic Screen
|
||||
|
||||
Реализован отдельный экран диагностики:
|
||||
|
||||
```text
|
||||
🔬 Диагностика · BTC
|
||||
```
|
||||
|
||||
Экран отображает explainable runtime diagnostics прямо внутри Telegram.
|
||||
|
||||
---
|
||||
|
||||
### Auto-refresh diagnostics
|
||||
|
||||
Реализован auto-refresh diagnostics screen через:
|
||||
|
||||
- AutoTradeRunner
|
||||
- ActiveScreenManager
|
||||
- render callbacks
|
||||
|
||||
Теперь diagnostics screen автоматически обновляется во время работы автоторговли.
|
||||
|
||||
---
|
||||
|
||||
### Navigation Layer
|
||||
|
||||
Реализован отдельный navigation flow:
|
||||
|
||||
```text
|
||||
Auto Screen
|
||||
↓
|
||||
Diagnostic Screen
|
||||
↓
|
||||
Back
|
||||
```
|
||||
|
||||
Diagnostics screen теперь существует как полноценный Telegram UI layer.
|
||||
|
||||
---
|
||||
|
||||
### Separate Diagnostic Keyboard
|
||||
|
||||
Реализована отдельная diagnostics keyboard:
|
||||
|
||||
- Обновить
|
||||
- Назад
|
||||
|
||||
Diagnostics screen больше не зависит от main auto keyboard.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
После этапа структура diagnostics стала следующей:
|
||||
|
||||
```text
|
||||
AutoTradeState
|
||||
↓
|
||||
SemanticDiagnosticSnapshotBuilder
|
||||
↓
|
||||
SemanticDiagnosticFormatter
|
||||
↓
|
||||
Telegram Diagnostic Screen
|
||||
```
|
||||
|
||||
Теперь Telegram UI отображает не raw runtime state, а explainable semantic representation.
|
||||
|
||||
---
|
||||
|
||||
# Что было сделано в части аналитики
|
||||
|
||||
## Полностью переработана severity logic
|
||||
|
||||
До этого этапа headline severity могла конфликтовать с runtime состоянием.
|
||||
|
||||
Например:
|
||||
|
||||
- HOLD signal
|
||||
- отсутствие momentum
|
||||
- отсутствие breakout
|
||||
|
||||
могли одновременно отображаться вместе с:
|
||||
|
||||
```text
|
||||
🟡 Осторожно
|
||||
```
|
||||
|
||||
что создавало логические противоречия.
|
||||
|
||||
После переработки:
|
||||
|
||||
- severity учитывает signal readiness
|
||||
- severity учитывает execution readiness
|
||||
- severity учитывает наличие momentum
|
||||
- severity учитывает HOLD state
|
||||
- severity учитывает runtime degradation
|
||||
- severity учитывает execution blockers
|
||||
- severity учитывает market semantic state
|
||||
|
||||
---
|
||||
|
||||
## Реализована WAITING semantic category
|
||||
|
||||
Добавлено отдельное semantic состояние:
|
||||
|
||||
```text
|
||||
WAITING
|
||||
```
|
||||
|
||||
Оно используется когда:
|
||||
|
||||
- сигнала ещё нет
|
||||
- momentum отсутствует
|
||||
- breakout отсутствует
|
||||
- рынок просто наблюдается
|
||||
- execution ещё не готов
|
||||
|
||||
Это устраняет ложные WARNING состояния.
|
||||
|
||||
---
|
||||
|
||||
## Разделены WAITING / YELLOW / RED
|
||||
|
||||
Теперь semantic severity hierarchy выглядит так:
|
||||
|
||||
### WAITING
|
||||
|
||||
Используется когда:
|
||||
|
||||
- рынок просто анализируется
|
||||
- signal ещё не подтверждён
|
||||
- HOLD является нормальным состоянием
|
||||
- momentum отсутствует
|
||||
- execution не готов, но не заблокирован
|
||||
|
||||
### YELLOW
|
||||
|
||||
Используется когда:
|
||||
|
||||
- execution рискованный
|
||||
- рынок шумный
|
||||
- есть unstable runtime conditions
|
||||
- есть WARNING execution quality
|
||||
- есть weak / noisy trend
|
||||
|
||||
### RED
|
||||
|
||||
Используется когда:
|
||||
|
||||
- execution заблокирован
|
||||
- snapshot устарел
|
||||
- runtime expired
|
||||
- spread блокирует execution
|
||||
- runtime критически degraded
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable runtime logic
|
||||
|
||||
Diagnostics screen теперь умеет объяснять:
|
||||
|
||||
- почему рынок считается noisy
|
||||
- почему рынок считается trend/range/pullback
|
||||
- почему execution blocked
|
||||
- почему execution warning
|
||||
- почему spread считается высоким
|
||||
- почему signal confirmation ещё не завершён
|
||||
- почему momentum слабый
|
||||
- почему breakout не подтверждён
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable signal interpretation
|
||||
|
||||
Signal block теперь отображает:
|
||||
|
||||
- signal state
|
||||
- confirmation progress
|
||||
- confirmation duration
|
||||
- semantic signal explanation
|
||||
- breakout explanation
|
||||
- trend confirmation explanation
|
||||
|
||||
Теперь HOLD перестал выглядеть как ошибка.
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable market interpretation
|
||||
|
||||
Market block теперь отображает:
|
||||
|
||||
- trend direction
|
||||
- trend strength
|
||||
- trend quality
|
||||
- market phase
|
||||
- volatility state
|
||||
- current movement context
|
||||
- market blockers
|
||||
- semantic market explanation
|
||||
|
||||
Теперь можно визуально понимать:
|
||||
|
||||
- тренд ли это
|
||||
- флэт ли это
|
||||
- squeeze ли это
|
||||
- pullback ли это
|
||||
- noisy ли рынок
|
||||
- есть ли directional movement
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable execution diagnostics
|
||||
|
||||
Execution diagnostics теперь объясняет:
|
||||
|
||||
- почему execution GOOD
|
||||
- почему execution WARNING
|
||||
- почему execution BLOCKED
|
||||
- почему spread опасен
|
||||
- почему snapshot считается stale
|
||||
- почему execution confidence низкий
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable adaptive sizing diagnostics
|
||||
|
||||
Adaptive sizing diagnostics теперь объясняет:
|
||||
|
||||
- почему размер позиции уменьшен
|
||||
- почему размер увеличен
|
||||
- почему adaptive size заблокировал вход
|
||||
- какие runtime factors повлияли на multiplier
|
||||
|
||||
---
|
||||
|
||||
## Реализована explainable runtime health diagnostics
|
||||
|
||||
Runtime diagnostics теперь отображает:
|
||||
|
||||
- freshness market data
|
||||
- freshness snapshot data
|
||||
- runtime degradation
|
||||
- stale analysis detection
|
||||
- signal age
|
||||
- runtime expiration
|
||||
|
||||
---
|
||||
|
||||
# Position Diagnostic Layer
|
||||
|
||||
## Реализован semantic position block
|
||||
|
||||
Добавлен полноценный explainable position diagnostics block.
|
||||
|
||||
---
|
||||
|
||||
## Реализовано отображение unrealized pnl
|
||||
|
||||
Теперь diagnostics screen показывает:
|
||||
|
||||
- текущую прибыль
|
||||
- текущий убыток
|
||||
- semantic pnl state
|
||||
|
||||
---
|
||||
|
||||
## Реализована подготовка flip diagnostics
|
||||
|
||||
Подготовлена структура для:
|
||||
|
||||
- old_side
|
||||
- new_side
|
||||
- flip pnl
|
||||
- cumulative cycle pnl
|
||||
- flip semantic rendering
|
||||
|
||||
---
|
||||
|
||||
## Реализована подготовка cycle pnl analytics
|
||||
|
||||
Добавлена подготовка данных для:
|
||||
|
||||
- cumulative realized pnl
|
||||
- cycle analytics
|
||||
- multi-trade cycle statistics
|
||||
|
||||
---
|
||||
|
||||
# Runtime & UX
|
||||
|
||||
## Реализован OFF diagnostics mode
|
||||
|
||||
Когда автоторговля выключена:
|
||||
|
||||
```text
|
||||
⛔️ Автоторговля · остановлена
|
||||
```
|
||||
|
||||
Diagnostics screen теперь не показывает misleading runtime analytics.
|
||||
|
||||
---
|
||||
|
||||
## Реализован lightweight OFF state
|
||||
|
||||
В OFF режиме diagnostics screen больше не отображает:
|
||||
|
||||
- signal diagnostics
|
||||
- market diagnostics
|
||||
- momentum diagnostics
|
||||
- execution diagnostics
|
||||
|
||||
Это устраняет ложное ощущение активной аналитики.
|
||||
|
||||
---
|
||||
|
||||
## Исключены misleading runtime states
|
||||
|
||||
Теперь:
|
||||
|
||||
- HOLD больше не выглядит как warning
|
||||
- отсутствие momentum больше не выглядит как ошибка
|
||||
- отсутствие breakout больше не выглядит как degraded runtime
|
||||
- noisy market больше не вызывает ложный RED severity
|
||||
|
||||
---
|
||||
|
||||
# Подготовка к следующим этапам
|
||||
|
||||
Этап подготавливает систему к:
|
||||
|
||||
- Diagnostic Journal Layer
|
||||
- Persistent Runtime Diagnostics
|
||||
- Semantic Trade Analytics
|
||||
- Cycle Analytics
|
||||
- Flip Analytics
|
||||
- Trade History Diagnostics
|
||||
- Runtime Dashboard
|
||||
- Auto-refresh Runtime Monitoring
|
||||
|
||||
---
|
||||
|
||||
# Итог
|
||||
|
||||
Этап **07.4.4.1.10.3 Telegram Diagnostic Screen** завершает превращение semantic diagnostics в полноценный explainable Telegram runtime interface.
|
||||
|
||||
Теперь автоторговля умеет:
|
||||
|
||||
- анализировать рынок
|
||||
- анализировать execution
|
||||
- анализировать momentum
|
||||
- анализировать runtime health
|
||||
- анализировать adaptive sizing
|
||||
- анализировать position state
|
||||
|
||||
и одновременно подробно объяснять всё это пользователю внутри Telegram UI.
|
||||
Reference in New Issue
Block a user