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

@@ -1,8 +1,10 @@
1
1
  """BxUltra exchange subclass"""
2
2
 
3
+ import asyncio
3
4
  from decimal import Decimal
4
5
  import json
5
6
  import logging
7
+ from typing import Type
6
8
  import uuid
7
9
 
8
10
  import httpx
@@ -10,9 +12,10 @@ import httpx
10
12
  import time
11
13
  from pathlib import Path
12
14
 
13
- from .common_utils import do_ultra_ss
14
- from .bx_types import (
15
+ from trd_utils.exchanges.bx_ultra.bx_utils import do_ultra_ss
16
+ from trd_utils.exchanges.bx_ultra.bx_types import (
15
17
  AssetsInfoResponse,
18
+ ContractOrdersHistoryResponse,
16
19
  ContractsListResponse,
17
20
  CopyTraderTradePositionsResponse,
18
21
  HintListResponse,
@@ -24,8 +27,11 @@ from .bx_types import (
24
27
  UserFavoriteQuotationResponse,
25
28
  ZenDeskABStatusResponse,
26
29
  ZoneModuleListResponse,
30
+ BxApiResponse,
27
31
  )
28
- from ..cipher import AESCipher
32
+ from trd_utils.cipher import AESCipher
33
+
34
+ from trd_utils.exchanges.exchange_base import ExchangeBase
29
35
 
30
36
  PLATFORM_ID_ANDROID = "10"
31
37
  PLATFORM_ID_WEB = "30"
@@ -42,25 +48,16 @@ TG_APP_VERSION = "5.0.15"
42
48
  logger = logging.getLogger(__name__)
43
49
 
44
50
 
45
- class BXUltraClient:
51
+ class BXUltraClient(ExchangeBase):
46
52
  ###########################################################
47
53
  # region client parameters
48
54
  we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
49
55
  we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
50
-
51
56
  original_base_host: str = "https://\u0062ing\u0078.co\u006d"
52
-
53
57
  qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
54
58
  qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
55
59
 
56
- device_id: str = None
57
- trace_id: str = None
58
- app_version: str = "4.28.3"
59
- platform_id: str = "10"
60
- install_channel: str = "officialAPK"
61
- channel_header: str = "officialAPK"
62
60
  origin_header: str = "https://\u0062ing\u0078.co\u006d"
63
- authorization_token: str = None
64
61
  app_id: str = "30004"
65
62
  main_app_id: str = "10009"
66
63
  trade_env: str = "real"
@@ -69,12 +66,6 @@ class BXUltraClient:
69
66
  device_brand: str = "SM-N976N"
70
67
  platform_lang: str = "en"
71
68
  sys_lang: str = "en"
72
- user_agent: str = "okhttp/4.12.0"
73
- x_requested_with: str = None
74
- httpx_client: httpx.AsyncClient = None
75
- account_name: str = "default"
76
-
77
- _fav_letter: str = "^"
78
69
 
79
70
  # endregion
80
71
  ###########################################################
@@ -87,6 +78,7 @@ class BXUltraClient:
87
78
  app_version: str = ANDROID_APP_VERSION,
88
79
  http_verify: bool = True,
89
80
  fav_letter: str = "^",
81
+ sessions_dir: str = "sessions",
90
82
  ):
91
83
  self.httpx_client = httpx.AsyncClient(
92
84
  verify=http_verify, http2=True, http1=False
@@ -97,13 +89,15 @@ class BXUltraClient:
97
89
  self.app_version = app_version
98
90
  self._fav_letter = fav_letter
99
91
 
100
- self.read_from_session_file(f"{self.account_name}.bx")
92
+ self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bx")
101
93
 
102
94
  # endregion
103
95
  ###########################################################
104
96
  # region api/coin/v1
105
97
  async def get_zone_module_info(
106
- self, only_one_position: int = 0, biz_type: int = 10
98
+ self,
99
+ only_one_position: int = 0,
100
+ biz_type: int = 10,
107
101
  ) -> ZoneModuleListResponse:
108
102
  """
109
103
  Fetches and returns zone module info from the API.
@@ -116,101 +110,113 @@ class BXUltraClient:
116
110
  }
117
111
  headers = self.get_headers(params)
118
112
  headers["Only_one_position"] = f"{only_one_position}"
119
- response = await self.httpx_client.get(
113
+ return await self.invoke_get(
120
114
  f"{self.we_api_base_url}/coin/v1/zone/module-info",
121
115
  headers=headers,
122
116
  params=params,
117
+ model=ZoneModuleListResponse,
123
118
  )
124
- return ZoneModuleListResponse.deserialize(response.json(parse_float=Decimal))
125
119
 
126
120
  async def get_user_favorite_quotation(
127
- self, only_one_position: int = 0, biz_type: int = 1
128
- ):
121
+ self,
122
+ only_one_position: int = 0,
123
+ biz_type: int = 1,
124
+ ) -> UserFavoriteQuotationResponse:
129
125
  params = {
130
126
  "bizType": f"{biz_type}",
131
127
  }
132
128
  headers = self.get_headers(params)
133
129
  headers["Only_one_position"] = f"{only_one_position}"
134
- response = await self.httpx_client.get(
130
+ return await self.invoke_get(
135
131
  f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
136
132
  headers=headers,
137
133
  params=params,
138
- )
139
- return UserFavoriteQuotationResponse.deserialize(
140
- response.json(parse_float=Decimal)
134
+ model=UserFavoriteQuotationResponse,
141
135
  )
142
136
 
143
- async def get_quotation_rank(self, only_one_position: int = 0, order_flag: int = 0):
137
+ async def get_quotation_rank(
138
+ self,
139
+ only_one_position: int = 0,
140
+ order_flag: int = 0,
141
+ ) -> QuotationRankResponse:
144
142
  params = {
145
143
  "orderFlag": f"{order_flag}",
146
144
  }
147
145
  headers = self.get_headers(params)
148
146
  headers["Only_one_position"] = f"{only_one_position}"
149
- response = await self.httpx_client.get(
147
+ return await self.invoke_get(
150
148
  f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
151
149
  headers=headers,
152
150
  params=params,
151
+ model=QuotationRankResponse,
153
152
  )
154
- return QuotationRankResponse.deserialize(response.json(parse_float=Decimal))
155
153
 
156
- async def get_hot_search(self, only_one_position: int = 0, biz_type: int = 30):
154
+ async def get_hot_search(
155
+ self,
156
+ only_one_position: int = 0,
157
+ biz_type: int = 30,
158
+ ) -> HotSearchResponse:
157
159
  params = {
158
160
  "bizType": f"{biz_type}",
159
161
  }
160
162
  headers = self.get_headers(params)
161
163
  headers["Only_one_position"] = f"{only_one_position}"
162
- response = await self.httpx_client.get(
164
+ return await self.invoke_get(
163
165
  f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
164
166
  headers=headers,
165
167
  params=params,
168
+ model=HotSearchResponse,
166
169
  )
167
- return HotSearchResponse.deserialize(response.json(parse_float=Decimal))
168
170
 
169
- async def get_homepage(self, only_one_position: int = 0, biz_type: int = 30):
171
+ async def get_homepage(
172
+ self,
173
+ only_one_position: int = 0,
174
+ biz_type: int = 30,
175
+ ) -> HomePageResponse:
170
176
  params = {
171
177
  "biz-type": f"{biz_type}",
172
178
  }
173
179
  headers = self.get_headers(params)
174
180
  headers["Only_one_position"] = f"{only_one_position}"
175
- response = await self.httpx_client.get(
181
+ return await self.invoke_get(
176
182
  f"{self.we_api_base_url}/coin/v1/discovery/homepage",
177
183
  headers=headers,
178
184
  params=params,
185
+ model=HomePageResponse,
179
186
  )
180
- return HomePageResponse.deserialize(response.json(parse_float=Decimal))
181
187
 
182
188
  # endregion
183
189
  ###########################################################
184
190
  # region customer
185
- async def get_zendesk_ab_status(self):
191
+ async def get_zendesk_ab_status(self) -> ZenDeskABStatusResponse:
186
192
  headers = self.get_headers()
187
- response = await self.httpx_client.get(
193
+ return await self.invoke_get(
188
194
  f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
189
195
  headers=headers,
196
+ model=ZenDeskABStatusResponse,
190
197
  )
191
- return ZenDeskABStatusResponse.deserialize(response.json(parse_float=Decimal))
192
198
 
193
199
  # endregion
194
200
  ###########################################################
195
201
  # region platform-tool
196
202
  async def get_hint_list(self) -> HintListResponse:
197
203
  headers = self.get_headers()
198
- response = await self.httpx_client.get(
204
+ return await self.invoke_get(
199
205
  f"{self.we_api_base_url}/platform-tool/v1/hint/list",
200
206
  headers=headers,
207
+ model=HintListResponse,
201
208
  )
202
- return HintListResponse.deserialize(response.json(parse_float=Decimal))
203
209
 
204
210
  # endregion
205
211
  ###########################################################
206
212
  # region asset-manager
207
213
  async def get_assets_info(self) -> AssetsInfoResponse:
208
214
  headers = self.get_headers(needs_auth=True)
209
- response = await self.httpx_client.get(
215
+ return await self.invoke_get(
210
216
  f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
211
217
  headers=headers,
218
+ model=AssetsInfoResponse,
212
219
  )
213
- return AssetsInfoResponse.deserialize(response.json(parse_float=Decimal))
214
220
 
215
221
  # endregion
216
222
  ###########################################################
@@ -234,12 +240,140 @@ class BXUltraClient:
234
240
  if margin_coin_name:
235
241
  params["marginCoinName"] = margin_coin_name
236
242
  headers = self.get_headers(params, needs_auth=True)
237
- response = await self.httpx_client.get(
243
+ return await self.invoke_get(
238
244
  f"{self.we_api_base_url}/v4/contract/order/hold",
239
245
  headers=headers,
240
246
  params=params,
247
+ model=ContractsListResponse,
248
+ )
249
+
250
+ async def get_contract_order_history(
251
+ self,
252
+ fund_type: int = 1,
253
+ paging_size: int = 10,
254
+ page_id: int = 0,
255
+ from_order_no: int = 0,
256
+ margin_coin_name: str = "USDT",
257
+ margin_type: int = 0,
258
+ ) -> ContractOrdersHistoryResponse:
259
+ params = {
260
+ "fundType": f"{fund_type}",
261
+ "pagingSize": f"{paging_size}",
262
+ "pageId": f"{page_id}",
263
+ "marginCoinName": margin_coin_name,
264
+ "marginType": f"{margin_type}",
265
+ }
266
+ if from_order_no:
267
+ params["fromOrderNo"] = f"{from_order_no}"
268
+
269
+ headers = self.get_headers(params, needs_auth=True)
270
+ return await self.invoke_get(
271
+ f"{self.we_api_base_url}/v2/contract/order/history",
272
+ headers=headers,
273
+ params=params,
274
+ model=ContractOrdersHistoryResponse,
275
+ )
276
+
277
+ async def get_today_contract_earnings(
278
+ self,
279
+ margin_coin_name: str = "USDT",
280
+ page_size: int = 10,
281
+ max_total_size: int = 500,
282
+ delay_per_fetch: float = 1.0,
283
+ ) -> Decimal:
284
+ """
285
+ Fetches today's earnings from the contract orders.
286
+ NOTE: This method is a bit slow due to the API rate limiting.
287
+ NOTE: If the user has not opened ANY contract orders today,
288
+ this method will return None.
289
+ """
290
+ return await self._get_period_contract_earnings(
291
+ period="day",
292
+ margin_coin_name=margin_coin_name,
293
+ page_size=page_size,
294
+ max_total_size=max_total_size,
295
+ delay_per_fetch=delay_per_fetch,
296
+ )
297
+
298
+ async def get_this_week_contract_earnings(
299
+ self,
300
+ margin_coin_name: str = "USDT",
301
+ page_size: int = 10,
302
+ max_total_size: int = 500,
303
+ delay_per_fetch: float = 1.0,
304
+ ) -> Decimal:
305
+ """
306
+ Fetches this week's earnings from the contract orders.
307
+ NOTE: This method is a bit slow due to the API rate limiting.
308
+ NOTE: If the user has not opened ANY contract orders this week,
309
+ this method will return None.
310
+ """
311
+ return await self._get_period_contract_earnings(
312
+ period="week",
313
+ margin_coin_name=margin_coin_name,
314
+ page_size=page_size,
315
+ max_total_size=max_total_size,
316
+ delay_per_fetch=delay_per_fetch,
317
+ )
318
+
319
+ async def get_this_month_contract_earnings(
320
+ self,
321
+ margin_coin_name: str = "USDT",
322
+ page_size: int = 10,
323
+ max_total_size: int = 500,
324
+ delay_per_fetch: float = 1.0,
325
+ ) -> Decimal:
326
+ """
327
+ Fetches this month's earnings from the contract orders.
328
+ NOTE: This method is a bit slow due to the API rate limiting.
329
+ NOTE: If the user has not opened ANY contract orders this week,
330
+ this method will return None.
331
+ """
332
+ return await self._get_period_contract_earnings(
333
+ period="month",
334
+ margin_coin_name=margin_coin_name,
335
+ page_size=page_size,
336
+ max_total_size=max_total_size,
337
+ delay_per_fetch=delay_per_fetch,
241
338
  )
242
- return ContractsListResponse.deserialize(response.json(parse_float=Decimal))
339
+
340
+ async def _get_period_contract_earnings(
341
+ self,
342
+ period: str,
343
+ margin_coin_name: str = "USDT",
344
+ page_size: int = 10,
345
+ max_total_size: int = 500,
346
+ delay_per_fetch: float = 1.0,
347
+ ) -> Decimal:
348
+ total_fetched = 0
349
+ total_earnings = Decimal("0.00")
350
+ has_earned_any = False
351
+ while total_fetched < max_total_size:
352
+ current_page = total_fetched // page_size
353
+ result = await self.get_contract_order_history(
354
+ page_id=current_page,
355
+ paging_size=page_size,
356
+ margin_coin_name=margin_coin_name,
357
+ )
358
+ if period == "day":
359
+ temp_earnings = result.get_today_earnings()
360
+ elif period == "week":
361
+ temp_earnings = result.get_this_week_earnings()
362
+ elif period == "month":
363
+ temp_earnings = result.get_this_month_earnings()
364
+ if temp_earnings is None:
365
+ # all ended
366
+ break
367
+ total_earnings += temp_earnings
368
+ has_earned_any = True
369
+ total_fetched += page_size
370
+ if result.get_orders_len() < page_size:
371
+ break
372
+ await asyncio.sleep(delay_per_fetch)
373
+
374
+ if not has_earned_any:
375
+ return None
376
+ return total_earnings
243
377
 
244
378
  # endregion
245
379
  ###########################################################
@@ -260,13 +394,11 @@ class BXUltraClient:
260
394
  "copyTradeLabelType": f"{copy_trade_label_type}",
261
395
  }
262
396
  headers = self.get_headers(params)
263
- response = await self.httpx_client.get(
397
+ return await self.invoke_get(
264
398
  f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
265
399
  headers=headers,
266
400
  params=params,
267
- )
268
- return CopyTraderTradePositionsResponse.deserialize(
269
- response.json(parse_float=Decimal)
401
+ model=CopyTraderTradePositionsResponse,
270
402
  )
271
403
 
272
404
  async def search_copy_traders(
@@ -298,25 +430,25 @@ class BXUltraClient:
298
430
  "nickName": nick_name,
299
431
  }
300
432
  headers = self.get_headers(payload)
301
- response = await self.httpx_client.post(
433
+ return await self.invoke_post(
302
434
  f"{self.we_api_base_url}/v6/copy-trade/search/search",
303
435
  headers=headers,
304
436
  params=params,
305
- content=json.dumps(payload, separators=(",", ":"), sort_keys=True),
437
+ content=payload,
438
+ model=SearchCopyTradersResponse,
306
439
  )
307
- return SearchCopyTradersResponse.deserialize(response.json(parse_float=Decimal))
308
440
 
309
441
  # endregion
310
442
  ###########################################################
311
443
  # region welfare
312
444
  async def do_daily_check_in(self):
313
445
  headers = self.get_headers(needs_auth=True)
314
- response = await self.httpx_client.post(
446
+ return await self.invoke_post(
315
447
  f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
316
448
  headers=headers,
317
449
  content="",
450
+ model=None,
318
451
  )
319
- return response.json()
320
452
 
321
453
  # endregion
322
454
  ###########################################################
@@ -352,7 +484,7 @@ class BXUltraClient:
352
484
  payload_data=payload,
353
485
  ),
354
486
  "Timestamp": f"{the_timestamp}",
355
- # 'Accept-Encoding': 'gzip, deflate',
487
+ 'Accept-Encoding': 'gzip, deflate',
356
488
  "User-Agent": self.user_agent,
357
489
  "Connection": "close",
358
490
  "appsiteid": "0",
@@ -365,6 +497,51 @@ class BXUltraClient:
365
497
  the_headers["Authorization"] = f"Bearer {self.authorization_token}"
366
498
  return the_headers
367
499
 
500
+ async def invoke_get(
501
+ self,
502
+ url: str,
503
+ headers: dict | None = None,
504
+ params: dict | None = None,
505
+ model: Type[BxApiResponse] | None = None,
506
+ parse_float=Decimal,
507
+ ) -> "BxApiResponse":
508
+ """
509
+ Invokes the specific request to the specific url with the specific params and headers.
510
+ """
511
+ response = await self.httpx_client.get(
512
+ url=url,
513
+ headers=headers,
514
+ params=params,
515
+ )
516
+ return model.deserialize(response.json(parse_float=parse_float))
517
+
518
+ async def invoke_post(
519
+ self,
520
+ url: str,
521
+ headers: dict | None = None,
522
+ params: dict | None = None,
523
+ content: dict | str | bytes = "",
524
+ model: Type[BxApiResponse] | None = None,
525
+ parse_float=Decimal,
526
+ ) -> "BxApiResponse":
527
+ """
528
+ Invokes the specific request to the specific url with the specific params and headers.
529
+ """
530
+
531
+ if isinstance(content, dict):
532
+ content = json.dumps(content, separators=(",", ":"), sort_keys=True)
533
+
534
+ response = await self.httpx_client.post(
535
+ url=url,
536
+ headers=headers,
537
+ params=params,
538
+ content=content,
539
+ )
540
+ if not model:
541
+ return response.json()
542
+
543
+ return model.deserialize(response.json(parse_float=parse_float))
544
+
368
545
  async def aclose(self) -> None:
369
546
  await self.httpx_client.aclose()
370
547
  logger.info("BXUltraClient closed")
@@ -1,9 +1,7 @@
1
1
  import hashlib
2
2
  import json
3
3
  import uuid
4
- from decimal import Decimal
5
4
 
6
- default_quantize = Decimal("1.00")
7
5
 
8
6
  default_e: str = (
9
7
  "\u0039\u0035\u0064\u0036\u0035\u0063\u0037\u0033\u0064\u0063\u0035"
@@ -17,12 +15,6 @@ long_accept_header1: str = (
17
15
  + "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
18
16
  )
19
17
 
20
- def dec_to_str(dec_value: Decimal) -> str:
21
- return format(dec_value.quantize(default_quantize), "f")
22
-
23
- def dec_to_normalize(dec_value: Decimal) -> str:
24
- return format(dec_value.normalize(), "f")
25
-
26
18
  def do_ultra_ss(
27
19
  e_param: str,
28
20
  se_param: str,
@@ -0,0 +1,76 @@
1
+
2
+ from decimal import Decimal
3
+ from typing import Any
4
+ from abc import ABC
5
+
6
+ import httpx
7
+
8
+
9
+ class ExchangeBase(ABC):
10
+ ###########################################################
11
+ # region client parameters
12
+ user_agent: str = "okhttp/4.12.0"
13
+ x_requested_with: str = None
14
+ httpx_client: httpx.AsyncClient = None
15
+ account_name: str = "default"
16
+ sessions_dir: str = "sessions"
17
+
18
+ authorization_token: str = None
19
+ device_id: str = None
20
+ trace_id: str = None
21
+ app_version: str = "4.28.3"
22
+ platform_id: str = "10"
23
+ install_channel: str = "officialAPK"
24
+ channel_header: str = "officialAPK"
25
+
26
+ _fav_letter: str = "^"
27
+ # endregion
28
+ ###########################################################
29
+ # region client helper methods
30
+ def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
31
+ pass
32
+
33
+ async def invoke_get(
34
+ self,
35
+ url: str,
36
+ headers: dict | None,
37
+ params: dict | None,
38
+ model: Any,
39
+ parse_float=Decimal,
40
+ ) -> Any:
41
+ """
42
+ Invokes the specific request to the specific url with the specific params and headers.
43
+ """
44
+ pass
45
+
46
+ async def invoke_post(
47
+ self,
48
+ url: str,
49
+ headers: dict | None = None,
50
+ params: dict | None = None,
51
+ content: str | bytes = "",
52
+ model: None = None,
53
+ parse_float=Decimal,
54
+ ):
55
+ """
56
+ Invokes the specific request to the specific url with the specific params and headers.
57
+ """
58
+ pass
59
+
60
+ async def aclose(self) -> None:
61
+ pass
62
+
63
+ def read_from_session_file(self, file_path: str) -> None:
64
+ """
65
+ Reads from session file; if it doesn't exist, creates it.
66
+ """
67
+ pass
68
+
69
+ def _save_session_file(self, file_path: str) -> None:
70
+ """
71
+ Saves current information to the session file.
72
+ """
73
+ pass
74
+
75
+ # endregion
76
+ ###########################################################