pybit 5.15.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pybit/__init__.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "5.15.0"
pybit/_helpers.py ADDED
@@ -0,0 +1,75 @@
1
+ import time
2
+ import re
3
+ import copy
4
+
5
+
6
+ def generate_timestamp():
7
+ """
8
+ Return a millisecond integer timestamp.
9
+ """
10
+ return int(time.time() * 10**3)
11
+
12
+
13
+ def identify_ws_method(input_wss_url, wss_dictionary):
14
+ """
15
+ This method matches the input_wss_url with a particular WSS method. This
16
+ helps ensure that, when subscribing to a custom topic, the topic
17
+ subscription message is sent down the correct WSS connection.
18
+ """
19
+ path = re.compile(r"(wss://)?([^/\s]+)(.*)")
20
+ input_wss_url_path = path.match(input_wss_url).group(3)
21
+ for wss_url, function_call in wss_dictionary.items():
22
+ wss_url_path = path.match(wss_url).group(3)
23
+ if input_wss_url_path == wss_url_path:
24
+ return function_call
25
+
26
+
27
+ def find_index(source, target, key):
28
+ """
29
+ Find the index in source list of the targeted ID.
30
+ """
31
+ return next(i for i, j in enumerate(source) if j[key] == target[key])
32
+
33
+
34
+ def make_private_args(args):
35
+ """
36
+ Exists to pass on the user's arguments to a lower-level class without
37
+ giving the user access to that classes attributes (ie, passing on args
38
+ without inheriting the parent class).
39
+ """
40
+ args.pop("self")
41
+ return args
42
+
43
+
44
+ def make_public_kwargs(private_kwargs):
45
+ public_kwargs = copy.deepcopy(private_kwargs)
46
+ public_kwargs.pop("api_key", "")
47
+ public_kwargs.pop("api_secret", "")
48
+ return public_kwargs
49
+
50
+
51
+ def are_connections_connected(active_connections):
52
+ for connection in active_connections:
53
+ if not connection.is_connected():
54
+ return False
55
+ return True
56
+
57
+
58
+ def is_inverse_contract(symbol: str):
59
+ if re.search(r"(USD)([HMUZ]\d\d|$)", symbol):
60
+ return True
61
+
62
+
63
+ def is_usdt_perpetual(symbol: str):
64
+ if symbol.endswith("USDT"):
65
+ return True
66
+
67
+
68
+ def is_usdc_perpetual(symbol: str):
69
+ if symbol.endswith("USDC"):
70
+ return True
71
+
72
+
73
+ def is_usdc_option(symbol: str):
74
+ if re.search(r"[A-Z]{3}-.*-[PC]$", symbol):
75
+ return True
pybit/_http_manager.py ADDED
@@ -0,0 +1,354 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass, field
3
+ import time
4
+ import hmac
5
+ import hashlib
6
+ from Crypto.Hash import SHA256
7
+ from Crypto.PublicKey import RSA
8
+ from Crypto.Signature import PKCS1_v1_5
9
+ import base64
10
+ import json
11
+ import logging
12
+ import requests
13
+
14
+ from datetime import datetime as dt, timezone
15
+
16
+ from .exceptions import FailedRequestError, InvalidRequestError
17
+ from . import _helpers
18
+
19
+ # Requests will use simplejson if available.
20
+ try:
21
+ from simplejson.errors import JSONDecodeError
22
+ except ImportError:
23
+ from json.decoder import JSONDecodeError
24
+
25
+ HTTP_URL = "https://{SUBDOMAIN}.{DOMAIN}.{TLD}"
26
+ SUBDOMAIN_TESTNET = "api-testnet"
27
+ SUBDOMAIN_MAINNET = "api"
28
+ DEMO_SUBDOMAIN_TESTNET = "api-demo-testnet"
29
+ DEMO_SUBDOMAIN_MAINNET = "api-demo"
30
+ DOMAIN_MAIN = "bybit"
31
+ DOMAIN_ALT = "bytick"
32
+ DOMAIN_TK = "bybit-tr" # Turkey
33
+ TLD_MAIN = "com" # Global
34
+ TLD_NL = "nl" # The Netherlands
35
+ TLD_HK = "com.hk" # Hong Kong
36
+ TLD_KZ = "kz" # Kazakhstan
37
+ TLD_EU = "eu" # European Economic Area. ONLY AVAILABLE TO INSTITUTIONS
38
+
39
+
40
+ class _RetryableRequestError(Exception):
41
+ def __init__(self, recv_window):
42
+ self.recv_window = recv_window
43
+ super().__init__("Retryable error occurred, retrying...")
44
+
45
+
46
+ def generate_signature(use_rsa_authentication, secret, param_str):
47
+ def generate_hmac():
48
+ hash = hmac.new(
49
+ bytes(secret, "utf-8"),
50
+ param_str.encode("utf-8"),
51
+ hashlib.sha256,
52
+ )
53
+ return hash.hexdigest()
54
+
55
+ def generate_rsa():
56
+ hash = SHA256.new(param_str.encode("utf-8"))
57
+ encoded_signature = base64.b64encode(
58
+ PKCS1_v1_5.new(RSA.importKey(secret)).sign(
59
+ hash
60
+ )
61
+ )
62
+ return encoded_signature.decode()
63
+
64
+ if not use_rsa_authentication:
65
+ return generate_hmac()
66
+ else:
67
+ return generate_rsa()
68
+
69
+
70
+ @dataclass
71
+ class _V5HTTPManager:
72
+ testnet: bool = field(default=False)
73
+ domain: str = field(default=DOMAIN_MAIN)
74
+ tld: str = field(default=TLD_MAIN)
75
+ demo: bool = field(default=False)
76
+ rsa_authentication: str = field(default=False)
77
+ api_key: str = field(default=None)
78
+ api_secret: str = field(default=None)
79
+ logging_level: logging = field(default=logging.INFO)
80
+ log_requests: bool = field(default=False)
81
+ timeout: int = field(default=10)
82
+ recv_window: bool = field(default=5000)
83
+ force_retry: bool = field(default=False)
84
+ retry_codes: defaultdict[dict] = field(default_factory=dict)
85
+ ignore_codes: set = field(default_factory=set)
86
+ max_retries: bool = field(default=3)
87
+ retry_delay: bool = field(default=3)
88
+ referral_id: str = field(default=None)
89
+ record_request_time: bool = field(default=False)
90
+ return_response_headers: bool = field(default=False)
91
+
92
+ def __post_init__(self):
93
+ subdomain = SUBDOMAIN_TESTNET if self.testnet else SUBDOMAIN_MAINNET
94
+ domain = DOMAIN_MAIN if not self.domain else self.domain
95
+ if self.demo:
96
+ if self.testnet:
97
+ subdomain = DEMO_SUBDOMAIN_TESTNET
98
+ else:
99
+ subdomain = DEMO_SUBDOMAIN_MAINNET
100
+ url = HTTP_URL.format(SUBDOMAIN=subdomain, DOMAIN=domain, TLD=self.tld)
101
+ self.endpoint = url
102
+
103
+ if not self.ignore_codes:
104
+ self.ignore_codes = set()
105
+ if not self.retry_codes:
106
+ self.retry_codes = {10002, 10006, 30034, 30035, 130035, 130150}
107
+ self.logger = logging.getLogger(__name__)
108
+ if len(logging.root.handlers) == 0:
109
+ # no handler on root logger set -> we add handler just for this logger to not mess with custom logic from
110
+ # outside
111
+ handler = logging.StreamHandler()
112
+ handler.setFormatter(
113
+ logging.Formatter(
114
+ fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
115
+ datefmt="%Y-%m-%d %H:%M:%S",
116
+ )
117
+ )
118
+ handler.setLevel(self.logging_level)
119
+ self.logger.addHandler(handler)
120
+
121
+ self.logger.debug("Initializing HTTP session.")
122
+
123
+ self.client = requests.Session()
124
+ self.client.headers.update(
125
+ {
126
+ "Content-Type": "application/json",
127
+ "Accept": "application/json",
128
+ }
129
+ )
130
+ if self.referral_id:
131
+ self.client.headers.update({"Referer": self.referral_id})
132
+
133
+ @staticmethod
134
+ def prepare_payload(method, parameters):
135
+ """
136
+ Prepares the request payload and validates parameter value types.
137
+ """
138
+
139
+ def cast_values():
140
+ string_params = [
141
+ "qty",
142
+ "price",
143
+ "triggerPrice",
144
+ "takeProfit",
145
+ "stopLoss",
146
+ ]
147
+ integer_params = ["positionIdx"]
148
+ for key, value in parameters.items():
149
+ if key in string_params:
150
+ if type(value) != str:
151
+ parameters[key] = str(value)
152
+ elif key in integer_params:
153
+ if type(value) != int:
154
+ parameters[key] = int(value)
155
+
156
+ if method == "GET":
157
+ payload = "&".join(
158
+ [
159
+ str(k) + "=" + str(v)
160
+ for k, v in sorted(parameters.items())
161
+ if v is not None
162
+ ]
163
+ )
164
+ return payload
165
+ else:
166
+ cast_values()
167
+ return json.dumps(parameters)
168
+
169
+ def _auth(self, payload, recv_window, timestamp):
170
+ """
171
+ Prepares authentication signature per Bybit API specifications.
172
+ """
173
+
174
+ if self.api_key is None or self.api_secret is None:
175
+ raise PermissionError("Authenticated endpoints require keys.")
176
+
177
+ param_str = str(timestamp) + self.api_key + str(recv_window) + payload
178
+
179
+ return generate_signature(
180
+ self.rsa_authentication, self.api_secret, param_str
181
+ )
182
+
183
+ def _submit_request(self, method=None, path=None, query=None, auth=False):
184
+ """
185
+ Submits the request to the API.
186
+ """
187
+ query = self._clean_query(query)
188
+ recv_window = self.recv_window
189
+ retries_attempted = self.max_retries
190
+
191
+ while retries_attempted > 0:
192
+ retries_attempted -= 1
193
+ try:
194
+ req_params = self.prepare_payload(method, query)
195
+ headers = self._prepare_headers(req_params, recv_window) if auth else {}
196
+
197
+ request = self._prepare_request(method, path, req_params, headers)
198
+ self._log_request(method, path, req_params, request.headers)
199
+
200
+ response = self.client.send(request, timeout=self.timeout)
201
+ self._check_status_code(response, method, path, req_params)
202
+
203
+ return self._handle_response(response, method, path, req_params, recv_window, retries_attempted)
204
+
205
+ except _RetryableRequestError as e:
206
+ recv_window = e.recv_window
207
+ continue
208
+ except (requests.exceptions.ReadTimeout, requests.exceptions.SSLError,
209
+ requests.exceptions.ConnectionError) as e:
210
+ self._handle_network_error(e, retries_attempted)
211
+ except JSONDecodeError as e:
212
+ self._handle_json_error(e, retries_attempted)
213
+
214
+ raise FailedRequestError(
215
+ request=f"{method} {path}: {req_params}",
216
+ message="Bad Request. Retries exceeded maximum.",
217
+ status_code=400,
218
+ time=dt.now(timezone.utc).strftime("%H:%M:%S"),
219
+ resp_headers=None,
220
+ )
221
+
222
+ def _clean_query(self, query):
223
+ """Remove None values and fix floats."""
224
+ if query is None:
225
+ return {}
226
+ for key in list(query.keys()):
227
+ if isinstance(query[key], float) and query[key] == int(query[key]):
228
+ query[key] = int(query[key])
229
+ return {k: v for k, v in query.items() if v is not None}
230
+
231
+ def _prepare_headers(self, payload, recv_window):
232
+ """Prepare headers for authenticated request."""
233
+ timestamp = _helpers.generate_timestamp()
234
+ signature = self._auth(payload=payload, recv_window=recv_window, timestamp=timestamp)
235
+ return {
236
+ "Content-Type": "application/json",
237
+ "X-BAPI-API-KEY": self.api_key,
238
+ "X-BAPI-SIGN": signature,
239
+ "X-BAPI-SIGN-TYPE": "2",
240
+ "X-BAPI-TIMESTAMP": str(timestamp),
241
+ "X-BAPI-RECV-WINDOW": str(recv_window),
242
+ }
243
+
244
+ def _prepare_request(self, method, path, params, headers):
245
+ """Prepare request object."""
246
+ if method == "GET" and params:
247
+ return self.client.prepare_request(requests.Request(method, f"{path}?{params}", headers=headers))
248
+ return self.client.prepare_request(requests.Request(method, path, data=params, headers=headers))
249
+
250
+ def _log_request(self, method, path, params, headers):
251
+ """Log request."""
252
+ if self.log_requests:
253
+ if params:
254
+ self.logger.debug(f"Request -> {method} {path}. Body: {params}. Headers: {headers}")
255
+ else:
256
+ self.logger.debug(f"Request -> {method} {path}. Headers: {headers}")
257
+
258
+ def _check_status_code(self, response, method, path, params):
259
+ """Check HTTP status code."""
260
+ if response.status_code != 200:
261
+ error_msg = "You have breached the IP rate limit or your IP is from the USA."\
262
+ if response.status_code == 403 else "HTTP status code is not 200."
263
+ self.logger.debug(f"Response text: {response.text}")
264
+ raise FailedRequestError(
265
+ request=f"{method} {path}: {params}",
266
+ message=error_msg,
267
+ status_code=response.status_code,
268
+ time=dt.now(timezone.utc).strftime("%H:%M:%S"),
269
+ resp_headers=response.headers,
270
+ )
271
+
272
+ def _handle_response(self, response, method, path, params, recv_window, retries_attempted):
273
+ """Handle JSON response and Bybit error codes."""
274
+ try:
275
+ s_json = response.json()
276
+ except JSONDecodeError as e:
277
+ raise e # Will be caught by main loop to retry.
278
+
279
+ ret_code = "retCode"
280
+ ret_msg = "retMsg"
281
+
282
+ if s_json.get(ret_code):
283
+ error_code = s_json[ret_code]
284
+ error_msg = f"{s_json[ret_msg]} (ErrCode: {error_code})"
285
+
286
+ if error_code in self.retry_codes:
287
+ recv_window = self._handle_retryable_error(
288
+ response, error_code, error_msg, recv_window
289
+ )
290
+ raise _RetryableRequestError(recv_window)
291
+
292
+ if error_code not in self.ignore_codes:
293
+ raise InvalidRequestError(
294
+ request=f"{method} {path}: {params}",
295
+ message=s_json[ret_msg],
296
+ status_code=error_code,
297
+ time=dt.now(timezone.utc).strftime("%H:%M:%S"),
298
+ resp_headers=response.headers,
299
+ )
300
+
301
+ if self.log_requests:
302
+ self.logger.debug(f"Response headers: {response.headers}")
303
+
304
+ if self.return_response_headers:
305
+ return s_json, response.elapsed, response.headers
306
+ elif self.record_request_time:
307
+ return s_json, response.elapsed
308
+ else:
309
+ return s_json
310
+
311
+ def _handle_retryable_error(self, response, error_code, error_msg, recv_window):
312
+ """Handle specific retryable Bybit errors."""
313
+ delay_time = self.retry_delay
314
+
315
+ if error_code == 10002: # recv_window error
316
+ error_msg += ". Added 2.5 seconds to recv_window"
317
+ recv_window += 2500
318
+ elif error_code == 10006: # rate limit error
319
+ self.logger.error(f"{error_msg}. Hit the API rate limit on {response.url}. Sleeping then trying again.")
320
+ limit_reset_time = int(
321
+ response.headers.get(
322
+ "X-Bapi-Limit-Reset-Timestamp",
323
+ _helpers.generate_timestamp() + 2000
324
+ )
325
+ )
326
+ limit_reset_str = dt.fromtimestamp(limit_reset_time / 10 ** 3).strftime("%H:%M:%S.%f")[:-3]
327
+ delay_time = (limit_reset_time - _helpers.generate_timestamp()) / 10 ** 3
328
+ error_msg = f"API rate limit will reset at {limit_reset_str}. Sleeping for {int(delay_time * 10 ** 3)} ms"
329
+
330
+ self.logger.error(f"{error_msg}. Retrying...")
331
+ time.sleep(delay_time)
332
+ return recv_window
333
+
334
+ def _handle_network_error(self, error, retries_attempted):
335
+ """Handle network-related exceptions."""
336
+ if self.force_retry and retries_attempted > 0:
337
+ self.logger.error(f"{error}. Retrying...")
338
+ time.sleep(self.retry_delay)
339
+ else:
340
+ raise error
341
+
342
+ def _handle_json_error(self, error, retries_attempted):
343
+ """Handle JSON decoding errors."""
344
+ if self.force_retry and retries_attempted > 0:
345
+ self.logger.error(f"{error}. Retrying JSON decode...")
346
+ time.sleep(self.retry_delay)
347
+ else:
348
+ raise FailedRequestError(
349
+ request="JSON decoding",
350
+ message="Conflict. Could not decode JSON.",
351
+ status_code=409,
352
+ time=dt.now(timezone.utc).strftime("%H:%M:%S"),
353
+ resp_headers=None,
354
+ )