Add Binance Testnet exchange gateway
This commit is contained in:
162
tests/test_binance_gateway.py
Normal file
162
tests/test_binance_gateway.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_trade_bot.binance_gateway import BinanceError, BinanceGateway
|
||||
from llm_trade_bot.config import BinanceAccountConfig, BinanceConfig, ScannerConfig
|
||||
from llm_trade_bot.models import (
|
||||
Category,
|
||||
OpenTradeRequest,
|
||||
OrderType,
|
||||
Side,
|
||||
ValidatedOrder,
|
||||
)
|
||||
|
||||
|
||||
class FakeBinanceClient:
|
||||
def __init__(self, exchange_info_path: str, mutation_results: list[dict[str, Any]]):
|
||||
self.exchange_info_path = exchange_info_path
|
||||
self.mutation_results = mutation_results
|
||||
self.requests: list[tuple[str, str, dict[str, Any] | None, dict[str, Any]]] = []
|
||||
self.mutations: list[tuple[str, str, dict[str, Any]]] = []
|
||||
|
||||
async def request(self, method: str, path: str, parameters=None, **kwargs):
|
||||
self.requests.append((method, path, parameters, kwargs))
|
||||
if path == self.exchange_info_path:
|
||||
return {
|
||||
"symbols": [
|
||||
{
|
||||
"symbol": "BTCUSDT",
|
||||
"status": "TRADING",
|
||||
"otoAllowed": True,
|
||||
"ocoAllowed": True,
|
||||
"filters": [
|
||||
{"filterType": "PRICE_FILTER", "tickSize": "0.1"},
|
||||
{"filterType": "LOT_SIZE", "minQty": "0.001", "stepSize": "0.001"},
|
||||
{"filterType": "MIN_NOTIONAL", "minNotional": "5"},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
if path == "/fapi/v1/positionSide/dual":
|
||||
return {"dualSidePosition": False}
|
||||
return {"ok": True}
|
||||
|
||||
async def mutate_with_recovery(self, method: str, path: str, parameters: dict[str, Any], **kwargs):
|
||||
self.mutations.append((method, path, parameters))
|
||||
return self.mutation_results.pop(0)
|
||||
|
||||
async def close(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def gateway(spot: FakeBinanceClient, usdm: FakeBinanceClient) -> BinanceGateway:
|
||||
return BinanceGateway(
|
||||
BinanceConfig(BinanceAccountConfig("spot-key", "spot-secret"), BinanceAccountConfig("future-key", "future-secret")),
|
||||
ScannerConfig(),
|
||||
spot_client=spot, # type: ignore[arg-type]
|
||||
usdm_client=usdm, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def request(category: Category, **changes: Any) -> OpenTradeRequest:
|
||||
values = {
|
||||
"category": category,
|
||||
"symbol": "BTCUSDT",
|
||||
"side": Side.BUY,
|
||||
"order_type": OrderType.LIMIT if category is Category.SPOT else OrderType.MARKET,
|
||||
"reference_price": Decimal(100),
|
||||
"limit_price": Decimal(100) if category is Category.SPOT else None,
|
||||
"stop_loss": Decimal(90),
|
||||
"take_profit": Decimal(120),
|
||||
"trailing_distance": None,
|
||||
"risk_pct": Decimal(2),
|
||||
"leverage": Decimal(3),
|
||||
"rationale": "test",
|
||||
}
|
||||
values.update(changes)
|
||||
return OpenTradeRequest(**values)
|
||||
|
||||
|
||||
def order(**changes: Any) -> ValidatedOrder:
|
||||
values = {
|
||||
"entry_price": Decimal(100),
|
||||
"quantity": Decimal(2),
|
||||
"notional": Decimal(200),
|
||||
"stop_loss": Decimal(90),
|
||||
"take_profit": Decimal(120),
|
||||
"trailing_distance": None,
|
||||
"leverage": Decimal(3),
|
||||
}
|
||||
values.update(changes)
|
||||
return ValidatedOrder(**values)
|
||||
|
||||
|
||||
async def test_spot_uses_single_atomic_otoco_with_market_tp_and_sl():
|
||||
spot = FakeBinanceClient(
|
||||
"/api/v3/exchangeInfo",
|
||||
[
|
||||
{
|
||||
"orderListId": 7,
|
||||
"listOrderStatus": "EXECUTING",
|
||||
"orderReports": [{"orderId": 11, "clientOrderId": "ltb-1-entry", "status": "NEW"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
usdm = FakeBinanceClient("/fapi/v1/exchangeInfo", [])
|
||||
result = await gateway(spot, usdm).open_trade(request(Category.SPOT), order(), "ltb-1")
|
||||
assert result["provider"] == "binance"
|
||||
assert result["category"] == "spot"
|
||||
assert result["order_id"] == "11"
|
||||
assert len(spot.mutations) == 1
|
||||
method, path, parameters = spot.mutations[0]
|
||||
assert (method, path) == ("POST", "/api/v3/orderList/otoco")
|
||||
assert parameters["workingType"] == "LIMIT"
|
||||
assert parameters["workingSide"] == "BUY"
|
||||
assert parameters["pendingSide"] == "SELL"
|
||||
assert parameters["pendingAboveType"] == "TAKE_PROFIT"
|
||||
assert parameters["pendingBelowType"] == "STOP_LOSS"
|
||||
|
||||
|
||||
async def test_futures_installs_stop_and_take_profit_after_market_fill():
|
||||
spot = FakeBinanceClient("/api/v3/exchangeInfo", [])
|
||||
usdm = FakeBinanceClient(
|
||||
"/fapi/v1/exchangeInfo",
|
||||
[
|
||||
{"orderId": 44, "status": "FILLED", "executedQty": "2", "avgPrice": "100"},
|
||||
{"algoId": 45},
|
||||
{"algoId": 46},
|
||||
],
|
||||
)
|
||||
result = await gateway(spot, usdm).open_trade(request(Category.LINEAR), order(), "ltb-2")
|
||||
assert result["status"] == "Filled"
|
||||
assert result["protection"]["status"] == "active"
|
||||
assert [path for _, path, _ in usdm.mutations] == [
|
||||
"/fapi/v1/order",
|
||||
"/fapi/v1/algoOrder",
|
||||
"/fapi/v1/algoOrder",
|
||||
]
|
||||
stop = usdm.mutations[1][2]
|
||||
take_profit = usdm.mutations[2][2]
|
||||
assert stop["type"] == "STOP_MARKET"
|
||||
assert stop["closePosition"] is True
|
||||
assert take_profit["type"] == "TAKE_PROFIT_MARKET"
|
||||
assert take_profit["closePosition"] is True
|
||||
assert any(path == "/fapi/v1/marginType" for _, path, _, _ in usdm.requests)
|
||||
assert any(path == "/fapi/v1/leverage" for _, path, _, _ in usdm.requests)
|
||||
|
||||
|
||||
async def test_trailing_rate_outside_binance_range_rejects_before_opening():
|
||||
spot = FakeBinanceClient("/api/v3/exchangeInfo", [])
|
||||
usdm = FakeBinanceClient("/fapi/v1/exchangeInfo", [])
|
||||
with pytest.raises(BinanceError, match="callbackRate"):
|
||||
await gateway(spot, usdm).open_trade(
|
||||
request(Category.LINEAR, take_profit=None, trailing_distance=Decimal("0.05")),
|
||||
order(take_profit=None, trailing_distance=Decimal("0.05")),
|
||||
"ltb-3",
|
||||
)
|
||||
assert not usdm.requests
|
||||
assert not usdm.mutations
|
||||
Reference in New Issue
Block a user