915 lines
29 KiB
Python
915 lines
29 KiB
Python
"""Test del client Bybit V5 (httpx puro) con pytest-httpx.
|
|
|
|
Tutte le chiamate HTTP vengono intercettate via `httpx_mock`. Le request
|
|
non hanno bisogno di un AsyncClient iniettato: BybitClient costruisce
|
|
internamente `httpx.AsyncClient`, e pytest-httpx applica un MockTransport
|
|
globale.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
from cerbero_mcp.exchanges.bybit.client import (
|
|
BASE_TESTNET,
|
|
BybitClient,
|
|
_f,
|
|
_i,
|
|
)
|
|
from pytest_httpx import HTTPXMock
|
|
|
|
# ── helpers / costanti ────────────────────────────────────────
|
|
|
|
|
|
def _url(path: str) -> str:
|
|
return BASE_TESTNET + path
|
|
|
|
|
|
def _last_request_json(httpx_mock: HTTPXMock) -> dict:
|
|
req = httpx_mock.get_requests()[-1]
|
|
return json.loads(req.content.decode("utf-8"))
|
|
|
|
|
|
# ── init / helpers basics ─────────────────────────────────────
|
|
|
|
|
|
def test_client_init_stores_attrs():
|
|
c = BybitClient(api_key="k", api_secret="s", testnet=True)
|
|
assert c.testnet is True
|
|
assert c.base_url == BASE_TESTNET
|
|
assert c.api_key == "k"
|
|
|
|
|
|
def test_client_init_mainnet_base_url():
|
|
c = BybitClient(api_key="k", api_secret="s", testnet=False)
|
|
assert c.testnet is False
|
|
assert c.base_url == "https://api.bybit.com"
|
|
|
|
|
|
def test_client_init_custom_base_url():
|
|
c = BybitClient(
|
|
api_key="k", api_secret="s", testnet=True, base_url="https://example.test"
|
|
)
|
|
assert c.base_url == "https://example.test"
|
|
|
|
|
|
def test_client_init_injected_http_not_owned():
|
|
http = httpx.AsyncClient()
|
|
c = BybitClient(api_key="k", api_secret="s", http=http)
|
|
assert c._http is http
|
|
assert c._owns_http is False
|
|
|
|
|
|
def test_parse_helpers():
|
|
assert _f("1.5") == 1.5
|
|
assert _f("") is None
|
|
assert _f(None) is None
|
|
assert _i("42") == 42
|
|
assert _i("") is None
|
|
assert _i(None) is None
|
|
|
|
|
|
async def test_aclose_owned_client_closes():
|
|
c = BybitClient(api_key="k", api_secret="s", testnet=True)
|
|
await c.aclose()
|
|
# An aclosed client cannot dispatch requests
|
|
assert c._http.is_closed is True
|
|
|
|
|
|
# ── market data (public) ──────────────────────────────────────
|
|
|
|
|
|
async def test_get_ticker(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=BTCUSDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"lastPrice": "60000",
|
|
"markPrice": "60010",
|
|
"bid1Price": "59995",
|
|
"ask1Price": "60005",
|
|
"volume24h": "1500.5",
|
|
"turnover24h": "90000000",
|
|
"fundingRate": "0.0001",
|
|
"openInterest": "50000",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
t = await client.get_ticker("BTCUSDT", category="linear")
|
|
assert t["symbol"] == "BTCUSDT"
|
|
assert t["last_price"] == 60000.0
|
|
assert t["mark_price"] == 60010.0
|
|
assert t["bid"] == 59995.0
|
|
assert t["ask"] == 60005.0
|
|
assert t["volume_24h"] == 1500.5
|
|
assert t["funding_rate"] == 0.0001
|
|
assert t["open_interest"] == 50000.0
|
|
|
|
|
|
async def test_get_ticker_batch(client, httpx_mock: HTTPXMock):
|
|
def _row(sym: str) -> dict:
|
|
return {
|
|
"symbol": sym,
|
|
"lastPrice": "1",
|
|
"markPrice": "1",
|
|
"bid1Price": "1",
|
|
"ask1Price": "1",
|
|
"volume24h": "0",
|
|
"turnover24h": "0",
|
|
"fundingRate": "0",
|
|
"openInterest": "0",
|
|
}
|
|
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=BTCUSDT"),
|
|
json={"retCode": 0, "result": {"list": [_row("BTCUSDT")]}},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=ETHUSDT"),
|
|
json={"retCode": 0, "result": {"list": [_row("ETHUSDT")]}},
|
|
)
|
|
out = await client.get_ticker_batch(["BTCUSDT", "ETHUSDT"], category="linear")
|
|
assert set(out.keys()) == {"BTCUSDT", "ETHUSDT"}
|
|
|
|
|
|
async def test_get_ticker_not_found(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=UNKNOWNUSDT"),
|
|
json={"retCode": 0, "result": {"list": []}},
|
|
)
|
|
t = await client.get_ticker("UNKNOWNUSDT", category="linear")
|
|
assert t == {"symbol": "UNKNOWNUSDT", "error": "not_found"}
|
|
|
|
|
|
async def test_get_orderbook(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/orderbook?category=linear&symbol=BTCUSDT&limit=25"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"s": "BTCUSDT",
|
|
"b": [["59990", "0.5"], ["59980", "1.0"]],
|
|
"a": [["60010", "0.3"], ["60020", "0.7"]],
|
|
"ts": 1700000000000,
|
|
},
|
|
},
|
|
)
|
|
ob = await client.get_orderbook("BTCUSDT", category="linear", limit=25)
|
|
assert ob["symbol"] == "BTCUSDT"
|
|
assert ob["bids"] == [[59990.0, 0.5], [59980.0, 1.0]]
|
|
assert ob["asks"] == [[60010.0, 0.3], [60020.0, 0.7]]
|
|
assert ob["timestamp"] == 1700000000000
|
|
|
|
|
|
async def test_get_historical(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url(
|
|
"/v5/market/kline?category=linear&symbol=BTCUSDT&interval=60&limit=1000"
|
|
"&start=1700000000000&end=1700003600000"
|
|
),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
[
|
|
"1700000000000",
|
|
"60000",
|
|
"60500",
|
|
"59500",
|
|
"60200",
|
|
"100",
|
|
"6020000",
|
|
],
|
|
[
|
|
"1700003600000",
|
|
"60200",
|
|
"60700",
|
|
"60000",
|
|
"60400",
|
|
"80",
|
|
"4832000",
|
|
],
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_historical(
|
|
"BTCUSDT",
|
|
category="linear",
|
|
interval="60",
|
|
start=1700000000000,
|
|
end=1700003600000,
|
|
)
|
|
assert len(out["candles"]) == 2
|
|
c0 = out["candles"][0]
|
|
assert c0["timestamp"] == 1700000000000
|
|
assert c0["open"] == 60000.0
|
|
assert c0["high"] == 60500.0
|
|
assert c0["low"] == 59500.0
|
|
assert c0["close"] == 60200.0
|
|
assert c0["volume"] == 100.0
|
|
|
|
|
|
async def test_get_indicators(client, httpx_mock: HTTPXMock):
|
|
rows = [
|
|
[
|
|
str(1700000000000 + i * 3600_000),
|
|
str(60000 + i * 10),
|
|
str(60000 + i * 10 + 5),
|
|
str(60000 + i * 10 - 5),
|
|
str(60000 + i * 10 + 2),
|
|
"100",
|
|
"6000000",
|
|
]
|
|
for i in range(35)
|
|
]
|
|
httpx_mock.add_response(
|
|
url=_url(
|
|
"/v5/market/kline?category=linear&symbol=BTCUSDT&interval=60&limit=1000"
|
|
),
|
|
json={"retCode": 0, "result": {"list": rows}},
|
|
)
|
|
out = await client.get_indicators(
|
|
"BTCUSDT",
|
|
category="linear",
|
|
indicators=["rsi", "atr", "macd", "adx"],
|
|
interval="60",
|
|
)
|
|
assert "rsi" in out and out["rsi"] is not None
|
|
assert "atr" in out and out["atr"] is not None
|
|
assert "macd" in out and out["macd"]["macd"] is not None
|
|
assert "adx" in out and out["adx"]["adx"] is not None
|
|
|
|
|
|
async def test_get_funding_rate(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=BTCUSDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"fundingRate": "0.0001",
|
|
"nextFundingTime": "1700003600000",
|
|
"lastPrice": "60000",
|
|
"markPrice": "60000",
|
|
"bid1Price": "0",
|
|
"ask1Price": "0",
|
|
"volume24h": "0",
|
|
"turnover24h": "0",
|
|
"openInterest": "0",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_funding_rate("BTCUSDT", category="linear")
|
|
assert out["symbol"] == "BTCUSDT"
|
|
assert out["funding_rate"] == 0.0001
|
|
assert out["next_funding_time"] == 1700003600000
|
|
|
|
|
|
async def test_get_funding_history(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url(
|
|
"/v5/market/funding/history?category=linear&symbol=BTCUSDT&limit=50"
|
|
),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"fundingRate": "0.0001",
|
|
"fundingRateTimestamp": "1700000000000",
|
|
},
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"fundingRate": "0.00008",
|
|
"fundingRateTimestamp": "1699996400000",
|
|
},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_funding_history("BTCUSDT", category="linear", limit=50)
|
|
assert len(out["history"]) == 2
|
|
assert out["history"][0]["rate"] == 0.0001
|
|
|
|
|
|
async def test_get_open_interest(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url(
|
|
"/v5/market/open-interest?category=linear&symbol=BTCUSDT"
|
|
"&intervalTime=5min&limit=100"
|
|
),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{"openInterest": "50000", "timestamp": "1700000000000"},
|
|
{"openInterest": "49000", "timestamp": "1699996400000"},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_open_interest(
|
|
"BTCUSDT", category="linear", interval="5min", limit=100
|
|
)
|
|
assert len(out["points"]) == 2
|
|
assert out["current_oi"] == 50000.0
|
|
|
|
|
|
async def test_get_instruments(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/instruments-info?category=linear"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"status": "Trading",
|
|
"baseCoin": "BTC",
|
|
"quoteCoin": "USDT",
|
|
"priceFilter": {"tickSize": "0.1"},
|
|
"lotSizeFilter": {
|
|
"qtyStep": "0.001",
|
|
"minOrderQty": "0.001",
|
|
},
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_instruments(category="linear")
|
|
assert len(out["instruments"]) == 1
|
|
inst = out["instruments"][0]
|
|
assert inst["symbol"] == "BTCUSDT"
|
|
assert inst["tick_size"] == 0.1
|
|
assert inst["qty_step"] == 0.001
|
|
|
|
|
|
async def test_get_option_chain(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/instruments-info?category=option&baseCoin=BTC"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTC-30JUN25-50000-C",
|
|
"baseCoin": "BTC",
|
|
"settleCoin": "USDC",
|
|
"optionsType": "Call",
|
|
"launchTime": "1700000000000",
|
|
"deliveryTime": "1719734400000",
|
|
},
|
|
{
|
|
"symbol": "BTC-30JUN25-50000-P",
|
|
"baseCoin": "BTC",
|
|
"settleCoin": "USDC",
|
|
"optionsType": "Put",
|
|
"launchTime": "1700000000000",
|
|
"deliveryTime": "1719734400000",
|
|
},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_option_chain(base_coin="BTC")
|
|
assert len(out["options"]) == 2
|
|
assert out["options"][0]["type"] == "Call"
|
|
|
|
|
|
# ── account / positions / orders (signed reads) ───────────────
|
|
|
|
|
|
async def test_get_positions(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/list?category=linear&settleCoin=USDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"side": "Buy",
|
|
"size": "0.1",
|
|
"avgPrice": "60000",
|
|
"unrealisedPnl": "50",
|
|
"leverage": "10",
|
|
"liqPrice": "50000",
|
|
"positionValue": "6000",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_positions(category="linear")
|
|
assert len(out) == 1
|
|
p = out[0]
|
|
assert p["symbol"] == "BTCUSDT"
|
|
assert p["side"] == "Buy"
|
|
assert p["size"] == 0.1
|
|
assert p["entry_price"] == 60000.0
|
|
assert p["liquidation_price"] == 50000.0
|
|
# signed: header X-BAPI-API-KEY presente
|
|
req = httpx_mock.get_requests()[-1]
|
|
assert req.headers.get("X-BAPI-API-KEY") == "test_key"
|
|
assert "X-BAPI-SIGN" in req.headers
|
|
|
|
|
|
async def test_get_account_summary(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/account/wallet-balance?accountType=UNIFIED"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"accountType": "UNIFIED",
|
|
"totalEquity": "10000",
|
|
"totalWalletBalance": "9500",
|
|
"totalMarginBalance": "9800",
|
|
"totalAvailableBalance": "9000",
|
|
"totalPerpUPL": "200",
|
|
"coin": [
|
|
{
|
|
"coin": "USDT",
|
|
"walletBalance": "9500",
|
|
"equity": "9700",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_account_summary()
|
|
assert out["equity"] == 10000.0
|
|
assert out["available_balance"] == 9000.0
|
|
assert out["unrealized_pnl"] == 200.0
|
|
assert len(out["coins"]) == 1
|
|
assert out["coins"][0]["coin"] == "USDT"
|
|
|
|
|
|
async def test_get_trade_history(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/execution/list?category=linear&limit=50"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"side": "Buy",
|
|
"execQty": "0.01",
|
|
"execPrice": "60000",
|
|
"execFee": "0.1",
|
|
"execTime": "1700000000000",
|
|
"orderId": "abc",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_trade_history(category="linear", limit=50)
|
|
assert len(out) == 1
|
|
assert out[0]["symbol"] == "BTCUSDT"
|
|
assert out[0]["size"] == 0.01
|
|
assert out[0]["price"] == 60000.0
|
|
|
|
|
|
async def test_get_open_orders(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/realtime?category=linear&settleCoin=USDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"orderId": "o1",
|
|
"side": "Buy",
|
|
"qty": "0.1",
|
|
"price": "59000",
|
|
"orderType": "Limit",
|
|
"orderStatus": "New",
|
|
"reduceOnly": False,
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_open_orders(category="linear")
|
|
assert len(out) == 1
|
|
assert out[0]["order_id"] == "o1"
|
|
assert out[0]["price"] == 59000.0
|
|
|
|
|
|
# ── basis / spreads ────────────────────────────────────────────
|
|
|
|
|
|
async def test_get_basis_spot_perp(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=spot&symbol=BTCUSDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"lastPrice": "60000",
|
|
"markPrice": "60000",
|
|
"bid1Price": "59995",
|
|
"ask1Price": "60005",
|
|
"volume24h": "0",
|
|
"turnover24h": "0",
|
|
"fundingRate": "0",
|
|
"openInterest": "0",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/market/tickers?category=linear&symbol=BTCUSDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"lastPrice": "60120",
|
|
"markPrice": "60120",
|
|
"bid1Price": "60115",
|
|
"ask1Price": "60125",
|
|
"volume24h": "0",
|
|
"turnover24h": "0",
|
|
"fundingRate": "0.0001",
|
|
"openInterest": "0",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
out = await client.get_basis_spot_perp("BTC")
|
|
assert out["asset"] == "BTC"
|
|
assert out["spot_price"] == 60000.0
|
|
assert out["perp_price"] == 60120.0
|
|
assert out["basis_abs"] == 120.0
|
|
assert round(out["basis_pct"], 3) == 0.2
|
|
|
|
|
|
# ── trading writes ────────────────────────────────────────────
|
|
|
|
|
|
async def test_place_order_limit(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create"),
|
|
method="POST",
|
|
json={
|
|
"retCode": 0,
|
|
"result": {"orderId": "ord123", "orderLinkId": ""},
|
|
},
|
|
)
|
|
out = await client.place_order(
|
|
category="linear",
|
|
symbol="BTCUSDT",
|
|
side="Buy",
|
|
qty=0.01,
|
|
order_type="Limit",
|
|
price=60000.0,
|
|
tif="GTC",
|
|
)
|
|
assert out["order_id"] == "ord123"
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "linear"
|
|
assert body["symbol"] == "BTCUSDT"
|
|
assert body["side"] == "Buy"
|
|
assert body["qty"] == "0.01"
|
|
assert body["orderType"] == "Limit"
|
|
assert body["price"] == "60000.0"
|
|
assert body["timeInForce"] == "GTC"
|
|
|
|
|
|
async def test_place_order_error(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create"),
|
|
method="POST",
|
|
json={"retCode": 10001, "retMsg": "insufficient balance"},
|
|
)
|
|
out = await client.place_order(
|
|
category="linear",
|
|
symbol="BTCUSDT",
|
|
side="Buy",
|
|
qty=0.01,
|
|
order_type="Market",
|
|
)
|
|
assert out.get("error") == "insufficient balance"
|
|
assert out.get("code") == 10001
|
|
|
|
|
|
async def test_amend_order(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/amend"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {"orderId": "ord1"}},
|
|
)
|
|
out = await client.amend_order(
|
|
category="linear", symbol="BTCUSDT", order_id="ord1", new_qty=0.02
|
|
)
|
|
assert out["order_id"] == "ord1"
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["orderId"] == "ord1"
|
|
assert body["qty"] == "0.02"
|
|
assert "price" not in body
|
|
|
|
|
|
async def test_place_order_option_adds_link_id(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create"),
|
|
method="POST",
|
|
json={
|
|
"retCode": 0,
|
|
"result": {"orderId": "opt1", "orderLinkId": "cerbero-abc"},
|
|
},
|
|
)
|
|
await client.place_order(
|
|
category="option",
|
|
symbol="BTC-24APR26-96000-C-USDT",
|
|
side="Buy",
|
|
qty=0.01,
|
|
order_type="Limit",
|
|
price=5.0,
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert "orderLinkId" in body
|
|
assert body["orderLinkId"].startswith("cerbero-")
|
|
|
|
|
|
async def test_place_order_linear_no_link_id(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {"orderId": "x"}},
|
|
)
|
|
await client.place_order(
|
|
category="linear",
|
|
symbol="BTCUSDT",
|
|
side="Buy",
|
|
qty=0.01,
|
|
order_type="Market",
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert "orderLinkId" not in body
|
|
|
|
|
|
async def test_place_combo_order_batch_option(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create-batch"),
|
|
method="POST",
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{"orderId": "ord-1", "orderLinkId": "cerbero-leg1"},
|
|
{"orderId": "ord-2", "orderLinkId": "cerbero-leg2"},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
legs = [
|
|
{
|
|
"symbol": "BTC-30APR26-75000-C-USDT",
|
|
"side": "Buy",
|
|
"qty": 0.01,
|
|
"order_type": "Limit",
|
|
"price": 5.0,
|
|
},
|
|
{
|
|
"symbol": "BTC-30APR26-80000-C-USDT",
|
|
"side": "Sell",
|
|
"qty": 0.01,
|
|
"order_type": "Limit",
|
|
"price": 3.0,
|
|
},
|
|
]
|
|
out = await client.place_combo_order(category="option", legs=legs)
|
|
assert len(out["orders"]) == 2
|
|
assert out["orders"][0]["order_id"] == "ord-1"
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "option"
|
|
request = body["request"]
|
|
assert len(request) == 2
|
|
assert request[0]["symbol"] == "BTC-30APR26-75000-C-USDT"
|
|
assert request[0]["qty"] == "0.01"
|
|
assert request[0]["orderType"] == "Limit"
|
|
assert "orderLinkId" in request[0]
|
|
|
|
|
|
async def test_place_combo_order_error(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create-batch"),
|
|
method="POST",
|
|
json={"retCode": 10001, "retMsg": "invalid leg"},
|
|
)
|
|
out = await client.place_combo_order(
|
|
category="option",
|
|
legs=[
|
|
{
|
|
"symbol": "X",
|
|
"side": "Buy",
|
|
"qty": 1,
|
|
"order_type": "Limit",
|
|
"price": 1.0,
|
|
},
|
|
{
|
|
"symbol": "Y",
|
|
"side": "Sell",
|
|
"qty": 1,
|
|
"order_type": "Limit",
|
|
"price": 1.0,
|
|
},
|
|
],
|
|
)
|
|
assert out["error"] == "invalid leg"
|
|
assert out["code"] == 10001
|
|
|
|
|
|
async def test_place_combo_order_rejects_non_option(client):
|
|
with pytest.raises(ValueError, match="option"):
|
|
await client.place_combo_order(
|
|
category="linear",
|
|
legs=[
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"side": "Buy",
|
|
"qty": 0.01,
|
|
"order_type": "Market",
|
|
},
|
|
{
|
|
"symbol": "ETHUSDT",
|
|
"side": "Sell",
|
|
"qty": 0.01,
|
|
"order_type": "Market",
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
async def test_cancel_order(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/cancel"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {"orderId": "ord1"}},
|
|
)
|
|
out = await client.cancel_order(category="linear", symbol="BTCUSDT", order_id="ord1")
|
|
assert out["order_id"] == "ord1"
|
|
assert out["status"] == "cancelled"
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "linear"
|
|
assert body["symbol"] == "BTCUSDT"
|
|
assert body["orderId"] == "ord1"
|
|
|
|
|
|
async def test_cancel_all_orders(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/cancel-all"),
|
|
method="POST",
|
|
json={
|
|
"retCode": 0,
|
|
"result": {"list": [{"orderId": "o1"}, {"orderId": "o2"}]},
|
|
},
|
|
)
|
|
out = await client.cancel_all_orders(category="linear", symbol="BTCUSDT")
|
|
assert out["cancelled_ids"] == ["o1", "o2"]
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "linear"
|
|
assert body["symbol"] == "BTCUSDT"
|
|
|
|
|
|
async def test_set_stop_loss(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/trading-stop"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {}},
|
|
)
|
|
out = await client.set_stop_loss(
|
|
category="linear", symbol="BTCUSDT", stop_loss=55000.0
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "linear"
|
|
assert body["symbol"] == "BTCUSDT"
|
|
assert body["stopLoss"] == "55000.0"
|
|
assert body.get("positionIdx", 0) == 0
|
|
assert out["status"] == "stop_loss_set"
|
|
|
|
|
|
async def test_set_take_profit(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/trading-stop"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {}},
|
|
)
|
|
out = await client.set_take_profit(
|
|
category="linear", symbol="BTCUSDT", take_profit=65000.0
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["takeProfit"] == "65000.0"
|
|
assert out["status"] == "take_profit_set"
|
|
|
|
|
|
async def test_close_position(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/list?category=linear&settleCoin=USDT"),
|
|
json={
|
|
"retCode": 0,
|
|
"result": {
|
|
"list": [
|
|
{
|
|
"symbol": "BTCUSDT",
|
|
"side": "Buy",
|
|
"size": "0.1",
|
|
"avgPrice": "60000",
|
|
"unrealisedPnl": "0",
|
|
"leverage": "10",
|
|
"liqPrice": "0",
|
|
"positionValue": "6000",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/order/create"),
|
|
method="POST",
|
|
json={
|
|
"retCode": 0,
|
|
"result": {"orderId": "closeord", "orderLinkId": ""},
|
|
},
|
|
)
|
|
out = await client.close_position(category="linear", symbol="BTCUSDT")
|
|
assert out["status"] == "submitted"
|
|
# the LAST request is the place_order body
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["side"] == "Sell"
|
|
assert body["qty"] == "0.1"
|
|
assert body["reduceOnly"] is True
|
|
assert body["orderType"] == "Market"
|
|
|
|
|
|
async def test_set_leverage(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/set-leverage"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {}},
|
|
)
|
|
out = await client.set_leverage(category="linear", symbol="BTCUSDT", leverage=5)
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["category"] == "linear"
|
|
assert body["symbol"] == "BTCUSDT"
|
|
assert body["buyLeverage"] == "5"
|
|
assert body["sellLeverage"] == "5"
|
|
assert out["status"] == "leverage_set"
|
|
|
|
|
|
async def test_switch_position_mode(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/position/switch-mode"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {}},
|
|
)
|
|
out = await client.switch_position_mode(
|
|
category="linear", symbol="BTCUSDT", mode="hedge"
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["mode"] == 3
|
|
assert out["status"] == "mode_switched"
|
|
|
|
|
|
async def test_transfer_asset(client, httpx_mock: HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=_url("/v5/asset/transfer/inter-transfer"),
|
|
method="POST",
|
|
json={"retCode": 0, "result": {"transferId": "tx123"}},
|
|
)
|
|
out = await client.transfer_asset(
|
|
coin="USDT", amount=100.0, from_type="UNIFIED", to_type="FUND"
|
|
)
|
|
body = _last_request_json(httpx_mock)
|
|
assert body["coin"] == "USDT"
|
|
assert body["amount"] == "100.0"
|
|
assert body["fromAccountType"] == "UNIFIED"
|
|
assert body["toAccountType"] == "FUND"
|
|
assert out["transfer_id"] == "tx123"
|