diff --git a/examples/async_direct_session.py b/examples/async_direct_session.py new file mode 100644 index 0000000..43878cf --- /dev/null +++ b/examples/async_direct_session.py @@ -0,0 +1,59 @@ +from pybit.unified_trading import AsyncHTTP +import asyncio + + +BYBIT_API_KEY = "api_key" +BYBIT_API_SECRET = "api_secret" +TESTNET = True # True means your API keys were generated on testnet.bybit.com + + +async def main(): + session = AsyncHTTP(api_key=BYBIT_API_KEY, + api_secret=BYBIT_API_SECRET, + testnet=True) + + # Place order + + response = await session.place_order( + category="spot", + symbol="ETHUSDT", + side="Sell", + orderType="Market", + qty="0.1", + timeInForce="GTC", + ) + + # Example to cancel orders + + response = await session.get_open_orders( + category="linear", + symbol="BTCUSDT", + ) + + orders = response["result"]["list"] + + for order in orders: + if order["orderStatus"] == "Untriggered": + await session.cancel_order( + category="linear", + symbol=order["symbol"], + orderId=order["orderId"], + ) + + # Batch cancel orders + + orders_to_cancel = [ + {"category": "option", "symbol": o["symbol"], "orderId": o["orderId"]} + for o in response["result"]["list"] + if o["orderStatus"] == "New" + ] + + response = await session.cancel_batch_order( + category="option", + request=orders_to_cancel, + ) + + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) + diff --git a/examples/async_websocket_example_quickstart.py b/examples/async_websocket_example_quickstart.py new file mode 100644 index 0000000..570e5a6 --- /dev/null +++ b/examples/async_websocket_example_quickstart.py @@ -0,0 +1,33 @@ +import asyncio +from pybit.asyncio.websocket import ( + AsyncWebsocket, + WSState +) + + +async def main(): + ws = AsyncWebsocket( + testnet=True, + api_key="api_key", + api_secret="api_secret" + ) + private_session = ws.order_stream(category="linear") + async with private_session as active_session: + while True: + if active_session.ws_state == WSState.EXITING: + break + response = await active_session.recv() + print(response) + break + + public_session = ws.kline_stream(symbols=["kline.60.BTCUSDT"], channel_type="linear") + async with public_session as active_session: + while True: + if active_session.ws_state == WSState.EXITING: + break + response = await active_session.recv() + print(response) + + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) diff --git a/pybit/_http_manager/__init__.py b/pybit/_http_manager/__init__.py new file mode 100644 index 0000000..bee4a00 --- /dev/null +++ b/pybit/_http_manager/__init__.py @@ -0,0 +1 @@ +from ._http_manager import _V5HTTPManager diff --git a/pybit/_http_manager/_auth.py b/pybit/_http_manager/_auth.py new file mode 100644 index 0000000..566365f --- /dev/null +++ b/pybit/_http_manager/_auth.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass, field +import hmac +import hashlib +from Crypto.Hash import SHA256 +from Crypto.PublicKey import RSA +from Crypto.Signature import PKCS1_v1_5 +import base64 + +from pybit import _helpers + + +def generate_signature(use_rsa_authentication, secret, param_str): + def generate_hmac(): + hash = hmac.new( + bytes(secret, "utf-8"), + param_str.encode("utf-8"), + hashlib.sha256, + ) + return hash.hexdigest() + + def generate_rsa(): + hash = SHA256.new(param_str.encode("utf-8")) + encoded_signature = base64.b64encode( + PKCS1_v1_5.new(RSA.importKey(secret)).sign( + hash + ) + ) + return encoded_signature.decode() + + if not use_rsa_authentication: + return generate_hmac() + else: + return generate_rsa() + + +@dataclass +class AuthService: + api_key: str = field(default=None) + api_secret: str = field(default=None) + rsa_authentication: str = field(default=False) + + def _auth(self, payload, recv_window, timestamp): + """ + Prepares authentication signature per Bybit API specifications. + """ + + if self.api_key is None or self.api_secret is None: + raise PermissionError("Authenticated endpoints require keys.") + + param_str = str(timestamp) + self.api_key + str(recv_window) + payload + + return generate_signature( + self.rsa_authentication, self.api_secret, param_str + ) + + def _prepare_auth_headers(self, recv_window, req_params) -> dict: + # Prepare signature. + timestamp = _helpers.generate_timestamp() + signature = self._auth( + payload=req_params, + recv_window=recv_window, + timestamp=timestamp, + ) + return { + "Content-Type": "application/json", + "X-BAPI-API-KEY": self.api_key, + "X-BAPI-SIGN": signature, + "X-BAPI-SIGN-TYPE": "2", + "X-BAPI-TIMESTAMP": str(timestamp), + "X-BAPI-RECV-WINDOW": str(recv_window), + } + + def _change_floating_numbers_for_auth_signature(self, query): + # Bug fix: change floating whole numbers to integers to prevent + # auth signature errors. + if query is not None: + for i in query.keys(): + if isinstance(query[i], float) and query[i] == int(query[i]): + query[i] = int(query[i]) diff --git a/pybit/_http_manager/_http_helpers.py b/pybit/_http_manager/_http_helpers.py new file mode 100644 index 0000000..61028be --- /dev/null +++ b/pybit/_http_manager/_http_helpers.py @@ -0,0 +1,26 @@ +import logging + +from datetime import datetime as dt + +from pybit import _helpers + + +def set_logger_handler(logger, logging_level): + if len(logging.root.handlers) == 0: + # no handler on root logger set -> we add handler just for this logger to not mess with custom logic from outside + handler = logging.StreamHandler() + handler.setFormatter( + logging.Formatter( + fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + handler.setLevel(logging_level) + logger.addHandler(handler) + + +def calculate_rate_limit_delay_time(x_bapi_limit_reset_timestamp: int): + limit_reset_str = dt.fromtimestamp(x_bapi_limit_reset_timestamp / 10 ** 3).strftime( + "%H:%M:%S.%f")[:-3] + delay_time = (int(x_bapi_limit_reset_timestamp) - _helpers.generate_timestamp()) / 10 ** 3 + return delay_time, limit_reset_str diff --git a/pybit/_http_manager.py b/pybit/_http_manager/_http_manager.py similarity index 55% rename from pybit/_http_manager.py rename to pybit/_http_manager/_http_manager.py index 3d3d3d2..2336fb7 100644 --- a/pybit/_http_manager.py +++ b/pybit/_http_manager/_http_manager.py @@ -1,20 +1,19 @@ from collections import defaultdict from dataclasses import dataclass, field import time -import hmac -import hashlib -from Crypto.Hash import SHA256 -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 -import base64 import json import logging import requests from datetime import datetime as dt -from .exceptions import FailedRequestError, InvalidRequestError -from . import _helpers +from pybit.exceptions import FailedRequestError, InvalidRequestError +from pybit._http_manager._auth import AuthService +from pybit._http_manager._response_handler import ( + ResponseHandler, + ForceRetryException +) +from pybit._http_manager import _http_helpers # Requests will use simplejson if available. try: @@ -34,32 +33,15 @@ TLD_HK = "com.hk" -def generate_signature(use_rsa_authentication, secret, param_str): - def generate_hmac(): - hash = hmac.new( - bytes(secret, "utf-8"), - param_str.encode("utf-8"), - hashlib.sha256, - ) - return hash.hexdigest() - - def generate_rsa(): - hash = SHA256.new(param_str.encode("utf-8")) - encoded_signature = base64.b64encode( - PKCS1_v1_5.new(RSA.importKey(secret)).sign( - hash - ) - ) - return encoded_signature.decode() - - if not use_rsa_authentication: - return generate_hmac() - else: - return generate_rsa() +RET_CODE = "retCode" +RET_MSG = "retMsg" @dataclass -class _V5HTTPManager: +class _V5HTTPManager( + AuthService, + ResponseHandler +): testnet: bool = field(default=False) domain: str = field(default=DOMAIN_MAIN) tld: str = field(default=TLD_MAIN) @@ -102,21 +84,11 @@ def __post_init__(self): if not self.retry_codes: self.retry_codes = {10002, 10006, 30034, 30035, 130035, 130150} self.logger = logging.getLogger(__name__) - if len(logging.root.handlers) == 0: - # no handler on root logger set -> we add handler just for this logger to not mess with custom logic from outside - handler = logging.StreamHandler() - handler.setFormatter( - logging.Formatter( - fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ) - handler.setLevel(self.logging_level) - self.logger.addHandler(handler) + _http_helpers.set_logger_handler(self.logger, self.logging_level) self.logger.debug("Initializing HTTP session.") - self.client = requests.Session() + self.client = self._init_request_client() self.client.headers.update( { "Content-Type": "application/json", @@ -126,6 +98,10 @@ def __post_init__(self): if self.referral_id: self.client.headers.update({"Referer": self.referral_id}) + @staticmethod + def _init_request_client(): + return requests.Session() + @staticmethod def prepare_payload(method, parameters): """ @@ -162,20 +138,6 @@ def cast_values(): cast_values() return json.dumps(parameters) - def _auth(self, payload, recv_window, timestamp): - """ - Prepares authentication signature per Bybit API specifications. - """ - - if self.api_key is None or self.api_secret is None: - raise PermissionError("Authenticated endpoints require keys.") - - param_str = str(timestamp) + self.api_key + str(recv_window) + payload - - return generate_signature( - self.rsa_authentication, self.api_secret, param_str - ) - @staticmethod def _verify_string(params, key): if key in params: @@ -185,6 +147,34 @@ def _verify_string(params, key): return True return True + def _log_request(self, req_params, method, path, headers): + if self.log_requests: + if req_params: + self.logger.debug( + f"Request -> {method} {path}. Body: {req_params}. " + f"Headers: {headers}" + ) + else: + self.logger.debug( + f"Request -> {method} {path}. Headers: {headers}" + ) + + def _prepare_request(self, recv_window, method=None, path=None, query=None, auth=False): + req_params = self.prepare_payload(method, query) + + # Authenticate if we are using a private endpoint. + headers = self._prepare_auth_headers(recv_window, req_params) if auth else {} + + if method == "GET": + path = path + f"?{req_params}" if req_params else path + data = None + else: + data = req_params + + return self.client.prepare_request( + requests.Request(method, path, data=data, headers=headers) + ) + def _submit_request(self, method=None, path=None, query=None, auth=False): """ Submits the request to the API. @@ -205,10 +195,7 @@ def _submit_request(self, method=None, path=None, query=None, auth=False): # Bug fix: change floating whole numbers to integers to prevent # auth signature errors. - if query is not None: - for i in query.keys(): - if isinstance(query[i], float) and query[i] == int(query[i]): - query[i] = int(query[i]) + self._change_floating_numbers_for_auth_signature(query) # Send request and return headers with body. Retry if failed. retries_attempted = self.max_retries @@ -227,57 +214,10 @@ def _submit_request(self, method=None, path=None, query=None, auth=False): retries_remaining = f"{retries_attempted} retries remain." - req_params = self.prepare_payload(method, query) - - # Authenticate if we are using a private endpoint. - if auth: - # Prepare signature. - timestamp = _helpers.generate_timestamp() - signature = self._auth( - payload=req_params, - recv_window=recv_window, - timestamp=timestamp, - ) - headers = { - "Content-Type": "application/json", - "X-BAPI-API-KEY": self.api_key, - "X-BAPI-SIGN": signature, - "X-BAPI-SIGN-TYPE": "2", - "X-BAPI-TIMESTAMP": str(timestamp), - "X-BAPI-RECV-WINDOW": str(recv_window), - } - else: - headers = {} + r = self._prepare_request(recv_window, method, path, query, auth) - if method == "GET": - if req_params: - r = self.client.prepare_request( - requests.Request( - method, path + f"?{req_params}", headers=headers - ) - ) - else: - r = self.client.prepare_request( - requests.Request(method, path, headers=headers) - ) - else: - r = self.client.prepare_request( - requests.Request( - method, path, data=req_params, headers=headers - ) - ) - # Log the request. - if self.log_requests: - if req_params: - self.logger.debug( - f"Request -> {method} {path}. Body: {req_params}. " - f"Headers: {r.headers}" - ) - else: - self.logger.debug( - f"Request -> {method} {path}. Headers: {r.headers}" - ) + self._log_request(req_params, method, path, r.headers) # Attempt the request. try: @@ -297,71 +237,40 @@ def _submit_request(self, method=None, path=None, query=None, auth=False): raise e # Check HTTP status code before trying to decode JSON. - if s.status_code != 200: - if s.status_code == 403: - error_msg = "You have breached the IP rate limit or your IP is from the USA." - else: - error_msg = "HTTP status code is not 200." - self.logger.debug(f"Response text: {s.text}") - raise FailedRequestError( - request=f"{method} {path}: {req_params}", - message=error_msg, - status_code=s.status_code, - time=dt.utcnow().strftime("%H:%M:%S"), - resp_headers=s.headers, - ) + self._check_status_code(s, method, path, req_params) # Convert response to dictionary, or raise if requests error. try: - s_json = s.json() - - # If we have trouble converting, handle the error and retry. - except JSONDecodeError as e: - if self.force_retry: - self.logger.error(f"{e}. {retries_remaining}") - time.sleep(self.retry_delay) - continue - else: - self.logger.debug(f"Response text: {s.text}") - raise FailedRequestError( - request=f"{method} {path}: {req_params}", - message="Conflict. Could not decode JSON.", - status_code=409, - time=dt.utcnow().strftime("%H:%M:%S"), - resp_headers=s.headers, - ) - - ret_code = "retCode" - ret_msg = "retMsg" + s_json = self._convert_to_dict(s, method, path, req_params) + except ForceRetryException as e: + self.logger.error(f"{e}. {retries_remaining}") + time.sleep(self.retry_delay) + continue # If Bybit returns an error, raise. - if s_json[ret_code]: + if s_json[RET_CODE]: # Generate error message. - error_msg = f"{s_json[ret_msg]} (ErrCode: {s_json[ret_code]})" + error_msg = f"{s_json[RET_MSG]} (ErrCode: {s_json[RET_CODE]})" # Set default retry delay. delay_time = self.retry_delay # Retry non-fatal whitelisted error requests. - if s_json[ret_code] in self.retry_codes: + if s_json[RET_CODE] in self.retry_codes: # 10002, recv_window error; add 2.5 seconds and retry. - if s_json[ret_code] == 10002: + if s_json[RET_CODE] == 10002: error_msg += ". Added 2.5 seconds to recv_window" recv_window += 2500 # 10006, rate limit error; wait until # X-Bapi-Limit-Reset-Timestamp and retry. - elif s_json[ret_code] == 10006: + elif s_json[RET_CODE] == 10006: self.logger.error( f"{error_msg}. Hit the API rate limit. " f"Sleeping, then trying again. Request: {path}" ) - # Calculate how long we need to wait in milliseconds. - limit_reset_time = int(s.headers["X-Bapi-Limit-Reset-Timestamp"]) - limit_reset_str = dt.fromtimestamp(limit_reset_time / 10**3).strftime( - "%H:%M:%S.%f")[:-3] - delay_time = (int(limit_reset_time) - _helpers.generate_timestamp()) / 10**3 + delay_time, limit_reset_str = _http_helpers.calculate_rate_limit_delay_time(int(s.headers["X-Bapi-Limit-Reset-Timestamp"])) error_msg = ( f"API rate limit will reset at {limit_reset_str}. " f"Sleeping for {int(delay_time * 10**3)} milliseconds" @@ -372,14 +281,14 @@ def _submit_request(self, method=None, path=None, query=None, auth=False): time.sleep(delay_time) continue - elif s_json[ret_code] in self.ignore_codes: + elif s_json[RET_CODE] in self.ignore_codes: pass else: raise InvalidRequestError( request=f"{method} {path}: {req_params}", - message=s_json[ret_msg], - status_code=s_json[ret_code], + message=s_json[RET_MSG], + status_code=s_json[RET_CODE], time=dt.utcnow().strftime("%H:%M:%S"), resp_headers=s.headers, ) diff --git a/pybit/_http_manager/_response_handler.py b/pybit/_http_manager/_response_handler.py new file mode 100644 index 0000000..589365d --- /dev/null +++ b/pybit/_http_manager/_response_handler.py @@ -0,0 +1,55 @@ +import logging +from json import JSONDecodeError +from dataclasses import dataclass, field + +from datetime import datetime as dt + +from pybit.exceptions import FailedRequestError +from pybit._http_manager._http_helpers import set_logger_handler + + +class ForceRetryException(Exception): ... + + +@dataclass +class ResponseHandler: + logging_level: logging = field(default=logging.INFO) + force_retry: bool = field(default=False) + + def __post_init__(self): + self.logger = logging.getLogger(__name__) + set_logger_handler(self.logger, self.logging_level) + + self.logger.debug("Initializing HTTP session.") + + def _check_status_code(self, response, method, path, req_params): + if response.status_code != 200: + if response.status_code == 403: + error_msg = "You have breached the IP rate limit or your IP is from the USA." + else: + error_msg = "HTTP status code is not 200." + self.logger.debug(f"Response text: {response.text}") + raise FailedRequestError( + request=f"{method} {path}: {req_params}", + message=error_msg, + status_code=response.status_code, + time=dt.utcnow().strftime("%H:%M:%S"), + resp_headers=response.headers, + ) + + def _convert_to_dict(self, response, method, path, req_params): + try: + return response.json() + # If we have trouble converting, handle the error and retry. + except JSONDecodeError as e: + if self.force_retry: + raise ForceRetryException(str(e)) + else: + self.logger.debug(f"Response text: {response.text}") + raise FailedRequestError( + request=f"{method} {path}: {req_params}", + message="Conflict. Could not decode JSON.", + status_code=409, + time=dt.utcnow().strftime("%H:%M:%S"), + resp_headers=response.headers, + ) diff --git a/pybit/_websocket_stream.py b/pybit/_websocket_stream.py index ff9714d..940e5a8 100644 --- a/pybit/_websocket_stream.py +++ b/pybit/_websocket_stream.py @@ -2,11 +2,11 @@ import threading import time import json -from ._http_manager import generate_signature +from pybit._http_manager._auth import generate_signature import logging import copy from uuid import uuid4 -from . import _helpers +from pybit import _helpers logger = logging.getLogger(__name__) diff --git a/pybit/_websocket_trading.py b/pybit/_websocket_trading.py index f7523ee..887ef2d 100644 --- a/pybit/_websocket_trading.py +++ b/pybit/_websocket_trading.py @@ -1,9 +1,8 @@ -from dataclasses import dataclass, field import json import uuid import logging -from ._websocket_stream import _WebSocketManager -from . import _helpers +from pybit._websocket_stream import _WebSocketManager +from pybit import _helpers logger = logging.getLogger(__name__) diff --git a/pybit/asyncio/__init__.py b/pybit/asyncio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pybit/asyncio/_http_manager.py b/pybit/asyncio/_http_manager.py new file mode 100644 index 0000000..f0ec744 --- /dev/null +++ b/pybit/asyncio/_http_manager.py @@ -0,0 +1,173 @@ +import asyncio +from dataclasses import dataclass +from datetime import datetime as dt +from typing import ( + Optional, + Union +) + +import httpx +from pybit._http_manager._http_manager import ( + _V5HTTPManager, + RET_MSG, + RET_CODE +) +from pybit._http_manager._response_handler import ForceRetryException +from pybit._http_manager import _http_helpers +from pybit.exceptions import FailedRequestError, InvalidRequestError + + +@dataclass +class _AsyncV5HTTPManager(_V5HTTPManager): + @staticmethod + def _init_request_client() -> httpx.AsyncClient: + return httpx.AsyncClient() + + async def close(self): + await self.client.aclose() + + def _prepare_request(self, + recv_window: int, + method: str = None, + path: str = None, + query: dict = None, + auth: bool = False): + req_params: Union[dict, str] = self.prepare_payload(method, query) + + # Authenticate if we are using a private endpoint. + headers: dict = self._prepare_auth_headers(recv_window, req_params) if auth else {} + + if method == "GET": + path = path + f"?{req_params}" if req_params else path + data = None + else: + data = req_params + + return self.client.build_request(method, path, data=data, headers=headers) + + async def _submit_request(self, + method: Optional[str] = None, + path: Optional[str] = None, + query: Optional[dict] = None, + auth: bool = False): + """ + Submits the request to the API. + + Notes + ------------------- + We use the params argument for the GET method, and data argument for + the POST method. Dicts passed to the data argument must be + JSONified prior to submitting request. + """ + + query = {} if query is None else query + + # Store original recv_window. + recv_window = self.recv_window + + # Bug fix: change floating whole numbers to integers to prevent + # auth signature errors. + self._change_floating_numbers_for_auth_signature(query) + + # Send request and return headers with body. Retry if failed. + retries_attempted = self.max_retries + req_params = None + + while True: + retries_attempted -= 1 + if retries_attempted < 0: + raise FailedRequestError( + request=f"{method} {path}: {req_params}", + message="Bad Request. Retries exceeded maximum.", + status_code=400, + time=dt.utcnow().strftime("%H:%M:%S"), + resp_headers=None, + ) + + retries_remaining = f"{retries_attempted} retries remain." + + req: httpx.Request = self._prepare_request(recv_window, method, path, query, auth) + + # Log the request. + self._log_request(req_params, method, path, req.headers) + + try: + # TODO make timeout + response = await self.client.send(req, ) + except ( + httpx._exceptions.ReadTimeout, + # TODO fill with network exceptions + ) as e: + if self.force_retry: + self.logger.error(f"{e}. {retries_remaining}") + await asyncio.sleep(self.retry_delay) + continue + else: + raise e + + # Convert response to dictionary, or raise if requests error. + try: + res_json = self._convert_to_dict(response, method, path, req_params) + except ForceRetryException as e: + self.logger.error(f"{e}. {retries_remaining}") + await asyncio.sleep(self.retry_delay) + continue + + # If Bybit returns an error, raise. + if res_json[RET_CODE]: + # Generate error message. + error_msg = f"{res_json[RET_MSG]} (ErrCode: {res_json[RET_CODE]})" + + # Set default retry delay. + delay_time = self.retry_delay + + # Retry non-fatal whitelisted error requests. + if res_json[RET_CODE] in self.retry_codes: + # 10002, recv_window error; add 2.5 seconds and retry. + if res_json[RET_CODE] == 10002: + error_msg += ". Added 2.5 seconds to recv_window" + recv_window += 2500 + + # 10006, rate limit error; wait until + # X-Bapi-Limit-Reset-Timestamp and retry. + elif res_json[RET_CODE] == 10006: + self.logger.error( + f"{error_msg}. Hit the API rate limit. " + f"Sleeping, then trying again. Request: {path}" + ) + + delay_time, limit_reset_str = _http_helpers.calculate_rate_limit_delay_time(int(response.headers["X-Bapi-Limit-Reset-Timestamp"])) + error_msg = ( + f"API rate limit will reset at {limit_reset_str}. " + f"Sleeping for {int(delay_time * 10**3)} milliseconds" + ) + + # Log the error. + self.logger.error(f"{error_msg}. {retries_remaining}") + await asyncio.sleep(delay_time) + continue + + elif res_json[RET_CODE] in self.ignore_codes: + pass + + else: + raise InvalidRequestError( + request=f"{method} {path}: {req_params}", + message=res_json[RET_MSG], + status_code=res_json[RET_CODE], + time=dt.utcnow().strftime("%H:%M:%S"), + resp_headers=response.headers, + ) + else: + if self.log_requests: + self.logger.debug( + f"Response headers: {response.headers}" + ) + + if self.return_response_headers: + # TODO elapsed + return res_json, response.elapsed, response.headers, + elif self.record_request_time: + return res_json, response.elapsed + else: + return res_json diff --git a/pybit/asyncio/v5_api/__init__.py b/pybit/asyncio/v5_api/__init__.py new file mode 100644 index 0000000..9ea20ef --- /dev/null +++ b/pybit/asyncio/v5_api/__init__.py @@ -0,0 +1,12 @@ +from ._v5_market import AsyncMarketHTTP +from ._v5_misc import AsyncMiscHTTP +from ._v5_trade import AsyncTradeHTTP +from ._v5_account import AsyncAccountHTTP +from ._v5_asset import AsyncAssetHTTP +from ._v5_position import AsyncPositionHTTP +from ._v5_pre_upgrade import AsyncPreUpgradeHTTP +from ._v5_spot_leverage_token import AsyncSpotLeverageHTTP +from ._v5_spot_margin_trade import AsyncSpotMarginTradeHTTP +from ._v5_user import AsyncUserHTTP +from ._v5_broker import AsyncBrokerHTTP +from ._v5_institutional_loan import AsyncInstitutionalLoanHTTP diff --git a/pybit/asyncio/v5_api/_v5_account.py b/pybit/asyncio/v5_api/_v5_account.py new file mode 100644 index 0000000..5ef3414 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_account.py @@ -0,0 +1,297 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.account import Account + + +class AsyncAccountHTTP(_AsyncV5HTTPManager): + async def get_wallet_balance(self, **kwargs): + """Obtain wallet balance, query asset information of each currency, and account risk rate information under unified margin mode. + By default, currency information with assets or liabilities of 0 is not returned. + + Required args: + accountType (string): Account type + Unified account: UNIFIED + Normal account: CONTRACT + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/wallet-balance + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_WALLET_BALANCE}", + query=kwargs, + auth=True, + ) + + async def upgrade_to_unified_trading_account(self, **kwargs): + """Upgrade Unified Account + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.UPGRADE_TO_UNIFIED_ACCOUNT}", + query=kwargs, + auth=True, + ) + + async def get_borrow_history(self, **kwargs): + """Get interest records, sorted in reverse order of creation time. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/borrow-history + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_BORROW_HISTORY}", + query=kwargs, + auth=True, + ) + + async def repay_liability(self, **kwargs): + """You can manually repay the liabilities of the Unified account + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/repay-liability + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.REPAY_LIABILITY}", + query=kwargs, + auth=True, + ) + + async def get_collateral_info(self, **kwargs): + """Get the collateral information of the current unified margin account, including loan interest rate, loanable amount, collateral conversion rate, whether it can be mortgaged as margin, etc. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/collateral-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_COLLATERAL_INFO}", + query=kwargs, + auth=True, + ) + + async def set_collateral_coin(self, **kwargs): + """You can decide whether the assets in the Unified account needs to be collateral coins. + + Required args: + coin (string): Coin name + collateralSwitch (string): ON: switch on collateral, OFF: switch off collateral + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/set-collateral + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.SET_COLLATERAL_COIN}", + query=kwargs, + auth=True, + ) + + async def batch_set_collateral_coin(self, **kwargs): + """You can decide whether the assets in the Unified account needs to be collateral coins. + + Required args: + request (array): Object + > coin (string): Coin name + > collateralSwitch (string): ON: switch on collateral, OFF: switch off collateral + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/batch-set-collateral + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.BATCH_SET_COLLATERAL_COIN}", + query=kwargs, + auth=True, + ) + + async def get_coin_greeks(self, **kwargs): + """Get current account Greeks information + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/coin-greeks + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_COIN_GREEKS}", + query=kwargs, + auth=True, + ) + + async def get_fee_rates(self, **kwargs): + """Get the trading fee rate of derivatives. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/fee-rate + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_FEE_RATE}", + query=kwargs, + auth=True, + ) + + async def get_account_info(self, **kwargs): + """Query the margin mode configuration of the account. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/account-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_ACCOUNT_INFO}", + query=kwargs, + auth=True, + ) + + async def get_transaction_log(self, **kwargs): + """Query transaction logs in Unified account. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/transaction-log + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_TRANSACTION_LOG}", + query=kwargs, + auth=True, + ) + + async def get_contract_transaction_log(self, **kwargs): + """Query transaction logs in Classic account. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/contract-transaction-log + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_CONTRACT_TRANSACTION_LOG}", + query=kwargs, + auth=True, + ) + + async def set_margin_mode(self, **kwargs): + """Default is regular margin mode. This mode is valid for USDT Perp, USDC Perp and USDC Option. + + Required args: + setMarginMode (string): REGULAR_MARGIN, PORTFOLIO_MARGIN + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/set-margin-mode + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.SET_MARGIN_MODE}", + query=kwargs, + auth=True, + ) + + async def set_mmp(self, **kwargs): + """ + Market Maker Protection (MMP) is an automated mechanism designed to protect market makers (MM) against liquidity risks + and over-exposure in the market. It prevents simultaneous trade executions on quotes provided by the MM within a short time span. + The MM can automatically pull their quotes if the number of contracts traded for an underlying asset exceeds the configured + threshold within a certain time frame. Once MMP is triggered, any pre-existing MMP orders will be automatically canceled, + and new orders tagged as MMP will be rejected for a specific duration — known as the frozen period — so that MM can + reassess the market and modify the quotes. + + Required args: + baseCoin (strin): Base coin + window (string): Time window (ms) + frozenPeriod (string): Frozen period (ms). "0" means the trade will remain frozen until manually reset + qtyLimit (string): Trade qty limit (positive and up to 2 decimal places) + deltaLimit (string): Delta limit (positive and up to 2 decimal places) + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/set-mmp + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.SET_MMP}", + query=kwargs, + auth=True, + ) + + async def reset_mmp(self, **kwargs): + """Once the mmp triggered, you can unfreeze the account by this endpoint + + Required args: + baseCoin (string): Base coin + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/reset-mmp + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Account.RESET_MMP}", + query=kwargs, + auth=True, + ) + + async def get_mmp_state(self, **kwargs): + """Get MMP state + + Required args: + baseCoin (string): Base coin + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/get-mmp-state + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_MMP_STATE}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_asset.py b/pybit/asyncio/v5_api/_v5_asset.py new file mode 100644 index 0000000..373c140 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_asset.py @@ -0,0 +1,472 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.asset import Asset + + +class AsyncAssetHTTP(_AsyncV5HTTPManager): + async def get_coin_exchange_records(self, **kwargs): + """Query the coin exchange records. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/exchange + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_COIN_EXCHANGE_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_option_delivery_record(self, **kwargs): + """Query option delivery records, sorted by deliveryTime in descending order + + Required args: + category (string): Product type. option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/option-delivery + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_OPTION_DELIVERY_RECORD}", + query=kwargs, + auth=True, + ) + + async def get_usdc_contract_settlement(self, **kwargs): + """Query session settlement records of USDC perpetual and futures + + Required args: + category (string): Product type. linear + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/settlement + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_USDC_CONTRACT_SETTLEMENT}", + query=kwargs, + auth=True, + ) + + async def get_spot_asset_info(self, **kwargs): + """Query asset information + + Required args: + accountType (string): Account type. SPOT + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/asset-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_SPOT_ASSET_INFO}", + query=kwargs, + auth=True, + ) + + async def get_coins_balance(self, **kwargs): + """You could get all coin balance of all account types under the master account, and sub account. + + Required args: + memberId (string): User Id. It is required when you use master api key to check sub account coin balance + accountType (string): Account type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/all-balance + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_ALL_COINS_BALANCE}", + query=kwargs, + auth=True, + ) + + async def get_coin_balance(self, **kwargs): + """Query the balance of a specific coin in a specific account type. Supports querying sub UID's balance. + + Required args: + memberId (string): UID. Required when querying sub UID balance + accountType (string): Account type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/account-coin-balance + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_SINGLE_COIN_BALANCE}", + query=kwargs, + auth=True, + ) + + async def get_transferable_coin(self, **kwargs): + """Query the transferable coin list between each account type + + Required args: + fromAccountType (string): From account type + toAccountType (string): To account type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/transferable-coin + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_TRANSFERABLE_COIN}", + query=kwargs, + auth=True, + ) + + async def create_internal_transfer(self, **kwargs): + """Create the internal transfer between different account types under the same UID. + + Required args: + transferId (string): UUID. Please manually generate a UUID + coin (string): Coin + amount (string): Amount + fromAccountType (string): From account type + toAccountType (string): To account type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/create-inter-transfer + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.CREATE_INTERNAL_TRANSFER}", + query=kwargs, + auth=True, + ) + + async def get_internal_transfer_records(self, **kwargs): + """Query the internal transfer records between different account types under the same UID. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/inter-transfer-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_INTERNAL_TRANSFER_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_sub_uid(self, **kwargs): + """Query the sub UIDs under a main UID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/sub-uid-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_SUB_UID}", + query=kwargs, + auth=True, + ) + + async def enable_universal_transfer_for_sub_uid(self, **kwargs): + """Transfer between sub-sub or main-sub + + Required args: + subMemberIds (array): This list has a single item. Separate multiple UIDs by comma, e.g., "uid1,uid2,uid3" + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/enable-unitransfer-subuid + """ + self.logger.warning("enable_universal_transfer_for_sub_uid() is depreciated. You no longer need to configure transferable sub UIDs. Now, all sub UIDs are automatically enabled for universal transfer.") + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.ENABLE_UT_FOR_SUB_UID}", + query=kwargs, + auth=True, + ) + + async def create_universal_transfer(self, **kwargs): + """Transfer between sub-sub or main-sub. Please make sure you have enabled universal transfer on your sub UID in advance. + + Required args: + transferId (string): UUID. Please manually generate a UUID + coin (string): Coin + amount (string): Amount + fromMemberId (integer): From UID + toMemberId (integer): To UID + fromAccountType (string): From account type + toAccountType (string): To account type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/unitransfer + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.CREATE_UNIVERSAL_TRANSFER}", + query=kwargs, + auth=True, + ) + + async def get_universal_transfer_records(self, **kwargs): + """Query universal transfer records + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/unitransfer-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_UNIVERSAL_TRANSFER_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_allowed_deposit_coin_info(self, **kwargs): + """Query allowed deposit coin information. To find out paired chain of coin, please refer coin info api. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/deposit-coin-spec + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_ALLOWED_DEPOSIT_COIN_INFO}", + query=kwargs, + auth=True, + ) + + async def set_deposit_account(self, **kwargs): + """Set auto transfer account after deposit. The same function as the setting for Deposit on web GUI + + Required args: + accountType (string): Account type: UNIFIED,SPOT,OPTION,CONTRACT,FUND + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/set-deposit-acct + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.SET_DEPOSIT_ACCOUNT}", + query=kwargs, + auth=True, + ) + + async def get_deposit_records(self, **kwargs): + """Query deposit records. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/deposit-record + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_DEPOSIT_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_sub_deposit_records(self, **kwargs): + """Query subaccount's deposit records by MAIN UID's API key. + + Required args: + subMemberId (string): Sub UID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/sub-deposit-record + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_SUB_ACCOUNT_DEPOSIT_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_internal_deposit_records(self, **kwargs): + """Query deposit records within the Bybit platform. These transactions are not on the blockchain. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/internal-deposit-record + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_INTERNAL_DEPOSIT_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_master_deposit_address(self, **kwargs): + """Query the deposit address information of MASTER account. + + Required args: + coin (string): Coin + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/master-deposit-addr + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_MASTER_DEPOSIT_ADDRESS}", + query=kwargs, + auth=True, + ) + + async def get_sub_deposit_address(self, **kwargs): + """Query the deposit address information of SUB account. + + Required args: + coin (string): Coin + chainType (string): Chain, e.g.,ETH + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/sub-deposit-addr + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_SUB_DEPOSIT_ADDRESS}", + query=kwargs, + auth=True, + ) + + async def get_coin_info(self, **kwargs): + """Query coin information, including chain information, withdraw and deposit status. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/coin-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_COIN_INFO}", + query=kwargs, + auth=True, + ) + + async def get_withdrawal_records(self, **kwargs): + """Query withdrawal records. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/withdraw-record + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_WITHDRAWAL_RECORDS}", + query=kwargs, + auth=True, + ) + + async def get_withdrawable_amount(self, **kwargs): + """Get withdrawable amount + + Required args: + coin (string): Coin name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/delay-amount + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_WITHDRAWABLE_AMOUNT}", + query=kwargs, + auth=True, + ) + + async def withdraw(self, **kwargs): + """Withdraw assets from your Bybit account. You can make an off-chain transfer if the target wallet address is from Bybit. This means that no blockchain fee will be charged. + + Required args: + coin (string): Coin + chain (string): Chain + address (string): Wallet address + tag (string): Tag. Required if tag exists in the wallet address list + amount (string): Withdraw amount + timestamp (integer): Current timestamp (ms). Used for preventing from withdraw replay + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/withdraw + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.WITHDRAW}", + query=kwargs, + auth=True, + ) + + async def cancel_withdrawal(self, **kwargs): + """Cancel the withdrawal + + Required args: + id (string): Withdrawal ID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/cancel-withdraw + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.CANCEL_WITHDRAWAL}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_broker.py b/pybit/asyncio/v5_api/_v5_broker.py new file mode 100644 index 0000000..62adda0 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_broker.py @@ -0,0 +1,19 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.broker import Broker + + +class AsyncBrokerHTTP(_AsyncV5HTTPManager): + async def get_broker_earnings(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/broker/earning + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Broker.GET_BROKER_EARNINGS}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_institutional_loan.py b/pybit/asyncio/v5_api/_v5_institutional_loan.py new file mode 100644 index 0000000..a26c0b2 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_institutional_loan.py @@ -0,0 +1,100 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.institutional_loan import InstitutionalLoan as InsLoan + + +class AsyncInstitutionalLoanHTTP(_AsyncV5HTTPManager): + async def get_product_info(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/margin-product-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{InsLoan.GET_PRODUCT_INFO}", + query=kwargs, + ) + + async def get_margin_coin_info(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/margin-coin-convert-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{InsLoan.GET_MARGIN_COIN_INFO}", + query=kwargs, + ) + + async def get_loan_orders(self, **kwargs) -> dict: + """ + Get loan orders information + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/loan-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{InsLoan.GET_LOAN_ORDERS}", + query=kwargs, + auth=True, + ) + + async def get_repayment_info(self, **kwargs) -> dict: + """ + Get a list of your loan repayment orders (orders which repaid the loan). + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/repay-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{InsLoan.GET_REPAYMENT_ORDERS}", + query=kwargs, + auth=True, + ) + + async def get_ltv(self, **kwargs) -> dict: + """ + Get your loan-to-value ratio. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/ltv-convert + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{InsLoan.GET_LTV}", + query=kwargs, + auth=True, + ) + + async def bind_or_unbind_uid(self, **kwargs) -> dict: + """ + For the institutional loan product, you can bind new UIDs to the risk + unit or unbind UID from the risk unit. + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/otc/bind-uid + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{InsLoan.BIND_OR_UNBIND_UID}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_market.py b/pybit/asyncio/v5_api/_v5_market.py new file mode 100644 index 0000000..8c1606c --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_market.py @@ -0,0 +1,299 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.market import Market + + +class AsyncMarketHTTP(_AsyncV5HTTPManager): + async def get_server_time(self) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/time + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_SERVER_TIME}", + ) + + async def get_kline(self, **kwargs) -> dict: + """Query the kline data. Charts are returned in groups based on the requested interval. + + Required args: + category (string): Product type: spot,linear,inverse + symbol (string): Symbol name + interval (string): Kline interval. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/kline + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_KLINE}", + query=kwargs, + ) + + async def get_mark_price_kline(self, **kwargs): + """Query the mark price kline data. Charts are returned in groups based on the requested interval. + + Required args: + category (string): Product type. linear,inverse + symbol (string): Symbol name + interval (string): Kline interval. 1,3,5,15,30,60,120,240,360,720,D,M,W + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/mark-kline + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_MARK_PRICE_KLINE}", + query=kwargs, + ) + + async def get_index_price_kline(self, **kwargs): + """Query the index price kline data. Charts are returned in groups based on the requested interval. + + Required args: + category (string): Product type. linear,inverse + symbol (string): Symbol name + interval (string): Kline interval. 1,3,5,15,30,60,120,240,360,720,D,M,W + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/index-kline + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_INDEX_PRICE_KLINE}", + query=kwargs, + ) + + async def get_premium_index_price_kline(self, **kwargs): + """Retrieve the premium index price kline data. Charts are returned in groups based on the requested interval. + + Required args: + category (string): Product type. linear + symbol (string): Symbol name + interval (string): Kline interval + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/preimum-index-kline + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_PREMIUM_INDEX_PRICE_KLINE}", + query=kwargs, + ) + + async def get_instruments_info(self, **kwargs): + """Query a list of instruments of online trading pair. + + Required args: + category (string): Product type. spot,linear,inverse,option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/instrument + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_INSTRUMENTS_INFO}", + query=kwargs, + ) + + async def get_orderbook(self, **kwargs): + """Query orderbook data + + Required args: + category (string): Product type. spot, linear, inverse, option + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/orderbook + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_ORDERBOOK}", + query=kwargs, + ) + + async def get_tickers(self, **kwargs): + """Query the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours. + + Required args: + category (string): Product type. spot,linear,inverse,option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/tickers + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_TICKERS}", + query=kwargs, + ) + + async def get_funding_rate_history(self, **kwargs): + """ + Query historical funding rate. Each symbol has a different funding interval. + For example, if the interval is 8 hours and the current time is UTC 12, then it returns the last funding rate, which settled at UTC 8. + To query the funding rate interval, please refer to instruments-info. + + Required args: + category (string): Product type. linear,inverse + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/history-fund-rate + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_FUNDING_RATE_HISTORY}", + query=kwargs, + ) + + async def get_public_trade_history(self, **kwargs): + """Query recent public trading data in Bybit. + + Required args: + category (string): Product type. spot,linear,inverse,option + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/recent-trade + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_PUBLIC_TRADING_HISTORY}", + query=kwargs, + ) + + async def get_open_interest(self, **kwargs): + """Get open interest of each symbol. + + Required args: + category (string): Product type. linear,inverse + symbol (string): Symbol name + intervalTime (string): Interval. 5min,15min,30min,1h,4h,1d + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/open-interest + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_OPEN_INTEREST}", + query=kwargs, + ) + + async def get_historical_volatility(self, **kwargs): + """Query option historical volatility + + Required args: + category (string): Product type. option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/iv + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_HISTORICAL_VOLATILITY}", + query=kwargs, + ) + + async def get_insurance(self, **kwargs): + """ + Query Bybit insurance pool data (BTC/USDT/USDC etc). + The data is updated every 24 hours. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/insurance + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_INSURANCE}", + query=kwargs, + ) + + async def get_risk_limit(self, **kwargs): + """Query risk limit of futures + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/risk-limit + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_RISK_LIMIT}", + query=kwargs, + ) + + async def get_option_delivery_price(self, **kwargs): + """Get the delivery price for option + + Required args: + category (string): Product type. option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/delivery-price + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_OPTION_DELIVERY_PRICE}", + query=kwargs, + ) + + async def get_long_short_ratio(self, **kwargs): + """ + Required args: + category (string): Product type. linear (USDT Perpetual only), inverse + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/market/long-short-ratio + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Market.GET_LONG_SHORT_RATIO}", + query=kwargs, + ) diff --git a/pybit/asyncio/v5_api/_v5_misc.py b/pybit/asyncio/v5_api/_v5_misc.py new file mode 100644 index 0000000..8437452 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_misc.py @@ -0,0 +1,40 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.misc import Misc + + +class AsyncMiscHTTP(_AsyncV5HTTPManager): + async def get_announcement(self, **kwargs) -> dict: + """ + Required args: + locale (string): Language symbol + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/announcement + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Misc.GET_ANNOUNCEMENT}", + query=kwargs, + ) + + async def request_demo_trading_funds(self) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/demo + """ + if not self.demo: + raise Exception( + "You must pass demo=True to the pybit HTTP session to use this " + "method." + ) + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Misc.REQUEST_DEMO_TRADING_FUNDS}", + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_position.py b/pybit/asyncio/v5_api/_v5_position.py new file mode 100644 index 0000000..38eb623 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_position.py @@ -0,0 +1,248 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.position import Position + + +class AsyncPositionHTTP(_AsyncV5HTTPManager): + async def get_positions(self, **kwargs): + """Query real-time position data, such as position size, cumulative realizedPNL. + + Required args: + category (string): Product type + Unified account: linear, option + Normal account: linear, inverse. + + Please note that category is not involved with business logic + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Position.GET_POSITIONS}", + query=kwargs, + auth=True, + ) + + async def set_leverage(self, **kwargs): + """Set the leverage + + Required args: + category (string): Product type + Unified account: linear + Normal account: linear, inverse. + + Please note that category is not involved with business logic + symbol (string): Symbol name + buyLeverage (string): [0, max leverage of corresponding risk limit]. + Note: Under one-way mode, buyLeverage must be the same as sellLeverage + sellLeverage (string): [0, max leverage of corresponding risk limit]. + Note: Under one-way mode, buyLeverage must be the same as sellLeverage + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/leverage + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_LEVERAGE}", + query=kwargs, + auth=True, + ) + + async def switch_margin_mode(self, **kwargs): + """Select cross margin mode or isolated margin mode + + Required args: + category (string): Product type. linear,inverse + + Please note that category is not involved with business logicUnified account is not applicable + symbol (string): Symbol name + tradeMode (integer): 0: cross margin. 1: isolated margin + buyLeverage (string): The value must be equal to sellLeverage value + sellLeverage (string): The value must be equal to buyLeverage value + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/cross-isolate + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SWITCH_MARGIN_MODE}", + query=kwargs, + auth=True, + ) + + async def set_tp_sl_mode(self, **kwargs): + """Set TP/SL mode to Full or Partial + + Required args: + category (string): Product type + Unified account: linear + Normal account: linear, inverse. + + Please note that category is not involved with business logic + symbol (string): Symbol name + tpSlMode (string): TP/SL mode. Full,Partial + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/tpsl-mode + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_TP_SL_MODE}", + query=kwargs, + auth=True, + ) + + async def switch_position_mode(self, **kwargs): + """ + It supports to switch the position mode for USDT perpetual and Inverse futures. + If you are in one-way Mode, you can only open one position on Buy or Sell side. + If you are in hedge mode, you can open both Buy and Sell side positions simultaneously. + + Required args: + category (string): Product type. linear,inverse + + Please note that category is not involved with business logicUnified account is not applicable + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/position-mode + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SWITCH_POSITION_MODE}", + query=kwargs, + auth=True, + ) + + async def set_risk_limit(self, **kwargs): + """ + The risk limit will limit the maximum position value you can hold under different margin requirements. + If you want to hold a bigger position size, you need more margin. This interface can set the risk limit of a single position. + If the order exceeds the current risk limit when placing an order, it will be rejected. Click here to learn more about risk limit. + + Required args: + category (string): Product type + Unified account: linear + Normal account: linear, inverse. + + Please note that category is not involved with business logic + symbol (string): Symbol name + riskId (integer): Risk limit ID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/set-risk-limit + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_RISK_LIMIT}", + query=kwargs, + auth=True, + ) + + async def set_trading_stop(self, **kwargs): + """Set the take profit, stop loss or trailing stop for the position. + + Required args: + category (string): Product type + Unified account: linear + Normal account: linear, inverse. + + Please note that category is not involved with business logic + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/trading-stop + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_TRADING_STOP}", + query=kwargs, + auth=True, + ) + + async def set_auto_add_margin(self, **kwargs): + """Turn on/off auto-add-margin for isolated margin position + + Required args: + category (string): Product type. linear + symbol (string): Symbol name + autoAddMargin (integer): Turn on/off. 0: off. 1: on + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/add-margin + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_AUTO_ADD_MARGIN}", + query=kwargs, + auth=True, + ) + + async def get_executions(self, **kwargs): + """Query users' execution records, sorted by execTime in descending order + + Required args: + category (string): + Product type Unified account: spot, linear, option + Normal account: linear, inverse. + + Please note that category is not involved with business logic + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/execution + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Position.GET_EXECUTIONS}", + query=kwargs, + auth=True, + ) + + async def get_closed_pnl(self, **kwargs): + """Query user's closed profit and loss records. The results are sorted by createdTime in descending order. + + Required args: + category (string): + Product type Unified account: linear + Normal account: linear, inverse. + + Please note that category is not involved with business logic + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/close-pnl + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Position.GET_CLOSED_PNL}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_pre_upgrade.py b/pybit/asyncio/v5_api/_v5_pre_upgrade.py new file mode 100644 index 0000000..e6b1c94 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_pre_upgrade.py @@ -0,0 +1,130 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.pre_upgrade import PreUpgrade + + +class AsyncPreUpgradeHTTP(_AsyncV5HTTPManager): + async def get_pre_upgrade_order_history(self, **kwargs) -> dict: + """ + After the account is upgraded to a Unified account, you can get the + orders which occurred before the upgrade. + + Required args: + category (string): Product type. linear, inverse, option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/order-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_ORDER_HISTORY}", + query=kwargs, + auth=True, + ) + + async def get_pre_upgrade_trade_history(self, **kwargs) -> dict: + """ + Get users' execution records which occurred before you upgraded the + account to a Unified account, sorted by execTime in descending order + + Required args: + category (string): Product type. linear, inverse, option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/execution + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_TRADE_HISTORY}", + query=kwargs, + auth=True, + ) + + async def get_pre_upgrade_closed_pnl(self, **kwargs) -> dict: + """ + Query user's closed profit and loss records from before you upgraded the + account to a Unified account. The results are sorted by createdTime in + descending order. + + Required args: + category (string): Product type linear, inverse + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/close-pnl + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_CLOSED_PNL}", + query=kwargs, + auth=True, + ) + + async def get_pre_upgrade_transaction_log(self, **kwargs) -> dict: + """ + Query transaction logs which occurred in the USDC Derivatives wallet + before the account was upgraded to a Unified account. + + Required args: + category (string): Product type. linear,option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/transaction-log + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_TRANSACTION_LOG}", + query=kwargs, + auth=True, + ) + + async def get_pre_upgrade_option_delivery_record(self, **kwargs) -> dict: + """ + Query delivery records of Option before you upgraded the account to a + Unified account, sorted by deliveryTime in descending order + Required args: + category (string): Product type. option + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/delivery + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_OPTION_DELIVERY_RECORD}", + query=kwargs, + auth=True, + ) + + async def get_pre_upgrade_usdc_session_settlement(self, **kwargs) -> dict: + """ + Required args: + category (string): Product type. linear + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/pre-upgrade/settlement + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{PreUpgrade.GET_PRE_UPGRADE_USDC_SESSION_SETTLEMENT}", + query=kwargs, + auth=True, + ) + + diff --git a/pybit/asyncio/v5_api/_v5_spot_leverage_token.py b/pybit/asyncio/v5_api/_v5_spot_leverage_token.py new file mode 100644 index 0000000..1d39db8 --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_spot_leverage_token.py @@ -0,0 +1,95 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.spot_leverage_token import SpotLeverageToken + + +class AsyncSpotLeverageHTTP(_AsyncV5HTTPManager): + async def get_leveraged_token_info(self, **kwargs): + """Query leverage token information + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/lt/leverage-token-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotLeverageToken.GET_LEVERAGED_TOKEN_INFO}", + query=kwargs, + ) + + async def get_leveraged_token_market(self, **kwargs): + """Get leverage token market information + + Required args: + ltCoin (string): Abbreviation of the LT, such as BTC3L + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/lt/leverage-token-reference + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotLeverageToken.GET_LEVERAGED_TOKEN_MARKET}", + query=kwargs, + ) + + async def purchase_leveraged_token(self, **kwargs): + """Purchase levearge token + + Required args: + ltCoin (string): Abbreviation of the LT, such as BTC3L + ltAmount (string): Purchase amount + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/lt/purchase + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotLeverageToken.PURCHASE}", + query=kwargs, + auth=True, + ) + + async def redeem_leveraged_token(self, **kwargs): + """Redeem leverage token + + Required args: + ltCoin (string): Abbreviation of the LT, such as BTC3L + quantity (string): Redeem quantity of LT + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/lt/redeem + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotLeverageToken.REDEEM}", + query=kwargs, + auth=True, + ) + + async def get_purchase_redemption_records(self, **kwargs): + """Get purchase or redeem history + + Required args: + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/lt/order-record + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotLeverageToken.GET_PURCHASE_REDEMPTION_RECORDS}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_spot_margin_trade.py b/pybit/asyncio/v5_api/_v5_spot_margin_trade.py new file mode 100644 index 0000000..fb1a5bc --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_spot_margin_trade.py @@ -0,0 +1,242 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.spot_margin_trade import SpotMarginTrade + + +class AsyncSpotMarginTradeHTTP(_AsyncV5HTTPManager): + async def spot_margin_trade_get_vip_margin_data(self, **kwargs): + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/vip-margin + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.VIP_MARGIN_DATA}", + query=kwargs, + ) + + async def spot_margin_trade_toggle_margin_trade(self, **kwargs): + """UTA only. Turn spot margin trade on / off. + + Required args: + spotMarginMode (string): 1: on, 0: off + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/switch-mode + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotMarginTrade.TOGGLE_MARGIN_TRADE}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_set_leverage(self, **kwargs): + """UTA only. Set the user's maximum leverage in spot cross margin + + Required args: + leverage (string): Leverage. [2, 5]. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/set-leverage + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotMarginTrade.SET_LEVERAGE}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_get_status_and_leverage(self): + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/status + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.STATUS_AND_LEVERAGE}", + auth=True, + ) + + async def spot_margin_trade_normal_get_vip_margin_data(self, **kwargs): + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/vip-margin + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_MARGIN_COIN_INFO}", + query=kwargs, + ) + + async def spot_margin_trade_normal_get_margin_coin_info(self, **kwargs): + """Normal (non-UTA) account only. Turn on / off spot margin trade + + Required args: + switch (string): 1: on, 0: off + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/margin-data + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_MARGIN_COIN_INFO}", + query=kwargs, + ) + + async def spot_margin_trade_normal_get_borrowable_coin_info(self, **kwargs): + """Normal (non-UTA) account only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/borrowable-data + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_BORROWABLE_COIN_INFO}", + query=kwargs, + ) + + async def spot_margin_trade_normal_get_interest_quota(self, **kwargs): + """Normal (non-UTA) account only. + + Required args: + coin (string): Coin name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/interest-quota + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_INTEREST_QUOTA}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_get_loan_account_info(self, **kwargs): + """Normal (non-UTA) account only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/account-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_LOAN_ACCOUNT_INFO}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_borrow(self, **kwargs): + """Normal (non-UTA) account only. + + Required args: + coin (string): Coin name + qty (string): Amount to borrow + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/borrow + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_BORROW}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_repay(self, **kwargs): + """Normal (non-UTA) account only. + + Required args: + coin (string): Coin name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/repay + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_REPAY}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_get_borrow_order_detail(self, **kwargs): + """Normal (non-UTA) account only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/borrow-order + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_BORROW_ORDER_DETAIL}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_get_repayment_order_detail(self, **kwargs): + """Normal (non-UTA) account only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/repay-order + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_GET_REPAYMENT_ORDER_DETAIL}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_toggle_margin_trade(self, **kwargs): + """Normal (non-UTA) account only. + + Required args: + switch (integer): 1: on, 0: off + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/switch-mode + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{SpotMarginTrade.NORMAL_TOGGLE_MARGIN_TRADE}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_trade.py b/pybit/asyncio/v5_api/_v5_trade.py new file mode 100644 index 0000000..ed6938b --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_trade.py @@ -0,0 +1,244 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.trade import Trade + + +class AsyncTradeHTTP(_AsyncV5HTTPManager): + async def place_order(self, **kwargs): + """This method supports to create the order for spot, spot margin, linear perpetual, inverse futures and options. + + Required args: + category (string): Product type Unified account: spot, linear, optionNormal account: linear, inverse. Please note that category is not involved with business logic + symbol (string): Symbol name + side (string): Buy, Sell + orderType (string): Market, Limit + qty (string): Order quantity + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/create-order + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.PLACE_ORDER}", + query=kwargs, + auth=True, + ) + + async def amend_order(self, **kwargs): + """Unified account covers: Linear contract / Options + Normal account covers: USDT perpetual / Inverse perpetual / Inverse futures + + Required args: + category (string): Product type Unified account: spot, linear, optionNormal account: linear, inverse. Please note that category is not involved with business logic + symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/amend-order + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.AMEND_ORDER}", + query=kwargs, + auth=True, + ) + + async def cancel_order(self, **kwargs): + """Unified account covers: Spot / Linear contract / Options + Normal account covers: USDT perpetual / Inverse perpetual / Inverse futures + + Required args: + category (string): Product type Unified account: spot, linear, optionNormal account: linear, inverse. Please note that category is not involved with business logic + symbol (string): Symbol name + orderId (string): Order ID. Either orderId or orderLinkId is required + orderLinkId (string): User customised order ID. Either orderId or orderLinkId is required + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/cancel-order + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.CANCEL_ORDER}", + query=kwargs, + auth=True, + ) + + async def get_open_orders(self, **kwargs): + """Query unfilled or partially filled orders in real-time. To query older order records, please use the order history interface. + + Required args: + category (string): Product type Unified account: spot, linear, optionNormal account: linear, inverse. Please note that category is not involved with business logic + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/open-order + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Trade.GET_OPEN_ORDERS}", + query=kwargs, + auth=True, + ) + + async def cancel_all_orders(self, **kwargs): + """Cancel all open orders + + Required args: + category (string): Product type + Unified account: spot, linear, option + Normal account: linear, inverse. + + Please note that category is not involved with business logic. If cancel all by baseCoin, it will cancel all linear & inverse orders + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/cancel-all + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.CANCEL_ALL_ORDERS}", + query=kwargs, + auth=True, + ) + + async def get_order_history(self, **kwargs): + """Query order history. As order creation/cancellation is asynchronous, the data returned from this endpoint may delay. + If you want to get real-time order information, you could query this endpoint or rely on the websocket stream (recommended). + + Required args: + category (string): Product type + Unified account: spot, linear, option + Normal account: linear, inverse. + + Please note that category is not involved with business logic + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/order-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Trade.GET_ORDER_HISTORY}", + query=kwargs, + auth=True, + ) + + async def place_batch_order(self, **kwargs): + """Covers: Option (Unified Account) + + Required args: + category (string): Product type. option + request (array): Object + > symbol (string): Symbol name + > side (string): Buy, Sell + > orderType (string): Market, Limit + > qty (string): Order quantity + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/batch-place + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.BATCH_PLACE_ORDER}", + query=kwargs, + auth=True, + ) + + async def amend_batch_order(self, **kwargs): + """Covers: Option (Unified Account) + + Required args: + category (string): Product type. option + request (array): Object + > symbol (string): Symbol name + > orderId (string): Order ID. Either orderId or orderLinkId is required + > orderLinkId (string): User customised order ID. Either orderId or orderLinkId is required + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/batch-amend + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.BATCH_AMEND_ORDER}", + query=kwargs, + auth=True, + ) + + async def cancel_batch_order(self, **kwargs): + """This endpoint allows you to cancel more than one open order in a single request. + + Required args: + category (string): Product type. option + request (array): Object + > symbol (string): Symbol name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/batch-cancel + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.BATCH_CANCEL_ORDER}", + query=kwargs, + auth=True, + ) + + def get_borrow_quota(self, **kwargs): + """Query the qty and amount of borrowable coins in spot account. + + Required args: + category (string): Product type. spot + symbol (string): Symbol name + side (string): Transaction side. Buy,Sell + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/spot-borrow-quota + """ + return self._submit_request( + method="GET", + path=f"{self.endpoint}{Trade.GET_BORROW_QUOTA}", + query=kwargs, + auth=True, + ) + + async def set_dcp(self, **kwargs): + """Covers: Option (Unified Account) + + Required args: + timeWindow (integer): Disconnection timing window time. [10, 300], unit: second + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/order/dcp + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Trade.SET_DCP}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/v5_api/_v5_user.py b/pybit/asyncio/v5_api/_v5_user.py new file mode 100644 index 0000000..8defaee --- /dev/null +++ b/pybit/asyncio/v5_api/_v5_user.py @@ -0,0 +1,196 @@ +from pybit.asyncio._http_manager import _AsyncV5HTTPManager +from pybit.user import User + + +class AsyncUserHTTP(_AsyncV5HTTPManager): + async def create_sub_uid(self, **kwargs): + """Create a new sub user id. Use master user's api key only. + + Required args: + username (string): Give a username of the new sub user id. 6-16 characters, must include both numbers and letters.cannot be the same as the exist or deleted one. + memberType (integer): 1: normal sub account, 6: custodial sub account + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/create-subuid + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.CREATE_SUB_UID}", + query=kwargs, + auth=True, + ) + + async def create_sub_api_key(self, **kwargs): + """To create new API key for those newly created sub UID. Use master user's api key only. + + Required args: + subuid (integer): Sub user Id + readOnly (integer): 0: Read and Write. 1: Read only + permissions (Object): Tick the types of permission. one of below types must be passed, otherwise the error is thrown + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/create-subuid-apikey + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.CREATE_SUB_API_KEY}", + query=kwargs, + auth=True, + ) + + async def get_sub_uid_list(self, **kwargs): + """Get all sub uid of master account. Use master user's api key only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/subuid-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{User.GET_SUB_UID_LIST}", + query=kwargs, + auth=True, + ) + + async def freeze_sub_uid(self, **kwargs): + """Froze sub uid. Use master user's api key only. + + Required args: + subuid (integer): Sub user Id + frozen (integer): 0: unfreeze, 1: freeze + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/froze-subuid + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.FREEZE_SUB_UID}", + query=kwargs, + auth=True, + ) + + async def get_api_key_information(self, **kwargs): + """Get the information of the api key. Use the api key pending to be checked to call the endpoint. Both master and sub user's api key are applicable. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/apikey-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{User.GET_API_KEY_INFORMATION}", + query=kwargs, + auth=True, + ) + + async def modify_master_api_key(self, **kwargs): + """Modify the settings of master api key. Use the api key pending to be modified to call the endpoint. Use master user's api key only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.MODIFY_MASTER_API_KEY}", + query=kwargs, + auth=True, + ) + + async def modify_sub_api_key(self, **kwargs): + """Modify the settings of sub api key. Use the api key pending to be modified to call the endpoint. Use sub user's api key only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.MODIFY_SUB_API_KEY}", + query=kwargs, + auth=True, + ) + + async def delete_master_api_key(self, **kwargs): + """Delete the api key of master account. Use the api key pending to be delete to call the endpoint. Use master user's api key only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/rm-master-apikey + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.DELETE_MASTER_API_KEY}", + query=kwargs, + auth=True, + ) + + async def delete_sub_api_key(self, **kwargs): + """Delete the api key of sub account. Use the api key pending to be delete to call the endpoint. Use sub user's api key only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/rm-sub-apikey + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.DELETE_SUB_API_KEY}", + query=kwargs, + auth=True, + ) + + async def get_affiliate_user_info(self, **kwargs): + """This API is used for affiliate to get their users information + + Required args: + uid (integer): The master account uid of affiliate's client + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/affiliate-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{User.GET_AFFILIATE_USER_INFO}", + query=kwargs, + auth=True, + ) + + async def get_uid_wallet_type(self, **kwargs): + """Get available wallet types for the master account or sub account + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/wallet-type + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{User.GET_UID_WALLET_TYPE}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/websocket/__init__.py b/pybit/asyncio/websocket/__init__.py new file mode 100644 index 0000000..4d3930c --- /dev/null +++ b/pybit/asyncio/websocket/__init__.py @@ -0,0 +1,2 @@ +from .async_websocket import AsyncWebsocket +from .enums import WSState diff --git a/pybit/asyncio/websocket/_utils.py b/pybit/asyncio/websocket/_utils.py new file mode 100644 index 0000000..ae7cd7a --- /dev/null +++ b/pybit/asyncio/websocket/_utils.py @@ -0,0 +1,17 @@ +import asyncio + + +def get_loop(): + """check if there is an event loop in the current thread, if not create one + inspired by https://stackoverflow.com/questions/46727787/runtimeerror-there-is-no-current-event-loop-in-thread-in-async-apscheduler + """ + try: + loop = asyncio.get_event_loop() + return loop + except RuntimeError as e: + if str(e).startswith("There is no current event loop in thread"): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + else: + raise diff --git a/pybit/asyncio/websocket/async_manager.py b/pybit/asyncio/websocket/async_manager.py new file mode 100644 index 0000000..f6554db --- /dev/null +++ b/pybit/asyncio/websocket/async_manager.py @@ -0,0 +1,225 @@ +import asyncio +import json +import traceback +import logging +from typing import Optional +from random import random + +from pybit import _helpers +from pybit._http_manager._auth import generate_signature +from pybit.exceptions import InvalidWebsocketSubscription +from pybit._websocket_stream import ( + SUBDOMAIN_MAINNET, + SUBDOMAIN_TESTNET, + DOMAIN_MAIN +) +from pybit.asyncio.websocket._utils import get_loop +from pybit.asyncio.websocket.enums import WSState +import websockets as ws +from websockets.exceptions import ConnectionClosedError + + +logger = logging.getLogger(__name__) + + +PING_INTERVAL = 20 +PINT_TIMEOUT = 10 +MESSAGE_TIMEOUT = 5 +PRIVATE_AUTH_EXPIRE = 1 + + +class AsyncWebsocketManager: + """ + Implementation of async API for Bybit + """ + + def __init__(self, + channel_type: str, + url: str, + testnet: bool, + subscription_message: str, + api_key: Optional[str] = None, + api_secret: Optional[str] = None): + self.channel_type = channel_type + self.testnet = testnet + self.api_key = api_key + self.api_secret = api_secret + self.url = url + self.subscription_message = subscription_message + self.queue = asyncio.Queue() + self._loop = get_loop() + self._handle_read_loop = None + + self.ws_state = WSState.INITIALISING + self.custom_ping_message = json.dumps({"op": "ping"}) + self.ws = None + self._conn = None + self._keepalive = None + self.MAX_RECONNECTS = 5 + self._reconnects = 0 + self.MAX_RECONNECT_SECONDS = 300 + self.MAX_QUEUE_SIZE = 10000 + + async def __aenter__(self) -> "AsyncWebsocketManager": + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + self.ws_state = WSState.EXITING + if self.ws: + self.ws.fail_connection() + if self._conn and hasattr(self._conn, 'protocol'): + await self._conn.__aexit__(exc_type, exc_val, exc_tb) + + def _handle_message(self, res: str) -> dict: + return json.loads(res) + + async def _read_loop(self): + logger.info(f"Start loop") + + while True: + try: + if self.ws_state == WSState.STREAMING: + res = await asyncio.wait_for(self.ws.recv(), timeout=MESSAGE_TIMEOUT) + res = self._handle_message(res) + # Check if message is pong + if res.get("op") == "pong": + continue + if res.get("op") == "subscribe": + if res.get("success", True) is False: + logger.error(f"Subscription error: {res}") + raise InvalidWebsocketSubscription(response=res) + + if res: + await self.queue.put(res) + elif self.ws_state == WSState.EXITING: + logger.info("_read_loop. Exit websocket") + await self.ws.close() + self._keepalive.cancel() + await asyncio.sleep(0.1) + await self.__aexit__(None, None, None) + break + elif self.ws_state == WSState.RECONNECTING: + while self.ws_state == WSState.RECONNECTING: + await self._reconnect() + + except asyncio.TimeoutError: + continue + except ConnectionResetError as e: + logger.warning(f"Received connection reset by peer. Error: {e}. Trying to reconnect.") + await self._reconnect() + except asyncio.CancelledError: + logger.warning("Cancelled error") + self._keepalive.cancel() + break + except OSError as e: + traceback.print_exc() + await self._reconnect() + continue + except ConnectionClosedError as e: + logger.warning("Connection closed") + await self._reconnect() + continue + except Exception as e: + traceback.print_exc() + logger.warning(f"Unknown exception: {e}.") + self.ws_state = WSState.EXITING + continue + + async def _auth(self): + """ + Prepares authentication signature per Bybit API specifications. + """ + + expires = _helpers.generate_timestamp() + (PRIVATE_AUTH_EXPIRE * 1000) + param_str = f"GET/realtime{expires}" + signature = generate_signature(use_rsa_authentication=False, secret=self.api_secret, param_str=param_str) + # Authenticate with API. + await self.ws.send(json.dumps({"op": "auth", "args": [self.api_key, expires, signature]})) + + async def _keepalive_task(self): + try: + while True: + await asyncio.sleep(PING_INTERVAL) + + await self.ws.send(self.custom_ping_message) + + except asyncio.CancelledError: + return + except ConnectionClosedError: + return + except Exception: + logger.error(f"Keepalive failed") + traceback.print_exc() + + async def _reconnect(self): + if self.ws_state == WSState.EXITING: + logger.warning(f"Websocket was closed.") + await self.queue.put({ + "success": False, + "ret_msg": "Max reconnect reached" + }) + return + + self.ws_state = WSState.RECONNECTING + if self._reconnects < self.MAX_RECONNECTS: + reconnect_wait = self._get_reconnect_wait(self._reconnects) + logger.info(f"{self.MAX_RECONNECTS - self._reconnects} left") + await asyncio.sleep(reconnect_wait) + self._reconnects += 1 + await self.connect() + logger.info(f"Reconnected. Key: ") + else: + logger.error("Max reconnections reached") + await self.queue.put({ + "success": False, + "ret_msg": "Max reconnect reached" + }) + self.ws_state = WSState.EXITING + + def _get_reconnect_wait(self, attempts: int) -> int: + expo = 2 ** attempts + return round(random() * min(self.MAX_RECONNECT_SECONDS, expo - 1) + 1) + + async def connect(self): + subdomain = SUBDOMAIN_TESTNET if self.testnet else SUBDOMAIN_MAINNET + endpoint = self.url.format(SUBDOMAIN=subdomain, DOMAIN=DOMAIN_MAIN) + self._conn = ws.connect(endpoint, + close_timeout=0.1, + ping_timeout=None, # We use custom ping task + ping_interval=None) + try: + self.ws = await self._conn.__aenter__() + except Exception as e: + await self._reconnect() + traceback.print_exc() + return + # Authenticate for private channels + if self.api_key and self.api_secret: + await self._auth() + + await self.ws.send(self.subscription_message) + self._reconnects = 0 + self.ws_state = WSState.STREAMING + if not self._handle_read_loop: + self._keepalive = asyncio.create_task(self._keepalive_task()) + logger.info(f"Connected successfully") + self._handle_read_loop = self._loop.call_soon_threadsafe(asyncio.create_task, self._read_loop()) + + async def close_connection(self): + self.ws_state = WSState.EXITING + + async def recv(self): + res = None + while not res: + if self.ws_state == WSState.EXITING: + break + try: + res = await asyncio.wait_for(self.queue.get(), timeout=MESSAGE_TIMEOUT) + if not res.get("data"): + # Only responses with "data" key contains useful information + # Other payloads contains system info + continue + except asyncio.TimeoutError: + continue + return res diff --git a/pybit/asyncio/websocket/async_websocket.py b/pybit/asyncio/websocket/async_websocket.py new file mode 100644 index 0000000..742f1ad --- /dev/null +++ b/pybit/asyncio/websocket/async_websocket.py @@ -0,0 +1,132 @@ +import json +from uuid import uuid4 +from typing import ( + Optional, + List +) + +from pybit import exceptions +from pybit.unified_trading import ( + PUBLIC_WSS, + PRIVATE_WSS, + AVAILABLE_CHANNEL_TYPES +) +from pybit.asyncio.websocket.async_manager import AsyncWebsocketManager + + +class AsyncWebsocket: + """ + Prepare payload for websocket connection + """ + + def __init__(self, + testnet: bool, + api_key: Optional[str] = None, + api_secret: Optional[str] = None): + self.api_key = api_key + self.api_secret = api_secret + self.testnet = testnet + + @staticmethod + def _check_channel_type(channel_type: str): + if channel_type not in AVAILABLE_CHANNEL_TYPES: + raise exceptions.InvalidChannelTypeError( + f"Channel type is not correct. Available: {AVAILABLE_CHANNEL_TYPES}") + + def _check_api_key_for_private_channel(self, channel_type: str = "private"): + if (self.api_key is None or self.api_secret is None) and channel_type == "private": + raise exceptions.UnauthorizedExceptionError( + "API_KEY or API_SECRET is not set. They both are needed in order to access private topics" + ) + + def _prepare_public_subscription(self, symbols: List[str]) -> str: + return json.dumps( + {"op": "subscribe", + "req_id": str(uuid4()), # Optional but can help handle multiple subscriptions + "args": symbols + } + ) + + def _prepare_private_trade_subscription(self, topic: str, category: str) -> str: + return json.dumps( + {"op": "subscribe", + "args": [f"{topic}.{category}"]} + ) + + def _get_public_async_manager(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + self._check_channel_type(channel_type) + return AsyncWebsocketManager(channel_type=channel_type, + testnet=self.testnet, + url=PUBLIC_WSS.replace("{CHANNEL_TYPE}", channel_type), + subscription_message=self._prepare_public_subscription(symbols)) + + def _get_private_async_manager(self, topic: str, category: str) -> AsyncWebsocketManager: + self._check_api_key_for_private_channel() + return AsyncWebsocketManager(channel_type="private", + testnet=self.testnet, + url=PRIVATE_WSS, + subscription_message=self._prepare_private_trade_subscription(topic, category), + api_key=self.api_key, + api_secret=self.api_secret) + + def orderbook_stream(self, symbols, channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def trade_stream(self, symbols, channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def kline_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def ticker_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def liquidation_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def lt_kline_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def lt_ticker_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def li_nav_stream(self, symbols: List[str], channel_type: str) -> AsyncWebsocketManager: + return self._get_public_async_manager(symbols, channel_type) + + def order_stream(self, category: str) -> AsyncWebsocketManager: + return self._get_private_async_manager("order", category) + + def position_stream(self, category: str) -> AsyncWebsocketManager: + return self._get_private_async_manager("position", category) + + def execution_stream(self, category: str) -> AsyncWebsocketManager: + return self._get_private_async_manager("execution", category) + + def execution_fast_stream(self, category: str) -> AsyncWebsocketManager: + return self._get_private_async_manager("execution.fast", category) + + def wallet_stream(self) -> AsyncWebsocketManager: + self._check_api_key_for_private_channel() + subscription_message = json.dumps( + {"op": "subscribe", + "args": ["wallet"]} + ) + return AsyncWebsocketManager(channel_type="private", + testnet=self.testnet, + url=PRIVATE_WSS, + subscription_message=subscription_message, + api_key=self.api_key, + api_secret=self.api_secret) + + def greeks_stream(self, ) -> AsyncWebsocketManager: + self._check_api_key_for_private_channel() + subscription_message = json.dumps( + {"op": "subscribe", + "args": ["greeks"]} + ) + return AsyncWebsocketManager(channel_type="private", + testnet=self.testnet, + url=PRIVATE_WSS, + subscription_message=subscription_message, + api_key=self.api_key, + api_secret=self.api_secret) diff --git a/pybit/asyncio/websocket/enums.py b/pybit/asyncio/websocket/enums.py new file mode 100644 index 0000000..b380be5 --- /dev/null +++ b/pybit/asyncio/websocket/enums.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class WSState(Enum): + INITIALISING = "Initialising" + STREAMING = "Streaming" + RECONNECTING = "Reconnecting" + EXITING = "Exiting" + FAILED = "Failed" diff --git a/pybit/exceptions.py b/pybit/exceptions.py index 67aaa02..a34a6e6 100644 --- a/pybit/exceptions.py +++ b/pybit/exceptions.py @@ -56,3 +56,8 @@ def __init__(self, request, message, status_code, time, resp_headers): f"{message} (ErrCode: {status_code}) (ErrTime: {time})" f".\nRequest → {request}." ) + + +class InvalidWebsocketSubscription(Exception): + def __init__(self, response): + self.response = response diff --git a/pybit/unified_trading.py b/pybit/unified_trading.py index 38eca70..be7b51d 100644 --- a/pybit/unified_trading.py +++ b/pybit/unified_trading.py @@ -4,20 +4,36 @@ TopicMismatchError, UnauthorizedExceptionError, ) -from ._v5_misc import MiscHTTP -from ._v5_market import MarketHTTP -from ._v5_trade import TradeHTTP -from ._v5_account import AccountHTTP -from ._v5_asset import AssetHTTP -from ._v5_position import PositionHTTP -from ._v5_pre_upgrade import PreUpgradeHTTP -from ._v5_spot_leverage_token import SpotLeverageHTTP -from ._v5_spot_margin_trade import SpotMarginTradeHTTP -from ._v5_user import UserHTTP -from ._v5_broker import BrokerHTTP -from ._v5_institutional_loan import InstitutionalLoanHTTP -from ._websocket_stream import _V5WebSocketManager -from ._websocket_trading import _V5TradeWebSocketManager +from pybit.v5_api import ( + MiscHTTP, + MarketHTTP, + TradeHTTP, + AccountHTTP, + AssetHTTP, + PositionHTTP, + PreUpgradeHTTP, + SpotLeverageHTTP, + SpotMarginTradeHTTP, + UserHTTP, + BrokerHTTP, + InstitutionalLoanHTTP +) +from pybit.asyncio.v5_api import ( + AsyncMarketHTTP, + AsyncMiscHTTP, + AsyncTradeHTTP, + AsyncAccountHTTP, + AsyncAssetHTTP, + AsyncPositionHTTP, + AsyncPreUpgradeHTTP, + AsyncSpotLeverageHTTP, + AsyncSpotMarginTradeHTTP, + AsyncUserHTTP, + AsyncBrokerHTTP, + AsyncInstitutionalLoanHTTP +) +from pybit._websocket_stream import _V5WebSocketManager +from pybit._websocket_trading import _V5TradeWebSocketManager WSS_NAME = "Unified V5" @@ -51,6 +67,25 @@ def __init__(self, **args): super().__init__(**args) +@dataclass +class AsyncHTTP( + AsyncMarketHTTP, + AsyncMiscHTTP, + AsyncTradeHTTP, + AsyncAccountHTTP, + AsyncAssetHTTP, + AsyncPositionHTTP, + AsyncPreUpgradeHTTP, + AsyncSpotLeverageHTTP, + AsyncSpotMarginTradeHTTP, + AsyncUserHTTP, + AsyncBrokerHTTP, + AsyncInstitutionalLoanHTTP +): + def __init__(self, **args): + super().__init__(**args) + + class WebSocket(_V5WebSocketManager): def _validate_public_topic(self): if "/v5/public" not in self.WS_URL: diff --git a/pybit/v5_api/__init__.py b/pybit/v5_api/__init__.py new file mode 100644 index 0000000..0bc5a52 --- /dev/null +++ b/pybit/v5_api/__init__.py @@ -0,0 +1,12 @@ +from ._v5_account import AccountHTTP +from ._v5_asset import AssetHTTP +from ._v5_broker import BrokerHTTP +from ._v5_institutional_loan import InstitutionalLoanHTTP +from ._v5_market import MarketHTTP +from ._v5_misc import MiscHTTP +from ._v5_position import PositionHTTP +from ._v5_pre_upgrade import PreUpgradeHTTP +from ._v5_spot_leverage_token import SpotLeverageHTTP +from ._v5_spot_margin_trade import SpotMarginTradeHTTP +from ._v5_trade import TradeHTTP +from ._v5_user import UserHTTP diff --git a/pybit/_v5_account.py b/pybit/v5_api/_v5_account.py similarity index 99% rename from pybit/_v5_account.py rename to pybit/v5_api/_v5_account.py index 8ba7f2a..f4eb78b 100644 --- a/pybit/_v5_account.py +++ b/pybit/v5_api/_v5_account.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .account import Account +from pybit._http_manager import _V5HTTPManager +from pybit.account import Account class AccountHTTP(_V5HTTPManager): diff --git a/pybit/_v5_asset.py b/pybit/v5_api/_v5_asset.py similarity index 99% rename from pybit/_v5_asset.py rename to pybit/v5_api/_v5_asset.py index 755a439..945aa58 100644 --- a/pybit/_v5_asset.py +++ b/pybit/v5_api/_v5_asset.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .asset import Asset +from pybit._http_manager import _V5HTTPManager +from pybit.asset import Asset class AssetHTTP(_V5HTTPManager): diff --git a/pybit/_v5_broker.py b/pybit/v5_api/_v5_broker.py similarity index 85% rename from pybit/_v5_broker.py rename to pybit/v5_api/_v5_broker.py index a97dbe6..cf61ad5 100644 --- a/pybit/_v5_broker.py +++ b/pybit/v5_api/_v5_broker.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .broker import Broker +from pybit._http_manager import _V5HTTPManager +from pybit.broker import Broker class BrokerHTTP(_V5HTTPManager): diff --git a/pybit/_v5_institutional_loan.py b/pybit/v5_api/_v5_institutional_loan.py similarity index 96% rename from pybit/_v5_institutional_loan.py rename to pybit/v5_api/_v5_institutional_loan.py index b0be380..0854106 100644 --- a/pybit/_v5_institutional_loan.py +++ b/pybit/v5_api/_v5_institutional_loan.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .institutional_loan import InstitutionalLoan as InsLoan +from pybit._http_manager import _V5HTTPManager +from pybit.institutional_loan import InstitutionalLoan as InsLoan class InstitutionalLoanHTTP(_V5HTTPManager): diff --git a/pybit/_v5_market.py b/pybit/v5_api/_v5_market.py similarity index 99% rename from pybit/_v5_market.py rename to pybit/v5_api/_v5_market.py index 5d79d7c..be28a27 100644 --- a/pybit/_v5_market.py +++ b/pybit/v5_api/_v5_market.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .market import Market +from pybit._http_manager import _V5HTTPManager +from pybit.market import Market class MarketHTTP(_V5HTTPManager): diff --git a/pybit/_v5_misc.py b/pybit/v5_api/_v5_misc.py similarity index 93% rename from pybit/_v5_misc.py rename to pybit/v5_api/_v5_misc.py index a7fc793..dbba8ce 100644 --- a/pybit/_v5_misc.py +++ b/pybit/v5_api/_v5_misc.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .misc import Misc +from pybit._http_manager import _V5HTTPManager +from pybit.misc import Misc class MiscHTTP(_V5HTTPManager): diff --git a/pybit/_v5_position.py b/pybit/v5_api/_v5_position.py similarity index 99% rename from pybit/_v5_position.py rename to pybit/v5_api/_v5_position.py index 2a40471..34f99ae 100644 --- a/pybit/_v5_position.py +++ b/pybit/v5_api/_v5_position.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .position import Position +from pybit._http_manager import _V5HTTPManager +from pybit.position import Position class PositionHTTP(_V5HTTPManager): diff --git a/pybit/_v5_pre_upgrade.py b/pybit/v5_api/_v5_pre_upgrade.py similarity index 97% rename from pybit/_v5_pre_upgrade.py rename to pybit/v5_api/_v5_pre_upgrade.py index 0489774..3fd9e25 100644 --- a/pybit/_v5_pre_upgrade.py +++ b/pybit/v5_api/_v5_pre_upgrade.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .pre_upgrade import PreUpgrade +from pybit._http_manager import _V5HTTPManager +from pybit.pre_upgrade import PreUpgrade class PreUpgradeHTTP(_V5HTTPManager): diff --git a/pybit/_v5_spot_leverage_token.py b/pybit/v5_api/_v5_spot_leverage_token.py similarity index 96% rename from pybit/_v5_spot_leverage_token.py rename to pybit/v5_api/_v5_spot_leverage_token.py index 76829e3..77fda04 100644 --- a/pybit/_v5_spot_leverage_token.py +++ b/pybit/v5_api/_v5_spot_leverage_token.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .spot_leverage_token import SpotLeverageToken +from pybit._http_manager import _V5HTTPManager +from pybit.spot_leverage_token import SpotLeverageToken class SpotLeverageHTTP(_V5HTTPManager): diff --git a/pybit/_v5_spot_margin_trade.py b/pybit/v5_api/_v5_spot_margin_trade.py similarity index 98% rename from pybit/_v5_spot_margin_trade.py rename to pybit/v5_api/_v5_spot_margin_trade.py index 6a6e858..387ae1a 100644 --- a/pybit/_v5_spot_margin_trade.py +++ b/pybit/v5_api/_v5_spot_margin_trade.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .spot_margin_trade import SpotMarginTrade +from pybit._http_manager import _V5HTTPManager +from pybit.spot_margin_trade import SpotMarginTrade class SpotMarginTradeHTTP(_V5HTTPManager): diff --git a/pybit/_v5_trade.py b/pybit/v5_api/_v5_trade.py similarity index 99% rename from pybit/_v5_trade.py rename to pybit/v5_api/_v5_trade.py index 35f6806..12b9f7c 100644 --- a/pybit/_v5_trade.py +++ b/pybit/v5_api/_v5_trade.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .trade import Trade +from pybit._http_manager import _V5HTTPManager +from pybit.trade import Trade class TradeHTTP(_V5HTTPManager): diff --git a/pybit/_v5_user.py b/pybit/v5_api/_v5_user.py similarity index 98% rename from pybit/_v5_user.py rename to pybit/v5_api/_v5_user.py index d8bed83..a65170a 100644 --- a/pybit/_v5_user.py +++ b/pybit/v5_api/_v5_user.py @@ -1,5 +1,5 @@ -from ._http_manager import _V5HTTPManager -from .user import User +from pybit._http_manager import _V5HTTPManager +from pybit.user import User class UserHTTP(_V5HTTPManager): diff --git a/requirements.txt b/requirements.txt index 0da22d3..645e57c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ requests>=2.22.0 websocket-client==1.5.0 -pycryptodome==3.20.0 \ No newline at end of file +pycryptodome==3.20.0 +httpx==0.27.0 +websockets==12.0