trd-utils 0.0.28__tar.gz → 0.0.29__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 (38) hide show
  1. {trd_utils-0.0.28 → trd_utils-0.0.29}/PKG-INFO +3 -1
  2. {trd_utils-0.0.28 → trd_utils-0.0.29}/pyproject.toml +3 -1
  3. trd_utils-0.0.29/trd_utils/__init__.py +3 -0
  4. trd_utils-0.0.29/trd_utils/common_utils/float_utils.py +21 -0
  5. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/base_types.py +8 -0
  6. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_types.py +144 -0
  7. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +211 -4
  8. trd_utils-0.0.29/trd_utils/exchanges/errors.py +10 -0
  9. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/exchange_base.py +60 -0
  10. trd_utils-0.0.28/trd_utils/__init__.py +0 -3
  11. trd_utils-0.0.28/trd_utils/common_utils/float_utils.py +0 -11
  12. {trd_utils-0.0.28 → trd_utils-0.0.29}/LICENSE +0 -0
  13. {trd_utils-0.0.28 → trd_utils-0.0.29}/README.md +0 -0
  14. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/cipher/__init__.py +0 -0
  15. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/common_utils/wallet_utils.py +0 -0
  16. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/date_utils/__init__.py +0 -0
  17. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/date_utils/datetime_helpers.py +0 -0
  18. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/README.md +0 -0
  19. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/__init__.py +0 -0
  20. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/__init__.py +0 -0
  21. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/blofin_client.py +0 -0
  22. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/blofin_types.py +0 -0
  23. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
  24. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
  25. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/README.md +0 -0
  26. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/__init__.py +0 -0
  27. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +0 -0
  28. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +0 -0
  29. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/__init__.py +0 -0
  30. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/okx_client.py +0 -0
  31. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/okx_types.py +0 -0
  32. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/html_utils/__init__.py +0 -0
  33. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/html_utils/html_formats.py +0 -0
  34. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/__init__.py +0 -0
  35. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/tradingview_client.py +0 -0
  36. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/tradingview_types.py +0 -0
  37. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/types_helper/__init__.py +0 -0
  38. {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/types_helper/base_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trd_utils
3
- Version: 0.0.28
3
+ Version: 0.0.29
4
4
  Summary: Common Basic Utils for Python3. By ALiwoto.
5
5
  Keywords: utils,trd_utils,basic-utils,common-utils
6
6
  Author: ALiwoto
@@ -19,6 +19,8 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Dist: cryptography (>=41.0.7)
21
21
  Requires-Dist: httpx (>=0.21.0)
22
+ Requires-Dist: python-dateutil (>=2.9.0.post0)
23
+ Requires-Dist: websockets (>=15.0.1)
22
24
  Project-URL: Homepage, https://github.com/ALiwoto/trd_utils
23
25
  Description-Content-Type: text/markdown
24
26
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "trd_utils"
3
- version = "0.0.28"
3
+ version = "0.0.29"
4
4
  description = "Common Basic Utils for Python3. By ALiwoto."
5
5
  authors = ["ALiwoto <aminnimaj@gmail.com>"]
6
6
  packages = [
@@ -20,6 +20,8 @@ homepage = "https://github.com/ALiwoto/trd_utils"
20
20
  python = ">=3.7"
21
21
  httpx = ">=0.21.0"
22
22
  cryptography = ">=41.0.7"
23
+ websockets = ">=15.0.1"
24
+ python-dateutil = ">=2.9.0.post0"
23
25
 
24
26
  [tool.poetry.dev-dependencies]
25
27
  pytest = ">=7.2"
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.0.29"
3
+
@@ -0,0 +1,21 @@
1
+
2
+ from decimal import Decimal
3
+
4
+
5
+ default_quantize = Decimal("1.00")
6
+
7
+ def dec_to_str(dec_value: Decimal) -> str:
8
+ return format(dec_value.quantize(default_quantize), "f")
9
+
10
+ def dec_to_normalize(dec_value: Decimal) -> str:
11
+ return format(dec_value.normalize(), "f")
12
+
13
+ def as_decimal(value) -> Decimal:
14
+ if value is None:
15
+ return None
16
+
17
+ if isinstance(value, Decimal):
18
+ # prevent extra allocation
19
+ return value
20
+
21
+ return Decimal(value)
@@ -37,6 +37,14 @@ class UnifiedPositionInfo(BaseModel):
37
37
  # The base unit that the open-price is based on (e.g. USD, USDT, USDC)
38
38
  open_price_unit: str | None = None
39
39
 
40
+ # The last price of this pair on the target exchange.
41
+ # not all exchanges support this yet, so use it with caution.
42
+ last_price: Decimal | None = None
43
+
44
+ # The last volume of this pair being traded on the target exchange.
45
+ # not all exchanges support this yet, so use it with caution.
46
+ last_volume: Decimal | None = None
47
+
40
48
  def __str__(self):
41
49
  parts = []
42
50
 
@@ -3,9 +3,11 @@ from decimal import Decimal
3
3
  from datetime import datetime, timedelta
4
4
  import pytz
5
5
 
6
+ from trd_utils.exchanges.errors import ExchangeError
6
7
  from trd_utils.types_helper import BaseModel
7
8
 
8
9
  from trd_utils.common_utils.float_utils import (
10
+ as_decimal,
9
11
  dec_to_str,
10
12
  dec_to_normalize,
11
13
  )
@@ -88,6 +90,28 @@ class ExchangeVo(BaseModel):
88
90
  return f"{self.exchange_name} ({self.exchange_id}) - {self.account_enum}"
89
91
 
90
92
 
93
+ class OrderLeverInfo(BaseModel):
94
+ lever_times: int = None
95
+ selected: bool = False
96
+
97
+ def __str__(self):
98
+ return f"{self.lever_times}x"
99
+
100
+ def __repr__(self):
101
+ return self.__str__()
102
+
103
+
104
+ class MarginDisplayInfo(BaseModel):
105
+ margin: Decimal = None
106
+ selected: bool = False
107
+
108
+
109
+ class SysForceVoInfo(BaseModel):
110
+ begin_level: int = None
111
+ end_level: int = None
112
+ adjust_margin_rate: Decimal = None
113
+
114
+
91
115
  # endregion
92
116
 
93
117
  ###########################################################
@@ -387,12 +411,15 @@ class ZenDeskABStatusResult(BaseModel):
387
411
  class ZenDeskABStatusResponse(BxApiResponse):
388
412
  data: ZenDeskABStatusResult = None
389
413
 
414
+
390
415
  class ZenDeskAuthResult(BaseModel):
391
416
  jwt: str = None
392
417
 
418
+
393
419
  class ZenDeskAuthResponse(BxApiResponse):
394
420
  data: ZenDeskAuthResult = None
395
421
 
422
+
396
423
  # endregion
397
424
 
398
425
  ###########################################################
@@ -550,6 +577,7 @@ class CopyTraderInfo(BaseModel):
550
577
  short_uid: int = None
551
578
  identity_type: int = None
552
579
 
580
+
553
581
  class CopyTraderVo(BaseModel):
554
582
  audit_status: int = None
555
583
  trader_status: int = None
@@ -565,6 +593,7 @@ class CopyTraderVo(BaseModel):
565
593
  rank_account_id: int = None
566
594
  last_trader_time: datetime = None
567
595
 
596
+
568
597
  class CopyTraderAccountGradeVO(BaseModel):
569
598
  uid: int = None
570
599
  api_identity: int = None
@@ -572,6 +601,7 @@ class CopyTraderAccountGradeVO(BaseModel):
572
601
  label: int = None
573
602
  uid_and_api: str = None
574
603
 
604
+
575
605
  class CopyTraderSharingAccount(BaseModel):
576
606
  category: int = None
577
607
  trader: int = None
@@ -588,6 +618,7 @@ class CopyTraderSharingAccount(BaseModel):
588
618
  hide_info: int = None
589
619
  copy_trade_label_type: Optional[int] = None
590
620
 
621
+
591
622
  class CopyTraderResumeResult(BaseModel):
592
623
  trader_info: CopyTraderInfo = None
593
624
  trader_vo: CopyTraderVo = None
@@ -605,9 +636,11 @@ class CopyTraderResumeResult(BaseModel):
605
636
  swap_copy_trade_label_type: int = None
606
637
  is_pro: int = None
607
638
 
639
+
608
640
  class CopyTraderResumeResponse(BxApiResponse):
609
641
  data: CopyTraderResumeResult = None
610
642
 
643
+
611
644
  # endregion
612
645
 
613
646
  ###########################################################
@@ -782,6 +815,13 @@ class SearchCopyTradersResponse(BxApiResponse):
782
815
  # region Account Assets types
783
816
 
784
817
 
818
+ class MinimalAssetInfo(BaseModel):
819
+ asset_id: int = None
820
+ asset_amount: Decimal = None
821
+ asset_name: str = None
822
+ has_value: bool = None
823
+
824
+
785
825
  class TotalAssetsInfo(BaseModel):
786
826
  amount: Any = None # unknown
787
827
  currency_amount: Decimal = None
@@ -1042,6 +1082,9 @@ class ContractOrdersHistoryResponse(BxApiResponse):
1042
1082
  found_any_for_today: bool = False
1043
1083
  today_earnings = Decimal("0.00")
1044
1084
  today = datetime.now(timezone).date()
1085
+ if not self.data and self.msg:
1086
+ raise ExchangeError(self.msg)
1087
+
1045
1088
  for current_order in self.data.orders:
1046
1089
  # check if the date is for today
1047
1090
  closed_date = (
@@ -1123,6 +1166,107 @@ class ContractOrdersHistoryResponse(BxApiResponse):
1123
1166
  return len(self.data.orders)
1124
1167
 
1125
1168
 
1169
+ class ContractConfigData(BaseModel):
1170
+ quotation_coin_id: int = None
1171
+ max_lever: OrderLeverInfo = None
1172
+ min_amount: Decimal = None
1173
+ levers: list[OrderLeverInfo] = None
1174
+ default_stop_loss_rate: Decimal = None
1175
+ default_stop_profit_rate: Decimal = None
1176
+ max_stop_loss_rate: Decimal = None
1177
+ max_stop_profit_rate: Decimal = None
1178
+ fee_rate: Decimal = None
1179
+ interest_rate: Decimal = None
1180
+ lever_fee_rate: Decimal = None
1181
+ sys_force_rate: Decimal = None
1182
+ new_sys_force_vo_list: list[SysForceVoInfo] = None
1183
+ margin_displays: list[MarginDisplayInfo] = None
1184
+ mlr: Decimal = None
1185
+ lsf: Decimal = None
1186
+ lsh: Decimal = None
1187
+ hold_amount: Decimal = None
1188
+ msr: Decimal = None
1189
+ sfa: Decimal = None
1190
+ available_asset: MinimalAssetInfo = None
1191
+ coupon_asset_value: Any = None
1192
+ contract_account_balance: MinimalAssetInfo = None
1193
+ delegate_order_up_threshold_rate: Any = None
1194
+ delegate_order_down_threshold_rate: Any = None
1195
+ profit_loss_extra_vo: Any = None
1196
+ fund_balance: Decimal = None
1197
+ balance: Decimal = None
1198
+ up_amount: Decimal = None
1199
+ down_amount: Decimal = None
1200
+ max_amount: Decimal = None
1201
+ stop_offset_rate: Decimal = None
1202
+
1203
+
1204
+ class ContractConfigResponse(BxApiResponse):
1205
+ data: ContractConfigData = None
1206
+
1207
+
1208
+ # endregion
1209
+
1210
+ ###########################################################
1211
+
1212
+ # region contract delegation types
1213
+
1214
+
1215
+ class CreateOrderDelegationData(BaseModel):
1216
+ order_id: str = None
1217
+ spread_rate: str = None
1218
+
1219
+
1220
+ class CreateOrderDelegationResponse(BxApiResponse):
1221
+ data: CreateOrderDelegationData = None
1222
+
1223
+
1224
+ # endregion
1225
+
1226
+ ###########################################################
1227
+
1228
+ # region candle types
1229
+
1230
+
1231
+ class SingleCandleInfo(BaseModel):
1232
+ # The pair in format of BTC/USDT.
1233
+ pair: str = None
1234
+
1235
+ # This candle's open price.
1236
+ open_price: Decimal = None
1237
+
1238
+ # The close price.
1239
+ close_price: Decimal = None
1240
+
1241
+ # volume in the first pair (e.g. BTC).
1242
+ volume: Decimal = None
1243
+
1244
+ # volume in the second part of the pair (e.g. USDT).
1245
+ quote_volume: Decimal = None
1246
+
1247
+ @staticmethod
1248
+ def deserialize_short(data: dict) -> "SingleCandleInfo":
1249
+ info = SingleCandleInfo()
1250
+ base: str = data.get("n", "")
1251
+ quote: str = data.get("m", "")
1252
+ info.pair = f"{base.upper()}/{quote.upper()}"
1253
+ info.open_price = as_decimal(data.get("o", None))
1254
+ info.close_price = as_decimal(data.get("c", None))
1255
+ info.volume = data.get("v", None)
1256
+ info.quote_volume = data.get("a", None)
1257
+
1258
+ return info
1259
+
1260
+ def __str__(self):
1261
+ return (
1262
+ f"{self.pair}, open: {self.open_price}, "
1263
+ f"close: {self.close_price}, volume: {self.quote_volume}"
1264
+ )
1265
+
1266
+ def __repr__(self):
1267
+ return super().__str__()
1268
+
1269
+
1126
1270
  # endregion
1127
1271
 
1128
1272
  ###########################################################
@@ -3,9 +3,12 @@ BxUltra exchange subclass
3
3
  """
4
4
 
5
5
  import asyncio
6
+ from datetime import datetime, timedelta, timezone
6
7
  from decimal import Decimal
7
8
  import json
8
9
  import logging
10
+ import gzip
11
+ from urllib.parse import urlencode
9
12
  import uuid
10
13
 
11
14
  import httpx
@@ -13,6 +16,11 @@ import httpx
13
16
  import time
14
17
  from pathlib import Path
15
18
 
19
+ import websockets
20
+ import websockets.asyncio
21
+ import websockets.asyncio.client
22
+ import websockets.asyncio.connection
23
+
16
24
  from trd_utils.exchanges.base_types import (
17
25
  UnifiedPositionInfo,
18
26
  UnifiedTraderInfo,
@@ -21,17 +29,20 @@ from trd_utils.exchanges.base_types import (
21
29
  from trd_utils.exchanges.bx_ultra.bx_utils import do_ultra_ss
22
30
  from trd_utils.exchanges.bx_ultra.bx_types import (
23
31
  AssetsInfoResponse,
32
+ ContractConfigResponse,
24
33
  ContractOrdersHistoryResponse,
25
34
  ContractsListResponse,
26
35
  CopyTraderFuturesStatsResponse,
27
36
  CopyTraderResumeResponse,
28
37
  CopyTraderTradePositionsResponse,
38
+ CreateOrderDelegationResponse,
29
39
  HintListResponse,
30
40
  HomePageResponse,
31
41
  HotSearchResponse,
32
42
  QuotationRankResponse,
33
43
  SearchCopyTraderCondition,
34
44
  SearchCopyTradersResponse,
45
+ SingleCandleInfo,
35
46
  UserFavoriteQuotationResponse,
36
47
  ZenDeskABStatusResponse,
37
48
  ZenDeskAuthResponse,
@@ -39,7 +50,7 @@ from trd_utils.exchanges.bx_ultra.bx_types import (
39
50
  )
40
51
  from trd_utils.cipher import AESCipher
41
52
 
42
- from trd_utils.exchanges.exchange_base import ExchangeBase
53
+ from trd_utils.exchanges.exchange_base import ExchangeBase, JWTManager
43
54
 
44
55
  PLATFORM_ID_ANDROID = "10"
45
56
  PLATFORM_ID_WEB = "30"
@@ -71,6 +82,7 @@ class BXUltraClient(ExchangeBase):
71
82
  # region client parameters
72
83
  we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
73
84
  we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
85
+ ws_we_api_base_url: str = "wss://ws-market-swap.w\u0065-\u0061pi.com/ws"
74
86
  original_base_host: str = "https://\u0062ing\u0078.co\u006d"
75
87
  qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
76
88
  qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
@@ -85,6 +97,9 @@ class BXUltraClient(ExchangeBase):
85
97
  platform_lang: str = "en"
86
98
  sys_lang: str = "en"
87
99
 
100
+ # a dict that maps "BTC/USDT" to it single candle info.
101
+ __last_candle_storage: dict = None
102
+ __last_candle_lock: asyncio.Lock = None
88
103
  # endregion
89
104
  ###########################################################
90
105
  # region client constructor
@@ -106,8 +121,16 @@ class BXUltraClient(ExchangeBase):
106
121
  self.device_brand = device_brand
107
122
  self.app_version = app_version
108
123
  self._fav_letter = fav_letter
124
+ self.sessions_dir = sessions_dir
109
125
 
110
- self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bx")
126
+ self.read_from_session_file(
127
+ file_path=f"{self.sessions_dir}/{self.account_name}.bx"
128
+ )
129
+ self.__last_candle_storage = {}
130
+ self.__last_candle_lock = asyncio.Lock()
131
+ self._internal_lock = asyncio.Lock()
132
+ self.extra_tasks = []
133
+ self.ws_connections = []
111
134
 
112
135
  # endregion
113
136
  ###########################################################
@@ -222,6 +245,15 @@ class BXUltraClient(ExchangeBase):
222
245
  model_type=ZenDeskAuthResponse,
223
246
  )
224
247
 
248
+ async def re_authorize_user(self) -> bool:
249
+ result = await self.do_zendesk_auth()
250
+ if not result.data.jwt:
251
+ return False
252
+
253
+ self.authorization_token = result.data.jwt
254
+ self._save_session_file(file_path=f"{self.sessions_dir}/{self.account_name}.bx")
255
+ return True
256
+
225
257
  # endregion
226
258
  ###########################################################
227
259
  # region platform-tool
@@ -244,9 +276,120 @@ class BXUltraClient(ExchangeBase):
244
276
  model_type=AssetsInfoResponse,
245
277
  )
246
278
 
279
+ # endregion
280
+ ###########################################################
281
+ # region ws-subscribes
282
+ async def do_price_subscribe(self) -> None:
283
+ """
284
+ Subscribes to the price changes coming from the exchange.
285
+ NOTE: This method DOES NOT return. you should do create_task
286
+ for it.
287
+ """
288
+ params = {
289
+ "platformid": self.platform_id,
290
+ "app_version": self.app_version,
291
+ "x-router-tag": self.x_router_tag,
292
+ "lang": self.platform_lang,
293
+ "device_id": self.device_id,
294
+ "channel": self.channel_header,
295
+ "device_brand": self.device_brand,
296
+ "traceId": self.trace_id,
297
+ }
298
+ url = f"{self.ws_we_api_base_url}?{urlencode(params, doseq=True)}"
299
+ async with websockets.connect(url, ping_interval=None) as ws:
300
+ await self._internal_lock.acquire()
301
+ self.ws_connections.append(ws)
302
+ self._internal_lock.release()
303
+
304
+ await ws.send(json.dumps({
305
+ "dataType": "swap.market.v2.contracts",
306
+ "id": uuid.uuid4().hex,
307
+ "reqType": "sub",
308
+ }))
309
+ async for msg in ws:
310
+ try:
311
+ decompressed_message = gzip.decompress(msg)
312
+ if decompressed_message.lower() == "ping":
313
+ await ws.send("Pong")
314
+ continue
315
+
316
+ data: dict = json.loads(decompressed_message, parse_float=Decimal)
317
+ if not isinstance(data, dict):
318
+ logger.warning(f"invalid data instance: {type(data)}")
319
+ continue
320
+
321
+ if data.get("code", 0) == 0 and data.get("data", None) is None:
322
+ # it's all fine
323
+ continue
324
+
325
+ if data.get("ping", None):
326
+ target_id = data["ping"]
327
+ target_time = data.get(
328
+ "time",
329
+ datetime.now(
330
+ timezone(timedelta(hours=8))
331
+ ).isoformat(timespec="seconds")
332
+ )
333
+ await ws.send(json.dumps({
334
+ "pong": target_id,
335
+ "time": target_time,
336
+ }))
337
+ continue
338
+
339
+ inner_data = data.get("data", None)
340
+ if isinstance(inner_data, dict):
341
+ if data.get("dataType", None) == "swap.market.v2.contracts":
342
+ list_data = inner_data.get("l", None)
343
+ await self.__last_candle_lock.acquire()
344
+ for current in list_data:
345
+ info = SingleCandleInfo.deserialize_short(current)
346
+ if info:
347
+ self.__last_candle_storage[info.pair.lower()] = info
348
+ self.__last_candle_lock.release()
349
+ continue
350
+
351
+ logger.info(f"we got some unknown data: {data}")
352
+ except Exception as ex:
353
+ logger.info(f"failed to handle ws message from exchange: {msg}; {ex}")
354
+
355
+ async def get_last_candle(self, pair: str) -> SingleCandleInfo:
356
+ """
357
+ Returns the last candle's info in this exchange.
358
+ This method is safe to be called ONLY from the exact same thread
359
+ that the loop is currently operating on.
360
+ """
361
+ await self.__last_candle_lock.acquire()
362
+ info = self.__last_candle_storage.get(pair.lower())
363
+ self.__last_candle_lock.release()
364
+ return info
365
+
247
366
  # endregion
248
367
  ###########################################################
249
368
  # region contract
369
+ async def get_contract_config(
370
+ self,
371
+ fund_type: int, # e.g. 1
372
+ coin_name: str, # e.g. "SOL"
373
+ valuation_name: str, # e.g. "USDT"
374
+ margin_coin_name: str, # e.g. "USDT"
375
+ ) -> ContractConfigResponse:
376
+ params = {
377
+ "fundType": f"{fund_type}",
378
+ "coinName": f"{coin_name}",
379
+ "valuationName": f"{valuation_name}",
380
+ "marginCoinName": f"{margin_coin_name}",
381
+ }
382
+ headers = self.get_headers(
383
+ payload=params,
384
+ )
385
+ return await self.invoke_get(
386
+ # "https://bingx.com/api/v2/contract/config",
387
+ f"{self.qq_os_base_url}/v2/contract/config",
388
+ headers=headers,
389
+ params=params,
390
+ model_type=CopyTraderTradePositionsResponse,
391
+ )
392
+
250
393
  async def get_contract_list(
251
394
  self,
252
395
  quotation_coin_id: int = -1,
@@ -403,6 +546,60 @@ class BXUltraClient(ExchangeBase):
403
546
 
404
547
  # endregion
405
548
  ###########################################################
549
+ # region contract delegation
550
+ async def create_order_delegation(
551
+ self,
552
+ balance_direction: int, # e.g. 1
553
+ delegate_price: Decimal, # e.g. 107414.70
554
+ fund_type: int, # # e.g. 1
555
+ large_spread_rate: int, # e.g. 0
556
+ lever_times: int, # e.g. 5
557
+ margin: int, # e.g. 5
558
+ margin_coin_name: str, # e.g. "USDT"
559
+ market_factor: int, # e.g. 1
560
+ order_type: int, # e.g. 0
561
+ price: Decimal, # the current price of the market??
562
+ stop_loss_rate: int, # e.g. -1
563
+ stop_profit_rate: int, # e.g. -1
564
+ quotation_coin_id: int, # e.g. 1
565
+ spread_rate: float, # something very low. e.g. 0.00003481
566
+ stop_loss_price: float, # e.g. -1
567
+ stop_profit_price: float, # e.g. -1
568
+ up_ratio: Decimal, # e.g. 0.5
569
+ ) -> CreateOrderDelegationResponse:
570
+ payload = {
571
+ "balanceDirection": balance_direction,
572
+ "delegatePrice": f"{delegate_price}",
573
+ "fundType": fund_type,
574
+ "largeSpreadRate": large_spread_rate or 0,
575
+ "leverTimes": lever_times or 1,
576
+ "margin": margin,
577
+ "marginCoinName": margin_coin_name or "USDT",
578
+ "marketFactor": market_factor or 1,
579
+ "orderType": f"{order_type or 0}",
580
+ "price": float(price), # e.g. 107161.27
581
+ "profitLossRateDto": {
582
+ "stopProfitRate": stop_profit_rate or -1,
583
+ "stopLossRate": stop_loss_rate or -1,
584
+ },
585
+ "quotationCoinId": quotation_coin_id or 1,
586
+ "spreadRate": float(spread_rate) or 0.00003481,
587
+ "stopLossPrice": stop_loss_price or -1,
588
+ "stopProfitPrice": stop_profit_price or -1,
589
+ "upRatio": f"{0.5 if up_ratio is None else up_ratio}",
590
+ }
591
+ headers = self.get_headers(
592
+ needs_auth=True,
593
+ payload=payload,
594
+ )
595
+ return await self.invoke_post(
596
+ f"{self.we_api_base_url}/v2/contract/order/delegation",
597
+ headers=headers,
598
+ content=payload,
599
+ model_type=CreateOrderDelegationResponse,
600
+ )
601
+ # endregion
602
+ ###########################################################
406
603
  # region copy-trade-facade
407
604
  async def get_copy_trader_positions(
408
605
  self,
@@ -505,6 +702,7 @@ class BXUltraClient(ExchangeBase):
505
702
  self,
506
703
  uid: int | str,
507
704
  ) -> int | str:
705
+ global user_api_identity_cache
508
706
  api_identity = user_api_identity_cache.get(uid, None)
509
707
  if not api_identity:
510
708
  resume = await self.get_copy_trader_resume(
@@ -595,6 +793,8 @@ class BXUltraClient(ExchangeBase):
595
793
  self.authorization_token = json_data.get(
596
794
  "authorization_token", self.authorization_token
597
795
  )
796
+ if self.authorization_token:
797
+ self.jwt_manager = JWTManager(self.jwt_manager)
598
798
  self.app_id = json_data.get("app_id", self.app_id)
599
799
  self.trade_env = json_data.get("trade_env", self.trade_env)
600
800
  self.timezone = json_data.get("timezone", self.timezone)
@@ -608,6 +808,9 @@ class BXUltraClient(ExchangeBase):
608
808
  """
609
809
  Saves current information to the session file.
610
810
  """
811
+ if file_path is None:
812
+ file_path = f"{self.sessions_dir}/{self.account_name}.bx"
813
+
611
814
  if not self.device_id:
612
815
  self.device_id = uuid.uuid4().hex.replace("-", "") + "##"
613
816
 
@@ -645,8 +848,6 @@ class BXUltraClient(ExchangeBase):
645
848
  self,
646
849
  uid: int | str,
647
850
  ) -> UnifiedTraderPositions:
648
- global user_api_identity_cache
649
-
650
851
  api_identity = await self.get_trader_api_identity(
651
852
  uid=uid,
652
853
  )
@@ -674,6 +875,12 @@ class BXUltraClient(ExchangeBase):
674
875
  unified_pos.open_price_unit = (
675
876
  position.valuation_coin_name or position.symbol.split("-")[-1]
676
877
  ) # TODO
878
+
879
+ last_candle = await self.get_last_candle(unified_pos.position_pair)
880
+ if last_candle:
881
+ unified_pos.last_price = last_candle.close_price
882
+ unified_pos.last_volume = last_candle.quote_volume
883
+
677
884
  unified_result.positions.append(unified_pos)
678
885
 
679
886
  return unified_result
@@ -0,0 +1,10 @@
1
+
2
+
3
+
4
+ class ExchangeError(RuntimeError):
5
+ """
6
+ Specifies an error coming from the exchange.
7
+ """
8
+ def __init__(self, *args):
9
+ super().__init__(*args)
10
+
@@ -1,13 +1,38 @@
1
+ import asyncio
1
2
  from decimal import Decimal
2
3
  import json
4
+ import logging
3
5
  from typing import Type
4
6
  from abc import ABC
5
7
 
8
+ import base64
9
+ import time
10
+
6
11
  import httpx
12
+ from websockets.asyncio.connection import Connection as WSConnection
7
13
 
8
14
  from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
9
15
  from trd_utils.types_helper.base_model import BaseModel
10
16
 
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class JWTManager():
20
+ _jwt_string: str = None
21
+
22
+ def __init__(self, jwt_string: str):
23
+ self._jwt_string = jwt_string
24
+ try:
25
+ payload_b64 = self._jwt_string.split('.')[1]
26
+ payload_bytes = base64.urlsafe_b64decode(payload_b64 + '==')
27
+ self.payload = json.loads(payload_bytes)
28
+ except Exception:
29
+ self.payload = {}
30
+
31
+ def is_expired(self):
32
+ if "exp" not in self.payload:
33
+ return False
34
+
35
+ return time.time() > self.payload["exp"]
11
36
 
12
37
  class ExchangeBase(ABC):
13
38
  ###########################################################
@@ -22,11 +47,23 @@ class ExchangeBase(ABC):
22
47
  device_id: str = None
23
48
  trace_id: str = None
24
49
  app_version: str = "4.28.3"
50
+ x_router_tag: str = "gray-develop"
25
51
  platform_id: str = "10"
26
52
  install_channel: str = "officialAPK"
27
53
  channel_header: str = "officialAPK"
28
54
 
55
+ jwt_manager: JWTManager = None
56
+
29
57
  _fav_letter: str = "^"
58
+
59
+ # the lock for internal operations.
60
+ _internal_lock: asyncio.Lock = None
61
+
62
+ # extra tasks to be cancelled when the client closes.
63
+ extra_tasks: list[asyncio.Task] = None
64
+
65
+ # the ws connections to be closed when this client is closed.
66
+ ws_connections: list[WSConnection] = None
30
67
  # endregion
31
68
  ###########################################################
32
69
 
@@ -169,9 +206,32 @@ class ExchangeBase(ABC):
169
206
  # Now parse the decompressed content
170
207
  return json.loads(content.decode("utf-8"), parse_float=parse_float)
171
208
 
209
+ async def __aenter__(self):
210
+ return self
211
+
212
+ async def __aexit__(
213
+ self,
214
+ exc_type=None,
215
+ exc_value=None,
216
+ traceback=None,
217
+ ) -> None:
218
+ await self.aclose()
219
+
172
220
  async def aclose(self) -> None:
221
+ await self._internal_lock.acquire()
173
222
  await self.httpx_client.aclose()
174
223
 
224
+ if self.ws_connections:
225
+ for current in self.ws_connections:
226
+ try:
227
+ await current.close()
228
+ except Exception as ex:
229
+ logger.warning(f"failed to close ws connection: {ex}")
230
+ continue
231
+ self.ws_connections = []
232
+
233
+ self._internal_lock.release()
234
+
175
235
  # endregion
176
236
  ###########################################################
177
237
  # region data-files related methods
@@ -1,3 +0,0 @@
1
-
2
- __version__ = "0.0.28"
3
-
@@ -1,11 +0,0 @@
1
-
2
- from decimal import Decimal
3
-
4
-
5
- default_quantize = Decimal("1.00")
6
-
7
- def dec_to_str(dec_value: Decimal) -> str:
8
- return format(dec_value.quantize(default_quantize), "f")
9
-
10
- def dec_to_normalize(dec_value: Decimal) -> str:
11
- return format(dec_value.normalize(), "f")
File without changes
File without changes