trd-utils 0.0.15__tar.gz → 0.0.17__tar.gz

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.

Potentially problematic release.


This version of trd-utils might be problematic. Click here for more details.

Files changed (33) hide show
  1. {trd_utils-0.0.15 → trd_utils-0.0.17}/PKG-INFO +1 -1
  2. {trd_utils-0.0.15 → trd_utils-0.0.17}/pyproject.toml +1 -1
  3. trd_utils-0.0.17/trd_utils/__init__.py +3 -0
  4. trd_utils-0.0.17/trd_utils/date_utils/__init__.py +8 -0
  5. trd_utils-0.0.17/trd_utils/date_utils/datetime_helpers.py +16 -0
  6. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/__init__.py +7 -7
  7. trd_utils-0.0.17/trd_utils/exchanges/base_types.py +106 -0
  8. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/blofin/__init__.py +1 -1
  9. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/blofin/blofin_client.py +57 -67
  10. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/blofin/blofin_types.py +1 -1
  11. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +126 -70
  12. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/exchange_base.py +36 -11
  13. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/hyperliquid/__init__.py +1 -1
  14. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +2 -50
  15. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/types_helper/__init__.py +1 -1
  16. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/types_helper/base_model.py +1 -1
  17. trd_utils-0.0.15/trd_utils/__init__.py +0 -3
  18. trd_utils-0.0.15/trd_utils/exchanges/base_types.py +0 -53
  19. {trd_utils-0.0.15 → trd_utils-0.0.17}/LICENSE +0 -0
  20. {trd_utils-0.0.15 → trd_utils-0.0.17}/README.md +0 -0
  21. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/cipher/__init__.py +0 -0
  22. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/common_utils/float_utils.py +0 -0
  23. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/README.md +0 -0
  24. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
  25. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/bx_ultra/bx_types.py +0 -0
  26. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
  27. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/hyperliquid/README.md +0 -0
  28. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +0 -0
  29. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/html_utils/__init__.py +0 -0
  30. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/html_utils/html_formats.py +0 -0
  31. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/tradingview/__init__.py +0 -0
  32. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/tradingview/tradingview_client.py +0 -0
  33. {trd_utils-0.0.15 → trd_utils-0.0.17}/trd_utils/tradingview/tradingview_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trd_utils
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary: Common Basic Utils for Python3. By ALiwoto.
5
5
  Keywords: utils,trd_utils,basic-utils,common-utils
6
6
  Author: ALiwoto
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "trd_utils"
3
- version = "0.0.15"
3
+ version = "0.0.17"
4
4
  description = "Common Basic Utils for Python3. By ALiwoto."
5
5
  authors = ["ALiwoto <aminnimaj@gmail.com>"]
6
6
  packages = [
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.0.17"
3
+
@@ -0,0 +1,8 @@
1
+
2
+ from .datetime_helpers import dt_from_ts
3
+
4
+
5
+ __all__ = [
6
+ "dt_from_ts",
7
+ ]
8
+
@@ -0,0 +1,16 @@
1
+
2
+
3
+
4
+
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ def dt_from_ts(timestamp: float) -> datetime:
9
+ """
10
+ Return a datetime from a timestamp.
11
+ :param timestamp: timestamp in seconds or milliseconds
12
+ """
13
+ if timestamp > 1e10:
14
+ # Timezone in ms - convert to seconds
15
+ timestamp /= 1000
16
+ return datetime.fromtimestamp(timestamp, tz=timezone.utc)
@@ -11,11 +11,11 @@ from .hyperliquid import HyperLiquidClient
11
11
 
12
12
 
13
13
  __all__ = [
14
- ExchangeBase,
15
- UnifiedTraderInfo,
16
- UnifiedTraderPositions,
17
- UnifiedPositionInfo,
18
- BXUltraClient,
19
- BlofinClient,
20
- HyperLiquidClient,
14
+ "ExchangeBase",
15
+ "UnifiedTraderInfo",
16
+ "UnifiedTraderPositions",
17
+ "UnifiedPositionInfo",
18
+ "BXUltraClient",
19
+ "BlofinClient",
20
+ "HyperLiquidClient",
21
21
  ]
@@ -0,0 +1,106 @@
1
+
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+
6
+ from trd_utils.types_helper.base_model import BaseModel
7
+
8
+ class UnifiedPositionInfo(BaseModel):
9
+ # The id of the position.
10
+ position_id: str = None
11
+
12
+ # The pnl (profit) of the position.
13
+ position_pnl: Decimal = None
14
+
15
+ # The position side, either "LONG" or "SHORT".
16
+ position_side: str = None
17
+
18
+ # The position's leverage.
19
+ position_leverage: Decimal = None
20
+
21
+ # The margin mode; e.g. cross or isolated. Please note that
22
+ # different exchanges might provide different kinds of margin modes,
23
+ # depending on what they support, that's why we can't support a unified
24
+ # enum type for this as of yet.
25
+ margin_mode: str = None
26
+
27
+ # The formatted pair string of this position.
28
+ # e.g. BTC/USDT.
29
+ position_pair: str = None
30
+
31
+ # The open time of this position.
32
+ # Note that not all public APIs might provide this field.
33
+ open_time: datetime = None
34
+
35
+ # Open price of the position.
36
+ open_price: Decimal = None
37
+
38
+ # The base unit that the open-price is based on (e.g. USD, USDT, USDC)
39
+ open_price_unit: str | None = None
40
+
41
+ def __str__(self):
42
+ parts = []
43
+
44
+ # Add position pair and ID
45
+ parts.append(f"Position: {self.position_pair or 'Unknown'} (ID: {self.position_id or 'N/A'})")
46
+
47
+ # Add side and leverage
48
+ side_str = f"Side: {self.position_side or 'Unknown'}"
49
+ if self.position_leverage is not None:
50
+ side_str += f", {self.position_leverage}x"
51
+ parts.append(side_str)
52
+
53
+ # Add margin mode if available
54
+ if self.margin_mode:
55
+ parts.append(f"Margin: {self.margin_mode}")
56
+
57
+ # Add open price if available
58
+ price_str = "Open price: "
59
+ if self.open_price is not None:
60
+ price_str += f"{self.open_price}"
61
+ if self.open_price_unit:
62
+ price_str += f" {self.open_price_unit}"
63
+ else:
64
+ price_str += "N/A"
65
+ parts.append(price_str)
66
+
67
+ # Add open time if available
68
+ if self.open_time:
69
+ parts.append(f"Opened: {self.open_time.strftime('%Y-%m-%d %H:%M:%S')}")
70
+
71
+ # Add PNL if available
72
+ if self.position_pnl is not None:
73
+ parts.append(f"PNL: {self.position_pnl}")
74
+
75
+ return " | ".join(parts)
76
+
77
+ def __repr__(self):
78
+ return self.__str__()
79
+
80
+ class UnifiedTraderPositions(BaseModel):
81
+ positions: list[UnifiedPositionInfo] = None
82
+
83
+ class UnifiedTraderInfo(BaseModel):
84
+ # Trader's id. Either int or str. In DEXes (such as HyperLiquid),
85
+ # this might be wallet address of the trader.
86
+ trader_id: int | str = None
87
+
88
+ # Name of the trader
89
+ trader_name: str = None
90
+
91
+ # The URL in which we can see the trader's profile
92
+ trader_url: str = None
93
+
94
+ # Trader's win-rate. Not all exchanges might support this field.
95
+ win_rate: Decimal = None
96
+
97
+ def __str__(self):
98
+ return (
99
+ f"Trader: {self.trader_name} (ID: {self.trader_id})"
100
+ f"{' | Win Rate: ' + str(round(self.win_rate, 2))}"
101
+ f"{' | Profile: ' + self.trader_url}"
102
+ )
103
+
104
+ def __repr__(self):
105
+ return self.__str__()
106
+
@@ -2,5 +2,5 @@
2
2
  from .blofin_client import BlofinClient
3
3
 
4
4
  __all__ = [
5
- BlofinClient,
5
+ "BlofinClient",
6
6
  ]
@@ -2,15 +2,18 @@ import asyncio
2
2
  from decimal import Decimal
3
3
  import json
4
4
  import logging
5
- from typing import Type
6
5
  import httpx
7
6
 
8
7
  import time
9
8
  from pathlib import Path
10
9
 
11
- from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
10
+ from trd_utils.date_utils.datetime_helpers import dt_from_ts
11
+ from trd_utils.exchanges.base_types import (
12
+ UnifiedPositionInfo,
13
+ UnifiedTraderInfo,
14
+ UnifiedTraderPositions,
15
+ )
12
16
  from trd_utils.exchanges.blofin.blofin_types import (
13
- BlofinApiResponse,
14
17
  CmsColorResponse,
15
18
  CopyTraderAllOrderHistory,
16
19
  CopyTraderAllOrderList,
@@ -22,6 +25,9 @@ from trd_utils.exchanges.blofin.blofin_types import (
22
25
  from trd_utils.cipher import AESCipher
23
26
  from trd_utils.exchanges.exchange_base import ExchangeBase
24
27
 
28
+
29
+ BASE_PROFILE_URL = "https://blofin.com/copy-trade/details/"
30
+
25
31
  logger = logging.getLogger(__name__)
26
32
 
27
33
 
@@ -65,7 +71,7 @@ class BlofinClient(ExchangeBase):
65
71
  return await self.invoke_get(
66
72
  f"{self.blofin_api_base_url}/cms/share_config",
67
73
  headers=headers,
68
- model=ShareConfigResponse,
74
+ model_type=ShareConfigResponse,
69
75
  )
70
76
 
71
77
  async def get_cms_color(self) -> CmsColorResponse:
@@ -73,7 +79,7 @@ class BlofinClient(ExchangeBase):
73
79
  return await self.invoke_get(
74
80
  f"{self.blofin_api_base_url}/cms/color",
75
81
  headers=headers,
76
- model=CmsColorResponse,
82
+ model_type=CmsColorResponse,
77
83
  )
78
84
 
79
85
  # endregion
@@ -88,26 +94,26 @@ class BlofinClient(ExchangeBase):
88
94
  f"{self.blofin_api_base_url}/copy/trader/info",
89
95
  headers=headers,
90
96
  content=payload,
91
- model=CopyTraderInfoResponse,
97
+ model_type=CopyTraderInfoResponse,
92
98
  )
93
99
 
94
100
  async def get_copy_trader_order_list(
95
101
  self,
96
- uid: int,
102
+ uid: int | str,
97
103
  from_param: int = 0,
98
104
  limit_param: int = 20,
99
105
  ) -> CopyTraderOrderListResponse:
100
106
  payload = {
101
107
  "from": from_param,
102
108
  "limit": limit_param,
103
- "uid": uid,
109
+ "uid": int(uid),
104
110
  }
105
111
  headers = self.get_headers()
106
112
  return await self.invoke_post(
107
113
  f"{self.blofin_api_base_url}/copy/trader/order/list",
108
114
  headers=headers,
109
115
  content=payload,
110
- model=CopyTraderOrderListResponse,
116
+ model_type=CopyTraderOrderListResponse,
111
117
  )
112
118
 
113
119
  async def get_copy_trader_all_order_list(
@@ -133,11 +139,13 @@ class BlofinClient(ExchangeBase):
133
139
  from_param=current_id_from,
134
140
  limit_param=chunk_limit,
135
141
  )
136
- if (
137
- not current_result
138
- or not isinstance(current_result, CopyTraderOrderListResponse)
139
- or not current_result.data
140
- ):
142
+ if not isinstance(current_result, CopyTraderOrderListResponse):
143
+ raise ValueError(
144
+ "get_copy_trader_order_list returned invalid value of "
145
+ f"{type(current_result)}",
146
+ )
147
+ if not current_result.data:
148
+ # we no longer have anything else here
141
149
  return result
142
150
 
143
151
  if current_result.data[0].id == current_id_from:
@@ -177,7 +185,7 @@ class BlofinClient(ExchangeBase):
177
185
  f"{self.blofin_api_base_url}/copy/trader/order/history",
178
186
  headers=headers,
179
187
  content=payload,
180
- model=CopyTraderOrderHistoryResponse,
188
+ model_type=CopyTraderOrderHistoryResponse,
181
189
  )
182
190
 
183
191
  async def get_copy_trader_all_order_history(
@@ -256,56 +264,6 @@ class BlofinClient(ExchangeBase):
256
264
  the_headers["Authorization"] = f"Bearer {self.authorization_token}"
257
265
  return the_headers
258
266
 
259
- async def invoke_get(
260
- self,
261
- url: str,
262
- headers: dict | None = None,
263
- params: dict | None = None,
264
- model: Type[BlofinApiResponse] | None = None,
265
- parse_float=Decimal,
266
- ) -> "BlofinApiResponse":
267
- """
268
- Invokes the specific request to the specific url with the specific params and headers.
269
- """
270
- response = await self.httpx_client.get(
271
- url=url,
272
- headers=headers,
273
- params=params,
274
- )
275
- return model.deserialize(response.json(parse_float=parse_float))
276
-
277
- async def invoke_post(
278
- self,
279
- url: str,
280
- headers: dict | None = None,
281
- params: dict | None = None,
282
- content: dict | str | bytes = "",
283
- model: Type[BlofinApiResponse] | None = None,
284
- parse_float=Decimal,
285
- ) -> "BlofinApiResponse":
286
- """
287
- Invokes the specific request to the specific url with the specific params and headers.
288
- """
289
-
290
- if isinstance(content, dict):
291
- content = json.dumps(content, separators=(",", ":"), sort_keys=True)
292
-
293
- response = await self.httpx_client.post(
294
- url=url,
295
- headers=headers,
296
- params=params,
297
- content=content,
298
- )
299
- if not model:
300
- return response.json()
301
-
302
- return model.deserialize(response.json(parse_float=parse_float))
303
-
304
- async def aclose(self) -> None:
305
- await self.httpx_client.aclose()
306
- logger.info("BlofinClient closed")
307
- return True
308
-
309
267
  def read_from_session_file(self, file_path: str) -> None:
310
268
  """
311
269
  Reads from session file; if it doesn't exist, creates it.
@@ -338,6 +296,8 @@ class BlofinClient(ExchangeBase):
338
296
  }
339
297
  aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
340
298
  target_path = Path(file_path)
299
+ if not target_path.exists():
300
+ target_path.mkdir(parents=True)
341
301
  target_path.write_text(aes.encrypt(json.dumps(json_data)))
342
302
 
343
303
  # endregion
@@ -347,13 +307,43 @@ class BlofinClient(ExchangeBase):
347
307
  self,
348
308
  uid: int | str,
349
309
  ) -> UnifiedTraderPositions:
350
- pass
310
+ result = await self.get_copy_trader_all_order_list(
311
+ uid=uid,
312
+ )
313
+ unified_result = UnifiedTraderPositions()
314
+ unified_result.positions = []
315
+ for position in result.data:
316
+ unified_pos = UnifiedPositionInfo()
317
+ unified_pos.position_id = position.id or position.order_id
318
+ unified_pos.position_pnl = position.real_pnl or position.pnl
319
+ unified_pos.position_side = (
320
+ "LONG" if position.order_side in ("LONG", "BUY") else "SHORT"
321
+ )
322
+ unified_pos.margin_mode = position.margin_mode
323
+ unified_pos.position_leverage = Decimal(position.leverage)
324
+ unified_pos.position_pair = position.symbol.replace("-", "/")
325
+ unified_pos.open_time = dt_from_ts(position.open_time)
326
+ unified_pos.open_price = position.avg_open_price
327
+ unified_pos.open_price_unit = position.symbol.split("-")[-1]
328
+ unified_result.positions.append(unified_pos)
329
+
330
+ return unified_result
351
331
 
352
332
  async def get_unified_trader_info(
353
333
  self,
354
334
  uid: int | str,
355
335
  ) -> UnifiedTraderInfo:
356
- pass
336
+ info_resp = await self.get_copy_trader_info(
337
+ uid=uid,
338
+ )
339
+ info = info_resp.data
340
+ unified_info = UnifiedTraderInfo()
341
+ unified_info.trader_id = info.uid
342
+ unified_info.trader_name = info.nick_name
343
+ unified_info.trader_url = f"{BASE_PROFILE_URL}{info.uid}"
344
+ unified_info.win_rate = info.win_rate
345
+
346
+ return unified_info
357
347
 
358
348
  # endregion
359
349
  ###########################################################
@@ -114,7 +114,7 @@ class CopyTraderSingleOrderInfo(BaseModel):
114
114
  price: Any = None
115
115
  fill_quantity: Any = None
116
116
  fill_quantity_cont: Any = None
117
- pnl: Any = None
117
+ pnl: Decimal = None
118
118
  cancel_source: Any = None
119
119
  order_type: Any = None
120
120
  order_open_state: Any = None
@@ -1,10 +1,11 @@
1
- """BxUltra exchange subclass"""
1
+ """
2
+ BxUltra exchange subclass
3
+ """
2
4
 
3
5
  import asyncio
4
6
  from decimal import Decimal
5
7
  import json
6
8
  import logging
7
- from typing import Type
8
9
  import uuid
9
10
 
10
11
  import httpx
@@ -12,6 +13,11 @@ import httpx
12
13
  import time
13
14
  from pathlib import Path
14
15
 
16
+ from trd_utils.exchanges.base_types import (
17
+ UnifiedPositionInfo,
18
+ UnifiedTraderInfo,
19
+ UnifiedTraderPositions,
20
+ )
15
21
  from trd_utils.exchanges.bx_ultra.bx_utils import do_ultra_ss
16
22
  from trd_utils.exchanges.bx_ultra.bx_types import (
17
23
  AssetsInfoResponse,
@@ -30,7 +36,6 @@ from trd_utils.exchanges.bx_ultra.bx_types import (
30
36
  ZenDeskABStatusResponse,
31
37
  ZenDeskAuthResponse,
32
38
  ZoneModuleListResponse,
33
- BxApiResponse,
34
39
  )
35
40
  from trd_utils.cipher import AESCipher
36
41
 
@@ -49,9 +54,17 @@ WEB_APP_VERSION = "4.78.12"
49
54
  TG_APP_VERSION = "5.0.15"
50
55
 
51
56
  ACCEPT_ENCODING_HEADER = "gzip, deflate, br, zstd"
57
+ BASE_PROFILE_URL = "https://bingx.com/en/CopyTrading/"
52
58
 
53
59
  logger = logging.getLogger(__name__)
54
60
 
61
+ # The cache in which we will be storing the api identities.
62
+ # The key of this dict is uid (long user identifier), to api-identity.
63
+ # Why is this a global variable, and not a class attribute? because as far as
64
+ # I've observed, api-identities in bx (unlike Telegram's access-hashes) are not
65
+ # specific to the current session that is fetching them,
66
+ user_api_identity_cache: dict[int, int] = {}
67
+
55
68
 
56
69
  class BXUltraClient(ExchangeBase):
57
70
  ###########################################################
@@ -119,7 +132,7 @@ class BXUltraClient(ExchangeBase):
119
132
  f"{self.we_api_base_url}/coin/v1/zone/module-info",
120
133
  headers=headers,
121
134
  params=params,
122
- model=ZoneModuleListResponse,
135
+ model_type=ZoneModuleListResponse,
123
136
  )
124
137
 
125
138
  async def get_user_favorite_quotation(
@@ -136,7 +149,7 @@ class BXUltraClient(ExchangeBase):
136
149
  f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
137
150
  headers=headers,
138
151
  params=params,
139
- model=UserFavoriteQuotationResponse,
152
+ model_type=UserFavoriteQuotationResponse,
140
153
  )
141
154
 
142
155
  async def get_quotation_rank(
@@ -153,7 +166,7 @@ class BXUltraClient(ExchangeBase):
153
166
  f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
154
167
  headers=headers,
155
168
  params=params,
156
- model=QuotationRankResponse,
169
+ model_type=QuotationRankResponse,
157
170
  )
158
171
 
159
172
  async def get_hot_search(
@@ -170,7 +183,7 @@ class BXUltraClient(ExchangeBase):
170
183
  f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
171
184
  headers=headers,
172
185
  params=params,
173
- model=HotSearchResponse,
186
+ model_type=HotSearchResponse,
174
187
  )
175
188
 
176
189
  async def get_homepage(
@@ -187,7 +200,7 @@ class BXUltraClient(ExchangeBase):
187
200
  f"{self.we_api_base_url}/coin/v1/discovery/homepage",
188
201
  headers=headers,
189
202
  params=params,
190
- model=HomePageResponse,
203
+ model_type=HomePageResponse,
191
204
  )
192
205
 
193
206
  # endregion
@@ -198,15 +211,17 @@ class BXUltraClient(ExchangeBase):
198
211
  return await self.invoke_get(
199
212
  f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
200
213
  headers=headers,
201
- model=ZenDeskABStatusResponse,
214
+ model_type=ZenDeskABStatusResponse,
202
215
  )
216
+
203
217
  async def do_zendesk_auth(self) -> ZenDeskAuthResponse:
204
218
  headers = self.get_headers(needs_auth=True)
205
219
  return await self.invoke_get(
206
220
  f"{self.we_api_base_url}/customer/v1/zendesk/auth/jwt",
207
221
  headers=headers,
208
- model=ZenDeskAuthResponse,
222
+ model_type=ZenDeskAuthResponse,
209
223
  )
224
+
210
225
  # endregion
211
226
  ###########################################################
212
227
  # region platform-tool
@@ -215,7 +230,7 @@ class BXUltraClient(ExchangeBase):
215
230
  return await self.invoke_get(
216
231
  f"{self.we_api_base_url}/platform-tool/v1/hint/list",
217
232
  headers=headers,
218
- model=HintListResponse,
233
+ model_type=HintListResponse,
219
234
  )
220
235
 
221
236
  # endregion
@@ -226,7 +241,7 @@ class BXUltraClient(ExchangeBase):
226
241
  return await self.invoke_get(
227
242
  f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
228
243
  headers=headers,
229
- model=AssetsInfoResponse,
244
+ model_type=AssetsInfoResponse,
230
245
  )
231
246
 
232
247
  # endregion
@@ -255,7 +270,7 @@ class BXUltraClient(ExchangeBase):
255
270
  f"{self.we_api_base_url}/v4/contract/order/hold",
256
271
  headers=headers,
257
272
  params=params,
258
- model=ContractsListResponse,
273
+ model_type=ContractsListResponse,
259
274
  )
260
275
 
261
276
  async def get_contract_order_history(
@@ -282,7 +297,7 @@ class BXUltraClient(ExchangeBase):
282
297
  f"{self.we_api_base_url}/v2/contract/order/history",
283
298
  headers=headers,
284
299
  params=params,
285
- model=ContractOrdersHistoryResponse,
300
+ model_type=ContractOrdersHistoryResponse,
286
301
  )
287
302
 
288
303
  async def get_today_contract_earnings(
@@ -389,7 +404,7 @@ class BXUltraClient(ExchangeBase):
389
404
  # endregion
390
405
  ###########################################################
391
406
  # region copy-trade-facade
392
- async def get_copy_trade_trader_positions(
407
+ async def get_copy_trader_positions(
393
408
  self,
394
409
  uid: int | str,
395
410
  api_identity: str,
@@ -409,7 +424,7 @@ class BXUltraClient(ExchangeBase):
409
424
  f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
410
425
  headers=headers,
411
426
  params=params,
412
- model=CopyTraderTradePositionsResponse,
427
+ model_type=CopyTraderTradePositionsResponse,
413
428
  )
414
429
 
415
430
  async def search_copy_traders(
@@ -446,7 +461,7 @@ class BXUltraClient(ExchangeBase):
446
461
  headers=headers,
447
462
  params=params,
448
463
  content=payload,
449
- model=SearchCopyTradersResponse,
464
+ model_type=SearchCopyTradersResponse,
450
465
  )
451
466
 
452
467
  async def get_copy_trader_futures_stats(
@@ -454,6 +469,11 @@ class BXUltraClient(ExchangeBase):
454
469
  uid: int | str,
455
470
  api_identity: str,
456
471
  ) -> CopyTraderFuturesStatsResponse:
472
+ """
473
+ Returns futures statistics of a certain trader.
474
+ If you do not have the api_identity parameter, please first invoke
475
+ get_copy_trader_resume method and get it from there.
476
+ """
457
477
  params = {
458
478
  "uid": f"{uid}",
459
479
  "apiIdentity": f"{api_identity}",
@@ -463,7 +483,7 @@ class BXUltraClient(ExchangeBase):
463
483
  f"{self.we_api_base_url}/copy-trade-facade/v4/trader/account/futures/stat",
464
484
  headers=headers,
465
485
  params=params,
466
- model=CopyTraderFuturesStatsResponse,
486
+ model_type=CopyTraderFuturesStatsResponse,
467
487
  )
468
488
 
469
489
  async def get_copy_trader_resume(
@@ -478,9 +498,22 @@ class BXUltraClient(ExchangeBase):
478
498
  f"{self.we_api_base_url}/copy-trade-facade/v1/trader/resume",
479
499
  headers=headers,
480
500
  params=params,
481
- model=CopyTraderResumeResponse,
501
+ model_type=CopyTraderResumeResponse,
482
502
  )
483
503
 
504
+ async def get_trader_api_identity(
505
+ self,
506
+ uid: int | str,
507
+ ) -> int | str:
508
+ api_identity = user_api_identity_cache.get(uid, None)
509
+ if not api_identity:
510
+ resume = await self.get_copy_trader_resume(
511
+ uid=uid,
512
+ )
513
+ api_identity = resume.data.api_identity
514
+ user_api_identity_cache[uid] = api_identity
515
+ return api_identity
516
+
484
517
  # endregion
485
518
  ###########################################################
486
519
  # region welfare
@@ -490,7 +523,7 @@ class BXUltraClient(ExchangeBase):
490
523
  f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
491
524
  headers=headers,
492
525
  content="",
493
- model=None,
526
+ model_type=None,
494
527
  )
495
528
 
496
529
  # endregion
@@ -540,56 +573,6 @@ class BXUltraClient(ExchangeBase):
540
573
  the_headers["Authorization"] = f"Bearer {self.authorization_token}"
541
574
  return the_headers
542
575
 
543
- async def invoke_get(
544
- self,
545
- url: str,
546
- headers: dict | None = None,
547
- params: dict | None = None,
548
- model: Type[BxApiResponse] | None = None,
549
- parse_float=Decimal,
550
- ) -> "BxApiResponse":
551
- """
552
- Invokes the specific request to the specific url with the specific params and headers.
553
- """
554
- response = await self.httpx_client.get(
555
- url=url,
556
- headers=headers,
557
- params=params,
558
- )
559
- return model.deserialize(response.json(parse_float=parse_float))
560
-
561
- async def invoke_post(
562
- self,
563
- url: str,
564
- headers: dict | None = None,
565
- params: dict | None = None,
566
- content: dict | str | bytes = "",
567
- model: Type[BxApiResponse] | None = None,
568
- parse_float=Decimal,
569
- ) -> "BxApiResponse":
570
- """
571
- Invokes the specific request to the specific url with the specific params and headers.
572
- """
573
-
574
- if isinstance(content, dict):
575
- content = json.dumps(content, separators=(",", ":"), sort_keys=True)
576
-
577
- response = await self.httpx_client.post(
578
- url=url,
579
- headers=headers,
580
- params=params,
581
- content=content,
582
- )
583
- if not model:
584
- return response.json()
585
-
586
- return model.deserialize(response.json(parse_float=parse_float))
587
-
588
- async def aclose(self) -> None:
589
- await self.httpx_client.aclose()
590
- logger.info("BXUltraClient closed")
591
- return True
592
-
593
576
  def read_from_session_file(self, file_path: str) -> None:
594
577
  """
595
578
  Reads from session file; if it doesn't exist, creates it.
@@ -650,7 +633,80 @@ class BXUltraClient(ExchangeBase):
650
633
  }
651
634
  aes = AESCipher(key=f"bx_{self.account_name}_bx", fav_letter=self._fav_letter)
652
635
  target_path = Path(file_path)
636
+ if not target_path.exists():
637
+ target_path.mkdir(parents=True)
653
638
  target_path.write_text(aes.encrypt(json.dumps(json_data)))
654
639
 
655
640
  # endregion
656
641
  ###########################################################
642
+ # region unified methods
643
+
644
+ async def get_unified_trader_positions(
645
+ self,
646
+ uid: int | str,
647
+ ) -> UnifiedTraderPositions:
648
+ global user_api_identity_cache
649
+
650
+ api_identity = await self.get_trader_api_identity(
651
+ uid=uid,
652
+ )
653
+
654
+ result = await self.get_copy_trader_positions(
655
+ uid=uid,
656
+ api_identity=api_identity,
657
+ page_size=50, # TODO: make this dynamic I guess...
658
+ )
659
+ if result.data.hide:
660
+ # TODO: do proper exceptions here...
661
+ raise ValueError("The trader has made their positions hidden")
662
+ unified_result = UnifiedTraderPositions()
663
+ unified_result.positions = []
664
+ for position in result.data.positions:
665
+ unified_pos = UnifiedPositionInfo()
666
+ unified_pos.position_id = position.position_no
667
+ unified_pos.position_pnl = position.unrealized_pnl
668
+ unified_pos.position_side = position.position_side
669
+ unified_pos.margin_mode = "isolated" # TODO: fix this
670
+ unified_pos.position_leverage = position.leverage
671
+ unified_pos.position_pair = (
672
+ position.symbol
673
+ ) # TODO: make sure correct format
674
+ unified_pos.open_time = None # TODO: do something for this?
675
+ unified_pos.open_price = position.avg_price
676
+ unified_pos.open_price_unit = position.symbol.split("-")[-1] # TODO
677
+ unified_result.positions.append(unified_pos)
678
+
679
+ return unified_result
680
+
681
+ async def get_unified_trader_info(
682
+ self,
683
+ uid: int | str,
684
+ ) -> UnifiedTraderInfo:
685
+ resume_resp = await self.get_copy_trader_resume(
686
+ uid=uid,
687
+ )
688
+ if resume_resp.code != 0 and not resume_resp.data:
689
+ if resume_resp.msg:
690
+ raise ValueError(f"got error from API: {resume_resp.msg}")
691
+ raise ValueError(
692
+ f"got unknown error from bx API while fetching resume for {uid}"
693
+ )
694
+
695
+ resume = resume_resp.data
696
+ api_identity = resume.api_identity
697
+
698
+ info_resp = await self.get_copy_trader_futures_stats(
699
+ uid=uid,
700
+ api_identity=api_identity,
701
+ )
702
+ info = info_resp.data
703
+ unified_info = UnifiedTraderInfo()
704
+ unified_info.trader_id = resume.trader_info.uid
705
+ unified_info.trader_name = resume.trader_info.nick_name
706
+ unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
707
+ unified_info.win_rate = Decimal(info.win_rate.rstrip("%")) / 100
708
+
709
+ return unified_info
710
+
711
+ # endregion
712
+ ###########################################################
@@ -1,10 +1,12 @@
1
1
  from decimal import Decimal
2
- from typing import Any
2
+ import json
3
+ from typing import Any, Type
3
4
  from abc import ABC
4
5
 
5
6
  import httpx
6
7
 
7
8
  from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
9
+ from trd_utils.types_helper.base_model import BaseModel
8
10
 
9
11
 
10
12
  class ExchangeBase(ABC):
@@ -66,32 +68,55 @@ class ExchangeBase(ABC):
66
68
  async def invoke_get(
67
69
  self,
68
70
  url: str,
69
- headers: dict | None,
70
- params: dict | None,
71
- model: Any,
71
+ headers: dict | None = None,
72
+ params: dict | None = None,
73
+ model_type: Type[BaseModel] | None = None,
72
74
  parse_float=Decimal,
73
- ) -> Any:
75
+ ) -> "BaseModel":
74
76
  """
75
77
  Invokes the specific request to the specific url with the specific params and headers.
76
78
  """
77
- pass
79
+ response = await self.httpx_client.get(
80
+ url=url,
81
+ headers=headers,
82
+ params=params,
83
+ )
84
+ return model_type.deserialize(response.json(parse_float=parse_float))
78
85
 
79
86
  async def invoke_post(
80
87
  self,
81
88
  url: str,
82
89
  headers: dict | None = None,
83
90
  params: dict | None = None,
84
- content: str | bytes = "",
85
- model: None = None,
91
+ content: dict | str | bytes = "",
92
+ model_type: Type[BaseModel] | None = None,
86
93
  parse_float=Decimal,
87
- ):
94
+ ) -> "BaseModel":
88
95
  """
89
96
  Invokes the specific request to the specific url with the specific params and headers.
90
97
  """
91
- pass
98
+
99
+ if isinstance(content, dict):
100
+ content = json.dumps(content, separators=(",", ":"), sort_keys=True)
101
+
102
+ response = await self.httpx_client.post(
103
+ url=url,
104
+ headers=headers,
105
+ params=params,
106
+ content=content,
107
+ )
108
+ if not model_type:
109
+ return response.json()
110
+
111
+ return model_type.deserialize(response.json(parse_float=parse_float))
112
+
92
113
 
93
114
  async def aclose(self) -> None:
94
- pass
115
+ await self.httpx_client.aclose()
116
+
117
+ # endregion
118
+ ###########################################################
119
+ # region data-files related methods
95
120
 
96
121
  def read_from_session_file(self, file_path: str) -> None:
97
122
  """
@@ -3,5 +3,5 @@ from .hyperliquid_client import HyperLiquidClient
3
3
 
4
4
 
5
5
  __all__ = [
6
- HyperLiquidClient,
6
+ "HyperLiquidClient",
7
7
  ]
@@ -102,56 +102,6 @@ class HyperLiquidClient(ExchangeBase):
102
102
  the_headers["Authorization"] = f"Bearer {self.authorization_token}"
103
103
  return the_headers
104
104
 
105
- async def invoke_get(
106
- self,
107
- url: str,
108
- headers: dict | None = None,
109
- params: dict | None = None,
110
- model: Type[HyperLiquidApiResponse] | None = None,
111
- parse_float=Decimal,
112
- ) -> "HyperLiquidApiResponse":
113
- """
114
- Invokes the specific request to the specific url with the specific params and headers.
115
- """
116
- response = await self.httpx_client.get(
117
- url=url,
118
- headers=headers,
119
- params=params,
120
- )
121
- return model.deserialize(response.json(parse_float=parse_float))
122
-
123
- async def invoke_post(
124
- self,
125
- url: str,
126
- headers: dict | None = None,
127
- params: dict | None = None,
128
- content: dict | str | bytes = "",
129
- model: Type[HyperLiquidApiResponse] | None = None,
130
- parse_float=Decimal,
131
- ) -> "HyperLiquidApiResponse":
132
- """
133
- Invokes the specific request to the specific url with the specific params and headers.
134
- """
135
-
136
- if isinstance(content, dict):
137
- content = json.dumps(content, separators=(",", ":"), sort_keys=True)
138
-
139
- response = await self.httpx_client.post(
140
- url=url,
141
- headers=headers,
142
- params=params,
143
- content=content,
144
- )
145
- if not model:
146
- return response.json()
147
-
148
- return model.deserialize(response.json(parse_float=parse_float))
149
-
150
- async def aclose(self) -> None:
151
- await self.httpx_client.aclose()
152
- logger.info("HyperLiquidClient closed")
153
- return True
154
-
155
105
  def read_from_session_file(self, file_path: str) -> None:
156
106
  """
157
107
  Reads from session file; if it doesn't exist, creates it.
@@ -182,6 +132,8 @@ class HyperLiquidClient(ExchangeBase):
182
132
  }
183
133
  aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
184
134
  target_path = Path(file_path)
135
+ if not target_path.exists():
136
+ target_path.mkdir(parents=True)
185
137
  target_path.write_text(aes.encrypt(json.dumps(json_data)))
186
138
 
187
139
  # endregion
@@ -2,5 +2,5 @@ from .base_model import BaseModel
2
2
 
3
3
 
4
4
  __all__ = [
5
- BaseModel,
5
+ "BaseModel",
6
6
  ]
@@ -138,7 +138,7 @@ def generic_obj_to_value(
138
138
  return expected_type(**value)
139
139
 
140
140
  if not expected_type_args:
141
- if isinstance(value, expected_type):
141
+ if value is None or isinstance(value, expected_type):
142
142
  return value
143
143
  return expected_type(value)
144
144
 
@@ -1,3 +0,0 @@
1
-
2
- __version__ = "0.0.15"
3
-
@@ -1,53 +0,0 @@
1
-
2
-
3
- from datetime import datetime
4
- from decimal import Decimal
5
-
6
- class UnifiedPositionInfo:
7
- # The id of the position.
8
- position_id: str = None
9
-
10
- # The pnl (profit) of the position.
11
- position_pnl: Decimal = None
12
-
13
- # The position side, either "LONG" or "SHORT".
14
- position_side: str = None
15
-
16
- # The formatted pair string of this position.
17
- # e.g. BTC/USDT.
18
- position_pair: str = None
19
-
20
- # Side but with a proper emoji alongside of it.
21
- side_with_emoji: str = None
22
-
23
- # The open time of this position.
24
- # Note that not all public APIs might provide this field.
25
- open_time: datetime = None
26
-
27
- # The relative open time of this position.
28
- relative_open_time: str = None
29
-
30
- # Open price of the position.
31
- open_price: Decimal = None
32
-
33
- # The string (and formatted) version of the open_price.
34
- # Optionally base unit also included (e.g. USDT or USD).
35
- open_price_str: str = None
36
-
37
-
38
- class UnifiedTraderPositions:
39
- positions: list[UnifiedPositionInfo] = None
40
-
41
- class UnifiedTraderInfo:
42
- # Name of the trader
43
- trader_name: str = None
44
-
45
- # The URL in which we can see the trader's profile
46
- trader_url: str = None
47
-
48
- # Trader's id. Either int or str. In DEXes (such as HyperLiquid),
49
- # this might be wallet address of the trader.
50
- trader_id: int | str = None
51
-
52
- # Trader's win-rate. Not all exchanges might support this field.
53
- win_rate: Decimal = None
File without changes
File without changes