from __future__ import annotations

from datetime import timedelta

from crypto_spot_bot.models import Candle, Position, Ticker, utc_now
from crypto_spot_bot.patterns import PatternAnalyzer
from crypto_spot_bot.strategy import SpotStrategy


def _ready_candles() -> list[Candle]:
    candles = []
    for index in range(205):
        candle = Candle(
            timestamp=index,
            open=100,
            high=103,
            low=99,
            close=101,
            volume=100,
            ema_20=100,
            ema_50=99,
            ema_200=98,
            rsi_14=45,
            atr_14=1.2,
            volume_ma_20=90,
        )
        candles.append(candle)
    return candles


def _rebound_candles() -> list[Candle]:
    candles = []
    tail = [98.0, 97.3, 96.6, 95.9, 95.2, 94.8, 94.55, 94.42, 94.38, 94.40, 94.45, 94.56]
    for index in range(205):
        close = 104 - index * 0.025
        if index >= 193:
            close = tail[index - 193]
        rsi = 45
        if index >= 193:
            rsi = min(42, 29 + max(0, index - 198))
        candles.append(
            Candle(
                timestamp=index,
                open=close - 0.12,
                high=close + 0.25,
                low=close - 0.26,
                close=close,
                volume=120,
                ema_20=close + 0.35,
                ema_50=close + 0.75,
                ema_200=close + 1.4,
                rsi_14=rsi,
                atr_14=0.45,
                volume_ma_20=100,
            )
        )
    candles[-1].open = candles[-1].close - 0.20
    candles[-1].low = candles[-1].close - 0.26
    return candles


def _trend_entry_candles(
    *,
    macd_cross_up: bool = True,
    close: float = 105.0,
    ema50: float = 100.0,
    rsi: float = 55.0,
) -> list[Candle]:
    candles = []
    for index in range(80):
        candles.append(
            Candle(
                timestamp=index,
                open=close - 0.4,
                high=close + 0.8,
                low=close - 0.8,
                close=close,
                volume=100,
                ema_20=close - 1.0,
                ema_50=ema50,
                ema_200=ema50 - 4.0,
                rsi_14=rsi,
                atr_14=1.0,
                macd=-0.1,
                macd_signal=0.0,
                macd_hist=-0.1,
                volume_ma_20=90,
            )
        )
    candles[-1].macd = 0.15 if macd_cross_up else -0.05
    candles[-1].macd_signal = 0.0
    candles[-1].macd_hist = candles[-1].macd - candles[-1].macd_signal
    return candles


def _daily_trend_candles(*, uptrend: bool = True) -> list[Candle]:
    close = 110.0 if uptrend else 95.0
    ema50 = 105.0 if uptrend else 98.0
    ema200 = 100.0
    return [
        Candle(
            timestamp=index,
            open=close - 1.0,
            high=close + 1.0,
            low=close - 2.0,
            close=close,
            volume=1000,
            ema_20=ema50,
            ema_50=ema50,
            ema_200=ema200,
            rsi_14=55,
            atr_14=4.0,
            macd=1.0,
            macd_signal=0.8,
            macd_hist=0.2,
            volume_ma_20=900,
        )
        for index in range(205)
    ]


def test_trend_macd_buys_only_when_rules_pass(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="trend_macd",
        max_position_usdt=50,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "BTCUSDT",
        _trend_entry_candles(),
        ticker,
        open_positions_for_symbol=0,
        account={"equity": 100.0},
        trend_candles=_daily_trend_candles(),
    )

    assert signal.action == "BUY"
    assert signal.diagnostics["strategy_mode"] == "trend_macd"
    assert signal.diagnostics["position_notional_usdt"] == 25.0
    assert signal.diagnostics["position_sizing"]["risk_per_trade_percent"] == 1.0


def test_trend_macd_blocks_when_daily_trend_filter_fails(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="trend_macd")
    strategy = SpotStrategy(settings)
    ticker = Ticker("ETHUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "ETHUSDT",
        _trend_entry_candles(),
        ticker,
        open_positions_for_symbol=0,
        account={"equity": 100.0},
        trend_candles=_daily_trend_candles(uptrend=False),
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["checks"]["daily_trend_ok"] is False


def test_trend_macd_blocks_second_position_for_symbol(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="trend_macd")
    strategy = SpotStrategy(settings)
    ticker = Ticker("SOLUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "SOLUSDT",
        _trend_entry_candles(),
        ticker,
        open_positions_for_symbol=1,
        account={"equity": 100.0},
        trend_candles=_daily_trend_candles(),
    )

    assert signal.action == "HOLD"
    assert "позиция" in signal.reason


def test_trend_macd_exits_on_macd_cross_down(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="trend_macd")
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles()
    candles[-2].macd = 0.2
    candles[-2].macd_signal = 0.0
    candles[-1].macd = -0.1
    candles[-1].macd_signal = 0.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100)
    ticker = Ticker("BTCUSDT", 104, 103.99, 104.01, 1_000_000, 100, 0)

    signal = strategy.exit_signal(position, candles, ticker)

    assert signal.action == "SELL"
    assert "MACD" in signal.reason


def test_trend_macd_exits_on_close_below_ema50(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="trend_macd")
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=99.0, ema50=100.0, macd_cross_up=False)
    candles[-2].macd = 0.1
    candles[-2].macd_signal = 0.0
    candles[-1].macd = 0.05
    candles[-1].macd_signal = 0.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100)
    ticker = Ticker("BTCUSDT", 99, 98.99, 99.01, 1_000_000, 100, 0)

    signal = strategy.exit_signal(position, candles, ticker)

    assert signal.action == "SELL"
    assert "EMA50" in signal.reason


def test_trend_macd_exits_on_atr_trailing_stop(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="trend_macd", atr_trailing_multiplier=2.2)
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=108.0, ema50=100.0, macd_cross_up=False)
    candles[-2].macd = 0.1
    candles[-2].macd_signal = 0.0
    candles[-1].macd = 0.05
    candles[-1].macd_signal = 0.0
    candles[-1].atr_14 = 1.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 110)
    ticker = Ticker("BTCUSDT", 107.7, 107.69, 107.71, 1_000_000, 100, 0)

    signal = strategy.exit_signal(position, candles, ticker)

    assert signal.action == "SELL"
    assert "ATR trailing" in signal.reason


def test_trend_macd_holds_below_stop_when_stop_loss_exit_disabled(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="trend_macd",
        stop_loss_exit_enabled=False,
        atr_trailing_multiplier=2.2,
    )
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=99.0, ema50=95.0, macd_cross_up=False)
    candles[-2].macd = 0.1
    candles[-2].macd_signal = 0.0
    candles[-1].macd = 0.1
    candles[-1].macd_signal = 0.0
    candles[-1].atr_14 = 1.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100.5)
    ticker = Ticker("BTCUSDT", 95.5, 95.49, 95.51, 1_000_000, 100, 0)

    signal = strategy.exit_signal(position, candles, ticker)

    assert signal.action == "HOLD"
    assert signal.diagnostics["stop_loss"] is None
    assert signal.diagnostics["stop_loss_exit_enabled"] is False


def test_torch_atr_trailing_without_stop_loss_waits_for_profit_stop(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        stop_loss_exit_enabled=False,
        atr_trailing_multiplier=2.2,
    )
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=99.0, ema50=95.0, macd_cross_up=False)
    candles[-1].atr_14 = 1.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 101.0)
    ticker = Ticker("BTCUSDT", 98.7, 98.69, 98.71, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        candles,
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": 0.4,
            "probability_up": 0.62,
            "skill": 0.18,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["atr_trailing_stop"] is None


def test_torch_atr_trailing_without_stop_loss_can_lock_profit(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        stop_loss_exit_enabled=False,
        atr_trailing_multiplier=2.2,
    )
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=104.0, ema50=95.0, macd_cross_up=False)
    candles[-1].atr_14 = 1.0
    position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 104.0)
    ticker = Ticker("BTCUSDT", 101.7, 101.69, 101.71, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        candles,
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": 0.4,
            "probability_up": 0.62,
            "skill": 0.18,
        },
    )

    assert signal.action == "SELL"
    assert "ATR trailing" in signal.reason


def test_torch_forecast_buys_only_from_positive_torch_edge(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        max_position_usdt=25,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=0,
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": 0.36,
            "probability_up": 0.66,
            "skill": 0.22,
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "BUY"
    assert signal.diagnostics["strategy_mode"] == "torch_forecast"
    assert signal.diagnostics["checks"]["torch_model_ok"] is True
    assert signal.diagnostics["position_sizing"]["method"] == "torch_forecast_fractional_kelly"
    assert settings.min_position_usdt <= signal.diagnostics["position_notional_usdt"] <= settings.max_position_usdt


def test_torch_forecast_blocks_without_valid_torch_model(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast")
    strategy = SpotStrategy(settings)
    ticker = Ticker("ETHUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "ETHUSDT",
        _trend_entry_candles(),
        ticker,
        open_positions_for_symbol=0,
        forecast={"usable": True, "model": "none", "expected_return_percent": 0.5, "probability_up": 0.7},
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["checks"]["torch_model_ok"] is False


def test_torch_forecast_allows_additional_entries_until_symbol_limit(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        min_position_usdt=1,
        max_symbol_exposure_usdt=3,
        max_positions_per_symbol=3,
        max_position_usdt=25,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
    forecast = {
        "usable": True,
        "model": "torch_gru",
        "expected_return_percent": 0.36,
        "probability_up": 0.66,
        "skill": 0.22,
        "block_entry": False,
    }

    additional = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=1,
        forecast=forecast,
        account={"equity": 100.0},
    )
    capped = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=3,
        forecast=forecast,
        account={"equity": 100.0},
    )

    assert additional.action == "BUY"
    assert capped.action == "HOLD"
    assert "symbol position limit" in capped.reason


def test_torch_forecast_kelly_buys_only_remaining_symbol_allocation(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        min_position_usdt=1,
        max_position_usdt=8,
        max_symbol_exposure_usdt=25,
        max_positions_per_symbol=6,
        stop_loss_percent=0.04,
        take_profit_percent=0.035,
        kelly_sizing_enabled=True,
        kelly_fraction=0.25,
        kelly_max_fraction=0.20,
        time_series_min_edge_percent=0.10,
        time_series_min_probability_up=0.47,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
    forecast = {
        "usable": True,
        "model": "torch_gru",
        "expected_return_percent": 0.60,
        "probability_up": 0.84,
        "skill": 0.22,
        "block_entry": False,
    }

    first = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=0,
        forecast=forecast,
        account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 0.0},
    )
    second = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=1,
        forecast=forecast,
        account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 8.0},
    )
    filled = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=2,
        forecast=forecast,
        account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 20.0},
    )

    first_sizing = first.diagnostics["position_sizing"]
    second_sizing = second.diagnostics["position_sizing"]
    assert first.action == "BUY"
    assert first_sizing["method"] == "torch_forecast_fractional_kelly"
    assert first_sizing["kelly_target_notional_usdt"] > settings.max_position_usdt
    assert first.diagnostics["position_notional_usdt"] == settings.max_position_usdt
    assert second.action == "BUY"
    assert 1 <= second.diagnostics["position_notional_usdt"] < settings.max_position_usdt
    assert second_sizing["kelly_open_symbol_exposure_usdt"] == 8.0
    assert filled.action == "HOLD"
    assert filled.diagnostics["checks"]["risk_size_ok"] is False


def test_torch_forecast_kelly_allows_next_exchange_minimum_layer(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        min_position_usdt=1,
        max_position_usdt=8,
        max_symbol_exposure_usdt=25,
        max_total_exposure_usdt=75,
        max_positions_per_symbol=6,
        stop_loss_percent=0.04,
        take_profit_percent=0.035,
        kelly_sizing_enabled=True,
        kelly_fraction=0.25,
        kelly_max_fraction=0.20,
        time_series_min_edge_percent=0.10,
        time_series_min_probability_up=0.47,
        time_series_min_confidence=0.4,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("HYPEUSDT", 63.14, 63.13, 63.15, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "HYPEUSDT",
        [],
        ticker,
        open_positions_for_symbol=1,
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": 0.2115,
            "probability_up": 0.5163,
            "skill": 0.0156,
            "block_entry": False,
        },
        account={
            "equity": 98.6,
            "cash": 88.54,
            "exposure": 10.07,
            "symbol": "HYPEUSDT",
            "symbol_exposure_usdt": 5.05,
            "open_positions_for_symbol": 1,
            "exchange_min_entry_usdt": 5.07,
        },
    )

    sizing = signal.diagnostics["position_sizing"]
    assert signal.action == "BUY"
    assert signal.diagnostics["checks"]["risk_size_ok"] is True
    assert sizing["kelly_target_notional_usdt"] < sizing["kelly_open_symbol_exposure_usdt"]
    assert sizing["kelly_raw_remaining_notional_usdt"] == 0.0
    assert sizing["kelly_layer_mode"] is True
    assert signal.diagnostics["position_notional_usdt"] == 5.07


def test_torch_forecast_blocks_failed_quality_gate(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        time_series_min_edge_percent=0.10,
        time_series_min_probability_up=0.57,
        max_position_usdt=25,
        stop_loss_percent=0.04,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=0,
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": 0.36,
            "probability_up": 0.66,
            "skill": 0.22,
            "block_entry": False,
            "quality_gate_passed": False,
            "quality_gate": {"status": "fail"},
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["checks"]["quality_gate_ok"] is False


def test_torch_forecast_probe_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        time_series_min_edge_percent=0.10,
        time_series_min_probability_up=0.52,
        time_series_probe_enabled=True,
        time_series_probe_min_edge_percent=0.02,
        time_series_probe_min_probability_up=0.55,
        time_series_probe_size_multiplier=0.40,
        max_position_usdt=25,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("SOLUSDT", 65, 64.99, 65.01, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "SOLUSDT",
        [],
        ticker,
        open_positions_for_symbol=0,
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": 0.04,
            "probability_up": 0.57,
            "skill": 0.05,
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["edge_mode"] == "probe"
    assert signal.diagnostics["checks"]["expected_edge_ok"] is True
    assert signal.diagnostics["checks"]["risk_size_ok"] is False
    assert signal.diagnostics["position_notional_usdt"] == 0.0


def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        time_series_min_edge_percent=0.10,
        time_series_min_probability_up=0.52,
        time_series_probe_enabled=True,
        time_series_probe_min_edge_percent=0.02,
        time_series_probe_min_probability_up=0.55,
    )
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 59_000, 58_999, 59_001, 10_000_000, 1000, 1.0)

    signal = strategy.entry_signal(
        "BTCUSDT",
        [],
        ticker,
        open_positions_for_symbol=0,
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": -0.03,
            "probability_up": 0.60,
            "skill": 0.16,
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["edge_mode"] == "blocked"
    assert signal.diagnostics["checks"]["expected_edge_ok"] is False


def test_torch_forecast_rebound_overlay_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        rebound_trading_enabled=True,
        rebound_entry_confidence=0.55,
        rebound_min_probability=0.55,
        rebound_max_position_usdt=6.0,
        time_series_min_edge_percent=0.10,
        time_series_probe_min_probability_up=0.55,
        max_position_usdt=25,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "РЅРёСЃС…РѕРґСЏС‰РёР№ С‚СЂРµРЅРґ", "score": 0.28},
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": 0.01,
            "probability_up": 0.56,
            "skill": 0.05,
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["rebound"]["active"] is True
    assert signal.diagnostics["model_rebound_entry_ok"] is True
    assert signal.diagnostics["rebound_entry_sized_ok"] is False
    assert signal.diagnostics["checks"]["expected_edge_ok"] is False
    assert signal.diagnostics["checks"]["risk_size_ok"] is False


def test_torch_forecast_rebound_overlay_does_not_buy_negative_forecast(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        rebound_trading_enabled=True,
        rebound_entry_confidence=0.55,
        rebound_min_probability=0.55,
        time_series_min_edge_percent=0.10,
        time_series_probe_min_probability_up=0.55,
    )
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "РЅРёСЃС…РѕРґСЏС‰РёР№ С‚СЂРµРЅРґ", "score": 0.28},
        forecast={
            "usable": True,
            "model": "torch_gru",
            "expected_return_percent": -0.01,
            "probability_up": 0.56,
            "skill": 0.05,
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["rebound"]["active"] is True
    assert signal.diagnostics["rebound_entry_ok"] is False
    assert signal.diagnostics["edge_mode"] == "blocked"


def test_torch_forecast_rebound_fallback_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        rebound_trading_enabled=True,
        rebound_entry_confidence=0.55,
        rebound_min_probability=0.55,
        rebound_max_position_usdt=6.0,
        time_series_rebound_fallback_enabled=True,
        stop_loss_percent=0.04,
        risk_per_trade_percent=0.01,
    )
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="HYPEUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )

    signal = strategy.entry_signal(
        "HYPEUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "РЅРёСЃС…РѕРґСЏС‰РёР№ С‚СЂРµРЅРґ", "score": 0.28},
        forecast={
            "usable": False,
            "model": "none",
            "reason": "no valid PyTorch LSTM/GRU model for symbol",
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["fallback_rebound_entry_ok"] is True
    assert signal.diagnostics["rebound_entry_sized_ok"] is False
    assert signal.diagnostics["missing_torch_model"] is True
    assert signal.diagnostics["checks"]["risk_size_ok"] is False


def test_torch_forecast_rebound_fallback_can_be_disabled(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        strategy_mode="torch_forecast",
        rebound_trading_enabled=True,
        rebound_entry_confidence=0.55,
        rebound_min_probability=0.55,
        time_series_rebound_fallback_enabled=False,
    )
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="HYPEUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )

    signal = strategy.entry_signal(
        "HYPEUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "РЅРёСЃС…РѕРґСЏС‰РёР№ С‚СЂРµРЅРґ", "score": 0.28},
        forecast={
            "usable": False,
            "model": "none",
            "reason": "no valid PyTorch LSTM/GRU model for symbol",
            "block_entry": False,
        },
        account={"equity": 100.0},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["missing_torch_model"] is True
    assert signal.diagnostics["fallback_rebound_entry_ok"] is False


def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", stop_loss_percent=0.04)
    strategy = SpotStrategy(settings)
    position = Position(
        1,
        "SOLUSDT",
        1,
        100,
        100,
        0.1,
        96,
        120,
        103,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("SOLUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(),
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": -0.08,
            "probability_up": 0.43,
            "skill": 0.18,
            "block_entry": True,
        },
    )

    assert signal.action == "SELL"
    assert "прогноз" in signal.reason


def test_torch_forecast_holds_negative_forecast_during_min_hold(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
    strategy = SpotStrategy(settings)
    position = Position(1, "SOLUSDT", 1, 100, 100, 0.1, 96, 120, 103)
    ticker = Ticker("SOLUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(),
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": -0.08,
            "probability_up": 0.43,
            "skill": 0.18,
            "block_entry": True,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["forecast_exit_blocked_by_min_hold"] is True


def test_torch_forecast_holds_fee_churn_exit_after_min_hold(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60)
    strategy = SpotStrategy(settings)
    position = Position(
        1,
        "BTCUSDT",
        1,
        100,
        100,
        0.1,
        96,
        120,
        100.1,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 99.99, 99.98, 100.0, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(),
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": -0.01,
            "probability_up": 0.499,
            "skill": 0.18,
            "block_entry": False,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["forecast_exit_blocked_by_cost"] is True


def test_torch_forecast_holds_atr_trailing_exit_that_does_not_cover_fees(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60)
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=100.2)
    candles[-1].atr_14 = 0.8
    position = Position(
        1,
        "MNTUSDT",
        1,
        100,
        100,
        0.1,
        96,
        120,
        102,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("MNTUSDT", 100.2, 100.19, 100.21, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        candles,
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": 0.4,
            "probability_up": 0.58,
            "skill": 0.18,
            "block_entry": False,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["atr_exit_blocked_by_cost"] is True


def test_torch_forecast_holds_atr_trailing_exit_below_min_profit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60, min_exit_net_percent=0.20)
    strategy = SpotStrategy(settings)
    candles = _trend_entry_candles(close=100.35)
    candles[-1].atr_14 = 0.6
    position = Position(
        1,
        "MNTUSDT",
        1,
        100,
        100,
        0.1,
        96,
        120,
        102,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("MNTUSDT", 100.35, 100.34, 100.36, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        candles,
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": 0.4,
            "probability_up": 0.58,
            "skill": 0.18,
            "block_entry": False,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["atr_exit_blocked_by_min_profit"] is True
    assert signal.diagnostics["estimated_exit_net_percent"] < settings.min_exit_net_percent


def test_torch_forecast_holds_negative_forecast_exit_below_min_profit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60, min_exit_net_percent=0.20)
    strategy = SpotStrategy(settings)
    position = Position(
        1,
        "BTCUSDT",
        1,
        100,
        100,
        0.1,
        96,
        120,
        100.5,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 100.35, 100.34, 100.36, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(close=100.35),
        ticker,
        forecast={
            "usable": True,
            "model": "torch_lstm",
            "expected_return_percent": -0.2,
            "probability_up": 0.40,
            "skill": 0.18,
            "block_entry": False,
            "reason": "model turned down",
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["forecast_exit_blocked_by_min_profit"] is True
    assert signal.diagnostics["estimated_exit_net_percent"] < settings.min_exit_net_percent


def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
    strategy = SpotStrategy(settings)
    position = Position(
        1,
        "XRPUSDT",
        5,
        1.0,
        5.0,
        0.005,
        0.96,
        1.035,
        1.0,
        entry_diagnostics={"entry_path": "rebound_fallback", "edge_mode": "rebound_fallback"},
    )
    ticker = Ticker("XRPUSDT", 1.001, 1.0009, 1.0011, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(close=1.0, ema50=0.98),
        ticker,
        forecast={
            "usable": False,
            "model": "none",
            "reason": "no valid PyTorch LSTM/GRU model for symbol",
            "block_entry": False,
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["rebound_fallback_position"] is True
    assert "rebound fallback" in signal.reason


def test_torch_forecast_rebound_fallback_still_sells_take_profit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, strategy_mode="torch_forecast")
    strategy = SpotStrategy(settings)
    position = Position(
        1,
        "XRPUSDT",
        5,
        1.0,
        5.0,
        0.005,
        0.96,
        1.035,
        1.04,
        opened_at=utc_now() - timedelta(seconds=600),
        entry_diagnostics={"entry_path": "rebound_fallback", "edge_mode": "rebound_fallback"},
    )
    ticker = Ticker("XRPUSDT", 1.036, 1.0359, 1.0361, 10_000_000, 1000, 1.0)

    signal = strategy.exit_signal(
        position,
        _trend_entry_candles(close=1.0, ema50=0.98),
        ticker,
        forecast={
            "usable": False,
            "model": "none",
            "reason": "no valid PyTorch LSTM/GRU model for symbol",
            "block_entry": False,
        },
    )

    assert signal.action == "SELL"
    assert "take-profit" in signal.reason


def test_strategy_emits_buy_when_score_passes_threshold(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )

    signal = strategy.entry_signal("BTCUSDT", _ready_candles(), ticker, open_positions_for_symbol=0)

    assert signal.action == "BUY"
    assert signal.confidence >= settings.min_signal_confidence


def test_strategy_blocks_negative_long_pattern(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "нисходящий тренд", "score": 0.28},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["entry_blocked_by_pattern"] is True


def test_strategy_blocks_strong_negative_learning(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "нейтрально", "score": 0.5},
        learning={
            "sample_size": 10,
            "net_pnl": -1.0,
            "win_rate": 0.1,
            "confidence_adjustment": -0.12,
            "reason": "test",
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["entry_blocked_by_learning"] is True


def test_strategy_blocks_entry_when_llm_advisor_blocks(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, llm_advisor_enabled=True)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=0,
        llm={
            "confidence_adjustment": -0.03,
            "block_entry": True,
            "reason_ru": "риск падения",
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["entry_blocked_by_llm"] is True
    assert signal.diagnostics["llm_adjustment"] == -0.03


def test_strategy_activates_grid_and_sets_position_size(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=0.1,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=2,
        pattern={
            "label": "боковик",
            "score": 0.48,
            "tags": ["боковик"],
            "metrics": {"high20": 105, "low20": 100, "ema_gap_percent": 0.1, "ret_20_percent": 0.2},
        },
        llm={"market_regime": "range", "grid_suitable": True, "risk_level": "medium"},
    )

    assert signal.action == "BUY"
    assert signal.diagnostics["trade_mode"] == "GRID"
    assert signal.diagnostics["grid"]["active"] is True
    assert 1 <= signal.diagnostics["position_notional_usdt"] <= settings.grid_max_position_usdt


def test_strategy_uses_fractional_kelly_position_size(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, max_position_usdt=20, kelly_fraction=0.25, kelly_max_fraction=0.20)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=0,
        forecast={"usable": True, "probability_up": 0.70, "volatility_percent": 0.2},
        account={"equity": 200.0},
    )

    sizing = signal.diagnostics["position_sizing"]
    assert signal.action == "BUY"
    assert sizing["method"] == "fractional_kelly"
    assert sizing["kelly_probability_source"] == "forecast"
    assert sizing["kelly_bankroll_usdt"] == 200.0
    assert sizing["kelly_effective_fraction"] > 0
    assert signal.diagnostics["position_notional_usdt"] == settings.max_position_usdt


def test_strategy_scales_kelly_with_positive_learning_multiplier(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, max_position_usdt=50, kelly_fraction=0.25, kelly_max_fraction=0.20)
    strategy = SpotStrategy(settings)
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=101,
        bid=100.99,
        ask=101.01,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=1.0,
    )
    common = dict(
        symbol="BTCUSDT",
        candles=_ready_candles(),
        ticker=ticker,
        open_positions_for_symbol=0,
        forecast={"usable": True, "probability_up": 0.62, "volatility_percent": 0.2},
        account={"equity": 200.0},
    )

    neutral = strategy.entry_signal(**common)
    scaled = strategy.entry_signal(
        **common,
        learning={"adaptive_rules": {"effective_position_size_multiplier": 1.5}},
    )

    assert neutral.action == "BUY"
    assert scaled.action == "BUY"
    assert scaled.diagnostics["position_sizing"]["risk_multiplier"] > neutral.diagnostics["position_sizing"]["risk_multiplier"]
    assert scaled.diagnostics["position_notional_usdt"] > neutral.diagnostics["position_notional_usdt"]
    assert scaled.diagnostics["position_notional_usdt"] <= settings.max_position_usdt


def test_strategy_buys_probabilistic_rebound_after_stabilized_drop(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, rebound_entry_confidence=0.58, rebound_min_probability=0.58)
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )
    pattern = PatternAnalyzer().analyze(candles, ticker).as_dict()

    signal = strategy.entry_signal(
        "BTCUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={**pattern, "label": "нисходящий тренд", "score": 0.28},
    )

    assert signal.action == "BUY"
    assert signal.diagnostics["trade_mode"] == "REBOUND"
    assert signal.diagnostics["rebound"]["active"] is True
    assert signal.diagnostics["entry_blocked_by_pattern"] is False
    assert signal.diagnostics["position_notional_usdt"] <= settings.rebound_max_position_usdt


def test_strategy_rebound_does_not_override_llm_block(make_settings, tmp_path) -> None:
    settings = make_settings(
        tmp_path,
        llm_advisor_enabled=True,
        rebound_entry_confidence=0.58,
        rebound_min_probability=0.58,
    )
    strategy = SpotStrategy(settings)
    candles = _rebound_candles()
    ticker = Ticker(
        symbol="BTCUSDT",
        last_price=candles[-1].close,
        bid=candles[-1].close * 0.9999,
        ask=candles[-1].close * 1.0001,
        turnover_24h=10_000_000,
        volume_24h=1000,
        change_24h=-2.0,
    )

    signal = strategy.entry_signal(
        "BTCUSDT",
        candles,
        ticker,
        open_positions_for_symbol=0,
        pattern={"label": "нисходящий тренд", "score": 0.28},
        llm={"block_entry": True, "reason_ru": "риск продолжения падения"},
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["entry_blocked_by_llm"] is True


def test_strategy_trailing_stop_only_exits_after_profit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    candles = _ready_candles()
    from crypto_spot_bot.models import Position

    position = Position(
        id=1,
        symbol="BTCUSDT",
        qty=1,
        entry_price=100,
        notional_usdt=100,
        entry_fee_usdt=0.1,
        stop_loss=90,
        take_profit=120,
        highest_price=100.5,
    )
    ticker = Ticker("BTCUSDT", 99.6, 99.5, 99.7, 1_000_000, 100, 0)

    signal = strategy.exit_signal(position, candles, ticker)

    assert signal.reason != "сработал trailing stop выше цены входа"


def test_strategy_adaptive_learning_holds_unprofitable_ema_exit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, min_hold_seconds=60)
    strategy = SpotStrategy(settings)
    candles = _ready_candles()
    candles[-2].close = 100.2
    candles[-1].close = 99.9
    candles[-1].ema_20 = 98.0
    candles[-1].ema_50 = 100.0
    position = Position(
        id=1,
        symbol="BTCUSDT",
        qty=1,
        entry_price=100,
        notional_usdt=100,
        entry_fee_usdt=0.1,
        stop_loss=90,
        take_profit=120,
        highest_price=100.5,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 99.9, 99.89, 99.91, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        candles,
        ticker,
        {
            "adaptive_rules": {
                "ema_exit_mode": "profit_only",
                "min_exit_profit_percent": 0.31,
                "min_hold_seconds": 60,
            }
        },
    )

    assert signal.action == "HOLD"
    assert "EMA50" in signal.reason


def test_strategy_blocks_entry_when_learning_exposure_target_exceeded(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path)
    strategy = SpotStrategy(settings)
    ticker = Ticker("BTCUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)
    signal = strategy.entry_signal(
        "BTCUSDT",
        _ready_candles(),
        ticker,
        open_positions_for_symbol=1,
        learning={
            "adaptive_rules": {
                "over_target_exposure": True,
                "target_total_exposure_usdt": 35,
                "current_total_exposure_usdt": 80,
            }
        },
    )

    assert signal.action == "HOLD"
    assert signal.diagnostics["entry_blocked_by_adaptive_rules"] is True
    assert signal.diagnostics["adaptive_block_reason"] == "экспозиция выше цели обучения"


def test_strategy_learning_reduce_now_sells_after_min_hold(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, min_hold_seconds=60)
    strategy = SpotStrategy(settings)
    position = Position(
        id=7,
        symbol="BTCUSDT",
        qty=1,
        entry_price=100,
        notional_usdt=100,
        entry_fee_usdt=0.1,
        stop_loss=90,
        take_profit=120,
        highest_price=101,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 99.5, 99.49, 99.51, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        _ready_candles(),
        ticker,
        {"adaptive_rules": {"reduce_exposure": True, "reduce_now": True, "min_hold_seconds": 60}},
    )

    assert signal.action == "SELL"
    assert "экспозицию" in signal.reason


def test_strategy_forecast_sells_to_lock_profit(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, min_hold_seconds=60)
    strategy = SpotStrategy(settings)
    position = Position(
        id=9,
        symbol="BTCUSDT",
        qty=1,
        entry_price=100,
        notional_usdt=100,
        entry_fee_usdt=0.1,
        stop_loss=90,
        take_profit=120,
        highest_price=101.5,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 101, 100.99, 101.01, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        _ready_candles(),
        ticker,
        forecast={
            "usable": True,
            "skill": 0.2,
            "expected_return_percent": -0.2,
            "probability_up": 0.35,
            "reason": "тестовый негативный прогноз",
        },
    )

    assert signal.action == "SELL"
    assert "прогноз временного ряда" in signal.reason


def test_strategy_forecast_sells_to_limit_loss_before_stop(make_settings, tmp_path) -> None:
    settings = make_settings(tmp_path, min_hold_seconds=60, stop_loss_percent=0.02)
    strategy = SpotStrategy(settings)
    position = Position(
        id=10,
        symbol="BTCUSDT",
        qty=1,
        entry_price=100,
        notional_usdt=100,
        entry_fee_usdt=0.1,
        stop_loss=98,
        take_profit=120,
        highest_price=100.4,
        opened_at=utc_now() - timedelta(seconds=600),
    )
    ticker = Ticker("BTCUSDT", 99.2, 99.19, 99.21, 1_000_000, 100, 0)

    signal = strategy.exit_signal(
        position,
        _ready_candles(),
        ticker,
        forecast={
            "usable": True,
            "skill": 0.2,
            "expected_return_percent": -0.2,
            "probability_up": 0.35,
            "reason": "тестовый негативный прогноз",
        },
    )

    assert signal.action == "SELL"
    assert "ограничиваем убыток" in signal.reason
