Files
dzentra_bot/app/src/notifications/templates/execution.py

324 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app/src/notifications/templates/execution.py
from __future__ import annotations
from src.notifications.models import NotificationMessage
from src.runtime_events.event_types import RuntimeEventType
from src.runtime_events.models import RuntimeEvent
from src.core.numbers import safe_float
def build_execution_notification(event: RuntimeEvent) -> NotificationMessage | None:
if event.event_type == RuntimeEventType.POSITION_OPENED:
return _build_position_opened(event)
if event.event_type == RuntimeEventType.POSITION_CLOSED:
return _build_position_closed(event)
if event.event_type == RuntimeEventType.POSITION_FLIPPED:
return _build_position_flipped(event)
if event.event_type == RuntimeEventType.POSITION_FLIP_BLOCKED:
return _build_flip_blocked(event)
return None
def _build_position_opened(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload
symbol = _format_symbol(payload.get("symbol"))
strategy = str(payload.get("strategy") or "").title()
side_raw = str(payload.get("side") or "").upper()
side = side_raw.title()
leverage = _format_leverage(payload.get("leverage"))
entry_price = _format_price(payload.get("entry_price"))
size = _format_size(payload.get("size"))
confidence = float(payload.get("confidence") or 0.0)
priority = _alert_priority(
confidence=confidence,
repeat_count=int(payload.get("repeat_count") or 0),
)
semantic_lines = payload.get("semantic_lines") or []
side_icon = "🟢" if side_raw == "LONG" else "🔴"
lines = [
"<b>🧾 Позиция открыта</b>",
"",
f"{side_icon} {symbol} · {strategy} · {side} {leverage}",
f"Вход: ${entry_price}",
f"Размер: {size}",
f"Объём: {_format_notional(entry_price=payload.get('entry_price'), size=payload.get('size'))}",
"",
f"{_strength_bar(priority)} Сигнал {_strength_label(priority).lower()} · {confidence:.2f}",
]
if semantic_lines:
lines.extend(
str(line).strip().rstrip(".")
for line in semantic_lines
if str(line).strip()
)
return NotificationMessage(
title=event.title,
text="\n".join(lines),
priority=event.priority,
dedupe_key=event.dedupe_key,
)
def _build_position_closed(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload
symbol = _format_symbol(payload.get("symbol"))
side = str(payload.get("side") or "").title()
leverage = _format_leverage(payload.get("leverage"))
entry_price = _format_price(payload.get("entry_price"))
exit_price = _format_price(payload.get("exit_price"))
size = _format_size(payload.get("size"))
pnl_value = float(payload.get("pnl") or 0.0)
pnl_text = _format_pnl_amount(pnl_value)
risk_reason = _human_close_reason(payload.get("risk_reason"))
pnl_icon = "🟢" if pnl_value >= 0 else "🔴"
pnl_label = "Прибыль" if pnl_value >= 0 else "Убыток"
lines = [
"<b>🧾 Сделка закрыта</b>",
f"{pnl_icon} {pnl_label} · {pnl_text}",
"",
f"{symbol} · {side} {leverage}",
f"Вход: ${entry_price}",
f"Выход: ${exit_price}",
f"Размер: {size}",
]
if risk_reason:
lines.extend([
"",
f"Закрытие по {risk_reason}",
])
return NotificationMessage(
title=event.title,
text="\n".join(lines),
priority=event.priority,
dedupe_key=event.dedupe_key,
)
def _format_pnl_amount(value: float) -> str:
amount = f"$ {abs(value):,.2f}".replace(",", " ").rstrip("0").rstrip(".")
if value > 0:
return f"+{amount}"
if value < 0:
return f"{amount}"
return "$ 0"
def _human_close_reason(value: object) -> str:
mapping = {
"STOP_LOSS": "Stop Loss",
"TAKE_PROFIT": "Take Profit",
"MAX_LOSS": "Max Loss",
}
return mapping.get(str(value or ""), "")
def _build_position_flipped(event: RuntimeEvent) -> NotificationMessage:
payload = event.payload
symbol = _format_symbol(payload.get("symbol"))
strategy = str(payload.get("strategy") or "").title()
old_side_raw = str(payload.get("old_side") or "").upper()
new_side_raw = str(
payload.get("new_side") or payload.get("side") or ""
).upper()
old_side = old_side_raw.title()
new_side = new_side_raw.title()
old_leverage = _format_leverage(
payload.get("old_leverage")
if payload.get("old_leverage") is not None
else payload.get("leverage")
)
new_leverage = _format_leverage(payload.get("leverage"))
entry_price = _format_price(payload.get("entry_price"))
exit_price = _format_price(payload.get("exit_price"))
new_entry_price = _format_price(payload.get("new_entry_price"))
old_size = _format_size(payload.get("old_size"))
new_size = _format_size(payload.get("new_size"))
pnl_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_raw == "LONG" else "🔴"
new_icon = "🟢" if new_side_raw == "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 "").title()
target_side = "Long" if signal == "BUY" else "Short" if signal == "SELL" else ""
icon = "🟢" if target_side == "LONG" else "🔴" if target_side == "SHORT" else ""
text = (
f"<b>⚠️ Flip отменён</b>\n\n"
f"{icon} {symbol} · {target_side}\n"
f"Текущая позиция: {position_side}\n\n"
f"Недостаточно условий для разворота\n"
f"{reason}\n"
f"Сила сигнала: {confidence:.2f}"
)
return NotificationMessage(
title=event.title,
text=text,
priority=event.priority,
dedupe_key=event.dedupe_key,
)
def _format_symbol(value: object) -> str:
symbol = str(value or "")
if not symbol or symbol == "":
return ""
return symbol.split("_", 1)[0].split("/", 1)[0].upper()
def _format_leverage(value: object) -> str:
number = safe_float(value)
if number is None:
return ""
return f"x{number:g}"
def _format_price(value: object) -> str:
number = safe_float(value)
if number is None:
return ""
return f"{number:,.2f}".replace(",", " ")
def _format_size(value: object) -> str:
number = safe_float(value)
if number is None:
return ""
return f"{number:.8f}".rstrip("0").rstrip(".")
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:
entry = safe_float(entry_price)
amount = safe_float(size)
if entry is None or amount is None:
return ""
value = entry * amount
return f"$ {value:,.2f}".replace(",", " ").rstrip("0").rstrip(".")