# app/src/trading/execution/flip.py

from __future__ import annotations

import time
from typing import Protocol

from src.core.event_bus import EventBus
from src.core.numbers import safe_float
from src.core.types import JsonDict, NumericLike
from src.trading.auto.state import AutoTradeState
from src.trading.auto.state_reset import (
    reset_autonomous_runtime_state,
    reset_execution_block_state,
)
from src.trading.execution.models import ExecutionDecision
from src.trading.execution.payloads import (
    build_adaptive_size_payload,
    build_autonomous_payload,
    build_execution_quality_payload,
    build_market_context_payload,
    build_position_health_payload,
    build_position_intelligence_payload,
    build_runtime_protection_payload,
    build_signal_payload,
)
from src.trading.execution.pricing import ExecutionPrice
from src.trading.journal.service import JournalService
from src.trading.position.state import PositionState
from src.trading.execution.position_metrics import build_position_metrics
from src.trading.execution.constants import (
    EXECUTION_ACTION_FLIP_BLOCKED,
    EXECUTION_ACTION_NONE,
    EXECUTION_MAX_CONSECUTIVE_LOSSES,
    EXECUTION_TYPE_FLIP,
    EXECUTION_TYPE_FLIP_BLOCKED,
    EXECUTION_TYPE_FLIP_REJECTED,
    FLIP_BREAKOUT_CONFIDENCE_THRESHOLD,
    FLIP_MIN_EXECUTION_CONFIDENCE,
    FLIP_MIN_HTF_CONFIRMATION_SCORE,
    MARKET_PHASE_FLIP_BLOCKED,
    MARKET_STATE_FLIP_BLOCKED,
    POSITION_SIDE_LONG,
    POSITION_SIDE_NONE,
    POSITION_SIDE_SHORT,
    PRICING_FLIP_MODE,
    SIGNAL_BUY,
    SIGNAL_SELL,
    build_flip_action,
)


class _ExecutionFlipProtocol(Protocol):
    _position: PositionState
    _min_flip_confidence: float
    _min_flip_repeat_count: int
    _min_flip_hold_seconds: int
    _flip_cooldown_seconds: int
    _loss_flip_confidence: float
    _last_flip_block_key: str | None
    _last_flip_diagnostic_key: str | None

    def _create_trade_id(self, state: AutoTradeState, side: str) -> str:
        ...

    def _exit_price_for_side(self, symbol: str, side: str) -> ExecutionPrice:
        ...

    def _entry_price_for_side(self, symbol: str, side: str) -> ExecutionPrice:
        ...

    def _calculate_position_size(
        self,
        state: AutoTradeState,
        *,
        entry_price: float | None = None,
    ) -> float:
        ...

    def _adjust_size_by_margin_limit(
        self,
        *,
        state: AutoTradeState,
        entry_price: float,
        size: float,
    ) -> float:
        ...

    def _sync_effective_risk_after_margin_limit(
        self,
        state: AutoTradeState,
        *,
        base_size: float,
        final_size: float,
    ) -> None:
        ...

    def _round_size(self, size: NumericLike | None) -> float:
        ...

    def _sync_state_from_position(self, state: AutoTradeState) -> None:
        ...

    def _now_time(self) -> str:
        ...

    def _reset_runtime_protection_state(self, state: AutoTradeState) -> None:
        ...

    def _reset_position_lifecycle_state(self, state: AutoTradeState) -> None:
        ...


class ExecutionFlipMixin(_ExecutionFlipProtocol):
    # ---------- Diagnostics ----------
    # Записать диагностическое событие по пути flip без изменения торговой логики.
    # Эти события нужны, чтобы понять, где именно разворот был остановлен:
    # READY, supervisor, flip guard, price, sizing или успешное исполнение.
    def _log_flip_diagnostic(
        self,
        *,
        state: AutoTradeState,
        stage: str,
        reason: str,
    ) -> None:
        position = type(self)._position
        confidence = safe_float(state.last_signal_confidence) or 0.0
        repeat_count = int(safe_float(state.last_signal_repeat_count) or 0)

        # Дедупликация защищает журнал от спама на каждом тике одного и того же
        # состояния. Если причина/стадия изменилась — событие будет записано.
        key = (
            f"{stage}:"
            f"{state.symbol}:"
            f"{position.side}:"
            f"{state.last_signal}:"
            f"{repeat_count}:"
            f"{confidence:.2f}:"
            f"{reason}"
        )

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

        type(self)._last_flip_diagnostic_key = key

        payload = {
            "execution_type": "FLIP_DIAGNOSTIC",
            "stage": stage,
            "reason": reason,
            "symbol": state.symbol,
            "position_side": position.side,
            "signal": state.last_signal,
            "signal_confidence": confidence,
            "signal_repeat_count": repeat_count,
            "decision_status": state.decision_status,
            "is_signal_ready": state.is_signal_ready,
            "is_signal_confirmed": state.is_signal_confirmed,
            "execution_confidence_score": state.execution_confidence_score,
            "execution_confidence_required_score": (
                state.execution_confidence_required_score
            ),
            "execution_block_reason": state.execution_block_reason,
            "entry_block_reason": state.entry_block_reason,
            "entry_block_message": state.entry_block_message,
            "unrealized_pnl_usd": state.unrealized_pnl_usd,
            **build_market_context_payload(state),
        }

        JournalService().log_ui_info(
            event_type="position_flip_diagnostic",
            message=f"Flip diagnostic: {stage} · {reason}",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

        EventBus.emit("paper_flip_diagnostic", payload)

    # ---------- Payload builders ----------
    # Собрать payload отказа flip без изменения состояния позиции.
    def _build_flip_rejected_payload(
        self,
        *,
        state: AutoTradeState,
        reason: str,
    ) -> JsonDict:
        position = type(self)._position

        return {
            "execution_type": EXECUTION_TYPE_FLIP_REJECTED,
            "symbol": state.symbol,
            "position_side": position.side,
            **build_signal_payload(state),
            **build_execution_quality_payload(state),
            "reject_reason": reason,
            "unrealized_pnl_usd": state.unrealized_pnl_usd,
            "entry_timing_state": state.entry_timing_state,
            **build_market_context_payload(state),
            "opened_at": position.opened_at,
            "updated_at": position.updated_at,
        }

    # Собрать payload блокировки flip guard'ами без изменения состояния позиции.
    def _build_flip_blocked_payload(
        self,
        *,
        state: AutoTradeState,
        reason: str,
        confidence: float,
    ) -> JsonDict:
        position = type(self)._position

        return {
            "execution_type": EXECUTION_TYPE_FLIP_BLOCKED,
            "symbol": state.symbol,
            "position_side": position.side,
            **build_signal_payload(
                state,
                confidence=confidence,
                reason=reason,
            ),
            **build_execution_quality_payload(state),
            "unrealized_pnl_usd": state.unrealized_pnl_usd,
            **build_market_context_payload(state),
            "opened_at": position.opened_at,
            "updated_at": position.updated_at,
        }

    # Собрать payload выполненного flip.
    # Здесь фиксируем и закрытую старую позицию, и параметры новой позиции.
    def _build_flip_executed_payload(
        self,
        *,
        state: AutoTradeState,
        old_trade_id: str | None,
        old_trade_sequence: int | None,
        old_trade_cycle_number: int | None,
        new_trade_id: str,
        old_side: str,
        new_side: str,
        old_entry_price: float | None,
        exit_price: float,
        new_entry_price: float,
        old_size: float | None,
        new_size: float,
        old_leverage: float | None,
        pnl: float,
        metrics,
        flip_action: str,
        now: str,
        opened_monotonic_at: float,
        old_opened_at: str | None,
        exit_execution: ExecutionPrice,
        entry_execution: ExecutionPrice,
    ) -> JsonDict:
        return {
            "trade_id": old_trade_id,
            "closed_trade_id": old_trade_id,
            "new_trade_id": new_trade_id,
            "trade_sequence": old_trade_sequence,
            "trade_cycle_number": old_trade_cycle_number,
            "closed_trade_sequence": old_trade_sequence,
            "closed_trade_cycle_number": old_trade_cycle_number,
            "new_trade_sequence": state.trade_sequence,
            "new_trade_cycle_number": state.current_trade_cycle_number,
            "execution_type": EXECUTION_TYPE_FLIP,
            "action": flip_action,
            "symbol": state.symbol,
            "old_side": old_side,
            "new_side": new_side,
            "side": new_side,
            "entry_price": old_entry_price,
            "exit_price": exit_price,
            "new_entry_price": new_entry_price,
            "old_size": old_size,
            "new_size": new_size,
            "size": new_size,
            "old_leverage": old_leverage,
            "leverage": state.leverage,
            "pnl": pnl,

            # ---------- PnL / Metrics ----------
            "net_pnl_usd": metrics.net_pnl_usd,
            "gross_pnl_usd": metrics.gross_pnl_usd,
            "commission_usd": metrics.commission_usd,
            "overnight_cashflow_usd": metrics.overnight_cashflow_usd,
            "pnl_percent": metrics.pnl_percent,
            "price_move_percent": metrics.price_move_percent,
            "entry_notional_usd": metrics.entry_notional_usd,
            "current_notional_usd": metrics.current_notional_usd,
            "margin_usd": metrics.margin_usd,
            "hold_seconds": metrics.hold_seconds,
            "overnight_count": metrics.overnight_count,

            **build_signal_payload(state),
            **build_execution_quality_payload(state),
            **build_adaptive_size_payload(state),
            "opened_at": old_opened_at,
            "new_opened_monotonic_at": opened_monotonic_at,
            "closed_at": now,
            "new_opened_at": now,
            **build_market_context_payload(state),

            # ---------- Position health ----------
            **build_position_health_payload(state),

            # ---------- Position intelligence ----------
            **build_position_intelligence_payload(state),

            # ---------- Autonomous ----------
            **build_autonomous_payload(state),

            # ---------- Runtime protection ----------
            **build_runtime_protection_payload(state),

            # ---------- Pricing diagnostics ----------
            "pricing": PRICING_FLIP_MODE,
            "exit_pricing_role": exit_execution.pricing_role,
            "exit_price_source": exit_execution.source,
            "exit_price_age_seconds": exit_execution.age_seconds,
            "exit_price_updated_at": exit_execution.updated_at,
            "entry_pricing_role": entry_execution.pricing_role,
            "entry_price_source": entry_execution.source,
            "entry_price_age_seconds": entry_execution.age_seconds,
            "entry_price_updated_at": entry_execution.updated_at,
        }

    # ---------- Journal helpers ----------
    # Записать отказ flip execution в журнал.
    # Reject отличается от block: reject происходит уже внутри попытки исполнения,
    # например из-за отсутствия цены или невозможности рассчитать size.
    def _log_flip_rejected(
        self,
        *,
        state: AutoTradeState,
        reason: str,
    ) -> None:
        payload = self._build_flip_rejected_payload(
            state=state,
            reason=reason,
        )

        JournalService().log_ui_warning(
            event_type="position_flip_rejected",
            message=f"Flip позиции отклонён: {reason}",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

    # ---------- Decision helpers ----------
    # Записать отказ flip и вернуть стандартное решение без исполнения.
    # diagnostic_stage указывает, на каком техническом этапе flip был отклонён.
    def _reject_flip(
        self,
        *,
        state: AutoTradeState,
        reason: str,
        diagnostic_stage: str | None = None,
    ) -> ExecutionDecision:
        if diagnostic_stage is not None:
            self._log_flip_diagnostic(
                state=state,
                stage=diagnostic_stage,
                reason=reason,
            )

        self._log_flip_rejected(state=state, reason=reason)
        return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)

    # Записать блокировку flip guard'ами в state, journal и event bus.
    def _block_flip(
        self,
        state: AutoTradeState,
        reason: str,
    ) -> ExecutionDecision:
        position = type(self)._position
        confidence = safe_float(state.last_signal_confidence) or 0.0

        # Диагностика отдельно показывает, что flip дошёл до flip.py,
        # но был остановлен именно flip-specific guard'ами.
        self._log_flip_diagnostic(
            state=state,
            stage="FLIP_BLOCKED_BY_FLIP_GUARD",
            reason=reason,
        )

        state.execution_block_reason = reason
        state.last_flip_block_reason = reason
        state.last_execution_action = EXECUTION_ACTION_FLIP_BLOCKED
        state.last_execution_reason = reason

        block_key = (
            f"{position.side}:"
            f"{state.last_signal}:"
            f"{state.last_signal_repeat_count}:"
            f"{confidence:.2f}:"
            f"{reason}"
        )

        if block_key != type(self)._last_flip_block_key:
            type(self)._last_flip_block_key = block_key

            payload = self._build_flip_blocked_payload(
                state=state,
                reason=reason,
                confidence=confidence,
            )

            JournalService().log_ui_warning(
                event_type="position_flip_blocked",
                message=f"Смена направления позиции заблокирована: {reason}.",
                screen="auto",
                action="paper_execution",
                payload=payload,
            )

            EventBus.emit("paper_flip_blocked", payload)

        return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)

    # ---------- Flip checks ----------
    # Проверить, нужен ли flip позиции по текущему сигналу.
    # Здесь только факт противоположного сигнала, без оценки качества рынка.
    def _should_flip_position(self, state: AutoTradeState) -> bool:
        position = type(self)._position
        signal = str(state.last_signal or "").upper()

        if position.side == POSITION_SIDE_NONE:
            return False

        if position.side == POSITION_SIDE_LONG and signal == SIGNAL_SELL:
            return True

        if position.side == POSITION_SIDE_SHORT and signal == SIGNAL_BUY:
            return True

        return False

    # Определить причину блокировки flip, если flip сейчас опасен.
    # Важно: пока торговую логику не меняем, только делаем её наблюдаемой.
    def _flip_block_reason(self, state: AutoTradeState) -> str | None:
        position = type(self)._position

        signal = str(state.last_signal or "").upper()
        confidence = safe_float(state.last_signal_confidence) or 0.0
        execution_confidence = safe_float(state.execution_confidence_score)
        repeat_count = int(safe_float(state.last_signal_repeat_count) or 0)
        unrealized_pnl = safe_float(state.unrealized_pnl_usd) or 0.0

        # hold_seconds считаем через position metrics.
        # current_price пока берём entry_price, чтобы не менять текущую механику.
        metrics = build_position_metrics(
            position,
            current_price=position.entry_price,
        )
        hold_seconds = metrics.hold_seconds

        market_state = str(getattr(state, "market_state", "") or "").upper()
        market_trend = str(getattr(state, "market_trend", "") or "").upper()
        market_phase = str(getattr(state, "market_phase", "") or "").upper()
        market_quality = str(getattr(state, "market_trend_quality", "") or "").upper()
        market_structure = str(getattr(state, "market_structure", "") or "").upper()
        htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()
        htf_score = safe_float(getattr(state, "htf_confirmation_score", None))
        entry_timing = str(getattr(state, "entry_timing_state", "") or "").upper()

        momentum_direction = str(getattr(state, "momentum_direction", "") or "").upper()
        momentum_state = str(getattr(state, "momentum_state", "") or "").upper()

        if confidence < self._min_flip_confidence:
            return (
                "уверенность flip-сигнала ниже порога "
                f"({confidence:.2f} < {self._min_flip_confidence:.2f})"
            )

        if (
            execution_confidence is not None
            and execution_confidence < FLIP_MIN_EXECUTION_CONFIDENCE
        ):
            return (
                "execution confidence для flip недостаточный "
                f"({execution_confidence:.2f} < {FLIP_MIN_EXECUTION_CONFIDENCE:.2f})"
            )

        if repeat_count < self._min_flip_repeat_count:
            return (
                "flip-сигнал ещё не подтверждён нужным количеством повторов "
                f"({repeat_count} < {self._min_flip_repeat_count})"
            )

        if hold_seconds is not None and hold_seconds < self._min_flip_hold_seconds:
            return (
                "позиция открыта слишком недавно "
                f"({hold_seconds}с < {self._min_flip_hold_seconds}с)"
            )

        if self._flip_cooldown_active(state):
            return f"flip cooldown активен (< {self._flip_cooldown_seconds}с)"

        if market_state in MARKET_STATE_FLIP_BLOCKED:
            return f"market state не подходит для flip: {market_state or 'UNKNOWN'}"

        if market_phase in MARKET_PHASE_FLIP_BLOCKED:
            return f"market phase не подходит для flip: {market_phase or 'UNKNOWN'}"

        if market_quality == "NOISY":
            return "flip заблокирован: тренд шумный"

        if htf_alignment != "ALIGNED":
            return f"flip заблокирован: HTF не подтверждает направление ({htf_alignment or 'UNKNOWN'})"

        if htf_score is None or htf_score < FLIP_MIN_HTF_CONFIRMATION_SCORE:
            return f"flip заблокирован: слабое HTF-подтверждение ({htf_score})"

        if entry_timing in {"LATE", "CHASING"}:
            return f"flip заблокирован: плохой тайминг входа ({entry_timing})"

        if signal == SIGNAL_BUY:
            if market_trend == "DOWN":
                return "BUY flip против основного market trend"

            if momentum_direction != "UP":
                return "momentum не подтверждает BUY flip"

            if momentum_state == "BREAKOUT_DOWN":
                return "BUY flip против breakout вниз"

            if market_structure == "LH_LL":
                return "BUY flip против bearish market structure"

        if signal == SIGNAL_SELL:
            if market_trend == "UP":
                return "SELL flip против основного market trend"

            if momentum_direction != "DOWN":
                return "momentum не подтверждает SELL flip"

            if momentum_state == "BREAKOUT_UP":
                return "SELL flip против breakout вверх"

            if market_structure == "HH_HL":
                return "SELL flip против bullish market structure"

        if market_structure == "MIXED":
            return "flip заблокирован: структура рынка смешанная"

        if (
            momentum_state in {"BREAKOUT_UP", "BREAKOUT_DOWN"}
            and confidence < FLIP_BREAKOUT_CONFIDENCE_THRESHOLD
        ):
            return (
                "flip заблокирован во время breakout impulse "
                f"({confidence:.2f} < {FLIP_BREAKOUT_CONFIDENCE_THRESHOLD:.2f})"
            )

        if unrealized_pnl < 0 and confidence < self._loss_flip_confidence:
            return (
                "позиция сейчас в минусе, а flip-сигнал недостаточно сильный "
                f"({confidence:.2f} < {self._loss_flip_confidence:.2f})"
            )

        return None

    # Проверить, активен ли cooldown после последнего flip.
    def _flip_cooldown_active(self, state: AutoTradeState) -> bool:
        ts = safe_float(getattr(state, "last_flip_monotonic_at", None))

        if ts is None:
            return False

        return (time.monotonic() - ts) < self._flip_cooldown_seconds

    # Определить сторону новой позиции по сигналу BUY / SELL.
    def _target_side_from_signal(self, signal: str | None) -> str | None:
        normalized_signal = str(signal or "").upper()

        if normalized_signal == SIGNAL_BUY:
            return POSITION_SIDE_LONG

        if normalized_signal == SIGNAL_SELL:
            return POSITION_SIDE_SHORT

        return None

    # ---------- Execution ----------
    # Закрыть текущую позицию и открыть новую в противоположную сторону.
    def _flip_position(self, state: AutoTradeState) -> ExecutionDecision:
        position = type(self)._position

        if position.side == POSITION_SIDE_NONE:
            self._sync_state_from_position(state)
            reason = "Нет позиции для flip."
            return self._reject_flip(
                state=state,
                reason=reason,
                diagnostic_stage="FLIP_REJECTED_NO_POSITION",
            )

        new_side = self._target_side_from_signal(state.last_signal)

        if new_side is None:
            reason = "Нет направления для flip."
            return self._reject_flip(
                state=state,
                reason=reason,
                diagnostic_stage="FLIP_REJECTED_NO_DIRECTION",
            )

        try:
            # Для flip нужны две цены:
            # 1) exit price — закрытие старой позиции;
            # 2) entry price — открытие новой позиции.
            exit_execution = self._exit_price_for_side(
                position.symbol or state.symbol,
                position.side,
            )
            entry_execution = self._entry_price_for_side(
                state.symbol,
                new_side,
            )
            exit_price = exit_execution.price
            new_entry_price = entry_execution.price

        except Exception as exc:
            reason = f"Ошибка получения цены для flip: {exc}"
            return self._reject_flip(
                state=state,
                reason=reason,
                diagnostic_stage="FLIP_REJECTED_BY_PRICE",
            )

        now = self._now_time()
        opened_monotonic_at = time.monotonic()
        metrics = build_position_metrics(
            position,
            current_price=exit_price,
        )

        # net_pnl_usd может быть None при неполных метриках,
        # поэтому нормализуем в 0.0, чтобы статистика цикла не падала.
        pnl = safe_float(metrics.net_pnl_usd) or 0.0

        new_size = self._calculate_position_size(
            state,
            entry_price=new_entry_price,
        )

        if new_size <= 0:
            reason = "Flip отменён: невозможно рассчитать adaptive size."
            return self._reject_flip(
                state=state,
                reason=reason,
                diagnostic_stage="FLIP_REJECTED_BY_SIZING",
            )

        new_size = self._adjust_size_by_margin_limit(
            state=state,
            entry_price=new_entry_price,
            size=new_size,
        )

        self._sync_effective_risk_after_margin_limit(
            state,
            base_size=state.adaptive_size_base or 0.0,
            final_size=new_size,
        )

        new_size = self._round_size(new_size)

        if new_size <= 0:
            reason = "Flip отменён: итоговый size равен 0."
            return self._reject_flip(
                state=state,
                reason=reason,
                diagnostic_stage="FLIP_REJECTED_BY_SIZING",
            )

        state.realized_pnl_usd += pnl
        state.cycle_realized_pnl_usd += pnl
        state.cycle_closed_trades += 1
        state.cycle_trade_fees_usd += abs(safe_float(metrics.commission_usd) or 0.0)
        state.cycle_overnight_fees_usd += safe_float(metrics.overnight_cashflow_usd) or 0.0

        if pnl > 0:
            state.cycle_winning_trades += 1

            # Прибыльный flip закрывает серию убытков и выключает loss cooldown.
            state.cycle_consecutive_losses = 0
            state.loss_cooldown_active = False
            state.loss_cooldown_reason = None

        elif pnl < 0:
            state.cycle_losing_trades += 1
            state.cycle_consecutive_losses += 1
            state.last_loss_monotonic_at = time.monotonic()

            if state.cycle_consecutive_losses >= EXECUTION_MAX_CONSECUTIVE_LOSSES:
                state.loss_cooldown_active = True
                state.loss_cooldown_reason = (
                    f"{state.cycle_consecutive_losses} подряд убыточных сделок"
                )

        old_side = position.side
        old_entry_price = position.entry_price
        old_size = position.size
        old_leverage = position.leverage
        old_opened_at = position.opened_at
        flip_action = build_flip_action(old_side, new_side)

        self._reset_runtime_protection_state(state)
        self._reset_position_lifecycle_state(state)

        # Flip открывает новую позицию, поэтому autonomous runtime прошлой позиции
        # нельзя переносить на новую сделку.
        reset_autonomous_runtime_state(state)

        state.last_flip_old_side = old_side
        state.last_flip_new_side = new_side
        state.last_flip_pnl_usd = pnl
        state.last_flip_reason = state.last_signal_reason
        state.last_flip_monotonic_at = time.monotonic()

        old_trade_id = position.trade_id or state.current_trade_id
        old_trade_sequence = position.trade_sequence or state.trade_sequence
        old_trade_cycle_number = (
            position.trade_cycle_number
            or state.current_trade_cycle_number
            or state.cycle_number
        )

        new_trade_id = self._create_trade_id(state, new_side)

        state.current_trade_id = new_trade_id
        state.current_trade_cycle_number = state.cycle_number

        type(self)._position = PositionState(
            trade_id=new_trade_id,
            trade_cycle_number=state.current_trade_cycle_number,
            trade_sequence=state.trade_sequence,
            side=new_side,
            symbol=state.symbol,
            entry_price=new_entry_price,
            size=new_size,
            leverage=state.leverage,
            unrealized_pnl_usd=0.0,
            opened_at=now,
            opened_monotonic_at=opened_monotonic_at,
            updated_at=now,
        )

        self._sync_state_from_position(state)

        state.position_opened_monotonic_at = opened_monotonic_at

        reset_execution_block_state(state)
        state.last_flip_block_reason = None
        state.last_execution_action = flip_action
        state.last_execution_reason = "Направление позиции изменено."
        state.last_flip_at = now

        type(self)._last_flip_block_key = None

        payload = self._build_flip_executed_payload(
            state=state,
            old_trade_id=old_trade_id,
            old_trade_sequence=old_trade_sequence,
            old_trade_cycle_number=old_trade_cycle_number,
            new_trade_id=new_trade_id,
            old_side=old_side,
            new_side=new_side,
            old_entry_price=old_entry_price,
            exit_price=exit_price,
            new_entry_price=new_entry_price,
            old_size=old_size,
            new_size=new_size,
            old_leverage=old_leverage,
            pnl=pnl,
            metrics=metrics,
            flip_action=flip_action,
            now=now,
            opened_monotonic_at=opened_monotonic_at,
            old_opened_at=old_opened_at,
            exit_execution=exit_execution,
            entry_execution=entry_execution,
        )

        # Отдельная диагностика успешного прохождения всего flip-пайплайна.
        self._log_flip_diagnostic(
            state=state,
            stage="FLIP_EXECUTED",
            reason=f"{old_side} -> {new_side}",
        )

        JournalService().log_ui_info(
            event_type="position_flipped",
            message=f"Направление позиции изменено: {old_side} → {new_side}.",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

        EventBus.emit("paper_position_flipped", payload)

        return ExecutionDecision(
            flip_action,
            True,
            f"Направление позиции изменено: {old_side} → {new_side}.",
        )