Stage 07.4.3.5 — Debug commands & test mode
This commit is contained in:
@@ -147,6 +147,11 @@ class AutoTradeRunner:
|
||||
|
||||
await asyncio.sleep(cls._analysis_interval_seconds)
|
||||
|
||||
@classmethod
|
||||
async def process_last_event_now(cls) -> None:
|
||||
state = AutoTradeService().get_state()
|
||||
await cls._handle_important_event(state)
|
||||
|
||||
@classmethod
|
||||
async def _handle_important_event(cls, state) -> None:
|
||||
event_type, payload = EventBus.last_event()
|
||||
@@ -178,7 +183,8 @@ class AutoTradeRunner:
|
||||
|
||||
alert_key = (
|
||||
f"{symbol}:{strategy}:{signal}:"
|
||||
f"{repeat_count}:{confidence:.2f}:{state.decision_status}"
|
||||
f"{repeat_count}:{confidence:.2f}:"
|
||||
f"{state.decision_status}:{reason}"
|
||||
)
|
||||
|
||||
if alert_key == cls._last_strong_alert_key:
|
||||
|
||||
@@ -32,6 +32,59 @@ class AutoTradeService:
|
||||
_last_signal_payload: dict | None = None
|
||||
_same_signal_count = 0
|
||||
|
||||
# debug: принудительно выставить сигнал и decision
|
||||
def debug_force_signal(
|
||||
self,
|
||||
*,
|
||||
signal: str,
|
||||
confidence: float = 0.9,
|
||||
repeat_count: int = 2,
|
||||
reason: str = "DEBUG SIGNAL",
|
||||
) -> AutoTradeState:
|
||||
state = self.get_state()
|
||||
|
||||
normalized_signal = signal.strip().upper()
|
||||
if normalized_signal not in {"BUY", "SELL", "HOLD"}:
|
||||
normalized_signal = "HOLD"
|
||||
|
||||
previous_signal = state.last_signal
|
||||
previous_decision_status = state.decision_status
|
||||
|
||||
state.last_signal = normalized_signal
|
||||
state.last_signal_repeat_count = repeat_count
|
||||
state.last_signal_confidence = confidence
|
||||
state.last_signal_reason = reason
|
||||
|
||||
if normalized_signal == "HOLD":
|
||||
state.decision_status = "WAITING"
|
||||
state.decision_reason = "Debug HOLD."
|
||||
state.is_signal_confirmed = False
|
||||
state.is_signal_ready = False
|
||||
else:
|
||||
state.decision_status = "READY"
|
||||
state.decision_reason = "Debug READY signal."
|
||||
state.is_signal_confirmed = True
|
||||
state.is_signal_ready = True
|
||||
|
||||
EventBus.emit(
|
||||
"auto_decision_changed",
|
||||
{
|
||||
"previous_signal": previous_signal,
|
||||
"previous_decision_status": previous_decision_status,
|
||||
"decision_status": state.decision_status,
|
||||
"signal": state.last_signal,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"confidence": state.last_signal_confidence,
|
||||
"symbol": state.symbol,
|
||||
"strategy": state.strategy,
|
||||
"leverage": state.leverage,
|
||||
"reason": state.last_signal_reason,
|
||||
"debug": True,
|
||||
},
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
# получить текущее состояние автоторговли
|
||||
def get_state(self) -> AutoTradeState:
|
||||
if not self._state.symbol:
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user