diff --git a/binance/client.py b/binance/client.py index 26ca54c3..1b636c74 100755 --- a/binance/client.py +++ b/binance/client.py @@ -44,6 +44,7 @@ class BaseClient: MARGIN_API_VERSION4 = 'v4' FUTURES_API_VERSION = 'v1' FUTURES_API_VERSION2 = 'v2' + FUTURES_API_VERSION3 = 'v3' OPTIONS_API_VERSION = 'v1' BASE_ENDPOINT_DEFAULT = '' @@ -231,7 +232,7 @@ def _create_futures_api_uri(self, path: str, version: int = 1) -> str: url = self.FUTURES_URL if self.testnet: url = self.FUTURES_TESTNET_URL - options = {1: self.FUTURES_API_VERSION, 2: self.FUTURES_API_VERSION2} + options = {1: self.FUTURES_API_VERSION, 2: self.FUTURES_API_VERSION2, 3: self.FUTURES_API_VERSION3} return url + '/' + options[version] + '/' + path def _create_futures_data_api_uri(self, path: str) -> str: @@ -244,7 +245,7 @@ def _create_futures_coin_api_url(self, path: str, version: int = 1) -> str: url = self.FUTURES_COIN_URL if self.testnet: url = self.FUTURES_COIN_TESTNET_URL - options = {1: self.FUTURES_API_VERSION, 2: self.FUTURES_API_VERSION2} + options = {1: self.FUTURES_API_VERSION, 2: self.FUTURES_API_VERSION2, 3: self.FUTURES_API_VERSION3} return url + "/" + options[version] + "/" + path def _create_futures_coin_data_api_url(self, path: str, version: int = 1) -> str: @@ -284,6 +285,14 @@ def _generate_signature(self, data: Dict) -> str: query_string = '&'.join([f"{d[0]}={d[1]}" for d in self._order_params(data)]) return sig_func(query_string) + @staticmethod + def _get_version(version: int, **kwargs) -> int: + if 'data' in kwargs and 'version' in kwargs['data']: + version_override = kwargs['data'].get('version') + del kwargs['data']['version'] + return version_override + return version + @staticmethod def uuid22(length=22): return format(random.getrandbits(length * 4), 'x') @@ -406,6 +415,7 @@ def _request_api( return self._request(method, uri, signed, **kwargs) def _request_futures_api(self, method, path, signed=False, version: int = 1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_api_uri(path, version) return self._request(method, uri, signed, True, **kwargs) @@ -416,11 +426,13 @@ def _request_futures_data_api(self, method, path, signed=False, **kwargs) -> Dic return self._request(method, uri, signed, True, **kwargs) def _request_futures_coin_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_coin_api_url(path, version=version) return self._request(method, uri, signed, True, **kwargs) def _request_futures_coin_data_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_coin_data_api_url(path, version=version) return self._request(method, uri, signed, True, **kwargs) @@ -431,6 +443,7 @@ def _request_options_api(self, method, path, signed=False, **kwargs) -> Dict: return self._request(method, uri, signed, True, **kwargs) def _request_margin_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_margin_api_uri(path, version) return self._request(method, uri, signed, **kwargs) @@ -4502,6 +4515,8 @@ def create_margin_order(self, **params): BinanceOrderInactiveSymbolException """ + if 'newClientOrderId' not in params: + params['newClientOrderId'] = self.SPOT_ORDER_PREFIX + self.uuid22() return self._request_margin_api('post', 'margin/order', signed=True, data=params) def cancel_margin_order(self, **params): @@ -7472,10 +7487,10 @@ def futures_countdown_cancel_all(self, **params): def futures_account_balance(self, **params): """Get futures account balance - https://binance-docs.github.io/apidocs/futures/en/#future-account-balance-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Futures-Account-Balance-V3 """ - return self._request_futures_api('get', 'balance', True, 2, data=params) + return self._request_futures_api('get', 'balance', True, 3, data=params) def futures_account(self, **params): """Get current account information. @@ -7523,7 +7538,7 @@ def futures_position_information(self, **params): https://binance-docs.github.io/apidocs/futures/en/#position-information-user_data """ - return self._request_futures_api('get', 'positionRisk', True, 2, data=params) + return self._request_futures_api('get', 'positionRisk', True, 3, data=params) def futures_account_trades(self, **params): """Get trades for the authenticated account and symbol. @@ -7592,6 +7607,13 @@ def futures_stream_close(self, listenKey): } return self._request_futures_api('delete', 'listenKey', signed=False, data=params) + # new methods + def futures_account_config(self, **params): + return self._request_futures_api('get', 'accountConfig', signed=True, version=1, data=params) + + def futures_symbol_config(self, **params): + return self._request_futures_api('get', 'symbolConfig', signed=True, version=1, data=params) + # COIN Futures API def futures_coin_ping(self): """Test connectivity to the Rest API @@ -8418,6 +8440,8 @@ def options_place_order(self, **params): :type recvWindow: int """ + if 'clientOrderId' not in params: + params['clientOrderId'] = self.CONTRACT_ORDER_PREFIX + self.uuid22() return self._request_options_api('post', 'order', signed=True, data=params) def options_place_batch_order(self, **params): @@ -8820,6 +8844,7 @@ async def _request_api(self, method, path, signed=False, version=BaseClient.PUBL return await self._request(method, uri, signed, **kwargs) async def _request_futures_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_api_uri(path, version=version) return await self._request(method, uri, signed, False, **kwargs) @@ -8830,11 +8855,13 @@ async def _request_futures_data_api(self, method, path, signed=False, **kwargs) return await self._request(method, uri, signed, True, **kwargs) async def _request_futures_coin_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_coin_api_url(path, version=version) return await self._request(method, uri, signed, True, **kwargs) async def _request_futures_coin_data_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_futures_coin_data_api_url(path, version=version) return await self._request(method, uri, signed, True, **kwargs) @@ -8845,6 +8872,7 @@ async def _request_options_api(self, method, path, signed=False, **kwargs) -> Di return await self._request(method, uri, signed, True, **kwargs) async def _request_margin_api(self, method, path, signed=False, version=1, **kwargs) -> Dict: + version = self._get_version(version, **kwargs) uri = self._create_margin_api_uri(path, version) return await self._request(method, uri, signed, **kwargs) @@ -9547,6 +9575,8 @@ async def repay_margin_loan(self, **params): repay_margin_loan.__doc__ = Client.repay_margin_loan.__doc__ async def create_margin_order(self, **params): + if 'newClientOrderId' not in params: + params['newClientOrderId'] = self.SPOT_ORDER_PREFIX + self.uuid22() return await self._request_margin_api('post', 'margin/order', signed=True, data=params) create_margin_order.__doc__ = Client.create_margin_order.__doc__ @@ -9967,9 +9997,9 @@ async def futures_cancel_orders(self, **params): async def futures_countdown_cancel_all(self, **params): return await self._request_futures_api('post', 'countdownCancelAll', True, data=params) - + async def futures_account_balance(self, **params): - return await self._request_futures_api('get', 'balance', True, version=2, data=params) + return await self._request_futures_api('get', 'balance', True, version=3, data=params) async def futures_account(self, **params): return await self._request_futures_api('get', 'account', True, version=2, data=params) @@ -9987,7 +10017,7 @@ async def futures_position_margin_history(self, **params): return await self._request_futures_api('get', 'positionMargin/history', True, data=params) async def futures_position_information(self, **params): - return await self._request_futures_api('get', 'positionRisk', True, version=2, data=params) + return await self._request_futures_api('get', 'positionRisk', True, version=3, data=params) async def futures_account_trades(self, **params): return await self._request_futures_api('get', 'userTrades', True, data=params) @@ -10026,6 +10056,13 @@ async def futures_stream_close(self, listenKey): } return await self._request_futures_api('delete', 'listenKey', signed=False, data=params) + # new methods + async def futures_account_config(self, **params): + return await self._request_futures_api('get', 'accountConfig', signed=True, version=1, data=params) + + async def futures_symbol_config(self, **params): + return await self._request_futures_api('get', 'symbolConfig', signed=True, version=1, data=params) + # COIN Futures API async def futures_coin_ping(self): @@ -10269,6 +10306,8 @@ async def options_bill(self, **params): return await self._request_options_api('post', 'bill', signed=True, data=params) async def options_place_order(self, **params): + if 'clientOrderId' not in params: + params['clientOrderId'] = self.CONTRACT_ORDER_PREFIX + self.uuid22() return await self._request_options_api('post', 'order', signed=True, data=params) async def options_place_batch_order(self, **params): diff --git a/tests/test_futures.py b/tests/test_futures.py new file mode 100644 index 00000000..fc512bc1 --- /dev/null +++ b/tests/test_futures.py @@ -0,0 +1,40 @@ + +import requests_mock +import pytest +import json +from binance.client import Client, AsyncClient +import re + +client = Client(api_key="api_key", api_secret="api_secret", ping=False) + +def test_futures_position_information(): + with requests_mock.mock() as m: + url_matcher = re.compile(r"https:\/\/fapi.binance.com\/fapi\/v3\/positionRisk\?.+") + response = [{'symbol': 'LTCUSDT', 'positionSide': 'LONG', 'positionAmt': '0.700', 'entryPrice': '75.6', 'breakEvenPrice': '75.63024', 'markPrice': '73.18000000', 'unRealizedProfit': '-1.69400000', 'liquidationPrice': '0', 'isolatedMargin': '0', 'notional': '51.22600000', 'marginAsset': 'USDT', 'isolatedWallet': '0', 'initialMargin': '10.24520000', 'maintMargin': '0.33296900', 'positionInitialMargin': '10.24520000', 'openOrderInitialMargin': '0', 'adl': 0, 'bidNotional': '0', 'askNotional': '0', 'updateTime': 1729436057076}] + m.register_uri("GET", url_matcher, json=json.dumps(response), status_code=200) + pos = client.futures_position_information(symbol="LTCUSDT") + assert m.last_request.qs['symbol'][0] == 'LTCUSDT'.lower() + assert m.last_request.path == '/fapi/v3/positionrisk' + +def test_futures_position_information_version_override(): + with requests_mock.mock() as m: + url_matcher = re.compile(r"https:\/\/fapi.binance.com\/fapi\/v2\/positionRisk\?.+") + response = [{'symbol': 'LTCUSDT', 'positionSide': 'LONG', 'positionAmt': '0.700', 'entryPrice': '75.6', 'breakEvenPrice': '75.63024', 'markPrice': '73.18000000', 'unRealizedProfit': '-1.69400000', 'liquidationPrice': '0', 'isolatedMargin': '0', 'notional': '51.22600000', 'marginAsset': 'USDT', 'isolatedWallet': '0', 'initialMargin': '10.24520000', 'maintMargin': '0.33296900', 'positionInitialMargin': '10.24520000', 'openOrderInitialMargin': '0', 'adl': 0, 'bidNotional': '0', 'askNotional': '0', 'updateTime': 1729436057076}] + m.register_uri("GET", url_matcher, json=json.dumps(response), status_code=200) + pos = client.futures_position_information(symbol="LTCUSDT", version=2) + assert m.last_request.qs['symbol'][0] == 'LTCUSDT'.lower() + assert m.last_request.path == '/fapi/v2/positionrisk' + +def test_futures_account_balance(): + with requests_mock.mock() as m: + url_matcher = re.compile(r"https:\/\/fapi.binance.com\/fapi\/v3\/balance\?.+") + m.register_uri("GET", url_matcher, json={}, status_code=200) + client.futures_account_balance() + assert m.last_request.path == '/fapi/v3/balance' + +def test_futures_account_config(): + with requests_mock.mock() as m: + url_matcher = re.compile(r"https:\/\/fapi.binance.com\/fapi\/v1\/accountConfig\?.+") + m.register_uri("GET", url_matcher, json={}, status_code=200) + client.futures_account_config() + assert m.last_request.path == '/fapi/v1/accountconfig' \ No newline at end of file diff --git a/tests/test_ids.py b/tests/test_ids.py index 792d15e4..f08fe81b 100644 --- a/tests/test_ids.py +++ b/tests/test_ids.py @@ -1,10 +1,6 @@ import requests_mock -import os, sys import pytest from aioresponses import aioresponses -root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(root) - from binance.client import Client, AsyncClient