trd-utils 0.0.57__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. trd_utils/__init__.py +3 -0
  2. trd_utils/cipher/__init__.py +44 -0
  3. trd_utils/common_utils/float_utils.py +21 -0
  4. trd_utils/common_utils/wallet_utils.py +26 -0
  5. trd_utils/date_utils/__init__.py +8 -0
  6. trd_utils/date_utils/datetime_helpers.py +25 -0
  7. trd_utils/exchanges/README.md +203 -0
  8. trd_utils/exchanges/__init__.py +28 -0
  9. trd_utils/exchanges/base_types.py +229 -0
  10. trd_utils/exchanges/binance/__init__.py +13 -0
  11. trd_utils/exchanges/binance/binance_client.py +389 -0
  12. trd_utils/exchanges/binance/binance_types.py +116 -0
  13. trd_utils/exchanges/blofin/__init__.py +6 -0
  14. trd_utils/exchanges/blofin/blofin_client.py +375 -0
  15. trd_utils/exchanges/blofin/blofin_types.py +173 -0
  16. trd_utils/exchanges/bx_ultra/__init__.py +6 -0
  17. trd_utils/exchanges/bx_ultra/bx_types.py +1338 -0
  18. trd_utils/exchanges/bx_ultra/bx_ultra_client.py +1123 -0
  19. trd_utils/exchanges/bx_ultra/bx_utils.py +51 -0
  20. trd_utils/exchanges/errors.py +10 -0
  21. trd_utils/exchanges/exchange_base.py +301 -0
  22. trd_utils/exchanges/hyperliquid/README.md +3 -0
  23. trd_utils/exchanges/hyperliquid/__init__.py +7 -0
  24. trd_utils/exchanges/hyperliquid/hyperliquid_client.py +292 -0
  25. trd_utils/exchanges/hyperliquid/hyperliquid_types.py +183 -0
  26. trd_utils/exchanges/okx/__init__.py +6 -0
  27. trd_utils/exchanges/okx/okx_client.py +219 -0
  28. trd_utils/exchanges/okx/okx_types.py +197 -0
  29. trd_utils/exchanges/price_fetcher.py +48 -0
  30. trd_utils/html_utils/__init__.py +26 -0
  31. trd_utils/html_utils/html_formats.py +72 -0
  32. trd_utils/tradingview/__init__.py +8 -0
  33. trd_utils/tradingview/tradingview_client.py +128 -0
  34. trd_utils/tradingview/tradingview_types.py +185 -0
  35. trd_utils/types_helper/__init__.py +12 -0
  36. trd_utils/types_helper/base_model.py +350 -0
  37. trd_utils/types_helper/decorators.py +20 -0
  38. trd_utils/types_helper/model_config.py +6 -0
  39. trd_utils/types_helper/ultra_list.py +39 -0
  40. trd_utils/types_helper/utils.py +40 -0
  41. trd_utils-0.0.57.dist-info/METADATA +42 -0
  42. trd_utils-0.0.57.dist-info/RECORD +44 -0
  43. trd_utils-0.0.57.dist-info/WHEEL +4 -0
  44. trd_utils-0.0.57.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,375 @@
1
+ import asyncio
2
+ from decimal import Decimal
3
+ import json
4
+ import logging
5
+ import httpx
6
+
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from trd_utils.date_utils.datetime_helpers import dt_from_ts
11
+ from trd_utils.exchanges.base_types import (
12
+ UnifiedPositionInfo,
13
+ UnifiedTraderInfo,
14
+ UnifiedTraderPositions,
15
+ )
16
+ from trd_utils.exchanges.blofin.blofin_types import (
17
+ CmsColorResponse,
18
+ CopyTraderAllOrderHistory,
19
+ CopyTraderAllOrderList,
20
+ CopyTraderInfoResponse,
21
+ CopyTraderOrderHistoryResponse,
22
+ CopyTraderOrderListResponse,
23
+ ShareConfigResponse,
24
+ )
25
+ from trd_utils.cipher import AESCipher
26
+ from trd_utils.exchanges.errors import ExchangeError
27
+ from trd_utils.exchanges.exchange_base import ExchangeBase
28
+ from trd_utils.types_helper import new_list
29
+
30
+
31
+ BASE_PROFILE_URL = "https://blofin.com/copy-trade/details/"
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class BlofinClient(ExchangeBase):
37
+ ###########################################################
38
+ # region client parameters
39
+ blofin_api_base_host: str = "https://\u0062lofin.co\u006d"
40
+ blofin_api_base_url: str = "https://\u0062lofin.co\u006d/uapi/v1"
41
+ origin_header: str = "https://\u0062lofin.co\u006d"
42
+
43
+ timezone: str = "Etc/UTC"
44
+
45
+ # endregion
46
+ ###########################################################
47
+ # region client constructor
48
+ def __init__(
49
+ self,
50
+ account_name: str = "default",
51
+ http_verify: bool = True,
52
+ fav_letter: str = "^",
53
+ read_session_file: bool = True,
54
+ sessions_dir: str = "sessions",
55
+ use_http1: bool = False,
56
+ use_http2: bool = True,
57
+ ):
58
+ self.httpx_client = httpx.AsyncClient(
59
+ verify=http_verify,
60
+ http1=use_http1,
61
+ http2=use_http2,
62
+ )
63
+ self.account_name = account_name
64
+ self._fav_letter = fav_letter
65
+ self.sessions_dir = sessions_dir
66
+ self.exchange_name = "blofin"
67
+
68
+ super().__init__()
69
+
70
+ if read_session_file:
71
+ self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
72
+
73
+ # endregion
74
+ ###########################################################
75
+ # region v1/cms/
76
+ async def get_share_config(self) -> ShareConfigResponse:
77
+ headers = self.get_headers()
78
+ return await self.invoke_get(
79
+ f"{self.blofin_api_base_url}/cms/share_config",
80
+ headers=headers,
81
+ model_type=ShareConfigResponse,
82
+ )
83
+
84
+ async def get_cms_color(self) -> CmsColorResponse:
85
+ headers = self.get_headers()
86
+ return await self.invoke_get(
87
+ f"{self.blofin_api_base_url}/cms/color",
88
+ headers=headers,
89
+ model_type=CmsColorResponse,
90
+ )
91
+
92
+ # endregion
93
+ ###########################################################
94
+ # region copy/trader
95
+ async def get_copy_trader_info(self, uid: int) -> CopyTraderInfoResponse:
96
+ payload = {
97
+ "uid": uid,
98
+ }
99
+ headers = self.get_headers()
100
+ return await self.invoke_post(
101
+ f"{self.blofin_api_base_url}/copy/trader/info",
102
+ headers=headers,
103
+ content=payload,
104
+ model_type=CopyTraderInfoResponse,
105
+ )
106
+
107
+ async def get_copy_trader_order_list(
108
+ self,
109
+ uid: int | str,
110
+ from_param: int = 0,
111
+ limit_param: int = 20,
112
+ ) -> CopyTraderOrderListResponse:
113
+ payload = {
114
+ "from": from_param,
115
+ "limit": limit_param,
116
+ "uid": int(uid),
117
+ }
118
+ headers = self.get_headers()
119
+ return await self.invoke_post(
120
+ f"{self.blofin_api_base_url}/copy/trader/order/list",
121
+ headers=headers,
122
+ content=payload,
123
+ model_type=CopyTraderOrderListResponse,
124
+ )
125
+
126
+ async def get_copy_trader_all_order_list(
127
+ self,
128
+ uid: int,
129
+ from_param: int = 0,
130
+ chunk_limit: int = 20,
131
+ sleep_delay: int = 0.5,
132
+ ) -> CopyTraderAllOrderList:
133
+ if chunk_limit < 1:
134
+ raise ValueError("chunk_limit parameter has to be more than 1")
135
+
136
+ result = CopyTraderAllOrderList(
137
+ code=200,
138
+ data=[],
139
+ total_count=0,
140
+ )
141
+ current_id_from = from_param
142
+ while True:
143
+ total_ignored = 0
144
+ current_result = await self.get_copy_trader_order_list(
145
+ uid=uid,
146
+ from_param=current_id_from,
147
+ limit_param=chunk_limit,
148
+ )
149
+ if current_result.code != 200:
150
+ if current_result.msg:
151
+ raise ExchangeError(
152
+ f"blofin get_copy_trader_all_order_list: {current_result.msg}; "
153
+ f"code: {current_result.code}"
154
+ )
155
+ raise ExchangeError(
156
+ "blofin get_copy_trader_all_order_list: unknown error; "
157
+ f"code: {current_result.code}"
158
+ )
159
+
160
+ if not isinstance(current_result, CopyTraderOrderListResponse):
161
+ raise ValueError(
162
+ "get_copy_trader_order_list returned invalid value of "
163
+ f"{type(current_result)}",
164
+ )
165
+ if not current_result.data:
166
+ # we no longer have anything else here
167
+ return result
168
+
169
+ if current_result.data[0].id == current_id_from:
170
+ if len(current_result.data) < 2:
171
+ return result
172
+ current_result.data = current_result.data[1:]
173
+ total_ignored += 1
174
+ elif current_id_from:
175
+ raise ValueError(
176
+ "Expected first array to have the same value as from_param: "
177
+ f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
178
+ )
179
+
180
+ current_id_from = current_result.data[-1].id
181
+ result.data.extend(current_result.data)
182
+ result.total_count += len(current_result.data)
183
+ if len(current_result.data) < chunk_limit - total_ignored:
184
+ # the trader doesn't have any more open orders
185
+ return result
186
+ if result.total_count > len(current_result.data) and sleep_delay:
187
+ # we don't want to sleep after 1 request only
188
+ await asyncio.sleep(sleep_delay)
189
+
190
+ async def get_copy_trader_order_history(
191
+ self,
192
+ uid: int,
193
+ from_param: int = 0,
194
+ limit_param: int = 20,
195
+ ) -> CopyTraderOrderHistoryResponse:
196
+ payload = {
197
+ "from": from_param,
198
+ "limit": limit_param,
199
+ "uid": uid,
200
+ }
201
+ headers = self.get_headers()
202
+ return await self.invoke_post(
203
+ f"{self.blofin_api_base_url}/copy/trader/order/history",
204
+ headers=headers,
205
+ content=payload,
206
+ model_type=CopyTraderOrderHistoryResponse,
207
+ )
208
+
209
+ async def get_copy_trader_all_order_history(
210
+ self,
211
+ uid: int,
212
+ from_param: int = 0,
213
+ chunk_limit: int = 20,
214
+ sleep_delay: int = 0.5,
215
+ ) -> CopyTraderAllOrderHistory:
216
+ if chunk_limit < 1:
217
+ raise ValueError("chunk_limit parameter has to be more than 1")
218
+
219
+ result = CopyTraderAllOrderHistory(
220
+ code=200,
221
+ data=[],
222
+ total_count=0,
223
+ )
224
+ current_id_from = from_param
225
+ while True:
226
+ total_ignored = 0
227
+ current_result = await self.get_copy_trader_order_history(
228
+ uid=uid,
229
+ from_param=current_id_from,
230
+ limit_param=chunk_limit,
231
+ )
232
+ if (
233
+ not current_result
234
+ or not isinstance(current_result, CopyTraderOrderHistoryResponse)
235
+ or not current_result.data
236
+ ):
237
+ return result
238
+
239
+ if current_result.data[0].id == current_id_from:
240
+ if len(current_result.data) < 2:
241
+ return result
242
+ current_result.data = current_result.data[1:]
243
+ total_ignored += 1
244
+ elif current_id_from:
245
+ raise ValueError(
246
+ "Expected first array to have the same value as from_param: "
247
+ f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
248
+ )
249
+
250
+ current_id_from = current_result.data[-1].id
251
+ result.data.extend(current_result.data)
252
+ result.total_count += len(current_result.data)
253
+ if len(current_result.data) < chunk_limit - total_ignored:
254
+ # the trader doesn't have any more orders history
255
+ return result
256
+ if result.total_count > len(current_result.data) and sleep_delay:
257
+ # we don't want to sleep after 1 request only
258
+ await asyncio.sleep(sleep_delay)
259
+
260
+ # endregion
261
+ ###########################################################
262
+ # region client helper methods
263
+ def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
264
+ the_timestamp = int(time.time() * 1000)
265
+ the_headers = {
266
+ # "Host": self.blofin_api_base_host,
267
+ "Content-Type": "application/json",
268
+ "Accept": "application/json",
269
+ "Origin": self.origin_header,
270
+ "X-Tz": self.timezone,
271
+ "Fp-Request-Id": f"{the_timestamp}.n1fDrN",
272
+ "Accept-Encoding": "gzip, deflate, br, zstd",
273
+ "User-Agent": self.user_agent,
274
+ "Connection": "close",
275
+ "appsiteid": "0",
276
+ }
277
+
278
+ if self.x_requested_with:
279
+ the_headers["X-Requested-With"] = self.x_requested_with
280
+
281
+ if needs_auth:
282
+ the_headers["Authorization"] = f"Bearer {self.authorization_token}"
283
+ return the_headers
284
+
285
+ def read_from_session_file(self, file_path: str) -> None:
286
+ """
287
+ Reads from session file; if it doesn't exist, creates it.
288
+ """
289
+ # check if path exists
290
+ target_path = Path(file_path)
291
+ if not target_path.exists():
292
+ return self._save_session_file(file_path=file_path)
293
+
294
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
295
+ content = aes.decrypt(target_path.read_text()).decode("utf-8")
296
+ json_data: dict = json.loads(content)
297
+
298
+ self.authorization_token = json_data.get(
299
+ "authorization_token",
300
+ self.authorization_token,
301
+ )
302
+ self.timezone = json_data.get("timezone", self.timezone)
303
+ self.user_agent = json_data.get("user_agent", self.user_agent)
304
+
305
+ def _save_session_file(self, file_path: str) -> None:
306
+ """
307
+ Saves current information to the session file.
308
+ """
309
+
310
+ json_data = {
311
+ "authorization_token": self.authorization_token,
312
+ "timezone": self.timezone,
313
+ "user_agent": self.user_agent,
314
+ }
315
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
316
+ target_path = Path(file_path)
317
+ if not target_path.exists():
318
+ target_path.mkdir(parents=True)
319
+ target_path.write_text(aes.encrypt(json.dumps(json_data)))
320
+
321
+ # endregion
322
+ ###########################################################
323
+ # region unified methods
324
+ async def get_unified_trader_positions(
325
+ self,
326
+ uid: int | str,
327
+ min_margin: Decimal = 0,
328
+ ) -> UnifiedTraderPositions:
329
+ result = await self.get_copy_trader_all_order_list(
330
+ uid=uid,
331
+ )
332
+ unified_result = UnifiedTraderPositions()
333
+ unified_result.positions = new_list()
334
+ for position in result.data:
335
+ unified_pos = UnifiedPositionInfo()
336
+ unified_pos.position_id = position.id or position.order_id
337
+ unified_pos.position_pnl = position.real_pnl or position.pnl
338
+ unified_pos.position_side = (
339
+ "LONG" if position.order_side in ("LONG", "BUY") else "SHORT"
340
+ )
341
+ unified_pos.margin_mode = position.margin_mode
342
+ unified_pos.position_leverage = Decimal(position.leverage)
343
+ unified_pos.position_pair = position.symbol.replace("-", "/")
344
+ unified_pos.open_time = dt_from_ts(position.open_time)
345
+ unified_pos.open_price = position.avg_open_price
346
+ unified_pos.open_price_unit = position.symbol.split("-")[-1]
347
+ unified_pos.initial_margin = position.get_initial_margin()
348
+ if min_margin and (
349
+ not unified_pos.initial_margin
350
+ or unified_pos.initial_margin < min_margin
351
+ ):
352
+ continue
353
+
354
+ unified_result.positions.append(unified_pos)
355
+
356
+ return unified_result
357
+
358
+ async def get_unified_trader_info(
359
+ self,
360
+ uid: int | str,
361
+ ) -> UnifiedTraderInfo:
362
+ info_resp = await self.get_copy_trader_info(
363
+ uid=uid,
364
+ )
365
+ info = info_resp.data
366
+ unified_info = UnifiedTraderInfo()
367
+ unified_info.trader_id = info.uid
368
+ unified_info.trader_name = info.nick_name
369
+ unified_info.trader_url = f"{BASE_PROFILE_URL}{info.uid}"
370
+ unified_info.win_rate = info.win_rate
371
+
372
+ return unified_info
373
+
374
+ # endregion
375
+ ###########################################################
@@ -0,0 +1,173 @@
1
+ from decimal import Decimal
2
+ from typing import Any
3
+ from trd_utils.types_helper import BaseModel
4
+
5
+
6
+ ###########################################################
7
+
8
+ # region common types
9
+
10
+
11
+ class BlofinApiResponse(BaseModel):
12
+ code: int = None
13
+ timestamp: int = None
14
+ msg: str = None
15
+
16
+ def __str__(self):
17
+ return f"code: {self.code}; timestamp: {self.timestamp}"
18
+
19
+ def __repr__(self):
20
+ return f"code: {self.code}; timestamp: {self.timestamp}"
21
+
22
+
23
+ # endregion
24
+
25
+ ###########################################################
26
+
27
+ # region api-config types
28
+
29
+
30
+ class PnlShareListInfo(BaseModel):
31
+ background_color: str = None
32
+ background_img_up: str = None
33
+ background_img_down: str = None
34
+
35
+
36
+ class ShareConfigResult(BaseModel):
37
+ pnl_share_list: list[PnlShareListInfo] = None
38
+
39
+
40
+ class ShareConfigResponse(BlofinApiResponse):
41
+ data: ShareConfigResult = None
42
+
43
+
44
+ class CmsColorResult(BaseModel):
45
+ color: str = None
46
+ city: str = None
47
+ country: str = None
48
+ ip: str = None
49
+
50
+
51
+ class CmsColorResponse(BlofinApiResponse):
52
+ data: CmsColorResult = None
53
+
54
+
55
+ # endregion
56
+
57
+ ###########################################################
58
+
59
+ # region copy-trader types
60
+
61
+
62
+ class CopyTraderInfoResult(BaseModel):
63
+ aum: str = None
64
+ can_copy: bool = None
65
+ copier_whitelist: bool = None
66
+ follow_state: int = None
67
+ followers: int = None
68
+ followers_max: int = None
69
+ forbidden_follow_type: int = None
70
+ hidden_all: bool = None
71
+ hidden_order: bool = None
72
+ joined_date: int = None
73
+ max_draw_down: Decimal = None
74
+ nick_name: str = None
75
+ order_amount_limit: Any = None
76
+ profile: str = None
77
+ profit_sharing_ratio: Decimal = None
78
+ real_pnl: Decimal = None
79
+ roi_d7: Decimal = None
80
+ self_introduction: str = None
81
+ sharing_period: str = None
82
+ source: int = None
83
+ uid: int = None
84
+ whitelist_copier: bool = None
85
+ win_rate: Decimal = None
86
+
87
+ def get_profile_url(self) -> str:
88
+ return f"https://blofin.com/copy-trade/details/{self.uid}"
89
+
90
+
91
+ class CopyTraderInfoResponse(BlofinApiResponse):
92
+ data: CopyTraderInfoResult = None
93
+
94
+
95
+ class CopyTraderSingleOrderInfo(BaseModel):
96
+ id: int = None
97
+ symbol: str = None
98
+ leverage: int = None
99
+ order_side: str = None
100
+ avg_open_price: Decimal = None
101
+ quantity: Decimal = None
102
+ quantity_cont: Any = None
103
+ open_time: int = None
104
+ close_time: Any = None
105
+ avg_close_price: Decimal = None
106
+ real_pnl: Any = None
107
+ close_type: Any = None
108
+ roe: Decimal = None
109
+ followers_profit: Decimal = None
110
+ followers: Any = None
111
+ order_id: Any = None
112
+ sharing: Any = None
113
+ order_state: Any = None
114
+ trader_name: Any = None
115
+ mark_price: Any = None
116
+ tp_trigger_price: Any = None
117
+ tp_order_type: Any = None
118
+ sl_trigger_price: Any = None
119
+ sl_order_type: Any = None
120
+ margin_mode: str = None
121
+ time_in_force: Any = None
122
+ position_side: str = None
123
+ order_category: Any = None
124
+ price: Any = None
125
+ fill_quantity: Any = None
126
+ fill_quantity_cont: Any = None
127
+ pnl: Decimal = None
128
+ cancel_source: Any = None
129
+ order_type: Any = None
130
+ order_open_state: Any = None
131
+ amount: Any = None
132
+ filled_amount: Any = None
133
+ create_time: Any = None
134
+ update_time: Any = None
135
+ open_fee: Any = None
136
+ close_fee: Any = None
137
+ id_md5: Any = None
138
+ tp_sl: Any = None
139
+ trader_uid: Any = None
140
+ available_quantity: Any = None
141
+ available_quantity_cont: Any = None
142
+ show_in_kline: Any = None
143
+ unrealized_pnl: Any = None
144
+ unrealized_pnl_ratio: Any = None
145
+ broker_id: Any = None
146
+ position_change_history: Any = None
147
+ user_id: Any = None
148
+
149
+ def get_initial_margin(self) -> Decimal:
150
+ if not self.avg_open_price or not self.quantity or not self.leverage:
151
+ return None
152
+ return (self.avg_open_price * self.quantity) / self.leverage
153
+
154
+
155
+ class CopyTraderOrderListResponse(BlofinApiResponse):
156
+ data: list[CopyTraderSingleOrderInfo] = None
157
+
158
+
159
+ class CopyTraderAllOrderList(CopyTraderOrderListResponse):
160
+ total_count: int = None
161
+
162
+
163
+ class CopyTraderOrderHistoryResponse(BlofinApiResponse):
164
+ data: list[CopyTraderSingleOrderInfo] = None
165
+
166
+
167
+ class CopyTraderAllOrderHistory(CopyTraderOrderHistoryResponse):
168
+ total_count: int = None
169
+
170
+
171
+ # endregion
172
+
173
+ ###########################################################
@@ -0,0 +1,6 @@
1
+
2
+ from .bx_ultra_client import BXUltraClient
3
+
4
+ __all__ = [
5
+ "BXUltraClient",
6
+ ]