Stage 07.4.4.1.14 — Execution refactoring and runtime semantics
This commit is contained in:
@@ -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(), "●○○")
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user