From 4d58fcf2bffe1b46ea05b3e2c983f14c007cbc4b Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 3 Jul 2026 12:21:07 +0300 Subject: [PATCH] Add flip execution diagnostics --- app/src/trading/execution/engine.py | 29 +++++++++++++- app/src/trading/execution/flip.py | 62 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/app/src/trading/execution/engine.py b/app/src/trading/execution/engine.py index 784b30f..d5bb631 100644 --- a/app/src/trading/execution/engine.py +++ b/app/src/trading/execution/engine.py @@ -52,6 +52,7 @@ class ExecutionEngine( _flip_cooldown_seconds = 45 _loss_flip_confidence = 0.75 _last_flip_block_key: str | None = None + _last_flip_diagnostic_key: str | None = None _runtime_action_cooldown_seconds = 30 _last_runtime_action_key: str | None = None _emergency_halt_drawdown_usd = 250.0 @@ -107,6 +108,18 @@ class ExecutionEngine( if protection_decision is not None: return protection_decision + # Flip diagnostics before READY / supervisor. + # Это не меняет торговую логику: только фиксирует, что противоположный + # сигнал появился при уже открытой позиции. + flip_requested = self._should_flip_position(state) + + if flip_requested: + self._log_flip_diagnostic( + state=state, + stage="FLIP_REQUESTED", + reason="opposite signal while position is open", + ) + # Signal readiness validation if state.decision_status != EXECUTION_DECISION_READY or not state.is_signal_ready: reason = ( @@ -115,6 +128,13 @@ class ExecutionEngine( f"ready={state.is_signal_ready})." ) + if flip_requested: + self._log_flip_diagnostic( + state=state, + stage="FLIP_BLOCKED_BY_READY", + reason=reason, + ) + return self._skip_execution( state, reason, @@ -123,6 +143,13 @@ class ExecutionEngine( # Execution supervisor supervisor_decision = self._process_execution_supervisor(state) if supervisor_decision is not None: + if flip_requested: + self._log_flip_diagnostic( + state=state, + stage="FLIP_BLOCKED_BY_SUPERVISOR", + reason=supervisor_decision.reason, + ) + return supervisor_decision # Existing position validation @@ -143,7 +170,7 @@ class ExecutionEngine( ) # Position flip - if self._should_flip_position(state): + if flip_requested: flip_block_reason = self._flip_block_reason(state) if flip_block_reason is not None: diff --git a/app/src/trading/execution/flip.py b/app/src/trading/execution/flip.py index bb3a55d..08fcc85 100644 --- a/app/src/trading/execution/flip.py +++ b/app/src/trading/execution/flip.py @@ -58,6 +58,7 @@ class _ExecutionFlipProtocol(Protocol): _flip_cooldown_seconds: int _loss_flip_confidence: float _last_flip_block_key: str | None + _last_flip_diagnostic_key: str | None def _create_trade_id(self, state: AutoTradeState, side: str) -> str: ... @@ -111,6 +112,67 @@ class _ExecutionFlipProtocol(Protocol): class ExecutionFlipMixin(_ExecutionFlipProtocol): + # ---------- Diagnostics ---------- + # записать диагностическое событие по пути flip без изменения торговой логики + def _log_flip_diagnostic( + self, + *, + state: AutoTradeState, + stage: str, + reason: str, + ) -> None: + position = type(self)._position + confidence = safe_float(state.last_signal_confidence) or 0.0 + repeat_count = int(safe_float(state.last_signal_repeat_count) or 0) + + key = ( + f"{stage}:" + f"{state.symbol}:" + f"{position.side}:" + f"{state.last_signal}:" + f"{repeat_count}:" + f"{confidence:.2f}:" + f"{reason}" + ) + + if key == type(self)._last_flip_diagnostic_key: + return + + type(self)._last_flip_diagnostic_key = key + + payload = { + "execution_type": "FLIP_DIAGNOSTIC", + "stage": stage, + "reason": reason, + "symbol": state.symbol, + "position_side": position.side, + "signal": state.last_signal, + "signal_confidence": confidence, + "signal_repeat_count": repeat_count, + "decision_status": state.decision_status, + "is_signal_ready": state.is_signal_ready, + "is_signal_confirmed": state.is_signal_confirmed, + "execution_confidence_score": state.execution_confidence_score, + "execution_confidence_required_score": ( + state.execution_confidence_required_score + ), + "execution_block_reason": state.execution_block_reason, + "entry_block_reason": state.entry_block_reason, + "entry_block_message": state.entry_block_message, + "unrealized_pnl_usd": state.unrealized_pnl_usd, + **build_market_context_payload(state), + } + + JournalService().log_ui_info( + event_type="position_flip_diagnostic", + message=f"Flip diagnostic: {stage} · {reason}", + screen="auto", + action="paper_execution", + payload=payload, + ) + + EventBus.emit("paper_flip_diagnostic", payload) + # ---------- Payload builders ---------- # собрать payload отказа flip без изменения состояния def _build_flip_rejected_payload(