07.4.4.1.10.3 — Telegram Diagnostic Screen

This commit is contained in:
2026-05-16 09:23:37 +03:00
parent 8e1c09ad66
commit 2c75f95b46
16 changed files with 2902 additions and 243 deletions

View File

@@ -1,7 +1,5 @@
# app/src/core/event_titles.py # app/src/core/event_titles.py
# app/src/core/event_titles.py
from __future__ import annotations from __future__ import annotations

View File

@@ -16,6 +16,9 @@ def build_execution_notification(event: RuntimeEvent) -> NotificationMessage | N
if event.event_type == RuntimeEventType.POSITION_FLIPPED: if event.event_type == RuntimeEventType.POSITION_FLIPPED:
return _build_position_flipped(event) return _build_position_flipped(event)
if event.event_type == RuntimeEventType.POSITION_FLIP_BLOCKED:
return _build_flip_blocked(event)
return None return None
@@ -24,23 +27,41 @@ def _build_position_opened(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload payload = event.payload
symbol = _format_symbol(payload.get("symbol")) 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")) leverage = _format_leverage(payload.get("leverage"))
entry_price = _format_price(payload.get("entry_price")) entry_price = _format_price(payload.get("entry_price"))
size = _format_size(payload.get("size")) 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 "🔴" side_icon = "🟢" if side == "LONG" else "🔴"
text = ( lines = [
f"<b>📄 Paper position opened {side_icon} {side}</b>\n\n" "<b>🧾 Позиция открыта</b>",
f"{symbol} · {leverage}\n" "",
f"Entry: $ {entry_price}\n" f"{side_icon} {symbol} · {strategy} · {side} {leverage}",
f"Size: {size}" 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( return NotificationMessage(
title=event.title, title=event.title,
text=text, text="\n".join(lines),
priority=event.priority, priority=event.priority,
dedupe_key=event.dedupe_key, dedupe_key=event.dedupe_key,
) )
@@ -50,63 +71,162 @@ def _build_position_closed(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload payload = event.payload
symbol = _format_symbol(payload.get("symbol")) 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")) leverage = _format_leverage(payload.get("leverage"))
entry_price = _format_price(payload.get("entry_price")) entry_price = _format_price(payload.get("entry_price"))
exit_price = _format_price(payload.get("exit_price")) exit_price = _format_price(payload.get("exit_price"))
size = _format_size(payload.get("size")) 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 = ( risk_reason = _human_close_reason(payload.get("risk_reason"))
f"<b>✅ Paper position closed</b>\n\n"
f"{side} · {symbol} · {leverage}\n" pnl_icon = "🟢" if pnl_value >= 0 else "🔴"
f"Entry: $ {entry_price}\n" pnl_label = "Прибыль" if pnl_value >= 0 else "Убыток"
f"Exit: $ {exit_price}\n"
f"Size: {size}\n\n" lines = [
f"PnL: {pnl}" "<b>🧾 Сделка закрыта</b>",
f"{risk_line}" 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( return NotificationMessage(
title=event.title, title=event.title,
text=text, text="\n".join(lines),
priority=event.priority, priority=event.priority,
dedupe_key=event.dedupe_key, 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: def _build_position_flipped(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload payload = event.payload
symbol = _format_symbol(payload.get("symbol")) 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 "") old_side = str(payload.get("old_side") or "").upper()
new_side = str(payload.get("new_side") or payload.get("side") or "") 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")) entry_price = _format_price(payload.get("entry_price"))
exit_price = _format_price(payload.get("exit_price")) exit_price = _format_price(payload.get("exit_price"))
new_entry_price = _format_price(payload.get("new_entry_price")) new_entry_price = _format_price(payload.get("new_entry_price"))
old_size = _format_size(payload.get("old_size")) old_size = _format_size(payload.get("old_size"))
new_size = _format_size(payload.get("new_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 "🔴" old_icon = "🟢" if old_side == "LONG" else "🔴"
new_icon = "🟢" if new_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 = ( text = (
f"<b>🔁 Paper position flipped {old_icon} {old_side}" f"<b>⚠️ Flip отменён</b>\n\n"
f"{new_icon} {new_side}</b>\n\n" f"{icon} {symbol} · {target_side}\n"
f"{symbol} · {leverage}\n\n" f"Текущая позиция: {position_side}\n\n"
f"Old entry: $ {entry_price}\n" f"Недостаточно условий для разворота\n"
f"Exit: $ {exit_price}\n" f"{reason}\n"
f"Old size: {old_size}\n\n" f"Сила сигнала: {confidence:.2f}"
f"New entry: $ {new_entry_price}\n"
f"New size: {new_size}\n\n"
f"PnL: {pnl}"
) )
return NotificationMessage( return NotificationMessage(
@@ -123,13 +243,7 @@ def _format_symbol(value: object) -> str:
if not symbol or symbol == "": if not symbol or symbol == "":
return "" return ""
base_symbol = symbol.split("_", 1)[0] return symbol.split("_", 1)[0].split("/", 1)[0].upper()
parts = base_symbol.split("/", 1)
if len(parts) == 2:
return f"{parts[0]} / {parts[1]}"
return base_symbol
def _format_leverage(value: object) -> str: def _format_leverage(value: object) -> str:
@@ -169,4 +283,45 @@ def _format_pnl(value: object) -> str:
if number < 0: if number < 0:
return f"🔴 {amount}" 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(".")

View File

@@ -14,37 +14,44 @@ def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None
payload = event.payload payload = event.payload
signal = str(payload.get("signal") or "").upper() signal = str(payload.get("signal") or "").upper()
symbol = str(payload.get("symbol") or "") symbol = _format_symbol(str(payload.get("symbol") or ""))
strategy = str(payload.get("strategy") or "")
confidence = float(payload.get("confidence") or 0.0) confidence = float(payload.get("confidence") or 0.0)
repeat_count = int(payload.get("repeat_count") or 0) position_context = str(payload.get("position_context") or "NONE").upper()
leverage = payload.get("leverage") semantic_lines = payload.get("semantic_lines") or []
reason = str(payload.get("reason") or "")
position_context = str(payload.get("position_context") or "NONE")
priority = str(event.priority or _alert_priority( priority = str(event.priority or _alert_priority(
confidence=confidence, confidence=confidence,
repeat_count=repeat_count, repeat_count=int(payload.get("repeat_count") or 0),
)).upper() )).upper()
icon = _signal_icon(signal) direction = _signal_direction(signal)
symbol_text = _format_symbol(symbol) icon = _direction_icon(direction)
leverage_text = _format_leverage(leverage) strength = _strength_label(priority)
priority_text = _priority_label(priority) strength_bar = _strength_bar(priority)
text = ( lines = [
f"<b>{priority_text} · {icon} {signal}</b>\n\n" f"<b>Сигнал {icon} {symbol} · {direction}</b>",
f"{symbol_text} · {strategy} · {leverage_text}\n" "",
f"Position: {position_context}\n\n" ]
f"🧠 Confidence: {confidence:.2f}\n"
f"🔁 Repeats: {repeat_count}\n\n" if position_context not in {"NONE", "", ""} and position_context != direction:
f"💡 Причина:\n" lines.extend([
f"{reason}" "⚠️ ПРОТИВ ПОЗИЦИИ",
) "",
])
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( return NotificationMessage(
title=event.title, title=event.title,
text=text, text="\n".join(lines),
priority=priority.lower(), priority=priority.lower(),
dedupe_key=event.dedupe_key or _dedupe_key(payload), dedupe_key=event.dedupe_key or _dedupe_key(payload),
) )
@@ -60,34 +67,49 @@ def _alert_priority(*, confidence: float, repeat_count: int) -> str:
return "LOW" return "LOW"
def _priority_label(priority: str) -> str: def _strength_label(priority: str) -> str:
mapping = { mapping = {
"HIGH": "🚨 HIGH", "HIGH": "Сильный",
"MEDIUM": "⚡ MEDIUM", "MEDIUM": "Средний",
"LOW": " LOW", "LOW": "Слабый",
} }
return mapping.get(priority.upper(), priority) return mapping.get(priority.upper(), priority)
def _signal_icon(signal: str) -> str: def _strength_bar(priority: str) -> str:
mapping = { mapping = {
"BUY": "🟢", "HIGH": "●●●",
"SELL": "🔴", "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: def _format_symbol(symbol: str) -> str:
if not symbol or symbol == "": if not symbol or symbol == "":
return "" return ""
base_symbol = symbol.split("_", 1)[0] return symbol.split("_", 1)[0].split("/", 1)[0].upper()
parts = base_symbol.split("/", 1)
if len(parts) == 2:
return f"{parts[0]} / {parts[1]}"
return base_symbol
def _format_leverage(leverage: object) -> str: def _format_leverage(leverage: object) -> str:

View File

@@ -11,6 +11,7 @@ class RuntimeEventType(str, Enum):
POSITION_OPENED = "position_opened" POSITION_OPENED = "position_opened"
POSITION_CLOSED = "position_closed" POSITION_CLOSED = "position_closed"
POSITION_FLIPPED = "position_flipped" POSITION_FLIPPED = "position_flipped"
POSITION_FLIP_BLOCKED = "position_flip_blocked"
EXECUTION_BLOCKED = "execution_blocked" EXECUTION_BLOCKED = "execution_blocked"
RISK_ALERT = "risk_alert" RISK_ALERT = "risk_alert"

View File

@@ -8,6 +8,7 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from src.telegram.handlers.auto.ui import ( from src.telegram.handlers.auto.ui import (
auto_diagnostics_keyboard,
auto_keyboard, auto_keyboard,
build_auto_text, build_auto_text,
is_auto_configured, 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.telegram.live.active_screen import ActiveScreenManager
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.diagnostics.formatter import SemanticDiagnosticFormatter
from src.trading.diagnostics.snapshot import SemanticDiagnosticSnapshotBuilder
router = Router(name="auto") router = Router(name="auto")
@@ -88,6 +91,58 @@ async def _prepare_auto_from_callback(callback: CallbackQuery) -> bool:
return True 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_({"🤖 Автоторговля", "🤖 Авто"})) @router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
async def open_auto(message: Message, state: FSMContext) -> None: async def open_auto(message: Message, state: FSMContext) -> None:
await state.clear() await state.clear()
@@ -173,4 +228,21 @@ async def auto_stop(callback: CallbackQuery) -> None:
await _prepare_auto_from_callback(callback) await _prepare_auto_from_callback(callback)
await render_auto_screen(callback.message, edit_mode=True) 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()

View File

@@ -15,15 +15,45 @@ from src.trading.auto.service import AutoTradeService
def auto_keyboard() -> InlineKeyboardMarkup: def auto_keyboard() -> InlineKeyboardMarkup:
state = AutoTradeService().get_state()
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.button(text="▶️ Start", callback_data="auto:start") status = (state.status or "").upper()
builder.button(text="👀 Watch", callback_data="auto:observe")
builder.button(text="🛑 Stop", callback_data="auto:stop") 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="settings:auto")
builder.button(text="🧯 Защита", callback_data="auto:risk") 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() return builder.as_markup()
@@ -281,7 +311,6 @@ def _build_waiting_text(state) -> str:
_order_header_line(state), _order_header_line(state),
f"{_price_label_for_signal(state)} · {_format_plain_or_dash(price)}", f"{_price_label_for_signal(state)} · {_format_plain_or_dash(price)}",
_estimated_size_text(state, price), _estimated_size_text(state, price),
_adaptive_size_line(state),
_max_reserved_line(state, price), _max_reserved_line(state, price),
_effective_risk_line(state), _effective_risk_line(state),
] ]
@@ -346,7 +375,6 @@ def _build_active_position_text(state) -> str:
"", "",
f"Размер · {_format_crypto_size(size)}", f"Размер · {_format_crypto_size(size)}",
f"Позиция · {_format_money_compact(notional)}", f"Позиция · {_format_money_compact(notional)}",
_adaptive_size_line(state),
f"Вход · {_format_plain_or_dash(state.entry_price)}", f"Вход · {_format_plain_or_dash(state.entry_price)}",
f"Цена · {_format_plain_or_dash(price_for_calc)}", f"Цена · {_format_plain_or_dash(price_for_calc)}",
"", "",

View File

@@ -18,6 +18,8 @@ from src.runtime_events.publisher import RuntimeEventPublisher
from src.trading.auto.service import AutoTradeService from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService from src.trading.journal.service import JournalService
from src.telegram.handlers.auto.ui import build_auto_semantic_text 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: class AutoTradeRunner:
@@ -269,13 +271,15 @@ class AutoTradeRunner:
if signal not in {"BUY", "SELL"}: if signal not in {"BUY", "SELL"}:
return return
if cls._is_position_aligned_signal(state=state, signal=signal): # Если сигнал совпадает с открытой позицией, не публикуем событие,
cls._log_position_aligned_signal_suppressed( # чтобы не создавать избыточные уведомления
state=state, #if cls._is_position_aligned_signal(state=state, signal=signal):
payload=payload, # cls._log_position_aligned_signal_suppressed(
signal=signal, # state=state,
) # payload=payload,
return # signal=signal,
# )
# return
cls._publish_strong_signal_event(state=state, payload=payload) cls._publish_strong_signal_event(state=state, payload=payload)
return return
@@ -284,6 +288,7 @@ class AutoTradeRunner:
"paper_position_opened", "paper_position_opened",
"paper_position_closed", "paper_position_closed",
"paper_position_flipped", "paper_position_flipped",
"paper_flip_blocked",
}: }:
cls._publish_execution_event( cls._publish_execution_event(
state=state, state=state,
@@ -292,6 +297,18 @@ class AutoTradeRunner:
) )
return 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 @classmethod
def _is_position_aligned_signal(cls, *, state, signal: str) -> bool: def _is_position_aligned_signal(cls, *, state, signal: str) -> bool:
position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper() position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
@@ -390,6 +407,8 @@ class AutoTradeRunner:
"reason": reason, "reason": reason,
"position_context": position_context, "position_context": position_context,
"decision_status": state.decision_status, "decision_status": state.decision_status,
"semantic_lines": cls._notification_reason_lines(state),
"position_side": position_context,
}, },
priority=priority.lower(), priority=priority.lower(),
dedupe_key=( dedupe_key=(
@@ -423,6 +442,8 @@ class AutoTradeRunner:
old_side = str(payload.get("old_side") or "") old_side = str(payload.get("old_side") or "")
new_side = str(payload.get("new_side") or side or "") new_side = str(payload.get("new_side") or side or "")
semantic_lines = cls._notification_reason_lines(state)
RuntimeEventPublisher.publish( RuntimeEventPublisher.publish(
RuntimeEvent( RuntimeEvent(
event_type=runtime_event_type, event_type=runtime_event_type,
@@ -436,6 +457,8 @@ class AutoTradeRunner:
"new_side": new_side, "new_side": new_side,
"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,
**payload, **payload,
"strategy": state.strategy,
"semantic_lines": semantic_lines,
}, },
priority="normal", priority="normal",
dedupe_key=cls._execution_dedupe_key( dedupe_key=cls._execution_dedupe_key(
@@ -451,6 +474,7 @@ class AutoTradeRunner:
"paper_position_opened": RuntimeEventType.POSITION_OPENED, "paper_position_opened": RuntimeEventType.POSITION_OPENED,
"paper_position_closed": RuntimeEventType.POSITION_CLOSED, "paper_position_closed": RuntimeEventType.POSITION_CLOSED,
"paper_position_flipped": RuntimeEventType.POSITION_FLIPPED, "paper_position_flipped": RuntimeEventType.POSITION_FLIPPED,
"paper_flip_blocked": RuntimeEventType.POSITION_FLIP_BLOCKED,
} }
return mapping.get(event_type) return mapping.get(event_type)
@@ -460,6 +484,7 @@ class AutoTradeRunner:
RuntimeEventType.POSITION_OPENED: "Paper position opened", RuntimeEventType.POSITION_OPENED: "Paper position opened",
RuntimeEventType.POSITION_CLOSED: "Paper position closed", RuntimeEventType.POSITION_CLOSED: "Paper position closed",
RuntimeEventType.POSITION_FLIPPED: "Paper position flipped", RuntimeEventType.POSITION_FLIPPED: "Paper position flipped",
RuntimeEventType.POSITION_FLIP_BLOCKED: "Flip blocked",
} }
return mapping.get(event_type, "Paper execution event") return mapping.get(event_type, "Paper execution event")

View File

@@ -230,6 +230,12 @@ class AutoTradeService:
state.status = "RUNNING" state.status = "RUNNING"
self._reset_signal_tracking() 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.last_signal = "HOLD"
state.signal_started_at = time.monotonic() state.signal_started_at = time.monotonic()
@@ -261,6 +267,12 @@ class AutoTradeService:
) )
if previous_status == "OFF": 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, "Включён режим наблюдения."
return state, "Автоторговля переведена в режим наблюдения." return state, "Автоторговля переведена в режим наблюдения."
@@ -275,6 +287,12 @@ class AutoTradeService:
return state, "Автоторговля уже выключена." return state, "Автоторговля уже выключена."
state.status = "OFF" 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() self.stop_loop()
EventBus.emit( EventBus.emit(

View File

@@ -94,6 +94,18 @@ class AutoTradeState:
# зафиксированный результат закрытых paper-сделок # зафиксированный результат закрытых paper-сделок
realized_pnl_usd: float = 0.0 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-действие # последнее execution-действие
last_execution_action: str | None = None last_execution_action: str | None = None

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import time
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -227,6 +228,13 @@ class ExecutionEngine:
) )
state.realized_pnl_usd += pnl 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_side = position.side
old_entry_price = position.entry_price 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) pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
state.realized_pnl_usd += pnl state.realized_pnl_usd += pnl
state.cycle_realized_pnl_usd += pnl
now = self._now_time() now = self._now_time()
@@ -404,6 +413,7 @@ class ExecutionEngine:
f"Позиция закрыта по правилу защиты: {forced_reason}.", f"Позиция закрыта по правилу защиты: {forced_reason}.",
) )
return ExecutionDecision("CLOSE", True, "Позиция закрыта.") return ExecutionDecision("CLOSE", True, "Позиция закрыта.")
def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None: def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:

View File

@@ -110,6 +110,9 @@ class TrendStrategy:
if len(prices) > self._window_size: if len(prices) > self._window_size:
prices.pop(0) prices.pop(0)
market_phase = self._normalized_market_phase(market)
market_phase_direction = self._normalized_market_phase_direction(market)
base_payload = { base_payload = {
"strategy": self.name, "strategy": self.name,
"symbol": symbol, "symbol": symbol,
@@ -125,8 +128,8 @@ class TrendStrategy:
"market_analysis": market.payload, "market_analysis": market.payload,
"market_trend_strength": market.trend_strength.value, "market_trend_strength": market.trend_strength.value,
"market_trend_quality": market.trend_quality.value, "market_trend_quality": market.trend_quality.value,
"market_phase": market.market_phase.value, "market_phase": market_phase,
"market_phase_direction": market.phase_direction.value, "market_phase_direction": market_phase_direction,
"market_phase_change_percent": market.phase_change_percent, "market_phase_change_percent": market.phase_change_percent,
"market_phase_direction_consistency": market.payload.get("market_phase_direction_consistency"), "market_phase_direction_consistency": market.payload.get("market_phase_direction_consistency"),
"market_phase_reason": market.phase_reason, "market_phase_reason": market.phase_reason,
@@ -305,7 +308,10 @@ class TrendStrategy:
momentum_direction = getattr(market, "momentum_direction", TrendDirection.UNKNOWN) momentum_direction = getattr(market, "momentum_direction", TrendDirection.UNKNOWN)
momentum_strength = float(getattr(market, "momentum_strength", 0.0) or 0.0) 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( return SignalResult(
signal=SignalType.BUY, signal=SignalType.BUY,
reason="BREAKOUT_UP подтверждён momentum/breakout semantic layer.", 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( return SignalResult(
signal=SignalType.SELL, signal=SignalType.SELL,
reason="BREAKOUT_DOWN подтверждён momentum/breakout semantic layer.", 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 return None
def _calculate_breakout_confidence(self, momentum_strength: float) -> float: def _calculate_breakout_confidence(self, momentum_strength: float) -> float:
@@ -384,6 +424,31 @@ class TrendStrategy:
return down_moves / total_moves 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( def _calculate_confidence(
self, self,
change_percent: float, change_percent: float,

View File

@@ -1205,6 +1205,58 @@
- diagnostic layer подготовлен к Diagnostic Journal Layer - diagnostic layer подготовлен к Diagnostic Journal Layer
- diagnostic layer подготовлен к Auto-refresh Diagnostic UI - 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 ### 07.4.5

View File

@@ -1131,6 +1131,58 @@
- execution runtime подготовлен к semantic breakout routing - execution runtime подготовлен к semantic breakout routing
- execution runtime подготовлен к AI-driven momentum interpretation - 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 ### 07.4.4.1.10 Semantic Runtime Diagnostics & Observability

View 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.