Stage 07.4.3.3 — Paper Position & Execution Engineg

This commit is contained in:
2026-05-03 07:45:07 +03:00
parent bd6b40fcb2
commit 24c910fade
8 changed files with 398 additions and 8 deletions

View File

@@ -0,0 +1,144 @@
# 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()
# получить текущую paper-позицию
def get_position(self) -> PositionState:
return 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.",
)
if state.decision_status != "READY" or not state.is_signal_ready:
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Сигнал ещё не готов к execution.",
)
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(
action="NONE",
can_execute=False,
reason="Нет торгового действия.",
)
# открыть paper-позицию, если позиции ещё нет
def _open_position_if_empty(
self,
*,
state: AutoTradeState,
side: str,
action: str,
) -> ExecutionDecision:
if self._position.side != "NONE":
self._sync_state_from_position(state)
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Позиция уже открыта.",
)
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}",
)
now = datetime.now().strftime("%H:%M:%S")
self._position.side = side
self._position.symbol = state.symbol
self._position.entry_price = entry_price
self._position.size = self._calculate_position_size(state)
self._position.leverage = state.leverage
self._position.unrealized_pnl_usd = 0.0
self._position.opened_at = now
self._position.updated_at = now
self._sync_state_from_position(state)
JournalService().log_ui_info(
event_type="paper_position_opened",
message=f"Paper-позиция открыта: {side} {state.symbol}",
screen="auto",
action="paper_execution",
payload={
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": self._position.size,
"leverage": state.leverage,
"signal": state.last_signal,
"confidence": state.last_signal_confidence,
},
)
EventBus.emit(
"paper_position_opened",
{
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": self._position.size,
"leverage": state.leverage,
},
)
return ExecutionDecision(
action=action,
can_execute=True,
reason=f"Paper-позиция {side} открыта.",
)
# временный расчёт размера позиции для 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)
# синхронизировать AutoTradeState с текущей paper-позицией
def _sync_state_from_position(self, state: AutoTradeState) -> None:
state.position_side = self._position.side
state.entry_price = self._position.entry_price
state.position_size = self._position.size
state.unrealized_pnl_usd = self._position.unrealized_pnl_usd