# app/src/trading/auto/signal_runtime.py

from __future__ import annotations

import time
from typing import Callable, cast

from src.core.event_bus import EventBus
from src.core.numbers import safe_float
from src.core.types import JsonDict, NumericLike
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
from src.trading.auto.state import AutoTradeState
from src.trading.auto.state_reset import (
    reset_after_market_runtime_expired,
    reset_after_signal_runtime_expired,
)
from src.trading.journal.service import JournalService


class AutoSignalRuntimeMixin:
    _loop_interval_seconds: int

    _confirm_repeats: int
    _confirm_min_duration_seconds: int
    _ready_confidence: float
    _execution_confidence_required_score: float

    _signal_ttl_seconds: int
    _market_analysis_ttl_seconds: int
    _last_logged_runtime_expired_key: str | None

    _last_signal_key: str | None
    _last_signal_value: str | None
    _last_signal_reason: str
    _last_signal_confidence: float
    _last_signal_payload: JsonDict | None
    _last_signal_started_at: float | None
    _same_signal_count: int

    # получить state из основного AutoTradeService
    def get_state(self) -> AutoTradeState:
        raise NotImplementedError

    # сбросить runtime tracking в основном AutoTradeService
    def _reset_signal_tracking(self) -> None:
        raise NotImplementedError

    # debug: принудительно выставить сигнал и decision
    def debug_force_signal(
        self,
        *,
        signal: str,
        confidence: NumericLike = 0.9,
        repeat_count: int = 2,
        reason: str = "DEBUG SIGNAL",
    ) -> AutoTradeState:
        state = self.get_state()
        confidence_value = safe_float(confidence) or 0.0

        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

        if previous_signal != normalized_signal or state.signal_started_at is None:
            state.signal_started_at = time.monotonic()

        state.last_signal = normalized_signal
        state.last_signal_repeat_count = repeat_count
        state.last_signal_confidence = confidence_value
        state.last_signal_reason = reason
        state.signal_confirmation_seconds = self._confirm_min_duration_seconds
        state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
        state.signal_confirmation_missing_repeats = 0
        state.signal_confirmation_progress = 1.0
        state.signal_confirmation_reason = "debug confirmation"

        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

        signal_intent = self._signal_intent(
            state=state,
            signal=state.last_signal,
        )

        EventBus.emit(
            "auto_decision_changed",
            {
                "previous_signal": previous_signal,
                "previous_decision_status": previous_decision_status,
                "decision_status": state.decision_status,
                "signal": state.last_signal,
                "signal_intent": signal_intent,
                "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 _signal_intent(self, *, state: AutoTradeState, signal: str | None) -> str:
        normalized_signal = (signal or "HOLD").upper()
        position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()

        if normalized_signal == "HOLD":
            return "HOLD_MARKET"

        if normalized_signal not in {"BUY", "SELL"}:
            return "NOISE"

        if position_side == "NONE":
            return "ENTRY_CANDIDATE"

        if position_side == "LONG" and normalized_signal == "BUY":
            return "REINFORCE_POSITION"

        if position_side == "SHORT" and normalized_signal == "SELL":
            return "REINFORCE_POSITION"

        if position_side == "LONG" and normalized_signal == "SELL":
            return "REVERSAL_CANDIDATE"

        if position_side == "SHORT" and normalized_signal == "BUY":
            return "REVERSAL_CANDIDATE"

        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,
        *,
        state: AutoTradeState,
        signal: str,
        confidence: float,
    ) -> None:
        state.is_signal_confirmed = False
        state.is_signal_ready = False
        state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds

        state.execution_confidence_score = None
        state.execution_confidence_level = None
        state.execution_confidence_required_score = self._execution_confidence_required_score
        state.execution_confidence_reason = None
        state.execution_confidence_factors = None

        if signal == "HOLD":
            state.signal_confirmation_seconds = 0
            state.signal_confirmation_missing_repeats = self._confirm_repeats
            state.signal_confirmation_progress = 0.0
            state.signal_confirmation_reason = None
            state.decision_status = "WAITING"
            state.decision_reason = "Нет торгового направления."
            return

        now = time.monotonic()

        signal_started = safe_float(state.signal_started_at)
        signal_age_seconds = (
            max(0, int(now - signal_started))
            if signal_started is not None
            else 0
        )

        missing_repeats = max(0, self._confirm_repeats - self._same_signal_count)
        missing_seconds = max(
            0,
            self._confirm_min_duration_seconds - signal_age_seconds,
        )

        repeat_progress = min(
            1.0,
            self._same_signal_count / max(1, self._confirm_repeats),
        )
        time_progress = min(
            1.0,
            signal_age_seconds / max(1, self._confirm_min_duration_seconds),
        )

        confirmation_progress = min(repeat_progress, time_progress)

        state.signal_confirmation_seconds = signal_age_seconds
        state.signal_confirmation_missing_repeats = missing_repeats
        state.signal_confirmation_progress = round(confirmation_progress, 3)

        if missing_repeats > 0 or missing_seconds > 0:
            state.decision_status = "CONFIRMING"
            state.signal_confirmation_reason = (
                f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
                f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с"
            )
            state.decision_reason = (
                f"Сигнал {signal} подтверждается: "
                f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
                f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с."
            )
            return

        state.is_signal_confirmed = True
        state.signal_confirmation_reason = "сигнал подтверждён"

        if confidence < self._ready_confidence:
            state.decision_status = "BLOCKED"
            state.decision_reason = (
                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(
            state=state,
            signal=signal,
            confidence=confidence,
        )

        if (
            state.execution_confidence_score is not None
            and state.execution_confidence_score < self._execution_confidence_required_score
        ):
            state.decision_status = "BLOCKED"
            state.decision_reason = (
                f"Execution confidence низкий: "
                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
        state.signal_confirmation_progress = 1.0
        state.decision_status = "READY"
        state.decision_reason = (
            f"Сигнал {signal} подтверждён по повторам и времени удержания."
        )

    # записать новый сигнал и итог предыдущей серии при смене сигнала
    def _log_signal_if_changed(
        self,
        *,
        strategy_name: str,
        state: AutoTradeState,
        signal: str,
        reason: str,
        confidence: float,
        payload: JsonDict | None,
    ) -> None:
        signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}"
        previous_signal = self._last_signal_value
        previous_count = self._same_signal_count
        is_same_signal = signal_key == self._last_signal_key
        now = time.monotonic()

        if is_same_signal:
            self._same_signal_count += 1
            self._last_signal_reason = reason
            self._last_signal_confidence = confidence
            self._last_signal_payload = payload

            self._update_signal_state_fields(
                state=state,
                signal=signal,
                reason=reason,
                confidence=confidence,
            )
            return

        if previous_signal is not None and previous_signal != signal:
            if previous_count > 1:
                self._log_signal_summary(
                    strategy_name=strategy_name,
                    state=state,
                    previous_signal=previous_signal,
                    previous_count=previous_count,
                    next_signal=signal,
                    reason=self._last_signal_reason,
                    confidence=self._last_signal_confidence,
                    payload=self._last_signal_payload,
                    duration_seconds=self._signal_duration_seconds(now=now),
                )
            else:
                self._log_signal_event(
                    strategy_name=strategy_name,
                    state=state,
                    signal=previous_signal,
                    reason=f"{previous_signal} завершился без серии.",
                    confidence=self._last_signal_confidence,
                    payload={
                        "previous_signal": previous_signal,
                        "next_signal": signal,
                    },
                )

        self._last_signal_key = signal_key
        self._last_signal_value = signal
        self._last_signal_reason = reason
        self._last_signal_confidence = confidence
        self._last_signal_payload = payload
        self._last_signal_started_at = now
        self._same_signal_count = 1

        self._update_signal_state_fields(
            state=state,
            signal=signal,
            reason=reason,
            confidence=confidence,
        )

    # рассчитать длительность текущей серии сигналов
    def _signal_duration_seconds(self, *, now: float) -> int:
        if self._last_signal_started_at is None:
            return max(0, int(self._same_signal_count * self._loop_interval_seconds))

        return max(0, int(now - self._last_signal_started_at))

    # отформатировать длительность для журнала
    def _format_duration(self, total_seconds: int) -> str:
        total_seconds = max(0, int(total_seconds))

        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60

        if hours > 0:
            return f"{hours}ч {minutes:02d}м {seconds:02d}с"

        if minutes > 0:
            return f"{minutes}м {seconds:02d}с"

        return f"{seconds}с"

    # обновить поля state для экрана автоторговли
    def _update_signal_state_fields(
        self,
        *,
        state: AutoTradeState,
        signal: str,
        reason: str,
        confidence: float,
    ) -> None:
        previous_signal = state.last_signal
        previous_decision_status = state.decision_status

        if previous_signal != signal or state.signal_started_at is None:
            state.signal_started_at = time.monotonic()

        state.last_signal = signal
        state.last_signal_repeat_count = self._same_signal_count
        state.last_signal_confidence = confidence
        state.last_signal_reason = reason
        state.signal_updated_at = time.monotonic()
        state.runtime_expired_reason = None
        state.runtime_expired_message = None

        self._update_decision_state(
            state=state,
            signal=signal,
            confidence=confidence,
        )

        signal_intent = self._signal_intent(
            state=state,
            signal=state.last_signal,
        )

        ready_changed = (
            previous_decision_status != state.decision_status
            and state.decision_status == "READY"
        )

        ready_signal_changed = (
            previous_signal != state.last_signal
            and state.decision_status == "READY"
        )

        if ready_changed or ready_signal_changed:
            self._log_ready_signal(
                state=state,
                signal=state.last_signal,
                reason=state.last_signal_reason or reason,
                confidence=state.last_signal_confidence,
                signal_intent=signal_intent,
            )

        if previous_signal != state.last_signal:
            EventBus.emit(
                "auto_signal_changed",
                {
                    "previous_signal": previous_signal,
                    "signal": state.last_signal,
                    "signal_intent": signal_intent,
                    "repeat_count": state.last_signal_repeat_count,
                    "confidence": state.last_signal_confidence,
                    "symbol": state.symbol,
                    "strategy": state.strategy,
                },
            )

        if (
            previous_decision_status != state.decision_status
            or ready_signal_changed
        ):
            EventBus.emit(
                "auto_decision_changed",
                {
                    "previous_signal": previous_signal,
                    "previous_decision_status": previous_decision_status,
                    "decision_status": state.decision_status,
                    "signal": state.last_signal,
                    "signal_intent": signal_intent,
                    "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,
                },
            )

    # одиночные BUY / SELL больше не пишем в журнал как полезные события
    def _log_signal_event(
        self,
        *,
        strategy_name: str,
        state: AutoTradeState,
        signal: str,
        reason: str,
        confidence: float,
        payload: JsonDict | None,
    ) -> None:
        return

    # собрать payload итогового события серии сигналов
    def _build_signal_summary_payload(
        self,
        *,
        strategy_name: str,
        state: AutoTradeState,
        previous_signal: str,
        next_signal: str,
        signal_intent: str,
        previous_count: int,
        duration_seconds: int,
        duration_text: str,
        confidence: float,
        reason: str,
        payload: JsonDict | None,
    ) -> JsonDict:
        return {
            "strategy": strategy_name,
            "status": state.status,
            "symbol": state.symbol,
            "signal": previous_signal,
            "next_signal": next_signal,
            "signal_intent": signal_intent,
            "repeat_count": previous_count,
            "duration_seconds": duration_seconds,
            "duration_text": duration_text,
            "confidence": confidence,
            "reason": reason,
            "is_strong_signal": False,
            "is_aggregated": True,
            "payload": payload or {},
        }

    # записать итог серии одинаковых сигналов при смене сигнала
    def _log_signal_summary(
        self,
        *,
        strategy_name: str,
        state: AutoTradeState,
        previous_signal: str,
        previous_count: int,
        next_signal: str,
        reason: str,
        confidence: float,
        payload: JsonDict | None,
        duration_seconds: int,
    ) -> None:
        if previous_signal != "HOLD":
            return

        duration_text = self._format_duration(duration_seconds)
        signal_intent = "HOLD_MARKET"

        try:
            JournalService().log_ui_info(
                event_type="signal_summary",
                message=(
                    f"HOLD длился {duration_text} и завершился сигналом {next_signal}."
                ),
                screen="auto",
                action="signal_summary",
                payload=self._build_signal_summary_payload(
                    strategy_name=strategy_name,
                    state=state,
                    previous_signal=previous_signal,
                    next_signal=next_signal,
                    signal_intent=signal_intent,
                    previous_count=previous_count,
                    duration_seconds=duration_seconds,
                    duration_text=duration_text,
                    confidence=confidence,
                    reason=reason,
                    payload=payload,
                ),
            )
        except Exception:
            pass

    # собрать payload события готовности сигнала
    def _build_ready_signal_payload(
        self,
        *,
        state: AutoTradeState,
        quote: Quote | None,
        signal: str,
        signal_intent: str,
        confidence: float,
        reason: str,
    ) -> JsonDict:
        return {
            # ---------- Event ----------
            "event_type": "signal_ready",
            "action": "signal_ready",
            "is_aggregated": False,
            "is_strong_signal": confidence > self._ready_confidence,

            # ---------- Runtime ----------
            "status": state.status,
            "strategy": state.strategy,
            "symbol": state.symbol,
            "cycle_number": state.cycle_number,

            # ---------- Signal ----------
            "signal": signal,
            "signal_intent": signal_intent,
            "confidence": confidence,
            "reason": reason,
            "repeat_count": state.last_signal_repeat_count,

            # ---------- Confirmation ----------
            "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,

            # ---------- 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,

            # ---------- Position Context ----------
            "position_side": state.position_side,
            "entry_price": state.entry_price,
            "position_size": state.position_size,
            "unrealized_pnl_usd": state.unrealized_pnl_usd,
            "current_trade_id": state.current_trade_id,
            "current_trade_cycle_number": state.current_trade_cycle_number,

            # ---------- Risk Settings ----------
            "risk_percent": state.risk_percent,
            "stop_loss_percent": state.stop_loss_percent,
            "take_profit_percent": state.take_profit_percent,
            "max_loss_usd": state.max_loss_usd,
            "max_reserved_balance_percent": state.max_reserved_balance_percent,
            "allocated_balance_usd": state.allocated_balance_usd,
            "leverage": state.leverage,

            # ---------- 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,

            # ---------- Live Snapshot ----------
            "bid_price": safe_float(quote.bid_price) if quote is not None else None,
            "ask_price": safe_float(quote.ask_price) if quote is not None else None,
            "last_price": safe_float(quote.last_price) if quote is not None else None,

            # ---------- Market Score ----------
            "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 ----------
            "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,

            # ---------- Candle ----------
            "last_closed_candle_change_percent": state.last_closed_candle_change_percent,
            "last_closed_candle_direction": state.last_closed_candle_direction,
            "current_interval_change_percent": state.current_interval_change_percent,
            "current_interval_direction": state.current_interval_direction,
            "current_interval_label": state.current_interval_label,

            # ---------- Structure ----------
            "market_structure": state.market_structure,
            "market_structure_reason": state.market_structure_reason,

            # ---------- Trend Quality ----------
            "market_trend_gap_percent": state.market_trend_gap_percent,
            "market_trend_consistency": state.market_trend_consistency,
            "market_trend_efficiency": state.market_trend_efficiency,
            "trend_quality_score": state.trend_quality_score,
            "ema_distance_atr_ratio": state.ema_distance_atr_ratio,
            "ema_distance_state": state.ema_distance_state,
            "entry_timing_state": state.entry_timing_state,
            "entry_timing_reason": state.entry_timing_reason,

            # ---------- Momentum / Breakout ----------
            "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_atr_percent": state.htf_atr_percent,
            "htf_atr_percent_baseline": state.htf_atr_percent_baseline,
            "htf_volatility_ratio": state.htf_volatility_ratio,
            "htf_volatility": state.htf_volatility,
            "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,

            # ---------- Runtime Health ----------
            "market_runtime_degraded": state.market_runtime_degraded,
            "runtime_expired_reason": state.runtime_expired_reason,
            "runtime_expired_message": state.runtime_expired_message,
            "market_is_open": state.market_is_open,
            "market_status": state.market_status,
            "market_status_message": state.market_status_message,
        }

    # записать событие готовности сигнала к исполнению
    def _log_ready_signal(
        self,
        *,
        state: AutoTradeState,
        signal: str | None,
        reason: str,
        confidence: float,
        signal_intent: str,
    ) -> None:
        normalized_signal = (signal or "HOLD").upper()
        if normalized_signal not in {"BUY", "SELL"}:
            return

        try:
            quote = ExchangeService().get_quote(
                state.symbol,
                runtime_key="auto",
            )
        except Exception:
            quote = None

        try:
            JournalService().log_ui_info(
                event_type="signal_ready",
                message=(
                    f"Сигнал {normalized_signal} подтверждён и готов к исполнению."
                ),
                screen="auto",
                action="signal_ready",
                payload=self._build_ready_signal_payload(
                    state=state,
                    quote=quote,
                    signal=normalized_signal,
                    signal_intent=signal_intent,
                    confidence=confidence,
                    reason=reason,
                ),
            )
        except Exception:
            pass

    # сбросить устаревшие signal / market runtime данные
    def _expire_runtime_if_needed(self, state: AutoTradeState) -> None:
        now = time.monotonic()

        signal_updated_at = getattr(state, "signal_updated_at", None)
        if signal_updated_at is not None:
            signal_updated = safe_float(signal_updated_at)
            if signal_updated is None:
                return

            signal_age = now - signal_updated
            if signal_age > self._signal_ttl_seconds:
                previous_signal = state.last_signal

                # Сбрасываем только signal runtime.
                # Нельзя вызывать _reset_signal_tracking(), потому что она также
                # очищает market/HTF/momentum context.
                self._last_signal_key = None
                self._last_signal_value = None
                self._last_signal_reason = ""
                self._last_signal_confidence = 0.0
                self._last_signal_payload = None
                self._last_signal_started_at = None
                self._same_signal_count = 0

                reset_after_signal_runtime_expired(state)

                state.decision_reason = "Сигнал устарел."
                state.signal_confirmation_missing_repeats = self._confirm_repeats
                state.execution_confidence_required_score = (
                    self._execution_confidence_required_score
                )

                state.runtime_expired_reason = "SIGNAL_TTL_EXPIRED"
                state.runtime_expired_message = "сигнал устарел и был сброшен"

                self._log_runtime_expired_if_changed(
                    state=state,
                    reason="SIGNAL_TTL_EXPIRED",
                    message="Сигнал устарел и был сброшен.",
                    payload={
                        "previous_signal": previous_signal,
                        "signal_age_seconds": int(signal_age),
                        "signal_ttl_seconds": self._signal_ttl_seconds,
                    },
                )

                return

        market_updated_at = getattr(state, "market_analysis_updated_at", None)
        if market_updated_at is not None:
            market_updated = safe_float(market_updated_at)

            if market_updated is None:
                return

            market_age = now - market_updated

            if market_age > self._market_analysis_ttl_seconds:
                reset_after_market_runtime_expired(state)

                state.runtime_expired_reason = "MARKET_ANALYSIS_TTL_EXPIRED"
                state.runtime_expired_message = "анализ рынка устарел"

                self._log_runtime_expired_if_changed(
                    state=state,
                    reason="MARKET_ANALYSIS_TTL_EXPIRED",
                    message="Анализ рынка устарел и был сброшен.",
                    payload={
                        "market_age_seconds": int(market_age),
                        "market_analysis_ttl_seconds": self._market_analysis_ttl_seconds,
                    },
                )

    # собрать payload события устаревания runtime данных без изменения состояния
    def _build_runtime_expired_payload(
        self,
        *,
        state: AutoTradeState,
        payload: JsonDict,
        reason: str,
    ) -> JsonDict:
        return {
            **payload,
            "symbol": state.symbol,
            "strategy": state.strategy,
            "status": state.status,
            "runtime_expired_reason": reason,
        }

    # записать событие устаревания runtime данных
    def _log_runtime_expired_if_changed(
        self,
        *,
        state: AutoTradeState,
        reason: str,
        message: str,
        payload: JsonDict,
    ) -> None:
        key = f"{state.status}:{state.symbol}:{state.strategy}:{reason}"

        if key == type(self)._last_logged_runtime_expired_key:
            return

        type(self)._last_logged_runtime_expired_key = key

        try:
            JournalService().log_ui_warning(
                event_type="runtime_expired",
                message=message,
                screen="auto",
                action="runtime_expiration",
                payload=self._build_runtime_expired_payload(
                    state=state,
                    payload=payload,
                    reason=reason,
                ),
            )
        except Exception:
            pass

    # собрать диагностические факторы execution confidence
    def _build_execution_confidence_factors(
        self,
        *,
        state: AutoTradeState,
        signal: str,
        signal_score: float,
        confirmation_score: float,
        market_score: float,
        execution_score: float,
    ) -> JsonDict:
        return {
            "signal_score": round(signal_score, 3),
            "confirmation_score": round(confirmation_score, 3),
            "market_score": round(market_score, 3),
            "market_score_raw": getattr(state, "market_score", None),
            "market_score_label": getattr(state, "market_score_label", None),
            "execution_score": round(execution_score, 3),
            "required_score": self._execution_confidence_required_score,
            "signal": signal,
            "market_state": state.market_state,
            "market_trend": state.market_trend,
            "market_trend_strength": state.market_trend_strength,
            "market_trend_quality": state.market_trend_quality,
            "market_phase": state.market_phase,
            "current_interval_change_percent": getattr(state, "current_interval_change_percent", None),
            "current_interval_direction": getattr(state, "current_interval_direction", None),
            "current_interval_label": getattr(state, "current_interval_label", None),
            "market_structure": getattr(state, "market_structure", None),
            "market_structure_reason": getattr(state, "market_structure_reason", None),
            "htf_alignment": getattr(state, "htf_alignment", None),
            "htf_confirmation_score": getattr(state, "htf_confirmation_score", None),
            "execution_quality": state.execution_quality,
            "execution_quality_reason": state.execution_quality_reason,
            "spread_percent": state.spread_percent,
            "momentum_state": getattr(state, "momentum_state", None),
            "momentum_direction": getattr(state, "momentum_direction", None),
            "momentum_change_percent": getattr(state, "momentum_change_percent", None),
            "momentum_strength": getattr(state, "momentum_strength", None),
            "breakout_level": getattr(state, "breakout_level", None),
            "breakout_distance_percent": getattr(state, "breakout_distance_percent", None),
            "breakout_reason": getattr(state, "breakout_reason", None),
        }

    # синхронизировать итоговый execution confidence
    def _sync_execution_confidence_state(
        self,
        *,
        state: AutoTradeState,
        signal: str,
        confidence: float,
    ) -> None:
        if signal not in {"BUY", "SELL"}:
            state.execution_confidence_score = None
            state.execution_confidence_level = None
            state.execution_confidence_required_score = self._execution_confidence_required_score
            state.execution_confidence_reason = None
            state.execution_confidence_factors = None
            return

        signal_score = self._clamp_score(confidence)
        confirmation_score = self._clamp_score(state.signal_confirmation_progress)

        # Сейчас market_score считается как entry-confidence.
        # Для reversal/flip это полезно диагностировать, но пока не меняем поведение.
        market_score = self._market_confidence_score(
            state=state,
            signal=signal,
        )

        execution_quality_confidence_score = cast(
            Callable[[AutoTradeState], float],
            getattr(self, "_execution_quality_confidence_score"),
        )
        execution_score = execution_quality_confidence_score(state)

        score = (
            signal_score * 0.35
            + confirmation_score * 0.20
            + market_score * 0.25
            + execution_score * 0.20
        )

        score = round(self._clamp_score(score), 3)

        state.execution_confidence_score = score
        state.execution_confidence_required_score = self._execution_confidence_required_score
        state.execution_confidence_level = self._execution_confidence_level(score)
        state.execution_confidence_reason = self._execution_confidence_reason(state)
        state.execution_confidence_factors = self._build_execution_confidence_factors(
            state=state,
            signal=signal,
            signal_score=signal_score,
            confirmation_score=confirmation_score,
            market_score=market_score,
            execution_score=execution_score,
        )

    # рассчитать market confidence для итогового execution confidence
    def _market_confidence_score(
        self,
        *,
        state: AutoTradeState,
        signal: str,
    ) -> float:
        market_state = str(state.market_state or "").upper()
        market_trend = str(state.market_trend or "").upper()
        strength = str(state.market_trend_strength or "").upper()
        quality = str(state.market_trend_quality or "").upper()
        phase = str(state.market_phase or "").upper()
        current_interval_direction = str(
            getattr(state, "current_interval_direction", "") or ""
        ).upper()
        current_interval_change_percent = safe_float(
            getattr(state, "current_interval_change_percent", None)
        )
        ema_distance_state = str(state.ema_distance_state or "").upper()
        entry_timing_state = str(state.entry_timing_state or "").upper()
        momentum_direction = str(getattr(state, "momentum_direction", "") or "").upper()
        momentum_state = str(getattr(state, "momentum_state", "") or "").upper()
        trend_quality_score = safe_float(state.trend_quality_score)
        htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()
        htf_confirmation_score = safe_float(getattr(state, "htf_confirmation_score", None))
        market_structure = str(getattr(state, "market_structure", "") or "").upper()
        normalized_signal = str(signal or "").upper()

        early_impulse_market = (
            market_state == "RANGE"
            and phase == "IMPULSE"
            and htf_alignment in {"ALIGNED", "SAME_INTERVAL"}
            and (
                (
                    normalized_signal == "BUY"
                    and market_trend == "UP"
                    and momentum_direction == "UP"
                    and momentum_state in {"MOMENTUM_UP", "BREAKOUT_UP"}
                )
                or (
                    normalized_signal == "SELL"
                    and market_trend == "DOWN"
                    and momentum_direction == "DOWN"
                    and momentum_state in {"MOMENTUM_DOWN", "BREAKOUT_DOWN"}
                )
            )
        )

        current_interval_supports_signal = (
            (
                normalized_signal == "BUY"
                and current_interval_direction == "UP"
            )
            or (
                normalized_signal == "SELL"
                and current_interval_direction == "DOWN"
            )
        )

        current_interval_against_signal = (
            (
                normalized_signal == "BUY"
                and current_interval_direction == "DOWN"
            )
            or (
                normalized_signal == "SELL"
                and current_interval_direction == "UP"
            )
        )

        current_interval_move_abs = abs(current_interval_change_percent or 0.0)

        if market_state in {"HIGH_VOLATILITY", "LOW_VOLATILITY", "UNKNOWN", ""}:
            return 0.15

        if market_state == "RANGE" and not early_impulse_market:
            return 0.15

        # Жёсткая защита от входа против локального тренда.
        # Для будущего этапа: именно это может быть слишком жёстко для flip.
        if normalized_signal == "BUY" and market_trend == "DOWN":
            return 0.05

        if normalized_signal == "SELL" and market_trend == "UP":
            return 0.05

        # Жёсткая защита от входа против momentum.
        # Для будущего этапа: reversal может начинаться до смены полного trend.
        if normalized_signal == "BUY" and momentum_direction == "DOWN":
            return 0.05

        if normalized_signal == "SELL" and momentum_direction == "UP":
            return 0.05

        # После ужесточения фильтров лучше не давать высокий confidence,
        # если momentum вообще не подтверждает направление входа.
        if normalized_signal == "BUY" and momentum_direction != "UP":
            return 0.25

        if normalized_signal == "SELL" and momentum_direction != "DOWN":
            return 0.25

        # HTF против входа должен почти обнулять рыночную часть confidence.
        if htf_alignment == "AGAINST":
            return 0.05

        if htf_alignment == "UNKNOWN":
            return 0.25

        if htf_confirmation_score is not None and htf_confirmation_score < 0.55:
            return 0.35

        # Структура против направления входа.
        if normalized_signal == "BUY" and market_structure == "LH_LL":
            return 0.10

        if normalized_signal == "SELL" and market_structure == "HH_HL":
            return 0.10

        if market_structure == "MIXED":
            score_penalty_for_structure = 0.08
        else:
            score_penalty_for_structure = 0.0

        score = 0.60
        score -= score_penalty_for_structure

        if early_impulse_market:
            score += 0.10

        # “Сейчас (5м)” — короткий подтверждающий фактор.
        # Он не открывает сделку сам по себе, но усиливает или ослабляет market_score.
        if current_interval_supports_signal:
            score += 0.05

        if current_interval_against_signal:
            score -= 0.10

            if current_interval_move_abs >= 0.12:
                score -= 0.05

        if strength == "STRONG":
            score += 0.16
        elif strength == "NORMAL":
            score += 0.08
        elif strength == "WEAK":
            score -= 0.25

        if quality == "CLEAN":
            score += 0.10
        elif quality == "NORMAL":
            score += 0.04
        elif quality == "NOISY":
            score -= 0.12

        if phase == "IMPULSE":
            score += 0.08
        elif phase == "PULLBACK":
            score -= 0.25
        elif phase in {"RANGE", "SQUEEZE"}:
            score -= 0.35

        if ema_distance_state == "HEALTHY":
            score += 0.08
        elif ema_distance_state == "EXTENDED":
            score -= 0.10
        elif ema_distance_state == "COMPRESSED":
            score -= 0.20
        elif ema_distance_state == "OVEREXTENDED":
            score -= 0.40

        if entry_timing_state == "NORMAL":
            score += 0.08
        elif entry_timing_state == "EARLY":
            score -= 0.08
        elif entry_timing_state == "LATE":
            score -= 0.25
        elif entry_timing_state == "CHASING":
            score -= 0.40

        if momentum_state in {"BREAKOUT_UP", "BREAKOUT_DOWN"}:
            score += 0.06
        elif momentum_state in {"MOMENTUM_UP", "MOMENTUM_DOWN"}:
            score += 0.04

        current_interval_penalty = 0.0

        if normalized_signal == "BUY" and current_interval_direction == "DOWN":
            current_interval_penalty = 0.08

        if normalized_signal == "SELL" and current_interval_direction == "UP":
            current_interval_penalty = 0.08

        if trend_quality_score is not None:
            if trend_quality_score >= 0.7:
                score += 0.06
            elif trend_quality_score < 0.45:
                score -= 0.18

        if htf_alignment == "ALIGNED":
            score += 0.12

        if htf_confirmation_score is not None and htf_confirmation_score >= 0.75:
            score += 0.06

        if normalized_signal == "BUY" and market_structure == "HH_HL":
            score += 0.08

        if normalized_signal == "SELL" and market_structure == "LH_LL":
            score += 0.08

        score -= current_interval_penalty

        return self._clamp_score(score)

    # определить уровень execution confidence
    def _execution_confidence_level(self, score: float) -> str:
        if score >= 0.75:
            return "HIGH"

        if score >= self._execution_confidence_required_score:
            return "NORMAL"

        return "LOW"

    # сформировать причину execution confidence
    def _execution_confidence_reason(self, state: AutoTradeState) -> str:
        score = state.execution_confidence_score

        if score is None:
            return "execution confidence не рассчитан"

        if score < self._execution_confidence_required_score:
            return "низкая совокупная уверенность входа"

        if state.execution_confidence_level == "HIGH":
            return "высокая совокупная уверенность входа"

        return "достаточная совокупная уверенность входа"

    # ограничить score диапазоном 0.0..1.0
    def _clamp_score(self, value: NumericLike | None) -> float:
        if value is None:
            return 0.0

        numeric = safe_float(value)

        if numeric is None:
            return 0.0

        return max(0.0, min(1.0, numeric))