Stage 07.4.3.5 — Debug commands & test mode

This commit is contained in:
2026-05-03 11:13:19 +03:00
parent af2d27761f
commit 8adfab7220
10 changed files with 917 additions and 26 deletions

View File

@@ -17,7 +17,7 @@ class ExecutionEngine:
# получить текущую paper-позицию
def get_position(self) -> PositionState:
return self._position
return type(self)._position
# обработать состояние автоторговли и принять paper-execution решение
def process(self, state: AutoTradeState) -> ExecutionDecision:
@@ -30,6 +30,8 @@ class ExecutionEngine:
reason="Execution доступен только в режиме RUNNING.",
)
self._update_unrealized_pnl(state)
if state.decision_status != "READY" or not state.is_signal_ready:
return ExecutionDecision(
action="NONE",
@@ -37,6 +39,9 @@ class ExecutionEngine:
reason="Сигнал ещё не готов к 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,
@@ -65,7 +70,9 @@ class ExecutionEngine:
side: str,
action: str,
) -> ExecutionDecision:
if self._position.side != "NONE":
position = type(self)._position
if position.side != "NONE":
self._sync_state_from_position(state)
return ExecutionDecision(
action="NONE",
@@ -83,16 +90,19 @@ class ExecutionEngine:
reason=f"Не удалось получить цену для paper execution: {exc}",
)
now = datetime.now().strftime("%H:%M:%S")
now = self._now_time()
size = self._calculate_position_size(state)
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
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)
@@ -105,7 +115,7 @@ class ExecutionEngine:
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": self._position.size,
"size": size,
"leverage": state.leverage,
"signal": state.last_signal,
"confidence": state.last_signal_confidence,
@@ -118,7 +128,7 @@ class ExecutionEngine:
"symbol": state.symbol,
"side": side,
"entry_price": entry_price,
"size": self._position.size,
"size": size,
"leverage": state.leverage,
},
)
@@ -129,6 +139,103 @@ class ExecutionEngine:
reason=f"Paper-позиция {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="Нет открытой позиции для закрытия.",
)
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}",
)
pnl = self._calculate_pnl(exit_price)
JournalService().log_ui_info(
event_type="paper_position_closed",
message=f"Позиция закрыта: {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,
},
)
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,
},
)
type(self)._position = PositionState()
self._sync_state_from_position(state)
return ExecutionDecision(
action="CLOSE",
can_execute=True,
reason="Позиция закрыта.",
)
# проверить, нужно ли закрывать позицию по противоположному сигналу
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
# обновить unrealized PnL по текущей цене
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)
# временный расчёт размера позиции для paper mode
def _calculate_position_size(self, state: AutoTradeState) -> float:
risk_percent = state.risk_percent or 0.0
@@ -136,9 +243,30 @@ class ExecutionEngine:
return round((risk_percent * leverage) / 100, 8)
# расчёт PnL для paper-позиции
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
# синхронизировать 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
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
# текущее время для paper execution
def _now_time(self) -> str:
return datetime.now().strftime("%H:%M:%S")