diff --git a/crypto_spot_bot/strategy.py b/crypto_spot_bot/strategy.py index 2f807bf..1d6c601 100644 --- a/crypto_spot_bot/strategy.py +++ b/crypto_spot_bot/strategy.py @@ -909,6 +909,15 @@ def _torch_forecast_exit_signal( if price >= position.take_profit: return Signal(position.symbol, "SELL", 0.96, "torch_forecast: take-profit hit", diagnostics) if atr_trailing_stop is not None and price <= atr_trailing_stop: + if estimated_exit_net_percent < 0: + diagnostics["atr_exit_blocked_by_cost"] = True + return Signal( + position.symbol, + "HOLD", + 0.45, + "torch_forecast: ATR trailing touched, but exit is not worth fees", + diagnostics, + ) return Signal(position.symbol, "SELL", 0.94, "torch_forecast: ATR trailing stop hit", diagnostics) if not _is_torch_forecast(forecast): if rebound_fallback_position: diff --git a/tests/test_strategy.py b/tests/test_strategy.py index 798fd37..8df6376 100644 --- a/tests/test_strategy.py +++ b/tests/test_strategy.py @@ -917,6 +917,43 @@ def test_torch_forecast_holds_fee_churn_exit_after_min_hold(make_settings, tmp_p 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_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)