From 6b9bddeb0a5abf79cb845f2b85ae93bc3c731864 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 23:30:10 +0300 Subject: [PATCH] Close stale paper positions outside trend universe --- crypto_spot_bot/bot.py | 28 +++++++++++++++++++++++- tests/test_execution.py | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/crypto_spot_bot/bot.py b/crypto_spot_bot/bot.py index 574acf5..a056225 100644 --- a/crypto_spot_bot/bot.py +++ b/crypto_spot_bot/bot.py @@ -7,7 +7,7 @@ from crypto_spot_bot.config import Settings from crypto_spot_bot.execution import LiveBroker, PaperBroker from crypto_spot_bot.learning import TradeLearner from crypto_spot_bot.market_data import MarketData -from crypto_spot_bot.models import BotStatus, Signal, utc_now +from crypto_spot_bot.models import BotStatus, Signal, Ticker, utc_now from crypto_spot_bot.patterns import PatternAnalyzer from crypto_spot_bot.strategy import SpotStrategy from crypto_spot_bot.storage import Storage @@ -50,6 +50,7 @@ class CryptoSpotBot: self.market.reset_stop() if not self.market.symbols: await self.market.bootstrap() + self._close_paper_positions_outside_symbol_universe() self._update_patterns() self._update_forecasts() self.learner.refresh() @@ -198,6 +199,31 @@ class CryptoSpotBot: enriched["over_target_exposure"] = current_exposure > target_total + self.settings.min_position_usdt return enriched + def _close_paper_positions_outside_symbol_universe(self) -> None: + if self.settings.strategy_mode != "trend_macd" or self.settings.trading_mode != "paper": + return + allowed_symbols = set(self.market.symbols or self.settings.symbols) + for position in list(self.broker.open_positions()): + if position.symbol in allowed_symbols: + continue + synthetic_ticker = Ticker( + symbol=position.symbol, + last_price=position.entry_price, + bid=position.entry_price, + ask=position.entry_price, + turnover_24h=0.0, + volume_24h=0.0, + change_24h=0.0, + ) + self.broker.sell( + position, + synthetic_ticker, + "trend_macd: закрыта старая paper-позиция вне списка разрешенных пар", + ) + self.storage.event( + f"{position.symbol}: старая paper-позиция закрыта при переходе на trend_macd" + ) + def _reduction_candidate_id(self, prices: dict[str, float]) -> int | None: rules = self._with_exposure_context(self.learner.state.adaptive_rules or {}) if not rules.get("reduce_exposure") or not rules.get("over_target_exposure"): diff --git a/tests/test_execution.py b/tests/test_execution.py index c52c00c..61cd8b7 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -1,9 +1,13 @@ from __future__ import annotations +from types import SimpleNamespace + from crypto_spot_bot.bybit import Instrument +from crypto_spot_bot.bot import CryptoSpotBot from crypto_spot_bot.execution import PaperBroker from crypto_spot_bot.models import Signal, Ticker from crypto_spot_bot.storage import Storage +from crypto_spot_bot.strategy import SpotStrategy def test_paper_broker_buy_and_sell_records_trade(make_settings, tmp_path) -> None: @@ -154,3 +158,47 @@ def test_trend_macd_broker_blocks_dca_for_same_symbol(make_settings, tmp_path) - assert first is not None assert second is None assert len(broker.open_positions()) == 1 + + +def test_trend_macd_closes_old_paper_positions_outside_symbol_universe(make_settings, tmp_path) -> None: + settings = make_settings( + tmp_path, + strategy_mode="trend_macd", + trading_mode="paper", + symbols=("BTCUSDT", "ETHUSDT", "SOLUSDT"), + min_position_usdt=1, + max_position_usdt=20, + max_symbol_exposure_usdt=20, + max_total_exposure_usdt=80, + max_open_positions=3, + max_entries_per_minute=0, + ) + storage = Storage(settings.database_path) + broker = PaperBroker(settings, storage) + ticker = Ticker("HYPEUSDT", 10, 9.99, 10.01, 10_000_000, 100, 0) + instrument = Instrument("HYPEUSDT", "HYPE", "USDT", "Trading", 0.001, 0.001, 0.001, 1) + position = broker.buy( + Signal("HYPEUSDT", "BUY", 0.8, "old", {"position_notional_usdt": 5}), + ticker, + instrument, + {"HYPEUSDT": 10}, + ) + assert position is not None + + bot = CryptoSpotBot( + settings, + storage, + SimpleNamespace(symbols=["BTCUSDT", "ETHUSDT", "SOLUSDT"]), + broker, + SpotStrategy(settings), + object(), + object(), + ) + bot._close_paper_positions_outside_symbol_universe() + + assert broker.open_positions() == [] + assert storage.open_positions() == [] + trade = storage.recent_trades(limit=1)[0] + assert trade["side"] == "SELL" + assert trade["symbol"] == "HYPEUSDT" + assert "trend_macd" in trade["reason"]