# app/src/trading/strategies/trend.py
 
from __future__ import annotations

import time
from typing import Any
from decimal import Decimal

from src.core.types import NumericLike
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
from src.trading.market_analysis.models import (
    EmaDistanceState,
    EntryTimingState,
    MarketPhase,
    MarketState,
    MomentumState,
    TrendDirection,
    TrendQuality,
    TrendStrength,
    VolatilityState,
)
from src.trading.market_analysis.service import MarketAnalysisService
from src.trading.strategies.base import StrategyContext
from src.trading.strategies.signals import SignalResult, SignalType


class TrendStrategy:
    name = "TREND"

    # Live-окно цен хранится в памяти стратегии.
    # Оно нужно не для общего анализа рынка, а для подтверждения,
    # что цена прямо сейчас действительно движется в нужную сторону.
    _price_window: dict[str, list[float]] = {}
    _price_window_updated_at: dict[str, float] = {}

    # Через сколько секунд live-окно считается устаревшим.
    _window_ttl_seconds = 60

    # Сколько последних live-точек используем для подтверждения импульса.
    _window_size = 8

    # Минимальное изменение цены внутри live-окна.
    # 0.05 = 0.05%.
    _threshold_percent = 0.05

    # Какая доля движений внутри окна должна идти в сторону сделки.
    # 0.6 = минимум 60% шагов должны быть в сторону входа.
    _min_direction_ratio = 0.6

    # Основной таймфрейм анализа стратегии.
    _market_interval = "5m"

    # PUBLIC API
    def reset_runtime(self, symbol: str | None = None) -> None:
        """
        Сбрасывает runtime-память стратегии.

        Используется при:
        - смене актива;
        - смене стратегии;
        - перезапуске автоторговли;
        - ручном сбросе состояния.

        Если symbol=None — очищаем все live-окна.
        Если symbol указан — очищаем только данные этого актива.
        """
        if symbol is None:
            self._price_window.clear()
            self._price_window_updated_at.clear()
            return

        normalized_symbol = symbol.upper()

        keys_to_delete = [
            key for key in self._price_window.keys()
            if key.upper() == normalized_symbol
        ]

        for key in keys_to_delete:
            self._price_window.pop(key, None)
            self._price_window_updated_at.pop(key, None)

    def analyze(self, context: StrategyContext) -> SignalResult:
        """
        Главная функция стратегии.

        Последовательность анализа:

        1. Получаем market analysis по свечам 5m:
           - состояние рынка;
           - тренд;
           - волатильность;
           - momentum;
           - breakout;
           - структура рынка;
           - EMA-дистанция;
           - HTF-тренд 1h.

        2. Получаем live snapshot:
           - bid;
           - ask;
           - last price.

        3. Рассчитываем рабочую цену анализа:
           - если есть bid/ask — берём середину;
           - иначе используем last price.

        4. Обновляем live-окно последних цен.

        5. Собираем base_payload:
           - все market metrics;
           - все причины блокировок;
           - данные для UI, журнала и execution confidence.

        6. Сначала проверяем breakout.
           Это важно: после выхода из сжатия рынок ещё может выглядеть
           как NOISY / COMPRESSED / RANGE, но первая сделка часто появляется
           именно в этот момент.

        7. Если breakout не найден — применяем защитные фильтры рынка.

        8. Если рынок разрешён — ждём достаточное live-окно.

        9. По live-окну подтверждаем направление:
           - TREND_UP + live-импульс вверх = BUY;
           - TREND_DOWN + live-импульс вниз = SELL;
           - иначе HOLD.

        Важно:
        Эта функция не открывает сделку.
        Она только возвращает BUY / SELL / HOLD.
        Сделку потом открывает ExecutionEngine, если сигнал подтвердился.
        """
        market = MarketAnalysisService().analyze(
            context.symbol,
            interval=self._market_interval,
            limit=200,
        )

        quote_result = self._quote_or_hold(
            context=context,
            market=market,
        )

        if isinstance(quote_result, SignalResult):
            return quote_result

        quote = quote_result
        symbol = quote.symbol or context.symbol
        current_price = self._analysis_price(quote)

        if current_price <= 0:
            return self._invalid_price_hold(
                symbol=symbol,
                quote=quote,
                market=market,
            )

        prices = self._update_price_window(
            symbol=symbol,
            current_price=current_price,
        )

        base_payload = self._base_payload(
            market=market,
            symbol=symbol,
            quote=quote,
            current_price=current_price,
            prices=prices,
        )

        # 1. Сначала проверяем пробой.
        # Пробой — это исключение из обычной логики фильтров.
        # Его нельзя ставить после market_block, иначе ранние входы
        # из сжатия будут отсеиваться как COMPRESSED / NOISY / RANGE.
        breakout_signal = self._breakout_signal(
            market=market,
            base_payload=base_payload,
        )

        if breakout_signal is not None:
            return breakout_signal

        # 2. Если пробоя нет — включаем обычную защиту.
        # Здесь отсекаются плохие условия:
        # - рынок не в тренде;
        # - волатильность плохая;
        # - HTF против входа;
        # - momentum не подтверждает;
        # - структура против входа;
        # - поздний вход.
        market_block = self._market_block_signal(
            market=market,
            base_payload=base_payload,
        )

        if market_block is not None:
            return market_block

        # 3. Если рынок хороший, но live-данных ещё мало,
        # не открываем сделку вслепую.
        if len(prices) < self._window_size:
            return self._hold(
                reason="Недостаточно live-данных для подтверждения TREND.",
                block_reason="NOT_ENOUGH_LIVE_DATA",
                block_message="мало данных",
                payload={
                    **base_payload,
                    "window_size": len(prices),
                    "required_window_size": self._window_size,
                },
            )

        # 4. Подтверждаем направление по live-движению.
        return self._live_trend_signal(
            market=market,
            base_payload=base_payload,
            prices=prices,
        )

    # STEP 1. SNAPSHOT / PRICE
    def _quote_or_hold(
        self,
        *,
        context: StrategyContext,
        market: Any,
    ) -> Quote | SignalResult:
        """
        Получает live snapshot с биржи.

        Snapshot нужен для:
        - актуальной цены;
        - bid/ask;
        - проверки spread дальше по цепочке;
        - формирования payload для UI и журнала.

        Если quote получить нельзя — стратегия возвращает HOLD.
        Это безопаснее, чем строить сигнал на устаревших свечах.
        """
        try:
            return ExchangeService().get_quote(
                context.symbol,
                runtime_key="auto",
            )
        except Exception as exc:
            return SignalResult(
                signal=SignalType.HOLD,
                reason="Не удалось получить рыночный snapshot. Безопасный HOLD.",
                confidence=0.0,
                payload={
                    **dict(market.payload or {}),
                    "strategy": self.name,
                    "symbol": context.symbol,
                    "error": str(exc),
                    "market_analysis": market.payload,
                    "entry_block_reason": "MARKET_SNAPSHOT_ERROR",
                    "entry_block_message": "нет данных рынка",
                },
            )

    def _invalid_price_hold(
        self,
        *,
        symbol: str,
        quote: Quote,
        market: Any,
    ) -> SignalResult:
        """
        Возвращает HOLD, если цена из quote некорректная.

        Без валидной цены нельзя:
        - обновить live-окно;
        - рассчитать импульс;
        - рассчитать цену входа.
        """
        return SignalResult(
            signal=SignalType.HOLD,
            reason="Некорректная рыночная цена. Безопасный HOLD.",
            confidence=0.0,
            payload={
                **dict(market.payload or {}),
                "strategy": self.name,
                "symbol": symbol,
                "snapshot": self._quote_snapshot(quote),
                "market_analysis": market.payload,
                "entry_block_reason": "INVALID_MARKET_PRICE",
                "entry_block_message": "нет цены",
            },
        )

    def _quote_snapshot(self, quote: Quote) -> dict[str, object]:
        return {
            "symbol": quote.symbol,
            "last_price": self._safe_float(quote.last_price),
            "bid_price": self._safe_float(quote.bid_price),
            "ask_price": self._safe_float(quote.ask_price),
            "source": quote.source,
        }

    def _analysis_price(
        self,
        quote: Quote,
    ) -> float:
        """
        Выбирает цену для анализа live-импульса.

        Приоритет:
        1. midpoint между bid и ask;
        2. last_price;
        3. 0.0, если цены нет.

        Midpoint лучше last_price, потому что меньше зависит
        от случайного последнего трейда.
        """
        bid = self._safe_float(quote.bid_price)
        ask = self._safe_float(quote.ask_price)

        if bid is not None and ask is not None and bid > 0 and ask > 0:
            return (bid + ask) / 2

        last = self._safe_float(quote.last_price)
        if last is not None:
            return last

        return 0.0

    # STEP 2. LIVE PRICE WINDOW
    def _update_price_window(
        self,
        *,
        symbol: str,
        current_price: float,
    ) -> list[float]:
        """
        Обновляет live-окно последних цен.

        Логика:
        - если окно устарело по TTL — очищаем его;
        - добавляем новую цену;
        - если цен больше лимита — удаляем самую старую.

        Это окно показывает не общий тренд по свечам,
        а краткосрочное движение прямо сейчас.
        """
        now = time.monotonic()
        previous_updated_at = self._price_window_updated_at.get(symbol)

        if (
            previous_updated_at is not None
            and now - previous_updated_at > self._window_ttl_seconds
        ):
            self._price_window.pop(symbol, None)
            self._price_window_updated_at.pop(symbol, None)

        prices = self._price_window.setdefault(symbol, [])
        prices.append(current_price)
        self._price_window_updated_at[symbol] = now

        if len(prices) > self._window_size:
            prices.pop(0)

        return prices

    # STEP 3. PAYLOAD
    def _base_payload(
        self,
        *,
        market: Any,
        symbol: str,
        quote: Quote,
        current_price: float,
        prices: list[float],
    ) -> dict[str, Any]:
        """
        Собирает единый payload стратегии.

        Payload используется дальше в:
        - AutoTradeState;
        - Telegram UI;
        - diagnostics;
        - journal;
        - execution confidence;
        - supervisor block logs.

        Поэтому сюда кладём не только итоговый сигнал,
        но и все промежуточные признаки рынка.
        """
        return {
            # Сначала переносим полный payload MarketAnalysisService.
            # Это защищает от потери новых market-полей:
            # market_long_score / market_short_score,
            # last_closed_candle_change_percent,
            # current_interval_change_percent и т.д.
            **dict(market.payload or {}),

            # Ниже стратегия добавляет/переопределяет runtime-поля.
            "strategy": self.name,
            "symbol": symbol,
            "analysis_price": current_price,
            "last_price": self._safe_float(quote.last_price),
            "bid_price": self._safe_float(quote.bid_price),
            "ask_price": self._safe_float(quote.ask_price),
            "market_state": market.state.value,
            "market_trend": market.trend.value,
            "market_volatility": market.volatility.value,
            "market_analysis_interval": market.interval,
            "market_analysis_reason": market.reason,
            "market_analysis": market.payload,
            "market_trend_strength": market.trend_strength.value,
            "market_trend_quality": market.trend_quality.value,
            "market_phase": self._normalized_market_phase(market),
            "market_phase_direction": self._normalized_market_phase_direction(market),
            "market_phase_change_percent": market.phase_change_percent,
            "market_phase_direction_consistency": market.payload.get(
                "market_phase_direction_consistency"
            ),
            "current_interval_change_percent": market.current_interval_change_percent,
            "current_interval_direction": (
                market.current_interval_direction.value
                if market.current_interval_direction is not None
                else "UNKNOWN"
            ),
            "current_interval_label": market.current_interval_label or market.interval,
            # Общая оценка рынка 0..100.
            # Она рассчитана в MarketAnalysisService из всех рыночных факторов
            # и дальше используется UI / diagnostics / execution / adaptive size.
            "market_score": market.market_score,
            "market_score_label": market.market_score_label,
            "market_long_score": market.market_long_score,
            "market_short_score": market.market_short_score,
            "last_closed_candle_change_percent": market.payload.get(
                "last_closed_candle_change_percent"
            ),
            "last_closed_candle_direction": market.payload.get(
                "last_closed_candle_direction"
            ),
            "market_phase_reason": market.phase_reason,
            "market_structure": (
                market.market_structure.value
                if market.market_structure is not None
                else "UNKNOWN"
            ),
            "market_structure_reason": market.market_structure_reason,
            "momentum_state": (
                market.momentum_state.value
                if market.momentum_state is not None
                else "UNKNOWN"
            ),
            "momentum_direction": (
                market.momentum_direction.value
                if market.momentum_direction is not None
                else "UNKNOWN"
            ),
            "momentum_change_percent": market.momentum_change_percent,
            "momentum_strength": market.momentum_strength,
            "breakout_level": market.breakout_level,
            "breakout_distance_percent": market.breakout_distance_percent,
            "breakout_reason": market.breakout_reason,
            "market_trend_gap_percent": market.trend_gap_percent,
            "market_trend_consistency": market.trend_consistency,
            "market_trend_efficiency": market.trend_efficiency,
            "trend_quality_score": market.trend_quality_score,
            "ema_distance_atr_ratio": market.ema_distance_atr_ratio,
            "ema_distance_state": (
                market.ema_distance_state.value
                if market.ema_distance_state is not None
                else "UNKNOWN"
            ),
            "entry_timing_state": (
                market.entry_timing_state.value
                if market.entry_timing_state is not None
                else "UNKNOWN"
            ),
            "entry_timing_reason": market.entry_timing_reason,
            "candle_noise_score": market.payload.get("candle_noise_score"),
            "price_position_score": market.payload.get("price_position_score"),
            "rsi": market.rsi,
            "rsi_overbought": market.payload.get("rsi_overbought"),
            "rsi_oversold": market.payload.get("rsi_oversold"),
            "htf_market_state": (
                market.htf_market_state.value
                if market.htf_market_state is not None
                else "UNKNOWN"
            ),
            "htf_trend": (
                market.htf_trend.value
                if market.htf_trend is not None
                else "UNKNOWN"
            ),
            "htf_trend_strength": (
                market.htf_trend_strength.value
                if market.htf_trend_strength is not None
                else "UNKNOWN"
            ),
            "htf_trend_quality": (
                market.htf_trend_quality.value
                if market.htf_trend_quality is not None
                else "UNKNOWN"
            ),
            "htf_market_phase": (
                market.htf_market_phase.value
                if market.htf_market_phase is not None
                else "UNKNOWN"
            ),
            "htf_alignment": market.htf_alignment,
            "htf_confirmation_score": market.htf_confirmation_score,
            "htf_reason": market.htf_reason,
            "runtime_window_ttl_seconds": self._window_ttl_seconds,
            "runtime_window_size": len(prices),
        }

    # STEP 4. BREAKOUT FIRST
    def _breakout_signal(
        self,
        *,
        market: Any,
        base_payload: dict[str, Any],
    ) -> SignalResult | None:
        """
        Проверяет ранний вход по пробою.

        Почему breakout проверяется ДО обычных блокировок:
        - после флэта EMA часто ещё сжаты;
        - качество может выглядеть шумным;
        - state может не успеть стать идеальным TREND_UP/TREND_DOWN;
        - но momentum уже показывает реальный пробой.

        Условия BUY:
        - momentum_state = BREAKOUT_UP;
        - локальный тренд вверх;
        - momentum вверх;
        - HTF не против входа.

        Условия SELL:
        - momentum_state = BREAKOUT_DOWN;
        - локальный тренд вниз;
        - momentum вниз;
        - HTF не против входа.
        """
        momentum_state = getattr(market, "momentum_state", MomentumState.UNKNOWN)
        momentum_strength = float(getattr(market, "momentum_strength", 0.0) or 0.0)

        if (
            momentum_state == MomentumState.BREAKOUT_UP
            and market.trend == TrendDirection.UP
            and market.momentum_direction == TrendDirection.UP
            and market.htf_alignment in {"ALIGNED", "SAME_INTERVAL"}
        ):
            return SignalResult(
                signal=SignalType.BUY,
                reason="BREAKOUT_UP подтверждён momentum/breakout semantic layer.",
                confidence=self._calculate_breakout_confidence(momentum_strength),
                payload={
                    **base_payload,
                    "entry_block_reason": None,
                    "entry_block_message": None,
                    "breakout_signal": True,
                    "expected_direction": "BUY",
                },
            )

        if (
            momentum_state == MomentumState.BREAKOUT_DOWN
            and market.trend == TrendDirection.DOWN
            and market.momentum_direction == TrendDirection.DOWN
            and market.htf_alignment in {"ALIGNED", "SAME_INTERVAL"}
        ):
            return SignalResult(
                signal=SignalType.SELL,
                reason="BREAKOUT_DOWN подтверждён momentum/breakout semantic layer.",
                confidence=self._calculate_breakout_confidence(momentum_strength),
                payload={
                    **base_payload,
                    "entry_block_reason": None,
                    "entry_block_message": None,
                    "breakout_signal": True,
                    "expected_direction": "SELL",
                },
            )

        return None

    # STEP 5. MARKET BLOCKS
    # определить ранний impulse-вход из RANGE/SQUEEZE
    def _early_impulse_direction(self, market: Any) -> str | None:
        if market.state not in {MarketState.RANGE, MarketState.LOW_VOLATILITY}:
            return None

        if market.market_phase != MarketPhase.IMPULSE:
            return None

        if market.htf_alignment not in {"ALIGNED", "SAME_INTERVAL"}:
            return None

        if (
            market.trend == TrendDirection.UP
            and market.momentum_direction == TrendDirection.UP
            and market.momentum_state in {
                MomentumState.MOMENTUM_UP,
                MomentumState.BREAKOUT_UP,
            }
        ):
            return "BUY"

        if (
            market.trend == TrendDirection.DOWN
            and market.momentum_direction == TrendDirection.DOWN
            and market.momentum_state in {
                MomentumState.MOMENTUM_DOWN,
                MomentumState.BREAKOUT_DOWN,
            }
        ):
            return "SELL"

        return None
    
    def _market_block_signal(
        self,
        *,
        market: Any,
        base_payload: dict[str, Any],
    ) -> SignalResult | None:
        early_impulse_direction = self._early_impulse_direction(market)
        """
        Проверяет защитные фильтры рынка.

        Эта функция НЕ ищет вход.
        Она только отвечает на вопрос:
        "Можно ли вообще рассматривать вход по TREND?"

        Если найден риск — возвращает HOLD с причиной.
        Если всё нормально — возвращает None.
        """
        if (
            market.state not in {MarketState.TREND_UP, MarketState.TREND_DOWN}
            and early_impulse_direction is None
        ):
            return self._hold(
                reason=f"Market state не подходит для TREND: {market.state.value}.",
                block_reason="MARKET_STATE_NOT_TREND",
                block_message="рынок не в тренде",
                payload=base_payload,
            )

        if market.volatility in {
            VolatilityState.LOW,
            VolatilityState.UNKNOWN,
        }:
            return self._hold(
                reason="Волатильность не подходит для входа.",
                block_reason="BAD_VOLATILITY",
                block_message="волатильность не подходит",
                payload=base_payload,
            )

        if market.htf_alignment == "AGAINST":
            return self._hold(
                reason="HTF trend против направления входа.",
                block_reason="HTF_TREND_AGAINST",
                block_message="старший таймфрейм против входа",
                payload=base_payload,
            )

        if market.htf_alignment not in {"ALIGNED", "SAME_INTERVAL"}:
            return self._hold(
                reason="HTF trend не подтвердил направление входа.",
                block_reason="HTF_TREND_NOT_CONFIRMED",
                block_message="старший таймфрейм не подтвердил вход",
                payload=base_payload,
            )

        # Слабый тренд больше не блокируем всегда.
        # Если рынок уже TREND_UP / TREND_DOWN, старший ТФ подтверждает вход,
        # качество не NOISY, а фаза IMPULSE — даём live-окну проверить движение.
        # Это снижает число пропущенных ранних трендовых входов.
        soft_trend_allowed = (
            market.trend_strength == TrendStrength.WEAK
            and market.market_phase == MarketPhase.IMPULSE
            and market.trend_quality != TrendQuality.NOISY
            and market.htf_alignment in {"ALIGNED", "SAME_INTERVAL"}
        )

        if (
            market.trend_strength == TrendStrength.WEAK
            and not soft_trend_allowed
        ):
            return self._hold(
                reason="TREND есть, но сила тренда слабая.",
                block_reason="WEAK_MARKET_TREND",
                block_message="слабый тренд",
                payload=base_payload,
            )

        # RANGE / SQUEEZE больше не режем полностью,
        # если уже появился momentum/breakout по направлению тренда.
        # Иначе бот слишком поздно входит после выхода из флэта.
        phase_breakout_context = (
            (
                market.state == MarketState.TREND_UP
                and market.momentum_state == MomentumState.BREAKOUT_UP
                and market.momentum_direction == TrendDirection.UP
            )
            or (
                market.state == MarketState.TREND_DOWN
                and market.momentum_state == MomentumState.BREAKOUT_DOWN
                and market.momentum_direction == TrendDirection.DOWN
            )
        )

        if (
            market.market_phase in {MarketPhase.RANGE, MarketPhase.SQUEEZE}
            and not phase_breakout_context
        ):
            return self._hold(
                reason="Фаза рынка не подходит для входа по TREND.",
                block_reason=f"MARKET_PHASE_{market.market_phase.value}",
                block_message="фаза рынка не подходит",
                payload=base_payload,
            )

        structure_block = self._market_structure_block(
            market=market,
            base_payload=base_payload,
        )

        if structure_block is not None:
            return structure_block

        momentum_block = self._momentum_block(
            market=market,
            base_payload=base_payload,
        )

        if momentum_block is not None:
            return momentum_block

        # COMPRESSED больше не блокируем здесь жёстко.
        # Сжатие EMA может быть не плохим рынком, а ранней стадией выхода из флэта.
        # OVEREXTENDED оставляем блокировкой, потому что это часто поздний вход.
        if market.ema_distance_state == EmaDistanceState.OVEREXTENDED:
            return self._hold(
                reason="EMA-дистанция не подходит для входа.",
                block_reason=f"EMA_DISTANCE_{market.ema_distance_state.value}",
                block_message="EMA-дистанция не подходит",
                payload=base_payload,
            )

        if market.entry_timing_state in {
            EntryTimingState.LATE,
            EntryTimingState.CHASING,
        }:
            return self._hold(
                reason="Тайминг входа запоздалый.",
                block_reason=f"ENTRY_TIMING_{market.entry_timing_state.value}",
                block_message="тайминг входа запоздалый",
                payload=base_payload,
            )

        if not market.is_trade_allowed:
            return self._hold(
                reason=f"Market filter: {market.reason}",
                block_reason="MARKET_FILTER_BLOCKED",
                block_message="рынок сейчас не подходит для входа",
                payload={
                    **base_payload,
                    "market_filter_blocked": True,
                },
            )

        return None

    def _market_structure_block(
        self,
        *,
        market: Any,
        base_payload: dict[str, Any],
    ) -> SignalResult | None:
        """
        Проверяет структуру рынка.

        Для LONG структура LH/LL плохая:
        - lower high;
        - lower low;
        - рынок делает понижающиеся экстремумы.

        Для SHORT структура HH/HL плохая:
        - higher high;
        - higher low;
        - рынок делает повышающиеся экстремумы.
        """
        market_structure = (
            market.market_structure.value
            if market.market_structure is not None
            else "UNKNOWN"
        )

        if market.state == MarketState.TREND_UP and market_structure == "LH_LL":
            return self._hold(
                reason="Структура рынка против LONG.",
                block_reason="MARKET_STRUCTURE_CONFLICT",
                block_message="структура рынка против LONG",
                payload={
                    **base_payload,
                    "expected_direction": "BUY",
                },
            )

        if market.state == MarketState.TREND_DOWN and market_structure == "HH_HL":
            return self._hold(
                reason="Структура рынка против SHORT.",
                block_reason="MARKET_STRUCTURE_CONFLICT",
                block_message="структура рынка против SHORT",
                payload={
                    **base_payload,
                    "expected_direction": "SELL",
                },
            )

        return None

    def _momentum_block(
        self,
        *,
        market: Any,
        base_payload: dict[str, Any],
    ) -> SignalResult | None:
        # Проверяет, не идёт ли momentum явно против входа.
        # Важно:
        # FLAT больше не блокируем сразу.
        # Если общий TREND-контекст хороший, live-окно ниже само проверит,
        # есть ли реальное движение прямо сейчас.
        # Блокируем только явный momentum против направления сделки.
        if market.state == MarketState.TREND_UP:
            if market.momentum_direction == TrendDirection.DOWN:
                return self._hold(
                    reason="Momentum явно против LONG.",
                    block_reason="MOMENTUM_CONFLICT",
                    block_message="momentum против LONG",
                    payload={
                        **base_payload,
                        "expected_direction": "BUY",
                    },
                )

        if market.state == MarketState.TREND_DOWN:
            if market.momentum_direction == TrendDirection.UP:
                return self._hold(
                    reason="Momentum явно против SHORT.",
                    block_reason="MOMENTUM_CONFLICT",
                    block_message="momentum против SHORT",
                    payload={
                        **base_payload,
                        "expected_direction": "SELL",
                    },
                )

        return None

    # =========================================================
    # STEP 6. LIVE TREND CONFIRMATION
    # =========================================================

    def _live_trend_signal(
        self,
        *,
        market: Any,
        base_payload: dict[str, Any],
        prices: list[float],
    ) -> SignalResult:
        """
        Финальное подтверждение обычного TREND-входа.

        Market analysis говорит:
        - рынок в тренде;
        - направление известно;
        - фильтры разрешили вход.

        Но перед сделкой нужно проверить live-окно:
        - цена реально пошла в нужную сторону;
        - движение не единичный случайный тик;
        - достаточно шагов подтверждают направление.
        """
        first_price = prices[0]
        last_price = prices[-1]

        if first_price <= 0:
            return self._hold(
                reason="Некорректная стартовая цена в live-окне.",
                block_reason="INVALID_WINDOW_PRICE",
                block_message="ошибка цены",
                payload={
                    **base_payload,
                    "prices": prices,
                },
            )

        change_percent = ((last_price - first_price) / first_price) * 100
        direction_ratio = self._direction_ratio(prices, change_percent)

        payload = {
            **base_payload,
            "first_price": first_price,
            "current_price": last_price,
            "change_percent": round(change_percent, 5),
            "direction_ratio": round(direction_ratio, 3),
            "window_size": len(prices),
            "threshold_percent": self._threshold_percent,
            "min_direction_ratio": self._min_direction_ratio,
        }

        early_impulse_direction = self._early_impulse_direction(market)

        if market.state == MarketState.TREND_UP:
            return self._trend_up_signal(
                change_percent=change_percent,
                direction_ratio=direction_ratio,
                payload=payload,
            )

        if market.state == MarketState.TREND_DOWN:
            return self._trend_down_signal(
                change_percent=change_percent,
                direction_ratio=direction_ratio,
                payload=payload,
            )

        if early_impulse_direction == "BUY":
            return self._trend_up_signal(
                change_percent=change_percent,
                direction_ratio=direction_ratio,
                payload={
                    **payload,
                    "early_impulse_signal": True,
                    "expected_direction": "BUY",
                },
            )

        if early_impulse_direction == "SELL":
            return self._trend_down_signal(
                change_percent=change_percent,
                direction_ratio=direction_ratio,
                payload={
                    **payload,
                    "early_impulse_signal": True,
                    "expected_direction": "SELL",
                },
            )
        
        return self._hold(
            reason=f"Market state не подходит для TREND: {market.state.value}.",
            block_reason="MARKET_STATE_NOT_TREND",
            block_message="рынок не в тренде",
            payload=payload,
        )

    def _trend_up_signal(
        self,
        *,
        change_percent: float,
        direction_ratio: float,
        payload: dict[str, Any],
    ) -> SignalResult:
        """
        Формирует BUY, если live-окно подтвердило рост.
        """
        if (
            change_percent >= self._threshold_percent
            and direction_ratio >= self._min_direction_ratio
        ):
            return SignalResult(
                signal=SignalType.BUY,
                reason="TREND_UP подтверждён market analysis и live-импульсом.",
                confidence=self._calculate_confidence(
                    change_percent,
                    direction_ratio,
                ),
                payload={
                    **payload,
                    "entry_block_reason": None,
                    "entry_block_message": None,
                    "expected_direction": "BUY",
                },
            )

        return self._hold(
            reason="TREND_UP есть, но live-импульс вверх недостаточно сильный.",
            block_reason="WEAK_UP_IMPULSE",
            block_message="слабый импульс вверх",
            payload={
                **payload,
                "expected_direction": "BUY",
            },
        )

    def _trend_down_signal(
        self,
        *,
        change_percent: float,
        direction_ratio: float,
        payload: dict[str, Any],
    ) -> SignalResult:
        """
        Формирует SELL, если live-окно подтвердило снижение.
        """
        if (
            change_percent <= -self._threshold_percent
            and direction_ratio >= self._min_direction_ratio
        ):
            return SignalResult(
                signal=SignalType.SELL,
                reason="TREND_DOWN подтверждён market analysis и live-импульсом.",
                confidence=self._calculate_confidence(
                    change_percent,
                    direction_ratio,
                ),
                payload={
                    **payload,
                    "entry_block_reason": None,
                    "entry_block_message": None,
                    "expected_direction": "SELL",
                },
            )

        return self._hold(
            reason="TREND_DOWN есть, но live-импульс вниз недостаточно сильный.",
            block_reason="WEAK_DOWN_IMPULSE",
            block_message="слабый импульс вниз",
            payload={
                **payload,
                "expected_direction": "SELL",
            },
        )

    # =========================================================
    # RESULT HELPERS
    # =========================================================

    def _hold(
        self,
        *,
        reason: str,
        block_reason: str,
        block_message: str,
        payload: dict[str, Any],
    ) -> SignalResult:
        """
        Унифицированный HOLD.

        Все блокировки входа должны проходить через эту функцию,
        чтобы UI, журнал и supervisor получали одинаковые поля:
        - entry_block_reason;
        - entry_block_message.
        """
        return SignalResult(
            signal=SignalType.HOLD,
            reason=reason,
            confidence=0.0,
            payload={
                **payload,
                "entry_block_reason": block_reason,
                "entry_block_message": block_message,
            },
        )

    # =========================================================
    # CALCULATIONS
    # =========================================================

    def _direction_ratio(self, prices: list[float], change_percent: float) -> float:
        """
        Считает долю движений в сторону общего изменения.

        Пример:
        prices = [100, 101, 102, 101.5, 103]

        Если итоговое движение вверх:
        считаем долю шагов, где цена росла.

        Если итоговое движение вниз:
        считаем долю шагов, где цена снижалась.

        Это защищает от ситуации:
        цена вроде изменилась на нужный процент,
        но внутри окна движение было рваным и случайным.
        """
        if len(prices) < 2:
            return 0.0

        up_moves = 0
        down_moves = 0

        for previous_price, current_price in zip(prices, prices[1:]):
            if current_price > previous_price:
                up_moves += 1
            elif current_price < previous_price:
                down_moves += 1

        total_moves = max(1, len(prices) - 1)

        if change_percent >= 0:
            return up_moves / total_moves

        return down_moves / total_moves

    def _calculate_breakout_confidence(self, momentum_strength: float) -> float:
        """
        Рассчитывает confidence для breakout-сигнала.

        Breakout начинается с базовой уверенности 0.55.
        Чем сильнее momentum_strength, тем выше confidence.
        Максимум ограничен 0.95, чтобы execution confidence
        всё равно учитывал spread, подтверждение и качество исполнения.
        """
        strength_score = min(1.0, max(0.0, momentum_strength) / 2)
        confidence = 0.55 + (strength_score * 0.35)

        return round(min(0.95, confidence), 2)

    def _calculate_confidence(
        self,
        change_percent: float,
        direction_ratio: float,
    ) -> float:
        """
        Рассчитывает confidence обычного TREND-сигнала.

        Учитываются:
        - сила движения относительно threshold;
        - направленность движения внутри live-окна.

        Чем сильнее и чище live-импульс,
        тем выше confidence.
        """
        strength = abs(change_percent) / self._threshold_percent

        if strength < 1:
            return 0.0

        strength_score = min(1.0, strength / 3)
        direction_score = min(1.0, direction_ratio)

        confidence = 0.3 + (strength_score * 0.4) + (direction_score * 0.3)

        return round(min(1.0, confidence), 2)

    # =========================================================
    # NORMALIZATION
    # =========================================================

    def _normalized_market_phase(self, market: Any) -> str:
        """
        Безопасно возвращает market_phase строкой.
        """
        if market.market_phase is None:
            return "UNKNOWN"

        return market.market_phase.value

    def _normalized_market_phase_direction(self, market: Any) -> str:
        """
        Безопасно возвращает phase_direction строкой.
        """
        if market.phase_direction is None:
            return "UNKNOWN"

        return market.phase_direction.value

    def _safe_float(
        self,
        value: NumericLike | Decimal | None,
    ) -> float | None:
        """
        Безопасно привести числовое значение к float.

        Поддерживаются:
        - Decimal из канонической модели Quote;
        - float;
        - int;
        - строковое представление числа;
        - None.
        """
        if value is None:
            return None

        try:
            return float(value)
        except (TypeError, ValueError):
            return None