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.
Files changed (44) hide show
  1. trd_utils/__init__.py +3 -0
  2. trd_utils/cipher/__init__.py +44 -0
  3. trd_utils/common_utils/float_utils.py +21 -0
  4. trd_utils/common_utils/wallet_utils.py +26 -0
  5. trd_utils/date_utils/__init__.py +8 -0
  6. trd_utils/date_utils/datetime_helpers.py +25 -0
  7. trd_utils/exchanges/README.md +203 -0
  8. trd_utils/exchanges/__init__.py +28 -0
  9. trd_utils/exchanges/base_types.py +229 -0
  10. trd_utils/exchanges/binance/__init__.py +13 -0
  11. trd_utils/exchanges/binance/binance_client.py +389 -0
  12. trd_utils/exchanges/binance/binance_types.py +116 -0
  13. trd_utils/exchanges/blofin/__init__.py +6 -0
  14. trd_utils/exchanges/blofin/blofin_client.py +375 -0
  15. trd_utils/exchanges/blofin/blofin_types.py +173 -0
  16. trd_utils/exchanges/bx_ultra/__init__.py +6 -0
  17. trd_utils/exchanges/bx_ultra/bx_types.py +1338 -0
  18. trd_utils/exchanges/bx_ultra/bx_ultra_client.py +1123 -0
  19. trd_utils/exchanges/bx_ultra/bx_utils.py +51 -0
  20. trd_utils/exchanges/errors.py +10 -0
  21. trd_utils/exchanges/exchange_base.py +301 -0
  22. trd_utils/exchanges/hyperliquid/README.md +3 -0
  23. trd_utils/exchanges/hyperliquid/__init__.py +7 -0
  24. trd_utils/exchanges/hyperliquid/hyperliquid_client.py +292 -0
  25. trd_utils/exchanges/hyperliquid/hyperliquid_types.py +183 -0
  26. trd_utils/exchanges/okx/__init__.py +6 -0
  27. trd_utils/exchanges/okx/okx_client.py +219 -0
  28. trd_utils/exchanges/okx/okx_types.py +197 -0
  29. trd_utils/exchanges/price_fetcher.py +48 -0
  30. trd_utils/html_utils/__init__.py +26 -0
  31. trd_utils/html_utils/html_formats.py +72 -0
  32. trd_utils/tradingview/__init__.py +8 -0
  33. trd_utils/tradingview/tradingview_client.py +128 -0
  34. trd_utils/tradingview/tradingview_types.py +185 -0
  35. trd_utils/types_helper/__init__.py +12 -0
  36. trd_utils/types_helper/base_model.py +350 -0
  37. trd_utils/types_helper/decorators.py +20 -0
  38. trd_utils/types_helper/model_config.py +6 -0
  39. trd_utils/types_helper/ultra_list.py +39 -0
  40. trd_utils/types_helper/utils.py +40 -0
  41. trd_utils-0.0.57.dist-info/METADATA +42 -0
  42. trd_utils-0.0.57.dist-info/RECORD +44 -0
  43. trd_utils-0.0.57.dist-info/WHEEL +4 -0
  44. 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,10 @@
1
+
2
+
3
+
4
+ class ExchangeError(RuntimeError):
5
+ """
6
+ Specifies an error coming from the exchange.
7
+ """
8
+ def __init__(self, *args):
9
+ super().__init__(*args)
10
+
@@ -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,3 @@
1
+ # Hyperdash
2
+
3
+ NOTE: [Hyperdash](https://hyperdash.info) is not an exchange, but a tracker for HyperLiquid.
@@ -0,0 +1,7 @@
1
+
2
+ from .hyperliquid_client import HyperLiquidClient
3
+
4
+
5
+ __all__ = [
6
+ "HyperLiquidClient",
7
+ ]
@@ -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
+ ###########################################################