Files
dzentra_bot/app/src/trading/execution/engine.py

215 lines
7.2 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/trading/execution/engine.py
from __future__ import annotations
from datetime import datetime
from src.core.event_bus import EventBus
from src.integrations.exchange.service import ExchangeService
from src.trading.auto.state import AutoTradeState
from src.trading.execution.models import ExecutionDecision
from src.trading.journal.service import JournalService
from src.trading.position.state import PositionState
class ExecutionEngine:
_position = PositionState()
def get_position(self) -> PositionState:
return type(self)._position
def process(self, state: AutoTradeState) -> ExecutionDecision:
self._sync_state_from_position(state)
if state.status != "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("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")
if state.last_signal == "SELL":
return self._open_position_if_empty(state=state, side="SHORT", action="OPEN_SHORT")
return ExecutionDecision("NONE", False, "Нет торгового действия.")
def _open_position_if_empty(
self,
*,
state: AutoTradeState,
side: str,
action: str,
) -> ExecutionDecision:
position = type(self)._position
if position.side != "NONE":
self._sync_state_from_position(state)
return ExecutionDecision("NONE", False, "Позиция уже открыта.")
try:
ticker = ExchangeService().get_price(state.symbol)
entry_price = ticker.price
except Exception as exc:
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
now = self._now_time()
size = self._calculate_position_size(state)
type(self)._position = PositionState(
side=side,
symbol=state.symbol,
entry_price=entry_price,
size=size,
leverage=state.leverage,
unrealized_pnl_usd=0.0,
opened_at=now,
updated_at=now,
)
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 ENTRY открыта: {side} {state.symbol}",
screen="auto",
action="paper_execution",
payload=payload,
)
EventBus.emit("paper_position_opened", payload)
return ExecutionDecision(action, True, f"Paper ENTRY {side} открыта.")
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
position = type(self)._position
if position.side == "NONE":
self._sync_state_from_position(state)
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
try:
ticker = ExchangeService().get_price(state.symbol)
exit_price = ticker.price
except Exception as 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"Paper EXIT закрыта: {position.side} {state.symbol}",
screen="auto",
action="paper_execution",
payload=payload,
)
EventBus.emit("paper_position_closed", payload)
type(self)._position = PositionState()
self._sync_state_from_position(state)
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
def _should_close_position(self, state: AutoTradeState) -> bool:
position = type(self)._position
if position.side == "NONE":
return False
if position.side == "LONG" and state.last_signal == "SELL":
return True
if position.side == "SHORT" and state.last_signal == "BUY":
return True
return False
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
position = type(self)._position
if position.side == "NONE":
self._sync_state_from_position(state)
return
try:
ticker = ExchangeService().get_price(position.symbol or state.symbol)
current_price = ticker.price
except Exception:
self._sync_state_from_position(state)
return
position.unrealized_pnl_usd = self._calculate_pnl(current_price)
position.updated_at = self._now_time()
self._sync_state_from_position(state)
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)
def _calculate_pnl(self, current_price: float) -> float:
position = type(self)._position
entry = position.entry_price or 0.0
size = position.size or 0.0
if position.side == "LONG":
return round((current_price - entry) * size, 4)
if position.side == "SHORT":
return round((entry - current_price) * size, 4)
return 0.0
def _sync_state_from_position(self, state: AutoTradeState) -> None:
position = type(self)._position
state.position_side = position.side
state.entry_price = position.entry_price
state.position_size = position.size
state.unrealized_pnl_usd = position.unrealized_pnl_usd
def _now_time(self) -> str:
return datetime.now().strftime("%H:%M:%S")