trd-utils 0.0.13__tar.gz → 0.0.15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of trd-utils might be problematic. Click here for more details.

Files changed (31) hide show
  1. {trd_utils-0.0.13 → trd_utils-0.0.15}/PKG-INFO +1 -1
  2. {trd_utils-0.0.13 → trd_utils-0.0.15}/pyproject.toml +1 -1
  3. trd_utils-0.0.15/trd_utils/__init__.py +3 -0
  4. trd_utils-0.0.15/trd_utils/exchanges/README.md +201 -0
  5. trd_utils-0.0.15/trd_utils/exchanges/__init__.py +21 -0
  6. trd_utils-0.0.15/trd_utils/exchanges/base_types.py +53 -0
  7. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/blofin/blofin_client.py +31 -10
  8. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/blofin/blofin_types.py +35 -35
  9. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/bx_ultra/bx_types.py +289 -42
  10. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +46 -3
  11. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/exchange_base.py +34 -1
  12. trd_utils-0.0.15/trd_utils/exchanges/hyperliquid/README.md +3 -0
  13. trd_utils-0.0.15/trd_utils/exchanges/hyperliquid/__init__.py +7 -0
  14. trd_utils-0.0.15/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +188 -0
  15. trd_utils-0.0.15/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +109 -0
  16. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/types_helper/base_model.py +11 -0
  17. trd_utils-0.0.13/trd_utils/__init__.py +0 -3
  18. trd_utils-0.0.13/trd_utils/exchanges/__init__.py +0 -11
  19. {trd_utils-0.0.13 → trd_utils-0.0.15}/LICENSE +0 -0
  20. {trd_utils-0.0.13 → trd_utils-0.0.15}/README.md +0 -0
  21. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/cipher/__init__.py +0 -0
  22. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/common_utils/float_utils.py +0 -0
  23. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/blofin/__init__.py +0 -0
  24. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
  25. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
  26. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/html_utils/__init__.py +0 -0
  27. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/html_utils/html_formats.py +0 -0
  28. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/tradingview/__init__.py +0 -0
  29. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/tradingview/tradingview_client.py +0 -0
  30. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/tradingview/tradingview_types.py +0 -0
  31. {trd_utils-0.0.13 → trd_utils-0.0.15}/trd_utils/types_helper/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trd_utils
3
- Version: 0.0.13
3
+ Version: 0.0.15
4
4
  Summary: Common Basic Utils for Python3. By ALiwoto.
5
5
  Keywords: utils,trd_utils,basic-utils,common-utils
6
6
  Author: ALiwoto
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "trd_utils"
3
- version = "0.0.13"
3
+ version = "0.0.15"
4
4
  description = "Common Basic Utils for Python3. By ALiwoto."
5
5
  authors = ["ALiwoto <aminnimaj@gmail.com>"]
6
6
  packages = [
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.0.15"
3
+
@@ -0,0 +1,201 @@
1
+ # trd_utils.exchanges
2
+
3
+ All exchange clients are stored inside of their own directory.
4
+ Some of these are not really _exchange_, but a tracker for a specific exchange/blockchain. We will still put them in this section as long as they are for a _specific_ one.
5
+
6
+ If they are for a very general platform, such as tradingview, they should be put in a separate directory entirely.
7
+
8
+ ## Writing code for a new exchange
9
+
10
+ Here is a boilerplate code that we can use for creating a new exchange/tracker class:
11
+
12
+ ```py
13
+ import asyncio
14
+ from decimal import Decimal
15
+ import json
16
+ import logging
17
+ from typing import Type
18
+ import httpx
19
+
20
+ import time
21
+ from pathlib import Path
22
+
23
+ # from trd_utils.exchanges.my_exchange.my_exchange_types import (
24
+ # SomeAPIType
25
+ # )
26
+ from trd_utils.cipher import AESCipher
27
+ from trd_utils.exchanges.exchange_base import ExchangeBase
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class MyExchangeClient(ExchangeBase):
33
+ ###########################################################
34
+ # region client parameters
35
+ my_exchange_api_base_host: str = "https://exchange.com"
36
+ my_exchange_api_base_url: str = "https://exchange.com/api/v1"
37
+ origin_header: str = "https://exchange.com/"
38
+
39
+ timezone: str = "Etc/UTC"
40
+
41
+ # endregion
42
+ ###########################################################
43
+ # region client constructor
44
+ def __init__(
45
+ self,
46
+ account_name: str = "default",
47
+ http_verify: bool = True,
48
+ fav_letter: str = "^",
49
+ read_session_file: bool = True,
50
+ sessions_dir: str = "sessions",
51
+ ):
52
+ self.httpx_client = httpx.AsyncClient(
53
+ verify=http_verify,
54
+ http2=True,
55
+ http1=False,
56
+ )
57
+ self.account_name = account_name
58
+ self._fav_letter = fav_letter
59
+ self.sessions_dir = sessions_dir
60
+
61
+ if read_session_file:
62
+ self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
63
+
64
+ # endregion
65
+ ###########################################################
66
+ # region something
67
+ # async def get_something_info(self) -> SomethingInfoResponse:
68
+ # headers = self.get_headers()
69
+ # return await self.invoke_get(
70
+ # f"{self.my_exchange_api_base_url}/something/info",
71
+ # headers=headers,
72
+ # model=SomethingInfoResponse,
73
+ # )
74
+ # endregion
75
+ ###########################################################
76
+ # region another-thing
77
+ # async def get_another_thing_info(self, uid: int) -> AnotherThingInfoResponse:
78
+ # payload = {
79
+ # "uid": uid,
80
+ # }
81
+ # headers = self.get_headers()
82
+ # return await self.invoke_post(
83
+ # f"{self.my_exchange_api_base_url}/another-thing/info",
84
+ # headers=headers,
85
+ # content=payload,
86
+ # model=CopyTraderInfoResponse,
87
+ # )
88
+
89
+ # endregion
90
+ ###########################################################
91
+ # region client helper methods
92
+ def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
93
+ the_timestamp = int(time.time() * 1000)
94
+ the_headers = {
95
+ # "Host": self.my_exchange_api_base_host,
96
+ "Content-Type": "application/json",
97
+ "Accept": "application/json",
98
+ "Origin": self.origin_header,
99
+ "X-Tz": self.timezone,
100
+ "Fp-Request-Id": f"{the_timestamp}.n1fDrN",
101
+ "Accept-Encoding": "gzip, deflate, br, zstd",
102
+ "User-Agent": self.user_agent,
103
+ "Connection": "close",
104
+ "appsiteid": "0",
105
+ }
106
+
107
+ if self.x_requested_with:
108
+ the_headers["X-Requested-With"] = self.x_requested_with
109
+
110
+ if needs_auth:
111
+ the_headers["Authorization"] = f"Bearer {self.authorization_token}"
112
+ return the_headers
113
+
114
+ async def invoke_get(
115
+ self,
116
+ url: str,
117
+ headers: dict | None = None,
118
+ params: dict | None = None,
119
+ model: Type[MyExchangeApiResponse] | None = None,
120
+ parse_float=Decimal,
121
+ ) -> "MyExchangeApiResponse":
122
+ """
123
+ Invokes the specific request to the specific url with the specific params and headers.
124
+ """
125
+ response = await self.httpx_client.get(
126
+ url=url,
127
+ headers=headers,
128
+ params=params,
129
+ )
130
+ return model.deserialize(response.json(parse_float=parse_float))
131
+
132
+ async def invoke_post(
133
+ self,
134
+ url: str,
135
+ headers: dict | None = None,
136
+ params: dict | None = None,
137
+ content: dict | str | bytes = "",
138
+ model: Type[MyExchangeApiResponse] | None = None,
139
+ parse_float=Decimal,
140
+ ) -> "MyExchangeApiResponse":
141
+ """
142
+ Invokes the specific request to the specific url with the specific params and headers.
143
+ """
144
+
145
+ if isinstance(content, dict):
146
+ content = json.dumps(content, separators=(",", ":"), sort_keys=True)
147
+
148
+ response = await self.httpx_client.post(
149
+ url=url,
150
+ headers=headers,
151
+ params=params,
152
+ content=content,
153
+ )
154
+ if not model:
155
+ return response.json()
156
+
157
+ return model.deserialize(response.json(parse_float=parse_float))
158
+
159
+ async def aclose(self) -> None:
160
+ await self.httpx_client.aclose()
161
+ logger.info("MyExchangeClient closed")
162
+ return True
163
+
164
+ def read_from_session_file(self, file_path: str) -> None:
165
+ """
166
+ Reads from session file; if it doesn't exist, creates it.
167
+ """
168
+ # check if path exists
169
+ target_path = Path(file_path)
170
+ if not target_path.exists():
171
+ return self._save_session_file(file_path=file_path)
172
+
173
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
174
+ content = aes.decrypt(target_path.read_text()).decode("utf-8")
175
+ json_data: dict = json.loads(content)
176
+
177
+ self.authorization_token = json_data.get(
178
+ "authorization_token",
179
+ self.authorization_token,
180
+ )
181
+ self.timezone = json_data.get("timezone", self.timezone)
182
+ self.user_agent = json_data.get("user_agent", self.user_agent)
183
+
184
+ def _save_session_file(self, file_path: str) -> None:
185
+ """
186
+ Saves current information to the session file.
187
+ """
188
+
189
+ json_data = {
190
+ "authorization_token": self.authorization_token,
191
+ "timezone": self.timezone,
192
+ "user_agent": self.user_agent,
193
+ }
194
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
195
+ target_path = Path(file_path)
196
+ target_path.write_text(aes.encrypt(json.dumps(json_data)))
197
+
198
+ # endregion
199
+ ###########################################################
200
+
201
+ ```
@@ -0,0 +1,21 @@
1
+
2
+ from .exchange_base import ExchangeBase
3
+ from .base_types import (
4
+ UnifiedTraderInfo,
5
+ UnifiedTraderPositions,
6
+ UnifiedPositionInfo,
7
+ )
8
+ from .blofin import BlofinClient
9
+ from .bx_ultra import BXUltraClient
10
+ from .hyperliquid import HyperLiquidClient
11
+
12
+
13
+ __all__ = [
14
+ ExchangeBase,
15
+ UnifiedTraderInfo,
16
+ UnifiedTraderPositions,
17
+ UnifiedPositionInfo,
18
+ BXUltraClient,
19
+ BlofinClient,
20
+ HyperLiquidClient,
21
+ ]
@@ -0,0 +1,53 @@
1
+
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+
6
+ class UnifiedPositionInfo:
7
+ # The id of the position.
8
+ position_id: str = None
9
+
10
+ # The pnl (profit) of the position.
11
+ position_pnl: Decimal = None
12
+
13
+ # The position side, either "LONG" or "SHORT".
14
+ position_side: str = None
15
+
16
+ # The formatted pair string of this position.
17
+ # e.g. BTC/USDT.
18
+ position_pair: str = None
19
+
20
+ # Side but with a proper emoji alongside of it.
21
+ side_with_emoji: str = None
22
+
23
+ # The open time of this position.
24
+ # Note that not all public APIs might provide this field.
25
+ open_time: datetime = None
26
+
27
+ # The relative open time of this position.
28
+ relative_open_time: str = None
29
+
30
+ # Open price of the position.
31
+ open_price: Decimal = None
32
+
33
+ # The string (and formatted) version of the open_price.
34
+ # Optionally base unit also included (e.g. USDT or USD).
35
+ open_price_str: str = None
36
+
37
+
38
+ class UnifiedTraderPositions:
39
+ positions: list[UnifiedPositionInfo] = None
40
+
41
+ class UnifiedTraderInfo:
42
+ # Name of the trader
43
+ trader_name: str = None
44
+
45
+ # The URL in which we can see the trader's profile
46
+ trader_url: str = None
47
+
48
+ # Trader's id. Either int or str. In DEXes (such as HyperLiquid),
49
+ # this might be wallet address of the trader.
50
+ trader_id: int | str = None
51
+
52
+ # Trader's win-rate. Not all exchanges might support this field.
53
+ win_rate: Decimal = None
@@ -8,6 +8,7 @@ import httpx
8
8
  import time
9
9
  from pathlib import Path
10
10
 
11
+ from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
11
12
  from trd_utils.exchanges.blofin.blofin_types import (
12
13
  BlofinApiResponse,
13
14
  CmsColorResponse,
@@ -108,7 +109,7 @@ class BlofinClient(ExchangeBase):
108
109
  content=payload,
109
110
  model=CopyTraderOrderListResponse,
110
111
  )
111
-
112
+
112
113
  async def get_copy_trader_all_order_list(
113
114
  self,
114
115
  uid: int,
@@ -132,10 +133,13 @@ class BlofinClient(ExchangeBase):
132
133
  from_param=current_id_from,
133
134
  limit_param=chunk_limit,
134
135
  )
135
- if not current_result or not isinstance(current_result, CopyTraderOrderListResponse) or \
136
- not current_result.data:
136
+ if (
137
+ not current_result
138
+ or not isinstance(current_result, CopyTraderOrderListResponse)
139
+ or not current_result.data
140
+ ):
137
141
  return result
138
-
142
+
139
143
  if current_result.data[0].id == current_id_from:
140
144
  if len(current_result.data) < 2:
141
145
  return result
@@ -146,7 +150,7 @@ class BlofinClient(ExchangeBase):
146
150
  "Expected first array to have the same value as from_param: "
147
151
  f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
148
152
  )
149
-
153
+
150
154
  current_id_from = current_result.data[-1].id
151
155
  result.data.extend(current_result.data)
152
156
  result.total_count += len(current_result.data)
@@ -157,7 +161,6 @@ class BlofinClient(ExchangeBase):
157
161
  # we don't want to sleep after 1 request only
158
162
  await asyncio.sleep(sleep_delay)
159
163
 
160
-
161
164
  async def get_copy_trader_order_history(
162
165
  self,
163
166
  uid: int,
@@ -200,10 +203,13 @@ class BlofinClient(ExchangeBase):
200
203
  from_param=current_id_from,
201
204
  limit_param=chunk_limit,
202
205
  )
203
- if not current_result or not isinstance(current_result, CopyTraderOrderHistoryResponse) or \
204
- not current_result.data:
206
+ if (
207
+ not current_result
208
+ or not isinstance(current_result, CopyTraderOrderHistoryResponse)
209
+ or not current_result.data
210
+ ):
205
211
  return result
206
-
212
+
207
213
  if current_result.data[0].id == current_id_from:
208
214
  if len(current_result.data) < 2:
209
215
  return result
@@ -214,7 +220,7 @@ class BlofinClient(ExchangeBase):
214
220
  "Expected first array to have the same value as from_param: "
215
221
  f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
216
222
  )
217
-
223
+
218
224
  current_id_from = current_result.data[-1].id
219
225
  result.data.extend(current_result.data)
220
226
  result.total_count += len(current_result.data)
@@ -336,3 +342,18 @@ class BlofinClient(ExchangeBase):
336
342
 
337
343
  # endregion
338
344
  ###########################################################
345
+ # region unified methods
346
+ async def get_unified_trader_positions(
347
+ self,
348
+ uid: int | str,
349
+ ) -> UnifiedTraderPositions:
350
+ pass
351
+
352
+ async def get_unified_trader_info(
353
+ self,
354
+ uid: int | str,
355
+ ) -> UnifiedTraderInfo:
356
+ pass
357
+
358
+ # endregion
359
+ ###########################################################
@@ -64,7 +64,7 @@ class CopyTraderInfoResult(BaseModel):
64
64
  joined_date: int = None
65
65
  max_draw_down: Decimal = None
66
66
  nick_name: str = None
67
- order_amount_limit: None
67
+ order_amount_limit: Any = None
68
68
  profile: str = None
69
69
  profit_sharing_ratio: Decimal = None
70
70
  real_pnl: Decimal = None
@@ -89,7 +89,7 @@ class CopyTraderSingleOrderInfo(BaseModel):
89
89
  order_side: str = None
90
90
  avg_open_price: str = None
91
91
  quantity: str = None
92
- quantity_cont: None
92
+ quantity_cont: Any = None
93
93
  open_time: int = None
94
94
  close_time: Any = None
95
95
  avg_close_price: Decimal = None
@@ -100,41 +100,41 @@ class CopyTraderSingleOrderInfo(BaseModel):
100
100
  followers: Any = None
101
101
  order_id: Any = None
102
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
103
+ order_state: Any = None
104
+ trader_name: Any = None
105
+ mark_price: Any = None
106
+ tp_trigger_price: Any = None
107
+ tp_order_type: Any = None
108
+ sl_trigger_price: Any = None
109
+ sl_order_type: Any = None
110
110
  margin_mode: str = None
111
- time_in_force: None
111
+ time_in_force: Any = None
112
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
113
+ order_category: Any = None
114
+ price: Any = None
115
+ fill_quantity: Any = None
116
+ fill_quantity_cont: Any = None
117
+ pnl: Any = None
118
+ cancel_source: Any = None
119
+ order_type: Any = None
120
+ order_open_state: Any = None
121
+ amount: Any = None
122
+ filled_amount: Any = None
123
+ create_time: Any = None
124
+ update_time: Any = None
125
+ open_fee: Any = None
126
+ close_fee: Any = None
127
+ id_md5: Any = None
128
+ tp_sl: Any = None
129
+ trader_uid: Any = None
130
+ available_quantity: Any = None
131
+ available_quantity_cont: Any = None
132
+ show_in_kline: Any = None
133
+ unrealized_pnl: Any = None
134
+ unrealized_pnl_ratio: Any = None
135
+ broker_id: Any = None
136
+ position_change_history: Any = None
137
+ user_id: Any = None
138
138
 
139
139
  class CopyTraderOrderListResponse(BlofinApiResponse):
140
140
  data: list[CopyTraderSingleOrderInfo] = None