trd-utils 0.0.15__py3-none-any.whl → 0.0.16__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.

Potentially problematic release.


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

trd_utils/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- __version__ = "0.0.15"
2
+ __version__ = "0.0.16"
3
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
  ]
@@ -3,7 +3,9 @@
3
3
  from datetime import datetime
4
4
  from decimal import Decimal
5
5
 
6
- class UnifiedPositionInfo:
6
+ from trd_utils.types_helper.base_model import BaseModel
7
+
8
+ class UnifiedPositionInfo(BaseModel):
7
9
  # The id of the position.
8
10
  position_id: str = None
9
11
 
@@ -13,41 +15,92 @@ class UnifiedPositionInfo:
13
15
  # The position side, either "LONG" or "SHORT".
14
16
  position_side: str = None
15
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
+
16
27
  # The formatted pair string of this position.
17
28
  # e.g. BTC/USDT.
18
29
  position_pair: str = None
19
30
 
20
- # Side but with a proper emoji alongside of it.
21
- side_with_emoji: str = None
22
-
23
31
  # The open time of this position.
24
32
  # Note that not all public APIs might provide this field.
25
33
  open_time: datetime = None
26
34
 
27
- # The relative open time of this position.
28
- relative_open_time: str = None
29
-
30
35
  # Open price of the position.
31
36
  open_price: Decimal = None
32
37
 
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:
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):
39
81
  positions: list[UnifiedPositionInfo] = None
40
82
 
41
- class UnifiedTraderInfo:
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
+
42
88
  # Name of the trader
43
89
  trader_name: str = None
44
90
 
45
91
  # The URL in which we can see the trader's profile
46
92
  trader_url: str = None
47
93
 
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
94
  # Trader's win-rate. Not all exchanges might support this field.
53
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.
@@ -347,13 +305,43 @@ class BlofinClient(ExchangeBase):
347
305
  self,
348
306
  uid: int | str,
349
307
  ) -> UnifiedTraderPositions:
350
- pass
308
+ result = await self.get_copy_trader_all_order_list(
309
+ uid=uid,
310
+ )
311
+ unified_result = UnifiedTraderPositions()
312
+ unified_result.positions = []
313
+ for position in result.data:
314
+ unified_pos = UnifiedPositionInfo()
315
+ unified_pos.position_id = position.id or position.order_id
316
+ unified_pos.position_pnl = position.real_pnl or position.pnl
317
+ unified_pos.position_side = (
318
+ "LONG" if position.order_side in ("LONG", "BUY") else "SHORT"
319
+ )
320
+ unified_pos.margin_mode = position.margin_mode
321
+ unified_pos.position_leverage = Decimal(position.leverage)
322
+ unified_pos.position_pair = position.symbol.replace("-", "/")
323
+ unified_pos.open_time = dt_from_ts(position.open_time)
324
+ unified_pos.open_price = position.avg_open_price
325
+ unified_pos.open_price_unit = position.symbol.split("-")[-1]
326
+ unified_result.positions.append(unified_pos)
327
+
328
+ return unified_result
351
329
 
352
330
  async def get_unified_trader_info(
353
331
  self,
354
332
  uid: int | str,
355
333
  ) -> UnifiedTraderInfo:
356
- pass
334
+ info_resp = await self.get_copy_trader_info(
335
+ uid=uid,
336
+ )
337
+ info = info_resp.data
338
+ unified_info = UnifiedTraderInfo()
339
+ unified_info.trader_id = info.uid
340
+ unified_info.trader_name = info.nick_name
341
+ unified_info.trader_url = f"{BASE_PROFILE_URL}{info.uid}"
342
+ unified_info.win_rate = info.win_rate
343
+
344
+ return unified_info
357
345
 
358
346
  # endregion
359
347
  ###########################################################
@@ -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.
@@ -654,3 +637,74 @@ class BXUltraClient(ExchangeBase):
654
637
 
655
638
  # endregion
656
639
  ###########################################################
640
+ # region unified methods
641
+
642
+ async def get_unified_trader_positions(
643
+ self,
644
+ uid: int | str,
645
+ ) -> UnifiedTraderPositions:
646
+ global user_api_identity_cache
647
+
648
+ api_identity = await self.get_trader_api_identity(
649
+ uid=uid,
650
+ )
651
+
652
+ result = await self.get_copy_trader_positions(
653
+ uid=uid,
654
+ api_identity=api_identity,
655
+ page_size=50, # TODO: make this dynamic I guess...
656
+ )
657
+ if result.data.hide:
658
+ # TODO: do proper exceptions here...
659
+ raise ValueError("The trader has made their positions hidden")
660
+ unified_result = UnifiedTraderPositions()
661
+ unified_result.positions = []
662
+ for position in result.data.positions:
663
+ unified_pos = UnifiedPositionInfo()
664
+ unified_pos.position_id = position.position_no
665
+ unified_pos.position_pnl = position.unrealized_pnl
666
+ unified_pos.position_side = position.position_side
667
+ unified_pos.margin_mode = "isolated" # TODO: fix this
668
+ unified_pos.position_leverage = position.leverage
669
+ unified_pos.position_pair = (
670
+ position.symbol
671
+ ) # TODO: make sure correct format
672
+ unified_pos.open_time = None # TODO: do something for this?
673
+ unified_pos.open_price = position.avg_price
674
+ unified_pos.open_price_unit = position.symbol.split("-")[-1] # TODO
675
+ unified_result.positions.append(unified_pos)
676
+
677
+ return unified_result
678
+
679
+ async def get_unified_trader_info(
680
+ self,
681
+ uid: int | str,
682
+ ) -> UnifiedTraderInfo:
683
+ resume_resp = await self.get_copy_trader_resume(
684
+ uid=uid,
685
+ )
686
+ if resume_resp.code != 0 and not resume_resp.data:
687
+ if resume_resp.msg:
688
+ raise ValueError(f"got error from API: {resume_resp.msg}")
689
+ raise ValueError(
690
+ f"got unknown error from bx API while fetching resume for {uid}"
691
+ )
692
+
693
+ resume = resume_resp.data
694
+ api_identity = resume.api_identity
695
+
696
+ info_resp = await self.get_copy_trader_futures_stats(
697
+ uid=uid,
698
+ api_identity=api_identity,
699
+ )
700
+ info = info_resp.data
701
+ unified_info = UnifiedTraderInfo()
702
+ unified_info.trader_id = resume.trader_info.uid
703
+ unified_info.trader_name = resume.trader_info.nick_name
704
+ unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
705
+ unified_info.win_rate = Decimal(info.win_rate.rstrip("%")) / 100
706
+
707
+ return unified_info
708
+
709
+ # endregion
710
+ ###########################################################
@@ -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.
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trd_utils
3
- Version: 0.0.15
3
+ Version: 0.0.16
4
4
  Summary: Common Basic Utils for Python3. By ALiwoto.
5
5
  Keywords: utils,trd_utils,basic-utils,common-utils
6
6
  Author: ALiwoto
@@ -0,0 +1,31 @@
1
+ trd_utils/__init__.py,sha256=TpbNgwUz9GdaWu47KkuekhUKxSBLfBCjqfgHJ3CAzUc,25
2
+ trd_utils/cipher/__init__.py,sha256=V05KNuzQwCic-ihMVHlC8sENaJGc3I8MCb4pg4849X8,1765
3
+ trd_utils/common_utils/float_utils.py,sha256=W-jv7nzjl88xwGB6gsEXmDDhF6DseOrrVT2qx7OvyCo,266
4
+ trd_utils/date_utils/__init__.py,sha256=Erg_E1TfKWNpiuZFm_NXRjCwoRMfxpPS2-mJK6V4lFM,77
5
+ trd_utils/date_utils/datetime_helpers.py,sha256=Ai9rDCLoMg2dRH8NIBTJQgUSfNQp15Vtp6a1IQwHEbw,366
6
+ trd_utils/exchanges/README.md,sha256=UwkpsfcoLCJaMvJe4yBsFkDpf8P6DOLYhtybb6xWMLc,6738
7
+ trd_utils/exchanges/__init__.py,sha256=x74_8-P7ktZyL-1AuAU_SDQD8eKZMPh1gfkLzPuS1xY,440
8
+ trd_utils/exchanges/base_types.py,sha256=bPsRUetQIHdW7ESMAQcjvX-Z2uixtCC93Vb-XnX6FFs,3229
9
+ trd_utils/exchanges/blofin/__init__.py,sha256=X4r9o4Nyjla4UeOBG8lrgtnGYO2aErFMKaJ7yQrFasE,76
10
+ trd_utils/exchanges/blofin/blofin_client.py,sha256=q9NrAalg8KtyBkDrFKA0Vkq0iw2aIK7SQugzVpPJHds,12261
11
+ trd_utils/exchanges/blofin/blofin_types.py,sha256=LnK3LEVlqU3Vg0Cg1jNmb5GGHQImwI6LCKaDw-aQa6A,4059
12
+ trd_utils/exchanges/bx_ultra/__init__.py,sha256=8Ssy-eOemQR32Nv1-FoPHm87nRqRO4Fm2PU5GHEFKfQ,80
13
+ trd_utils/exchanges/bx_ultra/bx_types.py,sha256=muHLa9lEjIOIDLQCb6xdyxSoQiIMSHErVe0NCoAS-RI,31017
14
+ trd_utils/exchanges/bx_ultra/bx_ultra_client.py,sha256=VcHBhyckn99T8bI_se9u2vyfOHl3w8VPuLrjE16-sQU,24978
15
+ trd_utils/exchanges/bx_ultra/bx_utils.py,sha256=PwapomwDW33arVmKIDj6cL-aP0ptu4BYy_lOCqSAPOo,1392
16
+ trd_utils/exchanges/exchange_base.py,sha256=ZY_9vL9LXtAbAmlWygdVyy0wC-QDla-JGWZ8HMRlkk8,4266
17
+ trd_utils/exchanges/hyperliquid/README.md,sha256=-qaxmDt_9NTus2xRuzyFGkKgYDWgWk7ufHVTSkyn3t4,105
18
+ trd_utils/exchanges/hyperliquid/__init__.py,sha256=QhwGRcneGFHREM-MMdYpbcx-aWdsWsu2WznHzx7LaUM,92
19
+ trd_utils/exchanges/hyperliquid/hyperliquid_client.py,sha256=tEYys_w0sGzxzGdLAUIfl14YTh-zapYwxH0K3B5jBxM,4683
20
+ trd_utils/exchanges/hyperliquid/hyperliquid_types.py,sha256=ueL7Q4yOAK4orlUqeLVNRk6u1AG83pDeGJasTeT3774,2666
21
+ trd_utils/html_utils/__init__.py,sha256=1WWs8C7JszRjTkmzIRLHpxWECHur_DrulTPGIeX88oM,426
22
+ trd_utils/html_utils/html_formats.py,sha256=unKsvOiiDmYTTaM0DYZEUNLEUzWQKKrqASJXvY54kvU,2299
23
+ trd_utils/tradingview/__init__.py,sha256=H0QYb-O5qvy7qC3yswtlcSWLmeBnaS6oJ3JtjvmaV_Y,154
24
+ trd_utils/tradingview/tradingview_client.py,sha256=g_eWYaCRQAL8Kvd-r6AnAdbH7Jha6C_GAyCuxh-RQUU,3917
25
+ trd_utils/tradingview/tradingview_types.py,sha256=z21MXPVdWHAduEl3gSeMIRhxtBN9yK-jPYHfZSMIbSA,6144
26
+ trd_utils/types_helper/__init__.py,sha256=lLbUiW1jUV1gjzTMFLthwkvF0hwauH-F_J2JZq--1U0,67
27
+ trd_utils/types_helper/base_model.py,sha256=cdkvzkjZ1RE__GY98YvhtALHKMux9M4uzcdd8ofQNYU,10617
28
+ trd_utils-0.0.16.dist-info/LICENSE,sha256=J1EP2xt87RjjmsTV1jTjHDQMLIM9FjdwEftTpw8hyv4,1067
29
+ trd_utils-0.0.16.dist-info/METADATA,sha256=vUC58vdFOVmQuZeOra-BglRiwTjzItdZBtEYTCzvOvE,1095
30
+ trd_utils-0.0.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
31
+ trd_utils-0.0.16.dist-info/RECORD,,
@@ -1,29 +0,0 @@
1
- trd_utils/__init__.py,sha256=tth9x7eIK8zwa9EZR2wIczpeBj0qNJRjG8svFe9CfmE,25
2
- trd_utils/cipher/__init__.py,sha256=V05KNuzQwCic-ihMVHlC8sENaJGc3I8MCb4pg4849X8,1765
3
- trd_utils/common_utils/float_utils.py,sha256=W-jv7nzjl88xwGB6gsEXmDDhF6DseOrrVT2qx7OvyCo,266
4
- trd_utils/exchanges/README.md,sha256=UwkpsfcoLCJaMvJe4yBsFkDpf8P6DOLYhtybb6xWMLc,6738
5
- trd_utils/exchanges/__init__.py,sha256=ghL9RKX76Fr6xhtZ6QEBwq5GCg0A3HRZMvsgD-BNnus,426
6
- trd_utils/exchanges/base_types.py,sha256=NkgTw81oAUG0BMWHOS_kMOBoR17N9N1FQaXVvwuWhvQ,1417
7
- trd_utils/exchanges/blofin/__init__.py,sha256=dQkY9aSbI5fZJDOSbkrbrbpHSbWbJjLEmjpkXxDMDD4,74
8
- trd_utils/exchanges/blofin/blofin_client.py,sha256=TWeo90mubIzfxIfRRX8gGT7EE_Ye6EShYxXrxKLK1oU,12092
9
- trd_utils/exchanges/blofin/blofin_types.py,sha256=ejBgJeGrbqVrLIJSZ8UZQIuIxhlDOVm1UOJFBI_qvJg,4055
10
- trd_utils/exchanges/bx_ultra/__init__.py,sha256=8Ssy-eOemQR32Nv1-FoPHm87nRqRO4Fm2PU5GHEFKfQ,80
11
- trd_utils/exchanges/bx_ultra/bx_types.py,sha256=muHLa9lEjIOIDLQCb6xdyxSoQiIMSHErVe0NCoAS-RI,31017
12
- trd_utils/exchanges/bx_ultra/bx_ultra_client.py,sha256=UtWbS__FYyYbYgGvxIrpIkuXLaD4nppP_AyEZWOe5ec,22591
13
- trd_utils/exchanges/bx_ultra/bx_utils.py,sha256=PwapomwDW33arVmKIDj6cL-aP0ptu4BYy_lOCqSAPOo,1392
14
- trd_utils/exchanges/exchange_base.py,sha256=-cphxYRbj_DTls2a_HsYMSsutAVCIxI_2lcOdwHGBZE,3330
15
- trd_utils/exchanges/hyperliquid/README.md,sha256=-qaxmDt_9NTus2xRuzyFGkKgYDWgWk7ufHVTSkyn3t4,105
16
- trd_utils/exchanges/hyperliquid/__init__.py,sha256=mgrug9TELB1K4T0QVynYzz4QDGR850_CKJLvjgAUY1k,90
17
- trd_utils/exchanges/hyperliquid/hyperliquid_client.py,sha256=BZe7GZ_Fcxsl3doapvTnfXibwidF5TesZY6dQU16QUs,6245
18
- trd_utils/exchanges/hyperliquid/hyperliquid_types.py,sha256=ueL7Q4yOAK4orlUqeLVNRk6u1AG83pDeGJasTeT3774,2666
19
- trd_utils/html_utils/__init__.py,sha256=1WWs8C7JszRjTkmzIRLHpxWECHur_DrulTPGIeX88oM,426
20
- trd_utils/html_utils/html_formats.py,sha256=unKsvOiiDmYTTaM0DYZEUNLEUzWQKKrqASJXvY54kvU,2299
21
- trd_utils/tradingview/__init__.py,sha256=H0QYb-O5qvy7qC3yswtlcSWLmeBnaS6oJ3JtjvmaV_Y,154
22
- trd_utils/tradingview/tradingview_client.py,sha256=g_eWYaCRQAL8Kvd-r6AnAdbH7Jha6C_GAyCuxh-RQUU,3917
23
- trd_utils/tradingview/tradingview_types.py,sha256=z21MXPVdWHAduEl3gSeMIRhxtBN9yK-jPYHfZSMIbSA,6144
24
- trd_utils/types_helper/__init__.py,sha256=VlEXDzOyn6fYH-dE86EGJ6u_el08QvdyOtJkj-0EAVA,65
25
- trd_utils/types_helper/base_model.py,sha256=uy3s_AzMBxCVQEIMQYZZw9a5go327oOEGIl1iIZP-gs,10600
26
- trd_utils-0.0.15.dist-info/LICENSE,sha256=J1EP2xt87RjjmsTV1jTjHDQMLIM9FjdwEftTpw8hyv4,1067
27
- trd_utils-0.0.15.dist-info/METADATA,sha256=eI7bHdiO4nCbVmy-5CU81hyt_Tej_mtSfV89aiccL74,1095
28
- trd_utils-0.0.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
29
- trd_utils-0.0.15.dist-info/RECORD,,