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
|
||||||
|
|
||||||
# app/src/core/event_titles.py
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(".")
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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)}",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
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
|
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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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