Add reversal signal block diagnostics

This commit is contained in:
2026-07-03 12:47:12 +03:00
parent 8a07d24bd7
commit 3dde8d3e87

View File

@@ -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
@@ -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