trd-utils 0.0.14__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.

@@ -64,7 +64,7 @@ class CopyTraderInfoResult(BaseModel):
64
64
  joined_date: int = None
65
65
  max_draw_down: Decimal = None
66
66
  nick_name: str = None
67
- order_amount_limit: None
67
+ order_amount_limit: Any = None
68
68
  profile: str = None
69
69
  profit_sharing_ratio: Decimal = None
70
70
  real_pnl: Decimal = None
@@ -89,7 +89,7 @@ class CopyTraderSingleOrderInfo(BaseModel):
89
89
  order_side: str = None
90
90
  avg_open_price: str = None
91
91
  quantity: str = None
92
- quantity_cont: None
92
+ quantity_cont: Any = None
93
93
  open_time: int = None
94
94
  close_time: Any = None
95
95
  avg_close_price: Decimal = None
@@ -100,41 +100,41 @@ class CopyTraderSingleOrderInfo(BaseModel):
100
100
  followers: Any = None
101
101
  order_id: Any = None
102
102
  sharing: Any = None
103
- order_state: None
104
- trader_name: None
105
- mark_price: None
106
- tp_trigger_price: None
107
- tp_order_type: None
108
- sl_trigger_price: None
109
- sl_order_type: None
103
+ order_state: Any = None
104
+ trader_name: Any = None
105
+ mark_price: Any = None
106
+ tp_trigger_price: Any = None
107
+ tp_order_type: Any = None
108
+ sl_trigger_price: Any = None
109
+ sl_order_type: Any = None
110
110
  margin_mode: str = None
111
- time_in_force: None
111
+ time_in_force: Any = None
112
112
  position_side: str = None
113
- order_category: None
114
- price: None
115
- fill_quantity: None
116
- fill_quantity_cont: None
117
- pnl: None
118
- cancel_source: None
119
- order_type: None
120
- order_open_state: None
121
- amount: None
122
- filled_amount: None
123
- create_time: None
124
- update_time: None
125
- open_fee: None
126
- close_fee: None
127
- id_md5: None
128
- tp_sl: None
129
- trader_uid: None
130
- available_quantity: None
131
- available_quantity_cont: None
132
- show_in_kline: None
133
- unrealized_pnl: None
134
- unrealized_pnl_ratio: None
135
- broker_id: None
136
- position_change_history: None
137
- user_id: None
113
+ order_category: Any = None
114
+ price: Any = None
115
+ fill_quantity: Any = None
116
+ fill_quantity_cont: Any = None
117
+ pnl: Decimal = None
118
+ cancel_source: Any = None
119
+ order_type: Any = None
120
+ order_open_state: Any = None
121
+ amount: Any = None
122
+ filled_amount: Any = None
123
+ create_time: Any = None
124
+ update_time: Any = None
125
+ open_fee: Any = None
126
+ close_fee: Any = None
127
+ id_md5: Any = None
128
+ tp_sl: Any = None
129
+ trader_uid: Any = None
130
+ available_quantity: Any = None
131
+ available_quantity_cont: Any = None
132
+ show_in_kline: Any = None
133
+ unrealized_pnl: Any = None
134
+ unrealized_pnl_ratio: Any = None
135
+ broker_id: Any = None
136
+ position_change_history: Any = None
137
+ user_id: Any = None
138
138
 
139
139
  class CopyTraderOrderListResponse(BlofinApiResponse):
140
140
  data: list[CopyTraderSingleOrderInfo] = 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,13 @@
1
-
2
1
  from decimal import Decimal
3
- from typing import Any
2
+ import json
3
+ from typing import Any, Type
4
4
  from abc import ABC
5
5
 
6
6
  import httpx
7
7
 
8
+ from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
9
+ from trd_utils.types_helper.base_model import BaseModel
10
+
8
11
 
9
12
  class ExchangeBase(ABC):
10
13
  ###########################################################
@@ -25,6 +28,38 @@ class ExchangeBase(ABC):
25
28
 
26
29
  _fav_letter: str = "^"
27
30
  # endregion
31
+ ###########################################################
32
+
33
+ # region abstract trading methods
34
+
35
+ async def get_unified_trader_positions(
36
+ self,
37
+ uid: int | str,
38
+ ) -> UnifiedTraderPositions:
39
+ """
40
+ Returns the unified version of all currently open positions of the specific
41
+ trader. Note that different exchanges might fill different fields, according to the
42
+ data they provide in their public APIs.
43
+ If you want to fetch past positions history, you have to use another method.
44
+ """
45
+ raise NotImplementedError(
46
+ "This method is not implemented in ExchangeBase class. "
47
+ "Please use a real exchange class inheriting and implementing this method."
48
+ )
49
+
50
+ async def get_unified_trader_info(self, uid: int | str) -> UnifiedTraderInfo:
51
+ """
52
+ Returns information about a specific trader.
53
+ Different exchanges might return and fill different information according to the
54
+ data returned from their public APIs.
55
+ """
56
+ raise NotImplementedError(
57
+ "This method is not implemented in ExchangeBase class. "
58
+ "Please use a real exchange class inheriting and implementing this method."
59
+ )
60
+
61
+ # endregion
62
+
28
63
  ###########################################################
29
64
  # region client helper methods
30
65
  def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
@@ -33,32 +68,55 @@ class ExchangeBase(ABC):
33
68
  async def invoke_get(
34
69
  self,
35
70
  url: str,
36
- headers: dict | None,
37
- params: dict | None,
38
- model: Any,
71
+ headers: dict | None = None,
72
+ params: dict | None = None,
73
+ model_type: Type[BaseModel] | None = None,
39
74
  parse_float=Decimal,
40
- ) -> Any:
75
+ ) -> "BaseModel":
41
76
  """
42
77
  Invokes the specific request to the specific url with the specific params and headers.
43
78
  """
44
- 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))
45
85
 
46
86
  async def invoke_post(
47
87
  self,
48
88
  url: str,
49
89
  headers: dict | None = None,
50
90
  params: dict | None = None,
51
- content: str | bytes = "",
52
- model: None = None,
91
+ content: dict | str | bytes = "",
92
+ model_type: Type[BaseModel] | None = None,
53
93
  parse_float=Decimal,
54
- ):
94
+ ) -> "BaseModel":
55
95
  """
56
96
  Invokes the specific request to the specific url with the specific params and headers.
57
97
  """
58
- 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
+
59
113
 
60
114
  async def aclose(self) -> None:
61
- pass
115
+ await self.httpx_client.aclose()
116
+
117
+ # endregion
118
+ ###########################################################
119
+ # region data-files related methods
62
120
 
63
121
  def read_from_session_file(self, file_path: str) -> None:
64
122
  """
@@ -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
+ ]