Fix Binance linear market context
This commit is contained in:
@@ -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", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user