Stage 07.4.4.1.14 — Execution refactoring and runtime semantics

This commit is contained in:
2026-07-02 16:25:18 +03:00
parent d9e6392e28
commit e08c604ff5
72 changed files with 13626 additions and 4233 deletions

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
from src.core.numbers import safe_float
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:
@@ -17,7 +17,7 @@ def build_execution_notification(event: RuntimeEvent) -> NotificationMessage | N
if event.event_type == RuntimeEventType.POSITION_FLIPPED:
return _build_position_flipped(event)
if event.event_type == RuntimeEventType.POSITION_FLIP_BLOCKED:
return _build_flip_blocked(event)
@@ -28,39 +28,46 @@ 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()
side_icon = _side_icon(side_raw)
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)
signal = str(payload.get("signal") or "").upper()
confidence = safe_float(payload.get("confidence")) or 0.0
repeat_count = int(safe_float(payload.get("repeat_count")) or 0)
priority = _alert_priority(
confidence=confidence,
repeat_count=int(payload.get("repeat_count") or 0),
repeat_count=repeat_count,
)
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}",
f"🧾 Открытие · <b>{symbol}</b> {side_icon} {side}",
f"{_strength_bar(priority)} {_strength_label(priority)} · {confidence:.2f}",
f"Серия {signal} · ×{repeat_count}",
]
if semantic_lines:
if isinstance(semantic_lines, list):
lines.extend(
str(line).strip().rstrip(".")
for line in semantic_lines
if str(line).strip()
)
lines.extend(
[
f"Цена входа · ${entry_price}",
f"Размер · {size}",
f"Плечо · {leverage}",
]
)
return NotificationMessage(
title=event.title,
text="\n".join(lines),
@@ -73,14 +80,14 @@ 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"))
side_raw = str(payload.get("side") or "").upper()
side = side_raw.title()
side_icon = _side_icon(side_raw)
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_value = safe_float(payload.get("pnl")) or 0.0
pnl_text = _format_pnl_amount(pnl_value)
risk_reason = _human_close_reason(payload.get("risk_reason"))
@@ -89,20 +96,14 @@ def _build_position_closed(event: RuntimeEvent) -> NotificationMessage:
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}",
f"💰 Закрытие · <b>{symbol}</b> {side_icon} {side}",
f"<b>{pnl_label}</b> {pnl_icon} {pnl_text}",
f"Вход · ${entry_price}",
f"Выход · ${exit_price}",
]
if risk_reason:
lines.extend([
"",
f"Закрытие по {risk_reason}",
])
lines.append(f"Причина · {risk_reason}")
return NotificationMessage(
title=event.title,
@@ -112,95 +113,51 @@ def _build_position_closed(event: RuntimeEvent) -> NotificationMessage:
)
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()
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"))
old_icon = _side_icon(old_side_raw)
new_icon = _side_icon(new_side_raw)
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_value = safe_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 "🔴"
signal = str(payload.get("signal") or "").upper()
confidence = safe_float(payload.get("confidence")) or 0.0
repeat_count = int(safe_float(payload.get("repeat_count")) or 0)
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"🔄 Разворот · <b>{symbol}</b> {old_icon} {old_side}{new_icon} {new_side}",
f"<b>{pnl_label}</b> {pnl_icon} {pnl_text}",
f"Закрытие · ${exit_price}",
f"Новый вход · ${new_entry_price}",
"",
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}",
f"{_strength_bar(priority)} {_strength_label(priority)} · {confidence:.2f}",
f"Серия {signal} · ×{repeat_count}",
]
if semantic_lines:
if isinstance(semantic_lines, list):
lines.extend(
str(line).strip().rstrip(".")
for line in semantic_lines
@@ -220,20 +177,25 @@ def _build_flip_blocked(event: RuntimeEvent) -> NotificationMessage:
symbol = _format_symbol(payload.get("symbol"))
signal = str(payload.get("signal") or "").upper()
confidence = float(payload.get("confidence") or 0.0)
confidence = safe_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 ""
if signal == "BUY":
target_side = "Long"
icon = "🟢"
elif signal == "SELL":
target_side = "Short"
icon = "🔴"
else:
target_side = ""
icon = "⚪️"
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}"
f"<b>Flip отменён {symbol} {icon} {target_side}</b>\n\n"
f"Текущая позиция · {position_side}\n"
f"Сила сигнала · {confidence:.2f}\n"
f"Причина · {reason}"
)
return NotificationMessage(
@@ -244,6 +206,58 @@ def _build_flip_blocked(event: RuntimeEvent) -> NotificationMessage:
)
def _side_icon(side: str) -> str:
normalized = str(side or "").upper()
if normalized == "LONG":
return "🟢"
if normalized == "SHORT":
return "🔴"
return "⚪️"
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",
"AUTONOMOUS_EXIT": "Autonomous Exit",
"TRAILING_STOP": "Trailing Stop",
"PROFIT_LOCK": "Profit Lock",
"BREAK_EVEN": "Break Even",
"LIFECYCLE_EXIT": "Lifecycle Exit",
"CONVICTION_BROKEN": "Conviction Broken",
"FATIGUE_EXIT": "Fatigue Exit",
"MOMENTUM_EXIT": "Momentum Exit",
"DEGRADATION_EXIT": "Degradation Exit",
"GIVEBACK_PROTECTION": "Giveback Protection",
"GIVEBACK_MOMENTUM_REVERSAL": "Giveback Momentum Reversal",
"GIVEBACK_FATIGUE_EXIT": "Giveback Fatigue Exit",
"GIVEBACK_REVERSAL_RISK": "Giveback Reversal Risk",
"TIME_DECAY_EXIT": "Time Decay",
"TIME_DECAY_FATIGUE_EXIT": "Time Decay Fatigue",
"TIME_DECAY_ADVERSE_MOMENTUM": "Time Decay Momentum",
"TIME_DECAY_DEGRADED_MARKET": "Time Decay Market",
"TIME_DECAY_CONTEXT_DECAY": "Time Decay Context",
}
return mapping.get(str(value or ""), "")
def _format_symbol(value: object) -> str:
symbol = str(value or "")
@@ -296,6 +310,7 @@ def _strength_label(priority: str) -> str:
"MEDIUM": "Средний",
"LOW": "Слабый",
}
return mapping.get(priority.upper(), priority)
@@ -305,20 +320,5 @@ def _strength_bar(priority: str) -> str:
"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(".")
return mapping.get(priority.upper(), "●○○")

View File

@@ -38,9 +38,27 @@ def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None
strength_bar = _strength_bar(priority)
lines = [
f"<b>Сигнал {icon} {symbol} · {direction}</b>",
f"⚡️ Сигнал · <b>{symbol}</b> {icon} {direction}",
f"{strength_bar} {strength} · {confidence:.2f}",
f"Серия {signal} · ×{repeat_count}",
]
if semantic_lines:
lines.extend(
str(line).strip().rstrip(".")
for line in semantic_lines
if str(line).strip()
)
price_lines = _market_price_lines(
direction=direction_key,
bid_price=payload.get("bid_price"),
ask_price=payload.get("ask_price"),
)
if price_lines:
lines.extend(price_lines)
position_line = _position_context_line(
signal=signal,
position_context=position_context,
@@ -49,27 +67,9 @@ def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None
if position_line:
lines.append(position_line)
price_lines = _market_price_lines(
direction=direction_key,
bid_price=payload.get("bid_price"),
ask_price=payload.get("ask_price"),
)
if price_lines:
lines.append("")
lines.extend(price_lines)
lines.extend([
"",
f"{strength_bar} {strength} · {confidence:.2f}",
])
if semantic_lines:
lines.extend(
str(line).strip().rstrip(".")
for line in semantic_lines
if str(line).strip()
)
block_lines = _execution_block_lines(payload)
if block_lines:
lines.extend(["", *block_lines])
return NotificationMessage(
title=event.title,
@@ -79,6 +79,25 @@ def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None
)
def _execution_block_lines(payload: JsonDict) -> list[str]:
title = str(payload.get("execution_block_title") or "").strip()
message = str(payload.get("execution_block_message") or "").strip()
action = str(payload.get("execution_block_action") or "").strip()
if not title or not message:
return []
lines = [
f"{title}",
message,
]
if action:
lines.append(action)
return lines
def _position_context_line(
*,
signal: str,
@@ -111,25 +130,13 @@ def _market_price_lines(
bid = _format_price_usd(bid_price)
ask = _format_price_usd(ask_price)
if bid == "" and ask == "":
return []
if direction == "LONG" and ask != "":
return [f"Цена входа · {ask} (Ask)"]
if direction == "LONG":
return [
f"Цена входа Long · {ask} (Ask)",
f"Цена Bid · {bid}",
]
if direction == "SHORT" and bid != "":
return [f"Цена входа · {bid} (Bid)"]
if direction == "SHORT":
return [
f"Цена входа Short · {bid} (Bid)",
f"Цена Ask · {ask}",
]
return [
f"Цена Bid · {bid}",
f"Цена Ask · {ask}",
]
return []
def _format_price_usd(value: NumericLike | None) -> str:
@@ -198,17 +205,8 @@ def _format_symbol(symbol: str) -> str:
return symbol.split("_", 1)[0].split("/", 1)[0].upper()
def _format_price(value: NumericLike | None) -> str:
number = safe_float(value)
if number is None:
return ""
return f"{number:,.2f}".replace(",", " ")
def _dedupe_key(payload: JsonDict) -> str:
confidence = safe_float(payload.get("confidence")) or 0.0
is_aligned_signal = bool(payload.get("is_position_aligned_signal"))
return (
f"auto_signal_ready:"
@@ -216,10 +214,8 @@ def _dedupe_key(payload: JsonDict) -> str:
f"{payload.get('symbol')}:"
f"{payload.get('strategy')}:"
f"{payload.get('signal')}:"
f"{payload.get('repeat_count')}:"
f"{confidence:.2f}:"
f"{payload.get('decision_status')}:"
f"{payload.get('reason')}"
f"aligned={is_aligned_signal}"
)