diff --git a/app/src/trading/auto/signal_runtime.py b/app/src/trading/auto/signal_runtime.py index 1348e7e..b9687a5 100644 --- a/app/src/trading/auto/signal_runtime.py +++ b/app/src/trading/auto/signal_runtime.py @@ -141,6 +141,180 @@ class AutoSignalRuntimeMixin: return "NOISE" + # собрать payload диагностики заблокированного reversal-сигнала + def _build_reversal_signal_blocked_payload( + self, + *, + state: AutoTradeState, + signal: str, + confidence: float, + block_stage: str, + block_reason: str, + ) -> JsonDict: + return { + "event_type": "reversal_signal_blocked", + "action": "reversal_signal_blocked", + "block_stage": block_stage, + "block_reason": block_reason, + + # ---------- Signal ---------- + "signal": signal, + "signal_intent": "REVERSAL_CANDIDATE", + "confidence": confidence, + "signal_reason": state.last_signal_reason, + "repeat_count": state.last_signal_repeat_count, + + # ---------- Decision ---------- + "decision_status": state.decision_status, + "decision_reason": state.decision_reason, + "is_signal_confirmed": state.is_signal_confirmed, + "is_signal_ready": state.is_signal_ready, + "confirmation_seconds": state.signal_confirmation_seconds, + "confirmation_required_seconds": state.signal_confirmation_required_seconds, + "confirmation_missing_repeats": state.signal_confirmation_missing_repeats, + "confirmation_progress": state.signal_confirmation_progress, + "confirmation_reason": state.signal_confirmation_reason, + + # ---------- Position ---------- + "symbol": state.symbol, + "strategy": state.strategy, + "position_side": state.position_side, + "entry_price": state.entry_price, + "position_size": state.position_size, + "unrealized_pnl_usd": state.unrealized_pnl_usd, + "position_pnl_percent": state.position_pnl_percent, + "position_health_status": state.position_health_status, + "position_health_reason": state.position_health_reason, + "position_risk_level": state.position_risk_level, + "position_risk_reason": state.position_risk_reason, + "position_trend_alignment": state.position_trend_alignment, + "position_adverse_momentum": state.position_adverse_momentum, + + # ---------- Execution Confidence ---------- + "execution_confidence_score": state.execution_confidence_score, + "execution_confidence_level": state.execution_confidence_level, + "execution_confidence_required_score": ( + state.execution_confidence_required_score + ), + "execution_confidence_reason": state.execution_confidence_reason, + "execution_confidence_factors": state.execution_confidence_factors, + + # ---------- Execution Quality ---------- + "execution_quality": state.execution_quality, + "execution_quality_reason": state.execution_quality_reason, + "execution_quality_message": state.execution_quality_message, + "spread_percent": state.spread_percent, + "snapshot_age_seconds": state.snapshot_age_seconds, + + # ---------- Market ---------- + "market_score": state.market_score, + "market_score_label": state.market_score_label, + "market_long_score": state.market_long_score, + "market_short_score": state.market_short_score, + "market_state": state.market_state, + "market_trend": state.market_trend, + "market_volatility": state.market_volatility, + "market_trend_strength": state.market_trend_strength, + "market_trend_quality": state.market_trend_quality, + "market_phase": state.market_phase, + "market_phase_direction": state.market_phase_direction, + + # ---------- Current interval ---------- + "current_interval_change_percent": state.current_interval_change_percent, + "current_interval_direction": state.current_interval_direction, + "current_interval_label": state.current_interval_label, + + # ---------- Structure / timing ---------- + "market_structure": state.market_structure, + "market_structure_reason": state.market_structure_reason, + "trend_quality_score": state.trend_quality_score, + "ema_distance_state": state.ema_distance_state, + "entry_timing_state": state.entry_timing_state, + "entry_timing_reason": state.entry_timing_reason, + + # ---------- Momentum ---------- + "momentum_state": state.momentum_state, + "momentum_direction": state.momentum_direction, + "momentum_change_percent": state.momentum_change_percent, + "momentum_strength": state.momentum_strength, + "breakout_level": state.breakout_level, + "breakout_distance_percent": state.breakout_distance_percent, + "breakout_reason": state.breakout_reason, + + # ---------- HTF ---------- + "htf_interval": state.htf_interval, + "htf_market_state": state.htf_market_state, + "htf_trend": state.htf_trend, + "htf_trend_strength": state.htf_trend_strength, + "htf_trend_quality": state.htf_trend_quality, + "htf_market_phase": state.htf_market_phase, + "htf_alignment": state.htf_alignment, + "htf_confirmation_score": state.htf_confirmation_score, + "htf_reason": state.htf_reason, + } + + # записать диагностику, если reversal-кандидат был заблокирован до READY + def _log_reversal_signal_blocked_if_needed( + self, + *, + state: AutoTradeState, + signal: str, + confidence: float, + block_stage: str, + block_reason: str, + ) -> None: + signal_intent = self._signal_intent( + state=state, + signal=signal, + ) + + if signal_intent != "REVERSAL_CANDIDATE": + return + + # Дедупликация: не пишем одно и то же состояние на каждом цикле. + key = ( + f"{state.status}:" + f"{state.symbol}:" + f"{state.strategy}:" + f"{state.position_side}:" + f"{signal}:" + f"{state.last_signal_repeat_count}:" + f"{confidence:.2f}:" + f"{block_stage}:" + f"{block_reason}:" + f"{state.execution_confidence_score}" + ) + + last_key = getattr(type(self), "_last_reversal_signal_block_key", None) + + if key == last_key: + return + + setattr(type(self), "_last_reversal_signal_block_key", key) + + payload = self._build_reversal_signal_blocked_payload( + state=state, + signal=signal, + confidence=confidence, + block_stage=block_stage, + block_reason=block_reason, + ) + + try: + JournalService().log_ui_warning( + event_type="reversal_signal_blocked", + message=( + f"Reversal-сигнал {signal} заблокирован: {block_reason}" + ), + screen="auto", + action="signal_runtime", + payload=payload, + ) + except Exception: + pass + + EventBus.emit("reversal_signal_blocked", payload) + # обновить статус решения по текущему сигналу def _update_decision_state( self, @@ -220,6 +394,14 @@ class AutoSignalRuntimeMixin: f"Сигнал {signal} подтверждён, но уверенность низкая: " f"{confidence:.2f} < {self._ready_confidence:.2f}." ) + + self._log_reversal_signal_blocked_if_needed( + state=state, + signal=signal, + confidence=confidence, + block_stage="LOW_SIGNAL_CONFIDENCE", + block_reason=state.decision_reason, + ) return self._sync_execution_confidence_state( @@ -238,6 +420,14 @@ class AutoSignalRuntimeMixin: f"{state.execution_confidence_score:.2f} < " f"{self._execution_confidence_required_score:.2f}." ) + + self._log_reversal_signal_blocked_if_needed( + state=state, + signal=signal, + confidence=confidence, + block_stage="LOW_EXECUTION_CONFIDENCE", + block_reason=state.decision_reason, + ) return state.is_signal_ready = True @@ -475,7 +665,7 @@ class AutoSignalRuntimeMixin: "is_aggregated": True, "payload": payload or {}, } - + # записать итог серии одинаковых сигналов при смене сигнала def _log_signal_summary( self, @@ -669,7 +859,7 @@ class AutoSignalRuntimeMixin: "market_status": state.market_status, "market_status_message": state.market_status_message, } - + # записать событие готовности сигнала к исполнению def _log_ready_signal( self, @@ -801,7 +991,7 @@ class AutoSignalRuntimeMixin: "status": state.status, "runtime_expired_reason": reason, } - + # записать событие устаревания runtime данных def _log_runtime_expired_if_changed( self, @@ -876,7 +1066,7 @@ class AutoSignalRuntimeMixin: "breakout_distance_percent": getattr(state, "breakout_distance_percent", None), "breakout_reason": getattr(state, "breakout_reason", None), } - + # синхронизировать итоговый execution confidence def _sync_execution_confidence_state( self, @@ -896,10 +1086,8 @@ class AutoSignalRuntimeMixin: signal_score = self._clamp_score(confidence) confirmation_score = self._clamp_score(state.signal_confirmation_progress) - # ВАЖНО: - # market_score теперь считается с учётом направления сигнала. - # Раньше BUY мог получить хороший market_score просто потому, - # что рынок трендовый, даже если тренд/моментум были против BUY. + # Сейчас market_score считается как entry-confidence. + # Для reversal/flip это полезно диагностировать, но пока не меняем поведение. market_score = self._market_confidence_score( state=state, signal=signal, @@ -1012,6 +1200,7 @@ class AutoSignalRuntimeMixin: return 0.15 # Жёсткая защита от входа против локального тренда. + # Для будущего этапа: именно это может быть слишком жёстко для flip. if normalized_signal == "BUY" and market_trend == "DOWN": return 0.05 @@ -1019,6 +1208,7 @@ class AutoSignalRuntimeMixin: return 0.05 # Жёсткая защита от входа против momentum. + # Для будущего этапа: reversal может начинаться до смены полного trend. if normalized_signal == "BUY" and momentum_direction == "DOWN": return 0.05