# app/src/trading/market_analysis/service.py

from __future__ import annotations

from enum import StrEnum
from math import isfinite

from src.core.numbers import safe_float
from src.integrations.exchange.service import ExchangeService
from src.trading.market_analysis.indicators import atr, ema, rsi
from src.trading.market_analysis.models import (
    EmaDistanceState,
    EntryTimingState,
    MarketAnalysisResult,
    MarketPhase,
    MarketState,
    MarketStructure,
    MomentumState,
    TrendDirection,
    TrendQuality,
    TrendStrength,
    VolatilityState,
)
from src.trading.market_analysis.structure import market_structure
from src.trading.market_analysis.filters import is_trade_allowed as check_trade_allowed
from src.trading.market_analysis.scoring import (
    classify_ema_distance_state,
    classify_entry_timing,
    trend_quality_score,
)
from src.trading.market_analysis.payload import build_market_analysis_payload
from src.trading.market_analysis.htf import (
    htf_trend_context as build_htf_trend_context,
    htf_volatility_context as build_htf_volatility_context,
)
from src.trading.market_analysis.indicators.momentum import (
    momentum_breakout_state,
    recent_change_percent,
)
from src.trading.market_analysis.indicators.trend import (
    classify_trend,
    classify_trend_quality,
    classify_trend_strength,
    ema_distance_atr_ratio as calculate_ema_distance_atr_ratio,
    ema_slope_percent,
    trend_consistency as calculate_trend_consistency,
    trend_efficiency as calculate_trend_efficiency,
    trend_gap_percent_value,
)
from src.trading.market_analysis.indicators.volatility import (
    adaptive_threshold,
    atr_percent_baseline as calculate_atr_percent_baseline,
    classify_volatility,
)
from src.trading.market_analysis.phase import (
    classify_market_phase,
    classify_phase_direction,
    phase_direction_consistency as calculate_phase_direction_consistency,
)
from src.trading.market_analysis.quality import (
    candle_noise_score as calculate_candle_noise_score,
    price_position_score as calculate_price_position_score,
)
from src.trading.market_analysis.state import classify_market_state
from src.trading.market_analysis.reason import build_market_reason
from src.trading.market_analysis.unknown import build_unknown_market_analysis_result
from src.trading.market_analysis.result import build_market_analysis_result


class EntrySide(StrEnum):
    BUY = "BUY"
    SELL = "SELL"


class MarketAnalysisService:
    # Базовые индикаторы
    _fast_ema_period = 20
    _slow_ema_period = 50
    _atr_period = 14
    _atr_baseline_window = 50
    _rsi_period = 14

    # Минимум свечей нужен, чтобы EMA/ATR/RSI/структура не считались
    # на слишком коротком и случайном участке.
    _min_candles = 60

    # Волатильность
    _low_volatility_atr_percent = 0.05
    _high_volatility_atr_percent = 1.8

    # Тренд
    _ema_fast_slope_window = 3
    _ema_slow_slope_window = 5
    _trend_consistency_window = 20

    # Качество свечей / положение цены
    _candle_noise_window = 12
    _min_clean_body_ratio = 0.45
    _min_clean_candle_score = 0.55

    _price_position_window = 5

    # Было 0.8 — это слишком жёстко.
    # При 5 свечах требовалось почти идеальное удержание цены по стороне EMA.
    # Из-за этого бот пропускал нормальные движения после отката.
    _min_price_position_score = 0.6

    # Фаза рынка / откаты
    _phase_window = 8
    _pullback_min_direction_consistency = 0.6

    # Momentum / breakout
    # Было 5 — поздно замечал импульс.
    # Для 5m свечей окно 3 быстрее ловит начало движения.
    _momentum_window = 3
    _momentum_decay_window = 2

    # Было 40 — слишком далеко смотрел назад.
    # После локального движения breakout часто не определялся вовремя.
    _breakout_lookback = 20

    # Старший таймфрейм
    _htf_interval = "1h"
    _htf_limit = 200

    # Было 0.65 — слишком строго.
    # HTF должен защищать от входа против рынка, но не душить нормальный вход.
    _min_htf_confirmation_score = 0.55

    # Структура рынка
    _structure_window = 30
    _structure_swing_left = 2
    _structure_swing_right = 2

    # RSI
    # Было 68 / 32 — слишком рано считал рынок перегретым.
    # Для трендовой стратегии это мешало входам по сильному движению.
    _rsi_overbought = 72.0
    _rsi_oversold = 28.0

    # Оценка направления входа
    _entry_trend_bonus = 12
    _entry_trend_penalty = 25
    _entry_momentum_bonus = 10
    _entry_momentum_penalty = 20
    _entry_structure_bonus = 8
    _entry_structure_penalty = 18
    _entry_current_candle_bonus = 8
    _entry_current_candle_penalty = 10
    _entry_strong_current_move_bonus = 4
    _entry_htf_against_penalty = 20

    # Общая оценка рынка (market_score)

    _market_score_start = 70

    _market_state_trend_bonus = 10
    _market_state_range_penalty = 15
    _market_state_high_volatility_penalty = 20
    _market_state_low_volatility_penalty = 20
    _market_state_unknown_penalty = 30

    _market_trend_bonus = 5
    _market_trend_flat_penalty = 10
    _market_trend_unknown_penalty = 20

    _market_volatility_normal_bonus = 8
    _market_volatility_high_penalty = 12
    _market_volatility_low_penalty = 10
    _market_volatility_unknown_penalty = 15

    _market_trend_strength_strong_bonus = 8
    _market_trend_strength_normal_bonus = 4
    _market_trend_strength_weak_penalty = 10

    _market_trend_quality_clean_bonus = 8
    _market_trend_quality_normal_bonus = 4
    _market_trend_quality_noisy_penalty = 25

    _market_phase_impulse_bonus = 6
    _market_phase_pullback_penalty = 4
    _market_phase_range_penalty = 10
    _market_phase_squeeze_penalty = 8

    _market_structure_bonus = 6
    _market_structure_mixed_penalty = 10

    _market_breakout_bonus = 6
    _market_momentum_bonus = 4
    _market_exhausted_penalty = 8
    _market_flat_momentum_penalty = 4

    _market_current_candle_large_bonus = 8
    _market_current_candle_medium_bonus = 6
    _market_current_candle_small_bonus = 3

    _market_ema_healthy_bonus = 5
    _market_ema_compressed_penalty = 8
    _market_ema_extended_penalty = 5
    _market_ema_overextended_penalty = 18

    _market_entry_normal_bonus = 6
    _market_entry_early_penalty = 3
    _market_entry_late_penalty = 12
    _market_entry_chasing_penalty = 20

    _market_htf_aligned_bonus = 10
    _market_htf_same_tf_bonus = 5
    _market_htf_neutral_penalty = 4
    _market_htf_against_penalty = 25
    _market_htf_unknown_penalty = 10

    _market_range_with_htf_bonus = 10

    _market_htf_confirmation_bonus = 5
    _market_htf_confirmation_penalty = 10

    _market_trade_not_allowed_penalty = 8

    # Главная функция анализа рынка.
    #
    # Последовательность:
    # 1. Получаем 5m свечи.
    # 2. Считаем EMA / ATR / RSI.
    # 3. Считаем адаптивные пороги от ATR.
    # 4. Определяем волатильность.
    # 5. Определяем направление тренда.
    # 6. Определяем силу и качество тренда.
    # 7. Определяем momentum / breakout.
    # 8. Определяем фазу рынка.
    # 9. Определяем тайминг входа.
    # 10. Определяем структуру рынка.
    # 11. Проверяем старший таймфрейм.
    # 12. Решаем, разрешён ли вход.
    # 13. Собираем payload для стратегии, UI и журнала.
    def analyze(
        self,
        symbol: str,
        *,
        interval: str = "5m",
        limit: int = 200,
    ) -> MarketAnalysisResult:
        try:
            candles = ExchangeService().get_candles(
                symbol=symbol,
                interval=interval,
                limit=limit,
            )
        except Exception as exc:
            return build_unknown_market_analysis_result(
                symbol=symbol,
                interval=interval,
                reason=f"Не удалось получить свечи: {exc}",
                htf_interval=self._htf_interval,
            )

        analysis_symbol = candles[0].symbol if candles else symbol

        if len(candles) < self._min_candles:
            return build_unknown_market_analysis_result(
                symbol=analysis_symbol,
                interval=interval,
                reason="Недостаточно свечей для анализа рынка.",
                candles_count=len(candles),
                htf_interval=self._htf_interval,
            )

        closes: list[float] = []

        for candle in candles:
            close_price_value = safe_float(candle.close_price)

            if (
                close_price_value is None
                or not isfinite(close_price_value)
            ):
                return build_unknown_market_analysis_result(
                    symbol=analysis_symbol,
                    interval=interval,
                    reason=(
                        "Получены некорректные цены закрытия свечей."
                    ),
                    candles_count=len(candles),
                    htf_interval=self._htf_interval,
                )

            closes.append(close_price_value)

        close_price = closes[-1] if closes else None
        ema_fast = ema(closes, self._fast_ema_period)
        ema_slow = ema(closes, self._slow_ema_period)
        atr_value = atr(candles, self._atr_period)
        rsi_value = rsi(closes, self._rsi_period)

        if (
            close_price is None
            or close_price <= 0
            or ema_fast is None
            or ema_slow is None
            or atr_value is None
        ):
            return build_unknown_market_analysis_result(
                symbol=analysis_symbol,
                interval=interval,
                reason="Недостаточно данных для расчёта EMA / ATR.",
                candles_count=len(candles),
                htf_interval=self._htf_interval,
            )

        atr_percent = (atr_value / close_price) * 100

        atr_percent_baseline = calculate_atr_percent_baseline(
            candles=candles,
            close_price=close_price,
            atr_period=self._atr_period,
            atr_baseline_window=self._atr_baseline_window,
        )

        volatility_ratio = (
            atr_percent / atr_percent_baseline
            if atr_percent_baseline is not None and atr_percent_baseline > 0
            else None
        )

        htf_context = build_htf_volatility_context(
            self,
            symbol=analysis_symbol,
            base_interval=interval,
        )

        # HTF context может быть пустым при ошибке получения старшего ТФ.
        htf_context = htf_context or {}

        htf_volatility_ratio = safe_float(
            htf_context.get("htf_volatility_ratio")
        )

        momentum_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.7,
            minimum=0.08,
        )

        momentum_decay_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.12,
            minimum=0.03,
        )

        breakout_distance_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.25,
            minimum=0.04,
        )

        phase_direction_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.18,
            minimum=0.04,
        )

        pullback_min_change_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.45,
            minimum=0.08,
        )

        fast_slope_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.08,
            minimum=0.01,
        )

        slow_slope_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.03,
            minimum=0.005,
        )

        weak_trend_gap_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.18,
            minimum=0.05,
        )

        strong_trend_gap_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.55,
            minimum=0.18,
        )

        trend_direction_gap_threshold_percent = adaptive_threshold(
            atr_percent=atr_percent,
            multiplier=0.12,
            minimum=0.025,
        )

        trend_gap_percent = trend_gap_percent_value(
            ema_fast=ema_fast,
            ema_slow=ema_slow,
        )

        ema_fast_slope_percent = ema_slope_percent(
            closes=closes,
            period=self._fast_ema_period,
            window=self._ema_fast_slope_window,
        )

        ema_slow_slope_percent = ema_slope_percent(
            closes=closes,
            period=self._slow_ema_period,
            window=self._ema_slow_slope_window,
        )

        volatility = classify_volatility(
            atr_percent=atr_percent,
            volatility_ratio=volatility_ratio,
            htf_volatility_ratio=htf_volatility_ratio,
            low_volatility_atr_percent=self._low_volatility_atr_percent,
            high_volatility_atr_percent=self._high_volatility_atr_percent,
        )

        trend = classify_trend(
            ema_fast=ema_fast,
            ema_slow=ema_slow,
            ema_fast_slope_percent=ema_fast_slope_percent,
            ema_slow_slope_percent=ema_slow_slope_percent,
            fast_slope_threshold_percent=fast_slope_threshold_percent,
            slow_slope_threshold_percent=slow_slope_threshold_percent,
            trend_direction_gap_threshold_percent=trend_direction_gap_threshold_percent,
        )

        trend_strength = classify_trend_strength(
            trend_gap_percent=trend_gap_percent,
            weak_threshold_percent=weak_trend_gap_threshold_percent,
            strong_threshold_percent=strong_trend_gap_threshold_percent,
        )

        trend_consistency = calculate_trend_consistency(
            closes=closes,
            trend=trend,
            trend_consistency_window=self._trend_consistency_window,
        )

        trend_efficiency = calculate_trend_efficiency(
            closes=closes,
            trend_consistency_window=self._trend_consistency_window,
        )

        ema_distance_atr_ratio = calculate_ema_distance_atr_ratio(
            ema_fast=ema_fast,
            ema_slow=ema_slow,
            atr_value=atr_value,
        )

        candle_noise_score = calculate_candle_noise_score(
            candles,
            candle_noise_window=self._candle_noise_window,
            min_clean_body_ratio=self._min_clean_body_ratio,
        )

        price_position_score = calculate_price_position_score(
            closes=closes,
            ema_fast=ema_fast,
            trend=trend,
            price_position_window=self._price_position_window,
        )

        trend_quality_score_value = trend_quality_score(
            trend_consistency=trend_consistency,
            trend_efficiency=trend_efficiency,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
        )

        ema_distance_state = classify_ema_distance_state(
            ema_distance_atr_ratio=ema_distance_atr_ratio,
        )

        trend_quality = classify_trend_quality(
            trend_consistency=trend_consistency,
            trend_efficiency=trend_efficiency,
            ema_distance_atr_ratio=ema_distance_atr_ratio,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
            trend_strength=trend_strength,
            min_clean_candle_score=self._min_clean_candle_score,
            min_price_position_score=self._min_price_position_score,
        )

        phase_change_percent = recent_change_percent(
            closes=closes,
            window=self._phase_window,
        )

        phase_direction = classify_phase_direction(
            phase_change_percent,
            threshold_percent=phase_direction_threshold_percent,
        )

        phase_direction_consistency = calculate_phase_direction_consistency(
            closes=closes,
            phase_direction=phase_direction,
            phase_window=self._phase_window,
        )

        # Текущая свеча — последняя свеча из canonical candles.
        # Обычно это ещё формирующаяся свеча текущего 5m-интервала.
        current_interval_change_percent = self._candle_change_percent(
            candles,
            index=-1,
        )

        current_interval_direction = classify_phase_direction(
            current_interval_change_percent,
            threshold_percent=phase_direction_threshold_percent,
        )

        (
            momentum_state,
            momentum_direction,
            momentum_change_percent,
            momentum_strength,
            breakout_level,
            breakout_distance_percent,
            breakout_reason,
        ) = momentum_breakout_state(
            closes=closes,
            momentum_window=self._momentum_window,
            momentum_decay_window=self._momentum_decay_window,
            breakout_lookback=self._breakout_lookback,
            momentum_change_threshold_percent=momentum_threshold_percent,
            momentum_decay_threshold_percent=momentum_decay_threshold_percent,
            breakout_distance_threshold_percent=breakout_distance_threshold_percent,
        )

        market_phase, phase_reason = classify_market_phase(
            trend=trend,
            volatility=volatility,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            rsi_value=rsi_value,
            phase_direction=phase_direction,
            phase_change_percent=phase_change_percent,
            phase_direction_consistency=phase_direction_consistency,
            pullback_min_change_percent=pullback_min_change_percent,
            pullback_min_direction_consistency=self._pullback_min_direction_consistency,
        )

        entry_timing_state, entry_timing_reason = classify_entry_timing(
            ema_distance_state=ema_distance_state,
            momentum_state=momentum_state,
            momentum_strength=momentum_strength,
            market_phase=market_phase,
        )

        market_structure_value, market_structure_reason = market_structure(
            candles,
            atr_percent=atr_percent,
            candle_noise_score=candle_noise_score,
            structure_window=self._structure_window,
            structure_swing_left=self._structure_swing_left,
            structure_swing_right=self._structure_swing_right,
            min_clean_candle_score=self._min_clean_candle_score,
        )

        state = classify_market_state(
            trend=trend,
            volatility=volatility,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            momentum_state=momentum_state,
            momentum_direction=momentum_direction,
            ema_fast_slope_percent=ema_fast_slope_percent,
            ema_slow_slope_percent=ema_slow_slope_percent,
            fast_slope_threshold_percent=fast_slope_threshold_percent,
            slow_slope_threshold_percent=slow_slope_threshold_percent,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
            min_clean_candle_score=self._min_clean_candle_score,
            min_price_position_score=self._min_price_position_score,
        )

        htf_trend_context = build_htf_trend_context(
            self,
            symbol=analysis_symbol,
            base_interval=interval,
            local_state=state,
            local_trend=trend,
        )

        # HTF trend context может быть пустым при ошибке анализа старшего ТФ.
        htf_trend_context = htf_trend_context or {}

        htf_confirmation_score = safe_float(
            htf_trend_context.get("htf_confirmation_score")
        )
        htf_alignment = str(htf_trend_context.get("htf_alignment") or "")

        is_trade_allowed = check_trade_allowed(
            state=state,
            trend=trend,
            volatility=volatility,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            market_structure=market_structure_value,
            momentum_state=momentum_state,
            momentum_direction=momentum_direction,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
            ema_fast_slope_percent=ema_fast_slope_percent,
            ema_distance_state=ema_distance_state,
            entry_timing_state=entry_timing_state,
            fast_slope_threshold_percent=fast_slope_threshold_percent,
            htf_alignment=htf_alignment,
            htf_confirmation_score=htf_confirmation_score,
            min_htf_confirmation_score=self._min_htf_confirmation_score,
            min_clean_candle_score=self._min_clean_candle_score,
            min_price_position_score=self._min_price_position_score,
            rsi_value=rsi_value,
            rsi_overbought=self._rsi_overbought,
            rsi_oversold=self._rsi_oversold,
        )

        # Общая оценка рынка 0..100.
        # Пока используется для UI/диагностики, дальше можно подключить
        # к execution confidence и adaptive sizing.
        market_score = self._market_score(
            state=state,
            trend=trend,
            volatility=volatility,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            market_structure=market_structure_value,
            momentum_state=momentum_state,
            momentum_direction=momentum_direction,
            ema_distance_state=ema_distance_state,
            entry_timing_state=entry_timing_state,
            current_interval_change_percent=current_interval_change_percent,
            current_interval_direction=current_interval_direction,
            htf_alignment=htf_alignment,
            htf_confirmation_score=htf_confirmation_score,
            is_trade_allowed=is_trade_allowed,
        )
        market_score_label = self._market_score_label(market_score)

        market_long_score = self._entry_score(
            side=EntrySide.BUY,
            market_score=market_score,
            trend=trend,
            momentum_direction=momentum_direction,
            market_structure=market_structure_value,
            current_interval_direction=current_interval_direction,
            current_interval_change_percent=current_interval_change_percent,
            htf_alignment=htf_alignment,
        )

        market_short_score = self._entry_score(
            side=EntrySide.SELL,
            market_score=market_score,
            trend=trend,
            momentum_direction=momentum_direction,
            market_structure=market_structure_value,
            current_interval_direction=current_interval_direction,
            current_interval_change_percent=current_interval_change_percent,
            htf_alignment=htf_alignment,
        )

        # API Dzengi возвращает последнюю формирующуюся свечу:
        #   index=-1 — текущая незакрытая свеча;
        #   index=-2 — последняя полностью закрытая свеча.
        last_closed_candle_change_percent = self._candle_change_percent(
            candles,
            index=-2,
        )

        last_closed_candle_direction = classify_phase_direction(
            last_closed_candle_change_percent,
            threshold_percent=phase_direction_threshold_percent,
        )

        reason = build_market_reason(
            state=state,
            volatility=volatility,
            atr_percent=atr_percent,
            rsi_value=rsi_value,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            momentum_state=momentum_state,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
            ema_distance_state=ema_distance_state,
            entry_timing_state=entry_timing_state,
            min_clean_candle_score=self._min_clean_candle_score,
            min_price_position_score=self._min_price_position_score,
            rsi_overbought=self._rsi_overbought,
            rsi_oversold=self._rsi_oversold,
        )

        payload = build_market_analysis_payload(
            symbol=analysis_symbol,
            interval=interval,
            state=state,
            trend=trend,
            volatility=volatility,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            phase_direction=phase_direction,
            phase_change_percent=phase_change_percent,
            phase_direction_consistency=phase_direction_consistency,
            current_interval_change_percent=current_interval_change_percent,
            current_interval_direction=current_interval_direction,
            current_interval_label=interval,
            last_closed_candle_change_percent=last_closed_candle_change_percent,
            last_closed_candle_direction=last_closed_candle_direction,
            phase_reason=phase_reason,
            market_structure=market_structure_value,
            market_structure_reason=market_structure_reason,
            momentum_state=momentum_state,
            momentum_direction=momentum_direction,
            momentum_change_percent=momentum_change_percent,
            momentum_strength=momentum_strength,
            breakout_level=breakout_level,
            breakout_distance_percent=breakout_distance_percent,
            breakout_reason=breakout_reason,
            trend_gap_percent=trend_gap_percent,
            ema_fast_slope_percent=ema_fast_slope_percent,
            ema_slow_slope_percent=ema_slow_slope_percent,
            trend_consistency=trend_consistency,
            trend_efficiency=trend_efficiency,
            trend_quality_score_value=trend_quality_score_value,
            ema_distance_atr_ratio=ema_distance_atr_ratio,
            ema_distance_state=ema_distance_state,
            entry_timing_state=entry_timing_state,
            entry_timing_reason=entry_timing_reason,
            candle_noise_score=candle_noise_score,
            price_position_score=price_position_score,
            close_price=close_price,
            ema_fast_period=self._fast_ema_period,
            ema_slow_period=self._slow_ema_period,
            ema_fast=ema_fast,
            ema_slow=ema_slow,
            atr_period=self._atr_period,
            atr_value=atr_value,
            atr_percent=atr_percent,
            atr_percent_baseline=atr_percent_baseline,
            volatility_ratio=volatility_ratio,
            rsi_period=self._rsi_period,
            rsi_value=rsi_value,
            rsi_overbought=self._rsi_overbought,
            rsi_oversold=self._rsi_oversold,
            candles_count=len(candles),
            is_trade_allowed=is_trade_allowed,
            htf_context=htf_context,
            htf_trend_context=htf_trend_context,
            market_score=market_score,
            market_score_label=market_score_label,
            market_long_score=market_long_score,
            market_short_score=market_short_score,
        )

        return build_market_analysis_result(
            symbol=analysis_symbol,
            interval=interval,
            state=state,
            trend=trend,
            volatility=volatility,
            close_price=close_price,
            ema_fast=ema_fast,
            ema_slow=ema_slow,
            atr_value=atr_value,
            atr_percent=atr_percent,
            rsi_value=rsi_value,
            candles_count=len(candles),
            reason=reason,
            is_trade_allowed=is_trade_allowed,
            payload=payload,
            trend_strength=trend_strength,
            trend_quality=trend_quality,
            market_phase=market_phase,
            market_structure=market_structure_value,
            market_structure_reason=market_structure_reason,
            trend_gap_percent=trend_gap_percent,
            trend_consistency=trend_consistency,
            trend_efficiency=trend_efficiency,
            ema_distance_atr_ratio=ema_distance_atr_ratio,
            phase_direction=phase_direction,
            phase_change_percent=phase_change_percent,
            phase_reason=phase_reason,
            ema_fast_slope_percent=ema_fast_slope_percent,
            ema_slow_slope_percent=ema_slow_slope_percent,
            phase_direction_consistency=phase_direction_consistency,
            current_interval_change_percent=current_interval_change_percent,
            current_interval_direction=current_interval_direction,
            current_interval_label=interval,
            momentum_state=momentum_state,
            momentum_direction=momentum_direction,
            momentum_change_percent=momentum_change_percent,
            momentum_strength=momentum_strength,
            breakout_level=breakout_level,
            breakout_distance_percent=breakout_distance_percent,
            breakout_reason=breakout_reason,
            trend_quality_score_value=trend_quality_score_value,
            ema_distance_state=ema_distance_state,
            entry_timing_state=entry_timing_state,
            entry_timing_reason=entry_timing_reason,
            htf_interval=self._htf_interval,
            htf_context=htf_context,
            htf_trend_context=htf_trend_context,
            market_score=market_score,
            market_score_label=market_score_label,
            market_long_score=market_long_score,
            market_short_score=market_short_score,
        )
    
    def _market_score(
        self,
        *,
        state: MarketState,
        trend: TrendDirection,
        volatility: VolatilityState,
        trend_strength: TrendStrength,
        trend_quality: TrendQuality,
        market_phase: MarketPhase,
        market_structure: MarketStructure,
        momentum_state: MomentumState,
        momentum_direction: TrendDirection,
        ema_distance_state: EmaDistanceState,
        entry_timing_state: EntryTimingState,
        current_interval_change_percent: float | None,
        current_interval_direction: TrendDirection,
        htf_alignment: str,
        htf_confirmation_score: float | None,
        is_trade_allowed: bool,
    ) -> int:
        # Стартовая точка — нейтрально-хороший рынок.
        # Все коэффициенты вынесены в начало класса, чтобы стратегию было
        # проще калибровать без поиска чисел внутри логики.
        score = self._market_score_start

        if state in {MarketState.TREND_UP, MarketState.TREND_DOWN}:
            score += self._market_state_trend_bonus
        elif state == MarketState.RANGE:
            score -= self._market_state_range_penalty
        elif state == MarketState.HIGH_VOLATILITY:
            score -= self._market_state_high_volatility_penalty
        elif state == MarketState.LOW_VOLATILITY:
            score -= self._market_state_low_volatility_penalty
        elif state == MarketState.UNKNOWN:
            score -= self._market_state_unknown_penalty

        if trend in {TrendDirection.UP, TrendDirection.DOWN}:
            score += self._market_trend_bonus
        elif trend == TrendDirection.FLAT:
            score -= self._market_trend_flat_penalty
        elif trend == TrendDirection.UNKNOWN:
            score -= self._market_trend_unknown_penalty

        if volatility == VolatilityState.NORMAL:
            score += self._market_volatility_normal_bonus
        elif volatility == VolatilityState.HIGH:
            score -= self._market_volatility_high_penalty
        elif volatility == VolatilityState.LOW:
            score -= self._market_volatility_low_penalty
        elif volatility == VolatilityState.UNKNOWN:
            score -= self._market_volatility_unknown_penalty

        if trend_strength == TrendStrength.STRONG:
            score += self._market_trend_strength_strong_bonus
        elif trend_strength == TrendStrength.NORMAL:
            score += self._market_trend_strength_normal_bonus
        elif trend_strength == TrendStrength.WEAK:
            score -= self._market_trend_strength_weak_penalty

        if trend_quality == TrendQuality.CLEAN:
            score += self._market_trend_quality_clean_bonus
        elif trend_quality == TrendQuality.NORMAL:
            score += self._market_trend_quality_normal_bonus
        elif trend_quality == TrendQuality.NOISY:
            score -= self._market_trend_quality_noisy_penalty

        if market_phase == MarketPhase.IMPULSE:
            score += self._market_phase_impulse_bonus
        elif market_phase == MarketPhase.PULLBACK:
            score -= self._market_phase_pullback_penalty
        elif market_phase == MarketPhase.RANGE:
            score -= self._market_phase_range_penalty
        elif market_phase == MarketPhase.SQUEEZE:
            score -= self._market_phase_squeeze_penalty

        if market_structure in {MarketStructure.HH_HL, MarketStructure.LH_LL}:
            score += self._market_structure_bonus
        elif market_structure == MarketStructure.MIXED:
            score -= self._market_structure_mixed_penalty

        if momentum_state in {MomentumState.BREAKOUT_UP, MomentumState.BREAKOUT_DOWN}:
            score += self._market_breakout_bonus
        elif momentum_state in {MomentumState.MOMENTUM_UP, MomentumState.MOMENTUM_DOWN}:
            score += self._market_momentum_bonus
        elif momentum_state == MomentumState.EXHAUSTED:
            score -= self._market_exhausted_penalty
        elif momentum_direction == TrendDirection.FLAT:
            score -= self._market_flat_momentum_penalty

        current_change = safe_float(current_interval_change_percent) or 0.0

        if current_interval_direction in {TrendDirection.UP, TrendDirection.DOWN}:
            if abs(current_change) >= 0.08:
                score += self._market_current_candle_large_bonus
            elif abs(current_change) >= 0.05:
                score += self._market_current_candle_medium_bonus
            elif abs(current_change) >= 0.03:
                score += self._market_current_candle_small_bonus

        if ema_distance_state == EmaDistanceState.HEALTHY:
            score += self._market_ema_healthy_bonus
        elif ema_distance_state == EmaDistanceState.COMPRESSED:
            score -= self._market_ema_compressed_penalty
        elif ema_distance_state == EmaDistanceState.EXTENDED:
            score -= self._market_ema_extended_penalty
        elif ema_distance_state == EmaDistanceState.OVEREXTENDED:
            score -= self._market_ema_overextended_penalty

        if entry_timing_state == EntryTimingState.NORMAL:
            score += self._market_entry_normal_bonus
        elif entry_timing_state == EntryTimingState.EARLY:
            score -= self._market_entry_early_penalty
        elif entry_timing_state == EntryTimingState.LATE:
            score -= self._market_entry_late_penalty
        elif entry_timing_state == EntryTimingState.CHASING:
            score -= self._market_entry_chasing_penalty

        htf_alignment_upper = str(htf_alignment or "").upper()

        if htf_alignment_upper == "ALIGNED":
            score += self._market_htf_aligned_bonus
        elif htf_alignment_upper == "SAME_INTERVAL":
            score += self._market_htf_same_tf_bonus
        elif htf_alignment_upper == "NEUTRAL":
            score -= self._market_htf_neutral_penalty
        elif htf_alignment_upper == "AGAINST":
            score -= self._market_htf_against_penalty
        elif htf_alignment_upper == "UNKNOWN":
            score -= self._market_htf_unknown_penalty

        # Если локально RANGE, но есть импульс/откат по направлению HTF,
        # не режем рынок слишком жёстко.
        if (
            state == MarketState.RANGE
            and market_phase in {MarketPhase.PULLBACK, MarketPhase.IMPULSE}
            and htf_alignment_upper in {"ALIGNED", "SAME_INTERVAL"}
        ):
            score += self._market_range_with_htf_bonus

        if htf_confirmation_score is not None:
            if htf_confirmation_score >= 0.75:
                score += self._market_htf_confirmation_bonus
            elif htf_confirmation_score < self._min_htf_confirmation_score:
                score -= self._market_htf_confirmation_penalty

        if not is_trade_allowed:
            score -= self._market_trade_not_allowed_penalty

        # Шумный рынок не должен выглядеть как идеальный,
        # даже если тренд и HTF совпадают.
        if trend_quality == TrendQuality.NOISY:
            score = min(score, 74)

        # Если вход запрещён market-фильтром,
        # рынок не должен быть "отличным".
        if not is_trade_allowed:
            score = min(score, 69)
            
        return max(0, min(100, int(round(score))))

    def _market_score_label(self, score: int) -> str:
        if score >= 90:
            return "отличный"

        if score >= 75:
            return "благоприятный"

        if score >= 55:
            return "нейтральный"

        if score >= 35:
            return "сложный"

        return "неблагоприятный"
    
    def _entry_score(
        self,
        *,
        side: EntrySide,
        market_score: int,
        trend: TrendDirection,
        momentum_direction: TrendDirection,
        market_structure: MarketStructure,
        current_interval_direction: TrendDirection,
        current_interval_change_percent: float | None,
        htf_alignment: str,
    ) -> int:
        """
        Направленная оценка входа.

        market_score — общий фон рынка.
        entry_score — оценка конкретного направления:
        Long или Short.

        Поэтому Long/Short могут сильно отличаться даже при одном общем рынке.
        """
        score = market_score

        expected_direction = (
            TrendDirection.UP
            if side == EntrySide.BUY
            else TrendDirection.DOWN
        )
        opposite_direction = (
            TrendDirection.DOWN
            if side == EntrySide.BUY
            else TrendDirection.UP
        )

        positive_structure = (
            MarketStructure.HH_HL
            if side == EntrySide.BUY
            else MarketStructure.LH_LL
        )
        negative_structure = (
            MarketStructure.LH_LL
            if side == EntrySide.BUY
            else MarketStructure.HH_HL
        )

        if trend == expected_direction:
            score += self._entry_trend_bonus
        elif trend == opposite_direction:
            score -= self._entry_trend_penalty

        if momentum_direction == expected_direction:
            score += self._entry_momentum_bonus
        elif momentum_direction == opposite_direction:
            score -= self._entry_momentum_penalty

        if market_structure == positive_structure:
            score += self._entry_structure_bonus
        elif market_structure == negative_structure:
            score -= self._entry_structure_penalty

        if current_interval_direction == expected_direction:
            score += self._entry_current_candle_bonus
        elif current_interval_direction == opposite_direction:
            score -= self._entry_current_candle_penalty

        current_change = abs(current_interval_change_percent or 0.0)

        if (
            current_interval_direction == expected_direction
            and current_change >= 0.08
        ):
            score += self._entry_strong_current_move_bonus

        if str(htf_alignment or "").upper() == "AGAINST":
            score -= self._entry_htf_against_penalty

        return max(0, min(100, int(round(score))))

    def _candle_change_percent(
        self,
        candles,
        *,
        index: int,
    ) -> float:
        """
        Изменение конкретной свечи в процентах.
        Для API Dzengi:
        index=-1 — текущая формирующаяся свеча.
        index=-2 — последняя полностью закрытая свеча.
        """
        if not candles:
            return 0.0

        try:
            candle = candles[index]
        except IndexError:
            return 0.0

        open_price = safe_float(getattr(candle, "open_price", None))
        close_price = safe_float(getattr(candle, "close_price", None))

        if open_price is None or open_price <= 0:
            return 0.0

        if close_price is None or close_price <= 0:
            return 0.0

        return round(((close_price - open_price) / open_price) * 100, 5)