Stage 07.4.3.3 — Paper Position & Execution Engineg
This commit is contained in:
144
app/src/trading/execution/engine.py
Normal file
144
app/src/trading/execution/engine.py
Normal 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
|
||||
17
app/src/trading/execution/models.py
Normal file
17
app/src/trading/execution/models.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# app/src/trading/execution/models.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionDecision:
|
||||
# действие: NONE / OPEN_LONG / OPEN_SHORT
|
||||
action: str
|
||||
|
||||
# можно ли выполнить действие
|
||||
can_execute: bool
|
||||
|
||||
# причина решения
|
||||
reason: str
|
||||
Reference in New Issue
Block a user