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.
- {trd_utils-0.0.28 → trd_utils-0.0.29}/PKG-INFO +3 -1
- {trd_utils-0.0.28 → trd_utils-0.0.29}/pyproject.toml +3 -1
- trd_utils-0.0.29/trd_utils/__init__.py +3 -0
- trd_utils-0.0.29/trd_utils/common_utils/float_utils.py +21 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/base_types.py +8 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_types.py +144 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +211 -4
- trd_utils-0.0.29/trd_utils/exchanges/errors.py +10 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/exchange_base.py +60 -0
- trd_utils-0.0.28/trd_utils/__init__.py +0 -3
- trd_utils-0.0.28/trd_utils/common_utils/float_utils.py +0 -11
- {trd_utils-0.0.28 → trd_utils-0.0.29}/LICENSE +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/common_utils/wallet_utils.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/date_utils/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/date_utils/datetime_helpers.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/blofin_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/blofin/blofin_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/okx_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/exchanges/okx/okx_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/tradingview_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/tradingview/tradingview_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.29}/trd_utils/types_helper/__init__.py +0 -0
- {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.
|
|
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.
|
|
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,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(
|
|
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
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|