hubble-futures 0.2.13__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.
- hubble_futures/__init__.py +151 -0
- hubble_futures/aster.py +601 -0
- hubble_futures/base.py +430 -0
- hubble_futures/config.py +34 -0
- hubble_futures/function_log.py +303 -0
- hubble_futures/version.py +8 -0
- hubble_futures/weex.py +1246 -0
- hubble_futures-0.2.13.dist-info/METADATA +217 -0
- hubble_futures-0.2.13.dist-info/RECORD +11 -0
- hubble_futures-0.2.13.dist-info/WHEEL +4 -0
- hubble_futures-0.2.13.dist-info/licenses/LICENSE +21 -0
hubble_futures/base.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for futures exchange clients.
|
|
3
|
+
|
|
4
|
+
Provides common functionality shared across all exchange implementations:
|
|
5
|
+
- HTTP session management
|
|
6
|
+
- Request retry logic for rate limits
|
|
7
|
+
- Decimal formatting for prices/quantities
|
|
8
|
+
- Time synchronization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
import time
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from decimal import ROUND_DOWN, Decimal
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
# Optional function logging (may not be available in all environments)
|
|
20
|
+
try:
|
|
21
|
+
from .function_log import finish_function_call, record_function_call
|
|
22
|
+
_FUNCTION_LOG_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
_FUNCTION_LOG_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseFuturesClient(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Abstract base class for futures exchange clients.
|
|
30
|
+
|
|
31
|
+
All exchange implementations should inherit from this class
|
|
32
|
+
and implement the abstract methods.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: str,
|
|
38
|
+
api_secret: str,
|
|
39
|
+
base_url: str,
|
|
40
|
+
max_retries: int = 5,
|
|
41
|
+
retry_delay: float = 1.0,
|
|
42
|
+
timeout: float = 5.0,
|
|
43
|
+
proxy_url: str | None = None
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize base client.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: API key
|
|
50
|
+
api_secret: API secret
|
|
51
|
+
base_url: API base URL
|
|
52
|
+
max_retries: Max retry attempts for rate limit errors
|
|
53
|
+
retry_delay: Base delay for exponential backoff
|
|
54
|
+
timeout: Request timeout in seconds
|
|
55
|
+
proxy_url: Optional proxy server URL for all requests.
|
|
56
|
+
Required for exchanges with IP whitelist restrictions.
|
|
57
|
+
Format: http://user:pass@host:port or http://host:port
|
|
58
|
+
"""
|
|
59
|
+
if not api_key:
|
|
60
|
+
raise ValueError("api_key is required")
|
|
61
|
+
if not api_secret:
|
|
62
|
+
raise ValueError("api_secret is required")
|
|
63
|
+
|
|
64
|
+
self.api_key = api_key
|
|
65
|
+
self.api_secret = api_secret
|
|
66
|
+
self.base_url = base_url
|
|
67
|
+
self.max_retries = max_retries
|
|
68
|
+
self.retry_delay = retry_delay
|
|
69
|
+
self.timeout = timeout
|
|
70
|
+
self.proxy_url = proxy_url
|
|
71
|
+
|
|
72
|
+
self.session = requests.Session()
|
|
73
|
+
self._setup_session_headers()
|
|
74
|
+
self._setup_session_proxy()
|
|
75
|
+
|
|
76
|
+
# Caches
|
|
77
|
+
self._symbol_filters: dict = {}
|
|
78
|
+
self._leverage_brackets: dict = {}
|
|
79
|
+
self._last_sync_time: float = 0
|
|
80
|
+
self.time_offset: int = 0
|
|
81
|
+
|
|
82
|
+
def _setup_session_headers(self) -> None:
|
|
83
|
+
"""Setup default session headers. Override in subclass if needed."""
|
|
84
|
+
self.session.headers.update({
|
|
85
|
+
"Content-Type": "application/json"
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
def _setup_session_proxy(self) -> None:
|
|
89
|
+
"""Setup proxy for session if proxy_url is configured."""
|
|
90
|
+
if self.proxy_url:
|
|
91
|
+
self.session.proxies = {
|
|
92
|
+
"http": self.proxy_url,
|
|
93
|
+
"https": self.proxy_url,
|
|
94
|
+
}
|
|
95
|
+
# Mask sensitive parts of proxy URL for logging
|
|
96
|
+
masked_proxy = self._mask_proxy_url(self.proxy_url)
|
|
97
|
+
logger.info(f"Using proxy: {masked_proxy}")
|
|
98
|
+
else:
|
|
99
|
+
self.session.proxies = {}
|
|
100
|
+
logger.debug("No proxy configured, using direct connection")
|
|
101
|
+
|
|
102
|
+
def _mask_proxy_url(self, proxy_url: str) -> str:
|
|
103
|
+
"""Mask sensitive parts of proxy URL for logging.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
proxy_url: Full proxy URL like "http://user:pass@host:port" or "http://host:port"
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Masked URL with credentials hidden and IP partially masked
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Parse proxy URL
|
|
113
|
+
if "@" in proxy_url:
|
|
114
|
+
# Format: http://user:pass@host:port
|
|
115
|
+
auth_part, endpoint = proxy_url.split("@", 1)
|
|
116
|
+
protocol = auth_part.split("://")[0]
|
|
117
|
+
# Mask credentials completely
|
|
118
|
+
return f"{protocol}://***@{self._mask_host(endpoint)}"
|
|
119
|
+
else:
|
|
120
|
+
# Format: http://host:port
|
|
121
|
+
protocol, endpoint = proxy_url.split("://", 1)
|
|
122
|
+
return f"{protocol}://{self._mask_host(endpoint)}"
|
|
123
|
+
except Exception:
|
|
124
|
+
# If parsing fails, return completely masked
|
|
125
|
+
return "***"
|
|
126
|
+
|
|
127
|
+
def _mask_host(self, endpoint: str) -> str:
|
|
128
|
+
"""Mask IP address or hostname in endpoint.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
endpoint: Endpoint like "192.168.1.1:8080" or "example.com:8080"
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Masked endpoint with IP partially hidden
|
|
135
|
+
"""
|
|
136
|
+
# Split host and port
|
|
137
|
+
if ":" in endpoint:
|
|
138
|
+
host, port = endpoint.rsplit(":", 1)
|
|
139
|
+
# Mask IP address (show first and last octet)
|
|
140
|
+
parts = host.split(".")
|
|
141
|
+
if len(parts) == 4: # IPv4
|
|
142
|
+
return f"{parts[0]}.***.{parts[3]}:{port}"
|
|
143
|
+
else: # Domain or other format
|
|
144
|
+
return f"***:{port}"
|
|
145
|
+
else:
|
|
146
|
+
# No port, just mask host
|
|
147
|
+
parts = endpoint.split(".")
|
|
148
|
+
if len(parts) == 4: # IPv4
|
|
149
|
+
return f"{parts[0]}.***.{parts[3]}"
|
|
150
|
+
else:
|
|
151
|
+
return "***"
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def _generate_signature(self, params: dict) -> str: # type: ignore[type-arg]
|
|
155
|
+
"""Generate request signature. Must be implemented by subclass."""
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
def _get_timestamp(self) -> int:
|
|
159
|
+
"""Get current timestamp in milliseconds with offset correction."""
|
|
160
|
+
if time.time() - self._last_sync_time > 3600:
|
|
161
|
+
self._sync_server_time()
|
|
162
|
+
return int(time.time() * 1000) + self.time_offset
|
|
163
|
+
|
|
164
|
+
def _get_server_time_endpoint(self) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Get server time endpoint. Override in subclass for different exchanges.
|
|
167
|
+
Return empty string to disable time sync.
|
|
168
|
+
"""
|
|
169
|
+
return "/fapi/v1/time"
|
|
170
|
+
|
|
171
|
+
def _parse_server_time(self, response_data: dict) -> int: # type: ignore[type-arg]
|
|
172
|
+
"""Parse server time from response. Override if response format differs."""
|
|
173
|
+
return response_data.get('serverTime', int(time.time() * 1000))
|
|
174
|
+
|
|
175
|
+
def _sync_server_time(self) -> None:
|
|
176
|
+
"""Sync with server time."""
|
|
177
|
+
endpoint = self._get_server_time_endpoint()
|
|
178
|
+
if not endpoint:
|
|
179
|
+
# Time sync disabled for this exchange
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
response = self.session.get(
|
|
184
|
+
f"{self.base_url}{endpoint}",
|
|
185
|
+
timeout=self.timeout
|
|
186
|
+
)
|
|
187
|
+
if response.status_code == 200:
|
|
188
|
+
server_time = self._parse_server_time(response.json())
|
|
189
|
+
local_time = int(time.time() * 1000)
|
|
190
|
+
self.time_offset = server_time - local_time
|
|
191
|
+
self._last_sync_time = time.time()
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.warning(f"Clock sync failed: {e}")
|
|
194
|
+
self.time_offset = 0
|
|
195
|
+
|
|
196
|
+
def _request(
|
|
197
|
+
self,
|
|
198
|
+
method: str,
|
|
199
|
+
endpoint: str,
|
|
200
|
+
signed: bool = False,
|
|
201
|
+
**kwargs: dict # type: ignore[type-arg]
|
|
202
|
+
) -> dict: # type: ignore[type-arg]
|
|
203
|
+
"""
|
|
204
|
+
Generic request method with retry logic for rate limits.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
method: HTTP method (GET/POST/DELETE)
|
|
208
|
+
endpoint: API endpoint
|
|
209
|
+
signed: Whether signature is required
|
|
210
|
+
**kwargs: Additional request parameters
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Response data as dict
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
requests.exceptions.HTTPError: For non-retryable HTTP errors
|
|
217
|
+
"""
|
|
218
|
+
url = f"{self.base_url}{endpoint}"
|
|
219
|
+
|
|
220
|
+
# Record function call start (if logging is available)
|
|
221
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
222
|
+
function_name = f"{method.lower()}_{endpoint.replace('/', '_')}"
|
|
223
|
+
record_params = {"endpoint": endpoint, "method": method}
|
|
224
|
+
if 'params' in kwargs:
|
|
225
|
+
record_params.update(kwargs['params'])
|
|
226
|
+
record_function_call(function_name, record_params)
|
|
227
|
+
|
|
228
|
+
for attempt in range(self.max_retries + 1):
|
|
229
|
+
if signed:
|
|
230
|
+
params = kwargs.get('params', {})
|
|
231
|
+
if 'signature' in params:
|
|
232
|
+
del params['signature']
|
|
233
|
+
params['timestamp'] = self._get_timestamp()
|
|
234
|
+
params['recvWindow'] = 5000
|
|
235
|
+
params['signature'] = self._generate_signature(params)
|
|
236
|
+
kwargs['params'] = params
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
if 'timeout' not in kwargs:
|
|
240
|
+
kwargs['timeout'] = self.timeout
|
|
241
|
+
|
|
242
|
+
response = self.session.request(method, url, **kwargs)
|
|
243
|
+
response.raise_for_status()
|
|
244
|
+
result = response.json()
|
|
245
|
+
|
|
246
|
+
# Record successful function call
|
|
247
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
248
|
+
finish_function_call(function_name, {"status": "succeeded", "data": result})
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
except requests.exceptions.HTTPError as e:
|
|
253
|
+
resp = e.response
|
|
254
|
+
|
|
255
|
+
if resp.status_code == 429:
|
|
256
|
+
if attempt < self.max_retries:
|
|
257
|
+
retry_after = resp.headers.get('Retry-After')
|
|
258
|
+
if retry_after:
|
|
259
|
+
try:
|
|
260
|
+
delay = float(retry_after)
|
|
261
|
+
except ValueError:
|
|
262
|
+
delay = self.retry_delay * (2 ** attempt)
|
|
263
|
+
else:
|
|
264
|
+
delay = self.retry_delay * (2 ** attempt)
|
|
265
|
+
|
|
266
|
+
jitter = delay * 0.2 * (2 * random.random() - 1)
|
|
267
|
+
delay = delay + jitter
|
|
268
|
+
|
|
269
|
+
logger.warning(
|
|
270
|
+
f"Rate limit hit (429) on {method} {endpoint}. "
|
|
271
|
+
f"Retry {attempt + 1}/{self.max_retries} after {delay:.2f}s"
|
|
272
|
+
)
|
|
273
|
+
time.sleep(delay)
|
|
274
|
+
continue
|
|
275
|
+
else:
|
|
276
|
+
logger.error(
|
|
277
|
+
f"Rate limit exceeded after {self.max_retries} retries"
|
|
278
|
+
)
|
|
279
|
+
logger.error(f"Response: {resp.text}")
|
|
280
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
281
|
+
finish_function_call(function_name, error=f"Rate limit exceeded")
|
|
282
|
+
raise
|
|
283
|
+
|
|
284
|
+
logger.error(f"API request failed: {e}")
|
|
285
|
+
logger.error(f"Response: {resp.text}")
|
|
286
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
287
|
+
finish_function_call(function_name, error=str(e))
|
|
288
|
+
raise
|
|
289
|
+
|
|
290
|
+
except requests.exceptions.Timeout as e:
|
|
291
|
+
logger.error(f"Request timeout: {method} {endpoint}")
|
|
292
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
293
|
+
finish_function_call(function_name, error=f"Timeout after {self.timeout}s")
|
|
294
|
+
raise Exception(f"API request timeout after {self.timeout}s") from e
|
|
295
|
+
|
|
296
|
+
except requests.exceptions.ConnectionError as e:
|
|
297
|
+
logger.error(f"Connection error: {method} {endpoint} - {e}")
|
|
298
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
299
|
+
finish_function_call(function_name, error=f"Connection error: {e}")
|
|
300
|
+
raise Exception(f"API connection failed: {e}") from e
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.error(f"Request exception: {method} {endpoint} - {e}")
|
|
304
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
305
|
+
finish_function_call(function_name, error=str(e))
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
if _FUNCTION_LOG_AVAILABLE:
|
|
309
|
+
finish_function_call(function_name, error="Exhausted all retry attempts")
|
|
310
|
+
raise Exception(f"Exhausted all retry attempts for {method} {endpoint}")
|
|
311
|
+
|
|
312
|
+
def _format_decimal(
|
|
313
|
+
self,
|
|
314
|
+
value: float,
|
|
315
|
+
step: float | None = None,
|
|
316
|
+
precision: int | None = None,
|
|
317
|
+
rounding: str = ROUND_DOWN
|
|
318
|
+
) -> str:
|
|
319
|
+
"""Format numeric values to comply with exchange precision rules."""
|
|
320
|
+
if value is None:
|
|
321
|
+
return ""
|
|
322
|
+
decimal_value = Decimal(str(value))
|
|
323
|
+
|
|
324
|
+
if step:
|
|
325
|
+
step_decimal = Decimal(str(step))
|
|
326
|
+
if step_decimal > 0:
|
|
327
|
+
multiple = (decimal_value / step_decimal).to_integral_value(rounding=rounding)
|
|
328
|
+
decimal_value = (multiple * step_decimal).quantize(step_decimal, rounding=rounding)
|
|
329
|
+
|
|
330
|
+
if precision is not None and precision >= 0:
|
|
331
|
+
quant = Decimal('1').scaleb(-precision)
|
|
332
|
+
decimal_value = decimal_value.quantize(quant, rounding=rounding)
|
|
333
|
+
|
|
334
|
+
normalized = decimal_value.normalize()
|
|
335
|
+
return format(normalized, 'f')
|
|
336
|
+
|
|
337
|
+
# ==================== Abstract Methods ====================
|
|
338
|
+
# Subclasses must implement these methods
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def from_config(cls, config: "ExchangeConfig") -> "BaseFuturesClient": # type: ignore[name-defined] # noqa: F821
|
|
343
|
+
"""Create client instance from ExchangeConfig."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Market Data
|
|
347
|
+
@abstractmethod
|
|
348
|
+
def get_klines(self, symbol: str, interval: str = "1h", limit: int = 200) -> list[dict]: # type: ignore[type-arg]
|
|
349
|
+
"""Fetch candlestick data."""
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
@abstractmethod
|
|
353
|
+
def get_mark_price(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
354
|
+
"""Fetch mark price info."""
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
@abstractmethod
|
|
358
|
+
def get_depth(self, symbol: str, limit: int = 20) -> dict: # type: ignore[type-arg]
|
|
359
|
+
"""Fetch orderbook."""
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
@abstractmethod
|
|
363
|
+
def get_ticker_24hr(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
364
|
+
"""Fetch 24h statistics."""
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
# Account
|
|
368
|
+
@abstractmethod
|
|
369
|
+
def get_account(self) -> dict: # type: ignore[type-arg]
|
|
370
|
+
"""Fetch account info."""
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
@abstractmethod
|
|
374
|
+
def get_positions(self, symbol: str | None = None) -> list[dict]: # type: ignore[type-arg]
|
|
375
|
+
"""Fetch open positions."""
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
@abstractmethod
|
|
379
|
+
def get_balance(self) -> dict: # type: ignore[type-arg]
|
|
380
|
+
"""Fetch balance summary."""
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
# Trading
|
|
384
|
+
@abstractmethod
|
|
385
|
+
def set_leverage(self, symbol: str, leverage: int) -> dict: # type: ignore[type-arg]
|
|
386
|
+
"""Set leverage for symbol."""
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
@abstractmethod
|
|
390
|
+
def place_order(
|
|
391
|
+
self,
|
|
392
|
+
symbol: str,
|
|
393
|
+
side: str,
|
|
394
|
+
order_type: str = "LIMIT",
|
|
395
|
+
quantity: float | None = None,
|
|
396
|
+
price: float | None = None,
|
|
397
|
+
stop_price: float | None = None,
|
|
398
|
+
reduce_only: bool = False,
|
|
399
|
+
time_in_force: str = "GTC",
|
|
400
|
+
client_order_id: str | None = None,
|
|
401
|
+
**kwargs: dict # type: ignore[type-arg]
|
|
402
|
+
) -> dict: # type: ignore[type-arg]
|
|
403
|
+
"""Place an order."""
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
@abstractmethod
|
|
407
|
+
def cancel_order(
|
|
408
|
+
self,
|
|
409
|
+
symbol: str,
|
|
410
|
+
order_id: int | None = None,
|
|
411
|
+
client_order_id: str | None = None
|
|
412
|
+
) -> dict: # type: ignore[type-arg]
|
|
413
|
+
"""Cancel an order."""
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
@abstractmethod
|
|
417
|
+
def get_open_orders(self, symbol: str | None = None) -> list[dict]: # type: ignore[type-arg]
|
|
418
|
+
"""Get open orders."""
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
# Helpers
|
|
422
|
+
@abstractmethod
|
|
423
|
+
def get_symbol_filters(self, symbol: str, force_refresh: bool = False) -> dict: # type: ignore[type-arg]
|
|
424
|
+
"""Get trading rules for symbol."""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
@abstractmethod
|
|
428
|
+
def close_position(self, symbol: str, percent: float = 100.0) -> dict: # type: ignore[type-arg]
|
|
429
|
+
"""Close position by percentage."""
|
|
430
|
+
pass
|
hubble_futures/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exchange configuration model.
|
|
3
|
+
|
|
4
|
+
Provides type-safe configuration for exchange clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ExchangeConfig:
|
|
13
|
+
"""
|
|
14
|
+
Exchange configuration.
|
|
15
|
+
|
|
16
|
+
Supported exchanges: asterdex, weex, binance, okx (more to come)
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
name: Exchange name ("asterdex", "weex", etc.)
|
|
20
|
+
api_key: API key
|
|
21
|
+
api_secret: API secret
|
|
22
|
+
base_url: API base URL (auto-detected if empty)
|
|
23
|
+
passphrase: API passphrase (required for WEEX, OKX)
|
|
24
|
+
proxy_url: Proxy server URL for API requests (optional).
|
|
25
|
+
Required for exchanges with IP whitelist restrictions.
|
|
26
|
+
Format: http://user:pass@host:port or http://host:port
|
|
27
|
+
If not provided, connects directly to exchange.
|
|
28
|
+
"""
|
|
29
|
+
name: str
|
|
30
|
+
api_key: str
|
|
31
|
+
api_secret: str
|
|
32
|
+
base_url: str = ""
|
|
33
|
+
passphrase: str = ""
|
|
34
|
+
proxy_url: Optional[str] = None
|