trd-utils 0.0.4__tar.gz → 0.0.6__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.4 → trd_utils-0.0.6}/LICENSE +1 -1
- {trd_utils-0.0.4 → trd_utils-0.0.6}/PKG-INFO +13 -3
- trd_utils-0.0.6/README.md +13 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/pyproject.toml +1 -1
- trd_utils-0.0.6/trd_utils/__init__.py +3 -0
- trd_utils-0.0.6/trd_utils/common_utils/float_utils.py +11 -0
- trd_utils-0.0.6/trd_utils/exchanges/__init__.py +11 -0
- trd_utils-0.0.6/trd_utils/exchanges/blofin/__init__.py +6 -0
- trd_utils-0.0.6/trd_utils/exchanges/blofin/blofin_client.py +238 -0
- trd_utils-0.0.6/trd_utils/exchanges/blofin/blofin_types.py +144 -0
- {trd_utils-0.0.4/trd_utils → trd_utils-0.0.6/trd_utils/exchanges}/bx_ultra/bx_types.py +3 -2
- {trd_utils-0.0.4/trd_utils → trd_utils-0.0.6/trd_utils/exchanges}/bx_ultra/bx_ultra_client.py +111 -66
- trd_utils-0.0.4/trd_utils/bx_ultra/common_utils.py → trd_utils-0.0.6/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -8
- trd_utils-0.0.6/trd_utils/exchanges/exchange_base.py +76 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/tradingview/tradingview_client.py +35 -39
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/types_helper/base_model.py +31 -20
- trd_utils-0.0.4/README.md +0 -3
- trd_utils-0.0.4/trd_utils/__init__.py +0 -3
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.4/trd_utils → trd_utils-0.0.6/trd_utils/exchanges}/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/tradingview/tradingview_types.py +0 -0
- {trd_utils-0.0.4 → trd_utils-0.0.6}/trd_utils/types_helper/__init__.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: trd_utils
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Common Basic Utils for Python3. By ALiwoto.
|
|
5
|
-
Home-page: https://github.com/ALiwoto/trd_utils
|
|
6
5
|
Keywords: utils,trd_utils,basic-utils,common-utils
|
|
7
6
|
Author: ALiwoto
|
|
8
7
|
Author-email: aminnimaj@gmail.com
|
|
@@ -20,9 +19,20 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Requires-Dist: cryptography (>=41.0.7)
|
|
22
21
|
Requires-Dist: httpx (>=0.21.0)
|
|
22
|
+
Project-URL: Homepage, https://github.com/ALiwoto/trd_utils
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# Trd Utils
|
|
26
26
|
|
|
27
27
|
Basic common utils for Python.
|
|
28
28
|
|
|
29
|
+
## How to run tests
|
|
30
|
+
|
|
31
|
+
Use this command first:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then run the tests in vscode.
|
|
38
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
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")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Type
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from trd_utils.exchanges.blofin.blofin_types import (
|
|
11
|
+
BlofinApiResponse,
|
|
12
|
+
CmsColorResponse,
|
|
13
|
+
CopyTraderInfoResponse,
|
|
14
|
+
CopyTraderOrderHistoryResponse,
|
|
15
|
+
CopyTraderOrderListResponse,
|
|
16
|
+
ShareConfigResponse,
|
|
17
|
+
)
|
|
18
|
+
from trd_utils.cipher import AESCipher
|
|
19
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlofinClient(ExchangeBase):
|
|
25
|
+
###########################################################
|
|
26
|
+
# region client parameters
|
|
27
|
+
blofin_api_base_host: str = "https://\u0062lofin.co\u006d"
|
|
28
|
+
blofin_api_base_url: str = "https://\u0062lofin.co\u006d/uapi/v1"
|
|
29
|
+
origin_header: str = "https://\u0062lofin.co\u006d"
|
|
30
|
+
|
|
31
|
+
timezone: str = "Etc/UTC"
|
|
32
|
+
|
|
33
|
+
# endregion
|
|
34
|
+
###########################################################
|
|
35
|
+
# region client constructor
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
account_name: str = "default",
|
|
39
|
+
http_verify: bool = True,
|
|
40
|
+
fav_letter: str = "^",
|
|
41
|
+
read_session_file: bool = True,
|
|
42
|
+
sessions_dir: str = "sessions",
|
|
43
|
+
):
|
|
44
|
+
self.httpx_client = httpx.AsyncClient(
|
|
45
|
+
verify=http_verify,
|
|
46
|
+
http2=True,
|
|
47
|
+
http1=False,
|
|
48
|
+
)
|
|
49
|
+
self.account_name = account_name
|
|
50
|
+
self._fav_letter = fav_letter
|
|
51
|
+
self.sessions_dir = sessions_dir
|
|
52
|
+
|
|
53
|
+
if read_session_file:
|
|
54
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
|
|
55
|
+
|
|
56
|
+
# endregion
|
|
57
|
+
###########################################################
|
|
58
|
+
# region v1/cms/
|
|
59
|
+
async def get_share_config(self) -> ShareConfigResponse:
|
|
60
|
+
headers = self.get_headers()
|
|
61
|
+
return await self.invoke_get(
|
|
62
|
+
f"{self.blofin_api_base_url}/cms/share_config",
|
|
63
|
+
headers=headers,
|
|
64
|
+
model=ShareConfigResponse,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def get_cms_color(self) -> CmsColorResponse:
|
|
68
|
+
headers = self.get_headers()
|
|
69
|
+
return await self.invoke_get(
|
|
70
|
+
f"{self.blofin_api_base_url}/cms/color",
|
|
71
|
+
headers=headers,
|
|
72
|
+
model=CmsColorResponse,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# endregion
|
|
76
|
+
###########################################################
|
|
77
|
+
# region copy/trader
|
|
78
|
+
async def get_copy_trader_info(self, uid: int) -> CopyTraderInfoResponse:
|
|
79
|
+
payload = {
|
|
80
|
+
"uid": uid,
|
|
81
|
+
}
|
|
82
|
+
headers = self.get_headers()
|
|
83
|
+
return await self.invoke_post(
|
|
84
|
+
f"{self.blofin_api_base_url}/copy/trader/info",
|
|
85
|
+
headers=headers,
|
|
86
|
+
content=payload,
|
|
87
|
+
model=CopyTraderInfoResponse,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def get_copy_trader_order_list(
|
|
91
|
+
self,
|
|
92
|
+
from_param: int,
|
|
93
|
+
limit_param: 0,
|
|
94
|
+
uid: int,
|
|
95
|
+
) -> CopyTraderOrderListResponse:
|
|
96
|
+
payload = {
|
|
97
|
+
"from": from_param,
|
|
98
|
+
"limit": limit_param,
|
|
99
|
+
"uid": uid,
|
|
100
|
+
}
|
|
101
|
+
headers = self.get_headers()
|
|
102
|
+
return await self.invoke_post(
|
|
103
|
+
f"{self.blofin_api_base_url}/copy/trader/order/list",
|
|
104
|
+
headers=headers,
|
|
105
|
+
content=payload,
|
|
106
|
+
model=CopyTraderOrderListResponse,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def get_copy_trader_order_history(
|
|
110
|
+
self,
|
|
111
|
+
from_param: int,
|
|
112
|
+
limit_param: 0,
|
|
113
|
+
uid: int,
|
|
114
|
+
) -> CopyTraderOrderHistoryResponse:
|
|
115
|
+
payload = {
|
|
116
|
+
"from": from_param,
|
|
117
|
+
"limit": limit_param,
|
|
118
|
+
"uid": uid,
|
|
119
|
+
}
|
|
120
|
+
headers = self.get_headers()
|
|
121
|
+
return await self.invoke_post(
|
|
122
|
+
f"{self.blofin_api_base_url}/copy/trader/order/history",
|
|
123
|
+
headers=headers,
|
|
124
|
+
content=payload,
|
|
125
|
+
model=CopyTraderOrderHistoryResponse,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# endregion
|
|
129
|
+
###########################################################
|
|
130
|
+
# region client helper methods
|
|
131
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
132
|
+
the_timestamp = int(time.time() * 1000)
|
|
133
|
+
the_headers = {
|
|
134
|
+
# "Host": self.blofin_api_base_host,
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
"Accept": "application/json",
|
|
137
|
+
"Origin": self.origin_header,
|
|
138
|
+
"X-Tz": self.timezone,
|
|
139
|
+
"Fp-Request-Id": f"{the_timestamp}.n1fDrN",
|
|
140
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
141
|
+
"User-Agent": self.user_agent,
|
|
142
|
+
"Connection": "close",
|
|
143
|
+
"appsiteid": "0",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if self.x_requested_with:
|
|
147
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
148
|
+
|
|
149
|
+
if needs_auth:
|
|
150
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
151
|
+
return the_headers
|
|
152
|
+
|
|
153
|
+
async def invoke_get(
|
|
154
|
+
self,
|
|
155
|
+
url: str,
|
|
156
|
+
headers: dict | None = None,
|
|
157
|
+
params: dict | None = None,
|
|
158
|
+
model: Type[BlofinApiResponse] | None = None,
|
|
159
|
+
parse_float=Decimal,
|
|
160
|
+
) -> "BlofinApiResponse":
|
|
161
|
+
"""
|
|
162
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
163
|
+
"""
|
|
164
|
+
response = await self.httpx_client.get(
|
|
165
|
+
url=url,
|
|
166
|
+
headers=headers,
|
|
167
|
+
params=params,
|
|
168
|
+
)
|
|
169
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
170
|
+
|
|
171
|
+
async def invoke_post(
|
|
172
|
+
self,
|
|
173
|
+
url: str,
|
|
174
|
+
headers: dict | None = None,
|
|
175
|
+
params: dict | None = None,
|
|
176
|
+
content: dict | str | bytes = "",
|
|
177
|
+
model: Type[BlofinApiResponse] | None = None,
|
|
178
|
+
parse_float=Decimal,
|
|
179
|
+
) -> "BlofinApiResponse":
|
|
180
|
+
"""
|
|
181
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if isinstance(content, dict):
|
|
185
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
186
|
+
|
|
187
|
+
response = await self.httpx_client.post(
|
|
188
|
+
url=url,
|
|
189
|
+
headers=headers,
|
|
190
|
+
params=params,
|
|
191
|
+
content=content,
|
|
192
|
+
)
|
|
193
|
+
if not model:
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
197
|
+
|
|
198
|
+
async def aclose(self) -> None:
|
|
199
|
+
await self.httpx_client.aclose()
|
|
200
|
+
logger.info("BlofinClient closed")
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
206
|
+
"""
|
|
207
|
+
# check if path exists
|
|
208
|
+
target_path = Path(file_path)
|
|
209
|
+
if not target_path.exists():
|
|
210
|
+
return self._save_session_file(file_path=file_path)
|
|
211
|
+
|
|
212
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
213
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
214
|
+
json_data: dict = json.loads(content)
|
|
215
|
+
|
|
216
|
+
self.authorization_token = json_data.get(
|
|
217
|
+
"authorization_token",
|
|
218
|
+
self.authorization_token,
|
|
219
|
+
)
|
|
220
|
+
self.timezone = json_data.get("timezone", self.timezone)
|
|
221
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
222
|
+
|
|
223
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Saves current information to the session file.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
json_data = {
|
|
229
|
+
"authorization_token": self.authorization_token,
|
|
230
|
+
"timezone": self.timezone,
|
|
231
|
+
"user_agent": self.user_agent,
|
|
232
|
+
}
|
|
233
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
234
|
+
target_path = Path(file_path)
|
|
235
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
236
|
+
|
|
237
|
+
# endregion
|
|
238
|
+
###########################################################
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# from typing import Any, Optional
|
|
2
|
+
# from decimal import Decimal
|
|
3
|
+
# from datetime import datetime, timedelta
|
|
4
|
+
# import pytz
|
|
5
|
+
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
from trd_utils.types_helper import BaseModel
|
|
9
|
+
|
|
10
|
+
# from trd_utils.common_utils.float_utils import (
|
|
11
|
+
# dec_to_str,
|
|
12
|
+
# dec_to_normalize,
|
|
13
|
+
# )
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BlofinApiResponse(BaseModel):
|
|
18
|
+
code: int = None
|
|
19
|
+
timestamp: int = None
|
|
20
|
+
msg: str = None
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
###########################################################
|
|
30
|
+
|
|
31
|
+
class PnlShareListInfo(BaseModel):
|
|
32
|
+
background_color: str = None
|
|
33
|
+
background_img_up: str = None
|
|
34
|
+
background_img_down: str = None
|
|
35
|
+
|
|
36
|
+
class ShareConfigResult(BaseModel):
|
|
37
|
+
pnl_share_list: list[PnlShareListInfo] = None
|
|
38
|
+
|
|
39
|
+
class ShareConfigResponse(BlofinApiResponse):
|
|
40
|
+
data: ShareConfigResult = None
|
|
41
|
+
|
|
42
|
+
class CmsColorResult(BaseModel):
|
|
43
|
+
color: str = None
|
|
44
|
+
city: str = None
|
|
45
|
+
country: str = None
|
|
46
|
+
ip: str = None
|
|
47
|
+
|
|
48
|
+
class CmsColorResponse(BlofinApiResponse):
|
|
49
|
+
data: CmsColorResult = None
|
|
50
|
+
|
|
51
|
+
###########################################################
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CopyTraderInfoResult(BaseModel):
|
|
55
|
+
aum: str = None
|
|
56
|
+
can_copy: bool = None
|
|
57
|
+
copier_whitelist: bool = None
|
|
58
|
+
follow_state: int = None
|
|
59
|
+
followers: int = None
|
|
60
|
+
followers_max: int = None
|
|
61
|
+
forbidden_follow_type: int = None
|
|
62
|
+
hidden_all: bool = None
|
|
63
|
+
hidden_order: bool = None
|
|
64
|
+
joined_date: int = None
|
|
65
|
+
max_draw_down: Decimal = None
|
|
66
|
+
nick_name: str = None
|
|
67
|
+
order_amount_limit: None
|
|
68
|
+
profile: str = None
|
|
69
|
+
profit_sharing_ratio: Decimal = None
|
|
70
|
+
real_pnl: Decimal = None
|
|
71
|
+
roi_d7: Decimal = None
|
|
72
|
+
self_introduction: str = None
|
|
73
|
+
sharing_period: str = None
|
|
74
|
+
source: int = None
|
|
75
|
+
uid: int = None
|
|
76
|
+
whitelist_copier: bool = None
|
|
77
|
+
win_rate: Decimal = None
|
|
78
|
+
|
|
79
|
+
def get_profile_url(self) -> str:
|
|
80
|
+
return f"https://blofin.com/copy-trade/details/{self.uid}"
|
|
81
|
+
|
|
82
|
+
class CopyTraderInfoResponse(BlofinApiResponse):
|
|
83
|
+
data: CopyTraderInfoResult = None
|
|
84
|
+
|
|
85
|
+
class CopyTraderSingleOrderInfo(BaseModel):
|
|
86
|
+
id: int = None
|
|
87
|
+
symbol: str = None
|
|
88
|
+
leverage: int = None
|
|
89
|
+
order_side: str = None
|
|
90
|
+
avg_open_price: str = None
|
|
91
|
+
quantity: str = None
|
|
92
|
+
quantity_cont: None
|
|
93
|
+
open_time: int = None
|
|
94
|
+
close_time: Any = None
|
|
95
|
+
avg_close_price: Decimal = None
|
|
96
|
+
real_pnl: Any = None
|
|
97
|
+
close_type: Any = None
|
|
98
|
+
roe: Decimal = None
|
|
99
|
+
followers_profit: Decimal = None
|
|
100
|
+
followers: Any = None
|
|
101
|
+
order_id: Any = None
|
|
102
|
+
sharing: Any = None
|
|
103
|
+
order_state: None
|
|
104
|
+
trader_name: None
|
|
105
|
+
mark_price: None
|
|
106
|
+
tp_trigger_price: None
|
|
107
|
+
tp_order_type: None
|
|
108
|
+
sl_trigger_price: None
|
|
109
|
+
sl_order_type: None
|
|
110
|
+
margin_mode: str = None
|
|
111
|
+
time_in_force: None
|
|
112
|
+
position_side: str = None
|
|
113
|
+
order_category: None
|
|
114
|
+
price: None
|
|
115
|
+
fill_quantity: None
|
|
116
|
+
fill_quantity_cont: None
|
|
117
|
+
pnl: None
|
|
118
|
+
cancel_source: None
|
|
119
|
+
order_type: None
|
|
120
|
+
order_open_state: None
|
|
121
|
+
amount: None
|
|
122
|
+
filled_amount: None
|
|
123
|
+
create_time: None
|
|
124
|
+
update_time: None
|
|
125
|
+
open_fee: None
|
|
126
|
+
close_fee: None
|
|
127
|
+
id_md5: None
|
|
128
|
+
tp_sl: None
|
|
129
|
+
trader_uid: None
|
|
130
|
+
available_quantity: None
|
|
131
|
+
available_quantity_cont: None
|
|
132
|
+
show_in_kline: None
|
|
133
|
+
unrealized_pnl: None
|
|
134
|
+
unrealized_pnl_ratio: None
|
|
135
|
+
broker_id: None
|
|
136
|
+
position_change_history: None
|
|
137
|
+
user_id: None
|
|
138
|
+
|
|
139
|
+
class CopyTraderOrderListResponse(BlofinApiResponse):
|
|
140
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CopyTraderOrderHistoryResponse(BlofinApiResponse):
|
|
144
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
|
-
from ..types_helper import BaseModel
|
|
3
2
|
from decimal import Decimal
|
|
4
3
|
from datetime import datetime, timedelta
|
|
5
4
|
import pytz
|
|
6
5
|
|
|
7
|
-
from .
|
|
6
|
+
from trd_utils.types_helper import BaseModel
|
|
7
|
+
|
|
8
|
+
from trd_utils.common_utils.float_utils import (
|
|
8
9
|
dec_to_str,
|
|
9
10
|
dec_to_normalize,
|
|
10
11
|
)
|
{trd_utils-0.0.4/trd_utils → trd_utils-0.0.6/trd_utils/exchanges}/bx_ultra/bx_ultra_client.py
RENAMED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
+
from typing import Type
|
|
7
8
|
import uuid
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
@@ -11,8 +12,8 @@ import httpx
|
|
|
11
12
|
import time
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
|
-
from .
|
|
15
|
-
from .bx_types import (
|
|
15
|
+
from trd_utils.exchanges.bx_ultra.bx_utils import do_ultra_ss
|
|
16
|
+
from trd_utils.exchanges.bx_ultra.bx_types import (
|
|
16
17
|
AssetsInfoResponse,
|
|
17
18
|
ContractOrdersHistoryResponse,
|
|
18
19
|
ContractsListResponse,
|
|
@@ -26,8 +27,11 @@ from .bx_types import (
|
|
|
26
27
|
UserFavoriteQuotationResponse,
|
|
27
28
|
ZenDeskABStatusResponse,
|
|
28
29
|
ZoneModuleListResponse,
|
|
30
|
+
BxApiResponse,
|
|
29
31
|
)
|
|
30
|
-
from
|
|
32
|
+
from trd_utils.cipher import AESCipher
|
|
33
|
+
|
|
34
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
31
35
|
|
|
32
36
|
PLATFORM_ID_ANDROID = "10"
|
|
33
37
|
PLATFORM_ID_WEB = "30"
|
|
@@ -44,25 +48,16 @@ TG_APP_VERSION = "5.0.15"
|
|
|
44
48
|
logger = logging.getLogger(__name__)
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
class BXUltraClient:
|
|
51
|
+
class BXUltraClient(ExchangeBase):
|
|
48
52
|
###########################################################
|
|
49
53
|
# region client parameters
|
|
50
54
|
we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
|
|
51
55
|
we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
|
|
52
|
-
|
|
53
56
|
original_base_host: str = "https://\u0062ing\u0078.co\u006d"
|
|
54
|
-
|
|
55
57
|
qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
|
|
56
58
|
qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
|
|
57
59
|
|
|
58
|
-
device_id: str = None
|
|
59
|
-
trace_id: str = None
|
|
60
|
-
app_version: str = "4.28.3"
|
|
61
|
-
platform_id: str = "10"
|
|
62
|
-
install_channel: str = "officialAPK"
|
|
63
|
-
channel_header: str = "officialAPK"
|
|
64
60
|
origin_header: str = "https://\u0062ing\u0078.co\u006d"
|
|
65
|
-
authorization_token: str = None
|
|
66
61
|
app_id: str = "30004"
|
|
67
62
|
main_app_id: str = "10009"
|
|
68
63
|
trade_env: str = "real"
|
|
@@ -71,12 +66,6 @@ class BXUltraClient:
|
|
|
71
66
|
device_brand: str = "SM-N976N"
|
|
72
67
|
platform_lang: str = "en"
|
|
73
68
|
sys_lang: str = "en"
|
|
74
|
-
user_agent: str = "okhttp/4.12.0"
|
|
75
|
-
x_requested_with: str = None
|
|
76
|
-
httpx_client: httpx.AsyncClient = None
|
|
77
|
-
account_name: str = "default"
|
|
78
|
-
|
|
79
|
-
_fav_letter: str = "^"
|
|
80
69
|
|
|
81
70
|
# endregion
|
|
82
71
|
###########################################################
|
|
@@ -89,6 +78,7 @@ class BXUltraClient:
|
|
|
89
78
|
app_version: str = ANDROID_APP_VERSION,
|
|
90
79
|
http_verify: bool = True,
|
|
91
80
|
fav_letter: str = "^",
|
|
81
|
+
sessions_dir: str = "sessions",
|
|
92
82
|
):
|
|
93
83
|
self.httpx_client = httpx.AsyncClient(
|
|
94
84
|
verify=http_verify, http2=True, http1=False
|
|
@@ -99,13 +89,15 @@ class BXUltraClient:
|
|
|
99
89
|
self.app_version = app_version
|
|
100
90
|
self._fav_letter = fav_letter
|
|
101
91
|
|
|
102
|
-
self.read_from_session_file(f"{self.account_name}.bx")
|
|
92
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bx")
|
|
103
93
|
|
|
104
94
|
# endregion
|
|
105
95
|
###########################################################
|
|
106
96
|
# region api/coin/v1
|
|
107
97
|
async def get_zone_module_info(
|
|
108
|
-
self,
|
|
98
|
+
self,
|
|
99
|
+
only_one_position: int = 0,
|
|
100
|
+
biz_type: int = 10,
|
|
109
101
|
) -> ZoneModuleListResponse:
|
|
110
102
|
"""
|
|
111
103
|
Fetches and returns zone module info from the API.
|
|
@@ -118,101 +110,113 @@ class BXUltraClient:
|
|
|
118
110
|
}
|
|
119
111
|
headers = self.get_headers(params)
|
|
120
112
|
headers["Only_one_position"] = f"{only_one_position}"
|
|
121
|
-
|
|
113
|
+
return await self.invoke_get(
|
|
122
114
|
f"{self.we_api_base_url}/coin/v1/zone/module-info",
|
|
123
115
|
headers=headers,
|
|
124
116
|
params=params,
|
|
117
|
+
model=ZoneModuleListResponse,
|
|
125
118
|
)
|
|
126
|
-
return ZoneModuleListResponse.deserialize(response.json(parse_float=Decimal))
|
|
127
119
|
|
|
128
120
|
async def get_user_favorite_quotation(
|
|
129
|
-
self,
|
|
130
|
-
|
|
121
|
+
self,
|
|
122
|
+
only_one_position: int = 0,
|
|
123
|
+
biz_type: int = 1,
|
|
124
|
+
) -> UserFavoriteQuotationResponse:
|
|
131
125
|
params = {
|
|
132
126
|
"bizType": f"{biz_type}",
|
|
133
127
|
}
|
|
134
128
|
headers = self.get_headers(params)
|
|
135
129
|
headers["Only_one_position"] = f"{only_one_position}"
|
|
136
|
-
|
|
130
|
+
return await self.invoke_get(
|
|
137
131
|
f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
|
|
138
132
|
headers=headers,
|
|
139
133
|
params=params,
|
|
140
|
-
|
|
141
|
-
return UserFavoriteQuotationResponse.deserialize(
|
|
142
|
-
response.json(parse_float=Decimal)
|
|
134
|
+
model=UserFavoriteQuotationResponse,
|
|
143
135
|
)
|
|
144
136
|
|
|
145
|
-
async def get_quotation_rank(
|
|
137
|
+
async def get_quotation_rank(
|
|
138
|
+
self,
|
|
139
|
+
only_one_position: int = 0,
|
|
140
|
+
order_flag: int = 0,
|
|
141
|
+
) -> QuotationRankResponse:
|
|
146
142
|
params = {
|
|
147
143
|
"orderFlag": f"{order_flag}",
|
|
148
144
|
}
|
|
149
145
|
headers = self.get_headers(params)
|
|
150
146
|
headers["Only_one_position"] = f"{only_one_position}"
|
|
151
|
-
|
|
147
|
+
return await self.invoke_get(
|
|
152
148
|
f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
|
|
153
149
|
headers=headers,
|
|
154
150
|
params=params,
|
|
151
|
+
model=QuotationRankResponse,
|
|
155
152
|
)
|
|
156
|
-
return QuotationRankResponse.deserialize(response.json(parse_float=Decimal))
|
|
157
153
|
|
|
158
|
-
async def get_hot_search(
|
|
154
|
+
async def get_hot_search(
|
|
155
|
+
self,
|
|
156
|
+
only_one_position: int = 0,
|
|
157
|
+
biz_type: int = 30,
|
|
158
|
+
) -> HotSearchResponse:
|
|
159
159
|
params = {
|
|
160
160
|
"bizType": f"{biz_type}",
|
|
161
161
|
}
|
|
162
162
|
headers = self.get_headers(params)
|
|
163
163
|
headers["Only_one_position"] = f"{only_one_position}"
|
|
164
|
-
|
|
164
|
+
return await self.invoke_get(
|
|
165
165
|
f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
|
|
166
166
|
headers=headers,
|
|
167
167
|
params=params,
|
|
168
|
+
model=HotSearchResponse,
|
|
168
169
|
)
|
|
169
|
-
return HotSearchResponse.deserialize(response.json(parse_float=Decimal))
|
|
170
170
|
|
|
171
|
-
async def get_homepage(
|
|
171
|
+
async def get_homepage(
|
|
172
|
+
self,
|
|
173
|
+
only_one_position: int = 0,
|
|
174
|
+
biz_type: int = 30,
|
|
175
|
+
) -> HomePageResponse:
|
|
172
176
|
params = {
|
|
173
177
|
"biz-type": f"{biz_type}",
|
|
174
178
|
}
|
|
175
179
|
headers = self.get_headers(params)
|
|
176
180
|
headers["Only_one_position"] = f"{only_one_position}"
|
|
177
|
-
|
|
181
|
+
return await self.invoke_get(
|
|
178
182
|
f"{self.we_api_base_url}/coin/v1/discovery/homepage",
|
|
179
183
|
headers=headers,
|
|
180
184
|
params=params,
|
|
185
|
+
model=HomePageResponse,
|
|
181
186
|
)
|
|
182
|
-
return HomePageResponse.deserialize(response.json(parse_float=Decimal))
|
|
183
187
|
|
|
184
188
|
# endregion
|
|
185
189
|
###########################################################
|
|
186
190
|
# region customer
|
|
187
|
-
async def get_zendesk_ab_status(self):
|
|
191
|
+
async def get_zendesk_ab_status(self) -> ZenDeskABStatusResponse:
|
|
188
192
|
headers = self.get_headers()
|
|
189
|
-
|
|
193
|
+
return await self.invoke_get(
|
|
190
194
|
f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
|
|
191
195
|
headers=headers,
|
|
196
|
+
model=ZenDeskABStatusResponse,
|
|
192
197
|
)
|
|
193
|
-
return ZenDeskABStatusResponse.deserialize(response.json(parse_float=Decimal))
|
|
194
198
|
|
|
195
199
|
# endregion
|
|
196
200
|
###########################################################
|
|
197
201
|
# region platform-tool
|
|
198
202
|
async def get_hint_list(self) -> HintListResponse:
|
|
199
203
|
headers = self.get_headers()
|
|
200
|
-
|
|
204
|
+
return await self.invoke_get(
|
|
201
205
|
f"{self.we_api_base_url}/platform-tool/v1/hint/list",
|
|
202
206
|
headers=headers,
|
|
207
|
+
model=HintListResponse,
|
|
203
208
|
)
|
|
204
|
-
return HintListResponse.deserialize(response.json(parse_float=Decimal))
|
|
205
209
|
|
|
206
210
|
# endregion
|
|
207
211
|
###########################################################
|
|
208
212
|
# region asset-manager
|
|
209
213
|
async def get_assets_info(self) -> AssetsInfoResponse:
|
|
210
214
|
headers = self.get_headers(needs_auth=True)
|
|
211
|
-
|
|
215
|
+
return await self.invoke_get(
|
|
212
216
|
f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
|
|
213
217
|
headers=headers,
|
|
218
|
+
model=AssetsInfoResponse,
|
|
214
219
|
)
|
|
215
|
-
return AssetsInfoResponse.deserialize(response.json(parse_float=Decimal))
|
|
216
220
|
|
|
217
221
|
# endregion
|
|
218
222
|
###########################################################
|
|
@@ -236,12 +240,12 @@ class BXUltraClient:
|
|
|
236
240
|
if margin_coin_name:
|
|
237
241
|
params["marginCoinName"] = margin_coin_name
|
|
238
242
|
headers = self.get_headers(params, needs_auth=True)
|
|
239
|
-
|
|
243
|
+
return await self.invoke_get(
|
|
240
244
|
f"{self.we_api_base_url}/v4/contract/order/hold",
|
|
241
245
|
headers=headers,
|
|
242
246
|
params=params,
|
|
247
|
+
model=ContractsListResponse,
|
|
243
248
|
)
|
|
244
|
-
return ContractsListResponse.deserialize(response.json(parse_float=Decimal))
|
|
245
249
|
|
|
246
250
|
async def get_contract_order_history(
|
|
247
251
|
self,
|
|
@@ -263,13 +267,11 @@ class BXUltraClient:
|
|
|
263
267
|
params["fromOrderNo"] = f"{from_order_no}"
|
|
264
268
|
|
|
265
269
|
headers = self.get_headers(params, needs_auth=True)
|
|
266
|
-
|
|
270
|
+
return await self.invoke_get(
|
|
267
271
|
f"{self.we_api_base_url}/v2/contract/order/history",
|
|
268
272
|
headers=headers,
|
|
269
273
|
params=params,
|
|
270
|
-
|
|
271
|
-
return ContractOrdersHistoryResponse.deserialize(
|
|
272
|
-
response.json(parse_float=Decimal)
|
|
274
|
+
model=ContractOrdersHistoryResponse,
|
|
273
275
|
)
|
|
274
276
|
|
|
275
277
|
async def get_today_contract_earnings(
|
|
@@ -282,7 +284,7 @@ class BXUltraClient:
|
|
|
282
284
|
"""
|
|
283
285
|
Fetches today's earnings from the contract orders.
|
|
284
286
|
NOTE: This method is a bit slow due to the API rate limiting.
|
|
285
|
-
NOTE: If the user has not opened ANY contract orders today,
|
|
287
|
+
NOTE: If the user has not opened ANY contract orders today,
|
|
286
288
|
this method will return None.
|
|
287
289
|
"""
|
|
288
290
|
return await self._get_period_contract_earnings(
|
|
@@ -303,7 +305,7 @@ class BXUltraClient:
|
|
|
303
305
|
"""
|
|
304
306
|
Fetches this week's earnings from the contract orders.
|
|
305
307
|
NOTE: This method is a bit slow due to the API rate limiting.
|
|
306
|
-
NOTE: If the user has not opened ANY contract orders this week,
|
|
308
|
+
NOTE: If the user has not opened ANY contract orders this week,
|
|
307
309
|
this method will return None.
|
|
308
310
|
"""
|
|
309
311
|
return await self._get_period_contract_earnings(
|
|
@@ -324,7 +326,7 @@ class BXUltraClient:
|
|
|
324
326
|
"""
|
|
325
327
|
Fetches this month's earnings from the contract orders.
|
|
326
328
|
NOTE: This method is a bit slow due to the API rate limiting.
|
|
327
|
-
NOTE: If the user has not opened ANY contract orders this week,
|
|
329
|
+
NOTE: If the user has not opened ANY contract orders this week,
|
|
328
330
|
this method will return None.
|
|
329
331
|
"""
|
|
330
332
|
return await self._get_period_contract_earnings(
|
|
@@ -368,11 +370,11 @@ class BXUltraClient:
|
|
|
368
370
|
if result.get_orders_len() < page_size:
|
|
369
371
|
break
|
|
370
372
|
await asyncio.sleep(delay_per_fetch)
|
|
371
|
-
|
|
373
|
+
|
|
372
374
|
if not has_earned_any:
|
|
373
375
|
return None
|
|
374
376
|
return total_earnings
|
|
375
|
-
|
|
377
|
+
|
|
376
378
|
# endregion
|
|
377
379
|
###########################################################
|
|
378
380
|
# region copy-trade-facade
|
|
@@ -392,13 +394,11 @@ class BXUltraClient:
|
|
|
392
394
|
"copyTradeLabelType": f"{copy_trade_label_type}",
|
|
393
395
|
}
|
|
394
396
|
headers = self.get_headers(params)
|
|
395
|
-
|
|
397
|
+
return await self.invoke_get(
|
|
396
398
|
f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
|
|
397
399
|
headers=headers,
|
|
398
400
|
params=params,
|
|
399
|
-
|
|
400
|
-
return CopyTraderTradePositionsResponse.deserialize(
|
|
401
|
-
response.json(parse_float=Decimal)
|
|
401
|
+
model=CopyTraderTradePositionsResponse,
|
|
402
402
|
)
|
|
403
403
|
|
|
404
404
|
async def search_copy_traders(
|
|
@@ -430,25 +430,25 @@ class BXUltraClient:
|
|
|
430
430
|
"nickName": nick_name,
|
|
431
431
|
}
|
|
432
432
|
headers = self.get_headers(payload)
|
|
433
|
-
|
|
433
|
+
return await self.invoke_post(
|
|
434
434
|
f"{self.we_api_base_url}/v6/copy-trade/search/search",
|
|
435
435
|
headers=headers,
|
|
436
436
|
params=params,
|
|
437
|
-
content=
|
|
437
|
+
content=payload,
|
|
438
|
+
model=SearchCopyTradersResponse,
|
|
438
439
|
)
|
|
439
|
-
return SearchCopyTradersResponse.deserialize(response.json(parse_float=Decimal))
|
|
440
440
|
|
|
441
441
|
# endregion
|
|
442
442
|
###########################################################
|
|
443
443
|
# region welfare
|
|
444
444
|
async def do_daily_check_in(self):
|
|
445
445
|
headers = self.get_headers(needs_auth=True)
|
|
446
|
-
|
|
446
|
+
return await self.invoke_post(
|
|
447
447
|
f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
|
|
448
448
|
headers=headers,
|
|
449
449
|
content="",
|
|
450
|
+
model=None,
|
|
450
451
|
)
|
|
451
|
-
return response.json()
|
|
452
452
|
|
|
453
453
|
# endregion
|
|
454
454
|
###########################################################
|
|
@@ -484,7 +484,7 @@ class BXUltraClient:
|
|
|
484
484
|
payload_data=payload,
|
|
485
485
|
),
|
|
486
486
|
"Timestamp": f"{the_timestamp}",
|
|
487
|
-
|
|
487
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
488
488
|
"User-Agent": self.user_agent,
|
|
489
489
|
"Connection": "close",
|
|
490
490
|
"appsiteid": "0",
|
|
@@ -497,6 +497,51 @@ class BXUltraClient:
|
|
|
497
497
|
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
498
498
|
return the_headers
|
|
499
499
|
|
|
500
|
+
async def invoke_get(
|
|
501
|
+
self,
|
|
502
|
+
url: str,
|
|
503
|
+
headers: dict | None = None,
|
|
504
|
+
params: dict | None = None,
|
|
505
|
+
model: Type[BxApiResponse] | None = None,
|
|
506
|
+
parse_float=Decimal,
|
|
507
|
+
) -> "BxApiResponse":
|
|
508
|
+
"""
|
|
509
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
510
|
+
"""
|
|
511
|
+
response = await self.httpx_client.get(
|
|
512
|
+
url=url,
|
|
513
|
+
headers=headers,
|
|
514
|
+
params=params,
|
|
515
|
+
)
|
|
516
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
517
|
+
|
|
518
|
+
async def invoke_post(
|
|
519
|
+
self,
|
|
520
|
+
url: str,
|
|
521
|
+
headers: dict | None = None,
|
|
522
|
+
params: dict | None = None,
|
|
523
|
+
content: dict | str | bytes = "",
|
|
524
|
+
model: Type[BxApiResponse] | None = None,
|
|
525
|
+
parse_float=Decimal,
|
|
526
|
+
) -> "BxApiResponse":
|
|
527
|
+
"""
|
|
528
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
if isinstance(content, dict):
|
|
532
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
533
|
+
|
|
534
|
+
response = await self.httpx_client.post(
|
|
535
|
+
url=url,
|
|
536
|
+
headers=headers,
|
|
537
|
+
params=params,
|
|
538
|
+
content=content,
|
|
539
|
+
)
|
|
540
|
+
if not model:
|
|
541
|
+
return response.json()
|
|
542
|
+
|
|
543
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
544
|
+
|
|
500
545
|
async def aclose(self) -> None:
|
|
501
546
|
await self.httpx_client.aclose()
|
|
502
547
|
logger.info("BXUltraClient closed")
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import json
|
|
3
3
|
import uuid
|
|
4
|
-
from decimal import Decimal
|
|
5
4
|
|
|
6
|
-
default_quantize = Decimal("1.00")
|
|
7
5
|
|
|
8
6
|
default_e: str = (
|
|
9
7
|
"\u0039\u0035\u0064\u0036\u0035\u0063\u0037\u0033\u0064\u0063\u0035"
|
|
@@ -17,12 +15,6 @@ long_accept_header1: str = (
|
|
|
17
15
|
+ "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
18
16
|
)
|
|
19
17
|
|
|
20
|
-
def dec_to_str(dec_value: Decimal) -> str:
|
|
21
|
-
return format(dec_value.quantize(default_quantize), "f")
|
|
22
|
-
|
|
23
|
-
def dec_to_normalize(dec_value: Decimal) -> str:
|
|
24
|
-
return format(dec_value.normalize(), "f")
|
|
25
|
-
|
|
26
18
|
def do_ultra_ss(
|
|
27
19
|
e_param: str,
|
|
28
20
|
se_param: str,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Any
|
|
4
|
+
from abc import ABC
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExchangeBase(ABC):
|
|
10
|
+
###########################################################
|
|
11
|
+
# region client parameters
|
|
12
|
+
user_agent: str = "okhttp/4.12.0"
|
|
13
|
+
x_requested_with: str = None
|
|
14
|
+
httpx_client: httpx.AsyncClient = None
|
|
15
|
+
account_name: str = "default"
|
|
16
|
+
sessions_dir: str = "sessions"
|
|
17
|
+
|
|
18
|
+
authorization_token: str = None
|
|
19
|
+
device_id: str = None
|
|
20
|
+
trace_id: str = None
|
|
21
|
+
app_version: str = "4.28.3"
|
|
22
|
+
platform_id: str = "10"
|
|
23
|
+
install_channel: str = "officialAPK"
|
|
24
|
+
channel_header: str = "officialAPK"
|
|
25
|
+
|
|
26
|
+
_fav_letter: str = "^"
|
|
27
|
+
# endregion
|
|
28
|
+
###########################################################
|
|
29
|
+
# region client helper methods
|
|
30
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
async def invoke_get(
|
|
34
|
+
self,
|
|
35
|
+
url: str,
|
|
36
|
+
headers: dict | None,
|
|
37
|
+
params: dict | None,
|
|
38
|
+
model: Any,
|
|
39
|
+
parse_float=Decimal,
|
|
40
|
+
) -> Any:
|
|
41
|
+
"""
|
|
42
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
async def invoke_post(
|
|
47
|
+
self,
|
|
48
|
+
url: str,
|
|
49
|
+
headers: dict | None = None,
|
|
50
|
+
params: dict | None = None,
|
|
51
|
+
content: str | bytes = "",
|
|
52
|
+
model: None = None,
|
|
53
|
+
parse_float=Decimal,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
async def aclose(self) -> None:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Saves current information to the session file.
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# endregion
|
|
76
|
+
###########################################################
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
import json
|
|
5
2
|
from typing import Optional
|
|
6
3
|
|
|
@@ -10,41 +7,45 @@ from .tradingview_types import CoinScanInfo
|
|
|
10
7
|
class TradingViewClient:
|
|
11
8
|
"""TradingViewClient class to interact with TradingView API."""
|
|
12
9
|
|
|
13
|
-
|
|
14
10
|
def __init__(self) -> None:
|
|
15
11
|
pass
|
|
16
12
|
|
|
17
|
-
async def get_coin_scan(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
async def get_coin_scan(
|
|
14
|
+
self,
|
|
15
|
+
coin_filter: Optional[str] = None,
|
|
16
|
+
limit: int = 200,
|
|
17
|
+
offset: int = 0,
|
|
18
|
+
) -> list["CoinScanInfo"]:
|
|
21
19
|
import httpx
|
|
20
|
+
|
|
22
21
|
cookies = {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
"cookiesSettings": '{"analytics":true,"advertising":true}',
|
|
23
|
+
"cookiePrivacyPreferenceBannerProduction": "accepted",
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
headers = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
"accept": "application/json",
|
|
28
|
+
"accept-language": "en-US,en;q=0.9",
|
|
29
|
+
"cache-control": "no-cache",
|
|
30
|
+
"content-type": "text/plain;charset=UTF-8",
|
|
31
|
+
"origin": "https://www.tradingview.com",
|
|
32
|
+
"pragma": "no-cache",
|
|
33
|
+
"priority": "u=1, i",
|
|
34
|
+
"referer": "https://www.tradingview.com/",
|
|
35
|
+
"sec-ch-ua": '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
|
|
36
|
+
"sec-ch-ua-mobile": "?0",
|
|
37
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
38
|
+
"sec-fetch-dest": "empty",
|
|
39
|
+
"sec-fetch-mode": "cors",
|
|
40
|
+
"sec-fetch-site": "same-site",
|
|
41
|
+
"user-agent": (
|
|
42
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
43
|
+
+ "(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
|
|
44
|
+
),
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
params = {
|
|
47
|
-
|
|
48
|
+
"label-product": "screener-coin",
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
data = {
|
|
@@ -83,19 +84,14 @@ class TradingViewClient:
|
|
|
83
84
|
"Volatility.D",
|
|
84
85
|
],
|
|
85
86
|
"ignore_unknown_fields": False,
|
|
86
|
-
"options": {
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
"range":[
|
|
87
|
+
"options": {"lang": "en"},
|
|
88
|
+
"range": [
|
|
90
89
|
offset,
|
|
91
90
|
offset + limit,
|
|
92
91
|
],
|
|
93
|
-
"sort": {
|
|
94
|
-
"sortBy": "crypto_total_rank",
|
|
95
|
-
"sortOrder": "asc"
|
|
96
|
-
},
|
|
92
|
+
"sort": {"sortBy": "crypto_total_rank", "sortOrder": "asc"},
|
|
97
93
|
"symbols": {},
|
|
98
|
-
"markets": ["coin"]
|
|
94
|
+
"markets": ["coin"],
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
if coin_filter:
|
|
@@ -103,13 +99,13 @@ class TradingViewClient:
|
|
|
103
99
|
{
|
|
104
100
|
"left": "base_currency,base_currency_desc",
|
|
105
101
|
"operation": "match",
|
|
106
|
-
"right": f"{coin_filter}"
|
|
102
|
+
"right": f"{coin_filter}",
|
|
107
103
|
}
|
|
108
104
|
]
|
|
109
105
|
data = json.dumps(data)
|
|
110
106
|
async with httpx.AsyncClient() as client:
|
|
111
107
|
response = await client.post(
|
|
112
|
-
|
|
108
|
+
"https://scanner.tradingview.com/coin/scan",
|
|
113
109
|
params=params,
|
|
114
110
|
cookies=cookies,
|
|
115
111
|
headers=headers,
|
|
@@ -127,4 +123,4 @@ class TradingViewClient:
|
|
|
127
123
|
continue
|
|
128
124
|
all_infos.append(CoinScanInfo._parse(current_data.get("d", [])))
|
|
129
125
|
|
|
130
|
-
return all_infos
|
|
126
|
+
return all_infos
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import (
|
|
3
|
-
Optional,
|
|
4
3
|
Union,
|
|
5
4
|
get_type_hints,
|
|
6
5
|
Any,
|
|
7
|
-
get_args as get_type_args
|
|
6
|
+
get_args as get_type_args,
|
|
8
7
|
)
|
|
9
8
|
|
|
10
9
|
from trd_utils.html_utils.html_formats import camel_to_snake
|
|
@@ -20,6 +19,7 @@ ULTRA_LIST_ENABLED: bool = False
|
|
|
20
19
|
# attribute names are converted to snake_case.
|
|
21
20
|
SET_CAMEL_ATTR_NAMES = False
|
|
22
21
|
|
|
22
|
+
|
|
23
23
|
def get_my_field_types(cls):
|
|
24
24
|
type_hints = {}
|
|
25
25
|
for current_cls in cls.__class__.__mro__:
|
|
@@ -28,24 +28,31 @@ def get_my_field_types(cls):
|
|
|
28
28
|
type_hints.update(get_type_hints(current_cls))
|
|
29
29
|
return type_hints
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
def get_real_attr(cls, attr_name):
|
|
32
33
|
if cls is None:
|
|
33
34
|
return None
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
if isinstance(cls, dict):
|
|
36
37
|
return cls.get(attr_name, None)
|
|
37
38
|
|
|
38
39
|
if hasattr(cls, attr_name):
|
|
39
40
|
return getattr(cls, attr_name)
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
return None
|
|
42
43
|
|
|
44
|
+
|
|
45
|
+
def is_any_type(target_type: type) -> bool:
|
|
46
|
+
return target_type == Any or target_type is type(None)
|
|
47
|
+
|
|
48
|
+
|
|
43
49
|
class UltraList(list):
|
|
44
50
|
def __getattr__(self, attr):
|
|
45
51
|
if len(self) == 0:
|
|
46
52
|
return None
|
|
47
53
|
return UltraList([get_real_attr(item, attr) for item in self])
|
|
48
54
|
|
|
55
|
+
|
|
49
56
|
def convert_to_ultra_list(value: Any) -> UltraList:
|
|
50
57
|
if not value:
|
|
51
58
|
return UltraList()
|
|
@@ -62,7 +69,7 @@ def convert_to_ultra_list(value: Any) -> UltraList:
|
|
|
62
69
|
return tuple(convert_to_ultra_list(v) for v in value)
|
|
63
70
|
elif isinstance(value, set):
|
|
64
71
|
return {convert_to_ultra_list(v) for v in value}
|
|
65
|
-
|
|
72
|
+
|
|
66
73
|
for attr, attr_value in get_my_field_types(value).items():
|
|
67
74
|
if isinstance(attr_value, list):
|
|
68
75
|
setattr(value, attr, convert_to_ultra_list(getattr(value, attr)))
|
|
@@ -71,6 +78,7 @@ def convert_to_ultra_list(value: Any) -> UltraList:
|
|
|
71
78
|
except Exception:
|
|
72
79
|
return value
|
|
73
80
|
|
|
81
|
+
|
|
74
82
|
class BaseModel:
|
|
75
83
|
def __init__(self, **kwargs):
|
|
76
84
|
annotations = get_my_field_types(self)
|
|
@@ -84,17 +92,19 @@ class BaseModel:
|
|
|
84
92
|
# just ignore and continue
|
|
85
93
|
annotations[key] = Any
|
|
86
94
|
annotations[corrected_key] = Any
|
|
87
|
-
|
|
95
|
+
|
|
88
96
|
expected_type = annotations[corrected_key]
|
|
89
97
|
if hasattr(self, "_get_" + corrected_key + "_type"):
|
|
90
98
|
try:
|
|
91
|
-
overridden_type = getattr(self, "_get_" + corrected_key + "_type")(
|
|
99
|
+
overridden_type = getattr(self, "_get_" + corrected_key + "_type")(
|
|
100
|
+
kwargs
|
|
101
|
+
)
|
|
92
102
|
if overridden_type:
|
|
93
103
|
expected_type = overridden_type
|
|
94
104
|
except Exception:
|
|
95
105
|
pass
|
|
96
|
-
|
|
97
|
-
is_optional_type = getattr(expected_type,
|
|
106
|
+
|
|
107
|
+
is_optional_type = getattr(expected_type, "_name", None) == "Optional"
|
|
98
108
|
# maybe in the future we can have some other usages for is_optional_type
|
|
99
109
|
# variable or something like that.
|
|
100
110
|
if is_optional_type:
|
|
@@ -103,11 +113,11 @@ class BaseModel:
|
|
|
103
113
|
except Exception:
|
|
104
114
|
# something went wrong, just ignore and continue
|
|
105
115
|
expected_type = Any
|
|
106
|
-
|
|
116
|
+
|
|
107
117
|
# Handle nested models
|
|
108
118
|
if isinstance(value, dict) and issubclass(expected_type, BaseModel):
|
|
109
119
|
value = expected_type(**value)
|
|
110
|
-
|
|
120
|
+
|
|
111
121
|
elif isinstance(value, list):
|
|
112
122
|
type_args = get_type_args(expected_type)
|
|
113
123
|
if not type_args:
|
|
@@ -119,27 +129,29 @@ class BaseModel:
|
|
|
119
129
|
nested_type = type_args[0]
|
|
120
130
|
if issubclass(nested_type, BaseModel):
|
|
121
131
|
value = [nested_type(**item) for item in value]
|
|
122
|
-
|
|
132
|
+
|
|
123
133
|
if ULTRA_LIST_ENABLED and isinstance(value, list):
|
|
124
134
|
value = convert_to_ultra_list(value)
|
|
125
|
-
|
|
135
|
+
|
|
126
136
|
# Type checking
|
|
127
|
-
elif expected_type
|
|
137
|
+
elif not (is_any_type(expected_type) or isinstance(value, expected_type)):
|
|
128
138
|
try:
|
|
129
139
|
value = expected_type(value)
|
|
130
140
|
except Exception:
|
|
131
|
-
raise TypeError(
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
raise TypeError(
|
|
142
|
+
f"Field {corrected_key} must be of type {expected_type},"
|
|
143
|
+
+ f" but it's {type(value)}"
|
|
144
|
+
)
|
|
145
|
+
|
|
134
146
|
setattr(self, corrected_key, value)
|
|
135
147
|
if SET_CAMEL_ATTR_NAMES and key != corrected_key:
|
|
136
148
|
setattr(self, key, value)
|
|
137
|
-
|
|
149
|
+
|
|
138
150
|
# Check if all required fields are present
|
|
139
151
|
# for field in self.__annotations__:
|
|
140
152
|
# if not hasattr(self, field):
|
|
141
153
|
# raise ValueError(f"Missing required field: {field}")
|
|
142
|
-
|
|
154
|
+
|
|
143
155
|
@classmethod
|
|
144
156
|
def deserialize(cls, json_data: Union[str, dict]):
|
|
145
157
|
if isinstance(json_data, str):
|
|
@@ -147,4 +159,3 @@ class BaseModel:
|
|
|
147
159
|
else:
|
|
148
160
|
data = json_data
|
|
149
161
|
return cls(**data)
|
|
150
|
-
|
trd_utils-0.0.4/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|