trd-utils 0.0.57__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.
- trd_utils/__init__.py +3 -0
- trd_utils/cipher/__init__.py +44 -0
- trd_utils/common_utils/float_utils.py +21 -0
- trd_utils/common_utils/wallet_utils.py +26 -0
- trd_utils/date_utils/__init__.py +8 -0
- trd_utils/date_utils/datetime_helpers.py +25 -0
- trd_utils/exchanges/README.md +203 -0
- trd_utils/exchanges/__init__.py +28 -0
- trd_utils/exchanges/base_types.py +229 -0
- trd_utils/exchanges/binance/__init__.py +13 -0
- trd_utils/exchanges/binance/binance_client.py +389 -0
- trd_utils/exchanges/binance/binance_types.py +116 -0
- trd_utils/exchanges/blofin/__init__.py +6 -0
- trd_utils/exchanges/blofin/blofin_client.py +375 -0
- trd_utils/exchanges/blofin/blofin_types.py +173 -0
- trd_utils/exchanges/bx_ultra/__init__.py +6 -0
- trd_utils/exchanges/bx_ultra/bx_types.py +1338 -0
- trd_utils/exchanges/bx_ultra/bx_ultra_client.py +1123 -0
- trd_utils/exchanges/bx_ultra/bx_utils.py +51 -0
- trd_utils/exchanges/errors.py +10 -0
- trd_utils/exchanges/exchange_base.py +301 -0
- trd_utils/exchanges/hyperliquid/README.md +3 -0
- trd_utils/exchanges/hyperliquid/__init__.py +7 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_client.py +292 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_types.py +183 -0
- trd_utils/exchanges/okx/__init__.py +6 -0
- trd_utils/exchanges/okx/okx_client.py +219 -0
- trd_utils/exchanges/okx/okx_types.py +197 -0
- trd_utils/exchanges/price_fetcher.py +48 -0
- trd_utils/html_utils/__init__.py +26 -0
- trd_utils/html_utils/html_formats.py +72 -0
- trd_utils/tradingview/__init__.py +8 -0
- trd_utils/tradingview/tradingview_client.py +128 -0
- trd_utils/tradingview/tradingview_types.py +185 -0
- trd_utils/types_helper/__init__.py +12 -0
- trd_utils/types_helper/base_model.py +350 -0
- trd_utils/types_helper/decorators.py +20 -0
- trd_utils/types_helper/model_config.py +6 -0
- trd_utils/types_helper/ultra_list.py +39 -0
- trd_utils/types_helper/utils.py +40 -0
- trd_utils-0.0.57.dist-info/METADATA +42 -0
- trd_utils-0.0.57.dist-info/RECORD +44 -0
- trd_utils-0.0.57.dist-info/WHEEL +4 -0
- trd_utils-0.0.57.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
default_e: str = (
|
|
7
|
+
"\u0039\u0035\u0064\u0036\u0035\u0063\u0037\u0033\u0064\u0063\u0035"
|
|
8
|
+
+ "\u0063\u0034\u0033\u0037"
|
|
9
|
+
)
|
|
10
|
+
default_se: str = "\u0030\u0061\u0065\u0039\u0030\u0031\u0038\u0066\u0062\u0037"
|
|
11
|
+
default_le: str = "\u0066\u0032\u0065\u0061\u0062\u0036\u0039"
|
|
12
|
+
|
|
13
|
+
long_accept_header1: str = (
|
|
14
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
|
|
15
|
+
+ "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def do_ultra_ss(
|
|
19
|
+
e_param: str,
|
|
20
|
+
se_param: str,
|
|
21
|
+
le_param: str,
|
|
22
|
+
timestamp: int,
|
|
23
|
+
trace_id: str,
|
|
24
|
+
device_id: str,
|
|
25
|
+
platform_id: str,
|
|
26
|
+
app_version: str,
|
|
27
|
+
payload_data: str = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
if not e_param:
|
|
30
|
+
e_param = default_e
|
|
31
|
+
|
|
32
|
+
if not se_param:
|
|
33
|
+
se_param = default_se
|
|
34
|
+
|
|
35
|
+
if not le_param:
|
|
36
|
+
le_param = default_le
|
|
37
|
+
|
|
38
|
+
first_part = f"{e_param}{se_param}{le_param}{timestamp}{trace_id}"
|
|
39
|
+
if not payload_data:
|
|
40
|
+
payload_data = "{}"
|
|
41
|
+
elif not isinstance(payload_data, str):
|
|
42
|
+
# convert to json
|
|
43
|
+
payload_data = json.dumps(payload_data, separators=(",", ":"), sort_keys=True)
|
|
44
|
+
|
|
45
|
+
if not trace_id:
|
|
46
|
+
trace_id = uuid.uuid4().hex.replace("-", "")
|
|
47
|
+
|
|
48
|
+
whole_parts = f"{first_part}{device_id}{platform_id}{app_version}{payload_data}"
|
|
49
|
+
|
|
50
|
+
# do SHA256
|
|
51
|
+
return hashlib.sha256(whole_parts.encode()).hexdigest().upper()
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Callable, Type
|
|
7
|
+
from abc import ABC
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from websockets.asyncio.connection import Connection as WSConnection
|
|
14
|
+
|
|
15
|
+
from trd_utils.exchanges.base_types import (
|
|
16
|
+
UnifiedFuturesMarketInfo,
|
|
17
|
+
UnifiedTraderInfo,
|
|
18
|
+
UnifiedTraderPositions,
|
|
19
|
+
)
|
|
20
|
+
from trd_utils.types_helper.base_model import BaseModel
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JWTManager:
|
|
26
|
+
_jwt_string: str = None
|
|
27
|
+
|
|
28
|
+
def __init__(self, jwt_string: str):
|
|
29
|
+
self._jwt_string = jwt_string
|
|
30
|
+
try:
|
|
31
|
+
payload_b64 = self._jwt_string.split(".")[1]
|
|
32
|
+
payload_bytes = base64.urlsafe_b64decode(payload_b64 + "==")
|
|
33
|
+
self.payload = json.loads(payload_bytes)
|
|
34
|
+
except Exception:
|
|
35
|
+
self.payload = {}
|
|
36
|
+
|
|
37
|
+
def is_expired(self):
|
|
38
|
+
if "exp" not in self.payload:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
return time.time() > self.payload["exp"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ExchangeBase(ABC):
|
|
45
|
+
###########################################################
|
|
46
|
+
# region client parameters
|
|
47
|
+
user_agent: str = "okhttp/4.12.0"
|
|
48
|
+
x_requested_with: str = None
|
|
49
|
+
httpx_client: httpx.AsyncClient = None
|
|
50
|
+
account_name: str = "default"
|
|
51
|
+
sessions_dir: str = "sessions"
|
|
52
|
+
|
|
53
|
+
authorization_token: str = None
|
|
54
|
+
device_id: str = None
|
|
55
|
+
trace_id: str = None
|
|
56
|
+
app_version: str = "4.28.3"
|
|
57
|
+
x_router_tag: str = "gray-develop"
|
|
58
|
+
platform_id: str = "10"
|
|
59
|
+
install_channel: str = "officialAPK"
|
|
60
|
+
channel_header: str = "officialAPK"
|
|
61
|
+
|
|
62
|
+
# The name of the exchange.
|
|
63
|
+
exchange_name: str = None
|
|
64
|
+
|
|
65
|
+
jwt_manager: JWTManager = None
|
|
66
|
+
|
|
67
|
+
_fav_letter: str = "^"
|
|
68
|
+
|
|
69
|
+
# the lock for internal operations.
|
|
70
|
+
_internal_lock: asyncio.Lock = None
|
|
71
|
+
|
|
72
|
+
# extra tasks to be cancelled when the client closes.
|
|
73
|
+
extra_tasks: list[asyncio.Task] = None
|
|
74
|
+
|
|
75
|
+
# the price ws connection to be closed when this client is closed.
|
|
76
|
+
price_ws_connection: WSConnection = None
|
|
77
|
+
# endregion
|
|
78
|
+
###########################################################
|
|
79
|
+
# region constructor method
|
|
80
|
+
|
|
81
|
+
def __init__(self):
|
|
82
|
+
self._internal_lock = asyncio.Lock()
|
|
83
|
+
self.extra_tasks = []
|
|
84
|
+
|
|
85
|
+
# endregion
|
|
86
|
+
###########################################################
|
|
87
|
+
# region abstract trading methods
|
|
88
|
+
|
|
89
|
+
async def get_unified_trader_positions(
|
|
90
|
+
self,
|
|
91
|
+
uid: int | str,
|
|
92
|
+
min_margin: Decimal = 0,
|
|
93
|
+
) -> UnifiedTraderPositions:
|
|
94
|
+
"""
|
|
95
|
+
Returns the unified version of all currently open positions of the specific
|
|
96
|
+
trader. Note that different exchanges might fill different fields, according to the
|
|
97
|
+
data they provide in their public APIs.
|
|
98
|
+
If you want to fetch past positions history, you have to use another method.
|
|
99
|
+
"""
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
"This method is not implemented in ExchangeBase class. "
|
|
102
|
+
"Please use a real exchange class inheriting and implementing this method."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def get_unified_trader_info(self, uid: int | str) -> UnifiedTraderInfo:
|
|
106
|
+
"""
|
|
107
|
+
Returns information about a specific trader.
|
|
108
|
+
Different exchanges might return and fill different information according to the
|
|
109
|
+
data returned from their public APIs.
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError(
|
|
112
|
+
"This method is not implemented in ExchangeBase class. "
|
|
113
|
+
"Please use a real exchange class inheriting and implementing this method."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async def get_unified_futures_market_info(
|
|
117
|
+
self,
|
|
118
|
+
sort_by: str = "percentage_change_24h",
|
|
119
|
+
descending: bool = True,
|
|
120
|
+
allow_delisted: bool = False,
|
|
121
|
+
filter_quote_token: str | None = None,
|
|
122
|
+
raise_on_invalid: bool = False,
|
|
123
|
+
filter_func: Callable | None = None,
|
|
124
|
+
) -> UnifiedFuturesMarketInfo:
|
|
125
|
+
"""
|
|
126
|
+
Returns the unified version of futures market information.
|
|
127
|
+
Different exchanges might return and fill different information according to the
|
|
128
|
+
data returned from their public APIs.
|
|
129
|
+
"""
|
|
130
|
+
raise NotImplementedError(
|
|
131
|
+
"This method is not implemented in ExchangeBase class. "
|
|
132
|
+
"Please use a real exchange class inheriting and implementing this method."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# endregion
|
|
136
|
+
###########################################################
|
|
137
|
+
# region client helper methods
|
|
138
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
async def invoke_get(
|
|
142
|
+
self,
|
|
143
|
+
url: str,
|
|
144
|
+
headers: dict | None = None,
|
|
145
|
+
params: dict | None = None,
|
|
146
|
+
model_type: Type[BaseModel] | None = None,
|
|
147
|
+
parse_float=Decimal,
|
|
148
|
+
raw_data: bool = False,
|
|
149
|
+
) -> "BaseModel":
|
|
150
|
+
"""
|
|
151
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
152
|
+
"""
|
|
153
|
+
response = await self.httpx_client.get(
|
|
154
|
+
url=url,
|
|
155
|
+
headers=headers,
|
|
156
|
+
params=params,
|
|
157
|
+
)
|
|
158
|
+
return self._handle_response(
|
|
159
|
+
response=response,
|
|
160
|
+
model_type=model_type,
|
|
161
|
+
parse_float=parse_float,
|
|
162
|
+
raw_data=raw_data,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def invoke_post(
|
|
166
|
+
self,
|
|
167
|
+
url: str,
|
|
168
|
+
headers: dict | None = None,
|
|
169
|
+
params: dict | None = None,
|
|
170
|
+
content: dict | str | bytes = "",
|
|
171
|
+
model_type: Type[BaseModel] | None = None,
|
|
172
|
+
parse_float=Decimal,
|
|
173
|
+
raw_data: bool = False,
|
|
174
|
+
) -> "BaseModel":
|
|
175
|
+
"""
|
|
176
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
if isinstance(content, dict):
|
|
180
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
181
|
+
|
|
182
|
+
response = await self.httpx_client.post(
|
|
183
|
+
url=url,
|
|
184
|
+
headers=headers,
|
|
185
|
+
params=params,
|
|
186
|
+
content=content,
|
|
187
|
+
)
|
|
188
|
+
return self._handle_response(
|
|
189
|
+
response=response,
|
|
190
|
+
model_type=model_type,
|
|
191
|
+
parse_float=parse_float,
|
|
192
|
+
raw_data=raw_data,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _handle_response(
|
|
196
|
+
self,
|
|
197
|
+
response: httpx.Response,
|
|
198
|
+
model_type: Type[BaseModel] | None = None,
|
|
199
|
+
parse_float=Decimal,
|
|
200
|
+
raw_data: bool = False,
|
|
201
|
+
) -> "BaseModel":
|
|
202
|
+
if raw_data:
|
|
203
|
+
return response.content
|
|
204
|
+
|
|
205
|
+
j_obj = self._resp_to_json(
|
|
206
|
+
response=response,
|
|
207
|
+
parse_float=parse_float,
|
|
208
|
+
)
|
|
209
|
+
if not model_type:
|
|
210
|
+
return j_obj
|
|
211
|
+
|
|
212
|
+
return model_type.deserialize(j_obj)
|
|
213
|
+
|
|
214
|
+
def _resp_to_json(
|
|
215
|
+
self,
|
|
216
|
+
response: httpx.Response,
|
|
217
|
+
parse_float=None,
|
|
218
|
+
) -> dict:
|
|
219
|
+
try:
|
|
220
|
+
return response.json(parse_float=parse_float)
|
|
221
|
+
except UnicodeDecodeError:
|
|
222
|
+
# try to decompress manually
|
|
223
|
+
import gzip
|
|
224
|
+
import brotli
|
|
225
|
+
|
|
226
|
+
content_encoding = response.headers.get("Content-Encoding", "").lower()
|
|
227
|
+
content = response.content
|
|
228
|
+
|
|
229
|
+
if "gzip" in content_encoding:
|
|
230
|
+
content = gzip.decompress(content)
|
|
231
|
+
elif "br" in content_encoding:
|
|
232
|
+
content = brotli.decompress(content)
|
|
233
|
+
elif "deflate" in content_encoding:
|
|
234
|
+
import zlib
|
|
235
|
+
|
|
236
|
+
content = zlib.decompress(content, -zlib.MAX_WBITS)
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"failed to detect content encoding: {content_encoding}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Now parse the decompressed content
|
|
243
|
+
return json.loads(content.decode("utf-8"), parse_float=parse_float)
|
|
244
|
+
|
|
245
|
+
async def _apply_filter_func(
|
|
246
|
+
self,
|
|
247
|
+
filter_func: Callable,
|
|
248
|
+
func_args: dict,
|
|
249
|
+
) -> bool:
|
|
250
|
+
if inspect.iscoroutinefunction(filter_func):
|
|
251
|
+
return await filter_func(**func_args)
|
|
252
|
+
elif inspect.isfunction(filter_func) or callable(filter_func):
|
|
253
|
+
result = filter_func(**func_args)
|
|
254
|
+
|
|
255
|
+
if inspect.iscoroutine(result):
|
|
256
|
+
return await result
|
|
257
|
+
return result
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError("filter_func must be a function or coroutine function.")
|
|
260
|
+
|
|
261
|
+
async def __aenter__(self):
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
async def __aexit__(
|
|
265
|
+
self,
|
|
266
|
+
exc_type=None,
|
|
267
|
+
exc_value=None,
|
|
268
|
+
traceback=None,
|
|
269
|
+
) -> None:
|
|
270
|
+
await self.aclose()
|
|
271
|
+
|
|
272
|
+
async def aclose(self) -> None:
|
|
273
|
+
await self._internal_lock.acquire()
|
|
274
|
+
await self.httpx_client.aclose()
|
|
275
|
+
|
|
276
|
+
if self.price_ws_connection:
|
|
277
|
+
try:
|
|
278
|
+
await self.price_ws_connection.close()
|
|
279
|
+
except Exception as ex:
|
|
280
|
+
logger.warning(f"failed to close ws connection: {ex}")
|
|
281
|
+
|
|
282
|
+
self._internal_lock.release()
|
|
283
|
+
|
|
284
|
+
# endregion
|
|
285
|
+
###########################################################
|
|
286
|
+
# region data-files related methods
|
|
287
|
+
|
|
288
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
291
|
+
"""
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Saves current information to the session file.
|
|
297
|
+
"""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# endregion
|
|
301
|
+
###########################################################
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytz
|
|
11
|
+
|
|
12
|
+
from trd_utils.cipher import AESCipher
|
|
13
|
+
from trd_utils.common_utils.wallet_utils import shorten_wallet_address
|
|
14
|
+
from trd_utils.exchanges.base_types import (
|
|
15
|
+
UnifiedFuturesMarketInfo,
|
|
16
|
+
UnifiedPositionInfo,
|
|
17
|
+
UnifiedSingleFutureMarketInfo,
|
|
18
|
+
UnifiedTraderInfo,
|
|
19
|
+
UnifiedTraderPositions,
|
|
20
|
+
)
|
|
21
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
22
|
+
from trd_utils.exchanges.hyperliquid.hyperliquid_types import (
|
|
23
|
+
MetaAssetCtxResponse,
|
|
24
|
+
TraderPositionsInfoResponse,
|
|
25
|
+
)
|
|
26
|
+
from trd_utils.types_helper import new_list
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
BASE_PROFILE_URL = "https://hypurrscan.io/address/"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HyperLiquidClient(ExchangeBase):
|
|
34
|
+
###########################################################
|
|
35
|
+
# region client parameters
|
|
36
|
+
hyperliquid_api_base_host: str = "https://api.hyperliquid.xyz"
|
|
37
|
+
hyperliquid_api_base_url: str = "https://api.hyperliquid.xyz"
|
|
38
|
+
origin_header: str = "app.hyperliquid.xy"
|
|
39
|
+
default_quote_token: str = "USDC"
|
|
40
|
+
|
|
41
|
+
# endregion
|
|
42
|
+
###########################################################
|
|
43
|
+
# region client constructor
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
account_name: str = "default",
|
|
47
|
+
http_verify: bool = True,
|
|
48
|
+
fav_letter: str = "^",
|
|
49
|
+
read_session_file: bool = False,
|
|
50
|
+
sessions_dir: str = "sessions",
|
|
51
|
+
use_http1: bool = True,
|
|
52
|
+
use_http2: bool = False,
|
|
53
|
+
):
|
|
54
|
+
# it looks like hyperliquid's api endpoints don't support http2 :(
|
|
55
|
+
self.httpx_client = httpx.AsyncClient(
|
|
56
|
+
verify=http_verify,
|
|
57
|
+
http1=use_http1,
|
|
58
|
+
http2=use_http2,
|
|
59
|
+
)
|
|
60
|
+
self.account_name = account_name
|
|
61
|
+
self._fav_letter = fav_letter
|
|
62
|
+
self.sessions_dir = sessions_dir
|
|
63
|
+
self.exchange_name = "hyperliquid"
|
|
64
|
+
|
|
65
|
+
super().__init__()
|
|
66
|
+
|
|
67
|
+
if read_session_file:
|
|
68
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.hl")
|
|
69
|
+
|
|
70
|
+
# endregion
|
|
71
|
+
###########################################################
|
|
72
|
+
# region info endpoints
|
|
73
|
+
async def get_trader_positions_info(
|
|
74
|
+
self,
|
|
75
|
+
uid: int | str,
|
|
76
|
+
) -> TraderPositionsInfoResponse:
|
|
77
|
+
payload = {
|
|
78
|
+
"type": "clearinghouseState",
|
|
79
|
+
"user": f"{uid}",
|
|
80
|
+
}
|
|
81
|
+
headers = self.get_headers()
|
|
82
|
+
return await self.invoke_post(
|
|
83
|
+
f"{self.hyperliquid_api_base_host}/info",
|
|
84
|
+
headers=headers,
|
|
85
|
+
content=payload,
|
|
86
|
+
model_type=TraderPositionsInfoResponse,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def get_meta_asset_ctx_info(
|
|
90
|
+
self,
|
|
91
|
+
allow_delisted: bool = False,
|
|
92
|
+
) -> MetaAssetCtxResponse:
|
|
93
|
+
payload = {
|
|
94
|
+
"type": "metaAndAssetCtxs",
|
|
95
|
+
}
|
|
96
|
+
headers = self.get_headers()
|
|
97
|
+
data = await self.invoke_post(
|
|
98
|
+
f"{self.hyperliquid_api_base_host}/info",
|
|
99
|
+
headers=headers,
|
|
100
|
+
content=payload,
|
|
101
|
+
model_type=None, # it has a weird response structure
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return MetaAssetCtxResponse.parse_from_api_resp(
|
|
105
|
+
data=data,
|
|
106
|
+
allow_delisted=allow_delisted,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# endregion
|
|
110
|
+
###########################################################
|
|
111
|
+
# region another-thing
|
|
112
|
+
# async def get_another_thing_info(self, uid: int) -> AnotherThingInfoResponse:
|
|
113
|
+
# payload = {
|
|
114
|
+
# "uid": uid,
|
|
115
|
+
# }
|
|
116
|
+
# headers = self.get_headers()
|
|
117
|
+
# return await self.invoke_post(
|
|
118
|
+
# f"{self.hyperliquid_api_base_url}/another-thing/info",
|
|
119
|
+
# headers=headers,
|
|
120
|
+
# content=payload,
|
|
121
|
+
# model_type=CopyTraderInfoResponse,
|
|
122
|
+
# )
|
|
123
|
+
|
|
124
|
+
# endregion
|
|
125
|
+
###########################################################
|
|
126
|
+
# region client helper methods
|
|
127
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
128
|
+
the_headers = {
|
|
129
|
+
# "Host": self.hyperliquid_api_base_host,
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"Accept": "application/json",
|
|
132
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
133
|
+
"User-Agent": self.user_agent,
|
|
134
|
+
"Connection": "close",
|
|
135
|
+
"appsiteid": "0",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if self.x_requested_with:
|
|
139
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
140
|
+
|
|
141
|
+
if needs_auth:
|
|
142
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
143
|
+
return the_headers
|
|
144
|
+
|
|
145
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
148
|
+
"""
|
|
149
|
+
# check if path exists
|
|
150
|
+
target_path = Path(file_path)
|
|
151
|
+
if not target_path.exists():
|
|
152
|
+
return self._save_session_file(file_path=file_path)
|
|
153
|
+
|
|
154
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
155
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
156
|
+
json_data: dict = json.loads(content)
|
|
157
|
+
|
|
158
|
+
self.authorization_token = json_data.get(
|
|
159
|
+
"authorization_token",
|
|
160
|
+
self.authorization_token,
|
|
161
|
+
)
|
|
162
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
163
|
+
|
|
164
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Saves current information to the session file.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
json_data = {
|
|
170
|
+
"authorization_token": self.authorization_token,
|
|
171
|
+
"user_agent": self.user_agent,
|
|
172
|
+
}
|
|
173
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
174
|
+
target_path = Path(file_path)
|
|
175
|
+
if not target_path.exists():
|
|
176
|
+
target_path.mkdir(parents=True)
|
|
177
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
178
|
+
|
|
179
|
+
# endregion
|
|
180
|
+
###########################################################
|
|
181
|
+
# region unified methods
|
|
182
|
+
async def get_unified_trader_positions(
|
|
183
|
+
self,
|
|
184
|
+
uid: int | str,
|
|
185
|
+
min_margin: Decimal = 0,
|
|
186
|
+
) -> UnifiedTraderPositions:
|
|
187
|
+
result = await self.get_trader_positions_info(
|
|
188
|
+
uid=uid,
|
|
189
|
+
)
|
|
190
|
+
unified_result = UnifiedTraderPositions()
|
|
191
|
+
unified_result.positions = new_list()
|
|
192
|
+
for position_container in result.asset_positions:
|
|
193
|
+
position = position_container.position
|
|
194
|
+
if min_margin and (
|
|
195
|
+
not position.margin_used or position.margin_used < min_margin
|
|
196
|
+
):
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
unified_pos = UnifiedPositionInfo()
|
|
200
|
+
unified_pos.position_id = position.get_position_id()
|
|
201
|
+
unified_pos.position_pnl = round(position.unrealized_pnl, 3)
|
|
202
|
+
unified_pos.position_side = position.get_side()
|
|
203
|
+
unified_pos.margin_mode = position.leverage.type
|
|
204
|
+
unified_pos.position_leverage = Decimal(position.leverage.value)
|
|
205
|
+
unified_pos.position_pair = f"{position.coin}/{self.default_quote_token}"
|
|
206
|
+
unified_pos.open_time = datetime.now(
|
|
207
|
+
pytz.UTC
|
|
208
|
+
) # hyperliquid doesn't provide this...
|
|
209
|
+
unified_pos.open_price = position.entry_px
|
|
210
|
+
unified_pos.open_price_unit = self.default_quote_token
|
|
211
|
+
unified_pos.position_size = abs(position.szi)
|
|
212
|
+
unified_pos.initial_margin = position.margin_used
|
|
213
|
+
unified_result.positions.append(unified_pos)
|
|
214
|
+
|
|
215
|
+
return unified_result
|
|
216
|
+
|
|
217
|
+
async def get_unified_trader_info(
|
|
218
|
+
self,
|
|
219
|
+
uid: int | str,
|
|
220
|
+
) -> UnifiedTraderInfo:
|
|
221
|
+
if not isinstance(uid, str):
|
|
222
|
+
uid = str(uid)
|
|
223
|
+
# sadly hyperliquid doesn't really have an endpoint to fetch information
|
|
224
|
+
# so we have to somehow *fake* these...
|
|
225
|
+
# maybe in future try to find a better way?
|
|
226
|
+
unified_info = UnifiedTraderInfo()
|
|
227
|
+
unified_info.trader_id = uid
|
|
228
|
+
unified_info.trader_name = shorten_wallet_address(uid)
|
|
229
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
|
|
230
|
+
unified_info.win_rate = None
|
|
231
|
+
|
|
232
|
+
return unified_info
|
|
233
|
+
|
|
234
|
+
async def get_unified_futures_market_info(
|
|
235
|
+
self,
|
|
236
|
+
sort_by: str = "percentage_change_24h",
|
|
237
|
+
descending: bool = True,
|
|
238
|
+
allow_delisted: bool = False,
|
|
239
|
+
filter_quote_token: str | None = None,
|
|
240
|
+
raise_on_invalid: bool = False,
|
|
241
|
+
filter_func: Callable | None = None,
|
|
242
|
+
) -> UnifiedFuturesMarketInfo:
|
|
243
|
+
asset_ctxs = await self.get_meta_asset_ctx_info(
|
|
244
|
+
allow_delisted=allow_delisted,
|
|
245
|
+
)
|
|
246
|
+
unified_info = UnifiedFuturesMarketInfo()
|
|
247
|
+
unified_info.sorted_markets = []
|
|
248
|
+
|
|
249
|
+
for current_asset in asset_ctxs.assets:
|
|
250
|
+
current_market = UnifiedSingleFutureMarketInfo()
|
|
251
|
+
current_market.name = current_asset.symbol
|
|
252
|
+
current_market.pair = f"{current_asset.symbol}/{self.default_quote_token}"
|
|
253
|
+
current_market.price = current_asset.mark_px
|
|
254
|
+
current_market.previous_day_price = current_asset.prev_day_px
|
|
255
|
+
current_market.absolute_change_24h = current_asset.change_abs
|
|
256
|
+
current_market.percentage_change_24h = current_asset.change_pct
|
|
257
|
+
current_market.funding_rate = current_asset.funding
|
|
258
|
+
current_market.daily_volume = current_asset.day_ntl_vlm
|
|
259
|
+
current_market.open_interest = current_asset.open_interest
|
|
260
|
+
|
|
261
|
+
if filter_func:
|
|
262
|
+
filter_args = {
|
|
263
|
+
"pair": current_market.pair,
|
|
264
|
+
"market_info": current_market,
|
|
265
|
+
"exchange_client": self,
|
|
266
|
+
}
|
|
267
|
+
# this is defined in exchange base.
|
|
268
|
+
should_include = await self._apply_filter_func(
|
|
269
|
+
filter_func=filter_func,
|
|
270
|
+
func_args=filter_args,
|
|
271
|
+
)
|
|
272
|
+
if not should_include:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
unified_info.sorted_markets.append(current_market)
|
|
276
|
+
|
|
277
|
+
if not sort_by:
|
|
278
|
+
# we won't sort anything
|
|
279
|
+
return unified_info
|
|
280
|
+
|
|
281
|
+
def key_fn(market: UnifiedSingleFutureMarketInfo):
|
|
282
|
+
return getattr(market, sort_by, Decimal(0))
|
|
283
|
+
|
|
284
|
+
unified_info.sorted_markets = new_list(sorted(
|
|
285
|
+
unified_info.sorted_markets,
|
|
286
|
+
key=key_fn,
|
|
287
|
+
reverse=descending,
|
|
288
|
+
))
|
|
289
|
+
return unified_info
|
|
290
|
+
|
|
291
|
+
# endregion
|
|
292
|
+
###########################################################
|