trd-utils 0.0.28__tar.gz → 0.0.30__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.30}/PKG-INFO +3 -1
- {trd_utils-0.0.28 → trd_utils-0.0.30}/pyproject.toml +3 -1
- trd_utils-0.0.30/trd_utils/__init__.py +3 -0
- trd_utils-0.0.30/trd_utils/common_utils/float_utils.py +21 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/base_types.py +8 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/bx_ultra/bx_types.py +144 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +219 -4
- trd_utils-0.0.30/trd_utils/exchanges/errors.py +10 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/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.30}/LICENSE +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/common_utils/wallet_utils.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/date_utils/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/date_utils/datetime_helpers.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/blofin/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/blofin/blofin_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/blofin/blofin_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/hyperliquid/README.md +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/hyperliquid/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/okx/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/okx/okx_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/exchanges/okx/okx_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/tradingview/tradingview_client.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/tradingview/tradingview_types.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/trd_utils/types_helper/__init__.py +0 -0
- {trd_utils-0.0.28 → trd_utils-0.0.30}/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.30
|
|
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.30"
|
|
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,8 @@ 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.
|
|
53
|
+
from trd_utils.exchanges.errors import ExchangeError
|
|
54
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase, JWTManager
|
|
43
55
|
|
|
44
56
|
PLATFORM_ID_ANDROID = "10"
|
|
45
57
|
PLATFORM_ID_WEB = "30"
|
|
@@ -71,6 +83,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
71
83
|
# region client parameters
|
|
72
84
|
we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
|
|
73
85
|
we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
|
|
86
|
+
ws_we_api_base_url: str = "wss://ws-market-swap.w\u0065-\u0061pi.com/ws"
|
|
74
87
|
original_base_host: str = "https://\u0062ing\u0078.co\u006d"
|
|
75
88
|
qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
|
|
76
89
|
qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
|
|
@@ -85,6 +98,9 @@ class BXUltraClient(ExchangeBase):
|
|
|
85
98
|
platform_lang: str = "en"
|
|
86
99
|
sys_lang: str = "en"
|
|
87
100
|
|
|
101
|
+
# a dict that maps "BTC/USDT" to it single candle info.
|
|
102
|
+
__last_candle_storage: dict = None
|
|
103
|
+
__last_candle_lock: asyncio.Lock = None
|
|
88
104
|
# endregion
|
|
89
105
|
###########################################################
|
|
90
106
|
# region client constructor
|
|
@@ -106,8 +122,16 @@ class BXUltraClient(ExchangeBase):
|
|
|
106
122
|
self.device_brand = device_brand
|
|
107
123
|
self.app_version = app_version
|
|
108
124
|
self._fav_letter = fav_letter
|
|
125
|
+
self.sessions_dir = sessions_dir
|
|
109
126
|
|
|
110
|
-
self.read_from_session_file(
|
|
127
|
+
self.read_from_session_file(
|
|
128
|
+
file_path=f"{self.sessions_dir}/{self.account_name}.bx"
|
|
129
|
+
)
|
|
130
|
+
self.__last_candle_storage = {}
|
|
131
|
+
self.__last_candle_lock = asyncio.Lock()
|
|
132
|
+
self._internal_lock = asyncio.Lock()
|
|
133
|
+
self.extra_tasks = []
|
|
134
|
+
self.ws_connections = []
|
|
111
135
|
|
|
112
136
|
# endregion
|
|
113
137
|
###########################################################
|
|
@@ -222,6 +246,15 @@ class BXUltraClient(ExchangeBase):
|
|
|
222
246
|
model_type=ZenDeskAuthResponse,
|
|
223
247
|
)
|
|
224
248
|
|
|
249
|
+
async def re_authorize_user(self) -> bool:
|
|
250
|
+
result = await self.do_zendesk_auth()
|
|
251
|
+
if not result.data.jwt:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
self.authorization_token = result.data.jwt
|
|
255
|
+
self._save_session_file(file_path=f"{self.sessions_dir}/{self.account_name}.bx")
|
|
256
|
+
return True
|
|
257
|
+
|
|
225
258
|
# endregion
|
|
226
259
|
###########################################################
|
|
227
260
|
# region platform-tool
|
|
@@ -244,9 +277,120 @@ class BXUltraClient(ExchangeBase):
|
|
|
244
277
|
model_type=AssetsInfoResponse,
|
|
245
278
|
)
|
|
246
279
|
|
|
280
|
+
# endregion
|
|
281
|
+
###########################################################
|
|
282
|
+
# region ws-subscribes
|
|
283
|
+
async def do_price_subscribe(self) -> None:
|
|
284
|
+
"""
|
|
285
|
+
Subscribes to the price changes coming from the exchange.
|
|
286
|
+
NOTE: This method DOES NOT return. you should do create_task
|
|
287
|
+
for it.
|
|
288
|
+
"""
|
|
289
|
+
params = {
|
|
290
|
+
"platformid": self.platform_id,
|
|
291
|
+
"app_version": self.app_version,
|
|
292
|
+
"x-router-tag": self.x_router_tag,
|
|
293
|
+
"lang": self.platform_lang,
|
|
294
|
+
"device_id": self.device_id,
|
|
295
|
+
"channel": self.channel_header,
|
|
296
|
+
"device_brand": self.device_brand,
|
|
297
|
+
"traceId": self.trace_id,
|
|
298
|
+
}
|
|
299
|
+
url = f"{self.ws_we_api_base_url}?{urlencode(params, doseq=True)}"
|
|
300
|
+
async with websockets.connect(url, ping_interval=None) as ws:
|
|
301
|
+
await self._internal_lock.acquire()
|
|
302
|
+
self.ws_connections.append(ws)
|
|
303
|
+
self._internal_lock.release()
|
|
304
|
+
|
|
305
|
+
await ws.send(json.dumps({
|
|
306
|
+
"dataType": "swap.market.v2.contracts",
|
|
307
|
+
"id": uuid.uuid4().hex,
|
|
308
|
+
"reqType": "sub",
|
|
309
|
+
}))
|
|
310
|
+
async for msg in ws:
|
|
311
|
+
try:
|
|
312
|
+
decompressed_message = gzip.decompress(msg)
|
|
313
|
+
if decompressed_message.lower() == "ping":
|
|
314
|
+
await ws.send("Pong")
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
data: dict = json.loads(decompressed_message, parse_float=Decimal)
|
|
318
|
+
if not isinstance(data, dict):
|
|
319
|
+
logger.warning(f"invalid data instance: {type(data)}")
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
if data.get("code", 0) == 0 and data.get("data", None) is None:
|
|
323
|
+
# it's all fine
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
if data.get("ping", None):
|
|
327
|
+
target_id = data["ping"]
|
|
328
|
+
target_time = data.get(
|
|
329
|
+
"time",
|
|
330
|
+
datetime.now(
|
|
331
|
+
timezone(timedelta(hours=8))
|
|
332
|
+
).isoformat(timespec="seconds")
|
|
333
|
+
)
|
|
334
|
+
await ws.send(json.dumps({
|
|
335
|
+
"pong": target_id,
|
|
336
|
+
"time": target_time,
|
|
337
|
+
}))
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
inner_data = data.get("data", None)
|
|
341
|
+
if isinstance(inner_data, dict):
|
|
342
|
+
if data.get("dataType", None) == "swap.market.v2.contracts":
|
|
343
|
+
list_data = inner_data.get("l", None)
|
|
344
|
+
await self.__last_candle_lock.acquire()
|
|
345
|
+
for current in list_data:
|
|
346
|
+
info = SingleCandleInfo.deserialize_short(current)
|
|
347
|
+
if info:
|
|
348
|
+
self.__last_candle_storage[info.pair.lower()] = info
|
|
349
|
+
self.__last_candle_lock.release()
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
logger.info(f"we got some unknown data: {data}")
|
|
353
|
+
except Exception as ex:
|
|
354
|
+
logger.info(f"failed to handle ws message from exchange: {msg}; {ex}")
|
|
355
|
+
|
|
356
|
+
async def get_last_candle(self, pair: str) -> SingleCandleInfo:
|
|
357
|
+
"""
|
|
358
|
+
Returns the last candle's info in this exchange.
|
|
359
|
+
This method is safe to be called ONLY from the exact same thread
|
|
360
|
+
that the loop is currently operating on.
|
|
361
|
+
"""
|
|
362
|
+
await self.__last_candle_lock.acquire()
|
|
363
|
+
info = self.__last_candle_storage.get(pair.lower())
|
|
364
|
+
self.__last_candle_lock.release()
|
|
365
|
+
return info
|
|
366
|
+
|
|
247
367
|
# endregion
|
|
248
368
|
###########################################################
|
|
249
369
|
# region contract
|
|
370
|
+
async def get_contract_config(
|
|
371
|
+
self,
|
|
372
|
+
fund_type: int, # e.g. 1
|
|
373
|
+
coin_name: str, # e.g. "SOL"
|
|
374
|
+
valuation_name: str, # e.g. "USDT"
|
|
375
|
+
margin_coin_name: str, # e.g. "USDT"
|
|
376
|
+
) -> ContractConfigResponse:
|
|
377
|
+
params = {
|
|
378
|
+
"fundType": f"{fund_type}",
|
|
379
|
+
"coinName": f"{coin_name}",
|
|
380
|
+
"valuationName": f"{valuation_name}",
|
|
381
|
+
"marginCoinName": f"{margin_coin_name}",
|
|
382
|
+
}
|
|
383
|
+
headers = self.get_headers(
|
|
384
|
+
payload=params,
|
|
385
|
+
)
|
|
386
|
+
return await self.invoke_get(
|
|
387
|
+
# "https://bingx.com/api/v2/contract/config",
|
|
388
|
+
f"{self.qq_os_base_url}/v2/contract/config",
|
|
389
|
+
headers=headers,
|
|
390
|
+
params=params,
|
|
391
|
+
model_type=CopyTraderTradePositionsResponse,
|
|
392
|
+
)
|
|
393
|
+
|
|
250
394
|
async def get_contract_list(
|
|
251
395
|
self,
|
|
252
396
|
quotation_coin_id: int = -1,
|
|
@@ -403,6 +547,60 @@ class BXUltraClient(ExchangeBase):
|
|
|
403
547
|
|
|
404
548
|
# endregion
|
|
405
549
|
###########################################################
|
|
550
|
+
# region contract delegation
|
|
551
|
+
async def create_order_delegation(
|
|
552
|
+
self,
|
|
553
|
+
balance_direction: int, # e.g. 1
|
|
554
|
+
delegate_price: Decimal, # e.g. 107414.70
|
|
555
|
+
fund_type: int, # # e.g. 1
|
|
556
|
+
large_spread_rate: int, # e.g. 0
|
|
557
|
+
lever_times: int, # e.g. 5
|
|
558
|
+
margin: int, # e.g. 5
|
|
559
|
+
margin_coin_name: str, # e.g. "USDT"
|
|
560
|
+
market_factor: int, # e.g. 1
|
|
561
|
+
order_type: int, # e.g. 0
|
|
562
|
+
price: Decimal, # the current price of the market??
|
|
563
|
+
stop_loss_rate: int, # e.g. -1
|
|
564
|
+
stop_profit_rate: int, # e.g. -1
|
|
565
|
+
quotation_coin_id: int, # e.g. 1
|
|
566
|
+
spread_rate: float, # something very low. e.g. 0.00003481
|
|
567
|
+
stop_loss_price: float, # e.g. -1
|
|
568
|
+
stop_profit_price: float, # e.g. -1
|
|
569
|
+
up_ratio: Decimal, # e.g. 0.5
|
|
570
|
+
) -> CreateOrderDelegationResponse:
|
|
571
|
+
payload = {
|
|
572
|
+
"balanceDirection": balance_direction,
|
|
573
|
+
"delegatePrice": f"{delegate_price}",
|
|
574
|
+
"fundType": fund_type,
|
|
575
|
+
"largeSpreadRate": large_spread_rate or 0,
|
|
576
|
+
"leverTimes": lever_times or 1,
|
|
577
|
+
"margin": margin,
|
|
578
|
+
"marginCoinName": margin_coin_name or "USDT",
|
|
579
|
+
"marketFactor": market_factor or 1,
|
|
580
|
+
"orderType": f"{order_type or 0}",
|
|
581
|
+
"price": float(price), # e.g. 107161.27
|
|
582
|
+
"profitLossRateDto": {
|
|
583
|
+
"stopProfitRate": stop_profit_rate or -1,
|
|
584
|
+
"stopLossRate": stop_loss_rate or -1,
|
|
585
|
+
},
|
|
586
|
+
"quotationCoinId": quotation_coin_id or 1,
|
|
587
|
+
"spreadRate": float(spread_rate) or 0.00003481,
|
|
588
|
+
"stopLossPrice": stop_loss_price or -1,
|
|
589
|
+
"stopProfitPrice": stop_profit_price or -1,
|
|
590
|
+
"upRatio": f"{0.5 if up_ratio is None else up_ratio}",
|
|
591
|
+
}
|
|
592
|
+
headers = self.get_headers(
|
|
593
|
+
needs_auth=True,
|
|
594
|
+
payload=payload,
|
|
595
|
+
)
|
|
596
|
+
return await self.invoke_post(
|
|
597
|
+
f"{self.we_api_base_url}/v2/contract/order/delegation",
|
|
598
|
+
headers=headers,
|
|
599
|
+
content=payload,
|
|
600
|
+
model_type=CreateOrderDelegationResponse,
|
|
601
|
+
)
|
|
602
|
+
# endregion
|
|
603
|
+
###########################################################
|
|
406
604
|
# region copy-trade-facade
|
|
407
605
|
async def get_copy_trader_positions(
|
|
408
606
|
self,
|
|
@@ -505,6 +703,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
505
703
|
self,
|
|
506
704
|
uid: int | str,
|
|
507
705
|
) -> int | str:
|
|
706
|
+
global user_api_identity_cache
|
|
508
707
|
api_identity = user_api_identity_cache.get(uid, None)
|
|
509
708
|
if not api_identity:
|
|
510
709
|
resume = await self.get_copy_trader_resume(
|
|
@@ -595,6 +794,8 @@ class BXUltraClient(ExchangeBase):
|
|
|
595
794
|
self.authorization_token = json_data.get(
|
|
596
795
|
"authorization_token", self.authorization_token
|
|
597
796
|
)
|
|
797
|
+
if self.authorization_token:
|
|
798
|
+
self.jwt_manager = JWTManager(self.jwt_manager)
|
|
598
799
|
self.app_id = json_data.get("app_id", self.app_id)
|
|
599
800
|
self.trade_env = json_data.get("trade_env", self.trade_env)
|
|
600
801
|
self.timezone = json_data.get("timezone", self.timezone)
|
|
@@ -608,6 +809,9 @@ class BXUltraClient(ExchangeBase):
|
|
|
608
809
|
"""
|
|
609
810
|
Saves current information to the session file.
|
|
610
811
|
"""
|
|
812
|
+
if file_path is None:
|
|
813
|
+
file_path = f"{self.sessions_dir}/{self.account_name}.bx"
|
|
814
|
+
|
|
611
815
|
if not self.device_id:
|
|
612
816
|
self.device_id = uuid.uuid4().hex.replace("-", "") + "##"
|
|
613
817
|
|
|
@@ -645,8 +849,6 @@ class BXUltraClient(ExchangeBase):
|
|
|
645
849
|
self,
|
|
646
850
|
uid: int | str,
|
|
647
851
|
) -> UnifiedTraderPositions:
|
|
648
|
-
global user_api_identity_cache
|
|
649
|
-
|
|
650
852
|
api_identity = await self.get_trader_api_identity(
|
|
651
853
|
uid=uid,
|
|
652
854
|
)
|
|
@@ -656,6 +858,13 @@ class BXUltraClient(ExchangeBase):
|
|
|
656
858
|
api_identity=api_identity,
|
|
657
859
|
page_size=50, # TODO: make this dynamic I guess...
|
|
658
860
|
)
|
|
861
|
+
if not result.data:
|
|
862
|
+
if result.msg:
|
|
863
|
+
raise ExchangeError(result.msg)
|
|
864
|
+
raise ExchangeError(
|
|
865
|
+
f"Unknown error happened while fetching positions of {uid}, "
|
|
866
|
+
f"code: {result.code}"
|
|
867
|
+
)
|
|
659
868
|
if result.data.hide == 0 and not result.data.positions:
|
|
660
869
|
# TODO: do proper exceptions here...
|
|
661
870
|
raise ValueError("The trader has made their positions hidden")
|
|
@@ -674,6 +883,12 @@ class BXUltraClient(ExchangeBase):
|
|
|
674
883
|
unified_pos.open_price_unit = (
|
|
675
884
|
position.valuation_coin_name or position.symbol.split("-")[-1]
|
|
676
885
|
) # TODO
|
|
886
|
+
|
|
887
|
+
last_candle = await self.get_last_candle(unified_pos.position_pair)
|
|
888
|
+
if last_candle:
|
|
889
|
+
unified_pos.last_price = last_candle.close_price
|
|
890
|
+
unified_pos.last_volume = last_candle.quote_volume
|
|
891
|
+
|
|
677
892
|
unified_result.positions.append(unified_pos)
|
|
678
893
|
|
|
679
894
|
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
|