diff --git a/README.md b/README.md index f2706ea..6a608cc 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,23 @@ Check out the example python files or the list of endpoints below for more infor endpoints and methods. Usage examples on the `HTTP` methods can be found in the [examples folder](https://github.com/bybit-exchange/pybit/tree/master/examples). +## Async Usage +You can retrieve a specific market like so: +```python +from pybit.asyncio.unified_trading import AsyncHTTP +``` +Create an HTTP session: +```python +client = AsyncHTTP( + testnet=False, + api_key="...", + api_secret="...", +) +await client.init_client() +# Or use context-manager +async with AsyncHTTP(testnet=False, api_key="...", api_secret="...") as client: + await client.get_orderbook(category="linear", symbol="BTCUSDT") +``` ## Contact Reach out for support on your chosen platform: diff --git a/examples/async_http_quickstart.py b/examples/async_http_quickstart.py new file mode 100644 index 0000000..e74549b --- /dev/null +++ b/examples/async_http_quickstart.py @@ -0,0 +1,21 @@ +import asyncio + +from pybit.asyncio.unified_trading import AsyncHTTP + +API_KEY = "..." +API_SECRET = "..." + + +async def test(): + async with AsyncHTTP(api_key=API_KEY, api_secret=API_SECRET, testnet=True) as client: + print(await client.get_orderbook(category="linear", symbol="BTCUSDT")) + + print(await client.place_order( + category="linear", + symbol="BTCUSDT", + side="Buy", + orderType="Market", + qty="0.001", + )) + +asyncio.run(test()) diff --git a/examples/async_websocket_example_quickstart.py b/examples/async_websocket_example_quickstart.py new file mode 100644 index 0000000..758046e --- /dev/null +++ b/examples/async_websocket_example_quickstart.py @@ -0,0 +1,34 @@ +import asyncio + +from pybit.asyncio.ws import AsyncWebsocketClient + + +API_KEY = "..." +API_SECRET = "..." + + +async def test_public(): + client = AsyncWebsocketClient(testnet=True, channel_type="linear") + stream = client.futures_kline_stream(symbols=["kline.60.BTCUSDT", "kline.60.ETHUSDT", "kline.60.SOLUSDT"]) + async with stream as ws: + while True: + print(await ws.recv()) + + +asyncio.run(test_public()) + + +async def test_private(): + client = AsyncWebsocketClient( + testnet=True, + channel_type="private", + api_key=API_KEY, + api_secret=API_SECRET, + ) + stream = client.user_futures_stream() + async with stream as ws: + while True: + print(await ws.recv()) + + +asyncio.run(test_private()) diff --git a/pybit/asyncio/__init__.py b/pybit/asyncio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pybit/asyncio/builder.py b/pybit/asyncio/builder.py new file mode 100644 index 0000000..8c95815 --- /dev/null +++ b/pybit/asyncio/builder.py @@ -0,0 +1,46 @@ +from typing import Optional + +from pybit import _http_manager + + +class RequestBuilder: + def __init__( + self, + api_key: str, + api_secret: str, + rsa_authentication: bool, + proxy: Optional[str] = None + ): + self._http_manager = _http_manager._V5HTTPManager( + api_key=api_key, + api_secret=api_secret, + rsa_authentication=rsa_authentication + ) + self._proxy = proxy + + def clean_query(self, query: Optional[dict]) -> dict: + return self._http_manager._clean_query(query) + + def prepare_payload(self, method: str, parameters: dict) -> str: + if not parameters: + return "" + return self._http_manager.prepare_payload(method, parameters) + + def _auth(self, payload, recv_window, timestamp): + return self._http_manager._auth(payload, recv_window, timestamp) + + def prepare_headers(self, payload: str, recv_window: int) -> dict: + return self._http_manager._prepare_headers(payload, recv_window) + + def prepare_request(self, method:str, path: str, headers: dict, payload: str) -> dict: + request = { + "url": path, + "headers": headers, + } + if method == "GET": + request["url"] += f"?{payload}" + else: + request["data"] = payload + if self._proxy: + request["proxy"] = self._proxy + return request diff --git a/pybit/asyncio/client.py b/pybit/asyncio/client.py new file mode 100644 index 0000000..6d150cc --- /dev/null +++ b/pybit/asyncio/client.py @@ -0,0 +1,275 @@ +import asyncio +import logging +import time +from json import JSONDecodeError +from typing import Optional +from datetime import ( + datetime as dt, + timezone +) + +import aiohttp + +from pybit import _http_manager +from pybit import _helpers +from pybit.exceptions import ( + FailedRequestError, + InvalidRequestError, +) +from pybit.asyncio.utils import get_event_loop +from pybit.asyncio.builder import RequestBuilder + + +class AsyncClient: + def __init__( + self, + testnet: bool = False, + domain: str = _http_manager.DOMAIN_MAIN, + tld: str = _http_manager.TLD_MAIN, + demo: bool = False, + rsa_authentication: bool = False, + api_key: str = None, + api_secret: str = None, + logging_level: int = logging.INFO, + log_requests: bool = False, + timeout: int = 10, + recv_window: int = 5000, + force_retry: bool = False, + retry_codes: set = None, + ignore_codes: set = None, + max_retries: int = 3, + retry_delay: int = 3, + referral_id: str = None, + record_request_time: bool = False, + return_response_headers: bool = False, + loop: asyncio.AbstractEventLoop = None, + proxy: Optional[str] = None + ): + self.testnet = testnet + self.domain = domain + self.tld = tld + self.demo = demo + self.rsa_authentication = rsa_authentication + self.api_key = api_key + self.api_secret = api_secret + self.logging_level = logging_level + self.log_requests = log_requests + self.timeout = aiohttp.ClientTimeout(total=timeout) + self.recv_window = recv_window + self.force_retry = force_retry + self.retry_codes = retry_codes or {} + self.ignore_codes = ignore_codes or {10002, 10006, 30034, 30035, 130035, 130150} + self.max_retries = max_retries + self.retry_delay = retry_delay + self.referral_id = referral_id + self.record_request_time = record_request_time + self.return_response_headers = return_response_headers + self._loop = loop or get_event_loop() + self._proxy = proxy + + subdomain = _http_manager.SUBDOMAIN_TESTNET if self.testnet else _http_manager.SUBDOMAIN_MAINNET + domain = _http_manager.DOMAIN_MAIN if not self.domain else self.domain + if self.demo: + if self.testnet: + subdomain = _http_manager.DEMO_SUBDOMAIN_TESTNET + else: + subdomain = _http_manager.DEMO_SUBDOMAIN_MAINNET + url = _http_manager.HTTP_URL.format(SUBDOMAIN=subdomain, DOMAIN=domain, TLD=self.tld) + self.endpoint = url + 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) + + self.logger.debug("Initializing HTTP session.") + self._session = None + self._headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if self.referral_id: + self._headers['Referer'] = self.referral_id + + self._request_builder = RequestBuilder( + api_key=self.api_key, + api_secret=self.api_secret, + rsa_authentication=self.rsa_authentication, + proxy=self._proxy + ) + + async def __aenter__(self): + await self.init_client() + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.close_connection() + + async def init_client(self): + self._session = aiohttp.ClientSession( + headers=self._headers, + loop=self._loop, + timeout=self.timeout + ) + + async def close_connection(self): + await self._session.close() + + async def _submit_request(self, method: str, path: str, auth: bool = False, query: Optional[dict] = None): + query = self._request_builder.clean_query(query) + recv_window = self.recv_window + retries_attempted = self.max_retries + req_params = None + + while retries_attempted > 0: + retries_attempted -= 1 + try: + req_params = self._request_builder.prepare_payload(method, query) + headers = self._request_builder.prepare_headers(req_params, recv_window) if auth else {} + _request = self._request_builder.prepare_request(method, path, headers, req_params) + self._log_request(method, path, req_params, headers) + + start_time = time.perf_counter() + async with getattr(self._session, method.lower())(**_request) as response: + end_time = time.perf_counter() + await self._check_status_code(response, method, path, req_params) + return await self._handle_response( + response, method, path, req_params, recv_window, end_time - start_time + ) + + except ( + aiohttp.ClientSSLError, + aiohttp.ClientResponseError, + aiohttp.ClientConnectionError, + asyncio.exceptions.TimeoutError + ) as e: + await self._handle_network_error(e, retries_attempted) + except (JSONDecodeError, aiohttp.ContentTypeError) as e: + await self._handle_json_error(e, retries_attempted) + + raise FailedRequestError( + request=f"{method} {path}: {req_params or query}", + message="Bad Request. Retries exceeded maximum.", + status_code=400, + time=dt.now(timezone.utc).strftime("%H:%M:%S"), + resp_headers=None, + ) + + async def _check_status_code( + self, + response: aiohttp.ClientResponse, + method: str, + path: str, + params: str + ): + """Check HTTP status code.""" + if response.status != 200: + error_msg = "You have breached the IP rate limit or your IP is from the USA." \ + if response.status == 403 else "HTTP status code is not 200." + self.logger.debug(f"Response text: {await response.text()}") + raise FailedRequestError( + request=f"{method} {path}: {params}", + message=error_msg, + status_code=response.status, + time=dt.now(timezone.utc).strftime("%H:%M:%S"), + resp_headers=response.headers, + ) + + async def _handle_response( + self, + response: aiohttp.ClientResponse, + method: str, + path: str, + params: str, + recv_window: int, + response_time: float + ): + try: + s_json = await response.json() + except (JSONDecodeError, aiohttp.ContentTypeError) as e: + raise e # Will be caught by main loop to retry. + + ret_code = "retCode" + ret_msg = "retMsg" + + if s_json.get(ret_code): + error_code = s_json[ret_code] + error_msg = f"{s_json[ret_msg]} (ErrCode: {error_code})" + + if error_code in self.retry_codes: + await self._handle_retryable_error(response, error_code, error_msg, recv_window) + raise Exception("Retryable error occurred, retrying...") + + if error_code not in self.ignore_codes: + raise InvalidRequestError( + request=f"{method} {path}: {params}", + message=s_json[ret_msg], + status_code=error_code, + time=dt.now(timezone.utc).strftime("%H:%M:%S"), + resp_headers=response.headers, + ) + + if self.log_requests: + self.logger.debug(f"Response headers: {response.headers}") + + if self.return_response_headers: + return s_json, response_time, response.headers + elif self.record_request_time: + return s_json, response_time + else: + return s_json + + def _log_request(self, method, path, params, headers): + """Log request.""" + if self.log_requests: + if params: + self.logger.debug(f"Request -> {method} {path}. Body: {params}. Headers: {headers}") + else: + self.logger.debug(f"Request -> {method} {path}. Headers: {headers}") + + async def _handle_retryable_error(self, response, error_code, error_msg, recv_window): + """Handle specific retryable Bybit errors.""" + delay_time = self.retry_delay + + if error_code == 10002: # recv_window error + error_msg += ". Added 2.5 seconds to recv_window" + recv_window += 2500 + elif error_code == 10006: # rate limit error + self.logger.error(f"{error_msg}. Hit the API rate limit on {response.url}. Sleeping then trying again.") + limit_reset_time = int(response.headers["X-Bapi-Limit-Reset-Timestamp"]) + limit_reset_str = dt.fromtimestamp(limit_reset_time / 10 ** 3).strftime("%H:%M:%S.%f")[:-3] + delay_time = (limit_reset_time - _helpers.generate_timestamp()) / 10 ** 3 + error_msg = f"API rate limit will reset at {limit_reset_str}. Sleeping for {int(delay_time * 10 ** 3)} ms" + + self.logger.error(f"{error_msg}. Retrying...") + await asyncio.sleep(delay_time) + + async def _handle_network_error(self, error, retries_attempted): + """Handle network-related exceptions.""" + if self.force_retry and retries_attempted > 0: + self.logger.error(f"{error}. Retrying...") + await asyncio.sleep(self.retry_delay) + else: + raise error + + async def _handle_json_error(self, error, retries_attempted): + """Handle JSON decoding errors.""" + if self.force_retry and retries_attempted > 0: + self.logger.error(f"{error}. Retrying JSON decode...") + await asyncio.sleep(self.retry_delay) + else: + raise FailedRequestError( + request="JSON decoding", + message="Conflict. Could not decode JSON.", + status_code=409, + time=dt.now(timezone.utc).strftime("%H:%M:%S"), + resp_headers=None, + ) diff --git a/pybit/asyncio/http/__init__.py b/pybit/asyncio/http/__init__.py new file mode 100644 index 0000000..26de99d --- /dev/null +++ b/pybit/asyncio/http/__init__.py @@ -0,0 +1,15 @@ +from .v5_misc import AsyncMiscHTTP +from .v5_market import AsyncMarketHTTP +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_load import AsyncInstitutionalLoanHTTP +from .v5_crypto_loan import AsyncCryptoLoanHTTP +from .v5_earn import AsyncEarnHTTP +from .v5_rate_limit import AsyncRateLimitHTTP diff --git a/pybit/asyncio/http/v5_account.py b/pybit/asyncio/http/v5_account.py new file mode 100644 index 0000000..58d8f9a --- /dev/null +++ b/pybit/asyncio/http/v5_account.py @@ -0,0 +1,316 @@ +from pybit.asyncio.client import AsyncClient +from pybit.account import Account + + +class AsyncAccountHTTP(AsyncClient): + async def get_wallet_balance(self, **kwargs) -> dict: + """Obtain wallet balance, query asset information of each currency, and account risk rate information under unified margin mode. + By async 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 get_transferable_amount(self, **kwargs) -> dict: + """Query the available amount to transfer of a specific coin in the Unified wallet. + + Required args: + coinName (string): Coin name, uppercase only + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/account/unified-trans-amnt + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Account.GET_TRANSFERABLE_AMOUNT}", + query=kwargs, + auth=True, + ) + + async def upgrade_to_unified_trading_account(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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/http/v5_asset.py b/pybit/asyncio/http/v5_asset.py new file mode 100644 index 0000000..75fc5a0 --- /dev/null +++ b/pybit/asyncio/http/v5_asset.py @@ -0,0 +1,596 @@ +from pybit.asyncio.client import AsyncClient +from pybit.asset import Asset + + +class AsyncAssetHTTP(AsyncClient): + async def get_coin_exchange_records(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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_address_list(self, **kwargs) -> dict: + """Query withdrawal address list. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/withdraw/withdraw-address + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_WITHDRAWAL_ADDRESS_LIST}", + query=kwargs, + auth=True, + ) + + async def get_withdrawal_records(self, **kwargs) -> dict: + """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) -> dict: + """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 get_exchange_entity_list(self, **kwargs) -> dict: + """Get the list of exchange entities (VASPs) supported for withdrawals. + + Returns: + Request results as dictionary. + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/withdraw/vasp-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_EXCHANGE_ENTITY_LIST}", + query=kwargs, + auth=True, + ) + + async def withdraw(self, **kwargs) -> dict: + """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) -> dict: + """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, + ) + + async def get_convert_coin_list(self, **kwargs) -> dict: + """Query for the list of coins you can convert to/from. + + Required args: + accountType (string): Wallet type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/convert/convert-coin-list + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_CONVERT_COIN_LIST}", + query=kwargs, + auth=True, + ) + + async def request_a_quote(self, **kwargs) -> dict: + """ + Required args: + fromCoin (string): Convert from coin (coin to sell) + toCoin (string): Convert to coin (coin to buy) + requestCoin (string): Request coin, same as fromCoin + requestAmount (string): request coin amount (the amount you want to sell) + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/convert/apply-quote + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.REQUEST_A_QUOTE}", + query=kwargs, + auth=True, + ) + + async def confirm_a_quote(self, **kwargs) -> dict: + """ + Required args: + quoteTxId (string): The quoteTxId from request_a_quote + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/convert/confirm-quote + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Asset.CONFIRM_A_QUOTE}", + query=kwargs, + auth=True, + ) + + async def get_convert_status(self, **kwargs) -> dict: + """ + Required args: + quoteTxId (string): Quote tx ID + accountType (string): Wallet type + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-result + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_CONVERT_STATUS}", + query=kwargs, + auth=True, + ) + + async def get_convert_history(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-history + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Asset.GET_CONVERT_HISTORY}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/http/v5_broker.py b/pybit/asyncio/http/v5_broker.py new file mode 100644 index 0000000..ca7f432 --- /dev/null +++ b/pybit/asyncio/http/v5_broker.py @@ -0,0 +1,38 @@ +from pybit.asyncio.client import AsyncClient +from pybit.broker import Broker + + +class AsyncBrokerHTTP(AsyncClient): + async def get_broker_earnings(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/broker/earning + """ + self.logger.warning( + "get_broker_earnings() is deprecated. See get_exchange_broker_earnings().") + + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Broker.GET_BROKER_EARNINGS}", + query=kwargs, + auth=True, + ) + + async def get_exchange_broker_earnings(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/broker/exchange-earning + """ + + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Broker.GET_EXCHANGE_BROKER_EARNINGS}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/http/v5_crypto_loan.py b/pybit/asyncio/http/v5_crypto_loan.py new file mode 100644 index 0000000..559a902 --- /dev/null +++ b/pybit/asyncio/http/v5_crypto_loan.py @@ -0,0 +1,197 @@ +from pybit.asyncio.client import AsyncClient +from pybit.crypto_loan import CryptoLoan + + +class AsyncCryptoLoanHTTP(AsyncClient): + async def get_collateral_coins(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/collateral-coin + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_COLLATERAL_COINS}", + query=kwargs, + ) + + async def get_borrowable_coins(self, **kwargs) -> dict: + """ + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/loan-coin + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_BORROWABLE_COINS}", + query=kwargs, + ) + + async def get_account_borrowable_or_collateralizable_limit(self, **kwargs) -> dict: + """ + Query for the minimum and maximum amounts your account can borrow and + how much collateral you can put up. + + Required args: + loanCurrency (string): Loan coin name + collateralCurrency (string): Collateral coin name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/acct-borrow-collateral + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_ACCOUNT_BORROWABLE_OR_COLLATERALIZABLE_LIMIT}", + query=kwargs, + auth=True, + ) + + async def borrow_crypto_loan(self, **kwargs) -> dict: + """ + Required args: + loanCurrency (string): Loan coin name + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/borrow + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{CryptoLoan.BORROW_CRYPTO_LOAN}", + query=kwargs, + auth=True, + ) + + async def repay_crypto_loan(self, **kwargs) -> dict: + """ + Required args: + orderId (string): Loan order ID + amount (string): Repay amount + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/repay + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{CryptoLoan.REPAY_CRYPTO_LOAN}", + query=kwargs, + auth=True, + ) + + async def get_unpaid_loans(self, **kwargs) -> dict: + """ + Query for your ongoing loans. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/repay + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_UNPAID_LOANS}", + query=kwargs, + auth=True, + ) + + async def get_loan_repayment_history(self, **kwargs) -> dict: + """ + Query for loan repayment transactions. A loan may be repaid in multiple + repayments. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/repay-transaction + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_LOAN_REPAYMENT_HISTORY}", + query=kwargs, + auth=True, + ) + + async def get_completed_loan_history(self, **kwargs) -> dict: + """ + Query for the last 6 months worth of your completed (fully paid off) + loans. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/completed-loan-order + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_COMPLETED_LOAN_ORDER_HISTORY}", + query=kwargs, + auth=True, + ) + + async def get_max_allowed_collateral_reduction_amount(self, **kwargs) -> dict: + """ + Query for the maximum amount by which collateral may be reduced by. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/reduce-max-collateral-amt + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_MAX_ALLOWED_COLLATERAL_REDUCTION_AMOUNT}", + query=kwargs, + auth=True, + ) + + async def adjust_collateral_amount(self, **kwargs) -> dict: + """ + You can increase or reduce your collateral amount. When you reduce, + please obey the max. allowed reduction amount. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/adjust-collateral + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.ADJUST_COLLATERAL_AMOUNT}", + query=kwargs, + auth=True, + ) + + async def get_crypto_loan_ltv_adjustment_history(self, **kwargs) -> dict: + """ + You can increase or reduce your collateral amount. When you reduce, + please obey the max. allowed reduction amount. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/crypto-loan/ltv-adjust-history + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{CryptoLoan.GET_CRYPTO_LOAN_LTV_ADJUSTMENT_HISTORY}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/http/v5_earn.py b/pybit/asyncio/http/v5_earn.py new file mode 100644 index 0000000..c717819 --- /dev/null +++ b/pybit/asyncio/http/v5_earn.py @@ -0,0 +1,77 @@ +from pybit.asyncio.client import AsyncClient +from pybit.earn import Earn + + +class AsyncEarnHTTP(AsyncClient): + async def get_earn_product_info(self, **kwargs) -> dict: + """ + Required args: + category (string): FlexibleSaving + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/earn/product-info + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Earn.GET_EARN_PRODUCT_INFO}", + query=kwargs, + ) + + async def stake_or_redeem(self, **kwargs) -> dict: + """ + Required args: + category (string): FlexibleSaving + orderType (string): Stake, Redeem + accountType (string): FUND, UNIFIED + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/earn/create-order + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Earn.STAKE_OR_REDEEM}", + query=kwargs, + auth=True, + ) + + async def get_stake_or_redemption_history(self, **kwargs) -> dict: + """ + Required args: + category (string): FlexibleSaving + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/earn/order-history + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Earn.GET_STAKE_OR_REDEMPTION_HISTORY}", + query=kwargs, + auth=True, + ) + + async def get_staked_position(self, **kwargs) -> dict: + """ + Required args: + category (string): FlexibleSaving + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/earn/position + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{Earn.GET_STAKED_POSITION}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/http/v5_institutional_load.py b/pybit/asyncio/http/v5_institutional_load.py new file mode 100644 index 0000000..f3b037e --- /dev/null +++ b/pybit/asyncio/http/v5_institutional_load.py @@ -0,0 +1,100 @@ +from pybit.asyncio.client import AsyncClient +from pybit.institutional_loan import InstitutionalLoan as InsLoan + + +class AsyncInstitutionalLoanHTTP(AsyncClient): + 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/http/v5_market.py b/pybit/asyncio/http/v5_market.py new file mode 100644 index 0000000..6d9d928 --- /dev/null +++ b/pybit/asyncio/http/v5_market.py @@ -0,0 +1,299 @@ +from pybit.asyncio.client import AsyncClient +from pybit.market import Market + + +class AsyncMarketHTTP(AsyncClient): + 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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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) -> dict: + """ + 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/http/v5_misc.py b/pybit/asyncio/http/v5_misc.py new file mode 100644 index 0000000..57bac05 --- /dev/null +++ b/pybit/asyncio/http/v5_misc.py @@ -0,0 +1,40 @@ +from pybit.asyncio.client import AsyncClient +from pybit.misc import Misc + + +class AsyncMiscHTTP(AsyncClient): + 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/http/v5_position.py b/pybit/asyncio/http/v5_position.py new file mode 100644 index 0000000..5113f55 --- /dev/null +++ b/pybit/asyncio/http/v5_position.py @@ -0,0 +1,269 @@ +from pybit.asyncio.client import AsyncClient +from pybit.position import Position + + +class AsyncPositionHTTP(AsyncClient): + async def get_positions(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """ + 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) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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/auto-add-margin + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.SET_AUTO_ADD_MARGIN}", + query=kwargs, + auth=True, + ) + + async def add_or_reduce_margin(self, **kwargs) -> dict: + """Manually add or reduce margin for isolated margin position + + Required args: + category (string): Product type. linear + symbol (string): Symbol name + margin (string): Add or reduce. To add, use 10. To reduce, use -10 + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/position/manual-add-margin + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{Position.ADD_MARGIN}", + query=kwargs, + auth=True, + ) + + async def get_executions(self, **kwargs) -> dict: + """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) -> dict: + """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/http/v5_pre_upgrade.py b/pybit/asyncio/http/v5_pre_upgrade.py new file mode 100644 index 0000000..9b3dba4 --- /dev/null +++ b/pybit/asyncio/http/v5_pre_upgrade.py @@ -0,0 +1,130 @@ +from pybit.asyncio.client import AsyncClient +from pybit.pre_upgrade import PreUpgrade + + +class AsyncPreUpgradeHTTP(AsyncClient): + 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/http/v5_rate_limit.py b/pybit/asyncio/http/v5_rate_limit.py new file mode 100644 index 0000000..c914256 --- /dev/null +++ b/pybit/asyncio/http/v5_rate_limit.py @@ -0,0 +1,78 @@ +from pybit.asyncio.client import AsyncClient +from pybit.rate_limit import RateLimit + + +class AsyncRateLimitHTTP(AsyncClient): + async def set_api_rate_limit(self, **kwargs) -> dict: + """ + Required args: + list (array): An array of objects + > uids (string): Multiple UIDs, separated by commas + > bizType (string): Business type + > rate (integer): API rate limit per second + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/rate-limit + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{RateLimit.SET_API_RATE_LIMIT}", + query=kwargs, + auth=True, + ) + + async def get_api_rate_limit(self, **kwargs) -> dict: + """ + Required args: + uids (string): Multiple UIDs, separated by commas + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/rate-limit + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{RateLimit.GET_API_RATE_LIMIT}", + query=kwargs, + auth=True, + ) + + async def get_api_rate_limit_cap(self): + """ + Required args: + None + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/rate-limit + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{RateLimit.GET_API_RATE_LIMIT_CAP}", + auth=True, + ) + + async def get_all_api_rate_limits(self, **kwargs) -> dict: + """ + Get all your account's API rate limits (master and subaccounts). + Required args: + None + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/rate-limit + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{RateLimit.GET_ALL_API_RATE_LIMITS}", + query=kwargs, + auth=True, + ) diff --git a/pybit/asyncio/http/v5_spot_leverage_token.py b/pybit/asyncio/http/v5_spot_leverage_token.py new file mode 100644 index 0000000..0c03cad --- /dev/null +++ b/pybit/asyncio/http/v5_spot_leverage_token.py @@ -0,0 +1,95 @@ +from pybit.asyncio.client import AsyncClient +from pybit.spot_leverage_token import SpotLeverageToken + + +class AsyncSpotLeverageHTTP(AsyncClient): + async def get_leveraged_token_info(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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/http/v5_spot_margin_trade.py b/pybit/asyncio/http/v5_spot_margin_trade.py new file mode 100644 index 0000000..bfb7469 --- /dev/null +++ b/pybit/asyncio/http/v5_spot_margin_trade.py @@ -0,0 +1,261 @@ +from pybit.asyncio.client import AsyncClient +from pybit.spot_margin_trade import SpotMarginTrade + + +class AsyncSpotMarginTradeHTTP(AsyncClient): + async def spot_margin_trade_get_vip_margin_data(self, **kwargs) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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_get_historical_interest_rate(self, **kwargs) -> dict: + """UTA only. Queries up to six months of borrowing interest rates. + + Required args: + currency (string): Coin name, uppercase only. + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/historical-interest + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{SpotMarginTrade.HISTORICAL_INTEREST}", + query=kwargs, + auth=True, + ) + + async def spot_margin_trade_normal_get_vip_margin_data(self, **kwargs) -> dict: + """ + 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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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/http/v5_trade.py b/pybit/asyncio/http/v5_trade.py new file mode 100644 index 0000000..00fbeff --- /dev/null +++ b/pybit/asyncio/http/v5_trade.py @@ -0,0 +1,244 @@ +from pybit.asyncio.client import AsyncClient +from pybit.trade import Trade + + +class AsyncTradeHTTP(AsyncClient): + async def place_order(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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, + ) + + async def get_borrow_quota(self, **kwargs) -> dict: + """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 await self._submit_request( + method="GET", + path=f"{self.endpoint}{Trade.GET_BORROW_QUOTA}", + query=kwargs, + auth=True, + ) + + async def set_dcp(self, **kwargs) -> dict: + """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/http/v5_user.py b/pybit/asyncio/http/v5_user.py new file mode 100644 index 0000000..fa73e3c --- /dev/null +++ b/pybit/asyncio/http/v5_user.py @@ -0,0 +1,234 @@ +from pybit.asyncio.client import AsyncClient +from pybit.user import User + + +class AsyncUserHTTP(AsyncClient): + async def create_sub_uid(self, **kwargs) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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) -> dict: + """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 delete_sub_uid(self, **kwargs) -> dict: + """Delete a sub UID. Before deleting the sub UID, please make sure there is no asset. Use master user's api key only. + + Required args: + subMemberId (integer): Sub UID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/rm-subuid + """ + return await self._submit_request( + method="POST", + path=f"{self.endpoint}{User.DELETE_SUB_UID}", + query=kwargs, + auth=True, + ) + + async def get_all_sub_api_keys(self, **kwargs) -> dict: + """Query all api keys information of a sub UID. + + Required args: + subMemberId (integer): Sub UID + + Returns: + Request results as dictionary. + + Additional information: + https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys + """ + return await self._submit_request( + method="GET", + path=f"{self.endpoint}{User.GET_ALL_SUB_API_KEYS}", + query=kwargs, + auth=True, + ) + + async def get_affiliate_user_info(self, **kwargs) -> dict: + """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) -> dict: + """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/unified_trading.py b/pybit/asyncio/unified_trading.py new file mode 100644 index 0000000..d4c5d32 --- /dev/null +++ b/pybit/asyncio/unified_trading.py @@ -0,0 +1,40 @@ +from pybit.asyncio.http import ( + AsyncMiscHTTP, + AsyncMarketHTTP, + AsyncTradeHTTP, + AsyncAccountHTTP, + AsyncAssetHTTP, + AsyncPositionHTTP, + AsyncPreUpgradeHTTP, + AsyncSpotLeverageHTTP, + AsyncSpotMarginTradeHTTP, + AsyncUserHTTP, + AsyncBrokerHTTP, + AsyncInstitutionalLoanHTTP, + AsyncCryptoLoanHTTP, + AsyncEarnHTTP, + AsyncRateLimitHTTP +) +from pybit.asyncio.ws import AsyncWebsocketClient + +__all__ = ["AsyncHTTP", "AsyncWebsocketClient"] + + +class AsyncHTTP( + AsyncMiscHTTP, + AsyncMarketHTTP, + AsyncTradeHTTP, + AsyncAccountHTTP, + AsyncAssetHTTP, + AsyncPositionHTTP, + AsyncPreUpgradeHTTP, + AsyncSpotLeverageHTTP, + AsyncSpotMarginTradeHTTP, + AsyncUserHTTP, + AsyncBrokerHTTP, + AsyncInstitutionalLoanHTTP, + AsyncCryptoLoanHTTP, + AsyncEarnHTTP, + AsyncRateLimitHTTP +): + ... diff --git a/pybit/asyncio/utils.py b/pybit/asyncio/utils.py new file mode 100644 index 0000000..b7eb197 --- /dev/null +++ b/pybit/asyncio/utils.py @@ -0,0 +1,17 @@ +import asyncio + + +def get_event_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/ws/__init__.py b/pybit/asyncio/ws/__init__.py new file mode 100644 index 0000000..5ec175b --- /dev/null +++ b/pybit/asyncio/ws/__init__.py @@ -0,0 +1,4 @@ +from pybit.asyncio.ws.client import ( + AsyncWebsocketClient, + AsyncWebsocketManager +) diff --git a/pybit/asyncio/ws/client.py b/pybit/asyncio/ws/client.py new file mode 100644 index 0000000..8c93753 --- /dev/null +++ b/pybit/asyncio/ws/client.py @@ -0,0 +1,118 @@ +import json +from typing import ( + Optional, + List +) +from uuid import uuid4 + +from pybit import exceptions +from pybit.unified_trading import ( + PRIVATE_WSS, + PUBLIC_WSS, + AVAILABLE_CHANNEL_TYPES, +) +from pybit.asyncio.ws.manager import AsyncWebsocketManager +from pybit.asyncio.ws.utils import chunks + + +class AsyncWebsocketClient: + """ + Prepare payload for websocket connection + """ + SPOT_MAX_CONNECTION_ARGS = 10 + # https://bybit-exchange.github.io/docs/v5/ws/connect + # Bybit Spot can input up to 10 args for each subscription request sent to one connection + + def __init__(self, + channel_type: str, + testnet: bool, + api_key: Optional[str] = None, + api_secret: Optional[str] = None, + proxy: Optional[str] = None): + self.api_key = api_key + self.api_secret = api_secret + self.proxy = proxy + self.testnet = testnet + self.channel_type = channel_type + if channel_type not in AVAILABLE_CHANNEL_TYPES: + raise exceptions.InvalidChannelTypeError( + f"Channel type is not correct. Available: {AVAILABLE_CHANNEL_TYPES}") + + if channel_type == "private": + self.WS_URL = PRIVATE_WSS + else: + self.WS_URL = PUBLIC_WSS.replace("{CHANNEL_TYPE}", channel_type) + # Do not pass keys and attempt authentication on a public connection + self.api_key = None + self.api_secret = None + + 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 spot_kline_stream(self, symbols: List[str]) -> AsyncWebsocketManager: + # Prepare subscription message + subscription_message = [] + for chunk in chunks(symbols, self.SPOT_MAX_CONNECTION_ARGS): + subscription_message.append( + json.dumps( + { + "op": "subscribe", + "req_id": str(uuid4()), + "args": chunk + } + ) + ) + # Return instance of manager for further usages + return AsyncWebsocketManager( + channel_type=self.channel_type, + url=PUBLIC_WSS.replace("{CHANNEL_TYPE}", self.channel_type), + subscription_message=subscription_message, + testnet=self.testnet, + ) + + def futures_kline_stream(self, symbols: List[str]) -> AsyncWebsocketManager: + # Prepare subscription message + subscription_message = json.dumps( + {"op": "subscribe", + "req_id": str(uuid4()), + "args": symbols} + ) + # Return instance of manager for further usages + return AsyncWebsocketManager( + channel_type=self.channel_type, + url=PUBLIC_WSS.replace("{CHANNEL_TYPE}", self.channel_type), + subscription_message=[subscription_message], + testnet=self.testnet, + ) + + def user_futures_stream(self) -> AsyncWebsocketManager: + subscription_message = json.dumps( + {"op": "subscribe", + "args": ["order.linear"]} + ) + return AsyncWebsocketManager( + channel_type=self.channel_type, + url=PRIVATE_WSS, + subscription_message=[subscription_message], + testnet=self.testnet, + api_key=self.api_key, + api_secret=self.api_secret, + proxy=self.proxy + ) + + def user_spot_stream(self) -> AsyncWebsocketManager: + subscription_message = json.dumps( + {"op": "subscribe", + "args": ["order.spot"]} + ) + return AsyncWebsocketManager( + channel_type=self.channel_type, + url=PRIVATE_WSS, + subscription_message=[subscription_message], + testnet=self.testnet, + api_key=self.api_key, + api_secret=self.api_secret, + proxy=self.proxy + ) diff --git a/pybit/asyncio/ws/manager.py b/pybit/asyncio/ws/manager.py new file mode 100644 index 0000000..28994e3 --- /dev/null +++ b/pybit/asyncio/ws/manager.py @@ -0,0 +1,274 @@ +import asyncio +import json +import traceback +import logging +import enum +from typing import ( + Optional, + List +) + +from pybit import _helpers +from pybit._http_manager import generate_signature +from pybit._websocket_stream import ( + SUBDOMAIN_MAINNET, + SUBDOMAIN_TESTNET, + DOMAIN_MAIN, + TLD_MAIN, +) +import websockets as ws +from websockets.exceptions import ConnectionClosedError +from websockets_proxy import ( + Proxy, + proxy_connect +) + +from pybit.asyncio.utils import get_event_loop +from pybit.asyncio.ws.utils import get_reconnect_wait + + +PING_INTERVAL = 20 +PINT_TIMEOUT = 10 +MESSAGE_TIMEOUT = 5 +PRIVATE_AUTH_EXPIRE = 1 + + +logger = logging.getLogger(__name__) + + +class WSState(enum.Enum): + INITIALISING = 1 + EXITING = 2 + STREAMING = 3 + RECONNECTING = 4 + + +class AsyncWebsocketManager: + """ + Implementation of async API for Bybit + """ + + def __init__( + self, + channel_type: str, + url: str, + subscription_message: List[str], + testnet: Optional[bool] = False, + rsa_authentication: Optional[bool] = False, + api_key: Optional[str] = None, + api_secret: Optional[str] = None, + proxy: Optional[str] = None, + queue: Optional[asyncio.Queue] = None, + tld: Optional[str] = TLD_MAIN, + ): + self.channel_type = channel_type + self.url = url + self.subscription_message = subscription_message + self.api_key = api_key + self.api_secret = api_secret + self.testnet = testnet + self.proxy = proxy + self.rsa_authentication = rsa_authentication + self.tld = tld + self.queue = queue or asyncio.Queue() + self._loop = get_event_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 = 60 + self._reconnects = 0 + self.MAX_QUEUE_SIZE = 10000 + self.downtime_callback = None + + 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: + await self.ws.close() + 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: + match self.ws_state: + case WSState.STREAMING: + res = await asyncio.wait_for(self.ws.recv(), timeout=MESSAGE_TIMEOUT) + res = self._handle_message(res) + if res.get("op") == "pong" or res.get("op") == "ping": + continue + if res.get("op") == "subscribe": + if res.get("success") is not None and res["success"] is False: + if res.get("ret_msg") == "Request not authorized": + logger.warning(f"Cancel task because request: {res}") + raise asyncio.CancelledError() + logger.error(f"False connecting: {res}") + self.ws_state = WSState.RECONNECTING + continue + + if res: + await self.queue.put(res) + + case WSState.EXITING: + logger.info("Exiting websocket") + await self.ws.close() + self._keepalive.cancel() + break + case WSState.RECONNECTING: + while self.ws_state == WSState.RECONNECTING: + await self._reconnect() + case self.ws.protocol.State.CLOSING: + await asyncio.sleep(0.1) + continue + case self.ws.protocol.State.CLOSED: + 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: + logger.warning(f"Os Error: {e}") + await self._reconnect() + continue + except ConnectionClosedError as e: + logger.warning(f"Connection Closed Error: {e}") + self._keepalive.cancel() + await self._reconnect() + continue + except Exception as e: + logger.warning(f"Unknown exception: {e}") + 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=self.rsa_authentication, + 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: + logger.error(f'Ping cancellation error') + traceback.print_exc() + return + except ConnectionClosedError as e: + logger.error(f'Ping connection closed error: {e}') + traceback.print_exc() + return + except Exception as e: + logger.error(f"Keepalive failed") + traceback.print_exc() + raise e + + 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 = 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") + else: + logger.error("Max reconnections reached") + await self.queue.put({ + "success": False, + "ret_msg": "Max reconnect reached" + }) + self.ws_state = WSState.EXITING + + async def connect(self): + if self.ws is None or self.ws_state == WSState.RECONNECTING: + subdomain = SUBDOMAIN_TESTNET if self.testnet else SUBDOMAIN_MAINNET + endpoint = self.url.format(SUBDOMAIN=subdomain, DOMAIN=DOMAIN_MAIN, TLD=self.tld) + + if self.proxy: + logger.info(f"Connected via: {self.proxy}") + self._conn = proxy_connect(endpoint, + close_timeout=0.1, + open_timeout=60, + ping_interval=None, + ping_timeout=None, + proxy=Proxy.from_url(self.proxy)) + else: + logger.info(f"Connected without proxies") + self._conn = ws.connect(endpoint, + close_timeout=0.1, + open_timeout=60, + ping_timeout=None, # We use custom ping task + ping_interval=None) + try: + self.ws = await self._conn.__aenter__() + except Exception as e: + traceback.print_exc() + await self._reconnect() + logger.error(f"Connecting error: {e}") + return + # Authenticate for private channels + if self.api_key and self.api_secret: + await self._auth() + + # subscribe to channels + for mes in self.subscription_message: + await self.ws.send(mes) + self._reconnects = 0 + self.ws_state = WSState.STREAMING + logger.info(f"Connected successfully") + if self.downtime_callback is not None: + try: + self.downtime_callback() + except Exception as e: + logger.error(f"Downtime callback error") + traceback.print_exc() + if self._keepalive: + self._keepalive.cancel() + self._keepalive = asyncio.create_task(self._keepalive_task()) + if not self._handle_read_loop: + 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, timeout: int = 5): + try: + return await asyncio.wait_for(self.queue.get(), timeout=timeout) + except asyncio.TimeoutError: + return None diff --git a/pybit/asyncio/ws/utils.py b/pybit/asyncio/ws/utils.py new file mode 100644 index 0000000..a1cb547 --- /dev/null +++ b/pybit/asyncio/ws/utils.py @@ -0,0 +1,14 @@ +from random import random +from typing import Iterable + + +def get_reconnect_wait(attempts: int) -> int: + expo = 2 ** attempts + # 900 - is max reconnect seconds + return round(random() * min(900, expo - 1) + 1) + + +def chunks(lst: list, n: int) -> Iterable: + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i:i + n] diff --git a/requirements.txt b/requirements.txt index 0da22d3..daf6de8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ requests>=2.22.0 websocket-client==1.5.0 -pycryptodome==3.20.0 \ No newline at end of file +pycryptodome==3.20.0 +aiohttp==3.13.2 +websockets==15.0.1 +websockets_proxy==0.1.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 1329687..1589045 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages from os import path here = path.abspath(path.dirname(__file__)) @@ -25,11 +25,14 @@ "Programming Language :: Python :: 3.10", ], keywords="bybit api connector", - packages=["pybit"], + packages=find_packages(include=["pybit", "pybit.*"]), python_requires=">=3.6", install_requires=[ "requests", "websocket-client", "pycryptodome", + "aiohttp", + "websockets", + "websockets_proxy" ], )