Stage 07.4.3.8 — Telegram execution alerts

This commit is contained in:
2026-05-04 19:52:35 +03:00
parent d8c077d066
commit 1253cda003
6 changed files with 358 additions and 122 deletions

View File

@@ -52,6 +52,34 @@ def format_usd_amount(value: float) -> str:
return f"{value:,.2f}".replace(",", " ")
def format_usd_price(value: float | int | str | None) -> str:
if value is None:
return ""
try:
return f"{float(value):,.2f}".replace(",", " ")
except (TypeError, ValueError):
return ""
def format_usd_pnl(value: float | int | str | None) -> str:
if value is None:
return ""
try:
pnl = float(value)
except (TypeError, ValueError):
return ""
if pnl > 0:
return f"🟢 +{format_usd_price(pnl)} USD"
if pnl < 0:
return f"🔴 -{format_usd_price(abs(pnl))} USD"
return f"{format_usd_price(0)} USD"
def render_currency_line(
*,
currency: str,

View File

@@ -13,6 +13,7 @@ from src.core.event_bus import EventBus
from src.integrations.exchange.market_data_runner import MarketDataRunner
from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
from src.telegram.ui.currency_ui import format_usd_pnl, format_usd_price
class AutoTradeRunner:
@@ -497,10 +498,11 @@ class AutoTradeRunner:
@classmethod
def _format_price(cls, value: object) -> str:
try:
return f"{float(value):.2f}"
except (TypeError, ValueError):
return ""
return format_usd_price(value)
@classmethod
def _format_pnl(cls, value: object) -> str:
return format_usd_pnl(value)
@classmethod
def _format_size(cls, value: object) -> str:
@@ -509,16 +511,6 @@ class AutoTradeRunner:
except (TypeError, ValueError):
return ""
@classmethod
def _format_pnl(cls, value: object) -> str:
try:
pnl = float(value)
except (TypeError, ValueError):
return ""
prefix = "+" if pnl > 0 else ""
return f"{prefix}{pnl:.4f} USD"
@classmethod
async def _refresh_screen(cls, *, force: bool = False) -> None:
now = time.monotonic()

View File

@@ -15,54 +15,31 @@ from src.trading.position.state import PositionState
class ExecutionEngine:
_position = PositionState()
# получить текущую paper-позицию
def get_position(self) -> PositionState:
return type(self)._position
# обработать состояние автоторговли и принять paper-execution решение
def process(self, state: AutoTradeState) -> ExecutionDecision:
self._sync_state_from_position(state)
if state.status != "RUNNING":
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Execution доступен только в режиме RUNNING.",
)
return ExecutionDecision("NONE", False, "Execution доступен только в режиме RUNNING.")
self._update_unrealized_pnl(state)
if state.decision_status != "READY" or not state.is_signal_ready:
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Сигнал ещё не готов к execution.",
)
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
if self._should_close_position(state):
return self._close_position(state)
if state.last_signal == "BUY":
return self._open_position_if_empty(
state=state,
side="LONG",
action="OPEN_LONG",
)
return self._open_position_if_empty(state=state, side="LONG", action="OPEN_LONG")
if state.last_signal == "SELL":
return self._open_position_if_empty(
state=state,
side="SHORT",
action="OPEN_SHORT",
)
return self._open_position_if_empty(state=state, side="SHORT", action="OPEN_SHORT")
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Нет торгового действия.",
)
return ExecutionDecision("NONE", False, "Нет торгового действия.")
# открыть paper-позицию, если позиции ещё нет
def _open_position_if_empty(
self,
*,
@@ -74,21 +51,13 @@ class ExecutionEngine:
if position.side != "NONE":
self._sync_state_from_position(state)
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Позиция уже открыта.",
)
return ExecutionDecision("NONE", False, "Позиция уже открыта.")
try:
ticker = ExchangeService().get_price(state.symbol)
entry_price = ticker.price
except Exception as exc:
return ExecutionDecision(
action="NONE",
can_execute=False,
reason=f"Не удалось получить цену для paper execution: {exc}",
)
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
now = self._now_time()
size = self._calculate_position_size(state)
@@ -106,102 +75,82 @@ class ExecutionEngine:
self._sync_state_from_position(state)
payload = {
"execution_type": "ENTRY",
"action": action,
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": size,
"leverage": state.leverage,
"signal": state.last_signal,
"confidence": state.last_signal_confidence,
"repeat_count": state.last_signal_repeat_count,
"reason": state.last_signal_reason,
"opened_at": now,
}
JournalService().log_ui_info(
event_type="paper_position_opened",
message=f"Paper-позиция открыта: {side} {state.symbol}",
message=f"Paper ENTRY открыта: {side} {state.symbol}",
screen="auto",
action="paper_execution",
payload={
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": size,
"leverage": state.leverage,
"signal": state.last_signal,
"confidence": state.last_signal_confidence,
},
payload=payload,
)
EventBus.emit(
"paper_position_opened",
{
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": size,
"leverage": state.leverage,
},
)
EventBus.emit("paper_position_opened", payload)
return ExecutionDecision(
action=action,
can_execute=True,
reason=f"Paper-позиция {side} открыта.",
)
return ExecutionDecision(action, True, f"Paper ENTRY {side} открыта.")
# закрыть текущую paper-позицию
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
position = type(self)._position
if position.side == "NONE":
self._sync_state_from_position(state)
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Нет открытой позиции для закрытия.",
)
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
try:
ticker = ExchangeService().get_price(state.symbol)
exit_price = ticker.price
except Exception as exc:
return ExecutionDecision(
action="NONE",
can_execute=False,
reason=f"Ошибка получения цены для закрытия: {exc}",
)
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
pnl = self._calculate_pnl(exit_price)
now = self._now_time()
payload = {
"execution_type": "EXIT",
"action": "CLOSE",
"symbol": state.symbol,
"side": position.side,
"entry_price": position.entry_price,
"exit_price": exit_price,
"size": position.size,
"leverage": position.leverage,
"pnl": pnl,
"signal": state.last_signal,
"confidence": state.last_signal_confidence,
"repeat_count": state.last_signal_repeat_count,
"reason": state.last_signal_reason,
"opened_at": position.opened_at,
"closed_at": now,
}
JournalService().log_ui_info(
event_type="paper_position_closed",
message=f"Позиция закрыта: {position.side} {state.symbol}",
message=f"Paper EXIT закрыта: {position.side} {state.symbol}",
screen="auto",
action="paper_execution",
payload={
"symbol": state.symbol,
"side": position.side,
"entry_price": position.entry_price,
"exit_price": exit_price,
"size": position.size,
"leverage": position.leverage,
"pnl": pnl,
},
payload=payload,
)
EventBus.emit(
"paper_position_closed",
{
"symbol": state.symbol,
"side": position.side,
"entry_price": position.entry_price,
"exit_price": exit_price,
"size": position.size,
"leverage": position.leverage,
"pnl": pnl,
},
)
EventBus.emit("paper_position_closed", payload)
type(self)._position = PositionState()
self._sync_state_from_position(state)
return ExecutionDecision(
action="CLOSE",
can_execute=True,
reason="Позиция закрыта.",
)
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
# проверить, нужно ли закрывать позицию по противоположному сигналу
def _should_close_position(self, state: AutoTradeState) -> bool:
position = type(self)._position
@@ -216,7 +165,6 @@ class ExecutionEngine:
return False
# обновить unrealized PnL по текущей цене
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
position = type(self)._position
@@ -236,14 +184,11 @@ class ExecutionEngine:
self._sync_state_from_position(state)
# временный расчёт размера позиции для paper mode
def _calculate_position_size(self, state: AutoTradeState) -> float:
risk_percent = state.risk_percent or 0.0
leverage = state.leverage or 1.0
return round((risk_percent * leverage) / 100, 8)
# расчёт PnL для paper-позиции
def _calculate_pnl(self, current_price: float) -> float:
position = type(self)._position
@@ -258,7 +203,6 @@ class ExecutionEngine:
return 0.0
# синхронизировать AutoTradeState с текущей paper-позицией
def _sync_state_from_position(self, state: AutoTradeState) -> None:
position = type(self)._position
@@ -267,6 +211,5 @@ class ExecutionEngine:
state.position_size = position.size
state.unrealized_pnl_usd = position.unrealized_pnl_usd
# текущее время для paper execution
def _now_time(self) -> str:
return datetime.now().strftime("%H:%M:%S")