diff --git a/src/llm_trade_bot/agent.py b/src/llm_trade_bot/agent.py index f506657..fae93bb 100644 --- a/src/llm_trade_bot/agent.py +++ b/src/llm_trade_bot/agent.py @@ -465,25 +465,16 @@ def _has_existing_idea(state: dict[str, Any], category: str, symbol: str) -> boo for item in state.get("positions", []) ): return True - if category == "spot": - base_coin = symbol.removesuffix("USDT") - wallets = state.get("wallet", []) - if wallets and any( - item.get("coin") == base_coin and Decimal(str(item.get("usdValue") or 0)) > 0 - for item in wallets[0].get("coin", []) - ): - return True - spot_balances = state.get("wallets", {}).get("spot", {}).get("balances", []) - if any( - item.get("asset") == base_coin - and Decimal(str(item.get("free") or 0)) + Decimal(str(item.get("locked") or 0)) > 0 - for item in spot_balances - ): - return True return any( item.get("symbol") == symbol and item.get("category", category) == category - and not bool(item.get("reduceOnly") or item.get("protective")) + and ( + not bool(item.get("reduceOnly") or item.get("protective")) + or ( + category == "spot" + and str(item.get("client_id") or item.get("orderLinkId") or "").startswith("ltb-") + ) + ) for item in state.get("open_orders", []) ) diff --git a/src/llm_trade_bot/binance_gateway.py b/src/llm_trade_bot/binance_gateway.py index 77f59f6..ff3026e 100644 --- a/src/llm_trade_bot/binance_gateway.py +++ b/src/llm_trade_bot/binance_gateway.py @@ -35,7 +35,6 @@ from llm_trade_bot.models import ( ) _INTERVALS = {"60": "1h", "240": "4h", "D": "1d"} -_STABLE_COINS = {"USDT", "USDC", "FDUSD", "BUSD", "DAI", "USDE"} class BinanceGateway: @@ -148,7 +147,7 @@ class BinanceGateway: async def account_snapshot(self, category: str | None = None) -> AccountSnapshot: state = await self.account_state() - active = _active_ideas(state, self.scanner.quote_coin) + active = _active_ideas(state) if category == "linear": wallet = state["wallets"]["linear"] gross = sum( @@ -241,7 +240,8 @@ class BinanceGateway: self._known_symbols[category].add(symbol) ticker, book = await asyncio.gather( client.request("GET", f"{prefix}/ticker/24hr", {"symbol": symbol}), - client.request("GET", f"{prefix}/depth", {"symbol": symbol, "limit": 25}), + # Binance accepts a discrete set of depth limits; 25 is a Bybit-only value. + client.request("GET", f"{prefix}/depth", {"symbol": symbol, "limit": 20}), ) frames: dict[str, Any] = {} for requested in self.scanner.candle_intervals: @@ -994,18 +994,18 @@ def _normalize_futures_event(event: dict[str, Any]) -> dict[str, Any]: return {"provider": "binance", "category": "linear", "kind": "account", "event_type": event.get("e"), "raw": event} -def _active_ideas(state: dict[str, Any], quote_coin: str) -> set[str]: +def _active_ideas(state: dict[str, Any]) -> set[str]: result = { f"linear:{item['symbol']}" for item in state.get("positions", []) if _decimal(item.get("size")) > 0 } - for balance in state.get("wallets", {}).get("spot", {}).get("balances", []): - asset = str(balance.get("asset")) - if asset not in _STABLE_COINS and _decimal(balance.get("free")) + _decimal(balance.get("locked")) > 0: - result.add(f"spot:{asset}{quote_coin}") result.update( f"{item['category']}:{item['symbol']}" for item in state.get("open_orders", []) if not item.get("protective") + or ( + item.get("category") == "spot" + and str(item.get("client_id") or "").startswith("ltb-") + ) ) return result diff --git a/tests/test_agent_tools.py b/tests/test_agent_tools.py index 9791a00..44b0d96 100644 --- a/tests/test_agent_tools.py +++ b/tests/test_agent_tools.py @@ -5,7 +5,7 @@ from zoneinfo import ZoneInfo import pytest -from llm_trade_bot.agent import AgentTools +from llm_trade_bot.agent import AgentTools, _has_existing_idea from llm_trade_bot.config import ( AppConfig, BraveConfig, @@ -102,3 +102,22 @@ async def test_pause_blocks_entry_from_running_session(tmp_path): ) finally: await database.close() + + +def test_seeded_spot_balance_does_not_block_new_agent_entry(): + state = { + "wallets": {"spot": {"balances": [{"asset": "SOL", "free": "4", "locked": "0"}]}}, + "positions": [], + "open_orders": [], + } + assert _has_existing_idea(state, "spot", "SOLUSDT") is False + + state["open_orders"] = [ + { + "category": "spot", + "symbol": "SOLUSDT", + "protective": True, + "client_id": "ltb-12-abcd-tp", + } + ] + assert _has_existing_idea(state, "spot", "SOLUSDT") is True diff --git a/tests/test_binance_gateway.py b/tests/test_binance_gateway.py index 329191a..1864c88 100644 --- a/tests/test_binance_gateway.py +++ b/tests/test_binance_gateway.py @@ -5,7 +5,7 @@ from typing import Any import pytest -from llm_trade_bot.binance_gateway import BinanceError, BinanceGateway +from llm_trade_bot.binance_gateway import BinanceError, BinanceGateway, _active_ideas from llm_trade_bot.config import BinanceAccountConfig, BinanceConfig, ScannerConfig from llm_trade_bot.models import ( Category, @@ -160,3 +160,36 @@ async def test_trailing_rate_outside_binance_range_rejects_before_opening(): ) assert not usdm.requests assert not usdm.mutations + + +async def test_linear_market_context_uses_a_valid_binance_depth_limit(): + class MarketClient(FakeBinanceClient): + async def request(self, method: str, path: str, parameters=None, **kwargs): + self.requests.append((method, path, parameters, kwargs)) + if path.endswith("/ticker/24hr"): + return {"lastPrice": "100"} + if path.endswith("/depth"): + return {"bids": [], "asks": []} + if path.endswith("/klines"): + return [[0, "1", "101", "99", "100", "10"] for _ in range(20)] + return await super().request(method, path, parameters, **kwargs) + + spot = MarketClient("/api/v3/exchangeInfo", []) + usdm = MarketClient("/fapi/v1/exchangeInfo", []) + context = await gateway(spot, usdm).market_context("linear", "BTCUSDT") + depth = next(parameters for _, path, parameters, _ in usdm.requests if path == "/fapi/v1/depth") + assert context["ticker"]["lastPrice"] == "100" + assert depth == {"symbol": "BTCUSDT", "limit": 20} + + +def test_seeded_spot_assets_are_exposure_but_not_active_ideas(): + state = { + "wallets": {"spot": {"balances": [{"asset": "SOL", "free": "4", "locked": "0"}]}}, + "positions": [], + "open_orders": [], + } + assert _active_ideas(state) == set() + state["open_orders"] = [ + {"category": "spot", "symbol": "SOLUSDT", "protective": True, "client_id": "ltb-1-tp"} + ] + assert _active_ideas(state) == {"spot:SOLUSDT"}