Fix Binance linear market context

This commit is contained in:
Azimkin
2026-09-19 17:41:42 +02:00
parent 93cafe8160
commit 0df55d295c
4 changed files with 69 additions and 26 deletions

View File

@@ -465,25 +465,16 @@ def _has_existing_idea(state: dict[str, Any], category: str, symbol: str) -> boo
for item in state.get("positions", []) for item in state.get("positions", [])
): ):
return True 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( return any(
item.get("symbol") == symbol item.get("symbol") == symbol
and item.get("category", category) == category 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", []) for item in state.get("open_orders", [])
) )

View File

@@ -35,7 +35,6 @@ from llm_trade_bot.models import (
) )
_INTERVALS = {"60": "1h", "240": "4h", "D": "1d"} _INTERVALS = {"60": "1h", "240": "4h", "D": "1d"}
_STABLE_COINS = {"USDT", "USDC", "FDUSD", "BUSD", "DAI", "USDE"}
class BinanceGateway: class BinanceGateway:
@@ -148,7 +147,7 @@ class BinanceGateway:
async def account_snapshot(self, category: str | None = None) -> AccountSnapshot: async def account_snapshot(self, category: str | None = None) -> AccountSnapshot:
state = await self.account_state() state = await self.account_state()
active = _active_ideas(state, self.scanner.quote_coin) active = _active_ideas(state)
if category == "linear": if category == "linear":
wallet = state["wallets"]["linear"] wallet = state["wallets"]["linear"]
gross = sum( gross = sum(
@@ -241,7 +240,8 @@ class BinanceGateway:
self._known_symbols[category].add(symbol) self._known_symbols[category].add(symbol)
ticker, book = await asyncio.gather( ticker, book = await asyncio.gather(
client.request("GET", f"{prefix}/ticker/24hr", {"symbol": symbol}), 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] = {} frames: dict[str, Any] = {}
for requested in self.scanner.candle_intervals: 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} 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 = { result = {
f"linear:{item['symbol']}" for item in state.get("positions", []) if _decimal(item.get("size")) > 0 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( result.update(
f"{item['category']}:{item['symbol']}" f"{item['category']}:{item['symbol']}"
for item in state.get("open_orders", []) for item in state.get("open_orders", [])
if not item.get("protective") if not item.get("protective")
or (
item.get("category") == "spot"
and str(item.get("client_id") or "").startswith("ltb-")
)
) )
return result return result

View File

@@ -5,7 +5,7 @@ from zoneinfo import ZoneInfo
import pytest 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 ( from llm_trade_bot.config import (
AppConfig, AppConfig,
BraveConfig, BraveConfig,
@@ -102,3 +102,22 @@ async def test_pause_blocks_entry_from_running_session(tmp_path):
) )
finally: finally:
await database.close() 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

View File

@@ -5,7 +5,7 @@ from typing import Any
import pytest 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.config import BinanceAccountConfig, BinanceConfig, ScannerConfig
from llm_trade_bot.models import ( from llm_trade_bot.models import (
Category, Category,
@@ -160,3 +160,36 @@ async def test_trailing_rate_outside_binance_range_rejects_before_opening():
) )
assert not usdm.requests assert not usdm.requests
assert not usdm.mutations 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"}